minlog
article thumbnail

 

회원가입 시 프로필 업로드

기존에는 프로필사진 없이 회원가입을 진행하는 로직이였는데,

프로필 사진도 가입 시 추가 가능하도록 리팩토링 해보았다. 

 

 

00) 설정 파일 수정

( build.gradle ,application.properties )

 

  • 디펜턴시 추가
    • Apache Commons IO 라이브러리에는 유틸리티 클래스, 스트림 구현, 파일 필터, 파일 비교기, endian 변환 클래스 등이 포함되어 사용할 수 있습니다.

📑build.gradle

더보기
// https://mvnrepository.com/artifact/commons-io/commons-io 파일 저장
implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'

 

  • properties 추가
    • 멀티파트 사용여부 : spring.servlet.multipart.enabled=true
    • 요청 파일 하나의 사이즈 : spring.servlet.multipart.max-file-size=10MB
    • 요청 파일 총 사이즈  : spring.servlet.multipart.max-request-size=20MB
    • 임시 파일 업로드 : spring.servlet.multipart.location= 경로
    • 실제 파일 업로드 : file.upload.path= 경로

📑application.properties

더보기
# file
# total file size cannot exceed 10MB.
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
# total request size for a multipart/form-data cannot exceed 20MB.
spring.servlet.multipart.max-request-size=20MB
spring.servlet.multipart.location= C:\\back_project\\travel\\src\\main\\resources\\static\\upload\\
file.upload.path= C:\\back_project\\travel\\src\\main\\resources\\static\\upload\\

 

 

01) 회원과 이미지 객체 Domain

( UserTravel, Image, UserImage )

이미지 객체는 회원 프로필 외에도 사용될 예정으로 공통 사용하는 속성을 정의하여 UserImage(프로필 객체)에서 상속받는다.

 

📑UserTravel.java

package com.example.travel.domain;

import lombok.*;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;


@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@ToString(exclude = "roleSet")
public class UserTravel extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userNo;
    private String userId;
    private String userEmail;
    private String password;
    private String name;
    private String userBirthday;
    private String userGender;
    private String userPhone;

    @OneToOne(fetch = FetchType.LAZY)
    private UserImage userImg;//프로필 사진
    //주소
    private String addressPostcode;
    private String address;
    private String addressDetail;
    private String addressExtra;

    private Boolean userAgree;//개인정보 동의

    private Boolean userSocial;//소셜회원 정보


    @ElementCollection(fetch = FetchType.LAZY)
    @Builder.Default
    private Set<UserRole> roleSet = new HashSet<>();
    public void roleAdd(UserRole userRole){
        roleSet.add(userRole);
    }


    //이미지 업데이트 시 사용
    public void updateUserImage(UserImage userImg) {
        this.userImg = userImg;
    }



}

 

📑Image.java

더보기
package com.example.travel.domain;

import lombok.*;
import lombok.experimental.SuperBuilder;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
@AllArgsConstructor
public abstract class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String uuid;
    private String fileName;
    private String originFileName;
    private String fileUrl;
}

 

📑UserImage.java

더보기
package com.example.travel.domain;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import javax.persistence.Entity;

@Entity
@Getter
@NoArgsConstructor
@SuperBuilder
public class UserImage extends Image {
}

 

 

 

02) 회원가입 폼 수정

(join.html)

  • Form태그에 속성추가 
    • <form id="joinForm" th:action="@{/join}" enctype="multipart/form-data" th:method="post" class="box content_box" >
  • 인풋 파일에 속성 추가
    • <input type="file" th:field="*{userImg}"  name="" id="userImg" accept="image/*" multiple="multiple">

📑join.html

