diff --git a/frontend/src/app/admin/_panels/RestaurantsPanel.tsx b/frontend/src/app/admin/_panels/RestaurantsPanel.tsx index 30f45d8..b104461 100644 --- a/frontend/src/app/admin/_panels/RestaurantsPanel.tsx +++ b/frontend/src/app/admin/_panels/RestaurantsPanel.tsx @@ -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(); } }} diff --git a/frontend/src/app/admin/_panels/VideosPanel.tsx b/frontend/src/app/admin/_panels/VideosPanel.tsx index 1988f65..0a14454 100644 --- a/frontend/src/app/admin/_panels/VideosPanel.tsx +++ b/frontend/src/app/admin/_panels/VideosPanel.tsx @@ -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);