Skip to content

Commit 5998fab

Browse files
authored
[TNT-268] feat: 수업 취소 기능 구현 (#72)
* [TNT-268] refactor: 불필요 field 삭제 * [TNT-268] feat: 수업 취소 기능 구현 - 수업 완료시 다른 수업 회차 증가 안되는 bug fix * [TNT-268] test: 테스트 코드 작성 * [TNT-268] test: 테스트 코드 추가 * [TNT-268] fix: 추가한 테스트 변경 * [TNT-268] test: 테스트 코드 추가
1 parent 71b5c77 commit 5998fab

File tree

9 files changed

+194
-57
lines changed

9 files changed

+194
-57
lines changed

src/main/java/com/tnt/application/member/SignUpService.java

-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import org.springframework.stereotype.Service;
1212
import org.springframework.transaction.annotation.Transactional;
1313

14-
import com.tnt.application.trainee.PtGoalService;
1514
import com.tnt.domain.member.Member;
1615
import com.tnt.domain.member.MemberType;
1716
import com.tnt.domain.trainee.PtGoal;
@@ -33,7 +32,6 @@ public class SignUpService {
3332

3433
private final SessionService sessionService;
3534
private final MemberService memberService;
36-
private final PtGoalService ptGoalService;
3735

3836
private final MemberRepository memberRepository;
3937
private final TrainerRepository trainerRepository;

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

+32-6
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
import com.tnt.infrastructure.mysql.repository.pt.PtLessonSearchRepository;
5656
import com.tnt.infrastructure.mysql.repository.pt.PtTrainerTraineeRepository;
5757
import com.tnt.infrastructure.mysql.repository.pt.PtTrainerTraineeSearchRepository;
58-
import com.tnt.infrastructure.mysql.repository.trainee.DietRepository;
5958

6059
import lombok.RequiredArgsConstructor;
6160

@@ -72,7 +71,6 @@ public class PtService {
7271
private final PtTrainerTraineeSearchRepository ptTrainerTraineeSearchRepository;
7372
private final PtLessonRepository ptLessonRepository;
7473
private final PtLessonSearchRepository ptLessonSearchRepository;
75-
private final DietRepository dietRepository;
7674

7775
@Transactional
7876
public ConnectWithTrainerDto connectWithTrainer(Long memberId, ConnectWithTrainerRequest request) {
@@ -99,8 +97,7 @@ public ConnectWithTrainerDto connectWithTrainer(Long memberId, ConnectWithTraine
9997
}
10098

10199
@Transactional(readOnly = true)
102-
public ConnectWithTraineeResponse getFirstTrainerTraineeConnect(Long memberId, Long trainerId,
103-
Long traineeId) {
100+
public ConnectWithTraineeResponse getFirstTrainerTraineeConnect(Long memberId, Long trainerId, Long traineeId) {
104101
validateIfNotConnected(trainerId, traineeId);
105102

106103
Trainer trainer = trainerService.getByMemberId(memberId);
@@ -213,7 +210,36 @@ public void completePtLesson(Long memberId, Long ptLessonId) {
213210
PtTrainerTrainee ptTrainerTrainee = ptLesson.getPtTrainerTrainee();
214211

215212
ptTrainerTrainee.completeLesson();
216-
ptLesson.completeLesson(ptTrainerTrainee.getFinishedPtCount());
213+
ptLesson.complete(ptTrainerTrainee.getFinishedPtCount());
214+
215+
List<PtLesson> lessonsNotCompleted =
216+
ptLessonRepository.findAllByPtTrainerTraineeAndIsCompletedIsFalseAndDeletedAtIsNull(ptTrainerTrainee);
217+
218+
lessonsNotCompleted.forEach(lesson -> {
219+
if (!lesson.getId().equals(ptLessonId)) {
220+
lesson.increaseSession();
221+
}
222+
});
223+
}
224+
225+
@Transactional
226+
public void cancelPtLesson(Long memberId, Long ptLessonId) {
227+
trainerService.validateTrainerRegistration(memberId);
228+
229+
PtLesson ptLesson = getPtLessonWithId(ptLessonId);
230+
PtTrainerTrainee ptTrainerTrainee = ptLesson.getPtTrainerTrainee();
231+
232+
List<PtLesson> lessonsNotCompleted =
233+
ptLessonRepository.findAllByPtTrainerTraineeAndIsCompletedIsFalseAndDeletedAtIsNull(ptTrainerTrainee);
234+
235+
lessonsNotCompleted.forEach(lesson -> {
236+
if (!lesson.getId().equals(ptLessonId) && lesson.getSession() > ptLesson.getSession()) {
237+
lesson.decreaseSession();
238+
}
239+
});
240+
241+
ptTrainerTrainee.cancelLesson();
242+
ptLesson.cancel(ptTrainerTrainee.getCurrentPtSession());
217243
}
218244

219245
@Transactional(readOnly = true)
@@ -249,7 +275,7 @@ public GetTraineeDailyRecordsResponse getDailyRecords(Long memberId, LocalDate d
249275
Trainee trainee = traineeService.getByMemberIdNoFetch(memberId);
250276

251277
// PT 정보 조회
252-
TraineeProjection.PtInfoDto ptResult = ptLessonSearchRepository.findAllByTraineeIdForDaily(trainee.getId(),
278+
TraineeProjection.PtInfoDto ptResult = ptLessonSearchRepository.findPtInfoByTraineeIdForDaily(trainee.getId(),
253279
date).orElse(new TraineeProjection.PtInfoDto(null, null, null, null, null));
254280

255281
// PT 정보 Mapping to PtInfo

src/main/java/com/tnt/domain/pt/PtLesson.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,22 @@ public PtLesson(Long id, PtTrainerTrainee ptTrainerTrainee, LocalDateTime lesson
7171
validateAndSetMemo(memo);
7272
}
7373

74-
public void completeLesson(Integer finishedSession) {
74+
public void complete(Integer finishedSession) {
7575
this.isCompleted = true;
7676
this.session = finishedSession;
7777
}
7878

79-
public void softDelete() {
80-
this.deletedAt = LocalDateTime.now();
79+
public void cancel(Integer finishedSession) {
80+
this.isCompleted = false;
81+
this.session = finishedSession;
82+
}
83+
84+
public void increaseSession() {
85+
this.session++;
86+
}
87+
88+
public void decreaseSession() {
89+
this.session--;
8190
}
8291

8392
private void validateAndSetMemo(String memo) {
@@ -91,4 +100,8 @@ private void validateAndSetMemo(String memo) {
91100

92101
this.memo = memo;
93102
}
103+
104+
public void softDelete() {
105+
this.deletedAt = LocalDateTime.now();
106+
}
94107
}

src/main/java/com/tnt/domain/pt/PtTrainerTrainee.java

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public void completeLesson() {
7575
this.finishedPtCount++;
7676
}
7777

78+
public void cancelLesson() {
79+
this.finishedPtCount--;
80+
}
81+
7882
public void softDelete() {
7983
this.deletedAt = LocalDateTime.now();
8084
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@
1010
public interface PtLessonRepository extends JpaRepository<PtLesson, Long> {
1111

1212
List<PtLesson> findAllByPtTrainerTraineeAndDeletedAtIsNull(PtTrainerTrainee ptTrainerTrainee);
13+
14+
List<PtLesson> findAllByPtTrainerTraineeAndIsCompletedIsFalseAndDeletedAtIsNull(PtTrainerTrainee ptTrainerTrainee);
1315
}

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

+32-32
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,38 @@ public List<PtLesson> findAllByTraineeIdForTraineeCalendar(Long traineeId, Local
8282
.fetch();
8383
}
8484

85+
public Optional<TraineeProjection.PtInfoDto> findPtInfoByTraineeIdForDaily(Long traineeId, LocalDate date) {
86+
return Optional.ofNullable(
87+
jpaQueryFactory
88+
.select(new QTraineeProjection_PtInfoDto(trainer.member.name, trainer.member.profileImageUrl,
89+
ptLesson.session, ptLesson.lessonStart, ptLesson.lessonEnd))
90+
.from(ptLesson)
91+
.join(ptLesson.ptTrainerTrainee, ptTrainerTrainee)
92+
.join(ptTrainerTrainee.trainer, trainer)
93+
.join(trainer.member, member)
94+
.where(ptTrainerTrainee.trainee.id.eq(traineeId),
95+
ptLesson.lessonStart.between(date.atStartOfDay(), date.atTime(LocalTime.MAX)),
96+
ptLesson.deletedAt.isNull(),
97+
ptTrainerTrainee.deletedAt.isNull(),
98+
trainer.deletedAt.isNull(),
99+
member.deletedAt.isNull())
100+
.fetchOne()
101+
);
102+
}
103+
104+
public Optional<PtLesson> findById(Long id) {
105+
return Optional.ofNullable(jpaQueryFactory
106+
.selectFrom(ptLesson)
107+
.join(ptLesson.ptTrainerTrainee, ptTrainerTrainee).fetchJoin()
108+
.where(
109+
ptLesson.id.eq(id),
110+
ptLesson.deletedAt.isNull(),
111+
ptTrainerTrainee.deletedAt.isNull()
112+
)
113+
.fetchOne()
114+
);
115+
}
116+
85117
public boolean existsByStartAndEnd(PtTrainerTrainee pt, LocalDateTime start, LocalDateTime end) {
86118
return jpaQueryFactory
87119
.selectOne()
@@ -113,36 +145,4 @@ public boolean existsByStart(PtTrainerTrainee pt, LocalDateTime start) {
113145
)
114146
.fetchFirst() != null;
115147
}
116-
117-
public Optional<TraineeProjection.PtInfoDto> findAllByTraineeIdForDaily(Long traineeId, LocalDate date) {
118-
return Optional.ofNullable(
119-
jpaQueryFactory
120-
.select(new QTraineeProjection_PtInfoDto(trainer.member.name, trainer.member.profileImageUrl,
121-
ptLesson.session, ptLesson.lessonStart, ptLesson.lessonEnd))
122-
.from(ptLesson)
123-
.join(ptLesson.ptTrainerTrainee, ptTrainerTrainee)
124-
.join(ptTrainerTrainee.trainer, trainer)
125-
.join(trainer.member, member)
126-
.where(ptTrainerTrainee.trainee.id.eq(traineeId),
127-
ptLesson.lessonStart.between(date.atStartOfDay(), date.atTime(LocalTime.MAX)),
128-
ptLesson.deletedAt.isNull(),
129-
ptTrainerTrainee.deletedAt.isNull(),
130-
trainer.deletedAt.isNull(),
131-
member.deletedAt.isNull())
132-
.fetchOne()
133-
);
134-
}
135-
136-
public Optional<PtLesson> findById(Long id) {
137-
return Optional.ofNullable(jpaQueryFactory
138-
.selectFrom(ptLesson)
139-
.join(ptLesson.ptTrainerTrainee, ptTrainerTrainee).fetchJoin()
140-
.where(
141-
ptLesson.id.eq(id),
142-
ptLesson.deletedAt.isNull(),
143-
ptTrainerTrainee.deletedAt.isNull()
144-
)
145-
.fetchOne()
146-
);
147-
}
148148
}

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

+8
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,12 @@ public void completePtLesson(@AuthMember Long memberId,
110110
@Parameter(description = "PT 수업 ID", example = "123456789") @PathVariable("ptLessonId") Long ptLessonId) {
111111
ptService.completePtLesson(memberId, ptLessonId);
112112
}
113+
114+
@Operation(summary = "PT 수업 취소 처리 API")
115+
@ResponseStatus(OK)
116+
@PutMapping("/lessons/{ptLessonId}/cancel")
117+
public void cancelPtLesson(@AuthMember Long memberId,
118+
@Parameter(description = "PT 수업 ID", example = "123456789") @PathVariable("ptLessonId") Long ptLessonId) {
119+
ptService.cancelPtLesson(memberId, ptLessonId);
120+
}
113121
}

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

+22
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.tnt.application.trainee.TraineeService;
2121
import com.tnt.application.trainer.TrainerService;
2222
import com.tnt.common.error.exception.ConflictException;
23+
import com.tnt.common.error.exception.NotFoundException;
2324
import com.tnt.domain.member.Member;
2425
import com.tnt.domain.pt.PtLesson;
2526
import com.tnt.domain.pt.PtTrainerTrainee;
@@ -152,6 +153,27 @@ void connectWithTrainer_already_connected_with_trainer_fail() {
152153
.isInstanceOf(ConflictException.class);
153154
}
154155

156+
@Test
157+
@DisplayName("트레이니와 연결 후 정보 조회 실패")
158+
void get_first_trainer_trainee_connect_fail() {
159+
// given
160+
Long traineeMemberId = 20L;
161+
162+
Member trainerMember = MemberFixture.getTrainerMember1();
163+
Member traineeMember = MemberFixture.getTraineeMember1();
164+
165+
Trainer trainer = TrainerFixture.getTrainerWithId1(trainerMember);
166+
Trainee trainee = TraineeFixture.getTrainee1WithId(traineeMember);
167+
168+
given(ptTrainerTraineeRepository.existsByTrainerIdAndTraineeIdAndDeletedAtIsNull(trainer.getId(),
169+
trainee.getId())).willReturn(false);
170+
171+
// when & then
172+
assertThatThrownBy(
173+
() -> ptService.getFirstTrainerTraineeConnect(traineeMemberId, trainer.getId(), trainee.getId()))
174+
.isInstanceOf(NotFoundException.class);
175+
}
176+
155177
@Test
156178
@DisplayName("트레이너 - 특정 날짜 수업 리스트 조회 성공")
157179
void getPtLessonsOnDate_success() {

src/test/java/com/tnt/presentation/trainer/TrainerControllerTest.java

+78-14
Original file line numberDiff line numberDiff line change
@@ -1095,33 +1095,97 @@ void complete_pt_lesson_success() throws Exception {
10951095

10961096
ptTrainerTraineeRepository.save(ptTrainerTrainee);
10971097

1098-
PtGoal ptGoal1 = PtGoal.builder()
1099-
.traineeId(trainee.getId())
1100-
.content("다이어트")
1098+
PtLesson ptLesson1 = PtLesson.builder()
1099+
.ptTrainerTrainee(ptTrainerTrainee)
1100+
.session(4)
1101+
.lessonStart(LocalDateTime.of(2025, 1, 1, 10, 0))
1102+
.lessonEnd(LocalDateTime.of(2025, 1, 1, 11, 0))
1103+
.memo("THIS IS MEMO")
11011104
.build();
11021105

1103-
PtGoal ptGoal2 = PtGoal.builder()
1104-
.traineeId(trainee.getId())
1105-
.content("체중 감량")
1106+
PtLesson ptLesson2 = PtLesson.builder()
1107+
.ptTrainerTrainee(ptTrainerTrainee)
1108+
.session(5)
1109+
.lessonStart(LocalDateTime.of(2025, 1, 2, 10, 0))
1110+
.lessonEnd(LocalDateTime.of(2025, 1, 2, 11, 0))
1111+
.memo("THIS IS MEMO2")
11061112
.build();
11071113

1108-
ptGoalRepository.saveAll(List.of(ptGoal1, ptGoal2));
1114+
ptLesson1 = ptLessonRepository.save(ptLesson1);
1115+
ptLessonRepository.save(ptLesson2);
11091116

1110-
PtLesson ptLesson = PtLesson.builder()
1117+
// when & then
1118+
assertThat(ptLesson1.getIsCompleted()).isFalse();
1119+
mockMvc.perform(put("/trainers/lessons/{ptLessonId}/complete", ptLesson1.getId()))
1120+
.andExpect(status().isOk());
1121+
//noinspection OptionalGetWithoutIsPresent
1122+
assertThat(ptLessonRepository.findById(ptLesson1.getId()).get().getIsCompleted()).isTrue();
1123+
}
1124+
1125+
@Test
1126+
@DisplayName("통합 테스트 - PT 수업 완료 취소 처리 성공")
1127+
void cancel_pt_lesson_success() throws Exception {
1128+
Member trainerMember = MemberFixture.getTrainerMember1();
1129+
Member traineeMember = MemberFixture.getTraineeMember1();
1130+
1131+
trainerMember = memberRepository.save(trainerMember);
1132+
traineeMember = memberRepository.save(traineeMember);
1133+
1134+
CustomUserDetails trainerUserDetails = new CustomUserDetails(trainerMember.getId(),
1135+
trainerMember.getId().toString(),
1136+
authoritiesMapper.mapAuthorities(List.of(new SimpleGrantedAuthority("ROLE_USER"))));
1137+
1138+
Authentication authentication = new UsernamePasswordAuthenticationToken(trainerUserDetails, null,
1139+
authoritiesMapper.mapAuthorities(trainerUserDetails.getAuthorities()));
1140+
1141+
SecurityContextHolder.getContext().setAuthentication(authentication);
1142+
1143+
Trainer trainer = Trainer.builder()
1144+
.member(trainerMember)
1145+
.build();
1146+
1147+
Trainee trainee = Trainee.builder()
1148+
.member(traineeMember)
1149+
.height(180.5)
1150+
.weight(78.4)
1151+
.cautionNote("주의사항")
1152+
.build();
1153+
1154+
trainer = trainerRepository.save(trainer);
1155+
trainee = traineeRepository.save(trainee);
1156+
1157+
PtTrainerTrainee ptTrainerTrainee = PtTrainerTraineeFixture.getPtTrainerTrainee1(trainer, trainee);
1158+
1159+
ptTrainerTraineeRepository.save(ptTrainerTrainee);
1160+
1161+
PtLesson ptLesson1 = PtLesson.builder()
11111162
.ptTrainerTrainee(ptTrainerTrainee)
1112-
.session(4)
1163+
.session(1)
11131164
.lessonStart(LocalDateTime.of(2025, 1, 1, 10, 0))
11141165
.lessonEnd(LocalDateTime.of(2025, 1, 1, 11, 0))
11151166
.memo("THIS IS MEMO")
11161167
.build();
11171168

1118-
ptLesson = ptLessonRepository.save(ptLesson);
1169+
PtLesson ptLesson2 = PtLesson.builder()
1170+
.ptTrainerTrainee(ptTrainerTrainee)
1171+
.session(2)
1172+
.lessonStart(LocalDateTime.of(2025, 1, 3, 10, 0))
1173+
.lessonEnd(LocalDateTime.of(2025, 1, 3, 11, 0))
1174+
.build();
1175+
1176+
PtLesson ptLesson3 = PtLesson.builder()
1177+
.ptTrainerTrainee(ptTrainerTrainee)
1178+
.session(3)
1179+
.lessonStart(LocalDateTime.of(2025, 1, 4, 10, 0))
1180+
.lessonEnd(LocalDateTime.of(2025, 1, 4, 11, 0))
1181+
.build();
1182+
1183+
ptLesson1.complete(1);
1184+
1185+
ptLessonRepository.saveAll(List.of(ptLesson1, ptLesson2, ptLesson3));
11191186

11201187
// when & then
1121-
assertThat(ptLesson.getIsCompleted()).isFalse();
1122-
mockMvc.perform(put("/trainers/lessons/{ptLessonId}/complete", ptLesson.getId()))
1188+
mockMvc.perform(put("/trainers/lessons/{ptLessonId}/cancel", ptLesson1.getId()))
11231189
.andExpect(status().isOk());
1124-
//noinspection OptionalGetWithoutIsPresent
1125-
assertThat(ptLessonRepository.findById(ptLesson.getId()).get().getIsCompleted()).isTrue();
11261190
}
11271191
}

0 commit comments

Comments
 (0)