From 559b2d49a48faaa65d76102ba9c83226acca8412 Mon Sep 17 00:00:00 2001 From: west_east Date: Wed, 16 Jul 2025 21:24:40 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[#19]=20feat:=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B3=84=EC=B8=B5=20=EB=A1=9C=EA=B7=B8=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../global/annotation/global/ServiceLog.java | 12 ++++++ .../global/ServiceLoggingAspect.java | 42 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/main/java/run/backend/global/annotation/global/ServiceLog.java create mode 100644 src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java diff --git a/build.gradle b/build.gradle index 19d6b59..c08ea6f 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-aop' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/run/backend/global/annotation/global/ServiceLog.java b/src/main/java/run/backend/global/annotation/global/ServiceLog.java new file mode 100644 index 0000000..70646df --- /dev/null +++ b/src/main/java/run/backend/global/annotation/global/ServiceLog.java @@ -0,0 +1,12 @@ +package run.backend.global.annotation.global; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ServiceLog { + String description() default ""; +} diff --git a/src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java b/src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java new file mode 100644 index 0000000..7fdf433 --- /dev/null +++ b/src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java @@ -0,0 +1,42 @@ +package run.backend.global.annotation.global; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; + +@Aspect +@Component +@Slf4j +public class ServiceLoggingAspect { + + @Around("@annotation(run.backend.global.annotation.global.ServiceLog)") + public Object logServiceMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String methodName = signature.getMethod().getName(); + String className = joinPoint.getTarget().getClass().getSimpleName(); + + ServiceLog serviceLog = signature.getMethod().getAnnotation(ServiceLog.class); + String description = serviceLog.description(); + + log.info("--- [ServiceLog] {} - {}() 시작 ---", className, methodName); + if (!description.isEmpty()) { + log.info("--- [ServiceLog] 설명: {} ---", description); + } + + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + Object result = null; + try { + result = joinPoint.proceed(); + } finally { + stopWatch.stop(); + log.info("--- [ServiceLog] {} - {}() 종료 (소요 시간: {} ms) ---", className, methodName, stopWatch.getTotalTimeMillis()); + } + return result; + } +} From 20a4690962548bf7257e754b8a39cc5e049ed99f Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 16:18:43 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[#19]=20feat:=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B3=84=EC=B8=B5=20=EB=A1=9C=EA=B7=B8=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/{ServiceLog.java => Logging.java} | 2 +- .../global/ServiceLoggingAspect.java | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) rename src/main/java/run/backend/global/annotation/global/{ServiceLog.java => Logging.java} (90%) diff --git a/src/main/java/run/backend/global/annotation/global/ServiceLog.java b/src/main/java/run/backend/global/annotation/global/Logging.java similarity index 90% rename from src/main/java/run/backend/global/annotation/global/ServiceLog.java rename to src/main/java/run/backend/global/annotation/global/Logging.java index 70646df..73d88ff 100644 --- a/src/main/java/run/backend/global/annotation/global/ServiceLog.java +++ b/src/main/java/run/backend/global/annotation/global/Logging.java @@ -7,6 +7,6 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -public @interface ServiceLog { +public @interface Logging { String description() default ""; } diff --git a/src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java b/src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java index 7fdf433..df00bd2 100644 --- a/src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java +++ b/src/main/java/run/backend/global/annotation/global/ServiceLoggingAspect.java @@ -7,26 +7,30 @@ import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; +import java.util.Arrays; @Aspect @Component @Slf4j public class ServiceLoggingAspect { - @Around("@annotation(run.backend.global.annotation.global.ServiceLog)") + @Around("@annotation(run.backend.global.annotation.global.Logging)") public Object logServiceMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodName = signature.getMethod().getName(); String className = joinPoint.getTarget().getClass().getSimpleName(); - ServiceLog serviceLog = signature.getMethod().getAnnotation(ServiceLog.class); + Logging serviceLog = signature.getMethod().getAnnotation(Logging.class); String description = serviceLog.description(); - log.info("--- [ServiceLog] {} - {}() 시작 ---", className, methodName); - if (!description.isEmpty()) { - log.info("--- [ServiceLog] 설명: {} ---", description); + Object[] args = joinPoint.getArgs(); + String parameterString = ""; + if (args != null && args.length > 0) { + parameterString = " (파라미터: " + Arrays.toString(args) + ")"; } + log.info("--- [Logging] {} - {}(){}{} 시작 ---", className, methodName, parameterString, (description.isEmpty() ? "" : " - " + description)); + StopWatch stopWatch = new StopWatch(); stopWatch.start(); @@ -35,8 +39,8 @@ public Object logServiceMethodExecution(ProceedingJoinPoint joinPoint) throws Th result = joinPoint.proceed(); } finally { stopWatch.stop(); - log.info("--- [ServiceLog] {} - {}() 종료 (소요 시간: {} ms) ---", className, methodName, stopWatch.getTotalTimeMillis()); + log.info("--- [Logging] {} - {}() 종료 (소요 시간: {} ms) ---", className, methodName, stopWatch.getTotalTimeMillis()); } return result; } -} +} \ No newline at end of file From 9a8f63d1dd121940ea9d82b845b875dc627f1fff Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 19:11:15 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[#19]=20feat:=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=EA=B0=80=20=EC=86=8D=ED=95=9C=20=ED=81=AC=EB=A3=A8=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/annotation/member/MemberCrew.java | 11 +++++ .../member/MemberCrewArgumentResolver.java | 49 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/run/backend/global/annotation/member/MemberCrew.java create mode 100644 src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java diff --git a/src/main/java/run/backend/global/annotation/member/MemberCrew.java b/src/main/java/run/backend/global/annotation/member/MemberCrew.java new file mode 100644 index 0000000..069328f --- /dev/null +++ b/src/main/java/run/backend/global/annotation/member/MemberCrew.java @@ -0,0 +1,11 @@ +package run.backend.global.annotation.member; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface MemberCrew { +} diff --git a/src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java b/src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java new file mode 100644 index 0000000..19ce90b --- /dev/null +++ b/src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java @@ -0,0 +1,49 @@ +package run.backend.global.annotation.member; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.entity.JoinCrew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.member.repository.MemberRepository; + +@Component +@RequiredArgsConstructor +public class MemberCrewArgumentResolver implements HandlerMethodArgumentResolver { + + private final JoinCrewRepository joinCrewRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasMemberCrewAnnotation = parameter.hasParameterAnnotation(MemberCrew.class); + boolean isCrewType = parameter.getParameterType().equals(Crew.class); + + return hasMemberCrewAnnotation && isCrewType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + Long memberId = getCurrentMemberId(); + + JoinCrew joinCrew = joinCrewRepository.findByMemberIdAndJoinStatus(memberId, JoinStatus.APPROVED) + .orElseThrow(() -> new IllegalArgumentException("사용자가 속한 크루가 없습니다")); + + return joinCrew.getCrew(); + } + + private Long getCurrentMemberId() { + // Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // CustomUserDetails principal = (CustomUserDetails) authentication.getPrincipal(); + // return principal.getMember().getId(); + + return 1L; + } +} From e71d0338976a3621026c1cabe5991fae4f3bd745 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 19:12:02 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[#19]=20feat:=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=EA=B0=80=20=EC=86=8D=ED=95=9C=20=ED=81=AC=EB=A3=A8=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/run/backend/global/config/WebConfig.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/run/backend/global/config/WebConfig.java b/src/main/java/run/backend/global/config/WebConfig.java index 222e86f..dcf52dc 100644 --- a/src/main/java/run/backend/global/config/WebConfig.java +++ b/src/main/java/run/backend/global/config/WebConfig.java @@ -5,6 +5,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import run.backend.global.annotation.member.LoginArgumentResolver; +import run.backend.global.annotation.member.MemberCrewArgumentResolver; import java.util.List; @@ -12,14 +13,18 @@ public class WebConfig implements WebMvcConfigurer { private final LoginArgumentResolver loginArgumentResolver; + private final MemberCrewArgumentResolver memberCrewArgumentResolver; @Autowired - public WebConfig(LoginArgumentResolver loginArgumentResolver) { + public WebConfig(LoginArgumentResolver loginArgumentResolver, + MemberCrewArgumentResolver memberCrewArgumentResolver) { this.loginArgumentResolver = loginArgumentResolver; + this.memberCrewArgumentResolver = memberCrewArgumentResolver; } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(loginArgumentResolver); + resolvers.add(memberCrewArgumentResolver); } } From 8d386ee2c53b334eb80c93124e273d5cebd3d1c4 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 20:23:03 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[#19]=20feat:=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=EA=B0=80=20=EC=86=8D=ED=95=9C=20=ED=81=AC=EB=A3=A8=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/annotation/member/MemberCrewArgumentResolver.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java b/src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java index 19ce90b..435e7ed 100644 --- a/src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java +++ b/src/main/java/run/backend/global/annotation/member/MemberCrewArgumentResolver.java @@ -11,7 +11,6 @@ import run.backend.domain.crew.entity.JoinCrew; import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.crew.repository.JoinCrewRepository; -import run.backend.domain.member.repository.MemberRepository; @Component @RequiredArgsConstructor From 011c7df953549de4b66f70d70c505edec57473e8 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 20:23:51 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[#19]=20feat:=20=EC=9A=B4=EC=98=81?= =?UTF-8?q?=EC=A7=84(=ED=81=AC=EB=A3=A8=EC=9E=A5=20=ED=8F=AC=ED=95=A8)=20R?= =?UTF-8?q?ole=20=EB=A6=AC=ED=84=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/run/backend/domain/member/enums/Role.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/run/backend/domain/member/enums/Role.java b/src/main/java/run/backend/domain/member/enums/Role.java index 253c471..91ec273 100644 --- a/src/main/java/run/backend/domain/member/enums/Role.java +++ b/src/main/java/run/backend/domain/member/enums/Role.java @@ -1,6 +1,7 @@ package run.backend.domain.member.enums; import lombok.Getter; +import java.util.Set; @Getter public enum Role { @@ -14,4 +15,8 @@ public enum Role { Role(String description) { this.description = description; } + + public static Set getManagementRoles() { + return Set.of(LEADER, MANAGER); + } } From 45919f267f87f0de3e3d6181bdda8d37bfd8a512 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 21:33:15 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[#19]=20feat:=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/repository/JoinCrewRepository.java | 36 ++++++++ .../event/controller/EventController.java | 36 ++++++++ .../event/dto/request/EventInfoRequest.java | 20 +++- .../response/EventCreationValidationDto.java | 8 ++ .../backend/domain/event/entity/Event.java | 2 +- .../domain/event/entity/PeriodicEvent.java | 9 +- .../event/exception/EventErrorCode.java | 15 +++ .../event/exception/EventException.java | 16 ++++ .../event/repository/EventRepository.java | 8 ++ .../repository/PeriodicEventRepository.java | 7 ++ .../domain/event/service/EventService.java | 17 ++-- .../event/service/EventServiceImpl.java | 91 +++++++++++++++++++ .../global/security/SecurityConfig.java | 3 +- 13 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java create mode 100644 src/main/java/run/backend/domain/event/controller/EventController.java create mode 100644 src/main/java/run/backend/domain/event/dto/response/EventCreationValidationDto.java create mode 100644 src/main/java/run/backend/domain/event/exception/EventErrorCode.java create mode 100644 src/main/java/run/backend/domain/event/exception/EventException.java create mode 100644 src/main/java/run/backend/domain/event/repository/EventRepository.java create mode 100644 src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java create mode 100644 src/main/java/run/backend/domain/event/service/EventServiceImpl.java diff --git a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java new file mode 100644 index 0000000..94732be --- /dev/null +++ b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java @@ -0,0 +1,36 @@ +package run.backend.domain.crew.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import run.backend.domain.crew.entity.JoinCrew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.event.dto.response.EventCreationValidationDto; + +public interface JoinCrewRepository extends JpaRepository { + + @Query("SELECT jc FROM JoinCrew jc WHERE jc.member.id = :memberId AND jc.joinStatus = :status") + Optional findByMemberIdAndJoinStatus(@Param("memberId") Long memberId, + @Param("status") JoinStatus status); + + @Query(""" + SELECT new run.backend.domain.event.dto.response.EventCreationValidationDto( + requesterJoin.crew, + captainJoin.member + ) + FROM JoinCrew requesterJoin + INNER JOIN JoinCrew captainJoin ON requesterJoin.crew.id = captainJoin.crew.id + WHERE requesterJoin.member.id = :requesterId + AND requesterJoin.joinStatus = :status + AND captainJoin.member.id = :runningCaptainId + AND captainJoin.joinStatus = :status + AND captainJoin.role IN :managementRoles + """) + Optional validateEventCreation( + @Param("requesterId") Long requesterId, + @Param("runningCaptainId") Long runningCaptainId, + @Param("status") JoinStatus status, + @Param("managementRoles") java.util.Set managementRoles + ); +} diff --git a/src/main/java/run/backend/domain/event/controller/EventController.java b/src/main/java/run/backend/domain/event/controller/EventController.java new file mode 100644 index 0000000..9391ae3 --- /dev/null +++ b/src/main/java/run/backend/domain/event/controller/EventController.java @@ -0,0 +1,36 @@ +package run.backend.domain.event.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import run.backend.domain.event.dto.request.EventInfoRequest; +import run.backend.domain.event.service.EventServiceImpl; +import run.backend.domain.member.entity.Member; +import run.backend.global.annotation.member.Login; +import run.backend.global.common.response.CommonResponse; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/events") +@Tag(name = "Events", description = "일정 관련 API") +public class EventController { + + private final EventServiceImpl eventService; + + @PostMapping + @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") + @Operation(summary = "일정 생성", description = "러닝 일정를 생성합니다. LEADER 또는 MANAGER 권한이 필요합니다.") + public CommonResponse createEvent( + @RequestBody EventInfoRequest eventInfoRequest, + @Login Member member + ) { + + eventService.createEvent(eventInfoRequest, member); + return new CommonResponse<>("일정 생성 성공"); + } +} diff --git a/src/main/java/run/backend/domain/event/dto/request/EventInfoRequest.java b/src/main/java/run/backend/domain/event/dto/request/EventInfoRequest.java index aa15102..b00b751 100644 --- a/src/main/java/run/backend/domain/event/dto/request/EventInfoRequest.java +++ b/src/main/java/run/backend/domain/event/dto/request/EventInfoRequest.java @@ -1,4 +1,22 @@ package run.backend.domain.event.dto.request; -public record EventInfoRequest() { +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.time.LocalTime; +import run.backend.domain.event.enums.RepeatCycle; +import run.backend.domain.event.enums.WeekDay; + +public record EventInfoRequest( + String title, + LocalDate baseDate, + @Schema(description = "반복 주기", example = "NONE / WEEKLY") + RepeatCycle repeatCycle, + @Schema(description = "반복 요일", example = "MONDAY / TUESDAY / WEDNESDAY / THURSDAY / FRIDAY / SATURDAY / SUNDAY", nullable = true) + WeekDay repeatDays, + LocalTime startTime, + LocalTime endTime, + String place, + @Schema(description = "러닝캡틴 ID", example = "1") + Long runningCaptainId +) { } diff --git a/src/main/java/run/backend/domain/event/dto/response/EventCreationValidationDto.java b/src/main/java/run/backend/domain/event/dto/response/EventCreationValidationDto.java new file mode 100644 index 0000000..ba59942 --- /dev/null +++ b/src/main/java/run/backend/domain/event/dto/response/EventCreationValidationDto.java @@ -0,0 +1,8 @@ +package run.backend.domain.event.dto.response; + +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.member.entity.Member; + +public record EventCreationValidationDto(Crew crew, Member runningCaptain) { + +} diff --git a/src/main/java/run/backend/domain/event/entity/Event.java b/src/main/java/run/backend/domain/event/entity/Event.java index af17ea1..9ec1891 100644 --- a/src/main/java/run/backend/domain/event/entity/Event.java +++ b/src/main/java/run/backend/domain/event/entity/Event.java @@ -50,7 +50,7 @@ public class Event extends BaseEntity { @JoinColumn(name = "record_id") private CrewRecord record; - @OneToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "running_captain") private Member member; diff --git a/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java b/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java index 5e6c839..a961168 100644 --- a/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java +++ b/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java @@ -8,6 +8,7 @@ import run.backend.domain.crew.entity.Crew; import run.backend.domain.event.enums.RepeatCycle; import run.backend.domain.event.enums.WeekDay; +import run.backend.domain.member.entity.Member; import run.backend.global.common.BaseEntity; import java.time.LocalDate; @@ -48,6 +49,10 @@ public class PeriodicEvent extends BaseEntity { @JoinColumn(name = "crew_id") private Crew crew; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "running_captain") + private Member member; + @Builder public PeriodicEvent( String title, @@ -57,7 +62,8 @@ public PeriodicEvent( LocalTime startTime, LocalTime endTime, String place, - Crew crew + Crew crew, + Member member ) { this.title = title; this.baseDate = baseDate; @@ -67,5 +73,6 @@ public PeriodicEvent( this.endTime = endTime; this.place = place; this.crew = crew; + this.member = member; } } diff --git a/src/main/java/run/backend/domain/event/exception/EventErrorCode.java b/src/main/java/run/backend/domain/event/exception/EventErrorCode.java new file mode 100644 index 0000000..8c6842c --- /dev/null +++ b/src/main/java/run/backend/domain/event/exception/EventErrorCode.java @@ -0,0 +1,15 @@ +package run.backend.domain.event.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import run.backend.global.exception.ErrorCode; + +@Getter +@AllArgsConstructor +public enum EventErrorCode implements ErrorCode { + + RUNNING_CAPTAIN_NOT_CREW_MANAGER(6001, "러닝캡이 크루의 운영진이 아닙니다."); + + private final int errorCode; + private final String errorMessage; +} diff --git a/src/main/java/run/backend/domain/event/exception/EventException.java b/src/main/java/run/backend/domain/event/exception/EventException.java new file mode 100644 index 0000000..4ceefe7 --- /dev/null +++ b/src/main/java/run/backend/domain/event/exception/EventException.java @@ -0,0 +1,16 @@ +package run.backend.domain.event.exception; + +import run.backend.global.exception.CustomException; + +public class EventException extends CustomException { + + public EventException(final EventErrorCode eventErrorCode) { + super(eventErrorCode); + } + + public static class InvalidEventCreationRequest extends EventException { + public InvalidEventCreationRequest() { + super(EventErrorCode.RUNNING_CAPTAIN_NOT_CREW_MANAGER); + } + } +} diff --git a/src/main/java/run/backend/domain/event/repository/EventRepository.java b/src/main/java/run/backend/domain/event/repository/EventRepository.java new file mode 100644 index 0000000..94b76e6 --- /dev/null +++ b/src/main/java/run/backend/domain/event/repository/EventRepository.java @@ -0,0 +1,8 @@ +package run.backend.domain.event.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import run.backend.domain.event.entity.Event; + +public interface EventRepository extends JpaRepository { + +} diff --git a/src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java b/src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java new file mode 100644 index 0000000..b3c0e23 --- /dev/null +++ b/src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java @@ -0,0 +1,7 @@ +package run.backend.domain.event.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import run.backend.domain.event.entity.PeriodicEvent; + +public interface PeriodicEventRepository extends JpaRepository { +} diff --git a/src/main/java/run/backend/domain/event/service/EventService.java b/src/main/java/run/backend/domain/event/service/EventService.java index 89a3558..78ca968 100644 --- a/src/main/java/run/backend/domain/event/service/EventService.java +++ b/src/main/java/run/backend/domain/event/service/EventService.java @@ -1,16 +1,17 @@ package run.backend.domain.event.service; - import run.backend.domain.event.dto.request.EventInfoRequest; -import run.backend.domain.event.dto.response.EventInfoResponse; +import run.backend.domain.member.entity.Member; public interface EventService { - void updateEvent(EventInfoRequest eventInfoRequest); - - void joinEvent(Long eventId, Long memberId); - - void deleteEvent(Long eventId); + void createEvent(EventInfoRequest eventInfoRequest, Member member); - EventInfoResponse getEventDetails(Long eventId); +// void updateEvent(EventInfoRequest eventInfoRequest); +// +// void joinEvent(Long eventId, Long memberId); +// +// void deleteEvent(Long eventId); +// +// EventInfoResponse getEventDetails(Long eventId); } diff --git a/src/main/java/run/backend/domain/event/service/EventServiceImpl.java b/src/main/java/run/backend/domain/event/service/EventServiceImpl.java new file mode 100644 index 0000000..2685828 --- /dev/null +++ b/src/main/java/run/backend/domain/event/service/EventServiceImpl.java @@ -0,0 +1,91 @@ +package run.backend.domain.event.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.event.dto.request.EventInfoRequest; +import run.backend.domain.event.dto.response.EventCreationValidationDto; +import run.backend.domain.event.entity.Event; +import run.backend.domain.event.entity.PeriodicEvent; +import run.backend.domain.event.enums.RepeatCycle; +import run.backend.domain.event.exception.EventException.InvalidEventCreationRequest; +import run.backend.domain.event.repository.EventRepository; +import run.backend.domain.event.repository.PeriodicEventRepository; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.Role; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class EventServiceImpl implements EventService { + + private final EventRepository eventRepository; + private final PeriodicEventRepository periodicEventRepository; + private final JoinCrewRepository joinCrewRepository; + + @Override + @Transactional + public void createEvent(EventInfoRequest eventInfoRequest, Member member) { + EventCreationValidationDto validation = joinCrewRepository + .validateEventCreation( + member.getId(), + eventInfoRequest.runningCaptainId(), + JoinStatus.APPROVED, + Role.getManagementRoles() + ) + .orElseThrow(InvalidEventCreationRequest::new); + + Crew crew = validation.crew(); + Member runningCaptain = validation.runningCaptain(); + + if (eventInfoRequest.repeatCycle() == RepeatCycle.NONE) { + createSingleEvent(eventInfoRequest, crew, runningCaptain); + } else { + createPeriodicAndSingleEvent(eventInfoRequest, crew, runningCaptain); + } + } + + private void createSingleEvent(EventInfoRequest request, Crew crew, Member runningCaptain) { + Event event = Event.builder() + .title(request.title()) + .date(request.baseDate()) + .startTime(request.startTime()) + .endTime(request.endTime()) + .place(request.place()) + .crew(crew) + .member(runningCaptain) + .build(); + + eventRepository.save(event); + } + + private void createPeriodicAndSingleEvent(EventInfoRequest request, Crew crew, Member runningCaptain) { + PeriodicEvent periodicEvent = PeriodicEvent.builder() + .title(request.title()) + .baseDate(request.baseDate()) + .repeatCycle(request.repeatCycle()) + .repeatDays(request.repeatDays()) + .startTime(request.startTime()) + .endTime(request.endTime()) + .place(request.place()) + .crew(crew) + .member(runningCaptain) + .build(); + + Event event = Event.builder() + .title(request.title()) + .date(request.baseDate()) + .startTime(request.startTime()) + .endTime(request.endTime()) + .place(request.place()) + .crew(crew) + .member(runningCaptain) + .build(); + + periodicEventRepository.save(periodicEvent); + eventRepository.save(event); + } +} diff --git a/src/main/java/run/backend/global/security/SecurityConfig.java b/src/main/java/run/backend/global/security/SecurityConfig.java index f26d7c2..296c76f 100644 --- a/src/main/java/run/backend/global/security/SecurityConfig.java +++ b/src/main/java/run/backend/global/security/SecurityConfig.java @@ -48,7 +48,8 @@ public class SecurityConfig { private final String[] PermitAllPatterns = { "/api/v1/members/**", - "/api/v1/auth/**" + "/api/v1/auth/**", + "/api/v1/events/**" }; @Bean From 0ae5b68bd05695f9e5a0b839d6e24d06bd519993 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 22:21:03 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[#19]=20feat:=20=EB=9F=AC=EB=8B=9D?= =?UTF-8?q?=EC=BA=A1=20=EC=97=AD=ED=95=A0=20=EC=A0=9C=ED=95=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/crew/repository/JoinCrewRepository.java | 4 +--- src/main/java/run/backend/domain/member/enums/Role.java | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java index 94732be..ba72f2a 100644 --- a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java +++ b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java @@ -25,12 +25,10 @@ Optional findByMemberIdAndJoinStatus(@Param("memberId") Long memberId, AND requesterJoin.joinStatus = :status AND captainJoin.member.id = :runningCaptainId AND captainJoin.joinStatus = :status - AND captainJoin.role IN :managementRoles """) Optional validateEventCreation( @Param("requesterId") Long requesterId, @Param("runningCaptainId") Long runningCaptainId, - @Param("status") JoinStatus status, - @Param("managementRoles") java.util.Set managementRoles + @Param("status") JoinStatus status ); } diff --git a/src/main/java/run/backend/domain/member/enums/Role.java b/src/main/java/run/backend/domain/member/enums/Role.java index 91ec273..253c471 100644 --- a/src/main/java/run/backend/domain/member/enums/Role.java +++ b/src/main/java/run/backend/domain/member/enums/Role.java @@ -1,7 +1,6 @@ package run.backend.domain.member.enums; import lombok.Getter; -import java.util.Set; @Getter public enum Role { @@ -15,8 +14,4 @@ public enum Role { Role(String description) { this.description = description; } - - public static Set getManagementRoles() { - return Set.of(LEADER, MANAGER); - } } From 1d9bd874ab5b2e328d62f3da5cd5336d0259db94 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 22:23:11 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[#19]=20feat:=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=A4=91=EB=B3=B5=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20&=20=EB=A9=A4=EB=B2=84,=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/event/entity/Event.java | 10 ++- .../domain/event/entity/JoinEvent.java | 1 + .../domain/event/entity/PeriodicEvent.java | 2 +- .../event/repository/JoinEventRepository.java | 8 ++ .../event/service/EventServiceImpl.java | 76 +++++++++---------- .../backend/domain/member/entity/Member.java | 4 + 6 files changed, 60 insertions(+), 41 deletions(-) create mode 100644 src/main/java/run/backend/domain/event/repository/JoinEventRepository.java diff --git a/src/main/java/run/backend/domain/event/entity/Event.java b/src/main/java/run/backend/domain/event/entity/Event.java index 9ec1891..af07741 100644 --- a/src/main/java/run/backend/domain/event/entity/Event.java +++ b/src/main/java/run/backend/domain/event/entity/Event.java @@ -70,10 +70,18 @@ public Event( this.startTime = startTime; this.endTime = endTime; this.place = place; - this.expectedParticipants = 1L; + this.expectedParticipants = 0L; this.actualParticipants = 0L; this.crew = crew; this.record = record; this.member = member; } + + public void incrementExpectedParticipants() { + this.expectedParticipants++; + } + + public void incrementActualParticipants() { + this.actualParticipants++; + } } diff --git a/src/main/java/run/backend/domain/event/entity/JoinEvent.java b/src/main/java/run/backend/domain/event/entity/JoinEvent.java index 0633682..48bc5ee 100644 --- a/src/main/java/run/backend/domain/event/entity/JoinEvent.java +++ b/src/main/java/run/backend/domain/event/entity/JoinEvent.java @@ -36,5 +36,6 @@ public JoinEvent( this.isRunning = false; this.member = member; this.event = event; + this.event.incrementExpectedParticipants(); } } diff --git a/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java b/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java index a961168..07b02d3 100644 --- a/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java +++ b/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java @@ -49,7 +49,7 @@ public class PeriodicEvent extends BaseEntity { @JoinColumn(name = "crew_id") private Crew crew; - @OneToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "running_captain") private Member member; diff --git a/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java new file mode 100644 index 0000000..bb974ea --- /dev/null +++ b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java @@ -0,0 +1,8 @@ +package run.backend.domain.event.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import run.backend.domain.event.entity.JoinEvent; + +public interface JoinEventRepository extends JpaRepository { + +} diff --git a/src/main/java/run/backend/domain/event/service/EventServiceImpl.java b/src/main/java/run/backend/domain/event/service/EventServiceImpl.java index 2685828..6911817 100644 --- a/src/main/java/run/backend/domain/event/service/EventServiceImpl.java +++ b/src/main/java/run/backend/domain/event/service/EventServiceImpl.java @@ -9,13 +9,15 @@ import run.backend.domain.event.dto.request.EventInfoRequest; import run.backend.domain.event.dto.response.EventCreationValidationDto; import run.backend.domain.event.entity.Event; +import run.backend.domain.event.entity.JoinEvent; import run.backend.domain.event.entity.PeriodicEvent; import run.backend.domain.event.enums.RepeatCycle; import run.backend.domain.event.exception.EventException.InvalidEventCreationRequest; import run.backend.domain.event.repository.EventRepository; +import run.backend.domain.event.repository.JoinEventRepository; import run.backend.domain.event.repository.PeriodicEventRepository; import run.backend.domain.member.entity.Member; -import run.backend.domain.member.enums.Role; +import run.backend.global.annotation.global.Logging; @Service @RequiredArgsConstructor @@ -25,56 +27,46 @@ public class EventServiceImpl implements EventService { private final EventRepository eventRepository; private final PeriodicEventRepository periodicEventRepository; private final JoinCrewRepository joinCrewRepository; + private final JoinEventRepository joinEventRepository; @Override @Transactional + @Logging public void createEvent(EventInfoRequest eventInfoRequest, Member member) { EventCreationValidationDto validation = joinCrewRepository .validateEventCreation( - member.getId(), - eventInfoRequest.runningCaptainId(), - JoinStatus.APPROVED, - Role.getManagementRoles() + member.getId(), + eventInfoRequest.runningCaptainId(), + JoinStatus.APPROVED ) .orElseThrow(InvalidEventCreationRequest::new); - + Crew crew = validation.crew(); Member runningCaptain = validation.runningCaptain(); - - if (eventInfoRequest.repeatCycle() == RepeatCycle.NONE) { - createSingleEvent(eventInfoRequest, crew, runningCaptain); - } else { - createPeriodicAndSingleEvent(eventInfoRequest, crew, runningCaptain); + + if (eventInfoRequest.repeatCycle() != RepeatCycle.NONE) { + createPeriodicEvent(eventInfoRequest, crew, runningCaptain); } + createSingleEvent(eventInfoRequest, crew, runningCaptain); } - private void createSingleEvent(EventInfoRequest request, Crew crew, Member runningCaptain) { - Event event = Event.builder() - .title(request.title()) - .date(request.baseDate()) - .startTime(request.startTime()) - .endTime(request.endTime()) - .place(request.place()) - .crew(crew) - .member(runningCaptain) - .build(); - - eventRepository.save(event); + private void createPeriodicEvent(EventInfoRequest request, Crew crew, Member runningCaptain) { + PeriodicEvent periodicEvent = PeriodicEvent.builder() + .title(request.title()) + .baseDate(request.baseDate()) + .repeatCycle(request.repeatCycle()) + .repeatDays(request.repeatDays()) + .startTime(request.startTime()) + .endTime(request.endTime()) + .place(request.place()) + .crew(crew) + .member(runningCaptain) + .build(); + + periodicEventRepository.save(periodicEvent); } - private void createPeriodicAndSingleEvent(EventInfoRequest request, Crew crew, Member runningCaptain) { - PeriodicEvent periodicEvent = PeriodicEvent.builder() - .title(request.title()) - .baseDate(request.baseDate()) - .repeatCycle(request.repeatCycle()) - .repeatDays(request.repeatDays()) - .startTime(request.startTime()) - .endTime(request.endTime()) - .place(request.place()) - .crew(crew) - .member(runningCaptain) - .build(); - + private void createSingleEvent(EventInfoRequest request, Crew crew, Member runningCaptain) { Event event = Event.builder() .title(request.title()) .date(request.baseDate()) @@ -84,8 +76,14 @@ private void createPeriodicAndSingleEvent(EventInfoRequest request, Crew crew, M .crew(crew) .member(runningCaptain) .build(); - - periodicEventRepository.save(periodicEvent); - eventRepository.save(event); + + Event savedEvent = eventRepository.save(event); + + JoinEvent joinEvent = JoinEvent.builder() + .event(savedEvent) + .member(runningCaptain) + .build(); + + joinEventRepository.save(joinEvent); } } diff --git a/src/main/java/run/backend/domain/member/entity/Member.java b/src/main/java/run/backend/domain/member/entity/Member.java index a15637f..bf8d670 100644 --- a/src/main/java/run/backend/domain/member/entity/Member.java +++ b/src/main/java/run/backend/domain/member/entity/Member.java @@ -75,4 +75,8 @@ public Member(String username, String nickname, Gender gender, int age, String o this.pushEnabled = true; this.role = Role.NONE; } + + public String toString() { + return "[멤버] 닉네임: " + this.nickname; + } } From aafecd5f81a42bf7eec160b676f787e44b5ee395 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 17 Jul 2025 22:23:46 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[#19]=20chore:=20=EB=9F=AC=EB=8B=9D?= =?UTF-8?q?=EC=BA=A1=20=EC=97=90=EB=9F=AC=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/run/backend/domain/event/exception/EventErrorCode.java | 2 +- .../java/run/backend/domain/event/exception/EventException.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/run/backend/domain/event/exception/EventErrorCode.java b/src/main/java/run/backend/domain/event/exception/EventErrorCode.java index 8c6842c..ebbd7c2 100644 --- a/src/main/java/run/backend/domain/event/exception/EventErrorCode.java +++ b/src/main/java/run/backend/domain/event/exception/EventErrorCode.java @@ -8,7 +8,7 @@ @AllArgsConstructor public enum EventErrorCode implements ErrorCode { - RUNNING_CAPTAIN_NOT_CREW_MANAGER(6001, "러닝캡이 크루의 운영진이 아닙니다."); + RUNNING_CAPTAIN_NOT_CREW_MEMBER(6001, "러닝캡이 크루원이 아닙니다."); private final int errorCode; private final String errorMessage; diff --git a/src/main/java/run/backend/domain/event/exception/EventException.java b/src/main/java/run/backend/domain/event/exception/EventException.java index 4ceefe7..8b12138 100644 --- a/src/main/java/run/backend/domain/event/exception/EventException.java +++ b/src/main/java/run/backend/domain/event/exception/EventException.java @@ -10,7 +10,7 @@ public EventException(final EventErrorCode eventErrorCode) { public static class InvalidEventCreationRequest extends EventException { public InvalidEventCreationRequest() { - super(EventErrorCode.RUNNING_CAPTAIN_NOT_CREW_MANAGER); + super(EventErrorCode.RUNNING_CAPTAIN_NOT_CREW_MEMBER); } } } From 2164848a2e4ad4f4d1ba1b982f08f5e5987027e2 Mon Sep 17 00:00:00 2001 From: west_east Date: Fri, 18 Jul 2025 16:02:57 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[#19]=20test:=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- .../event/service/EventServiceImplTest.java | 213 ++++++++++++++++++ 2 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/test/java/run/backend/domain/event/service/EventServiceImplTest.java diff --git a/.gitignore b/.gitignore index 69678cc..53294a9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ secrets.yml .idea .env src/test/resources/application.yml -/uploads \ No newline at end of file +/uploads +./scripts \ No newline at end of file diff --git a/src/test/java/run/backend/domain/event/service/EventServiceImplTest.java b/src/test/java/run/backend/domain/event/service/EventServiceImplTest.java new file mode 100644 index 0000000..47b8731 --- /dev/null +++ b/src/test/java/run/backend/domain/event/service/EventServiceImplTest.java @@ -0,0 +1,213 @@ +package run.backend.domain.event.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.event.dto.request.EventInfoRequest; +import run.backend.domain.event.dto.response.EventCreationValidationDto; +import run.backend.domain.event.entity.Event; +import run.backend.domain.event.entity.JoinEvent; +import run.backend.domain.event.entity.PeriodicEvent; +import run.backend.domain.event.enums.RepeatCycle; +import run.backend.domain.event.enums.WeekDay; +import run.backend.domain.event.exception.EventException.InvalidEventCreationRequest; +import run.backend.domain.event.repository.EventRepository; +import run.backend.domain.event.repository.JoinEventRepository; +import run.backend.domain.event.repository.PeriodicEventRepository; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.Gender; +import run.backend.domain.member.enums.OAuthType; + +@ExtendWith(MockitoExtension.class) +@DisplayName("EventServiceImpl") +class EventServiceImplTest { + + @Mock + private EventRepository eventRepository; + + @Mock + private PeriodicEventRepository periodicEventRepository; + + @Mock + private JoinCrewRepository joinCrewRepository; + + @Mock + private JoinEventRepository joinEventRepository; + + @InjectMocks + private EventServiceImpl sut; // System Under Test + + private Member requestMember; + private Member runningCaptain; + private Crew crew; + private Event savedEvent; + + @BeforeEach + void setUp() { + requestMember = createMember("요청자"); + runningCaptain = createMember("러닝캡틴"); + crew = createCrew("테스트크루"); + savedEvent = createEvent(); + } + + @Nested + @DisplayName("createEvent 메서드는") + class CreateEventTest { + + @Test + @DisplayName("일반 일정 생성 시 Event와 JoinEvent를 저장한다") + void shouldCreateSingleEventSuccessfully() { + // given + EventInfoRequest request = createSingleEventRequest(); + EventCreationValidationDto validation = new EventCreationValidationDto(crew, runningCaptain); + + given(joinCrewRepository.validateEventCreation(any(), any(), any())) + .willReturn(Optional.of(validation)); + + given(eventRepository.save(any(Event.class))) + .willReturn(savedEvent); + + // when + sut.createEvent(request, requestMember); + + // then + then(eventRepository).should().save(any(Event.class)); + then(joinEventRepository).should().save(any(JoinEvent.class)); + then(periodicEventRepository).should(never()).save(any(PeriodicEvent.class)); + } + + @Test + @DisplayName("주기적 일정 생성 시 PeriodicEvent, Event, JoinEvent를 모두 저장한다") + void shouldCreatePeriodicEventSuccessfully() { + // given + EventInfoRequest request = createPeriodicEventRequest(); + EventCreationValidationDto validation = new EventCreationValidationDto(crew, runningCaptain); + + given(joinCrewRepository.validateEventCreation(any(), any(), any())) + .willReturn(Optional.of(validation)); + + given(eventRepository.save(any(Event.class))) + .willReturn(savedEvent); + + // when + sut.createEvent(request, requestMember); + + // then + then(periodicEventRepository).should().save(any(PeriodicEvent.class)); + then(eventRepository).should().save(any(Event.class)); + then(joinEventRepository).should().save(any(JoinEvent.class)); + } + + @Test + @DisplayName("유효하지 않은 요청 시 InvalidEventCreationRequest 예외를 발생시킨다") + void shouldThrowExceptionWhenValidationFails() { + // given + EventInfoRequest request = createSingleEventRequest(); + + given(joinCrewRepository.validateEventCreation(any(), any(), any())) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> sut.createEvent(request, requestMember)) + .isInstanceOf(InvalidEventCreationRequest.class); + + then(eventRepository).should(never()).save(any(Event.class)); + then(joinEventRepository).should(never()).save(any(JoinEvent.class)); + then(periodicEventRepository).should(never()).save(any(PeriodicEvent.class)); + } + + @Test + @DisplayName("일정 생성 후 러닝캡틴이 자동으로 참가 처리된다") + void shouldAutoJoinRunningCaptainToEvent() { + // given + EventInfoRequest request = createSingleEventRequest(); + EventCreationValidationDto validation = new EventCreationValidationDto(crew, runningCaptain); + + given(joinCrewRepository.validateEventCreation(any(), any(), any())) + .willReturn(Optional.of(validation)); + + given(eventRepository.save(any(Event.class))) + .willReturn(savedEvent); + + // when + sut.createEvent(request, requestMember); + + // then + then(joinEventRepository).should().save(any(JoinEvent.class)); + } + } + + private Member createMember(String nickname) { + return Member.builder() + .username("test_user") + .nickname(nickname) + .gender(Gender.MALE) + .age(25) + .oauthId("oauth_id") + .oauthType(OAuthType.GOOGLE) + .profileImage("profile.jpg") + .build(); + } + + private Crew createCrew(String name) { + return Crew.builder() + .name(name) + .description("테스트 크루 설명") + .image("crew.jpg") + .build(); + } + + private Event createEvent() { + return Event.builder() + .title("테스트 일정") + .date(LocalDate.of(2025, 7, 18)) + .startTime(LocalTime.of(9, 0)) + .endTime(LocalTime.of(10, 0)) + .place("테스트 장소") + .crew(crew) + .member(runningCaptain) + .build(); + } + + private EventInfoRequest createSingleEventRequest() { + return new EventInfoRequest( + "테스트 일정", + LocalDate.of(2025, 7, 18), + RepeatCycle.NONE, + null, + LocalTime.of(9, 0), + LocalTime.of(10, 0), + "테스트 장소", + 1L + ); + } + + private EventInfoRequest createPeriodicEventRequest() { + return new EventInfoRequest( + "주기적 일정", + LocalDate.of(2025, 7, 18), + RepeatCycle.WEEKLY, + WeekDay.MONDAY, + LocalTime.of(9, 0), + LocalTime.of(10, 0), + "테스트 장소", + 1L + ); + } +} From 0986f4594ed1f9c06348cb4a80805c364ff34aa1 Mon Sep 17 00:00:00 2001 From: west_east Date: Fri, 18 Jul 2025 16:18:11 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[#19]=20chore:=20ddl=20=EC=98=B5=EC=85=98?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8c40a93..2af2f57 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,7 @@ spring: jpa: database: mysql hibernate: - ddl-auto: update + ddl-auto: create properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect