비동기 작업
오래 걸리거나 재시도할 작업을 API 응답과 분리합니다.
작업 등록
- job.report는 즉시 job 정보를 반환하고 handler는 비동기로 실행합니다.
- config.attempts는 최대 10회이며 backoff는 실패할 때마다 두 배로 증가합니다.
- config.delay로 최대 30일 뒤에 실행할 수 있습니다.
- 입력은 JSON 값이어야 하며 64KB까지 저장합니다.
// server/api/report.post.ts
export default async (data: {
name: string;
rows: { item: string; amount: number }[];
}) => {
const work = await job.report(data);
return { jobId: work.id };
};Job handler
- 파일 경로가 타입이 연결된 호출 경로가 됩니다. server/tasks/report.job.ts는 job.report(data)로 등록합니다.
- config.filter는 worker 실행 직전에 입력을 다시 검증합니다.
- 결과도 JSON 값 64KB까지 저장합니다.
- 완료 결과에는 메시지, files 안의 저장 경로와 실행 환경의 전체 다운로드 URL을 반환합니다.
- app.url은 검수에서 -test 전체 주소, 운영에서 운영 전체 주소를 제공합니다.
- 비동기 job은 원래 HTTP 요청이 끝난 뒤 완료되므로 excel.download를 직접 반환할 수 없습니다.
- job 결과 파일은 excel.save로 보관하되 만료 후 제거하거나 같은 경로를 덮어쓰는 정책을 둡니다.
- 같은 XLSX 경로를 덮어써 재시작 복구와 재시도에도 결과가 누적되지 않습니다.
// server/tasks/report.job.ts
export const config = {
filter: {
name: z.string().regex(/^[a-z0-9-]{1,40}$/),
rows: z.array(
z.object({
item: z.string().min(1).max(100),
amount: z.number().min(0)
})
).min(1).max(1000)
},
sample: {
name: "weekly-sales",
rows: [
{ item: "Chocolate cake", amount: 42000 },
{ item: "Cheesecake", amount: 38000 }
]
},
attempts: 3,
backoff: 1000,
timeout: 30_000
};
export default async (data: Input<typeof config.filter>) => {
const rows = [
["상품", "금액"],
...data.rows.map((row) => [row.item, row.amount])
];
const path = `reports/${data.name}.xlsx`;
const file = await excel.save(path, rows);
return {
message: "보고서 저장이 완료되었습니다.",
path: file.path,
url: new URL(`/download/${data.name}`, app.url).toString(),
rows: data.rows.length,
size: file.size,
completedAt: now()
};
};상태 조회
- 상태는 queued, delayed, active, completed 또는 failed입니다.
- 완료되면 output에서 저장 완료 메시지, 저장 경로와 다운로드 URL을 확인합니다.
- server 데이터의 작업은 실행 예약 기간과 종료 후 7일 동안 웹사이트 전용 Redis에 보존합니다.
- local 데이터의 작업은 메모리에만 보관해 프로세스 재시작 시 사라집니다.
- SSE 없이도 job.$get을 일정 간격으로 호출해 진행 상태를 표시할 수 있습니다.
// server/api/report/[id].get.ts
export default async (_data: unknown, context: ApiContext) => {
const work = await job.$get(context.params.id);
if (!work) return { state: "expired" };
return {
state: work.state,
output: work.output,
error: work.error
};
};생성 파일 다운로드
- files 객체는 기본 /files/* 경로로도 공개됩니다.
- 다운로드 route는 storage.get으로 local files와 Object Storage를 모두 지원합니다.
- Content-Disposition: attachment로 브라우저 다운로드를 명시합니다.
- 동적 파일 이름은 허용한 형식인지 확인한 뒤 storage 경로에 사용합니다.
// server/routes/download/[name].get.ts
const xlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
const reportName = /^[a-z0-9-]{1,40}$/;
export default async (_data: unknown, context: ApiContext) => {
const name = context.params.name;
if (!reportName.test(name)) {
return new Response("보고서 이름이 올바르지 않습니다.", { status: 400 });
}
const file = await storage.get(`reports/${name}.xlsx`);
if (!file) {
return new Response("보고서 파일이 없습니다.", { status: 404 });
}
return new Response(file, {
headers: {
"cache-control": "no-store",
"content-disposition": `attachment; filename="${name}.xlsx"`,
"content-type": xlsx
}
});
};Queue는 느린 파일·AI·메일·외부 API 후속 작업에 사용합니다. 단순 CRUD, 즉시 결과 또는 결제 상태의 최초 기록에는 사용하지 않습니다.