Light Blue Pointer
본문 바로가기
개발일지

2024-01-12, Today I Learned

by 개발바닥곰발바닥!!! 2024. 1. 12.

오늘 한 일

1. 글 검색기능 간략하게 해보기 : store에 대해서, 추후 필드 추가는 생각해봄

2. 글 작성페이지 만들기

3. 프론트랑 백 연결하기

4. 모집마감 기능 구현하기

 

1. 글 검색기능

글 검색기능 만들어봄

일단은 가게 이름만 검색하고 공백 상관없이 다 맞아야 검색되는걸로

public record PostSearchRequest(
    @NotNull(message = "검색어를 입력하세요.")
    String keyword
) {

}
//글 검색하기
    @DeleteMapping("/posts/search")
    public ApiResponse<List<BriefPostResponse>> searchPost(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestBody PostSearchRequest postSearchReq
    ){
        return new ApiResponse<>(HttpStatus.OK.value(), "검색 결과",postService.searchPost(postSearchReq, userDetails.getUser()));
    }
@Override
    public List<BriefPostResponse> searchPost(PostSearchRequest postSearchReq, User user) {
        //search store
        List<Post> posts = postRepository.findAllByStoreEquals(postSearchReq.keyword()).orElse(null);
        //List<Post> -> List<BriefPostResponse>
        return postsToBriefResponses(posts);
    }

//-> usage 3개임!!
private List<BriefPostResponse> postsToBriefResponses(List<Post> posts){
        return posts.stream()
                .map((Post post)-> new BriefPostResponse(
                    post.getId(),
                    getAuthor(getUserPostsByPost(post)).getNickname(),
                    post.getAddress(),
                    post.getStore(),
                    post.getMinPrice(),
                    getSumPrice(getUserPostsByPost(post),post),
                    getDeadline(post)
                )).toList();
    }

 

 

🚩문제 : MethodArgumentTypeMismatchException

{
    "search":"가게가게"
}

Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; For input string: "search"]

String을 날렸는데 무슨말을 하는거지

postRepository.findAllByStoreEquals(postSearchReq.keyword())

이부분이 문제인가?

Optional<List<Post>> findAllByStoreEquals(String store);

음 String으로 받는데??

@NotNull(message = "검색어를 입력하세요.")
String keyword

Not null 빼봄

public record PostSearchRequest(
    //@NotNull(message = "검색어를 입력하세요.")
    String keyword
) {

}

MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; For input string: "search"]

그건 문제가 아니었던걸로

MethodArgumentTypeMismatchException

//글 검색하기
    @DeleteMapping("/posts/search")
    public ApiResponse<List<BriefPostResponse>> searchPost(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestBody PostSearchRequest postSearchReq
    ){
        return new ApiResponse<>(HttpStatus.OK.value(), "검색 결과",postService.searchPost(postSearchReq, userDetails.getUser()));
    }

지금보니 Controller단 메서드가 DeleteMapping으로 잘못되어있어서 요청이 GetMapping 메서드로 날아간게 아닐까 추측해본다

//글 단독 조회, 글 상세페이지
    @GetMapping("/posts/{postId}")
    public ApiResponse<DetailedPostResponse> getPost(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable(name = "postId") Long postId
    ){
        return new ApiResponse<>(HttpStatus.OK.value(), "글 상세페이지 조회에 성공했습니다.",postService.getPost(postId, userDetails.getUser()));
    }

posts/밑에 path가 더 있는 GetMapping 메서드중에 그게 변수인게 저거밖에 없어서 저기로 날아간듯 하다!

@GetMapping으로 고쳐줌

⛳해결 :  Controller단 Mapping 방법 + path 고쳐줌

//글 검색하기
    @GetMapping("/posts/search")
    public ApiResponse<List<BriefPostResponse>> searchPost(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestBody PostSearchRequest postSearchReq
    ){
        return new ApiResponse<>(HttpStatus.OK.value(), "검색 결과",postService.searchPost(postSearchReq, userDetails.getUser()));
    }

맨날 시간 절약하려고 복붙하다 까먹고 뭐 안 바꿔서 문제생기니까 그냥 처음부터 타이핑으로 치는게 나을 거 같다

{
    "search":"가게가게"
}
{
    "status": 200,
    "message": "검색 결과",
    "data": []
}

잘 된다!

 

 

이제 가게가게인걸 추가하고 다른것도 추가해보고 테스트해보겠음

