[Springboot] 회원 게시판 #1

728x90

인텔리제이 -> SpringMybatisMini 프로젝트

 

🔔 TODO : 회원 게시판

1) 로그인 한 경우에만 글쓰기 버튼이 보이게

2) 

 

 

🖐 새로 배운 것

autofocus="autofocus"
// 폼 이동시 이 것을 설정해준 쪽으로 화면이 바로 이동한다.

margin : 상(top) 우(right) 하(bottom) 좌(left) 
//margin 한번에 주기

select * from bootmember order by num desc limit 0,2;
//bootmember 테이블을 조회하는데, 0번부터 2번까지 만 조회한다 
//10개의 글이 있으면 2개만 조회 된다

 

select ifnull(Max(num),0) from memboard

- 기본적으로 조회하면 max(num)은 null 이 나오는데, 이것을 자바에서 처리해주기 귀찮으닌까 sql쿼리에서 ifnull 을 사용해서 default 값을 0으로 한다.

      -> oracle에서는 MVL을 사용 

 

Model -> MemboardService / MemboardRepository

View -> memboard 

Controller -> MemBoardController

Mapper -> memboardsql

 


#1 멤버 게시판을 만들고 데이터를 처리하기 위해선 데이터를 보내줄 폼을 작성했다

addform.jsp (게시글 등록폼)

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
  <link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
  <script src="https://code.jquery.com/jquery-3.7.0.js"></script>
  <title>글 쓰기</title>
</head>
<body>
<div style="margin-left: 330px;">
<form action="insert" method="post" enctype="multipart/form-data">
    <table class="table table-bordered" style="width:500px;">
      <caption align="top"><b>회원전용 글쓰기</b></caption>
      <tr>
        <th>제목</th>
        <td><input type="text" name="subject" class="form-control" required="required" autofocus="autofocus"></td>
      </tr>

      <tr>
        <th>파일업로드</th>
        <td><input type="file" name="upload" class="form-control"></td>
      </tr>

      <tr>
        <td colspan="2">
          <textarea style="width:490px; height: 200px;" required="required" class="form-control" name="content"></textarea>
        </td>
      </tr>

      <tr>
        <td colspan="2" align="center">
          <button type="submit" class="btn btn-outline-dark">등록</button>
          <button type="button" class="btn btn-outline-dark" onclick="location.href='list'">목록</button>
          <button type="button" class="btn btn-outline-dark" onclick="location.href='content1'">글 목록</button>
        </td>
      </tr>
</div>
    </table>
</form>
</body>
</html>

content.jsp ( 내가 쓴 글 보는 페이지 )

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
  <link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
  <script src="https://code.jquery.com/jquery-3.7.0.js"></script>
  <title>Insert title here</title>
</head>
<body>

  <div style="margin : 50px 150px;">
    <table class="table table-bordered" style="width:800px;">
        <tr>
          <td>
            <h4><b>${dto.subject}</b>
              <span style="font-size:0.7em; color:gray; float: right">
                  🔔 조회수 : ${dto.readcount}&nbsp;&nbsp;
                  🕰 작성일 : <fmt:formatDate value="${dto.writeday}" pattern="yyyy-MM-dd HH:mm:ss"/>
              </span>
            </h4>
            <span>작성자 : ${dto.name} (${dto.myid})</span>

              <c:if test="${dto.uploadfile!= 'no'}">
                <span style="float:right">
                  <a href="download?clip=${dto.uploadfile}">
                    <i class="bi bi-arrow-down-circle">&nbsp;</i><b>${dto.uploadfile}</b>
                  </a>
                </span>
              </c:if>
          </td>
        </tr>

      <tr>
        <td>
          <c:if test="${bupload ==true}">
          <img src="../savefile/${dto.uploadfile}" style="width:350px; height:400px;">
          </c:if>
          <br><br>
          <pre>
            ${dto.content}
          </pre>
        </td>
      </tr>

      <c:if test="${sessionScope.loginok!=null}">
      <tr>
        <td>
          <button type="button" class="btn btn-outline-dark" onclick="location.href='form'" style="width:100px;">글작성</button>
          <button type="button" class="btn btn-outline-dark" onclick="location.href='list'" style="width:100px;">목록</button>

          <c:if test="${sessionScope.loginok!=null and sessionScope.myid ==dto.myid}">
          <button type="button" class="btn btn-outline-dark" onclick="location.href='update?num=${dto.num}'" style="width:100px;">수정</button>
          <button type="button" class="btn btn-outline-dark" onclick="location.href='delete?num=${dto.num}'" style="width:100px;">삭제</button>
          </c:if>
        </td>
      </tr>
      </c:if>
    </table>
  </div>
