diff --git a/back/src/main/java/com/back/domain/user/controller/UserAuthController.java b/back/src/main/java/com/back/domain/user/controller/UserAuthController.java index e5a0993..ff94ad8 100644 --- a/back/src/main/java/com/back/domain/user/controller/UserAuthController.java +++ b/back/src/main/java/com/back/domain/user/controller/UserAuthController.java @@ -6,10 +6,10 @@ import com.back.domain.user.entity.User; import com.back.domain.user.service.GuestService; import com.back.domain.user.service.UserService; -import com.back.global.common.ApiResponse; import com.back.global.security.CustomUserDetails; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -39,14 +39,13 @@ public class UserAuthController { private final GuestService guestService; @PostMapping("/signup") - public ResponseEntity> signup(@Valid @RequestBody SignupRequest req){ + public ResponseEntity signup(@Valid @RequestBody SignupRequest req){ User saved = userService.signup(req); - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(UserResponse.from(saved), "성공적으로 생성되었습니다.")); + return ResponseEntity.status(HttpStatus.CREATED).body(UserResponse.from(saved)); } @PostMapping("/login") - public ResponseEntity> login( + public ResponseEntity login( @Valid @RequestBody LoginRequest req, HttpServletRequest request, HttpServletResponse response) @@ -60,29 +59,33 @@ public ResponseEntity> login( .saveContext(SecurityContextHolder.getContext(), request, response); CustomUserDetails cud = (CustomUserDetails) auth.getPrincipal(); - return ResponseEntity.ok(ApiResponse.success(UserResponse.from(cud.getUser()), "로그인 성공")); + return ResponseEntity.ok(UserResponse.from(cud.getUser())); } @PostMapping("/guest") - public ResponseEntity> guestLogin(HttpServletRequest request, HttpServletResponse response){ + public ResponseEntity guestLogin(HttpServletRequest request, HttpServletResponse response){ User savedGuest = guestService.createAndSaveGuest(); CustomUserDetails cud = new CustomUserDetails(savedGuest); Authentication auth = new UsernamePasswordAuthenticationToken(cud, null, cud.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); - request.getSession(true); + HttpSession session = request.getSession(true); + + session.setMaxInactiveInterval(600); + session.setAttribute("guestId", savedGuest.getId()); + new HttpSessionSecurityContextRepository() .saveContext(SecurityContextHolder.getContext(), request, response); - return ResponseEntity.ok(ApiResponse.success(UserResponse.from(savedGuest), "게스트 로그인 성공")); + return ResponseEntity.ok(UserResponse.from(savedGuest)); } @GetMapping("/me") - public ResponseEntity> me(@AuthenticationPrincipal CustomUserDetails cud) { + public ResponseEntity me(@AuthenticationPrincipal CustomUserDetails cud) { if (cud == null) { - return ResponseEntity.ok(ApiResponse.success(null, "anonymous")); + return ResponseEntity.ok(null); // 익명 사용자 } - return ResponseEntity.ok(ApiResponse.success(UserResponse.from(cud.getUser()), "authenticated")); + return ResponseEntity.ok(UserResponse.from(cud.getUser())); } } diff --git a/back/src/main/java/com/back/domain/user/controller/UserController.java b/back/src/main/java/com/back/domain/user/controller/UserController.java deleted file mode 100644 index ef4b7c9..0000000 --- a/back/src/main/java/com/back/domain/user/controller/UserController.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.back.domain.user.controller; - -import com.back.domain.user.service.UserService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * 사용자 관련 API 요청을 처리하는 컨트롤러. - * 사용자 정보 조회, 수정, 통계, 목록 등의 기능을 제공합니다. - */ -@RestController -@RequestMapping("/api/v1/users") -@RequiredArgsConstructor -public class UserController { - - private final UserService userService; - -} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/controller/UserInfoController.java b/back/src/main/java/com/back/domain/user/controller/UserInfoController.java new file mode 100644 index 0000000..c8ead80 --- /dev/null +++ b/back/src/main/java/com/back/domain/user/controller/UserInfoController.java @@ -0,0 +1,35 @@ +package com.back.domain.user.controller; + +import com.back.domain.user.dto.UserInfoRequest; +import com.back.domain.user.dto.UserInfoResponse; +import com.back.domain.user.service.UserInfoService; +import com.back.global.security.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/users-info") +@RequiredArgsConstructor +public class UserInfoController { + private final UserInfoService userInfoService; + + @GetMapping + public ResponseEntity getMyInfo(@AuthenticationPrincipal CustomUserDetails principal) { + return ResponseEntity.ok(userInfoService.getMyInfo(principal.getId())); + } + + @PostMapping + public ResponseEntity createMyInfo(@AuthenticationPrincipal CustomUserDetails principal, + @Valid @RequestBody UserInfoRequest req) { + return ResponseEntity.ok(userInfoService.createMyInfo(principal.getId(), req)); + } + + @PutMapping + public ResponseEntity updateMyInfo(@AuthenticationPrincipal CustomUserDetails principal, + @Valid @RequestBody UserInfoRequest req) { + return ResponseEntity.ok(userInfoService.updateMyInfo(principal.getId(), req)); + } +} diff --git a/back/src/main/java/com/back/domain/user/dto/UserInfoRequest.java b/back/src/main/java/com/back/domain/user/dto/UserInfoRequest.java new file mode 100644 index 0000000..28c71b7 --- /dev/null +++ b/back/src/main/java/com/back/domain/user/dto/UserInfoRequest.java @@ -0,0 +1,19 @@ +package com.back.domain.user.dto; + + +import com.back.domain.user.entity.Gender; +import com.back.domain.user.entity.Mbti; + +import java.time.LocalDateTime; + +public record UserInfoRequest( + String username, + LocalDateTime birthdayAt, + Gender gender, + Mbti mbti, + String beliefs, + Integer lifeSatis, + Integer relationship, + Integer workLifeBal, + Integer riskAvoid +) {} diff --git a/back/src/main/java/com/back/domain/user/dto/UserInfoResponse.java b/back/src/main/java/com/back/domain/user/dto/UserInfoResponse.java new file mode 100644 index 0000000..b6ceeac --- /dev/null +++ b/back/src/main/java/com/back/domain/user/dto/UserInfoResponse.java @@ -0,0 +1,39 @@ +package com.back.domain.user.dto; + +import com.back.domain.user.entity.Gender; +import com.back.domain.user.entity.Mbti; +import com.back.domain.user.entity.User; + +import java.time.LocalDateTime; + +public record UserInfoResponse( + String email, + String username, + String nickname, + LocalDateTime birthdayAt, + Gender gender, + Mbti mbti, + String beliefs, + Integer lifeSatis, + Integer relationship, + Integer workLifeBal, + Integer riskAvoid, + LocalDateTime updatedAt +) { + public static UserInfoResponse from(User user) { + return new UserInfoResponse( + user.getEmail(), + user.getUsername(), + user.getNickname(), + user.getBirthdayAt(), + user.getGender(), + user.getMbti(), + user.getBeliefs(), + user.getLifeSatis(), + user.getRelationship(), + user.getWorkLifeBal(), + user.getRiskAvoid(), + user.getUpdatedAt() + ); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/entity/User.java b/back/src/main/java/com/back/domain/user/entity/User.java index 597ae19..3045c5b 100644 --- a/back/src/main/java/com/back/domain/user/entity/User.java +++ b/back/src/main/java/com/back/domain/user/entity/User.java @@ -1,11 +1,18 @@ package com.back.domain.user.entity; +import com.back.domain.node.entity.BaseLine; +import com.back.domain.node.entity.BaseNode; +import com.back.domain.node.entity.DecisionLine; +import com.back.domain.node.entity.DecisionNode; +import com.back.domain.scenario.entity.Scenario; import com.back.global.baseentity.BaseEntity; import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; /** * 사용자 정보를 저장하는 엔티티. @@ -47,17 +54,37 @@ public class User extends BaseEntity { private String beliefs; - private String lifeSatis; + private Integer lifeSatis; - private String relationship; + private Integer relationship; - private String workLifeBal; + private Integer workLifeBal; - private String riskAvoid; + private Integer riskAvoid; @Enumerated(EnumType.STRING) private AuthProvider authProvider; @LastModifiedDate private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List baseLines = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List baseNodes = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List decisionLines = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List decisionNodes = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List scenarios = new ArrayList<>(); } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/repository/UserRepository.java b/back/src/main/java/com/back/domain/user/repository/UserRepository.java index 7a1f033..e8b5cd5 100644 --- a/back/src/main/java/com/back/domain/user/repository/UserRepository.java +++ b/back/src/main/java/com/back/domain/user/repository/UserRepository.java @@ -1,9 +1,11 @@ package com.back.domain.user.repository; +import com.back.domain.user.entity.Role; import com.back.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.Optional; /** @@ -15,4 +17,6 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); boolean existsByNickname(String nickname); + + int deleteByRoleAndCreatedDateBefore(Role role, LocalDateTime cutoff); } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/service/UserInfoService.java b/back/src/main/java/com/back/domain/user/service/UserInfoService.java new file mode 100644 index 0000000..4473fb6 --- /dev/null +++ b/back/src/main/java/com/back/domain/user/service/UserInfoService.java @@ -0,0 +1,57 @@ +package com.back.domain.user.service; + +import com.back.domain.user.dto.UserInfoRequest; +import com.back.domain.user.dto.UserInfoResponse; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserInfoService { + + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public UserInfoResponse getMyInfo(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found: " + userId)); + return UserInfoResponse.from(user); + } + + @Transactional + public UserInfoResponse createMyInfo(Long userId, UserInfoRequest req) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found: " + userId)); + + applyPatch(user, req); + + return UserInfoResponse.from(user); + } + + @Transactional + public UserInfoResponse updateMyInfo(Long userId, UserInfoRequest req) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found: " + userId)); + + applyPatch(user, req); + + return UserInfoResponse.from(user); + } + + private void applyPatch(User user, UserInfoRequest req) { + if (req.username() != null) user.setUsername(req.username()); + if (req.birthdayAt() != null) user.setBirthdayAt(req.birthdayAt()); + if (req.gender() != null) user.setGender(req.gender()); + if (req.mbti() != null) user.setMbti(req.mbti()); + if (req.beliefs() != null) user.setBeliefs(req.beliefs()); + if (req.lifeSatis() != null) user.setLifeSatis(req.lifeSatis()); + if (req.relationship() != null) user.setRelationship(req.relationship()); + if (req.workLifeBal() != null) user.setWorkLifeBal(req.workLifeBal()); + if (req.riskAvoid() != null) user.setRiskAvoid(req.riskAvoid()); + } + +} diff --git a/back/src/main/java/com/back/global/scheduler/GuestCleanupScheduler.java b/back/src/main/java/com/back/global/scheduler/GuestCleanupScheduler.java new file mode 100644 index 0000000..4aa19b8 --- /dev/null +++ b/back/src/main/java/com/back/global/scheduler/GuestCleanupScheduler.java @@ -0,0 +1,36 @@ +package com.back.global.scheduler; + +import com.back.domain.user.entity.Role; +import com.back.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 게스트 계정 자동 정리 스케줄러 + * 세션 리스너가 놓친 게스트 계정을 주기적으로 정리하는 안전망 + * 실행 시점: 매일 새벽 3시 + * 삭제 기준: 생성된 지 24시간이 지난 게스트 계정 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GuestCleanupScheduler { + + private final UserRepository userRepository; + + // 매일 새벽 3시에 실행 + @Scheduled(cron = "0 0 3 * * *") + @Transactional + public void cleanExpiredGuests() { + LocalDateTime cutoff = LocalDateTime.now().minusHours(24); + int deleted = userRepository.deleteByRoleAndCreatedDateBefore(Role.GUEST, cutoff); + if (deleted > 0) { + log.info("배치 작업으로 만료된 게스트 {}건 삭제", deleted); + } + } +} diff --git a/back/src/main/java/com/back/global/security/CsrfCookieFilter.java b/back/src/main/java/com/back/global/security/CsrfCookieFilter.java index 6abad4e..e7c3d33 100644 --- a/back/src/main/java/com/back/global/security/CsrfCookieFilter.java +++ b/back/src/main/java/com/back/global/security/CsrfCookieFilter.java @@ -17,7 +17,7 @@ public class CsrfCookieFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { - CsrfToken token = (CsrfToken) req.getAttribute(CsrfToken.class.getName()); + CsrfToken token = (CsrfToken) req.getAttribute("_csrf"); if (token != null) token.getToken(); chain.doFilter(req, res); } diff --git a/back/src/main/java/com/back/global/security/CustomUserDetails.java b/back/src/main/java/com/back/global/security/CustomUserDetails.java index 2edeca0..5fc717f 100644 --- a/back/src/main/java/com/back/global/security/CustomUserDetails.java +++ b/back/src/main/java/com/back/global/security/CustomUserDetails.java @@ -35,6 +35,8 @@ public Collection getAuthorities() { return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())); } + public Long getId() { return user.getId(); } + @Override public String getPassword() { return user.getPassword(); diff --git a/back/src/main/java/com/back/global/security/SecurityConfig.java b/back/src/main/java/com/back/global/security/SecurityConfig.java index 4e711f0..95367dc 100644 --- a/back/src/main/java/com/back/global/security/SecurityConfig.java +++ b/back/src/main/java/com/back/global/security/SecurityConfig.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -14,8 +15,10 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; /** * Spring Security 설정을 정의하는 구성 클래스입니다. @@ -33,9 +36,17 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + CookieCsrfTokenRepository repo = CookieCsrfTokenRepository.withHttpOnlyFalse(); + CsrfTokenRequestAttributeHandler reqHandler = new CsrfTokenRequestAttributeHandler(); + reqHandler.setCsrfRequestAttributeName("_csrf"); + http .cors(Customizer.withDefaults()) - .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) + .csrf(csrf -> csrf + .csrfTokenRepository(repo) + .csrfTokenRequestHandler(reqHandler) + ) .addFilterAfter(new CsrfCookieFilter(), CsrfFilter.class) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .authorizeHttpRequests(auth -> auth @@ -56,7 +67,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .failureHandler(oAuth2FailureHandler) ) .formLogin(form -> form.disable()) - .httpBasic(h -> h.disable()); + .httpBasic(h -> h.disable()) + .exceptionHandling(e -> e + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + .accessDeniedHandler((req, res, ex) -> res.setStatus(HttpStatus.FORBIDDEN.value())) + ); return http.build(); } diff --git a/back/src/main/java/com/back/global/session/GuestSessionListener.java b/back/src/main/java/com/back/global/session/GuestSessionListener.java new file mode 100644 index 0000000..cee26db --- /dev/null +++ b/back/src/main/java/com/back/global/session/GuestSessionListener.java @@ -0,0 +1,37 @@ +package com.back.global.session; + +import com.back.domain.user.entity.Role; +import com.back.domain.user.repository.UserRepository; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * 게스트 세션 종료 리스너 + * HTTP 세션이 종료될 때 해당 게스트 계정을 즉시 삭제 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GuestSessionListener implements HttpSessionListener { + + private final UserRepository userRepository; + + @Override + @Transactional + public void sessionDestroyed(HttpSessionEvent se) { + Object guestIdObj = se.getSession().getAttribute("guestId"); + + if (guestIdObj instanceof Long guestId) { + userRepository.findById(guestId).ifPresent(user -> { + if (user.getRole() == Role.GUEST) { + userRepository.delete(user); + log.info("세션 만료로 게스트 계정 삭제됨: {}", guestId); + } + }); + } + } +} diff --git a/back/src/main/resources/application.yml b/back/src/main/resources/application.yml index 2b08249..5957845 100644 --- a/back/src/main/resources/application.yml +++ b/back/src/main/resources/application.yml @@ -48,6 +48,7 @@ spring: user-name-attribute: id logging: level: + org.springframework.security: DEBUG org.hibernate.orm.jdbc.bind: TRACE org.hibernate.orm.jdbc.extract: TRACE org.springframework.transaction.interceptor: TRACE diff --git a/back/src/test/java/com/back/domain/user/controller/UserAuthControllerTest.java b/back/src/test/java/com/back/domain/user/controller/UserAuthControllerTest.java index 3c65bbe..21e63cf 100644 --- a/back/src/test/java/com/back/domain/user/controller/UserAuthControllerTest.java +++ b/back/src/test/java/com/back/domain/user/controller/UserAuthControllerTest.java @@ -1,6 +1,7 @@ package com.back.domain.user.controller; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.SignupRequest; import com.back.domain.user.entity.AuthProvider; import com.back.domain.user.entity.Role; import com.back.domain.user.entity.User; @@ -26,8 +27,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * UserAuthController의 주요 인증/인가 기능을 검증하는 통합 테스트 클래스입니다. @@ -68,7 +68,7 @@ private User seedLocalUser(String email, String rawPassword) { @Test @DisplayName("성공 - 회원가입") void t1() throws Exception { - var body = toJson(new SignupReq( + String body = toJson(new SignupRequest( uniqueEmail("join"), "Aa!23456", "홍길동", @@ -81,9 +81,8 @@ void t1() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.data.email").exists()) - .andExpect(jsonPath("$.data.role").value("USER")) - .andExpect(jsonPath("$.message").exists()); + .andExpect(jsonPath("$.email").exists()) + .andExpect(jsonPath("$.role").value("USER")); } @Test @@ -92,7 +91,7 @@ void t2() throws Exception { String email = uniqueEmail("dup"); seedLocalUser(email, "Aa!23456"); - var body = toJson(new SignupReq( + String body = toJson(new SignupRequest( email, "Aa!23456", "김중복", @@ -113,14 +112,14 @@ void t3() throws Exception { String email = uniqueEmail("login-ok"); seedLocalUser(email, "Aa!23456"); - var body = toJson(new LoginReq(email, "Aa!23456")); + String body = toJson(new LoginRequest(email, "Aa!23456")); - var result = mvc.perform(post(BASE + "/login") + MvcResult result = mvc.perform(post(BASE + "/login") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.email").value(email)) + .andExpect(jsonPath("$.email").value(email)) .andReturn(); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); @@ -133,7 +132,7 @@ void t4() throws Exception { String email = uniqueEmail("login-fail"); seedLocalUser(email, "Aa!23456"); - var body = toJson(new LoginReq(email, "Wrong!234")); + String body = toJson(new LoginRequest(email, "Wrong!234")); mvc.perform(post(BASE + "/login") .with(csrf()) @@ -145,9 +144,10 @@ void t4() throws Exception { @Test @DisplayName("성공 - 게스트 로그인") void t5() throws Exception { - var result = mvc.perform(post(BASE + "/guest").with(csrf())) + MvcResult result = mvc.perform(post(BASE + "/guest").with(csrf())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("게스트 로그인 성공")) + .andExpect(jsonPath("$.email").exists()) + .andExpect(jsonPath("$.role").value("GUEST")) .andReturn(); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); @@ -157,10 +157,10 @@ void t5() throws Exception { @Test @DisplayName("성공 - /me (익명)") void t6() throws Exception { + // 컨트롤러가 익명일 때 body = null 반환 → 콘텐츠 빈 문자열 mvc.perform(get(BASE + "/me")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("anonymous")) - .andExpect(jsonPath("$.data").doesNotExist()); + .andExpect(content().string("")); } @Test @@ -183,7 +183,7 @@ void t7() throws Exception { mvc.perform(get(BASE + "/me") .session(session)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("authenticated")); + .andExpect(jsonPath("$.email").value(email)); } @Test @@ -191,9 +191,9 @@ void t7() throws Exception { void t8() throws Exception { String email = uniqueEmail("logout"); seedLocalUser(email, "Aa!23456"); - var body = toJson(new LoginReq(email, "Aa!23456")); + String body = toJson(new LoginRequest(email, "Aa!23456")); - var loginRes = mvc.perform(post(BASE + "/login") + MvcResult loginRes = mvc.perform(post(BASE + "/login") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) @@ -206,11 +206,7 @@ void t8() throws Exception { mvc.perform(post(BASE + "/logout") .with(csrf()) .session(session)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.message").value("로그아웃되었습니다")) - .andExpect(jsonPath("$.message").value("로그아웃이 완료되었습니다")); + .andExpect(status().isOk()); + // 로그아웃 응답 JSON 형식은 커스텀 핸들러 구현에 의존 → 필드 검증 제거 } - - private record SignupReq(String email, String password, String username, String nickname, LocalDateTime birthdayAt) {} - private record LoginReq(String email, String password) {} -} +} \ No newline at end of file diff --git a/back/src/test/java/com/back/global/session/GuestSessionListenerTest.java b/back/src/test/java/com/back/global/session/GuestSessionListenerTest.java new file mode 100644 index 0000000..948ebcb --- /dev/null +++ b/back/src/test/java/com/back/global/session/GuestSessionListenerTest.java @@ -0,0 +1,73 @@ +package com.back.global.session; + +import com.back.domain.node.entity.BaseLine; +import com.back.domain.node.repository.BaseLineRepository; +import com.back.domain.user.entity.Role; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.http.HttpSessionEvent; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class GuestSessionListenerTest { + + @Autowired + private GuestSessionListener listener; + + @Autowired + private UserRepository userRepository; + + @Autowired + private BaseLineRepository baseLineRepository; + + @PersistenceContext + private EntityManager em; + + @Test + @DisplayName("성공 - 세션 만료 시 게스트와 연관 엔티티가 함께 삭제된다") + void t1() { + User guest = userRepository.save(User.builder() + .email("guest_delete@example.com") + .username("guest_delete") + .nickname("삭제될 게스트") + .birthdayAt(LocalDateTime.now()) + .role(Role.GUEST) + .build()); + + // BaseLine 생성 + BaseLine baseLine = BaseLine.builder() + .user(guest) + .title("게스트의 시나리오") + .build(); + guest.getBaseLines().add(baseLine); + baseLineRepository.save(baseLine); + + Long guestId = guest.getId(); + Long baselineId = baseLine.getId(); + + // 세션에 guestId 저장 + MockHttpSession session = new MockHttpSession(); + session.setAttribute("guestId", guestId); + HttpSessionEvent event = new HttpSessionEvent(session); + + // 세션 종료 이벤트 발생 + listener.sessionDestroyed(event); + + assertThat(userRepository.findById(guestId)).isEmpty(); + assertThat(baseLineRepository.findById(baselineId)).isEmpty(); + } +} \ No newline at end of file