maybeCafe -1-

하파타카·2022년 4월 29일
0

SpringBoot와 MyBatis를 이용한 maybeCafe(아마도카페)프로젝트


컨셉: 프로그래밍 공부카페 (네이버카페 벤치마크)

기능
전체회원

  • 가입인사
  • 출석체크 (우선순위 낮음)
  • 질문게시판
    • 글번호
    • 제목
    • 작성자
  • 공부게시판
  • 건의게시판
  • 회원별 프로필
    • 방명록
      관리자
  • 회원관리
  • 게시글 관리
  • 공지글 작성

고민해볼 기능

  • 신고누적 10회 이상시 해당 글 블라인드
  • 회원 등급별 기능제한
  • 북마크

DB구성

  • user
    • uno 회원번호
    • uid 회원id
    • upassword 회원비밀번호
    • 이름
  • 관리자
    • id
    • password
  • questionBoard
    • qid
    • writer 작성자id (예 - hongGD)
    • 제목
    • 내용
    • 작성시간
    • 수정시간
    • 조회수
    • 추천수
  • studyBoard
    • questionBoard 중복생략
  • guestBoard
    • questionBoard 중복생략
  • reply
    • 덧글번호
    • 글번호
    • 덧글내용
    • 덧글작성자

화면구성

  • 카페 메인페이지
    • 가입인사
    • 공부 게시판
    • 건의 게시판
  • 탈퇴하기

CREATE SCHEMA maybecafe;

메인페이지

각 게시판으로 이동할 메인페이지를 만듬.
테스트편의를 위해 작성판 페이지이므로 추후에 내용은 수정할 예정.

- IndexController -

@Controller
public class IndexController {	
	@GetMapping("/")
	public String indexPage() {
		return "index";
	}
}

- index.html -

<a th:href="@{/studyboard/list}">공부 게시판</a>

- application.properties -

# DB setting
spring.datasource.url=jdbc:mysql://localhost:3306/maybecafe?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234

