국비교육(22-23)

62일차(1)/Spring Boot(12) : 로그인 폼 css추가, 로그인정보 저장 기능 구현

서리/Seori 2023. 1. 5. 00:16

62일차(1)/Spring Boot(12) : 로그인 폼 css추가, 로그인정보 저장 기능 구현

 

- 지난 코드 리뷰 (custom.properties, ResponseEntity)

- 모바일 App 개발 간단 소개

- 로그인 폼 css추가

- 로그인 폼 로그인정보 저장 기능 구현

 

 

- Boot07 프로젝트는 레거시 프로젝트와의 호환성을 고려해서 만듦!

- master 브랜치 이후의 커밋은 boot의 기능을 적극 활용하여 만든 기능

 

 

- spring boot에서는 src-main-webapp이 디폴트값으로 존재하지 않는다.

- java jsp 페이지를 활용하려면 폴더를 따로 만들어야 한다.

- webapp 안에 직접 폴더를 구성해줌으로서 jsp 페이지를 사용할 수 있다.

 

- 전에 사용하던 resources폴더는 경로만 안다면 직접 요청해서 사용할 수 있다.

(controller를 거치지 않고 직접 받아갈 수 있게 하는 내용을 담아둠.)

 

- context path 의 최상위경로 기준으로

 boot07/resources 폴더 안에 있는것들은 경로만 맞으면 클라이언트가 직접 요청을 통해서 받아갈 수 있다.

 

 

- resources 처럼 사용할 수 있는 spring의 폴더가 있다. static !

- /boot07/ xxx  으로 경로뒤에 바로 파일명이 들어간다.

- 정적인 자원을 여기에 넣어서 사용해도 된다.

- 단, 이곳은 jsp는 사용할 수 없다. 해석되지 않는다.

 

- resources 폴더를 만든다고 무조건 동작하는것은 아니고,

 WebConfig에서 WebMvcConfigurer 인터페이스를 구현해서 

 addResourceHandlers 메소드를 override해서 저 내용을 넣어주어야 한다.

- 이 내용이 있어야 resources 폴더 안에 있는 것들을 요청 없이 사용할 수 있다.

 


 

- 파일 자료실에서 원래 upload 폴더 안에 자료실의 업로드된 이미지, 갤러리, 프로필 이미지 등을 저장했는데,

 

- 이것을 custom으로 다른 위치로 옮겨주었다.

- 출력되는 이미지파일을 여기에 다 업로드하기로 한 것!

 

- 이 위치는 동적으로 바뀔 가능성이 크다. 나중에 서비스를 배포하면 새 폴더가 지정되어야 할 것이다.

- 그러면 그때 전체 파일을 수정하는것이 아니라, 이 파일만 수정하면 되도록 클래스와 분리해 놓은 것이다.

- 필요에 의해서 새로 만든 custom property이다.

 

- 커스텀 property는 메인클래스에 이렇게 @PropertySource 설정해놓아야 쓸 수 있다.

 

 

- 로딩되었을 때 java 객체에서 이것을 읽어내는 방법은?

- @Value("${file.location}") 한 다음 필드를 선언한다.

- 업로드된 파일을 어떤위치에 저장해두고 그것을 읽어오는 구조를 만들었다.

 

- 여러곳에 배포하더라도 저 custom.properties만 수정하면 된다.

- 저 값은 서버 환경에 맞춰서 거의 무조건 바뀌는 값이다.

 클래스를 수정하지 않고 이것만 수정해도 되기 때문에 따로 만들어놓은 것!

 

 

GalleryServiceImpl

- @Autowired와 똑같이 @Value 도 주입받아서 사용

- 서비스도 특정 어노테이션으로 Spring 의 bean이 되어 있어야 한다.

 


 

- 웹 프로그래밍은 클라이언트와 서버 간의 대화를 프로그래밍하는 것이다.

 

- Client 는 브라우저, Server는 Boot application

- 지금은 같은 pc로 연습하지만, 사실 두가지는 멀리 떨어져 있다.

 

 

- 여기서 보이는 이 이미지의 출처는?

→ 서버의 파일시스템에 있는 이미지를 응답받아서 브라우저 화면상으로 보고 있는 것.

→ 서버 컴퓨터의 파일 시스템 > Server App 을 거쳐서 > 브라우저로 출력

 

- 이미지가 웹앱 안에 있으면 클라이언트가 직접 요청을 통해 받아갈 수도 있다.

- context 경로 하위에 요청함으로써 경로를 구성해주면 빋아갈 수 있다.

 

 

- 하지만 파일시스템 안에 있는 이미지는 요청경로를 통해서 접근이 불가능하다.

- 그래서 요청이 들어오면 파일시스템 안의 이미지를 읽어서 응답해줄 컨트롤러가 필요한 것이다.

 

