본문 바로가기

Spring

[Project] Lv_2 스케줄 프로젝트

 

 


 

요구사항

작성자와 일정의 연결

  • ✅ 설명
    • ✅ 동명이인의 작성자가 있어 어떤 작성자가 등록한 ‘할 일’인지 구별할 수 없음
    • ✅ 작성자를 식별하기 위해 이름으로만 관리하던 작성자에게 고유 식별자를 부여합니다.
    • ✅ 작성자를 할 일과 분리해서 관리합니다.
    • ✅ 작성자 테이블을 생성하고 일정 테이블에 FK를 생성해 연관관계를 설정해 봅니다.

✅ 조건

  • ✅ 작성자는 이름 외에 이메일, 등록일, 수정일 정보를 가지고 있습니다.
    • ✅ 작성자의 정보는 추가로 받을 수 있습니다.(조건만 만족한다면 다른 데이터 추가 가능)
  • ✅ 작성자의 고유 식별자를 통해 일정이 검색이 될 수 있도록 전체 일정 조회 코드 수정.
  • ✅ 작성자의 고유 식별자가 일정 테이블의 외래키가 될 수 있도록 합니다.

 


 

 

문제풀이

ScheduleController

더보기
@RestController
@RequestMapping("/schedules")
public class ScheduleController {

    private final ScheduleService scheduleService;

    public ScheduleController(ScheduleService scheduleService) {
        this.scheduleService = scheduleService;
    }

    /**
     * 스케쥴 생성 API
     * param : {@link ScheduleRequestDto} 스케쥴 생성 요청 객체
     * @return : {@link ResponseEntity<ScheduleResponseDto>} JSON 응답
     */
    @PostMapping
    public ResponseEntity<ScheduleResponseDto> createSchedule(@RequestBody ScheduleRequestDto requestDto) {
        return new ResponseEntity<>(scheduleService.saveSchedule(requestDto), HttpStatus.CREATED);
    }

    /**
     * 스케쥴 다건 조회 API
     * @return : {@link  List<ScheduleResponseDto>} JSON 응답
     */
    @GetMapping
    public List<ScheduleResponseDto> findAllSchedule() {
        return scheduleService.findAllSchedules();
    }

    /**
     * 스케쥴 단건 조회 API
     * @param id 식별자
     * @return : {@link ResponseEntity<ScheduleResponseDto>} JSON 응답
     * @exception ResponseStatusException 식별자로 조회된 Schedule이 없는 경우 404 Not Found
     */
    @GetMapping("/{id}")
    public ResponseEntity<ScheduleResponseDto> findScheduleById(@PathVariable Long id) {
        return new ResponseEntity<>(scheduleService.findScheduleById(id), HttpStatus.OK);
    }

    /**
     * 스케줄 단건 수정 API
     * Param id 식별자
     * Return : {@link ResponseEntity<ScheduleResponseDto>} JSON 응답
     * @exception ResponseStatusException 식별자로 조회된 Schedule이 없는 경우 404 Not Found
     */
    @PatchMapping("/{id}")
    public ResponseEntity<ScheduleResponseDto> updatetoDoContentOrUserName(
            @PathVariable long id,
            @RequestBody ScheduleRequestDto requestDto
    ) {
        return new ResponseEntity<>(scheduleService.updateUserNameOrToDoContent(id, requestDto.getPassword(), requestDto.getUserName(), requestDto.getToDoContent()), HttpStatus.OK);
    }

