Skip to content

Commit 22522b7

Browse files
authored
Merge pull request #105 from Modelly-official/feat/jwt
[feat] 탈퇴하기 API
2 parents 0a89593 + f8c8647 commit 22522b7

32 files changed

+569
-40
lines changed

src/main/java/modelly/modelly_be/domain/auth/controller/AuthController.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import modelly.modelly_be.domain.auth.service.SmsAuthService;
1717
import modelly.modelly_be.global.apiPayload.ApiResponse;
1818
import modelly.modelly_be.global.apiPayload.code.SimpleMessageDTO;
19+
import modelly.modelly_be.global.apiPayload.code.status.ErrorStatus;
1920
import modelly.modelly_be.global.apiPayload.code.status.SuccessStatus;
21+
import modelly.modelly_be.global.apiPayload.exception.GeneralException;
2022
import modelly.modelly_be.global.security.jwt.CookieUtil;
2123
import org.springframework.beans.factory.annotation.Value;
2224
import org.springframework.web.bind.annotation.*;
@@ -287,4 +289,24 @@ public ApiResponse<SimpleMessageDTO> resetPassword(@Valid @RequestBody ResetPass
287289
accountRecoveryService.resetPassword(request);
288290
return ApiResponse.onSuccess(new SimpleMessageDTO("비밀번호 재설정 완료"));
289291
}
292+
293+
/* ---------- 탈퇴하기 ---------- */
294+
@Operation(summary = "탈퇴하기", description = "로그인된 사용자의 계정을 탈퇴(Soft Delete) 처리합니다.")
295+
@DeleteMapping("/auth/withdraw")
296+
public ApiResponse<SimpleMessageDTO> withdraw(HttpServletRequest request, HttpServletResponse response) {
297+
298+
// 토큰 검증 및 Soft Delete 수행
299+
authService.withdraw(request);
300+
301+
// 쿠키(Refresh Token) 삭제 (로그아웃 처리)
302+
var del = CookieUtil.deleteRefreshCookie(
303+
cookieDomain,
304+
refreshCookieSecure,
305+
""
306+
);
307+
response.addHeader("Set-Cookie", del.toString());
308+
309+
return ApiResponse.onSuccess(new SimpleMessageDTO("회원 탈퇴가 완료되었습니다."));
310+
}
311+
290312
}

src/main/java/modelly/modelly_be/domain/auth/service/AuthService.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package modelly.modelly_be.domain.auth.service;
22

33
import com.google.maps.model.LatLng;
4+
import jakarta.persistence.EntityNotFoundException;
45
import jakarta.servlet.http.HttpServletRequest;
56
import lombok.RequiredArgsConstructor;
67

@@ -139,11 +140,12 @@ public SignupResponse signup(SignupRequest req) {
139140
}
140141

