Skip to content

Commit ad21235

Browse files
authored
[TNT-193] 트레이너 달력 표시에 필요한 데이터 요청 API 구현 (#45)
1 parent e6264ce commit ad21235

File tree

14 files changed

+227
-23
lines changed

14 files changed

+227
-23
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ dependencies {
182182
testImplementation 'org.springframework.security:spring-security-test'
183183

184184
// Testcontainer
185+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
185186
testImplementation "org.testcontainers:testcontainers:1.20.4"
186187
testImplementation "org.testcontainers:junit-jupiter:1.20.4"
187188

src/main/java/com/tnt/application/pt/PtService.java

+22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static com.tnt.common.error.model.ErrorMessage.PT_TRAINER_TRAINEE_NOT_FOUND;
66

77
import java.time.LocalDate;
8+
import java.util.LinkedHashMap;
89
import java.util.List;
910
import java.util.stream.Collectors;
1011

@@ -25,6 +26,8 @@
2526
import com.tnt.dto.trainer.ConnectWithTrainerDto;
2627
import com.tnt.dto.trainer.request.ConnectWithTrainerRequest;
2728
import com.tnt.dto.trainer.response.ConnectWithTraineeResponse;
29+
import com.tnt.dto.trainer.response.GetCalendarPtLessonCountResponse;
30+
import com.tnt.dto.trainer.response.GetCalendarPtLessonCountResponse.CalendarPtLessonCount;
2831
import com.tnt.dto.trainer.response.GetPtLessonsOnDateResponse;
2932
import com.tnt.dto.trainer.response.GetPtLessonsOnDateResponse.Lesson;
3033
import com.tnt.infrastructure.mysql.repository.pt.PtLessonRepository;
@@ -105,6 +108,25 @@ public GetPtLessonsOnDateResponse getPtLessonsOnDate(Long memberId, LocalDate da
105108
return new GetPtLessonsOnDateResponse(ptLessons.size(), date, lessons);
106109
}
107110

111+
@Transactional(readOnly = true)
112+
public GetCalendarPtLessonCountResponse getCalendarPtLessonCount(Long memberId, Integer year, Integer month) {
113+
Trainer trainer = trainerService.getTrainerWithMemberId(memberId);
114+
115+
List<PtLesson> ptLessons = ptLessonSearchRepository.findAllByTraineeIdForCalendar(trainer.getId(), year, month);
116+
117+
List<CalendarPtLessonCount> counts = ptLessons.stream()
118+
.collect(Collectors.groupingBy(
119+
lesson -> lesson.getLessonStart().toLocalDate(),
120+
LinkedHashMap::new,
121+
Collectors.counting()
122+
))
123+
.entrySet().stream()
124+
.map(entry -> new CalendarPtLessonCount(entry.getKey(), entry.getValue().intValue()))
125+
.toList();
126+
127+
return new GetCalendarPtLessonCountResponse(counts);
128+
}
129+
108130
public boolean isPtTrainerTraineeExistWithTrainerId(Long trainerId) {
109131
return ptTrainerTraineeRepository.existsByTrainerIdAndDeletedAtIsNull(trainerId);
110132
}

src/main/java/com/tnt/application/trainer/TrainerService.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ public InvitationCodeResponse getInvitationCode(Long memberId) {
2828
}
2929

3030
public InvitationCodeVerifyResponse verifyInvitationCode(String invitationCode) {
31-
boolean isVerified = trainerRepository.findByInvitationCodeAndDeletedAtIsNull(invitationCode).isPresent();
31+
boolean isVerified = trainerRepository.existsByInvitationCodeAndDeletedAtIsNull(invitationCode);
32+
String trainerName = isVerified ? getTrainerWithInvitationCode(invitationCode).getMember().getName() : null;
3233

33-
return new InvitationCodeVerifyResponse(isVerified);
34+
return new InvitationCodeVerifyResponse(isVerified, trainerName);
3435
}
3536

3637
@Transactional
@@ -55,5 +56,4 @@ public Trainer getTrainerWithInvitationCode(String invitationCode) {
5556
return trainerSearchRepository.findByInvitationCode(invitationCode)
5657
.orElseThrow(() -> new NotFoundException(TRAINER_NOT_FOUND));
5758
}
58-
5959
}

src/main/java/com/tnt/domain/member/Member.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,9 @@ public void softDelete() {
164164
this.deletedAt = LocalDateTime.now();
165165
}
166166

167-
public String getAge() {
167+
public Integer getAge() {
168168
if (isNull(this.birthday)) {
169-
return "비공개";
169+
return null;
170170
}
171171

172172
LocalDate currentDate = LocalDate.now();
@@ -177,6 +177,6 @@ public String getAge() {
177177
age--;
178178
}
179179

180-
return String.valueOf(age);
180+
return age;
181181
}
182182
}

src/main/java/com/tnt/dto/trainer/response/ConnectWithTraineeResponse.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public record ConnectWithTraineeResponse(
1616
@Schema(description = "트레이니 프로필 이미지 URL", example = "https://images.tntapp.co.kr/3h4f.jpg", nullable = false)
1717
String traineeProfileImageUrl,
1818

19-
@Schema(description = "트레이니 나이", examples = {"20세", "비공개"}, nullable = false)
20-
String traineeAge,
19+
@Schema(description = "트레이니 나이", example = "25", nullable = true)
20+
Integer traineeAge,
2121

2222
@Schema(description = "트레이니 키", example = "178.6cm", nullable = false)
2323
Double height,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.tnt.dto.trainer.response;
2+
3+
import java.time.LocalDate;
4+
import java.util.List;
5+
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
8+
@Schema(description = "트레이너 - 달력 스케쥴 개수 표시 응답")
9+
public record GetCalendarPtLessonCountResponse(
10+
@Schema(description = "해당 월의 각 날짜의 PT 수업 개수", nullable = true)
11+
List<CalendarPtLessonCount> calendarPtLessonCounts
12+
) {
13+
14+
public record CalendarPtLessonCount(
15+
@Schema(description = "해당 날짜", example = "2025-01-29", nullable = false)
16+
LocalDate date,
17+
18+
@Schema(description = "해당 날짜의 총 PT 수업 개수", example = "5", nullable = false)
19+
Integer count
20+
) {
21+
22+
}
23+
}

src/main/java/com/tnt/dto/trainer/response/InvitationCodeVerifyResponse.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
@Schema(description = "트레이너 초대 코드 인증 응답")
66
public record InvitationCodeVerifyResponse(
77
@Schema(description = "초대 코드 인증 여부", example = "true", nullable = false)
8-
boolean isVerified
8+
Boolean isVerified,
9+
10+
@Schema(description = "트레이너 이름", example = "조만제", nullable = true)
11+
String trainerName
912
) {
1013

1114
}

src/main/java/com/tnt/infrastructure/mysql/repository/pt/PtLessonSearchRepository.java

+18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static com.tnt.domain.trainer.QTrainer.trainer;
88

99
import java.time.LocalDate;
10+
import java.time.LocalDateTime;
1011
import java.time.LocalTime;
1112
import java.util.List;
1213

@@ -38,4 +39,21 @@ public List<PtLesson> findAllByTrainerIdAndDate(Long trainerId, LocalDate date)
3839
.orderBy(ptLesson.lessonStart.asc())
3940
.fetch();
4041
}
42+
43+
public List<PtLesson> findAllByTraineeIdForCalendar(Long traineeId, Integer year, Integer month) {
44+
LocalDateTime startDate = LocalDateTime.of(year, month, 1, 0, 0);
45+
LocalDateTime endDate = startDate.plusMonths(1).minusNanos(1);
46+
47+
return jpaQueryFactory
48+
.selectFrom(ptLesson)
49+
.join(ptLesson.ptTrainerTrainee, ptTrainerTrainee).fetchJoin()
50+
.join(ptTrainerTrainee.trainer, trainer).fetchJoin()
51+
.where(
52+
trainer.id.eq(traineeId),
53+
ptLesson.lessonStart.between(startDate, endDate),
54+
ptLesson.deletedAt.isNull()
55+
)
56+
.orderBy(ptLesson.lessonStart.asc())
57+
.fetch();
58+
}
4159
}

