Lecture, Problem API

SangYeon Min·2024년 6월 30일
0

PROJECT-HEARUS

목록 보기
7/12
post-thumbnail

Project Milestone

Hearus 프로젝트의 마일스톤을 위와 같이 설정하였다.
공개SW 개발자대회 1차 출품을 위해서 총 10주의 시간이 주어지는데, SPRINT 5까지의 시간동안 MVP 모델의 1차적인 개발을 마치고 배포를 통한 중간점검을 수행할 예정이다.


Postman Workspace

또한 비교적 무거운 SpringBoot를 사용하고 있기 때문에 FE와의 협업을 강화하기 위하여 Postman Workspace를 활용하였다.
Workspace 팀을 생성하고, BE 팀원을 초대한 이후 기존의 API들에 대한 exmaple들을 생성해주어 Mock Server를 생성하고 이를 FE 팀원들에게 전달하였다.


Figma 디자인 초안

테스트, 즉 Problem의 경우 위와 같으 화면에서 총 4개의 유형으로 생성할 수 있도록 디자인되었다.
또한 위는 실제로 문제를 사용자에게 제공하는 화면으로
해당 디자인에 기반하여 BE의 데이터를 구조화하고 제공할 예정이다.


API, DB 명세

/lecture 라우트의 경우 위와 같이 총 9개의 API로 구성되며
강의에 관한 데이터를 저장하는 Lecture Collection은 MongoDb에 위와 같이 구성된다.
또한 Problem들의 경우 각 Lecture 하위에 고유한 ID를 가지며 Array 형태로 저장되어
추후 Lang Chain을 통해 문제를 생성하고 저장할 때의 유연성을 향상시켰다.

LectureAPI

MongoDB Connection

또한 MongoDB의 경우에는 기존 Express.js로 개발할 때의 DB를 그대로 사용한다.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

Spring Boot에 위와 같이 spring-boot-starter-data-mongodb 라이브러리를 import하고

application-private.properties

# application-private.properties

# MongoDB
# `mongodb+srv://<유저이름>:<비밀번호>@<클러스터이름>.ergif.mongodb.net/<데이터베이스이름>?retryWrites=true&w=majority`
spring.data.mongodb.uri=mongodb+srv://<>:<>@<>...mongodb.net/<>?retryWrites=true&w=majority&appName=allkul

MongoDB 연결을 위한 URI를 설정하면 정상적으로 클러스터에 연결되는 모습을 볼 수 있다.

Model Configurance

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
// Document 어노테이션은 해당 클래스가 MongoDB에 저장될 문서임을 나타냄
@Document(collection = "lecture")
public class LectureModel {
    @Id
    @Column(unique = true)
    private String id;

    @Column(unique = true)
    private String name;

    private List<String> processedScript;

    private String scheduleElementId;

    private Date lectureDate;

    private Date createdAt;

    private List<Problem> problems;

    public void addProblem(Problem problem) {
        if (problems == null) {
            problems = new ArrayList<>();
        }
        problem.setId(UUID.randomUUID().toString());
        problems.add(problem);
    }
    
    public void updateProblem(String problemId, Problem newProblem) {
        if (problems != null) {
            for (int i = 0; i < problems.size(); i++) {
                Problem problem = problems.get(i);
                if (problem.getId().equals(problemId)) {
                    newProblem.setId(problemId);
                    problems.set(i, newProblem);
                    break;
                }
            }
        }
    }

    public void deleteProblem(String problemId) {
        if (problems != null) {
            problems.removeIf(problem -> problem.getId().equals(problemId));
        }
    }
}

LectureModel은 위와 같이 구현되었으며 Problem들과 관련된 별도의 메소드들을 두어 Problem List를 효율적으로 관리할 수 있게 하였다.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Problem {
    @Id
    @Column(unique = true)
    private String id;

    private String type;

    private String direction;

    private List<String> options;

    private String answer;
}

