궤도
[Spring] [면적면적[13]] 프로필 페이지 본문
북적북적의 백엔드를 클론하고 있다.
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를 구현했다.