Skip to content

Commit 75e2a24

Browse files
authored
feat: 회원 탈퇴 기능 및 포트폴리오 데모 로그인 추가 (#60)
- 회원 탈퇴: WithdrawalService로 계정·연관 데이터 일괄 삭제 (스케줄, 수강 등록, 피드백, 세션 등) - admin/study/teach 설정에 회원 탈퇴 페이지 및 WithdrawForm 컴포넌트 추가 - 역할별(관리자/강사/학생) 원클릭 로그인 버튼(RoleLoginButtons) - seed-demo-accounts 스크립트로 데모 계정 시드 데이터 추가
1 parent 7bd8c35 commit 75e2a24

16 files changed

Lines changed: 334 additions & 2 deletions

File tree

api/src/main/java/org/junotb/api/auth/SessionRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ public interface SessionRepository extends JpaRepository<Session, String> {
2020
@Modifying
2121
@Query("DELETE FROM Session s WHERE s.expiresAt < :now")
2222
long deleteByExpiresAtBefore(@Param("now") OffsetDateTime now);
23+
24+
/** 회원 탈퇴: 해당 사용자의 모든 세션 삭제 */
25+
@Modifying
26+
@Query("DELETE FROM Session s WHERE s.userId = :userId")
27+
int deleteByUserId(@Param("userId") String userId);
2328
}

api/src/main/java/org/junotb/api/registration/RegistrationRepository.java

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

33
import jakarta.persistence.LockModeType;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
56
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
67
import org.springframework.data.jpa.repository.Lock;
78
import org.springframework.data.jpa.repository.Query;
@@ -110,4 +111,14 @@ List<Registration> findRecentCompletedRegistrationsByStudentId(
110111
@Param("studentId") String studentId,
111112
Pageable pageable
112113
);
114+
115+
/** 학생 탈퇴: 해당 학생의 모든 수강 등록 삭제 */
116+
@Modifying
117+
@Query("DELETE FROM Registration r WHERE r.student.id = :userId")
118+
int deleteByStudentId(@Param("userId") String userId);
119+
120+
/** 강사 탈퇴: 해당 강사 스케줄의 모든 수강 등록 삭제 */
121+
@Modifying
122+
@Query("DELETE FROM Registration r WHERE r.schedule.user.id = :userId")
123+
int deleteBySchedule_UserId(@Param("userId") String userId);
113124
}

api/src/main/java/org/junotb/api/schedule/ScheduleRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.data.domain.Pageable;
55
import org.springframework.data.jpa.repository.JpaRepository;
66
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
7+
import org.springframework.data.jpa.repository.Modifying;
78
import org.springframework.data.jpa.repository.Lock;
89
import org.springframework.data.jpa.repository.Query;
910
import org.springframework.data.repository.query.Param;
@@ -133,4 +134,9 @@ List<Schedule> findRecentCompletedSchedulesForTeacher(
133134
@Param("teacherId") String teacherId,
134135
Pageable pageable
135136
);
137+
138+
/** 강사 탈퇴: 해당 강사의 모든 스케줄 삭제 */
139+
@Modifying
140+
@Query("DELETE FROM Schedule s WHERE s.user.id = :userId")
141+
int deleteByUserId(@Param("userId") String userId);
136142
}

api/src/main/java/org/junotb/api/schedulefeedback/ScheduleFeedbackRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.junotb.api.schedulefeedback;
22

33
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Modifying;
45
import org.springframework.data.jpa.repository.Query;
56
import org.springframework.data.repository.query.Param;
67

@@ -9,4 +10,8 @@
910
public interface ScheduleFeedbackRepository extends JpaRepository<ScheduleFeedback, Long> {
1011
@Query("SELECT sf FROM ScheduleFeedback sf JOIN FETCH sf.schedule s WHERE s.id = :scheduleId")
1112
Optional<ScheduleFeedback> findByScheduleId(@Param("scheduleId") Long scheduleId);
13+
14+
@Modifying
15+
@Query("DELETE FROM ScheduleFeedback sf WHERE sf.schedule.user.id = :userId")
16+
int deleteBySchedule_UserId(@Param("userId") String userId);
1217
}

api/src/main/java/org/junotb/api/user/TeacherTimeOffRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.junotb.api.user;
22

33
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Modifying;
45
import org.springframework.data.jpa.repository.Query;
56
import org.springframework.data.repository.query.Param;
67

@@ -30,4 +31,13 @@ List<TeacherTimeOff> findOverlappingByTeacherAndRange(
3031
@Param("rangeStart") OffsetDateTime rangeStart,
3132
@Param("rangeEnd") OffsetDateTime rangeEnd
3233
);
34+
35+
/**
36+
* 강사 탈퇴: 해당 강사의 모든 휴무 삭제
37+
* @param teacherId 강사 ID
38+
* @return 삭제된 휴무 개수
39+
*/
40+
@Modifying
41+
@Query("DELETE FROM TeacherTimeOff t WHERE t.teacher.id = :teacherId")
42+
int deleteByTeacherId(@Param("teacherId") String teacherId);
3343
}

api/src/main/java/org/junotb/api/user/UserController.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.junotb.api.user.web.UserUpdateRequest;
1212
import org.springframework.data.domain.Pageable;
1313
import org.springframework.http.ResponseEntity;
14+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1415
import org.springframework.web.bind.annotation.*;
1516

