53일차(1)/Spring(12) : ajax 요청 JSON으로 응답하기, 파일 업로드 처리
- updateform 요청/메소드 작성
- update 요청/메소드 작성
- spring에서 json으로 응답하는 방법 실습
- 파일 업로드 메소드 작성(ajax 요청 처리)
- 서버를 시작하면 가장 기본으로 응답되는 페이지.(home)
- 처음 프로젝트를 서버로 열면 이 경로로 요청해주지만, 이렇게 직접 요청할 수는 없다.
(WEB-INF는 클라이언트에게 공개되지 않음. 직접 입력하면 이동되지 않는다)
- context 경로의 최상위 경로를 요청하게 되면 home.jsp로 forward 이동이 되어서 이동한다.
- 물리적인 위치는 webapp 안쪽이라고 생각하면 된다.
- 저 문자열은 포워드 이동할 위치를 가리키는 것이다.
- 포워드 이동이란 다른 페이지로 응답을 위임하는 것.
- 경로의 앞, 뒤로 접두어, 접미어를 붙여서 이동시켜 준다.
- 공지사항은 request 영역으로부터 읽어낸 것이다. EL로 읽어온 것.
- 이런 응답에 필요한 데이터를 모델이라고도 한다.
- request 영역이나 session 영역에 담아서 전달받는다.
- 위 내용은 HomeController에서 request 영역에 담아서 포워드 이동시킨 것이다.
- 담긴 내용이 List<>이므로 item로 받아서 바로 출력한 것
(제너릭이 string타입이므로 바로 출력가능)
** Controller와 view페이지 사이의 관계를 알아둘 것!
- 현재 JSTL을 사용해서 home의 내용을 조건부로 출력하게 했다. 로그인 여부에 따라!
- 사실 sessionScope. 도 생략 가능하다. 키 값만 가지고도 자동으로 request 영역, session 영역을 찾아준다.
- Controller는 Spring bean container로부터 필요한 객체를 주입(DI)받아서 사용한다.
- 클라이언트가 전달해준 dto를 받고, 로그인처리를 위해 필요한 session도 받아서 그 안에서 처리해준다.
- Controller는 필요한 객체를 넣어주고, 여러 비즈니스 로직은 service에서 해준다.
- Service는 Controller의 유틸리티 같은 것.
- 서비스에서 DB에 있는 정보가 맞는지 확인해서 로그인 처리를 한다.
- 서비스는 dao에 의존한다. DB에서 작업할 게 있으면 dao에서 한다.
- Interceptor 는 컨트롤러 메소드의 수행 직전에 관여하도록 했다.
- 어떤 것에 관여할지 기준이 되는 것은 servlet-context에 명시되어 있다.
- 먼저 spring에서 bean이 되어야 하고, 어떤 요청에 대해 동작할지는 mapping에 명시해두었다.
- login 하고 난 후에 접속되도록 처리해주는 것.
- 로그인 성공 후에 원래 가려던 곳으로 보내줄 수 있도록 encodedUrl도 같이 넣어준다.
- 파일 업로드 기능 구현 (프로필 이미지 변경)
- MultipartFile 객체를 사용한다.
- jsp에서는 직접 응답했는데, 그 내용을 그대로 구현하고자 한다면 필요한 javascript가 있다.
sy_util.js
// webapp/js/sy_util.js
/*
ajaxPromise("요청url", "요청메소드", query string or object)
와 같은 형식으로 사용하고
Promise type 을 리턴해주는 함수
*/
function ajaxPromise(url, method, data){
//만일 필요한 값이 전달 되지 않으면 기본값을 method 와 data 에 넣어준다.
if(method == undefined || method == null){
method="GET";
}
if(data == undefined || data == null){
data={};
}
let queryString;
if(typeof data == "string"){
queryString=data;
}else{
queryString=toQueryString(data);
}
// Promise 객체를 담을 변수 만들기
let promise;
if(method=="GET" || method=="get"){//만일 GET 방식 전송이면
//fetch() 함수를 호출하고 리턴되는 Promise 객체를 변수에 담는다.
promise=fetch(url+"?"+queryString);
}else if(method=="POST" || method=="post"){//만일 POST 방식 전송이면
//fetch() 함수를 호출하고 리턴되는 Promise 객체를 변수에 담는다.
promise=fetch(url,{
method:"POST",
headers:{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8"},
body:queryString
});
}
return promise;
}
//함수의 인자로 ajax 전송할 폼의 참조값을 넣어주면 알아서 ajax 전송되도록 하는 함수
function ajaxFormPromise(form){
const url=form.getAttribute("action");
const method=form.getAttribute("method");
// Promise 객체를 담을 변수 만들기
let promise;
//파일 업로드 폼인지 확인해서
if(form.getAttribute("enctype") == "multipart/form-data"){
//폼에 입력한 데이터를 FormData 에 담고
let data=new FormData(form);
// fetch() 함수가 리턴하는 Promise 객체를
promise=fetch(url,{
method:"post",
body:data
});
return promise;//리턴해 준다 (여기서 함수가 종료 된다.)
}
const queryString=new URLSearchParams(new FormData(form)).toString();
if(method=="GET" || method=="get"){//만일 GET 방식 전송이면
//fetch() 함수를 호출하고 리턴되는 Promise 객체를 변수에 담는다.
promise=fetch(url+"?"+queryString);
}else if(method=="POST" || method=="post"){//만일 POST 방식 전송이면
//fetch() 함수를 호출하고 리턴되는 Promise 객체를 변수에 담는다.
promise=fetch(url,{
method:"POST",
headers:{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8"},
body:queryString
});
}
return promise;
}
//함수의 인자로 요청 url 과 ajax 전송할 내용이 있는 input 요소의 참조값을 전달하면 ajax 전송해주는 함수
function ajaxInputPromise(url, input){
const type=input.getAttribute("type");
const name=input.getAttribute("name");
let promise;
if(type=="file"){ // input type="file" 인 경우
let data=new FormData();
data.append(name, input.files[0]);
promise=fetch(url,{
method:"post",
body:data
});
}else{ //아닌경우
//전송할 쿼리 문자열 구성
const data=name+"="+encodeURIComponent(input.value);
promise=fetch(url,{
method:"POST",
headers:{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8"},
body:data
});
}
return promise;
}
//인자로 전달하는 object 를 이용해서 query 문자열을 만들어서 리턴해주는 함수
function toQueryString(obj){
//빈배열을 일단 만든다.
let arr=[];
//반복문 돌면서 obj 에 있는 정보를 "key=value" 형태로 만들어서 배열에 저장한다.
for(let key in obj){
//value 는 인코딩도 해준다.
let tmp=key+"="+encodeURIComponent(obj[key]);
arr.push(tmp);
}
//query 문자열을 얻어낸다
let queryString=arr.join("&");
//결과를 리턴해준다.
return queryString;
}
- 이런 폼의 속성들을 읽어와서 작업해주는 js 코드가 들어있다.
- 이 폼의 참조값을 전달하면 ajax를 리턴하고, json으로 응답해준다.
- 업로드 폴더는 resources/upload 여기에. sy_util 파일이 들어있는 js 폴더도 이곳에 넣어준다.
- 컨트롤러 작성(updateform)
//회원정보 수정 폼 요청 처리
@RequestMapping("/users/updateform")
public ModelAndView updateForm(HttpSession session, ModelAndView mView) {
service.getInfo(session, mView);
mView.setViewName("users/updateform");
return mView;
}
- session과 mView를 전달하면 mView에 회원정보가 담기는 메소드 작성
- 메소드에 인자를 선언하는 것만으로도 알아서 객체가 들어온다.
- 서비스의 메소드에서 이 작업을 해준다.
- ModelAndView 객체의 addObject는 request.setAttribute를 대신하는 것이다.
- 보통은 위와 같이 하는데, 이것을 mView로 대신할 수 있다.
- 데이터(model) 도 담고 이동할 view page도 담는다.
- 담는다고 해서 무조건 req 영역에 담기는 것은 아니고, controller에서 이 mView를 리턴해줘야 한다.
updateform.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/views/users/updateform.jsp</title>
<style>
/* 이미지 업로드 폼을 숨긴다 */
#imageForm{
display: none;
}
#profileImage{
width: 100px;
height: 100px;
border: 1px solid #cecece;
border-radius: 50%;
}
</style>
</head>
<body>
<div class="container">
<h3>회원 가입 수정 폼 입니다.</h3>
<a id="profileLink" href="javascript:">
<c:choose>
<c:when test="${ empty dto.profile }">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
</c:when>
<c:otherwise>
<img id="profileImage" src="${pageContext.request.contextPath }${dto.profile}">
</c:otherwise>
</c:choose>
</a>
<form action="${pageContext.request.contextPath}/users/update" method="post">
<input type="hidden" name="profile"
value="${ empty dto.profile ? 'empty' : dto.profile }"/>
<div>
<label for="id">아이디</label>
<input type="text" id="id" value="${dto.id}" disabled/>
</div>
<div>
<label for="email">이메일</label>
<input type="text" id="email" name="email" value="${dto.email}"/>
</div>
<button type="submit">수정확인</button>
<button type="reset">취소</button>
</form>
<form id="imageForm" action="${pageContext.request.contextPath}/users/profile_upload" method="post" enctype="multipart/form-data">
프로필 사진
<input type="file" id="image" name="image" accept=".jpg, .png, .gif"/>
<button type="submit">업로드</button>
</form>
</div>
<!-- sy_util.js 로딩 -->
<script src="${pageContext.request.contextPath }/resources/js/sy_util.js"></script>
<script>
//프로필 이미지 링크를 클릭하면
document.querySelector("#profileLink").addEventListener("click", function(){
// input type="file" 을 강제 클릭 시킨다.
document.querySelector("#image").click();
});
//프로필 이미지를 선택하면(바뀌면) 실행할 함수 등록
document.querySelector("#image").addEventListener("change", function(){
//ajax 전송할 폼의 참조값 얻어오기
const form=document.querySelector("#imageForm");
//gura_util.js 에 있는 함수를 이용해서 ajax 전송하기
ajaxFormPromise(form)
.then(function(response){
return response.json();
})
.then(function(data){
console.log(data);
// input name="profile" 요소의 value 값으로 이미지 경로 넣어주기
document.querySelector("input[name=profile]").value=data.imagePath;
// img 요소를 문자열로 작성한 다음
let img=`<img id="profileImage"
src="${pageContext.request.contextPath }\${data.imagePath}">`;
//id 가 profileLink 인 요소의 내부(자식요소)에 덮어쓰기 하면서 html 형식으로 해석해 주세요 라는 의미
document.querySelector("#profileLink").innerHTML=img;
});
});
</script>
</body>
</html>
- 이전 jsp 페이지에서 했던 이부분은 필요없다. 삭제!
- 서비스의 getInfo 메소드에서 이렇게 담았기 때문에 EL을 사용해서 추출할 수 있는 것이다.
- key 값과 type 을 기억하기 : "dto" 키워드로 UsersDto 타입을 담아두었다!
- 어떤값이 없는지 확인하려면 empty를 쓰는게 좋다. 깔끔하게 사용하기!
- 필드명만 적어도 알아서 getter메소드 호출해줌.
- profile 값이 null이라면(비어있다면) 기본 이미지를 출력해준다.
- 저장한 이미지가 있다면 출력되도록 해주는 것.
- 이 위치에는 이런 문자열이 들어가도록 해주어야 한다.
- 아래를 위와 같이 수정해주기
- ${ } EL 안에서도 3항연산자를 쓸 수 있다.
- 프로필 이미지는 empty 값이 넘어온다.
- resources 폴더 안으로 위치시켜주기
- 그러면 수정 폼이 위와 같이 나온다.(현재 프로필 값이 null이므로 기본이미지)
- 그러면 이제 update 요청을 해야한다.
controller
//회원정보 수정 반영 요청 처리
@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;
}
서비스 updateUser
@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);
}
- empty라는 문자열로 바뀐 profile 값을 다시 null로 돌려주는 작업!
- mapper의 update문 수정
- myBatis의 장점 중 하나! 동적 sql문이 가능하다.(Dynamic sql)
- sql문을 조건부로 작성할 수 있다.
- 그런데 JDBC에서는 null을 집어넣을 수 있었는데, myBatis에서는 안된다.
→ 그렇다면 null이 아닐 때에만 조건부로 profile값이 들어가도록 수정
- 이것이 동적 SQL문이다! JSTL과 비슷하지만 myBatis만의 문법이다.
- parameter에 저장된 필드값을 가지고 비교하는 것이다.
- 아니면 이렇게 수정해줄 수도 있다.
- 이것은 원래 ? 인 부분에 myBatis가 setString, setInt, setDouble 등으로 알아서 넣어주는 것인데
null을 넣으려고 하면 myBatis가 타입을 몰라서 오류가 나는 것이다.
- VARCHAR이라고 jdbcType을 명시해서 넣어줄 수 있도록 하면 된다.
- 프로필사진을 업로드하는 요청 작성(ajax 요청 처리)
- ajax요청에 대한 응답은 json으로 처리한다.
- 먼저 spring에서 json으로 어떻게 응답하는지를 연습할 예정
- java의 jsp 파일에서는 json응답이 불편했다. spring에서는 편해진다!
- home.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/home.jsp</title>
</head>
<body>
<div class="container">
<c:choose>
<c:when test="${ empty sessionScope.id}">
<a href="${pageContext.request.contextPath}/users/loginform">로그인</a>
<a href="${pageContext.request.contextPath}/users/signup_form">회원가입</a>
</c:when>
<c:otherwise>
<p>
<a href="${pageContext.request.contextPath}/users/info">${sessionScope.id }</a> 로그인중...
<a href="${pageContext.request.contextPath}/users/logout">로그아웃</a>
</p>
</c:otherwise>
</c:choose>
<h1>인덱스 페이지입니다.</h1>
<ul>
<li><a href="get_msg">@ResponseBody 어노테이션 테스트</a></li>
<li><a href="get_person">한명의 정보</a></li>
<li><a href="get_user">회원 한명 정보</a></li>
<li><a href="get_friends">친구목록</a></li>
<li><a href="get_users">회원 목록</a></li>
</ul>
<h3>공지사항입니다.</h3>
<ul>
<c:forEach var="tmp" items="${noticeList }">
<li>${tmp }</li>
</c:forEach>
</ul>
</div>
</body>
</html>
- 기본패키지에 JSONTestController 생성
package com.sy.spring04;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.sy.spring04.users.dto.UsersDto;
/*
* 1. jackson-databind 라이브러리가 dependency에 명시가 되어 있고
* 2. servlet-context.xml 에 <annotation-driven/> 이 명시되어 있고
* 3. 컨트롤러의 메소드에 @ResponseBody 어노테이션이 붙어 있으면
* Map or dto or List 객체에 담긴 내용이 json 문자열로 변환되어 응답된다.
*/
@Controller
public class JSONTestController {
@ResponseBody //컨트롤러에서 리턴한 문자열을 그대로 클라이언트에게 출력하는 기능
@RequestMapping("/get_msg")
public String getMsg() {
return "hello";
}
@ResponseBody
@RequestMapping("/get_person")
public Map<String, Object> getPerson(){
Map<String, Object> map=new HashMap<>();
map.put("num", 1);
map.put("name", "바나나");
map.put("isMan", false);
return map;
}
@ResponseBody
@RequestMapping("/get_user")
public UsersDto getUser() {
UsersDto dto=new UsersDto();
dto.setId("banana");
dto.setPwd("1234");
dto.setEmail("banana@a.com");
return dto;
}
@ResponseBody
@RequestMapping("/get_friends")
public List<String> getFriends(){
List<String> friends=new ArrayList<>();
friends.add("바나나");
friends.add("딸기");
friends.add("복숭아");
return friends;
}
@ResponseBody
@RequestMapping("/get_users")
public List<UsersDto> getUsers(){
List<UsersDto> list=new ArrayList<>();
UsersDto dto=new UsersDto();
dto.setId("banana");
dto.setPwd("1234");
dto.setEmail("banana@a.com");
UsersDto dto2=new UsersDto();
dto2.setId("berry");
dto2.setPwd("1234");
dto2.setEmail("berry@a.com");
list.add(dto);
list.add(dto2);
return list;
}
}
1. jackson-databind 라이브러리가 dependency에 명시되어 있고,
2. servlet-context.xml 에 <annotation-driven/> 이 명시되어 있고,
3. 컨트롤러의 메소드에 @ResponseBody 어노테이션이 붙어 있으면
Map or dto or List 객체에 담긴 내용이 json 문자열로 변환되어 응답된다.
- 1번은 pom.xml에 이미 세팅되어 있다. 2번도 되어 있다.
- /get_msg 에서 /views/hello.jsp 로 forward 이동하는 형태
- 리턴으로 웹브라우저에 새 페이지를 보여주는 것이 아니라 바로 저 문자열만 전달하는 방법이 있다.
→ @ResponseBody 를 사용한다.
- controller에서 리턴한 문자열이 바로 응답된다.
- return에 넣은 문자열이 다이렉트로 날아온다!
- 이 annotation과 jackson-databind 라이브러리를 잘 사용하면
map, list, dto 등을 JSON으로 간편하게 응답할 수 있다.
home.jsp에 링크 추가
get_person
- 위와 같이 작성하면 map에 담긴 내용이 JSON문자열로 변환되어 응답된다.
- 응답의 Body로서 바로 응답을 한다.
get_user
- UsersDto 폼 안에 넣어져서 (없으면 null) 응답된다.
- { } JSON 문자열로 변환되어서 응답된다.
get_friends
- List를 넣어주면 배열 형태로 응답된다.
** JSON의 응답방식은 아래와 같은 형태로 응답된다.
Map => { }
Dto => { }
List => [ ]
List<Map> => [ { }, { }. { }. ... ]
List<Dto>
Map<String, List<Map>> => { "key" : [ { }, { }. ...] }
get_users
[ { } , { } , { }. ... ]
- 이런 형태로 list에 2명의 정보를 담아 리턴해줄 수 있다.
- updateform의 내용을 json으로 응답해보기
- service에 Map을 리턴하는 서비스가 있다.(saveProfileImage)
- 이 폴더가 있어야 업로드 가능하다.
- 이렇게 지시하는 것을 예로 들면, 이 자체가 이미지인 것은 아니다.
- 이 경로로 웹브라우저가 요청(request)을 해서 이미지가 출력되는 것이다.
- 이 경로에 없으면 이미지가 출력되지 않는다.
- spring에서는 이렇게 요청한다.
- 하지만 저 경로에는 파일이 없으니 응답하지 않는다.
- 그래서 resources 안에 upload 폴더를 만들어놓고, 이 안에 존재한다면 응답하도록 한다.
- 이렇게 요청하면 이 resources 하위의 요청에 대해서는 관여하지 않는다.
- 필요할 때 불러올 수 있도록 이 문자열을 DB에 저장해 놓는다.
- 저장할 때는 파일명을 바꾸어서 알아서 저장한다.
- 이전에 작성한 jsp에서는 전체 경로를 구성해서 넣어주었는데,
Spring에서는 multipartFile을 사용해서 처리한다.
- Service 메소드 작성(saveProfileImage)
@Override
public Map<String, Object> saveProfileImage(HttpServletRequest request, MultipartFile mFile) {
//업로드된 파일에 대한 정보를 MultipartFile 객체를 이용해서 얻어낼수 있다.
//원본 파일명
String orgFileName=mFile.getOriginalFilename();
//upload 폴더에 저장할 파일명을 직접구성한다.
// 1234123424343xxx.jpg
String saveFileName=System.currentTimeMillis()+orgFileName;
// webapp/upload 폴더까지의 실제 경로 얻어내기
String realPath=request.getServletContext().getRealPath("/resources/upload");
// 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", "/resources/upload/"+saveFileName);
return map;
}
- 경로가 구성된 파일 객체를 transferTo() 메소드에 전달하면 알아서 저장해준다.
- 그리고 json 문자열을 응답할 준비를 하기위해서 Map안에 넣어주는 것.
- imagePath라는 키값으로 이미지경로가 저장된 것이다.
- javascript에서 이 json문자열을 받아서 object로 바꿔서 쓰면 된다.
- controller에 저 ajax 요청에 대한 응답을 하는 메소드가 필요하다.
controller에 메소드 작성
//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);
}
- controller의 MultipartFile은 폼의 name 속성의 값과 같아야 한다.
- 서버는 json문자열을 응답하고, 웹브라우저는 그 값을 object로 바꾸어서 data에 전달한다.
- data가 object이므로 아래에서 data. 점찍어서 사용할 수 있는 것이다.
- response.json 으로 코드를 작성했기 때문에 { } object type 으로 바꾸어 준 것이다.
- 만약 response.text 한다면 문자열이 된다.
- service.saveProfileImage() 메소드에서 map을 리턴해준 것이므로,
return map; 과 같은 방식으로 리턴된다고 보면 된다.
<!--
Multipart 폼 전송 처리를 위한 bean 설정
최대 업로드 사이즈 제한하기
name="maxUploadSize" value="byte 단위"
-->
<beans:bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="maxUploadSize" value="102400000"/>
</beans:bean>
- 그리고 파일 업로드하려면 반드시 servlet-context에 MultipartResolver가 있어야 한다.
- 이렇게 이미지가 들어간다.
- 수정확인을 누르면 최종 저장되어 들어가진다!
'국비교육(22-23)' 카테고리의 다른 글
54일차(2)/Spring(14) : 자료실 파일 다운로드, 삭제, 검색 기능 구현 (0) | 2022.12.22 |
---|---|
54일차(1)/Spring(13) : 자료실 게시판 만들기, 파일 업로드 기능 구현 (1) | 2022.12.22 |
52일차(2)/Spring(11) : Interceptor 추가, 비밀번호 암호화 및 수정, 회원 삭제 기능 구현 (1) | 2022.12.20 |
52일차(1)/Spring(10) : 회원가입, 로그인, 로그아웃, 회원정보 보기 기능 구현 (1) | 2022.12.20 |
51일차(2)/Spring(9) : 파일 업로드 기능 구현 / SmartEditor 적용 (0) | 2022.12.19 |