</body>
</html>

 

#2 다음으로는 이제 이 데이터를 처리해줄 로직입니다.

MemBoardMapperInter.java

import java.util.HashMap;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import boot.data.dto.MemBoardDto;

@Mapper
public interface MemBoardMapperInter {
	public int getTotalCount();
	public void updateReadcount(String num);
	public void insertBoard(MemBoardDto memBoardDto);
	public MemBoardDto getData(String num);
	public int getMaxNum();
	public List<MemBoardDto> getList(HashMap<String,Integer> map);
}
  • Mybatis에서 sql문을 처리해줄 로직을 모아둔 곳 입니다
    • 나중에 이 로직을 복사해서 진짜로 처리해줄, Repository에 모아둘 예정 입니다.
  • getTotalCount()  --> 글의 개수를 구하기 위한 로직
  • updateReadCount(String num)  --> num값에 따른 dto들이 클릭 되었을 때 마다 조회수를 올리기 위한 로직
  • insertBoard(MemboardDto dto) --> DB에 저장할 로직
  • getData(String num) --> num값에 따른 dto값을 가져오기 위한 로직 ex) 2번 num의 값들을 다 불러온다
  • getMaxNum() --> 이 메소드를 사용하는 이유는 내가 글을 쓴 이후 detailpage 즉 내가 쓴글을 보기위해서는 쓴 글에 대한 num값이 필요하다. 그러므로 getMaxNum()을 사용해서 제일 최근에 올라온 num값이 detailpage로 넘겨주는num이다
    • 위 글은 나중에 확실하게 이해해서 다시 쓰도록 하겠습니다.
  • getList(Hash<String,Integer> map) --> 리스트 출력 및 페이징 처리하기 위한 로직

 

