프런트엔드

고등학교 동아리 프로젝트 vue.js : 일정 관리 홈페이지 만들기

@thiskorea 2024. 6. 23. 22:23

vue.js를 공부한 다음 인공지능 chatGPT 4o를 사용해서 만들어보았다. 역시 공부하기 전에는 기본 구조부터 프로그램의 돌아가는 원리를 잘 이해하지 못해 gpt를 사용해도 만들지 못했을 것이다. 공부한 후에는 구조와 원리를 파악한 후 수정할 부분을 파악할 수 있었고 익스플로어나 크롬에 있는 개발자 도구를 사용해 버그도 찾아낼 수 있었다. 

우선 재사용 가능한 컴포넌트를 만드는데 집중하였고, 그 컴포넌트를 여기저기 쓸 수 있게 되었다. 스케쥴 등록, 수정, 삭제가능한 컴포넌트를 만들었고 그 다음 캘린더 컴포넌트를 붙이고 emit 명령어를 사용하여 연결하였다. 이 과정에서 개발자 도구를 사용하여 에러를 찾아서 수정하였다. 특별히 데이터베이스를 사용하지 않고 로컬 저장소를 사용하여 아주 간단한 로직을 구현하였다.

Calendar.vue

<template>
  <div class="calendar">
    <div class="header">
      <button @click="prevMonth">Prev</button>
      <h2>{{ monthNames[currentMonth] }} {{ currentYear }}</h2>
      <button @click="nextMonth">Next</button>
    </div>
    <div class="weekdays">
      <div v-for="day in weekDays" :key="day">{{ day }}</div>
    </div>
    <div class="days">
      <div
        v-for="day in daysInMonth"
        :key="day.date ? day.date.toDateString() : day.index"
        :class="{'today': isToday(day.date), 'has-event': hasEvent(day.date)}"
      >
        <span v-if="day.date">{{ day.date.getDate() }}</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    events: Array
  },
  data() {
    return {
      currentDate: new Date(),
      currentMonth: new Date().getMonth(),
      currentYear: new Date().getFullYear(),
      weekDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
      monthNames: [
        'January', 'February', 'March', 'April', 'May', 'June',
        'July', 'August', 'September', 'October', 'November', 'December'
      ]
    };
  },
  computed: {
    daysInMonth() {
      const days = [];
      const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
      const lastDate = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
      for (let i = 0; i < firstDay; i++) {
        days.push({ date: null, index: `empty-${i}` });
      }
      for (let date = 1; date <= lastDate; date++) {
        days.push({ date: new Date(this.currentYear, this.currentMonth, date) });
      }
      return days;
    }
  },
  methods: {
    prevMonth() {
      if (this.currentMonth === 0) {
        this.currentMonth = 11;
        this.currentYear--;
      } else {
        this.currentMonth--;
      }
    },
    nextMonth() {
      if (this.currentMonth === 11) {
        this.currentMonth = 0;
        this.currentYear++;
      } else {
        this.currentMonth++;
      }
    },
    isToday(date) {
      if (!date) return false;
      const today = new Date();
      return date.getDate() === today.getDate() &&
             date.getMonth() === today.getMonth() &&
             date.getFullYear() === today.getFullYear();
    },
    hasEvent(date) {
      if (!date) return false;
      return this.events.some(event => {
        const eventDate = new Date(event.date);
        return eventDate.getDate() === date.getDate() &&
               eventDate.getMonth() === date.getMonth() &&
               eventDate.getFullYear() === date.getFullYear();
      });
    }
  }
};
</script>

