게시물 제목을 클릭하면 본문인 가계도가 뜨는 기능을 추가했다.
이 때, 가계도를 구성하는 노드와 엣지는 db상에서 게시물(board)와 다른 테이블에 저장되어 있다. 그래서 이들을 불러오기 위해서는 제목을 클릭할 때 id를 서버에 같이 넘겨줘야 한다.

1. 전체 흐름

이러한 흐름으로 controller, service, 각종 html파일이 작동한다.
2. 큰 흐름
우선 실선 화살표로 표시된 흐름을 따라가며 코드를 리뷰하자.
BoardController에 TitleClick()을 추가했다. view로 해당 게시글의 데이터를 보내는 코드이다.
이전에 저장된 노드들을 불러와 본문에 띄워야하기 때문에 NodeEditService의 GetNode를 통해 이들을 불러온다.
네트워크를 다루는 코드는 network_sample.html 파일에 전부 있기 때문에 일단 이리로 모든 데이터를 보낸다. 컨트롤러에서 뷰로 데이터를 전달하므로 Model을 사용하여 게시물 id와 노드 리스트를 보낸다.
@GetMapping("board/{no}")
String TitleClick(@PathVariable("no")Long no, Model model){
List<Node>nodes=nodeEditService.GetNode(no); //해당 게시글에 있었던 노드 전부 가져오기
model.addAttribute("nodeList",nodes);
model.addAttribute("BoardID",no);
return "network_sample";
}
network_sample.html의 saveNode를 수정했다. 노드 수정사항들을 서버로 보낼 때 게시물 id도 함께 보내서 어떤 게시물에 대한 노드들인지 알려줘야 한다. saveNodes는 BoardID는 url에 담아서, 수정된 노드 리스트는 바디에 담아서 서버로 보낸다.
여기서 BoardID를 다루는 방법이 익숙치 않아 잠시 시행착오가 있었다.
${BoardID} : 변수로 아예 인식을 못함.
[[${BoardID}]] : 정상 작동.
내가 착각했던거였는데, 서버에서 받은 데이터를 다루려면 thymeleaf 문법에서는 html에선 ${변수}, js에서는 [[${변수}]] 꼴로 다루어야 한다.
function saveNodes() { //updatedNodes(수정된 노드) 정보를 controller로 전송
BoardID=[[${BoardID}]];
let xhr1 = new XMLHttpRequest();
xhr1.open('POST', '/network/board/'+parseInt(BoardID));
xhr1.setRequestHeader('Content-Type', 'application/json');
xhr1.send(JSON.stringify(updatedNodes));
updatedNodes.splice(0, updatedNodes.length);
}
NetworkController의 saveNetwork()도 이에 맞추어 수정했다. PathVariable을 사용하여 /network/board 뒤에 올 BoardID인 no를 다루도록 했다.
@PostMapping("/network/board/{no}")
String saveNetwork(@RequestBody ArrayList<NodeDTO> nodeList, @PathVariable("no")Long no) throws Exception {
for(NodeDTO element: nodeList){
System.out.println(element.getType());
System.out.println(element.getName());
System.out.println(element.getDetails());
nodeEditService.EditNode(nodeList,no);
}
return "redirect:/";
}
3. 디테일
1) Optional을 피하려면 .orElseThrow를 적극 활용하자
NodeEditService의 EditNode()부터 살펴보자.

여기서 add부분만 보면 된다. 넘겨받은 BoardID로 db에서 Board 객체를 찾아와야해서 findByID()를 사용했다. findBy꼴 함수들은 해당 데이터가 존재하지 않을 때는 Null을 리턴하기 때문에 Optional을 사용하지 않으면 빨간줄이 뜬다. 그런데 이 찾은 객체를 새롭게 생성한 Node 객체에 넣어줘야 하는데, 생성자에 Optional을 넣는건 허용되지 않는다. 따라서 여기서 에러를 발생시켜 즉각적으로 처리해야 아래와같이 Optional이 아닌 원래 객체 타입 그대로 리턴받을 수 있다.
public void EditNode(List<NodeDTO> nodelist, Long BoardID) throws Exception {
for(int i=0; i<nodelist.size(); i++){
NodeDTO curNode=nodelist.get(i);
int type=curNode.getType();
//add
if(type==0){
Board board=boardRepository.findById(BoardID).orElseThrow(()->new Exception());
Node newNode=new Node(curNode.getId(), board, curNode.isHub(),curNode.getPhotoUrl(),curNode.getAuthorID(),curNode.getName(),curNode.getDetails());
System.out.println(newNode.getId());
nodeRepository.save(newNode);
}
//delete
else if(type==1){
Node delNode=nodeRepository.findById(curNode.getId()).orElseThrow(()->new IllegalStateException("존재하지 않는 노드"));
nodeRepository.delete(delNode);
}
//update
else if(nodelist.get(i).getType()==2){
Node updateNode=nodeRepository.findById(curNode.getId()).orElseThrow(()->new IllegalStateException("존재하지 않는 노드"));
updateNode.setHub(curNode.isHub());
updateNode.setPhotoUrl(curNode.getPhotoUrl());
updateNode.setAuthorID(curNode.getAuthorID());
updateNode.setName(curNode.getName());
updateNode.setDetails(curNode.getDetails());
//똑같이 save를 호출해도 내부적으로 업데이트로 처리해줌.
nodeRepository.save(updateNode);
}
}
}
2) 객체를 찾는 findBy꼴 함수를 추가하려면 @Query 어노테이션을, 그리고 콜론(:)과 @Param도 넣어줘야 한다.
NodeEditService의 GetNode()를 살펴보자

GetNode는 findByboard()를 통해 db에서 해당 게시물에 포함된 노드를 찾아오는 함수이다. 보통 Repository를 생성하면 기본적으로 제공하는 함수는 findByid 등과 같이 기본적인 꼴 밖에 없기 때문에 엔티티의 다른 속성을 통해 DB에서 이를 찾아오려면 직접 Repository에 이를 등록해야 한다.
public ArrayList<Node> GetNode(Long BoardID){
ArrayList<Node>nodelist= (ArrayList<Node>) nodeRepository.findByboard(BoardID);
return nodelist;
}
원래는 아래의 예시처럼 "findBy속성명" 꼴로 Repository에 적어주기만 하면 자동으로 이러한 메소드가 등록된다.
List<Node> findByboard(Long id);
하지만 이번 경우에는 java.lang.IllegalArgumentException: Argument [5] of type [java.lang.Long] did not match parameter type [ (n/a)] 이런 에러가 뜬다. 왜냐하면 Node에 Board는 id가 아닌 Board 타입 그 자체로 저장되어 있기 때문에, Long 타입 변수로 Board 타입 객체를 찾을 수 없기 때문이다. 따라서 @Query 어노테이션을 통해 직접 쿼리를 작성해주어야 한다.

NodeRepository에 아래와 같은 방식으로 findByboard를 추가했다.
@Query("SELECT a FROM Node a WHERE a.board.id = :id")
List<Node> findByboard(@Param("id") Long id);
쿼리에서 우변을 보면 id 앞에 콜론이 있다. id에 해당하는 값을 동적으로 받기 위함이다. findByboard 함수에서 변수로 들어온 id 값이 쿼리에 전달되는데, id 앞의 @Param 어노테이션을 통해 이루어진다.