Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#10] 나의 여행 등록 설정 #12

Merged
merged 35 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8e5d8fd
feat: 토큰 검증 argument resolver 추가
hynxp Jan 23, 2025
5e9d7fc
feat: 사용자 여행 일정 관련 도메인 추가
hynxp Jan 23, 2025
6a910f6
chore: starter-validation 의존성 추가
hynxp Jan 23, 2025
844bbb0
style: travel->travelschedule 패키지명 변경
hynxp Jan 23, 2025
4cfa1d9
test: city, country 기본 데이터 추가 세팅 및 테스트 db 변경
hynxp Jan 23, 2025
753f179
feat: 나의 여행 일정 추가 구현
hynxp Jan 23, 2025
f1a551a
test: 나의 여행 일정 서비스 테스트 추가
hynxp Jan 23, 2025
8891965
refactor: 미사용 repository 제거
hynxp Jan 23, 2025
d35d3ee
feat: 여행 일정 조회 구현
hynxp Jan 24, 2025
ee4048e
test: 여행 일정 조회 테스트 추가 및 기존 테스트 수정
hynxp Jan 24, 2025
1706182
test: 미사용 필드 제거
hynxp Jan 24, 2025
cc689d6
test: 여행 일정 컨트롤러 테스트 추가
hynxp Jan 24, 2025
e92d5a5
refactor: request, responsea 직렬화/역직렬화 어노테이션 설정
hynxp Jan 24, 2025
6d6449e
refactor: 여행 일정 설정 메서드 네이밍 변경
hynxp Jan 25, 2025
2f86ff1
refactor: 여행 일정 설정 메서드 네이밍 변경, city 테이블 반복 조회 개선
hynxp Jan 28, 2025
912d976
test: 존재하지 않는 도시 id로 여행 일정 등록 시 예외 테스트 코드 수정
hynxp Jan 28, 2025
1e7610b
refactor: 테스트 쪽 환경변수 및 스크립트 파일 제거
hynxp Jan 28, 2025
117d0fd
test: argument resolve 모킹 제거, 컨트롤러 테스트 REST-Assured으로 수정
hynxp Jan 28, 2025
354957e
refactor: member.sql 중복 삽입 방지
hynxp Jan 28, 2025
9200d68
refactor: member 내 테스트용 정적 팩토리 메서드 제거
hynxp Jan 28, 2025
5e9c116
refactor: 도메인 MemberTravelSchedule -> TravelSchedule 네이밍 수정
hynxp Jan 28, 2025
6f9420f
refactor: 여행 일정 관련 도메인 지연 로딩으로 변경
hynxp Jan 28, 2025
edf2890
test: @DisplayName -> 메서드명으로 변경, RestAssured로 수정
hynxp Jan 28, 2025
3361608
chore: 토큰 시간 관련 임의 환경변수로 세팅 및 테스트 코드 제거
hynxp Jan 29, 2025
4a02c5f
refactor: ~~NotFoundException 통일
hynxp Jan 29, 2025
fa4a5a5
refactor: 토큰에서 member추출 메서드 네이밍 변경
hynxp Jan 30, 2025
4fa5360
refactor: 사용하지 않는 에러코드 제거
hynxp Jan 30, 2025
105b7a3
refactor: 여행 일정 도시 추가 메서드 네이밍 변경
hynxp Jan 30, 2025
5686450
refactor: 테스트 환경 h2DB로 변경
hynxp Jan 30, 2025
ccafacc
refactor: Member TestFixture 사용, 테스트 데이터 초기화 클래스 추가
hynxp Jan 30, 2025
0865b99
chore: TestOauth 패키지 이동
hynxp Jan 30, 2025
73255cd
refactor: 멤버 id 상수 사용 부분 제거
hynxp Jan 30, 2025
41d769a
refactor: TravelScheduleResponse 변환 책임 이동, 국가 이름도 반환
hynxp Jan 30, 2025
39f0227
Merge branch 'main' of https://github.com/f-lab-edu/Udong into feat/r…
hynxp Jan 31, 2025
ef6d45f
refactor: staleObjectStateException, ObjectOptimisticLockingFailureEx…
hynxp Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -41,6 +42,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

