From 56e4c0c29b4e0df5b7419af731116f02441a9e9b Mon Sep 17 00:00:00 2001 From: jisangs Date: Sun, 6 Sep 2026 11:46:33 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B2=B4=ED=97=98=20=EA=B3=84=EC=A0=95?= =?UTF-8?q?=EB=8F=84=20=EC=97=B0=EC=9E=A5=20=EC=8A=B9=EC=9D=B8=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +-- .../[maestroId]/ExtensionRequestsSection.tsx | 6 +---- app/(admin)/maestros/[maestroId]/page.tsx | 1 - lib/extension-requests.ts | 26 ++++++++++++------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c179019..9eabfdb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Admin panel for the chocomae service (mouse-typing), extracted into a standalone - **Docker service names must be unique per environment**: Docker registers each `services:` key as a DNS alias in shared networks. The stage service is `chocoadmin-stage` — **never rename it to `chocoadmin`**. Identical names cause DNS round-robin in `proxy-network`; NPM's `set $server "chocoadmin"` will randomly route requests to either container, mixing production and stage traffic. - **Server actions must be module-level named exports**: Inline `action={async () => { "use server"; ... }}` closures get a new action ID on every build. After redeployment the browser sends the stale ID and Next.js responds `Failed to find Server Action`. Always extract to a top-level export in a separate `actions.ts` file (e.g. `app/(admin)/actions.ts`). - **After redeployment, reload NPM nginx**: Run `docker exec npm nginx -s reload` on the NAS to flush the upstream DNS cache. Without this NPM may continue routing to the previous container's IP. -- **Extension requests** (`POST /api/maestros/[id]/extension-requests`) require `ActivateStatus = 2` (ACTIVE). Trial (1) and cancelled (100) accounts are rejected with 400. Approval (`PATCH /api/extension-requests/[id]`) re-checks `ActivateStatus` and throws 409 if not ACTIVE (transaction rolls back the atomic claim). +- **Extension requests** (`POST /api/maestros/[id]/extension-requests`) accept `ActivateStatus = 2` (ACTIVE) or `1` (TRIAL). PENDING (0) and CANCELED (100) are rejected with 400. `AccountType` is not restricted — any type (paid tier or trial type) may request an extension. Approval (`PATCH /api/extension-requests/[id]`) mirrors this — accepts ACTIVE and TRIAL, promotes trial to ACTIVE on approval. PENDING/CANCELED throw 409 (transaction rolls back the atomic claim). - **Upgrade requests**: TRIAL accounts (`ActivateStatus = 1`) may request the **same tier** (trial-to-paid conversion, `requestedAccountType >= AccountType`). ACTIVE accounts must use a strictly higher tier (`>`). At approval time, this direction is re-verified against the maestro's **current** `AccountType` — if it became invalid (e.g., admin manually raised the tier), the approval throws 409 and rolls back. - Admin-initiated extension/upgrade registrations do **not** send an email (unlike self-registration flows which send payment-instruction emails). This is intentional. @@ -85,7 +85,7 @@ Always record a log entry after a successful DB change. Use these `Type` values: - Phase 10: Maestro info edit form (`PATCH /api/maestros/[id]`) + student list with server-side pagination (`GET /api/maestros/[id]/students`). - Phase 11: Email auto-send via Nodemailer after extension/upgrade approval (`lib/mail.ts`). Upgrade email edge-case verification (trial→paid, paid→paid) still pending. - Phase 12: Admin-initiated extension/upgrade request registration from maestro detail page. - - `POST /api/maestros/[id]/extension-requests` — inserts into `maestro_extension` using maestro's current `AccountType` read server-side; rejects trial account types. + - `POST /api/maestros/[id]/extension-requests` — inserts into `maestro_extension` using maestro's current `AccountType` read server-side. - `POST /api/maestros/[id]/upgrade-requests` — inserts into `maestro_upgrade` inside a transaction; validates `requestedAccountType > maestro.AccountType`. - `ExtensionRequestsSection.tsx`, `UpgradeRequestsSection.tsx` — client components with inline action UI; call `router.refresh()` on success. - `PAID_ACCOUNT_TYPE_VALUES` consolidated in `lib/constants.ts` — shared by `lib/maestros.ts`, `lib/extension-requests.ts`, `lib/upgrade-requests.ts`, and both new API routes. diff --git a/app/(admin)/maestros/[maestroId]/ExtensionRequestsSection.tsx b/app/(admin)/maestros/[maestroId]/ExtensionRequestsSection.tsx index bc840cd..e506404 100644 --- a/app/(admin)/maestros/[maestroId]/ExtensionRequestsSection.tsx +++ b/app/(admin)/maestros/[maestroId]/ExtensionRequestsSection.tsx @@ -5,20 +5,17 @@ import { useRouter } from "next/navigation"; import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { PAID_ACCOUNT_TYPE_VALUES } from "@/lib/constants"; import { formatDateTime, getAccountTypeLabel, getRequestStatusLabel } from "@/lib/utils"; import type { MaestroDetail } from "@/lib/maestros"; type ExtensionRequestsSectionProps = { maestroID: number; - accountType: number; totalCount: number; extensionRequests: MaestroDetail["extensionRequests"]; }; export function ExtensionRequestsSection({ maestroID, - accountType, totalCount, extensionRequests, }: ExtensionRequestsSectionProps) { @@ -27,7 +24,6 @@ export function ExtensionRequestsSection({ const [isCreating, setIsCreating] = useState(false); const [errorMessage, setErrorMessage] = useState(""); - const isPaidAccount = (PAID_ACCOUNT_TYPE_VALUES as readonly number[]).includes(accountType); const isDisabled = isCreating || isPending; async function handleCreate() { @@ -73,7 +69,7 @@ export function ExtensionRequestsSection({