{
    "address":"주소주소",
    "store": "storestore",
    "minPrice": 20000,
    "deliveryCost":2000,
    "deadlineHours":0,
    "deadlineMins":30,
    "category":"KOREAN"

}
{
    "address":"주소주소",
    "store": "가게가게",
    "minPrice": 20000,
    "deliveryCost":2000,
    "deadlineHours":0,
    "deadlineMins":30,
    "category":"KOREAN"

}
{
    "search":"가게가게"
}
{
    "status": 200,
    "message": "검색 결과",
    "data": []
}

store가 “가게가게”인 것을 추가하고 다시 검색하는데 안 된다 ㅋㅋㅋ

🚩문제 :

{
    "search":"storestore"
}
{
    "status": 200,
    "message": "검색 결과",
    "data": []
}

 

Q.왜 안될까요?

Post entity를 보게되면 String으로 저장되어있음!

@Column(nullable = false)
    private String store;

JPA QueryMethod의 equals가 모종의 이유로 안 먹히는 거 같아서 로직을 가져와서 String비교하는걸로 바꿔봄

stream filter 써봄!

https://www.baeldung.com/java-stream-filter-lambda

2.2. Filtering Collections with Multiple Criteria

Furthermore, we can use multiple conditions with filter(). For example, we can filter by points and name:

List<Customer> charlesWithMoreThan100Points = customers
  .stream()
  .filter(c -> c.getPoints() > 100 && c.getName().startsWith("Charles"))
  .collect(Collectors.toList());

assertThat(charlesWithMoreThan100Points).hasSize(1);
assertThat(charlesWithMoreThan100Points).contains(charles);

저거 보고 이렇게 해봄

@Override
    public List<BriefPostResponse> searchPost(PostSearchRequest postSearchReq, User user) {
        //get all posts
        List<Post> posts = postRepository.findAll().orElse(null);
        //filter by search keyword
        List<Post> filtered = posts.stream().filter(post->post.getStore().contains(postSearchReq.keyword())).toList();
        //List<Post> -> List<BriefPostResponse>
        return postsToBriefResponses(filtered);
    }
java.lang.NullPointerException: Cannot invoke "java.lang.CharSequence.toString()" because "s" is null
	at java.base/java.lang.String.contains(String.java:2856) ~[na:na]

아 ㅋㅋ

여태까지 이 모든 문제가 dto랑 보내는 JSON field name 안 맞춰줘서 발생했던 걸 저 메시지 보고 꺠달음

⛳해결 : DTO와 보내는 JSON의 field name 맞춰줌

public record PostSearchRequest(
    @NotNull(message = "검색어를 입력하세요.")
    String keyword
) {

}

keyword로 써두시고

search로 보내시면 어떡하나요

{
    "search":"storestore"
}

search를 keyword로 바꿔봄

{
    "keyword":"storestore"
}
{
    "status": 404,
    "message": "작성자를 찾을 수 없습니다.",
    "data": null
}

새로운 문제 발생

return postsToBriefResponses(filtered);

아마 여기서 문제생긴듯

private List<BriefPostResponse> postsToBriefResponses(List<Post> posts){
        return posts.stream()
                .map((Post post)-> new BriefPostResponse(
                    post.getId(),
                    getAuthor(getUserPostsByPost(post)).getNickname(),
                    post.getAddress(),
                    post.getStore(),
                    post.getMinPrice(),
                    getSumPrice(getUserPostsByPost(post),post),
                    getDeadline(post)
                )).toList();
    }

 

 

🚀 issue : 근데 항상 글쓴이 닉네임 하나 가져오려고 table두개를 싹 돌아서 겨우 .getNickname하는데 저게 맞나…싶다

Post에 글쓴이 이름 String/ 글쓴이 User를 넣는게 나을거같음

근데 생각해보면 Nickname만 쓰는데가 상당히 많고

어차피 User를 달아두면 User 테이블에서 id로 다시 조회해와야 하니까 닉네임을 다는 편이 나을거 같음

그리고 ! Host인지 판별하는것도 그냥 findByUserAndPost한 다음에 Role을 보는게 어떨까… 싶기도 함

나중에 리팩토링 진행해야지!!

 

🚀 issue: 그리고 저번에 함수 나누거나 위에서 따로 빼기 귀찮아서

getAuthor(getUserPostsByPost(post)).getNickname(),
getSumPrice(getUserPostsByPost(post),post),

이렇게 두번 db조회하게 한것도 수정이 필요할수도?

아예 똑같은 쿼리를 날리면 임시저장되어있는 데이터가 불려온단 소리도 있긴 한데

그래도 괜히 거칠 필요 없는 과정같아서

 

 