LectureModelProblem Class는 위와 같이 구현되었으며 Problem들과 관련된 별도의 메소드들을 두어 Problem List를 효율적으로 관리할 수 있게 하였다.

LectureRepository

public interface LectureRepository extends MongoRepository<LectureModel, String> {

    // READ
    List<LectureModel> findAll();
    LectureModel findFirstById(String id);
    LectureModel findFirstByName(String id);
    boolean existsById(String id);
    boolean existsByName(String name);

    // DELETE
    void deleteById(String id);
}

LectureDAO

@Slf4j
@Service
public class LectureDAOImpl implements LectureDAO {

    UserRepository userRepository;
    ScheduleElementRepository scheduleElementRepository;
    LectureRepository lectureRepository;

    @Autowired
    public LectureDAOImpl(ScheduleElementRepository scheduleElementRepository, UserRepository userRepository, LectureRepository lectureRepository) {
        this.scheduleElementRepository = scheduleElementRepository;
        this.userRepository = userRepository;
        this.lectureRepository = lectureRepository;
    }

    @Override
    @Transactional
    public CommonResponse addLecture(String userId, LectureModel lecture) {
        try {
            if(lectureRepository.existsByName(lecture.getName()))
                return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Lecture name already exists");

            lecture.setCreatedAt(new Date());
            LectureModel savedLecture = lectureRepository.save(lecture);

            // User의 savedLectures에 저장된 강의 ID 추가
            UserEntity user = userRepository.findById(userId)
                    .orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));

            // UserRepository에 새로운 SavedLectures 추가
            List<String> newSavedLectures = user.getSavedLectures();
            if (newSavedLectures == null) {
                newSavedLectures = new ArrayList<>();
            }
            newSavedLectures.add(savedLecture.getId());
            user.setSavedLectures(newSavedLectures);
            UserEntity updatedUser = userRepository.save(user);

            log.info("[LectureDAOImpl]-[addLecture] User {} SavedLecturesSize {}", userId, updatedUser.getSavedLectures().size());

            return CommonResponse.builder()
                    .status(HttpStatus.OK)
                    .isSuccess(true)
                    .msg("Lecture added successfully")
                    .object(savedLecture)
                    .build();
        } catch (Exception e) {
            log.error("Failed to add lecture", e);
            return CommonResponse.builder()
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .isSuccess(false)
                    .msg("Failed to add lecture")
                    .build();
        }
    }

    @Override
    public CommonResponse putScript(String lectureId, String script) {
        try{
            LectureModel lecture = lectureRepository.findFirstById(lectureId);
            if(lecture == null)
                return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
            lecture.getProcessedScript().add(script);
            LectureModel savedLecture = lectureRepository.save(lecture);

            return new CommonResponse(true, HttpStatus.OK, "Lecture ProcessedScript Added", savedLecture);
        }catch (Exception e){
            return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to put Script");
        }
    }

    @Override
    public CommonResponse updateLecture(LectureModel lecture) {
        try{
            if(lecture == null)
                return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
            LectureModel updatedLecture = lectureRepository.save(lecture);

            return new CommonResponse(true, HttpStatus.OK, "Lecture Updated", updatedLecture);
        }catch (Exception e){
            return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to put Script");
        }
    }

    @Transactional
    @Override
    public CommonResponse deleteLecture(String userId, String lectureId) {
        try {
            lectureRepository.deleteById(lectureId);

            UserEntity user = userRepository.findById(userId)
                    .orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));

            List<String> newSavedLectures = user.getSavedLectures();
            newSavedLectures.remove(lectureId);
            user.setSavedLectures(newSavedLectures);
            UserEntity updatedUser = userRepository.save(user);

            log.info("[LectureDAOImpl]-[deleteLecture] User {} SavedLecturesSize {}", userId, updatedUser.getSavedLectures().size());
            return new CommonResponse(true, HttpStatus.OK, "Lecture Deleted successfully");
        } catch (Exception e) {
            log.error("Failed to delete lecture", e);
            return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete lecture");
        }
    }

    @Override
    public CommonResponse addProblem(String lectureId, Problem problem) {
        try{
            LectureModel lecture = lectureRepository.findFirstById(lectureId);
            if(lecture == null)
                return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");

            lecture.addProblem(problem);
            LectureModel updatedLecture = lectureRepository.save(lecture);

            return new CommonResponse(true, HttpStatus.OK, "Problem Added", updatedLecture.getProblems());
        }catch (Exception e){
            return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to add Problem");
        }
    }

    @Override
    public CommonResponse updateProblem(String lectureId, String problemId, Problem newProblem) {
        try{
            LectureModel lecture = lectureRepository.findFirstById(lectureId);
            if(lecture == null)
                return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");

            lecture.updateProblem(problemId, newProblem);
            LectureModel updatedLecture = lectureRepository.save(lecture);

            return new CommonResponse(true, HttpStatus.OK, "Lecture Updated", updatedLecture.getProblems());
        }catch (Exception e){
            return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to update Problem");
        }
    }

    @Override
    public CommonResponse deleteProblem(String lectureId, String problemId) {
        try{
            LectureModel lecture = lectureRepository.findFirstById(lectureId);
            if(lecture == null)
                return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");

            lecture.deleteProblem(problemId);
            LectureModel updatedLecture = lectureRepository.save(lecture);

            return new CommonResponse(true, HttpStatus.OK, "Lecture Updated", updatedLecture.getProblems());
        }catch (Exception e){
            return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete Problem");
        }
    }

    @Override
    public CommonResponse getLecture(String lectureId) {
        try{
            LectureModel lecture = lectureRepository.findFirstById(lectureId);
            if(lecture == null)
                return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
            return new CommonResponse(true, HttpStatus.OK, "LectureModel", lecture);
        }catch (Exception e){
            return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to get Lecture");
        }
    }
}

