전체 코드, 구조 검토 - P1-6 코드 수정 진행

This commit is contained in:
2026-07-04 15:40:33 +09:00
parent 41a246ad7b
commit 2231ac4bfe
5 changed files with 93 additions and 72 deletions
+2 -2
View File
@@ -40,12 +40,12 @@ export const PATCH = withApiHandler<{ id: string }>(
id: maestroExtensionID, id: maestroExtensionID,
duration: Date.now() - t0, duration: Date.now() - t0,
}); });
void sendExtensionDoneEmail( const emailSent = await sendExtensionDoneEmail(
result.maestroName, result.maestroName,
result.maestroEmail, result.maestroEmail,
formatDate(result.availableActivateDateTime) formatDate(result.availableActivateDateTime)
); );
return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime }); return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime, emailSent });
} }
await cancelExtensionRequest(maestroExtensionID); await cancelExtensionRequest(maestroExtensionID);
+2 -2
View File
@@ -40,7 +40,7 @@ export const PATCH = withApiHandler<{ id: string }>(
id: maestroUpgradeID, id: maestroUpgradeID,
duration: Date.now() - t0, duration: Date.now() - t0,
}); });
void sendUpgradeDoneEmail( const emailSent = await sendUpgradeDoneEmail(
result.maestroName, result.maestroName,
result.maestroEmail, result.maestroEmail,
result.registeredActivateStatus, result.registeredActivateStatus,
@@ -48,7 +48,7 @@ export const PATCH = withApiHandler<{ id: string }>(
result.requestedAccountType, result.requestedAccountType,
formatDate(result.availableActivateDateTime) formatDate(result.availableActivateDateTime)
); );
return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime }); return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime, emailSent });
} }
await rejectUpgradeRequest(maestroUpgradeID); await rejectUpgradeRequest(maestroUpgradeID);
@@ -108,19 +108,16 @@ function ExtensionRequestActions({
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [emailWarning, setEmailWarning] = useState(false);
const isRequested = request.status === REQUEST_STATUSES.REQUESTED; const isRequested = request.status === REQUEST_STATUSES.REQUESTED;
if (!isRequested) { if (!isRequested && !emailWarning) {
return <span className="text-xs text-muted-foreground"> </span>; return <span className="text-xs text-muted-foreground"> </span>;
} }
async function submitAction(action: "approve" | "cancel") { async function submitAction(action: "approve" | "cancel") {
const actionLabel = action === "approve" ? "승인" : "취소"; const actionLabel = action === "approve" ? "승인" : "취소";
const confirmed = window.confirm( if (!window.confirm(`${request.maestroName} 연장 신청을 ${actionLabel}하시겠습니까?`)) {
`${request.maestroName} 연장 신청을 ${actionLabel}하시겠습니까?`
);
if (!confirmed) {
return; return;
} }
@@ -136,14 +133,19 @@ function ExtensionRequestActions({
} }
); );
if (!response.ok) { const body = (await response.json().catch(() => null)) as {
const body = (await response.json().catch(() => null)) as { message?: string;
message?: string; emailSent?: boolean;
} | null; } | null;
if (!response.ok) {
throw new Error(body?.message ?? "연장 신청 처리에 실패했습니다."); throw new Error(body?.message ?? "연장 신청 처리에 실패했습니다.");
} }
if (action === "approve" && body?.emailSent === false) {
setEmailWarning(true);
}
startTransition(() => { startTransition(() => {
router.refresh(); router.refresh();
}); });
@@ -158,27 +160,36 @@ function ExtensionRequestActions({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> {isRequested ? (
<Button <div className="flex items-center gap-2">
disabled={isPending} <Button
onClick={() => void submitAction("approve")} disabled={isPending}
size="sm" onClick={() => void submitAction("approve")}
type="button" size="sm"
> type="button"
<Check className="size-4" aria-hidden="true" /> >
<Check className="size-4" aria-hidden="true" />
</Button>
<Button </Button>
disabled={isPending} <Button
onClick={() => void submitAction("cancel")} disabled={isPending}
size="sm" onClick={() => void submitAction("cancel")}
type="button" size="sm"
variant="outline" type="button"
> variant="outline"
<X className="size-4" aria-hidden="true" /> >
<X className="size-4" aria-hidden="true" />
</Button>
</div> </Button>
</div>
) : (
<span className="text-xs text-muted-foreground"> </span>
)}
{emailWarning ? (
<p className="max-w-56 text-xs text-amber-600 dark:text-amber-400">
</p>
) : null}
{errorMessage ? ( {errorMessage ? (
<p className="max-w-56 text-xs text-destructive">{errorMessage}</p> <p className="max-w-56 text-xs text-destructive">{errorMessage}</p>
) : null} ) : null}
@@ -113,19 +113,16 @@ function UpgradeRequestActions({
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [emailWarning, setEmailWarning] = useState(false);
const isRequested = request.status === REQUEST_STATUSES.REQUESTED; const isRequested = request.status === REQUEST_STATUSES.REQUESTED;
if (!isRequested) { if (!isRequested && !emailWarning) {
return <span className="text-xs text-muted-foreground"> </span>; return <span className="text-xs text-muted-foreground"> </span>;
} }
async function submitAction(action: "approve" | "reject") { async function submitAction(action: "approve" | "reject") {
const actionLabel = action === "approve" ? "승인" : "거절"; const actionLabel = action === "approve" ? "승인" : "거절";
const confirmed = window.confirm( if (!window.confirm(`${request.maestroName} 업그레이드 신청을 ${actionLabel}하시겠습니까?`)) {
`${request.maestroName} 업그레이드 신청을 ${actionLabel}하시겠습니까?`
);
if (!confirmed) {
return; return;
} }
@@ -141,14 +138,17 @@ function UpgradeRequestActions({
} }
); );
if (!response.ok) { const body = (await response.json().catch(() => null)) as {
const body = (await response.json().catch(() => null)) as { message?: string;
message?: string; emailSent?: boolean;
} | null; } | null;
throw new Error( if (!response.ok) {
body?.message ?? "업그레이드 신청 처리에 실패했습니다." throw new Error(body?.message ?? "업그레이드 신청 처리에 실패했습니다.");
); }
if (action === "approve" && body?.emailSent === false) {
setEmailWarning(true);
} }
startTransition(() => { startTransition(() => {
@@ -165,27 +165,36 @@ function UpgradeRequestActions({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> {isRequested ? (
<Button <div className="flex items-center gap-2">
disabled={isPending} <Button
onClick={() => void submitAction("approve")} disabled={isPending}
size="sm" onClick={() => void submitAction("approve")}
type="button" size="sm"
> type="button"
<Check className="size-4" aria-hidden="true" /> >
<Check className="size-4" aria-hidden="true" />
</Button>
<Button </Button>
disabled={isPending} <Button
onClick={() => void submitAction("reject")} disabled={isPending}
size="sm" onClick={() => void submitAction("reject")}
type="button" size="sm"
variant="outline" type="button"
> variant="outline"
<X className="size-4" aria-hidden="true" /> >
<X className="size-4" aria-hidden="true" />
</Button>
</div> </Button>
</div>
) : (
<span className="text-xs text-muted-foreground"> </span>
)}
{emailWarning ? (
<p className="max-w-56 text-xs text-amber-600 dark:text-amber-400">
</p>
) : null}
{errorMessage ? ( {errorMessage ? (
<p className="max-w-56 text-xs text-destructive">{errorMessage}</p> <p className="max-w-56 text-xs text-destructive">{errorMessage}</p>
) : null} ) : null}
+4 -3
View File
@@ -168,9 +168,10 @@
- P1-5: `request_extension_maestro` / `request_upgrade_maestro` 로그 추가 - P1-5: `request_extension_maestro` / `request_upgrade_maestro` 로그 추가
- P1-7: 관리자 대행 등록 시 입금 메일 미발송 — 의도 확인, AGENTS.md에 명시 - P1-7: 관리자 대행 등록 시 입금 메일 미발송 — 의도 확인, AGENTS.md에 명시
- P2-10: 신청 시점 스냅샷 유지 — 변경 없음 - P2-10: 신청 시점 스냅샷 유지 — 변경 없음
3. **Step 3 (P1-6 이메일 + 검증)** 3. **Step 3 (P1-6 이메일 + 검증) ✅ 완료 (2026-07-04)**
- emailSent 응답 + UI 경고, stage에서 연장/업그레이드(체험→유료, 유료→유료) 메일 실측 - API route 2곳: `void sendXxxDoneEmail``await emailSent`, 응답에 `emailSent: boolean` 포함
(phase11 잔여 항목 겸) - 테이블 UI 2곳: 응답 body를 단일 파싱으로 통일, 승인 성공 후 `emailSent === false`이면 인라인 경고 표시 (`router.refresh()` 후에도 client state 유지됨)
- stage 실측(체험→유료, 유료→유료 메일 검증)은 별도 수동 테스트 항목으로 남김
4. **Step 4 (P2 일괄)** 4. **Step 4 (P2 일괄)**
- 소규모 항목 묶음 처리 후 AGENTS.md / phase 문서 갱신 - 소규모 항목 묶음 처리 후 AGENTS.md / phase 문서 갱신