다시 문제로 돌아가서…

getAuthor(getUserPostsByPost(post)).getNickname(),

이게 문제일거임

private User getAuthor(List<UserPost> userPosts){
        for(UserPost userpost : userPosts ){
            if(userpost.getRole().equals(UserPostRole.HOST)){
                return userpost.getUser();
            }
        }
        throw new GlobalException(UserPostErrorCode.NOT_FOUND_HOST);
    }

근데 저건 발생하면 안 되는 에러인데,,?

어제 delete기능 헤매면서 테이블 수동으로 만져서 뭔가 꼬인거같다

테이블 싹 다 드랍하고 다시해봄

{
    "keyword":"storestore"
}
{
    "status": 200,
    "message": "검색 결과",
    "data": [
        {
            "id": 3,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 0,
            "deadline": "2024-01-12T13:22:44"
        },
        {
            "id": 4,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 0,
            "deadline": "2024-01-12T13:22:45"
        }
    ]
}

잘 나온다!!

{
    "keyword":"가게가게"
}
{
    "status": 200,
    "message": "검색 결과",
    "data": [
        {
            "id": 1,
            "author": "가나다라",
            "address": "주소주소",
            "store": "가게가게",
            "minPrice": 20000,
            "sumPrice": 10000,
            "deadline": "2024-01-12T13:22:33"
        },
        {
            "id": 2,
            "author": "가나다라",
            "address": "주소주소",
            "store": "가게가게",
            "minPrice": 20000,
            "sumPrice": 0,
            "deadline": "2024-01-12T13:22:34"
        }
    ]
}
@Override
    public List<BriefPostResponse> searchPost(PostSearchRequest postSearchReq, User user) {
        //get all posts
        List<Post> posts = postRepository.findAll().orElse(null);
        //filter by search keyword
        List<Post> filtered = posts.stream().filter(post->post.getStore().contains(postSearchReq.keyword())).toList();
        //List<Post> -> List<BriefPostResponse>
        return postsToBriefResponses(filtered);
    }

일단 나중에 제목이나 내용이나 더 검색 가능한 field 나오면 추가하려고/공백 띄어쓰기 상관없이 contain해보려고 저렇게 해놨는데

팀원이 findPostByStoreContaining 써보는건 어떻냐고 해서 써봄

@Override
    public List<BriefPostResponse> searchPost(PostSearchRequest postSearchReq, User user) {
        //get all posts filtered by search keyword
        List<Post> posts = postRepository.findPostByStoreContaining(postSearchReq.keyword()).orElse(null);
        //List<Post> -> List<BriefPostResponse>
        return postsToBriefResponses(posts);
    }

 

🚀issue: 보다보니 모든 메뉴를 다 돌아서 조회해서 그 글의 참여자의 메뉴인지 조회하는게 좀 그래보이긴 하는데

이건 지금 생각한 기능이 Offer시에 그 참여자의 메뉴를 볼 수 있었으면 좋겠어서 그런거임

일단 Menu를 해당 글로 발생시키고 Offer를 할 수도 있으니까

글에 참여하고 있는 사람의 Menu만 값을 다 가져와서 더해야함!

이건 Offer를 아무것도 없는 상태로 수락하고 그 상태에서만 Menu를 발생시킬지 팀원들과 좀 기능에 대한 정확한 정의를 해서 정해봐야 한다,

난 저래놓으면 나중에 내가 담은 거 전체로 모아주는 장바구니 페이지 기능같이 확장도 할 수 있다고 생각해서 좋은거같음

→ 팀원들과 논의 후에 그냥 메뉴는 계속 발생시킬 수 있는걸로 하기로 함!

→ 그래서 새로 발생한

🚀issue: sumPrice를 활용하는게 좋을까?, Offer수락시에 그 사람의 메뉴값 다 더해서 넣고 메뉴 수정/삭제가 일어날때마다 참가자인지 판별해서 sumPrice조작하는게 db조회 더 많이 할 거 같긴 함 상세페이지 출력마다 Menu 테이블 조회하는것보다 ㅋㅋ 일단 지금 방식대로 가면 sumPrice 칼럼은 걍 삭제해도 됨

 

🚀 issue: 생각해보니 MyMenus로 내가 여기다 뭐 담아놨는지도 따로 보여줘야함

그부분 내가 왜 뺐더라

→ 메뉴 생성/삭제를 내 메뉴 조회 페이지를 따로 빼서 거기서 하니까 메뉴 조회 api를 만드는 것으로 해결

 

2,3. 글 작성 페이지 개발, 프론트와 백 연결

 