    /**
     * 스케줄 단건 수정 API
     * Param id 식별자
     * return {@link ResponseEntity<Void>} 성공시 Data 없이 200OK 상태코드만 응답.
     * @exception ResponseStatusException 식별자로 조회된 Schedule이 없는 경우 404 Not Found
     */

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteSchedule(
            @PathVariable long id,
            @RequestBody ScheduleRequestDto requestDto
    ) {
        scheduleService.deleteSchedule(id, requestDto.getPassword());
        // 성공한 경우
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

 

 

ScheduleService

더보기
public interface ScheduleService {

    ScheduleResponseDto saveSchedule(ScheduleRequestDto requestDto);

    List<ScheduleResponseDto> findAllSchedules();

    ScheduleResponseDto findScheduleById(long id);

    ScheduleResponseDto updateUserNameOrToDoContent(long id, long password, String userName, String toDoContent);

    void deleteSchedule(long id, long password);
}

 

 

ScheduleServiceImpl

더보기
@Service
public class ScheduleServiceImpl implements ScheduleService{

    private final ScheduleRepository scheduleRepository;

    public ScheduleServiceImpl(ScheduleRepository scheduleRepository) {
        this.scheduleRepository = scheduleRepository;
    }

    @Override
    public ScheduleResponseDto saveSchedule(ScheduleRequestDto requestDto) {
        Schedule schedule = new Schedule(requestDto.getPassword(), requestDto.getUserName(), requestDto.getToDoContent());
        return scheduleRepository.saveSchedule(schedule);
    }

    @Override
    public List<ScheduleResponseDto> findAllSchedules() {
        // 전체 조회
        return scheduleRepository.findAllSchedules();
    }

    @Override
    public ScheduleResponseDto findScheduleById(long id) {
        Schedule schedule = scheduleRepository.findScheduleByIdElseThrow(id);
        return new ScheduleResponseDto(schedule);
    }

    @Override
    public ScheduleResponseDto updateUserNameOrToDoContent(long id, long password, String userName, String toDoContent) {
        // 필수값 검증
        if (userName == null || toDoContent == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The userName and toDoContent are required values.");
        }

        // 비밀번호 검증
        if (password != scheduleRepository.findSchedulePasswordByIdElseThrow(id).getPassword()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The password is not the same as the password for this schedule");
        }

        int updatedRow = scheduleRepository.updateUserNameOrToDoContent(id, userName, toDoContent);

        // NPE 방지
        if (updatedRow == 0) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
        }

        // 식별자의 schedule 없다면?
        Schedule schedule = scheduleRepository.findScheduleByIdElseThrow(id);

        return new ScheduleResponseDto(schedule);
    }

    @Override
    public void deleteSchedule(long id, long password) {
        // 비밀번호 검증
        if (password != scheduleRepository.findSchedulePasswordByIdElseThrow(id).getPassword()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The password is not the same as the password for this schedule");
        }

        int deletedRow = scheduleRepository.deleteSchedule(id);

        // NPE 방지
        if(deletedRow == 0) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
        }
    }
}
  • 스케줄을 수정 및 삭제 할 때 : 새로 입력받은 비밀번호와 수정, 삭제하길 원하는 스케줄에 저장된 비밀번호가 일치하는지 확인하는 로직을 구현하여, 입력값(비밀번호)를 검증합니다.

 

 

ScheduleRepository

더보기
package org.example.scheduledweb.repository;

import org.example.scheduledweb.dto.ScheduleResponseDto;
import org.example.scheduledweb.entity.Schedule;

import java.util.List;

public interface ScheduleRepository {

    ScheduleResponseDto saveSchedule(Schedule schedule);

    List<ScheduleResponseDto> findAllSchedules();

    Schedule findScheduleByIdElseThrow(long id);

    Schedule findSchedulePasswordByIdElseThrow(long id);

    int updateUserNameOrToDoContent(long id, String userName, String toDoContent);

    int deleteSchedule (long id);
}

 

 

JdbcTemplateScheduleRepository

더보기
@Repository
public class JdbcTemplateScheduleRepository implements ScheduleRepository{

    private final JdbcTemplate jdbcTemplate;

    public JdbcTemplateScheduleRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public ScheduleResponseDto saveSchedule(Schedule schedule) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("schedule") // 테이블명
                .usingGeneratedKeyColumns("scheduleId") // 자동 생성된 키
                .usingColumns("password", "userName", "toDoContent");

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("password", schedule.getPassword());
        parameters.put("userName", schedule.getUserName());
        parameters.put("toDoContent", schedule.getTodoContent());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        long scheduleId = key.longValue();