LectureDAO에서는 MongoDB의 데이터들과 MariaDB의 데이터들 사이의 관계를 정의하기 위한 여러 메소드들과 Lecture Model내의 Problem과 관련된 여러 메소드들을 위와 같이 정의하였다.

LectureService

@Service
public class LectureServiceImpl implements LectureService {
    @Autowired
    private LectureDAOImpl lectureDAO;
    @Override
    public CommonResponse addLecture(String userId, LectureModel lecture) {
        return lectureDAO.addLecture(userId, lecture);
    }

    @Override
    public CommonResponse putScript(String lectureId, String script) {
        return lectureDAO.putScript(lectureId, script);
    }

    @Override
    public CommonResponse updateLecture(LectureModel lecture) {
        return lectureDAO.updateLecture(lecture);
    }

    @Override
    public CommonResponse deleteLecture(String userId, String lectureId) {
        return lectureDAO.deleteLecture(userId, lectureId);
    }

    @Override
    public CommonResponse addProblem(String lectureId, Problem problem) {
        return lectureDAO.addProblem(lectureId, problem);
    }

    @Override
    public CommonResponse updateProblem(String lectureId, String problemId, Problem newProblem) {
        return lectureDAO.updateProblem(lectureId, problemId, newProblem);
    }

    @Override
    public CommonResponse deleteProblem(String lectureId, String problemId) {
        return lectureDAO.deleteProblem(lectureId, problemId);
    }

    @Override
    public CommonResponse getLecture(String lectureId) {
        return lectureDAO.getLecture(lectureId);
    }
}

LectureController

@Slf4j
@RestController
@RequestMapping("/api/v1/lecture")
public class LectureController {
    @Autowired
    private LectureService lectureService;

    private CommonResponse response;

    private String getUserIdFromContext(){
        // SecurityContext에서 Authentication으로 UserID를 받아온다
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return (String) authentication.getPrincipal();
    }

