1. 팀 프로젝트 홈페이지: 호텔 델루나 홈페이지
2. 호텔로 선정한 이유:
코로나19로 인한 침체된 관광 산업에 속한 분야 중 하나인데, 시간이 지나면 회복될 것을 생각하여 전보다 호텔을 찾는 경우가 늘어날 것을 예상했습니다.
그래서 앞으로는 더 이러한 산업군이 활발해지지 않을까 하여 정하게 되었습니다.
3. 기술 스택:
java jre 1.8버전, tomcat 8.5, myBatis 3.4, spring 5.0.2, Mysql-jdbc 8.0.24, junit mockito-junit-jupiter 3.8.0
4. 담당: Q&A, 로그인, FAQ, 이벤트 기능을 담당
5. ERD로 설계하기
6. starUML로 설계하기
1) 로그인 UML
1-1) 로그인 요소 간 uml
1-2) 프로세스 uml
2) Q&A UML
2-1) 요소 간 uml
2-2) 프로세스 uml
로그인
1. 비밀번호 찾기 때, 이메일 라이브러리를 적용하였습니다.
1) pom.xml에 이메일 library
<!-- mail전송을 위한 API : javax.mail, spring-context-support 2개 -->
<!-- https://mvnrepository.com/artifact/javax.mail/javax.mail-api -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.1</version>
</dependency>
2. service 항목으로 email 관련 service 넣기
1. EmailToType은 VO를 나타냄
2. EmailMessageUtil은 이메일에 넣을 내용 나타내기
3. EmailCodeUtil은 이메일 코드를 작성 및 가져오기 할 수 있는 getter & setter를 나타냄
2-1) VO
public class EmailToType {
String email;
String type;
String id;
String pw;
String name;
이 아래는 각각의 getter 와 setter를 나타냄
}
2-2) 이메일에 넣을 내용 나타내기(html로 작성함)
public class EmailMessgeUtil {
public static EmailHash emailHash;
public static String getFindPwEmailMessage(int authNo) {
emailHash = new EmailHash();
return emailHash.getFindPwMessage(authNo);
}
}
여기서 authNo는 곧, 이메일로 넘길 인증번호를 나타냄(authNo는 이메일을 수행하는 비즈니스 파트에서 구현한다)
class EmailHash{
String html;
String getFindPwMessage(int authNo) {
html = "<html lang=\"ko\">";
html += "<head><title>Emogrifier Example</title></head>";
html += "<body>";
html += "<div class=\"wrap\" style=\"margin: 0 auto;width: 900px;background-color: #fff;border: 1px solid #c4c4c4;box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3);font-size: 16px;line-height: 22px;color: #333;\">";
html += "<div class=\"inner_wrap\" style=\"padding: 30px;\">";
html += "<div class=\"box\">";
html += "<h1 class=\"title\" style=\"padding-bottom: 20px;\">HOTEL DEULLA</h1>";
html += "<p class=\"desc\" style=\"margin: 40px 0;\">";
html += "호텔 델루나 로그인을 위한 인증번호입니다.<br> 아래 인증번호로 재 로그인 하세요";
html += "</p>";
html += "</div>";
html += "<div class=\"box2\" style=\"border-bottom: 1px solid #c4c4c4;height: 150px;\">";
html += "<h2 class=\"title\" style=\"padding-bottom: 20px;\">인증코드</h2>";
html += "<p class=\"desc\" style=\"margin: 40px 0;\">"+authNo+"</p>";
html += "</div>";
html += "<p class=\"small\" style=\"font-size: 12px;color: rgba(0, 0, 0, .6);\">";
html += "본 메일은 발신전용입니다.<br> Copyright 2021 HOTEL DELLUNA All Rights Reserved";
html += "</div></div></body></html>";
return html;
}
}
2-3) EmailCodeUtil
public class EmailCodeUtil {
String code;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
** 컨트롤러에서 사용 법*business result 층에서 동작 수행 및 컨트롤러에서는 구현
@ResponseBody
@RequestMapping(value="/member/mailcode")
public String sendCodeMail(@RequestBody EmailToType toType, HttpSession session) {
BusinessResult br = joinService.sendCodeMail(toType, session);
if(br.getCode() !=RESULTCODE.정상) {}//비정상처리
return (String)br.getValue();
}
현재는 joinService에서 회원 가입 시에도 적용시켜야 했기 때문에 한 군데에서 수행함
아래는 BusinessResult의 모습을 나타낸 것
package com.delluna.hotels.util;
public class BusinessResult {
RESULTCODE code = RESULTCODE.정상;
String message = null;//던져줄 메시지
Object value = null;//DB에서 요청오는 모든 것들은 여기에 담기.
public BusinessResult() {//값은 없는 성공
code=RESULTCODE.정상;
message=null;
value=null;
}
public BusinessResult(Object value) {//값이 있는 성공
code=RESULTCODE.정상;
message=null;
this.value=value;
}
public BusinessResult(RESULTCODE code, String message) {//code와 메시지 직접 지정(주로 실패시)
this.code=code;
this.message=message;
}
public RESULTCODE getCode() {
return code;
}
public void setCode(RESULTCODE code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
이는, 실무에서 어떠한 처리가 오갈지 모르기 때문에 비즈니스 계층을 따로 만들어 값을 처리하거나, 해당 코드에 대한 메세지를 표현하기 위해 나타낸 것
아래는, joinservice에서 실현한 email 코드
//회원이메일
@Override
public BusinessResult sendCodeMail(EmailToType toType, HttpSession session) {
try {
MimeMessage mailMessage = javaMailSenderImpl.createMimeMessage();
MimeMessageHelper message;
message = new MimeMessageHelper(mailMessage,true,"utf-8");
message.setTo(toType.getEmail());
String html = null;
//회원가입 - 인증코드 보내기 (AJAX)
if(toType.getType().equals("verify")) {
//중복아닐 시 메일 보내기
int authNo = (int)(Math.random() * (99999 - 10000 + 1)) + 10000;
System.out.println(String.valueOf(authNo));
//이메일중복확인
boolean isinEmail = false;
try {
isinEmail = memberDAO.isinEmail(toType.getEmail());
} catch (Exception e) {
if(e instanceof ConnectException) {
return new BusinessResult(RESULTCODE.NETWORK_ERROR,"네트워크 통신이 원활하지 않습니다.");
}
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
if(isinEmail) {
return new BusinessResult("No");
}
message.setFrom("dojh555@gmail.com");
message.setSubject("HOTEL DELLUNA 회원가입 인증 메일입니다.");
session.setAttribute("authNum", authNo);
session.setMaxInactiveInterval(60*20);
html = EmailMessgeUtil.getVerifyEmailMessge(authNo);
}else if(toType.getType().equals("findPw")) {
int authNo = (int)(Math.random() * (99999 - 10000 + 1)) + 10000;
//비교를 해야해 (type으로 들어온 데이터를 비교할 것)
String id = toType.getId();
//비교를 할 id를 불러와야 해(memberDAO의 데이터를 가져올 것)
String name = toType.getName();
System.out.println(toType.getName());
String email = toType.getEmail();
if(!memberDAO.gomail(id, name, email,"가입","가입",1))
{
return new BusinessResult("No");
}
message.setFrom("yeong973@gmail.com");
message.setSubject("HOTEL DELLUNA 비밀번호 찾기 메일입니다.");
html = EmailMessgeUtil.getFindPwEmailMessage(authNo);
Member member = new Member();
String i = Integer.toString(authNo);
session.setAttribute("id", id);
session.setAttribute("pw", i);
}else if(toType.getType().equals("findAdmPw")) {//관리자 페이지 패스워드 찾기
int authNo = (int)(Math.random() * (99999 - 10000 + 1)) + 10000;
String pw = Integer.toString(authNo);
String id = toType.getId();
String name = toType.getName();
String email = toType.getEmail();
if(!memberDAO.gomail(id, name, email,"대기","가입",8)) {
return new BusinessResult("No");
}
message.setFrom("dojh555@gmail.com");
message.setSubject("HOTEL DELLUNA 비밀번호 찾기 메일입니다.");
//메일 메시지
html = EmailMessgeUtil.getFindPwEmailMessage(authNo);
//패스워드 바꾸기
memberDAO.uppw(id, pw);
}
message.setText(html, true);
// 메일발송
javaMailSenderImpl.send(mailMessage);
System.out.println("success to send email");
}catch (MessagingException e) {
e.printStackTrace();
}
return new BusinessResult("Yes");
}
mimemessageHelper를 이용한 메세지 전송
1. 파일 인코딩 형식
2. 받는 이, 보내는 이 주소
3. 보내는 이메일 제목, 내용(html)
* 이메일로 인증번호를 받음(이는 곧 새로운 비밀번호로 업데이트 됨)
Q&A
1. 사용자 파트: 질문 등록하기
2. 관리자 파트:
1-1) 체크리스트 동작 화면
* 위의 사진은, 현재 클라이언트 단에서 질문을 등록한게 올라온 것임
1-2) 체크리스트 구현 코드
<script>
var clck = [];
function getCheckboxValue(event)
{
$('#chk').attr("readonly",false);
console.log(event);
console.log(event.target.value);
if(event.target.checked)
{clck.push(event.target.value);}
else{
clck=clck.filter(v=>v!==event.target.value);}
}
function submitHandler(event){
const fd = new FormData();
fd.append("idlist",clck);
event.preventDefault(); //event를 눌렀을 때 submit하면서 reload가 되면서! 이벤트를 막는다.(on submit 시에만!!!!) url에 ?를 막아주는 거다!!(강제로)
if(clck=="")
{
alert("질문을 선택해주십시오");
$('#chk').attr("readonly",false);
}
else{
$.ajax({
url : "/adm/adm-qna-delete"
, type : "post"
// , data : JSON.stringify({idlist:clck})
, data : fd
, processData: false
, contentType: false
, success : function(result) {
console.log(result.data);
window.location.reload(true);
alert("삭제되었습니다.");
}
, error : function(result){
console.log(result);
}
});
}
}
function bHandler(event){
var rdata=0;
if(clck=="")
{
alert("질문을 선택해주십시오");
$('#chk').attr("readonly",false);
}
else{
if(clck.length>1)
{
alert("하나만 선택하십시오");
$("input[type=checkbox]").prop("checked", false);
clck.length=0;
}
else{
//버튼의 상태를 가져오기(답변관련상태)
$.ajax({
url:"/adm/adm-qna-state",
type:"post",
data: JSON.stringify({'clck':clck}),
contentType:"application/json; charset=utf-8",
dataType:"json"
,success:function(result){
if(result.rno==-1){
location.href="/adm/adm-rqna-write/"+result.bno;
}
else{//답변완료다
location.href="/adm/adm-rqna-edit/"+result.bno;
}
}
});
}
}
}
function selectAll(event) {
const checkboxes
= document.getElementsByName('chk');
checkboxes.forEach((checkbox) => {
checkbox.checked = selectAll.checked;
})
clck.push(selectAll.target.value);
console.log(clck);
}
</script>
<form onsubmit="return submitHandler(event)" method="post" >
<table>
<thead>
<tr class="headline">
<td></td>
<td>번호</td>
<td>이름</td>
<td>질문 유형</td>
<td>제목</td>
<td>날짜</td>
<td>답변상태</td>
</tr>
</thead><br>
<tbody>
<c:forEach var="qna" items="${qlist}" varStatus="status">
<tr>
<th scope="col" class="chk_all_box1">
<input type="checkbox" id="chk" name="chk" value="${qna.no}" onclick="getCheckboxValue(event)" readonly>
<td class="delbtn"><input type="submit" id="del" value="질문삭제" ></td>
<td class="savebtn"><input type="button" value="답변등록" onclick="bHandler(event)">
</td>
</tr>
</table>
1. 먼저 체크리스트 된 값들을 읽어서 각각의 값을 추출한다
2. form에서 script의 이벤트 중, 'submitHandler'를 동작시킴
-> 체크된 데이터들이 clck라는 배열에 입력이 된 것을 ajax 통신으로 adm-qna-delete라는 곳으로 서버에 전송
-> 그리고 수행이 되었으면(success면,) 해당 글은 삭제가 되는 것.
* 배열로 체크된 값이 들어간 것이라서 체크된 것들이 선택되어 삭제 및 선택되는 것이다.
※ 삭제를 실제로 구현한 코드
//질문 삭제
@RequestMapping(value="/adm/adm-qna-delete", method=RequestMethod.POST)
public String deleteqlist(@RequestParam("idlist") List<String> idlist) {
for(int i=0;i<idlist.size();i++)
{
Qna qna = qnaDAO.findByNo(Integer.parseInt(idlist.get(i)));
qnaDAO.delete(qna.getNo());
qnaDAO.deleteR(qna.getNo());
qnaDAO.udState("답변대기","답변대기",Integer.parseInt(idlist.get(i)));
System.out.println(Integer.parseInt(idlist.get(i)));
}
return "redirect:/adm/adm-qna-list";
}
-> 여기서 어려운 점이 있었다면, 현재 배열된 값을 읽어야 하는 것이고, 해당 리스트 값은 문자열 형태로 되어있어서 값으로 넘어온(JSON형태의 값) 값을 @RequestParam으로 읽는 것이다.
그리고 직접 하나씩 읽어서 삭제하는 쿼리를 실행시킴.
2) 글자수 실시간 조회
이는, fn이라는 taglib의 prefix로 형식을 쓴다. 그리고 실제 값의 길이를 나타낸다.
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<script type="text/javascript">
$(document).ready(function(){
$('#content').on('keyup',function(){
$('#result').html("("+$(this).val().length+"/200)");
if($(this).val().length>200){
$(this).val($(this).val().substring(0,200));
$('#result').html("(200/200)");
}
});
});
</script>
<div id="result">(<c:out value="${fn:length(response)}"/>/200)</div>
즉각적으로 실행이 될 수 있도록 script의 제일 첫번째에 등록한다.
그리고 키보드의 이벤트 중, keyup이라는 이벤트를 통해 키보드 동작이 on이 될 때마다, 해당 길이를 나타내는 로직을 작성하였다.
3) 질문 등록부터 답변 등록, 수정, 삭제까지 MySQL을 이용한 쿼리를 작성하여 구현하였다.
FAQ
* w3school의 html에서 아코디언 파트를 적용하여 구현하였습니다.
7. 응용(안드로이드에 적용)
안드로이드 스튜디오에서 해당 url을 입력하여 안드로이드 앱으로 나타내는 실습을 진행함
@Override
public void onClick(View v)
{
Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse("localhost:192.168.0.76:9000/index"));
startActivity(intent);
}
해당 주소를 괄호 안에 넣어서 실행시킴.