From 61924491d691c4461670bf63de49865176ea858c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Sun, 12 Jan 2025 18:25:46 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[refactor]=20SecurityConfig=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95=20=20#164?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로그인 필터 빈 등록 --- .../classfit/auth/security/config/SecurityConfig.java | 7 +++++-- .../classfit/auth/security/filter/CustomLoginFilter.java | 4 ---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/classfit/example/classfit/auth/security/config/SecurityConfig.java b/src/main/java/classfit/example/classfit/auth/security/config/SecurityConfig.java index d3f43193..3a6cc197 100644 --- a/src/main/java/classfit/example/classfit/auth/security/config/SecurityConfig.java +++ b/src/main/java/classfit/example/classfit/auth/security/config/SecurityConfig.java @@ -32,7 +32,6 @@ public class SecurityConfig { private final RedisUtil redisUtil; private final JWTUtil jwtUtil; - @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); @@ -50,6 +49,10 @@ public CustomAuthenticationProvider customAuthenticationProvider() { @Bean public SecurityFilterChain filterChain(HttpSecurity security) throws Exception { + + CustomLoginFilter customLoginFilter = new CustomLoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, redisUtil); + customLoginFilter.setFilterProcessesUrl("/api/v1/signin"); + security .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) @@ -70,7 +73,7 @@ public SecurityFilterChain filterChain(HttpSecurity security) throws Exception { security .addFilterBefore(new JWTFilter(customUserDetailService, jwtUtil), CustomLoginFilter.class) - .addFilterAt(new CustomLoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, redisUtil), UsernamePasswordAuthenticationFilter.class) + .addFilterAt(customLoginFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exception -> exception.authenticationEntryPoint(customAuthenticationEntryPoint)); return security.build(); diff --git a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java index 70014193..9ff20c58 100644 --- a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java +++ b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java @@ -25,10 +25,6 @@ @RequiredArgsConstructor public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { - { - setFilterProcessesUrl("/api/v1/signin"); - } - private final AuthenticationManager authenticationManager; private final JWTUtil jwtUtil; private final RedisUtil redisUtil; From 34b5f27015f5c0949c428d81ab1df6c96712fb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Sun, 12 Jan 2025 20:49:26 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[refactor]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=20#164?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/request/UserRequest.java | 19 +++- .../custom/CustomAuthenticationProvider.java | 34 +++----- .../custom/CustomUserDetailService.java | 4 +- .../exception/ClassfitAuthException.java | 20 +++++ .../CustomAuthenticationEntryPoint.java | 30 +++---- .../security/filter/CustomLoginFilter.java | 86 ++++++++----------- .../classfit/auth/service/AuthService.java | 2 +- .../classfit/common/util/CookieUtil.java | 2 +- .../member/repository/MemberRepository.java | 7 +- 9 files changed, 104 insertions(+), 100 deletions(-) create mode 100644 src/main/java/classfit/example/classfit/auth/security/exception/ClassfitAuthException.java diff --git a/src/main/java/classfit/example/classfit/auth/dto/request/UserRequest.java b/src/main/java/classfit/example/classfit/auth/dto/request/UserRequest.java index e0089b2c..33787e58 100644 --- a/src/main/java/classfit/example/classfit/auth/dto/request/UserRequest.java +++ b/src/main/java/classfit/example/classfit/auth/dto/request/UserRequest.java @@ -1,17 +1,34 @@ package classfit.example.classfit.auth.dto.request; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import java.util.Optional; +import java.util.Set; + public record UserRequest ( @NotBlank(message = "이메일은 공백일 수 없습니다.") - @Email(message = "형식이 올바르지 않습니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") String email, @NotBlank(message = "비밀번호는 공백일 수 없습니다.") @Size(min = 8, max = 20, message = "비밀번호는 8 ~ 20자리로 입력해 주세요") String password ) { + + public Optional validate() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + + Set> violations = validator.validate(this); + return violations.stream() + .map(ConstraintViolation::getMessage) + .findFirst(); + } } diff --git a/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java b/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java index c4d2d53a..70b33464 100644 --- a/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java +++ b/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java @@ -1,10 +1,9 @@ package classfit.example.classfit.auth.security.custom; -import classfit.example.classfit.common.exception.ClassfitException; +import classfit.example.classfit.auth.security.exception.ClassfitAuthException; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -19,26 +18,19 @@ public CustomAuthenticationProvider(UserDetailsService userDetailsService, Passw @Override public Authentication authenticate(Authentication authentication) { - try { - Authentication result = super.authenticate(authentication); - CustomUserDetails userDetails = (CustomUserDetails) result.getPrincipal(); - - if (userDetails.member().getAcademy() == null) { - throw new ClassfitException( - "해당 회원은 학원이 등록되지 않았습니다. 학원을 등록해주세요.", - HttpStatus.UNPROCESSABLE_ENTITY - ); - } - - return new CustomAuthenticationToken( - authentication.getName(), - authentication.getCredentials().toString(), - result.getAuthorities() - ); - } catch (ClassfitException e) { - throw new AuthenticationException(e.getMessage(), e) { - }; + Authentication result = super.authenticate(authentication); + CustomUserDetails userDetails = (CustomUserDetails) result.getPrincipal(); + + if (userDetails.member().getAcademy() == null) { + throw new ClassfitAuthException("해당 회원은 학원이 등록되지 않았습니다.", HttpStatus.UNPROCESSABLE_ENTITY); } + + return new CustomAuthenticationToken( + authentication.getName(), + authentication.getCredentials().toString(), + result.getAuthorities() + ); + } @Override diff --git a/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java b/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java index 40f35562..d529caed 100644 --- a/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java +++ b/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java @@ -1,7 +1,9 @@ package classfit.example.classfit.auth.security.custom; +import classfit.example.classfit.auth.security.exception.ClassfitAuthException; import classfit.example.classfit.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @@ -16,6 +18,6 @@ public class CustomUserDetailService implements UserDetailsService { public CustomUserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return memberRepository.findByEmail(email) .map(CustomUserDetails::new) - .orElseThrow(() -> new UsernameNotFoundException("해당 계정은 존재하지 않습니다. 이메일: " + email)); + .orElseThrow(() -> new ClassfitAuthException("해당 계정은 존재하지 않습니다", HttpStatus.UNAUTHORIZED)); } } diff --git a/src/main/java/classfit/example/classfit/auth/security/exception/ClassfitAuthException.java b/src/main/java/classfit/example/classfit/auth/security/exception/ClassfitAuthException.java new file mode 100644 index 00000000..ae894e21 --- /dev/null +++ b/src/main/java/classfit/example/classfit/auth/security/exception/ClassfitAuthException.java @@ -0,0 +1,20 @@ +package classfit.example.classfit.auth.security.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; + +@Getter +public class ClassfitAuthException extends AuthenticationException { + + private final HttpStatus httpStatus; + + public ClassfitAuthException(String message, HttpStatus httpStatus) { + super(message); + this.httpStatus = httpStatus; + } + + public int getHttpStatusCode() { + return httpStatus.value(); + } +} diff --git a/src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java b/src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java index 5b21dd2f..06396f87 100644 --- a/src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java +++ b/src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java @@ -1,9 +1,7 @@ package classfit.example.classfit.auth.security.exception; -import classfit.example.classfit.common.exception.ClassfitException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; @@ -11,26 +9,24 @@ import java.io.IOException; @Component -@Slf4j public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - // 요청에 저장된 예외 확인 - Throwable exception = (Throwable) request.getAttribute("exception"); + Throwable exception = (Throwable) request.getAttribute("AuthException"); - if (exception instanceof ClassfitException classfitException) { - log.error("ClassfitException 확인: {}", classfitException.getMessage()); - response.setStatus(classfitException.getHttpStatus().value()); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write("{\"message\": \"" + classfitException.getMessage() + "\", \"status\": " + classfitException.getHttpStatus().value() + "}"); - } else { - log.error("기타 예외 처리: {}", authException.getMessage()); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write("{\"message\": \"로그인 후 이용 가능합니다. 토큰을 입력해 주세요\", \"status\": 401}"); + if (exception instanceof ClassfitAuthException classfitAuthException) { + writeErrorResponse(response, classfitAuthException.getMessage(), classfitAuthException.getHttpStatusCode()); + return; } + writeErrorResponse(response, "아이디 또는 비밀번호가 잘못 되었습니다.", HttpServletResponse.SC_UNAUTHORIZED); + } + + private void writeErrorResponse(HttpServletResponse response, String message, int status) throws IOException { + response.setStatus(status); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + String jsonResponse = String.format("{\"message\": \"%s\", \"status\": %d}", message, status); + response.getWriter().write(jsonResponse); } } \ No newline at end of file diff --git a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java index 9ff20c58..f73723f2 100644 --- a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java +++ b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java @@ -2,12 +2,13 @@ import classfit.example.classfit.auth.dto.request.UserRequest; import classfit.example.classfit.auth.security.custom.CustomAuthenticationToken; +import classfit.example.classfit.auth.security.exception.ClassfitAuthException; import classfit.example.classfit.auth.security.jwt.JWTUtil; -import classfit.example.classfit.common.exception.ClassfitException; import classfit.example.classfit.common.util.CookieUtil; import classfit.example.classfit.common.util.RedisUtil; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -15,12 +16,10 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.io.IOException; -import java.util.Collection; -import java.util.Iterator; + @RequiredArgsConstructor public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { @@ -31,66 +30,49 @@ public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { - - try { - ObjectMapper objectMapper = new ObjectMapper(); - UserRequest userRequest = objectMapper.readValue(request.getInputStream(), UserRequest.class); - - CustomAuthenticationToken authRequest = new CustomAuthenticationToken(userRequest.email(), userRequest.password(), null); - return authenticationManager.authenticate(authRequest); - - } catch (IOException e) { - throw new ClassfitException("입력 형식이 잘못되었습니다.", HttpStatus.BAD_REQUEST); - } + UserRequest userRequest = parseRequest(request); + userRequest.validate().ifPresent(errorMessage -> { + throw new ClassfitAuthException(errorMessage, HttpStatus.BAD_REQUEST); + }); + + CustomAuthenticationToken authRequest = new CustomAuthenticationToken( + userRequest.email(), userRequest.password(), null + ); + return authenticationManager.authenticate(authRequest); } @Override - protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) { - + protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) throws IOException, ServletException { CustomAuthenticationToken customAuth = (CustomAuthenticationToken) authResult; + String role = customAuth.getAuthorities().iterator().next().getAuthority(); - Collection authorities = customAuth.getAuthorities(); - Iterator iterator = authorities.iterator(); - GrantedAuthority auth = iterator.next(); - + String accessToken = jwtUtil.createJwt("access", customAuth.getEmail(), role, 1000 * 60 * 60 * 5L); + String refreshToken = jwtUtil.createJwt("refresh", customAuth.getEmail(), role, 1000 * 60 * 60 * 24 * 7L); - String role = auth.getAuthority(); - String access = jwtUtil.createJwt("access", customAuth.getEmail(), role, 1000 * 60 * 5L); // 5분 - String refresh = jwtUtil.createJwt("refresh", customAuth.getEmail(), role, 1000 * 60 * 60 * 24 * 7L); // 7일 - - addRefreshEntity(authResult.getName(), refresh, 1000 * 60 * 60 * 24 * 7L); - - res.setHeader("Authorization", "Bearer " + access); - CookieUtil.addCookie(res, "refresh", refresh, 7 * 24 * 60 * 60); - res.setStatus(HttpStatus.OK.value()); + redisUtil.setDataExpire("refresh:" + customAuth.getEmail(), refreshToken, 60 * 60 * 24 * 7L); + setResponse(res, accessToken, refreshToken); } @Override - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); + protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException { + if (failed instanceof ClassfitAuthException) { + req.setAttribute("AuthException", failed); + } + super.unsuccessfulAuthentication(req, res, failed); + } - if (failed.getCause() instanceof ClassfitException classfitException) { - response.setStatus(classfitException.getHttpStatus().value()); - String jsonResponse = String.format( - "{ \"message\": \"%s\", \"status\": %d }", - classfitException.getMessage(), - classfitException.getHttpStatus().value() - ); - response.getWriter().write(jsonResponse); - } else { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - String jsonResponse = String.format( - "{ \"message\": \"로그인에 실패하였습니다. 이메일 또는 비밀번호를 확인해주세요.\", \"status\": %d }", - HttpServletResponse.SC_UNAUTHORIZED - ); - response.getWriter().write(jsonResponse); + private UserRequest parseRequest(HttpServletRequest request) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(request.getInputStream(), UserRequest.class); + } catch (IOException e) { + throw new ClassfitAuthException("입력 형식이 잘못되었습니다.", HttpStatus.BAD_REQUEST); } } - private void addRefreshEntity(String email, String refresh, Long expiredMs) { - String redisKey = "refresh:" + email; - redisUtil.setDataExpire(redisKey, refresh, expiredMs); + private void setResponse(HttpServletResponse res, String accessToken, String refreshToken) { + res.setHeader("Authorization", "Bearer " + accessToken); + CookieUtil.setCookie(res, "refresh", refreshToken, 7 * 24 * 60 * 60); + res.setStatus(HttpStatus.OK.value()); } } diff --git a/src/main/java/classfit/example/classfit/auth/service/AuthService.java b/src/main/java/classfit/example/classfit/auth/service/AuthService.java index 61667704..6eea7c53 100644 --- a/src/main/java/classfit/example/classfit/auth/service/AuthService.java +++ b/src/main/java/classfit/example/classfit/auth/service/AuthService.java @@ -61,7 +61,7 @@ public ResponseEntity reissue(HttpServletRequest request, HttpServletResponse addRefreshEntity(email, newRefresh, 1000 * 60 * 60 * 24 * 7L); response.setHeader("Authorization", "Bearer " + newAccess); - CookieUtil.addCookie(response, "refresh", refresh, 7 * 24 * 60 * 60); + CookieUtil.setCookie(response, "refresh", refresh, 7 * 24 * 60 * 60); return new ResponseEntity<>(HttpStatus.OK); } diff --git a/src/main/java/classfit/example/classfit/common/util/CookieUtil.java b/src/main/java/classfit/example/classfit/common/util/CookieUtil.java index 17bbaa5c..3fb2151d 100644 --- a/src/main/java/classfit/example/classfit/common/util/CookieUtil.java +++ b/src/main/java/classfit/example/classfit/common/util/CookieUtil.java @@ -6,7 +6,7 @@ public class CookieUtil { - public static void addCookie(HttpServletResponse response, String name, String value, long maxAge) { + public static void setCookie(HttpServletResponse response, String name, String value, long maxAge) { ResponseCookie cookie = ResponseCookie.from(name, value) .path("/") .sameSite("None") diff --git a/src/main/java/classfit/example/classfit/member/repository/MemberRepository.java b/src/main/java/classfit/example/classfit/member/repository/MemberRepository.java index 8d995286..183a1736 100644 --- a/src/main/java/classfit/example/classfit/member/repository/MemberRepository.java +++ b/src/main/java/classfit/example/classfit/member/repository/MemberRepository.java @@ -1,12 +1,10 @@ package classfit.example.classfit.member.repository; import classfit.example.classfit.member.domain.Member; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -17,7 +15,4 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); Optional> findByAcademyId(Long academyId); - - @Query("SELECT m.academy.id FROM Member m WHERE m.name = :memberName") - Long findAcademyIdByMemberName(@Param("memberName") String memberName); } From 496a143bab4e4d334c2df5f3fd543842d88ff86a Mon Sep 17 00:00:00 2001 From: dpfls0922 Date: Sun, 12 Jan 2025 22:00:05 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[refactor]=20=EA=B8=B0=EB=8A=A5=EB=B3=84?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20#172?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/service/EventCreateService.java | 108 ++++++++ .../event/service/EventGetService.java | 63 +++++ .../event/service/EventRepeatService.java | 126 +++++++++ .../classfit/event/service/EventService.java | 244 ++---------------- .../event/service/EventUpdateService.java | 80 ++++++ 5 files changed, 396 insertions(+), 225 deletions(-) create mode 100644 src/main/java/classfit/example/classfit/event/service/EventCreateService.java create mode 100644 src/main/java/classfit/example/classfit/event/service/EventGetService.java create mode 100644 src/main/java/classfit/example/classfit/event/service/EventRepeatService.java create mode 100644 src/main/java/classfit/example/classfit/event/service/EventUpdateService.java diff --git a/src/main/java/classfit/example/classfit/event/service/EventCreateService.java b/src/main/java/classfit/example/classfit/event/service/EventCreateService.java new file mode 100644 index 00000000..a61464df --- /dev/null +++ b/src/main/java/classfit/example/classfit/event/service/EventCreateService.java @@ -0,0 +1,108 @@ +package classfit.example.classfit.event.service; + +import classfit.example.classfit.calendarCategory.domain.CalendarCategory; +import classfit.example.classfit.calendarCategory.repository.CalendarCategoryRepository; +import classfit.example.classfit.event.domain.Event; +import classfit.example.classfit.event.dto.request.EventCreateRequest; +import classfit.example.classfit.event.dto.request.EventModalRequest; +import classfit.example.classfit.event.dto.response.EventResponse; +import classfit.example.classfit.event.repository.EventRepository; +import classfit.example.classfit.member.domain.Member; +import classfit.example.classfit.member.service.MemberService; +import classfit.example.classfit.memberCalendar.domain.MemberCalendar; +import classfit.example.classfit.memberCalendar.repository.MemberCalendarRepository; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class EventCreateService { + private final EventRepository eventRepository; + private final EventRepeatService eventRepeatService; + private final CalendarCategoryRepository calendarCategoryRepository; + private final MemberCalendarRepository memberCalendarRepository; + private final MemberService memberService; + + @Transactional + public EventResponse createEvent(Member member, EventCreateRequest request) { + Event event = buildEvent(member, request); + Event savedEvent = eventRepository.save(event); + + addAttendeesToEvent(savedEvent, request.memberIds()); + eventRepeatService.addRepeatedEvents(member, request); + + return EventResponse.of( + savedEvent.getId(), + savedEvent.getName(), + savedEvent.getEventType(), + savedEvent.getStartDate(), + savedEvent.getEndDate() + ); + } + + private Event buildEvent(Member member, EventCreateRequest request) { + CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); + MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); + + Event event = Event.builder() + .name(request.name()) + .eventType(request.eventType()) + .category(category) + .memberCalendar(memberCalendar) + .startDate(request.startDate()) + .endDate(request.getEndDate()) + .isAllDay(request.isAllDay()) + .eventRepeatType(request.eventRepeatType()) + .repeatEndDate(request.repeatEndDate().orElse(null)) + .location(request.location().orElse(null)) + .memo(request.memo().orElse(null)) + .build(); + event.setDates(request.isAllDay(), request.startDate(), request.getEndDate()); + return event; + } + + private void addAttendeesToEvent(Event event, List memberIds) { + for (Long memberId : memberIds) { + Member member = memberService.getMembers(memberId); + event.addAttendee(member); + } + } + + @Transactional + public EventResponse createModalEvent(Member member, EventModalRequest request) { + Event event = buildModalEvent(member, request); + Event savedEvent = eventRepository.save(event); + + eventRepeatService.addRepeatedModalEvents(member, request); + + return EventResponse.of( + savedEvent.getId(), + savedEvent.getName(), + savedEvent.getEventType(), + savedEvent.getStartDate(), + savedEvent.getEndDate() + ); + } + + private Event buildModalEvent(Member member, EventModalRequest request) { + CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); + MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); + + Event event = Event.builder() + .name(request.name()) + .category(category) + .memberCalendar(memberCalendar) + .eventType(request.eventType()) + .startDate(request.startDate()) + .endDate(request.getEndDate()) + .isAllDay(request.isAllDay()) + .eventRepeatType(request.eventRepeatType()) + .repeatEndDate(request.repeatEndDate().orElse(null)) + .build(); + event.setDates(request.isAllDay(), request.startDate(), request.getEndDate()); + return event; + } +} diff --git a/src/main/java/classfit/example/classfit/event/service/EventGetService.java b/src/main/java/classfit/example/classfit/event/service/EventGetService.java new file mode 100644 index 00000000..e4437eff --- /dev/null +++ b/src/main/java/classfit/example/classfit/event/service/EventGetService.java @@ -0,0 +1,63 @@ +package classfit.example.classfit.event.service; + +import static classfit.example.classfit.common.exception.ClassfitException.EVENT_NOT_FOUND; + +import classfit.example.classfit.common.exception.ClassfitException; +import classfit.example.classfit.event.domain.Event; +import classfit.example.classfit.event.dto.response.EventMontylyResponse; +import classfit.example.classfit.event.dto.response.EventResponse; +import classfit.example.classfit.event.repository.EventRepository; +import classfit.example.classfit.memberCalendar.domain.CalendarType; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class EventGetService { + private final EventRepository eventRepository; + + @Transactional(readOnly = true) + public EventResponse getEvent(long eventId) { + Event event = getEventById(eventId); + return EventResponse.of( + event.getId(), + event.getName(), + event.getEventType(), + event.getStartDate(), + event.getEndDate() + ); + } + + private Event getEventById(long eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> new ClassfitException(EVENT_NOT_FOUND, HttpStatus.NOT_FOUND)); + } + + @Transactional(readOnly = true) + public List getMonthlyEventsByCalendarType(CalendarType calendarType, int year, int month) { + LocalDateTime startOfMonth = LocalDateTime.of(year, month, 1, 0, 0, 0, 0); + LocalDateTime endOfMonth = startOfMonth.plusMonths(1).minusSeconds(1); + + List events = eventRepository.findByCalendarTypeAndStartDateBetween(calendarType, startOfMonth, endOfMonth); + return mapToEventCreateResponse(events); + } + + private List mapToEventCreateResponse(List events) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + return events.stream() + .map(event -> EventMontylyResponse.of( + String.valueOf(event.getId()), + event.getName(), + event.getEventType().toString(), + event.getStartDate().format(formatter), + event.getEndDate().format(formatter))) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java b/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java new file mode 100644 index 00000000..92442792 --- /dev/null +++ b/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java @@ -0,0 +1,126 @@ +package classfit.example.classfit.event.service; + +import classfit.example.classfit.calendarCategory.domain.CalendarCategory; +import classfit.example.classfit.calendarCategory.repository.CalendarCategoryRepository; +import classfit.example.classfit.event.domain.Event; +import classfit.example.classfit.event.domain.EventRepeatType; +import classfit.example.classfit.event.dto.request.EventCreateRequest; +import classfit.example.classfit.event.dto.request.EventModalRequest; +import classfit.example.classfit.event.repository.EventRepository; +import classfit.example.classfit.member.domain.Member; +import classfit.example.classfit.memberCalendar.domain.MemberCalendar; +import classfit.example.classfit.memberCalendar.repository.MemberCalendarRepository; +import java.time.LocalDateTime; +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class EventRepeatService { + private final EventRepository eventRepository; + private final CalendarCategoryRepository calendarCategoryRepository; + private final MemberCalendarRepository memberCalendarRepository; + + public void addRepeatedEvents(Member member, EventCreateRequest request) { + if (request.eventRepeatType() == EventRepeatType.NONE) { + return; + } + + LocalDateTime currentStartDate = request.startDate(); + LocalDateTime currentEndDate = request.endDate(); + + EventRepeatType eventRepeatType = request.eventRepeatType(); + LocalDateTime repeatEndDate = request.repeatEndDate().orElse(null); + + if (repeatEndDate == null) { + repeatEndDate = currentStartDate.plusMonths(6); + } + + while (shouldCreateEvent(currentEndDate, repeatEndDate)) { + Event repeatedEvent = buildEventWithUpdatedDates(member, request, currentStartDate, currentEndDate); + eventRepository.save(repeatedEvent); + + currentStartDate = getNextRepeatDate(currentStartDate, eventRepeatType); + currentEndDate = getNextRepeatDate(currentEndDate, eventRepeatType); + } + } + + private Event buildEventWithUpdatedDates(Member member, EventCreateRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { + CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); + MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); + + Event event = Event.builder() + .name(request.name()) + .eventType(request.eventType()) + .category(category) + .memberCalendar(memberCalendar) + .startDate(currentStartDate) + .endDate(currentEndDate) + .isAllDay(request.isAllDay()) + .eventRepeatType(request.eventRepeatType()) + .repeatEndDate(request.repeatEndDate().orElse(null)) + .location(request.location().orElse(null)) + .memo(request.memo().orElse(null)) + .build(); + event.setDates(request.isAllDay(), currentStartDate, currentEndDate); + return event; + } + + public void addRepeatedModalEvents(Member member, EventModalRequest request) { + if (request.eventRepeatType() == EventRepeatType.NONE) { + return; + } + + LocalDateTime currentStartDate = request.startDate(); + LocalDateTime currentEndDate = request.endDate(); + + + EventRepeatType eventRepeatType = request.eventRepeatType(); + LocalDateTime repeatEndDate = request.repeatEndDate().orElse(null); + + if (repeatEndDate == null) { + repeatEndDate = currentStartDate.plusMonths(6); + } + + while (shouldCreateEvent(currentEndDate, repeatEndDate)) { + Event repeatedEvent = buildModalEventWithUpdatedDates(member, request, currentStartDate, currentEndDate); + eventRepository.save(repeatedEvent); + + currentStartDate = getNextRepeatDate(currentStartDate, eventRepeatType); + currentEndDate = getNextRepeatDate(currentEndDate, eventRepeatType); + } + } + + private Event buildModalEventWithUpdatedDates(Member member, EventModalRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { + CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); + MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); + + Event event = Event.builder() + .name(request.name()) + .category(category) + .memberCalendar(memberCalendar) + .eventType(request.eventType()) + .startDate(currentStartDate) + .endDate(currentEndDate) + .isAllDay(request.isAllDay()) + .eventRepeatType(request.eventRepeatType()) + .repeatEndDate(request.repeatEndDate().orElse(null)) + .build(); + event.setDates(request.isAllDay(), request.startDate(), request.getEndDate()); + return event; + } + + private boolean shouldCreateEvent(LocalDateTime currentEndDate, LocalDateTime repeatEndDate) { + return currentEndDate.isBefore(repeatEndDate) || currentEndDate.isEqual(repeatEndDate); + } + + private LocalDateTime getNextRepeatDate(LocalDateTime date, EventRepeatType eventRepeatType) { + return switch (eventRepeatType) { + case DAILY -> date.plusDays(1); + case WEEKLY -> date.plusWeeks(1); + case MONTHLY -> date.plusMonths(1); + case YEARLY -> date.plusYears(1); + default -> throw new IllegalArgumentException("유효한 반복 타입을 지정해주세요."); + }; + } +} diff --git a/src/main/java/classfit/example/classfit/event/service/EventService.java b/src/main/java/classfit/example/classfit/event/service/EventService.java index 202b71bd..6b215f23 100644 --- a/src/main/java/classfit/example/classfit/event/service/EventService.java +++ b/src/main/java/classfit/example/classfit/event/service/EventService.java @@ -2,11 +2,8 @@ import static classfit.example.classfit.common.exception.ClassfitException.EVENT_NOT_FOUND; -import classfit.example.classfit.calendarCategory.domain.CalendarCategory; -import classfit.example.classfit.calendarCategory.repository.CalendarCategoryRepository; import classfit.example.classfit.common.exception.ClassfitException; import classfit.example.classfit.event.domain.Event; -import classfit.example.classfit.event.domain.EventRepeatType; import classfit.example.classfit.event.dto.request.EventCreateRequest; import classfit.example.classfit.event.dto.request.EventDragUpdate; import classfit.example.classfit.event.dto.request.EventModalRequest; @@ -14,14 +11,8 @@ import classfit.example.classfit.event.dto.response.EventResponse; import classfit.example.classfit.event.repository.EventRepository; import classfit.example.classfit.member.domain.Member; -import classfit.example.classfit.member.service.MemberService; import classfit.example.classfit.memberCalendar.domain.CalendarType; -import classfit.example.classfit.memberCalendar.domain.MemberCalendar; -import classfit.example.classfit.memberCalendar.repository.MemberCalendarRepository; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -31,198 +22,38 @@ @RequiredArgsConstructor public class EventService { private final EventRepository eventRepository; - private final CalendarCategoryRepository calendarCategoryRepository; - private final MemberCalendarRepository memberCalendarRepository; - private final MemberService memberService; + private final EventCreateService eventCreationService; + private final EventUpdateService eventUpdateService; + private final EventGetService eventGetService; - @Transactional public EventResponse createEvent(Member member, EventCreateRequest request) { - Event event = buildEvent(member, request); - Event savedEvent = eventRepository.save(event); - - addRepeatedEvents(member, request); - addAttendeesToEvent(savedEvent, request.memberIds()); - - return EventResponse.of( - savedEvent.getId(), - savedEvent.getName(), - savedEvent.getEventType(), - savedEvent.getStartDate(), - savedEvent.getEndDate() - ); - } - - private Event buildEvent(Member member, EventCreateRequest request) { - CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); - MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); - - Event event = Event.builder() - .name(request.name()) - .eventType(request.eventType()) - .category(category) - .memberCalendar(memberCalendar) - .startDate(request.startDate()) - .endDate(request.getEndDate()) - .isAllDay(request.isAllDay()) - .eventRepeatType(request.eventRepeatType()) - .repeatEndDate(request.repeatEndDate().orElse(null)) - .location(request.location().orElse(null)) - .memo(request.memo().orElse(null)) - .build(); - event.setDates(request.isAllDay(), request.startDate(), request.getEndDate()); - return event; - } - - private void addRepeatedEvents(Member member, EventCreateRequest request) { - if (request.eventRepeatType() == EventRepeatType.NONE) { - return; - } - - LocalDateTime currentStartDate = request.startDate(); - LocalDateTime currentEndDate = request.endDate(); - - if (request.isAllDay()) { - currentStartDate = currentStartDate.toLocalDate().atStartOfDay(); - currentEndDate = currentEndDate.toLocalDate().atTime(23, 59, 59); - } - - EventRepeatType eventRepeatType = request.eventRepeatType(); - LocalDateTime repeatEndDate = request.repeatEndDate().orElse(null); - if (repeatEndDate == null) { - repeatEndDate = currentStartDate.plusMonths(6); - } - - while (shouldCreateEvent(currentEndDate, repeatEndDate)) { - Event repeatedEvent = buildEventWithUpdatedDates(member, request, currentStartDate, currentEndDate); - - eventRepository.save(repeatedEvent); - - currentStartDate = getNextRepeatDate(currentStartDate, eventRepeatType); - currentEndDate = getNextRepeatDate(currentEndDate, eventRepeatType); - } - } - - private boolean shouldCreateEvent(LocalDateTime currentEndDate, LocalDateTime repeatEndDate) { - return currentEndDate.isBefore(repeatEndDate) || currentEndDate.isEqual(repeatEndDate); + return eventCreationService.createEvent(member, request); } - private Event buildEventWithUpdatedDates(Member member, EventCreateRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { - Event event = buildEvent(member, request); - return event.toBuilder() - .startDate(currentStartDate) - .endDate(currentEndDate) - .build(); - } - - private LocalDateTime getNextRepeatDate(LocalDateTime date, EventRepeatType eventRepeatType) { - return switch (eventRepeatType) { - case DAILY -> date.plusDays(1); - case WEEKLY -> date.plusWeeks(1); - case MONTHLY -> date.plusMonths(1); - case YEARLY -> date.plusYears(1); - default -> throw new IllegalArgumentException("유효한 반복 타입을 지정해주세요."); - }; - } - - private void addAttendeesToEvent(Event event, List memberIds) { - for (Long memberId : memberIds) { - Member member = memberService.getMembers(memberId); - event.addAttendee(member); - } - } - - @Transactional(readOnly = true) - public List getMonthlyEventsByCalendarType(CalendarType calendarType, int year, int month) { - LocalDateTime startOfMonth = LocalDateTime.of(year, month, 1, 0, 0, 0, 0); - LocalDateTime endOfMonth = startOfMonth.plusMonths(1).minusSeconds(1); - - List events = eventRepository.findByCalendarTypeAndStartDateBetween(calendarType, startOfMonth, endOfMonth); - - return mapToEventCreateResponse(events); - } - - private List mapToEventCreateResponse(List events) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - return events.stream() - .map(event -> EventMontylyResponse.of( - String.valueOf(event.getId()), - event.getName(), - event.getEventType().toString(), - event.getStartDate().format(formatter), - event.getEndDate().format(formatter))) - .collect(Collectors.toList()); - } - - @Transactional public EventResponse createModalEvent(Member member, EventModalRequest request) { - Event event = buildModalEvent(member, request); - Event savedEvent = eventRepository.save(event); - - addRepeatedModalEvents(member, request); - - return EventResponse.of(savedEvent.getId(), savedEvent.getName(), savedEvent.getEventType(), savedEvent.getStartDate(), savedEvent.getEndDate()); + return eventCreationService.createModalEvent(member, request); } - private Event buildModalEvent(Member member, EventModalRequest request) { - CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); - MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); - - Event event = Event.builder() - .name(request.name()) - .category(category) - .memberCalendar(memberCalendar) - .eventType(request.eventType()) - .startDate(request.startDate()) - .endDate(request.getEndDate()) - .isAllDay(request.isAllDay()) - .eventRepeatType(request.eventRepeatType()) - .repeatEndDate(request.repeatEndDate().orElse(null)) - .build(); - event.setDates(request.isAllDay(), request.startDate(), request.getEndDate()); - return event; + public EventResponse updateEvent(Member member, long eventId, EventModalRequest request) { + return eventUpdateService.updateEvent(member, eventId, request); } - private void addRepeatedModalEvents(Member member, EventModalRequest request) { - if (request.eventRepeatType() == EventRepeatType.NONE) { - return; - } - - LocalDateTime currentStartDate = request.startDate(); - LocalDateTime currentEndDate = request.endDate(); - EventRepeatType eventRepeatType = request.eventRepeatType(); - LocalDateTime repeatEndDate = request.repeatEndDate().orElse(null); - - if (request.isAllDay()) { - currentStartDate = currentStartDate.toLocalDate().atStartOfDay(); - currentEndDate = currentEndDate.toLocalDate().atTime(23, 59, 59); - } - - if (repeatEndDate == null) { - repeatEndDate = currentStartDate.plusMonths(6); - } - - while (shouldCreateEvent(currentEndDate, repeatEndDate)) { - Event repeatedEvent = buildModalEventWithUpdatedDates(member, request, currentStartDate, currentEndDate); - eventRepository.save(repeatedEvent); - - currentStartDate = getNextRepeatDate(currentStartDate, eventRepeatType); - currentEndDate = getNextRepeatDate(currentEndDate, eventRepeatType); - } + public EventResponse getEvent(long eventId) { + return eventGetService.getEvent(eventId); } - private Event buildModalEventWithUpdatedDates(Member member, EventModalRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { - Event event = buildModalEvent(member, request); - return event.toBuilder() - .startDate(currentStartDate) - .endDate(currentEndDate) - .build(); + public List getMonthlyEventsByCalendarType( + CalendarType calendarType, + int year, + int month + ) { + return eventGetService.getMonthlyEventsByCalendarType(calendarType, year, month); } - @Transactional(readOnly = true) - public EventResponse getEvent(long eventId) { + @Transactional + public void deleteEvent(long eventId) { Event event = getEventById(eventId); - return EventResponse.of(event.getId(), event.getName(), event.getEventType(), event.getStartDate(), event.getEndDate()); + eventRepository.delete(event); } private Event getEventById(long eventId) { @@ -230,44 +61,7 @@ private Event getEventById(long eventId) { .orElseThrow(() -> new ClassfitException(EVENT_NOT_FOUND, HttpStatus.NOT_FOUND)); } - @Transactional - public EventResponse updateEvent(Member member, long eventId, EventModalRequest request) { - Event event = getEventById(eventId); - CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); - MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); - - event.update( - request.name(), - category, - request.eventType(), - request.startDate(), - request.getEndDate(), - request.isAllDay(), - memberCalendar - ); - - eventRepository.save(event); - addRepeatedModalEvents(member, request); - - return EventResponse.of(event.getId(), event.getName(), event.getEventType(), event.getStartDate(), event.getEndDate()); - } - - @Transactional - public void deleteEvent(long eventId) { - Event event = getEventById(eventId); - eventRepository.delete(event); - } - - @Transactional public EventResponse dragUpdateEvent(Long eventId, EventDragUpdate request) { - Event event = getEventById(eventId); - - event.dragUpdate( - request.startDate(), - request.endDate() - ); - - eventRepository.save(event); - return EventResponse.of(event.getId(), event.getName(), event.getEventType(), event.getStartDate(), event.getEndDate()); + return eventUpdateService.dragUpdateEvent(eventId, request); } } \ No newline at end of file diff --git a/src/main/java/classfit/example/classfit/event/service/EventUpdateService.java b/src/main/java/classfit/example/classfit/event/service/EventUpdateService.java new file mode 100644 index 00000000..2dc4a303 --- /dev/null +++ b/src/main/java/classfit/example/classfit/event/service/EventUpdateService.java @@ -0,0 +1,80 @@ +package classfit.example.classfit.event.service; + +import static classfit.example.classfit.common.exception.ClassfitException.EVENT_NOT_FOUND; + +import classfit.example.classfit.calendarCategory.domain.CalendarCategory; +import classfit.example.classfit.calendarCategory.repository.CalendarCategoryRepository; +import classfit.example.classfit.common.exception.ClassfitException; +import classfit.example.classfit.event.domain.Event; +import classfit.example.classfit.event.dto.request.EventDragUpdate; +import classfit.example.classfit.event.dto.request.EventModalRequest; +import classfit.example.classfit.event.dto.response.EventResponse; +import classfit.example.classfit.event.repository.EventRepository; +import classfit.example.classfit.member.domain.Member; +import classfit.example.classfit.memberCalendar.domain.MemberCalendar; +import classfit.example.classfit.memberCalendar.repository.MemberCalendarRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class EventUpdateService { + private final EventRepository eventRepository; + private final EventRepeatService eventRepeatService; + private final CalendarCategoryRepository calendarCategoryRepository; + private final MemberCalendarRepository memberCalendarRepository; + + @Transactional + public EventResponse updateEvent(Member member, long eventId, EventModalRequest request) { + Event event = getEventById(eventId); + CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); + MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); + + event.update( + request.name(), + category, + request.eventType(), + request.startDate(), + request.getEndDate(), + request.isAllDay(), + memberCalendar + ); + + eventRepository.save(event); + eventRepeatService.addRepeatedModalEvents(member, request); + + return EventResponse.of( + event.getId(), + event.getName(), + event.getEventType(), + event.getStartDate(), + event.getEndDate() + ); + } + + @Transactional + public EventResponse dragUpdateEvent(Long eventId, EventDragUpdate request) { + Event event = getEventById(eventId); + + event.dragUpdate( + request.startDate(), + request.endDate() + ); + + eventRepository.save(event); + return EventResponse.of( + event.getId(), + event.getName(), + event.getEventType(), + event.getStartDate(), + event.getEndDate() + ); + } + + private Event getEventById(long eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> new ClassfitException(EVENT_NOT_FOUND, HttpStatus.NOT_FOUND)); + } +} From 052cb5c9044343a564aae566f92541b6b611b0f9 Mon Sep 17 00:00:00 2001 From: dpfls0922 Date: Sun, 12 Jan 2025 22:44:00 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[refactor]=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=95=EC=9D=98=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20#172?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/classfit/event/domain/Event.java | 11 ++++++ .../event/service/EventCreateService.java | 19 ++--------- .../event/service/EventGetService.java | 8 +---- .../event/service/EventRepeatService.java | 34 ++++++++++++------- .../event/service/EventUpdateService.java | 17 ++-------- 5 files changed, 38 insertions(+), 51 deletions(-) diff --git a/src/main/java/classfit/example/classfit/event/domain/Event.java b/src/main/java/classfit/example/classfit/event/domain/Event.java index 93144559..bd0f463f 100644 --- a/src/main/java/classfit/example/classfit/event/domain/Event.java +++ b/src/main/java/classfit/example/classfit/event/domain/Event.java @@ -2,6 +2,7 @@ import classfit.example.classfit.calendarCategory.domain.CalendarCategory; import classfit.example.classfit.common.domain.BaseEntity; +import classfit.example.classfit.event.dto.response.EventResponse; import classfit.example.classfit.eventMember.domain.EventMember; import classfit.example.classfit.member.domain.Member; import classfit.example.classfit.memberCalendar.domain.MemberCalendar; @@ -129,4 +130,14 @@ public void dragUpdate( this.startDate = startDate; this.endDate = endDate; } + + public static EventResponse buildEventResponse(Event event) { + return EventResponse.of( + event.getId(), + event.getName(), + event.getEventType(), + event.getStartDate(), + event.getEndDate() + ); + } } diff --git a/src/main/java/classfit/example/classfit/event/service/EventCreateService.java b/src/main/java/classfit/example/classfit/event/service/EventCreateService.java index a61464df..c5e73261 100644 --- a/src/main/java/classfit/example/classfit/event/service/EventCreateService.java +++ b/src/main/java/classfit/example/classfit/event/service/EventCreateService.java @@ -11,7 +11,6 @@ import classfit.example.classfit.member.service.MemberService; import classfit.example.classfit.memberCalendar.domain.MemberCalendar; import classfit.example.classfit.memberCalendar.repository.MemberCalendarRepository; -import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -33,14 +32,7 @@ public EventResponse createEvent(Member member, EventCreateRequest request) { addAttendeesToEvent(savedEvent, request.memberIds()); eventRepeatService.addRepeatedEvents(member, request); - - return EventResponse.of( - savedEvent.getId(), - savedEvent.getName(), - savedEvent.getEventType(), - savedEvent.getStartDate(), - savedEvent.getEndDate() - ); + return Event.buildEventResponse(savedEvent); } private Event buildEvent(Member member, EventCreateRequest request) { @@ -77,14 +69,7 @@ public EventResponse createModalEvent(Member member, EventModalRequest request) Event savedEvent = eventRepository.save(event); eventRepeatService.addRepeatedModalEvents(member, request); - - return EventResponse.of( - savedEvent.getId(), - savedEvent.getName(), - savedEvent.getEventType(), - savedEvent.getStartDate(), - savedEvent.getEndDate() - ); + return Event.buildEventResponse(savedEvent); } private Event buildModalEvent(Member member, EventModalRequest request) { diff --git a/src/main/java/classfit/example/classfit/event/service/EventGetService.java b/src/main/java/classfit/example/classfit/event/service/EventGetService.java index e4437eff..a703350e 100644 --- a/src/main/java/classfit/example/classfit/event/service/EventGetService.java +++ b/src/main/java/classfit/example/classfit/event/service/EventGetService.java @@ -25,13 +25,7 @@ public class EventGetService { @Transactional(readOnly = true) public EventResponse getEvent(long eventId) { Event event = getEventById(eventId); - return EventResponse.of( - event.getId(), - event.getName(), - event.getEventType(), - event.getStartDate(), - event.getEndDate() - ); + return Event.buildEventResponse(event); } private Event getEventById(long eventId) { diff --git a/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java b/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java index 92442792..f52cd9c1 100644 --- a/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java +++ b/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java @@ -28,13 +28,19 @@ public void addRepeatedEvents(Member member, EventCreateRequest request) { LocalDateTime currentStartDate = request.startDate(); LocalDateTime currentEndDate = request.endDate(); + LocalDateTime repeatEndDate = request.repeatEndDate().orElse(currentStartDate.plusMonths(6)); - EventRepeatType eventRepeatType = request.eventRepeatType(); - LocalDateTime repeatEndDate = request.repeatEndDate().orElse(null); + generateRepeatedEvents(member, request, currentStartDate, currentEndDate, repeatEndDate); + } - if (repeatEndDate == null) { - repeatEndDate = currentStartDate.plusMonths(6); - } + private void generateRepeatedEvents( + Member member, + EventCreateRequest request, + LocalDateTime currentStartDate, + LocalDateTime currentEndDate, + LocalDateTime repeatEndDate + ) { + EventRepeatType eventRepeatType = request.eventRepeatType(); while (shouldCreateEvent(currentEndDate, repeatEndDate)) { Event repeatedEvent = buildEventWithUpdatedDates(member, request, currentStartDate, currentEndDate); @@ -44,8 +50,7 @@ public void addRepeatedEvents(Member member, EventCreateRequest request) { currentEndDate = getNextRepeatDate(currentEndDate, eventRepeatType); } } - - private Event buildEventWithUpdatedDates(Member member, EventCreateRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { + private Event buildEventWithUpdatedDates(Member member, EventCreateRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); @@ -73,14 +78,19 @@ public void addRepeatedModalEvents(Member member, EventModalRequest request) { LocalDateTime currentStartDate = request.startDate(); LocalDateTime currentEndDate = request.endDate(); + LocalDateTime repeatEndDate = request.repeatEndDate().orElse(currentStartDate.plusMonths(6)); + generateRepeatedModalEvents(member, request, currentStartDate, currentEndDate, repeatEndDate); + } + private void generateRepeatedModalEvents( + Member member, + EventModalRequest request, + LocalDateTime currentStartDate, + LocalDateTime currentEndDate, + LocalDateTime repeatEndDate + ) { EventRepeatType eventRepeatType = request.eventRepeatType(); - LocalDateTime repeatEndDate = request.repeatEndDate().orElse(null); - - if (repeatEndDate == null) { - repeatEndDate = currentStartDate.plusMonths(6); - } while (shouldCreateEvent(currentEndDate, repeatEndDate)) { Event repeatedEvent = buildModalEventWithUpdatedDates(member, request, currentStartDate, currentEndDate); diff --git a/src/main/java/classfit/example/classfit/event/service/EventUpdateService.java b/src/main/java/classfit/example/classfit/event/service/EventUpdateService.java index 2dc4a303..ec2ce2e8 100644 --- a/src/main/java/classfit/example/classfit/event/service/EventUpdateService.java +++ b/src/main/java/classfit/example/classfit/event/service/EventUpdateService.java @@ -44,14 +44,7 @@ public EventResponse updateEvent(Member member, long eventId, EventModalRequest eventRepository.save(event); eventRepeatService.addRepeatedModalEvents(member, request); - - return EventResponse.of( - event.getId(), - event.getName(), - event.getEventType(), - event.getStartDate(), - event.getEndDate() - ); + return Event.buildEventResponse(event); } @Transactional @@ -64,13 +57,7 @@ public EventResponse dragUpdateEvent(Long eventId, EventDragUpdate request) { ); eventRepository.save(event); - return EventResponse.of( - event.getId(), - event.getName(), - event.getEventType(), - event.getStartDate(), - event.getEndDate() - ); + return Event.buildEventResponse(event); } private Event getEventById(long eventId) { From 8b329bdb8dc93eeaf471e10737cba21756c6248b Mon Sep 17 00:00:00 2001 From: dpfls0922 Date: Sun, 12 Jan 2025 22:46:21 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[refactor]=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=ED=8F=AC=EB=A9=A7=ED=8C=85=20#172?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/service/EventRepeatService.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java b/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java index f52cd9c1..7868f235 100644 --- a/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java +++ b/src/main/java/classfit/example/classfit/event/service/EventRepeatService.java @@ -50,7 +50,13 @@ private void generateRepeatedEvents( currentEndDate = getNextRepeatDate(currentEndDate, eventRepeatType); } } - private Event buildEventWithUpdatedDates(Member member, EventCreateRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { + + private Event buildEventWithUpdatedDates( + Member member, + EventCreateRequest request, + LocalDateTime currentStartDate, + LocalDateTime currentEndDate + ) { CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); @@ -101,7 +107,12 @@ private void generateRepeatedModalEvents( } } - private Event buildModalEventWithUpdatedDates(Member member, EventModalRequest request, LocalDateTime currentStartDate, LocalDateTime currentEndDate) { + private Event buildModalEventWithUpdatedDates( + Member member, + EventModalRequest request, + LocalDateTime currentStartDate, + LocalDateTime currentEndDate + ) { CalendarCategory category = calendarCategoryRepository.findById(request.categoryId()); MemberCalendar memberCalendar = memberCalendarRepository.findByMemberAndType(member, request.calendarType()); From 07e797f73b337e60298b760ba17599ae032444dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Sun, 12 Jan 2025 23:46:48 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[feat]=20AccessDeniedHandler=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#164?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomAccessDeniedHandler.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/classfit/example/classfit/auth/exception/CustomAccessDeniedHandler.java diff --git a/src/main/java/classfit/example/classfit/auth/exception/CustomAccessDeniedHandler.java b/src/main/java/classfit/example/classfit/auth/exception/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..a38e594f --- /dev/null +++ b/src/main/java/classfit/example/classfit/auth/exception/CustomAccessDeniedHandler.java @@ -0,0 +1,20 @@ +package classfit.example.classfit.auth.exception; + +import classfit.example.classfit.common.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + ApiResponse.errorResponse(response, "접근 권한이 없습니다.", HttpStatus.UNAUTHORIZED.value()); + } +} From 8edf491de3fd464a21dbaf9fbb91482783974b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Sun, 12 Jan 2025 23:47:22 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[refactor]=20JWT=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20#164?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthMemberArgumentResolver.java | 6 +- .../CustomAuthenticationEntryPoint.java | 20 ++++++ .../custom/CustomAuthenticationProvider.java | 3 +- .../custom/CustomUserDetailService.java | 2 +- .../CustomAuthenticationEntryPoint.java | 32 ---------- .../security/filter/CustomLoginFilter.java | 28 +++++---- .../classfit/auth/security/jwt/JWTFilter.java | 36 +++++------ .../classfit/auth/security/jwt/JWTUtil.java | 12 ++-- .../classfit/auth/service/AuthService.java | 62 ++++++++++--------- .../example/classfit/common/ApiResponse.java | 11 ++++ .../config/SecurityConfig.java | 11 ++-- .../exception/ClassfitAuthException.java | 2 +- .../config => common/util}/SecurityUtil.java | 6 +- 13 files changed, 119 insertions(+), 112 deletions(-) create mode 100644 src/main/java/classfit/example/classfit/auth/exception/CustomAuthenticationEntryPoint.java delete mode 100644 src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java rename src/main/java/classfit/example/classfit/{auth/security => common}/config/SecurityConfig.java (88%) rename src/main/java/classfit/example/classfit/{auth/security => common}/exception/ClassfitAuthException.java (88%) rename src/main/java/classfit/example/classfit/{auth/security/config => common/util}/SecurityUtil.java (71%) diff --git a/src/main/java/classfit/example/classfit/auth/annotation/AuthMemberArgumentResolver.java b/src/main/java/classfit/example/classfit/auth/annotation/AuthMemberArgumentResolver.java index 49325487..a29e65af 100644 --- a/src/main/java/classfit/example/classfit/auth/annotation/AuthMemberArgumentResolver.java +++ b/src/main/java/classfit/example/classfit/auth/annotation/AuthMemberArgumentResolver.java @@ -1,7 +1,7 @@ package classfit.example.classfit.auth.annotation; -import classfit.example.classfit.auth.security.config.SecurityUtil; -import classfit.example.classfit.common.exception.ClassfitException; +import classfit.example.classfit.common.exception.ClassfitAuthException; +import classfit.example.classfit.common.util.SecurityUtil; import classfit.example.classfit.member.domain.Member; import classfit.example.classfit.member.repository.MemberRepository; import lombok.NonNull; @@ -30,6 +30,6 @@ public boolean supportsParameter(MethodParameter parameter) { public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Long currentMemberId = SecurityUtil.getCurrentMemberId(); return memberRepository.findById(currentMemberId) - .orElseThrow(() -> new ClassfitException("사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); + .orElseThrow(() -> new ClassfitAuthException("사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND)); } } \ No newline at end of file diff --git a/src/main/java/classfit/example/classfit/auth/exception/CustomAuthenticationEntryPoint.java b/src/main/java/classfit/example/classfit/auth/exception/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000..a5f3b69b --- /dev/null +++ b/src/main/java/classfit/example/classfit/auth/exception/CustomAuthenticationEntryPoint.java @@ -0,0 +1,20 @@ +package classfit.example.classfit.auth.exception; + +import classfit.example.classfit.common.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + ApiResponse.errorResponse(response, "접근 권한이 없습니다.", HttpStatus.UNAUTHORIZED.value()); + } +} diff --git a/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java b/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java index 70b33464..8423ee4c 100644 --- a/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java +++ b/src/main/java/classfit/example/classfit/auth/security/custom/CustomAuthenticationProvider.java @@ -1,6 +1,6 @@ package classfit.example.classfit.auth.security.custom; -import classfit.example.classfit.auth.security.exception.ClassfitAuthException; +import classfit.example.classfit.common.exception.ClassfitAuthException; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; @@ -30,7 +30,6 @@ public Authentication authenticate(Authentication authentication) { authentication.getCredentials().toString(), result.getAuthorities() ); - } @Override diff --git a/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java b/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java index d529caed..7cc9f667 100644 --- a/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java +++ b/src/main/java/classfit/example/classfit/auth/security/custom/CustomUserDetailService.java @@ -1,6 +1,6 @@ package classfit.example.classfit.auth.security.custom; -import classfit.example.classfit.auth.security.exception.ClassfitAuthException; +import classfit.example.classfit.common.exception.ClassfitAuthException; import classfit.example.classfit.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java b/src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java deleted file mode 100644 index 06396f87..00000000 --- a/src/main/java/classfit/example/classfit/auth/security/exception/CustomAuthenticationEntryPoint.java +++ /dev/null @@ -1,32 +0,0 @@ -package classfit.example.classfit.auth.security.exception; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Component -public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - Throwable exception = (Throwable) request.getAttribute("AuthException"); - - if (exception instanceof ClassfitAuthException classfitAuthException) { - writeErrorResponse(response, classfitAuthException.getMessage(), classfitAuthException.getHttpStatusCode()); - return; - } - writeErrorResponse(response, "아이디 또는 비밀번호가 잘못 되었습니다.", HttpServletResponse.SC_UNAUTHORIZED); - } - - private void writeErrorResponse(HttpServletResponse response, String message, int status) throws IOException { - response.setStatus(status); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - String jsonResponse = String.format("{\"message\": \"%s\", \"status\": %d}", message, status); - response.getWriter().write(jsonResponse); - } -} \ No newline at end of file diff --git a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java index f73723f2..979ca371 100644 --- a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java +++ b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java @@ -2,13 +2,13 @@ import classfit.example.classfit.auth.dto.request.UserRequest; import classfit.example.classfit.auth.security.custom.CustomAuthenticationToken; -import classfit.example.classfit.auth.security.exception.ClassfitAuthException; import classfit.example.classfit.auth.security.jwt.JWTUtil; +import classfit.example.classfit.common.ApiResponse; +import classfit.example.classfit.common.exception.ClassfitAuthException; import classfit.example.classfit.common.util.CookieUtil; import classfit.example.classfit.common.util.RedisUtil; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -24,6 +24,11 @@ @RequiredArgsConstructor public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { + private static final String CREDENTIAL = "Authorization"; + private static final String SECURITY_SCHEMA_TYPE = "Bearer "; + private static final String ACCESS_TOKEN_CATEGORY = "access"; + private static final String REFRESH_TOKEN_CATEGORY = "refresh"; + private final AuthenticationManager authenticationManager; private final JWTUtil jwtUtil; private final RedisUtil redisUtil; @@ -42,23 +47,24 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } @Override - protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) throws IOException, ServletException { + protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) { CustomAuthenticationToken customAuth = (CustomAuthenticationToken) authResult; String role = customAuth.getAuthorities().iterator().next().getAuthority(); - String accessToken = jwtUtil.createJwt("access", customAuth.getEmail(), role, 1000 * 60 * 60 * 5L); - String refreshToken = jwtUtil.createJwt("refresh", customAuth.getEmail(), role, 1000 * 60 * 60 * 24 * 7L); + String accessToken = jwtUtil.createJwt(ACCESS_TOKEN_CATEGORY, customAuth.getEmail(), role, 1000 * 60 * 5L); + String refreshToken = jwtUtil.createJwt(REFRESH_TOKEN_CATEGORY, customAuth.getEmail(), role, 1000 * 60 * 60 * 24 * 7L); - redisUtil.setDataExpire("refresh:" + customAuth.getEmail(), refreshToken, 60 * 60 * 24 * 7L); + redisUtil.setDataExpire(REFRESH_TOKEN_CATEGORY + ":" + customAuth.getEmail(), refreshToken, 60 * 60 * 24 * 7L); setResponse(res, accessToken, refreshToken); } @Override - protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException { + protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException { if (failed instanceof ClassfitAuthException) { - req.setAttribute("AuthException", failed); + ApiResponse.errorResponse(res, failed.getMessage(), ((ClassfitAuthException) failed).getHttpStatusCode()); + return; } - super.unsuccessfulAuthentication(req, res, failed); + ApiResponse.errorResponse(res, "아이디 또는 비밀번호가 잘못되었습니다.", HttpServletResponse.SC_UNAUTHORIZED); } private UserRequest parseRequest(HttpServletRequest request) { @@ -71,8 +77,8 @@ private UserRequest parseRequest(HttpServletRequest request) { } private void setResponse(HttpServletResponse res, String accessToken, String refreshToken) { - res.setHeader("Authorization", "Bearer " + accessToken); - CookieUtil.setCookie(res, "refresh", refreshToken, 7 * 24 * 60 * 60); + res.setHeader(CREDENTIAL, SECURITY_SCHEMA_TYPE + accessToken); + CookieUtil.setCookie(res, REFRESH_TOKEN_CATEGORY, refreshToken, 7 * 24 * 60 * 60); res.setStatus(HttpStatus.OK.value()); } } diff --git a/src/main/java/classfit/example/classfit/auth/security/jwt/JWTFilter.java b/src/main/java/classfit/example/classfit/auth/security/jwt/JWTFilter.java index 2b7af7d2..58e0af05 100644 --- a/src/main/java/classfit/example/classfit/auth/security/jwt/JWTFilter.java +++ b/src/main/java/classfit/example/classfit/auth/security/jwt/JWTFilter.java @@ -2,7 +2,8 @@ import classfit.example.classfit.auth.security.custom.CustomUserDetailService; import classfit.example.classfit.auth.security.custom.CustomUserDetails; -import classfit.example.classfit.common.exception.ClassfitException; +import classfit.example.classfit.common.ApiResponse; +import classfit.example.classfit.common.exception.ClassfitAuthException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -12,7 +13,6 @@ import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; @@ -22,43 +22,41 @@ @RequiredArgsConstructor public class JWTFilter extends OncePerRequestFilter { + private static final String ACCESS_TOKEN_CATEGORY = "access"; + private static final String SECURITY_SCHEMA_TYPE = "Bearer "; + private final CustomUserDetailService customUserDetailService; private final JWTUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String access = jwtUtil.resolveToken(request); + String accessToken = jwtUtil.extractToken(request); - if (access == null) { + if (accessToken == null || accessToken.startsWith(SECURITY_SCHEMA_TYPE)) { filterChain.doFilter(request, response); return; } try { - if (jwtUtil.isExpired(access)) { - throw new ClassfitException("엑세스 토큰이 만료되었습니다.", HttpStatus.REQUEST_TIMEOUT); + if (jwtUtil.isExpired(accessToken)) { + throw new ClassfitAuthException("액세스 토큰이 만료되었습니다.", HttpStatus.REQUEST_TIMEOUT); } - if (!"access".equals(jwtUtil.getCategory(access))) { - throw new ClassfitException("유효하지 않은 토큰입니다.", HttpStatus.UNAUTHORIZED); + if (!ACCESS_TOKEN_CATEGORY.equals(jwtUtil.getCategory(accessToken))) { + throw new ClassfitAuthException("유효하지 않은 토큰입니다.", HttpStatus.UNAUTHORIZED); } - String email = jwtUtil.getEmail(access); + String email = jwtUtil.getEmail(accessToken); CustomUserDetails userDetails = customUserDetailService.loadUserByUsername(email); - Authentication authToken = new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities() - ); - + Authentication authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authToken); - filterChain.doFilter(request, response); - } catch (ClassfitException ex) { - request.setAttribute("exception", ex); - throw new AuthenticationException(ex.getMessage()) { - }; + filterChain.doFilter(request, response); + } catch (ClassfitAuthException ex) { + ApiResponse.errorResponse(response, ex.getMessage(), ex.getHttpStatusCode()); } - } } diff --git a/src/main/java/classfit/example/classfit/auth/security/jwt/JWTUtil.java b/src/main/java/classfit/example/classfit/auth/security/jwt/JWTUtil.java index 3d341dc3..bb186111 100644 --- a/src/main/java/classfit/example/classfit/auth/security/jwt/JWTUtil.java +++ b/src/main/java/classfit/example/classfit/auth/security/jwt/JWTUtil.java @@ -18,8 +18,9 @@ @Component public class JWTUtil { + private static final String CREDENTIAL = "Authorization"; + private static final String SECURITY_SCHEMA_TYPE = "Bearer "; private final SecretKey secretKey; - public static final String AUTHORIZATION_HEADER = "Authorization"; public JWTUtil(@Value("${spring.jwt.secret}") String secretKey) { this.secretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); @@ -41,15 +42,14 @@ public Boolean isExpired(String token) { try { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } catch (ExpiredJwtException e) { - return true; // 만료된 경우 true 반환 + return true; } } - public String resolveToken(HttpServletRequest request) { + public String extractToken(HttpServletRequest request) { + String bearerToken = request.getHeader(CREDENTIAL); - String bearerToken = request.getHeader(AUTHORIZATION_HEADER); - - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(SECURITY_SCHEMA_TYPE)) { return bearerToken.substring(7); } diff --git a/src/main/java/classfit/example/classfit/auth/service/AuthService.java b/src/main/java/classfit/example/classfit/auth/service/AuthService.java index 6eea7c53..4c55048e 100644 --- a/src/main/java/classfit/example/classfit/auth/service/AuthService.java +++ b/src/main/java/classfit/example/classfit/auth/service/AuthService.java @@ -5,7 +5,6 @@ import classfit.example.classfit.common.util.CookieUtil; import classfit.example.classfit.common.util.RedisUtil; import classfit.example.classfit.member.domain.Member; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -15,70 +14,73 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import java.util.Arrays; +import java.util.Optional; + @Slf4j @RequiredArgsConstructor @Service public class AuthService { + private static final String CREDENTIAL = "Authorization"; + private static final String SECURITY_SCHEMA_TYPE = "Bearer "; + private static final String ACCESS_TOKEN_CATEGORY = "access"; + private static final String REFRESH_TOKEN_CATEGORY = "refresh"; + private final JWTUtil jwtUtil; private final RedisUtil redisUtil; public ResponseEntity reissue(HttpServletRequest request, HttpServletResponse response) { - String refresh = null; - Cookie[] cookies = request.getCookies(); - for (Cookie cookie : cookies) { + String refreshToken = Arrays.stream(Optional.ofNullable(request.getCookies()) + .orElseThrow(() -> new ClassfitException("쿠키가 존재하지 않습니다.", HttpStatus.BAD_REQUEST))) + .filter(cookie -> REFRESH_TOKEN_CATEGORY.equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElseThrow(() -> new ClassfitException("Refresh 토큰이 존재하지 않습니다.", HttpStatus.BAD_REQUEST)); - if (cookie.getName().equals("refresh")) { - refresh = cookie.getValue(); - } + if (jwtUtil.isExpired(refreshToken)) { + throw new ClassfitException("Refresh 토큰이 만료되었습니다.", HttpStatus.BAD_REQUEST); } - try { - jwtUtil.isExpired(refresh); - } catch (ExpiredJwtException e) { - throw new ClassfitException("Refresh Token이 만료되었습니다.", HttpStatus.BAD_REQUEST); + if (!REFRESH_TOKEN_CATEGORY.equals(jwtUtil.getCategory(refreshToken))) { + throw new ClassfitException("유효하지 않은 토큰입니다.", HttpStatus.BAD_REQUEST); } - String email = jwtUtil.getEmail(refresh); - String category = jwtUtil.getCategory(refresh); - String role = jwtUtil.getRole(refresh); + String email = jwtUtil.getEmail(refreshToken); + String role = jwtUtil.getRole(refreshToken); - if (!category.equals("refresh")) { - throw new ClassfitException("invalid refresh token", HttpStatus.BAD_REQUEST); - } - String redisKey = "refresh:" + email; + String redisKey = REFRESH_TOKEN_CATEGORY + ":" + email; String existedToken = redisUtil.getData(redisKey); if (existedToken == null) { - throw new ClassfitException("invalid refresh token", HttpStatus.BAD_REQUEST); + throw new ClassfitException("존재하지 않는 Refresh 토큰입니다.", HttpStatus.BAD_REQUEST); } - String newAccess = jwtUtil.createJwt("access", email, role, 1000 * 60 * 5L); - String newRefresh = jwtUtil.createJwt("refresh", email, role, 1000 * 60 * 60 * 24 * 7L); + String newAccess = jwtUtil.createJwt(ACCESS_TOKEN_CATEGORY, email, role, 1000 * 60 * 5L); + String newRefresh = jwtUtil.createJwt(REFRESH_TOKEN_CATEGORY, email, role, 1000 * 60 * 60 * 24 * 7L); redisUtil.deleteData(redisKey); - addRefreshEntity(email, newRefresh, 1000 * 60 * 60 * 24 * 7L); + addRefreshEntity(email, newRefresh); - response.setHeader("Authorization", "Bearer " + newAccess); - CookieUtil.setCookie(response, "refresh", refresh, 7 * 24 * 60 * 60); + response.setHeader(CREDENTIAL, SECURITY_SCHEMA_TYPE + newAccess); + CookieUtil.setCookie(response, REFRESH_TOKEN_CATEGORY, refreshToken, 60 * 60 * 24 * 7L); return new ResponseEntity<>(HttpStatus.OK); } public void logout(Member member) { - String redisRefreshTokenKey = "refresh:" + member.getEmail(); - + String redisRefreshTokenKey = REFRESH_TOKEN_CATEGORY + ":" + member.getEmail(); String refreshToken = redisUtil.getData(redisRefreshTokenKey); if (jwtUtil.isExpired(refreshToken) || refreshToken == null) { - throw new ClassfitException("Refresh 토큰이 유효하지 않거나 만료되었습니다..", HttpStatus.NOT_FOUND); + throw new ClassfitException("Refresh 토큰이 유효하지 않거나 만료되었습니다.", HttpStatus.NOT_FOUND); } redisUtil.deleteData(redisRefreshTokenKey); } - private void addRefreshEntity(String email, String refresh, Long expiredMs) { - String redisKey = "refresh:" + email; - redisUtil.setDataExpire(redisKey, refresh, expiredMs); + private void addRefreshEntity(String email, String refresh) { + String redisKey = REFRESH_TOKEN_CATEGORY + ":" + email; + redisUtil.setDataExpire(redisKey, refresh, 1000 * 60 * 60 * 24 * 7L); } } diff --git a/src/main/java/classfit/example/classfit/common/ApiResponse.java b/src/main/java/classfit/example/classfit/common/ApiResponse.java index 44da26e4..39a08f99 100644 --- a/src/main/java/classfit/example/classfit/common/ApiResponse.java +++ b/src/main/java/classfit/example/classfit/common/ApiResponse.java @@ -2,6 +2,9 @@ import classfit.example.classfit.common.domain.ResultType; import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; @JsonInclude(JsonInclude.Include.NON_NULL) public record ApiResponse(int statusCode, ResultType resultType, T data, ErrorResult error, @@ -16,6 +19,14 @@ public static ApiResponse fail(int statusCode, String errorMessage) { null); } + public static void errorResponse(HttpServletResponse response, String message, int status) throws IOException { + response.setStatus(status); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + String jsonResponse = String.format("{\"message\": \"%s\", \"status\": %d}", message, status); + response.getWriter().write(jsonResponse); + } + private record ErrorResult(String message) { } diff --git a/src/main/java/classfit/example/classfit/auth/security/config/SecurityConfig.java b/src/main/java/classfit/example/classfit/common/config/SecurityConfig.java similarity index 88% rename from src/main/java/classfit/example/classfit/auth/security/config/SecurityConfig.java rename to src/main/java/classfit/example/classfit/common/config/SecurityConfig.java index 3a6cc197..b9a5790b 100644 --- a/src/main/java/classfit/example/classfit/auth/security/config/SecurityConfig.java +++ b/src/main/java/classfit/example/classfit/common/config/SecurityConfig.java @@ -1,8 +1,9 @@ -package classfit.example.classfit.auth.security.config; +package classfit.example.classfit.common.config; +import classfit.example.classfit.auth.exception.CustomAccessDeniedHandler; +import classfit.example.classfit.auth.exception.CustomAuthenticationEntryPoint; import classfit.example.classfit.auth.security.custom.CustomAuthenticationProvider; import classfit.example.classfit.auth.security.custom.CustomUserDetailService; -import classfit.example.classfit.auth.security.exception.CustomAuthenticationEntryPoint; import classfit.example.classfit.auth.security.filter.CustomLoginFilter; import classfit.example.classfit.auth.security.jwt.JWTFilter; import classfit.example.classfit.auth.security.jwt.JWTUtil; @@ -28,7 +29,6 @@ public class SecurityConfig { private final AuthenticationConfiguration authenticationConfiguration; private final CustomUserDetailService customUserDetailService; - private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final RedisUtil redisUtil; private final JWTUtil jwtUtil; @@ -74,7 +74,10 @@ public SecurityFilterChain filterChain(HttpSecurity security) throws Exception { security .addFilterBefore(new JWTFilter(customUserDetailService, jwtUtil), CustomLoginFilter.class) .addFilterAt(customLoginFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling(exception -> exception.authenticationEntryPoint(customAuthenticationEntryPoint)); + .exceptionHandling(exception -> exception + .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) // 인증되지 않은 사용자 처리 + .accessDeniedHandler(new CustomAccessDeniedHandler()) // 인증 + ); return security.build(); } diff --git a/src/main/java/classfit/example/classfit/auth/security/exception/ClassfitAuthException.java b/src/main/java/classfit/example/classfit/common/exception/ClassfitAuthException.java similarity index 88% rename from src/main/java/classfit/example/classfit/auth/security/exception/ClassfitAuthException.java rename to src/main/java/classfit/example/classfit/common/exception/ClassfitAuthException.java index ae894e21..638122c4 100644 --- a/src/main/java/classfit/example/classfit/auth/security/exception/ClassfitAuthException.java +++ b/src/main/java/classfit/example/classfit/common/exception/ClassfitAuthException.java @@ -1,4 +1,4 @@ -package classfit.example.classfit.auth.security.exception; +package classfit.example.classfit.common.exception; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/classfit/example/classfit/auth/security/config/SecurityUtil.java b/src/main/java/classfit/example/classfit/common/util/SecurityUtil.java similarity index 71% rename from src/main/java/classfit/example/classfit/auth/security/config/SecurityUtil.java rename to src/main/java/classfit/example/classfit/common/util/SecurityUtil.java index b13cc5db..451ae2b2 100644 --- a/src/main/java/classfit/example/classfit/auth/security/config/SecurityUtil.java +++ b/src/main/java/classfit/example/classfit/common/util/SecurityUtil.java @@ -1,7 +1,7 @@ -package classfit.example.classfit.auth.security.config; +package classfit.example.classfit.common.util; import classfit.example.classfit.auth.security.custom.CustomUserDetails; -import classfit.example.classfit.common.exception.ClassfitException; +import classfit.example.classfit.common.exception.ClassfitAuthException; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -14,7 +14,7 @@ public static Long getCurrentMemberId() { if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails customUserDetails) { return customUserDetails.getMemberId(); } else { - throw new ClassfitException("로그인한 회원의 정보를 확인할 수 없습니다", HttpStatus.NOT_FOUND); + throw new ClassfitAuthException("로그인한 회원의 정보를 확인할 수 없습니다", HttpStatus.NOT_FOUND); } } } From 5b012b36886fea4f476ecfec9503971898614b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Sun, 12 Jan 2025 23:58:24 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[refactor]=20EnableMethodSecurity=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#164?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classfit/example/classfit/common/config/SecurityConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/classfit/example/classfit/common/config/SecurityConfig.java b/src/main/java/classfit/example/classfit/common/config/SecurityConfig.java index b9a5790b..bbee0f11 100644 --- a/src/main/java/classfit/example/classfit/common/config/SecurityConfig.java +++ b/src/main/java/classfit/example/classfit/common/config/SecurityConfig.java @@ -14,6 +14,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -24,6 +25,7 @@ @Configuration @EnableWebSecurity +@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { From a8446254950b7bdefc6c979539a2d1accf6e9a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Mon, 13 Jan 2025 19:48:02 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[fix]=20=ED=9C=B4=EC=A7=80=ED=86=B5=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=8B=9C,=20=EC=9B=90=EB=B3=B8=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=82=AD=EC=A0=9C=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20#179?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classfit/drive/service/DriveTrashService.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java b/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java index 652b5920..de794957 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java @@ -10,6 +10,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -66,9 +68,20 @@ public List moveToTrash(Member member, DriveType driveType, String folde trashPaths.add(trashPath); } + + createPlaceHolder(member, driveType, folderPath); return trashPaths; } + private void createPlaceHolder(Member member, DriveType driveType, String folderPath) { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(0); + + String fullFolderPath = DriveUtil.generatedOriginPath(member, driveType, folderPath, ""); + InputStream emptyContent = new ByteArrayInputStream(new byte[0]); + amazonS3.putObject(new PutObjectRequest(bucketName, fullFolderPath, emptyContent, metadata)); + } + private void addDeleteTagsToS3Object(String objectKey, Member member) { GetObjectTaggingRequest getTaggingRequest = new GetObjectTaggingRequest(bucketName, objectKey); GetObjectTaggingResult taggingResult = amazonS3.getObjectTagging(getTaggingRequest); From 0d31e870f80040de2b71da94a25b869019273436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Mon, 13 Jan 2025 20:29:19 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[feat]=20=ED=8F=B4=EB=8D=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=AC=ED=98=84=20#179?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 하위 폴더 존재 시, 휴지통으로 이동 --- .../security/filter/CustomLoginFilter.java | 3 ++- .../classfit/common/util/DriveUtil.java | 15 ++++++++---- .../controller/DriveFolderController.java | 24 ++++++++++++++----- .../drive/service/DriveFolderService.java | 15 ++++++++++++ 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java index 979ca371..42c41e3d 100644 --- a/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java +++ b/src/main/java/classfit/example/classfit/auth/security/filter/CustomLoginFilter.java @@ -5,6 +5,7 @@ import classfit.example.classfit.auth.security.jwt.JWTUtil; import classfit.example.classfit.common.ApiResponse; import classfit.example.classfit.common.exception.ClassfitAuthException; +import classfit.example.classfit.common.exception.ClassfitException; import classfit.example.classfit.common.util.CookieUtil; import classfit.example.classfit.common.util.RedisUtil; import com.fasterxml.jackson.databind.ObjectMapper; @@ -72,7 +73,7 @@ private UserRequest parseRequest(HttpServletRequest request) { ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.readValue(request.getInputStream(), UserRequest.class); } catch (IOException e) { - throw new ClassfitAuthException("입력 형식이 잘못되었습니다.", HttpStatus.BAD_REQUEST); + throw new ClassfitException("입력 형식이 잘못되었습니다.", HttpStatus.BAD_REQUEST); } } diff --git a/src/main/java/classfit/example/classfit/common/util/DriveUtil.java b/src/main/java/classfit/example/classfit/common/util/DriveUtil.java index 0498bbe1..e3ea9390 100644 --- a/src/main/java/classfit/example/classfit/common/util/DriveUtil.java +++ b/src/main/java/classfit/example/classfit/common/util/DriveUtil.java @@ -7,13 +7,13 @@ import classfit.example.classfit.member.domain.Member; import com.amazonaws.services.s3.model.S3ObjectSummary; import com.amazonaws.services.s3.model.Tag; -import java.util.Map; import org.jetbrains.annotations.NotNull; import org.springframework.http.HttpStatus; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Map; import static classfit.example.classfit.drive.domain.DriveType.PERSONAL; import static classfit.example.classfit.drive.domain.DriveType.SHARED; @@ -75,8 +75,8 @@ public static String extractTags(List tags, String tagKey) { @NotNull public static FileResponse getFileResponse(S3ObjectSummary summary, String fileName, - String fileUrl, - Map tagMap) { + String fileUrl, + Map tagMap) { FileType fileType = getFileType(fileName); String originalFileName = tagMap.getOrDefault("originalFileName", ""); String fileNameWithoutPrefix = getFileNameWithoutPrefix(fileName); @@ -130,7 +130,12 @@ private static String formatFileSize(long sizeInBytes) { private static String getFileNameWithoutPrefix(String objectKey) { String fileNameWithoutPrefix = objectKey.replaceFirst("^personal/\\d+/|^shared/\\d+/", ""); - fileNameWithoutPrefix = fileNameWithoutPrefix.replaceAll("[^a-zA-Z0-9-_./]", "").trim(); - return fileNameWithoutPrefix; + + int lastSlashIndex = fileNameWithoutPrefix.lastIndexOf("/"); + if (lastSlashIndex != -1) { + fileNameWithoutPrefix = fileNameWithoutPrefix.substring(lastSlashIndex + 1); + } + + return fileNameWithoutPrefix.trim(); } } diff --git a/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java b/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java index 10831bad..aa5ff523 100644 --- a/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java +++ b/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java @@ -8,19 +8,18 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; +import jakarta.annotation.Nullable; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/drive") @Tag(name = "드라이브 폴더 컨트롤러", description = "드라이브 폴더 관련 API입니다.") public class DriveFolderController { + private final DriveFolderService driveFolderService; @PostMapping("/folder") @@ -50,4 +49,17 @@ public ApiResponse> getFolders( List folders = driveFolderService.getFolders(member, driveType, folderPath); return ApiResponse.success(folders, 200, "폴더 목록 조회 성공"); } + + @DeleteMapping("/folder") + @Operation(summary = "폴더 삭제", description = "폴더를 삭제하는 API입니다.") + public ApiResponse deleteFolder( + @AuthMember Member member, + @Parameter(description = "내 드라이브는 PERSONAL, 공용 드라이브는 SHARED 입니다.") + @RequestParam DriveType driveType, + @Parameter(description = "삭제할 폴더 이름입니다.") + @RequestParam String folderName + ) { + driveFolderService.deleteFolder(member, driveType, folderName); + return ApiResponse.success(null, 200, "SUCCESS"); + } } diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java b/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java index ceb29bd7..ed3d667b 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java @@ -2,6 +2,7 @@ import classfit.example.classfit.common.util.DriveUtil; import classfit.example.classfit.drive.domain.DriveType; +import classfit.example.classfit.drive.dto.response.FileResponse; import classfit.example.classfit.member.domain.Member; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.*; @@ -23,6 +24,8 @@ public class DriveFolderService { private final AmazonS3 amazonS3; + private final DriveGetService driveGetService; + private final DriveTrashService driveTrashService; @Value("${cloud.aws.s3.bucket}") private String bucketName; @@ -101,4 +104,16 @@ public List getFolders(Member member, DriveType driveType, String folder .map(folder -> folder.substring(prefix.length())) .collect(Collectors.toList()); } + + public void deleteFolder(Member member, DriveType driveType, String folderName) { + List filesFromS3 = driveGetService.getFilesFromS3(member, driveType, folderName); + List strings = filesFromS3.stream() + .map(FileResponse::fileName) + .collect(Collectors.toList()); + + driveTrashService.moveToTrash(member, driveType, folderName, strings); + + String prefix = DriveUtil.buildPrefix(driveType, member, folderName); + amazonS3.deleteObject(new DeleteObjectRequest(bucketName, prefix)); + } } From 7aa733a1c7be6f19b75d7811864ae1bc8367afbe Mon Sep 17 00:00:00 2001 From: dpfls0922 Date: Mon, 13 Jan 2025 22:35:07 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[chore]=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=ED=95=98=EB=8A=94=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?#176?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drive/service/DriveDownloadService.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java b/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java index 61e2946e..5800f7d3 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java @@ -5,12 +5,16 @@ import classfit.example.classfit.member.domain.Member; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.GetObjectTaggingRequest; import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.services.s3.model.Tag; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import lombok.RequiredArgsConstructor; @@ -46,7 +50,8 @@ private void addFileToZip(Member member, DriveType driveType, String folderPath, S3Object s3Object = amazonS3.getObject(new GetObjectRequest(bucketName, originPath)); InputStream inputStream = s3Object.getObjectContent(); - ZipEntry zipEntry = new ZipEntry(fileName); + String originalFileName = getOriginalFileName(originPath, fileName); + ZipEntry zipEntry = new ZipEntry(originalFileName); zipOutputStream.putNextEntry(zipEntry); byte[] buffer = new byte[1024]; @@ -58,4 +63,12 @@ private void addFileToZip(Member member, DriveType driveType, String folderPath, zipOutputStream.closeEntry(); inputStream.close(); } + + private String getOriginalFileName(String objectKey, String fileName) { + Map tagMap = amazonS3.getObjectTagging(new GetObjectTaggingRequest(bucketName, objectKey)) + .getTagSet().stream() + .collect(Collectors.toMap(Tag::getKey, Tag::getValue)); + + return tagMap.getOrDefault("originalFileName", fileName); + } } From f82fd0e2167003a2051f874b5f5d6ac93fca5209 Mon Sep 17 00:00:00 2001 From: dpfls0922 Date: Tue, 14 Jan 2025 00:24:29 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[fix]=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20#176?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classfit/drive/controller/DriveFileController.java | 3 +-- .../classfit/drive/service/DriveDownloadService.java | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/classfit/example/classfit/drive/controller/DriveFileController.java b/src/main/java/classfit/example/classfit/drive/controller/DriveFileController.java index 0194d2d6..ac9c8731 100644 --- a/src/main/java/classfit/example/classfit/drive/controller/DriveFileController.java +++ b/src/main/java/classfit/example/classfit/drive/controller/DriveFileController.java @@ -45,10 +45,9 @@ public ResponseEntity downloadMultipleFiles( @AuthMember Member member, @Parameter(description = "내 드라이브는 PERSONAL, 공용 드라이브는 SHARED 입니다.") @RequestParam DriveType driveType, - @RequestParam(required = false, defaultValue = "") String folderPath, @RequestParam List fileNames ) { - InputStreamResource resource = driveDownloadService.downloadMultipleFiles(member, driveType, folderPath, fileNames); + InputStreamResource resource = driveDownloadService.downloadMultipleFiles(member, driveType, fileNames); String zipFileName = "files.zip"; HttpHeaders headers = new HttpHeaders(); diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java b/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java index 5800f7d3..6b2cff4e 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveDownloadService.java @@ -30,12 +30,12 @@ public class DriveDownloadService { @Value("${cloud.aws.s3.bucket}") private String bucketName; - public InputStreamResource downloadMultipleFiles(Member member, DriveType driveType, String folderPath, List fileNames) { + public InputStreamResource downloadMultipleFiles(Member member, DriveType driveType, List fileNames) { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { for (String fileName : fileNames) { - addFileToZip(member, driveType, folderPath, fileName, zipOutputStream); + addFileToZip(member, driveType, fileName, zipOutputStream); } } catch (IOException e) { throw new RuntimeException("파일 다운로드 중 오류가 발생했습니다.", e); @@ -45,8 +45,8 @@ public InputStreamResource downloadMultipleFiles(Member member, DriveType driveT return new InputStreamResource(byteArrayInputStream); } - private void addFileToZip(Member member, DriveType driveType, String folderPath, String fileName, ZipOutputStream zipOutputStream) throws IOException { - String originPath = DriveUtil.generatedOriginPath(member, driveType, folderPath, fileName); + private void addFileToZip(Member member, DriveType driveType, String fileName, ZipOutputStream zipOutputStream) throws IOException { + String originPath = DriveUtil.generatedOriginPath(member, driveType, "", fileName); S3Object s3Object = amazonS3.getObject(new GetObjectRequest(bucketName, originPath)); InputStream inputStream = s3Object.getObjectContent(); From d27999d853040cdd83c5cd4313750bb6003ae8cb Mon Sep 17 00:00:00 2001 From: dpfls0922 Date: Tue, 14 Jan 2025 01:10:29 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[refactor]=20=EC=9B=94=EB=B3=84=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=83=89=EC=83=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classfit/event/dto/response/EventMontylyResponse.java | 5 +++-- .../example/classfit/event/service/EventGetService.java | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/classfit/example/classfit/event/dto/response/EventMontylyResponse.java b/src/main/java/classfit/example/classfit/event/dto/response/EventMontylyResponse.java index 92a13a7f..5efe15b4 100644 --- a/src/main/java/classfit/example/classfit/event/dto/response/EventMontylyResponse.java +++ b/src/main/java/classfit/example/classfit/event/dto/response/EventMontylyResponse.java @@ -4,11 +4,12 @@ public record EventMontylyResponse ( String id, String name, + String color, String eventType, String startDate, String endDate ) { - public static EventMontylyResponse of(final String id, final String name, final String eventType, final String startDate, final String endDate) { - return new EventMontylyResponse(id, name, eventType, startDate, endDate); + public static EventMontylyResponse of(final String id, final String name, final String color, final String eventType, final String startDate, final String endDate) { + return new EventMontylyResponse(id, name, color, eventType, startDate, endDate); } } diff --git a/src/main/java/classfit/example/classfit/event/service/EventGetService.java b/src/main/java/classfit/example/classfit/event/service/EventGetService.java index a703350e..f3e8ab72 100644 --- a/src/main/java/classfit/example/classfit/event/service/EventGetService.java +++ b/src/main/java/classfit/example/classfit/event/service/EventGetService.java @@ -49,6 +49,7 @@ private List mapToEventCreateResponse(List events) .map(event -> EventMontylyResponse.of( String.valueOf(event.getId()), event.getName(), + event.getCategory() != null ? String.valueOf(event.getCategory().getColor().getHexCode()) : "000000", event.getEventType().toString(), event.getStartDate().format(formatter), event.getEndDate().format(formatter)))