Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 86일차(18주차4일) - Spring Framework: myapp-64 중간(Client rendering), 프로젝트 절차(Actor 식별, Use-case 식별) 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Spring Framework, Spring Boot

[비트캠프] 86일차(18주차4일) - Spring Framework: myapp-64 중간(Client rendering), 프로젝트 절차(Actor 식별, Use-case 식별)

끈기JK 2023. 3. 9. 14:15

 

64. Front-end / Back-end

 

fetch(게시글 조회) 를 ① 호출 하면 AJAX 작업 을 ② 실행 하고 종료된다.

fetch(로그인 사용자 조회) 를 ③ 호출 하면 AJAX 작업 을 ④ 실행 하고 종료된다.

AJAX 작업 두 개가 비동기 실행한다. 순차적으로 실행하는 것이 아니라 서로 독립적으로 병행하여 실행한다.

 

 

fetch(게시글 조회) 를 ① 호출 하면 Promise 객체를 리턴한다. 여기서 .then(response) 로 결과를 통지한다. 여기서 Promise 객체를 리턴한다. 여기서 .then(result) 로 결과를 통지한다. 여기서 function checkowner(작성자 번호) { fetch(로그인 사용자 조회) 로 Promise 객체를 리턴한다. 결과를 .then(response) 에 통지한다. 여기서 Promise 객체를 리턴한다. 여기서 결과를 .then(result) 로 통지한다. }

 

 

문서화 참고 사이트 : https://cloud.kt.com/docs/open-api-guide/d/guide/how-to-use-openstack

 

 

 

① 예전

Client 가 주로 PC 이었기 대문에 hTML 을 만들어 주면 되었다.

PC 인 Client 가 ① 요청을 Server 로 하면 ② HTML 생성하고 ③ HTML 응답을 Client 로 한다. 

 

② 현재

Client 가 다양한 유형이기 때문에 서버는 XML / JSON 형식으로 data 를 만들어 준다.

PC, Android, iOS Client 가 ① 요청을 Server 로 하면 ② JSON 생성해서 ③ 응답(JSON) 을 PC Android, iOS Client 로 한다. 각 Client 에서 ④ HTML 생성, Android UI 생성, iOS UI 생성 한다.

 

클라이언트가 자신에 맞게끔 UI 생성

 

 

 

### 64. Back-end 와 Front-end 분리하기: 클라이언트 렌더링 방식으로 전환 

 

util 패키지 생성해서 자주 쓰는 문자열을 상수로 만든다.

static 중첩 클래스는 분류를 위해 사용한다.

클래스 명을 소문자로 사용하는 이유는 필드명처럼 사용하기 위함이다.

package bitcamp.util;

public class ErrorCode {
  public static final class rest {
    public static final String UNAUTHORIZED = "401";
    public static final String NO_DATA = "501";
  }
}
package bitcamp.util;

public class RestStatus {
  public static final String SUCCESS = "success";
  public static final String FAILURE = "failure";
}

 

결과를 Map 객체로 전달하지 않고 객체를 만들어 메서드 체이닝 기법으로 담아 전달한다.

package bitcamp.util;

public class RestResult {
  String status;
  String errorCode;
  Object data;

  public String getStatus() {
    return status;
  }
  public RestResult setStatus(String status) {
    this.status = status;
    return this;
  }
  public String getErrorCode() {
    return errorCode;
  }
  public RestResult setErrorCode(String errorCode) {
    this.errorCode = errorCode;
    return this;
  }
  public Object getData() {
    return data;
  }
  public RestResult setData(Object data) {
    this.data = data;
    return this;
  }
}

 

모든 request handler 가 @ResponseBody 를 붙이므로 그 대신 클래스에 @RestController 를 붙인다.

return 으로 RestResult 객체 생성해서 메서드 체이닝 기법으로 값 세팅한다.

Model 객체 사용하지 않으므로 파라미터에서 삭제한다.

"com.fasterxml.jackson.core:jackson-databind" 라이브러리 사용하여 Java 객체 → JSON 문자열 변환시, Content-Type 헤더 값은 "application/json" 으로 설정된다.

package bitcamp.myapp.controller;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import bitcamp.myapp.service.BoardService;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;
import bitcamp.util.ErrorCode;
import bitcamp.util.RestResult;
import bitcamp.util.RestStatus;

