2023. 10. 23. 20:00ㆍSpringboot
1. Requirements
- Spring boot version 2.4.5
2. Stacks
Springboot
- Loombook
- Spring Data JPA
- Spring Boot DevTools
- Spring Security
- Spring Web
- Spring boot version 2.4.5
Mybatis
3. Code
해당 코드는 다른 패키지에서 test코드를 사용하여 이전 회원가입의 패키지와 다를 수 있으니
꼭 확인 부탁드립니다.
1. update.jsp
- localhost:8080/user/update으로 접속했을 때 나오는 첫 페이지
- 해당 밑에 있는 코드는 인증된 정보에 접근하는 방법이며 필자는 모든 jsp파일의 header.jsp에
적어줌으로써 다른 jsp파일은 안 적혀있습니다.
- ${principal}에 해당하는 변수를 사용할 시 꼭 필요합니다.
- 해당 jsp는 form형태를 put방식으로 보냅니다.(form은 post방식 가능)
- 해당 form형태를 보내기 위해 ajax를 사용하여 폼데이터를 보내줍니다.
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<sec:authorize access="isAuthenticated()">
<!--인증된 정보에 대한 접근 방법-->
<sec:authentication property="principal" var="principal"/>
</sec:authorize>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="../layout/header.jsp"%>
<!--프로필셋팅 메인-->
<main class="main">
<!--프로필셋팅 섹션-->
<section class="setting-container">
<!--프로필셋팅 아티클-->
<article class="setting__content">
<!--프로필셋팅 아이디영역-->
<div class="content-item__01">
<div class="item__img">
<img src="#" onerror="this.src='/images/profilePicture.jpeg'" />
</div>
<div class="item__username">
<h2>${principal.user.name}</h2>
</div>
</div>
<!--프로필셋팅 아이디영역end-->
<!--프로필 수정-->
<form id="profileUpdate" onsubmit="update(${principal.user.id}, event)">
<div class="content-item__02">
<div class="item__title">이름</div>
<div class="item__input">
<input type="text" name="name" placeholder="이름"
value="${principal.user.name}" required="required"/>
</div>
</div>
<div class="content-item__03">
<div class="item__title">유저네임</div>
<div class="item__input">
<input type="text" name="username" placeholder="유저네임"
value="${principal.user.username}" readonly="readonly" />
</div>
</div>
<div class="content-item__04">
<div class="item__title">패스워드</div>
<div class="item__input">
<input type="password" name="password" placeholder="패스워드" required="required"/>
</div>
</div>
<div class="content-item__05">
<div class="item__title">웹사이트</div>
<div class="item__input">
<input type="text" name="website" placeholder="웹 사이트"
value="${principal.user.website}" />
</div>
</div>
<div class="content-item__06">
<div class="item__title">소개</div>
<div class="item__input">
<textarea name="bio" id="" rows="3">${principal.user.bio}</textarea>
</div>
</div>
<div class="content-item__07">
<div class="item__title"></div>
<div class="item__input">
<span><b>개인정보</b></span> <span>비즈니스나 반려동물 등에 사용된 계정인 경우에도
회원님의 개인 정보를 입력하세요. 공개 프로필에는 포함되지 않습니다.</span>
</div>
</div>
<div class="content-item__08">
<div class="item__title">이메일</div>
<div class="item__input">
<input type="text" name="email" placeholder="이메일"
value="${principal.user.email}" readonly="readonly" />
</div>
</div>
<div class="content-item__09">
<div class="item__title">전화번호</div>
<div class="item__input">
<input type="text" name="phone" placeholder="전화번호"
value="${principal.user.phone}" />
</div>
</div>
<div class="content-item__10">
<div class="item__title">성별</div>
<div class="item__input">
<input type="text" name="gender" value="${principal.user.gender}" />
</div>
</div>
<!--제출버튼-->
<div class="content-item__11">
<div class="item__title"></div>
<div class="item__input">
<button>제출</button>
</div>
</div>
<!--제출버튼end-->
</form>
<!--프로필수정 form end-->
</article>
</section>
</main>
<script src="/js/update.js"></script>
<%@ include file="../layout/footer.jsp"%>
2. update.js
- javascript에서 받은 form형태를 ajax의 비동기 통신을 통하여 put방법으로 controller로 보냅니다.
- 해당 $. ajax({}) 안에 존재하는 url부분은 홑따옴표('')가 아닌 빽틱(``)입니다.
// 파일 업로드
function update(userId, event) {
event.preventDefault(); // 폼태그 액션을 막음
let data = $("#profileUpdate").serialize();
// 폼의 객체를 한번에 받기 위해 주로 ajax 사용할때
console.log(data);
$.ajax({
type:"put",
url: `/api/user/${userId}`,
data: data,
contentType:"application/x-www-form-urlencoded; charset=utf-8",
dataType:"json"
}).done(res=>{ // Http 200번대 성공
alert("success");
location.href=`/user/${userId}`;
}).fail(error=>{ // Http 200번대가 아닐때
console.log(error);
if(error.data == null){
alert(error.responseJSON.message);
}else{
alert(JSON.stringify(error.responseJSON.data));
}
});
}
3. application.yml
- springboot를 실행할 때 기본적으로 필요한 요소를 적음
- datasource는 mariadb를 직접 지정해야 사용할 수 있음
- 변경점 없음
server:
port: 8080(자신이 7070으로 설정했다면 localhost:7070으로 들어가야함)
servlet:
context-path: /
encoding:
charset: utf-8
enabled: true
spring:
mvc:
view:
prefix: (src/main밑에 있는 폴더명에 저장)
suffix: .jsp
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:(mybatis 연결포트)/(자신이 연결한 데이터베이스 이름)?serverTimezone=Asia/Seoul
username: (자신이 만든 아이디)
password: (자신이 만든 비밀번호)
jpa:
open-in-view: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
servlet:
multipart:
enabled: true
max-file-size: 2MB
security:
user:
name: test
password: 1234
4. UserController.java
- @RequiredArgsConstrutor : private final키워드의 생성자를 사용할 수 있게 합니다.
package com.pjh.comapny.web;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.pjh.comapny.config.auth.PrincipalDetails;
import com.pjh.comapny.domain.user.User;
import com.pjh.comapny.service.UserService;
import com.pjh.comapny.web.dto.user.UserProfileDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class UserController {
//private final UserService userService;
@GetMapping("/user/{id}/update")
public String update(
@PathVariable int id,
@AuthenticationPrincipal PrincipalDetails principalDetails) {
//System.out.println("session information : "+principalDetails.getUser());
// 직접 찾는 방법
/*Authentication auth = SecurityContextHolder.getContext().getAuthentication();
PrincipalDetails mPrincipalDetails = (PrincipalDetails) auth.getPrincipal();
System.out.println("direct session information : "+mPrincipalDetails);*/
//model.addAttribute("principal", principalDetails.getUser());
return "/user/update";
}
}
5. UserApiController.java
- @RestController : jsp파일에서 json데이터를 받아 데이터를 리턴합니다.
package com.pjh.comapny.web.api;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.nimbusds.oauth2.sdk.http.HTTPEndpoint;
import com.pjh.comapny.config.auth.PrincipalDetails;
import com.pjh.comapny.domain.user.User;
import com.pjh.comapny.handler.ex.CustomValidationApiException;
import com.pjh.comapny.handler.ex.CustomValidationException;
import com.pjh.comapny.service.SubscribeService;
import com.pjh.comapny.service.UserService;
import com.pjh.comapny.web.dto.CMRespDto;
import com.pjh.comapny.web.dto.subscribe.SubscribeDto;
import com.pjh.comapny.web.dto.user.UserUpdateDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@RestController
public class UserApiController {
private final UserService userService;
@PutMapping("/api/user/{id}") // 갱신
public CMRespDto<?> update(
@PathVariable int id,
@Valid UserUpdateDto userUpdateDto,
BindingResult bindingResult,
// bindingResult는 꼭 @Valid가 적혀있는 parameter에 적어줘야함.
// --> 강제임, 순서 뒤바뀌면 에러남
@AuthenticationPrincipal PrincipalDetails principalDetails) {
if(bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
for(FieldError error : bindingResult.getFieldErrors()) {
errorMap.put(error.getField(), error.getDefaultMessage());
/*System.out.println("===================================");
System.out.println(error.getDefaultMessage());
// testMessage
System.out.println("===================================");*/
}
throw new CustomValidationApiException("유효성 검사 실패", errorMap);
//RuntimeException->exceptionHandler로 처리
}else {
User userEntity = userService.회원수정(id, userUpdateDto.toEntity());
principalDetails.setUser(userEntity); // 세선 정보 변경
return new CMRespDto<>(1, "회원수정 완료",userEntity);
// 응답시에 userEntity의 모든 getter함수가 호출되어 JSON으로 파싱하여 응답
// List<Image> image -> 무한루프
}
}
}
6. UserUpdateDto.java
- toEntity()의 builder(). build() 형태를 통해 순서가 뒤 바뀌어도 유저 객체에 전달되게 합니다.
package com.pjh.comapny.web.dto.user;
import javax.validation.constraints.NotBlank;
import com.pjh.comapny.domain.user.User;
import lombok.Builder;
import lombok.Data;
@Data
public class UserUpdateDto {
@NotBlank
private String name; // 필수
@NotBlank
private String password; // 필수
private String website;
private String bio;
private String phone;
private String gender;
// 필수제약조건이 아닌 자료형은 Entity형으로 묶어서 builder하는 것은 안좋음.
// User 객체에 @Builder가 있기에 해당 toEntity() method는 그대로 사용 가능
public User toEntity() {
return User.builder()
.name(name) // name을 기재 안했다면 문제 생김, Validation check
.password(password) // password를 기재 안했다면 문제 생김, Validation check
.website(website)
.bio(bio)
.phone(phone)
.gender(gender)
.build();
}
}
7. CMRespDto.java
- 해당 파일은 공통적으로 발생하는 응답에 대한 dto입니다.
package com.pjh.comapny.web.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto<T> {
private int code; // 1(성공), -1(실패)
private String message;
private T data;
}
8. UserService.java
- @Service 비즈니스 로직을 구현을 위해 생성하는 service 어노테이션입니다.
- @Transactional DB에 트랜잭션(일의 최소 단위) 처리를 springboot에서 구현한 형태로 수행할 수 있게 합니다.
- BCrytPasswordEncoder : DB에 저장된 패스워드를 bcryt 화하여 저장합니다.
패스워드를 암호화해 주는 메서드입니다. encde() 메서드는 SHA-1,
8바이트로 결합된 해쉬, 랜덤 하게 생성된 솔트(salt)를 지원합니다.
ex) 비밀번호 : 1234 -> db에 저장되는 비밀번호 :
$2a$10$0i10 aVWcB9 SDN6 UYT2 hav.m3 phbL6 rFhlqSKTIIHsqyoFmfHdrGbC
package com.pjh.comapny.service;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.pjh.comapny.config.auth.PrincipalDetails;
import com.pjh.comapny.domain.user.User;
import com.pjh.comapny.domain.user.UserRepository;
import com.pjh.comapny.domain.user.subscribe.SubScribeRepository;
import com.pjh.comapny.handler.ex.CustomApiException;
import com.pjh.comapny.handler.ex.CustomException;
import com.pjh.comapny.handler.ex.CustomValidationApiException;
import com.pjh.comapny.web.dto.user.UserProfileDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Transactional
public User 회원수정(int id, User user) {
User userEntity = userRepository.findById(id)
.orElseThrow(()->{
return new CustomValidationApiException("찾을 수 없는 id입니다.");
});
// 1. 영속화 -> 프로그램이 종료되더라도 데이터(userEntity)는 남아있음
// db에 table와 table의 key, value가 남아있다 = 영속화
// optional -> 1. 무조건 찾을 수 있게 2. 못찾았으면 익센션 발동 orElseThrow()
// db에 회원 id를 찾는 과정
userEntity.setName(user.getName());
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
userEntity.setPassword(encPassword);
userEntity.setBio(user.getBio());
userEntity.setWebsite(user.getWebsite());
userEntity.setPhone(user.getPhone());
userEntity.setGender(user.getGender());
// 2. 영속화된 오브젝트를 수정 - 더티체킹(업데이트 완료)
return userEntity;
// 더티체킹
}
}
8. UserRepository
- DB와 직접 맞닿아 있는 인터페이스입니다.
- DB에 처리할 SQL문을 직접 사용할 수 있고 extends 하여 JpaRepository의 crud형태의 기본적인 연산이 지원됩니다.
- 변경점 없
package com.pjh.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
// DAO
// 자동으로 Bean 등록
public interface UserRepository extends JpaRepository<User, Integer>{
// table User을 관리하는 인터페이스, PK는 integer
User findByUsername(String username);
}
'Springboot' 카테고리의 다른 글
스프링 이미지 업로드(2) (0) | 2023.11.07 |
---|---|
스프링 메모장 (1) | 2023.11.01 |
스프링 이미지 업로드(1) (1) | 2023.10.30 |
스프링 회원가입(2) (1) | 2023.10.23 |
스프링 회원가입(1) (2) | 2023.10.20 |