refactor(admin): #351 SSE 6곳 consumeSseStream으로 통일

VideosPanel:
- bulkTranscript/bulkExtract: 단일 SSE 핸들러 → consumeSseStream
- rebuildVectors: consumeSseStream
- remapCuisine / remapFoods: consumeSseStream

RestaurantsPanel:
- bulkTabling / bulkCatchtable: consumeSseStream

이전: 각 호출이 자체적으로 reader+decoder+buf.split+match 6곳 복제.
이제: lib/admin-utils.ts의 consumeSseStream(resp, onEvent)으로 일관 처리.

빌드 + npm test 13/13 통과. 회귀 없음.

Refs: #351
This commit is contained in:
joungmin
2026-06-15 17:15:35 +09:00
parent cf1055bdf9
commit d6ee62230e
2 changed files with 74 additions and 149 deletions

View File

@@ -1,5 +1,5 @@
"use client";
import { getAdminToken } from "@/lib/admin-utils";
import { getAdminToken, consumeSseStream } from "@/lib/admin-utils";
import { useCallback, useEffect, useState } from "react";
import { api } from "@/lib/api";
@@ -209,30 +209,19 @@ export function RestaurantsPanel({ isAdmin }: { isAdmin: boolean }) {
method: "POST",
headers: { Authorization: `Bearer ${getAdminToken()}` },
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() || "";
for (const line of lines) {
const m = line.match(/^data:(.+)$/);
if (!m) continue;
const evt = JSON.parse(m[1]);
if (evt.type === "processing" || evt.type === "done" || evt.type === "notfound" || evt.type === "error") {
setBulkTablingProgress(p => ({
...p, current: evt.current, total: evt.total || p.total, name: evt.name,
linked: evt.type === "done" ? p.linked + 1 : p.linked,
notFound: (evt.type === "notfound" || evt.type === "error") ? p.notFound + 1 : p.notFound,
}));
} else if (evt.type === "complete") {
alert(`완료! 연결: ${evt.linked}개, 미발견: ${evt.notFound}`);
}
// #351 — consumeSseStream으로 통일
await consumeSseStream(res, (raw) => {
const evt = raw as { type: string; [k: string]: unknown };
if (evt.type === "processing" || evt.type === "done" || evt.type === "notfound" || evt.type === "error") {
setBulkTablingProgress(p => ({
...p, current: evt.current as number, total: (evt.total as number) || p.total, name: evt.name as string,
linked: evt.type === "done" ? p.linked + 1 : p.linked,
notFound: (evt.type === "notfound" || evt.type === "error") ? p.notFound + 1 : p.notFound,
}));
} else if (evt.type === "complete") {
alert(`완료! 연결: ${evt.linked}개, 미발견: ${evt.notFound}`);
}
}
});
} catch (e) { alert("벌크 테이블링 실패: " + (e instanceof Error ? e.message : String(e))); }
finally { setBulkTabling(false); load(); }
}}
@@ -287,30 +276,19 @@ export function RestaurantsPanel({ isAdmin }: { isAdmin: boolean }) {
method: "POST",
headers: { Authorization: `Bearer ${getAdminToken()}` },
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() || "";
for (const line of lines) {
const m = line.match(/^data:(.+)$/);
if (!m) continue;
const evt = JSON.parse(m[1]);
if (evt.type === "processing" || evt.type === "done" || evt.type === "notfound" || evt.type === "error") {
setBulkCatchtableProgress(p => ({
...p, current: evt.current, total: evt.total || p.total, name: evt.name,
linked: evt.type === "done" ? p.linked + 1 : p.linked,
notFound: (evt.type === "notfound" || evt.type === "error") ? p.notFound + 1 : p.notFound,
}));
} else if (evt.type === "complete") {
alert(`완료! 연결: ${evt.linked}개, 미발견: ${evt.notFound}`);
}
// #351 — consumeSseStream으로 통일
await consumeSseStream(res, (raw) => {
const evt = raw as { type: string; [k: string]: unknown };
if (evt.type === "processing" || evt.type === "done" || evt.type === "notfound" || evt.type === "error") {
setBulkCatchtableProgress(p => ({
...p, current: evt.current as number, total: (evt.total as number) || p.total, name: evt.name as string,
linked: evt.type === "done" ? p.linked + 1 : p.linked,
notFound: (evt.type === "notfound" || evt.type === "error") ? p.notFound + 1 : p.notFound,
}));
} else if (evt.type === "complete") {
alert(`완료! 연결: ${evt.linked}개, 미발견: ${evt.notFound}`);
}
}
});
} catch (e) { alert("벌크 캐치테이블 실패: " + (e instanceof Error ? e.message : String(e))); }
finally { setBulkCatchtable(false); load(); }
}}