장단콩 보드게임 & 미니어처 게임 클럽

일정 캘린더 리디자인 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: board.html의 일정 캘린더를 표(<table>) 느낌의 고정 60px 셀 레이아웃에서, 반응형 CSS Grid 카드형 캘린더로 리디자인한다. 오늘/주말 강조, 이벤트 pill 축약(+N), 클릭 시 팝오버로 전체 일정 표시 기능을 추가한다.

Architecture: assets/js/board.jsrenderCalendar() / buildCalendar()<table> 생성에서 CSS Grid <div> 생성으로 교체한다. assets/css/board.css에 새 그리드/셀/팝오버 스타일을 추가하고 미사용 .fc-* 규칙을 제거한다. 의존성 추가 없음 — 순수 JS/CSS.

Tech Stack: Vanilla JS (ES module), Firebase Firestore (기존 loadEvents() 그대로), CSS Grid.

Global Constraints


File Structure


Task 1: 그리드 레이아웃 뼈대로 교체 (테이블 제거)

Files:

Interfaces:

이 태스크는 순수 리팩터링(시각적 변화 최소, 구조만 grid로 전환)이라 테스트는 “레이아웃이 깨지지 않았는지” 수동 확인으로 대체한다. 자동화 단위 테스트가 없는 프로젝트이므로, 브라우저에서 직접 확인하는 것이 유일하고 적절한 검증 수단이다.

assets/css/board.css에서 아래 4줄을 삭제한다 (파일 11~15번째 줄):