글 생성하기 페이지 만들기!

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @GetMapping(value = "/")
    public ModelAndView home() {
        ModelAndView modelAndView = new ModelAndView();

        modelAndView.setViewName("home");

        Map<String, Object> map = new HashMap<>();
        map.put("name", "MoayoEats");
        map.put("date", LocalDateTime.now());

        modelAndView.addObject("data", map);

        return modelAndView;
    }
}

예시코드를 응용해서 바꿔봄

@Controller
@RequestMapping(value = "/api/v1")
public class PostController {
    @PostMapping("/posts")
    public ModelAndView createpost() {
       
    }
}

그리고 나는 저 ModelAndView를 Get하는게 아니라 Post해야할 거 같은데?

//Test
    @PostMapping("/test/posts/")
    public ApiResponse<Void> createPostTest(
        @RequestBody PostRequest postReq
    ){
        postService.createPostTest(postReq);
        return new ApiResponse<>(HttpStatus.CREATED.value(), "글을 생성했습니다.");
    }

Test용으로 복붙해두긴 했는데

이제 @ModelAttrubute로 바꿔야한다!!

//Test
    @PostMapping("/test/posts")
    public ApiResponse<Void> createPostTest(
        @ModelAttribute PostRequest postReq
    ){
        postService.createPostTest(postReq);
        return new ApiResponse<>(HttpStatus.CREATED.value(), "글을 생성했습니다.");
    }

변경 완료!!

package com.moayo.moayoeats.frontend.domain.post.controller.dto;

import com.moayo.moayoeats.domain.post.entity.CategoryEnum;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
public class PostRequestDto {

    @NotBlank
    String address;
    @NotBlank
    String store;
    @NotNull
    Integer minPrice;
    @NotNull
    Integer deliveryCost;

    @NotNull
    @Max(59)
    Integer deadlineMins;
    @NotNull
    @Max(3)
    Integer deadlineHours;
    CategoryEnum category;

}
@Controller
@RequestMapping(value = "/api/v1")
public class PostFrontController {
    @GetMapping("/test")
    public String index(){
        return "/createpost";
    }

    @PostMapping("/send")
    public String ajaxHome(Model model, PostRequestDto dto){
        model.addAttribute("address",dto.getAddress());
        model.addAttribute("store",dto.getAddress());
        model.addAttribute("minPrice",dto.getAddress());
        model.addAttribute("deliveryCost",dto.getAddress());
        return "ajax :: #resultDiv";
    }
}

이렇게 하고

http://localhost:8080/api/v1/test

여기다 요청 보냈는데 안 됨

Access to localhost was denied You don't have authorization to view this page.

왜지?

WebSecurityConfig 파일에서 이 부분 걸러줬는데?

//login,signup 접근 허용 그 외 인증 필요
        http.authorizeHttpRequests((authorizeHttpRequests) ->
            authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                .permitAll() // resources 접근 허용 설정
                .requestMatchers("/").permitAll() // 메인 페이지 요청 허가
                .requestMatchers("/api/v1/users/sign-up/**").permitAll() // singup이후로 접근 허가
                .requestMatchers("/api/v1/users/login/**").permitAll() // singup이후로 접근 허가
                .requestMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()
                .requestMatchers("/api/v1/test/**").permitAll()
                .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

콘솔 가보니 그부분이 문제가 아니었음

org.thymeleaf.exceptions.TemplateInputException: Error resolving template [/layout/basic], template might not exist or might not be accessible by any of the configured Template Resolvers (template: "/createpost" - line 4, col 11)

여기가 문제임

2024-01-12T16:29:38.817+09:00 ERROR 18904 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates//createpost.html]")] with root cause

아 /를 한번 더 하면 안됐네!

@GetMapping("/test")
    public String index(){
        return "/createpost";
    }

이렇게 하면 안됨

@GetMapping("/test")
    public String index(){
        return "createpost";
    }

이렇게 고쳐봄

2024-01-12T16:33:11.398+09:00 ERROR 5456 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/createpost.html]")] with root cause

또 이렇게 뜸!

html파일을 처리하는데 문법오류가 있었나 싶어서 다시 html파일을 들여다봄

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

  <th:block th:fragment="content">
    <div class="container">
      <div class="row mt-3">
        <div class="card text-center">
          <h5 class="card-header">글 작성 페이지</h5>
          <div class="card-body">
            <input id="address" placeholder="주소" type="text" class="form-control">
            <input id="store" placeholder="가게" type="text" class="form-control">
            <input id="minPrice" placeholder="최소주문금액" type="text" class="form-control">
            <input id="deliveryCost" placeholder="배달비" type="text" class="form-control">
            <button class="btn btn-outline-secondary" type="button" onclick="dataSend()">작성 완료
            </button>
          </div>
        </div>
        <div id="resultDiv">
          <p th:if="${msg}!=null" th:text="${msg}"></p>
        </div>
      </div>
    </div>
  </th:block>
