kintex/src/frontend/src/screens/docs/docsApi.ts
zio 23ff7d1799 feat(document): report PDF generation with embedded NanumGothic (G-05)
openhtmltopdf renderer with bundled NanumGothic for Korean text;
report authoring page gains PDF download wired to the new endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:26:52 +09:00

117 lines
4.3 KiB
TypeScript

/*
* M6 서류·마일스톤(SCR-22/23) 실배선 API 계약.
* 공용 클라이언트(../../api/client)만 사용 — client.ts·endpoints.ts·types.ts 무수정.
* 정본: 백엔드 DocumentController 시그니처
* - GET /api/events/{eventId}/milestones → ApiResponse<MilestoneRow[]>
* - GET /api/events/{eventId}/documents → ApiResponse<RequiredDocRow[]>
* - GET /api/events/{eventId}/documents/review → ApiResponse<DocReview>
* - POST /api/events/{eventId}/documents/{docType} → ApiResponse<RequiredDocRow> (action: save|submit)
* - POST /api/events/{eventId}/documents/{docType}/pdf → ApiResponse<GeneratedDoc> (G-05 서류 PDF)
* - POST /api/events/{eventId}/document-summary/pdf → ApiResponse<GeneratedDoc> (G-05 현황 요약 PDF)
* - GET /api/events/{eventId}/documents/files/{fileId}→ application/pdf (인증 다운로드)
* ★ HWP 렌더는 스코프 밖(후속) — HWP 버튼만 disabled 유지. PDF 는 실구현.
*/
import { api, getAccessToken, ApiRequestError } from '../../api/client';
export type MilestoneState = 'done' | 'active' | 'todo';
export interface MilestoneRow {
label: string;
sub: string;
state: MilestoneState;
dueDate: string | null;
}
export type DocStatus = 'pending' | 'draft' | 'submitted' | 'approved' | 'rejected';
export interface RequiredDocRow {
docType: string;
name: string;
status: DocStatus;
dueDate: string | null;
dday: number | null;
sortOrder: number;
}
export interface ReviewIssueRow {
tone: 'warn' | 'info';
title: string;
description: string;
}
export interface DocReview {
issues: ReviewIssueRow[];
progressPct: number;
}
/** save→임시저장(draft), submit→제출(submitted). */
export type DocAction = 'save' | 'submit';
/** SCR-23 웹폼 → PDF 생성 페이로드(백엔드 ReportPdfRequest 와 1:1, 모두 선택). */
export interface ReportPdfPayload {
docTitle?: string;
eventName?: string;
eventDate?: string;
venue?: string;
visitors?: string;
safetyManager?: string;
safetyPhone?: string;
guardCount?: string;
fireStation?: string;
policeStation?: string;
medical?: string;
hazardous?: string;
}
/** PDF 생성 결과 — downloadUrl 을 인증 fetch 하여 Blob 다운로드. */
export interface GeneratedDoc {
documentId: string;
fileName: string;
downloadUrl: string;
}
const base = (eventId: string) => `/api/events/${encodeURIComponent(eventId)}`;
export const docsApi = {
milestones: (eventId: string) => api.get<MilestoneRow[]>(`${base(eventId)}/milestones`),
documents: (eventId: string) => api.get<RequiredDocRow[]>(`${base(eventId)}/documents`),
review: (eventId: string) => api.get<DocReview>(`${base(eventId)}/documents/review`),
transition: (eventId: string, docType: string, action: DocAction) =>
api.post<RequiredDocRow>(
`${base(eventId)}/documents/${encodeURIComponent(docType)}`,
{ action },
),
/** 서류(안전관리 계획서) PDF 생성 — 저장 후 다운로드 메타 반환. */
generateReportPdf: (eventId: string, docType: string, payload: ReportPdfPayload) =>
api.post<GeneratedDoc>(
`${base(eventId)}/documents/${encodeURIComponent(docType)}/pdf`,
payload,
),
/** 서류·마일스톤 현황 요약 PDF 생성(서버 저장 데이터로 조립). */
generateSummaryPdf: (eventId: string) =>
api.post<GeneratedDoc>(`${base(eventId)}/document-summary/pdf`, {}),
};
/**
* 인증 다운로드 — 공용 client.request 는 JSON 봉투를 강제 파싱하므로 PDF Blob 은 여기서 직접 fetch 한다
* (client.ts 무수정 원칙). Authorization 헤더를 부여해 브라우저 다운로드를 트리거한다.
*/
export async function downloadDoc(doc: GeneratedDoc): Promise<void> {
const token = getAccessToken();
const res = await fetch(doc.downloadUrl, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!res.ok) {
throw new ApiRequestError('UNKNOWN', 'PDF 다운로드에 실패했습니다.', res.status);
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = doc.fileName;
document.body.appendChild(a);
a.click();
a.remove();
// 다음 틱에 해제(다운로드 시작 보장).
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}