- 이 경로에 저장된 이미지의 이름만 잘 전달해주면

 경로변수(pathVariable)를 읽어내서 해당 이미지를 직접 읽어서 이미지 자체를 @ResponseBody 로서 응답한다.

- 그래서 이미지가 전달되는(읽을 수 있는) 것이다.

 

 

- 페이지 소스에서 모양은 이렇게 보이지만 c:\\data 폴더 안에 있다.

- 이 이미지를 위한 컨트롤러를 만드는 것!

- 경로변수를 사용해서 해당 파일에 빨대를 꽂는 데 활용ㅎㅎ 정확한 위치에 빨대를 꽂아서 쭉 읽어들이는 것이다.

- 바이트 알갱이를 배열에 담아서 리턴하면, 웹브라우저는 그 바이트 알갱이를 담아서 이미지로 만들어준다.

 

 

- 서버가 바이트 알갱이를 보내주면 브라우저가 이것이 어떤 타입의 파일인지는 어떻게 알 수 있을까?

- 바이트 알갱이는 문자열일수도, 동영상일수도, 워드 문서일수도, 이미지일 수도 있다.

- 그래서 웹브라우저에게 이것이 무슨 바이트 알갱이인지 정보를 주는 것이 필요하다.

→ 그래서 produces 를 사용한다.

 

- 이 메소드가 jsp, png, gif를 생산한다는 것을 알려준 것.

- 이렇게 MediaType을 전달하면 저 정보가 응답 헤더에 자동으로 붙는다.

 

- 이렇게 주소창에 직접 입력하면 웹브라우저가 바이트 알갱이를 해석해서 이미지로 구성해 준다.

 

 

- GetMapping에서 produces를 지워보고 테스트했더니 이렇게 나온다.

- 정보가 없으면 보통 문자열로 해석한다.

- 이미지로 된 byte 알갱이를 문자열로 간주하고 해석했더니 이렇게 나온 것이다.

- 간혹 안에 진짜 문자열 정보가 들어있기도 하지만 이미지가 구성되지는 않는다.

 

- 무엇을 produce 할것인지는 produce에 점찍어보면 상수 안에 정의되어 들어가 있다.(pdf, text, json 등...)

- 직접 텍스트로 입력해도 되지만 오타가 날 수도 있고, 가독성이 떨어지므로 

  static final 상수로 미리 정의되어 있는 것을 사용하면 좋다.

 

- produces의 기능, 경로변수 기억하기!

 


 

- 자료실의 기능을 구현하면서 업로드한 파일이 이미지가 아니어도 똑같은 위치에 저장했다.(upload폴더)

 

- 컨트롤러에서 다운로드시켜주었다.

- 특정파일을 클라이언트에게 전달하면 서버가 주는 바이트알갱이를 받아 파일을 만들어내야 한다.(이미지를 보여주는것과는 다르다) 파일을 만들어내려면 파일의 이름도 중요하다.

 

- 누군가가 올렸던 파일명도 활용해서 그것으로 파일을 구성한다.

- 웹브라우저-서버간의 약속을 지켜가면서 응답을 하면 웹브라우저가 파일을 잘 만들어준다.

 

- ResponseEntity<InputStreamResource> : 응답 정보가 담겨있는 객체

- 이것을 사용하면 produces 없이 바로 파일을 다운로드시켜줄 수 있다.

 

 

 

- 원본 파일명을 얻어내서 인코딩하고, 파일 이름을 전달한다.

- 그러면 응답 header 정보에 이것을 담는다.(응답 윗부분에 헤더정보가 있다.)

