[과제] 강의 사이트 서버 만들기

느려도 꾸준히·2023년 11월 14일
post-thumbnail

Spring Lv.3

스파르타 강의 사이트 서버 만들기

🚩 ! 요구사항 확인: 필수 구현 기능

  • 회원 가입 기능
    • 이메일, 비밀번호, 성별, 전화번호, 주소, 권한을 저장할 수 있습니다.
      • ADMIN, USER 권한이 있습니다.
      • 이메일올바른 이메일 형식을 지켜야 합니다.
      • 비밀번호최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9), 특수문자로 구성되어야 합니다.
    • 회원가입 성공을 확인할 수 있는 값을 반환합니다.
      • ex) HTTP Status Code, Error Message …
  • 로그인 기능
    • 회원은 이메일, 비밀번호를 입력하여 서버에 로그인을 요청할 수 있습니다.
    • 로그인 성공 시, 회원의 정보JWT를 활용하여 토큰을 발급하고,
      발급한 토큰을 Header에 추가한 후 로그인 성공을 확인할 수 있는 값과 함께 반환합니다.
      - ex) HTTP Status Code, Error Message …
  • 강사 등록 기능
    • 이름, 경력(년차), 회사, 전화번호, 소개를 저장할 수 있습니다.
      • 로그인을 통해 발급받은 JWT가 함께 요청됩니다.
      • ADMIN 권한을 가진 회원만 강사 등록이 가능합니다.
    • 등록된 강사의 정보를 반환 받아 확인할 수 있습니다.
  • 강의 등록 기능
    • 강의명, 가격, 소개, 카테고리, 강사, 등록일을 저장할 수 있습니다.
      • Spring, React, Node 카테고리가 있습니다.
      • 강사 한 명이 여러 개의 강의를 촬영할 수도 있습니다.
      • 로그인을 통해 발급받은 JWT가 함께 요청됩니다.
      • ADMIN 권한을 가진 회원만 강의 등록이 가능합니다.
    • 등록된 강의의 정보를 반환 받아 확인할 수 있습니다.
  • 선택한 강의 조회 기능
    • 선택한 강의의 정보를 조회할 수 있습니다.
      • 모든 사용자가 강의를 조회할 수 있습니다.
      • 강의를 촬영한 강사의 정보를 확인할 수 있습니다.
        • 강사의 정보에 전화번호는 제외 되어있습니다.
  • 카테고리별 강의 목록 조회 기능
    • 선택한 카테고리에 포함된 강의를 조회할 수 있습니다.
      • 모든 사용자가 강의를 조회할 수 있습니다.
      • 강사의 정보는 제외됩니다.
    • 조회된 강의 목록은 선택한 기준에 의해 정렬됩니다.
      • 강의명, 가격, 등록일 중 기준을 선택할 수 있습니다.
      • 내림차순, 오름차순을 선택할 수 있습니다.
  • 선택한 강의 댓글 기능
    • 선택한 강의에 댓글을 등록할 수 있습니다.
      • 로그인을 통해 발급받은 JWT가 함께 요청됩니다.
      • 회원만 댓글 등록이 가능합니다.
    • 댓글 등록 성공을 확인할 수 있는 값을 반환합니다.
      • ex) HTTP Status Code, Error Message …
    • 선택한 강의를 조회할 때 해당 강의에 등록된 댓글들도 함께 조회할 수 있습니다.
  • 선택한 강의의 선택한 댓글 수정 기능
    • 선택한 강의의 선택한 댓글을 수정할 수 있습니다.
      • 로그인을 통해 발급받은 JWT가 함께 요청됩니다.
      • 해당 댓글을 등록한 회원만 댓글 수정이 가능합니다.
    • 댓글 수정 성공을 확인할 수 있는 값을 반환합니다.
      • ex) HTTP Status Code, Error Message …
  • 선택한 강의의 선택한 댓글 삭제 기능
    • 선택한 강의의 선택한 댓글을 삭제할 수 있습니다.
      • 로그인을 통해 발급받은 JWT가 함께 요청됩니다.
      • 해당 댓글을 등록한 회원만 댓글 삭제가 가능합니다.
    • 댓글 삭제 성공을 확인할 수 있는 값을 반환합니다.
      • ex) HTTP Status Code, Error Message …
  • 선택한 강의 좋아요 기능
    • 선택한 강의에 좋아요를 등록할 수 있습니다.
      • 로그인을 통해 발급받은 JWT가 함께 요청됩니다.
      • 회원만 좋아요 등록이 가능합니다.
      • 이미 해당 강의에 좋아요를 한 상태라면 좋아요가 취소됩니다.
    • 좋아요 등록/취소 성공을 확인할 수 있는 값을 반환합니다.
      • ex) HTTP Status Code, Error Message …
    • 선택한 강의를 조회할 때 해당 강의의 좋아요 수를 함께 조회할 수 있습니다.
  • < Use Case Diagram >

  • < API 명세서 >

  • < ERD >

.
-> 이번 과제에서는 저번 3과제에서 이어서 강의와 강사간의 연관관계에 대해 한번 더 도전해보았다. 이번에는 조건에 써져있는 것처럼 선택한 강의를 조회할시 그 강의를 찍은 강사에 대한 정보가 나오도록 코드를 보완해 보았다.

먼저 강의 Entity에 강사를 단방향으로 연결해주고