</th:block>
</html>

 

가보니 저렇게 써져있는데

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

이부분은 뭘 의미하는거지?

왠지 다른 파일 불러와서 화면 split해서 그부분만 다른거 출력하는거같은데

아무튼 일단 모르는건 다 뺌

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">

<th:block th:fragment="content">
  <div class="container">
    <div class="row mt-3">
      <div class="card text-center">
        <h5 class="card-header">글 작성 페이지</h5>
        <div class="card-body">
          <input id="address" placeholder="주소" type="text" class="form-control">
          <input id="store" placeholder="가게" type="text" class="form-control">
          <input id="minPrice" placeholder="최소주문금액" type="text" class="form-control">
          <input id="deliveryCost" placeholder="배달비" type="text" class="form-control">
          <button class="btn btn-outline-secondary" type="button" onclick="dataSend()">작성 완료
          </button>
        </div>
      </div>
    </div>
  </div>
</th:block>
</html>

실행시켜봄

와~~ 페이지가 나온다!!

이렇게 해서 날려봄

작성 완료 누름!

🚀밑에 메시지 출력되게 하고싶다…

 

오~ 진짜 출력만 되고 전송이 안 됨

@PostMapping("/send")
    public String ajaxHome(Model model, PostRequestDto dto){
        model.addAttribute("address",dto.getAddress());
        model.addAttribute("store",dto.getAddress());
        model.addAttribute("minPrice",dto.getAddress());
        model.addAttribute("deliveryCost",dto.getAddress());
        return "ajax :: #resultDiv";
    }

지금보니 이상한데로 전송하고 있었음

@PostMapping("test/posts")
    public void ajaxHome(Model model, PostRequestDto dto){
        model.addAttribute("address",dto.getAddress());
        model.addAttribute("store",dto.getAddress());
        model.addAttribute("minPrice",dto.getAddress());
        model.addAttribute("deliveryCost",dto.getAddress());
    }

이렇게 해봄

오 빌드 실패함

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Injection of autowired dependencies failed

springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Ambiguous mapping. Cannot map 'postFrontController' method com.moayo.moayoeats.frontend.domain.post.controller.PostFrontController#ajaxHome(Model, PostRequestDto)

?

PostFrontController#ajaxHome(Model, PostRequestDto)

ajaxHome은 대체 뭐임 저런건 한 적이 없는데?

$.ajax({
      url: "/test/posts",
      data: postReqestDto,
      type: 'POST',
    }).done(function (data) {
      $("#resultDiv").replaceWith(data);
    });

저것도 일단 빼봄

$.ajax({
      url: "/test/posts",
      data: postReqestDto,
      type: 'POST',
    });

아 저 주소가 그 주소가 아닌가?

PostController에 있는 주소 써놨는데 그게 아닌가보다

$.ajax({
      url: "/test/posts",
      data: postReqestDto,
      type: 'POST',
    });

여기는 갈 부분 주소 써놓고

@PostMapping("test/posts")
    public void ajaxHome(Model model, PostRequestDto dto){
        model.addAttribute("address",dto.getAddress());
        model.addAttribute("store",dto.getAddress());
        model.addAttribute("minPrice",dto.getAddress());
        model.addAttribute("deliveryCost",dto.getAddress());
    }

여기는 그거 받을 거 써야되나..?

헐 문제가 되는 ajaxHome 저기써있음

function dataSend() {
    const data = $("#input").val();
    const postReqestDto = {
      result: data
    };
    $.ajax({
      url: "/test/posts",
      data: postReqestDto,
      type: 'POST',
    });
  }

이부분이 지금보니 잘못된게 많음