다음으로는 이 메소드들을 sql쿼리로 처리해주기 위한 Mapper 이다

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="boot.data.mapper.MemBoardMapperInter">
    <select id="getTotalCount" resultType="int">
        select count(*) from memboard
    </select>
    <update id="updateReadcount" parameterType="String">
        update memboard set readcount=readcount+1 where num=#{num}
    </update>
    <insert id="insertBoard" parameterType="MemBoardDto">
        insert into memboard(myid,name,subject,content,uploadfile,writeday) values (#{myid},#{name},#{subject},#{content},#{uploadfile},now())
    </insert>
    <select id="getData" resultType="MemboardDto" parameterType="String">
        select * from memboard where num=#{num}
    </select>
    <select id="getMaxNum" resultType="int">
        select ifnull(Max(num),0) from memboard
    </select>
    <select id="getList" parameterType="HashMap" resultType="MemboardDto">
        select * from memboard order by num desc limit #{start},#{perpage}
    </select>
</mapper>
  •   MemBoardMapperInter클래스에 있는 메소드들 이름이랑 mapper에서 처리해줄 id랑 name이 무조건 같아야 한다. 그렇게 처리를 해놨다
  • resultType, ParameterType 이 동일한 이유는 Dto를 생성해준곳에서 @Alias(MemBoardDto) 를 해주었기 때문에,이 어노테이션에 의해 MemBoardDto로 한다 (원래 alias는 별명으로 더 줄여서 하는데, 저는 그냥 똑같이 함)

#3 다음으로는 위 메소드들을 처리해주긴 위한 로직 입니다.

위 Mapper 메소드들을 그대로 복사해와서 메소드를 모아둘 인터페이스를 생성했다

 

MemboardServiceRepository.java

package boot.data.service;

import java.util.HashMap;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import boot.data.dto.MemBoardDto;

@Mapper
@Repository
public interface MemboardServiceRepo {
	public int getTotalCount();
	public void updateReadcount(String num);
	public void insertBoard(MemBoardDto memBoardDto);
	public MemBoardDto getData(String num);
	public int getMaxNum();
	public List<MemBoardDto> getList(int start, int perpage);
}
  • 여기서 궁금한게 Mapper와 repository 를 둘다 왜 선언했는지가 궁금 할 수도 있다
    • https://pamyferret.tistory.com/69 이 글을 보고 확실이 이해할 수 있었습니다    (궁금하신 분은 읽어보세요)
      • 결론만 말하면 Repository라는 박스안에, 작은 박스중 하나가 Mapper라는 것을 알수 있었다.
  • 그리고 이상한게 하나 있는데, Mapper에서는 List 매개변수를 HashMap<> 으로 처리하였지만, 여기서는 Hash를 넣지않고 직접적인 start, perpage 라는 int 변수를 넣었다.  왜 일까?
    • 제 생각으로는 Mapper에서는 sql문을 처리해주기 위한 간접적인 메소드 생성하는 곳이고, repository는 이제   직접적으로 메소드를 처리해주어야기 때문에 확실한 값 들을 매개변수로 주었다고 생각이 듭니다..!

 

다음으로는 이 메소드들을 진짜로 implements 받아 구현해줄 메소드들을 보겠습니다

 

MemboardService.java

package boot.data.service;

import java.util.HashMap;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import boot.data.dto.MemBoardDto;
import boot.data.mapper.MemBoardMapperInter;

@Service
public class MemboardService implements MemboardServiceRepo{
	@Autowired
	MemBoardMapperInter memBoardMapperInter;
	@Override
	public int getTotalCount() {
		return memBoardMapperInter.getTotalCount();
	}
	@Override
	public void updateReadcount(String num) {
		memBoardMapperInter.updateReadcount(num);
	}
	@Override
	public void insertBoard(MemBoardDto memBoardDto) {
		memBoardMapperInter.insertBoard(memBoardDto);
	}
	@Override
	public MemBoardDto getData(String num) {
		return memBoardMapperInter.getData(num);
	}
	@Override
	public int getMaxNum() {
		return memBoardMapperInter.getMaxNum();
	}

	@Override
	public List<MemBoardDto> getList(int start, int perpage) {
		HashMap<String,Integer> map = new HashMap<>();
		map.put("start",start);
		map.put("perpage",perpage);

		return memBoardMapperInter.getList(map);
	}
}
  • List를 출력 할 떄 HashMap으로 담아서 페이징을 시켜줄 것이다
  • map을 제네릭 타입이 <String,Integer>인 이유는 <key,value> 이기 때문에 key는 보통 String 값이 들어가서 나중에 꺼내서 쓸때 문자열을 호출해서 사용할 테고, 우리는 페이징을 해줘서 페이징 값은 숫자로 처리해줘야 하기 때문에, Integer를 넣었다.
    • 그리고 왜 int 가 아닌 Integer를 넣었는지 궁금 할 수 도 있다.
      • 이 것에 대한 해답을 원하면 제네릭 타입에 대하여 공부해야 한다 ✔
      • Java에서 제네릭 클래스나 인터페이스를 사용할 때, 기본 데이터 타입(예: int, double)은 허용되지 않습니다. 대신, 해당 기본 데이터 타입에 해당하는 래퍼 클래스를 사용해야 합니다.

 

최종적으로 data를 처리하고 반환해줄 controller를 보겠습니다

	@PostMapping("/insert")
	public String insert(@ModelAttribute MemBoardDto dto , HttpSession httpSession) {

		String path = httpSession.getServletContext().getRealPath("/savefile");
		SimpleDateFormat sdf=  new SimpleDateFormat("yyyyMMddHHmmss");
		//System.out.println(path);

		//업로드 할게 없으면은
		if(dto.getUpload().getOriginalFilename().equals("")) {
			dto.setUploadfile("no");
		} else { //업로드 한 경우
			String uploadFile = sdf.format(new Date()) + dto.getUpload().getOriginalFilename();
			dto.setUploadfile(uploadFile);

			try {
				dto.getUpload().transferTo(new File(path+"\\"+uploadFile));
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}

		//아이디를 insert안하고 세션을 통해서 얻오는 방법
		String myid = (String)httpSession.getAttribute("myid");
		//세션에서 저장한 아이디를 dto에 저장시킨다.
		dto.setMyid(myid);

		//이름을 insert안하고 서비스를 통해서 얻는 방법
		String name = memberService.getName(myid);
		dto.setName(name);

		memboardService.insertBoard(dto);
		return "redirect:content?num="+memboardService.getMaxNum(); //num을 통해서 자신이 쓴 글에 목록으로 들어가진다.
	}

	@GetMapping("/content")
	public ModelAndView content(@RequestParam String num , @RequestParam(defaultValue = "1") int currentPage) {
		ModelAndView modelAndView = new ModelAndView();

		memboardService.updateReadcount(num); //위 글 누르면은. 조회수가 올라가게 끔

		MemBoardDto dto =memboardService.getData(num);
		modelAndView.addObject("dto",dto);

		//업로드 파일의 확장자 얻기
		int dotLoc = dto.getUploadfile().lastIndexOf('.');
		String ext = dto.getUploadfile().substring(dotLoc+1); //다음글자부터 끝까지 추출
		System.out.println(dotLoc + ext);

		if(ext.equalsIgnoreCase("jpg") || ext.equalsIgnoreCase("gif") || ext.equalsIgnoreCase("png") || ext.equalsIgnoreCase("jpeg")) {
			modelAndView.addObject("bupload",true);
		} else {
			modelAndView.addObject("bupload",false);
		}
		modelAndView.addObject("currentPage",currentPage);
		modelAndView.setViewName("/memboard/content");
		return modelAndView;
	}
  • insert 할 때 파일을 담아서 보내줘야한다. 그래서 form에서 enctype="multipart-formdata" 를 해줬다
  • 위 insert 로직에서 파일을 담아줄 path 변수를 만들어 둔다.
    • 그리고 업로드 했을 때 조건이랑, 업로드 안했을 때 조건을 지정해 준다.
  • 그리고 세션에서 저장된 id랑 name을 dto에 담아서 insert를 해준다. 
    • 왜 이렇게 하냐? 물론 dto에도 db에 저장된 레코드 들이 있을 것이다. 하지만 현재 세션에 저장되어 로그인된       아이디와 현재 로그인 한 사람을 이름을 가져와서, dto에 담아서 데이터를 넘겨주면은 나중에 form에서 myid랑  name을 비교하는 로직을 짤 수가 있어서 넘긴다고 생각한다.
  • 그리고 return 하는 주소를 적을 때는 content중에서, 제일 높은 숫자를 redirect를 해줍니다. 
    • 그래서 getMaxNum() 메소드를 만들어 준 것 이다.

 

  • 다음은 content에 내용을 띄어줄 list를 작성하는 메소드 이다.
    • updateReadCount 미리 만들어둔 메소드를 통해, 새로고침 및 누군가 내 글을 눌러서 보면은 조회수가 올라가게한다
  • 그리고 num값에 따른 dto들을 modelandview 에 담아서 form으로 보내준다.
  • 업로드 파일의 확장자 얻기 이 로직은 그냥 이런게 있다는 것을 알아두고 이 로직을 작성하게 되면은 내가 파일을 업로드를 하면은 그 업로드된 파일을 다시 내가 다운받을 수 있다
    • 위 로직은 아래 코드에서 더 자세하게 알 수 있다.

 

 

DowonloadController.java

package boot.data.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
public class DownloadController {

	//외부서버의 파일을 내 컴퓨터로 다운로드하는소스
	@GetMapping("/memboard/download")
	public void download(HttpServletRequest request,
		HttpServletResponse response,
		@RequestParam String clip)
	{
		String path=request.getSession().getServletContext().getRealPath("/savefile");
		File file=new File(path+"\\"+clip);
		System.out.println("파일 경로:"+file);
		setHeaderType(response, request, file);

		try {
			transport(new FileInputStream(file),
				response.getOutputStream(), file);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	private void setHeaderType(HttpServletResponse response,
		HttpServletRequest request,
		File file)
	{
		String mime = request.getSession().getServletContext().getMimeType(file.toString());
		if(mime != null)
			mime = "application/octet-stream";
		response.setContentType(mime);
		response.setHeader("Content-Disposition",
			"attachment;filename=" + toEng(file.getName()));
		response.setHeader("Content-Length", "" + file.length());

	}

	private void transport(InputStream in, OutputStream out, File file)
		throws IOException
	{
		BufferedInputStream bin = null;
		BufferedOutputStream bos = null;

		try{
			bin = new BufferedInputStream(in);
			bos = new BufferedOutputStream(out);

			byte[] buf=new byte[(int)file.length()];
			int read=0;
			while((read = bin.read(buf)) != -1)
			{
				bos.write(buf, 0, read);   //객체, 시작(offset), 길이
			}
		}catch(Exception e){
			System.out.println("transport error : " + e);
		}finally{
			bos.close();
			bin.close();
		}
	}
	//////////////////////////////////////////////////////////////
	public String toEng(String str)
	{
		String tmp=null;
		try{
			tmp = new String(str.getBytes("utf-8"), "8859_1");
		}catch(Exception e){}
		return tmp;
	}

}

 

 

이상 입니다.

728x90