    @PostMapping(value="/addLecture")
    public ResponseEntity<CommonResponse> addSchedule(@Valid @RequestBody LectureModel lectureModel){
        log.info("[LectureController]-[addLecture] API Call");

        if(lectureModel.getScheduleElementId().isEmpty() || lectureModel.getName().isEmpty()){
            log.warn("[LectureController]-[addLecture] Failed : Empty Variables");
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Name");
            return ResponseEntity.status(response.getStatus()).body(response);
        }

        String userId = getUserIdFromContext();

        response = lectureService.addLecture(userId, lectureModel);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @PutMapping(value="/putScript")
    public ResponseEntity<CommonResponse> putScript(@Valid @RequestBody Map<String, Object> requestBody){
        ObjectMapper objectMapper = new ObjectMapper();

        String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
        String script = objectMapper.convertValue(requestBody.get("script"), String.class);

        log.info("[LectureController]-[putScript] API Call - LectureId : {}", lectureId);

        if(lectureId.isEmpty() || script.isEmpty()){
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Variables");
            return ResponseEntity.status(response.getStatus()).body(response);
        }

        response = lectureService.putScript(lectureId, script);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @PutMapping(value="/updateLecture")
    public ResponseEntity<CommonResponse> updateLecture(@Valid @RequestBody LectureModel lectureModel){
        log.info("[LectureController]-[updateLecture] API Call");

        if(lectureModel.getId().isEmpty()){
            log.warn("[LectureController]-[updateLecture] Failed : Empty Variables");
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Id");
            return ResponseEntity.status(response.getStatus()).body(response);
        }
        response = lectureService.updateLecture(lectureModel);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @DeleteMapping(value="/deleteLecture")
    public ResponseEntity<CommonResponse> deleteLecture(@Valid @RequestBody Map<String, String> requestBody){
        log.info("[LectureController]-[deleteLecture] API Call");

        ObjectMapper objectMapper = new ObjectMapper();
        String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);

        if(lectureId.isEmpty()){
            log.warn("[LectureController]-[deleteLecture] Failed : Empty LectureId");
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
            return ResponseEntity.status(response.getStatus()).body(response);
        }
        response = lectureService.deleteLecture(getUserIdFromContext(), lectureId);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @PostMapping(value="/addProblem")
    public ResponseEntity<CommonResponse> addProblem(@Valid @RequestBody Map<String, Object> requestBody){
        log.info("[LectureController]-[addProblem] API Call");

        ObjectMapper objectMapper = new ObjectMapper();
        String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
        Problem problem = objectMapper.convertValue(requestBody.get("problem"), Problem.class);

        if(lectureId.isEmpty()){
            log.warn("[LectureController]-[addLecture] Failed : Empty LectureId");
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
            return ResponseEntity.status(response.getStatus()).body(response);
        }

        response = lectureService.addProblem(lectureId, problem);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @PutMapping(value="/updateProblem")
    public ResponseEntity<CommonResponse> updateProblem(@Valid @RequestBody Map<String, Object> requestBody){
        log.info("[LectureController]-[addProblem] API Call");

        ObjectMapper objectMapper = new ObjectMapper();
        String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
        Problem newProblem = objectMapper.convertValue(requestBody.get("problem"), Problem.class);

        if(lectureId.isEmpty()){
            log.warn("[LectureController]-[addLecture] Failed : Empty LectureId");
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
            return ResponseEntity.status(response.getStatus()).body(response);
        }

        response = lectureService.updateProblem(lectureId, newProblem.getId(), newProblem);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @DeleteMapping(value="/deleteProblem")
    public ResponseEntity<CommonResponse> deleteProblem(@Valid @RequestBody Map<String, String> requestBody){
        log.info("[LectureController]-[addProblem] API Call");

        ObjectMapper objectMapper = new ObjectMapper();
        String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
        String problemId = objectMapper.convertValue(requestBody.get("problemId"), String.class);

        if(lectureId.isEmpty() || problemId.isEmpty()){
            log.warn("[LectureController]-[addLecture] Failed : Empty Variables");
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Variables");
            return ResponseEntity.status(response.getStatus()).body(response);
        }

        response = lectureService.deleteProblem(lectureId, problemId);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @GetMapping("/getLecture")
    public ResponseEntity<CommonResponse> getLecture(@RequestParam("lectureId") String lectureId) {
        log.info("[LectureController]-[getLecture] API Call");
        if(lectureId.isEmpty()){
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
            return ResponseEntity.status(response.getStatus()).body(response);
        }
        response = lectureService.getLecture(lectureId);
        return ResponseEntity.status(response.getStatus()).body(response);
    }

    @GetMapping("/getProblem")
    public ResponseEntity<CommonResponse> getProblem(@RequestParam("lectureId") String lectureId) {
        log.info("[LectureController]-[getLecture] API Call");
        if(lectureId.isEmpty()){
            response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
            return ResponseEntity.status(response.getStatus()).body(response);
        }
        response = lectureService.getLecture(lectureId);

        LectureModel lecture = (LectureModel) response.getObject();
        response.setObject(lecture.getProblems());

        return ResponseEntity.status(response.getStatus()).body(response);
    }

}

LectureController에서는 기존 API 명세서에서 정의한 여러 API들을 구현하였다.
다양한 타입, 조건의 Request Body로 구성되므로 ObjectMapper를 통해 JSON 형태의 Req Body에서 여러 필드 값들을 추출하여 사용할 수 있도록 정의하였다.


API Test

/api/v1/lecture/addLecture

새로운 Lecture를 정상적으로 생성하는 것을 볼 수 있으며
같은 이름의 Lecture를 생성했을 때 위와 같이 실패하는 것을 볼 수 있다.

/api/v1/lecture/putScript

STT 처리가 완료된 Script를 기존의 Lecture에 정상적으로 추가하는 것을 볼 수 있고
MongoDB에도 해당 결과가 정상적으로 반영되는 것을 볼 수 있다.

/api/v1/lecture/updateLecture

Lecutere의 스크립트를 업데이트하거나, Date를 업데이트할 때 사용하는 API로 정상적으로 UPDATE 요청한 결과가 MongoDB에도 정상적으로 반영되는 것을 볼 수 있다.

/api/v1/lecture/deleteLecture

먼저 /deleteLecture를 테스트하기 위하여 Dummy Lecture Data를 위와 같이 추가하면 위와 같이 서버의 로그에 현재 몇개의 LectureID가 User 테이블에 존재하는지 확인할 수 있다. 또한 MongoDB에 새로운 Dummy Lecture가 추가된 것을 볼 수 있으며 위와 같이 LectureId에 대한 DELETE 요청을 보내면
Lecture가 삭제되고 Server의 로그에도 정상적으로 User 테이블에서 삭제된 것을 볼 수 있다.

/api/v1/lecture/addProblem

위와 같이 문제를 추가하는 요청을 보내면
MongoDB에서 문제가 정상적으로 추가된 것을 확인할 수 있다.

/api/v1/lecture/updateProblem

LectureModel의 메소드로 생성된 Problem의 ID를 담아 문제를 수정하는 요청을 전송하게되면
위와 같이 기존의 Index 위치로 문제가 수정된 것을 볼 수 있다.

/api/v1/lecture/deleteProblem

위와 같이 새로운 Dummy 문제를 생성한 이후
해당 문제의 ID와 LectureId를 통해 DELETE를 요청하면
문제가 정상적으로 삭제되는 것을 확인할 수 있다.

/api/v1/lecture/getLecture?id=

Lecture의 아이디를 파라미터로 하여 GET 요청을 보내면 강의 정보를 JSON 형태로 전달받을 수 있다.

/api/v1/lecture/getProblems?id=

Lecture의 아이디를 파라미터로 하여 GET 요청을 보내면 위처럼 문제만 제공받을 수 있도록 API를 구현하였다.

0개의 댓글