더보기
<form id="joinForm" th:action="@{/join}" enctype="multipart/form-data" th:method="post" class="box content_box" >
  <input th:type="hidden" name="idCheckValue" id="idCheckValue" value="false">
  <input th:type="hidden" name="emailCheckValue" id="emailCheckValue" value="false">
  <input th:field="*{userEmail}" name="userEmail" id="userEmail" th:errorclass="err" class="form-control" type="hidden">
  <table>
    <colgroup>
      <col style="width:100px">
      <col style="width:calc(100% - 100px)">
    </colgroup>
    <tbody>
    <tr>
      <th>
        <label for="userId">
          아이디 <span class="txt_info ck">*</span>
        </label>
      </th>
      <td>
        <div class="x_auto">
          <input th:field="*{userId}" name="userId" id="userId" class="form-control id"
               th:errorclass="err" type="text" placeholder="아이디를 입력하세요.">
          <button type="button" id="btnIdCheck" class="btn btn-dark btn-sm ml-2">확인</button>
        </div>
        <p class="check-msg-id txt_info mt-1"></p>
      </td>
    </tr>
    <tr>
      <th>비밀번호 <span class="txt_info ck">*</span></th>
      <td>
        <input th:field="*{password}" name="password" id="password" type="password" class="form-control" placeholder="영어와 숫자로 포함해서 6~12자리 이내로 입력해주세요.">
      </td>
    </tr>
    <tr>
      <th><label for="name">이름 <span class="txt_info ck">*</span></label></th>
      <td><input th:field="*{name}"  name="name" id="name" class="form-control" type="text" placeholder="이름을 입력하세요."></td>
    </tr>
    <tr>
      <th>생년월일 <span class="txt_info ck">*</span></th>
      <td><input th:field="*{userBirthday}"  name="userBirthday" id="userBirthday" th:errorclass="err" class="form-control" type="text" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');" placeholder="생년월일 6자리를 입력하세요." maxlength="6"></td>
    </tr>

    <tr>
      <th><label for="userEmail1">이메일 <span class="txt_info ck">*</span></label></th>
      <td>

        <div class="input-group">
          <input type="text" class="form-control" name="userEmail1" id="userEmail1" placeholder="이메일" >
          <select class="form-select select" name="userEmail2" id="userEmail2" >
            <option>@naver.com</option>
            <option>@daum.net</option>
            <option>@gmail.com</option>
            <option>@hanmail.com</option>
            <option>@yahoo.co.kr</option>
          </select>
          <div class="input-group-addon">
            <button type="button" class="btn btn-dark" id="mail-Check-Btn" >본인인증</button>
          </div>
        </div>
        <div class="x_auto mt-1 mail-check-wrap" style="display:none;">
          <div class="mail-check-box">
            <input class="form-control mail-check-input" placeholder="인증번호 8자리를 입력해주세요!" disabled="disabled" maxlength="8">
            <p id="timerUp">
              <span id="timer" class="point-color"></span>
              <i class='bi bi-clock point-color'></i>
            </p>
          </div>
          <button id="mailCheckPw" class="btn btn-danger btn-sm ml-2" type="button">인증확인</button>
        </div>
        <p class="mail-check-warn check-msg-email txt_info mt-1"></p>
      </td>
    </tr>

    <tr>
      <th>연락처 <span class="txt_info ck">*</span></th>
      <td><input th:field="*{userPhone}" name="userPhone" id="userPhone"   class="form-control" type="text"  placeholder="연락처를 입력하세요. EX) 010-0000-0000"></td>
    </tr>
    <tr>
      <th>주소  <span class="txt_info ck">*</span></th>
      <td class="address_wrap">
        <div class="address_post x_auto">
          <input type="text" id="addressPostcode" th:field="*{addressPostcode}" placeholder="우편번호" class="form-control">
          <input type="button" onclick="addressPostcodes()" value="우편번호 찾기" class="btn btn-dark btn-sm ml-2">
        </div>
        <div class="address_info">
          <input type="text" id="address" th:field="*{address}" placeholder="주소" class="form-control">
        </div>
        <div class="address_detail x_auto">
          <input type="text" id="addressDetail" th:field="*{addressDetail}" placeholder="상세주소" class="form-control">
          <input type="text" id="addressExtra" th:field="*{addressExtra}" placeholder="참고항목" class="form-control ml-2">
        </div>


      </td>
    </tr>
    <tr>
      <th>성별</th>
      <td>
        <label for="userGenderM">
          <input name="userGender" id="userGenderM" value="userGenderM" type="radio"  th:checked="${userTravel.userGender eq 'userGenderM'}" checked="checked" /> 남성
        </label>
        <label for="userGenderW" class="ml-2">
          <input  name="userGender" id="userGenderW" value="userGenderW" type="radio" th:checked="${userTravel.userGender eq 'userGenderW'}" /> 여성
        </label>
      </td>
    </tr>
    <tr>
      <th>프로필 <br />이미지</th>
      <td>
        <div class="userimg_wrap">
          <div class="userimg_box">
            <img id="preview" src="" alt="">
          </div>
          <label for="userImg" class="btn btn-dark btn-sm ">
            👆프로필 선택하기👆
          </label>
          <input type="file" th:field="*{userImg}"  name="" id="userImg" accept="image/*" multiple="multiple">
        </div>
      </td>
    </tr>
    </tbody>
  </table>

  <label for="userAgree" class="mt-2">
    <input th:field="*{userAgree}" name="userAgree" id="userAgree" type="checkbox"/> 개인정보 동의 <span class="txt_info ck">*</span>
  </label>
  <div class="user_agree_wrap mt-2">
    여행지 찾기'메뉴 에서 모두와 공유하고 소통할 수 있어요.
  </div>

  <a class="w-100 btn btn-primary mt-4" href="javascript:void(0)" onclick="joinForm()" >가입하기</a>


