Task Manager

개인 프로젝트로 개발한 단위 테스크 관리 앱을 소개합니다.

이 앱은 PC와 모바일에서 모두 사용할 수 있으며 , 주 단위로 일정을 관리하고 실시간으로 데이터를 동기화할 수 있는 기능을 갖추고 있습니다.


1. 프로젝트 개요

이번 프로젝트의 목표는 단순하지만 효율적인 개인 일정 관리 도구를 만드는 것이었습니다.
매주 해야 할 일을 추가하고 , 색깔별로 실패 , 완료 , 진행중인 테스크를 하며 , 필요에 따라 시간과 날짜를 자유롭게 수정할 수 있습니다.

1-1. PC










1-2. 모바일












 


2. 주요 기능 소개

2-1. 기본 기능

앱의 핵심 기능은 테스크 관리입니다.

  • 테스크 추가: 새로운 일을 손쉽게 등록

  • 테스크 삭제: 필요 없는 항목 제거

  • 테스크 완료: 완료된 일을 체크하여 관리

  • 시간과 날짜 수정: 계획 변경에 유연하게 대응

















2-2. 실시간 데이터 연동

이 앱은 Firebase를 활용하여 PC와 모바일 간 데이터를 실시간으로 동기화합니다.
즉 , 한 기기에서 테스크를 추가하거나 수정하면 , 다른 기기에서도 즉시 반영됩니다.









 


2-3. 재사용 가능한 UI 컴포넌트

개발 과정에서 반복되는 기능을 재사용 가능한 컴포넌트로 제작했습니다.

  • CalendarView: 날짜를 선택할 수 있는 달력 컴포넌트












  • Time Selector: 시간을 선택할 수 있는 컴포넌트







이 컴포넌트들은 다른 프로젝트에서도 재사용할 수 있도록 설계했습니다.


3. 소스 코드

3-1. CalendarView.cs
using System;
using System.Collections.Generic;
using PB.ATTRIBUTE;
using PB.BUTTON;
using TMPro;
using UnityEngine;

namespace PB.UI
{
    public class PCalendarView : MonoBehaviour
    {
        [SerializeField] List<PCalendarDate> dates = new();
        [SerializeField] TextMeshProUGUI tmpDisplayDate;
        [SerializeField] TextMeshProUGUI tmpSelectedDate;
        [SerializeField] PButton btnPrevMonth;
        [SerializeField] PButton btnNextMonth;

        [PAttribute_ShowOnly] public bool Interactable = true;

        // Current Selected
        public (int year, int month, int day) SelectedDate => selectedDate;
        private (int year, int month, int day) selectedDate;
        public int SelectedYear => selectedDate.year;
        public int SelectedMonth => selectedDate.month;
        public int SelectedDay => selectedDate.day;

        // Current Displayed
        private (int year, int month) displayDate;
        private int displayYear { get => displayDate.year; set => displayDate.year = value; }
        private int displayMonth { get => displayDate.month; set => displayDate.month = value; }


        private void Awake()
        {
            btnPrevMonth.onClick.AddListener(OnPrevMonth);
            btnNextMonth.onClick.AddListener(OnNextMonth);
        }

        public void Init(int year, int month, int day)
        {
            displayDate = (year, month);
            selectedDate = (year, month, day);
            SetupCalendar(year, month);
        }

        private void SetupCalendar(int year, int month)
        {
            var firstDayOfMonth = new DateTime(year, month, 1);
            var dayOfWeek = (int)firstDayOfMonth.DayOfWeek;
            var startIndex = dayOfWeek == 0 ? 6 : dayOfWeek - 1;
            var daysInMonth = DateTime.DaysInMonth(year, month);
            var totalDates = dates.Count;
            var dateIndex = 0;

            for (; dateIndex < startIndex && dateIndex < totalDates; dateIndex++)
            {
                dates[dateIndex].SetEmpty();
            }

            for (int day = 1; day <= daysInMonth && dateIndex < totalDates; day++, dateIndex++)
            {
                var date = (year, month, day);
                dates[dateIndex].Init(date, OnDateSelected);
            }

            var sameYear = selectedDate.year == year;
            var sameMonth = selectedDate.month == month;

            if (sameYear && sameMonth)
            {
                dates.Find(d => d.Date == selectedDate)?.SetSelected(true);
            }

            for (; dateIndex < totalDates; dateIndex++)
            {
                dates[dateIndex].SetEmpty();
            }

            tmpDisplayDate.text = $"{year}{month:D2}";
            tmpSelectedDate.text = $"{selectedDate.year} / {selectedDate.month:D2} / {selectedDate.day:D2}";
        }

