today_is

[ spring ] 설문 및 투표 + 설문결과보기 기능 구현 본문

spring

[ spring ] 설문 및 투표 + 설문결과보기 기능 구현

ye_rang 2024. 1. 9. 09:27

오늘의 목표

 

투표 기능을 추가한 설문지를 만들어보자


SurveyDTO

package com.itbank.model;

import org.springframework.web.multipart.MultipartFile;

// 	TABLE : SURVEY
//	IDX     NOT NULL NUMBER        
//	TITLE   NOT NULL VARCHAR2(500) 
//	WRITER           VARCHAR2(500) 
//	OPTION1 NOT NULL VARCHAR2(500) 
//	OPTION2 NOT NULL VARCHAR2(500) 
//	IMAGE1  NOT NULL VARCHAR2(500) 
//	IMAGE2  NOT NULL VARCHAR2(500) 

public class SurveyDTO {

	private int idx;
	private String title;
	private String writer;
	private String option1;
	private String option2;
	private String image1;
	private String image2;
	
	private MultipartFile upload1;
	private MultipartFile upload2;
	
	private int responseCount;	// 설문에 참여한 인원의 수
	private int choice1Count;
	private int choice2Count;
	private double choice1Rate;
	private double choice2Rate;
	
	public double getChoice1Rate() {
		return choice1Rate;
	}
	public void setChoice1Rate(double choice1Rate) {
		this.choice1Rate = choice1Rate;
	}
	public double getChoice2Rate() {
		return choice2Rate;
	}
	public void setChoice2Rate(double choice2Rate) {
		this.choice2Rate = choice2Rate;
	}
	public int getChoice1Count() {
		return choice1Count;
	}
	public void setChoice1Count(int choice1Count) {
		this.choice1Count = choice1Count;
	}
	public int getChoice2Count() {
		return choice2Count;
	}
	public void setChoice2Count(int choice2Count) {
		this.choice2Count = choice2Count;
	}
	public int getIdx() {
		return idx;
	}
	public void setIdx(int idx) {
		this.idx = idx;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getWriter() {
		return writer;
	}
	public void setWriter(String writer) {
		this.writer = writer;
	}
	public String getOption1() {
		return option1;
	}
	public void setOption1(String option1) {
		this.option1 = option1;
	}
	public String getOption2() {
		return option2;
	}
	public void setOption2(String option2) {
		this.option2 = option2;
	}
	public String getImage1() {
		return image1;
	}
	public void setImage1(String image1) {
		this.image1 = image1;
	}
	public String getImage2() {
		return image2;
	}
	public void setImage2(String image2) {
		this.image2 = image2;
	}
	public MultipartFile getUpload1() {
		return upload1;
	}
	public void setUpload1(MultipartFile upload1) {
		this.upload1 = upload1;
	}
	public MultipartFile getUpload2() {
		return upload2;
	}
	public void setUpload2(MultipartFile upload2) {
		this.upload2 = upload2;
	}
	public int getResponseCount() {
		return responseCount;
	}
	public void setResponseCount(int responseCount) {
		this.responseCount = responseCount;
	}

}

 

 

 

SurveyController

 

@RequestMapping (@GetMapping, @PostMapping)
: 특정 주소, 특정 메서드로 요청을 받으면 자동으로 실행되는 이벤트 함수의 성격을 가진다 

 

 

@ExceptionHandler
: 예외가 발생하면 @ExceptionHandler 어노테이션이 붙은 함수가 자동으로 실행된다

이때, 발생하는 예외의 타입에 따라 서로 다른 함수를 실행할 수 있다
컨트롤러 내부에서도  ExceptionHandler 를 작성할 수 있지만
컨트롤러는 본래, 요청에 따른 처리를 작성하는 클래스이므로,
별도의 클래스를 만들어서 예외만 전문적으로 처리하는 스프링 빈을 작성할 수도 있다

 

package com.itbank.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.itbank.model.ChoiceDTO;
import com.itbank.model.SurveyDTO;
import com.itbank.service.SurveyService;

@Controller
@RequestMapping("/survey")
public class SurveyController {
	
