diff --git a/pom.xml b/pom.xml index fab508f..37851a8 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,10 @@ thymeleaf-extras-springsecurity5 + io.github.openfeign + feign-okhttp + + org.json json 20210307 diff --git a/src/main/java/com/t3t/frontserver/auth/controller/LoginController.java b/src/main/java/com/t3t/frontserver/auth/controller/LoginController.java index 91cc62f..4727a45 100644 --- a/src/main/java/com/t3t/frontserver/auth/controller/LoginController.java +++ b/src/main/java/com/t3t/frontserver/auth/controller/LoginController.java @@ -5,8 +5,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.t3t.frontserver.auth.model.request.LoginRequestDto; import com.t3t.frontserver.auth.service.LoginService; +import com.t3t.frontserver.member.model.constant.MemberStatus; +import com.t3t.frontserver.member.model.response.MemberInfoResponse; +import com.t3t.frontserver.member.service.MemberService; import feign.FeignException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -26,24 +30,29 @@ import java.util.Base64; import java.util.Map; - +@Slf4j @Controller @RequiredArgsConstructor public class LoginController { private final LoginService loginService; + private final MemberService memberService; + + /** * 로그인 페이지 뷰 반환 + * * @return 로그인 페이지 뷰 * @author joohyun1996(이주현) */ @GetMapping("/login") - public String loginPage(Model model){ + public String loginPage(Model model) { model.addAttribute("loginRequestDto", new LoginRequestDto()); return "main/page/login"; } /** * 로그인 요청 처리 + * * @param loginRequestDto,redirectAttributes,resp * @return 성공시 : redirect:/, 실패시 : redirect:/login * @throws JsonProcessingException @@ -51,18 +60,28 @@ public String loginPage(Model model){ */ @PostMapping("/login") public String doLogin(@ModelAttribute @Valid LoginRequestDto loginRequestDto, - RedirectAttributes redirectAttributes, - HttpServletResponse resp) throws JsonProcessingException { + RedirectAttributes redirectAttributes, + Model model, + HttpServletResponse resp) throws JsonProcessingException { try { ResponseEntity responseEntity = loginService.login(loginRequestDto); String access = responseEntity.getHeaders().getFirst(HttpHeaders.AUTHORIZATION).trim().split(" ")[1]; + MemberInfoResponse memberInfoResponse = memberService.getMemberInfoResponseById(getMemberId(access)); + + if (memberInfoResponse.getStatus().equals(MemberStatus.INACTIVE)) { + model.addAttribute("memberId", memberInfoResponse.getMemberId()); + model.addAttribute("memberLatestLogin", memberInfoResponse.getLatestLogin()); + model.addAttribute("memberName", memberInfoResponse.getName()); + return "main/page/activateMemberIssue"; + } + Cookie cookie = new Cookie("t3t", access); cookie.setHttpOnly(true); cookie.setMaxAge(-1); resp.addCookie(cookie); - + // contextholder에 추가 SecurityContextHolder.getContext().setAuthentication(getAuthentication(access)); @@ -75,6 +94,7 @@ public String doLogin(@ModelAttribute @Valid LoginRequestDto loginRequestDto, /** * Security Context Holder를 사용하기 위해 임의의 CustomUserDetails를 넣고 반환해주는 메소드 + * * @param token * @return Authentication * @throws JsonProcessingException @@ -92,4 +112,15 @@ public UsernamePasswordAuthenticationToken getAuthentication(String token) throw UserDetails userDetails = User.withUsername(member).password("").roles(role).build(); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } + + public long getMemberId(String token) throws JsonProcessingException { + byte[] decodedPayload = Base64.getDecoder().decode(token.split("\\.")[1]); + ObjectMapper objectMapper = new ObjectMapper(); + Map map = objectMapper.readValue(new String(decodedPayload), + new TypeReference>() { + }); + + log.info("memberId : {}", map.get("username")); + return Long.parseLong(map.get("username").toString()); + } } diff --git a/src/main/java/com/t3t/frontserver/auth/decoder/CustomErrorDecoder.java b/src/main/java/com/t3t/frontserver/auth/decoder/CustomErrorDecoder.java index a30de5b..59ac5f4 100644 --- a/src/main/java/com/t3t/frontserver/auth/decoder/CustomErrorDecoder.java +++ b/src/main/java/com/t3t/frontserver/auth/decoder/CustomErrorDecoder.java @@ -11,10 +11,9 @@ public Exception decode(String s, Response response) { Response.Body responseBody = response.body(); HttpStatus responseStatus = HttpStatus.valueOf(response.status()); - if(responseStatus.is4xxClientError()){ - return new RestApiClientException(response); - }else{ - return new Exception(responseBody.toString()); + if(responseStatus.value()==HttpStatus.UNAUTHORIZED.value()){ + return new RestApiClientException("Login again"); } + return new Exception(response.reason()); } } diff --git a/src/main/java/com/t3t/frontserver/auth/error/CustomAuthenticationPoint.java b/src/main/java/com/t3t/frontserver/auth/error/CustomAuthenticationPoint.java index 415b4a7..e402d14 100644 --- a/src/main/java/com/t3t/frontserver/auth/error/CustomAuthenticationPoint.java +++ b/src/main/java/com/t3t/frontserver/auth/error/CustomAuthenticationPoint.java @@ -24,6 +24,10 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A return; } + if(Objects.isNull(authentication) && request.getRequestURI().startsWith("/mypage")){ + response.sendRedirect("/login"); + } + if(response.getStatus()==HttpServletResponse.SC_FORBIDDEN || response.getStatus()==HttpServletResponse.SC_UNAUTHORIZED){ request.setAttribute("errorMessage", "인증시간이 지났습니다. 다시 로그인 해주세요"); response.sendRedirect("/login"); diff --git a/src/main/java/com/t3t/frontserver/auth/exception/RestApiClientException.java b/src/main/java/com/t3t/frontserver/auth/exception/RestApiClientException.java index bbc4834..a67b1aa 100644 --- a/src/main/java/com/t3t/frontserver/auth/exception/RestApiClientException.java +++ b/src/main/java/com/t3t/frontserver/auth/exception/RestApiClientException.java @@ -1,26 +1,10 @@ package com.t3t.frontserver.auth.exception; -import feign.Response; import lombok.Getter; -import java.io.IOException; - @Getter public class RestApiClientException extends RuntimeException{ - private String responseBody; - private int status; - private String message; - - public RestApiClientException(Response response) { - super(); - this.status = response.status(); - this.message = response.reason(); - - try{ - this.responseBody = response.body() != null ? new String(response.body().asInputStream().readAllBytes()) : null; - } catch (IOException e){ - this.responseBody = null; - } - + public RestApiClientException(String message) { + super(message); } } diff --git a/src/main/java/com/t3t/frontserver/book/client/BookApiClient.java b/src/main/java/com/t3t/frontserver/book/client/BookApiClient.java index 126bc72..07263b0 100644 --- a/src/main/java/com/t3t/frontserver/book/client/BookApiClient.java +++ b/src/main/java/com/t3t/frontserver/book/client/BookApiClient.java @@ -1,20 +1,98 @@ package com.t3t.frontserver.book.client; -import com.t3t.frontserver.book.model.request.BookRegisterRequest; +import com.t3t.frontserver.book.model.dto.ParticipantMapDto; +import com.t3t.frontserver.book.model.request.ModifyBookDetailRequest; import com.t3t.frontserver.book.model.response.BookDetailResponse; +import com.t3t.frontserver.book.model.response.BookListResponse; import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.*; -@FeignClient(name = "bookAdaptor", url = "${t3t.feignClient.url}") +import javax.validation.Valid; +import java.util.List; + +@FeignClient(name = "bookApiClient", url = "${t3t.feignClient.url}") public interface BookApiClient { + /** + * 책의 상세 정보를 조회 + * @param bookId 도서의 ID + * @return 200 OK, 성공 메세지 + */ @GetMapping(value = "/t3t/bookstore/books/{bookId}") ResponseEntity> getBook(@PathVariable Long bookId); -// @PostMapping(value = "/t3t/bookstore/books") -// ResponseEntity> createBook(@RequestBody BookRegisterRequest request); + /** + * 도서 목록을 페이징하여 가져오는 요청을 처리 + * @param pageNo 페이지 번호 + * @param pageSize 페이지 크기 + * @return 도서 목록과 관련된 응답 데이터를 포함하는 ResponseEntity + * @author Yujin-nKim(김유진) + */ + @GetMapping(value = "/t3t/bookstore/books") + ResponseEntity>> getAllBooks(@RequestParam int pageNo, @RequestParam int pageSize); + + /** + * 특정 도서의 상세 정보를 수정 + * @param bookId 수정할 도서의 식별자 + * @param request 수정할 도서의 상세 정보를 담은 요청 객체 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping(value = "/t3t/bookstore/books/{bookId}/book-detail") + ResponseEntity> updateBookDetail(@PathVariable Long bookId, + @RequestBody @Valid ModifyBookDetailRequest request); + + /** + * 특정 도서의 출판사 정보를 수정 + * @param bookId 수정할 도서의 식별자 + * @param publisherId 수정할 출판사의 id + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/t3t/bookstore/books/{bookId}/publisher") + ResponseEntity> updateBookPublisher(@PathVariable Long bookId, + @RequestParam Long publisherId); + + /** + * 특정 도서의 참여자를 수정 + * @param bookId 수정할 도서의 식별자 + * @param participantList 수정할 참여자 매핑 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/t3t/bookstore/books/{bookId}/participant") + ResponseEntity> updateBookParticipant(@PathVariable Long bookId, + @RequestBody @Valid List participantList); + + /** + * 특정 도서의 태그를 수정 + * @param bookId 수정할 도서의 식별자 + * @param tagList 수정할 태그 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/t3t/bookstore/books/{bookId}/tag") + ResponseEntity> updateBookTag(@PathVariable Long bookId, + @RequestBody @Valid List tagList); + + /** + * 특정 도서의 카테고리를 수정 + * @param bookId 수정할 도서의 식별자 + * @param categoryList 수정할 카테고리 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/t3t/bookstore/books/{bookId}/category") + ResponseEntity> updateBookCategory(@PathVariable Long bookId, + @RequestBody @Valid List categoryList); + + /** + * 도서 삭제 요청을 처리 + * @param bookId 삭제하고자 하는 도서 id + * @author Yujin-nKim(김유진) + */ + @DeleteMapping(value = "/t3t/bookstore/books/{bookId}") + ResponseEntity> deleteBook(@PathVariable Long bookId); } \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/book/client/BookFormApiClient.java b/src/main/java/com/t3t/frontserver/book/client/BookFormApiClient.java new file mode 100644 index 0000000..808a0b2 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/book/client/BookFormApiClient.java @@ -0,0 +1,55 @@ +package com.t3t.frontserver.book.client; + +import com.t3t.frontserver.book.model.request.RegisterBookRequest; +import com.t3t.frontserver.config.FormConfiguration; +import com.t3t.frontserver.model.response.BaseResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * Feign을 사용하여 멀티파트 폼 데이터를 전송하는 BookFormApiClient 인터페이스 + * 이 인터페이스는 Feign을 사용하여 원격 서버에 HTTP 요청을 보내고, BookRegisterRequest 객체를 멀티파트 폼 데이터로 전송함 + * @author Yujin-nKim(김유진) + */ +@FeignClient(name = "bookFormApiClient", url = "${t3t.feignClient.url}", configuration = FormConfiguration.class) +public interface BookFormApiClient { + + /** + * 새 책을 생성하는 POST 요청 + * 요청 바디에는 BookRegisterRequest 객체가 멀티파트 폼 데이터로 전송됨 + * + * @param request 책을 등록하기 위한 요청 객체 + * @return 책 생성 요청에 대한 응답 + * @author Yujin-nKim(김유진) + */ + @PostMapping(value = "/t3t/bookstore/books", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + ResponseEntity> createBook(@ModelAttribute RegisterBookRequest request); + + /** + * 특정 도서의 썸네일을 수정 + * @param bookId 수정할 도서의 식별자 + * @param image 수정할 썸네일 이미지 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping(value = "/t3t/bookstore/books/{bookId}/book-thumbnail", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + ResponseEntity> updateBookThumbnail(@PathVariable Long bookId, + @ModelAttribute MultipartFile image); + + /** + * 특정 도서의 이미지를 수정 + * @param bookId 수정할 도서의 식별자 + * @param imageList 수정할 이미지 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/t3t/bookstore/books/{bookId}/book-image") + ResponseEntity> updateBookImage(@PathVariable Long bookId, + @ModelAttribute List imageList); + +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/book/controller/AdminBookController.java b/src/main/java/com/t3t/frontserver/book/controller/AdminBookController.java index 775cc5a..9983678 100644 --- a/src/main/java/com/t3t/frontserver/book/controller/AdminBookController.java +++ b/src/main/java/com/t3t/frontserver/book/controller/AdminBookController.java @@ -1,14 +1,28 @@ package com.t3t.frontserver.book.controller; import com.t3t.frontserver.book.client.BookApiClient; -import com.t3t.frontserver.book.model.request.BookRegisterRequest; +import com.t3t.frontserver.book.client.BookFormApiClient; +import com.t3t.frontserver.book.model.dto.ParticipantMapDto; +import com.t3t.frontserver.book.model.request.RegisterBookRequest; +import com.t3t.frontserver.book.model.request.ModifyBookDetailRequest; +import com.t3t.frontserver.book.model.response.BookDetailResponse; +import com.t3t.frontserver.book.model.response.BookListResponse; import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import feign.FeignException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import javax.validation.Valid; +import java.util.List; +import java.util.Objects; @Slf4j @RequiredArgsConstructor @@ -17,29 +31,261 @@ public class AdminBookController { private final BookApiClient bookApiClient; + private final BookFormApiClient bookFormApiClient; /** * 도서 등록 페이지를 요청 * @param model 데이터를 뷰에 전달하기 위한 Model 객체 - * @return 도서 등록 페이지의 뷰 이름인 "admin/page/registerBook"을 반환 + * @return 도서 등록 페이지 반환 * @author Yujin-nKim(김유진) */ @GetMapping("/new") public String getRegisterBookAdminPage(Model model) { - - model.addAttribute("bookRegisterRequest", new BookRegisterRequest()); + model.addAttribute("bookRegisterRequest", new RegisterBookRequest()); return "admin/page/registerBook"; } + /** + * 도서 수정 페이지를 요청 + * @param model 데이터를 뷰에 전달하기 위한 Model 객체 + * @param bookId 수정할 책의 ID + * @return 도서 수정 페이지 반환 + * @author Yujin-nKim(김유진) + */ + @GetMapping("/{bookId}/edit") + public String getEditBookAdminPage(Model model, @PathVariable Long bookId) { + try { + ResponseEntity> response = bookApiClient.getBook(bookId); + model.addAttribute("bookDetail", Objects.requireNonNull(response.getBody()).getData()); + model.addAttribute("modifyBookDetailRequest", new ModifyBookDetailRequest()); + } catch (FeignException exception) { + log.error(exception.getMessage()); + model.addAttribute("errorMessage", "데이터를 가져오는데 실패했습니다."); + } + return "admin/page/editBook"; + } + + /** + * 도서 목록을 조회 + * @param model 데이터를 뷰에 전달하기 위한 Model 객체 + * @param pageNo 요청된 페이지 번호 (기본값은 0이며, 0부터 시작) + * @param pageSize 페이지당 도서 수 (기본값은 20) + * @return 도서 목록 조회 페이지 반환 + * @author Yujin-nKim(김유진) + */ + @GetMapping + public String getBookListAdminPage(Model model, + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "20", required = false) int pageSize) { + + try { + ResponseEntity>> response = bookApiClient.getAllBooks(pageNo, pageSize); + if (response.getStatusCode() == HttpStatus.OK) { + PageResponse bookList = Objects.requireNonNull(response.getBody()).getData(); + if (bookList != null) { + int blockLimit = 5; // 현재 페이지 앞뒤로 보여줄 개수 설정 + int nowPage = bookList.getPageNo() + 1; + int startPage = Math.max(nowPage - blockLimit, 1); + int endPage = Math.min(nowPage + blockLimit, bookList.getTotalPages()); + + model.addAttribute("nowPage", nowPage); + model.addAttribute("startPage", startPage); + model.addAttribute("endPage", endPage); + model.addAttribute("bookList", bookList.getContent()); + } + } else { + log.info(response.getStatusCode().toString()); + } + } catch (FeignException exception) { + log.error(exception.getMessage()); + model.addAttribute("errorMessage", "데이터를 가져오는데 실패했습니다."); + } + return "admin/page/bookList"; + } + /** * 도서를 등록하는 요청을 처리 * @param request 등록하고자 하는 도서 정보를 담고 있는 BookRegisterRequest 객체 * @author Yujin-nKim(김유진) */ @PostMapping - public String createBook(@ModelAttribute("bookRegisterRequest") BookRegisterRequest request) { - log.info(request.toString()); -// ResponseEntity> response = bookApiClient.createBook(request); - return "admin/page/registerBook"; + public String createBook(@ModelAttribute(value = "bookRegisterRequest") RegisterBookRequest request, RedirectAttributes redirectAttributes) { + + log.info("도서 등록 요청 = {}", request.toString()); + + try { + ResponseEntity> response = bookFormApiClient.createBook(request); + Long bookId = Objects.requireNonNull(response.getBody()).getData(); + String message = response.getBody().getMessage(); + redirectAttributes.addFlashAttribute("successMessage", message + "\n저장된 도서 Id : " + bookId); + return "redirect:/admin/books/new"; + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookRegisterRequest", request); + redirectAttributes.addFlashAttribute("errorMessage", "도서 등록에 실패했습니다."); + return "redirect:/admin/books/new"; + } + } + + /** + * 특정 도서의 상세 정보를 수정 + * @param bookId 수정할 도서의 식별자 + * @param request 수정할 도서의 상세 정보를 담은 요청 객체 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PostMapping("/{bookId}/edit") + public String updateBookDetail(@PathVariable Long bookId, @ModelAttribute(value = "modifyBookDetailRequest") ModifyBookDetailRequest request, RedirectAttributes redirectAttributes) { + + log.info("도서 상세 정보 수정 요청 = {}", request.toString()); + + try { + ResponseEntity> response = bookApiClient.updateBookDetail(bookId, request); + redirectAttributes.addFlashAttribute("bookDetailModifySuccess", Objects.requireNonNull(response.getBody()).getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookDetailModifyError", "도서 상세 정보 수정에 실패했습니다."); + } + return "redirect:/admin/books/"+bookId+"/edit"; + } + + /** + * 특정 도서의 출판사 정보를 수정 + * @param bookId 수정할 도서의 식별자 + * @param publisherId 수정할 출판사의 id + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/{bookId}/publisher") + public String updateBookPublisher(@PathVariable Long bookId, + @RequestParam Long publisherId, + RedirectAttributes redirectAttributes) { + try { + ResponseEntity> response = bookApiClient.updateBookPublisher(bookId, publisherId); + redirectAttributes.addFlashAttribute("bookDetailModifySuccess", Objects.requireNonNull(response.getBody()).getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookDetailModifyError", "도서 출판사 정보 수정에 실패했습니다."); + } + return "redirect:/admin/books/"+bookId+"/edit"; + } + + /** + * 특정 도서의 참여자를 수정 + * @param bookId 수정할 도서의 식별자 + * @param participantList 수정할 참여자 매핑 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/{bookId}/participant") + public String updateBookParticipant(@PathVariable Long bookId, + @RequestBody @Valid List participantList, + RedirectAttributes redirectAttributes) { + try { + ResponseEntity> response = bookApiClient.updateBookParticipant(bookId, participantList); + redirectAttributes.addFlashAttribute("bookDetailModifySuccess", Objects.requireNonNull(response.getBody()).getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookDetailModifyError", "도서 참여자 정보 수정에 실패했습니다."); + } + return "redirect:/admin/books/"+bookId+"/edit"; + } + + /** + * 특정 도서의 태그를 수정 + * @param bookId 수정할 도서의 식별자 + * @param tagList 수정할 태그 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/{bookId}/tag") + public String updateBookTag(@PathVariable Long bookId, @RequestBody @Valid List tagList, + RedirectAttributes redirectAttributes) { + try { + ResponseEntity> response = bookApiClient.updateBookTag(bookId, tagList); + redirectAttributes.addFlashAttribute("bookDetailModifySuccess", Objects.requireNonNull(response.getBody()).getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookDetailModifyError", "도서 태그 정보 수정에 실패했습니다."); + } + return "redirect:/admin/books/"+bookId+"/edit"; + } + + /** + * 특정 도서의 카테고리를 수정 + * @param bookId 수정할 도서의 식별자 + * @param categoryList 수정할 카테고리 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/{bookId}/category") + public String updateBookCategory(@PathVariable Long bookId, + @RequestBody @Valid List categoryList, + RedirectAttributes redirectAttributes) { + try { + ResponseEntity> response = bookApiClient.updateBookCategory(bookId, categoryList); + redirectAttributes.addFlashAttribute("bookDetailModifySuccess", Objects.requireNonNull(response.getBody()).getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookDetailModifyError", "도서 카테고리 정보 수정에 실패했습니다."); + } + return "redirect:/admin/books/"+bookId+"/edit"; + } + + /** + * 특정 도서의 썸네일을 수정 + * @param bookId 수정할 도서의 식별자 + * @param image 수정할 썸네일 이미지 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/{bookId}/book-thumbnail") + public String updateBookThumbnail(@PathVariable Long bookId, @RequestParam MultipartFile image, + RedirectAttributes redirectAttributes) { + try { + ResponseEntity> response = bookFormApiClient.updateBookThumbnail(bookId, image); + redirectAttributes.addFlashAttribute("bookDetailModifySuccess", Objects.requireNonNull(response.getBody()).getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookDetailModifyError", "도서 썸네일 정보 수정에 실패했습니다."); + } + return "redirect:/admin/books/"+bookId+"/edit"; + } + + /** + * 특정 도서의 이미지를 수정 + * @param bookId 수정할 도서의 식별자 + * @param imageList 수정할 이미지 리스트 + * @return 200 OK, 성공 메세지 + * @author Yujin-nKim(김유진) + */ + @PutMapping("/{bookId}/book-image") + String updateBookImage(@PathVariable Long bookId, @RequestParam("imageList") List imageList, + RedirectAttributes redirectAttributes) { + try { + ResponseEntity> response = bookFormApiClient.updateBookImage(bookId, imageList); + redirectAttributes.addFlashAttribute("bookDetailModifySuccess", Objects.requireNonNull(response.getBody()).getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("bookDetailModifyError", "도서 이미지 정보 수정에 실패했습니다."); + } + return "redirect:/admin/books/"+bookId+"/edit"; + } + + /** + * 도서 삭제 요청을 처리 + * @param bookId 삭제하고자 하는 도서 id + * @author Yujin-nKim(김유진) + */ + @GetMapping("/{bookId}/delete") + public String deleteBook(@PathVariable Long bookId, RedirectAttributes redirectAttributes) { + try { + ResponseEntity> response = bookApiClient.deleteBook(bookId); + redirectAttributes.addFlashAttribute("successMessage", response.getBody().getMessage()); + } catch (FeignException e) { + log.error(e.getMessage()); + redirectAttributes.addFlashAttribute("errorMessage", "도서 삭제에 실패했습니다."); + } + return "redirect:/admin/books"; } } diff --git a/src/main/java/com/t3t/frontserver/book/model/dto/ParticipantMapDto.java b/src/main/java/com/t3t/frontserver/book/model/dto/ParticipantMapDto.java index 076d22c..c51aa96 100644 --- a/src/main/java/com/t3t/frontserver/book/model/dto/ParticipantMapDto.java +++ b/src/main/java/com/t3t/frontserver/book/model/dto/ParticipantMapDto.java @@ -10,10 +10,9 @@ * @author Yujin-nKim(김유진) */ @Data -@ToString @NoArgsConstructor @AllArgsConstructor public class ParticipantMapDto { - private Integer participantId; // 도서 참여자 id + private Long participantId; // 도서 참여자 id private Integer participantRoleId; // 도서 참여자 역할 id } diff --git a/src/main/java/com/t3t/frontserver/book/model/dto/PublisherDto.java b/src/main/java/com/t3t/frontserver/book/model/dto/PublisherDto.java new file mode 100644 index 0000000..98f4f44 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/book/model/dto/PublisherDto.java @@ -0,0 +1,17 @@ +package com.t3t.frontserver.book.model.dto; + +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +import javax.validation.constraints.NotNull; + +@Data +@Getter +@Builder +public class PublisherDto { + @NotNull + private Long publisherId; + private String publisherName; + private String publisherEmail; +} diff --git a/src/main/java/com/t3t/frontserver/book/model/request/ModifyBookDetailRequest.java b/src/main/java/com/t3t/frontserver/book/model/request/ModifyBookDetailRequest.java new file mode 100644 index 0000000..d542a03 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/book/model/request/ModifyBookDetailRequest.java @@ -0,0 +1,46 @@ +package com.t3t.frontserver.book.model.request; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 도서 수정 요청을 나타내는 객체 + * @author Yujin-nKim(김유진) + */ +@Data +public class ModifyBookDetailRequest { + @NotBlank(message = "도서 제목을 입력해주세요.") + private String bookTitle; // 도서 제목 + + @NotBlank(message = "도서 ISBN을 입력해주세요.") + private String bookIsbn; // 도서 isbn + + @NotNull(message = "도서 가격을 입력해주세요.") + @DecimalMin(value = "0.0", message = "도서 가격은 0 이상이어야 합니다.") + private BigDecimal bookPrice; // 도서 정가 + + @DecimalMin(value = "0.0", message = "도서 할인율은 0 이상이어야 합니다.") + @DecimalMax(value = "99.99", message = "도서 할인율은 100 미만이여야 합니다.") + private BigDecimal bookDiscountRate; // 도서 할인율 + + @NotNull(message = "포장 가능 여부를 입력해주세요.") + private Integer packagingAvailableStatus; // 포장 가능 여부 + + @NotNull(message = "도서 출판일을 입력해주세요.") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate bookPublished; // 도서 출판일 + + @NotNull(message = "도서 재고를 입력해주세요.") + @Min(value = 0, message = "도서 재고는 0 이상이어야 합니다.") + private Integer bookStock; // 재고 + + @NotBlank + private String bookIndex; // 도서 목차 + + @NotBlank + private String bookDesc; // 도서 설명 +} diff --git a/src/main/java/com/t3t/frontserver/book/model/request/BookRegisterRequest.java b/src/main/java/com/t3t/frontserver/book/model/request/RegisterBookRequest.java similarity index 88% rename from src/main/java/com/t3t/frontserver/book/model/request/BookRegisterRequest.java rename to src/main/java/com/t3t/frontserver/book/model/request/RegisterBookRequest.java index f750d31..0f23c63 100644 --- a/src/main/java/com/t3t/frontserver/book/model/request/BookRegisterRequest.java +++ b/src/main/java/com/t3t/frontserver/book/model/request/RegisterBookRequest.java @@ -2,7 +2,6 @@ import com.t3t.frontserver.book.model.dto.ParticipantMapDto; import lombok.Data; -import lombok.ToString; import org.springframework.web.multipart.MultipartFile; import javax.validation.constraints.*; @@ -14,8 +13,7 @@ * @author Yujin-nKim(김유진) */ @Data -@ToString -public class BookRegisterRequest { +public class RegisterBookRequest { @NotBlank(message = "도서 제목을 입력해주세요.") private String bookTitle; // 도서 제목 @@ -48,7 +46,7 @@ public class BookRegisterRequest { private String bookDesc; // 도서 설명 @NotNull(message = "출판사 ID를 입력해주세요.") - private Integer publisherId; // 출판사 id + private Long publisherId; // 출판사 id @NotEmpty(message = "도서 참여자를 선택해주세요.") private List participantMapList; // 도서 참여자 - 참여자 역할 선택 리스트 @@ -58,9 +56,7 @@ public class BookRegisterRequest { private List bookImageList; // 도서 미리보기 이미지 - @NotEmpty(message = "카테고리를 선택해주세요.") private List categoryList; // 카테고리 id 리스트 - @NotEmpty(message = "태그를 선택해주세요.") - private List tagList; // 태그 id 리스트 + private List tagList; // 태그 id 리스트 } diff --git a/src/main/java/com/t3t/frontserver/book/model/response/BookListResponse.java b/src/main/java/com/t3t/frontserver/book/model/response/BookListResponse.java new file mode 100644 index 0000000..4140e14 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/book/model/response/BookListResponse.java @@ -0,0 +1,32 @@ +package com.t3t.frontserver.book.model.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 도서 전체 목록 조회시 사용되는 DTO + * @author Yujin-nKim(김유진) + */ +@Data +@Builder +@RequiredArgsConstructor +@AllArgsConstructor +public class BookListResponse { + private Long id; // 도서 id + private String isbn; // isbn + private String bookName; // 도서 제목 + private BigDecimal price; // 정가 + private BigDecimal discountRate; // 할인율 + private LocalDate published; // 출판일시 + private Float averageScore; // 평균 평점 + private Integer likeCount; // 좋아요 수 + private Integer stock; // 재고 수 + private BigDecimal discountedPrice; // 할인가 + private boolean packagingAvailableStatus; // 포장 가능 여부 + private boolean deletedStatus; // 삭제 여부 +} diff --git a/src/main/java/com/t3t/frontserver/category/controller/CategoryController.java b/src/main/java/com/t3t/frontserver/category/controller/CategoryController.java new file mode 100644 index 0000000..57a8c7c --- /dev/null +++ b/src/main/java/com/t3t/frontserver/category/controller/CategoryController.java @@ -0,0 +1,25 @@ +package com.t3t.frontserver.category.controller; + +import com.t3t.frontserver.category.client.CategoryApiClient; +import com.t3t.frontserver.category.response.CategoryTreeResponse; +import com.t3t.frontserver.model.response.BaseResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Controller +public class CategoryController { + private final CategoryApiClient categoryApiClient; + + @GetMapping("/categories") + ResponseEntity>> getCategoryTreeByDepth(@RequestParam Integer startDepth, @RequestParam Integer maxDepth) { + return categoryApiClient.getCategoryTreeByDepth(startDepth, maxDepth); + } +} diff --git a/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java b/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java index 4feb51b..4878003 100644 --- a/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java @@ -1,40 +1,42 @@ package com.t3t.frontserver.common.exception; -import com.t3t.frontserver.auth.exception.RestApiClientException; -import org.json.JSONObject; +import feign.FeignException; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; +@Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) - public String handleException(Model model, Exception e){ - model.addAttribute("message", e.getMessage()); - return "main/page/message"; + public String handleException(RedirectAttributes redirectAttributes, Exception e) { + redirectAttributes.addAttribute("message", e.getMessage()); + return "redirect:/message"; } - @ExceptionHandler(RestApiClientException.class) - public String handleRestApiClientException(Model model, RestApiClientException e, HttpServletResponse response){ - JSONObject json = new JSONObject(e.getResponseBody()); - String message = json.getString("message"); - - if (e.getStatus() == 401 && "Login again".equals(message)) { - Cookie cookie = new Cookie("t3t", null); - cookie.setMaxAge(0); - cookie.setPath("/"); - SecurityContextHolder.clearContext(); - response.addCookie(cookie); - - return "redirect:/"; - }else { - model.addAttribute("message", e.getResponseBody()); - return "main/page/message"; - } +/* @ExceptionHandler(RestApiClientException.class) + public String handleUnAuthorizedException(Model model, RestApiClientException e, HttpServletResponse response) { + Cookie cookie = new Cookie("t3t", null); + cookie.setMaxAge(0); + cookie.setPath("/"); + SecurityContextHolder.clearContext(); + response.addCookie(cookie); + return "redirect:/login"; + }*/ + @ExceptionHandler(FeignException.Unauthorized.class) + public String handleLogoutException(Model model, FeignException.Unauthorized e, HttpServletResponse response) { + Cookie cookie = new Cookie("t3t", null); + cookie.setMaxAge(0); + cookie.setPath("/"); + SecurityContextHolder.clearContext(); + response.addCookie(cookie); + return "redirect:/login"; } } diff --git a/src/main/java/com/t3t/frontserver/config/FeignClientConfig.java b/src/main/java/com/t3t/frontserver/config/FeignClientConfig.java index 879bc7e..2a89645 100644 --- a/src/main/java/com/t3t/frontserver/config/FeignClientConfig.java +++ b/src/main/java/com/t3t/frontserver/config/FeignClientConfig.java @@ -34,7 +34,7 @@ public RequestInterceptor requestInterceptor() { } }; } - @Bean +// @Bean public ErrorDecoder errorDecoder(){ return new CustomErrorDecoder(); } diff --git a/src/main/java/com/t3t/frontserver/config/FormConfiguration.java b/src/main/java/com/t3t/frontserver/config/FormConfiguration.java new file mode 100644 index 0000000..08547c6 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/config/FormConfiguration.java @@ -0,0 +1,22 @@ +package com.t3t.frontserver.config; + +import feign.codec.Encoder; +import feign.form.spring.SpringFormEncoder; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.support.SpringEncoder; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + +public class FormConfiguration { + + /** + * 멀티파트 폼 인코더 빈을 생성하고 구성함 + * 이 인코더는 Feign 클라이언트를 사용하여 요청을 할 때 멀티파트 폼 데이터를 인코딩하는 역할을 담당 + * @return 구성된 멀티파트 폼 인코더 + * @author Yujin-nKim(김유진) + */ + @Bean + public Encoder multipartFormEncoder() { + return new SpringFormEncoder(new SpringEncoder(() -> new HttpMessageConverters(new RestTemplate().getMessageConverters()))); + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/config/SecurityConfig.java b/src/main/java/com/t3t/frontserver/config/SecurityConfig.java index 7ed04c9..dd3c556 100644 --- a/src/main/java/com/t3t/frontserver/config/SecurityConfig.java +++ b/src/main/java/com/t3t/frontserver/config/SecurityConfig.java @@ -54,7 +54,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .httpBasic().disable() .authorizeRequests((auth) -> auth .antMatchers("/admin/**").hasRole("ADMIN") - .antMatchers("/myPage/**").authenticated() + .antMatchers("/mypage/**").authenticated() .antMatchers("/logout").authenticated() .antMatchers("/**").permitAll()) .addFilterAt(new GlobalTokenFilter(), SessionManagementFilter.class) @@ -74,7 +74,7 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc // 관리자 권한이 없습니다. 메시지 추가 response.sendRedirect("/"); } - if (request.getRequestURI().startsWith("/myPage")){ + if (request.getRequestURI().startsWith("/mypage")){ response.sendRedirect("/login"); } } diff --git a/src/main/java/com/t3t/frontserver/elastic/adaptor/ElasticAdaptor.java b/src/main/java/com/t3t/frontserver/elastic/adaptor/ElasticAdaptor.java deleted file mode 100644 index 5b63236..0000000 --- a/src/main/java/com/t3t/frontserver/elastic/adaptor/ElasticAdaptor.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.t3t.frontserver.elastic.adaptor; - -import com.t3t.frontserver.elastic.model.response.ElasticResponse; -import com.t3t.frontserver.model.response.BaseResponse; -import com.t3t.frontserver.model.response.PageResponse; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient(name = "ElasticAdaptor", url = "${t3t.feignClient.url}") -public interface ElasticAdaptor { - @GetMapping("/t3t/bookstore/search") - ResponseEntity>> - getSearchPage(@RequestParam String query, - @RequestParam String searchType, - @RequestParam int pageNo, - @RequestParam(value = "sortBy", defaultValue = "_score", required = false) String sortBy); -} diff --git a/src/main/java/com/t3t/frontserver/elastic/client/ElasticClient.java b/src/main/java/com/t3t/frontserver/elastic/client/ElasticClient.java new file mode 100644 index 0000000..9f4eb02 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/elastic/client/ElasticClient.java @@ -0,0 +1,50 @@ +package com.t3t.frontserver.elastic.client; + +import com.t3t.frontserver.elastic.model.response.ElasticResponse; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; + +@FeignClient(name = "ElasticAdaptor", url = "${t3t.feignClient.url}") +public interface ElasticClient { + /** + * + * elasticsearch 기반 text 검색 + * + * @param query text 검색어 + * @param searchType 검색 유형 + * @param pageNo 페이지 번호 + * @param sortBy 정렬 기준 (기본값: "_socre") + * @return 서버의 데이터를 가지고 옴 + */ + @GetMapping("/t3t/bookstore/search") + ResponseEntity>> + getSearchPage(@RequestParam String query, + @RequestParam String searchType, + @RequestParam int pageNo, + @RequestParam(value = "sortBy", defaultValue = "_score", required = false) String sortBy); + /** + * + * elasticsearch 기반 text 검색 + * + * @param query text 검색어 + * @param searchType 검색 유형 + * @param pageNo 페이지 번호 + * @param categoryId 카테고리 검색을 위한 카테고리번호 + * @param sortBy 정렬 기준 (기본값: "_socre") + * @return 서버의 데이터를 가지고 옴 + */ + @GetMapping("/t3t/bookstore/category/{categoryId}/search") + ResponseEntity>> + getCategorySearchPage(@RequestParam String query, + @RequestParam String searchType, + @RequestParam int pageNo, + @PathVariable(value = "categoryId",required = false) BigDecimal categoryId, + @RequestParam(value = "sortBy", defaultValue = "_score", required = false) String sortBy); +} diff --git a/src/main/java/com/t3t/frontserver/elastic/controller/ElasticCategoryController.java b/src/main/java/com/t3t/frontserver/elastic/controller/ElasticCategoryController.java new file mode 100644 index 0000000..da71db9 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/elastic/controller/ElasticCategoryController.java @@ -0,0 +1,85 @@ +package com.t3t.frontserver.elastic.controller; + +import com.t3t.frontserver.category.client.CategoryApiClient; +import com.t3t.frontserver.category.response.CategoryTreeResponse; +import com.t3t.frontserver.elastic.client.ElasticClient; +import com.t3t.frontserver.elastic.model.response.ElasticResponse; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; +import java.util.List; + +import static com.t3t.frontserver.util.ServiceUtils.handleResponse; + +@Controller +@RequiredArgsConstructor +public class ElasticCategoryController { + private final ElasticClient elasticAdaptor; + private final CategoryApiClient categoryAdaptor; + /** + * + * elasticsearch 기반 text 검색 + * + * @param query text 검색어 + * @param searchType 검색 유형 + * @param pageNo 페이지 번호 + * @param categoryId 카테고리 검색을 위한 카테고리번호 + * @param sortBy 정렬 기준 (기본값: "_socre") + * @return 페이지로 정보를 가지고 이동 + */ + @GetMapping("/category/{categoryId}/search") + public String searchBooks(@RequestParam(value = "query") String query, + @RequestParam("searchType") String searchType, + @PathVariable(value = "categoryId",required = false) BigDecimal categoryId, + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "sortBy", defaultValue = "_score", required = false) String sortBy, + Model model) { + List categoryList = getDataFromCategoryAdaptor(1, 2); + PageResponse bookList = getSearchPageAdaptor(query, searchType, pageNo, categoryId, sortBy); + + if (bookList != null) { + int blockLimit = 3; + int nowPage = bookList.getPageNo() + 1; + int startPage = Math.max(nowPage - blockLimit, 1); + int endPage = Math.min(nowPage + blockLimit, bookList.getTotalPages()); + + model.addAttribute("nowPage", nowPage); + model.addAttribute("startPage", startPage); + model.addAttribute("endPage", endPage); + model.addAttribute("bookList", bookList.getContent()); + model.addAttribute("query", query); //페이징을 위한 검색어 + model.addAttribute("searchType", searchType);//페이징을 위한 검색유형 + model.addAttribute("sortBy", sortBy); //정렬 방식을 위한 객체 + if (categoryId != null) { + model.addAttribute("categoryId", categoryId); + } + model.addAttribute("categoryList", categoryList); + } + + return "main/page/elasticSearch"; + + } + + private PageResponse getSearchPageAdaptor(String query, + String searchType, + int pageNo, + BigDecimal categoryId, + String sortBy) { + ResponseEntity>> elasticResponse + = elasticAdaptor.getCategorySearchPage(query, searchType, pageNo, categoryId, sortBy); + + return handleResponse(elasticResponse); + } + private List getDataFromCategoryAdaptor(Integer startDepth, Integer maxDepth ) { + ResponseEntity>> categoriesResponse = categoryAdaptor.getCategoryTreeByDepth(startDepth, maxDepth); + return handleResponse(categoriesResponse); + } +} diff --git a/src/main/java/com/t3t/frontserver/elastic/controller/ElasticController.java b/src/main/java/com/t3t/frontserver/elastic/controller/ElasticController.java index f4db05a..0610685 100644 --- a/src/main/java/com/t3t/frontserver/elastic/controller/ElasticController.java +++ b/src/main/java/com/t3t/frontserver/elastic/controller/ElasticController.java @@ -1,62 +1,77 @@ package com.t3t.frontserver.elastic.controller; -import com.t3t.frontserver.elastic.adaptor.ElasticAdaptor; +import com.t3t.frontserver.category.client.CategoryApiClient; +import com.t3t.frontserver.category.response.CategoryTreeResponse; +import com.t3t.frontserver.elastic.client.ElasticClient; import com.t3t.frontserver.elastic.model.response.ElasticResponse; import com.t3t.frontserver.model.response.BaseResponse; import com.t3t.frontserver.model.response.PageResponse; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; import static com.t3t.frontserver.util.ServiceUtils.handleResponse; @Controller @RequiredArgsConstructor public class ElasticController { - private final ElasticAdaptor elasticAdaptor; - + private final ElasticClient elasticAdaptor; + private final CategoryApiClient categoryAdaptor; + /** + * + * elasticsearch 기반 text 검색 + * + * @param query text 검색어 + * @param searchType 검색 유형 + * @param pageNo 페이지 번호 + * @param sortBy 정렬 기준 (기본값: "_socre") + * @return 페이지로 정보를 가지고 이동 + */ @GetMapping("/search") public String searchBooks(@RequestParam(value = "query") String query, @RequestParam("searchType") String searchType, @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, @RequestParam(value = "sortBy", defaultValue = "_score", required = false) String sortBy, Model model) { + List categoryList = getDataFromCategoryAdaptor(1, 2); + PageResponse bookList = getSearchPageAdaptor(query, searchType, pageNo, sortBy); - PageResponse bookList = getSearchPageAdaptor(query, searchType, pageNo, sortBy); + if (bookList != null) { + int blockLimit = 3; + int nowPage = bookList.getPageNo() + 1; + int startPage = Math.max(nowPage - blockLimit, 1); + int endPage = Math.min(nowPage + blockLimit, bookList.getTotalPages()); - if (bookList != null) { - int blockLimit = 3; - int nowPage = bookList.getPageNo() + 1; - int startPage = Math.max(nowPage - blockLimit, 1); - int endPage = Math.min(nowPage + blockLimit, bookList.getTotalPages()); + model.addAttribute("nowPage", nowPage); + model.addAttribute("startPage", startPage); + model.addAttribute("endPage", endPage); + model.addAttribute("bookList", bookList.getContent()); + model.addAttribute("query", query); //페이징을 위한 검색어 + model.addAttribute("searchType", searchType);//페이징을 위한 검색유형 + model.addAttribute("sortBy", sortBy); //정렬 방식을 위한 객체 - model.addAttribute("nowPage", nowPage); - model.addAttribute("startPage", startPage); - model.addAttribute("endPage", endPage); - model.addAttribute("bookList", bookList.getContent()); - model.addAttribute("query",query); //페이징을 위한 검색어 - model.addAttribute("searchType",searchType);//페이징을 위한 검색유형 - model.addAttribute("sortBy",sortBy); //정렬 방식을 위한 객체 - } + model.addAttribute("categoryList", categoryList); + } - return "main/page/elasticSearch"; - } + return "main/page/elasticSearch"; + } private PageResponse getSearchPageAdaptor(String query, String searchType, int pageNo, String sortBy) { - ResponseEntity>> elasticResponse - = elasticAdaptor.getSearchPage(query, searchType, pageNo, sortBy); + = elasticAdaptor.getSearchPage(query, searchType, pageNo, sortBy); return handleResponse(elasticResponse); } + private List getDataFromCategoryAdaptor(Integer startDepth, Integer maxDepth) { + ResponseEntity>> categoriesResponse = categoryAdaptor.getCategoryTreeByDepth(startDepth, maxDepth); + return handleResponse(categoriesResponse); + } } diff --git a/src/main/java/com/t3t/frontserver/elastic/model/response/ElasticResponse.java b/src/main/java/com/t3t/frontserver/elastic/model/response/ElasticResponse.java index 7315d8d..6f6aec8 100644 --- a/src/main/java/com/t3t/frontserver/elastic/model/response/ElasticResponse.java +++ b/src/main/java/com/t3t/frontserver/elastic/model/response/ElasticResponse.java @@ -23,6 +23,7 @@ public class ElasticResponse { private String coverImageUrl;//썸네일 private String authorName;//참여자 private String authorRole;//참여자 역할 + private BigDecimal categoryId; private float score; //유사도 점수 private long count; //검색한 책의 수 } diff --git a/src/main/java/com/t3t/frontserver/member/adaptor/MemberAdaptor.java b/src/main/java/com/t3t/frontserver/member/adaptor/MemberAdaptor.java new file mode 100644 index 0000000..d4cfd30 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/adaptor/MemberAdaptor.java @@ -0,0 +1,131 @@ +package com.t3t.frontserver.member.adaptor; + +import com.t3t.frontserver.member.client.MemberApiClient; +import com.t3t.frontserver.member.exception.MemberApiClientException; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.request.MemberPasswordModifyRequest; +import com.t3t.frontserver.member.model.request.MemberRegistrationRequest; +import com.t3t.frontserver.member.model.response.MemberInfoResponse; +import com.t3t.frontserver.member.model.response.MemberRegistrationResponse; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.util.FeignClientUtils; +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class MemberAdaptor { + private final MemberApiClient memberApiClient; + + /** + * 회원 가입 요청 + * + * @param memberRegistrationRequest 회원 가입 요청 정보 + * @author woody35545(구건모) + */ + public MemberRegistrationResponse registerMember(MemberRegistrationRequest memberRegistrationRequest) { + try { + return Optional.ofNullable(memberApiClient.registerMember(memberRegistrationRequest).getBody()) + .map(BaseResponse::getData) + .orElseThrow(MemberApiClientException::new); + } catch (FeignException e) { + throw new MemberApiClientException("회원 가입에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + + /** + * 회원 식별자로 특정 회원 정보를 조회 + * + * @param memberId 조회하려는 회원의 식별자 + * @author woody35545(구건모) + */ + public MemberInfoResponse getMemberInfoResponseById(long memberId) { + try { + return Optional.ofNullable(memberApiClient.getMemberById(memberId).getBody()) + .map(BaseResponse::getData) + .orElseThrow(MemberApiClientException::new); + } catch (FeignException e) { + throw new MemberApiClientException("회원 정보 조회에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 회원 식별자로 특정 회원이 등록한 모든 회원 주소 정보들을 조회 + * + * @param memberId 조회하려는 회원의 식별자 + * @return 회원 주소 목록 + * @author woody35545(구건모) + */ + public List getMemberAddressDtoListByMemberId(long memberId) { + try { + return Optional.ofNullable(memberApiClient.getMemberAddressListByMemberId(memberId).getBody()) + .map(BaseResponse::getData) + .orElseThrow(MemberApiClientException::new); + } catch (FeignException e) { + throw new MemberApiClientException("회원 주소 목록 조회에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 회원 비밀번호 변경 + * + * @param memberId 회원 식별자 + * @param request 비밀번호 변경 요청 정보 + * @autor woody35545(구건모) + */ + public void modifyPassword(long memberId, MemberPasswordModifyRequest request) { + try { + memberApiClient.modifyMemberPassword(memberId, request); + } catch (FeignException e) { + throw new MemberApiClientException("비밀번호 변경에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 회원 탈퇴 + * + * @param memberId 탈퇴할 회원 식별자 + * @autor woody35545(구건모) + */ + public void withdrawMember(long memberId) { + try { + memberApiClient.withdrawMember(memberId); + } catch (FeignException e) { + throw new MemberApiClientException("회원 탈퇴에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 회원 휴면 계정 활성화 코드 발급 + * + * @param memberId 대상 회원 식별자 + * @author woody35545(구건모) + */ + public void issueMemberActivationCode(long memberId) { + try { + memberApiClient.issueMemberActivationCertCode(memberId); + } catch (FeignException e) { + throw new MemberApiClientException("회원 활성화 코드 발급에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 회원 휴면 계정 활성화 코드 검증 + * + * @param memberId 대상 회원 식별자 + * @param code 인증 코드 + * @author woody35545(구건모) + */ + public void verifyMemberActivationCode(long memberId, String code) { + try { + memberApiClient.verifyMemberActivationCertCode(memberId, code); + } catch (FeignException e) { + throw new MemberApiClientException("회원 활성화 코드 검증에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } +} diff --git a/src/main/java/com/t3t/frontserver/member/adaptor/MemberAddressAdaptor.java b/src/main/java/com/t3t/frontserver/member/adaptor/MemberAddressAdaptor.java new file mode 100644 index 0000000..1c2e638 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/adaptor/MemberAddressAdaptor.java @@ -0,0 +1,62 @@ +package com.t3t.frontserver.member.adaptor; + +import com.t3t.frontserver.member.client.MemberAddressApiClient; +import com.t3t.frontserver.member.exception.MemberAddressApiClientException; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.request.MemberAddressCreationRequest; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.util.FeignClientUtils; +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class MemberAddressAdaptor { + private final MemberAddressApiClient memberAddressApiClient; + + /** + * 회원 주소 생성 + * + * @author woody35545(구건모) + */ + public MemberAddressDto createMemberAddress(MemberAddressCreationRequest request) { + try { + return Optional.ofNullable(memberAddressApiClient.createMemberAddress(request).getBody()) + .map(BaseResponse::getData) + .orElseThrow(MemberAddressApiClientException::new); + } catch (FeignException e) { + throw new MemberAddressApiClientException(FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 회원 기본 주소 설정 및 변경 + * + * @param memberAddressId 변경하려는 회원 주소 식별자 + * @author woody35545(구건모) + */ + public void modifyDefaultAddress(long memberAddressId) { + try { + memberAddressApiClient.modifyDefaultAddress(memberAddressId); + } catch (FeignException e) { + throw new MemberAddressApiClientException(FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 회원 주소 삭제 + * + * @param memberAddressId 삭제하려는 회원 주소 식별자 + * @author woody35545(구건모) + */ + public void deleteMemberAddress(long memberAddressId) { + try { + memberAddressApiClient.deleteMemberAddress(memberAddressId); + } catch (FeignException e) { + throw new MemberAddressApiClientException(FeignClientUtils.getMessageFromFeignException(e)); + } + } +} diff --git a/src/main/java/com/t3t/frontserver/member/client/MemberAddressApiClient.java b/src/main/java/com/t3t/frontserver/member/client/MemberAddressApiClient.java new file mode 100644 index 0000000..1139a28 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/client/MemberAddressApiClient.java @@ -0,0 +1,37 @@ +package com.t3t.frontserver.member.client; + +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.request.MemberAddressCreationRequest; +import com.t3t.frontserver.model.response.BaseResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@FeignClient(name = "MemberAddressApiClient", url = "${t3t.feignClient.url}") +public interface MemberAddressApiClient { + /** + * 회원 주소 등록 API + * + * @author woody35545(구건모) + */ + @PostMapping("/t3t/bookstore/member-addresses") + ResponseEntity> createMemberAddress(@RequestBody MemberAddressCreationRequest request); + + /** + * 기본 주소 설정 및 변경 API + * + * @param memberAddressId 변경할 회원 주소 식별자 + * @author woody35545(구건모) + */ + @PatchMapping("/t3t/bookstore/member-addresses/{memberAddressId}/default") + BaseResponse modifyDefaultAddress(@PathVariable("memberAddressId") long memberAddressId); + + /** + * 회원 주소 삭제 API + * + * @param memberAddressId 삭제할 회원 주소 식별자 + * @author woody35545(구건모) + */ + @DeleteMapping("/t3t/bookstore/member-addresses/{memberAddressId}") + ResponseEntity> deleteMemberAddress(@PathVariable("memberAddressId") long memberAddressId); +} diff --git a/src/main/java/com/t3t/frontserver/member/client/MemberApiClient.java b/src/main/java/com/t3t/frontserver/member/client/MemberApiClient.java index 64a6785..a75fce9 100644 --- a/src/main/java/com/t3t/frontserver/member/client/MemberApiClient.java +++ b/src/main/java/com/t3t/frontserver/member/client/MemberApiClient.java @@ -1,23 +1,86 @@ package com.t3t.frontserver.member.client; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.request.MemberPasswordModifyRequest; import com.t3t.frontserver.member.model.request.MemberRegistrationRequest; +import com.t3t.frontserver.member.model.response.MemberInfoResponse; import com.t3t.frontserver.member.model.response.MemberRegistrationResponse; +import com.t3t.frontserver.model.response.BaseResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.*; + +import java.util.List; /** * 회원 가입 요청 API 호출을 위한 Feign Client + * * @author woody35545(구건모) */ @FeignClient(name = "MemberApiClient", url = "${t3t.feignClient.url}") public interface MemberApiClient { /** * 회원 가입 요청 API 호출 + * * @param memberRegistrationRequest 회원 가입 요청 정보 * @author woody35545(구건모) */ - @PostMapping(value = "/bookstore/members") - ResponseEntity registerMember(@RequestBody MemberRegistrationRequest memberRegistrationRequest); + @PostMapping(value = "/t3t/bookstore/members") + ResponseEntity> registerMember(@RequestBody MemberRegistrationRequest memberRegistrationRequest); + + /** + * 회원 식별자로 특정 회원 정보를 조회하는 API + * + * @param memberId 조회하려는 회원의 식별자 + * @author woody35545(구건모) + */ + @GetMapping(value = "/t3t/bookstore/members/{memberId}") + ResponseEntity> getMemberById(@PathVariable("memberId") long memberId); + + /** + * 회원 식별자로 특정 회원이 등록한 모든 회원 주소 정보들을 조회하는 API + * + * @param memberId 조회하려는 회원의 식별자 + * @return 회원 주소 목록 + * @author woody35545(구건모) + */ + @GetMapping("/t3t/bookstore/members/{memberId}/addresses") + ResponseEntity>> getMemberAddressListByMemberId(@PathVariable("memberId") long memberId); + + /** + * 회원 비밀번호 변경 API + * + * @param memberId 회원 식별자 + * @param request 비밀번호 변경 요청 정보 + * @author woody35545(구건모) + */ + @PatchMapping("/t3t/bookstore/members/{memberId}") + ResponseEntity> modifyMemberPassword(@PathVariable("memberId") long memberId, @RequestBody MemberPasswordModifyRequest request); + + + /** + * 회원 탈퇴 API + * + * @param memberId 탈퇴할 회원 식별자 + * @author woody35545(구건모) + */ + @DeleteMapping("/t3t/bookstore/members/{memberId}") + BaseResponse withdrawMember(@PathVariable("memberId") long memberId); + + /** + * 회원 휴면 계정 활성화 인증 코드 발급 API + * + * @author wooody35545(구건모) + */ + @PostMapping("/t3t/bookstore/members/{memberId}/codes?type=issue") + BaseResponse issueMemberActivationCertCode(@PathVariable("memberId") Long memberId); + + /** + * 회원 휴면 계정 활성화 인증 코드 검증 API + * + * @author wooody35545(구건모) + */ + @PostMapping("/t3t/bookstore/members/{memberId}/codes?type=verify") + BaseResponse verifyMemberActivationCertCode(@PathVariable("memberId") Long memberId, @RequestParam("value") String code); + } diff --git a/src/main/java/com/t3t/frontserver/member/controller/MemberAddressController.java b/src/main/java/com/t3t/frontserver/member/controller/MemberAddressController.java new file mode 100644 index 0000000..a1074f7 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/controller/MemberAddressController.java @@ -0,0 +1,81 @@ +package com.t3t.frontserver.member.controller; + +import com.t3t.frontserver.auth.util.SecurityContextUtils; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.request.MemberAddressCreationRequest; +import com.t3t.frontserver.member.service.MemberAddressService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class MemberAddressController { + + private final MemberAddressService memberAddressService; + + /** + * 회원 주소 등록 요청 처리 + * + * @author woody35545(구건모) + */ + @PostMapping("/member-addresses") + public String createMemberAddress(@RequestParam("roadNameAddress") String roadNameAddress, + @RequestParam("addressDetail") String addressDetail, + @RequestParam("addressNumber") Integer addressNumber, + @RequestParam("addressNickname") String addressNickname) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + MemberAddressCreationRequest request = + MemberAddressCreationRequest.builder() + .memberId(SecurityContextUtils.getMemberId()) + .roadNameAddress(roadNameAddress) + .addressDetail(addressDetail) + .addressNumber(addressNumber) + .addressNickname(addressNickname) + .build(); + + MemberAddressDto memberAddressDto = memberAddressService.createMemberAddress(request); + + log.info("memberAddressDto => {}", memberAddressDto); + + return "redirect:/mypage/address"; + } + + /** + * 회원 기본 주소 설정 및 변경 요청 처리 + */ + @PatchMapping("/member-addresses/default") + public String modifyDefaultAddress(@RequestParam("memberAddressId") Long memberAddressId) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + log.info("memberAddressId => {}", memberAddressId); + memberAddressService.modifyDefaultAddress(memberAddressId); + + return "redirect:/mypage/address"; + } + + /** + * 회원 주소 삭제 요청 처리 + */ + @DeleteMapping("/member-addresses") + public String deleteMemberAddress(@RequestParam("memberAddressId") Long memberAddressId) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + log.info("memberAddressId => {}", memberAddressId); + memberAddressService.deleteMemberAddress(memberAddressId); + + return "redirect:/mypage/address"; + } +} diff --git a/src/main/java/com/t3t/frontserver/member/controller/MemberController.java b/src/main/java/com/t3t/frontserver/member/controller/MemberController.java index 0384751..b63e4fd 100644 --- a/src/main/java/com/t3t/frontserver/member/controller/MemberController.java +++ b/src/main/java/com/t3t/frontserver/member/controller/MemberController.java @@ -1,14 +1,22 @@ package com.t3t.frontserver.member.controller; +import com.t3t.frontserver.auth.util.SecurityContextUtils; +import com.t3t.frontserver.member.model.request.MemberPasswordModifyRequest; import com.t3t.frontserver.member.model.request.MemberRegistrationRequest; import com.t3t.frontserver.member.service.MemberService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; 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 javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +@Slf4j @Controller @RequiredArgsConstructor public class MemberController { @@ -16,6 +24,7 @@ public class MemberController { /** * 회원 가입 페이지 뷰 반환 + * * @return 회원 가입 뷰 * @author woody35545(구건모) */ @@ -27,13 +36,108 @@ public String registerView(Model model) { /** * 회원 가입 요청 처리 + * * @return 성공 또는 실패 뷰 * @author woody35545(구건모) */ @PostMapping("/member/register") - public String register(Model model, @Valid @ModelAttribute MemberRegistrationRequest memberRegistrationRequest) { + public String register(RedirectAttributes redirectAttributes, @Valid @ModelAttribute MemberRegistrationRequest memberRegistrationRequest) { memberService.registerMember(memberRegistrationRequest); - model.addAttribute("message", "회원가입이 완료되었습니다."); - return "main/page/message"; + redirectAttributes.addAttribute("message", "회원가입이 완료되었습니다."); + return "redirect:/message"; + } + + /** + * 비밀번호 변경 처리 + * + * @author woody35545(구건모) + */ + @PatchMapping("/members/password") + public String modifyPassword(RedirectAttributes redirectAttributes, + @RequestParam("currentPassword") String currentPassword, + @RequestParam("newPassword") String newPassword, + @RequestParam("confirmNewPassword") String confirmNewPassword) { + + if (!SecurityContextUtils.isLoggedIn()) { + redirectAttributes.addAttribute("message", "로그인이 필요합니다."); + return "redirect:/message"; + } + + if (!newPassword.equals(confirmNewPassword)) { + redirectAttributes.addAttribute("message", "변경할 비밀번호가 일치하지 않습니다."); + return "redirect:/message"; + } + + MemberPasswordModifyRequest request = MemberPasswordModifyRequest.builder() + .currentPassword(currentPassword) + .newPassword(newPassword) + .build(); + + memberService.modifyPassword(SecurityContextUtils.getMemberId(), request); + redirectAttributes.addAttribute("message", "비밀번호가 변경되었습니다."); + return "redirect:/message"; + } + + /** + * 회원 탈퇴 요청 처리 + * + * @author woody35545(구건모) + */ + @DeleteMapping("/members") + public String deleteMember(RedirectAttributes redirectAttributes, HttpServletResponse response) { + + if (!SecurityContextUtils.isLoggedIn()) { + redirectAttributes.addAttribute("message", "로그인이 필요합니다."); + return "redirect:/message"; + } + + memberService.withdrawMember(SecurityContextUtils.getMemberId()); + + SecurityContextHolder.clearContext(); + + Cookie cookie = new Cookie("t3t", null); + cookie.setMaxAge(0); + cookie.setHttpOnly(true); + response.addCookie(cookie); + + redirectAttributes.addAttribute("message", "회원 탈퇴가 완료되었습니다."); + + return "redirect:/message"; + } + + /** + * 휴면 회원 활성화 인증 코드 발급 + * @author woody35545(구건모) + */ + @PostMapping("/member/activation/issue") + public String issueMemberActivationCertCode(@RequestParam("memberId") long memberId, + @RequestParam("memberLatestLogin") String memberLatestLogin, + @RequestParam("memberName") String memberName, + Model model) { + memberService.issueMemberActivationCode(memberId); + + model.addAttribute("memberLatestLogin", memberLatestLogin); + model.addAttribute("memberId", memberId); + model.addAttribute("memberName", memberName); + + return "main/page/activateMemberVerify"; + } + + /** + * 휴면 회원 활성화 인증 코드 검증 + * + * @param memberId 회원 식별자 + * @param code 인증 코드 + * @author woody35545(구건모) + */ + @PostMapping("/member/activation/verify") + public String verifyMemberActivationCertCode(@RequestParam("memberId") long memberId, + @RequestParam("activationCode") String code, + RedirectAttributes redirectAttributes) { + + memberService.verifyMemberActivationCode(memberId, code); + + redirectAttributes.addAttribute("message", "회원 활성화가 완료되었습니다. 다시 로그인 해주세요."); + return "redirect:/message"; } } diff --git a/src/main/java/com/t3t/frontserver/member/controller/MyPageController.java b/src/main/java/com/t3t/frontserver/member/controller/MyPageController.java new file mode 100644 index 0000000..7aca139 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/controller/MyPageController.java @@ -0,0 +1,165 @@ +package com.t3t.frontserver.member.controller; + +import com.t3t.frontserver.auth.util.SecurityContextUtils; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.dto.MyPageAddressViewDto; +import com.t3t.frontserver.member.model.dto.MyPageInfoViewDto; +import com.t3t.frontserver.member.model.response.MemberInfoResponse; +import com.t3t.frontserver.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.List; +import java.util.stream.Collectors; + +@Controller +@RequiredArgsConstructor +public class MyPageController { + + private final MemberService memberService; + + /** + * 마이페이지 - 회원 기본 정보 관리 뷰 + * + * @author woody35545(구건모) + */ + @GetMapping("/mypage/info") + public String myPageInfoView(Model model) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + Long memberId = SecurityContextUtils.getMemberId(); + + MemberInfoResponse memberInfoResponse = memberService.getMemberInfoResponseById(memberId); + List memberAddressDtoList = memberService.getMemberAddressDtoListByMemberId(memberId); + + MyPageInfoViewDto myPageInfoViewDto = + MyPageInfoViewDto.builder() + .accountId(memberInfoResponse.getAccountId()) + .email(memberInfoResponse.getEmail()) + .name(memberInfoResponse.getName()) + .phone(memberInfoResponse.getPhone()) + .birthDate(memberInfoResponse.getBirthDate()) + .latestLogin(memberInfoResponse.getLatestLogin()) + .point(memberInfoResponse.getPoint()) + .gradeId(memberInfoResponse.getGradeId()) + .gradeName(memberInfoResponse.getGradeName()) + .status(memberInfoResponse.getStatus().name()) + .role(memberInfoResponse.getRole().name()) + .defaultMemberAddressInfo(memberAddressDtoList.stream() + .filter(MemberAddressDto::getIsDefaultAddress) + .map(memberAddressDto -> MyPageInfoViewDto.MemberAddressInfo.builder() + .id(memberAddressDto.getId()) + .roadNameAddress(memberAddressDto.getRoadNameAddress()) + .addressDetail(memberAddressDto.getAddressDetail()) + .nickName(memberAddressDto.getAddressNickname()) + .isDefault(memberAddressDto.getIsDefaultAddress()) + .build()) + .findFirst() + .orElse(null)) + .memberAddressList(memberAddressDtoList.stream() + .map(addressDto -> MyPageInfoViewDto.MemberAddressInfo.builder() + .id(addressDto.getId()) + .roadNameAddress(addressDto.getRoadNameAddress()) + .addressDetail(addressDto.getAddressDetail()) + .nickName(addressDto.getAddressNickname()) + .isDefault(addressDto.getIsDefaultAddress()) + .build()) + .collect(Collectors.toList())) + .build(); + + model.addAttribute("myPageInfoViewDto", myPageInfoViewDto); + + return "main/page/mypageInfo"; + } + + /** + * 마이페이지 주소 관리 뷰 + * + * @author woody35545(구건모) + */ + @GetMapping("/mypage/address") + public String myPageAddressView(Model model) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + List memberAddressDtoList = memberService.getMemberAddressDtoListByMemberId(SecurityContextUtils.getMemberId()); + + MyPageAddressViewDto.MemberAddressInfo defaultMemberAddressInfo = memberAddressDtoList.stream() + .filter(MemberAddressDto::getIsDefaultAddress) + .map(memberAddressDto -> MyPageAddressViewDto.MemberAddressInfo.builder() + .id(memberAddressDto.getId()) + .roadNameAddress(memberAddressDto.getRoadNameAddress()) + .addressDetail(memberAddressDto.getAddressDetail()) + .nickName(memberAddressDto.getAddressNickname()) + .isDefault(memberAddressDto.getIsDefaultAddress()) + .build()) + .findFirst() + .orElse(null); + + MyPageAddressViewDto myPageAddressViewDto = MyPageAddressViewDto.builder() + .memberAddressInfoList(memberService.getMemberAddressDtoListByMemberId(SecurityContextUtils.getMemberId()) + .stream() + .map(addressDto -> MyPageAddressViewDto.MemberAddressInfo.builder() + .id(addressDto.getId()) + .roadNameAddress(addressDto.getRoadNameAddress()) + .addressDetail(addressDto.getAddressDetail()) + .nickName(addressDto.getAddressNickname()) + .isDefault(addressDto.getIsDefaultAddress()) + .build()) + .collect(Collectors.toList())) + + .defaultMemberAddressInfo(defaultMemberAddressInfo) + .build(); + + model.addAttribute("myPageAddressViewDto", myPageAddressViewDto); + + return "main/page/mypageAddress"; + } + + /** + * 마이페이지 - 회원 탈퇴 페이지 뷰 + * + * @author woody35545(구건모) + */ + @GetMapping("/mypage/withdrawal") + public String withdrawalView() { + return "main/page/mypageWithdrawal"; + } + + /** + * 마이페이지 - 회원 등급 페이지 뷰 + * + * @author woody35545(구건모) + */ + + @GetMapping("/mypage/grade") + public String gradeView(Model model) { + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + long memberId = SecurityContextUtils.getMemberId(); + MemberInfoResponse memberInfoResponse = memberService.getMemberInfoResponseById(memberId); + + model.addAttribute("gradeId", memberInfoResponse.getGradeId()); + + return "main/page/mypageGrade"; + } + + /** + * 마이페이지 - 회원 주문 페이지 뷰 + * @author woody35545(구건모) + */ + @GetMapping("/mypage/order") + public String orderView() { + return "main/page/mypageOrder"; + } + +} diff --git a/src/main/java/com/t3t/frontserver/member/exception/MemberAddressApiClientException.java b/src/main/java/com/t3t/frontserver/member/exception/MemberAddressApiClientException.java new file mode 100644 index 0000000..4f5b82f --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/exception/MemberAddressApiClientException.java @@ -0,0 +1,21 @@ +package com.t3t.frontserver.member.exception; + + +/** + * 회원 주소 API 호출 실패시 발생하는 예외 + * + * @author woody35545(구건모) + */ +public class MemberAddressApiClientException extends RuntimeException { + + private static final String DEFAULT_MESSAGE = "회원 주소 API 호출에 실패하였습니다."; + + public MemberAddressApiClientException() { + super(DEFAULT_MESSAGE); + } + + public MemberAddressApiClientException(String reason) { + super(DEFAULT_MESSAGE + " " + reason); + } + +} diff --git a/src/main/java/com/t3t/frontserver/member/exception/MemberApiClientException.java b/src/main/java/com/t3t/frontserver/member/exception/MemberApiClientException.java new file mode 100644 index 0000000..9a28a1a --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/exception/MemberApiClientException.java @@ -0,0 +1,15 @@ +package com.t3t.frontserver.member.exception; + +/** + * 회원 API 호출 실패시 발생하는 예외 + * @author woody35545(구건모) + */ +public class MemberApiClientException extends RuntimeException{ + private static final String DEFAULT_MESSAGE = "회원 API 호출에 실패하였습니다."; + public MemberApiClientException(){ + super(DEFAULT_MESSAGE); + } + public MemberApiClientException(String message) { + super(message); + } +} diff --git a/src/main/java/com/t3t/frontserver/member/model/dto/MemberAddressDto.java b/src/main/java/com/t3t/frontserver/member/model/dto/MemberAddressDto.java new file mode 100644 index 0000000..d0b3c8b --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/model/dto/MemberAddressDto.java @@ -0,0 +1,26 @@ +package com.t3t.frontserver.member.model.dto; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 회원 주소 정보에 대한 DTO + * + * @author woody35545(구건모) + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemberAddressDto { + private Long id; + private Long memberId; + private Integer addressNumber; + private String roadNameAddress; + private String addressNickname; + private String addressDetail; + private Boolean isDefaultAddress; +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/member/model/dto/MyPageAddressViewDto.java b/src/main/java/com/t3t/frontserver/member/model/dto/MyPageAddressViewDto.java new file mode 100644 index 0000000..a9002c4 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/model/dto/MyPageAddressViewDto.java @@ -0,0 +1,34 @@ +package com.t3t.frontserver.member.model.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +/** + * 마이페이지 주소 관리에서 사용될 정보를 담는 DTO + * + * @author woody35545(구건모) + */ +@Getter +@Builder +public class MyPageAddressViewDto { + + private List memberAddressInfoList; + private MemberAddressInfo defaultMemberAddressInfo; + + /** + * MemberAddress 관련 정보를 담는 DTO + * + * @author woody35545(구건모) + */ + @Getter + @Builder + public static class MemberAddressInfo { + private Long id; // MemberAddress 식별자 + private String roadNameAddress; // 도로명 주소 + private String addressDetail; // 상세 주소 + private String nickName; // 주소 별칭 + private Boolean isDefault; // 기본 주소 여부 + } +} diff --git a/src/main/java/com/t3t/frontserver/member/model/dto/MyPageInfoViewDto.java b/src/main/java/com/t3t/frontserver/member/model/dto/MyPageInfoViewDto.java new file mode 100644 index 0000000..1df0984 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/model/dto/MyPageInfoViewDto.java @@ -0,0 +1,42 @@ +package com.t3t.frontserver.member.model.dto; + +import lombok.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 회원 정보 페이지 뷰에 필요한 정보를 정의한 DTO + * + * @author woody35545(구건모) + */ +@ToString +@Getter +@Builder +public class MyPageInfoViewDto { + private String accountId; + private Long memberId; + private String name; + private String phone; + private String email; + private LocalDate birthDate; + private LocalDateTime latestLogin; + private Long point; + private Integer gradeId; + private String gradeName; + private String status; + private String role; + private MemberAddressInfo defaultMemberAddressInfo; + private List memberAddressList; + + @Getter + @Builder + public static class MemberAddressInfo { + private Long id; // MemberAddress 식별자 + private String roadNameAddress; // 도로명 주소 + private String addressDetail; // 상세 주소 + private String nickName; // 주소 별칭 + private Boolean isDefault; // 기본 주소 여부 + } +} diff --git a/src/main/java/com/t3t/frontserver/member/model/request/MemberAddressCreationRequest.java b/src/main/java/com/t3t/frontserver/member/model/request/MemberAddressCreationRequest.java new file mode 100644 index 0000000..8cba7c8 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/model/request/MemberAddressCreationRequest.java @@ -0,0 +1,31 @@ +package com.t3t.frontserver.member.model.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 회원 주소 생성 요청 객체 + * + * @author woody35545(구건모) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MemberAddressCreationRequest { + @NotNull(message = "회원 식별자가 누락되었습니다.") + private Long memberId; + @NotNull(message = "우편 주소가 누락되었습니다.") + private Integer addressNumber; + @NotBlank(message = "도로명 주소가 누락되었습니다.") + private String roadNameAddress; + @NotBlank(message = "주소 별칭이 누락되었습니다.") + private String addressNickname; + @NotBlank(message = "상세 주소가 누락되었습니다.") + private String addressDetail; +} diff --git a/src/main/java/com/t3t/frontserver/member/model/request/MemberPasswordModifyRequest.java b/src/main/java/com/t3t/frontserver/member/model/request/MemberPasswordModifyRequest.java new file mode 100644 index 0000000..4bbd436 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/model/request/MemberPasswordModifyRequest.java @@ -0,0 +1,21 @@ +package com.t3t.frontserver.member.model.request; + +import lombok.*; + +import javax.validation.constraints.NotBlank; + +/** + * 회원 비밀번호 수정 요청 객체 + * + * @author woody35545(구건모) + */ +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MemberPasswordModifyRequest { + @NotBlank(message = "현재 비밀번호가 누락되었습니다.") + private String currentPassword; + @NotBlank(message = "새로운 비밀번호가 누락되었습니다.") + private String newPassword; +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/member/model/response/MemberInfoResponse.java b/src/main/java/com/t3t/frontserver/member/model/response/MemberInfoResponse.java new file mode 100644 index 0000000..b4ad83d --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/model/response/MemberInfoResponse.java @@ -0,0 +1,36 @@ +package com.t3t.frontserver.member.model.response; + +import com.t3t.frontserver.member.model.constant.MemberRole; +import com.t3t.frontserver.member.model.constant.MemberStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 회원과 관련된 정보를 종합적으로 담기 위한 응답 클래스 + * + * @author woody35545(구건모) + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberInfoResponse { + private String accountId; + private Long memberId; + private String name; + private String phone; + private String email; + private LocalDate birthDate; + private LocalDateTime latestLogin; + private Long point; + private Integer gradeId; + private String gradeName; + private MemberStatus status; + private MemberRole role; +} diff --git a/src/main/java/com/t3t/frontserver/member/service/MemberAddressService.java b/src/main/java/com/t3t/frontserver/member/service/MemberAddressService.java new file mode 100644 index 0000000..4f3b443 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/member/service/MemberAddressService.java @@ -0,0 +1,44 @@ +package com.t3t.frontserver.member.service; + +import com.t3t.frontserver.member.adaptor.MemberAddressAdaptor; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.request.MemberAddressCreationRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MemberAddressService { + private final MemberAddressAdaptor memberAddressAdaptor; + + /** + * 회원 주소 생성 + * + * @param request + */ + public MemberAddressDto createMemberAddress(MemberAddressCreationRequest request) { + return memberAddressAdaptor.createMemberAddress(request); + } + + /** + * 회원 기본 주소 설정 및 변경 + * + * @param memberAddressId 변경하려는 회원 주소 식별자 + * @author woody35545(구건모) + */ + public void modifyDefaultAddress(long memberAddressId) { + memberAddressAdaptor.modifyDefaultAddress(memberAddressId); + } + + /** + * 회원 주소 삭제 + * + * @param memberAddressId 삭제하려는 회원 주소 식별자 + * @author woody35545(구건모) + */ + public void deleteMemberAddress(long memberAddressId) { + memberAddressAdaptor.deleteMemberAddress(memberAddressId); + } +} diff --git a/src/main/java/com/t3t/frontserver/member/service/MemberService.java b/src/main/java/com/t3t/frontserver/member/service/MemberService.java index 3c4f9b4..5572c11 100644 --- a/src/main/java/com/t3t/frontserver/member/service/MemberService.java +++ b/src/main/java/com/t3t/frontserver/member/service/MemberService.java @@ -1,24 +1,95 @@ package com.t3t.frontserver.member.service; -import com.t3t.frontserver.member.client.MemberApiClient; +import com.t3t.frontserver.member.adaptor.MemberAdaptor; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.request.MemberPasswordModifyRequest; import com.t3t.frontserver.member.model.request.MemberRegistrationRequest; +import com.t3t.frontserver.member.model.response.MemberInfoResponse; import com.t3t.frontserver.member.model.response.MemberRegistrationResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -@Slf4j +import java.util.List; + + @Service @RequiredArgsConstructor public class MemberService { - private final MemberApiClient memberApiClient; + + private final MemberAdaptor memberAdaptor; /** * 회원 가입 요청 + * * @param memberRegistrationRequest 회원 가입 요청 정보 * @author woody35545(구건모) */ - public MemberRegistrationResponse registerMember(MemberRegistrationRequest memberRegistrationRequest){ - return memberApiClient.registerMember(memberRegistrationRequest).getBody(); + public MemberRegistrationResponse registerMember(MemberRegistrationRequest memberRegistrationRequest) { + return memberAdaptor.registerMember(memberRegistrationRequest); + } + + + /** + * 회원 식별자로 특정 회원 정보를 조회 + * + * @param memberId 조회하려는 회원의 식별자 + * @author woody35545(구건모) + */ + public MemberInfoResponse getMemberInfoResponseById(long memberId) { + return memberAdaptor.getMemberInfoResponseById(memberId); + } + + /** + * 회원 식별자로 특정 회원이 등록한 모든 회원 주소 정보들을 조회 + * + * @param memberId 조회하려는 회원의 식별자 + * @return 회원 주소 목록 + * @author woody35545(구건모) + */ + public List getMemberAddressDtoListByMemberId(long memberId) { + return memberAdaptor.getMemberAddressDtoListByMemberId(memberId); + } + + /** + * 회원 비밀번호 변경 + * + * @param memberId 회원 식별자 + * @param request 비밀번호 변경 요청 정보 + * @author woody35545(구건모) + */ + public void modifyPassword(long memberId, MemberPasswordModifyRequest request) { + memberAdaptor.modifyPassword(memberId, request); + } + + /** + * 회원 탈퇴 + * + * @param memberId 탈퇴할 회원 식별자 + * @author woody35545(구건모) + */ + public void withdrawMember(long memberId) { + memberAdaptor.withdrawMember(memberId); + } + + /** + * 휴면 회원 활성화 인증 코드 발급 + * + * @param memberId 회원 식별자 + * @author woody35545(구건모) + */ + public void issueMemberActivationCode(long memberId) { + memberAdaptor.issueMemberActivationCode(memberId); + } + + /** + * 휴면 회원 활성화 인증 코드 검증 + * + * @param memberId 회원 식별자 + * @param activationCode 활성화 코드 + * @author woody35545(구건모) + */ + public void verifyMemberActivationCode(long memberId, String activationCode) { + memberAdaptor.verifyMemberActivationCode(memberId, activationCode); } } diff --git a/src/main/java/com/t3t/frontserver/message/controller/MessageController.java b/src/main/java/com/t3t/frontserver/message/controller/MessageController.java new file mode 100644 index 0000000..31d9502 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/message/controller/MessageController.java @@ -0,0 +1,18 @@ +package com.t3t.frontserver.message.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class MessageController { + + @GetMapping("/message") + public String messageView(@RequestParam("message") String message, Model model) { + + model.addAttribute("message", message); + + return "main/page/message"; + } +} diff --git a/src/main/java/com/t3t/frontserver/participant/client/ParticipantApiClient.java b/src/main/java/com/t3t/frontserver/participant/client/ParticipantApiClient.java new file mode 100644 index 0000000..a2e3a91 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/participant/client/ParticipantApiClient.java @@ -0,0 +1,26 @@ +package com.t3t.frontserver.participant.client; + +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.participant.dto.ParticipantDto; +import com.t3t.frontserver.participant.dto.ParticipantRoleDto; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "categoryApiClient", url = "${t3t.feignClient.url}") +public interface ParticipantApiClient { + + @GetMapping("/t3t/bookstore/participants") + ResponseEntity>> getParticipantList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "participantId", required = false) String sortBy); + + @GetMapping("/t3t/bookstore/participantRoles") + ResponseEntity>> getParticipantRoleList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "participantRoleId", required = false) String sortBy); +} diff --git a/src/main/java/com/t3t/frontserver/participant/controller/ParticipantController.java b/src/main/java/com/t3t/frontserver/participant/controller/ParticipantController.java new file mode 100644 index 0000000..0932849 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/participant/controller/ParticipantController.java @@ -0,0 +1,38 @@ +package com.t3t.frontserver.participant.controller; + +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.participant.client.ParticipantApiClient; +import com.t3t.frontserver.participant.dto.ParticipantDto; +import com.t3t.frontserver.participant.dto.ParticipantRoleDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Slf4j +@RequiredArgsConstructor +@Controller +public class ParticipantController { + private final ParticipantApiClient participantApiClient; + + @GetMapping("/participants") + ResponseEntity>> getParticipantList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "participantId", required = false) String sortBy) { + + return participantApiClient.getParticipantList(pageNo, pageSize, sortBy); + } + + @GetMapping("/participantRoles") + ResponseEntity>> getParticipantRoleList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "participantRoleId", required = false) String sortBy) { + + return participantApiClient.getParticipantRoleList(pageNo, pageSize, sortBy); + } +} diff --git a/src/main/java/com/t3t/frontserver/participant/dto/ParticipantDto.java b/src/main/java/com/t3t/frontserver/participant/dto/ParticipantDto.java new file mode 100644 index 0000000..cb30e38 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/participant/dto/ParticipantDto.java @@ -0,0 +1,21 @@ +package com.t3t.frontserver.participant.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 도서 참여자에 대한 데이터 전송 객체(DTO)
+ * 각 객체는 도서 참여자의 식별자(ID)와 이름, 이메일을 가지고 있음 + * @author Yujin-nKim(김유진) + */ +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ParticipantDto { + private Long id; + private String name; + private String email; +} diff --git a/src/main/java/com/t3t/frontserver/participant/dto/ParticipantRoleDto.java b/src/main/java/com/t3t/frontserver/participant/dto/ParticipantRoleDto.java new file mode 100644 index 0000000..28ccf2b --- /dev/null +++ b/src/main/java/com/t3t/frontserver/participant/dto/ParticipantRoleDto.java @@ -0,0 +1,21 @@ +package com.t3t.frontserver.participant.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 도서 참여자 역할에 대한 데이터 전송 객체(DTO)
+ * 각 객체는 도서 참여자 역할의 식별자(ID)와 영어 이름, 한국어 이름을 가지고 있음 + * @author Yujin-nKim(김유진) + */ +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ParticipantRoleDto { + private Integer id; + private String roleNameEn; + private String roleNameKr; +} diff --git a/src/main/java/com/t3t/frontserver/publishers/client/PublisherApiClient.java b/src/main/java/com/t3t/frontserver/publishers/client/PublisherApiClient.java new file mode 100644 index 0000000..97548ac --- /dev/null +++ b/src/main/java/com/t3t/frontserver/publishers/client/PublisherApiClient.java @@ -0,0 +1,18 @@ +package com.t3t.frontserver.publishers.client; + +import com.t3t.frontserver.book.model.dto.PublisherDto; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "publisherApiClient", url = "${t3t.feignClient.url}") +public interface PublisherApiClient { + @GetMapping("t3t/bookstore/publishers") + ResponseEntity>> getPublisherList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "publisherId", required = false) String sortBy); +} diff --git a/src/main/java/com/t3t/frontserver/publishers/controller/PublisherController.java b/src/main/java/com/t3t/frontserver/publishers/controller/PublisherController.java new file mode 100644 index 0000000..9b40ca2 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/publishers/controller/PublisherController.java @@ -0,0 +1,29 @@ +package com.t3t.frontserver.publishers.controller; + +import com.t3t.frontserver.book.model.dto.PublisherDto; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.publishers.client.PublisherApiClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Slf4j +@RequiredArgsConstructor +@Controller +public class PublisherController { + + private final PublisherApiClient publisherApiClient; + + @GetMapping("/publishers") + public ResponseEntity>> getPublisherList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "publisherId", required = false) String sortBy) { + + return publisherApiClient.getPublisherList(pageNo, pageSize, sortBy); + } +} diff --git a/src/main/java/com/t3t/frontserver/tag/client/TagApiClient.java b/src/main/java/com/t3t/frontserver/tag/client/TagApiClient.java new file mode 100644 index 0000000..a8dfdc4 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/tag/client/TagApiClient.java @@ -0,0 +1,20 @@ +package com.t3t.frontserver.tag.client; + +import com.t3t.frontserver.book.model.dto.TagDto; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "tagApiClient", url = "${t3t.feignClient.url}") +public interface TagApiClient { + + @GetMapping(value = "/t3t/bookstore/tags") + ResponseEntity>> getTagList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "tagId", required = false) String sortBy); + +} diff --git a/src/main/java/com/t3t/frontserver/tag/controller/TagController.java b/src/main/java/com/t3t/frontserver/tag/controller/TagController.java new file mode 100644 index 0000000..e16badc --- /dev/null +++ b/src/main/java/com/t3t/frontserver/tag/controller/TagController.java @@ -0,0 +1,28 @@ +package com.t3t.frontserver.tag.controller; + +import com.t3t.frontserver.book.model.dto.TagDto; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.tag.client.TagApiClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Slf4j +@RequiredArgsConstructor +@Controller +public class TagController { + private final TagApiClient tagApiClient; + + @GetMapping("/tags") + public ResponseEntity>> getTagList( + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10", required = false) int pageSize, + @RequestParam(value = "sortBy", defaultValue = "tagId", required = false) String sortBy) { + + return tagApiClient.getTagList(pageNo, pageSize, sortBy); + } +} diff --git a/src/main/java/com/t3t/frontserver/util/FeignClientUtils.java b/src/main/java/com/t3t/frontserver/util/FeignClientUtils.java new file mode 100644 index 0000000..92658ff --- /dev/null +++ b/src/main/java/com/t3t/frontserver/util/FeignClientUtils.java @@ -0,0 +1,31 @@ +package com.t3t.frontserver.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.FeignException; + +import java.util.Map; + +/** + * FeignClientException 이 발생한 경우 응답 본분에서 메시지를 추출하기 위한 유틸리티 클래스 + * 메시지가 없는 경우 빈 문자열을 반환한다. + * + * @author woody35545(구건모) + * @see FeignException + */ +public class FeignClientUtils { + private static ObjectMapper objectMapper = new ObjectMapper(); + + public static String getMessageFromFeignException(FeignException e) { + try { + String contentUTF8 = e.contentUTF8(); + if (contentUTF8 != null && !contentUTF8.isEmpty()) { + Map contentMap = objectMapper.readValue(contentUTF8, Map.class); + return contentMap.getOrDefault("message", ""); + } + } catch (JsonProcessingException ex) { + ex.printStackTrace(); + } + return ""; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6f5c4e6..5314b30 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,6 +14,13 @@ spring: devtools: restart: enabled: true + mvc: + hiddenmethod: + filter: + enabled: true +feign: + okhttp: + enabled: true eureka: instance: diff --git a/src/main/resources/static/assets/admin/js/adminBook.js b/src/main/resources/static/assets/admin/js/adminBook.js index 512d0b3..6ad1363 100644 --- a/src/main/resources/static/assets/admin/js/adminBook.js +++ b/src/main/resources/static/assets/admin/js/adminBook.js @@ -14,7 +14,6 @@ var bookDescEditor = new toastui.Editor({ height: '500px', }); - /* * 문서가 로드되었을 때 실행되는 이벤트 핸들러 * 폼 데이터를 유효성 검사하고, 유효한 경우 폼을 제출 @@ -104,10 +103,6 @@ document.addEventListener("DOMContentLoaded", function() { this.appendChild(tagInput); }, this); - // TODO : API 연동 후에 제거 - alert("성공") - event.preventDefault(); - return; }); /** @@ -190,9 +185,9 @@ document.addEventListener("DOMContentLoaded", function() { var discountRate = discountRateInput.value.trim(); // 입력값이 숫자로 이루어져 있는지 확인하는 정규표현식 var regex = /^\d+$/; - // 숫자로만 이루어져 있고 1 이상 99 이하의 값인지 확인 - if (!regex.test(discountRate) || parseInt(discountRate) < 1 || parseInt(discountRate) > 99) { - discountRateValidationMessage.textContent = "할인율은 숫자로 이루어진 1 이상 99 이하의 정수값이어야 합니다."; + // 숫자로만 이루어져 있고 0 이상 99 이하의 값인지 확인 + if (!regex.test(discountRate) || parseInt(discountRate) < 0 || parseInt(discountRate) > 99.9) { + discountRateValidationMessage.textContent = "할인율은 숫자로 이루어진 0 이상 99.9 이하의 정수값이어야 합니다."; discountRateValidationMessage.style.color = "red"; return false; } else { @@ -239,7 +234,6 @@ document.addEventListener("DOMContentLoaded", function() { */ function isPublisherIdValid(publisherIdInput) { if (publisherIdInput) { - // publisherIdValue = publisherIdInput.split(',')[0]; publisherIdValue = publisherIdInput.value; return [true, publisherIdValue]; } else { @@ -309,24 +303,18 @@ document.addEventListener("DOMContentLoaded", function() { * @author Yujin-nKim(김유진) */ function isCategoryValid(categoryList) { - if (categoryList.length == 0 ) { - alert("카테고리를 선택해주세요."); - event.preventDefault(); - return false; - } - var newCategoryList = []; - for(var i = 0; i < categoryList.length; i++) { - var categoryValue = categoryList[i].value; - - var categoryInput = document.createElement('input'); - categoryInput.type = 'hidden'; - categoryInput.id = 'categoryId' + i; - categoryInput.name = 'categoryList[' + i + ']'; - categoryInput.value = categoryValue; - newCategoryList.push(categoryInput); + if(categoryList.length > 0) { + for(var i = 0; i < categoryList.length; i++) { + var categoryValue = categoryList[i].value; + var categoryInput = document.createElement('input'); + categoryInput.type = 'hidden'; + categoryInput.id = 'categoryId' + i; + categoryInput.name = 'categoryList[' + i + ']'; + categoryInput.value = categoryValue; + newCategoryList.push(categoryInput); + } } - return[true, newCategoryList]; } @@ -337,24 +325,18 @@ document.addEventListener("DOMContentLoaded", function() { * @author Yujin-nKim(김유진) */ function isTagValid(tagList) { - if (tagList.length == 0 ) { - alert("태그를 선택해주세요."); - event.preventDefault(); - return false; - } - var newTagList = []; - for(var i = 0; i < tagList.length; i++) { - var tagValue = tagList[i].value; - - var tagInput = document.createElement('input'); - tagInput.type = 'hidden'; - tagInput.id = 'tagId' + i; - tagInput.name = 'tagList[' + i + ']'; - tagInput.value = tagValue; - newTagList.push(tagInput); + if (tagList.length > 0) { + for(var i = 0; i < tagList.length; i++) { + var tagValue = tagList[i].value; + var tagInput = document.createElement('input'); + tagInput.type = 'hidden'; + tagInput.id = 'tagId' + i; + tagInput.name = 'tagList[' + i + ']'; + tagInput.value = tagValue; + newTagList.push(tagInput); + } } - return[true, newTagList]; } }); @@ -427,6 +409,21 @@ document.getElementById('openTagModal').addEventListener('click', function() { fetchTagsAndUpdateModal(); }) +// '선택 카테고리 지우기' 버튼 클릭 이벤트 핸들러 +document.getElementById('clearSelectedCategory').addEventListener('click', function() { + const selectedCategoryContainer = $('#selectedCategory'); + const selectedCategoryInModal = $('#selectedCategoryInModal'); + selectedCategoryContainer.empty(); + selectedCategoryInModal.empty(); +}) + +// '선택 태그 지우기' 버튼 클릭 이벤트 핸들러 +document.getElementById('clearSelectedTag').addEventListener('click', function() { + const selectedTagContainer = $('#selectedTag'); + const selectedTagInModal = $('#selectedTagInModal'); + selectedTagContainer.empty(); + selectedTagInModal.empty(); +}) /* * '도서 관련 정보 선택' 모달이 닫힐때 실행되는 함수 diff --git a/src/main/resources/static/assets/admin/js/api.js b/src/main/resources/static/assets/admin/js/api.js index e4f3cf7..9c7453d 100644 --- a/src/main/resources/static/assets/admin/js/api.js +++ b/src/main/resources/static/assets/admin/js/api.js @@ -12,8 +12,7 @@ const APP_KEY = document.querySelector("#appKey").getAttribute("data-contextPath */ function fetchPublishersAndUpdateModal() { $.ajax({ - // url: 'http://localhost:8081/publishers', - url: APP_KEY + '/t3t/bookstore/publishers', + url : '/publishers', type: 'GET', data: { pageNo: currentPage, @@ -47,8 +46,7 @@ function fetchPublishersAndUpdateModal() { */ function fetchParticipantsAndUpdateModal() { $.ajax({ - // url: 'http://localhost:8081/participants', - url: APP_KEY + '/t3t/bookstore/participants', + url: '/participants', type: 'GET', data: { pageNo: currentPage, @@ -82,8 +80,7 @@ function fetchParticipantsAndUpdateModal() { */ function fetchParticipantRolesAndUpdateModal() { $.ajax({ - // url: 'http://localhost:8081/participantRoles', - url: APP_KEY + '/t3t/bookstore/participantRoles', + url: '/participantRoles', type: 'GET', data: { pageNo: currentPage, @@ -116,8 +113,7 @@ function fetchParticipantRolesAndUpdateModal() { */ function fetchCategoriesAndUpdateModal(startDepth, maxDepth) { $.ajax({ - // url: 'http://localhost:8081/categories', - url: APP_KEY + '/t3t/bookstore/categories', + url: '/categories', type: 'GET', data: { startDepth: startDepth, @@ -148,8 +144,7 @@ function fetchCategoriesAndUpdateModal(startDepth, maxDepth) { */ function fetchTagsAndUpdateModal() { $.ajax({ - // url: 'http://localhost:8081/tags', - url: APP_KEY + '/t3t/bookstore/tags', + url: '/tags', type: 'GET', data: { pageNo: currentPage, @@ -176,3 +171,85 @@ function fetchTagsAndUpdateModal() { } }); } + +/** + * 출판사 변경 요청을 보내는 함수 + * @param {number} bookId - 도서 ID + * @param {number} publisherId - 변경할 출판사 ID + * @author Yujin-nKim(김유진) + */ +function sendPublisherUpdateRequest(bookId, publisherId) { + $.ajax({ + url: `/admin/books/${bookId}/publisher`, + type: 'PUT', + data: { + publisherId: publisherId + } + }); +} + +/** + * 도서 참여자 변경 요청을 보내는 함수 + * @param {number} bookId - 도서 ID + * @param {number} participantList - 변경할 도서 참여자 맵핑 리스트 + * @author Yujin-nKim(김유진) + */ +function sendParticipantUpdateRequest(bookId, participantList) { + var jsonData = JSON.stringify(participantList); + $.ajax({ + url: `/admin/books/${bookId}/participant`, + type: 'PUT', + contentType: 'application/json', + data: jsonData + }); +} + +/** + * 도서 썸네일 변경 요청을 보내는 함수 + * @param {number} bookId - 도서의 ID + * @param {FormData} data - 변경할 도서 썸네일 + * @author Yujin-nKim(김유진) + */ +function sendThumbnailUpdateRequest(bookId, formData) { + $.ajax({ + url: `/admin/books/${bookId}/book-thumbnail`, + type: 'PUT', + data: formData, + processData: false, + contentType: false + }); +} + +/** + * 도서 미리보기 이미지 변경 요청을 보내는 함수 + * @param {number} bookId - 도서의 ID + * @param {FormData} data - 변경할 도서 이미지 + * @author Yujin-nKim(김유진) + */ +function sendBookImageUpdateRequest(bookId, formData) { + $.ajax({ + url: `/admin/books/${bookId}/book-image`, + type: 'PUT', + data: formData, + processData: false, + contentType: false + }); +} + +function updateBookCategoryRequest(bookId, categoryList) { + $.ajax({ + url: `/admin/books/${bookId}/category`, + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(categoryList) + }); +} + +function updateBookTagRequest(bookId, tagList) { + $.ajax({ + url: `/admin/books/${bookId}/tag`, + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(tagList) + }); +} \ No newline at end of file diff --git a/src/main/resources/static/assets/admin/js/modifyBook.js b/src/main/resources/static/assets/admin/js/modifyBook.js new file mode 100644 index 0000000..3024065 --- /dev/null +++ b/src/main/resources/static/assets/admin/js/modifyBook.js @@ -0,0 +1,354 @@ + +/* + * TOAST UI Viewer 생성 JavaScript 코드 + */ +var indexViewer = new toastui.Editor.factory({ + el: document.querySelector('#indexViewer'), + viewer: true, + initialValue: document.querySelector('#indexViewer').getAttribute('value'), +}); + +var descViewer = new toastui.Editor.factory({ + el: document.querySelector('#descViewer'), + viewer: true, + initialValue: document.querySelector('#descViewer').getAttribute('value') +}); +// viewer 글자 크기 설정 +var indexViewerElement = document.querySelector('#indexViewer .toastui-editor-contents'); +indexViewerElement.style.fontSize = '20px'; +var descViewerElement = document.querySelector('#descViewer .toastui-editor-contents'); +descViewerElement.style.fontSize = '20px'; + +document.addEventListener("DOMContentLoaded", function() { + var bookModifyForm = document.getElementsByName('BookDetailModifyForm')[0]; + + // 변경 전 값 + var beforeTitle = document.getElementById('beforeTitle').textContent; + var beforeIndex = document.querySelector('#indexViewer').getAttribute('value'); + var beforeDesc = document.querySelector('#descViewer').getAttribute('value'); + var beforeIsbn = document.getElementById('beforeIsbn').textContent; + var beforePrice = document.getElementById('beforePrice').textContent; + var beforeDiscountRate = document.getElementById('beforeDiscountRate').textContent; + var beforePackagingAvailableStatus = document.getElementById('beforePackagingAvailableStatus').getAttribute('value'); + var beforePublished = document.getElementById('beforePublished').textContent; + var beforeStock = document.getElementById('beforeStock').textContent; + + // input 값 + var titleInput = document.getElementById('bookTitle'); + var isbnInput = document.getElementById('bookIsbn'); + var priceInput = document.getElementById('bookPrice'); + var discountRateInput = document.getElementById('bookDiscountRate'); + var publishedInput = document.getElementById('bookPublished'); + var stockInput = document.getElementById('bookStock'); + + // validation message를 표시할 div 영역 + var titleValidationMessage = document.getElementById('titleValidationMessage'); + var isbnValidationMessage = document.getElementById('isbnValidationMessage'); + var priceValidationMessage = document.getElementById('priceValidationMessage'); + var discountRateValidationMessage = document.getElementById('discountRateValidationMessage'); + + // form의 submit 버튼 클릭시 입력값 유효성 검사 후 폼에 데이터 추가해서 전송 + bookModifyForm.addEventListener('submit', function(event) { + + // 폼 유효성 검사 + var isFormValid = isTitleValid() && isIsbnValid() && isPriceValid() && isDiscountRateValid(); + + // 유효성 검사 통과 시 폼을 제출 + if (!isFormValid) { + event.preventDefault(); + return; + } + + // 값이 변경되었다면 변경된 값을, 변경되지 않았다면 변경 이전의 값을 담음 + titleInput.value = titleInput.value === '' ? beforeTitle : titleInput.value; + isbnInput.value = isbnInput.value === '' ? beforeIsbn : isbnInput.value; + priceInput.value = priceInput.value === '' ? beforePrice : priceInput.value; + discountRateInput.value = discountRateInput.value === '' ? beforeDiscountRate : discountRateInput.value; + publishedInput.value = publishedInput.value === '' ? beforePublished : publishedInput.value; + stockInput.value = stockInput.value === '' ? beforeStock : stockInput.value; + + var selectedValue = getSelectedRadioValue(); // 포장 여부 + var bookIndexInput = bookIndexEditor.getMarkdown().trim() === '' ? beforeIndex : bookIndexEditor.getMarkdown(); // 도서 목차 + var bookDescInput = bookDescEditor.getMarkdown().trim() === '' ? beforeDesc : bookDescEditor.getMarkdown(); // 도서 설명 + + /** + * 주어진 부모 요소에 hidden input 요소를 추가하는 함수 + * @param {HTMLElement} parent - hidden input 요소를 추가할 부모 요소 + * @param {string} id - hidden input 요소의 id 속성 값 + * @param {string} name - hidden input 요소의 name 속성 값 + * @param {string} value - hidden input 요소의 value 속성 값 + * @author Yujin-nKim(김유진) + */ + function addHiddenInput(parent, id, name, value) { + var hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.id = id; + hiddenInput.name = name; + hiddenInput.value = value; + parent.appendChild(hiddenInput); + } + + // 폼 데이터 추가 + addHiddenInput(this, 'packagingAvailableStatus', 'packagingAvailableStatus', selectedValue); + addHiddenInput(this, 'bookIndex', 'bookIndex', bookIndexInput); + addHiddenInput(this, 'bookDesc', 'bookDesc', bookDescInput); + + }); + + /** + * 라디오 버튼 중 선택된 값을 반환하는 함수 + * @returns {string|null} 선택된 라디오 버튼의 값 + * @author Yujin-nKim(김유진) + */ + function getSelectedRadioValue() { + // 선택된 라디오 버튼 요소 가져오기 + var selectedRadioButton = document.querySelector('input[name="gridRadios"]:checked'); + // 선택된 값이 있다면 해당 값 반환, 없다면 기존값 반환 + return selectedRadioButton ? selectedRadioButton.value : beforePackagingAvailableStatus; + } + + /** + * 도서 제목 입력값의 유효성을 검사하고 결과를 반환 + * @returns {boolean} - 도서 제목의 유효성 여부 + * @author Yujin-nKim(김유진) + */ + function isTitleValid() { + var title = titleInput.value.trim(); + if (title !== '' && title.length > 255) { + titleValidationMessage.textContent = "도서 제목은 255자 이하여야 합니다."; + titleValidationMessage.style.color = "red"; + return false; + } else { + titleValidationMessage.textContent = ""; + return true; + } + } + + /** + * 도서 ISBN 입력값의 유효성을 검사하고 결과를 반환 + * @returns {boolean} - ISBN의 유효성 여부 + * @author Yujin-nKim(김유진) + */ + function isIsbnValid() { + var isbn = isbnInput.value.trim(); + // ISBN이 숫자로 이루어진 13자리의 숫자인지 확인 + if (isbn !== '' && !/^\d{13}$/.test(isbn)) { + isbnValidationMessage.textContent = "숫자로만 이루어진 13자리 숫자이어야 합니다."; + isbnValidationMessage.style.color = "red"; + return false; + } else { + isbnValidationMessage.textContent = ""; + return true; + } + } + + /** + * 도서 정가 입력값의 유효성을 검사하고 결과를 반환 + * @returns {boolean} - 도서 정가의 유효성 여부 + * @author Yujin-nKim(김유진) + */ + function isPriceValid() { + var price = priceInput.value.trim(); + // 입력값이 숫자로 이루어져 있는지 확인하는 정규표현식 + var regex = /^\d+$/; + if (price !== '' && !regex.test(price) || parseInt(price) < 0) { + priceValidationMessage.textContent = "올바른 도서 정가를 입력해주세요."; + priceValidationMessage.style.color = "red"; + return false; + } else { + priceValidationMessage.textContent = ""; + return true; + } + } + + /** + * 도서 할인율 입력값의 유효성을 검사하고 결과를 반환 + * @returns {boolean} - 도서 할인율의 유효성 여부 + * @author Yujin-nKim(김유진) + */ + function isDiscountRateValid() { + var discountRate = discountRateInput.value.trim(); + // 입력값이 숫자로 이루어져 있는지 확인하는 정규표현식 + var regex = /^\d+$/; + // 숫자로만 이루어져 있고 1 이상 99 이하의 값인지 확인 + if (discountRate !== '' && !regex.test(discountRate) || parseInt(discountRate) < 0 || parseInt(discountRate) > 99.9) { + discountRateValidationMessage.textContent = "할인율은 숫자로 이루어진 0 이상 99.9 이하의 정수값이어야 합니다."; + discountRateValidationMessage.style.color = "red"; + return false; + } else { + discountRateValidationMessage.textContent = ""; + return true; + } + } +}); + +/** + * '출판사 변경 요청' 버튼 클릭시 실행되는 이벤트 핸들러 + * @author Yujin-nKim(김유진) + */ +document.getElementById('modifyPublisherBtn').addEventListener('click', function() { + if (!document.getElementById('selected-value-publisher')) { + alert('출판사를 선택하세요.'); + } else { + var publisherId = document.getElementById('selected-value-publisher').value; + var bookId = document.getElementById('bookId').value; + sendPublisherUpdateRequest(bookId, publisherId); + } +}); + +/** + * '도서 참여자 변경 요청' 버튼 클릭시 실행되는 이벤트 핸들러 + * @author Yujin-nKim(김유진) + */ +document.getElementById('modifyParticipantBtn').addEventListener('click', function() { + var tbody = document.querySelector("#participantButtonTable tbody"); + + if (tbody.children.length === 0) { + alert('도서 참여자 정보를 선택하세요.'); + } else { + var count = document.getElementById('participantCount').value; + var selectedParticipants = document.querySelectorAll('.selected-value-participant'); + var selectedParticipantRoles = document.querySelectorAll('.selected-value-participantRole'); + var newParticipantMapList = isParticipantMapValid(parseInt(count), selectedParticipants, selectedParticipantRoles) + + if (newParticipantMapList) { + var bookId = document.getElementById('bookId').value; + sendParticipantUpdateRequest(bookId, newParticipantMapList); + } + } +}); + +/** + * '썸네일 이미지 변경 요청' 버튼 클릭시 실행되는 이벤트 핸들러 + * @author Yujin-nKim(김유진) + */ +document.getElementById('modifyThumbnailBtn').addEventListener('click', function() { + var thumbnailImageInput = document.getElementById('inputThumbnailImage'); + var thumbnailImage = thumbnailImageInput.files[0]; + if (!thumbnailImage) { + alert("썸네일 이미지를 선택해주세요."); + return; + } else { + var bookId = document.getElementById('bookId').value; + var formData = new FormData(); + formData.append('image', thumbnailImage); + sendThumbnailUpdateRequest(bookId, formData); + } +}); + +/** + * '도서 이미지 변경 요청' 버튼 클릭시 실행되는 이벤트 핸들러 + * @author Yujin-nKim(김유진) + */ +document.getElementById('modifyImageBtn').addEventListener('click', function() { + var bookImageInput = document.getElementById('inputBookImage'); + var bookImage = bookImageInput.files; + if (bookImage.length == 0) { + alert("도서 이미지를 선택해주세요."); + return; + } else { + var bookId = document.getElementById('bookId').value; + var formData = new FormData(); + for (var i = 0; i < bookImage.length; i++) { + formData.append('imageList', bookImage[i]); + } + sendBookImageUpdateRequest(bookId, formData); + } +}); + +/** + * 도서 참여자와 역할 매핑 입력값의 유효성을 검사하고, 유효한 경우 맵핑 리스트 생성 + * @param {number} count 도서 참여자 수 + * @param {NodeListOf} participantList 도서 참여자 목록 + * @param {NodeListOf} participantRoleList 도서 참여자 역할 목록 + * @returns {Array|undefined} - 유효한 경우 도서 참여자 및 역할의 DTO 배열을 반환 + * @author Yujin-nKim(김유진) + */ +function isParticipantMapValid(count, participantList, participantRoleList) { + if (participantList.length !== count || participantRoleList.length !== count) { + alert('도서 참여자 정보를 선택하세요.'); + return; + } + + var pairSet = new Set(); + var participantMapList = []; + // participantList와 participantRoleList를 돌면서 각 요소를 짝지어서 확인 + for (var i = 0; i < count; i++) { + + var participantValue = participantList[i].value; + var participantRoleValue = participantRoleList[i].value; + var pair = participantValue + ':' + participantRoleValue; + + // 이미 존재하는 짝인지 확인하고 있다면 false를 반환 + if (pairSet.has(pair)) { + alert("도서 참여자에 중복된 값이 존재합니다."); + return; + } + // 셋에 짝을 추가 + pairSet.add(pair); + + var participantMapDto = { + participantId: participantValue, + participantRoleId: participantRoleValue + }; + participantMapList.push(participantMapDto); + } + return participantMapList; +} + +/** + * '카테고리 변경 요청' 버튼 클릭시 실행되는 이벤트 핸들러 + * @author Yujin-nKim(김유진) + */ +document.getElementById('modifyCategoryBtn').addEventListener('click', function() { + var categoryList = document.querySelectorAll('.selected-value-category'); + var bookId = document.getElementById('bookId').value; + var categoryListRequest = isCategoryValid(categoryList); + updateBookCategoryRequest(bookId, categoryListRequest); + +}); + +function isCategoryValid(categoryList) { + if (categoryList.length == 0 ) { + alert("카테고리를 선택해주세요."); + return; + } + + var newCategoryList = []; + for(var i = 0; i < categoryList.length; i++) { + var categoryValue = categoryList[i].value; + + newCategoryList.push(categoryValue); + } + + return newCategoryList; +} + + +/** + * '태그 변경 요청' 버튼 클릭시 실행되는 이벤트 핸들러 + * @author Yujin-nKim(김유진) + */ +document.getElementById('modifyTagBtn').addEventListener('click', function() { + var tagList = document.querySelectorAll('.selected-value-tag'); + var bookId = document.getElementById('bookId').value; + var tagListRequest = isTagValid(tagList); + updateBookTagRequest(bookId, tagListRequest); + +}); + +function isTagValid(tagList) { + if (tagList.length == 0 ) { + alert("태그를 선택해주세요."); + return; + } + + var newTagList = []; + for(var i = 0; i < tagList.length; i++) { + var tagValue = tagList[i].value; + + newTagList.push(tagValue); + } + + return newTagList; +} \ No newline at end of file diff --git a/src/main/resources/templates/admin/fragment/sidebar.html b/src/main/resources/templates/admin/fragment/sidebar.html index 8b488d5..ecc4056 100644 --- a/src/main/resources/templates/admin/fragment/sidebar.html +++ b/src/main/resources/templates/admin/fragment/sidebar.html @@ -8,7 +8,7 @@