testImplementation("io.rest-assured:rest-assured:5.5.0")
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import com.hyun.udong.auth.presentation.dto.KakaoTokenResponse;
import com.hyun.udong.auth.presentation.dto.LoginResponse;
import com.hyun.udong.auth.util.JwtTokenProvider;
import com.hyun.udong.common.exception.NotFoundException;
import com.hyun.udong.member.application.service.MemberService;
import com.hyun.udong.member.domain.Member;
import com.hyun.udong.member.domain.SocialType;
import com.hyun.udong.member.exception.MemberNotFoundException;
import com.hyun.udong.member.infrastructure.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -80,8 +80,14 @@ private void validateIsTokenOwner(String refreshToken, Long memberId) {

private Member updateRefreshToken(Long memberId, String refreshToken) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> MemberNotFoundException.EXCEPTION);
.orElseThrow(() -> new NotFoundException("해당하는 회원이 존재하지 않습니다."));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 적용하라는 의미는 아니고 아이디어를 하나 드립니다.
매번 해당하는 ~가 존재하지 않습니다 메시지 적기 귀찮을 수 있으니, NotFoundException 단에서 "해당하는 %이 존재하지 않습니다." 상수를 만들어 놓고 생성자를 통해 문자열만 받거나 클래스 자체를 넘기는 방법도 있어요! 후자를 선택한다면 약간의 리플렉션 작업(클래스 이름 추출)을 하긴 해야하지만요 ㅎㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오오.. 다른 프로젝트에서 본 것 같아요!
에러 핸들링 이슈(#9)에서 적용해보겠습니다~

member.updateRefreshToken(refreshToken);
return member;
}

