Skip to content

Commit 0a09173

Browse files
authored
Feat : 회원 탈퇴 및 재가입 방지, 검증 (#66)
1 parent b4c446f commit 0a09173

File tree

11 files changed

+312
-14
lines changed

11 files changed

+312
-14
lines changed

src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,28 @@ int countByPartyIdAndStatus(
4444
@Param("status") ParticipantStatus status
4545
);
4646

47+
/**
48+
* 사용자가 참여중인 파티 개수 조회 (호스트 + 참가자)
49+
*/
50+
@Query("SELECT COUNT(DISTINCT pp.party.id) " +
51+
"FROM PartyParticipant pp " +
52+
"WHERE pp.user.userId = :userId " +
53+
"AND pp.party.status IN :activeStatuses " +
54+
"AND pp.status = :participantStatus")
55+
long countActivePartiesByUserId(
56+
@Param("userId") Long userId,
57+
@Param("activeStatuses") List<PartyStatus> activeStatuses,
58+
@Param("participantStatus") ParticipantStatus participantStatus
59+
);
60+
61+
/**
62+
* 사용자가 호스트인 활성 파티 개수
63+
*/
64+
@Query("SELECT COUNT(p) FROM Party p " +
65+
"WHERE p.host.userId = :userId " +
66+
"AND p.status IN :activeStatuses")
67+
long countActivePartiesByHostId(
68+
@Param("userId") Long userId,
69+
@Param("activeStatuses") List<PartyStatus> activeStatuses
70+
);
4771
}

src/main/java/ita/tinybite/domain/user/controller/UserController.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package ita.tinybite.domain.user.controller;
22