(헤더에는 보통 key=value형태의 문자열이 들어있다.

 

- 헤더 아래에는 뭔가로 구분이 되어있고, 그 뒷부분(하단)에 데이터 정보가 쭉 명시되어 있다.

 

 

- 이 응답에 대한 정보를 알려주는 것 헤더이다. 클라이언트의 브라우저가 그것을 읽고 반응한다.

- 약속된 키 값, 약속된 value를 사용해서 전달하면 클라이언트 브라우저가 그것에 맞게 동작한다.

 

- HttpHeaders의 도움을 받아서 헤더를 구성하고, Entity에다가 헤더 정보를 전달한다.

 

- content-transfer-encoding은 없어서 따로 전달해준 것이다.(추가)

- 그리고 body에는 inputStreamResource 객체를 받아서 전달해주었다.

 


 

- 백엔드의 기능이 완벽하게 구현되고 나면 -> 디자인을 입히고 UI 를 구성한다.(프론트엔드)

 

 

- view port 설정이 중요하다.

- 이 기능이 없으면 모바일기기로 봤을 때 폰화면에 pc 브라우저로 보이는 화면 전체가 들어간다.

- 모바일에서 좀더 효율적으로 보이게 하려면 이 viewport 설정도 기본으로 주어야한다.

 

- 이클립스에서 jsp의 기본 템플릿으로 있도록 설정하기.(템플릿에 추가)

 

<meta name="viewport" content="width=device-width, initial-scale=1">

 

- preference - template - web - JSP Files - Template (그냥 temp로 검색하면 나옴)

 

- html 5 를 edit!

 

- 모바일 웹브라우저를 고려해서 이렇게 넣어주면 좋다.

 

- bootstrap css를 사용하려면 css, javascript cdn 도 추가해주어야 한다.

 

- Grid : 반응형 웹페이지 만들때 사용하는 것

- 갤러리에서 화면의 폭에 따라서 레이아웃이 바뀌듯이!!

 

- 모든 페이지를 무조건 반응형 웹페이지로 만들어야 한다.

- 이제 모바일 기기를 쓰지 않는 곳이 없기 때문에...

 

- 동일한 url 주소에는 동일한 내용이 나오는 것이 원칙이다.

- m. 으로 시작하는 모바일 버전 화면이 따로 있는 것은 일반적으로 바람직하지 않다.

 

- 폰으로 위아래 스크롤은 일반적이지만 좌우로 스크롤하면 피로하다. 눈에 잘 들어오지 않는다.

- 반응형으로 만들지 않으면 사용자가 사용이 불편하게 된다.

- 이미 만들어진 css의 도움을 받지 않고 반응형 페이지를 만들려면 매우 힘들다.. 그래서 Grid를 사용한다.

 

- 반응형 페이지로 웹사이트를 만들어놓으면

  android app이나 ios app도 금방 만들 수 있다.

 

 

 

- 안드 앱 : 개발언어 java / kotlin  (java에서 kotlin 으로 변화하는 추세이다)

- ios 앱 : 개발언어 swift / object c

 

- 앱 개발이란? 화면상에 동작하는(클라이언트의 요청에 반응하는) 어떤 UI를 만드는 것.

- 이 UI는 안드로이드 native UI / ios native UI 가 있다.

 

- UI 에는 이런 것들이 있다 : button, imageview, webView ....

(이름이 같지는 않고 안드, ios 각각의 고유한 이름이 있다)

 

- 안드&ios따로 만들어서 배포하면 비용이 많이 든다.

- 앱은 만들면 계속 유지보수가 필요하다.

- 그래서 요즘은 WebView를 사용해서 앱을 많이 만든다.(쇼핑몰, 영화 등 앱. 게임은X)

 

 

** WebView 란 무엇인가?

- 앱 안에 넣을 수 있는 주소창이 없는 웹브라우저

- 앱의 일부 화면을 구성할 수 있는 주소창 없는 웹브라우저라고 생각하면 된다.

 

- 주소창 없는 웹브라우저의 특징 : 화면 구성을 html, css, javascript로 할 수 있다.

- 즉, native UI를 배우지않아도 앱개발을 할수있다.

- WebView 페이지로 전체 페이지를 감싸버리면 된다.

 

- WebView 페이지를 하나 만들어놓고 양쪽에서 쓸 수 있다는 것이 장점이다.

- 비용, 관리 문제로 이렇게 앱을 많이 만든다.

- 이런식으로 만드는 앱을 하이브리드 앱이라고 한다.

- 하이브리드 앱을 좀더 쉽게 만들어주는 도구로는 React Native, Flutter 등등이 있다.

- React Native 앱을 사용하면 android, ios 겸용으로 개발할 수 있다.

 

- 수업에서는 이 중 4개를 배울 예정!

- Android native UI, WebView, Kotlin, Android Wear(시계, 착용하는 종류)까지 배울 예정이다.

- 웹사이트 UI를 반응형으로 만들어놓으면 저 WebView를 사용하는 앱을 쉽게 만들 수 있다.

 (모바일 앱처럼 보이지만 사실 엄밀하게 말하면 모바일 앱은 아니다.)

 

 

- 웹서버에 클라이언트 브라우저에서 어떤 경로를 요청

- 웹브라우저에 서버가 html, css, js 등을 응답. 이 응답으로 화면 UI가 구성된다.

 

- 하지만 아래와 같이 만들 수도 있다.(분홍색 박스)

- Android App인데 앱의 화면 전체를 WebView로 감싼 상태.

 java code: http://xxx/xxx/xxx 페이지를 로딩하라고 코딩하면 WebView 가 서버에 요청하고 응답된다.

 응답하면 UI 화면이 구성된다.

- IOS App도 마찬가지이다.

 

 

- 서버에서 응답하는 컨텐츠가 반응형으로 되어있으면, 이 앱에서 사용하는 데에도 문제가 없다.

- UI도 3가지에서 모두 같다.

- 이러면 웹에서 앱으로 wrapping이 금방 일어날 수 있다.

- 사실 진정한 의미의 앱 제작은 아니지만... 이렇게도 모바일App을 만들어서 사용할 수 있다.

 

- 모바일은 화면크기(폭)이 작다. 즉 레이아웃 구성이 잘 되어있어야 한다.

- 웹페이지도 가급적이면 반응형으로 만들어야 모바일 앱에서 쉽게 보여줄 수 있다.

 

- WebView로 만들면 UI가 안드로이드 고유의 UI가 아닌 WebView가 해석하는 UI가 된다.

- native ui보다는 반응이 좀 느리다는 단점이 있다. 하지만 게임이 아닌 이상 체감될 정도로 느리지는 않을 것이다..

ex) 카톡 앱 : native ui / 지마켓, 쿠팡 등 앱 : 화면의 일부는 WebView 사용

 

