[Vue] 에어비앤비 달력만들기

DongEun·2023년 2월 2일
3
post-thumbnail

내가 살다살다 datePicker 까지 만들줄이야,,


gh-pages : https://kimdongeun12.github.io/vue2-practice/
github : https://github.com/kimdongeun12/vue2-practice

왜 했을까?

지금 하는 프로젝트에서 datePicker가 에어비앤비 datepicker처럼 새롭게 나와야 하는데 vuetify에 있는 datepicker가 프로젝트와 어울리지도 않고 에어비앤비 기능에 해당하는 모듈을 추가하기에 여러 제약사항들이 있기도 해서 이럴바에 그냥 새롭게 만드는게 코드가 더 깔끔해지고 간략하지 않을까? 라는 생각으로 차라리 만들자! 하고 시작했던일인데 3일정도 걸릴줄 알았더니 예상보다 순조롭게 풀려 이틀만에 구현이 가능했어요

현재 프로젝트에 맞게 코드가 되어있지만 충분히 커스텀이 잘 될거라 믿고 소스코드를 공유드릴게요

<template>
  <div class="date-picker-wrap custom">
    <div class="week-wrap">
      <span v-for="(weekName, index) in weekNames" v-bind:key="index">
        {{ weekName }}</span
      >
    </div>
    <div
      class="date-wrap"
      v-for="(calendar, index) in currentCalendar"
      v-bind:key="index"
    >
      <p class="date-tit">{{ calendar.dateTitle }}</p>
      <ul class="picker-wrap">
        <li
          v-for="(el, index) in calendar.dateStart"
          :key="`blank${index}`"
          class="blank-date"
        ></li>
        <li v-for="(el, index) in calendar.dateArr" :key="index">
          <button
            v-bind:disabled="!checkDate(el)"
            v-on:click="onHandleRangeDate(el)"
            v-bind:class="[
              rangeActiveDate(el) && 'data-active',
              rangeSelectDate(el) && 'data-select',
              rangePositionDate(el),
            ]"
          >
            <span>{{ el.getDate() }}</span>
          </button>
        </li>
      </ul>
    </div>
  </div>
</template>

HTML의 구조는 위와같고 vuetify를 써야지 써야지 했는데 입맛에 맞는 애들이 없어서 그냥 하드코딩 하는거로 하였고
ul로 할지 사람들이 제일 많이 구현하는 table로 구현할지 고민하였는데 생각해보면 위에 상단 요일을 표시하는게 빠져야하므로 테이블을 안쓰고 li로 작업하는게 오히려 css를 좀 더 효율적으로 짤 수 있지 않을까? 하고 li 형식으로 작성하게 되었어요

그럼 기능 드가자~

기능은 어떻게 구현했어?

<script>

export default {
  ...
  methods: {
    initCalendar: function () {
      const startDate = this.startDate; // 현재 날짜가 검사 시작 날짜를 넘지 못했을 경우 : 현재 날짜가 검사 시작 날짜를 넘었을경우
      const endDate = this.endDate; // 검사 마지막 날짜
      const diffDate = (endDate - startDate) / 1000; // 두 달의 차이 값
      const monthCount = Math.round(diffDate / (60 * 60 * 24 * 30)); // 남은 개월수
      const tempArr = new Array(monthCount + 1).fill({}); // 개월 수 만큼 배열 공간 만들기

      this.currentCalendar = tempArr.map((_, idx) => {
        const selectDate = new Date(
          startDate.getFullYear(),
          startDate.getMonth() + idx
        ); // 선택된 날짜의 연도와 달 구하기
        const dateTitle = `${selectDate.getFullYear()}${
          selectDate.getMonth() + 1
        }`; // 선택된 날짜의 연도와 달 구하기
        const dateArrFill = new Array(
          new Date(
            selectDate.getFullYear(),
            selectDate.getMonth() + 1,
            0
          ).getDate()
        ).fill(" "); // 선택된 달의 말일 구하기
        const dateArr = dateArrFill.map((_, idx) => {
          return new Date(
            selectDate.getFullYear(),
            selectDate.getMonth(),
            idx + 1
          ); // 일자별로 배열에 담기
        });
        const dateStart = selectDate.getDay(); // 달의 시작 날짜 알아오기;
        const tamp = {
          dateTitle,
          dateStart,
          dateArr,
        };
        return tamp;
      });
      console.log(this.currentCalendar);
    },
    ...
  },
};
</script>

처음에 달력이 달에 맞게 생성이 되어야 했고 뿐만아니라 이전 다음달을 보여주는게 아니라 아래로 길게 스크롤이 되어야하는 형식이여서 diffDate로 시작하는달과 끝나는달의 차이값을 구해왔고 만약에 무한스크롤을 되게한다하면 스크롤이 맨 아래 지점이 되었을때 endDate가 증가하게하면 가능할 거 같네요

	...
    onHandleRangeDate(date) {
      // console.log(date, this.rangeDate[0], date > this.rangeDate);
      if (date < this.rangeDate[0]) {
        this.rangeDate = [];
      }
      if (this.rangeDate.length >= 2) {
        this.rangeDate = [];
      }
      const select = date;
      const selectArr = [...this.rangeDate, select].sort((a, b) => {
        if (a > b) {
          return 1;
        } else {
          return -1;
        }
      });
      this.rangeDate = [...selectArr];
      console.log(this.rangeDate);
    },
    ...