src/main/java/com/tnt/infrastructure/mysql/repository/trainer/TrainerRepository.java

+2
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ public interface TrainerRepository extends JpaRepository<Trainer, Long> {
1111
Optional<Trainer> findByMemberIdAndDeletedAtIsNull(Long memberId);
1212

1313
Optional<Trainer> findByInvitationCodeAndDeletedAtIsNull(String invitationCode);
14+
15+
boolean existsByInvitationCodeAndDeletedAtIsNull(String invitationCode);
1416
}

src/main/java/com/tnt/presentation/trainer/TrainerController.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
import com.tnt.application.pt.PtService;
1717
import com.tnt.application.trainer.TrainerService;
1818
import com.tnt.dto.trainer.response.ConnectWithTraineeResponse;
19+
import com.tnt.dto.trainer.response.GetCalendarPtLessonCountResponse;
1920
import com.tnt.dto.trainer.response.GetPtLessonsOnDateResponse;
2021
import com.tnt.dto.trainer.response.InvitationCodeResponse;
2122
import com.tnt.dto.trainer.response.InvitationCodeVerifyResponse;
2223
import com.tnt.gateway.config.AuthMember;
2324

2425
import io.swagger.v3.oas.annotations.Operation;
26+
import io.swagger.v3.oas.annotations.Parameter;
2527
import io.swagger.v3.oas.annotations.tags.Tag;
28+
import jakarta.validation.constraints.Max;
29+
import jakarta.validation.constraints.Min;
2630
import lombok.RequiredArgsConstructor;
2731

