From bfcb4ce40b861a09423fc01d2c3c05047832670e Mon Sep 17 00:00:00 2001 From: minhyung Date: Thu, 26 Jun 2025 22:24:36 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20#57=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=20SoftDeletableEntity=20=EC=83=81=EC=86=8D=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/demo/pteam/global/entity/BaseEntity.java | 6 ++++++ .../pteam/global/entity/SoftDeletableEntity.java | 9 ++++++++- .../repository/entity/ScheduleEntity.java | 15 +++++---------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/demo/pteam/global/entity/BaseEntity.java b/src/main/java/com/demo/pteam/global/entity/BaseEntity.java index e898c418..4dcd7032 100644 --- a/src/main/java/com/demo/pteam/global/entity/BaseEntity.java +++ b/src/main/java/com/demo/pteam/global/entity/BaseEntity.java @@ -5,11 +5,13 @@ import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; import lombok.Getter; +import lombok.NoArgsConstructor; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Getter +@NoArgsConstructor @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { @@ -21,4 +23,8 @@ public abstract class BaseEntity { @LastModifiedDate private LocalDateTime updatedAt; + protected BaseEntity(LocalDateTime createdAt) { + this.createdAt = createdAt; + this.updatedAt = createdAt; + } } diff --git a/src/main/java/com/demo/pteam/global/entity/SoftDeletableEntity.java b/src/main/java/com/demo/pteam/global/entity/SoftDeletableEntity.java index 8cc13b2a..a4aee275 100644 --- a/src/main/java/com/demo/pteam/global/entity/SoftDeletableEntity.java +++ b/src/main/java/com/demo/pteam/global/entity/SoftDeletableEntity.java @@ -1,12 +1,19 @@ package com.demo.pteam.global.entity; +import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor @MappedSuperclass public abstract class SoftDeletableEntity extends BaseEntity { + protected SoftDeletableEntity(LocalDateTime createdAt) { + super(createdAt); + } - protected LocalDateTime deletedAt; + @Column(insertable = false) + private LocalDateTime deletedAt; } diff --git a/src/main/java/com/demo/pteam/schedule/repository/entity/ScheduleEntity.java b/src/main/java/com/demo/pteam/schedule/repository/entity/ScheduleEntity.java index ce85226f..3463dbaf 100644 --- a/src/main/java/com/demo/pteam/schedule/repository/entity/ScheduleEntity.java +++ b/src/main/java/com/demo/pteam/schedule/repository/entity/ScheduleEntity.java @@ -1,6 +1,7 @@ package com.demo.pteam.schedule.repository.entity; import com.demo.pteam.authentication.repository.entity.AccountEntity; +import com.demo.pteam.global.entity.SoftDeletableEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -13,7 +14,7 @@ @Table(name = "schedule") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class ScheduleEntity { +public class ScheduleEntity extends SoftDeletableEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -29,16 +30,10 @@ public class ScheduleEntity { private LocalDateTime startTime; private LocalDateTime endTime; - // TODO: 임시 구현 - @Column(updatable = false) - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - @Column(insertable = false) - private LocalDateTime deletedAt; - @Builder - public ScheduleEntity(AccountEntity userAccountEntity, AccountEntity trainerAccountEntity, LocalDateTime startTime, LocalDateTime endTime) { + public ScheduleEntity(LocalDateTime createdAt, AccountEntity userAccountEntity, AccountEntity trainerAccountEntity, + LocalDateTime startTime, LocalDateTime endTime) { + super(createdAt); this.userAccountEntity = userAccountEntity; this.trainerAccountEntity = trainerAccountEntity; this.startTime = startTime; From 4730cedb53d03b5bdaa19c7b41697b1201e614e3 Mon Sep 17 00:00:00 2001 From: minhyung Date: Thu, 26 Jun 2025 22:25:47 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20#57=20repository=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원 일정 조회 구현 - 트레이너 일정 조회 구현 --- .../demo/pteam/schedule/domain/Schedule.java | 25 +++++++ .../repository/ScheduleJPARepository.java | 7 ++ .../ScheduleJPARepositoryCustom.java | 11 ++++ .../ScheduleJPARepositoryCustomImpl.java | 65 +++++++++++++++++++ .../repository/ScheduleRepository.java | 7 ++ .../repository/ScheduleRepositoryImpl.java | 24 +++++++ .../schedule/repository/dto/ScheduleDto.java | 28 ++++++++ 7 files changed, 167 insertions(+) create mode 100644 src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepository.java create mode 100644 src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustom.java create mode 100644 src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustomImpl.java create mode 100644 src/main/java/com/demo/pteam/schedule/repository/ScheduleRepositoryImpl.java create mode 100644 src/main/java/com/demo/pteam/schedule/repository/dto/ScheduleDto.java diff --git a/src/main/java/com/demo/pteam/schedule/domain/Schedule.java b/src/main/java/com/demo/pteam/schedule/domain/Schedule.java index 57ea91ea..faca96c4 100644 --- a/src/main/java/com/demo/pteam/schedule/domain/Schedule.java +++ b/src/main/java/com/demo/pteam/schedule/domain/Schedule.java @@ -1,4 +1,29 @@ package com.demo.pteam.schedule.domain; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter public class Schedule { + private final Long id; + private final Long userId; + private final Long trainerId; + private final String nickname; + private final LocalDate date; + private final LocalDateTime startTime; + private final LocalDateTime endTime; + + @Builder + private Schedule(Long id, Long userId, Long trainerId, String nickname, LocalDateTime startTime, LocalDateTime endTime) { + this.id = id; + this.userId = userId; + this.trainerId = trainerId; + this.nickname = nickname; + this.date = startTime.toLocalDate(); + this.startTime = startTime; + this.endTime = endTime; + } } diff --git a/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepository.java b/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepository.java new file mode 100644 index 00000000..a7dd6522 --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepository.java @@ -0,0 +1,7 @@ +package com.demo.pteam.schedule.repository; + +import com.demo.pteam.schedule.repository.entity.ScheduleEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ScheduleJPARepository extends JpaRepository, ScheduleJPARepositoryCustom { +} diff --git a/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustom.java b/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustom.java new file mode 100644 index 00000000..92c24214 --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustom.java @@ -0,0 +1,11 @@ +package com.demo.pteam.schedule.repository; + +import com.demo.pteam.schedule.repository.dto.ScheduleDto; + +import java.time.LocalDateTime; +import java.util.List; + +public interface ScheduleJPARepositoryCustom { + List findByUserIdWithinPeriod(Long userId, LocalDateTime start, LocalDateTime end); + List findByTrainerIdWithinPeriod(Long trainerId, LocalDateTime start, LocalDateTime end); +} diff --git a/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustomImpl.java b/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustomImpl.java new file mode 100644 index 00000000..f9d543f7 --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/repository/ScheduleJPARepositoryCustomImpl.java @@ -0,0 +1,65 @@ +package com.demo.pteam.schedule.repository; + +import com.demo.pteam.schedule.repository.dto.ScheduleDto; +import com.querydsl.core.Tuple; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.demo.pteam.authentication.repository.entity.QAccountEntity.accountEntity; +import static com.demo.pteam.schedule.repository.entity.QScheduleEntity.scheduleEntity; + +@Repository +@RequiredArgsConstructor +public class ScheduleJPARepositoryCustomImpl implements ScheduleJPARepositoryCustom { + private final JPAQueryFactory queryFactory; + + @Override + public List findByUserIdWithinPeriod(Long userId, LocalDateTime start, LocalDateTime end) { + return findByAccountRoleWithinPeriod(userId, start, end, scheduleEntity.userAccountEntity.id); + } + + @Override + public List findByTrainerIdWithinPeriod(Long trainerId, LocalDateTime start, LocalDateTime end) { + return findByAccountRoleWithinPeriod(trainerId, start, end, scheduleEntity.trainerAccountEntity.id); + } + + private List findByAccountRoleWithinPeriod(Long accountId, LocalDateTime start, LocalDateTime end, NumberPath joinTarget) { + List result = queryFactory + .select( + scheduleEntity.id, + accountEntity.id, + scheduleEntity.userAccountEntity.id, + scheduleEntity.trainerAccountEntity.id, + accountEntity.nickname, + scheduleEntity.startTime, + scheduleEntity.endTime) + .from(accountEntity) + .join(scheduleEntity).on( + joinTarget.eq(accountEntity.id), + scheduleEntity.startTime.goe(start), + scheduleEntity.endTime.lt(end)) + .where(accountEntity.id.eq(accountId), + accountEntity.deletedAt.isNull()) + .fetch(); + return createScheduleDto(result); + } + + private List createScheduleDto(List result) { + return result.stream() + .map(tuple -> ScheduleDto.builder() + .id(tuple.get(scheduleEntity.id)) + .accountId(tuple.get(accountEntity.id)) + .userId(tuple.get(scheduleEntity.userAccountEntity.id)) + .trainerId(tuple.get(scheduleEntity.trainerAccountEntity.id)) + .nickname(tuple.get(accountEntity.nickname)) + .startTime(tuple.get(scheduleEntity.startTime)) + .endTime(tuple.get(scheduleEntity.endTime)) + .build() + ).toList(); + } +} diff --git a/src/main/java/com/demo/pteam/schedule/repository/ScheduleRepository.java b/src/main/java/com/demo/pteam/schedule/repository/ScheduleRepository.java index b25d3d50..a9ad1074 100644 --- a/src/main/java/com/demo/pteam/schedule/repository/ScheduleRepository.java +++ b/src/main/java/com/demo/pteam/schedule/repository/ScheduleRepository.java @@ -1,4 +1,11 @@ package com.demo.pteam.schedule.repository; +import com.demo.pteam.schedule.repository.dto.ScheduleDto; + +import java.time.LocalDateTime; +import java.util.List; + public interface ScheduleRepository { + List findByUserIdWithinPeriod(Long userId, LocalDateTime start, LocalDateTime end); + List findByTrainerIdWithinPeriod(Long trainerId, LocalDateTime start, LocalDateTime end); } diff --git a/src/main/java/com/demo/pteam/schedule/repository/ScheduleRepositoryImpl.java b/src/main/java/com/demo/pteam/schedule/repository/ScheduleRepositoryImpl.java new file mode 100644 index 00000000..215b081d --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/repository/ScheduleRepositoryImpl.java @@ -0,0 +1,24 @@ +package com.demo.pteam.schedule.repository; + +import com.demo.pteam.schedule.repository.dto.ScheduleDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ScheduleRepositoryImpl implements ScheduleRepository { + private final ScheduleJPARepository scheduleJPARepository; + + @Override + public List findByUserIdWithinPeriod(Long userId, LocalDateTime start, LocalDateTime end) { + return scheduleJPARepository.findByUserIdWithinPeriod(userId, start, end); + } + + @Override + public List findByTrainerIdWithinPeriod(Long trainerId, LocalDateTime start, LocalDateTime end) { + return scheduleJPARepository.findByTrainerIdWithinPeriod(trainerId, start, end); + } +} diff --git a/src/main/java/com/demo/pteam/schedule/repository/dto/ScheduleDto.java b/src/main/java/com/demo/pteam/schedule/repository/dto/ScheduleDto.java new file mode 100644 index 00000000..1d89ed69 --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/repository/dto/ScheduleDto.java @@ -0,0 +1,28 @@ +package com.demo.pteam.schedule.repository.dto; + +import com.demo.pteam.schedule.domain.Schedule; +import lombok.Builder; + +import java.time.LocalDateTime; + +@Builder +public record ScheduleDto( + Long id, + Long accountId, + Long userId, + Long trainerId, + String nickname, + LocalDateTime startTime, + LocalDateTime endTime +) { + public Schedule toSchedule() { + return Schedule.builder() + .id(id) + .userId(userId) + .trainerId(trainerId) + .nickname(nickname) + .startTime(startTime) + .endTime(endTime) + .build(); + } +} From 67230f79e9b9133684d6e39a5d3bc6f036c5769f Mon Sep 17 00:00:00 2001 From: minhyung Date: Thu, 26 Jun 2025 22:28:14 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20#57=20ObjectMapper=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/demo/pteam/global/config/ApplicationConfig.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java b/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java index 93e06297..5ce9409d 100644 --- a/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java +++ b/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java @@ -1,6 +1,7 @@ package com.demo.pteam.global.config; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,6 +9,8 @@ public class ApplicationConfig { @Bean public ObjectMapper objectMapper() { - return new ObjectMapper(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + return objectMapper; } } From c7a13b3b0ce560f929452272dcf49c347fa4fd22 Mon Sep 17 00:00:00 2001 From: minhyung Date: Thu, 26 Jun 2025 22:28:29 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20#57=20EnableMethodSecurity=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/demo/pteam/PteamApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/demo/pteam/PteamApplication.java b/src/main/java/com/demo/pteam/PteamApplication.java index 1a8e4d96..7a1f4eaa 100644 --- a/src/main/java/com/demo/pteam/PteamApplication.java +++ b/src/main/java/com/demo/pteam/PteamApplication.java @@ -4,10 +4,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @EnableJpaAuditing @EnableScheduling +@EnableMethodSecurity @SpringBootApplication public class PteamApplication { From b07671c255de5a7fa46b29b5bbcc2961eb383bb9 Mon Sep 17 00:00:00 2001 From: minhyung Date: Thu, 26 Jun 2025 22:29:41 +0900 Subject: [PATCH 05/14] =?UTF-8?q?chore:=20#57=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/demo/pteam/security/config/SecurityConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/demo/pteam/security/config/SecurityConfig.java b/src/main/java/com/demo/pteam/security/config/SecurityConfig.java index 3c888db1..390ae8a6 100644 --- a/src/main/java/com/demo/pteam/security/config/SecurityConfig.java +++ b/src/main/java/com/demo/pteam/security/config/SecurityConfig.java @@ -9,7 +9,6 @@ import com.demo.pteam.security.authentication.handler.JwtAuthenticationSuccessHandler; import com.demo.pteam.security.configurer.ApiLoginConfigurer; import com.demo.pteam.security.configurer.JwtAuthenticationConfigurer; -import com.demo.pteam.security.jwt.JwtProvider; import com.demo.pteam.security.login.handler.LoginAuthenticationFailureHandler; import com.demo.pteam.security.login.handler.LoginAuthenticationSuccessHandler; import com.demo.pteam.security.login.LoginAuthenticationProvider; From 8c4d2f29d21d6783b80f75dc49d3389f61f88e82 Mon Sep 17 00:00:00 2001 From: minhyung Date: Thu, 26 Jun 2025 22:30:42 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20#57=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20service=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/ReadScheduleRequest.java | 13 ++++++ .../controller/dto/RequestSchedule.java | 4 -- .../controller/dto/ResponseSchedule.java | 4 -- .../controller/dto/ScheduleResponse.java | 34 ++++++++++++++++ .../pteam/schedule/domain/ScheduleDate.java | 40 +++++++++++++++++++ .../schedule/service/ScheduleService.java | 38 ++++++++++++++++++ 6 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java delete mode 100644 src/main/java/com/demo/pteam/schedule/controller/dto/RequestSchedule.java delete mode 100644 src/main/java/com/demo/pteam/schedule/controller/dto/ResponseSchedule.java create mode 100644 src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java create mode 100644 src/main/java/com/demo/pteam/schedule/domain/ScheduleDate.java diff --git a/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java b/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java new file mode 100644 index 00000000..fee310cb --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java @@ -0,0 +1,13 @@ +package com.demo.pteam.schedule.controller.dto; + +import com.demo.pteam.schedule.domain.ScheduleDate; + +public record ReadScheduleRequest( + String roleType, + Integer year, + Integer month +) { + public ScheduleDate toScheduleDate() { + return ScheduleDate.of(year, month); + } +} diff --git a/src/main/java/com/demo/pteam/schedule/controller/dto/RequestSchedule.java b/src/main/java/com/demo/pteam/schedule/controller/dto/RequestSchedule.java deleted file mode 100644 index f7d3ab43..00000000 --- a/src/main/java/com/demo/pteam/schedule/controller/dto/RequestSchedule.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.demo.pteam.schedule.controller.dto; - -public class RequestSchedule { -} diff --git a/src/main/java/com/demo/pteam/schedule/controller/dto/ResponseSchedule.java b/src/main/java/com/demo/pteam/schedule/controller/dto/ResponseSchedule.java deleted file mode 100644 index 70b0b351..00000000 --- a/src/main/java/com/demo/pteam/schedule/controller/dto/ResponseSchedule.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.demo.pteam.schedule.controller.dto; - -public class ResponseSchedule { -} diff --git a/src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java b/src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java new file mode 100644 index 00000000..f608b3d3 --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java @@ -0,0 +1,34 @@ +package com.demo.pteam.schedule.controller.dto; + +import com.demo.pteam.schedule.domain.Schedule; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +public record ScheduleResponse( + Long scheduleId, + Long userId, + Long trainerId, + String nickname, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate date, + @JsonFormat(pattern = "HH:mm") + LocalDateTime startTime, + @JsonFormat(pattern = "HH:mm") + LocalDateTime endTime +) { + public static ScheduleResponse from(Schedule schedule) { + return ScheduleResponse.builder() + .scheduleId(schedule.getId()) + .userId(schedule.getUserId()) + .trainerId(schedule.getTrainerId()) + .nickname(schedule.getNickname()) + .date(schedule.getDate()) + .startTime(schedule.getStartTime()) + .endTime(schedule.getEndTime()) + .build(); + } +} diff --git a/src/main/java/com/demo/pteam/schedule/domain/ScheduleDate.java b/src/main/java/com/demo/pteam/schedule/domain/ScheduleDate.java new file mode 100644 index 00000000..2bc22fa8 --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/domain/ScheduleDate.java @@ -0,0 +1,40 @@ +package com.demo.pteam.schedule.domain; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; + +public class ScheduleDate { + private static final int MIN_YEAR = 1900; + private static final int MAX_YEAR = LocalDate.now().getYear() + 10; + + private final int year; + private final int month; + + private ScheduleDate(int year, int month) { + if (year < MIN_YEAR || year > MAX_YEAR) { + throw new IllegalArgumentException( + "Year must be between " + MIN_YEAR + " and " + MAX_YEAR + ". Invalid year: " + year); + } + if (month < 1 || month > 12) { + throw new IllegalArgumentException("Invalid month: " + month); + } + this.year = year; + this.month = month; + } + + public static ScheduleDate of(Integer year, Integer month) { + LocalDate localDate = LocalDate.now(); + int safeYear = Objects.isNull(year) ? localDate.getYear() : year; + int safeMonth = Objects.isNull(month) ? localDate.getMonthValue() : month; + return new ScheduleDate(safeYear, safeMonth); + } + + public LocalDateTime getStartOfMonth() { + return LocalDate.of(year, month, 1).atStartOfDay(); + } + + public LocalDateTime getEndOfMonth() { + return getStartOfMonth().plusMonths(1); + } +} diff --git a/src/main/java/com/demo/pteam/schedule/service/ScheduleService.java b/src/main/java/com/demo/pteam/schedule/service/ScheduleService.java index 70de0b3c..cee39256 100644 --- a/src/main/java/com/demo/pteam/schedule/service/ScheduleService.java +++ b/src/main/java/com/demo/pteam/schedule/service/ScheduleService.java @@ -1,4 +1,42 @@ package com.demo.pteam.schedule.service; +import com.demo.pteam.schedule.controller.dto.ReadScheduleRequest; +import com.demo.pteam.schedule.controller.dto.ScheduleResponse; +import com.demo.pteam.schedule.domain.ScheduleDate; +import com.demo.pteam.schedule.repository.ScheduleRepository; +import com.demo.pteam.schedule.repository.dto.ScheduleDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor public class ScheduleService { + private final ScheduleRepository scheduleRepository; + + @PreAuthorize("authentication.principal.id == #userId") + @Transactional(readOnly = true) + public List findAllSchedules(Long userId, ReadScheduleRequest request) { + ScheduleDate date = request.toScheduleDate(); + List scheduleDtoList = switch (request.roleType()) { + case "user" -> findAllUserSchedules(userId, date); + case "trainer" -> findAllTrainerSchedules(userId, date); + default -> throw new IllegalStateException("Unexpected value: " + request.roleType()); + }; + return scheduleDtoList.stream() + .filter(dto -> dto.accountId().equals(userId)) + .map(dto -> ScheduleResponse.from(dto.toSchedule())) + .toList(); + } + + public List findAllUserSchedules(Long userId, ScheduleDate date) { + return scheduleRepository.findByUserIdWithinPeriod(userId, date.getStartOfMonth(), date.getEndOfMonth()); + } + + public List findAllTrainerSchedules(Long trainerId, ScheduleDate date) { + return scheduleRepository.findByTrainerIdWithinPeriod(trainerId, date.getStartOfMonth(), date.getEndOfMonth()); + } } From 279323e60cdefef128bbc3fa851fde30d8775cfa Mon Sep 17 00:00:00 2001 From: minhyung Date: Thu, 26 Jun 2025 22:30:53 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20#57=20controller=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ScheduleController.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java b/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java index 2d1e8ba1..fb681237 100644 --- a/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java +++ b/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java @@ -1,4 +1,27 @@ package com.demo.pteam.schedule.controller; +import com.demo.pteam.global.response.ApiResponse; +import com.demo.pteam.schedule.controller.dto.ReadScheduleRequest; +import com.demo.pteam.schedule.controller.dto.ScheduleResponse; +import com.demo.pteam.schedule.service.ScheduleService; +import com.demo.pteam.security.principal.UserPrincipal; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/schedules") +@RequiredArgsConstructor public class ScheduleController { + private final ScheduleService scheduleService; + + @GetMapping + public ResponseEntity>> readSchedules(@AuthenticationPrincipal UserPrincipal principal, + @ModelAttribute ReadScheduleRequest requestParams) { + List schedules = scheduleService.findAllSchedules(principal.id(), requestParams); + return ResponseEntity.ok(ApiResponse.success("회원정보 조회 성공", schedules)); + } } From 94bb1fba3621586707b79836f712c3bbed9bb7ee Mon Sep 17 00:00:00 2001 From: minhyung Date: Fri, 27 Jun 2025 20:37:06 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20#57=20response=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - json 필드 변경 --- .../schedule/controller/ScheduleController.java | 1 + .../schedule/controller/dto/ScheduleResponse.java | 11 ++++------- .../com/demo/pteam/schedule/domain/Schedule.java | 14 +------------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java b/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java index fb681237..404b287b 100644 --- a/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java +++ b/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java @@ -21,6 +21,7 @@ public class ScheduleController { @GetMapping public ResponseEntity>> readSchedules(@AuthenticationPrincipal UserPrincipal principal, @ModelAttribute ReadScheduleRequest requestParams) { + // TODO: 파라미터 검증 추가 List schedules = scheduleService.findAllSchedules(principal.id(), requestParams); return ResponseEntity.ok(ApiResponse.success("회원정보 조회 성공", schedules)); } diff --git a/src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java b/src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java index f608b3d3..14615524 100644 --- a/src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java +++ b/src/main/java/com/demo/pteam/schedule/controller/dto/ScheduleResponse.java @@ -9,24 +9,21 @@ @Builder public record ScheduleResponse( - Long scheduleId, + Long id, Long userId, Long trainerId, String nickname, - @JsonFormat(pattern = "yyyy-MM-dd") - LocalDate date, - @JsonFormat(pattern = "HH:mm") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") LocalDateTime startTime, - @JsonFormat(pattern = "HH:mm") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") LocalDateTime endTime ) { public static ScheduleResponse from(Schedule schedule) { return ScheduleResponse.builder() - .scheduleId(schedule.getId()) + .id(schedule.getId()) .userId(schedule.getUserId()) .trainerId(schedule.getTrainerId()) .nickname(schedule.getNickname()) - .date(schedule.getDate()) .startTime(schedule.getStartTime()) .endTime(schedule.getEndTime()) .build(); diff --git a/src/main/java/com/demo/pteam/schedule/domain/Schedule.java b/src/main/java/com/demo/pteam/schedule/domain/Schedule.java index faca96c4..07d97883 100644 --- a/src/main/java/com/demo/pteam/schedule/domain/Schedule.java +++ b/src/main/java/com/demo/pteam/schedule/domain/Schedule.java @@ -3,27 +3,15 @@ import lombok.Builder; import lombok.Getter; -import java.time.LocalDate; import java.time.LocalDateTime; @Getter +@Builder public class Schedule { private final Long id; private final Long userId; private final Long trainerId; private final String nickname; - private final LocalDate date; private final LocalDateTime startTime; private final LocalDateTime endTime; - - @Builder - private Schedule(Long id, Long userId, Long trainerId, String nickname, LocalDateTime startTime, LocalDateTime endTime) { - this.id = id; - this.userId = userId; - this.trainerId = trainerId; - this.nickname = nickname; - this.date = startTime.toLocalDate(); - this.startTime = startTime; - this.endTime = endTime; - } } From 9390208841bc2cb28461a978f741d1c102319280 Mon Sep 17 00:00:00 2001 From: minhyung Date: Fri, 27 Jun 2025 20:47:15 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20#57=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pteam/schedule/controller/ScheduleController.java | 4 ++-- .../schedule/controller/dto/ReadScheduleRequest.java | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java b/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java index 404b287b..8e39a743 100644 --- a/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java +++ b/src/main/java/com/demo/pteam/schedule/controller/ScheduleController.java @@ -5,6 +5,7 @@ import com.demo.pteam.schedule.controller.dto.ScheduleResponse; import com.demo.pteam.schedule.service.ScheduleService; import com.demo.pteam.security.principal.UserPrincipal; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -20,8 +21,7 @@ public class ScheduleController { @GetMapping public ResponseEntity>> readSchedules(@AuthenticationPrincipal UserPrincipal principal, - @ModelAttribute ReadScheduleRequest requestParams) { - // TODO: 파라미터 검증 추가 + @ModelAttribute @Valid ReadScheduleRequest requestParams) { List schedules = scheduleService.findAllSchedules(principal.id(), requestParams); return ResponseEntity.ok(ApiResponse.success("회원정보 조회 성공", schedules)); } diff --git a/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java b/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java index fee310cb..8b639ad5 100644 --- a/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java +++ b/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java @@ -1,10 +1,18 @@ package com.demo.pteam.schedule.controller.dto; import com.demo.pteam.schedule.domain.ScheduleDate; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import org.hibernate.validator.constraints.Range; public record ReadScheduleRequest( + @NotBlank(message = "roleType을 입력해주세요.") + @Pattern(regexp = "^(user|trainer)$", message = "user 또는 trainer만 입력 가능합니다.") String roleType, + @Positive(message = "년도 입력 형식이 올바르지 않습니다.") Integer year, + @Range(min = 1, max = 12, message = "1 이상 12 이하인 값으로 입력해주세요.") Integer month ) { public ScheduleDate toScheduleDate() { From acf7f681387075fee4156175866dac5e039c26d4 Mon Sep 17 00:00:00 2001 From: minhyung Date: Mon, 30 Jun 2025 22:52:51 +0900 Subject: [PATCH 10/14] =?UTF-8?q?test:=20#57=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 해당 클래스를 상속한 후 getHttpHeaders()를 호출하면 인증된 토큰을 반환 --- .../com/demo/pteam/AuthenticatedTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/test/java/com/demo/pteam/AuthenticatedTest.java diff --git a/src/test/java/com/demo/pteam/AuthenticatedTest.java b/src/test/java/com/demo/pteam/AuthenticatedTest.java new file mode 100644 index 00000000..e8be0a79 --- /dev/null +++ b/src/test/java/com/demo/pteam/AuthenticatedTest.java @@ -0,0 +1,116 @@ +package com.demo.pteam; + +import com.demo.pteam.security.jwt.InMemoryTokenStore; +import com.demo.pteam.security.jwt.JwtProvider; +import com.demo.pteam.security.jwt.TokenData; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import javax.crypto.SecretKey; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.Map; + +import static org.mockito.Mockito.*; + +@SpringBootTest +@Transactional +public abstract class AuthenticatedTest { + protected static final String PREFIX = "Bearer "; + protected static final String ACCESS_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwicm9sZSI6IlJPTEVfVVNFUiIsInZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3NDcyMDc2OTksImV4cCI6MTc0NzIxMTI5OX0.M2IjaJJCfnV7Eheijp72nKtVlL1pgkghNr-Zc1i6Oks"; + protected static final String REFRESH_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNzQ3MjA3NzAwLCJleHAiOjE3NDc4MTI1MDB9.NXnMg9s2NpZFIgf6EmRdGpC9qyXDOWGbRklF39vOLBg"; + protected static final String AUTHORIZATION_HEADER = PREFIX + ACCESS_TOKEN; + protected static final String REFRESH_TOKEN_HEADER = PREFIX + REFRESH_TOKEN; + + private static final Date NOW = createDate(2025, 5, 14, 16, 28, 20); + + private static final long REFRESH_TOKEN_EXPIRATION = 1747812500000L; + + @Value("${jwt.secret}") + protected String jwtSecretKey; + + @Autowired + private WebApplicationContext context; + protected MockMvc mockMvc; + + @Autowired + private FilterChainProxy filterChainProxy; + + @MockitoSpyBean + private JwtProvider jwtProvider; + + @MockitoSpyBean + private InMemoryTokenStore spyTokenStore; + + @BeforeEach + public void authenticationSetUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .addFilters(filterChainProxy) + .build(); + + // refreshToken 저장 + TokenData tokenData = new TokenData(REFRESH_TOKEN, REFRESH_TOKEN_EXPIRATION); + TokenData spyTokenData = spy(tokenData); + Map store = (Map) ReflectionTestUtils.getField(spyTokenStore, "store"); + store.clear(); + store.put(1L, spyTokenData); + doReturn(NOW.getTime() > REFRESH_TOKEN_EXPIRATION).when(spyTokenData).isExpired(); + + doAnswer(invocation -> { + String token = invocation.getArgument(0); + return jwtDecode(token, jwtSecretKey, NOW); + }).when(jwtProvider).parseClaims(anyString()); + } + + public static HttpHeaders getHttpHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", AUTHORIZATION_HEADER); + headers.set("Refresh-Token", REFRESH_TOKEN_HEADER); + return headers; + } + + public static HttpHeaders getHttpHeaders(String authHeader, String refreshHeader) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", authHeader); + headers.set("Refresh-Token", refreshHeader); + return headers; + } + + private static Date createDate(int year, int month, int dayOfMonth, int hour, int minute, int second) { + return Date.from( + LocalDateTime.of(year, month, dayOfMonth, hour, minute, second) + .atZone(ZoneId.of("Asia/Seoul")) + .toInstant() + ); + } + + private static Claims jwtDecode(String token, String secretKey, Date now) throws JwtException { + return Jwts.parser() + .clock(() -> now) + .verifyWith(createSigningKey(secretKey)) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private static SecretKey createSigningKey(String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} From a6b732cad5524e70ddc89aaa24d2e025bc274402 Mon Sep 17 00:00:00 2001 From: minhyung Date: Mon, 30 Jun 2025 22:53:25 +0900 Subject: [PATCH 11/14] =?UTF-8?q?test:=20#57=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/ScheduleIntegrationTest.java | 125 ++++++++++++++++++ .../resources/testdb/data/R__test_data.sql | 15 ++- 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java diff --git a/src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java b/src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java new file mode 100644 index 00000000..588e25fb --- /dev/null +++ b/src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java @@ -0,0 +1,125 @@ +package com.demo.pteam.schedule; + +import com.demo.pteam.AuthenticatedTest; +import com.demo.pteam.security.exception.AuthenticationErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ScheduleIntegrationTest extends AuthenticatedTest { + private static final String SCHEDULE_URL = "/api/schedules"; + + // year, month 생략시 현재 날짜 기준으로 조회 + @DisplayName("일정 조회 성공") + @CsvSource(value = { + "2025, 5", + "2025,", // month 생략 + ",5", // year 생략 + "," // month, year 생략 + }) + @ParameterizedTest + void readScheduleByUser(String year, String month) throws Exception { + // given + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("roleType", "user"); + params.add("year", year); + params.add("month", month); + + // when + ResultActions resultActions = mockMvc.perform( + get(SCHEDULE_URL).params(params).headers(getHttpHeaders()) + ); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("message").value("회원정보 조회 성공")) + .andExpect(jsonPath("data[*].userId", everyItem(equalTo(1)))) + .andExpect(jsonPath("data[*].startTime", everyItem(startsWith("2025-05")))); + } + + /* + // year, month 생략시 현재 날짜 기준으로 조회 + @DisplayName("일정 조회 성공") + @CsvSource(value = { + "2025, 5", + "2025,", // month 생략 + ",5", // year 생략 + "," // month, year 생략 + }) + @ParameterizedTest + void readScheduleByTrainer(String year, String month) throws Exception { + // given + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("roleType", "trainer"); + params.add("year", year); + params.add("month", month); + + // when + ResultActions resultActions = mockMvc.perform( + get(SCHEDULE_URL).params(params).headers(getHttpHeaders()) + ); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("message").value("회원정보 조회 성공")) + .andExpect(jsonPath("data[*].trainerId", everyItem(equalTo(3)))) + .andExpect(jsonPath("data[*].startTime", everyItem(startsWith("2025-05")))); + } + */ + + @DisplayName("로그인 x") + @Test + void notLogin() throws Exception { + // given + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("roleType", "user"); + params.add("year", "2025"); + params.add("month", "5"); + + // when + ResultActions resultActions = mockMvc.perform( + get(SCHEDULE_URL).params(params) + ); + + // then + AuthenticationErrorCode errorCode = AuthenticationErrorCode.NOT_AUTHENTICATED; + + resultActions.andExpect(status().isUnauthorized()) + .andExpect(jsonPath("code").value(errorCode.getCode())) + .andExpect(jsonPath("message").value(errorCode.getMessage())); + } + + @DisplayName("유효하지 않은 파라미터") + @CsvSource(value = { + ",2025,5", // roleType 생략 + "role,2025,5", // 유효하지 않는 roleType + "user,-2025,5", // 유효하지 않는 year + "user,2025,0", // 유효하지 않는 month + "user,2025,13" // 유효하지 않는 month + }) + @ParameterizedTest + void invalidParams(String roleType, String year, String month) throws Exception { + // given + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("roleType", roleType); + params.add("year", year); + params.add("month", month); + + // when + ResultActions resultActions = mockMvc.perform( + get(SCHEDULE_URL).params(params).headers(getHttpHeaders()) + ); + + // then + resultActions.andExpect(status().isBadRequest()); + } +} \ No newline at end of file diff --git a/src/test/resources/testdb/data/R__test_data.sql b/src/test/resources/testdb/data/R__test_data.sql index 30d37020..f76f4704 100644 --- a/src/test/resources/testdb/data/R__test_data.sql +++ b/src/test/resources/testdb/data/R__test_data.sql @@ -3,7 +3,9 @@ -- entity_type: 1(로컬), 2(소셜) insert ignore into accounts(id, `name`, nickname, role, entity_type) values (1, '회원1', '회원닉네임1', 1, 1), - (2, '회원2', '회원닉네임2', 1, 1); + (2, '회원2', '회원닉네임2', 1, 1), + (3, '트레이너1', '트레이너닉네임1', 1, 1), + (4, '트레이너2', '트레이너닉네임2', 1, 1); -- 로컬 계정 -- status: -1(삭제)/0(정지)/1(활성)/2(미인증) @@ -19,4 +21,13 @@ set @user_id2 = (select id from accounts where nickname = '회원닉네임2'); set @test_pwd = '{bcrypt}$2a$10$eXthWEeajRbGgRfvlfVBl.LlD6jDWoyAgyRSDa.FdRUTM4vfnYh86'; -- password = 1234567aA! insert ignore into local_accounts(id, username, `password`, email, `status`) values (@user_id1,'usertest1', @test_pwd, 'usertest1@gmail.com', 1), -- 활성 - (@user_id2, 'usertest2', @test_pwd, 'usertest2@gmail.com', 0); -- 정지 + (@user_id2, 'usertest2', @test_pwd, 'usertest2@gmail.com', 0), -- 정지 + (3,'trainertest1', @test_pwd, 'trainertest1@gmail.com', 1), -- 활성 + (4,'trainertest2', @test_pwd, 'trainertest2@gmail.com', 1); -- 활성 + + +-- 일정 +insert IGNORE into `schedule`(user_accounts_id, trainer_accounts_id, start_time, end_time) values + (1, 5, '2025-05-12 14:00:00', '2025-05-12 15:30:00'), + (1, 6, '2025-05-17 12:00:00', '2025-05-12 13:00:00'), + (1, 5, '2025-05-24 17:00:00', '2025-05-12 18:00:00'); \ No newline at end of file From 87592db2713f6e28b1e505ba0d2cef5709a1f31a Mon Sep 17 00:00:00 2001 From: minhyung Date: Tue, 1 Jul 2025 22:17:12 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20#57=20year=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EB=B2=94=EC=9C=84=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/ReadScheduleRequest.java | 4 +-- .../pteam/schedule/validator/YearRange.java | 31 +++++++++++++++++++ .../validator/YearRangeValidator.java | 27 ++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/demo/pteam/schedule/validator/YearRange.java create mode 100644 src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java diff --git a/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java b/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java index 8b639ad5..72e21f18 100644 --- a/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java +++ b/src/main/java/com/demo/pteam/schedule/controller/dto/ReadScheduleRequest.java @@ -1,16 +1,16 @@ package com.demo.pteam.schedule.controller.dto; import com.demo.pteam.schedule.domain.ScheduleDate; +import com.demo.pteam.schedule.validator.YearRange; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Positive; import org.hibernate.validator.constraints.Range; public record ReadScheduleRequest( @NotBlank(message = "roleType을 입력해주세요.") @Pattern(regexp = "^(user|trainer)$", message = "user 또는 trainer만 입력 가능합니다.") String roleType, - @Positive(message = "년도 입력 형식이 올바르지 않습니다.") + @YearRange Integer year, @Range(min = 1, max = 12, message = "1 이상 12 이하인 값으로 입력해주세요.") Integer month diff --git a/src/main/java/com/demo/pteam/schedule/validator/YearRange.java b/src/main/java/com/demo/pteam/schedule/validator/YearRange.java new file mode 100644 index 00000000..f39b5fdd --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/validator/YearRange.java @@ -0,0 +1,31 @@ +package com.demo.pteam.schedule.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 입력된 연도가 유효한 범위 내에 있는지 검증하는 어노테이션입니다. + * + *

