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

내 정보 페이지 방문 통계 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: mypage.html에 “아지트 방문 일수”와 “참여 게임 세션 수”를 표시한다.

Architecture: 순수 계산 로직을 assets/js/mypage-logic.js에 분리해 유닛 테스트로 검증하고, assets/js/mypage.js에서 Firestore posts 쿼리 결과를 이 로직에 넣어 실시간 계산한 뒤 mypage.html의 새 섹션에 렌더링한다. 저장 카운터 없음 — 매 페이지 로드 시 재계산.

Tech Stack: Vanilla JS (ES modules), Firebase Firestore v10 modular SDK, node:test for unit tests.

Global Constraints


Task 1: 순수 계산 로직 (mypage-logic.js)

Files:

Interfaces:

assets/js/mypage-logic.test.mjs:

import assert from "node:assert";
import { test } from "node:test";
import { computeVisitStats } from "./mypage-logic.js";

function ts(dateStr) {
  const d = new Date(dateStr);
  return { toDate: () => d };
}

test("computeVisitStats: 참여 이벤트가 없으면 0/0", () => {
  assert.deepStrictEqual(computeVisitStats([], "u1"), { visitDays: 0, participatedSessions: 0 });
});

test("computeVisitStats: closedAt 없는 이벤트는 제외", () => {
  const events = [
    { type: "event", closedAt: null, confirmedAttendees: ["u1"], eventDate: ts("2026-07-01T10:00:00") }
  ];
  assert.deepStrictEqual(computeVisitStats(events, "u1"), { visitDays: 0, participatedSessions: 0 });
});

test("computeVisitStats: confirmedAttendees에 uid 없으면 제외 (노쇼)", () => {
  const events = [
    { type: "event", closedAt: ts("2026-07-01T20:00:00"), confirmedAttendees: ["u2"], eventDate: ts("2026-07-01T10:00:00") }
  ];
  assert.deepStrictEqual(computeVisitStats(events, "u1"), { visitDays: 0, participatedSessions: 0 });
});

test("computeVisitStats: type이 event가 아니면 제외", () => {
  const events = [
    { type: "notice", closedAt: ts("2026-07-01T20:00:00"), confirmedAttendees: ["u1"], eventDate: ts("2026-07-01T10:00:00") }
  ];
  assert.deepStrictEqual(computeVisitStats(events, "u1"), { visitDays: 0, participatedSessions: 0 });
});

test("computeVisitStats: 같은 날 2개 세션 참여 시 방문일수 1, 세션수 2", () => {
  const events = [
    { type: "event", closedAt: ts("2026-07-01T20:00:00"), confirmedAttendees: ["u1"], eventDate: ts("2026-07-01T10:00:00") },
    { type: "event", closedAt: ts("2026-07-01T21:00:00"), confirmedAttendees: ["u1"], eventDate: ts("2026-07-01T18:00:00") }
  ];
  assert.deepStrictEqual(computeVisitStats(events, "u1"), { visitDays: 1, participatedSessions: 2 });
});

test("computeVisitStats: 다른 날 참여는 각각 별도 방문일로 계산", () => {
  const events = [
    { type: "event", closedAt: ts("2026-07-01T20:00:00"), confirmedAttendees: ["u1"], eventDate: ts("2026-07-01T10:00:00") },
    { type: "event", closedAt: ts("2026-07-02T20:00:00"), confirmedAttendees: ["u1"], eventDate: ts("2026-07-02T10:00:00") }
  ];
  assert.deepStrictEqual(computeVisitStats(events, "u1"), { visitDays: 2, participatedSessions: 2 });
});

test("computeVisitStats: eventDate가 순수 Date 객체여도 동작", () => {
  const events = [
    { type: "event", closedAt: ts("2026-07-01T20:00:00"), confirmedAttendees: ["u1"], eventDate: new Date("2026-07-01T10:00:00") }
  ];
  assert.deepStrictEqual(computeVisitStats(events, "u1"), { visitDays: 1, participatedSessions: 1 });
});

