궤도

[Spring] [면적면적[13]] 프로필 페이지 본문

💻 현생/📕 면적면적(스프링 실습)

[Spring] [면적면적[13]] 프로필 페이지

영이오 2021. 8. 19. 20:05

북적북적의 백엔드를 클론하고 있다.

 

Github

 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.5.2'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
	id 'java'
}

group = 'jpa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
	all {
		exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-log4j2'
	implementation 'io.jsonwebtoken:jjwt:0.9.1'
	implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE'
	implementation 'org.mapstruct:mapstruct:1.4.2.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
	implementation 'com.querydsl:querydsl-jpa'
	implementation 'org.projectlombok:lombok:1.18.18'
	runtimeOnly 'mysql:mysql-connector-java'
	implementation 'com.googlecode.json-simple:json-simple:1.1.1'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

def querydslDir = "$buildDir/generated/querydsl"

querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}

sourceSets {
	main.java.srcDir querydslDir
}

configurations {
	querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}

test {
	useJUnitPlatform()
}

 

 

src/main/resources/application.yml

aladin:
  url: http://www.aladin.co.kr/ttb/api/ItemLookUp.aspx?ttbkey=비밀키&itemIdType=ISBN13&output=js&Version=20131101&ItemId={isbn}
naver:
  id: 아이디
  secret: 비번
secret:
  key: 비번
cloud:
  aws:
    credentials:
      access-key: 엑세스 키
      secret-key: 시크릿 키
    s3:
      bucket: 버킷 이름
    region:
      static: 지역
    stack:
      auto: false
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3305/데베?serverTimezone=UTC&characterEncoding=UTF-8
    username: root
    password: 비번
  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    open-in-view: false
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        format_sql: true
        show-sql: true
#logging:
#  level:
#    org:
#      hibernate:
#        type:
#          descriptor:
#            sql: trace
server:
  servlet:
    encoding:
      force-response: true
logging:
  level:
    com:
      amazonaws:
        util:
          EC2MetadataUtils: error

요구사항 정리

프로필

[Request] {GET}

[Response] {nickname, img, HttpStatus.OK}

 

유저 정보

[Request] {GET}

[Response] {nickname, email, HttpStatus.OK}

 

프로필 이미지 수정

[Request] {img, PUT}

[Response] {HttpStatus.NO_CONTENT}

 

닉네임 수정

[Request] {nickname, PUT}

[Response] {HttpStatus.NO_CONTENT}

 

로그아웃

[Request] {POST}

[Response] {HttpStatus.NO_CONTENT}


프로필 정보

 

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class UserProfileDto {

        private String nickname;
        private String img;
    }

닉네임과 이미지를 리턴하는 dto

 

    /**
     * profile
     *
     * @param user
     * @return UserProfileDto
     */
    public UserProfileDto profile(User user) {
        return UserProfileDto.builder()
                .nickname(user.getNickname())
                .img(user.getImg())
                .build();
    }

서비스

 

    /**
     * 프로필 정보
     * localhost:8080/profile
     *
     * @param user
     * @return ResponseEntity
     */
    @GetMapping("")
    public ResponseEntity<?> profile(@AuthenticationPrincipal User user) {
        log.info("[Request] User profile " + user.getEmail());
        return new ResponseEntity<>(profileService.profile(user), HttpStatus.OK);
    }

컨트롤러

 

    @Test
    @DisplayName("Profile | Success")
    void profileSuccess() throws Exception {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        UserProfileDto profile = profileService.profile(user);
        assertEquals("hello", profile.getNickname());
        assertNull(profile.getImg());
    }

테스트 코드.

이건 나중에 한번에 돌려보도록 하겠다.

 


유저 정보

 

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class UserInfoDto {

        private String email;
        private String nickname;
    }

사용자의 이메일과 닉네임을 리턴하는 dto

 

    /**
     * info
     *
     * @param user
     * @return UserInfoDto
     */
    public UserInfoDto info(User user) {
        return UserInfoDto.builder()
                .email(user.getEmail())
                .nickname(user.getNickname())
                .build();
    }

서비스

 

    /**
     * 사용자 정보
     * localhost:8080/profile/info
     *
     * @param user
     * @return ResponseEntity
     */
    @GetMapping("/info")
    public ResponseEntity<?> info(@AuthenticationPrincipal User user) {
        log.info("[Request] User info "+user.getEmail());
        return new ResponseEntity<>(profileService.info(user), HttpStatus.OK);
    }

컨트롤러

 

    @Test
    @DisplayName("User info | Success")
    void infoSuccess() throws Exception {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        UserInfoDto info = profileService.info(user);
        assertEquals("test@test.com", info.getEmail());
        assertEquals("hello", info.getNickname());
    }

테스트 코드

 


프로필 이미지 수정

 