# MyBatis
# mapper.xml
mybatis.mapper-locations: mapper/*.xml

# model 열이름 언더바 '_' -> camel case setting
mybatis.configuration.map-underscore-to-camel-case=true

# 패키지 result type의 패키지를 생략할 수 있도록 model packge 위치 지정
mybatis.type-aliases-package=com.myapp.maybeCafe.model

# mapper log level setting
logging.level.com.myapp.mybatis.mapper=TRACE

공부 게시판 - 전체조회

DB 테이블 생성

create table maybecafe.studyboard(
    sno int auto_increment primary key,
    title varchar(150) not null,
    content varchar(2000) not null,
    writer varchar(50) not null,
    regdate timestamp default now() not null,
    updatedate timestamp default now() not null,
    hit int not null,
    like_no int not null
);
insert into studyboard(title, content, writer, hit, like_no) values ('테스트 제목1', '테스트 내용1', '작가1', 0, 0);
insert into studyboard(title, content, writer, hit, like_no) values ('테스트 제목2', '테스트 내용2', '작가2', 0, 0);
insert into studyboard(title, content, writer, hit, like_no) values ('테스트 제목3', '테스트 내용3', '작가3', 0, 0);
insert into studyboard(title, content, writer, hit, like_no) values ('테스트 제목4', '테스트 내용4', '작가4', 0, 0);
insert into studyboard(title, content, writer, hit, like_no) values ('테스트 제목5', '테스트 내용5', '작가5', 0, 0);

bean클래스

- StudyBoardVO -

@Data
public class StudyBoardVO {
	
	private int sno;		// 글번호
	private String writer;	// 작성자id
	private String title;	// 글제목
	private String content;	// 내용
	private LocalDateTime regdate;	// 작성일자
	private LocalDateTime updatedate;	// 수정일자
	private int hit;		// 조회수
	private int like_no;	// 추천수
}

service, mapper

- StudyBoardService.interface -

public interface StudyBoardService {
	public List<StudyBoardVO> getStudyBoardList();	// 공부게시판 전체 게시글 가져오기
}

- StudyBoardServiceImpl -

@Service
public class StudyBoardServiceImpl implements StudyBoardService{
	
	private StudyBoardMapper sBoardMapper;
	
	// 생성자 주입
	public StudyBoardServiceImpl(StudyBoardMapper studyBoardMapper) {
		this.sBoardMapper = studyBoardMapper;
	}

	@Override
	public List<StudyBoardVO> getStudyBoardList() {
		// 공부게시판 전체 가져오기
		return sBoardMapper.getStudyBoardList();
	}
}

- StudyBoardMapper.interface -

@Mapper
public interface StudyBoardMapper {
	public List<StudyBoardVO> getStudyBoardList();	// 공부게시판 전체 게시글 가져오기
}

- StudyBoardMapper.xml -

<?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="com.myapp.maybeCafe.dao.StudyBoardMapper">

	<!-- 게시판 목록 -->
	<select id="getStudyBoardList" resultType="StudyBoardVO">
		SELECT * FROM studyboard
	</select>
	
</mapper>

controller

- StudyBoardController -

@Controller
@RequestMapping("/studyboard")
@Log		// 콘솔에 로그 출력 (print out 대신 로그출력)
public class StudyBoardController {
	
	private StudyBoardService sBoardService;
	
	public StudyBoardController(StudyBoardService studyBoardService) {
		this.sBoardService = studyBoardService;
	}

	@GetMapping("list")
	public String studyboardListGET(Model model) {
		model.addAttribute("studyboard", sBoardService.getStudyBoardList());
		return "studyBoard/studyList";
	}
}

view

- studyBoardList.html-

<div>
  <a th:href="@{/index.html}">메인페이지</a>
</div>
<div>
  <table>
    <thead>
      <tr>
        <th>NO</th>
        <th>TITLE</th>
        <th>등록일</th>
        <th>조회수</th>
        <th>추천수</th>
      </tr>
    </thead>
    <tbody>
      <tr th:each="board : ${studyboard}">
        <td><span th:text="${board.sno}"></span></td>
        <td>
          <a href="#"><span th:text="${board.title}"></span></a>
        </td>
        <td><span th:text="${#temporals.format(board.regdate , 'yyyy-MM-dd a hh:mm:ss')}"></span></td>
        <td><span th:text="${board.hit}"></span></td>
        <td><span th:text="${board.like_no}"></span></td>
      </tr>
    </tbody>
  </table>
</div>

테스트

http://localhost:8080/studyboard/list


JUnit5로 테스트환경 조성

controller클래스의 JUnit5를 이용한 테스트를 위해 환경조성

- pom.xml -

<!-- 마이바티스 테스트 -->
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter-test</artifactId>
	<version>2.2.2</version>
	<scope>test</scope>
</dependency>

mybatis테스트 dependency를 pom.xml에 수동으로 추가

- StudyBoardMapperTest -

@MybatisTest
@AutoConfigureTestDatabase(replace = Replace.NONE)	// 현재 연결된 실제DB로 테스트함을 의미
@Rollback(value = false)							// 테스트시 롤백 안함
@Log
public class StudyBoardMapperTest {
	// JUnit 5버전으로 테스트
    
    @Autowired
	private StudyBoardMapper sBoardMapper;
    
    테스트 코드 작성
}

공부게시판 - 단일 글 조회

service, mapper

- StudyBoardService -

public StudyBoardVO getPage(int sno);	// 게시글 조회

- StudyBoardServiceImpl -

@Override
public StudyBoardVO getPage(int sno) {
	// 공부게시판 하나의 게시글 가져오기
	return sBoardMapper.getPage(sno);
}

- StudyBoardMapper -

public StudyBoardVO getPage(int sno);	// 공부게시판 단일 게시글 조회

- StudyBoardMapper.xml -

<!-- 단일 게시글 조회 -->
<select id="getPage" resultType="StudyBoardVO">
	select * from studyboard WHERE sno = #{sno}
</select>

JUnit5 테스트

- StudyBoardMapperTest -

@MybatisTest
@AutoConfigureTestDatabase(replace = Replace.NONE)	// 현재 연결된 실제DB로 테스트함을 의미
@Rollback(value = false)							// 테스트시 롤백 안함
@Log
public class StudyBoardMapperTest {
	// JUnit 5버전으로 테스트
	@Autowired
	private StudyBoardMapper sBoardMapper;

	@Test
	public void studyBoardGet() {
		int sno = 1;
		log.info("" + sBoardMapper.getPage(sno));
	}
}

1번 게시글을 조회

controller

- StudyBoardController -

@GetMapping("/get")
public String studyboardPageGET(@RequestParam("sno") int sno, Model model) {
	model.addAttribute("board", sBoardService.getPage(sno));
	return "studyBoard/studyGet";
}

view

- studyBoardGet.html -

<div>
  <a th:href="@{/studyboard/list}">돌아가기</a>
</div>
<div>
  <table>
    <tr>
      <th>NO</th>
      <td th:text="${board.sno}"></td>
      <th>작성일</th>
      <td th:text="${#temporals.format(board.regdate, 'yyyy-MM-dd a hh:mm:ss')}"></td>
    </tr>
    <tr>
      <th>작성자</th>
      <td th:text="${board.writer}"></td>
      <th>수정일</th>
      <td th:text="${#temporals.format(board.updatedate, 'yyyy-MM-dd a hh:mm:ss')}"></td>
    </tr>
    <tr>
      <th>조회수</th>
      <td th:text="${board.hit}"></td>
    </tr>
    <tr>
      <th>추천수</th>
      <td th:text="${board.like_no}"></td>
    </tr>
    <tr>
      <th>제목</th>
      <td th:text="${board.title}"></td>
    </tr>
    <tr>
      <th>내용</th>
      <td th:text="${board.content}"></td>
    </tr>
  </table>
</div>

테스트

http://localhost:8080/studyboard/get?sno=1


공부게시판 - 게시글 작성

service, mapper

- StudyBoardService.interface -

public void write(StudyBoardVO sBoardVO); // 게시글 작성

- StudyBoardMapper.interface -

public void write(StudyBoardVO sBoardVO);	// 게시글 작성

- StudyBoardMapper.xml -

<!-- 게시글 등록 -->
<select id="write" resultType="StudyBoardVO">
	INSERT INTO studyboard (title, content, writer, hit, like_no)
       VALUES (#{title}, #{content}, #{writer}, 0, 0)
</select>

- StudyBoardServiceImpl -

@Override
public void write(StudyBoardVO sBoardVO) {
	// 공부게시판 게시글 작성
	sBoardMapper.write(sBoardVO);
}

JUnit5 테스트

- StudyBoardMapperTest -

@Test
public void testWrite() {
	StudyBoardVO vo = new StudyBoardVO();
	vo.setTitle("JUnit5테스트_제목");
	vo.setContent("JUnit5테스트_컨텐츠내용");
	vo.setWriter("JUnit5테스트_작성자");
	vo.setHit(0);
	vo.setLike_no(0);
	sBoardMapper.write(vo);
}

controller

- StudyBoardController -
작성페이지로 이동

@GetMapping("/write")
public String goWritePage(Model model) {
	model.addAttribute("board", new StudyBoardVO());
	return "studyBoard/studyWrite";
}

post로 전송받은 데이터를 DB에 저장

@PostMapping("/write")
public String boardWritePOST(StudyBoardVO board, RedirectAttributes attr) {
	sBoardService.write(board);
	attr.addFlashAttribute("message", "게시글이 등록되었습니다.");
	return "redirect:/studyboard/list";
}

view

- studyBoardWrite.html -

<div>
  <a th:href="@{/studyboard/list}">돌아가기</a>
</div>
<div>
  <h4>새로운 게시글 작성</h4>
  <form th:action="@{/studyboard/write}" method="post" th:object="${board}">
    <div>
      <label for="title">제목</label>
      <input type="text" th:field="*{title}" required />
    </div>
    <div>
      <label for="content">내용</label>
      <textarea th:field="*{content}" required></textarea>
    </div>
    <div>
      <label for="writer">작성자</label>
      <input type="text" th:field="*{writer}" required />
    </div>
    <button type="submit">작성하기</button>
  </form>
</div>

- studyBoardList.html -

<script th:if="${message}">
  let m = '[[${message}]]'; // 리다이렉트로 넘어온 메시지 저장
  alert(m); // 화면에 메시지 띄우기
</script>

글작성이 완료되어 리스트페이지로 이동시 message 에 글이 성공적으로 등록되었다는 확인문구를 입력하여 같이 이동함.
리스트페이지에서는 message에 값이 있는지 확인하여 있을 경우 alert창으로 값을 출력함.

테스트

http://localhost:8080/studyboard/write


등록이 완료되면 studyBoardList.html페이지로 이동하며 alert창에 attr.addFlashAttribute("message", "게시글이 등록되었습니다.");로 인해 전달받은 message를 출력한다.


공부게시판 - 게시글 수정

수정은 조회, 작성, 삭제와 달리 '하나의 게시글의 데이터를 가져오는 과정'과 '수정하는 과정'으로 총 2개의 과정으로 만들어야 한다.
우선 데이터를 가져오는 과정을 만들어 테스트 후 수정과정을 만듦.
=> 가져오는 과정자체는 게시글 단일조회(getPage)와 동일하므로 service, mapper는 단일조회의 기능을 사용. controller만 새로 만들어줌.

controller

@GetMapping("/modify")
public String studyModifyGET(@RequestParam("sno") int sno, Model model) {
	model.addAttribute("board", sBoardService.getPage(sno));
	return "studyBoard/studyModify";
}

view

새 게시글작성과 거의 동일함

- studyModify.html -

<h4>게시글 수정</h4>
<form th:action="@{/studyboard/modify}" method="post" th:object="${board}">
  <input type="hidden" th:field="*{sno}" />
  <div>
    <label for="title">제목</label>
    <input type="text" th:field="*{title}" required />
  </div>
  <div>
    <label for="content">내용</label>
    <textarea th:field="*{content}" rows="6" required></textarea>
  </div>
  <div>
    <label for="writer">작성자</label>
    <input type="text" th:field="*{writer}" required />
  </div>
  <button type="submit">수정하기</button>
</form>

여기서 <input type="hidden" th:field="*{sno}" />가 없으면 sno를 가져오지못해 수정데이터가 반영되지 않음.
=> DB의 where절에서 sno가 맞는 레코드를 찾아 수정사항을 반영하므로 sno를 전달받지 못하면 sql이 정상적으로 동작할 수 없음.

수정페이지에 해당 글번호에 해당하는 데이터를 가져오는데까지 성공.

service, mapper

- StudyBoardService.interface -

public void modify(StudyBoardVO sBoardVO);	// 게시글 수정

- StudyBoardMapper.interface -

public void modify(StudyBoardVO sBoardVO);	// 게시글 수정

mapper인터페이스의 메서드이름과 mapper.xml의 id를 맞춰주어 둘이 연동되어 동작하도록 함.

- StudyBoardMapper.xml -

<!-- 게시글 수정 -->
<update id="modify">
	UPDATE studyboard SET title = #{title}, content = #{content}, updatedate = now() WHERE sno = #{sno}
</update>

- StudyBoardServiceImpl -

@Override
public void modify(StudyBoardVO sBoardVO) {
	sBoardMapper.modify(sBoardVO);
}

controller

@PostMapping("/modify")
public String studyModifyPOST(StudyBoardVO board, RedirectAttributes attr) {
	sBoardService.modify(board);
	System.out.println(board);
	attr.addFlashAttribute("message", "수정이 완료되었습니다.");
	return "redirect:/studyboard/list";
}

attr.addFlashAttribute을 통해 "message"객체를 전달.
게시글 작성도 똑같은 방식으로 전달하되 객체의 데이터만 달랐으므로, list페이지에서 alert창이 뜨는것까지 동일하게 동작함.

테스트




수정날짜(updatedate)까지 정상적으로 반영됨을 확인.


공부게시판 - 게시글 삭제

service, mapper

- StudyBoardService.interface -

public void delete(int sno);	// 게시글 삭제

- StudyBoardMapper.interface -

public void delete(int sno);	// 게시글 삭제

- StudyBoardMapper.xml -

<delete id="delete">
	DELETE FROM studyboard WHERE sno = #{sno}
</delete>

- StudyBoardServiceImpl -

@Override
public void delete(int sno) {
	// 공부게시판 게시글 삭제
	sBoardMapper.delete(sno);
}

JUnit5 테스트

- StudyBoardMapperTest -

@Test
public void testDelete() {
	int sno = 5;
	sBoardMapper.delete(sno);
}


=> 5번 게시글 삭제완료

controller

- StudyBoardController

@GetMapping("/delete")
public String boardDeleteGET(@RequestParam("sno") int sno, RedirectAttributes attr) {
	sBoardService.delete(sno);
	return "redirect:/studyboard/list";
}

view

- studyBoardGet.html -

<button onclick="deleteConfirm();">삭제하기</button>

<script>
  function deleteConfirm() {
    if (confirm('정말로 삭제할까요?')) {
      location.href = '/studyboard/delete?sno=' + '[[${board.sno}]]';
    }
  }
</script>

삭제하기 버튼 추가, javascript로 확인창을 띄운 후 확인버튼을 누르면 http://localhost:8080/studyboard/delete?sno=삭제할글번호 으로 이동하도록 함

테스트


8번 게시글 삭제



가입인사

가입인사는 간단한 인삿말을 남기는 간이게시판으로 제작할 예정.
제목없이 내용이 바로 나타나며, 리스트페이지에 모든 가입인사가 바로 나타난다.

DB 테이블

create table maybecafe.greetboard(
    gno int auto_increment primary key,
    content varchar(1000) not null,
    writer varchar(50) not null,
    regdate timestamp default now() not null
);

가입인사 모두 불러오기

bean클래스

- GreetBoardVO -

@Data
public class GreetBoardVO {
	private int gno;
	private String content;
	private String writer;
	private LocalDateTime regdate;
}

service, mapper

- GreetBoardService.interface -

public interface GreetBoardService {
	public List<GreetBoardVO> getGreetBoardList();	// 가입인사 모두 가져오기
}

- GreetboardMapper.interface -

@Mapper
public interface GreetBoardMapper {
	public List<GreetBoardVO> getGreetBoardList();	// 가입인사 모두 가져오기
}

- GreetboardMapper.xml -

<?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="com.myapp.maybeCafe.dao.GreetBoardMapper">
	<select id="getGreetBoardList" resultType="GreetBoardVO">
		SELECT * FROM greetboard ORDER BY gno DESC
	</select>
</mapper>

- GreetBoardServiceImpl -

@Service
public class GreetBoardServiceImpl implements GreetBoardService {
	
	private GreetBoardMapper gBoardMapper;
	
	// 생성자 주입
	public GreetBoardServiceImpl(GreetBoardMapper greetBoardMapper) {
		this.gBoardMapper = greetBoardMapper;
	}

	@Override
	public List<GreetBoardVO> getGreetBoardList() {
		// 가입인사 전체 가져오기
		return gBoardMapper.getGreetBoardList();
	}
}

JUnit5 테스트

- GreetBoardMapperTest -

@MybatisTest
@AutoConfigureTestDatabase(replace = Replace.NONE)	// 현재 연결된 실제DB로 테스트함을 의미
@Rollback(value = false)							// 테스트시 롤백 안함
@Log
public class GreetBoardMapperTest {
	// JUnit 5버전으로 테스트
	@Autowired
	private GreetBoardMapper gBoardMapper;
	
	@Test
	public void testGetGreetBoardList() {
		List<GreetBoardVO> list = gBoardMapper.getGreetBoardList();
		list.forEach(board -> log.info("" + board));
	}
}

controller

- GreetBoardController -

@Controller
@RequestMapping("/greetboard")
@Log		// 콘솔에 로그 출력 (print out 대신 로그출력)
public class GreetBoardController {
	
	private GreetBoardService gBoardService;
	
	public GreetBoardController(GreetBoardService greetBoardService) {
		this.gBoardService = greetBoardService;
	}

	@GetMapping("list")
	public String greetListGET(Model model) {
		model.addAttribute("greetboard", gBoardService.getGreetBoardList());
		return "greetBoard/greetList";
	}
}

view

- greetList.html -

<div>
  <table>
    <thead>
      <tr>
        <th>NO</th>
        <th>내용</th>
        <th>등록일</th>
      </tr>
    </thead>
    <tbody>
      <tr th:each="board : ${greetboard}">
        <td><span th:text="${board.gno}"></span></td>
        <td><span th:text="${#temporals.format(board.regdate , 'yyyy-MM-dd a hh:mm:ss')}"></span></td>
        <td><span th:text="${board.content}"></span></td>
      </tr>
    </tbody>
  </table>
</div>
<script th:if="${message}">
  let m = '[[${message}]]'; // 리다이렉트로 넘어온 메시지 저장
  alert(m); // 화면에 메시지 띄우기
</script>

테스트

http://localhost:8080/greetboard/list


가입인사 - 작성하기

service, mapper

- GreetBoardService.interface -

public void write(GreetBoardVO gBoardVO);	// 게시글 작성

- GreetboardMapper.interface -

public void write(GreetBoardVO gBoardVO);	// 가입인사 작성

- GreetboardMapper.xml -

<select id="write" resultType="GreetBoardVO">
	INSERT INTO greetboard(content, writer)
		VALUES (#{content}, #{writer})
</select>

- GreetBoardServiceImpl -

@Override
public void write(GreetBoardVO gBoardVO) {
	// 가입인사 작성
	gBoardMapper.write(gBoardVO);
}

controller

@GetMapping("list")
public String greetListGET(Model model) {
	model.addAttribute("greetboard", gBoardService.getGreetBoardList());
	model.addAttribute("board", new GreetBoardVO());
	return "greetBoard/greetList";
}

@PostMapping("/write")
public String greetWritePOST(GreetBoardVO board, RedirectAttributes attr) {
	gBoardService.write(board);
	return "redirect:/greetboard/list";
}

앞서 생성한 studyBoard와는 달리 greetBoard는 작성란과 리스트란이 모두 한 페이지에 있음.

@GetMapping("list")에서 model.addAttribute("board", new GreetBoardVO());로 새로운 GreetBoardVO객체를 만들어줘야 form태그로 객체를 사용할 수 있다.
만들어주지 않을 경우 th:object태그를 이용해 form작성이 불가능함.
=> 이 부분을 빼먹어서 타임리프 오류발생!

view

<h3>가입인사</h3>
<div>
  <p><a th:href="@{/}">메인페이지</a></p>
</div>
<div>
  <form th:action="@{/greetboard/write}" method="post" th:object="${board}">
    <div>
      <label for="content">내용</label>
      <textarea th:field="*{content}" required></textarea>
    </div>
    <div>
      <label for="writer">작성자</label>
      <input type="text" th:field="*{writer}" required />
    </div>
    <button type="submit">가입인사 남기기</button>
  </form>
</div>
<div>
  <table>
    <thead>
      <tr>
        <th>NO</th>
        <th>등록일</th>
        <th>작성자</th>
        <th>내용</th>
        <th></th>
      </tr>
    </thead>
    <tbody>
      <tr th:each="board : ${greetboard}">
        <td><span th:text="${board.gno}"></span></td>
        <td><span th:text="${#temporals.format(board.regdate , 'yyyy-MM-dd a hh:mm:ss')}"></span></td>
        <td><span th:text="${board.writer}"></span></td>
        <td><span th:text="${board.content}"></span></td>
        <td><a th:href="@{/greetboard/delete(gno=${board.gno})}">삭제하기</a></td>
      </tr>
    </tbody>
  </table>
</div>
<script th:if="${message}">
  let m = '[[${message}]]'; // 리다이렉트로 넘어온 메시지 저장
  alert(m); // 화면에 메시지 띄우기
</script>

테스트

http://localhost:8080/greetboard/list



참고링크

스프링 빈과 의존관계
스프링으로 추천기능 만들기
스프링 게시판 조회수

profile
천 리 길도 가나다라부터

0개의 댓글