141142
/* 로그인 */
142-
@Transactional(readOnly = true)
143+
@Transactional
143144
public LoginResult login(LoginRequest req) {
144145
User user = userRepository.findByLoginId(req.getLoginId())
145146
.orElseThrow(() -> new GeneralException(ErrorStatus.LOGIN_FAIL));
146147

148+
147149
Designer designer = null;
148150
if (user.getUserRole()==UserRole.DESIGNER) {
149151
designer = designerService.getByUser(user);
@@ -152,6 +154,11 @@ public LoginResult login(LoginRequest req) {
152154
if (!passwordEncoder.matches(req.getPassword(), user.getPassword()))
153155
throw new GeneralException(ErrorStatus.LOGIN_FAIL);
154156

157+
// 탈퇴한 회원인지 확인
158+
if (user.getDeletedAt() != null) {
159+
user.recoverAccount();
160+
}
161+
155162
TokenResponse tokens = tokenProvider.createToken(user);
156163

157164
// refresh token
@@ -233,6 +240,10 @@ else if(tokenStatus == TokenStatus.INVALID || tokenStatus == TokenStatus.MISSING
233240
User user = userRepository.findById(Long.valueOf(userId))
234241
.orElseThrow(() -> new GeneralException(ErrorStatus.NOT_FOUND_USER));
235242

243+
if (user.getDeletedAt() != null) {
244+
throw new GeneralException(ErrorStatus.WITHDRAWN_USER);
245+
}
246+
236247
// Access, Refresh 발급
237248
String newAccess = tokenProvider.createAccessToken(user);
238249
String newRefresh = tokenProvider.createRefreshToken(user);
@@ -442,11 +453,17 @@ private SocialLoginResult processSocialLogin(String email, String name, LoginTyp
442453
if (optionalUser.isPresent()) {
443454
user = optionalUser.get();
444455

456+
445457
// 로그인 타입 검증
446458
if (user.getLoginType() != loginType) {
447459
throw new GeneralException(ErrorStatus.DUPLICATE_USER_REGISTERED);
448460
}
449461

462+
// 이미 가입된 유저지만 탈퇴 상태인 경우
463+
if (user.getDeletedAt() != null) {
464+
user.recoverAccount();
465+
}
466+
450467
// 회원가입 완료 여부 확인
451468
boolean alreadyCompleted =
452469
designerRepository.existsByUser_Id(user.getId()) ||
@@ -501,5 +518,33 @@ private SocialLoginResult processSocialLogin(String email, String name, LoginTyp
501518

502519
return SocialLoginResult.of(loginResponse, refreshToken, ttlSec);
503520
}
521+
522+
/* ---------- 탈퇴하기 ---------- */
523+
@Transactional
524+
public void withdraw(HttpServletRequest request) {
525+
// 1. 토큰 추출 및 검증
526+
String accessToken = tokenProvider.resolveToken(request);
527+
if (accessToken == null || accessToken.isBlank()) {
528+
throw new GeneralException(ErrorStatus.TOKEN_INVALID);
529+
}
530+
531+
// 2. 유저 ID 추출
532+
String userIdStr = tokenProvider.getUserIdFromToken(accessToken);
533+
Long userId = Long.valueOf(userIdStr);
534+
535+
// 3. 유저 조회
536+
User user = userRepository.findById(userId)
537+
.orElseThrow(() -> new GeneralException(ErrorStatus.NOT_FOUND_USER));
538+
539+
// 4. Soft Delete 처리
540+
if (user.getDeletedAt() != null) {
541+
throw new GeneralException(ErrorStatus.WITHDRAWN_USER);
542+
}
543+
544+
redisService.deleteValue(RT_KEY_PREFIX + userId + ":current");
545+
redisService.deleteValue(RT_KEY_PREFIX + userId + ":previous");
546+
547+
user.markAsDeleted();
548+
}
504549
}
505550

src/main/java/modelly/modelly_be/domain/chat/entity/ChatRoom.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ public class ChatRoom extends BaseEntity {
2424
private Long id;
2525

2626
@ManyToOne(fetch = FetchType.LAZY)
27-
@JoinColumn(name = "designer_id", nullable = false)
27+
@JoinColumn(name = "designer_id")
2828
private Designer designer;
2929

3030
@ManyToOne(fetch = FetchType.LAZY)
31-
@JoinColumn(name = "model_id", nullable = false)
31+
@JoinColumn(name = "model_id")
3232
private Model model;
3333

3434
/* constructor */

src/main/java/modelly/modelly_be/domain/chat/repository/ChatRoomRepository.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.data.domain.Page;
88
import org.springframework.data.domain.Pageable;
99
import org.springframework.data.jpa.repository.JpaRepository;
10+
import org.springframework.data.jpa.repository.Modifying;
1011
import org.springframework.data.jpa.repository.Query;
1112
import org.springframework.data.repository.query.Param;
1213

@@ -19,14 +20,18 @@ public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {
1920

2021
// 모델 ID로 조회 (카테고리 필터링)
2122
@Query("""
22-
SELECT r FROM ChatRoom r
23-
JOIN r.designer d
24-
LEFT JOIN Chatting c ON c.chatRoom = r
25-
WHERE r.model.id = :modelId
26-
AND (:category IS NULL OR d.category = :category)
27-
GROUP BY r
28-
ORDER BY COALESCE(MAX(c.createdAt), r.createdAt) DESC
29-
""")
23+
SELECT r FROM ChatRoom r
24+
LEFT JOIN r.designer d
25+
LEFT JOIN Chatting c ON c.chatRoom = r
26+
WHERE r.model.id = :modelId
27+
AND (
28+
:category IS NULL
29+
OR d IS NULL
30+
OR d.category = :category
31+
)
32+
GROUP BY r
33+
ORDER BY COALESCE(MAX(c.createdAt), r.createdAt) DESC
34+
""")
3035
Page<ChatRoom> findAllByModelIdOrderByLastMessageTimeDesc(
3136
@Param("modelId") Long modelId,
3237
@Param("category") Category category,
@@ -50,5 +55,13 @@ Page<ChatRoom> findAllByDesignerIdOrderByLastMessageTimeDesc(
5055
Pageable pageable
5156
);
5257

58+
// 해당 디자이너가 포함된 채팅방의 designer 필드를 null로 설정 (디자이너 탈퇴 시 이용)
59+
@Modifying(clearAutomatically = true)
60+
@Query("UPDATE ChatRoom c SET c.designer = NULL WHERE c.designer.id = :designerId")
61+
void setDesignerNull(@Param("designerId") Long designerId);
5362

63+
// 해당 모델이 포함된 채팅방의 model 필드를 null로 설정 (모델 탈퇴 시 이용)
64+
@Modifying(clearAutomatically = true)
65+
@Query("UPDATE ChatRoom c SET c.model = NULL WHERE c.model.id = :modelId")
66+
void setModelNull(@Param("modelId") Long modelId);
5467
}

src/main/java/modelly/modelly_be/domain/chat/service/ChatRoomService.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ else if (isDesigner) {
180180
}
181181

182182
public OpponentInfoResponse getOpponentInfo(ChatRoom room, Long currentUserId) {
183-
Model model = room.getModel();
184-
Designer designer = room.getDesigner();
183+
Model model = room.getModel() == null ? null : room.getModel();
184+
Designer designer = room.getDesigner() == null ? null : room.getDesigner();
185185

186186
boolean iAmModel = model != null
187187
&& model.getUser().getId().equals(currentUserId);
@@ -193,18 +193,18 @@ public OpponentInfoResponse getOpponentInfo(ChatRoom room, Long currentUserId) {
193193

194194
// 현재 유저가 모델인 경우 → 상대는 디자이너
195195
if (iAmModel) {
196-
User designerUser = designer.getUser();
197-
opponentUserId = designerUser.getId();
198-
opponentName = designer.getNickname(); // 활동명
199-
opponentProfileImageUrl = designerUser.getImageUrl();
196+
User designerUser = designer == null ? null : designer.getUser();
197+
opponentUserId = designerUser == null ? null : designerUser.getId();
198+
opponentName = designer == null ? null : designer.getNickname(); // 활동명
199+
opponentProfileImageUrl = designerUser == null ? null : designerUser.getImageUrl();
200200
opponentRole = UserRole.DESIGNER;
201201
}
202202
// 현재 유저가 디자이너인 경우 → 상대는 모델
203203
else {
204-
User modelUser = model.getUser();
205-
opponentUserId = modelUser.getId();
206-
opponentName = modelUser.getName(); // 이름
207-
opponentProfileImageUrl = modelUser.getImageUrl();
204+
User modelUser = model == null ? null : model.getUser();
205+
opponentUserId = modelUser == null ? null : modelUser.getId();
206+
opponentName = modelUser == null ? null : modelUser.getName(); // 이름
207+
opponentProfileImageUrl = modelUser == null ? null : modelUser.getImageUrl();
208208
opponentRole = UserRole.MODEL;
209209
}
210210

src/main/java/modelly/modelly_be/domain/like/repository/designerLikeRepository/DesignerLikeRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import modelly.modelly_be.domain.user.entity.Model;
66
import modelly.modelly_be.global.entity.Category;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
89
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
911

1012
public interface DesignerLikeRepository extends JpaRepository<DesignerLike, Long>, DesignerLikeRepositoryCustom {
1113

@@ -16,4 +18,14 @@ public interface DesignerLikeRepository extends JpaRepository<DesignerLike, Long
1618
@Query("SELECT COUNT(dl.id) FROM DesignerLike dl " +
1719
"WHERE dl.model = :model AND (:category IS NULL OR dl.designer.category = :category)")
1820
Long countByModelAndCategory(Model model, Category category);
21+
22+
// 해당 디자이너가 포함된 찜 삭제
23+
@Modifying(clearAutomatically = true)
24+
@Query("DELETE FROM DesignerLike d WHERE d.designer.id = :designerId")
25+
void deleteByDesignerId(@Param("designerId") Long designerId);
26+
27+
// 해당 모델이 포함된 찜 삭제
28+
@Modifying(clearAutomatically = true)
29+
@Query("DELETE FROM DesignerLike d WHERE d.model.id = :modelId")
30+
void deleteByModelId(@Param("modelId") Long modelId);
1931
}

src/main/java/modelly/modelly_be/domain/like/repository/recruitmentLikeRepository/RecruitmentLikeRepository.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import modelly.modelly_be.domain.user.entity.Model;
66
import modelly.modelly_be.global.entity.Category;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
89
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
911

1012
public interface RecruitmentLikeRepository extends JpaRepository<RecruitmentLike, Long>, RecruitmentLikeRepositoryCustom {
1113
void deleteAllByRecruitment(Recruitment recruitment);
@@ -17,4 +19,17 @@ public interface RecruitmentLikeRepository extends JpaRepository<RecruitmentLike
1719
@Query("SELECT COUNT(rl.id) FROM RecruitmentLike rl " +
1820
"WHERE rl.model = :model AND (:category IS NULL OR rl.recruitment.category = :category) ")
1921
Long countByModelAndCategory(Model model, Category category);
22+
23+
// 해당 모델의 공고 찜 삭제
24+
@Modifying(clearAutomatically = true)
25+
@Query("DELETE FROM RecruitmentLike r WHERE r.model.id = :modelId")
26+
void deleteByModelId(@Param("modelId") Long modelId);
27+
28+
// 해당 디자이너가 포함된 공고찜 삭제
29+
@Modifying
30+
@Query("""
31+
delete from RecruitmentLike rl
32+
where rl.recruitment.designer.id = :designerId
33+
""")
34+
void deleteByDesignerId(Long designerId);
2035
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package modelly.modelly_be.domain.portfolio.repository;
2+
3+
import modelly.modelly_be.domain.portfolio.entity.PortfolioImage;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
8+
public interface PortfolioImageRepository extends JpaRepository<PortfolioImage, Long> {
9+
// 해당 디자이너의 포트폴리오 이미지 삭제
10+
@Modifying
11+
@Query("""
12+
delete from PortfolioImage pi
13+
where pi.portfolio.designer.id = :designerId
14+
""")
15+
void deleteByDesignerId(Long designerId);
16+
}

src/main/java/modelly/modelly_be/domain/portfolio/repository/PortfolioRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import modelly.modelly_be.domain.user.entity.Designer;
66
import org.springframework.data.domain.Pageable;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
89
import org.springframework.data.jpa.repository.Query;
910

1011
import java.util.List;
@@ -26,4 +27,13 @@ public interface PortfolioRepository extends JpaRepository<Portfolio, Long> {
2627
Optional<Portfolio> findByPortfolioId(Long portfolioId);
2728

2829
Long countByDesigner(Designer designer);
30+
31+
32+
@Modifying
33+
@Query("delete from Portfolio p where p.designer.id = :designerId")
34+
void deleteByDesignerId(Long designerId);
35+
36+
// 해당 디자이너의 포트폴리오 조회
37+
List<Portfolio> findAllByDesignerId(Long designerId);
38+
2939
}

src/main/java/modelly/modelly_be/domain/recruitment/repository/RecruitmentDateRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
import modelly.modelly_be.domain.recruitment.entity.RecruitmentDate;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
57

68
public interface RecruitmentDateRepository extends JpaRepository<RecruitmentDate, Long> {
9+
// 해당 디자이너의 RecruitmentDate 삭제
10+
@Modifying
11+
@Query("""
12+
delete from RecruitmentDate rd
13+
where rd.recruitment.designer.id = :designerId
14+
""")
15+
void deleteByDesignerId(Long designerId);
716
}

0 commit comments

Comments
 (0)