<style scoped>
.calendar {
  max-width: 400px;
  margin: auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.weekdays {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}
.days {
  display: flex;
  flex-wrap: wrap;
}
.days div {
  width: calc(100% / 7);
  height: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 10px;
}
.today {
  background-color: #42b983;
  color: white;
  border-radius: 50%;
}
.has-event {
  background-color: purple;
  color: white;
  border-radius: 50%;
}
</style>

Schedule.vue

<template>
    <div class="schedule">
      <h2>My Schedule</h2>
      <ul>
        <li v-for="event in events" :key="event.id">
          <span @click="editEvent(event)">{{ event.date }} {{ event.time }}: {{ event.title }}</span>
          <button @click="deleteEvent(event.id)">Delete</button>
        </li>
      </ul>
      <input v-model="newEventTitle" placeholder="Event Title" />
      <input v-model="newEventDate" type="date" />
      <input v-model="newEventTime" type="time" />
      <button @click="addEvent">Add Event</button>
      <button v-if="isEditing" @click="updateEvent">Update Event</button>
    </div>
  </template>
  
  <script>
  export default {
    data() {
      return {
        events: JSON.parse(localStorage.getItem('events') || '[]'),
        newEventTitle: '',
        newEventDate: '',
        newEventTime: '',
        isEditing: false,
        currentEvent: null
      };
    },
    methods: {
      addEvent() {
        if (this.newEventTitle && this.newEventDate && this.newEventTime) {
          const newEvent = {
            id: Date.now(),
            title: this.newEventTitle,
            date: this.newEventDate,
            time: this.newEventTime
          };
          this.events.push(newEvent);
          this.saveEvents();
          this.resetForm();
          this.$emit('update-events', this.events);
        }
      },
      editEvent(event) {
        this.isEditing = true;
        this.currentEvent = event;
        this.newEventTitle = event.title;
        this.newEventDate = event.date;
        this.newEventTime = event.time;
      },
      updateEvent() {
        if (this.currentEvent) {
          this.currentEvent.title = this.newEventTitle;
          this.currentEvent.date = this.newEventDate;
          this.currentEvent.time = this.newEventTime;
          this.saveEvents();
          this.resetForm();
          this.$emit('update-events', this.events);
        }
      },
      deleteEvent(id) {
        this.events = this.events.filter(event => event.id !== id);
        this.saveEvents();
        this.$emit('update-events', this.events);
      },
      saveEvents() {
        localStorage.setItem('events', JSON.stringify(this.events));
      },
      resetForm() {
        this.newEventTitle = '';
        this.newEventDate = '';
        this.newEventTime = '';
        this.isEditing = false;
        this.currentEvent = null;
      }
    }
  };
  </script>
  
  
  <style scoped>
  .schedule {
    max-width: 400px;
    margin: auto;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  }
  input {
    display: block;
    width: 100%;
    padding: 8px;
    margin: 10px 0;
    border: 1px solid #ccc;
    border-radius: 4px;
  }
  button {
    padding: 10px 15px;
    background-color: #42b983;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  button:hover {
    background-color: #38a373;
  }
  ul {
    list-style-type: none;
    padding: 0;
  }
  li {
    margin: 10px 0;
  }
  span {
    cursor: pointer;
  }
  </style>

마지막으로 홈페이지 구실을 하기위해 header, menu, article, side, footer를 구성하고 side에 캘린더와 일정 관리를 재사용하였다. 

SideBar.vue

<template>
    <div>
      <Calendar :events="events" />
      <Schedule @update-events="updateEvents" />
    </div>
  </template>
  
  <script>
  import Calendar from './Calendar.vue';
  import Schedule from './Schedule.vue';
  
  export default {
    name: 'SideBar',
    components: {
      Calendar,
      Schedule
    },
    data() {
      return {
        events: JSON.parse(localStorage.getItem('events') || '[]')
      };
    },
    methods: {
      updateEvents(events) {
        this.events = events;
      }
    }
  };
  </script>

결과물과 만드는 과정은 아래 링크를 통해서 확인할 수 있다. 단 이 페이지는 최종 완성물이 아니라서 최적화 과정을 거쳐서 자신만의 웹페이지를 만들 수 있으면 한다. 

My Personal Website (jocular-boba-1b5d4b.netlify.app)

 

My Personal Website

 

jocular-boba-1b5d4b.netlify.app