【Java】날짜 기간 중복 체크 샘플

개요



날짜로 기간을 가진 데이터를 등록할 때 이미 등록한 기간과 중복되지 않도록 하고 싶다.
LocalDate를 사용한 Java 샘플이별로 발견되지 않았기 때문에 공개합니다.

환경


  • Java 1.8
  • SpringBoot 2.2.1.RELEASE
  • thymeleaf 3.0.11.RELEASE

  • 기간이 중복되는 패턴



    날짜의 기간이 중복되는 패턴은 모두 4개.

    1. 전반부가 쓰는 패턴(①의 종료일과 초록의 시작일이 동일도 포함한다)
    2. 후반이 쓰는 패턴(②의 시작일과 녹색의 종료일이 동일도 포함한다)
    3. 전부 쓰는 패턴(개시일과 종료일이 정확히 동일도 포함한다)
    4. 일부가 쓰는 패턴

    모든 패턴을 망라할 수 있는 조건식은 아래와 같다.

    조건식
    .開始日 <= .終了日 && .終了日 => .開始日
    

    샘플



    화면으로부터 개시일과 종료일을 입력해, 그 입력 기간이 기존의 기간(복수)과 중복되어 있지 않은 경우만 등록할 수 있는 샘플.

    등록할 날짜 기간이 있는 Model 클래스



    등록하는 시작일과 종료일, 자동 번호 매기기의 ID를 가지는 화면에 건네주는 Model 클래스.

    DurationModel.java
    package com.tamorieeeen.sample.model;
    
    import java.time.LocalDate;
    import org.springframework.format.annotation.DateTimeFormat;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    /**
     *
     * @author tamorieeeen
     *
     */
    @Getter
    @Setter
    @NoArgsConstructor
    public class DurationModel {
    
        // 保存時にauto_incrementで採番
        private int id;
    
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private LocalDate startDate;
    
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private LocalDate endDate;
    }
    

    기간 중복 판정 로직 Service 클래스



    Controller에서 호출되는 Service 클래스입니다.

    DurationService.java
    package com.tamorieeeen.sample.service;
    
    import java.time.LocalDate;
    import java.util.List;
    import javax.transaction.Transactional;
    import org.springframework.stereotype.Service;
    import com.tamorieeeen.sample.model.DurationModel;
    
    /**
     *
     * @author tamorieeeen
     *
     */
    @Service
    public class DurationService {
    
        /**
         * すでに登録済の期間とかぶってないかチェック
         */
        public boolean isInvalid(DurationModel model) {
    
            return this.getDurationList()
                    .stream()
                    .filter(u -> model.getId() != u.getId()) // ※1
                    .anyMatch(u ->
                            (order.getStartDate().isBefore(u.getEndDate())
                            && order.getEndDate().isAfter(u.getStartDate()))
                            || order.getStartDate().isEqual(u.getEndDate())
                            || order.getEndDate().isEqual(u.getStartDate()));
        }
    
        /**
         * 一覧を取得
         */
        private List<DurationModel> getDurationList() {
    
            // TODO すでに登録済のデータを取得
        }
    
        /**
         * 新規登録/更新
         */
        @Transactional
        public void saveDuration(DurationModel model) {
    
            // TODO DBなどへのデータ保存処理
        }
    }
    

    ※1: 비교원(model)의 ID는 기간 체크를 제외한다
    이렇게 하지 않으면 데이터 갱신시에 this.getDurationList() 에 비교원 데이터도 포함되어 있기 때문에 중복 취급으로 validation에 걸려 버리기 (위해)때문에.
    신규 등록 밖에 생각하지 않는다면, 이 행은 불필요.

    Controller 클래스



    실제로는 밸리데이션 체크에 @ValidatedBindingResult 를 사용하고 있지만 그 부분은 생략.

    DurationController.java
    /**
     *
     * @author tamorieeeen
     *
     */
    @Controller
    public class DurationController {
    
        @Autowired
        private DurationService durationService;
    
        /**
         * 新規登録
         */
        @GetMapping("/duration/register")
        public String register(Model model) {
    
            model.addAttribute("duration", new DurationModel());
    
            return "duration/register";
        }
    
        /**
         * 新規登録処理
         */
        @PostMapping("/duration/register")
        public String registerComplete(Model model,
                @ModelAttribute("duration") DurationModel duration,
                RedirectAttributes redirect) {
    
            // バリデーションチェック
            if (durationService.isInvalid(duration)) {
    
                model.addAttribute("invalid", true);
    
                return "duration/register";
            }
    
            durationService.saveDuration(duration);
    
            redirect.addFlashAttribute("complete", true);
    
            return "redirect:/duration/register";
        }
    }
    

    화면측 html(thymeleaf)



    html 헤더는 공통화하고 있지만 이번에는 관계 없기 때문에 생략. 신경이 쓰이는 사람은 Thymeleaf에서 머리글 바닥글을 공통화하는 방법 를 참고해 주세요.

    register.html
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="common :: meta_header('sample',~{::link},~{::script},~{::meta})">
    </head>
    <body>
        <div th:if="${complete}">
            <p>期間を登録しました。</p>
        </div>
        <div th:if="${invalid}">
            <p>期間が重複しているため、登録できません。</p>
        </div>
        <form th:action="@{/duration/register}" method="post" th:object="${duration}">
            <table>
                <tr><td>開始日</td><td>
                    <input type="date" th:field="*{startDate}" th:value="*{startDate}" />
                </td></tr>
                <tr><td>終了日</td><td>
                    <input type="date" th:field="*{endDate}" th:value="*{endDate}" />
                </td></tr>
            </table>
            <input type="button" th:value="登録する" onclick="submit();" />
        </form>
    </body>
    </html>
    

    참고


  • 날짜 기간 중복 확인
  • 좋은 웹페이지 즐겨찾기