Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/#17 강의 등록 관련 API 개발 #20

Merged
merged 8 commits into from
Nov 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.awt.*;

import static inha.dayoook_e.common.code.status.SuccessStatus.APPLICATION_CREATE_OK;
import static inha.dayoook_e.common.code.status.SuccessStatus.SONG_CREATE_OK;
import static inha.dayoook_e.common.code.status.SuccessStatus.*;


/**
Expand All @@ -44,4 +40,31 @@ public BaseResponse<ApplicationResponse> apply(@AuthenticationPrincipal User use
@RequestBody ApplyRequest applyRequest) {
return BaseResponse.of(APPLICATION_CREATE_OK, applicationService.apply(user, applyRequest));
}

/**
* 강의 신청 승인 api
* @param user 로그인 한 튜터
* @param applicationId 승인할 신청 ID
* @return 강의 신청 승인 결과
*/
@PostMapping("/{applicationId}/approve")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PreAuthorize("hasAuthority('�tutor:create')")이런식으로 어노테이션 추가하면 튜터, 관리자만 호출할 수 있는 api로 지정할 수 있어요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

! 확인했습니다. 감사합니다.

@Operation(summary = "강의 신청 승인 API", description = "튜터가 강의 신청을 승인합니다.")
public BaseResponse<ApplicationResponse> approveApplication(@AuthenticationPrincipal User user,
@PathVariable("applicationId") Integer applicationId) {
return BaseResponse.of(APPLICATION_APPROVE_OK, applicationService.approveApplication(user, applicationId));
}

