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">© 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 속성을 출력할 것인지 말 것인지를 이곳에 출력하게 할 수 있다.
- 이런 정보를 구현하려면 프론트엔드-백엔드 개발자 간의 소통이 필요하다..
'국비교육(22-23)' 카테고리의 다른 글
63일차(2)/Spring Boot(13) : Boot에서 war파일 생성(3) (0) | 2023.01.05 |
---|---|
63일차(1)/GitHub : Pull Request (0) | 2023.01.05 |
61일차(2)/Spring Boot(11) : 파일 저장경로 수정, 에러 페이지 응답 기능 구현 (0) | 2023.01.03 |
61일차(1)/Spring Boot(10) : file, gallery 게시판 파일 저장경로, 다운로드 기능 수정 / Boot 기능 활용 (0) | 2023.01.03 |
60일차(2)/Spring Boot(9) : 파일 저장경로 수정 / Boot 기능 활용 (0) | 2023.01.03 |