본문 바로가기
카테고리 없음

홈페이지 만들기 BoardProject

by 융기융 2024. 9. 13.
반응형

 

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

/* 다음 주소 API로 주소 검색하기 */

function findAddress() {
  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();
}

/* 주소 검색 버튼 클릭 시*/
document.querySelector("#findAddressBtn")
  .addEventListener("click", findAddress);

// 함수명만 작성하는 경우
// 함수명() 작성하는 경우 : 함수에 정의된 내용을 실행



// -------------------------------------------------------------------




// -------------------------------------------------------------------

/* ============= 유효성 검사(Validation) ============= */

// 입력 값이 유효한 형태인지 표시하는 객체(체크리스트)
const checkObj = {
  "memberNickname" : true
}

/* 닉네임 검사 */
// - 3글자 이상
// - 중복 X
const memberNickname = document.querySelector("#memberNickname");
memberNickname.addEventListener("input", () => {
  // input 이벤트 : 입력과 관련된 모든 동작(JS를 이용한 값세팅 제외)

  // 입력된 값 얻어오기(양쪽 공백제거)
  const inputValue = memberNickname.value.trim();

  if(inputValue.length < 3 ){ // 3글자 미만

    // 클래스 제거
    memberNickname.classList.remove("green");
    memberNickname.classList.remove("red");

    // 닉네임이 유효하지 않다고 기록
    checkObj.memberNickname = false;

    return;
  }

  // 비동기로 입력된 닉네임이
  // DB에 존재하는지 확인하는 Ajax 코드(fetch() API) 작성

  // get방식 요청 (쿼리스트링으로 파라미터 전달)
  fetch("/myPage/checkNickname?input=" + inputValue)
  .then(response => {
    if(response.ok){ // 응답 상태코드 200(성공)인 경우
      return response.text(); // 응답 결과를 text 형태로 변환
    }

    throw new Error("중복검사 실패 : " + response.status);
   
  })
  .then(result => {
    // result == 첫 번째 then에서 return 된 값

    if(result > 0){ // 중복인 경우
      memberNickname.classList.add("red");
      memberNickname.classList.remove("green");
      checkObj.memberNickname = false; // 체크리스트에 false 기록
      return;
    }

    // 중복이 아닌경우
    memberNickname.classList.add("green");
    memberNickname.classList.remove("red");
    checkObj.memberNickname = true; // 체크리스트에 ture 기록

  })
  .catch(err => console.error(err));



});

main.js

// 쿠키에 저장된 여러 값 중 key가 일치하는 value 반환
function getCookie(key){

  // 1. cookie 전부 얻어오기(string)
  const cookies = document.cookie; // "K=V;K=v;..."
  // console.log(cookies);

  // 2. ";" 을 구분자로 삼아서 배열형태로 쪼개기(split)
  const arr = cookies.split(";"); // ["K=V", "K=V"]
  // console.log(arr);

  // 3. 쪼개진 배열 요소를 하나씩 꺼내서
  //  JS 객체에 K:V 형태로 추가

  const cookieObj = {}; // 빈 객체 생성

  for(let entry of arr){
    // entry == "K=V"
    // -> "=" 기준으로 쪼개기(split)
    const temp = entry.split("="); // ["K", "V"]

    cookieObj[temp[0]] = temp[1];

  }

  // console.log(cookieObj);

  // 매개변수로 전달받은 key와 일치하는 value 반환
  return cookieObj[key];

}

// HTML 로딩(랜더링)이 끝난 후 수행
document.addEventListener("DOMContentLoaded", () => {
 
  const saveEmail = getCookie("saveEmail"); // 쿠키에 저장된 Email 얻어오기

  // 저장된 이메일이 없을경우
  if(saveEmail == undefined) return;

  const memberEmail =
    document.querySelector("#loginForm input[name=memberEmail]");

  const checkbox =
    document.querySelector("#loginForm input[name=saveEmail]");

  // 로그인 상태인 경우, 함수종료
  if(memberEmail == null) return;

  // 이메일 입력란에 저장된 이메일 출력
  memberEmail.value = saveEmail;

  // 이메일 저장 체크박스를 체크상태로 바꾸기
  checkbox.checked = true;

})

 

 

반응형