@RestController
@RequestMapping("/board")
public class BoardController {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("BoardController 생성됨!");
  }

  @Autowired private ServletContext servletContext;
  @Autowired private BoardService boardService;

  @PostMapping("insert")
  public Object insert(
      Board board,
      List<MultipartFile> files,
      HttpSession session) throws Exception{

    Member loginUser = (Member) session.getAttribute("loginUser");

    Member writer = new Member();
    writer.setNo(loginUser.getNo());
    board.setWriter(writer);

    List<BoardFile> boardFiles = new ArrayList<>();
    for (MultipartFile file : files) {
      if (file.isEmpty()) {
        continue;
      }

      String filename = UUID.randomUUID().toString();
      file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));

      BoardFile boardFile = new BoardFile();
      boardFile.setOriginalFilename(file.getOriginalFilename());
      boardFile.setFilepath(filename);
      boardFile.setMimeType(file.getContentType());
      boardFiles.add(boardFile);
    }
    board.setAttachedFiles(boardFiles);

    boardService.add(board);

    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @GetMapping("list")
  public Object list(String keyword) {
    log.debug("BoardController.list() 호출됨!");

    // MappingJackson2HttpMessageConverter 가 jackson 라이브러리를 이용해
    // 자바 객체를 JSON 문자열로 변환하여 클라이언트로 보낸다.
    // 이 컨버터를 사용하면 굳이 UTF-8 변환을 설정할 필요가 없다.
    // 즉 produces = "application/json;charset=UTF-8" 를 설정하지 않아도 된다.
    return new RestResult()
        .setStatus(RestStatus.SUCCESS)
        .setData(boardService.list(keyword));
  }

  @GetMapping("view")
  public Object view(int no) {
    Board board = boardService.get(no);
    if (board != null) {
      return new RestResult()
          .setStatus(RestStatus.SUCCESS)
          .setData(board);
    } else {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.NO_DATA);
    }
  }

  @PostMapping("update")
  public Object update(
      Board board,
      List<MultipartFile> files,
      HttpSession session) throws Exception {

    Member loginUser = (Member) session.getAttribute("loginUser");

    Board old = boardService.get(board.getNo());
    if (old.getWriter().getNo() != loginUser.getNo()) {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.UNAUTHORIZED)
          .setData("권한이 없습니다.");
    }

    List<BoardFile> boardFiles = new ArrayList<>();
    for (MultipartFile file : files) {
      if (file.isEmpty()) {
        continue;
      }

      String filename = UUID.randomUUID().toString();
      file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));

      BoardFile boardFile = new BoardFile();
      boardFile.setOriginalFilename(file.getOriginalFilename());
      boardFile.setFilepath(filename);
      boardFile.setMimeType(file.getContentType());
      boardFile.setBoardNo(board.getNo());
      boardFiles.add(boardFile);
    }
    board.setAttachedFiles(boardFiles);

    boardService.update(board);

    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @PostMapping("delete")
  public Object delete(int no, HttpSession session) {
    Member loginUser = (Member) session.getAttribute("loginUser");

    Board old = boardService.get(no);
    if (old.getWriter().getNo() != loginUser.getNo()) {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.UNAUTHORIZED)
          .setData("권한이 없습니다.");
    }
    boardService.delete(no);

    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @PostMapping("filedelete")
  public Object filedelete(int boardNo, int fileNo, HttpSession session) {
    Member loginUser = (Member) session.getAttribute("loginUser");
    Board old = boardService.get(boardNo);

    if (old.getWriter().getNo() != loginUser.getNo()) {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.UNAUTHORIZED)
          .setData("권한이 없습니다.");

    } else {
      boardService.deleteFile(fileNo);
      return new RestResult()
          .setStatus(RestStatus.SUCCESS);
    }
  }

}

 

package bitcamp.myapp.controller;

import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import bitcamp.myapp.service.StudentService;
import bitcamp.myapp.service.TeacherService;
import bitcamp.myapp.vo.Member;
import bitcamp.util.RestResult;
import bitcamp.util.RestStatus;

@RestController
@RequestMapping("/auth")
public class AuthController {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("AuthController 생성됨!");
  }

  @Autowired private StudentService studentService;
  @Autowired private TeacherService teacherService;

  @PostMapping("login")
  public Object login(
      String usertype,
      String email,
      String password,
      HttpSession session) {

    Member member = null;
    switch (usertype) {
      case "student":
        member = studentService.get(email, password);
        break;
      case "teacher":
        member = teacherService.get(email, password);
        break;
    }

    if (member != null) {
      session.setAttribute("loginUser", member);
      return new RestResult()
          .setStatus(RestStatus.SUCCESS);
    } else {
      return new RestResult()
          .setStatus(RestStatus.FAILURE);
    }
  }

  @GetMapping("logout")
  public Object logout(HttpSession session) {
    session.invalidate();
    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @RequestMapping("user")
  public Object user(HttpSession session) {
    Member loginUser = (Member) session.getAttribute("loginUser");
    if (loginUser != null) {
      return new RestResult()
          .setStatus(RestStatus.SUCCESS)
          .setData(loginUser);
    } else {
      return new RestResult()
          .setStatus(RestStatus.FAILURE);
    }
  }

}

 

