07_Framework
현재 문제점 :
로그인이 되지 않아도 주소창에 마이페이지 URL 입력 시, 마이페이지로 이동됨
해결 방법 : filter
-> 마이페이지로 요청이 왔을 때 로그인이 되어있지 않다면 거르는 기능(ex. 메인페이지로 다시 돌아가게끔 하기)
cf ) Dispatcher Servlet <-- intersepter --> Controller
package edu.kh.project.common.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
// * cf ) 코드 옮기는 법 : 코드 드래그 후 alt + 방향키
// Filter : 클라이언트의 요청/응답을 걸러내거나, 첨가하는 클래스
// 클라이언트 <-> Filter <-> Dispatcher Servlet
// @WebFilter : 해당 클래스를 필터로 등록하고, 지정된 주소로 요청이 올 때 마다 동작
// filterName : 순서 정할 때 사용하는 것 // urlPatterns : 지정된 주소
@WebFilter(filterName = "loginFilter", urlPatterns = {"/myPage/*"})
public class LoginFilter implements Filter {
public void init(FilterConfig fConfig) throws ServletException {
// 서버가 켜질 때 필터가 생성됨
// -> 생성 시 초기화 용도로 사용하는 메서드
System.out.println("--- 로그인 필터 생성 ---");
}
public void destroy() {
// 필터 코드가 변경 되었을 때
// 변경 이전 필터를 파괴하는 메서드
System.out.println("--- 이전 로그인 필터 파괴 ---");
}
// ServletRequest : HttpServletRequest의 부모
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 필터링 코드를 작성하는 메서드
// 1) ServletRequest, ServletResponse 다운 캐스팅
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse resp = (HttpServletResponse)response;
// 2) HttpServletRequest를 이용해서 HttpSession 얻어오기
HttpSession session = req.getSession();
// 3) session 에서 "loginMember" key를 가진 속성을 얻어와
// null인 경우 메인페이지로 redirect 시키기.
if ( session.getAttribute("loginMember") == null) {
resp.sendRedirect("/");
}
// 4) 로그인 상태인 경우 다음 필터 또는 DispatcherServlet으로 전달
else {
chain.doFilter(request, response);
}
}
}
로그인 하지 않고 myPage로 이동할 경우, 현재 페이지로 리다이렉트 됨
cf ) 일반 사용자 / 관리자인지 구분할 때 많이 사용 -> 참고하기
-> 상단에 꼭 추가하기!
과제 1)
내 정보 수정 후 수정하기 버튼 누르면 변경된 정보 db에 저장되고, 해당 페이지로 이동하기
- 현재 문제점:
js 조금 이상한 것 같다?!
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>마이페이지</title>
<link rel="stylesheet" href="/resources/css/myPage/myPage-style.css">
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp" />
<section class="myPage-content">
<jsp:include page="/WEB-INF/views/myPage/sideMenu.jsp" />
<section class="myPage-main">
<h1 class="myPage-title">내 정보</h1>
<span class="myPage-subject">원하는 회원 정보를 수정할 수 있습니다.</span>
<form action="info" method="POST" name="myPageFrm" id="updateInfo">
<div class="myPage-row">
<label>닉네임</label>
<input type="text" name="memberNickname" maxlength="10"
value="${loginMember.memberNickname}" id="memberNickname"
>
</div>
<div class="myPage-row">
<label>전화번호</label>
<input type="text" name="memberTel" maxlength="11"
value="${loginMember.memberTel}" id="memberTel"
>
</div>
<div class="myPage-row info-title">
<span>주소</span>
</div>
<%--
${fn:split(문자열, 구분자)}
문자열을 구분자로 나누어 배열 형태로 반환
상단에 taglib fn 추가해야 함
--%>
<%-- 04540^^^서울시 중구 남대문로 120^^^2층 --%>
<c:set var="addr" value="${fn:split(loginMember.memberAddress, '^^^')}" />
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="우편번호"
id="postcode" value="${addr[0]}"
>
<button type="button" onclick="postCode()">검색</button>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="도로명/지번 주소"
id="address" value="${addr[1]}"
>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="상세 주소"
id="detailAddress" value="${addr[2]}"
>
</div>
<button class="myPage-submit">수정하기</button>
</form>
</section>
</section>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp" />
<!-- 다음 주소 api 추가 -->
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
function postCode() {
new daum.Postcode({
oncomplete: function(data) {
// 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
// 각 주소의 노출 규칙에 따라 주소를 조합한다.
// 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
var addr = ''; // 주소 변수
//사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
if (data.userSelectedType === 'R') { // 사용자가 도로명 주소를 선택했을 경우
addr = data.roadAddress;
} else { // 사용자가 지번 주소를 선택했을 경우(J)
addr = data.jibunAddress;
}
// 우편번호와 주소 정보를 해당 필드에 넣는다.
document.getElementById('postcode').value = data.zonecode;
document.getElementById("address").value = addr;
// 커서를 상세주소 필드로 이동한다.
document.getElementById("detailAddress").focus();
}
}).open();
}
</script>
<script src="../../../resources/js/myPage/myPage.js"></script>
</body>
</html>
// 내 정보(수정) 페이지
const memberNickname = document.getElementById("memberNickname");
const memberTel = document.getElementById("memberTel");
const updateInfo = document.getElementById("updateInfo");
// 내 정보 수정 form태그가 존재할 때 (내 정보 페이지)
if(updateInfo != null){
// 전역변수로 수정 전 닉네임/전화번호를 저장
initNickname = memberNickname.value;
initTel = memberTel.value;
// 닉네임 유효성 검사
memberNickname.addEventListener("input", () => {
// 변경 전 닉네임과 입력 값이 같을 경우
if(memberNickname.value == initNickname){
memberNickname.removeAttribute("style");
return;
}
// 정규 표현식으로 유효성 검사
const regEx = /^[가-힣\w\d]{2,10}$/;
if(regEx.test(memberNickname.value)){ // 유효
memberNickname.style.color = "green";
}else{ // 무효
memberNickname.style.color = "red";
}
});
// 전화번호 유효성 검사
memberTel.addEventListener("input", () => {
// 변경 전 전화번호와 입력 값이 같을 경우
if(memberTel.value == initTel){
memberTel.removeAttribute("style");
return;
}
// 정규 표현식으로 유효성 검사
const regEx = /^0(1[01679]|2|[3-6][1-5]|70)[1-9]\d{2,3}\d{4}$/;
if(regEx.test(memberTel.value)){ // 유효
memberTel.style.color = "green";
}else{ // 무효
memberTel.style.color = "red";
}
});
// form태그 제출 시 유효하지 않은 값이 있으면 제출 X
updateInfo.addEventListener("submit", e => {
// 닉네임이 유효하지 않을 경우
if(memberNickname.style.color == "red"){
alert("닉네임이 유효하지 않습니다.");
memberNickname.focus(); // 포커스 이동
e.preventDefault(); // 기본 이벤트 제거
return;
}
// 전화번호가 유효하지 않을 경우
if(memberTel.style.color == "red"){
alert("전화번호가 유효하지 않습니다.");
memberTel.focus(); // 포커스 이동
e.preventDefault(); // 기본 이벤트 제거
return;
}
});
} // if end
// 비밀번호 변경 제출 시
const changePwFrm = document.getElementById("changePwFrm");
const currentPw = document.getElementById("currentPw");
const newPw = document.getElementById("newPw");
const newPwConfirm = document.getElementById("newPwConfirm");
if(changePwFrm != null){ // 비밀번호 변경 페이지인 경우
changePwFrm.addEventListener("submit", e => {
// 현재 비밀번호 미작성 시
if(currentPw.value.trim() == ""){
alert("현재 비밀번호를 입력해주세요");
e.preventDefault();
currentPw.focus();
return;
}
// 비밀번호 유효성 검사
const regEx = /^[a-zA-Z0-9\!\@\#\-\_]{6,20}$/;
if(!regEx.test(newPw.value)){
alert("비밀번호가 유효하지 않습니다");
e.preventDefault();
newPw.focus();
return;
}
// 비밀번호 == 비밀번호 확인
if(newPw.value != newPwConfirm.value){
alert("비밀번호가 일치하지 않습니다");
e.preventDefault();
newPwConfirm.focus();
return;
}
});
}
const secessionFrm = document.getElementById("secessionFrm");
if(secessionFrm != null){ // 탈퇴 페이지인 경우
const memberPw = document.getElementById("memberPw");
const agree = document.getElementById("agree");
secessionFrm.addEventListener("submit", e => {
if(memberPw.value.trim() == ""){ // 비밀번호 미작성
alert("비밀번호를 작성해주세요");
e.preventDefault();
memberPw.focus();
return;
}
if(!agree.checked){ // 동의 체크가 되지 않은 경우
alert("약관 동의 후 탈퇴 버튼을 눌러주세요");
e.preventDefault();
agree.focus();
return;
}
if(!confirm("정말 탈퇴 하시겠습니까?")){ // 취소 클릭 시
alert("탈퇴 취소");
e.preventDefault();
return;
}
});
}
//----------------------------------------------------
// 프로필 이미지 추가/변경/삭제
const profileImage = document.getElementById("profileImage"); // img 태그
const imageInput = document.getElementById("imageInput"); // input 태그
const deleteImage = document.getElementById("deleteImage"); // x버튼
let initCheck; // 초기 프로필 이미지 상태를 저장하는 변수
// false == 기본 이미지, true == 이전 업로드 이미지
let deleteCheck = -1;
// 프로필 이미지가 새로 업로드 되거나 삭제 되었음을 나타내는 변수
// -1 == 초기값 , 0 == 프로필 삭제(x버튼), 1 == 새 이미지 업로드
let originalImage; // 초기 프로필 이미지 파일 경로 저장
if(imageInput != null){ // 화면에 imageInput이 있을 경우 ( if 굳이 안해도 되긴 함 )
// 프로필 이미지가 출력되는 img태그의 src 속성을 저장
originalImage = profileImage.getAttribute("src");
// 회원 프로필 화면 진입 시
// 현재 회원의 프로필 이미지 상태를 확인
if(profileImage.getAttribute("src") == "/resources/images/user.png"){
// 기본 이미지인 경우
initCheck = false;
}else{
initCheck = true;
}
// change 이벤트 : 값이 변했을 때
// - input type="file", "checkbox", "radio" 에서 많이 사용
// - text/number 형식 사용 가능
// -> 이 때 input값 입력 후 포커스를 잃었을 때
// 이전 값과 다르면 change 이벤트 발생
imageInput.addEventListener("change", e => {
// 2MB로 최대 크기 제한
const maxSize = 1 * 1024 * 1024 * 2; // 파일 최대 크기 지정(바이트 단위)
console.log(e.target); // input
console.log(e.target.value); // 업로드된 파일 경로
console.log(e.target.files); // 업로드된 파일의 정보가 담긴 배열
const file = e.target.files[0]; // 업로드한 파일의 정보가 담긴 객체
// 파일을 한번 선택한 후 취소했을 때 ( file이 undefined가 된다 )
if(file == undefined){
console.log("파일 선택이 취소됨");
deleteCheck = -1; // 취소 == 파일 없음 == 초기상태
// 취소 시 기존 프로필 이미지로 변경 ( 기존 이미지에서 변경되는게 없게 하겠다는거죠 )
profileImage.setAttribute("src", originalImage);
return;
}
if( file.size > maxSize){ // 선택된 파일의 크기가 최대 크기를 초과한 경우
alert("2MB 이하의 이미지를 선택해주세요.");
imageInput.value = "";
// input type="file" 태그에 대입할 수 있는 value는 "" (빈칸) 뿐이다!
deleteCheck = -1; // 취소 == 파일 없음 == 초기상태
// 기존 프로필 이미지로 변경
profileImage.setAttribute("src", originalImage);
return;
}
// JS에서 파일을 읽는 객체
// - 파일을 읽고 클라이언트 컴퓨터에 파일을 저장할 수 있음
const reader = new FileReader();
reader.readAsDataURL(file);
// 매개변수에 작성된 파일을 읽어서 저장 후
// 파일을 나타내는 URL을 result 속성으로 얻어올 수 있게 함.
// 다 읽었을 때
reader.onload = e => {
//console.log(e.target);
console.log(e.target.result); // 읽은 파일의 URL
/* 개발자도구에서 Application탭에서 Frames > top > images 안에서 확인가능 */
const url = e.target.result;
// 프로필이미지(img) 태그에 src 속성으로 추가
profileImage.setAttribute("src", url);
deleteCheck = 1;
}
});
// x버튼 클릭 시
deleteImage.addEventListener('click', () => {
imageInput.value = ""; // input type="file"의 value 삭제
profileImage.setAttribute("src", "/resources/images/user.png");
// 프로필 이미지를 기본 이미지로 변경
deleteCheck = 0;
});
// #profileFrm이 제출 되었을 때
document.getElementById("profileFrm").addEventListener("submit", e => {
// initCheck
// 초기 프로필 이미지 상태를 저장하는 변수
// false == 기본 이미지, true == 이전 업로드 이미지
// deleteCheck
// 프로필 이미지가 새로 업로드 되거나 삭제 되었음을 나타내는 변수
// -1 == 초기값 , 0 == 프로필 삭제(x버튼), 1 == 새 이미지 업로드
let flag = true; // 제출하면 안되는 경우의 초기값 플래그 true로 지정
// 이전 프로필 이미지가 없으면서, 새 이미지 업로드를 했다 -> 처음으로 이미지 추가
if(!initCheck && deleteCheck == 1) flag = false;
// 이전 프로필 이미지가 있으면서, 새 이미지 업로드를 했다 -> 새 이미지로 변경
if(initCheck && deleteCheck == 1) flag = false;
// 이전 프로필 이미지가 있으면서, 프로필 삭제 버튼을 눌렀다 -> 삭제
if(initCheck && deleteCheck == 0) flag = false;
if(flag){ // flag == true -> 제출하면 안되는 경우
e.preventDefault(); // form 기본 이벤트 제거
alert("이미지 변경 후 클릭하세요");
}
return true;
});
}
package edu.kh.project.myPage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.member.model.dto.Member;
import edu.kh.project.myPage.MypageService;
@SessionAttributes({"loginMember"})
// 1) Model에 세팅된 값의 key와 {} 작성된 값이 일치하면 session scope로 이동
// 2) Session으로 올려둔 값을 해당 클래스에서 얻어와 사용 가능하게함
// -> @SessionAttribute(key)로 사용
@RequestMapping("/myPage")
@Controller
public class MyPageController {
@Autowired
private MypageService service;
// 내 정보 페이지로 이동
@GetMapping("/info")
public String info() {
return "myPage/myPage-info";
}
// 프로필 페이지 이동
@GetMapping("/profile")
public String profile() {
return "myPage/myPage-profile";
}
// 비밀번호 변경 페이지 이동
@GetMapping("/changePw")
public String changePw() {
return "myPage/myPage-changePw";
}
// 탈퇴 페이지 이동
@GetMapping("/secession")
public String secession() {
return "myPage/myPage-secession";
}
// 회원 정보 수정
@PostMapping("/info")
public String updateInfo(@SessionAttribute("loginMember") Member loginMember,
Member updateMember,
String[] memberAddress,
RedirectAttributes ra) {
/*
* @SessionAttribute("loginMember") Member loginMember
* : Session에서 얻어온 "loginMember"에 해당하는 객체를
* 매개변수 Member loginMember에 저장
*
*
* Member updateMember
* : 수정할 닉네임, 전화번호 담긴 커맨드 객체
*
*
* String[] memberAddress
* : name="memberAddress"인 input 3개의 값(주소)
*
*
* RedirectAttributes ra : 리다이렉트 시 값 전달용 객체
*
* */
// 주소 하나로 합치자 (우편번호^^^주소^^^상세주소)
if(updateMember.getMemberAddress().equals(",,")) {
updateMember.setMemberAddress(null);
} else {
// updateMember 에 주소문자열 세팅
String addr = String.join("^^^", memberAddress);
updateMember.setMemberAddress(addr);
}
// 로그인한 회원의 번호를 updateMember에 세팅
updateMember.setMemberNo( loginMember.getMemberNo() );
// DB 회원 정보 수정 (update) 서비스 호출
int result = service.updateInfo(updateMember);
String message = null;
// 결과값으로 성공
if(result > 0) {
// -> 성공 시 Session에 로그인된 회원 정보도 수정(동기화)
System.out.println("내 정보 수정 성공"); // 확인용 콘솔창
loginMember.setMemberNickname( updateMember.getMemberNickname() );
loginMember.setMemberTel( updateMember.getMemberTel() );
loginMember.setMemberAddress( updateMember.getMemberAddress() );
message = "회원 정보 수정 성공";
} else {
// 실패에 따른 처리
System.out.println("내 정보 수정 실패"); // 확인용 콘솔창
message = "회원 정보 수정 실패";
}
ra.addFlashAttribute("message", message);
return "redirect:info"; // 상대경로 (절대경로로 작성하려면? /myPage/info)
}
}
package edu.kh.project.myPage;
import edu.kh.project.member.model.dto.Member;
public interface MypageService {
int updateInfo(Member updateMember);
}
package edu.kh.project.myPage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import edu.kh.project.member.model.dto.Member;
@Service
public class MypageServiceImpl implements MypageService{
@Autowired
private myPageDAO dao;
@Transactional
@Override
public int updateInfo(Member updateMember) {
return dao.updateInfo(updateMember);
}
}
package edu.kh.project.myPage;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import edu.kh.project.member.model.dto.Member;
@Repository
public class myPageDAO {
@Autowired
private SqlSessionTemplate sqlSession;
public int updateInfo(Member updateMember) {
return sqlSession.update("myPageMapper.updateInfo", updateMember);
}
}
<?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="myPageMapper">
<!-- 회원 정보 수정 -->
<update id="updateInfo" parameterType="Member">
UPDATE "MEMBER" SET
MEMBER_NICKNAME = #{memberNickname},
MEMBER_TEL = #{memberTel},
MEMBER_ADDR = #{memberAddress}
WHERE MEMBER_NO = #{memberNo}
</update>
</mapper>
수정할 닉네임, 전화번호, 주소 입력 후 수정하기 버튼 클릭 시, 해당 alert창 뜸
바뀐 내 정보가 화면에 보임
( 수정된 DB 정보 확인 )
과제 2) 비밀번호 변경, 회원탈퇴
기한 : 주말까지
Tip! 비밀번호 변경 -> 암호화 거치기
Tip! 회원탈퇴 -> 쿠키 삭제해야함