2832
@Tag(name = "트레이너", description = "트레이너 관련 API")
@@ -59,15 +63,24 @@ public InvitationCodeResponse reissueInvitationCode(@AuthMember Long memberId) {
5963
@ResponseStatus(OK)
6064
@GetMapping("/first-connected-trainee")
6165
public ConnectWithTraineeResponse getFirstConnectedTrainee(@AuthMember Long memberId,
62-
@RequestParam("trainerId") Long trainerId, @RequestParam Long traineeId) {
66+
@RequestParam("trainerId") Long trainerId, @RequestParam("traineeId") Long traineeId) {
6367
return ptService.getFirstTrainerTraineeConnect(memberId, trainerId, traineeId);
6468
}
6569

6670
@Operation(summary = "특정 날짜의 PT 리스트 불러오기 API")
6771
@ResponseStatus(OK)
6872
@GetMapping("/lessons/{date}")
6973
public GetPtLessonsOnDateResponse getPtLessonsOnDate(@AuthMember Long memberId,
70-
@PathVariable("date") LocalDate date) {
74+
@Parameter(description = "날짜", example = "2025-01-03") @PathVariable("date") LocalDate date) {
7175
return ptService.getPtLessonsOnDate(memberId, date);
7276
}
77+
78+
@Operation(summary = "달력 스케쥴 개수 표시에 필요한 데이터 요청 API")
79+
@ResponseStatus(OK)
80+
@GetMapping("/lessons/calendar")
81+
public GetCalendarPtLessonCountResponse getCalendarPtLessonCount(@AuthMember Long memberId,
82+
@Parameter(description = "년도", example = "2021") @RequestParam("year") @Min(1900) @Max(2100) Integer year,
83+
@Parameter(description = "월", example = "3") @RequestParam("month") @Min(1) @Max(12) Integer month) {
84+
return ptService.getCalendarPtLessonCount(memberId, year, month);
85+
}
7386
}