</form>

 

 

 

03) 회원가입 폼 저장 Controller

( UserController.java )

위 화면에서 가입 버튼을 클릭하면 Post 방식으로 📑UserController.java 파일에 전달된다.

 

  • 전달 파라미터 
    • 화면에서 전달 받은 값 📑UserDTO  :  @Valid @ModelAttribute("userTravel") UserDTO user@ModelAttribute  : 객체를 맵핑 하여 받아온다.@Valid : 객체에서 지정한 제약 조건을 검사하는 어노테이션이다. 
    • 검증오류 내용을 보관하는 객체 :  BindingResult bindingResult
    • 리다이렉트로 페이지를 이동할 시 전달 하는 값: RedirectAttributes redirectAttributes
  • 오류가 있을 시, 다시 작성폼으로 이동
    • bindingResult.hasErrors()
    • 📑join.html   : ... <span th:if="${#fields.hasErrors('userEmail')}" th:errors="*{userEmail}"></span> ...
  • 오류가 없을 시 
    • 📑UserService 회원가입 로직  실행 : UserTravel userTravel1 = userService.userSave(user);
    • 📑join.html   : ... <span th:if="${#fields.hasErrors('userEmail')}" th:errors="*{userEmail}"></span> ...
    • 📑MailSendService 회원가입 메일 전달  : mailSendService.sendEmail(userEmail, userTravel1.getName(),"joinSuccess");
    • 성공 메시지 리턴 화면으로 전달 : redirectAttributes.addFlashAttribute("joinSuccess","회원가입이 완료되었습니다.");
    • 회원 성공 후 로그인 페이지로 이동 : return "redirect:/loginForm";

 

📑UserController.java

더보기
@PostMapping("join")
public String userJoin(@Valid @ModelAttribute("userTravel") UserDTO user,
                       BindingResult bindingResult,
                       RedirectAttributes redirectAttributes
) throws Exception, IOException {

        if(bindingResult.hasErrors()) {
            log.info("회원 가입 실패");
            log.info(bindingResult.hasErrors());
            return "member/join";
        }else{
            //성공 로직
            System.out.println(user.getUserImg());
            System.out.println("-------------------------------");
            UserTravel userTravel1 = userService.userSave(user);

            log.info("회원가입 성공 :{}" , userTravel1);


            String userEmail = user.getUserEmail();
            mailSendService.sendEmail(userEmail, userTravel1.getName(),"joinSuccess");
            redirectAttributes.addFlashAttribute("joinSuccess","회원가입이 완료되었습니다.");
            return "redirect:/loginForm";
        }

}

 

📑UserDTO.java

더보기
package com.example.travel.dto.user;


import com.example.travel.domain.UserRole;
import lombok.*;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;