Run: node --test assets/js/mypage-logic.test.mjs Expected: FAIL — Cannot find module './mypage-logic.js'

assets/js/mypage-logic.js:

function toDateOrNull(value) {
  if (!value) return null;
  if (value instanceof Date) return value;
  if (typeof value.toDate === "function") return value.toDate();
  return null;
}

function toDayKey(date) {
  const dt = new Date(date);
  dt.setHours(0, 0, 0, 0);
  return dt.getTime();
}

export function computeVisitStats(events, uid) {
  const confirmedEvents = events.filter(
    (p) => p.type === "event" && !!p.closedAt && (p.confirmedAttendees || []).includes(uid)
  );

  const participatedSessions = confirmedEvents.length;

  const dayKeys = new Set(
    confirmedEvents
      .map((p) => toDateOrNull(p.eventDate))
      .filter((d) => d instanceof Date)
      .map(toDayKey)
  );

  return { visitDays: dayKeys.size, participatedSessions };
}

Run: node --test assets/js/mypage-logic.test.mjs Expected: PASS, # tests 7, # fail 0

git add assets/js/mypage-logic.js assets/js/mypage-logic.test.mjs
git commit -m "feat: add pure visit-stats calculation for mypage"

Task 2: mypage.js에 통계 조회/렌더링 연결

Files:

Interfaces:

mypage.html#section-trophies 바로 위에 삽입 (현재 82번째 줄 <div id="section-trophies" class="mb-4"> 앞):

      <div id="section-visit-stats" class="mb-4">
        <div style="font-size:0.78rem;font-weight:600;color:var(--text-muted);margin-bottom:4px">활동 기록</div>
        <div id="visit-stats-text" style="font-size:0.92rem;color:var(--text-secondary)"></div>
      </div>

assets/js/mypage.js 최상단 import 블록(현재 1~10번째 줄)에 추가:

import { collection, query, where, getDocs } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js";
import { computeVisitStats } from "./mypage-logic.js";

기존 doc, deleteDoc, getDoc, updateDoc import 줄(현재 4~6번째 줄)과 합쳐도 되지만, 별도 줄로 추가해도 무방.

assets/js/mypage.js의 기존 rating stats try/catch 블록(현재 37~52번째 줄, computeAverages 호출 부분) 바로 뒤, await setupParties(user); 호출 전에 추가:

  await renderVisitStats(user);

파일 하단에 (예: showError 함수 뒤, 96~99번째 줄 부근) 새 함수 추가:

async function renderVisitStats(user) {
  const el = document.getElementById("visit-stats-text");
  try {
    const q = query(
      collection(db, "posts"),
      where("type", "==", "event"),
      where("confirmedAttendees", "array-contains", user.uid)
    );
    const snap = await getDocs(q);
    const events = snap.docs.map((d) => d.data());
    const { visitDays, participatedSessions } = computeVisitStats(events, user.uid);
    el.textContent = `아지트 방문 일수 ${visitDays}일 · 참여 게임 세션 ${participatedSessions}회`;
  } catch (e) {
    console.error("방문 통계 로드 실패", e);
    el.textContent = "";
  }
}

Run: 로컬 Jekyll 서버 기동 후 로그인한 계정으로 /mypage/ 접속 (또는 기존 로컬 개발 서버 실행 방법 사용). Expected: “활동 기록” 라벨 아래 “아지트 방문 일수 N일 · 참여 게임 세션 M회” 텍스트가 표시되고, 콘솔에 에러가 없다. 참여 이력이 있는 테스트 계정으로 확인해 N, M이 실제 확정 참여 이력과 일치하는지 확인.

Run: node --test assets/js/*.test.mjs Expected: 기존 테스트 전부 PASS, Task 1의 mypage-logic.test.mjs 포함.

git add assets/js/mypage.js mypage.html
git commit -m "feat: show visit-day and session-count stats on mypage"

Self-Review Notes