function dataSend() {
    const address = $("#address").val();
    const store = $("#store").val();
    const minPrice = $("#minPrice").val();
    const deliveryCost = $("#deliveryCost").val();

    const postReqestDto = {
      address: address,
      store: store,
      minPrice: minPrice,
      deliveryCost: deliveryCost
    };

일단 이렇게 고침

작성 완료 눌러도 이제 오류 안 뜨긴 함

데이터가 어디로도 안 가는거 같지만 ㅋㅋ

function dataSend() {
    const address = $("#address").val();
    const store = $("#store").val();
    const minPrice = $("#minPrice").val();
    const deliveryCost = $("#deliveryCost").val();

    const postReqestDto = {
      address: address,
      store: store,
      minPrice: minPrice,
      deliveryCost: deliveryCost
    };
    $.ajax({
      url: "/test/posts",
      contentType: 'application/json',
      data: JSON.stringify(postReqestDto),
      type: 'POST',
    });

이렇게 바꾸고 버튼을 눌러도 안 날아감 ㅋㅋ…

 

다른거 보니 그냥 <script>길래 나도 바꿈

<script>
  function dataSend() {
    let address = $('#address').val();
    let store = $('#store').val();
    let minPrice = $('#minPrice').val();
    let deliveryCost = $('#deliveryCost').val();

    $.ajax({
      url: "/test/posts",
      contentType: 'application/json',
      data: JSON.stringify({
        address: address,
        store: store,
        minPrice: minPrice,
        deliveryCost: deliveryCost
      }),
      type: 'POST',
    });
  }
</script>

이렇게 해봄

 

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">

<th:block th:fragment="content">
  <div class="container">
    <div class="row mt-3">
      <div class="card text-center">
        <h5 class="card-header">글 작성 페이지</h5>
        <div class="card-body">
          <input id="address" placeholder="주소" type="text" class="form-control">
          <input id="store" placeholder="가게" type="text" class="form-control">
          <input id="minPrice" placeholder="최소주문금액" type="text" class="form-control">
          <input id="deliveryCost" placeholder="배달비" type="text" class="form-control">
          <button class="btn btn-outline-secondary" type="button" onclick="dataSend()">작성 완료
          </button>
        </div>
      </div>
    </div>
  </div>
</th:block>
</html>
<script>
  function dataSend() {
    let address = $('#address').val();
    let store = $('#store').val();
    let minPrice = $('#minPrice').val();
    let deliveryCost = $('#deliveryCost').val();

    $.ajax({
      url: "/test/posts",
      contentType: 'application/json',
      data: JSON.stringify({
        address: address,
        store: store,
        minPrice: minPrice,
        deliveryCost: deliveryCost
      }),
      type: 'POST',
    });
  }
</script>

다른거 보다가 깨달음 파일이 여태까지 script로 끝나고 html로 안 끝났던게 문제일수도?

html로 끝나게 해봄

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
  <title>Data Sending Page</title>
  <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
</head>
<body>
<th:block th:fragment="content">
  <div class="container">
    <div class="row mt-3">
      <div class="card text-center">
        <h5 class="card-header">글 작성 페이지</h5>
        <div class="card-body">
          <input id="address" placeholder="주소" type="text" class="form-control">
          <input id="store" placeholder="가게" type="text" class="form-control">
          <input id="minPrice" placeholder="최소주문금액" type="text" class="form-control">
          <input id="deliveryCost" placeholder="배달비" type="text" class="form-control">
          <button class="btn btn-outline-secondary" type="button" onclick="dataSend()">작성 완료
          </button>
        </div>
      </div>
    </div>
  </div>
</th:block>
<script th:inline="javascript">
  function dataSend() {
    $.ajax({
      url: "/test/posts",
      contentType: 'application/json',
      data: JSON.stringify({
        address: address,
        store: store,
        minPrice: minPrice,
        deliveryCost: deliveryCost
      }),
      type: 'POST'
    });
  }
</script>

</body>

</html>

 

지금보니

 		let address = $('#address').val();
    let store = $('#store').val();
    let minPrice = $('#minPrice').val();
    let deliveryCost = $('#deliveryCost').val();

이부분 없어서 다시 넣어봄

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
  <title>Data Sending Page</title>
  <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
</head>

<body>
  <th:block th:fragment="content">
    <div class="container">
      <div class="row mt-3">
        <div class="card text-center">
          <h5 class="card-header">글 작성 페이지</h5>
          <div class="card-body">
            <input id="address" placeholder="주소" type="text" class="form-control">
            <input id="store" placeholder="가게" type="text" class="form-control">
            <input id="minPrice" placeholder="최소주문금액" type="text" class="form-control">
            <input id="deliveryCost" placeholder="배달비" type="text" class="form-control">
            <button class="btn btn-outline-secondary" type="button" onclick="dataSend()">작성 완료</button>
          </div>
        </div>
      </div>
    </div>
  </th:block>

  <script th:inline="javascript">
    function dataSend() {
      let address = $('#address').val();
      let store = $('#store').val();
      let minPrice = $('#minPrice').val();
      let deliveryCost = $('#deliveryCost').val();
      
      $.ajax({
        url: "/test/posts",
        data: JSON.stringify({
          address: address,
          store: store,
          minPrice: minPrice,
          deliveryCost: deliveryCost
        }),
        type: 'POST'
      });
    }
  </script>

</body>

</html>
 

💡💡코드 보면서 멍때리는데 갑자기 찾아온 벼락같은 깨달음

url: "/test/posts",

생각해보니 이렇게만 쓰면 안 됨!!

url: "/api/v1/test/posts",

주소 수정해봄!!

 

<script th:inline="javascript">
  function dataSend() {
    let address = $('#address').val();
    let store = $('#store').val();
    let minPrice = $('#minPrice').val();
    let deliveryCost = $('#deliveryCost').val();
    $.ajax({
      url: "/api/v1/test/posts",
      contentType: 'application/json',
      data: JSON.stringify({
        address: address,
        store: store,
        minPrice: minPrice,
        deliveryCost: deliveryCost
      }),
      type: 'POST',
      success: function(response) {
        console.log('Success:', response);
      },
      error: function(error) {
        console.error('Error:', error);
      }
    });
  }
</script>

contentType: 'application/json',

요새는 이거 빼도 자동으로 된다길래 코드 삭제해봄

2024-01-12T17:55:17.949+09:00 WARN 3092 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported]

와 드디어 콘솔에 뭐가 떴다

 

json인거 빼니까 저러는거같은데

contentType: 'application/json',

이거 다시 넣어줌

와 여태 몰랐는데 서버 재구동하면 다시 그 api로 들어가서 열어줘야되는 거였나봄?

java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "com.moayo.moayoeats.domain.post.dto.request.PostRequest.deadlineMins()" is null
	at com.moayo.moayoeats.domain.post.service.impl.PostServiceImpl.createPostTest(PostServiceImpl.java:195) ~[main/:na]
	at com.moayo.moayoeats.domain.post.controller.PostController.createPostTest(PostController.java:84) ~[main/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:262) ~[spring-web-6.1.2.jar:6.1.2]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190) ~[spring-web-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:917) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:829) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.1.2.jar:6.1.2]