        private void OnDateSelected((int year, int month, int day) date)
        {
            if (selectedDate == date) return;
            if (Interactable == false) return;

            dates.Find(d => d.Date == selectedDate)?.SetSelected(false);
            dates.Find(d => d.Date == date)?.SetSelected(true);

            selectedDate = date;
            tmpSelectedDate.text = $"{selectedDate.year} / {selectedDate.month:D2} / {selectedDate.day:D2}";
        }

        public void OnPrevMonth()
        {
            if (Interactable == false) return;

            if (displayMonth == 1)
            {
                displayYear--;
                displayMonth = 12;
            }
            else displayMonth--;

            SetupCalendar(displayYear, displayMonth);
        }

        public void OnNextMonth()
        {
            if (Interactable == false) return;
           
            if (displayMonth == 12)
            {
                displayYear++;
                displayMonth = 1;
            }
            else displayMonth++;

            SetupCalendar(displayYear, displayMonth);
        }
    }
}


3-2. TimeSelector.cs
using PB.ATTRIBUTE;
using PB.BUTTON;
using TMPro;
using UnityEngine;

namespace PB.UI
{
    public class PTimeSelector : MonoBehaviour
    {
        [PAttribute_Header("Button")]
        [SerializeField] PPressButton increaseHour;
        [SerializeField] PPressButton decreaseHour;
        [SerializeField] PPressButton increaseMinute;
        [SerializeField] PPressButton decreaseMinute;

        [PAttribute_Header("Text")]
        [SerializeField] TextMeshProUGUI tmpHour;
        [SerializeField] TextMeshProUGUI tmpMinute;

        [PAttribute_ShowOnly] public bool Interactable = true;

        public (int hour, int minute) SelectedTime => (selectedHour, selectedMinute);
        public int SelectedHour => selectedHour;
        public int SelectedMinute => selectedMinute;
        private int selectedHour;
        private int selectedMinute;

        private const int MaxHours = 24;
        private const int MaxMinutes = 60;

        private void Awake()
        {
            increaseHour.onPress.AddListener(OnIncreaseHour);
            decreaseHour.onPress.AddListener(OnDecreaseHour);
            increaseMinute.onPress.AddListener(OnIncreaseMinute);
            decreaseMinute.onPress.AddListener(OnDecreaseMinute);

            increaseHour.onClick.AddListener(OnIncreaseHour);
            decreaseHour.onClick.AddListener(OnDecreaseHour);
            increaseMinute.onClick.AddListener(OnIncreaseMinute);
            decreaseMinute.onClick.AddListener(OnDecreaseMinute);
        }

        public void Init(int hour, int minute)
        {
            selectedHour = Mathf.Clamp(hour, 0, MaxHours - 1);
            selectedMinute = Mathf.Clamp(minute, 0, MaxMinutes - 1);
            UpdateHourText();
            UpdateMinuteText();
        }

        private void OnIncreaseHour()
        {
            if (Interactable == false) return;
            selectedHour = (selectedHour + 1) % MaxHours;
            UpdateHourText();
        }

        private void OnDecreaseHour()
        {
            if (Interactable == false) return;
            selectedHour = (selectedHour - 1 + MaxHours) % MaxHours;
            UpdateHourText();
        }

        private void OnIncreaseMinute()
        {
            if (Interactable == false) return;
            selectedMinute = (selectedMinute + 1) % MaxMinutes;
            UpdateMinuteText();
        }

        private void OnDecreaseMinute()
        {
            if (Interactable == false) return;
            selectedMinute = (selectedMinute - 1 + MaxMinutes) % MaxMinutes;
            UpdateMinuteText();
        }

        private void UpdateHourText()
        {
            tmpHour.text = selectedHour.ToString("D2");
        }

        private void UpdateMinuteText()
        {
            tmpMinute.text = selectedMinute.ToString("D2");
        }
    }
}