	@Autowired private SurveyService service;
	
	@ExceptionHandler(DuplicateKeyException.class)
	public ModelAndView dupKey() {
		ModelAndView mav = new ModelAndView("alert");
		mav.addObject("msg", "이미 참여한 투표입니다");
		return mav;
	}

	@GetMapping("/add")
	public void add() {}
	
	@PostMapping("/add")
	public String add(SurveyDTO dto) {
		int row = service.add(dto);
		System.out.println(row != 0 ? "등록 성공" : "등록 실패");
		return "redirect:/";
	}
	
	@GetMapping("/list")
	public ModelAndView list() {
		ModelAndView mav = new ModelAndView();
		List<SurveyDTO> list = service.getList();
		mav.addObject("list", list);
		return mav;
	}
	
	@GetMapping("/vote/{idx}")
	public ModelAndView view(@PathVariable("idx") int idx) {
		ModelAndView mav = new ModelAndView("/survey/vote");
		SurveyDTO dto = service.getSurvey(idx);
		mav.addObject("dto", dto);
		return mav;
	}
	
	@PostMapping("/vote/{idx}")
	public String view(ChoiceDTO dto) {
		int row = service.addChoice(dto);
		System.out.println(row != 0 ? "응답 성공" : "응답 실패");
		return "redirect:/survey/result/{idx}";
	}
	
	@GetMapping("/result/{idx}")
	public ModelAndView result(@PathVariable("idx") int idx) {
		ModelAndView mav = new ModelAndView("/survey/result");
		SurveyDTO dto = service.getResult(idx);
		mav.addObject("dto", dto);
		return mav;
	}
	
}

 

 

ServeyService

package com.itbank.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.itbank.component.FileComponent;
import com.itbank.model.ChoiceDTO;
import com.itbank.model.SurveyDTO;
import com.itbank.repository.SurveyDAO;

@Service
public class SurveyService {
	
	@Autowired private SurveyDAO dao;
	@Autowired private FileComponent fileComponent;

	public int add(SurveyDTO dto) {
		String image1 = fileComponent.upload(dto.getUpload1());
		String image2 = fileComponent.upload(dto.getUpload2());
		dto.setImage1(image1);
		dto.setImage2(image2);
		return dao.insert(dto);
	}

	public List<SurveyDTO> getList() {
		return dao.selectList();
	}

	public SurveyDTO getSurvey(int idx) {
		return dao.selectOne(idx);
	}

	public int addChoice(ChoiceDTO dto) {
		return dao.insertChoice(dto);
	}

	public SurveyDTO getResult(int idx) {
		return dao.selectResult(idx);
	}

}

 

 

 

SurveyDAO

package com.itbank.repository;

import java.util.List;

import com.itbank.model.ChoiceDTO;
import com.itbank.model.SurveyDTO;

public interface SurveyDAO {

	int insert(SurveyDTO dto);

	List<SurveyDTO> selectList();

	SurveyDTO selectOne(int idx);

	int insertChoice(ChoiceDTO dto);

	SurveyDTO selectResult(int idx);

}

 

 

survey-mapper.xml

: surveyDAO의 상세 쿼리문

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC 
	"-//mybatis.org//DTD Mapper 3.0//EN" 
	"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
	
<mapper namespace="com.itbank.repository.SurveyDAO">
	
	<insert id="insert" parameterType="survey">
		insert into survey (writer, title, option1, option2, image1, image2)
			values (
				#{writer},
				#{title},
				#{option1},
				#{option2},
				#{image1},
				#{image2}
			)
	</insert>
	
	<select id="selectList" resultType="survey">
		select 
		    survey.*,
		    (select count(*) from survey_choice where survey_idx = survey.idx) as responseCount
		        from survey order by survey.idx desc
	</select>
	