- 웹페이지를 grid 시스템으로 만들어놓으면 나중에 모바일로 제작할 경우도 대비할 수 있다.

 


 

* bootstrap examples : 링크

- 부트스트랩 홈페이지에서 어떤 css를 적용했을 때의 샘플을 제공하고 있다.

- 다운받아서 압축풀어서 VS code로 열어보면, css가 종류별로 있다.(앨범형태)

 

- 사용하려면 일단 bootstrap, js css 가 필요하다.

- 저 파일 안에 있는 내용을 따로 가져다가 추가해야 한다!

 

- 마크업을 어떤 구조로 해두면 저 내용이 나오는지 확인하고 복붙+일부 수정해서 사용

 

 

- 이 안에서만 사용되는 별도의 custom css가 있는데 이것도 같이 가져가야 한다. 

- example을 보고 어떤 css와 마크업구조가 필요한지 분석해서 사용하면 된다.

 

- navbar, Masonry, sign-in 등 다양하게 적용할 수 있는 샘플이 있다.

 


 

- 만들어둔 Boot07의 로그인 페이지에 BootStrap 적용하기

 

loginform.jsp

<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>/views/users/loginform.jsp</title>
<!-- bootstrap css 로딩 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<!-- custom css -->
<style>
   	html,
	body {
	  height: 100%;
	}
	
	body {
	  display: flex;
	  align-items: center;
	  padding-top: 40px;
	  padding-bottom: 40px;
	  background-color: #f5f5f5;
	}
	
	.form-signin {
	  max-width: 330px;
	  padding: 15px;
	}
	
	.form-signin .form-floating:focus-within {
	  z-index: 2;
	}
	
	.form-signin input[type="email"] {
	  margin-bottom: -1px;
	  border-bottom-right-radius: 0;
	  border-bottom-left-radius: 0;
	}
	
	.form-signin input[type="password"] {
	  margin-bottom: 10px;
	  border-top-left-radius: 0;
	  border-top-right-radius: 0;
	}
    
      .bd-placeholder-img {
        font-size: 1.125rem;
        text-anchor: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        user-select: none;
      }
      @media (min-width: 768px) {
        .bd-placeholder-img-lg {
          font-size: 3.5rem;
        }
      }
      .b-example-divider {
        height: 3rem;
        background-color: rgba(0, 0, 0, .1);
        border: solid rgba(0, 0, 0, .15);
        border-width: 1px 0;
        box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
      }
      .b-example-vr {
        flex-shrink: 0;
        width: 1.5rem;
        height: 100vh;
      }
      .bi {
        vertical-align: -.125em;
        fill: currentColor;
      }
      .nav-scroller {
        position: relative;
        z-index: 2;
        height: 2.75rem;
        overflow-y: hidden;
      }
      .nav-scroller .nav {
        display: flex;
        flex-wrap: nowrap;
        padding-bottom: 1rem;
        margin-top: -1px;
        overflow-x: auto;
        text-align: center;
        white-space: nowrap;
        -webkit-overflow-scrolling: touch;
      }
    </style>
    <link href="sign-in.css" rel="stylesheet">
