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

feat: 회원 탈퇴 사유 저장 로직 추가 및 cors 관련 재설정 #123

Merged
merged 11 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public WebSecurityCustomizer webSecurityCustomizer() {
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable).cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(request -> request.requestMatchers(PATTERNS).permitAll()
.requestMatchers(HttpMethod.POST, "/users/registers").permitAll()
.requestMatchers(HttpMethod.POST, "/users").permitAll()
.anyRequest().authenticated()).httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS))
Expand All @@ -64,7 +64,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.addExposedHeader("Authorization");
Expand Down
26 changes: 18 additions & 8 deletions src/main/java/net/teumteum/meeting/domain/Meeting.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
package net.teumteum.meeting.domain;

import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.teumteum.core.entity.TimeBaseEntity;
import org.springframework.util.Assert;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "meeting")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Meeting extends TimeBaseEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ private UserAroundLocationsResponse getUserAroundLocationsResponse(GeoResults<Ge
int count = 0;

for (GeoResult<GeoLocation<Object>> geoResult : Objects.requireNonNull(geoResults)) {
String userSavedTime = String.valueOf(geoResult.getContent().getName()).split(":")[1];
String userSavedTime = String.valueOf(geoResult.getContent().getName()).split(":")[5];
long timestamp = Long.parseLong(userSavedTime);

if (currentTime - timestamp < LOCATION_EXPIRATION.toMillis()) {
String userDataJson = String.valueOf(geoResult.getContent().getName()).split(":")[0];
String savedUserLocation = String.valueOf(geoResult.getContent().getName());
String userDataJson = savedUserLocation.substring(savedUserLocation.lastIndexOf(":") + 1);

UserData userData = null;
try {
userData = objectMapper.readValue(userDataJson, UserData.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import net.teumteum.core.security.service.SecurityService;
import net.teumteum.user.domain.request.UserRegisterRequest;
import net.teumteum.user.domain.request.UserUpdateRequest;
import net.teumteum.user.domain.request.UserWithdrawRequest;
import net.teumteum.user.domain.response.FriendsResponse;
import net.teumteum.user.domain.response.InterestQuestionResponse;
import net.teumteum.user.domain.response.UserGetResponse;
Expand All @@ -20,7 +21,6 @@
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -87,10 +87,10 @@ public InterestQuestionResponse getInterestQuestion(@RequestParam("user-id") Lis
return userService.getInterestQuestionByUserIds(userIds, balance);
}

@DeleteMapping
@PostMapping("/withdraw")
@ResponseStatus(HttpStatus.OK)
public void withdraw() {
userService.withdraw(getCurrentUserId());
public void withdraw(@Valid @RequestBody UserWithdrawRequest request) {
userService.withdraw(request, getCurrentUserId());
}

@PostMapping
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/net/teumteum/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -24,9 +25,9 @@
import org.springframework.util.Assert;

@Getter
@Entity(name = "users")
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends TimeBaseEntity {

@Id
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/net/teumteum/user/domain/WithdrawReason.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.teumteum.user.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.teumteum.core.entity.TimeBaseEntity;

@Getter
@AllArgsConstructor
@Entity(name = "withdraw_reasons")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class WithdrawReason extends TimeBaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "withdraw_reason", nullable = false)
private String reason;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.teumteum.user.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface WithdrawReasonRepository extends JpaRepository<WithdrawReason, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.teumteum.user.domain.request;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import jakarta.validation.constraints.Size;
import java.util.List;
import net.teumteum.user.domain.WithdrawReason;

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public record UserWithdrawRequest(
@Size(min = 1, max = 3, message = "탈퇴 사유는 최소 1개, 최대 3개의 입력값입니다.")
List<String> withdrawReasons
) {

private static final Long IGNORE_ID = null;

public List<WithdrawReason> toEntity() {
return withdrawReasons.stream()
.map(withdrawReason -> new WithdrawReason(IGNORE_ID, withdrawReason))
.toList();
}
}
7 changes: 6 additions & 1 deletion src/main/java/net/teumteum/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import net.teumteum.user.domain.InterestQuestion;
import net.teumteum.user.domain.User;
import net.teumteum.user.domain.UserRepository;
import net.teumteum.user.domain.WithdrawReasonRepository;
import net.teumteum.user.domain.request.UserRegisterRequest;
import net.teumteum.user.domain.request.UserUpdateRequest;
import net.teumteum.user.domain.request.UserWithdrawRequest;
import net.teumteum.user.domain.response.FriendsResponse;
import net.teumteum.user.domain.response.InterestQuestionResponse;
import net.teumteum.user.domain.response.UserGetResponse;
Expand All @@ -28,6 +30,7 @@
public class UserService {

private final UserRepository userRepository;
private final WithdrawReasonRepository withdrawReasonRepository;
private final InterestQuestion interestQuestion;
private final RedisService redisService;
private final JwtService jwtService;
Expand Down Expand Up @@ -71,11 +74,13 @@ public void addFriends(Long myId, Long friendId) {
}

@Transactional
public void withdraw(Long userId) {
public void withdraw(UserWithdrawRequest request, Long userId) {
var existUser = getUser(userId);

userRepository.delete(existUser);
Copy link
Member

Choose a reason for hiding this comment

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

withdraw에서 id참조로 유저를 바라보게 되면, 소프트 삭제를 진행해야 할거 같아요.
다만, 이렇게하면 모든 조회에 소프트 삭제된 유저는 조회하지 않는 조건이 들어가게됩니다. 이게 번거롭다면, withdraw필드에 유저 정보를 반정규화 해서 저장해도 됩니다. (id 참조가 아닌 유저 정보를 모두 풀어서 withdraw 테이블에 카피해서 저장)

redisService.deleteData(String.valueOf(userId));
Copy link
Member

Choose a reason for hiding this comment

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

트랜잭션안에서 I/O작업과 같이 스레드가 노는 작업 하는건 피해야 합니다.
다른 스레드에서 트랜잭션을 얻지 못해 대기하게 되어요
(후처리) 비동기 이벤트(전처리) 레이어를 하나 더 둬서 해결하는데 수정하려면 많은 부분을 바꿔야해서 냅두고 나중에 고도화 하게 되면 바꿔봐요

Copy link
Member Author

Choose a reason for hiding this comment

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

오호 그런 부분까지 생각이 부족했네요
리뷰 감사합니다!!


withdrawReasonRepository.saveAll(request.toEntity());
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
create table if not exists withdraw_reason
(
id bigint not null auto_increment,
withdraw_reason varchar(30) not null,
created_at timestamp(6) not null,
updated_at timestamp(6) not null,
primary key (id)
Comment on lines +3 to +7
Copy link
Member

Choose a reason for hiding this comment

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

UserEntity 참조는 어디서 진행되고 있나요??
withdraw가 어떤 user의 탈퇴 사유 로 보이는데, 지금은 withdraw가 어떤 유저와 매핑되어있는지 알 수 없는거 같아요.

제가 잘못이해한거면 그냥 넘겨주세요

Copy link
Member Author

Choose a reason for hiding this comment

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

이거 탈퇴사유가 서비스의 탈퇴 사유 데이터 수집을 순수하게 수집하는 목적으로 이해해서 유저의 id 을 포하시키지 않았습니다
굳이 user의 id 을 가지고 있을 필요가 있나 해서요

);
10 changes: 10 additions & 0 deletions src/main/resources/db/migration/V8__rename_withdraw_reason.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
drop table withdraw_reason;

create table if not exists withdraw_reasons
(
id bigint not null auto_increment,
withdraw_reason varchar(30) not null,
created_at timestamp(6) not null,
updated_at timestamp(6) not null,
primary key (id)
);
9 changes: 6 additions & 3 deletions src/test/java/net/teumteum/integration/Api.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.teumteum.teum_teum.domain.request.UserLocationRequest;
import net.teumteum.user.domain.request.UserRegisterRequest;
import net.teumteum.user.domain.request.UserUpdateRequest;
import net.teumteum.user.domain.request.UserWithdrawRequest;
import org.springframework.boot.test.context.TestComponent;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -129,10 +130,12 @@ ResponseSpec reissueJwt(String accessToken, String refreshToken) {
.exchange();
}

ResponseSpec withdrawUser(String accessToken) {
return webTestClient.delete()
.uri("/users")
ResponseSpec withdrawUser(String accessToken, UserWithdrawRequest request) {
return webTestClient
.post()
.uri("/users/withdraw")
.header(HttpHeaders.AUTHORIZATION, accessToken)
.bodyValue(request)
.exchange();
}

Expand Down
6 changes: 6 additions & 0 deletions src/test/java/net/teumteum/integration/RequestFixture.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.teumteum.integration;

import java.util.List;
import java.util.UUID;
import net.teumteum.core.security.Authenticated;
import net.teumteum.user.domain.User;
Expand All @@ -8,9 +9,14 @@
import net.teumteum.user.domain.request.UserRegisterRequest.Terms;
import net.teumteum.user.domain.request.UserUpdateRequest;
import net.teumteum.user.domain.request.UserUpdateRequest.NewJob;
import net.teumteum.user.domain.request.UserWithdrawRequest;

public class RequestFixture {

public static UserWithdrawRequest userWithdrawRequest(List<String> withdrawReasons) {
return new UserWithdrawRequest(withdrawReasons);
}

public static UserUpdateRequest userUpdateRequest(User user) {
return new UserUpdateRequest(user.getId(), "new_name", user.getBirth(), user.getCharacterId(),
user.getActivityArea(), user.getMbti(), user.getStatus().name(), user.getGoal(), newJob(user),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,10 @@ void Withdraw_user_info_api() {

loginContext.setUserId(me.getId());

// when & then
var request = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요"));

assertThatCode(() -> api.withdrawUser(VALID_TOKEN))
// when & then
assertThatCode(() -> api.withdrawUser(VALID_TOKEN, request))
.doesNotThrowAnyException();
}

Expand All @@ -252,8 +253,10 @@ void Return_500_error_if_user_not_exist() {
// given
repository.clearUserRepository();

var request = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요"));

// when
var result = api.withdrawUser(VALID_TOKEN);
var result = api.withdrawUser(VALID_TOKEN, request);

// then
Assertions.assertThat(result.expectStatus().is5xxServerError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import net.teumteum.core.security.SecurityConfig;
import net.teumteum.core.security.filter.JwtAuthenticationFilter;
import net.teumteum.core.security.service.JwtService;
Expand All @@ -22,6 +24,7 @@
import net.teumteum.user.domain.User;
import net.teumteum.user.domain.UserFixture;
import net.teumteum.user.domain.request.UserRegisterRequest;
import net.teumteum.user.domain.request.UserWithdrawRequest;
import net.teumteum.user.domain.response.UserRegisterResponse;
import net.teumteum.user.service.UserService;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -35,7 +38,6 @@
import org.springframework.context.annotation.FilterType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;

@WebMvcTest(value = UserController.class,
excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class),
Expand All @@ -48,8 +50,10 @@
public class UserControllerTest {

@Autowired
private MockMvc mockMvc;
ObjectMapper objectMapper;

@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;

Expand Down Expand Up @@ -79,7 +83,7 @@ void Register_user_card_with_201_created() throws Exception {

// when & then
mockMvc.perform(post("/users")
.content(new ObjectMapper().writeValueAsString(request))
.content(objectMapper.writeValueAsString(request))
.contentType(APPLICATION_JSON)
.with(csrf())
.header(AUTHORIZATION, VALID_ACCESS_TOKEN))
Expand All @@ -98,7 +102,7 @@ void Register_user_card_with_400_bad_request() throws Exception {
// when
// then
mockMvc.perform(post("/users")
.content(new ObjectMapper().writeValueAsString(request))
.content(objectMapper.writeValueAsString(request))
.contentType(APPLICATION_JSON)
.with(csrf())
.header(AUTHORIZATION, VALID_ACCESS_TOKEN))
Expand All @@ -107,4 +111,26 @@ void Register_user_card_with_400_bad_request() throws Exception {
.andExpect(jsonPath("$.message").isNotEmpty());
}
}

@Nested
@DisplayName("회원 탈퇴 API는")
class Withdraw_user_api_unit {

@Test
@DisplayName("회원 탈퇴 사유와 회원 탈퇴 요청이 들어오면, 탈퇴를 진행하고 200 OK을 반환한다.")
void Withdraw_user_with_200_ok() throws Exception {
// given
UserWithdrawRequest request
= RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요"));

// when & then
mockMvc.perform(post("/users/withdraw")
.content(new ObjectMapper().writeValueAsString(request))
.contentType(APPLICATION_JSON)
.with(csrf())
.header(AUTHORIZATION, VALID_ACCESS_TOKEN))
.andDo(print())
.andExpect(status().isOk());
}
}
}
Loading
Loading