onHandleRangeDate는 처음 선택한 달과 두번쨰 선택한 달을 가져오기 위해서 작성하였고

selectArr.sort()를 추가했던 이유는 두번쨰 선택한 달이 첫번째 선택한 달보다 미래의 날짜면 두 날짜의 위치를 변경해주기 위해서 추가했어요 하지만 위에 있는 if(date < this.rangeDate[0]) 가 있기에 작동은 하지 않을거에요 추후에 사용하실거면 지우고 사용하셔도 괜찮을거 같네요

	...
    checkDate(date) {
      const yseterDate = new Date().setDate(this.todayDate.getDate() - 1);
      return (
        date >= this.startDate &&
        date <= this.endDate &&
        date.getDay() !== 0 &&
        date >= yseterDate
      );
    },
    ...

checkDate를 추가한 이유는 클릭이되면 안되는 날짜를 추가해둔거에요
지금은 그냥 return으로 해두었는데 저거를 props로 받아올 수 있게 변경할 예정이에요

	...
    rangeActiveDate(date) {
      return date >= this.rangeDate[0] && date <= this.rangeDate[1];
    },
    rangeSelectDate(date) {
      return this.rangeDate.includes(date);
    },
    rangePositionDate(date) {
      let className = "";
      if (date === this.rangeDate[0]) {
        className += "data-first ";
      }
      if (date === this.rangeDate[1]) {
        className += "data-last ";
      }
      return className;
    },
    ...

여기서부터는 스타일을 위한 클래스 추가부분이에요

TS(Class Component)


<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

interface ICurrentCalendar {
  dateTitle: string;
  dateStart: number;
  dateArr: Date[];
}

@Component
export default class TestPicker extends Vue {
  weekNames: string[] = ["일", "월", "화", "수", "목", "금", "토"];
  todayDate: Date = new Date();
  startDate: Date = new Date("2023/1/12");
  endDate: Date = new Date("2026/12/11");
  rangeDate: Date[] = [];
  currentCalendar: ICurrentCalendar[] = [];

  mounted(): void {
    this.init();
  }

  private init() {
    this.initCalendar();
  }

  private initCalendar() {
    const startDate = this.startDate; // 현재 날짜가 검사 시작 날짜를 넘지 못했을 경우 : 현재 날짜가 검사 시작 날짜를 넘었을경우
    const endDate = this.endDate; // 검사 마지막 날짜
    const diffDate = (endDate.getTime() - startDate.getTime()) / 1000; // 두 달의 차이 값
    const monthCount = Math.round(diffDate / (60 * 60 * 24 * 30)); // 남은 개월수
    const tempArr = new Array(monthCount + 1).fill({}); // 개월 수 만큼 배열 공간 만들기

    this.currentCalendar = tempArr.map((_, idx) => {
      const selectDate = new Date(
        startDate.getFullYear(),
        startDate.getMonth() + idx
      ); // 선택된 날짜의 연도와 달 구하기
      const dateTitle = `${selectDate.getFullYear()}년 ${
        selectDate.getMonth() + 1
      }월`; // 선택된 날짜의 연도와 달 구하기
      const dateArrFill = new Array(
        new Date(
          selectDate.getFullYear(),
          selectDate.getMonth() + 1,
          0
        ).getDate()
      ).fill(" "); // 선택된 달의 말일 구하기
      const dateArr = dateArrFill.map((_, idx) => {
        return new Date(
          selectDate.getFullYear(),
          selectDate.getMonth(),
          idx + 1
        ); // 일자별로 배열에 담기
      });
      const dateStart = selectDate.getDay(); // 달의 시작 날짜 알아오기;
      const tamp: ICurrentCalendar = {
        dateTitle,
        dateStart,
        dateArr,
      };
      return tamp;
    });
    console.log(this.currentCalendar);
  }
  
  public onHandleRangeDate(date: Date) {
    // console.log(date, this.rangeDate[0], date > this.rangeDate);
    if (date < this.rangeDate[0]) {
      this.rangeDate = [];
    }
    if (this.rangeDate.length >= 2) {
      this.rangeDate = [];
    }
    const select = date;
    const selectArr = [...this.rangeDate, select].sort((a, b) => {
      if (a > b) {
        return 1;
      } else {
        return -1;
      }
    });
    this.rangeDate = [...selectArr];
    console.log(this.rangeDate);
  }
  
  public checkDate(date: Date) {
    const yseterDate = new Date().setDate(this.todayDate.getDate() - 1);
    return (
      date >= this.startDate &&
      date <= this.endDate &&
      date.getDay() !== 0 &&
      date.getTime() >= yseterDate
    );
  }
  
  public rangeActiveDate(date: Date) {
    return date >= this.rangeDate[0] && date <= this.rangeDate[1];
  }
  
  public rangeSelectDate(date: Date) {
    return this.rangeDate.includes(date);
  }
  
  public rangePositionDate(date: Date) {
    let className = "";
    if (date === this.rangeDate[0]) {
      className += "data-first ";
    }
    if (date === this.rangeDate[1]) {
      className += "data-last ";
    }
    return className;
  }
}

궁금한 사항들은 깃의 코드를 보시고 궁금한게 있다면 댓글 남겨주세용~

profile
다채로운 프론트엔드 개발자

0개의 댓글