</head>
<body class="text-center">
<main class="form-signin w-100 m-auto">
	<form action="${pageContext.request.contextPath}/users/login" method="post">
      <c:choose>
         <c:when test="${ empty param.url }">
            <input type="hidden" name="url" value="${pageContext.request.contextPath}/"/>
         </c:when>
         <c:otherwise>
            <input type="hidden" name="url" value="${param.url }"/>
         </c:otherwise>
      </c:choose>

      <h1 class="h3 mb-3 fw-normal">로그인 폼</h1>

      <div class="form-floating">
         <input name="id" value="${cookie.savedId.value }" type="text" class="form-control" id="floatingInput" placeholder="아이디 입력...">
         <label for="floatingInput">아이디</label>
      </div>
      <div class="form-floating">
         <input name="pwd" value="${cookie.savedPwd.value }" type="password" class="form-control" id="floatingPassword" placeholder="비밀번호 입력...">
         <label for="floatingPassword">비밀번호</label>
      </div>

      <div class="checkbox mb-3">
         <label>
         	<!-- 체크박스를 체크하지 않으면 isSave라는 파라미터 값으로 넘어오는 문자열이 null이고 
         		체크를 하면 isSave라는 파라미터 값으로 넘어오는 문자열은 "yes"dlek.-->
           <input name="isSave" type="checkbox" value="yes" ${empty cookie.savedId ? '' : 'checked' }>로그인 정보 저장
         </label>
      </div>
      <button class="w-100 btn btn-lg btn-primary" type="submit">로그인</button>
      <p class="mt-5 mb-3 text-muted">&copy; 2017–2022</p>
	</form>
   </main>
</body>
</html>

 

- 해당 css 를 적용하려면 같은 폴더안에 이 css가 있어야 한다.

 

- 그냥 이 안에 있는 내용을 복사해서 loginform의 <style> 태그 안에 넣어주기

 

- main 요소는 div 요소와 같은 것이라고 생각하면 된다.

- main 요소를 전체복사해서 붙여넣기

 

- form action, c:choose 부분 추가

 

- input에 입력값 추출을 위한 name 속성 넣어주기

 

- 로그인폼에 bootstrap이 적용됨.

 


 

UsersController

package com.sy.boot07.users.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.sy.boot07.users.dto.UsersDto;
import com.sy.boot07.users.service.UsersService;

@Controller
public class UsersController {

	@Autowired
	private UsersService service;
		
	@Value("${file.location}")
	private String fileLocation;
	
	@GetMapping(
			value="/users/images/{imageName}",
			produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE, MediaType.IMAGE_GIF_VALUE}
		)
	@ResponseBody
	public byte[] profileImage(@PathVariable("imageName") String imageName) throws IOException {
		
		String absolutePath=fileLocation+File.separator+imageName;
		//파일에서 읽어들일 InputStream
		InputStream is=new FileInputStream(absolutePath);
		//이미지 데이터(byte)를 읽어서 배열에 담아서 클라이언트에게 응답한다.
		return IOUtils.toByteArray(is);
	}	
	
	/*
	 * GET 방식 /users/signup_form 요청을 처리할 메소드
	 * - 요청방식이 다르면 실행되지 않는다.
	 */
	@RequestMapping(method = RequestMethod.GET, value = "/users/signup_form")
	public String signupform() {
		
		return "users/signup_form";
	}
	//회원가입 요청 처리
	@RequestMapping(method = RequestMethod.POST, value = "/users/signup")
	public ModelAndView signup(ModelAndView mView, UsersDto dto) {
		service.addUser(dto);
		mView.setViewName("users/signup");		
		return mView;
	}
	//로그인 폼 요청 처리
	@RequestMapping(method = RequestMethod.GET, value="/users/loginform")
	public String loginForm() {
		
		return "users/loginform";
	}	
	//로그인 요청 처리
	@RequestMapping("/users/login")
	public ModelAndView login(ModelAndView mView, UsersDto dto, String url, 
			HttpSession session, HttpServletResponse response) {
		/*
		 * 서비스에서 비즈니스 로직을 처리할 때 필요로 하는 객체를 컨트롤러에서 직접 전달해주어야 한다.
		 * 주로, HttpServletRequest, HttpServletResponse, HttpSession, ModelAndView
		 * 등등의 객체이다.
		 */
		service.loginProcess(dto, session, response);
		
		//로그인 후에 가야할 목적지 정보를 
		String encodedUrl=URLEncoder.encode(url);
		mView.addObject("url", url);
		mView.addObject("encodedUrl", encodedUrl);
		mView.setViewName("users/login");
		return mView;
	}
	
	//로그아웃 요청 처리
	@RequestMapping("/users/logout")
	public String logout(HttpSession session) {		
		//세션에서 id라는 키값으로 저장된 값 삭제
		session.removeAttribute("id");
		return "users/logout";
	}
	//개인정보 보기 요청 처리
	@RequestMapping("/users/info")
	public ModelAndView info(HttpSession session, ModelAndView mView) {
		
		service.getInfo(session, mView);
		
		mView.setViewName("users/info");
		return mView;		
	}	
	//비밀번호 수정 폼 요청 처리
	@RequestMapping("/users/pwd_updateform")
	public String pwdUpdateForm() {
		
		return "users/pwd_updateform";
	}
	//비밀번호 수정 폼 요청 처리
	@RequestMapping("users/pwd_update")
	public ModelAndView pwdUpdate(UsersDto dto, ModelAndView mView, HttpSession session) {
		//서비스에 필요한 객체의 참조값을 전달해서 비밀번호 수정 로직을 처리한다.
		service.updateUserPwd(session, dto, mView);
		//view page로 forward 이동해서 작업결과를 응답한다.
		mView.setViewName("users/pwd_update");
		return mView;
	}	
	//회원 탈퇴 요청 처리
	@RequestMapping("/users/delete")
	public ModelAndView delete(HttpSession session, ModelAndView mView) {
		
		service.deleteUser(session, mView);
		
		mView.setViewName("users/delete");
		return mView;
	}
	
	//회원정보 수정 폼 요청 처리
	@RequestMapping("/users/updateform")
	public ModelAndView updateForm(HttpSession session, ModelAndView mView) {
		service.getInfo(session, mView);		
		mView.setViewName("users/updateform");
		return mView;
	}
	
	//회원정보 수정 반영 요청 처리
	@RequestMapping(value="/users/update", method = RequestMethod.POST)
	public ModelAndView update(UsersDto dto, HttpSession session, ModelAndView mView,
			HttpServletRequest request) {
		//서비스를 이용해서 개인정보를 수정하고
		service.updateUser(dto, session);
		//개인정보 보기로 리다이렉트 이동시킨다.
		mView.setViewName("redirect:/users/info");		
		return mView;
	}
	
	//ajax 프로필 사진 업로드 요청 처리	
	@RequestMapping(value = "/users/profile_upload", method = RequestMethod.POST)
	@ResponseBody
	public Map<String, Object> profileUpload(HttpServletRequest request, MultipartFile image){
		//서비스를 이용해서 이미지를 upload 폴더에 저장하고 리턴되는 Map을 리턴해서 json 문자열 응답하기
		return service.saveProfileImage(request, image);		
	}
}

 