헐 데이터 잘 넘어감!!!!

근데 내가 NotNull을 붙여놨었나봄

public record PostRequest(

    @NotBlank
    String address,
    @NotBlank
    String store,
    @NotNull
    Integer minPrice,
    @NotNull
    Integer deliveryCost,

    @NotNull
    @Max(59)
    Integer deadlineMins,
    @NotNull
    @Max(3)
    Integer deadlineHours,
    CategoryEnum category

    ) {

}

그렇네

와악 감격과 기쁨의 눈물 ㅜㅜ

db에 데이터 들어감!!!!

 

이거 하면서 맨 처음에 브라우저에서 입력한게 Controller로 가면 그 Controller에서 html파일 부른다음에 거기서 ajax로 내가 개발한 Controller로 JSON 전송하는 흐름인걸 알게됨

4. 메뉴 조회페이지 함

간단하게 함

Controller단

//자신의 메뉴 조회
    @GetMapping("/menus")
    public ApiResponse<List<MenuResponse>> getMenus(
        @Valid @RequestBody MenuReadRequest menuReadReq,
        @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        return new ApiResponse<>(HttpStatus.OK.value(), "나의 메뉴 조회",
            menuService.getMenus(menuReadReq, userDetails.getUser()));
    }

Service단

@Override
    public List<MenuResponse> getMenus(MenuReadRequest menuReadReq, User user) {
        Post post = findPostById(menuReadReq.postId());
        List<Menu> menus = menuRepository.findAllByUserAndPost(user, post);
        return menus.stream().map(menu -> new MenuResponse(menu.getMenuname(), menu.getPrice()))
            .toList();
    }

5. 모집마감기능

모집마감 → 더이상 참가가 되지 않아야 함+메뉴가 더이상 써지지 않아야 함, → 관련 boolean 필드가 있어야 할듯

결제완료 → 나가기 기능이 없어져야 함, → 관련 boolean 필드가 있어야 할듯

아 나가기 기능도 있어야되네 ㅋㅋㅋㅋ

관련 메뉴 싹 다 삭제하고 참가자것만 남길것, post에서 연관관계 끊고 리뷰에다가 만듦

수령완료 →Order발생시키고 글이랑 메뉴 관계 다 끊은다음에 메뉴 여기다 다 옮김→Order를 보고 리뷰작성 가능해질것!!