import javax.persistence.ElementCollection;
import javax.persistence.FetchType;
import javax.validation.constraints.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "roleSet")
@Builder
@Validated
public class UserDTO {
    private Long userNo;
    @NotNull(message = "아이디 : 아이디를 입력하세요.")
    private String userId;
    @Email(message = "이메일 : 올바른 형식의 이메일주소를 입력해주세요.")
    @NotBlank(message = "이메일 : 이메일을 입력하세요.")
    private String userEmail;

    @NotBlank(message = "비밀번호 : 비밀번호는 영어와 숫자로 포함해서 6~12자리 이내로 입력해주세요.")
    @Pattern(regexp="[a-zA-Z1-9]{6,12}", message = "비밀번호는 영어와 숫자로 포함해서 6~12자리 이내로 입력해주세요.")
    private String password;

    @NotBlank(message = "이름 : 이름을 입력하세요.")
    private String name;

    @NotBlank(message = "연락처 : 연락처를 입력하세요.")
    private String userPhone;

    @NotBlank(message = "생년월일 : 생년월일 6자리를 입력하세요.")
    @Length(min = 6,max = 6,message = "생년월일 : 생년월일 6자리를 입력하세요.")
    private String userBirthday;

    //주소
    private String addressPostcode;
    private String address;
    private String addressDetail;
    private String addressExtra;

    private String userGender;
    private MultipartFile userImg;


    @NotNull(message = "개인정보 동의를 체크해주세요.")
    private Boolean userAgree; //개인정보 동의

    private Boolean userSocial;

    @ElementCollection(fetch = FetchType.LAZY)
    @Builder.Default
    private Set<UserRole> roleSet = new HashSet<>();
    public void roleAdd(UserRole userRole){
        roleSet.add(userRole);
    }

}

 

 

04) 회원가입 서비스 로직  Service

(UserService.interface  , UserServiceImpl.java,FileService.java)

회원관련 서비스를 정의하는 📑UserService 인터페이스와  📑UserService 인터페이스를 상속받아 구현한 📑UserServiceImpl 서비스 파일을 저장하는 로직을 가지고 있는 📑FileService가 사용된다.

 

📑UserService.interface

더보기
package com.example.travel.service.user;

...

public interface UserService {
    public UserTravel userSave(UserDTO userSaveDTO); // user 저장
    ...
    @Transactional
    public UserResponseDTO userInfo(UserTravelDTO userDTO);


    //entity - dto  : 받는 값
    default UserTravel dtoToEntity(UserDTO dto){
        UserTravel result = UserTravel.builder()
                .userNo(dto.getUserNo())
                .userId(dto.getUserId())
                .userEmail(dto.getUserEmail())
                .password(dto.getPassword())
                .name(dto.getName())
                .userBirthday(dto.getUserBirthday())
                .userGender(dto.getUserGender())
                .userPhone(dto.getUserPhone())
                //.userImg((UserImage)dto.getUserImg())
                .address(dto.getAddress())
                .addressPostcode(dto.getAddressPostcode())
                .addressDetail(dto.getAddressDetail())
                .addressExtra(dto.getAddressExtra())
                .userAgree(dto.getUserAgree())
                .userSocial(dto.getUserSocial())
                .build();

        System.out.println("role ---------------");
        Set<UserRole> roleSet = dto.getRoleSet();
        roleSet.stream().forEach(userRole -> {
            result.roleAdd(userRole);
        });


        return result;
    }

    default UserDTO entityToDto(UserTravel userTravel){
        UserDTO result = UserDTO.builder()
                .userNo(userTravel.getUserNo())
                .userId(userTravel.getUserId())
                .userEmail(userTravel.getUserEmail())
                .password(userTravel.getPassword())
                .name(userTravel.getName())
                .userBirthday(userTravel.getUserBirthday())
                .userGender(userTravel.getUserGender())
                .userPhone(userTravel.getUserPhone())
                //.userImg((MultipartFile)userTravel.getUserImg())
                .address(userTravel.getAddress())
                .addressPostcode(userTravel.getAddressPostcode())
                .addressDetail(userTravel.getAddressDetail())
                .addressExtra(userTravel.getAddressExtra())
                .userSocial(userTravel.getUserSocial())
                .build();

        System.out.println("role ---------------");
        Set<UserRole> roleSet = userTravel.getRoleSet();
        roleSet.stream().forEach(userRole -> {
            result.roleAdd(userRole);
        });

        return result;
    }