1617
import java.util.Map;
@@ -25,6 +26,20 @@
2526
@RequiredArgsConstructor
2627
public class UserController {
2728
private final UserService userService;
29+
private final WithdrawalService withdrawalService;
30+
31+
/**
32+
* 회원 탈퇴. 본인 인증 후 회원·스케줄·수강 등록 등 모든 연관 데이터를 삭제합니다.
33+
*
34+
* @param userId 인증된 사용자 ID
35+
* @return 204 No Content
36+
*/
37+
@Operation(summary = "회원 탈퇴", description = "본인 계정 및 연관 데이터를 모두 삭제합니다.")
38+
@PostMapping("/me/withdraw")
39+
public ResponseEntity<Void> withdraw(@AuthenticationPrincipal String userId) {
40+
withdrawalService.withdraw(userId);
41+
return ResponseEntity.noContent().build();
42+
}
2843

2944
/**
3045
* 사용자 목록을 페이징하여 조회. 이름·역할·상태로 필터링 가능.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package org.junotb.api.user;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.junotb.api.common.exception.ResourceNotFoundException;
6+
import org.junotb.api.schedule.ScheduleRepository;
7+
import org.junotb.api.schedulefeedback.ScheduleFeedbackRepository;
8+
import org.junotb.api.auth.SessionRepository;
9+
import org.junotb.api.registration.RegistrationRepository;
10+
import org.springframework.jdbc.core.JdbcTemplate;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
/**
15+
* 회원 탈퇴 서비스.
16+
* 사용자와 연관된 모든 데이터(스케줄, 수강 등록, 피드백, 가용 시간, 휴무, 세션, 계정)를 삭제합니다.
17+
*/
18+
@Service
19+
@Transactional
20+
@RequiredArgsConstructor
21+
@Slf4j
22+
public class WithdrawalService {
23+
24+
private final UserRepository userRepository;
25+
private final ScheduleFeedbackRepository scheduleFeedbackRepository;
26+
private final RegistrationRepository registrationRepository;
27+
private final ScheduleRepository scheduleRepository;
28+
private final TeacherTimeOffRepository teacherTimeOffRepository;
29+
private final TeacherAvailabilityRepository teacherAvailabilityRepository;
30+
private final SessionRepository sessionRepository;
31+
private final JdbcTemplate jdbcTemplate;
32+
33+
/**
34+
* 회원 탈퇴 처리. FK 제약 순서에 맞춰 연관 데이터를 모두 삭제합니다.
35+
*
36+
* @param userId 탈퇴할 사용자 ID (인증된 본인)
37+
* @throws ResourceNotFoundException 사용자 미존재 시
38+
*/
39+
public void withdraw(String userId) {
40+
if (!userRepository.existsById(userId)) {
41+
throw new ResourceNotFoundException("User", userId);
42+
}
43+
44+
log.info("회원 탈퇴 시작: userId={}", userId);
45+
46+
// 1. 수업 피드백 (스케줄 소유 시)
47+
int feedbackDeleted = scheduleFeedbackRepository.deleteBySchedule_UserId(userId);
48+
log.debug("schedule_feedback 삭제: {}건", feedbackDeleted);
49+
50+
// 2. 수강 등록 (학생으로 등록한 것 + 강사 스케줄에 등록된 것)
51+
int regByStudent = registrationRepository.deleteByStudentId(userId);
52+
int regBySchedule = registrationRepository.deleteBySchedule_UserId(userId);
53+
log.debug("registration 삭제: student={}, schedule={}", regByStudent, regBySchedule);
54+
55+
// 3. 스케줄 (강사 소유)
56+
int scheduleDeleted = scheduleRepository.deleteByUserId(userId);
57+
log.debug("schedule 삭제: {}건", scheduleDeleted);
58+
59+
// 4. 강사 휴무
60+
int timeOffDeleted = teacherTimeOffRepository.deleteByTeacherId(userId);
61+
log.debug("teacher_time_off 삭제: {}건", timeOffDeleted);
62+
63+
// 5. 강사 가용 시간
64+
teacherAvailabilityRepository.deleteByTeacher(userRepository.getReferenceById(userId));
65+
66+
// 6. 세션 (Better-Auth + Backend 공통 테이블)
67+
int sessionDeleted = sessionRepository.deleteByUserId(userId);
68+
log.debug("session 삭제: {}건", sessionDeleted);
69+
70+
// 7. 계정 (Better-Auth, password 등 - entity 없음)
71+
int accountDeleted = jdbcTemplate.update("DELETE FROM account WHERE \"userId\" = ?", userId);
72+
log.debug("account 삭제: {}건", accountDeleted);
73+
74+
// 8. 사용자
75+
userRepository.deleteById(userId);
76+
log.info("회원 탈퇴 완료: userId={}", userId);
77+
}
78+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import WithdrawForm from "@/components/settings/WithdrawForm";
2+
3+
export default function AdminWithdrawPage() {
4+
return <WithdrawForm />;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import WithdrawForm from "@/components/settings/WithdrawForm";
2+
3+
export default function StudyWithdrawPage() {
4+
return <WithdrawForm />;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import WithdrawForm from "@/components/settings/WithdrawForm";
2+
3+
export default function TeachWithdrawPage() {
4+
return <WithdrawForm />;
5+
}

0 commit comments

Comments
 (0)