import { getSessionAccessToken, verifySession } from '@/lib/auth/dal'; import { backendFetchStream } from '@/lib/http/backend-fetch'; /** * 관리자 회원 목록 엑셀 다운로드 — 백엔드가 만든 xlsx를 브라우저로 중계한다. * * 라우트 핸들러인 이유는 학생 목록(`students/excel/route.ts`)과 같다 — 브라우저는 백엔드를 직접 * 호출하지 않고, 백엔드 엑셀 API가 요구하는 ROLE_ADMIN 토큰은 httpOnly 세션 안에만 있다. 본문은 * 파싱하지 않고 업스트림 스트림을 그대로 흘려보낸다. * * 파일에 담기는 열은 백엔드가 정한다 — 번호·이름·아이디·휴대폰번호·이메일·생성일 * (`MngrAdminApiController.downloadExcel`). 화면의 "역할" 열은 파일에 없다. */ const ADMIN_MEMBER_EXCEL_PATH = '/api/v1/mngr/admin/excel/download'; /** * 파일은 화면의 검색·페이징과 무관하게 **항상 전체**를 담는다(학생 목록과 같은 사양). * * 백엔드 엑셀 API가 목록과 같은 `selectPagination`을 그대로 쓰므로 페이징이 걸린다 — 파라미터를 * 생략하면 기본값(`recordCountPerPage=10`)이 적용돼 10건만 담긴 파일이 나온다. "전체" 모드가 따로 * 없어 충분히 큰 상한을 명시해 1페이지로 전부 받는다. 이 상한을 넘으면 파일이 조용히 잘린다. */ const EXCEL_ROW_LIMIT = 100_000; /** 전체 행을 모아 워크북을 만드는 시간이 있어 일반 조회보다 넉넉히 잡는다. */ const EXCEL_TIMEOUT_MS = 60_000; const DOWNLOAD_FAILED_MESSAGE = '엑셀 파일을 내려받지 못했습니다. 잠시 후 다시 시도해 주세요.'; const XLSX_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; export async function GET() { // 라우트 핸들러는 UI를 거치지 않고 직접 호출될 수 있으므로 여기서 직접 인증을 확인한다 // (proxy의 쿠키 존재 확인은 낙관적 필터일 뿐이다 — 설계서 §8). await verifySession(); const accessToken = await getSessionAccessToken(); const result = await backendFetchStream(ADMIN_MEMBER_EXCEL_PATH, { query: { pageIndex: 1, recordCountPerPage: EXCEL_ROW_LIMIT }, accessToken: accessToken ?? undefined, timeoutMs: EXCEL_TIMEOUT_MS, }); if (!result.ok) { // 실패 사유(코드·백엔드 메시지)는 backendFetchStream이 서버 콘솔에 남긴다. return new Response(DOWNLOAD_FAILED_MESSAGE, { status: 502, headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } const upstream = result.data; return new Response(upstream.body, { status: 200, headers: { 'Content-Type': upstream.headers.get('content-type') ?? XLSX_CONTENT_TYPE, 'Content-Disposition': upstream.headers.get('content-disposition') ?? 'attachment; filename="admins.xlsx"', }, }); }