33
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
45
import io.swagger.v3.oas.annotations.media.ArraySchema;
56
import io.swagger.v3.oas.annotations.media.Content;
67
import io.swagger.v3.oas.annotations.media.Schema;
78
import io.swagger.v3.oas.annotations.responses.ApiResponse;
89
import io.swagger.v3.oas.annotations.responses.ApiResponses;
910
import ita.tinybite.domain.party.dto.response.PartyCardResponse;
1011
import ita.tinybite.domain.user.dto.req.UpdateUserReqDto;
12+
import ita.tinybite.domain.user.dto.res.RejoinValidationResponse;
1113
import ita.tinybite.domain.user.dto.res.UserResDto;
14+
import ita.tinybite.domain.user.dto.res.WithDrawValidationResponse;
1215
import ita.tinybite.domain.user.service.UserService;
1316
import ita.tinybite.global.response.APIResponse;
1417
import jakarta.validation.Valid;
@@ -67,17 +70,47 @@ public APIResponse<?> updateLocation(@RequestParam(defaultValue = "37.3623504988
6770
return success();
6871
}
6972

73+
@Operation(
74+
summary = "회원 탈퇴 가능 여부 확인",
75+
description = "진행 중인 파티가 있는지 확인하여 탈퇴 가능 여부를 반환합니다."
76+
)
77+
@ApiResponses({
78+
@ApiResponse(responseCode = "200", description = "확인 성공"),
79+
@ApiResponse(responseCode = "401", description = "인증 실패")
80+
})
81+
@GetMapping("/me/withdrawal/validate")
82+
public APIResponse<WithDrawValidationResponse> validateWithdrawal(
83+
@Parameter(hidden = true) @AuthenticationPrincipal Long userId) {
84+
WithDrawValidationResponse response = userService.validateWithdrawal(userId);
85+
return success(response);
86+
}
87+
7088
@Operation(summary = "회원 탈퇴", description = "현재 로그인한 사용자를 삭제합니다.")
7189
@ApiResponses({
7290
@ApiResponse(responseCode = "200", description = "탈퇴 성공"),
7391
@ApiResponse(responseCode = "401", description = "인증 실패")
7492
})
7593
@DeleteMapping("/me")
76-
public APIResponse<?> deleteUser() {
77-
userService.deleteUser();
94+
public APIResponse<?> deleteUser(@AuthenticationPrincipal Long userId) {
95+
userService.deleteUser(userId);
7896
return success();
7997
}
8098

99+
@Operation(
100+
summary = "재가입 가능 여부 확인",
101+
description = "탈퇴 후 30일 이내인지 확인합니다."
102+
)
103+
@ApiResponses({
104+
@ApiResponse(responseCode = "200", description = "확인 성공")
105+
})
106+
@GetMapping("/rejoin/validate")
107+
public APIResponse<RejoinValidationResponse> validateRejoin(
108+
@Parameter(description = "이메일", required = true)
109+
@RequestParam String email) {
110+
RejoinValidationResponse response = userService.validateRejoin(email);
111+
return success(response);
112+
}
113+
81114
@Operation(summary = "활성 파티 목록 조회", description = "사용자가 참여 중인 활성 파티 목록을 조회합니다.")
82115
@ApiResponses({
83116
@ApiResponse(responseCode = "200", description = "조회 성공",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package ita.tinybite.domain.user.dto.res;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.time.LocalDateTime;
8+
9+
@Getter
10+
@AllArgsConstructor
11+
@Builder
12+
public class RejoinValidationResponse {
13+
private boolean canRejoin;
14+
private Long daysRemaining;
15+
private LocalDateTime canRejoinAt;
16+
private String message;
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package ita.tinybite.domain.user.dto.res;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
@Builder
10+
public class WithDrawValidationResponse {
11+
private boolean canWithdraw;
12+
private long activePartyCount;
13+
private long hostPartyCount;
14+
private long participantPartyCount;
15+
private String message;
16+
}

src/main/java/ita/tinybite/domain/user/entity/User.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import lombok.*;
1111
import org.hibernate.annotations.Comment;
1212

13+
import java.time.LocalDateTime;
1314
import java.util.ArrayList;
1415
import java.util.List;
1516

@@ -49,6 +50,8 @@ public class User extends BaseEntity {
4950
@Column(length = 100)
5051
private String location;
5152

53+
private LocalDateTime withdrawAt;
54+
5255
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
5356
private List<UserTermAgreement> agreements = new ArrayList<>();;
5457

@@ -72,4 +75,16 @@ public void updateSignupInfo(GoogleAndAppleSignupRequest req, String email, Logi
7275
public void addTerms(List<UserTermAgreement> agreements) {
7376
this.agreements.addAll(agreements);
7477
}
78+
79+
public void withdraw() {
80+
this.nickname = "탈퇴한 사용자";
81+
this.profileImage = "/images/default-profile.jpg";
82+
this.status = UserStatus.WITHDRAW;
83+
this.withdrawAt = LocalDateTime.now();
84+
}
85+
86+
// 탈퇴 여부 확인
87+
public boolean isWithdrawn() {
88+
return this.status == UserStatus.WITHDRAW;
89+
}
7590
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package ita.tinybite.domain.user.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.LocalDateTime;
10+
import java.time.temporal.ChronoUnit;
11+
12+
@Entity
13+
@Table(name = "withdrawn_users")
14+
@Getter
15+
@NoArgsConstructor
16+
@AllArgsConstructor
17+
@Builder
18+
public class WithDrawUser {
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
private Long id;
23+
24+
@Column(nullable = false, unique = true)
25+
private String email; // 또는 소셜 로그인 ID
26+
27+
@Column(nullable = false)
28+
private LocalDateTime withdrawnAt;
29+
30+
@Column(nullable = false)
31+
private LocalDateTime canRejoinAt; // 재가입 가능 일시 (탈퇴 + 30일)
32+
33+
public static WithDrawUser from(User user) {
34+
LocalDateTime withdrawnAt = LocalDateTime.now();
35+
return WithDrawUser.builder()
36+
.email(user.getEmail())
37+
.withdrawnAt(withdrawnAt)
38+
.canRejoinAt(withdrawnAt.plusDays(30))
39+
.build();
40+
}
41+
42+
public boolean canRejoin() {
43+
return LocalDateTime.now().isAfter(canRejoinAt);
44+
}
45+
46+
public long getDaysUntilRejoin() {
47+
return ChronoUnit.DAYS.between(LocalDateTime.now(), canRejoinAt);
48+
}
49+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ita.tinybite.domain.user.repository;
2+
3+
import io.lettuce.core.dynamic.annotation.Param;
4+
import ita.tinybite.domain.user.entity.WithDrawUser;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
8+
import java.time.LocalDateTime;
9+
import java.util.Optional;
10+
11+
public interface WithDrawUserRepository extends JpaRepository<WithDrawUser, Long> {
12+
Optional<WithDrawUser> findByEmail(String email);
13+
14+
boolean existsByEmail(String email);
15+
16+
@Query("SELECT w FROM WithDrawUser w " +
17+
"WHERE w.email = :email " +
18+
"AND w.canRejoinAt > :now")
19+
Optional<WithDrawUser> findActiveWithdrawUser(
20+
@Param("email") String email,
21+
@Param("now") LocalDateTime now
22+
);
23+
}

0 commit comments

Comments
 (0)