.fc-event { cursor: pointer; border-radius: 4px !important; font-size: 0.78rem !important; }
.fc-toolbar-title { font-size: 1rem !important; font-weight: 700 !important; }
.fc-button { background: #1a1a1a !important; border-color: #1a1a1a !important; font-size: 0.8rem !important; padding: 5px 12px !important; border-radius: 5px !important; }
.fc-button:hover { background: #333 !important; border-color: #333 !important; }
.fc-button-active { background: #444 !important; border-color: #444 !important; }

assets/css/board.css 끝에 추가:

.cal-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 14px;
}
.cal-toolbar strong { font-size: 1.05rem; font-weight: 700; color: #1a1a1a; }
.cal-nav-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid #e8e8e4;
  background: #fff;
  color: #1a1a1a;
  font-size: 0.85rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s, border-color 0.12s;
}
.cal-nav-btn:hover { background: #f5f5f3; border-color: #d8d8d4; }

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  position: relative;
}
.cal-weekday {
  text-align: center;
  font-size: 0.78rem;
  font-weight: 600;
  color: #888;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 8px 0;
  border-bottom: 1px solid #e8e8e4;
}
.cal-cell {
  aspect-ratio: 1 / 0.85;
  border-right: 1px solid #f0f0ee;
  border-bottom: 1px solid #f0f0ee;
  padding: 6px;
  overflow: hidden;
  position: relative;
  cursor: default;
}
.cal-cell.is-empty { cursor: default; }
.cal-grid .cal-cell:nth-child(7n) { border-right: none; }
.cal-day-num {
  font-size: 0.82rem;
  color: #1a1a1a;
  display: inline-block;
}

assets/js/board.js의 59~130번째 줄 전체(function renderCalendar(events) { ... })를 아래로 교체:

function renderCalendar(events) {
  const now = new Date();
  let year = now.getFullYear();
  let month = now.getMonth(); // 0-indexed

  function buildCalendar() {
    const el = document.getElementById("calendar");
    const firstDay = new Date(year, month, 1).getDay();
    const daysInMonth = new Date(year, month + 1, 0).getDate();

    const monthNames = ["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"];
    const weekdayNames = ["","","","","","",""];

    // 이 달의 이벤트만 필터
    const monthEvents = {};
    events.forEach(e => {
      const d = e.eventDate?.toDate();
      if (!d) return;
      if (d.getFullYear() === year && d.getMonth() === month) {
        const day = d.getDate();
        if (!monthEvents[day]) monthEvents[day] = [];
        monthEvents[day].push(e);
      }
    });

    let html = `
      <div class="cal-toolbar">
        <button class="cal-nav-btn" id="cal-prev">◀</button>
        <strong>${year}${monthNames[month]}</strong>
        <button class="cal-nav-btn" id="cal-next">▶</button>
      </div>
      <div class="cal-grid">`;

    weekdayNames.forEach((wd, i) => {
      html += `<div class="cal-weekday">${wd}</div>`;
    });

    const totalCells = firstDay + daysInMonth;
    const totalRows = Math.ceil(totalCells / 7);

    let day = 1;
    for (let i = 0; i < totalRows * 7; i++) {
      const col = i % 7;
      if (i < firstDay || day > daysInMonth) {
        html += `<div class="cal-cell is-empty"></div>`;
      } else {
        const evts = monthEvents[day] || [];
        html += `<div class="cal-cell" data-day="${day}"><span class="cal-day-num">${day}</span></div>`;
        day++;
      }
    }

    html += `</div>`;
    el.innerHTML = html;

    document.getElementById("cal-prev").addEventListener("click", () => {
      month--;
      if (month < 0) { month = 11; year--; }
      buildCalendar();
    });
    document.getElementById("cal-next").addEventListener("click", () => {
      month++;
      if (month > 11) { month = 0; year++; }
      buildCalendar();
    });
  }

  buildCalendar();
}

이 단계에서는 아직 이벤트 pill, 오늘/주말 강조, 팝오버가 없다 — Task 2, 3에서 추가한다. monthEvents는 계산만 해두고 다음 태스크에서 사용한다.

로컬 Jekyll 서버 실행 후 /board/ 페이지 접속 (없다면 bundle exec jekyll serve 실행):

bundle exec jekyll serve

브라우저에서 http://localhost:4000/board/ 접속해 확인:

git add assets/js/board.js assets/css/board.css
git commit -m "refactor: replace table-based calendar with CSS Grid layout"

Task 2: 오늘/주말 강조 + 이벤트 pill (최대 2개 + N개 더보기)

Files:

Interfaces:

assets/css/board.css 끝에 추가:

.cal-weekday.cal-sun { color: #c62828; }
.cal-weekday.cal-sat { color: #1565c0; }
.cal-cell.is-sun .cal-day-num { color: #c62828; }
.cal-cell.is-sat .cal-day-num { color: #1565c0; }
.cal-cell.is-today { background: #fff8e1; }
.cal-cell.is-today .cal-day-num {
  background: #1a1a1a;
  color: #fff;
  border-radius: 50%;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.76rem;
}

.cal-pill {
  margin-top: 3px;
  font-size: 0.68rem;
  padding: 2px 6px;
  border-radius: 4px;
  background: #eef2ff;
  color: #3949ab;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.cal-pill:hover { background: #e0e6fb; }
.cal-more {
  margin-top: 3px;
  font-size: 0.68rem;
  padding: 2px 6px;
  border-radius: 4px;
  background: #f0f0ee;
  color: #666;
  cursor: pointer;
  display: inline-block;
}
.cal-more:hover { background: #e5e5e1; }

assets/js/board.jsbuildCalendar 내부, Task 1에서 작성한 이 블록:

      } else {
        const evts = monthEvents[day] || [];
        html += `<div class="cal-cell" data-day="${day}"><span class="cal-day-num">${day}</span></div>`;
        day++;
      }

를 아래로 교체:

      } else {
        const evts = monthEvents[day] || [];
        const isToday = year === now.getFullYear() && month === now.getMonth() && day === now.getDate();
        const cellClasses = ["cal-cell"];
        if (isToday) cellClasses.push("is-today");
        if (col === 0) cellClasses.push("is-sun");
        if (col === 6) cellClasses.push("is-sat");

        const shown = evts.slice(0, 2);
        const rest = evts.length - shown.length;
        let evtHtml = shown.map(e =>
          `<div class="cal-pill" onclick="location.href='/post/?id=${e.id}'">${e.title}</div>`
        ).join("");
        if (rest > 0) {
          evtHtml += `<div class="cal-more" data-day="${day}">+${rest}개</div>`;
        }

        html += `<div class="${cellClasses.join(" ")}" data-day="${day}"><span class="cal-day-num">${day}</span>${evtHtml}</div>`;
        day++;
      }

그리고 같은 함수 내 요일 헤더 생성 부분:

    weekdayNames.forEach((wd, i) => {
      html += `<div class="cal-weekday">${wd}</div>`;
    });

를 아래로 교체 (일요일/토요일 색상 클래스 부여):

    weekdayNames.forEach((wd, i) => {
      const cls = i === 0 ? " cal-sun" : i === 6 ? " cal-sat" : "";
      html += `<div class="cal-weekday${cls}">${wd}</div>`;
    });

/board/ 페이지 새로고침 후 확인:

git add assets/js/board.js assets/css/board.css
git commit -m "feat: add today/weekend highlighting and event pill collapsing"

Task 3: 팝오버 (셀/+N 클릭 시 전체 일정 드롭다운)

Files:

Interfaces:

assets/css/board.css 끝에 추가:

.cal-popover {
  position: absolute;
  z-index: 20;
  background: #fff;
  border: 1px solid #e8e8e4;
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.12);
  padding: 8px;
  min-width: 160px;
  max-width: 240px;
}
.cal-popover-item {
  font-size: 0.8rem;
  padding: 6px 8px;
  border-radius: 5px;
  cursor: pointer;
  color: #1a1a1a;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cal-popover-item:hover { background: #f5f5f3; }

assets/js/board.jsbuildCalendar 함수 안, el.innerHTML = html; 바로 다음 줄(현재 document.getElementById("cal-prev")... 이전)에 아래 코드를 삽입:

    el.innerHTML = html;

    function closePopover() {
      const existing = document.querySelector(".cal-popover");
      if (existing) existing.remove();
      document.removeEventListener("click", onOutsideClick);
    }
    function onOutsideClick(ev) {
      if (!ev.target.closest(".cal-popover") && !ev.target.closest("[data-day]")) {
        closePopover();
      }
    }
    function openPopover(cellEl, day) {
      closePopover();
      const evts = monthEvents[day] || [];
      if (!evts.length) return;
      const pop = document.createElement("div");
      pop.className = "cal-popover";
      pop.innerHTML = evts.map(e =>
        `<div class="cal-popover-item" onclick="location.href='/post/?id=${e.id}'">${e.title}</div>`
      ).join("");
      const rect = cellEl.getBoundingClientRect();
      const gridRect = el.querySelector(".cal-grid").getBoundingClientRect();
      pop.style.top = (rect.bottom - gridRect.top + 4) + "px";
      pop.style.left = (rect.left - gridRect.left) + "px";
      el.querySelector(".cal-grid").appendChild(pop);
      setTimeout(() => document.addEventListener("click", onOutsideClick), 0);
    }

    el.querySelectorAll(".cal-cell[data-day]").forEach(cell => {
      cell.addEventListener("click", (ev) => {
        if (ev.target.closest(".cal-pill")) return; // pill 클릭은 바로 이동, 팝오버 안 띄움
        openPopover(cell, cell.dataset.day);
      });
    });

/board/ 페이지 새로고침 후 확인:

git add assets/js/board.js assets/css/board.css
git commit -m "feat: add click-to-popover day view for calendar cells"

Self-Review Notes