public Member getMemberFromToken(String token) {
hynxp marked this conversation as resolved.
Show resolved Hide resolved
Long memberId = Long.parseLong(jwtTokenProvider.getSubjectFromToken(token));
return memberRepository.findById(memberId)
.orElseThrow(() -> InvalidTokenException.EXCEPTION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hyun.udong.auth.exception;

import com.hyun.udong.common.exception.ErrorCode;
import com.hyun.udong.common.exception.UdongException;

public class UnAuthenticatedMemberException extends UdongException {
public static final UdongException EXCEPTION = new UnAuthenticatedMemberException();

private UnAuthenticatedMemberException() {
super(ErrorCode.UNAUTHENTICATED_MEMBER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.hyun.udong.auth.presentation.resolver;

import com.hyun.udong.auth.application.service.AuthService;
import com.hyun.udong.auth.exception.UnAuthenticatedMemberException;
import com.hyun.udong.common.annotation.LoginMember;
import com.hyun.udong.member.domain.Member;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

private static final String PREFIX = "Bearer ";

private final AuthService authService;

public AuthenticationArgumentResolver(AuthService authService) {
this.authService = authService;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginMember.class)
&& parameter.getParameterType().equals(Member.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String token = request.getHeader("Authorization");

isValidToken(token);

return authService.getMemberFromToken(token.substring(PREFIX.length()));
}

private void isValidToken(String token) {
if (token == null || !token.startsWith(PREFIX)) {
throw UnAuthenticatedMemberException.EXCEPTION;
}
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/hyun/udong/auth/util/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
@Slf4j
public class JwtTokenProvider {

@Value("${ACCESS_TOKEN_EXPIRE_TIME}")
@Value("${jwt.access-token-expire-time}")
private long accessTokenExpireTime;

@Value("${REFRESH_TOKEN_EXPIRE_TIME}")
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;

@Value("${jwt.secret}")
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/hyun/udong/common/annotation/DateFormat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.hyun.udong.common.annotation;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;

@JacksonAnnotationsInside
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul")
@DateTimeFormat(pattern = "yyyy-MM-dd")
public @interface DateFormat {
}
11 changes: 11 additions & 0 deletions src/main/java/com/hyun/udong/common/annotation/LoginMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.hyun.udong.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginMember {
}
21 changes: 21 additions & 0 deletions src/main/java/com/hyun/udong/common/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.hyun.udong.common.config;

import com.hyun.udong.auth.presentation.resolver.AuthenticationArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final AuthenticationArgumentResolver authenticationArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authenticationArgumentResolver);
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/hyun/udong/common/entity/BaseTimeEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.hyun.udong.common.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

@Column(updatable = false)
@CreatedDate
private LocalDateTime createdAt;

@Column
@LastModifiedDate
private LocalDateTime updatedAt;
}
10 changes: 9 additions & 1 deletion src/main/java/com/hyun/udong/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@
public enum ErrorCode {
MEMBER_NOT_FOUND(BAD_REQUEST, "해당하는 회원이 없습니다."),
INVALID_TOKEN(UNAUTHORIZED, "유효하지 않은 토큰입니다."),
TOKEN_EXPIRED(UNAUTHORIZED, "만료된 토큰입니다.");
TOKEN_EXPIRED(UNAUTHORIZED, "만료된 토큰입니다."),
UNAUTHENTICATED_MEMBER(UNAUTHORIZED, "인증되지 않은 회원입니다."),
CITY_NOT_FOUND(BAD_REQUEST, "해당하는 도시가 없습니다."),
MEMBER_TRAVEL_SCHEDULE_NOT_FOUND(BAD_REQUEST, "해당 회원의 여행 일정이 존재하지 않습니다."),
hynxp marked this conversation as resolved.
Show resolved Hide resolved
NOT_FOUND(BAD_REQUEST);

private final HttpStatus status;
private final String message;

ErrorCode(HttpStatus status) {
this(status, null);
}

ErrorCode(HttpStatus status, String message) {
this.status = status;
this.message = message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.hyun.udong.common.exception;

public class NotFoundException extends UdongException {
public NotFoundException(String message) {
super(ErrorCode.MEMBER_TRAVEL_SCHEDULE_NOT_FOUND, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public UdongException(ErrorCode errorCode) {
this.errorCode = errorCode;
}

public UdongException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}

public ErrorCode getErrorCode() {
return errorCode;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.hyun.udong.member.application.service;

import com.hyun.udong.common.exception.NotFoundException;
import com.hyun.udong.member.domain.Member;
import com.hyun.udong.member.domain.SocialType;
import com.hyun.udong.member.exception.MemberNotFoundException;
import com.hyun.udong.member.infrastructure.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -28,7 +28,7 @@ public Optional<Member> findBySocialIdAndSocialType(Long socialId, SocialType so

public Member findById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> MemberNotFoundException.EXCEPTION);
.orElseThrow(() -> new NotFoundException("해당 회원이 존재하지 않습니다."));
}

}
20 changes: 11 additions & 9 deletions src/main/java/com/hyun/udong/member/domain/Member.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.hyun.udong.member.domain;

import com.hyun.udong.common.entity.BaseTimeEntity;
import com.hyun.udong.travelschedule.domain.TravelSchedule;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -9,10 +11,11 @@
@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Member {
public class Member extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;

private Long socialId;
Expand All @@ -30,15 +33,10 @@ public class Member {

private String refreshToken;

public Member(Long socialId, SocialType socialType, String nickname, String profileImageUrl) {
this.socialId = socialId;
this.socialType = socialType;
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
}
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private TravelSchedule travelSchedule;

public Member(Long id, Long socialId, SocialType socialType, String nickname, String profileImageUrl) {
this.id = id;
public Member(Long socialId, SocialType socialType, String nickname, String profileImageUrl) {
this.socialId = socialId;
this.socialType = socialType;
this.nickname = nickname;
Expand All @@ -48,4 +46,8 @@ public Member(Long id, Long socialId, SocialType socialType, String nickname, St
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public void updateTravelSchedule(TravelSchedule travelSchedule) {
this.travelSchedule = travelSchedule;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.hyun.udong.travelschedule.application.service;

import com.hyun.udong.common.exception.NotFoundException;
import com.hyun.udong.member.domain.Member;
import com.hyun.udong.member.infrastructure.repository.MemberRepository;
import com.hyun.udong.travelschedule.domain.City;
import com.hyun.udong.travelschedule.domain.TravelSchedule;
import com.hyun.udong.travelschedule.domain.TravelScheduleCity;
import com.hyun.udong.travelschedule.infrastructure.repository.CityRepository;
import com.hyun.udong.travelschedule.infrastructure.repository.TravelScheduleRepository;
import com.hyun.udong.travelschedule.presentation.dto.TravelScheduleRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class TravelScheduleService {

private final MemberRepository memberRepository;
private final TravelScheduleRepository travelScheduleRepository;
private final CityRepository cityRepository;

@Transactional
public TravelSchedule updateTravelSchedule(Long memberId, TravelScheduleRequest request) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException("해당 회원이 존재하지 않습니다."));

TravelSchedule travelSchedule = TravelSchedule.builder()
.startDate(request.getStartDate())
.endDate(request.getEndDate())
.build();

List<City> cities = cityRepository.findAllById(request.getCityIds());
if (cities.size() != request.getCityIds().size()) {
throw new NotFoundException("해당 도시가 존재하지 않습니다.");
}
hynxp marked this conversation as resolved.
Show resolved Hide resolved
List<TravelScheduleCity> travelScheduleCities = cities.stream()
.map(city -> new TravelScheduleCity(travelSchedule, city))
.toList();
travelSchedule.addTravelScheduleCities(travelScheduleCities);
travelScheduleRepository.save(travelSchedule);

member.updateTravelSchedule(travelSchedule);

return travelSchedule;
}

public TravelSchedule findTravelSchedule(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException("해당 회원이 존재하지 않습니다."));

TravelSchedule travelSchedule = member.getTravelSchedule();
if (travelSchedule == null) {
throw new NotFoundException("여행 일정이 존재하지 않습니다.");
}
return travelSchedule;
}
}
Loading
Loading