	<select id="selectOne" parameterType="int" resultType="survey">
		select * from survey
			where
				idx = #{idx}
	</select>
	
	<insert id="insertChoice" parameterType="choice">
		insert into survey_choice (writer, survey_idx, choice) 
			values (#{writer}, #{survey_idx}, #{choice})
	</insert>
	
	<select id="selectResult" parameterType="int" resultType="survey">
		select 
		    A.*, 
		    trunc(choice1Count * 100 / responseCount, 2) as choice1Rate, 
		    trunc(choice2Count * 100 / responseCount, 2) as choice2Rate
		    from (
		    select 
		    	S.*,
		        (select count(*) from survey_choice where survey_idx = #{idx}) as responseCount,
		        (select count(*) from survey_choice where survey_idx = #{idx} and choice = 1) as choice1Count,    
		        (select count(*) from survey_choice where survey_idx = #{idx} and choice = 2) as choice2Count
		            from survey S
		            where S.idx = #{idx}
		    ) A
	</select>
	
</mapper>

 

 

add.jsp

: 설문조사 추가 , FileComponent 를 이용하여 이미지 업로드도 가능하도록 했음

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="../header.jsp" %>
<style>
	input[type="text"] {
		all: unset;
		border-bottom: 1px solid #dadada;
		padding: 10px 0;
		margin: 10px auto;
	}
	input[name="title"] {
		font-size: 24px;	
		width: 500px;
	}
	div.flex {
		display: flex;
		justify-content: space-around;
		width: 800px;
		margin: 20px auto;
	}
	form div.flex > div {
		box-shadow: 5px 5px 5px grey;
		background-color: #eee;
		padding: 20px;
	}
	form > *:not(.flex) {
		display: flex;
		justify-content: center;
	} 
	
</style>
<h3>추가</h3>

<form method="POST" enctype="multipart/form-data">
	<div><input type="text" name="title" placeholder="설문 제목" required autofocus></div>
	<div class="flex">
		<div>
			<div><input type="file" name="upload1" required></div>
			<div><input type="text" name="option1" placeholder="문항1" required></div>
		</div>
		<div>
			<div><input type="file" name="upload2" required></div>
			<div><input type="text" name="option2" placeholder="문항2" required></div>
		</div>
	</div>
	<input type="hidden" name="writer" value="${login.userid }" readonly>
	<p><input type="submit"></p>
</form>

</body>
</html>

 

 

 

list.jsp

: 설문 목록 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="../header.jsp" %>

<style>
	table.surveyList {
		border: 1px solid black;
		border-collapse: collapse;
		width: 800px;
		margin: 20px auto;
	}
	td, th {
		padding: 5px 10px;
	}
	tr {
		border-bottom: 1px solid grey;
	}
	table tr:first-child {
		background-color: #dadada;
	}
	.sb {
		display: flex;
		justify-content: space-between;
	}
</style>

<h3>설문 목록</h3>

<table class="surveyList">
	<tr>
		<th>번호</th>
		<th>제목</th>
		<th>작성자</th>
		<th>참여인원</th>
	</tr>
	<c:forEach var="dto" items="${list }">
	<tr>
		<td>${dto.idx }</td>
		<td>
			<div class="sb">
				<div>${dto.title }</div>
				<div>
					<a href="${cpath }/survey/vote/${dto.idx}"><button>설문참여</button></a>
					<a href="${cpath }/survey/result/${dto.idx}"><button>결과보기</button></a>
				</div>
			</div>
		</td>
		<td>${dto.writer }</td>
		<td>${dto.responseCount }</td>
	</tr>
	</c:forEach>
</table>

</body>
</html>

 

 

 

 

vote.jsp

: 설문 참여

 

input radio 와 value 를 이용하여, choice 값을 선택할 수 있도록 한다.

 

설문 참여를 하면, 결과값이 insert 되어야한다

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="../header.jsp" %>

<style>
	div.flex {
		display: flex;
		justify-content: space-around;
		width: 800px;
		margin: 20px auto;
	}
	form div.flex > div {
		box-shadow: 5px 5px 5px grey;
		background-color: #eee;
		padding: 20px;
		border: 3px solid transparent; 
	}
	form > *:not(.flex) {
		display: flex;
		justify-content: center;
	} 
	form div.flex > div.selected {
		border: 3px solid lime; 
	}
	input[type="radio"] {
		display: none;
	}
</style>

<h3>${dto.title }</h3>

<form method="POST">
	<input type="hidden" name="writer" value="${login.userid }">
	<input type="hidden" name="survey_idx" value="${dto.idx }">
	<div class="flex">
		<div>
			<h4>${dto.option1 }</h4>
			<label>
				<img src="${cpath }/upload/${dto.image1}" height="200">
				<input type="radio" name="choice" value="1">
			</label>
		</div>
		<div>
			<h4>${dto.option2 }</h4>
			<label>
				<img src="${cpath }/upload/${dto.image2}" height="200">
				<input type="radio" name="choice" value="2">
			</label>
		</div>
	</div>
	<p align="center"><input type="submit"></p>
</form>

<script>
	const itemList = document.querySelectorAll('.flex > div')

	itemList.forEach(item => item.onclick = function() {
		itemList.forEach(e => e.classList.remove('selected'))
		item.classList.add('selected')
	})
</script>

</body>
</html>

 

 

 

result.jsp

: 설문 결과

 

 

[  choiceCount 구하는 쿼리문 ]

->   select count(*) from survey_choice where survey_idx = #{idx} and choice = 1) as choice1Count

 

 

[ choiceRate 구하는 쿼리문 ]

->   trunc(choice1Count * 100 / responseCount, 2) as choice1Rate

 

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="../header.jsp" %>

<style>
	div.flex {
		display: flex;
		justify-content: space-around;
		width: 800px;
		margin: 20px auto;
	}
</style>

<h3>${dto.title }</h3>

<div class="flex">
		<div>
			<h4>${dto.option1 }</h4>
			<img src="${cpath }/upload/${dto.image1}" height="200">
			<div>득표 수 : ${dto.choice1Count } (${dto.choice1Rate }%)</div>
		</div>
		<div>
			<h4>${dto.option2 }</h4>
			<img src="${cpath }/upload/${dto.image2}" height="200">
			<div>득표 수 : ${dto.choice2Count } (${dto.choice2Rate }%)</div>
		</div>
	</div>

</body>
</html>

 

 


study_review

 

어려웠던점

 

1) ExceptionHandler 사용

: 생소한 어노테이션이라서 이해하는데에 시간이 좀 걸렸다.

특히나, 별다른 선언없이 DuplicateKeyException 을 어떻게 사용할 수 있는지 궁금하였다.

처음엔 단순히 스프링 내장 exception 인줄 알았으나

검색을 통해 알아보니, jdbcTemplate 덕분에 사용할 수 있는 것이라고 한다. 

다음번에는 예외처리만을 다루는 Exception 을 직접 만들어봐야겠다

 

2) 비율 구하는 쿼리문 작성

비율 구하는 공식 그 자체는 복잡하진 않지만, 쿼리문이 길어지다보니까 헷갈렸었다.

처음부터 sqlDeveloper에 쿼리문을 select 로 작성하면서

결과물을 계속 확인 해보면서 작성했더니 그나마 빨리 끝낼 수 있었다

그러나, 이런 식의 쿼리문은 가독성은 별로 좋지 않은 것 같다 

 

'spring' 카테고리의 다른 글

[ spring ] 간단한 POS 구현  (0) 2024.01.19
[ spring ] RestController  (0) 2024.01.16
[ spring ] FileComponent , 다중 파일업로드  (0) 2024.01.04
[ spring ] 어노테이션 문법정리  (0) 2024.01.02
[ spring ] Interceptor  (0) 2024.01.01