@ManyToOne
@JoinColumn(name = "tutor_id") // 데이터베이스의 외래키 컬럼 지정
    private Tutor tutor;
    
     public Lecture(LectureRequestDto requestDto) {
        this.title = requestDto.getTitle();
        this.price = requestDto.getPrice();
        this.description = requestDto.getDescription();
        this.category = requestDto.getCategory();
        this.tutor = tutor; // Tutor 객체 설정

강의의 RequestDto에도 tutorId를 추가 해줬다.
그런 후에 LectureService 클래스 내의 registerLecture 메서드에서 Lecture 엔티티를 생성하고 저장하는 로직을 수정했다. 현재 Lecture 엔티티를 생성할 때 Tutor 객체가 연결되지 않은 상태로 저장되고 있기 때문에, Tutor 객체를 찾아 Lecture 객체에 연결한 후 저장하는 과정을 추가해야 했다.
그런데 강의 등록때는 말고 조회시에만 보일수는 없을까?

조회시에는 전화번호는 빼야하니까 LectureResponseDto 클래스는 강의 생성때를 위해 놔두고 강의 조회 기능에서 Map을 사용하여 응답을 생성하도록 만들었다.

@Service
@RequiredArgsConstructor
@Slf4j
public class LectureService {

    private final LectureRepository lectureRepository;
    private final TutorRepository tutorRepository;
    private final LectureLikeRepository lectureLikeRepository; // 좋아요 기능 때문에 추가

// 강의 등록
public LectureResponseDto registerLecture(LectureRequestDto requestDto) {
        // 로그 남기기
        log.info("Registering a new lecture with title: {}", requestDto.getTitle());

        // 중복 강의 체크
        if (lectureRepository.existsByTitle(requestDto.getTitle())) {
            // 중복 강의가 있는 경우 로그를 남기고 예외를 발생.
            log.warn("Lecture registration failed: Duplicate lecture title {}", requestDto.getTitle());
            throw new IllegalStateException("Lecture with title " + requestDto.getTitle() + " already exists");
        }

        // category 유효성 검사
        // Enum 값 검증
        if (!EnumSet.allOf(LectureEnum.class).contains(requestDto.getCategory())) {
            throw new IllegalArgumentException("Invalid category: " + requestDto.getCategory());
        }

        // Tutor 조회
        Tutor tutor = tutorRepository.findById(requestDto.getTutorId())
                .orElseThrow(() -> new EntityNotFoundException("Tutor not found"));

        // Lecture 엔티티 생성
        Lecture lecture = new Lecture();
        lecture.setTitle(requestDto.getTitle());
        lecture.setPrice(requestDto.getPrice());
        lecture.setDescription(requestDto.getDescription());
        lecture.setCategory(requestDto.getCategory());
        lecture.setTutor(tutor); // 여기에서 Tutor 설정

        // 강의 저장
        Lecture savedLecture;

        // 저장 시도
        try {
            savedLecture = lectureRepository.save(lecture);
        } catch (DataAccessException e) {
            // 데이터베이스 액세스 중 예외 발생 시 로그를 발생
            log.error("Lecture registration failed: Database access error", e);
            throw e; // 예외를 상위 계층으로 전파
        }

        // 저장 성공 로그
        log.info("Lecture registered successfully with id: {}", savedLecture.getLectureId());

        // LectureResponseDto 생성 및 반환
        return new LectureResponseDto(savedLecture);
    }
// 강의 조회
    public Map<String, Object> getLectureDetails(Long lectureId) {
        Lecture lecture = lectureRepository.findById(lectureId)
                .orElseThrow(() -> new EntityNotFoundException("Lecture not found"));

        // 좋아요 수를 가져옴
        Long likeCount = lectureLikeRepository.countByLecture(lecture);

        Map<String, Object> response = new HashMap<>();
        response.put("lectureId", lecture.getLectureId());
        response.put("title", lecture.getTitle());
        response.put("price", lecture.getPrice());
        response.put("description", lecture.getDescription());
        response.put("category", lecture.getCategory());

        Tutor tutor = lecture.getTutor();
        if (tutor != null) {
            Map<String, Object> tutorDetails = new HashMap<>();
            tutorDetails.put("tutorName", tutor.getTutorName());
            tutorDetails.put("experienceYears", tutor.getExperienceYears());
            tutorDetails.put("company", tutor.getCompany());
            // 전화번호는 제외
            response.put("tutor", tutorDetails);

            response.put("likeCount", likeCount); // 좋아요 수 추가
        }

        return response;
    }
// 강의 조회
    @GetMapping("/user/lecture/{lectureId}")
    public ResponseEntity<Map<String, Object>> getLecture(@PathVariable Long lectureId) {
        Map<String, Object> lectureDetails = lectureService.getLectureDetails(lectureId);
        return ResponseEntity.ok(lectureDetails);
    }

그에맞게 Controller 부분도 수정 해주었다.
이렇게 하니 등록시에는 강의 등록 정보만나오고 조회시에 강사 정보도 포함해서 나오게 되었다.
이걸 바탕으로 강의에 대한 댓글 기능과 좋아요 기능도 완성했다.
각 Entity에 강의과 유저를 Id로 찾아와 연결하고 수정과 삭제시에는 findById로 작성자가 맞는지 찾고 맞으면 실행시킨다.

0개의 댓글