코딩 76 일 / 2022.10.12 / Spring MVC 회원관리 프로그램, Spring MVC 댓글 게시판 프로그램
복습
파일명 중복문제 해결
- fileupload 라이브러리 사용시 중복문제를 자동 해결해주지 않음
- UUID (java.util) 클래스는 통해 문자형태의 난수값을 만들어 준다
- 이 난수값을 String 형으로 변환하고 확장자를 결합해서 중복문제 해결
- 이 방법 외에도 다른 방법들도 있다
- 난수를 발생시키는 방법을 정리한 예제 RandomFile.java
import java.util.UUID;
public class RandomFile {
public static void main(String[] args) {
// TODO Auto-generated method stub
String filename = "clock.jpg";
String extension = filename.substring(filename.lastIndexOf("."), filename.length());
System.out.println("extension:"+extension);
UUID uuid = UUID.randomUUID();
System.out.println("uuid:"+uuid);
String newfilename = uuid.toString() + extension;
System.out.println("newfilename:"+newfilename);
}
}
1. clock.jpg 라는 첨부파일을 저장한다고 하면, 확장자를 subsgring() 을 사용해서 분리한 후 extension 에 저장
2. UUID 클래스로 난수를 발생시킨다
3. 그 난수를 String 형으로 변환시킨 후 확장자를 붙이면 파일명을 문자형태 난수로 만들 수 있다
- 난수가 중복될 확률은 현저히 낮다
첨부파일명 컬럼 크기 주의
- join_profile 컬럼에 난수화된 파일명이 들어가 있다
- 문자형 난수값이 들어가기때문에 첨부파일명 저장되는 컬럼 크기를 50Byte 이상으로 해두기
첨부파일 입력양식 name 값 주의
- member_join.jsp 부분
<tr>
<th>프로필사진</th>
<td>
<input type="file" name="join_profile1" />
</td>
</tr>
- DTO 에서 첨부파일명이 저장되는 컬럼과 폼에서 첨부파일 입력양식의 name 을 같은 이름으로 써선 안된다!
- DTO 에 저장하는건 첨부파일"명"이고 첨부파일 입력양식에서 입력하는건 파일이다
- 그 join_profile1 은 여기 @RequestParam("") 에 들어가는 값이다
- 그리고 의도한대로 DB 에 저장할 파일명 프로퍼티명과 폼의 name 이 일치하지 않아으므로 DTO 객체에 자동으로 매핑되어 저장되지 않게 됨
- 이후 따로 중복문제를 처리한 파일명을 구해서 Setter 메소드로 세팅해야한다
- newfile 은 중복문제 해결 후의 파일명
INSERT SQL 문 작성시 주의
<!-- 회원저장 -->
<insert id="member_join" parameterType="member">
insert into join_member (join_code,join_id,join_pwd,join_name,
join_zip1,join_addr1,join_addr2,join_tel,join_phone,join_email,join_profile,
join_regdate,join_state) values(join_member_joincode_seq.nextval,
#{join_id},#{join_pwd},#{join_name},
#{join_zip1},#{join_addr1},#{join_addr2},#{join_tel},
#{join_phone},#{join_email},#{join_profile, jdbcType=VARCHAR},sysdate,1)
</insert>
출처: https://laker99.tistory.com/149?category=1087168 [레이커 갓생일기:티스토리]
- 사용자가 첨부파일을 선택할 수도 있고 선택하지 않을 수도 있다
- MyBatis 는 null 값을 허용하지 않기때문에 null 값이 들어가면 오류가 생김
- jdbcType=VARCHAR 속성을 넣어서 null 값을 허용시켜야 한다
Controller 클래스에서 바로 다시 요청하기
return "redirect:member_login.do";
- Controller 클래스에서 위 문장을 작성하면, 다시 "member_login.do" 로 요청하며 로그인 폼으로 가게 됨
- 이전까지는 Controller 클래스에서 View 페이지로만 이동했다
- 만약 return 하며 다시 요청을 할때는 "redirect:" 를 붙여야한다
회원 관리 프로그램 (이어서)
id 중복검사
- member.jsp 부분
//아이디 중복확인
$.ajax({
type:"POST",
url:"member_idcheck.do",
data: {"memid":memid},
success: function (data) {
alert("return success="+data);
if(data==1){ //중복 ID
var newtext='<font color="red">중복 아이디입니다.</font>';
$("#idcheck").text('');
$("#idcheck").show();
$("#idcheck").append(newtext);
$("#join_id").val('').focus();
return false;
}else{ //사용 가능한 ID
var newtext='<font color="blue">사용가능한 아이디입니다.</font>';
$("#idcheck").text('');
$("#idcheck").show();
$("#idcheck").append(newtext);
$("#join_pwd1").focus();
}
}
,
error:function(e){
alert("data error"+e);
}
});//$.ajax
- ajax() 메소드를 사용했고 url 로 "member_idcheck.do" 로 요청한다, 전달할 데이터는 json 형태로 전달
- 여기서 전달한 값은 받는 쪽에서 request.getParameter() 또는 @RequestParam("") 으로 받음
- 콜백함수로 1 또는 -1 을 돌려받도록 했다, 그것에 맞춰서 처리를 함
- Controller 클래스 MemberAction.java 에서 "member_idcheck.do" 요청 처리 부분만
// ID중복검사 ajax함수로 처리부분
@RequestMapping(value = "/member_idcheck.do", method = RequestMethod.POST)
public String member_idcheck(@RequestParam("memid") String id, Model model) throws Exception {
System.out.println("id:"+id);
int result = memberService.checkMemberId(id);
model.addAttribute("result", result);
return "member/idcheckResult";
}
- 요청을 받아서 @RequestParam("memid") 로 전달된 값 memid 를 받아온다
- 이 memid 는 사용자가 입력했던 아이디이다
- checkMemberId() 메소드를 호출하며 중복검사를 한다 * 아래에 설명
- 그 결과 1 또는 -1 을 result 변수로 받고 그걸 Model 객체에 저장 후 jsp/idcheckResult.jsp 로 이동
- Service 클래스 MemberServiceImpl.java 에서 checkMemberId() 메소드 부분만
public int checkMemberId(String id) throws Exception{
return memberDao.checkMemberId(id);
}
- DAO 클래스 MemberDaoImpl.java 에서 checkMemberId() 메소드 부분만
/***** 아이디 중복 체크 *****/
// @Transactional
public int checkMemberId(String id) throws Exception {
int re = -1; // 사용 가능한 ID
MemberBean mb = sqlSession.selectOne("login_check", id);
if (mb != null)
re = 1; // 중복id
return re;
}
- 검색된 결과가 있으면 중복 id 이므로 -1 이 리턴되고, 검색된 결과가 없으면 중복이 아니므로 1 을 리턴한다
- Mapper 파일 member.xml 에서 id 가 "login_check" 인 SQL문 부분만
<!-- 로그인 인증 -->
<select id="login_check" parameterType="String" resultType="member">
select * from join_member where join_id=#{id} and join_state=1
</select>
- 이제는 검색할때 where 조건문에 join_state = 1 이라는 조건을 추가해서 현재 가입된 회원중에서만 검색해야한다
- 해당 id 의 회원 상세정보를 DTO 객체에 저장해서 돌려준다
- 해당 SQL문은 id 중복검사시에도 사용하고 로그인 시에도 사용함
- 다시 돌아가며 DAO -> Service -> Controller -> View 페이지로 넘어왔을때
- idCheckResult.jsp
${result}
- 이 값이 출력되므로 member.js 의 콜백함수로 반환되는 값임, 성공시 1 이 반환, 실패시 -1 이 반환됨
로그인 기능
- 가입이 끝나고 로그인 부분을 보자
return "redirect:member_login.do";
- 회원가입이 끝나면 Conroller 클래스에서 return redirect: 에 의해 "member_login.do" 로 요청한다
- Controller 클래스 MemberAction.java 에서 "member_login.do" 요청 부분만
/* 로그인 폼 뷰 */
@RequestMapping(value = "/member_login.do")
public String member_login() {
return "member/member_login";
// member 폴더의 member_login.jsp 뷰 페이지 실행
}
- 로그인 폼 member_login.jsp 로 가게 됨
- member_login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/admin.css" />
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/member.css" />
<!-- <script src="./js/jquery.js"></script> -->
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
function check(){
if($.trim($("#id").val())==""){
alert("로그인 아이디를 입력하세요!");
$("#id").val("").focus();
return false;
}
if($.trim($("#pwd").val())==""){
alert("비밀번호를 입력하세요!");
$("#pwd").val("").focus();
return false;
}
}
/*비번찾기 공지창*/
function pwd_find(){
window.open("pwd_find.do","비번찾기","width=450,height=500");
//자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성")
//메서드로 새로운 공지창을 만듬.폭이 400,높이가 400인 새로운 공지창을 만듬.단위는 픽셀
}
</script>
</head>
<body>
<div id="login_wrap">
<h2 class="login_title">로그인</h2>
<form method="post" action="member_login_ok.do" onsubmit="return check()">
<table id="login_t">
<tr>
<th>아이디</th>
<td>
<input name="id" id="id" size="20" class="input_box" />
</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input type="password" name="pwd" id="pwd" size="20" class="input_box"/>
</td>
</tr>
</table>
<div id="login_menu">
<input type="submit" value="로그인" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#id').focus();" />
<input type="button" value="회원가입" class="input_button"
onclick="location='member_join.do'" />
<input type="button" value="비번찾기" class="input_button"
onclick="pwd_find()" />
</div>
</form>
</div>
</body>
</html>
+ submit 버튼은 하나여야 한다
- 유효성 검사, 비밀번호 찾기 팝업창을 띄워주는 함수 정의, 입력양식 으로 구성되어 있음
- 아이디, 비밀번호를 입력 후 "로그인" 버튼 클릭시 "member_login_ok.do" 로 요청, 그 요청에서 아이디, 비번이 일치하는지 DB에서 확인한다
- Controller 클래스 MeberAction.java 에서 "member_login_ok.do" 요청 부분만
/* 로그인 인증 */
@RequestMapping(value = "/member_login_ok.do", method = RequestMethod.POST)
public String member_login_ok(@RequestParam("id") String id,
@RequestParam("pwd") String pwd,
HttpSession session,
Model model) throws Exception {
int result=0;
MemberBean m = memberService.userCheck(id);
if (m == null) { // 등록되지 않은 회원일때
result = 1;
model.addAttribute("result", result);
return "member/loginResult";
} else { // 등록된 회원일때
if (m.getJoin_pwd().equals(pwd)) {// 비번이 같을때
session.setAttribute("id", id);
String join_name = m.getJoin_name();
String join_profile = m.getJoin_profile();
model.addAttribute("join_name", join_name);
model.addAttribute("join_profile", join_profile);
return "member/main";
} else {// 비번이 다를때
result = 2;
model.addAttribute("result", result);
return "member/loginResult";
}
}
}
- 사용자가 입력한 아이디 , 비밀번호를 @RequestParam 으로 받음
- 회원인증 성공시 SESSION 으로 공유설정해야하므로 session 객체를 매개변수에 선언해서 받는다, 자동으로 생성됨
- Service 클래스의 userCheck() 메소드를 호출해서 회원 인증을 한다, 이때 id 를 전달
- id 만으로 DB에서 해당 id를 가진 데이터가 있는지 확인 후 그 데이터를 가져와서 객체 m 에 저장
- 데이터가 있는 경우에는 그 데이터에서 DB 비밀번호를 m.getJoin_pwd() 로 가져와서 사용자가 입력한 비밀번호가 맞는지 확인하고 있다
- 아이디에 해당하는 데이터가 존재하고, 비밀번호도 일치시 session 객체로 id 를 공유설정함
- 로그인이 되어있는 한 계속 SESSION 값 id 가 공유됨, 사용 가능
- id 가 틀렸을땐 변수 result 에 1을 저장, 비번 틀렸을떈 변수 result 에 2 를 저장
- 이후로그인 성공시 main.jsp 로 이동, 로그인 실패시 loginResult.jsp 로 이동해서 처리
- main.jsp 로 이동할때 사용자의 이름, 프로필 첨부파일명을 Model 객체에 저장해서 전달
- Service 클래스 MemberServiceImpl.java 에서 userCheck() 메소드 부분만
public MemberBean userCheck(String id) throws Exception{
return memberDao.userCheck(id);
}
- DAO 클래스 MemberDaoImpl.java 에서 userCheck() 메소드 부분만
/* 로그인 인증 체크 */
// @Transactional
public MemberBean userCheck(String id) throws Exception {
return sqlSession.selectOne("login_check", id);
}
- Mapper 파일 member.xml 에서 id 가 "login_check" 인 SQL문 부분만
<!-- 로그인 인증 -->
<select id="login_check" parameterType="String" resultType="member">
select * from join_member where join_id=#{id} and join_state=1
</select>
- id 중복체크 시에도 해당 SQL문을 사용했었음
- 로그인 실패시엔 loginResult.jsp 로 이동
- loginResult.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:if test="${result == 1}">
<script>
alert("등록되지 않는 회원 입니다.");
history.go(-1);
</script>
</c:if>
<c:if test="${result == 2}">
<script>
alert("회원정보가 틀렸습니다.");
history.go(-1);
</script>
</c:if>
- 로그인 성공시 일종의 마이페이지인 main.jsp 로 이동
- main.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>사용자 메인화면</title>
<link rel="stylesheet" type="text/css" href="./css/main.css" />
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
</head>
<body>
<c:if test="${sessionScope.id == null }">
<script>
alert("다시 로그인 해주세요!");
location.href="<%=request.getContextPath()%>/member_login.do";
</script>
</c:if>
<c:if test="${sessionScope.id != null }">
<div id="main_wrap">
<h2 class="main_title">사용자 메인화면</h2>
<form method="post" action="member_logout.do">
<table id="main_t">
<tr>
<th colspan="2">
<input type="button" value="정보수정" class="input_button"
onclick="location='member_edit.do'" />
<input type="button" value="회원탈퇴" class="input_button"
onclick="location='member_del.do'" />
<input type="submit" value="로그아웃" class="input_button" />
</th>
</tr>
<tr>
<th>회원이름</th>
<td>${join_name}님 로그인을 환영합니다</td>
</tr>
<tr>
<th>프로필사진</th>
<td>
<c:if test="${empty join_profile}">
</c:if>
<c:if test="${!empty join_profile}">
<img src="<%=request.getContextPath() %>/upload/${join_profile}" height="100" width="100" />
</c:if>
</td>
</tr>
</table>
</form>
</div>
</c:if>
</body>
</html>
- 전체를 if 태그로 감싸서 세션이 있는 경우에만 해당 내용이 출력됨
- 로그인 성공 후, 또는 수정 성공 후 여기로 이동하므로, "main.do" 는 인터셉터 매핑을 잡아두지 않았고, 여기서 세션값이 없을땐 로그인 폼으로 보내주는 처리를 한다
* 프로필 이미지 출력은 나중에 정보 수정 후 main.jsp 로 돌아왔을때 수정
+ 인터셉터 설정
- servlet-context.xml 부분
<!-- 인터셉터 설정 -->
<beans:bean id="sessionChk" class="myspring.controller.SessionCheckInter"/>
<interceptors>
<interceptor>
<mapping path="/member_edit.do"/>
<mapping path="/member_edit_ok.do"/>
<mapping path="/member_del.do"/>
<mapping path="/member_del_ok.do"/>
<mapping path="/member_logout.do"/>
<beans:ref bean="sessionChk"/>
</interceptor>
</interceptors>
- 로그인해야만 쓸 수 있는 메뉴 요청에 대해 매핑을 잡아둠
- 인터셉터 사용 이유 : 비정상적인 접근(세션이 없는 경우) 을 막기 위해서
로그아웃 기능
버튼 처리 : "로그아웃" 버튼 클릭시
<form method="post" action="member_logout.do">
<input type="button" value="정보수정" class="input_button"
onclick="location='member_edit.do'" />
- main.jsp 의 "로그아웃" 버튼은 submit 버튼이므로 action 에 있는 "member_logout.do" 로 요청한다
- 해당 요청은 인터셉터에 등록되었으므로 세션이 있을때만 Controler 의 member_logout.do 로 감
- 세션이 없으면 로그인 폼으로 이동
- Controller 클래스 MemberAction.java "member_logout.do" 요청 부분만
// 로그아웃
@RequestMapping("member_logout.do")
public String logout(HttpSession session) {
session.invalidate();
return "member/member_logout";
}
- 인터셉터를 통과해야 즉, 세션값이 있을때만 여기에 도착함
- 로그아웃 시 해야할 일 2가지 : 세션 삭제 , 로그아웃되었다는 메세지 뿌리고 로그인 폼으로 이동
- member_logout.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<script>
alert("로그아웃 되었습니다!");
location="member_login.do";
</script>
정보 수정폼 기능
버튼 처리 : "정보수정" 버튼 클릭시
<input type="button" value="정보수정" class="input_button"
onclick="location='member_edit.do'" />
- main.jsp 에서 "정보수정" 클릭시 "member_edit.do" 로 요청
- 해당 요청은 인터셉터에 등록되었으므로 인터셉터를 거쳐 세션이 있을때만 Controler 의 member_edit.do 로 감
- 세션이 없으면 로그인 폼으로 이동
- "member_edit.do" 로 가면서 아무 값을 가져가지 않음, 세션값을 활용한다
- Controller 클래스 MemberAction.java "member_edit.do" 요청 부분만
/* 회원정보 수정 폼 */
@RequestMapping(value = "/member_edit.do")
public String member_edit(HttpSession session, Model m) throws Exception {
String id = (String) session.getAttribute("id");
MemberBean editm = memberService.userCheck(id);
String join_tel = editm.getJoin_tel();
StringTokenizer st01 = new StringTokenizer(join_tel, "-");
// java.util 패키지의 StringTokenizer 클래스는 첫번째 전달인자를
// 두번째 -를 기준으로 문자열을 파싱해준다.
String join_tel1 = st01.nextToken();// 첫번째 전화번호 저장
String join_tel2 = st01.nextToken(); // 두번째 전번 저장
String join_tel3 = st01.nextToken();// 세번째 전번 저장
String join_phone = editm.getJoin_phone();
StringTokenizer st02 = new StringTokenizer(join_phone, "-");
// java.util 패키지의 StringTokenizer 클래스는 첫번째 전달인자를
// 두번째 -를 기준으로 문자열을 파싱해준다.
String join_phone1 = st02.nextToken();// 첫번째 전화번호 저장
String join_phone2 = st02.nextToken(); // 두번째 전번 저장
String join_phone3 = st02.nextToken();// 세번째 전번 저장
String join_email = editm.getJoin_email();
StringTokenizer st03 = new StringTokenizer(join_email, "@");
// java.util 패키지의 StringTokenizer 클래스는 첫번째 전달인자를
// 두번째 @를 기준으로 문자열을 파싱해준다.
String join_mailid = st03.nextToken();// 첫번째 전화번호 저장
String join_maildomain = st03.nextToken(); // 두번째 전번 저장
m.addAttribute("editm", editm);
m.addAttribute("join_tel1", join_tel1);
m.addAttribute("join_tel2", join_tel2);
m.addAttribute("join_tel3", join_tel3);
m.addAttribute("join_phone1", join_phone1);
m.addAttribute("join_phone2", join_phone2);
m.addAttribute("join_phone3", join_phone3);
m.addAttribute("join_mailid", join_mailid);
m.addAttribute("join_maildomain", join_maildomain);
return "member/member_edit";
}
- 수정폼에 상세정보를 뿌려줘야 한다
- 세션에 공유된 id 를 session.getAttribuate("id") 로 받아서 그 id 를 매개변수로 userCheck() 호출하여 상세 정보를 구해옴
- 구해온 상세정보를 DTO 객체 editm 으로 받는다, 그 editm 을 Model 객체에 저장해서 member_edit.jsp 로 전달
- 그 DB에 저장된 정보인 editm 에서 결합된 전화번호, 휴대폰번호, 이메일을 잘라서 따로 저장하고 Model 객체에 저장해서 member_edit.jsp 로 전달
- userCheck() 메소드는 이전에도 설명 했으므로 생략, 바로 View 를 보자
- member_edit.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원정보 수정</title>
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
<link rel="stylesheet" type="text/css" href="./css/member.css" />
<script src="./js/jquery.js"></script>
<script src="./js/member.js"></script>
<script src="http://dmaps.daum.net/map_js_init/postcode.v2.js"></script>
<script>
//우편번호, 주소 Daum API
function openDaumPostcode() {
new daum.Postcode({
oncomplete : function(data) {
// 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
// 우편번호와 주소 정보를 해당 필드에 넣고, 커서를 상세주소 필드로 이동한다.
document.getElementById('join_zip1').value = data.zonecode;
document.getElementById('join_addr1').value = data.address;
}
}).open();
}
</script>
</head>
<body>
<div id="join_wrap">
<h2 class="join_title">회원수정</h2>
<form name="f" method="post" action="member_edit_ok.do"
onsubmit="return edit_check()" enctype="multipart/form-data">
<!-- 이진파일을 업로드 할려면 enctype 속성을 지정 -->
<table id="join_t">
<tr>
<th>회원아이디</th>
<td>
${id}
</td>
</tr>
<tr>
<th>회원비번</th>
<td>
<input type="password" name="join_pwd" id="join_pwd1" size="14"
class="input_box" />
</td>
</tr>
<tr>
<th>회원비번확인</th>
<td>
<input type="password" name="join_pwd2" id="join_pwd2" size="14"
class="input_box" />
</td>
</tr>
<tr>
<th>회원이름</th>
<td>
<input name="join_name" id="join_name" size="14" class="input_box"
value="${editm.join_name}" />
</td>
</tr>
<tr>
<th>우편번호</th>
<td>
<input name="join_zip1" id="join_zip1" size="5" class="input_box"
readonly onclick="post_search()" value="${editm.join_zip1}"/>
<%-- -<input name="join_zip2" id="join_zip2" size="3" class="input_box" readonly
onclick="post_search()" value="${editm.join_zip2}"/> --%>
<input type="button" value="우편번호검색" class="input_button"
onclick="openDaumPostcode()" />
</td>
</tr>
<tr>
<th>주소</th>
<td>
<input name="join_addr1" id="join_addr1" size="50" class="input_box"
readonly value="${editm.join_addr1}" onclick="post_search()" />
</td>
</tr>
<tr>
<th>나머지 주소</th>
<td>
<input name="join_addr2" id="join_addr2" size="37"
value="${editm.join_addr2}" class="input_box" />
</td>
</tr>
<tr>
<th>집전화번호</th>
<td>
<%@ include file="../../jsp/include/tel_number.jsp"%>
<select name="join_tel1" >
<c:forEach var="t" items="${tel}" begin="0" end="16">
<option value="${t}" <c:if test="${join_tel1 == t}">${'selected'}
</c:if>>${t}
</option>
</c:forEach>
</select>-<input name="join_tel2" id="join_tel2" size="4"
maxlength="4" class="input_box" value="${join_tel2}"/>-<input name="join_tel3"
id="join_tel3" size="4" maxlength="4" class="input_box"
value="${join_tel3}"/>
</td>
</tr>
<tr>
<th>휴대전화번호</th>
<td>
<%@ include file="../../jsp/include/phone_number.jsp" %>
<select name="join_phone1">
<c:forEach var="p" items="${phone}" begin="0" end="5">
<option value="${p}" <c:if test="${join_phone1 == p}">${'selected'}
</c:if>>${p}
</option>
</c:forEach>
</select>-<input name="join_phone2" id="join_phone2" size="4"
maxlength="4" class="input_box" value="${join_phone2}"/>-<input name="join_phone3"
id="join_phone3" size="4" maxlength="4" class="input_box"
value="${join_phone3}"/>
</td>
</tr>
<tr>
<th>전자우편</th>
<td>
<input name="join_mailid" id="join_mailid" size="10"
class="input_box" value="${join_mailid}"/>@<input name="join_maildomain"
id="join_maildomain" size="20" class="input_box" readonly
value="${join_maildomain}" />
<!--readonly는 단지 쓰기,수정이 불가능하고 읽기만 가능하다 //-->
<select name="mail_list" onchange="domain_list()">
<option value="">=이메일선택=</option>
<option value="daum.net"
<c:if test="${join_maildomain == 'daum.net'}">${'selected'}
</c:if>>daum.net</option>
<option value="nate.com"
<c:if test="${join_maildomain == 'nate.com'}">${'selected'}
</c:if>>nate.com</option>
<option value="naver.com"
<c:if test="${join_maildomain == 'naver.com'}">${'selected'}
</c:if>>naver.com</option>
<option value="hotmail.com"
<c:if test="${join_maildomain == 'hotmail.com'}">${'selected'}
</c:if>>hotmail.com</option>
<option value="gmail.com"
<c:if test="${join_maildomain == 'gmail.com'}">${'selected'}
</c:if>>gmail.com</option>
<option value="0">직접입력</option>
</select>
</td>
</tr>
<tr>
<th>프로필사진</th>
<td>
<input type="file" name="join_profile1" />
</td>
</tr>
</table>
<div id="join_menu">
<input type="submit" value="회원수정" class="input_button" />
<input type="reset" value="수정취소" class="input_button"
onclick="$('#join_pwd1').focus();" />
</div>
</form>
</div>
</body>
</html>
- value 속성에 상세정보를 뿌려준다
+ 주솟값이 저장되는 DB 테이블의 컬럼이 두개이다, 주소와 상세정보
- 현재 수정폼에서는 DB와 연동해서 비밀번호가 맞는지 확인하고 있지 않다
- 수정폼의 비밀번호와 비밀번호확인이 같다면 문제없이 "member_edit_ok.do" 로 요청
- 요청하면서 id 값을 가져가지 않는다, 여기선 세션으로 해결하지만 hidden 으로 전달하는 편이 나음
회원 아이디 출력 (member_edit.jsp)
<th>회원아이디</th>
<td>
${id}
</td>
- 넘어온 객체 editm에도 id 값이 들어있고 Session 값에도 id 가 들어있다
- 현재는 Session 에 공유설정된 id 를 가져오고 있다, 앞에 SessionScope 가 생략된것
- ${editm.id} 로 출력해도 된다
집전화번호 앞자리에 값뿌리기
<tr>
<th>집전화번호</th>
<td>
<%@ include file="../../jsp/include/tel_number.jsp"%>
<select name="join_tel1" >
<c:forEach var="t" items="${tel}" begin="0" end="16">
<option value="${t}" <c:if test="${join_tel1 == t}">${'selected'}
</c:if>>${t}
</option>
</c:forEach>
</select>-<input name="join_tel2" id="join_tel2" size="4"
maxlength="4" class="input_box" value="${join_tel2}"/>-<input name="join_tel3"
id="join_tel3" size="4" maxlength="4" class="input_box"
value="${join_tel3}"/>
</td>
</tr>
+ tel_number.jsp 에서 배열 tel 에 지역번호들을 저장하고, REQUEST 공유설정했다
- 여기 member_edit 에서 ${tel} 을 forEach 의 items 에 넣고 Model 에서 넘어온 전화번호 앞자리와 일치하는지 확인해서 일치시 selected 를 추가함
- 휴대폰 번호 앞자리도 마찬가지로 처리
- 이외에도 jQuery 로 간략하게 처리할 수도 있다
이메일 도메인 select-option 처리
<tr>
<th>전자우편</th>
<td>
<input name="join_mailid" id="join_mailid" size="10"
class="input_box" value="${join_mailid}"/>@<input name="join_maildomain"
id="join_maildomain" size="20" class="input_box" readonly
value="${join_maildomain}" />
<!--readonly는 단지 쓰기,수정이 불가능하고 읽기만 가능하다 //-->
<select name="mail_list" onchange="domain_list()">
<option value="">=이메일선택=</option>
<option value="daum.net"
<c:if test="${join_maildomain == 'daum.net'}">${'selected'}
</c:if>>daum.net</option>
<option value="nate.com"
<c:if test="${join_maildomain == 'nate.com'}">${'selected'}
</c:if>>nate.com</option>
<option value="naver.com"
<c:if test="${join_maildomain == 'naver.com'}">${'selected'}
</c:if>>naver.com</option>
<option value="hotmail.com"
<c:if test="${join_maildomain == 'hotmail.com'}">${'selected'}
</c:if>>hotmail.com</option>
<option value="gmail.com"
<c:if test="${join_maildomain == 'gmail.com'}">${'selected'}
</c:if>>gmail.com</option>
<option value="0">직접입력</option>
</select>
</td>
</tr>
- if 태그를 option 태그에 일일히 사용함
+ readonly vs disabled
- readonly 는 form의 action 으로 값이 넘어가고 disabled 는 값이 넘어가지 않음!
정보 수정
- 수정폼 member_edit.jsp 에서 값을 입력 후 "회원수정" 클릭시 "member_edit_ok.do" 로 요청함
- id 값은 넘어오지 않았다
- Controller 클래스 MemberAction.java 에서 "member_edit_ok.do" 요청 부분만
/* 회원정보 수정(fileupload) */
@RequestMapping(value = "/member_edit_ok.do", method = RequestMethod.POST)
public String member_edit_ok(@RequestParam("join_profile1") MultipartFile mf,
MemberBean member,
HttpServletRequest request,
HttpSession session,
Model model) throws Exception {
String filename = mf.getOriginalFilename();
int size = (int) mf.getSize();
String path = request.getRealPath("upload");
System.out.println("path:"+path);
int result=0;
String file[] = new String[2];
// file = filename.split(".");
// System.out.println(file.length);
// System.out.println("file0="+file[0]);
// System.out.println("file1="+file[1]);
String newfilename = "";
if(filename != ""){ // 첨부파일이 전송된 경우
// 파일 중복문제 해결
String extension = filename.substring(filename.lastIndexOf("."), filename.length());
System.out.println("extension:"+extension);
UUID uuid = UUID.randomUUID();
newfilename = uuid.toString() + extension;
System.out.println("newfilename:"+newfilename);
StringTokenizer st = new StringTokenizer(filename, ".");
file[0] = st.nextToken(); // 파일명
file[1] = st.nextToken(); // 확장자
if(size > 100000){
result=1;
model.addAttribute("result", result);
return "member/uploadResult";
}else if(!file[1].equals("jpg") &&
!file[1].equals("gif") &&
!file[1].equals("png") ){
result=2;
model.addAttribute("result", result);
return "member/uploadResult";
}
}
if (size > 0) { // 첨부파일이 전송된 경우
mf.transferTo(new File(path + "/" + newfilename));
}
String id = (String) session.getAttribute("id");
String join_tel1 = request.getParameter("join_tel1").trim();
String join_tel2 = request.getParameter("join_tel2").trim();
String join_tel3 = request.getParameter("join_tel3").trim();
String join_tel = join_tel1 + "-" + join_tel2 + "-" + join_tel3;
String join_phone1 = request.getParameter("join_phone1").trim();
String join_phone2 = request.getParameter("join_phone2").trim();
String join_phone3 = request.getParameter("join_phone3").trim();
String join_phone = join_phone1 + "-" + join_phone2 + "-" + join_phone3;
String join_mailid = request.getParameter("join_mailid").trim();
String join_maildomain = request.getParameter("join_maildomain").trim();
String join_email = join_mailid + "@" + join_maildomain;
MemberBean editm = this.memberService.userCheck(id);
if (size > 0 ) { // 첨부 파일이 수정되면
member.setJoin_profile(newfilename);
} else { // 첨부파일이 수정되지 않으면
member.setJoin_profile(editm.getJoin_profile());
}
member.setJoin_id(id);
member.setJoin_tel(join_tel);
member.setJoin_phone(join_phone);
member.setJoin_email(join_email);
memberService.updateMember(member);// 수정 메서드 호출
model.addAttribute("join_name", member.getJoin_name());
model.addAttribute("join_profile", member.getJoin_profile());
return "member/main";
}
1. 수정폼에선 넘어온 첨부파일은 @RequestParam("join_profile1") 으로 받아서 MultipartFile 객체 mf 에 저장
2. 나머지 수정폼에서 넘어온 값들은 DTO 객체 member 에 저장해서 받음
3. mf.getOriginalFilename() 으로 파일명 filename 을 구한다
4. mf.getSize() 메소드는 return 타입이 LONG 형이지만 int 형으로 강제형변환시켜서 변수 size 에 저장
5. 확장자 처리를 위해 String 배열 file 을 만들어둠
6. 확장자를 분리시켜 변수 extension 에 저장, UUID.randomUUID() 로 난수 발생시켜서 extension 과 붙여서 새로운 파일명 newfilename 을 구함
6. 파일 사이즈 제한에 대한 설정은 root-context.xml 에서 했지만, 그 코드는 더 큰 파일 첨부시 실행을 멈추고 오류를 발생시킴, 그러므로 여기서 if 문을 통해 더 큰 파일 첨부시 return uploadResult 로 보내면서 업로드를 불가능하게 만듬
7. filename 에서 확장자를 빼서 file[1] 에 저장후 file[1] 이 특정 확장자가 아닌 경우 return uploadResult 로 보내면서 업로드를 불가능하게 만듬
8. 첨부파일 업로드를 mf.transferTo() 메소드로 먼저 한다, 그 후 첨부파일을 제외한 다른 수정폼에 입력한 값들을 따로 update 시킨다
9. 수정폼의 전화번호, 휴대폰 번호, 이메일은 분리되어 값이 넘어온 상태이므로 @ModelAttribute 에 의해 DTO 객체 member 에 저장되지 않은 상태이다
10. 수정폼의 분리되어 넘어온 전화번호, 휴대폰 번호, 이메일을 결합해서 Setter 메소드로 객체 member 에 세팅해준다
11. 그리고 그 객체 member 를 매개변수로 updateMember() 를 호출해서 실제 수정을 한다 * 아래에서 설명
+ 현재는 비번 비교를 하지 않음
12. 수정 후 돌아와서 마이페이지 main.jsp 로 이동할 것이므로 회원 이름과 프로필 파일명을 Model 객체에 저장해서 전달해야만한다
- main.jsp 에서는 이 회원 이름과 프로필 파일을 출력하고 있으므로 main.jsp 로 가려면 반드시 여기서 회원 이름과 프로필 파일명을 전달해야함
첨부파일을 수정할때와 수정하지 않았을때 ( MemberAction.java "member_edit_ok.do" 요청 부분 일부)
if (size > 0 ) { // 첨부 파일이 수정되면
member.setJoin_profile(newfilename);
} else { // 첨부파일이 수정되지 않으면
member.setJoin_profile(editm.getJoin_profile());
}
- 사용자가 첨부파일을 수정할때와 수정하지 않을떄의 경우를 나눠서 처리해야한다
- 수정시에는 수정된 파일명이 컬럼에 저장되어야하고 수정되지 않았을땐 기존 파일명이 컬럼에 저장되어야한다
- size 는 사용자가 첨부한 파일을 받은 객체 mf 에서 mf.getSize() 로 구해온 값을 저장하고 있는 변수이다, 즉 사용자가 첨부를 했으면 size 는 0 보다 크고, 첨부를 하지 않았으면 0 이다
- 첨부파일을 수정하면 새 파일의 난수화된 파일명을 객체 member 에 Setter 메소드로 세팅
- 첨부파일을 수정하지 않으면 기존에 DB에 저장된 파일명을 그대로 다시 객체 member 에 Setter 메소드로 세팅
- Service 클래스 MemberServiceImpl.java 에서 updateMember() 메소드 부분만
public void updateMember(MemberBean member) throws Exception{
memberDao.updateMember(member);
}
- DAO클래스 MemberDaoImpl.java 에서 updateMember() 메소드 부분만
/* 회원수정 */
// @Transactional
public void updateMember(MemberBean member) throws Exception {
sqlSession.update("member_edit", member);
}
- Mapper 파일 member.xml 에서 id 가 "member_edit" 인 SQL문 부분만
<!-- 회원수정 -->
<update id="member_edit" parameterType="member">
update join_member set join_pwd=#{join_pwd},join_name=#{join_name},
join_zip1=#{join_zip1},join_addr1=#{join_addr1},
join_addr2=#{join_addr2},join_tel=#{join_tel},join_phone=#{join_phone},
join_email=#{join_email},join_profile=#{join_profile,jdbcType=VARCHAR}
where join_id=#{join_id}
</update>
- update 문을 사용해서 해당 회원의 정보룰 수정
- 이때도 join_profile 컬럼에 null 이 저장될 수 있다, 기존에도 파일이 없었는데 수정시에도 파일을 업로드 하지 않으면 join_profile 에는 null 이 들어간다
- 그러므로 null 을 허용하기 위해 jdbcType=VARCHAR 를 추가해야한다
- 정보 수정 후 main.jsp 로 돌아옴
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>사용자 메인화면</title>
<link rel="stylesheet" type="text/css" href="./css/main.css" />
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
</head>
<body>
<c:if test="${sessionScope.id == null }">
<script>
alert("다시 로그인 해주세요!");
location.href="<%=request.getContextPath()%>/member_login.do";
</script>
</c:if>
<c:if test="${sessionScope.id != null }">
<div id="main_wrap">
<h2 class="main_title">사용자 메인화면</h2>
<form method="post" action="member_logout.do">
<table id="main_t">
<tr>
<th colspan="2">
<input type="button" value="정보수정" class="input_button"
onclick="location='member_edit.do'" />
<input type="button" value="회원탈퇴" class="input_button"
onclick="location='member_del.do'" />
<input type="submit" value="로그아웃" class="input_button" />
</th>
</tr>
<tr>
<th>회원이름</th>
<td>${join_name}님 로그인을 환영합니다</td>
</tr>
<tr>
<th>프로필사진</th>
<td>
<c:if test="${empty join_profile}">
</c:if>
<c:if test="${!empty join_profile}">
<img src="<%=request.getContextPath() %>/upload/${join_profile}" height="100" width="100" />
</c:if>
</td>
</tr>
</table>
</form>
</div>
</c:if>
</body>
</html>
- ${join_profile} 을 사용해 경로를 잡아서 img 태그로 이미지를 불러옴
회원 탈퇴폼 기능
버튼 처리 : "회원탈퇴" 버튼 클릭시
<input type="button" value="회원탈퇴" class="input_button"
onclick="location='member_del.do'" />
- main.jsp 에서 "회원탈퇴" 클릭시 "member_del.do" 로 요청
- 해당 요청은 인터셉터에 등록되었으므로 인터셉터를 거쳐 세션이 있을때만 Controler 의 member_del.do 로 감
- 세션이 없으면 로그인 폼으로 이동
- "member_del.do" 로 가면서 아무 값을 가져가지 않음, 세션값을 활용한다
- 사용자가 회원 탈퇴를 해도 고객 정보를 실제로 DB에서 삭제하지 않고, 상태값을 변경시켜서 탈퇴 회원 처리를 함
- 비밀번호가 맞아야 탈퇴 가능
- 테이블 join_member 에 탈퇴 사유와 탈퇴 날짜를 저장할 수 있는 컬럼 또한 있다
join_member 테이블에서 탈퇴 관련 컬럼 3가지
1. 회원 상태 (가입 또는 탈퇴)
2. 탈퇴 사유
3. 탈퇴 날짜
- 먼저 삭제폼으로 이동
- Controller 클래스 MemberAction.java "member_del.do" 요청 부분만
/* 회원정보 삭제 폼 */
@RequestMapping(value = "/member_del.do")
public String member_del(HttpSession session, Model dm) throws Exception {
String id = (String) session.getAttribute("id");
MemberBean deleteM = memberService.userCheck(id);
dm.addAttribute("d_id", id);
dm.addAttribute("d_name", deleteM.getJoin_name());
return "member/member_del";
}
- 비밀번호 비교를 하기 위해, 세션에서 id 값을 가져와서 그 id 를 매개변수로 userCheck() 를 호출하여 해당 id 회원의 상세정보를 구해옴
- 탈퇴폼에서 회원 아이디, 이름을 뿌릴 것이므로 회원 아이디와 이름을 Model 객체에 저장하고 탈퇴폼 member_del.jsp 로 이동
- userCheck() 메소드는 이전에도 설명했으므로 설명 생략
- member_del.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원탈퇴</title>
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
<link rel="stylesheet" type="text/css" href="./css/member.css" />
<script src="./js/jquery.js"></script>
<script>
function check(){
if($.trim($("#pwd").val())==""){
alert("비밀번호를 입력하세요!");
$("#pwd").val("").focus();
return false;
}
if($.trim($("#del_cont").val())==""){
alert("탈퇴사유를 입력하세요!");
$("#del_cont").val("").focus();
return false;
}
}
</script>
</head>
<body>
<div id="del_wrap">
<h2 class="del_title">회원탈퇴</h2>
<form method="post" action="member_del_ok.do" onsubmit="return check()">
<table id="del_t">
<tr>
<th>회원아이디</th>
<td>
${d_id}
</td>
</tr>
<tr>
<th>회원이름</th>
<td>${d_name}</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input type="password" name="pwd" id="pwd" size="14"
class="input_box" />
</td>
</tr>
<tr>
<th>탈퇴사유</th>
<td>
<textarea name="del_cont" id="del_cont" rows="7"
cols="30" class="input_box"></textarea>
</td>
</tr>
</table>
<div id="del_menu">
<input type="submit" value="탈퇴" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#pwd').focus();" />
</div>
</form>
</div>
</body>
</html>
- 회원 아이디, 회원 이름을 출력함
- 비밀번호와 탈퇴 사유를 입력하고 "탈퇴" 버튼 클릭시 "member_del_ok.do" 로 요청한다
- 이때 아이디와 회원 이름은 넘어가지 않음
회원 탈퇴
- 회원 탈퇴폼 member_del.jsp 에서 비밀번호와 탈퇴 사유 입력 후 "탈퇴" 클릭시 "member_del_ok.do" 로 요청함
- Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분만
/* 회원정보 삭제 완료 */
@RequestMapping(value = "/member_del_ok.do", method = RequestMethod.POST)
public String member_del_ok(@RequestParam("pwd") String pass,
@RequestParam("del_cont") String del_cont,
HttpSession session) throws Exception {
String id = (String) session.getAttribute("id");
MemberBean member = this.memberService.userCheck(id);
if (!member.getJoin_pwd().equals(pass)) {
return "member/deleteResult";
} else { // 비번이 같은 경우
String up = session.getServletContext().getRealPath("upload");
String fname = member.getJoin_profile();
System.out.println("up:"+up);
// 디비에 저장된 기존 이진파일명을 가져옴
if (fname != null) {// 기존 이진파일이 존재하면
File delFile = new File(up +"/"+fname);
delFile.delete();// 기존 이진파일을 삭제
}
MemberBean delm = new MemberBean();
delm.setJoin_id(id);
delm.setJoin_delcont(del_cont);
memberService.deleteMember(delm);// 삭제 메서드 호출
session.invalidate(); // 세션만료
return "redirect:member_login.do";
}
}
- 넘어온 비밀번호와 탈퇴사유를 @RequestParam 으로 각각 변수 pass, del_cont 에 받는다
- 회원 아이디는 세션에서 구해온다, 그 id 를 매개변수로, 비밀번호가 맞는지 확인하기 위해 userCheck() 메소드 호출
- DB의 비번이 사용자가 탈퇴폼에서 입력한 비밀번호와 일치하지 않으면 deleteResult.jsp 로 이동, 거기서 탈퇴 실패 처리
- 비번이 일치하면, 첨부파일이 있는 경우 첨부파일을 삭제해야한다
첨부파일 삭제 (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)
String up = session.getServletContext().getRealPath("upload");
String fname = member.getJoin_profile();
System.out.println("up:"+up);
// 디비에 저장된 기존 이진파일명을 가져옴
if (fname != null) {// 기존 이진파일이 존재하면
File delFile = new File(up +"/"+fname);
delFile.delete();// 기존 이진파일을 삭제
}
- 업로드 폴더 경로를 구해온다, 현재는 session 객체로 절대 경로를 구해서 변수 up 에 저장
+ request 객체로 getReaulPath() 해서 경로를 구할 수도 있다
- DB에 저장된 프로필 파일명을 fname 파일명에 저장
- up 과 filename 을 사용해서 File 객체를 생성 하고 그 파일 객체 delFile 을 통해 delte() 로 해당 파일 삭제
탈퇴 SQL문에 전달하기 위해 DTO객체 생성 후 두가지 값 세팅 (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)
MemberBean delm = new MemberBean();
delm.setJoin_id(id);
delm.setJoin_delcont(del_cont);
memberService.deleteMember(delm);// 삭제 메서드 호출
- DTO 객체 delm을 생성해서 id 값과 탈퇴 사유를 delm 저장함
- 탈퇴 처리를 위해 id 도 필요하고, 탈퇴 사유도 필요하다. id 는 where 조건절에 들어갈 것
- Mapper 파일에 값을 전달할때 1개의 값만 전달가능하므로 DTO 객체 안에 두가지 값을 저장해서 DTO 객체를 넘길 것
탈퇴 메소드 호출 (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)
memberService.deleteMember(delm);// 삭제 메서드 호출
- Service 클래스의 메소드 deleteMember() 호출 * 아래에서 설명
세션 끊고 (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)
session.invalidate(); // 세션만료
return "redirect:member_login.do";
- DB에서 탈퇴 처리한 후 세션을 삭제해줌
- 이후 "member_login.do" 로 요청하면서 로그인 폼으로 이동
- Service 클래스 MemberServiceImpl.java 에서 deleteMember() 메소드 부분만
public void deleteMember(MemberBean member) throws Exception{
memberDao.deleteMember(member);
}
- DAO클래스 MemberDaoImpl.java 에서 deleteMember() 메소드 부분만
/* 회원삭제 */
// @Transactional
public void deleteMember(MemberBean delm) throws Exception {
sqlSession.update("member_delete", delm);
}
- 값을 하나만 전달가능하지만 탈퇴 SQL문에서 id 도 필요하고 탈퇴사유도 필요하므로 두개를 저장한 DTO 객체 delm 을 넘김
- Mapper 파일 member.xml 에서 id 가 "member_delete" 인 SQL문 부분만
<!-- 회원삭제 -->
<update id="member_delete" parameterType="member">
update join_member set join_delcont=#{join_delcont}, join_state=2,
join_deldate=sysdate where join_id=#{join_id}
</update>
- where 조건절에 DTO 로 넘어온 id 값이 들어가서 해당 회원의 데이터를 변경킴
- delete 가 아닌 update 문으로 join_state 를 2로 변경시켜서 탈퇴를 처리함
- 이때 DTO 로 넘어온 탈퇴 사유를 세팅하고, 탈퇴 날짜를 sysdate 로 세팅
비번 찾기 기능 실행
- 실행해서 비번 찾기를 해보자
- 회원가입시 등록했던 이메일 주소의 받은 메일함에서 확인
- 아래에서 비번 찾기 기능 코드 설명할 것
+ 임시비번 기능 활용
- 이메일로 임시비번을 저장하고 DB에도 그 임시비번으로 비밀번호를 업데이트 시켜둬야한다
- 그래야 그 임시비번으로 로그인 가능
비번 찾기 기능
- 로그인 폼 member_login.jsp 에서 버튼 "비번찾기" 클릭시 pwd_find() 메소드 호출함
- 비번 찾기도 비번 찾기 폼과 비번 찾기가 있음, 여기선 한번에 설명
- pwd_find() 메소드는 팝업창을 띄운다
- member_login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/admin.css" />
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/member.css" />
<!-- <script src="./js/jquery.js"></script> -->
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
function check(){
if($.trim($("#id").val())==""){
alert("로그인 아이디를 입력하세요!");
$("#id").val("").focus();
return false;
}
if($.trim($("#pwd").val())==""){
alert("비밀번호를 입력하세요!");
$("#pwd").val("").focus();
return false;
}
}
/*비번찾기 공지창*/
function pwd_find(){
window.open("pwd_find.do","비번찾기","width=450,height=500");
//자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성")
//메서드로 새로운 공지창을 만듬.폭이 400,높이가 400인 새로운 공지창을 만듬.단위는 픽셀
}
</script>
</head>
<body>
<div id="login_wrap">
<h2 class="login_title">로그인</h2>
<form method="post" action="member_login_ok.do" onsubmit="return check()">
<table id="login_t">
<tr>
<th>아이디</th>
<td>
<input name="id" id="id" size="20" class="input_box" />
</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input type="password" name="pwd" id="pwd" size="20" class="input_box"/>
</td>
</tr>
</table>
<div id="login_menu">
<input type="submit" value="로그인" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#id').focus();" />
<input type="button" value="회원가입" class="input_button"
onclick="location='member_join.do'" />
<input type="button" value="비번찾기" class="input_button"
onclick="pwd_find()" />
</div>
</form>
</div>
</body>
</html>
팝업창 띄우기 (member_login.jsp 부분)
/*비번찾기 공지창*/
function pwd_find(){
window.open("pwd_find.do","비번찾기","width=450,height=500");
//자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성")
//메서드로 새로운 공지창을 만듬.폭이 400,높이가 400인 새로운 공지창을 만듬.단위는 픽셀
}
- 자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성") 을 써서 팝업창 띄움
- Model 1 에서는 jsp 파일을 첫번째 매개변수에 썼지만 지금은 Model 2 기반이므로 Controller 를 거쳐야한다, 그러므로 요청명을 쓴다
- 즉 사용자가 "비번찾기" 클릭시 pwd_find() 메소드가 호출되어 "pwd_find.do" 로 요청한다
- Controller 클래스 MemberAction.java 에서 "pwd_find.do" 요청 부분만
/* 비번찾기 폼 */
@RequestMapping(value = "/pwd_find.do")
public String pwd_find() {
return "member/pwd_find";
// member 폴더의 pwd_find.jsp 뷰 페이지 실행
}
- Controller 클래스를 거쳐 pwd_find.jsp 로 간다, 그 pwd_find.jsp 안의 내용이 바로 팝업창에 출력될 내용임
- pwd_find.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비번찾기</title>
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
<link rel="stylesheet" type="text/css" href="./css/member.css" />
<script src="./js/jquery.js"></script>
<script>
function check(){
if($.trim($("#id").val())==""){
alert("아이디를 입력하세요!");
$("#id").val("").focus();
return false;
}
if($.trim($("#name").val())==""){
alert("회원이름을 입력하세요!");
$("#name").val("").focus();
return false;
}
}
</script>
</head>
<body>
<div id="pwd_wrap">
<c:if test="${empty pwdok}">
<h2 class="pwd_title">비번찾기</h2>
<form method="post" action="pwd_find_ok.do" onsubmit="return check()">
<table id="pwd_t">
<tr>
<th>아이디</th>
<td><input name="join_id" id="id" size="14" class="input_box" /></td>
</tr>
<tr>
<th>회원이름</th>
<td><input name="join_name" id="name" size="14" class="input_box" /></td>
</tr>
</table>
<div id="pwd_menu">
<input type="submit" value="찾기" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#id').focus();"/>
</div>
<div id="pwd_close">
<input type="button" value="닫기" class="input_button"
onclick="self.close();" />
<!-- close()메서드로 공지창을 닫는다. self.close()는 자바스크립트이다. -->
</div>
</form>
</c:if>
<c:if test="${!empty pwdok}">
<h2 class="pwd_title2">비번찾기 결과</h2>
<table id="pwd_t2">
<tr>
<th>검색한 비번:</th>
<td>${pwdok}</td>
</tr>
</table>
<div id="pwd_close2">
<input type="button" value="닫기" class="input_button"
onclick="self.close();" />
<!-- close()메서드로 공지창을 닫는다. self.close()는 자바스크립트이다. -->
</div>
</c:if>
</div>
</body>
</html>
- 이 파일 pwd_find.jsp 안의 내용이 팝업창에 나타난다
- 조건식을 써서 pwdok 가 empty 인 경우와 empty 가 아닌경우로 나누어서 다른 내용을 실행함
1. 처음 이 파일에 올때는 pwdok 이 empty 인 상태이다,
2. 사용자가 아이디, 비번을 쓰고 "찾기" 클릭시 "pwd_find_ok.do" 로 요청하며 폼에 작성된 아이디, 비번값을 전송한다
* 아래에서 설명
3. DB에서 아이디, 비번이 일치되면 비밀번호를 메일을 보낸 후 Controller 클래스에서 pwdok 에서 pwdok 에 메세지를 저장한 뒤 다시 return 으로 pwd_find.jsp 로 온다
4. 다시 돌아오면 pwdok 가 empty 가 아니게 되므로 아래의 if 태그를 실행, 즉 아래 화면이 출력됨
- Controller 클래스 MemberAction.java 에서 "pwd_find_ok.do" 요청 부분만
/* 비번찾기 완료 */
@RequestMapping(value = "/pwd_find_ok.do", method = RequestMethod.POST)
public String pwd_find_ok(@ModelAttribute MemberBean mem, HttpServletResponse response, Model model)
throws Exception {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
MemberBean member = memberService.findpwd(mem);
if (member == null) {// 값이 없는 경우
return "member/pwdResult";
} else {
// Mail Server 설정
String charSet = "utf-8";
String hostSMTP = "smtp.naver.com";
String hostSMTPid = "아이디@naver.com";
String hostSMTPpwd = "비밀번호"; // 비밀번호 입력해야함
// 보내는 사람 EMail, 제목, 내용
String fromEmail = "아이디@naver.com";
String fromName = "관리자";
String subject = "비밀번호 찾기";
// 받는 사람 E-Mail 주소
String mail = member.getJoin_email();
try {
HtmlEmail email = new HtmlEmail();
email.setDebug(true);
email.setCharset(charSet);
email.setSSL(true);
email.setHostName(hostSMTP);
email.setSmtpPort(587);
email.setAuthentication(hostSMTPid, hostSMTPpwd);
email.setTLS(true);
email.addTo(mail, charSet);
email.setFrom(fromEmail, fromName, charSet);
email.setSubject(subject);
email.setHtmlMsg("<p align = 'center'>비밀번호 찾기</p><br>" + "<div align='center'> 비밀번호 : "
+ member.getJoin_pwd() + "</div>");
email.send();
} catch (Exception e) {
System.out.println(e);
}
model.addAttribute("pwdok", "등록된 email을 확인 하세요~!!");
return "member/pwd_find";
}
}
- 넘어온 아이디와 회원이름은 @ModelAttribute 로 DTO MemberBean 객체 mem 에 바로 받아서 저장한다
- 그 객체 mem 을 사용해서 Service 클래스의 findpwd() 메소드를 호출해서 회원 상세정보를 받아옴
+ SQL문에 값 1개만 전달 가능하므로 id 와 이름을 DTO 객체에 저장해서 통째로 넘김
+ findpwd() 를 따라가보자 * 아래에서 설명
- findpwd() 에서 돌아온 후, 돌아온 회원의 상세정보를 객체 member 에 저장
- member 값이 없다면 없는 회원이므로 pwdResult.jsp 로 이동, 값이 있다면 메일을 보내기위한 설정을 한다
- 메일을 보낸 후 Model 객체를 통해 "pwdok" 에 메세지를 저장하고 다시 pwd_find.jsp 로 이동
이메일 보내는 설정 및 보내는 코드 (Controller 클래스 MemberAction.java 에서 "pwd_find_ok.do" 요청 부분 일부)
// Mail Server 설정
String charSet = "utf-8";
String hostSMTP = "smtp.naver.com";
String hostSMTPid = "아이디@naver.com";
String hostSMTPpwd = "비밀번호"; // 비밀번호 입력해야함
// 보내는 사람 EMail, 제목, 내용
String fromEmail = "아이디@naver.com";
String fromName = "관리자";
String subject = "비밀번호 찾기";
// 받는 사람 E-Mail 주소
String mail = member.getJoin_email();
try {
HtmlEmail email = new HtmlEmail();
email.setDebug(true);
email.setCharset(charSet);
email.setSSL(true);
email.setHostName(hostSMTP);
email.setSmtpPort(587);
email.setAuthentication(hostSMTPid, hostSMTPpwd);
email.setTLS(true);
email.addTo(mail, charSet);
email.setFrom(fromEmail, fromName, charSet);
email.setSubject(subject);
email.setHtmlMsg("<p align = 'center'>비밀번호 찾기</p><br>" + "<div align='center'> 비밀번호 : "
+ member.getJoin_pwd() + "</div>");
email.send();
} catch (Exception e) {
System.out.println(e);
}
- 받는 사람의 이메일 주소로는 객체 member 에서 member.getJoin_email() 로 회원의 이메일 주소를 가져와서 설정
- HtmlEmail 객체 email 를 생성, setSubject() 로 제목 설정, setHtmlMsg() 로 내용 설정하며 검색된 비밀번호를 보냄
- Service 클래스 MemberServiceImpl.java 에서 findpwd() 메소드 부분만
public MemberBean findpwd(MemberBean m)throws Exception {
return memberDao.findpwd(m);
}
- DAO 클래스 MemberDaoImpl.java 에서 findpwd() 메소드 부분만
/* 비번 검색 */
// @Transactional
public MemberBean findpwd(MemberBean pm) throws Exception {
return sqlSession.selectOne("pwd_find", pm);
}
- Mapper 파일 member.xml 에서 id 가 "pwd_find" 인 SQL문 부분만
<!-- 비번 검색 -->
<select id="pwd_find" resultType="member" parameterType="member">
select * from join_member where join_id=#{join_id} and join_name=#{join_name}
</select>
- 사용자가 비밀번호 찾기 폼에 입력한 id 와 이름이 모두 일치하는 데이터를 검색한다
댓글 게시판 프로그램
실습 준비
- 클라우드의 springboard 프로젝트 다운, 압축 해제, import 하기
파일들 살펴보기 : pom.xml
- inject 라이브러리 : @inject 어노테이션을 쓰기 위한 라이브러리, @Autowired 와 같은 역할
- 메일 보내기 위한 라이브러리 등 라이브러리가 들어가있다
파일들 살펴보기 : web.xml
- 반드시 WEB-INF 폴더 하위에 있어야함
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- The definition of the Root Spring Container shared by all Servlets
and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
- url-patteron 을 *.do 로 설정했다
- servlet-context.xml 과 root-context.xml 을 불러옴
- filter 와 filter-mapping 을 통해 한글값 인코딩 처리를 함
파일들 살펴보기 : servlet-context.xml
- 이름과 위치가 지정되어있지 않다
- resources 폴더 하위에 있을 수도 있음
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/jsp/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="myspring" />
</beans:beans>
- base-package 를 지정했다 = 어노테이션 기반으로 쓰겠다는 의미
- base-package 를 "myspring" 으로 지정, 이게 JAVA 파일이 저장될 최상위 디렉토리이다
- webapp 가 기준이므로 prefix 로 설정된 /jsp/ 에서 jsp 폴더는 webapp 폴더 하위에 있어야한다
- 폴더 jsp 는 View 파일이 저장될 최상위 디렉토리이다.
파일들 살펴보기 : mybatis-config.xml
- MyBatis 환경설정 파일이다
- MyBatis 환경설정 파일 mybatis-config.xml (이름이 다를 수도 있음) 과 Mapper 파일은 주로 resources 폴더에 있음
- resources 폴더 하위에 있을때는 classpath: 를 붙여서 경로를 구함
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias type="myspring.model.BoardBean" alias="board"></typeAlias>
</typeAliases>
</configuration>
- alias 로 DTO BoardBean 의 별칭을 "board" 로 설정
파일들 살펴보기 : board.xml
- Mapper 파일이다
- MyBatis 환경설정 파일 mybatis-config.xml (이름이 다를 수도 있음) 과 Mapper 파일은 주로 resources 폴더에 있음
- resources 폴더 하위에 있을때는 classpath: 를 붙여서 경로를 구함
<?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="Test">
<!-- 게시판 저장 -->
<insert id="board_insert" parameterType="board">
insert into board53
(board_num,board_name,board_pass,board_subject,
board_content,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
values
(board53_num_seq.nextval,#{board_name},#{board_pass},#{board_subject},
#{board_content},board53_num_seq.nextval,0,0,0,SYSDATE)
</insert>
<!-- 게시판 총게시물 수 -->
<select id="board_count" resultType="int">
select count(board_num) from board53
</select>
<!-- 게시판 목록 (page번호를 전달받아서 startRow와 endRow를 구함) -->
<select id="board_list" parameterType="int" resultType="board">
<![CDATA[
select * from
(select rownum rnum,BOARD_NUM,BOARD_NAME,BOARD_SUBJECT,BOARD_CONTENT,
BOARD_RE_REF,BOARD_RE_LEV,BOARD_RE_SEQ,BOARD_READCOUNT,
BOARD_DATE from
(select * from board53 order by BOARD_RE_REF desc,BOARD_RE_SEQ asc))
where rnum >= ((#{page}-1) * 10+1) and rnum <= (#{page} * 10)
]]>
</select>
<!-- 게시판 내용보기 -->
<select id="board_cont" resultType="board"
parameterType="int">
select * from board53 where board_num=#{board_num}
</select>
<!-- 게시판 조회수 증가 -->
<update id="board_hit" parameterType="int">
update board53 set
board_readcount=board_readcount+1
where board_num=#{board_num}
</update>
<!-- 게시물 수정 -->
<update id="board_edit" parameterType="board">
update board53 set
board_name=#{board_name},
board_subject=#{board_subject},
board_content=#{board_content}
where board_num=#{board_num}
</update>
<!-- 게시물 삭제 -->
<delete id="board_del" parameterType="int">
delete from board53 where
board_num=#{board_num}
</delete>
<!-- 답변글 레벨 증가 -->
<update id="board_Level" parameterType="board">
update board53 set
board_re_seq=board_re_seq+1
where board_re_ref=#{board_re_ref} and
board_re_seq > #{board_re_seq}
</update>
<!-- 답변글 저장 -->
<insert id="board_reply" parameterType="board">
insert into board53
(board_num,board_name,board_subject,board_content,
board_pass,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
values(board53_num_seq.nextval,#{board_name},#{board_subject},#{board_content},
#{board_pass},#{board_re_ref},#{board_re_lev},#{board_re_seq},0,SYSDATE)
</insert>
</mapper>
파일들 살펴보기 : root-context.xml
- 이름과 위치가 지정되어있지 않다, resources 폴더 하위에 있을 수도 있음
- root-context.xml 에서 mybatis-config.xml 과 Mapper 파일을 불러오기 때문에 root-context.xml 은 마지막에 설정해야한다
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
<!-- Data Source -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="spring" />
<property name="password" value="spring123" />
</bean>
<!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="spring" />
<property name="password" value="spring123" />
</bean> -->
<!-- 스프링으로 oracle 디비 연결 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:util/mybatis-config.xml" />
<property name="mapperLocations" value="classpath:sql/*.xml" />
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
</beans>
- 이 파일을 세팅해야만 DAO 클래스에서 @Autowired 어노테이션으로 SqlSession 주입 가능
- 여기서 Constructor DI 로 생성된 SqlSession 객체 sqlSessionFactory 를 어노테이션 기반으로 DAO 클래스의 @Autowried 쪽으로 주입한다
+ SqlSession 클래스는 MyBatis 지원 인터페이스, SqlSessionTemplate 는 SqlSession 인터페이스의 구현 클래스
- root-context.xml 에 대한 자세한 설명은 이전에 했으므로 생략
- root-context.xml 코드 자세한 설명 (검색어 : 파일들 살펴보기 : root-context.xml ) : https://laker99.tistory.com/147
Data Source Bean 생성 중 url 부분
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
- 이후 프로젝트할땐 localhost 대신 AWS 에 오라클을 설치하고, 받은 IP 를 여기 작성한다
댓글 게시판 테이블 생성
- root-context.xml 에서 지정했던 계정 (spring 계정)을 활성화 시키기
- Connection Profile 도 설정하기
- board53.sql
--board53.sql
select * from tab;
select * from board53;
create table board53(
board_num number(38) primary key
, board_name varchar2(50) not null
, board_pass varchar2(30) not null
, board_subject varchar2(100) not null
, board_content varchar2(4000) not null
, board_re_ref number
, board_re_lev number
, board_re_seq number
, board_readcount number
, board_date date
);
create sequence board53_num_seq
increment by 1 start with 1 nocache;
- 실행해서 테이블 board53, 시퀀스 board53_num_seq 를 생성
테이블 board53 컬럼 설명
- board_num : 글 번호, board53_num_seq 값을 넣음
- board_pass : 글 비밀번호, 비밀번호를 알야아 수정 / 삭제 가능
- board_re_ref : 댓글 관련 컬럼 1, 원문은 board_num 과 값 같음, 댓글은 부모의 boadr_re_ref 와 값 같음
- board_re_lev : 댓글 관련 컬럼 2, 댓글의 깊이 저장, 원문은 0, 댓글은 1, 대댓글은 2
- board_re_seq : 댓글 관련 컬럼 3, 댓글의 출력 순서 저장, 원문은 0, 댓글들은 바뀜
- 값의 변화가 빈번하게 일어나는 것은 number 타입, 아닌 것은 varchar2 타입으로 설정
- 조회수는 자주 변화하므로 board_readcount 는 number 타입으로 설정
댓글 게시판 프로그램 흐름 보기
- 프로젝트 실행시 webapp 폴더 안의 index 파일을 실행해준다
- 프로젝트 또는 index 파일 실행
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<script>
// location.href="test.do";
location.href="board_list.do";
</script>
- "글쓰기" 를 눌러 원문 글을 작성해보자
+ 원문과 댓글 작성 폼이 따로 있다
- 제목 클릭시 상세페이지로 이동하고 조회수 1 증가
- "답변" 을 누르면 댓글을 달 수 있다
- 댓글과 대댓글 달기
* 가장 최근에 달린 글이 위에 온다
- 위의 글 5개는 모두 같은 board_re_ref 값을 가지고 있음
- board_re_seq 의 오름차순 순으로 정렬되었다
- 원문의 board_re_seq 는 0 이고, 가장 최근에 달린 1단계 댓글의 board_re_seq 는 1
- SQL Developer 참고
댓글 게시판 프로그램 코드 보기
글 목록 (게시판) 기능 (간략, 자세한 설명은 나중에)
- index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<script>
// location.href="test.do";
location.href="board_list.do";
</script>
- "board_list.do" 로 요청, 목록을 가져오는 요청이다
- Controller 클래스 BoardController.java 에서 "board_list.do" 요청 부분만
/* 게시판 목록 */
@RequestMapping(value = "/board_list.do")
public String list(HttpServletRequest request, Model model) throws Exception {
List<BoardBean> boardlist = new ArrayList<BoardBean>();
int page = 1;
int limit = 10; // 한 화면에 출력할 레코드수
if (request.getParameter("page") != null) {
page = Integer.parseInt(request.getParameter("page"));
}
// 총 리스트 수를 받아옴.
int listcount = boardService.getListCount();
// 페이지 번호(page)를 DAO클래스에게 전달한다.
boardlist = boardService.getBoardList(page); // 리스트를 받아옴.
// 총 페이지 수.
int maxpage = (int) ((double) listcount / limit + 0.95); // 0.95를 더해서 올림
// 처리.
// 현재 페이지에 보여줄 시작 페이지 수(1, 11, 21 등...)
int startpage = (((int) ((double) page / 10 + 0.9)) - 1) * 10 + 1;
// 현재 페이지에 보여줄 마지막 페이지 수.(10, 20, 30 등...)
int endpage = maxpage;
if (endpage > startpage + 10 - 1)
endpage = startpage + 10 - 1;
model.addAttribute("page", page);
model.addAttribute("startpage", startpage);
model.addAttribute("endpage", endpage);
model.addAttribute("maxpage", maxpage);
model.addAttribute("listcount", listcount);
model.addAttribute("boardlist", boardlist);
return "board/board_list";
}
- 자세한 설명은 원문 글 작성 기능 설명 후
- 기본 변수와 파생변수들을 만들고 Model 객체에 저장한 후 board_list.jsp 로 이동
- board_list.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*"%>
<%@ page import="myspring.model.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>게시판 목록</title>
<link rel="stylesheet" href="<%=request.getContextPath() %>/css/bbs.css" type="text/css">
</head>
<body>
<!-- 게시판 리스트 -->
<div id="bbslist_wrap">
<h2 class="bbslist_title">게시판 목록</h2>
<div id="bbslist_c">글 개수 : ${listcount}</div>
<table id="bbslist_t">
<tr align="center" valign="middle" bordercolor="#333333">
<td style="font-family: Tahoma; font-size: 11pt;" width="8%"
height="26">
<div align="center">번호</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="47%">
<div align="center">제목</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
<div align="center">작성자</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="17%">
<div align="center">날짜</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
<div align="center">조회수</div>
</td>
</tr>
<!-- 화면 출력 번호 변수 정의 -->
<c:set var="num" value="${listcount-(page-1)*10}"/>
<!-- 반복문 시작 -->
<c:forEach var="b" items="${boardlist}">
<tr align="center" valign="middle" bordercolor="#333333"
onmouseover="this.style.backgroundColor='F8F8F8'"
onmouseout="this.style.backgroundColor=''">
<td height="23" style="font-family: Tahoma; font-size: 10pt;">
<!-- 번호 출력 부분 -->
<c:out value="${num}"/>
<c:set var="num" value="${num-1}"/>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="left">
<c:if test="${b.board_re_lev != 0}">
<c:forEach var="k" begin="1" end="${b.board_re_lev}">
</c:forEach>
<img src="./images/AnswerLine.gif">
</c:if>
<!-- 제목 출력 부분 -->
<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
${b.board_subject}
</a>
</div>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="center">${b.board_name}</div>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="center">${b.board_date}</div>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="center">${b.board_readcount}</div>
</td>
</tr>
</c:forEach>
<!-- 반복문 끝 -->
</table>
<div id="bbslist_paging">
<c:if test="${page <=1 }">
[이전]
</c:if>
<c:if test="${page > 1 }">
<a href="board_list.do?page=${page-1}">[이전]</a>
</c:if>
<c:forEach var="a" begin="${startpage}" end="${endpage}">
<c:if test="${a == page }">
[${a}]
</c:if>
<c:if test="${a != page }">
<a href="board_list.do?page=${a}">[${a}]</a>
</c:if>
</c:forEach>
<c:if test="${page >= maxpage }">
[다음]
</c:if>
<c:if test="${page < maxpage }">
<a href="board_list.do?page=${page+1}">[다음]</a>
</c:if>
</div>
<div id="bbslist_w">
<input type="button" value="글쓰기" class="input_button"
onclick="location='board_write.do?page=${page}'">
</div>
</div>
</body>
</html>
- 자세한 설명은 원문 글 작성 기능 설명 후
- 목록 페이지 board_list.jsp 에서 "글쓰기" 클릭시 "board_write.do" 요청
원문 글 작성 기능
- Controller 클래스 BoardController.java 에서 "board_write.do" 요청 부분만
/* 게시판 글쓰기 폼 */
@RequestMapping(value = "/board_write.do")
public String board_write() {
return "board/board_write";
}
- 원문 글 작성 폼 board_write.jsp 로 이동
- board_write.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>게시판 입력폼</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/bbs.css" />
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="<%=request.getContextPath() %>/js/board.js"></script>
</head>
<body>
<div id="bbswrite_wrap">
<h2 class="bbswrite_title">게시판 입력폼</h2>
<form method="post" action="<%=request.getContextPath() %>/board_write_ok.do" onSubmit="return board_check()">
<table id="bbswrite_t">
<tr>
<th>글쓴이</th>
<td>
<input name="board_name" id="board_name" size="14" class="input_box" />
</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input type="password" name="board_pass" id="board_pass" size="14"
class="input_box" />
</td>
</tr>
<tr>
<th>글제목</th>
<td>
<input name="board_subject" id="board_subject" size="40"
class="input_box" />
</td>
</tr>
<tr>
<th>글내용</th>
<td>
<textarea name="board_content" id="board_content" rows="8" cols="50"
class="input_box"></textarea>
</td>
</tr>
</table>
<div id="bbswrite_menu">
<input type="submit" value="등록" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#board_name').focus();" />
</div>
</form>
</div>
</body>
</html>
+ Spring 에서는 어노테이션 기반이므로 Model 2 와 달리 action 으로 <%=request.getContext() 가 없어도 잘 찾아간다
- 원문 글 작성 폼의 입력양식 name 값 을 DTO 프로퍼티명과 일치시킨다
- 입력 후 "등록" 클릭시 "board_write_ok.do" 로 요청한다
- Controller 클래스 BoardController.java 에서 "board_write_ok.do" 요청 부분만
/* 게시판 저장 */
@RequestMapping(value = "/board_write_ok.do", method = RequestMethod.POST)
public String board_write_ok(@ModelAttribute BoardBean board)
throws Exception {
// public String board_write_ok(@RequestParam HashMap board)
// throws Exception {
boardService.insert(board);// 저장 메서드 호출
return "redirect:/board_list.do";
}
- 입력양식의 name 값을 DTO 프로퍼티명과 일치시켰으므로 @ModelAttribute 를 통해 입력되어 넘어온 값들을 모두 생성된 DTO 객체 board 에 저장
- 이 DTO 객체 board 에 board_re_ref, board_re_lev, board_re_seq, board_readcount 는 자동으로 0으로 초기화된다
+ DTO int 형 멤버변수(필드, 프로퍼티) 는 자동으로 0 으로 초기화 된다
+ 주석으로 설정된 부분은 Map 을 통해서 넘어온 값들을 받는 방법이다, 키 밸류 형태로 저장됨
- Service 클래스의 메소드 insert() 를 호출하고, 삽입할 글을 담은 객체 board 를 매개변수로 전달
<돌아온 후>
- 글 작성 이후에 목록 페이지로 돌아갈 것이므로 여기서 redirect: 를 써서 "board_list.do" 로 요청
- Service 클래스 BoardServiceImpl.java 에서 insert() 메소드 부분만
/* 게시판 저장 */
public void insert(BoardBean b) throws Exception {
boardDao.insertBoard(b);
}
- DAO 클래스 BoardServiceImpl.java 에서 insertBoard() 메소드 부분만
/* 게시판 저장 */
public void insertBoard(BoardBean b) throws Exception {
sqlSession.insert("Test.board_insert", b);
}
- Mapper 파일 중 namespace 가 Test 인 board.xml 에서 id 가 "board_insert" 인 SQL문 부분만
<!-- 게시판 저장 -->
<insert id="board_insert" parameterType="board">
insert into board53
(board_num,board_name,board_pass,board_subject,
board_content,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
values
(board53_num_seq.nextval,#{board_name},#{board_pass},#{board_subject},
#{board_content},board53_num_seq.nextval,0,0,0,SYSDATE)
</insert>
- 원문이므로 board_re_ref 는 board_num 과 같은 값이 들어간다
- 즉 board_num 과 board_re_ref 는 같은 시퀀스로 값이 입력됨
- 원믄 글이므로 board_re_lev, board_re_seq 는 0 이 들어감, 글 작성 SQL문이므로 조회수도 0 으로 설정
- 작성 날짜는 SYSDATE 로 넣기
- 나머지는 전달받은 객체 board 로 부터 값을 꺼내서 세팅
글 목록 (게시판) 기능
- 게시판 글 목록 출력은 많이 했으므로 간략한 설명 또는 처음하는 부분만 설명
- Controller 클래스 BoardController.java 에서 "board_list.do" 요청 부분만
/* 게시판 목록 */
@RequestMapping(value = "/board_list.do")
public String list(HttpServletRequest request, Model model) throws Exception {
List<BoardBean> boardlist = new ArrayList<BoardBean>();
int page = 1;
int limit = 10; // 한 화면에 출력할 레코드수
if (request.getParameter("page") != null) {
page = Integer.parseInt(request.getParameter("page"));
}
// 총 리스트 수를 받아옴.
int listcount = boardService.getListCount();
// 페이지 번호(page)를 DAO클래스에게 전달한다.
boardlist = boardService.getBoardList(page); // 리스트를 받아옴.
// 총 페이지 수.
int maxpage = (int) ((double) listcount / limit + 0.95); // 0.95를 더해서 올림
// 처리.
// 현재 페이지에 보여줄 시작 페이지 수(1, 11, 21 등...)
int startpage = (((int) ((double) page / 10 + 0.9)) - 1) * 10 + 1;
// 현재 페이지에 보여줄 마지막 페이지 수.(10, 20, 30 등...)
int endpage = maxpage;
if (endpage > startpage + 10 - 1)
endpage = startpage + 10 - 1;
model.addAttribute("page", page);
model.addAttribute("startpage", startpage);
model.addAttribute("endpage", endpage);
model.addAttribute("maxpage", maxpage);
model.addAttribute("listcount", listcount);
model.addAttribute("boardlist", boardlist);
return "board/board_list";
}
- 자세한 설명은 원문 글 작성 기능 설명 후
- 기본 변수와 파생변수들을 만들고, 그 페이지에 해당하는 리스트를 받아온 후 변수와 리스트를 Model 객체에 저장한 후 board_list.jsp 로 이동
총 페이지 수 계산 부분 (BoardController.java 부분)
// 총 페이지 수.
int maxpage = (int) ((double) listcount / limit + 0.95); // 0.95를 더해서 올림 처리
- 이렇게 해도 계산 된다, 새로운 방법
startpage, endpage 변수 설정 (BoardController.java 부분)
// 현재 페이지에 보여줄 시작 페이지 수(1, 11, 21 등...)
int startpage = (((int) ((double) page / 10 + 0.9)) - 1) * 10 + 1;
// 현재 페이지에 보여줄 마지막 페이지 수.(10, 20, 30 등...)
int endpage = maxpage;
- 이렇게 해도 계산 된다, 새로운 방법
+ DAO 클래스 BoardDaoImpl.java 에서 getListCount() 부분만
/* 게시판 총 갯수 */
public int getListCount() throws Exception {
int count = 0;
count = ((Integer) sqlSession.selectOne("Test.board_count")).intValue();
return count;
}
- selectOne() 메소드의 리턴자료형은 Object 이다, 원래는 (Integer) 과 .intValue() 로 다운캐스팅, 언박싱을 해야한다
- 하지만 생략해도 됨, MyBatis 기능이다
+ Mapper 파일 board.xml 에서 id 가 "board_list" 인 SQL문 부분만
<!-- 게시판 목록 (page번호를 전달받아서 startRow와 endRow를 구함) -->
<select id="board_list" parameterType="int" resultType="board">
<![CDATA[
select * from
(select rownum rnum,BOARD_NUM,BOARD_NAME,BOARD_SUBJECT,BOARD_CONTENT,
BOARD_RE_REF,BOARD_RE_LEV,BOARD_RE_SEQ,BOARD_READCOUNT,
BOARD_DATE from
(select * from board53 order by BOARD_RE_REF desc,BOARD_RE_SEQ asc))
where rnum >= ((#{page}-1) * 10+1) and rnum <= (#{page} * 10)
]]>
</select>
- 두번째 서브쿼리를 별칭을 쓰지 않고 있으므로, 첫번째 서브쿼리에서 별칭.* 으로 처리하는 대신 일일히 두번째 서브쿼리의 모든 컬럼을 쓰고 있다
- board_re_ref 로 내림차순 정렬하고 두번째 정렬로 board_re_seq 를 오름차순으로 정렬함
+ 같은 부모를 가진 댓글들의 board_re_ref 는 부모의 board_re_ref 값으로 모두 같다
- < , > 를 인식하지 못하므로 대신해서 > %lt; 를 쓰거나 또는 전체를 CDATA 로 묶은 뒤 < , > 사용
- where 절에 있는 10 은 limit 를 의미함
- View 페이지로 이동
- board_list.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*"%>
<%@ page import="myspring.model.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>게시판 목록</title>
<link rel="stylesheet" href="<%=request.getContextPath() %>/css/bbs.css" type="text/css">
</head>
<body>
<!-- 게시판 리스트 -->
<div id="bbslist_wrap">
<h2 class="bbslist_title">게시판 목록</h2>
<div id="bbslist_c">글 개수 : ${listcount}</div>
<table id="bbslist_t">
<tr align="center" valign="middle" bordercolor="#333333">
<td style="font-family: Tahoma; font-size: 11pt;" width="8%"
height="26">
<div align="center">번호</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="47%">
<div align="center">제목</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
<div align="center">작성자</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="17%">
<div align="center">날짜</div>
</td>
<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
<div align="center">조회수</div>
</td>
</tr>
<!-- 화면 출력 번호 변수 정의 -->
<c:set var="num" value="${listcount-(page-1)*10}"/>
<!-- 반복문 시작 -->
<c:forEach var="b" items="${boardlist}">
<tr align="center" valign="middle" bordercolor="#333333"
onmouseover="this.style.backgroundColor='F8F8F8'"
onmouseout="this.style.backgroundColor=''">
<td height="23" style="font-family: Tahoma; font-size: 10pt;">
<!-- 번호 출력 부분 -->
<c:out value="${num}"/>
<c:set var="num" value="${num-1}"/>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="left">
<c:if test="${b.board_re_lev != 0}">
<c:forEach var="k" begin="1" end="${b.board_re_lev}">
</c:forEach>
<img src="./images/AnswerLine.gif">
</c:if>
<!-- 제목 출력 부분 -->
<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
${b.board_subject}
</a>
</div>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="center">${b.board_name}</div>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="center">${b.board_date}</div>
</td>
<td style="font-family: Tahoma; font-size: 10pt;">
<div align="center">${b.board_readcount}</div>
</td>
</tr>
</c:forEach>
<!-- 반복문 끝 -->
</table>
<div id="bbslist_paging">
<c:if test="${page <=1 }">
[이전]
</c:if>
<c:if test="${page > 1 }">
<a href="board_list.do?page=${page-1}">[이전]</a>
</c:if>
<c:forEach var="a" begin="${startpage}" end="${endpage}">
<c:if test="${a == page }">
[${a}]
</c:if>
<c:if test="${a != page }">
<a href="board_list.do?page=${a}">[${a}]</a>
</c:if>
</c:forEach>
<c:if test="${page >= maxpage }">
[다음]
</c:if>
<c:if test="${page < maxpage }">
<a href="board_list.do?page=${page+1}">[다음]</a>
</c:if>
</div>
<div id="bbslist_w">
<input type="button" value="글쓰기" class="input_button"
onclick="location='board_write.do?page=${page}'">
</div>
</div>
</body>
</html>
- 화면 출력 번호를 만들어서 forEach 루프가 돌아갈때마다 화면 출력번호를 출력 하고, 재정의해서 1씩 감소시킴
- 글 목록에서의 원문과 댓글의 제목 출력 형태를 다르게 함, 댓글의 깊이 board_re_lev만큼 루프를 돌려서 왼쪽 간격 띄움
+ 여기서는 [이전] 클릭시 이전 페이지 ${page-1} 로, [다음] 클릭시 다음 페이지 ${page+1} 로 이동
제목 클릭시 상세 페이지로 이동시 넘기는 값 (board_list.jsp 부분)
<!-- 제목 출력 부분 -->
<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
${b.board_subject}
</a>
- 상세페이지로 이동하려고 한다, "board_cont.do" 로 요청함
- 요청하면서 전달하는 값이 글 번호와 페이지 번호 외에도 state 라는 변수에 cont 라는 값을 저장해서 전달함
- 상세 페이지, 수정, 삭제 등 여러 기능을 1개의 요청으로 처리하기 위해서 state 값을 다르게 설정함
- Controller 클래스에서 "board_cont.do" 요청 처리 부분을 보자
/* 게시판 내용보기,삭제폼,수정폼,답변글폼 */
@RequestMapping(value = "/board_cont.do")
public String board_cont(@RequestParam("board_num") int board_num,
@RequestParam("page") String page,
@RequestParam("state") String state,
Model model) throws Exception {
if (state.equals("cont")) { // 내용보기일때만
boardService.hit(board_num); // 조회수 증가
}
BoardBean board = boardService.board_cont(board_num);
model.addAttribute("bcont", board);
model.addAttribute("page", page);
if (state.equals("cont")) {// 내용보기일때
return "board/board_cont";// 내용보기 페이지 설정
// String board_cont = board.getBoard_content().replace("\n",
// "<br/>");
// 글내용중 엔터키 친부분을 웹상에 보이게 할때 다음줄로 개행
// contM.addObject("board_cont", board_cont);
} else if (state.equals("edit")) {// 수정폼
return "board/board_edit";
} else if (state.equals("del")) {// 삭제폼
return "board/board_del";
} else if (state.equals("reply")) {// 답변달기 폼
return "board/board_reply";
}
return null;
}
- 상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청을 모두 이 @RequestMapping("/board_cont.do") 에서 처리한다
- state 값을 @RequestParam 으로 받아옴
- state 값이 "cont" 인 경우, 즉 상세페이지 요청인 경우에만 조회수 값을 증가시키고 있다
- 상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청은 글 번호와 페이지 번호를 전달하는 등의 같은 형식으로 되어있고 상세 정보를 구해오는 등의 비슷한 기능을 수행
- 그러므로 같은 요청으로 처리하고 다른 부분은 if-else if 문으로 state 값에 따른 다른 처리를 함