	...



}

 

 

 

  •   파일저장 경로 : @Value 어노테이션을 사용하여  📑application.properties 에서 필요한 값을 가져온다.
        @Value("${spring.servlet.multipart.location}")
        private String uploadPath;
  • 회원가입 시 전달 받은 이미지가 있다면 이미지 파일 저장 후 이미지 객체 전달하여 User객체를 저장 한다.

 

📑UserServiceImpl.java

더보기
package com.example.travel.service.user;

...

@RequiredArgsConstructor
@Service
@Log4j2
public class UserServiceImpl implements UserService {

    final UserRepository userRepository;
    final PasswordEncoder passwordEncoder;

    // 이미지 관련 추가
    private final FileService fileService;
    private final UserImageRepository userImageRepository;

    @Value("${spring.servlet.multipart.location}")
    private String uploadPath;

    @Override
    @Transactional
    public UserTravel userSave(UserDTO userDto) {
        log.info("user 객체 저장 및 반환 ==========================");
        log.info("userSaveDTO : {}" , userDto);
        // 일반회원 가입
        userDto.setUserSocial(false);
        userDto.setPassword(passwordEncoder.encode(userDto.getPassword())); // 패스워드 암호화
        UserTravel entity = dtoToEntity(userDto); //entity 변경
        entity.roleAdd(UserRole.USER); // 권한 추가

        System.out.println();
        log.info("getUserImg : " + userDto.getUserImg());
        if(!(userDto.getUserImg() == null)) {
            log.info("이미지 파일이 있을때");
            UserImage userImage = saveMemberImage(userDto.getUserImg());
            entity.updateUserImage(userImage);
        }

        UserTravel result = userRepository.save(entity);
        return result;
    }



    //이미지 관련
    @Transactional(readOnly = false)
    UserImage saveMemberImage(MultipartFile file) {
        log.info("이미지 저장 ==========================");
        if(file.getContentType().startsWith("image") == false) {
            log.warn("이미지 파일이 아닙니다.");
            return null;
        }

        String originalName = file.getOriginalFilename();
        Path root = Paths.get(uploadPath, "member");

        try {
            ImageDTO imageDTO =  fileService.createImageDTO(originalName, root);
            UserImage memberImage = UserImage.builder()
                    .originFileName(imageDTO.getOriginFileName())
                    .uuid(imageDTO.getUuid())
                    .fileName(imageDTO.getFileName())
                    .fileUrl(imageDTO.getFileUrl())
                    .build();

            file.transferTo(Paths.get(imageDTO.getFileUrl()));

            return userImageRepository.save(memberImage);
        } catch (IOException e) {
            e.printStackTrace();
            log.warn("업로드 폴더 생성 실패: " + e.getMessage());
        }

        return null;
    }


   		...
}

 

📑FileService.java

더보기
package com.example.travel.service;

import com.example.travel.dto.ImageDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;

@Service
@Log4j2
public class FileService {

    @Transactional
    public ImageDTO createImageDTO(String originalSourceName, Path path) throws IOException {
        String fileName = originalSourceName.substring(originalSourceName.lastIndexOf("\\") + 1);
        String uuid = UUID.randomUUID().toString();
        String fileUrl = getDirectory(path) + File.separator + uuid + "_" + fileName;
        String originFileName = uuid + "_" + fileName;
        return ImageDTO.builder()
                .fileName(fileName)
                .originFileName(originFileName)
                .uuid(uuid)
                .fileUrl(fileUrl)
                .build();
    }

    private String getDirectory(Path path) throws IOException {
        if(!Files.exists(path)) {
            Files.createDirectory(path);
        }
        return path.toString();
    }
}

 

profile

minlog

@jimin-log

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!