MemberControll
package edu.kh.project.member.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.member.dto.Member;
import edu.kh.project.member.service.MemberService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@SessionAttributes({"loginMember"})
@Controller // 요청/응답 제어 역할 명시 + Bean 등록 (IOC)
@RequestMapping("member") // /member로 시작하는 요청 매핑
@Slf4j // log 필드 자동생성 Lombok 어노테이션
public class MemberController {
@Autowired // 등록된 Bean 중에서 같은 타입의 Bean 을 대입! 의존성 주입(DI)
private MemberService service;
/** 로그인 컨트롤러!
* @param memberEmail : 제출된 이메일
* @param memberPw : 제출된 비밀번호
* @param saveEmail : 이메일 저장여부(체크 안하면 null)
* @param ra : 리다이렉트 시 requset scope로 값 전달하는 객체
* @param model : 데이터 전달용 객체(기본값 request scope)
* @param resp : 응답 방법을 제공하는 객체
* @return
*/
@PostMapping("login")
public String login(
@RequestParam("memberEmail") String memberEmail,
@RequestParam("memberPw") String memberPw,
@RequestParam(name = "saveEmail", required = false) String saveEmail,
// RequestParam 속성이 들어있음 require -> true 인데 false면 필수가 아니란뜻
RedirectAttributes ra,
Model model,
HttpServletResponse resp) {
// log.debug("memberEmail : {}", memberEmail);
// log.debug("memberPw : {}", memberPw);
// 로그인 서비스 호출
Member loginMember = service.login(memberEmail, memberPw);
if(loginMember == null) { //로그인 실패
ra.addFlashAttribute("message", "이메일 또는 비밀번호가 일치하지 않습니다");
}else{ // 로그인 성공
// loginMember를 session scope 에 추가
// 방법1) HttpSession 이용
// 방법2) @SessionAttributes + Model 이용방법
/* Model을 이용해서 Session scope에 값 추가하는 방법 */
// 1. model에 값 추가
model.addAttribute("loginMember", loginMember);
// 2. 클래스 선언부 위에 @SessionAttributes({"key"}) 추가
// -> key 값은 model에 추가된 key값 "loginMember" 작성
// (request -> session)으로 바뀜
// @SessionAttributes :
// Model에 추가된 값 중 session scope로 올리고 싶은 값의
// key를 작성하는 어노테이션
// ---------------------------------------------------------
/* 이메일 저장코드(Cookie) */
// 1. Cookie 객체 생성(K:V)
Cookie cookie = new Cookie("saveEmail", memberEmail);
// 2. 만들어진 Cookie 사용될 경로(url)
cookie.setPath("/"); // localhost 또는 현재 ip 이하 모든 주소
// 3. Cookie가 유지되는 시간(수명) 설정
if(saveEmail == null) { // 체크 X
cookie.setMaxAge(0); // 만들어지자마자 만료
// == 기존에 쿠키가 있으면 덮어씌우고 없어짐
}else { // 체크 O
cookie.setMaxAge(60 * 60 * 24 * 30); // 30일 초 단위로 작성
}
// 4. resp 객체에 추가해서 클라이언트에게 전달
resp.addCookie(cookie);
// ---------------------------------------------------------
}
return "redirect:/"; // 메인페이지 리다이렉트
}
/** 로그아웃
* @param status
* @return
*/
@GetMapping("logout")
public String logout(SessionStatus status) {
/* SessionStatus
* - @SessionAttributes 를 이용해 등록된 객체(값)의 상태를
* 관리하는 객체
*
* - SessionStatus.setComplete();
* -> 세션 상태 완료 == 없앰(만료)
*/
status.setComplete();
return "redirect:/"; // 메인페이지
}
}
/* Cookie란?
* - 클라이언트 측(브라우저)에서 관리하는 데이터(파일 형식)
*
* - Cookie에는 만료기간, 데이터(key=value), 사용하는 사이트(주소)
* 가 기록되어 있음
*
* - 클라이언트가 쿠키에 기록된 사이트로 요청으로 보낼 때
* 요청에 쿠키가 담겨져서 서버로 넘어감
*
* - Cookie의 생성, 수정, 삭제는 Server가 관리
* 저장은 Client가 함
*
* - Cookie는 HttpServletResponse를 이용해서 생성,
* 클라이언트에게 전달(응답) 할 수 있다
*/
MemberMapper
package edu.kh.project.member.mapper;
import org.apache.ibatis.annotations.Mapper;
import edu.kh.project.member.dto.Member;
// @Mapper
// - Mybatis 제공 어노테이션(컴파일러에게 지시를 내림)
// - 해당 인터페이스를 상속받은 클래스 자동구현 + Bean 등록
@Mapper
public interface MemberMapper {
/** memberEmail이 일치하는 회원정보 조회
* @param memberEmail
* @return loginMember 또는 null
*/
Member login(String memberEmail);
}
MemberService
import edu.kh.project.member.dto.Member;
public interface MemberService {
/** 로그인 서비스
* @param memberEmail
* @param memberPw
* @return loginMember 또는 null(email 또는 pw 불일치)
*/
Member login(String memberEmail, String memberPw);
}
MemberServiceImpl
package edu.kh.project.member.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import edu.kh.project.member.dto.Member;
import edu.kh.project.member.mapper.MemberMapper;
import lombok.extern.slf4j.Slf4j;
// 왜 Service 인터페이스 상속 받을까?
// - 팀 프로젝트, 유지보수에 굉장히 도움이 많이 되기 때문에!
// + AOP Proxy 적용을 위해서
@Slf4j
@Service // 비즈니스 로직을 처리하는 역할 명시 + Bean 등록(IOC)
public class MemberServiceImpl implements MemberService{
@Autowired // 등록된 Bean 중에서 같은 타입의 Bean을 대입(DI) dependency injection 의존성 주입
private MemberMapper mapper;
@Autowired // BCrypt 암호화 객체 의존성 주입 받기
private BCryptPasswordEncoder encoder;
/** 비밀번호 암호화
* - 하는 이유 : 평문 상태로 비밀번호 저장하면 안됨!
*
* - 아주 옛날 방식 : 데이터 -> 암호화,
* 암호화된 데이터 -> 복호화 -> 원본 데이터
*
* - 약간 과거 또는 현재 : 데이터를 암화화만 가능(SHA 방식)
* -> 복호화 방법 제공 X
*
* -> 마구잡이로 대입해서 만들어진 암호화 데이터 테이블에 뚫림
*
* - 요즘 많이 사용하는 방식 : BCrypt 암호화 (Spring security)
*
* - 입력된 문자열(비밀번호)에 salt를 추가한 후 암호화
* -> 암호화 할 때 마다 결과가 다름
* -> DB에 입력받은 비밀번호를 암호화해서 넘겨줘도
* 비교 불가능!!
* -> Bcrypt 가 함꼐 제공하는 평문, 암호화 데이터 비교 메서드잉ㄴ
* matches()를 이용하면 된다! (같으면 true, 다르면 false)
*
* --> matches() 메서드는 자바에서 동작하는 메서드
* -> DB에 저장된 암호화된 비밀번호를 조회해서 가져와야 한다!
*/
// 로그인 서비스
@Override
public Member login(String memberEmail, String memberPw) {
// 암호화 테스트
// log.debug("memberPw : {}", memberPw);
// log.debug("암호화된 memberPw : {}", encoder.encode(memberPw) );
// 1. memberEmail이 일치하는 회원의 정보를 DB에서 조회
// (비밀번호 포함!)
Member loginMember = mapper.login(memberEmail);
// 2. 이메일(id)이 일치하는 회원정보가 없을 경우
if(loginMember == null) return null;
// 3. DB에서 조회된 비밀번호와 입력받은 비밀번호가 같은지 확인
// log.debug("비밀번호 일치? : {}",
// encoder.matches(memberPw, loginMember.getMemberPw()));
// 입력받은 비밀번호와 DB에서 조회된 비밀번호가 일치하지 않을 때
if( !encoder.matches(memberPw, loginMember.getMemberPw()) ) {
return null;
}
// 4. 로그인 결과 반환
return loginMember;
}
}
MyPageController
package edu.kh.project.myPage.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
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.dto.Member;
import edu.kh.project.myPage.service.MyPageService;
// @SessionAttribute"s" 용도
// 1. Model을 이용하여 값을 request -> session으로 scope 변경
// 2. @SessionAttribute를 이용해
// @SessionAttribute"s"에 의해서 session에 등록된 값을
// 얻어올 수 있음
@SessionAttributes({"loginMember"})
@Controller
@RequestMapping("myPage")
public class MyPageController {
@Autowired // DI
private MyPageService service;
/** 마이페이지(내 정보) 전환
* @param loginMember : 세션에 저장된 로그인한 회원정보
* @return model : 데이터 전달용 객체(request)
*/
@GetMapping("info")
public String info(
@SessionAttribute("loginMember") Member loginMember,
Model model) {
// 로그인 회원정보에 주소가 있을경우
if(loginMember.getMemberAddress() != null) {
// 주소를 , 기준으로 쪼개서 String[] 형태로 반환
String[] arr
= loginMember.getMemberAddress().split(",");
// "04540,서울 중구 남대문로 120,2층"
// -> {"04540", "서울 중구 남대문로 120", "2층"}
model.addAttribute("postcode" , arr[0]);
model.addAttribute("address" , arr[1]);
model.addAttribute("detailAddress", arr[2]);
}
return "myPage/myPage-info";
}
/** 내 정보 수정
* @param inputMember : 수정할 닉네임, 전화번호, 주소
* @param loginMember : 현재 로그인된 회원정보
* session에 저장된 Member 객체의 주소가 반환됨
* == session 에 저장된 Member 객체의 데이터를 수정할 수 있음
* @param ra : 리다이렉트시 request scope로 값 전달
* @return
*/
@PostMapping("info")
public String updateInfo(
@ModelAttribute Member inputMember,
@SessionAttribute("loginMember") Member loginMember,
RedirectAttributes ra) {
// @SessionAttribute("key")
// - @SessionAttribute"s"를 통해 session에 올라간 값을 얻어오는
// 어노테이션
// - 사용방법
// 1) 클래스 위에 @SessionAttribute"s" 어노테이션을 작성하고
// 해당 클래스에서 꺼내서 사용할 값의 key를 작성
// --> 그럼 세션에서 값을 미리 얻어와 놓음
// 2) 필요한 메서드 매개변수에
// @SessionAttribute("key")를 작성하면
// 해당 key와 일치하는 session 값을 얻어와서 대입
// 1. inputMember에 로그인된 회원번호를 추가
int memberNo = loginMember.getMemberNo();
inputMember.setMemberNo(memberNo);
// 2. 회원정보 수정 서비스 호출 후 결과 반환받기
int result = service.updateInfo(inputMember);
// 3. 수정결과에 따라 message 지정
String message = null;
if(result > 0) {
message = "수정 성공!!";
// 4. 수정 성공시
// session에 저장된 로그인 회원정보를
// 수정값으로 변경해서 DB와 같은 데이터를 가지게함
// == 동기화
loginMember.setMemberNickname(inputMember.getMemberNickname());
loginMember.setMemberTel (inputMember.getMemberTel());
loginMember.setMemberAddress (inputMember.getMemberAddress());
}
else {
message = "수정 실패..";
}
ra.addFlashAttribute("message", message);
// -> footer.html 조각에서 alert() 수행
return "redirect:info"; // /myPage/info GET방식 요청
}
/** (비동기) 닉네임 중복검사
* @param input
* @return 0 : 중복 X / 1 : 중복 O
*/
@ResponseBody // 응답 본문(ajax 코드)에 값 그대로 반환
@GetMapping("checkNickname")
public int checkNickname(@RequestParam("input") String input) {
return service.checkNickname(input);
}
}
MypageService
package edu.kh.project.myPage.service;
import edu.kh.project.member.dto.Member;
public interface MyPageService {
/** 회원 정보수정
* @param inputMember
* @return result
*/
int updateInfo(Member inputMember);
/** 닉네임 중복검사
* @param input
* @return result
*/
int checkNickname(String input);
}
MyPageServiceImpl
package edu.kh.project.myPage.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import edu.kh.project.member.dto.Member;
import edu.kh.project.myPage.mapper.MyPageMapper;
@Transactional // 서비스 내 메서드 수행 중
// UnCheckedException 발생 시 rollback 수행
// 아니면 메서드 종료시 commit 수행
@Service // Service 역할 명시 + Bean 등록
public class MyPageServieImpl implements MyPageService{
@Autowired // 등록된 Bean 중에서 같은 자료형의 Bean을 의존성 주입(DI)
private MyPageMapper mapper;
@Override
public int updateInfo(Member inputMember) {
// 만약 주소가 입력되지 않은 경우(,,) null로 변경
if(inputMember.getMemberAddress().equals(",,")) {
inputMember.setMemberAddress(null);
// UPDATE 구문 수행 시 MEMBER_ADDRESS 컬럼 값이 NULL이 됨!
}
// SQL 수행 후 결과 반환받기 // DML은 트랜잭션 무조건!!!!
return mapper.updateInfo(inputMember);
}
// 닉네임 중복검사
@Override
public int checkNickname(String input) {
return mapper.checkNickname(input);
}
}
MyPageMapper
package edu.kh.project.myPage.mapper;
import org.apache.ibatis.annotations.Mapper;
import edu.kh.project.member.dto.Member;
@Mapper // 상속받은 클래스 구현 + Bean 등록
public interface MyPageMapper {
/** 회원정보 수정
* @param inputMember
* @return
*/
int updateInfo(Member inputMember);
/** 닉네임 중복 검사
* @param input
* @return
*/
int checkNickname(String input);
}
member-mapper
<?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 속성 값으로 Mapper 인터페이스의 패키지명 + 인터페이스명 작성
-->
<mapper namespace="edu.kh.project.member.mapper.MemberMapper">
<!-- <cache-ref namespace=""/> 항상 지우기-->
<!-- [TIP]
parameterType 속성은 필수가 아니다!
-> Mybatis TypeHandler가 파라미터의 타입을 알아서 판별할 수 있다!
** parameterType 잘 쓰던가, 쓰지말던가!
단, <select> 태그에서 resultType은 필수 !!!
(<insert>, <update>, <delete>는 resultType 작성 불가!)
-->
<!-- 로그인 -->
<!-- Member == 별칭 (DBConfig 참고) -->
<select id="login" resultType="Member">
SELECT
MEMBER_NO,
MEMBER_EMAIL,
MEMBER_NICKNAME,
MEMBER_PW,
MEMBER_TEL,
MEMBER_ADDRESS,
PROFILE_IMG,
AUTHORITY,
TO_CHAR(ENROLL_DATE,
'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') ENROLL_DATE
FROM "MEMBER"
WHERE MEMBER_EMAIL = #{memberEmail}
AND MEMBER_DEL_FL = 'N'
</select>
<!-- MEMBER_DEL_FL = 'N' -> 탈퇴하지 않은 회원 == 정상회원 -->
</mapper>
myPage-mapper
<?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="edu.kh.project.myPage.mapper.MyPageMapper">
<!-- parameterType은 작성하지 않아도
TypeHandler가 자동으로 인식!! -->
<!-- 회원정보수정 -->
<update id="updateInfo">
UPDATE "MEMBER"
SET
MEMBER_NICKNAME = #{memberNickname},
MEMBER_TEL = #{memberTel},
MEMBER_ADDRESS = #{memberAddress}
WHERE
MEMBER_NO = #{memberNo}
</update>
<!-- *** select 태그 resultType||Map 필수 !! *** -->
<!-- 닉네임 중복검사 -->
<select id="checkNickname" resultType="_int">
SELECT COUNT(*)
FROM "MEMBER"
WHERE MEMBER_NO > 0
AND MEMBER_NICKNAME = #{input}
</select>
<!-- WHERE MEMBER_NO > 0
인덱스 객체를 사용해서 검색속도 향상시키기
-->
</mapper>
Mypage.js
main.js