        String sql = "SELECT userName, toDoContent, updateAt FROM schedule WHERE scheduleId = ?";
        return jdbcTemplate.queryForObject(sql, (rs, rowNum) ->
                new ScheduleResponseDto(
                        rs.getString("userName"),
                        rs.getString("toDoContent"),
                        rs.getDate("updateAt")
                )
                ,scheduleId
        );
    }

    @Override
    public List<ScheduleResponseDto> findAllSchedules() {
        return jdbcTemplate.query("select * from schedule ORDER BY updateAt DESC", scheduleRowMapper());
    }

    @Override
    public Schedule findScheduleByIdElseThrow(long id) {
        List<Schedule> result = jdbcTemplate.query("select * from schedule where scheduleId = ?", scheduleRowMapper2(), id);
        return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exists id = " + id));
    }

    @Override
    public Schedule findSchedulePasswordByIdElseThrow(long id) {
        List<Schedule> result = jdbcTemplate.query("select * from schedule where scheduleId = ?", scheduleRowMapper3(), id);
        return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exists id = " + id));
    }

    @Override
    public int updateUserNameOrToDoContent(long id, String userName, String toDoContent) {
        return jdbcTemplate.update("update schedule set userName = ?, toDoContent = ? where scheduleId = ?", userName, toDoContent, id);
    }

    @Override
    public int deleteSchedule(long id) {
        return jdbcTemplate.update("delete from schedule where scheduleId = ?", id);
    }

    private RowMapper<ScheduleResponseDto> scheduleRowMapper() {
        return (rs, rowNum) -> new ScheduleResponseDto(
                rs.getString("userName"),
                rs.getString("todoContent"),
                rs.getDate("updateAt")
        );
    }

    private RowMapper<Schedule> scheduleRowMapper2() {
        return (rs, rowNum) -> new Schedule(
                rs.getString("userName"),
                rs.getString("todoContent"),
                rs.getDate("updateAt")
        );
    }

    private RowMapper<Schedule> scheduleRowMapper3() {
        return (rs, rowNum) -> new Schedule(
                rs.getLong("password")
        );
    }
}
  • findSchedulePasswordByIdElseThrow : 비밀번호 검증을 위해 비밀번호만 갖는 객체를 반환합니다.

 

 

ScheduleRequestDto

더보기
@Getter
public class ScheduleRequestDto {

    private Long password;
    private String userName;
    private String toDoContent;
}

 

 

ScheduleResponseDto

더보기
@Getter
@AllArgsConstructor
public class ScheduleResponseDto {

    private String userName;
    private String todoContent;
    private Date updateAt;

    public ScheduleResponseDto(Schedule schedule) {
        this.userName = schedule.getUserName();
        this.todoContent = schedule.getTodoContent();
        this.updateAt = schedule.getUpdateAt();
    }
}

 

 

Schedule(Entity)

더보기
@Getter
@AllArgsConstructor
public class Schedule {

    private long scheduleId;
    private long password;
    private String userName;
    private String todoContent;
    private Date createAt;
    private Date updateAt;

    public Schedule(long password, String userName, String todoContent) {
        this.password = password;
        this.userName = userName;
        this.todoContent = todoContent;
    }

    public Schedule(String userName, String todoContent, Date updateAt) {
        this.userName = userName;
        this.todoContent = todoContent;
        this.updateAt = updateAt;
    }

    public Schedule(long password) {
        this.password = password;
    }
}

 


 

회고

  1. Lv_0 API 명세서 작성을 통해 전체적인 틀을 만들어 놓고, 기능을 순서대로 구현하기 때문에, 큰 어려움은 없었습니다.
  2. 3 Layerd Architecture 방식을 이용하니, 역할이 분리되어있어 확실히 코드를 짤 때 편리하다고 느꼈습니다.