Loading...

Spring/Blog-V1 / / 2022. 3. 18. 22:55

스프링 37강. 블로그 세부페이지 수정

반응형

 

글쓰기 상세페이지에 삭제 버튼을 추가해주자.

 

수정 버튼은 수정 페이지 이동이니까 get요청 -> 하이퍼링크

삭제는 get 요청으로 할 수 없으니까 버튼으로 만들어준다.

 

 

 

현재 상태는 다른 아이디로 로그인해도 글 상세보기 페이지에서

수정과 삭제 버튼이 보이게 되어있다.

 

해당 게시글의 userId(FK)와 세션의 principal id를 비교해서 권한을 체크해준다.

동일하면 글 삭제 가능, 같지 않으면 글 삭제 불가능하게!

 

delete 요청에는 body가 없기 때문에

게시글의 id로 한번 더 SELECT 해서 userid를 확인해야 한다.

 

<script>

  async function deleteById () {
    let id = $("#id").val();

    // 무조건 서버는 json 응답! 생긴건 json 모양이지만 결국 통신은 문자열
    let response = await fetch("/s/post/" + id, {
      method: "DELETE"
    });

    let responseObject = await response.json();

    if(responseObject.code == 1) { // 통신 성공
      alert("삭제 성공");
      location.href = "/";
    } else { // 통신 실패 code = -1
      alert("삭제 실패");
      console.log("삭제 실패 : " + responseObject.msg);
    }
  }

  $("#btn-delete").click((event) => {
    // DELETE:/post/{id}
    deleteById();
  });
</script>
// DELETE 글 삭제 /post/{id} -> 글 목록으로 가기 - 인증 O
@DeleteMapping("/s/post/{id}")
public @ResponseBody ResponseDto<String> delete(@PathVariable Integer id) {

    // 인증
    User principal = (User) session.getAttribute("principal");

    if (principal == null) { // 로그인 안됨
        return new ResponseDto<String>(-1, "로그인에 실패하였습니다.", null);
    }

    // 권한
    Post postEntity = postService.글상세보기(id); // 재활용

    if(principal.getId() != postEntity.getUser().getId()) {
        return new ResponseDto<String>(-1, "해당 글을 삭제할 권한이 없습니다.", null);
    }


    postService.글삭제하기(id); // 내부적으로 exception이 터지면 무조건 stackTrace를 리턴

    return new ResponseDto<String>(1, "성공", null);
}
@Transactional
public void 글삭제하기(Integer id) {
    postRepository.deleteById(id); // 실패했을 때 내부적으로 exception 터짐
}

 

cos로 로그인했는데 ssar이 게시한 글의 수정, 삭제 버튼이 보인다.

권한 체크해주자.

포스트 컨트롤러에서 모델에 값 하나 더 보낼 것이다.

 