src/test/java/com/tnt/application/member/SignUpServiceTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static org.assertj.core.api.Assertions.assertThat;
77
import static org.junit.jupiter.api.Assertions.assertThrows;
88
import static org.mockito.ArgumentMatchers.any;
9+
import static org.mockito.ArgumentMatchers.anyList;
910
import static org.mockito.ArgumentMatchers.anyString;
1011
import static org.mockito.BDDMockito.given;
1112
import static org.mockito.Mockito.doThrow;
@@ -89,7 +90,7 @@ void save_trainee_success() {
8990
given(memberService.saveMember(any(Member.class))).willReturn(traineeMember);
9091
given(traineeService.saveTrainee(any(Trainee.class))).willReturn(
9192
Trainee.builder().id(1L).member(traineeMember).height(180.0).weight(75.0).build());
92-
given(ptGoalService.saveAllPtGoals(any(List.class))).willReturn(Stream.of("목표1", "목표2")
93+
given(ptGoalService.saveAllPtGoals(anyList())).willReturn(Stream.of("목표1", "목표2")
9394
.map(content -> PtGoal.builder().traineeId(traineeMember.getId()).content(content).build())
9495
.toList());
9596

src/test/java/com/tnt/application/pt/PtServiceTest.java

+51
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.tnt.domain.trainer.Trainer;
2828
import com.tnt.dto.trainer.ConnectWithTrainerDto;
2929
import com.tnt.dto.trainer.request.ConnectWithTrainerRequest;
30+
import com.tnt.dto.trainer.response.GetCalendarPtLessonCountResponse;
3031
import com.tnt.dto.trainer.response.GetPtLessonsOnDateResponse;
3132
import com.tnt.fixture.MemberFixture;
3233
import com.tnt.fixture.PtLessonsFixture;
@@ -264,4 +265,54 @@ void get_pt_lessons_success() {
264265
// then
265266
assertThat(result).isEqualTo(ptLessons);
266267
}
268+
269+
@Test
270+
@DisplayName("특정 월의 캘린더 PT 레슨 수 조회 성공")
271+
void get_calendar_pt_lesson_count_success() {
272+
// given
273+
Member trainerMember = MemberFixture.getTrainerMember1WithId();
274+
Member traineeMember = MemberFixture.getTraineeMember1WithId();
275+
276+
Trainer trainer = TrainerFixture.getTrainer2(trainerMember);
277+
Trainee trainee = TraineeFixture.getTrainee2(traineeMember);
278+
279+
PtTrainerTrainee ptTrainerTrainee = PtTrainerTraineeFixture.getPtTrainerTrainee1(trainer, trainee);
280+
281+
int year = 2025;
282+
int month = 1;
283+
LocalDateTime date = LocalDate.of(year, month, 1).atTime(10, 0);
284+
285+
List<PtLesson> ptLessons = List.of(PtLesson.builder()
286+
.id(1L)
287+
.ptTrainerTrainee(ptTrainerTrainee)
288+
.lessonStart(date)
289+
.lessonEnd(date.plusHours(1))
290+
.build(),
291+
PtLesson.builder()
292+
.id(2L)
293+
.ptTrainerTrainee(ptTrainerTrainee)
294+
.lessonStart(date.plusHours(4))
295+
.lessonEnd(date.plusHours(5))
296+
.build(),
297+
PtLesson.builder()
298+
.id(3L)
299+
.ptTrainerTrainee(ptTrainerTrainee)
300+
.lessonStart(date.plusDays(1))
301+
.lessonEnd(date.plusDays(1).plusHours(1))
302+
.build());
303+
304+
given(trainerService.getTrainerWithMemberId(trainer.getId())).willReturn(trainer);
305+
given(ptLessonSearchRepository.findAllByTraineeIdForCalendar(trainer.getId(), year, month))
306+
.willReturn(ptLessons);
307+
308+
// when
309+
GetCalendarPtLessonCountResponse result = ptService.getCalendarPtLessonCount(trainer.getId(), year, month);
310+
311+
// then
312+
assertThat(result.calendarPtLessonCounts()).hasSize(2);
313+
assertThat(result.calendarPtLessonCounts().getFirst().date()).isEqualTo(LocalDate.of(year, month, 1));
314+
assertThat(result.calendarPtLessonCounts().getFirst().count()).isEqualTo(2);
315+
assertThat(result.calendarPtLessonCounts().getLast().date()).isEqualTo(LocalDate.of(year, month, 2));
316+
assertThat(result.calendarPtLessonCounts().getLast().count()).isEqualTo(1);
317+
}
267318
}

src/test/java/com/tnt/application/trainer/TrainerServiceTest.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ void verify_invitation_code_success() {
156156
.memberType(TRAINER)
157157
.build();
158158

159-
given(trainerRepository.findByInvitationCodeAndDeletedAtIsNull(code))
159+
given(trainerRepository.existsByInvitationCodeAndDeletedAtIsNull(code))
160+
.willReturn(true);
161+
given(trainerSearchRepository.findByInvitationCode(code))
160162
.willReturn(java.util.Optional.of(Trainer.builder().member(member).build()));
161163

162164
// when

0 commit comments

Comments
 (0)