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

Event Registration Count 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: Ensure deleting a game-event post atomically removes its registration count from the author’s statistics, and reset only the existing “다소” member’s registration count to zero.

Architecture: Extract the pure, boundary-safe postCount decrement into a small logic module with Node tests. In the post page, replace direct deletion with a Firestore transaction that re-reads the post, conditionally updates stats/global, then deletes the post. The historical correction is a one-time Firestore Console edit to one nested field after identifying the exact user UID.

Tech Stack: Browser ES modules, Firebase Firestore v10.12.2 transactions, Node built-in node:test and node:assert.

Global Constraints


File structure

Task 1: Add and verify the safe count-decrement calculation

Files:

Interfaces:

Create assets/js/event-deletion-logic.test.mjs with:

import assert from "node:assert";
import { test } from "node:test";
import { nextEventPostCount } from "./event-deletion-logic.js";

test("nextEventPostCount: positive count decreases by one", () => {
  assert.strictEqual(nextEventPostCount(3), 2);
});

test("nextEventPostCount: zero never becomes negative", () => {
  assert.strictEqual(nextEventPostCount(0), 0);
});

test("nextEventPostCount: missing or invalid count becomes zero", () => {
  assert.strictEqual(nextEventPostCount(undefined), 0);
  assert.strictEqual(nextEventPostCount(-2), 0);
  assert.strictEqual(nextEventPostCount("3"), 0);
});

Run: node --test assets/js/event-deletion-logic.test.mjs

Expected: FAIL because assets/js/event-deletion-logic.js does not yet exist.

Create assets/js/event-deletion-logic.js with:

export function nextEventPostCount(currentPostCount) {
  if (!Number.isInteger(currentPostCount) || currentPostCount <= 0) return 0;
  return currentPostCount - 1;
}

Run: node --test assets/js/event-deletion-logic.test.mjs

Expected: PASS with 3 passing subtests and 0 failures.

git add assets/js/event-deletion-logic.js assets/js/event-deletion-logic.test.mjs
git commit -m "test: cover event registration count decrement"

Task 2: Make event deletion and its statistic update atomic

Files:

Interfaces:

Add this test to assets/js/event-deletion-logic.test.mjs:

test("nextEventPostCount: one decreases to zero", () => {
  assert.strictEqual(nextEventPostCount(1), 0);
});

Run: node --test assets/js/event-deletion-logic.test.mjs

Expected: PASS with 4 passing subtests and 0 failures.

Below the existing imports in assets/js/post.js, add:

import { nextEventPostCount } from "./event-deletion-logic.js";

Above loadPost(), add:

async function deletePostWithEventStats() {
  const postRef = doc(db, "posts", postId);
  const statsRef = doc(db, "stats", "global");

  await runTransaction(db, async (tx) => {
    const latestPostSnap = await tx.get(postRef);
    if (!latestPostSnap.exists()) throw new Error("게시글을 찾을 수 없습니다.");

    const latestPost = latestPostSnap.data();
    if (latestPost.type === "event" && latestPost.authorUid) {
      const statsSnap = await tx.get(statsRef);
      const currentPostCount = statsSnap.data()?.members?.[latestPost.authorUid]?.postCount;
      tx.set(statsRef, {
        updatedAt: serverTimestamp(),
        [`members.${latestPost.authorUid}.postCount`]: nextEventPostCount(currentPostCount)
      }, { merge: true });
    }

    tx.delete(postRef);
  });
}

The transaction reads all required documents before it writes. It uses the latest stored post, so a stale page cannot decrement a notice or a changed author incorrectly. tx.set(..., { merge: true }) writes only the required nested field if the stats document or member entry was missing.

Replace the existing delete-button handler body:

if (!confirm("게시글을 삭제하시겠습니까?")) return;
await deleteDoc(doc(db, "posts", postId));
location.href = "/board/";

with:

if (!confirm("게시글을 삭제하시겠습니까?")) return;
try {
  await deletePostWithEventStats();
  location.href = "/board/";
} catch (e) {
  console.error("post deletion failed", e);
  alert("게시글 삭제 중 오류가 발생했습니다. 다시 시도해주세요.");
}

Keep the existing deleteDoc import because comment deletion later in the same file still uses it.

Run: node --check assets/js/post.js && node --check assets/js/event-deletion-logic.js && node --test assets/js/event-deletion-logic.test.mjs

Expected: both syntax checks exit with code 0; the test runner reports 4 passing subtests and 0 failures.

  1. As an authorized event author or administrator, record a test author’s stats/global.members.<uid>.postCount as N.
  2. Delete a test event from /post/?id=<eventId>.
  3. Confirm the event document is absent and postCount is max(N - 1, 0).
  4. Create and delete a test notice, then confirm the same author’s postCount is unchanged.
  5. Attempt an event deletion while the browser is offline; confirm the error is shown and, after reconnecting, both the event and count remain unchanged until a successful retry.
git add assets/js/post.js assets/js/event-deletion-logic.js assets/js/event-deletion-logic.test.mjs
git commit -m "fix: decrement event registration count on deletion"

Task 3: Perform the approved one-time “다소” correction

Files:

Interfaces:

In Firebase Console → Firestore Database → users, find the document whose nickname field is exactly 다소. Copy its document ID as <dasoUid>. Confirm the nickname in that document; do not infer the UID from a display label or email.

In Firebase Console → Firestore Database → statsglobalmembers<dasoUid>, record the current nickname, attendCount, ratingCount, and ratingSum values if present.

Edit only postCount in that member map and set its numeric value to 0. Save the document. Do not use the stats rebuild action: it would alter other members and recompute rather than explicitly reset this user’s count.

Refresh stats/global. Confirm members.<dasoUid>.postCount is numeric 0 and the fields recorded in Step 2 are unchanged. Refresh /stats/ as an administrator and verify the 일정 등록 랭킹 no longer counts the historical value for 다소.

Include the exact verified UID only in a private deployment/change log if one exists; do not commit personal account identifiers to this public repository. Report that the scoped Firestore correction completed and only postCount changed.

Plan self-review