이 어노테이션은 필드 및 메서드 파라미터에 적용되며, 연도가 다음 조건을 만족하는지 확인합니다: + *

    + *
  • 1900년 이상
  • + *
  • 현재 연도 기준으로 최대 10년 이하
  • + *
+ * + *

유효하지 않은 값이 입력되면 검증 오류가 발생합니다. + */ +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = YearRangeValidator.class) +public @interface YearRange { + String message() default "입력 형식이 올바르지 않습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java b/src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java new file mode 100644 index 00000000..251f5643 --- /dev/null +++ b/src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java @@ -0,0 +1,27 @@ +package com.demo.pteam.schedule.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class YearRangeValidator implements ConstraintValidator { + private static final int MIN_YEAR = 1900; + private static final int MAX_YEAR = LocalDateTime.now().getYear() + 10; + + @Override + public boolean isValid(Integer year, ConstraintValidatorContext context) { + if (Objects.isNull(year)) { + return true; // year이 null인 경우는 검증 대상 아님 + } + if (year < MIN_YEAR || year > MAX_YEAR) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate( + String.format("%d 이상 %d 이하의 값으로 입력해주세요.", MIN_YEAR, MAX_YEAR) + ).addConstraintViolation(); + return false; + } + return true; + } +} From d9d254e72f411fc10c704985ca199698344987b0 Mon Sep 17 00:00:00 2001 From: minhyung Date: Tue, 1 Jul 2025 22:24:28 +0900 Subject: [PATCH 13/14] =?UTF-8?q?test:=20#57=20test=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/ScheduleIntegrationTest.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java b/src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java index 588e25fb..b8f14123 100644 --- a/src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java +++ b/src/test/java/com/demo/pteam/schedule/ScheduleIntegrationTest.java @@ -71,7 +71,7 @@ void readScheduleByTrainer(String year, String month) throws Exception { // then resultActions.andExpect(status().isOk()) .andExpect(jsonPath("message").value("회원정보 조회 성공")) - .andExpect(jsonPath("data[*].trainerId", everyItem(equalTo(3)))) + .andExpect(jsonPath("data[*].trainerId", everyItem(equalTo(1)))) .andExpect(jsonPath("data[*].startTime", everyItem(startsWith("2025-05")))); } */ @@ -103,6 +103,8 @@ void notLogin() throws Exception { ",2025,5", // roleType 생략 "role,2025,5", // 유효하지 않는 roleType "user,-2025,5", // 유효하지 않는 year + "user,1899,5", // 유효하지 않는 year + "user,2036,5", // 유효하지 않는 year "user,2025,0", // 유효하지 않는 month "user,2025,13" // 유효하지 않는 month }) @@ -122,4 +124,32 @@ void invalidParams(String roleType, String year, String month) throws Exception // then resultActions.andExpect(status().isBadRequest()); } + + @DisplayName("유효한 파라미터") + @CsvSource(value = { + "user,1900,5", + "trainer,1900,5", + "user,2035,5", + "trainer,2035,5", + "user,2025,1", + "trainer,2025,1", + "user,2025,12", + "trainer,2025,12" + }) + @ParameterizedTest + void validParams(String roleType, String year, String month) throws Exception { + // given + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("roleType", roleType); + params.add("year", year); + params.add("month", month); + + // when + ResultActions resultActions = mockMvc.perform( + get(SCHEDULE_URL).params(params).headers(getHttpHeaders()) + ); + + // then + resultActions.andExpect(status().isOk()); + } } \ No newline at end of file From c9afb74df69bde35caeb26475b33be8614227834 Mon Sep 17 00:00:00 2001 From: minhyung Date: Tue, 1 Jul 2025 22:29:34 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20#57=20LocalDate=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/demo/pteam/schedule/validator/YearRangeValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java b/src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java index 251f5643..bb6bd384 100644 --- a/src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java +++ b/src/main/java/com/demo/pteam/schedule/validator/YearRangeValidator.java @@ -3,12 +3,12 @@ import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.Objects; public class YearRangeValidator implements ConstraintValidator { private static final int MIN_YEAR = 1900; - private static final int MAX_YEAR = LocalDateTime.now().getYear() + 10; + private static final int MAX_YEAR = LocalDate.now().getYear() + 10; @Override public boolean isValid(Integer year, ConstraintValidatorContext context) {