스프링 회원 수정

2023. 10. 23. 20:00Springboot

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