/**
* 강의 신청 거절 api
* @param user 로그인 한 튜터
* @param applicationId 거절할 신청 ID
* @return 강의 신청 거절 결과
*/
@PostMapping("/{applicationId}/deny")
@Operation(summary = "강의 신청 거절 API", description = "튜터가 강의 신청을 거절합니다.")
public BaseResponse<ApplicationResponse> rejectApplication(@AuthenticationPrincipal User user,
@PathVariable("applicationId") Integer applicationId) {
return BaseResponse.of(APPLICATION_REJECT_OK, applicationService.rejectApplication(user, applicationId));
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package inha.dayoook_e.application.api.controller.dto.response;

import inha.dayoook_e.application.domain.enums.Status;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.validation.constraints.NotNull;

public record ApplicationResponse (
@NotNull
@Schema(description = "신청 id", example = "1")
Integer id
Integer id,

@NotNull
@Schema(description = "신청 상태", example = "APPLYING")
@Enumerated(EnumType.STRING)
Status status
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import inha.dayoook_e.application.api.controller.dto.request.ApplyRequest;
import inha.dayoook_e.application.api.controller.dto.response.ApplicationResponse;
import inha.dayoook_e.application.domain.Application;
import inha.dayoook_e.user.domain.User;

public interface ApplicationService {
ApplicationResponse apply(User user, ApplyRequest applyRequest);
ApplicationResponse approveApplication(User tutor, Integer applicationId);
ApplicationResponse rejectApplication(User user, Integer applicationId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
import inha.dayoook_e.application.api.controller.dto.response.ApplicationResponse;
import inha.dayoook_e.application.api.mapper.ApplicationMapper;
import inha.dayoook_e.application.domain.enums.Status;
import inha.dayoook_e.common.BaseEntity;
import inha.dayoook_e.common.exceptions.BaseException;
import inha.dayoook_e.application.domain.Application;
import inha.dayoook_e.course.api.controller.dto.request.CreateCourseRequest;
import inha.dayoook_e.course.api.controller.dto.response.CourseResponse;
import inha.dayoook_e.course.api.mapper.CourseMapper;
import inha.dayoook_e.course.api.service.CourseService;
import inha.dayoook_e.course.domain.Course;
import inha.dayoook_e.mapping.domain.Day;
import inha.dayoook_e.mapping.domain.TimeSlot;
import inha.dayoook_e.mapping.domain.repository.DayJpaRepository;
import inha.dayoook_e.mapping.domain.repository.TimeSlotJpaRepository;
import inha.dayoook_e.song.domain.TuteeSongProgress;
import inha.dayoook_e.tutor.api.mapper.TutorScheduleMapper;
import inha.dayoook_e.tutor.domain.TutorSchedule;
import inha.dayoook_e.application.domain.repository.ApplicationJpaRepository;
Expand Down Expand Up @@ -46,6 +49,8 @@ public class ApplicationServiceImpl implements ApplicationService{
private final UserJpaRepository userJpaRepository;
private final ApplicationMapper applicationMapper;
private final TutorScheduleMapper tutorScheduleMapper;
private final CourseService courseService;
private final CourseMapper courseMapper;

/**
* 강의 신청
Expand All @@ -69,7 +74,7 @@ public ApplicationResponse apply(User tutee, ApplyRequest applyRequest) {

// 4. 시간대 조회
TimeSlot timeSlot = timeSlotJpaRepository.findById(applyRequest.timeSlotId())
.orElseThrow(() -> new BaseException(NOF_FIND_TIMESLOT));
.orElseThrow(() -> new BaseException(NOT_FIND_TIMESLOT));

// 5. 튜터 스케줄 조회, 없으면 생성
TutorScheduleId scheduleId = new TutorScheduleId(applyRequest.tutorId(), applyRequest.dayId(), applyRequest.timeSlotId());
Expand All @@ -80,16 +85,86 @@ public ApplicationResponse apply(User tutee, ApplyRequest applyRequest) {
});


// 6-1. 스케쥴이 available 하다면 신청 생성
// 6-1. 스케줄이 available 하다면 신청 생성
if (tutorSchedule.getIsAvailable()) {
Application application = applicationMapper.toApplication(tutee, tutor, day, timeSlot,
LocalDateTime.now(), Status.APPLYING, applyRequest.message());
Application savedApplication = applicationJpaRepository.save(application);
return applicationMapper.applicationToApplicationResponse(savedApplication);
}
// 6-2. 스케쥴이 unavailable 하다면 오류 반환
// 6-2. 스케줄이 unavailable 하다면 오류 반환
else {
throw new BaseException(SCHEDULE_ALREADY_BOOKED);
}
}

/**
* 강의 신청 승인
*
* @param tutor 로그인 한 튜터
* @param applicationId 승인할 신청 id
* @return 신청 승인 결과
*/
@Override
public ApplicationResponse approveApplication(User tutor, Integer applicationId) {
// 1. tutor의 권한이 tutor인지 확인
if (!tutor.getRole().equals(Role.TUTOR))
throw new BaseException(INVALID_ROLE);

// 2. application 조회
Application application = applicationJpaRepository.findById(applicationId)
.orElseThrow(() -> new BaseException(APPLICATION_NOT_FOUND));


// 2. 튜터 스케줄 조회, 없으면 오류 반환
TutorScheduleId scheduleId = new TutorScheduleId(application.getTutor().getId(), application.getDay().getId(), application.getTimeSlot().getId());
TutorSchedule tutorSchedule = tutorScheduleJpaRepository.findById(scheduleId)
.orElseThrow(() -> new BaseException(SCHEDULE_NOT_FOUND));

// 3. 스케줄이 available 하다면 신청 승인
if (tutorSchedule.getIsAvailable()) {

// 3-1. 스케쥴 isAvailable 변경
tutorSchedule.makeUnavailable();

// 3-2. application status 변경
application.changeStatus(Status.APPROVED);

// 3-2. Course 생성
CreateCourseRequest createCourseRequest = courseMapper.toCreateCourseRequest(application);
CourseResponse courseResponse = courseService.createCourse(createCourseRequest);
log.info("Course 생성 성공, Course ID : {}", courseResponse.id());

// 3-3. Application response 반환
return applicationMapper.applicationToApplicationResponse(application);
}
// 4. 스케줄이 unavailable 하다면 오류 반환
else {
throw new BaseException(SCHEDULE_ALREADY_BOOKED);
}
}

/**
* 강의 신청 거절
*
* @param tutor 로그인 한 튜터
* @param applicationId 거절할 신청 id
* @return 신청 거절 결과
*/
@Override
public ApplicationResponse rejectApplication(User tutor, Integer applicationId) {
// 1. tutor의 권한이 tutor인지 확인
if (!tutor.getRole().equals(Role.TUTOR))
throw new BaseException(INVALID_ROLE);

// 2. application 조회
Application application = applicationJpaRepository.findById(applicationId)
.orElseThrow(() -> new BaseException(APPLICATION_NOT_FOUND));

// 3. 신청 거절 (거절에는 이유가 필요 없음)
application.changeStatus(Status.REJECTED);

// 4. application Reponse반환
return applicationMapper.applicationToApplicationResponse(application);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public class Application {
@JoinColumn(name = "time_slot_id", nullable = false)
private TimeSlot timeSlot;


public void changeStatus(Status status) {
this.status = status;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ public enum ErrorStatus implements BaseErrorCode {
INVALID_LANGUAGE_ID(HttpStatus.BAD_REQUEST, "LANGUAGE4000", "유효하지 않은 언어 ID입니다."),
COUNTRY_NOT_FOUND(HttpStatus.NOT_FOUND, "COUNTRY4000", "국가를 찾을 수 없습니다."),
NOT_FIND_DAY(HttpStatus.NOT_FOUND, "DAY4000", "요일을 찾을 수 없습니다."),
NOF_FIND_TIMESLOT(HttpStatus.NOT_FOUND, "TIMESLOT4000", "시간대를 찾을 수 없습니다."),
NOT_FIND_TIMESLOT(HttpStatus.NOT_FOUND, "TIMESLOT4000", "시간대를 찾을 수 없습니다."),

INVALID_ROLE(HttpStatus.BAD_REQUEST, "ROLE4000", "잘못된 유저 권한입니다."),


SONG_NOT_FOUND(HttpStatus.NOT_FOUND, "SONG4000", "동요를 찾을 수 없습니다."),
SONG_ALREADY_COMPLETE(HttpStatus.BAD_REQUEST, "SONG4001", "이미 완료한 동요입니다."),

SCHEDULE_ALREADY_BOOKED(HttpStatus.BAD_REQUEST, "SCHEDULE4000", "이미 예약된 스케줄입니다."),
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "SCHEDULE4000", "스케줄을 찾을 수 없습니다."),
SCHEDULE_ALREADY_BOOKED(HttpStatus.BAD_REQUEST, "SCHEDULE4001", "이미 예약된 스케줄입니다."),

APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "APPLICATION4000", "신청 정보를 찾을 수 없습니다."),

STORYBOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "STORYBOOK4000", "동화를 찾을 수 없습니다."),
STORYBOOK_ALREADY_COMPLETE(HttpStatus.BAD_REQUEST, "STORYBOOK4001", "이미 완료한 동화입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ public enum SuccessStatus implements BaseCode {
SONG_SEARCH_OK(HttpStatus.OK, "SONG2002", "동요 상세 조회 성공"),
SONG_TOGGLE_LIKE_OK(HttpStatus.OK, "SONG2003", "동요 좋아요 토글 성공"),
SONG_COMPLETE_OK(HttpStatus.OK, "SONG2004", "동요 완료 성공"),

APPLICATION_CREATE_OK(HttpStatus.CREATED, "APPLICATION2000", "신청 생성 성공"),
APPLICATION_APPROVE_OK(HttpStatus.CREATED, "APPLICATION2001", "신청 승인 성공"),
APPLICATION_REJECT_OK(HttpStatus.CREATED, "APPLICATION2002", "신청 거절 성공"),


STORYBOOK_CREATE_OK(HttpStatus.CREATED, "STORYBOOK2000", "동화 생성 성공"),
STORYBOOK_TOGGLE_LIKE_OK(HttpStatus.OK, "STORYBOOK2001", "동화 좋아요 토글 성공"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package inha.dayoook_e.course.api.controller.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record CreateCourseRequest(
@NotNull
@Schema(description = "튜터 id", example = "1")
Integer tutorId,

@NotNull
@Schema(description = "튜티 id", example = "1")
Integer tuteeId,

@NotNull
@Schema(description = "신청 요일 id", example = "1")
Integer dayId,

@NotNull
@Schema(description = "신청 시간대 id", example = "1")
Integer timeSlotId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package inha.dayoook_e.course.api.controller.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record CourseResponse (
@NotNull
@Schema(description = "강의 id", example = "1")
Integer id
) {}
34 changes: 34 additions & 0 deletions src/main/java/inha/dayoook_e/course/api/mapper/CourseMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package inha.dayoook_e.course.api.mapper;

import inha.dayoook_e.application.api.controller.dto.response.ApplicationResponse;
import inha.dayoook_e.application.domain.Application;
import inha.dayoook_e.course.api.controller.dto.request.CreateCourseRequest;
import inha.dayoook_e.course.api.controller.dto.response.CourseResponse;
import inha.dayoook_e.course.domain.Course;
import org.mapstruct.Mapper;
import org.mapstruct.NullValuePropertyMappingStrategy;

/**
* CourseMapper은 강의와 관련된 데이터 변환 기능을 제공.
*/
@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface CourseMapper {

/**
* Application을 CreateCourseRequest로 변환해주는 매퍼
* @param application 등록할 신청 정보
* @return createCourseRequest
*/
default CreateCourseRequest toCreateCourseRequest(Application application) {
return new CreateCourseRequest(application.getTutor().getId(), application.getTutee().getId(),
application.getDay().getId(), application.getTimeSlot().getId());
};

/**
* Course를 CourseResponse로 변환하는 매퍼
*
* @param savedCourse 강의
* @return CourseResponse
*/
CourseResponse toCourseResponse(Course savedCourse);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package inha.dayoook_e.course.api.service;

import inha.dayoook_e.course.api.controller.dto.request.CreateCourseRequest;
import inha.dayoook_e.course.api.controller.dto.response.CourseResponse;

public interface CourseService {
CourseResponse createCourse(CreateCourseRequest createCourseRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package inha.dayoook_e.course.api.service;

import inha.dayoook_e.common.exceptions.BaseException;
import inha.dayoook_e.course.api.controller.dto.request.CreateCourseRequest;
import inha.dayoook_e.course.api.controller.dto.response.CourseResponse;
import inha.dayoook_e.course.api.mapper.CourseMapper;
import inha.dayoook_e.course.domain.Course;
import inha.dayoook_e.course.domain.repository.CourseJpaRepository;
import inha.dayoook_e.mapping.domain.Day;
import inha.dayoook_e.mapping.domain.TimeSlot;
import inha.dayoook_e.mapping.domain.repository.DayJpaRepository;
import inha.dayoook_e.mapping.domain.repository.TimeSlotJpaRepository;
import inha.dayoook_e.user.domain.User;
import inha.dayoook_e.user.domain.repository.UserJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

import static inha.dayoook_e.common.BaseEntity.State.ACTIVE;
import static inha.dayoook_e.common.code.status.ErrorStatus.*;

/**
* CourseServiceImpl은 강의 관련 비즈니스 로직을 처리하는 서비스 클래스.
*/
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class CourseServiceImpl implements CourseService{

private final CourseMapper courseMapper;
private final CourseJpaRepository courseJpaReposiotry;
private final UserJpaRepository userJpaRepository;
private final TimeSlotJpaRepository timeSlotJpaRepository;
private final DayJpaRepository dayJpaRepository;

@Override
public CourseResponse createCourse(CreateCourseRequest createCourseRequest) {
// 1. 튜터,튜티,요일,시간대 조회
User tutor = userJpaRepository.findByIdAndState(createCourseRequest.tutorId(), ACTIVE)
.orElseThrow(() -> new BaseException(NOT_FIND_USER));
User tutee = userJpaRepository.findByIdAndState(createCourseRequest.tuteeId(), ACTIVE)
.orElseThrow(() -> new BaseException(NOT_FIND_USER));
Day day = dayJpaRepository.findById(createCourseRequest.dayId())
.orElseThrow(() -> new BaseException(NOT_FIND_DAY));
TimeSlot timeSlot = timeSlotJpaRepository.findById(createCourseRequest.timeSlotId())
.orElseThrow(() -> new BaseException(NOT_FIND_TIMESLOT));


// 2. 강의 생성
// Mapper 동작 오류로 인해 직접 생성
Course course = Course.builder()
.tutor(tutor)
.tutee(tutee)
.day(day)
.timeSlot(timeSlot)
.createdAt(LocalDateTime.now())
.build();

// 3. 강의 등록
Course savedCourse = courseJpaReposiotry.save(course);
return courseMapper.toCourseResponse(savedCourse);
}
}
Loading
Loading