변경, 삭제 버튼은 display: none 으로 안보이게 되어있다가 작성자면 보이게 한다.

버튼은 submit 으로 처리하지 말고 onclick 으로 처리한다.

return false 로 기본 동작인 페이지 이동을 막는다.

<!-- view.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
<style>
.guest {
  display: none;
}
</style>
</head>
<body>
<h1>게시판</h1>

<div>
  <form id='board-form' action='update' method='post' enctype="multipart/form-data">
  <table border='1'>
  <tr>
    <th>번호</th>
    <td><input type='text' name='no' readonly></td>
  </tr>
  <tr>
    <th>제목</th>
    <td><input type='text' name='title'></td>
  </tr>
  <tr>
    <th>내용</th>
    <td><textarea name='content' rows='10' cols='60'></textarea></td>
  </tr>
  <tr>
    <th>작성자</th>
    <td><span id="f-writer-name"></span></td>
  </tr>
  <tr>
    <th>등록일</th>
    <td><span id="f-created-date"></span></td>
  </tr>
  <tr>
    <th>조회수</th>
    <td><span id="f-view-count"></span></td>
  </tr>
  <tr>
    <th>첨부파일</th>
    <td>
      <input type="file" name='files' multiple>
      <ul id="f-files"></ul>
    </td>
  </tr>
  </table>
  
  <div>
    <button id='btn-list' type='button'>목록</button>
    <button id="btn-update" type="button" class="guest">변경</button>
    <button id='btn-delete' type='button' class="guest">삭제</button>
  </div>
  </form>
</div>


<script>
// http://localhost:8080/web/board/view.html?no=100
const values = location.href.split('?');
if (values.length == 1) {
  alert("URL이 옳지 않습니다.");
  location.href = "list.html";
}

// no=100
const param = values[1].split("=")
if (param.length == 1 || param[0] != 'no') {
  alert("URL이 옳지 않습니다.");
  location.href = "list.html";
}

let no = parseInt(param[1]);
if (isNaN(no)) {
  alert("URL이 옳지 않습니다.");
  location.href = "list.html";
}

fetch("../app/board/view?no=" + no)
.then(response => {
  return response.json();
})
.then(result => {
  if (result.status == 'failure') {
    alert('게시글을 조회할 수 없습니다.');
    location.href = "list.html";
    return;
  }
  
  let board = result.data;
  //console.log(board);
  document.querySelector("input[name='no']").value = board.no;
  document.querySelector("input[name='title']").value = board.title;
  document.querySelector("textarea[name='content']").value = board.content;
  document.querySelector("#f-writer-name").innerHTML = board.writer.name;
  document.querySelector("#f-created-date").innerHTML = board.createdDate;
  document.querySelector("#f-view-count").innerHTML = board.viewCount;
  
  let ul = "";
  board.attachedFiles.forEach(file => {
    console.log(file);
    if (file.no == 0) return;
    let html = `
      <li id="li-${file.no}">
        <a href="../download/boardfile?fileNo=${file.no}">${file.originalFilename}</a>
        [<a href="#" onclick="deleteFile(${board.no}, ${file.no}); return false;">삭제</a>]
      </li>`;
    ul += html;
  });
  document.querySelector("#f-files").innerHTML = ul;
  
  checkOwner(board.writer.no);
});

function checkOwner(writerNo) {
  fetch("../app/auth/user")
  .then(response => {
    return response.json();
  })
  .then(result => {
  	console.log(result);
    if (result.status == 'success') {
      if (result.data.no == writerNo) {
        document.querySelector('#btn-update').classList.remove('guest');
        document.querySelector('#btn-delete').classList.remove('guest');
      }
    }
  })
  .catch(exception => {
    alert("로그인 사용자 정보 조회 중 오류 발생!");
    console.log(exception);
  });
}

function deleteFile(boardNo, fileNo) {
  const formData = new FormData();
  formData.append("boardNo", boardNo);
  formData.append("fileNo", fileNo);
  
  fetch("../app/board/filedelete", {
    method: "post",
    body: formData
  })
  .then(response => {
    return response.json();
  })
  .then(result => {
    if (result.status == 'success') {
      let li = document.querySelector('#li-' + fileNo);
      document.querySelector("#f-files").removeChild(li);
    } else {
      alert('파일 삭제 실패!');
    }
  })
  .catch(exception => {
    alert('파일 삭제 중 오류 발생!');
    console.log(exception);
  });
}


document.querySelector('#btn-list').onclick = function() {
  location.href = 'list.html';
}