→ 저걸 여러개의 boolean값들로 하기보다 그냥 Status Enum을 하나 둬서 관리하는게 낫다고 판단해서 그렇게 진행함!

 

일단 상태 Enum을 만들어봄

public enum PostStatusEnum {
    OPEN,
    CLOSED,
    ORDERED,
    RECEIVED
}

그리고 Post Entity에 달아줌!

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "tb_post")
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String address;

    @Column(nullable = false)
    private String store;

    @Column(nullable = false)
    private Integer minPrice;

    @Column(nullable = false)
    private Integer deliveryCost;

    @Column
    @Enumerated(EnumType.STRING)
    private CategoryEnum category;

    @Column
    private Long sumPrice;

    @Column
    private LocalDateTime deadline;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Menu> menus;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Offer> offers;

    @Column
    @Enumerated(EnumType.STRING)
    private PostStatusEnum postStatus;<-!! 여기 추가함

    @Builder
    public Post(String address, String store, Integer minPrice, Integer deliveryCost,
        CategoryEnum category, LocalDateTime deadline,PostStatusEnum postStatus) {
        this.address = address;
        this.store = store;
        this.minPrice = minPrice;
        this.deliveryCost = deliveryCost;
        this.deadline = deadline;
        this.category = category;
        this.postStatus = postStatus;
    }

    public void closeApplication(){<-모집마감 상태변경 메서드!
        this.postStatus = PostStatusEnum.CLOSED;
    }
@Override
    public void closeApplication(PostIdRequest postIdReq, User user) {
        
    }

작성자인지 알아보는 로직!

private void checkIfHost(User user, Post post){
        if(!userPostRepository.existsByUserIdAndPostIdAndRole(user.getId(), post.getId(), UserPostRole.HOST)){
            throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS);
        }
    }

하던중에 에러코드 해봐야지 싶어서 하는데

//403
    FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN.value(), "작성자만 글을 수정/삭제할 수 있습니다."),

하려고 수정함

//403
    FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN.value(), "작성자만 할 수 있는 기능입니다."),
@Override
    public void closeApplication(PostIdRequest postIdReq, User user) {
        //check if there is a post with the post id
        Post post = getPostById(postIdReq.postId());
        //check if the user is the host of the post
        checkIfHost(user,post);
        post.closeApplication();
        postRepository.save(post);
    }

테스트해봄

{
    "postId":"1"
}
{
    "status": 200,
    "message": "모집이 마감되었습니다.",
    "data": null
}

여러번 눌러도 똑같다 ㅋㅋ..

상태 체크를 안 함 내가

//400
    ALREADY_CLOSED(HttpStatus.BAD_REQUEST.value(), "이미 모집이 마감된 게시글 입니다."),

이거 넣어서 상태 체크를 함

{
    "status": 400,
    "message": "이미 모집이 마감된 게시글 입니다.",
    "data": null
}

음 ~ 잘 뜸

모집마감 후에는 메뉴생성을 할 수 없는 걸 해봄

MENU_NOT_ALLOWED(HttpStatus.BAD_REQUEST.value(), "모집마감 후에는 메뉴를 생성/삭제할 수 없습니다."),

이거 한줄씩 추가함

checkIfPostIsClosed(post);
public void createMenu(MenuRequest menuReq, User user) {

        Post post = findPostById(menuReq.postId());
        checkIfPostIsClosed(post);

        Menu menu = Menu.builder().post(post).menuname(menuReq.name()).price(menuReq.price())
            .user(user).build();

        menuRepository.save(menu);
    }

    public void deleteMenu(MenuDeleteRequest menuDeleteReq, User user) {
        Menu menu = findMenuById(menuDeleteReq.menuId(), user);
        checkIfPostIsClosed(menu.getPost());
        menuRepository.delete(menu);
    }
private void checkIfPostIsClosed(Post post){
        //모집마감 후에는 메뉴를 생성/삭제할 수 없음
        if(post.getPostStatus()!= PostStatusEnum.OPEN){
            throw new GlobalException(PostErrorCode.MENU_NOT_ALLOWED);
        }
    }

잘 동작한다~~

 

 

'개발일지' 카테고리의 다른 글

2024-01-14, Today I Learned  (0) 2024.01.15
2024-01-13, Today I Learned  (0) 2024.01.14
2024-01-11, Today I Learned  (1) 2024.01.11
2023-01-10, Today I Learned  (0) 2024.01.10
2024-01-09, Today I Learned @IdClass Composite key in Spring  (0) 2024.01.09