이 부분은 지난번 책 정보를 수정할 때와 유사하다.

 

    /**
     * 사용자 이미지 수정
     * localhost:8080/profile/info/img
     *
     * @param user
     * @param img
     * @return ResponseEntity
     */
    @PutMapping("/info/img")
    public ResponseEntity<?> img(@AuthenticationPrincipal User user,
                                 @RequestPart(required = false) MultipartFile img) {
        log.info("[Request] Update user img "+user.getEmail());
        profileService.updateImg(user, img);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

컨트롤러구요

 

    /**
     * updateImg
     *
     * @param user
     * @param file
     */
    @Transactional
    public void updateImg(User user, MultipartFile file) {
        String img = null;
        if (file != null) {
            try {
                img = s3Service.upload(file);
            } catch (IOException e) {
                throw new S3Exception("file = " + file.getOriginalFilename());
            }
        }
        user.updateUserImg(img);
        userRepository.save(user);
    }

이건 서비스단이고

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class User implements UserDetails {

    @Id
    @Column(name = "user_sn")
    @GeneratedValue
    private Long id;

    @Column(unique = true)
    @NotNull
    private String email;

    @Column(length = 300)
    @NotNull
    private String password;

    @NotNull
    private String nickname;

    @Column(name = "user_img")
    private String img;

    @Builder.Default
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Book> books = new ArrayList<>();

    @Builder.Default
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<UserCharacter> userCharacters = new ArrayList<>();

    @Setter
    private String refreshTokenValue;

    @ElementCollection(fetch = FetchType.EAGER)
    @Builder.Default
    private List<String> roles = new ArrayList<>();

    public void updateUserImg(String img) {
        this.img = img;
    }
}

프사 업데이트 로직은 유저 도메인에 있습니다.

 

    @Test
    @DisplayName("Update img | Success")
    void updateImgSuccess() throws Exception {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        user.updateUserImg("tmp_tmp");
        userRepository.save(user);

        assertNotNull(user.getImg());
        profileService.updateImg(user, null);
        assertNull(user.getImg());
    }

간단한 테스트 코드

이건 프사가 있는 상태에서 프사를 삭제하는 테스트

 

 


닉네임 수정

 

    /**
     * 사용자 닉네임 수정
     * localhost:8080/profile/info/nickname
     *
     * @param user
     * @param userNickNameReqDto
     * @return ResponseEntity
     */
    @PutMapping("/info/nickname")
    public ResponseEntity<?> nickname(@AuthenticationPrincipal User user,
                                      @RequestBody UserNickNameReqDto userNickNameReqDto) {
        log.info("[Request] Update user nickname "+user.getNickname());
        profileService.updateNickname(user, userNickNameReqDto);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

컨트롤러

 

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @NotNull
    public static class UserNickNameReqDto {

        private String nickname;
    }

겨우 하나 받는거긴 하지만 뭐...dto도 만들고

 

    /**
     * updateNickname
     *
     * @param user
     * @param userNickNameReqDto
     */
    @Transactional
    public void updateNickname(User user, UserNickNameReqDto userNickNameReqDto) {
        user.updateNickname(userNickNameReqDto.getNickname());
        userRepository.save(user);
    }
    public void updateNickname(String nickname) {
        this.nickname = nickname;
    }

서비스 코드도 간단하게 만들고 비즈니스 로직은 도메인에 넣기

 

    @Test
    @DisplayName("Update nickname | Success")
    void updateNickname() throws Exception {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        profileService.updateNickname(user, new UserNickNameReqDto("new nickname"));
        assertEquals("new nickname", user.getNickname());
    }

테스트

 

 


로그아웃

 

사실 로그아웃은 맨처음 회원가입, 로그인 구현했을 때 만들었는데 이제야 올린다.

로그아웃으로 시작해서 로그아웃으로 끝나는 수미상관 구조...

 

알아보니까 로그아웃을 하려면 refresh token을 없애면 된다고 한다. access token은 유효기간이 짧기도 하고 프론트에서 폐기해야 한다는 것 같다.

 

    /**
     * 로그아웃
     * localhost:8080/profile/sign-out
     *
     * @param user
     * @return ResponseEntity
     */
    @PostMapping("/sign-out")
    public ResponseEntity<?> signOut(@AuthenticationPrincipal User user) {
        log.info("[Request] sign-out " + user.getEmail());
        profileService.signOut(user);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

컨트롤러

 

    /**
     * signOut
     *
     * @param user
     * @return UserDto
     */
    @Transactional
    public void signOut(User user) {
        user.setRefreshTokenValue(null);
        userRepository.save(user);
    }

refresh token은 그냥 setter를 열어놨었다. 그래서 그냥 null로 set하고 save해서 반영한다.

 

    @Test
    @DisplayName("User Sign-Out | Success")
    void signOutSuccess() throws Exception {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);
        assertNotNull(user.getRefreshTokenValue());
        profileService.signOut(user);
        assertNull(user.getRefreshTokenValue());
    }

테스트 코드

 

이전 테스트코드들까지 한번에 돌린 테스트 결과

 

 

이렇게 북적북적의 모든 API를 구현했다.

Comments