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:
@@ -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(); }
|
||||
}}
|
||||
|
||||
@@ -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,39 +209,25 @@ export function VideosPanel({ isAdmin }: { isAdmin: boolean }) {
|
||||
setBulkProgress(null);
|
||||
return;
|
||||
}
|
||||
const reader = resp.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
if (!reader) { setRunning(false); return; }
|
||||
|
||||
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) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
try {
|
||||
const ev = JSON.parse(line.slice(6));
|
||||
if (ev.type === "processing") {
|
||||
setBulkProgress((p) => p ? { ...p, current: ev.index + 1, currentTitle: ev.title, waiting: undefined } : p);
|
||||
} else if (ev.type === "wait") {
|
||||
setBulkProgress((p) => p ? { ...p, waiting: ev.delay } : p);
|
||||
} else if (ev.type === "done") {
|
||||
const detail = isTranscript
|
||||
? `${ev.source} / ${ev.length?.toLocaleString()}자`
|
||||
: `${ev.restaurants}개 식당`;
|
||||
setBulkProgress((p) => p ? { ...p, results: [...p.results, { title: ev.title, detail }] } : p);
|
||||
} else if (ev.type === "error") {
|
||||
setBulkProgress((p) => p ? { ...p, results: [...p.results, { title: ev.title, detail: ev.message, error: true }] } : p);
|
||||
} else if (ev.type === "complete") {
|
||||
setRunning(false);
|
||||
load();
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
// #351 — consumeSseStream으로 통일
|
||||
await consumeSseStream(resp, (raw) => {
|
||||
const ev = raw as { type: string; [k: string]: unknown };
|
||||
if (ev.type === "processing") {
|
||||
setBulkProgress((p) => p ? { ...p, current: (ev.index as number) + 1, currentTitle: ev.title as string, waiting: undefined } : p);
|
||||
} else if (ev.type === "wait") {
|
||||
setBulkProgress((p) => p ? { ...p, waiting: ev.delay as number } : p);
|
||||
} else if (ev.type === "done") {
|
||||
const detail = isTranscript
|
||||
? `${ev.source} / ${(ev.length as number)?.toLocaleString()}자`
|
||||
: `${ev.restaurants}개 식당`;
|
||||
setBulkProgress((p) => p ? { ...p, results: [...p.results, { title: ev.title as string, detail }] } : p);
|
||||
} else if (ev.type === "error") {
|
||||
setBulkProgress((p) => p ? { ...p, results: [...p.results, { title: ev.title as string, detail: ev.message as string, error: true }] } : p);
|
||||
} else if (ev.type === "complete") {
|
||||
setRunning(false);
|
||||
load();
|
||||
}
|
||||
}
|
||||
});
|
||||
setRunning(false);
|
||||
load();
|
||||
} catch {
|
||||
@@ -264,30 +250,17 @@ export function VideosPanel({ isAdmin }: { isAdmin: boolean }) {
|
||||
setRebuildingVectors(false);
|
||||
return;
|
||||
}
|
||||
const reader = resp.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
if (!reader) { setRebuildingVectors(false); return; }
|
||||
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) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
try {
|
||||
const ev = JSON.parse(line.slice(6));
|
||||
if (ev.status === "progress" || ev.type === "progress") {
|
||||
setVectorProgress({ phase: ev.phase, current: ev.current, total: ev.total, name: ev.name });
|
||||
} else if (ev.status === "done" || ev.type === "done") {
|
||||
setVectorProgress({ phase: "done", current: ev.total, total: ev.total });
|
||||
} else if (ev.type === "error") {
|
||||
alert(`벡터 재생성 오류: ${ev.message}`);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
// #351 — consumeSseStream으로 통일
|
||||
await consumeSseStream(resp, (raw) => {
|
||||
const ev = raw as { status?: string; type?: string; [k: string]: unknown };
|
||||
if (ev.status === "progress" || ev.type === "progress") {
|
||||
setVectorProgress({ phase: ev.phase as string, current: ev.current as number, total: ev.total as number, name: ev.name as string });
|
||||
} else if (ev.status === "done" || ev.type === "done") {
|
||||
setVectorProgress({ phase: "done", current: ev.total as number, total: ev.total as number });
|
||||
} else if (ev.type === "error") {
|
||||
alert(`벡터 재생성 오류: ${ev.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
setRebuildingVectors(false);
|
||||
} catch {
|
||||
setRebuildingVectors(false);
|
||||
@@ -309,30 +282,17 @@ export function VideosPanel({ isAdmin }: { isAdmin: boolean }) {
|
||||
setRemappingCuisine(false);
|
||||
return;
|
||||
}
|
||||
const reader = resp.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
if (!reader) { setRemappingCuisine(false); return; }
|
||||
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) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
try {
|
||||
const ev = JSON.parse(line.slice(6));
|
||||
if (ev.type === "processing" || ev.type === "batch_done") {
|
||||
setRemapProgress({ current: ev.current, total: ev.total, updated: ev.updated || 0 });
|
||||
} else if (ev.type === "complete") {
|
||||
setRemapProgress({ current: ev.total, total: ev.total, updated: ev.updated });
|
||||
} else if (ev.type === "error") {
|
||||
alert(`재분류 오류: ${ev.message}`);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
// #351 — consumeSseStream으로 통일
|
||||
await consumeSseStream(resp, (raw) => {
|
||||
const ev = raw as { type: string; [k: string]: unknown };
|
||||
if (ev.type === "processing" || ev.type === "batch_done") {
|
||||
setRemapProgress({ current: ev.current as number, total: ev.total as number, updated: (ev.updated as number) || 0 });
|
||||
} else if (ev.type === "complete") {
|
||||
setRemapProgress({ current: ev.total as number, total: ev.total as number, updated: ev.updated as number });
|
||||
} else if (ev.type === "error") {
|
||||
alert(`재분류 오류: ${ev.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
setRemappingCuisine(false);
|
||||
} catch {
|
||||
setRemappingCuisine(false);
|
||||
@@ -354,30 +314,17 @@ export function VideosPanel({ isAdmin }: { isAdmin: boolean }) {
|
||||
setRemappingFoods(false);
|
||||
return;
|
||||
}
|
||||
const reader = resp.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
if (!reader) { setRemappingFoods(false); return; }
|
||||
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) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
try {
|
||||
const ev = JSON.parse(line.slice(6));
|
||||
if (ev.type === "processing" || ev.type === "batch_done") {
|
||||
setFoodsProgress({ current: ev.current, total: ev.total, updated: ev.updated || 0 });
|
||||
} else if (ev.type === "complete") {
|
||||
setFoodsProgress({ current: ev.total, total: ev.total, updated: ev.updated });
|
||||
} else if (ev.type === "error") {
|
||||
alert(`메뉴 태그 재생성 오류: ${ev.message}`);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
// #351 — consumeSseStream으로 통일
|
||||
await consumeSseStream(resp, (raw) => {
|
||||
const ev = raw as { type: string; [k: string]: unknown };
|
||||
if (ev.type === "processing" || ev.type === "batch_done") {
|
||||
setFoodsProgress({ current: ev.current as number, total: ev.total as number, updated: (ev.updated as number) || 0 });
|
||||
} else if (ev.type === "complete") {
|
||||
setFoodsProgress({ current: ev.total as number, total: ev.total as number, updated: ev.updated as number });
|
||||
} else if (ev.type === "error") {
|
||||
alert(`메뉴 태그 재생성 오류: ${ev.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
setRemappingFoods(false);
|
||||
} catch {
|
||||
setRemappingFoods(false);
|
||||
|
||||
Reference in New Issue
Block a user