UsersDto

package com.sy.boot07.users.dto;

import org.apache.ibatis.type.Alias;

@Alias("usersDto")
public class UsersDto {
	//필드
	private String id;
	private String pwd;
	private String email;
	private String profile;
	private String regdate;
	private String newPwd;
	private String isSave;
	
	//디폴트 생성자
	public UsersDto() {}

	public UsersDto(String id, String pwd, String email, String profile, String regdate, String newPwd, String isSave) {
		super();
		this.id = id;
		this.pwd = pwd;
		this.email = email;
		this.profile = profile;
		this.regdate = regdate;
		this.newPwd = newPwd;
		this.isSave = isSave;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getPwd() {
		return pwd;
	}

	public void setPwd(String pwd) {
		this.pwd = pwd;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getProfile() {
		return profile;
	}

	public void setProfile(String profile) {
		this.profile = profile;
	}

	public String getRegdate() {
		return regdate;
	}

	public void setRegdate(String regdate) {
		this.regdate = regdate;
	}

	public String getNewPwd() {
		return newPwd;
	}

	public void setNewPwd(String newPwd) {
		this.newPwd = newPwd;
	}

	public String getIsSave() {
		return isSave;
	}

	public void setIsSave(String isSave) {
		this.isSave = isSave;
	}	
		
}

 

UsersService

package com.sy.boot07.users.service;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.sy.boot07.users.dto.UsersDto;

public interface UsersService {
	public Map<String, Object> isExistId(String inputId);
	public void addUser(UsersDto dto);
	public void loginProcess(UsersDto dto, HttpSession session, HttpServletResponse response);
	public void getInfo(HttpSession session, ModelAndView mView);
	public void updateUserPwd(HttpSession session, UsersDto dto, ModelAndView mView);
	public Map<String, Object> saveProfileImage(HttpServletRequest request, 
			MultipartFile mFile);
	public void updateUser(UsersDto dto, HttpSession session);
	public void deleteUser(HttpSession session, ModelAndView mView);
}

 

UsersServiceImpl

package com.sy.boot07.users.service;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.sy.boot07.users.dao.UsersDao;
import com.sy.boot07.users.dto.UsersDto;

@Service
public class UsersServiceImpl implements UsersService{

	@Autowired
	private UsersDao dao;
	
	@Value("${file.location}")
	private String fileLocation;
	
	@Override
	public Map<String, Object> isExistId(String inputId) {
		// TODO Auto-generated method stub
		return null;
	}
	
	//회원 한명의 정보를 추가하는 메소드
	@Override
	public void addUser(UsersDto dto) {
		//가입시 입력한 비밀번호를 읽어와서
		String pwd=dto.getPwd();
		//암호화한 후에
		BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
		String encodedPwd=encoder.encode(pwd);
		//dto에 다시 넣어준다.
		dto.setPwd(encodedPwd);
		//암호화된 비밀번호가 들어있는 dto를 dao에 전달해서 새로운 회원정보를 추가한다.
		dao.insert(dto);		
	}
	//로그인 처리를 하는 메소드
	@Override
	public void loginProcess(UsersDto dto, HttpSession session, HttpServletResponse response) {
		//입력한 정보가 맞는지 여부
		boolean isValid=false;
		//아이디를 이용해서 회원정보를 얻어온다.
		UsersDto resultDto=dao.getData(dto.getId());
		//만일 select된 회원정보가 존재하고 (아이디가 없을경우 null이 리턴된다.)
		if(resultDto!=null) {
			//Bcrypt 클래스의 static 메소드를 이용해서 입력한 비밀번호와 암호화해서 저장된 비밀번호의 일치 여부를 알아내야 한다.
			isValid=BCrypt.checkpw(dto.getPwd(), resultDto.getPwd());
		}
		//만일 유효한 정보이면
		if(isValid) {
			//로그인 처리를 한다.
			session.setAttribute("id", resultDto.getId());			
		}
		//로그인 정보를 저장하기로 했는지 확인해서 저장하기로 했다면 쿠키로 응답한다.
		String isSave=dto.getIsSave();
		if(isSave == null){//체크 박스를 체크 안했다면
		      //저장된 쿠키 삭제 
		      Cookie idCook=new Cookie("savedId", dto.getId());
		      idCook.setMaxAge(0);//삭제하기 위해 0 으로 설정 
		      response.addCookie(idCook);
		      
		      Cookie pwdCook=new Cookie("savedPwd", dto.getPwd());
		      pwdCook.setMaxAge(0);
		      response.addCookie(pwdCook);
		}else{//체크 박스를 체크 했다면 
		      //아이디와 비밀번호를 쿠키에 저장
		      Cookie idCook=new Cookie("savedId", dto.getId());
		      idCook.setMaxAge(60*60*24);//하루동안 유지
		      response.addCookie(idCook);
		      
		      Cookie pwdCook=new Cookie("savedPwd", dto.getPwd());
		      pwdCook.setMaxAge(60*60*24);
		      response.addCookie(pwdCook);
	       }
			
		
	}
	//정보를 가져오는 메소드
	@Override
	public void getInfo(HttpSession session, ModelAndView mView) {
		//로그인된 아이디를 읽어온다.
		String id=(String)session.getAttribute("id");
		//DB에서 회원정보를 얻어와서
		UsersDto dto=dao.getData(id);
		//ModelAndView 객체에 담아준다.
		mView.addObject("dto", dto);		
	}

	@Override
	public void updateUserPwd(HttpSession session, UsersDto dto, ModelAndView mView) {
		 //세션 영역에서 로그인된 아이디 읽어오기
	      String id=(String)session.getAttribute("id");
	      //DB 에 저장된 회원정보 얻어오기
	      UsersDto resultDto=dao.getData(id);
	      //DB 에 저장된 암호화된 비밀 번호
	      String encodedPwd=resultDto.getPwd();
	      //클라이언트가 입력한 비밀번호
	      String inputPwd=dto.getPwd();
	      //두 비밀번호가 일치하는지 확인
	      boolean isValid=BCrypt.checkpw(inputPwd, encodedPwd);
	      //만일 일치 한다면
	      if(isValid) {
	         //새로운 비밀번호를 암호화 한다.
	         BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
	         String encodedNewPwd=encoder.encode(dto.getNewPwd());
	         //암호화된 비밀번호를 dto 에 다시 넣어준다.
	         dto.setNewPwd(encodedNewPwd);
	         //dto 에 로그인된 아이디도 넣어준다.
	         dto.setId(id);
	         //dao 를 이용해서 DB 에 수정 반영한다.
	         dao.updatePwd(dto);
	         //로그아웃 처리
	         session.removeAttribute("id");
	      }
	      //작업의 성공여부를 ModelAndView 객체에 담아 놓는다(결국 HttpServletRequest 에 담긴다)
	      mView.addObject("isSuccess", isValid);
	      //로그인된 아이디도 담아준다.
	      mView.addObject("id", id);
		
	}

	@Override
	public Map<String, Object> saveProfileImage(HttpServletRequest request, MultipartFile mFile) {
		//업로드된 파일에 대한 정보를 MultipartFile 객체를 이용해서 얻어낼수 있다.   
	      
	    //원본 파일명
	    String orgFileName=mFile.getOriginalFilename();
	    //upload 폴더에 저장할 파일명을 직접구성한다.
	    // 1234123424343xxx.jpg
	    String saveFileName=System.currentTimeMillis()+orgFileName;
	      
	    //이미지를 저장할 실제 경로 
	    String realPath=fileLocation;
	    // upload 폴더가 존재하지 않을경우 만들기 위한 File 객체 생성
	    File upload=new File(realPath);
	    if(!upload.exists()) {//만일 존재 하지 않으면
	       upload.mkdir(); //만들어준다.
	    }
	    try {
	       //파일을 저장할 전체 경로를 구성한다.  
	       String savePath=realPath+File.separator+saveFileName;
	       //임시폴더에 업로드된 파일을 원하는 파일을 저장할 경로에 전송한다.
	       mFile.transferTo(new File(savePath));
	    }catch(Exception e) {
	       e.printStackTrace();
	    }
	    
	    // json 문자열을 출력하기 위한 Map 객체 생성하고 정보 담기 
	    Map<String, Object> map=new HashMap<String, Object>();
	    map.put("imagePath", saveFileName);
	    
	    return map;
	}

	@Override
	public void updateUser(UsersDto dto, HttpSession session) {
		//수정할 회원의 아이디(아이디는 넘어오지 않으므로)
		String id=(String)session.getAttribute("id");
		//dto에 넣어준다.
		dto.setId(id);
		//만약 프로필 이미지를 등록하지 않은 상태이면
		if(dto.getProfile().equals("empty")) {
			//users 테이블의 profile 칼럼을 null인 상태로 유지하기 위해 profile에 null을 넣어준다.
			dto.setProfile(null);
		}
		dao.update(dto);
	}

	@Override
	public void deleteUser(HttpSession session, ModelAndView mView) {
		//로그인된 아이디를 얻어와서
		String id=(String)session.getAttribute("id");
		//해당 정보를 DB에서 삭제하고
		dao.delete(id);
		//로그아웃 처리도 한다.
		session.removeAttribute("id");
		//ModelAndView 객체에 탈퇴한 회원의 아이디를 담아준다.
		mView.addObject("id", id);		
	}

}

 

- 아이디, 패스워드를 저장하는 기능 구현하기.

- 쿠키를 사용해서 로그인 정보를 저장한다.

- 이 사실을 기억하고 서버에서 처리하면 된다.

 

- 체크박스에 value 값을 추가한다. 이 값을 service메소드로 넘겨서 활용하기!

- 넘어오면 yes이고, 넘어오지 않으면 null이다.

 

 

UsersController

- dto 안에 id, pwd를 받아오게 되어있는데, 이제 isSave 라는 값까지 받아와야 한다. isSave 필드 추가!

- isSave에 들어오는 값은 "yes" 아니면 null 이다. 체크박스 체크 여부에 따라 달라진다!

 

 

dto 수정

- 체크박스의 name값과 일치시켜 주어야 한다.

 

 

UsersServiceImpl

- loginprocess 메소드에 새로운 조건 추가!

- 유효한 정보이면 로그인처리 하고 + 체크되어 있으면 id저장 까지 같이 하기

- 읽어오려면 HttpServletResponse 객체가 필요하다. 이것을 컨트롤러에서 전달해주어야 한다..

 

인터페이스수정

- loginProcess 메소드를 response를 전달받는 구조로 바꾼다.

 

- ServiceImpl 메소드에서도 추가하기

 

- 컨트롤러에서 response 객체를 받아와서 서비스로 전달해주는 구조!!

 

ServiceImpl에서 다시 수정

- 받아온 response 값을 활용한다.

- 체크하지 않았다면 setMaxAge를 0으로 정해서 값을 지워버리고,

체크했다면 savedId라는 키 값으로 id, pwd를 쿠키에 심어놓는다.(쿠키 유지시간은 초 단위로 작성하면 된다)

- response 객체에다가 쿠키를 담아놓으면 응답하면서 클라이언트 웹브라우저에 심어지는것이다.

 

- 쿠키값은 뷰 페이지에서 ${ cookie.savedId.value } 로 간단하게 읽어낼 수 있다.

 

loginform.jsp

- 값이 있으면 읽어와서 value로 내보낼 것이고, 없으면 비어있을 것이다.("" 을 응답한다)

 

- 이렇게 아이디 저장 기능이 구현되었다!

 

 

- 쿠키에 담겨있는 정보를 읽어서 value에 출력한 것이다.

 

- checked 속성을 한번 체크되면 계속 체크인 상태로 있도록 하려면?

- loginform의 체크박스 요소 안에 코딩해주면 된다.

- 쿠키에 저장된 아이디 정보가 있다면 checked 속성을 출력할 것인지 말 것인지를 이곳에 출력하게 할 수 있다.

 

 

- 이런 정보를 구현하려면 프론트엔드-백엔드 개발자 간의 소통이 필요하다..