전체 코드, 구조 검토 - P1 코드 수정 진행
This commit is contained in:
@@ -20,6 +20,9 @@ Admin panel for the chocomae service (mouse-typing), extracted into a standalone
|
||||
- Upgrade approval sets `AvailableActivateDateTime = NOW() + 1 year`. **Do not change `PlayerCount`** on upgrade.
|
||||
- When approving an extension or upgrade, close all other `Status = 1` requests for the same `MaestroID` by setting them to `Status = 2`, then set the approved request to `Status = 3`.
|
||||
- **Docker containers must set `TZ: Asia/Seoul`** so that date calculations (`setHours(0,0,0,0)`, `formatDate`) match the KST-based existing data in the shared MariaDB.
|
||||
- **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).
|
||||
- **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.
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -62,6 +65,8 @@ Always record a log entry after a successful DB change. Use these `Type` values:
|
||||
| `update_maestro_account_type` | AccountType changed (admin, new) | `{prev} -> {new}` (numeric) |
|
||||
| `update_maestro_available_date` | AvailableActivateDateTime changed (admin, new) | `{YYYY-MM-DD} -> {YYYY-MM-DD}` |
|
||||
| `update_maestro_allow_enter_code` | AllowEditEnterCode changed (admin, new) | `{prev} -> {new}` (0 or 1) |
|
||||
| `request_extension_maestro` | Admin-initiated extension request created | `{maestroName}({accountTypeLabel})` |
|
||||
| `request_upgrade_maestro` | Admin-initiated upgrade request created | `{registeredAccountTypeLabel} -> {requestedAccountTypeLabel}` |
|
||||
|
||||
`Remark` column is `char(100)` — keep values under 100 characters.
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import { useRouter } from "next/navigation";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ACCOUNT_TYPES } from "@/lib/constants";
|
||||
import { ACCOUNT_TYPES, ACTIVATE_STATUSES } from "@/lib/constants";
|
||||
import { formatDateTime, getAccountTypeLabel, getRequestStatusLabel } from "@/lib/utils";
|
||||
import type { MaestroDetail } from "@/lib/maestros";
|
||||
|
||||
type UpgradeRequestsSectionProps = {
|
||||
maestroID: number;
|
||||
accountType: number;
|
||||
activateStatus: number;
|
||||
totalCount: number;
|
||||
upgradeRequests: MaestroDetail["upgradeRequests"];
|
||||
};
|
||||
@@ -30,6 +31,7 @@ const selectClass =
|
||||
export function UpgradeRequestsSection({
|
||||
maestroID,
|
||||
accountType,
|
||||
activateStatus,
|
||||
totalCount,
|
||||
upgradeRequests,
|
||||
}: UpgradeRequestsSectionProps) {
|
||||
@@ -39,8 +41,10 @@ export function UpgradeRequestsSection({
|
||||
const [selectedAccountType, setSelectedAccountType] = useState<number | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const isTrial = activateStatus === ACTIVATE_STATUSES.TRIAL;
|
||||
const isUpgradeEnabled =
|
||||
selectedAccountType !== null && selectedAccountType > accountType;
|
||||
selectedAccountType !== null &&
|
||||
(isTrial ? selectedAccountType >= accountType : selectedAccountType > accountType);
|
||||
const isDisabled = isCreating || isPending;
|
||||
|
||||
async function handleUpgrade() {
|
||||
@@ -107,7 +111,7 @@ export function UpgradeRequestsSection({
|
||||
<option value="">요금제 선택</option>
|
||||
{upgradeOptions.map((opt) => (
|
||||
<option
|
||||
disabled={opt.value <= accountType}
|
||||
disabled={isTrial ? opt.value < accountType : opt.value <= accountType}
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
>
|
||||
|
||||
@@ -102,6 +102,7 @@ export default async function MaestroDetailPage({
|
||||
|
||||
<UpgradeRequestsSection
|
||||
accountType={maestro.accountType}
|
||||
activateStatus={maestro.activateStatus}
|
||||
maestroID={maestroID}
|
||||
totalCount={detail.counts.upgradeRequests}
|
||||
upgradeRequests={detail.upgradeRequests}
|
||||
|
||||
@@ -160,10 +160,14 @@
|
||||
- P0-1: extension `getActionTarget` select 전환 + db.ts Password 마스킹
|
||||
- P0-2: 승인/취소/거절 4개 함수 조건부 UPDATE 가드
|
||||
- P0-3: docker-compose `TZ: Asia/Seoul` + 날짜 문자열 변환 통일
|
||||
2. **Step 2 (P1 비즈니스 결정 → 반영)**
|
||||
- 결정 필요 3건: 연장 대상 ActivateStatus 제한(P1-1), 대행 등록 입금 메일(P1-7),
|
||||
승인 시 remark 기준값(P2-10)
|
||||
- 코드 반영: P1-2(TRIAL 동일 tier 허용), P1-3(승인 재검증), P1-4(트랜잭션), P1-5(신청 로그)
|
||||
2. **Step 2 (P1 비즈니스 결정 → 반영) ✅ 완료 (2026-07-04)**
|
||||
- P1-1: 연장 신청·승인 모두 ActivateStatus=2(ACTIVE) 필수 검증 추가
|
||||
- P1-2: TRIAL 계정의 동일 tier 업그레이드(체험→유료 전환) 허용
|
||||
- P1-3: 업그레이드 승인 시점에 현재 AccountType 기준 방향 재검증
|
||||
- P1-4: `createExtensionRequest` 트랜잭션 감싸기
|
||||
- P1-5: `request_extension_maestro` / `request_upgrade_maestro` 로그 추가
|
||||
- P1-7: 관리자 대행 등록 시 입금 메일 미발송 — 의도 확인, AGENTS.md에 명시
|
||||
- P2-10: 신청 시점 스냅샷 유지 — 변경 없음
|
||||
3. **Step 3 (P1-6 이메일 + 검증)**
|
||||
- emailSent 응답 + UI 경고, stage에서 연장/업그레이드(체험→유료, 유료→유료) 메일 실측
|
||||
(phase11 잔여 항목 겸)
|
||||
|
||||
@@ -139,11 +139,16 @@ export async function approveExtensionRequest(
|
||||
Name: true,
|
||||
Email: true,
|
||||
AvailableActivateDateTime: true,
|
||||
ActivateStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (request!.maestro.ActivateStatus !== ACTIVATE_STATUSES.ACTIVE) {
|
||||
throw new ApiError("활성화된 계정만 연장 승인이 가능합니다.", 409);
|
||||
}
|
||||
|
||||
const availableActivateDateTime = calculateExtendedAvailableDate(
|
||||
request!.maestro.AvailableActivateDateTime
|
||||
);
|
||||
@@ -185,9 +190,10 @@ export async function approveExtensionRequest(
|
||||
export async function createExtensionRequest(
|
||||
maestroID: number
|
||||
): Promise<{ maestroExtensionID: number; requestedDateTime: string; status: number }> {
|
||||
const maestro = await db.maestro.findUnique({
|
||||
return db.$transaction(async (tx) => {
|
||||
const maestro = await tx.maestro.findUnique({
|
||||
where: { MaestroID: maestroID },
|
||||
select: { AccountType: true },
|
||||
select: { AccountType: true, ActivateStatus: true, Name: true },
|
||||
});
|
||||
|
||||
if (!maestro) {
|
||||
@@ -198,7 +204,11 @@ export async function createExtensionRequest(
|
||||
throw new ApiError("유료 요금제 계정만 연장 신청이 가능합니다.", 400);
|
||||
}
|
||||
|
||||
const request = await db.maestro_extension.create({
|
||||
if (maestro.ActivateStatus !== ACTIVATE_STATUSES.ACTIVE) {
|
||||
throw new ApiError("활성화된 계정만 연장 신청이 가능합니다.", 400);
|
||||
}
|
||||
|
||||
const request = await tx.maestro_extension.create({
|
||||
data: {
|
||||
MaestroID: maestroID,
|
||||
AccountType: maestro.AccountType,
|
||||
@@ -212,11 +222,21 @@ export async function createExtensionRequest(
|
||||
},
|
||||
});
|
||||
|
||||
await tx.maestro_log.create({
|
||||
data: {
|
||||
Type: "request_extension_maestro",
|
||||
MaestroID: maestroID,
|
||||
LogDateTime: new Date(),
|
||||
Remark: buildExtensionLogRemark(maestro.Name, maestro.AccountType),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
maestroExtensionID: request.MaestroExtensionID,
|
||||
requestedDateTime: request.RequestedDateTime.toISOString(),
|
||||
status: request.Status,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelExtensionRequest(
|
||||
|
||||
+38
-3
@@ -143,10 +143,22 @@ export async function approveUpgradeRequest(
|
||||
RequestedAccountType: true,
|
||||
RegisteredActivateStatus: true,
|
||||
RegisteredAccountType: true,
|
||||
maestro: { select: { Name: true, Email: true } },
|
||||
maestro: { select: { Name: true, Email: true, AccountType: true, ActivateStatus: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Re-verify upgrade direction at approval time (maestro AccountType may have changed since request)
|
||||
const isApprovalTrial = request!.maestro.ActivateStatus === ACTIVATE_STATUSES.TRIAL;
|
||||
const isStillValidUpgrade = isApprovalTrial
|
||||
? request!.RequestedAccountType >= request!.maestro.AccountType
|
||||
: request!.RequestedAccountType > request!.maestro.AccountType;
|
||||
if (!isStillValidUpgrade) {
|
||||
throw new ApiError(
|
||||
"마에스트로의 현재 요금제 기준으로 유효하지 않은 업그레이드입니다. 신청 내용을 확인해 주세요.",
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const availableActivateDateTime = calculateUpgradeAvailableDate();
|
||||
|
||||
await tx.maestro.update({
|
||||
@@ -202,8 +214,18 @@ export async function createUpgradeRequest(
|
||||
throw new ApiError("마에스트로를 찾을 수 없습니다.", 404);
|
||||
}
|
||||
|
||||
if (requestedAccountType <= maestro.AccountType) {
|
||||
throw new ApiError("현재 요금제보다 높은 요금제로만 업그레이드 신청이 가능합니다.", 400);
|
||||
const isTrial = maestro.ActivateStatus === ACTIVATE_STATUSES.TRIAL;
|
||||
const isValidUpgrade = isTrial
|
||||
? requestedAccountType >= maestro.AccountType
|
||||
: requestedAccountType > maestro.AccountType;
|
||||
|
||||
if (!isValidUpgrade) {
|
||||
throw new ApiError(
|
||||
isTrial
|
||||
? "체험 계정은 동일 요금제 이상으로만 업그레이드 신청이 가능합니다."
|
||||
: "현재 요금제보다 높은 요금제로만 업그레이드 신청이 가능합니다.",
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const request = await tx.maestro_upgrade.create({
|
||||
@@ -223,6 +245,19 @@ export async function createUpgradeRequest(
|
||||
},
|
||||
});
|
||||
|
||||
await tx.maestro_log.create({
|
||||
data: {
|
||||
Type: "request_upgrade_maestro",
|
||||
MaestroID: maestroID,
|
||||
LogDateTime: new Date(),
|
||||
Remark: buildUpgradeLogRemark(
|
||||
maestro.ActivateStatus,
|
||||
maestro.AccountType,
|
||||
requestedAccountType
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
maestroUpgradeID: request.MaestroUpgradeID,
|
||||
registeredAccountType: request.RegisteredAccountType,
|
||||
|
||||
Reference in New Issue
Block a user