// 글 상세보기 페이지 /post/{id} (삭제버튼 만들어두면 되니까 삭제페이지 필요 X)
@GetMapping("/post/{id}") // Get요청에 /post 제외시키기(인증 X)
public String detail(@PathVariable Integer id, Model model) { // int는 null이 없음, 초기값이 0
                                                              // Integer는 초기값이 null

    User principal = (User) session.getAttribute("principal");

    // 권한
    Post postEntity = postService.글상세보기(id); // 재활용, EAGER니까 user 가지고있음

    // 게시물이 없으면 error 페이지 이동
    if(postEntity == null){
        return "error/page1";
    }

    // 로그인 안했을때 터짐 null.getId했던것
    if(principal != null) {
        // 권한 확인해서 view로 값 넘김
        if(principal.getId() == postEntity.getUser().getId()) { // 권한이 있다는 뜻
            model.addAttribute("pageOwner", true);
        } else {
            model.addAttribute("pageOwner", false);
        }

    }
    
    model.addAttribute("post", postEntity);
    return "post/detail";
    
}
{{#pageOwner}} <!--true이면 실행-->
<a href="/s/post/{{post.id}}/updateForm" class="btn btn-secondary">수정</a>
<button class="btn btn-danger" id="btn-delete">삭제</button>
{{/pageOwner}}

ssar로 로그인

 

cos로 로그인

 


 

글 수정해보자.

수정 버튼을 누르면 해당 주소로 가고 404 에러가 뜬다.

해당 페이지는 우리가 만든 적 없는 페이지 -> 스프링이 제공해준 것

 

저 화면보다는 좀 더 예쁜 페이지를 리턴해서 이전 화면으로 돌아가 주게 하는 게 UX가 좋겠다.

이건 버전 2에서 만들어보자.

 

404 에러는 Not Found 상태 코드 확인해보자.

컨트롤러 자체를 못 찾았던지 컨트롤러가 리턴하는 파일이 없는 것이다.

우리는 컨트롤러는 있는데 /post/updateForm이 없다.

writeForm 파일을 복사하여 만들어주면 되겠다.

 

수정은 put 요청이라서 form 태그를 사용할 수 없다. fetch 사용!

name은 필요 없다. 자바스크립트로 찾아야 하니까 id로 변경해주자.

 

다 수정하면 submit이 아니라 button 타입이여야한다.

버튼은 원래 타입을 안 적으면 디폴트가 button 타입이다.

근데 폼 태그 내부에 있는 버튼은 디폴트가 submit 타입이다.

 

헷갈리니까 웬만하면 버튼의 타입을 적어주는 것을 습관 하면 좋겠다.

 

 

게시글 수정하는 거니까 원래 적혀있던 글 내용(value)이 있어야겠다.

 

이제 원래 적혀있던 글 내용을 가져와보자.

글 수정 페이지 컨트롤러로 갈 때 모델을 하나 가져가야 한다.

그리고 인증과 권한 체크도 필요하다.

다이렉트하게 주소로 접근할 수 있기 때문이다.

 

// 글 수정 페이지 /post/{id}/updateForm - 인증 O
@GetMapping("/s/post/{id}/updateForm")
public String updateForm(@PathVariable Integer id, Model model) {

    // 인증
    User principal = (User) session.getAttribute("principal");

    if(principal == null) {
        return "error/page1";
    }

    // 권한
    Post postEntity = postService.글상세보기(id);

    if(postEntity.getUser().getId() != principal.getId()) {
        return "error/page1";
    }

    model.addAttribute("post", postEntity);

    return "post/updateForm"; // ViewResolver 도움 받음
}
{{> /layout/header}}

<!-- 컨테이너 시작 -->
<div class="container mt-3"> <!-- container, mt-3은 css 클래스임!! -->

    <!-- 글쓰기 수정 폼 시작 --> 
    <form>
        <div class="mb-3 mt-3">
          <input type="text" class="form-control" placeholder="Enter title" id="title" value="{{post.title}}">
        </div>
        <div class="mb-3">
          <textarea class="form-control" id="content" rows="10">{{post.content}}</textarea>
        </div>
        
        <button type="submit" class="btn btn-secondary" id="btn-update">글쓰기 수정완료</button>
    <!-- 글쓰기 수정 폼 끝 -->
  </form>
</div>
<!-- 컨테이너 끝 -->

{{> /layout/footer}}

이제 수정 완료 버튼을 눌리면 fetch로 put요청해야 한다.

<script>

  async function update() {

    let post = {
      title: $("#title").val(),
      content: $("#content").val()
    }
    console.log("title : " + post.title);
    console.log("content : " + post.content);

    let postJson = JSON.stringify(post);
    console.log("postJson : " + postJson);

    
    let id = $("#id").val();
    let response = await fetch("/s/post/" + id, {
      headers: {
        'Content-Type': 'application/json;charset=utf-8'
      },
      method: 'PUT',
      body: postJson
    });

    console.log("response : " + response);

    let responseObject = await response.json();
    console.log("responseObject.code : " + responseObject.code);

    if(responseObject.code == 1) {
      alert("수정이 정상적으로 완료되었습니다.")
      location.href = "/post/" + id;
    }

  }

  $("#btn-update").click((event) => {
    update();
  });
</script>

 

자바스크립트로 요청할 때는 반드시 데이터로 응답받아야 한다.

 

// UPDATE 글 수정 /post/{id} -> 글 상세보기 페이지 가기 - 인증 O
@PutMapping("/s/post/{id}")
public @ResponseBody ResponseDto<String> update(@PathVariable Integer id, @RequestBody Post post) {

    // 인증
    User principal = (User) session.getAttribute("principal");

    if(principal == null) {
        return new ResponseDto<String>(-1, "로그인에 실패하였습니다.", null);
    }

    // 권한
    Post postEntity = postService.글상세보기(id);

    if(postEntity.getUser().getId() != principal.getId()) {
        return new ResponseDto<String>(-1, "해당 게시글을 수정할 권한이 없습니다.", null);
    }

    postService.글수정하기(post, id);

    return new ResponseDto<String>(1, "성공", null);
}
@Transactional
public void 글수정하기(Post post, Integer id) {
    // 영속화
    Optional<Post> postOp = postRepository.findById(id);

    // 변경감지
    if(postOp.isPresent()) {
        Post postEntity = postOp.get();
        postEntity.setTitle(post.getTitle());
        postEntity.setContent(post.getContent());
    }
} // 더티체킹 완료 (수정됨)

 


 

지금까지 배운 내용이 CRUD 기본이다.

 

모든 웹이 CRUD의 반복이다.

 


 

글 content가 담기는 textarea가

UX가 좋고 텍스트 편집하기 좋게 만들어주자.

 

https://summernote.org/

 

Summernote - Super Simple WYSIWYG editor

Super Simple WYSIWYG Editor on Bootstrap Summernote is a JavaScript library that helps you create WYSIWYG editors online.

summernote.org

 

 

cdn 헤더에  추가해주면 사용이 가능하다.

분명 cdn이 중복되는 게 있을 텐데 중복되는 건 지워줘야 한다.

 

<!-- include summernote css/js -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>

 

<div id="summernote"><p>Hello Summernote</p></div>
<script>
  $(document).ready(function() {
      $('#summernote').summernote();
  });
</script>

 

글쓰기 textarea에 걸어줘야 한다.

 

{{> /layout/header}}

<!-- 컨테이너 시작 -->
<div class="container mt-3"> <!-- container, mt-3은 css 클래스임!! -->

    <!-- 글쓰기 폼 시작 --> 
    <form action="/s/post" method="post"> 
        <div class="mb-3 mt-3">
          <input type="text" class="form-control" placeholder="Enter title" name="title">
        </div>
        <div class="mb-3">
          <textarea class="form-control" name="content" rows="10" id="summernote"></textarea>
        </div>
        
        <button type="submit" class="btn btn-secondary">글쓰기</button>
    </form>
    <!-- 글쓰기 폼 끝 -->
    
</div>
<!-- 컨테이너 끝 -->

<script>
  $("#summernote").summernote();
</script>

{{> /layout/footer}}

 

 

 

 

비교 표현식 

 

content자리에 <script>alert("안녕");</script>를 넣으면

섬머 노트가 디비에 넣을 때 꺽쇠를 비교 표현식으로 치환해서 들어간다.

&lt;script&gt; 이런 식으로!

 

그냥 브라우저에서 열면 알아서 치환되어 잘 나오는데

섬머 노트가 아닌 포스트맨으로 접근하면 치환되지 않고 꺽쇠가 그대로 디비에 들어가게 된다.

 

그리고 브라우저에서 들어가 보면 공격이 정상적으로 작동된다.

 

이를 막기 위해 디비에 넣을 때 치환해서 넣거나 디비에서 당겨올 때 치환해서 가져와야 한다.

자동으로 치환해서 디비에 저장된 게 아니라 섬머 노트가 해준 것이었다.

 

우리는 디비에서 빼올 때 막아보도록 하자.

 

// <> -> &lt;&gt; 치환해주기
String rawContent = postEntity.getContent();
String encContent = rawContent
        .replaceAll("<script>", "&lt;script&gt;")
        .replaceAll("</script>", "&lt;/script&gt;");
postEntity.setContent(encContent);

치환해주는 게 비즈니스 로직은 아니니까 컨트롤러에서 만들어준다.

컨트롤러의 핵심 로직은 2줄밖에 안되는데 부가 로직이 더 많다.

 

이거 말고도 막아야 할게 진짜 많다.

lucy filter 같은 라이브러리(방어 코드)를 쓰면 한방에 해결 가능하다.

 


 

여기까지 블로그 프로젝트 버전 1이 끝났다.

자바 코드는 총 9개가 나왔다.

코드를 다시 반복하면서 리팩토링 해보자.

 

이제 x-www-form-urlencoded 안 쓰고 전부 fetch 요청할 것이다.

깔끔하고 심플하게 통일하자.

 

 

[출처]

 

https://cafe.naver.com/metacoding

 

메타코딩 : 네이버 카페

코린이들의 궁금증

cafe.naver.com

메타 코딩 유튜브

https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9

 

메타코딩

문의사항 : getinthere@naver.com 인스타그램 : https://www.instagram.com/meta4pm 깃헙 : https://github.com/codingspecialist 유료강좌 : https://www.easyupclass.com

www.youtube.com

 

 

반응형