document.querySelector('#btn-update').onclick = function() {
  const form = document.querySelector('#board-form');
  const formData = new FormData(form);
  
  fetch("../app/board/update", {
    method: "post",
    body: formData
  })
  .then(response => {
    return response.json();
  })
  .then(result => {
    if (result.status == 'success') {
      location.href = 'list.html';
    } else {
      alert('변경 실패!');
    }
  })
  .catch(exception => {
	  alert('변경 중 오류 발생!');
	  console.log(exception);
  });
}
	
document.querySelector('#btn-delete').onclick = function() {
  const formData = new FormData();
  formData.append("no", document.querySelector('input[name="no"]').value);
  
  fetch("../app/board/delete", {
    method: "post",
    body: formData
  })
  .then(response => {
    return response.json();
  })
  .then(result => {
    if (result.status == 'success') {
      location.href = 'list.html';
    } else {
      alert('삭제 실패!');
    }
  })
  .catch(exception => {
    alert('삭제 중 오류 발생!');
    console.log(exception);
  });
}
</script>

</body>
</html>

 

 

 

 

프로젝트 절차

 

 

 

Actor 식별

 

시스템을 사용하는 사람, 프로세스, 시스템

Primary Actor 는 시스템을 이용한다.

시스템이 이용하는 것은 Secondary Actor 이다.

 

은행시스템의 Actor 는 다음과 같다.

사람 : 은행원, 고객

프로세스 : 타이머

시스템 : ATM

 

은행시스템이 이용하는 Secondary Actor(시스템이 이용하는 외부 시스템)은 다음과 같다.

신용평가시스템, 신원조회시스템

 

 

 

Actor 식별 - 상속 관계

 

비회원, 회원이 있다.

회원을 상속하는 판매자, 관리자가 있다.

상위 Actor 는 하위 Actor 들의 공통 역할 수행

하위 Actor 는 상위 Actor 의 모든 역할을 상속 받는다.

 

 

 

Use-case 식별

 

Actor 가 시스템을 이용하여 달성하고자 하는 업무 목표(사용 예 → 사용 사례)

은행원 : 통장개설 하기, 입금 하기, 출금 하기 등 동사구 형태로 이름을 작성

 

Use-case 시나리오 (시스템을 사용하는 시나리오)

액터 ↔ 시스템

① 출금 계좌번호를 입력한다.

        ② 출금 계좌의 정보를 출력한다.

③ 출금액을 입력한다.

        ④ 출금액이 잔액보다 작거나 같으면 잔액에서 출금액을 뺀다. 뺀 결과를 출력한다.

 

 

wiki 'use case' 그림

 

 

 

Use-case 식별 - include / extend 관계

 

회원가입 하다가 전화번호인증으로 갔다가 다시 돌아와서 계속 진행한다.

회원가입하기 ----------> 전화번호 인증  은 《include》 필수 이다.

암호변경 ---------> 전화번호 인증 은 《include》 필수 이다.

전화번호 인증은 여러 Use-case 가 공유하는 기능(시나리오)이다.

 

 

회원가입하기를 진행한다.

선택사항으로 우편번호 찾기(extend point, 확장점)를 하면 주소검색 하기로 갔다가 돌아온다.

주소 검색하기 ------------> 회원가입하기(Base Use-case) 는 《extend》 선택 이다.

파일 첨부 ----------> 게시글 등록하기(Base Use-case) 는 《extend》 선택 이다.

 

모든 관계를 다 표현할 필요가 없다. 중요하고 반드시 명시해야만 알 수 있는 관계를 표현하라!

 

 

 

Use-case 식별 Guide (지침)

① 한 액터가 한 번에 한 순간에 실행하는 업무

→ 상품 구매를 고객이 한다. 주문, 결제를 별도로 할 수 있으므로 나누어서 처리한다.

    배송사가 배송을 한다.

    전체를 상품 구매로 묶는다.

② 시작과 끝이 명확하여 셀 수 있는 업무

→ 게시글 관리를 관리자가 한다. 게시글 등록, 조회, 변경, 삭제를 몇 건 진행했는지 셀 수 있다.

③ (개발할)시스템이 할 수 있는 업무

팩스 전송 우리가 개발할 시스템이 수행하는 것이 아니다.

 

⇒ 지침서에 따라서 Use-case 를 식별하면 2주~4주 안에 개발할 수 있는 크기로 시스템 기능을 쪼갤 수 있다. 개발 관리가 용이한 단위로 시스템 기능을 정의한다.

 

 

Use-case 는 업무여야 한다. 비밀번호 확인, 로그인 등은 업무가 아니다.

게시글 목록 보기에서 게시글 상세 보기는 따로 빼서 목록 보기와 동일 선상에서 봐야 한다.

Use-case 는 동사구 형태를 띤다.

 

 

 


 

 

조언

 

*

 

 


 

과제

 

/