diff --git a/app/api/extension-requests/[id]/route.ts b/app/api/extension-requests/[id]/route.ts index c54c344..c26776d 100644 --- a/app/api/extension-requests/[id]/route.ts +++ b/app/api/extension-requests/[id]/route.ts @@ -40,12 +40,12 @@ export const PATCH = withApiHandler<{ id: string }>( id: maestroExtensionID, duration: Date.now() - t0, }); - void sendExtensionDoneEmail( + const emailSent = await sendExtensionDoneEmail( result.maestroName, result.maestroEmail, formatDate(result.availableActivateDateTime) ); - return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime }); + return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime, emailSent }); } await cancelExtensionRequest(maestroExtensionID); diff --git a/app/api/upgrade-requests/[id]/route.ts b/app/api/upgrade-requests/[id]/route.ts index a445da6..7cc0bcf 100644 --- a/app/api/upgrade-requests/[id]/route.ts +++ b/app/api/upgrade-requests/[id]/route.ts @@ -40,7 +40,7 @@ export const PATCH = withApiHandler<{ id: string }>( id: maestroUpgradeID, duration: Date.now() - t0, }); - void sendUpgradeDoneEmail( + const emailSent = await sendUpgradeDoneEmail( result.maestroName, result.maestroEmail, result.registeredActivateStatus, @@ -48,7 +48,7 @@ export const PATCH = withApiHandler<{ id: string }>( result.requestedAccountType, formatDate(result.availableActivateDateTime) ); - return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime }); + return NextResponse.json({ ok: true, availableActivateDateTime: result.availableActivateDateTime, emailSent }); } await rejectUpgradeRequest(maestroUpgradeID); diff --git a/components/data-table/extension-requests-table.tsx b/components/data-table/extension-requests-table.tsx index 2b4b483..2fa93f4 100644 --- a/components/data-table/extension-requests-table.tsx +++ b/components/data-table/extension-requests-table.tsx @@ -108,19 +108,16 @@ function ExtensionRequestActions({ const router = useRouter(); const [isPending, startTransition] = useTransition(); const [errorMessage, setErrorMessage] = useState(""); + const [emailWarning, setEmailWarning] = useState(false); const isRequested = request.status === REQUEST_STATUSES.REQUESTED; - if (!isRequested) { + if (!isRequested && !emailWarning) { return 처리 완료; } async function submitAction(action: "approve" | "cancel") { const actionLabel = action === "approve" ? "승인" : "취소"; - const confirmed = window.confirm( - `${request.maestroName} 연장 신청을 ${actionLabel}하시겠습니까?` - ); - - if (!confirmed) { + if (!window.confirm(`${request.maestroName} 연장 신청을 ${actionLabel}하시겠습니까?`)) { return; } @@ -136,14 +133,19 @@ function ExtensionRequestActions({ } ); - if (!response.ok) { - const body = (await response.json().catch(() => null)) as { - message?: string; - } | null; + const body = (await response.json().catch(() => null)) as { + message?: string; + emailSent?: boolean; + } | null; + if (!response.ok) { throw new Error(body?.message ?? "연장 신청 처리에 실패했습니다."); } + if (action === "approve" && body?.emailSent === false) { + setEmailWarning(true); + } + startTransition(() => { router.refresh(); }); @@ -158,27 +160,36 @@ function ExtensionRequestActions({ return (
+ 이메일 발송 실패 — 수동 확인 필요 +
+ ) : null} {errorMessage ? ({errorMessage}
) : null} diff --git a/components/data-table/upgrade-requests-table.tsx b/components/data-table/upgrade-requests-table.tsx index 65d36c0..a0cb1df 100644 --- a/components/data-table/upgrade-requests-table.tsx +++ b/components/data-table/upgrade-requests-table.tsx @@ -113,19 +113,16 @@ function UpgradeRequestActions({ const router = useRouter(); const [isPending, startTransition] = useTransition(); const [errorMessage, setErrorMessage] = useState(""); + const [emailWarning, setEmailWarning] = useState(false); const isRequested = request.status === REQUEST_STATUSES.REQUESTED; - if (!isRequested) { + if (!isRequested && !emailWarning) { return 처리 완료; } async function submitAction(action: "approve" | "reject") { const actionLabel = action === "approve" ? "승인" : "거절"; - const confirmed = window.confirm( - `${request.maestroName} 업그레이드 신청을 ${actionLabel}하시겠습니까?` - ); - - if (!confirmed) { + if (!window.confirm(`${request.maestroName} 업그레이드 신청을 ${actionLabel}하시겠습니까?`)) { return; } @@ -141,14 +138,17 @@ function UpgradeRequestActions({ } ); - if (!response.ok) { - const body = (await response.json().catch(() => null)) as { - message?: string; - } | null; + const body = (await response.json().catch(() => null)) as { + message?: string; + emailSent?: boolean; + } | null; - throw new Error( - body?.message ?? "업그레이드 신청 처리에 실패했습니다." - ); + if (!response.ok) { + throw new Error(body?.message ?? "업그레이드 신청 처리에 실패했습니다."); + } + + if (action === "approve" && body?.emailSent === false) { + setEmailWarning(true); } startTransition(() => { @@ -165,27 +165,36 @@ function UpgradeRequestActions({ return (+ 이메일 발송 실패 — 수동 확인 필요 +
+ ) : null} {errorMessage ? ({errorMessage}
) : null} diff --git a/docs/plan/improvement-plan.md b/docs/plan/improvement-plan.md index bd191ef..8b65aae 100644 --- a/docs/plan/improvement-plan.md +++ b/docs/plan/improvement-plan.md @@ -168,9 +168,10 @@ - P1-5: `request_extension_maestro` / `request_upgrade_maestro` 로그 추가 - P1-7: 관리자 대행 등록 시 입금 메일 미발송 — 의도 확인, AGENTS.md에 명시 - P2-10: 신청 시점 스냅샷 유지 — 변경 없음 -3. **Step 3 (P1-6 이메일 + 검증)** - - emailSent 응답 + UI 경고, stage에서 연장/업그레이드(체험→유료, 유료→유료) 메일 실측 - (phase11 잔여 항목 겸) +3. **Step 3 (P1-6 이메일 + 검증) ✅ 완료 (2026-07-04)** + - API route 2곳: `void sendXxxDoneEmail` → `await emailSent`, 응답에 `emailSent: boolean` 포함 + - 테이블 UI 2곳: 응답 body를 단일 파싱으로 통일, 승인 성공 후 `emailSent === false`이면 인라인 경고 표시 (`router.refresh()` 후에도 client state 유지됨) + - stage 실측(체험→유료, 유료→유료 메일 검증)은 별도 수동 테스트 항목으로 남김 4. **Step 4 (P2 일괄)** - 소규모 항목 묶음 처리 후 AGENTS.md / phase 문서 갱신