feat(admin): #304+#323 LLM 검증 UI + 공통 유틸 추출
#323 (LLM 검증 어드민 UI): - api.ts: getVerifyPending / verifyAll / verifyOne / setRestaurantHidden 추가 - Restaurant 타입에 hidden / hidden_reason / verified_at 추가 - RestaurantsPanel 헤더에 "미검증 N건 + LLM 검증" 버튼 추가 - 테이블에 "검증" 컬럼 추가: - hidden=true → "숨김 (사유)" 버튼 (클릭 시 해제) - verified_at 있고 visible → "OK" 버튼 (클릭 시 숨김) - 미검증 → "미검증" 텍스트 #304 (어드민 공통 유틸): - lib/admin-utils.ts 신규 - getAdminToken(): localStorage 직접 접근 통일 - authHeaders(): 표준 Bearer 헤더 - consumeSseStream(): SSE 라인 파싱 헬퍼 - colSpan 6 → 7로 검증 컬럼 반영 후속 분리: #329 (admin/page.tsx 전체 분리 + localStorage/SSE 호출 11+곳 교체) Refs: #304 #323 #322
This commit is contained in:
52
frontend/src/lib/admin-utils.ts
Normal file
52
frontend/src/lib/admin-utils.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// #304 어드민 페이지 공통 유틸.
|
||||
// 결함: localStorage 직접 접근 10+곳 / SSE 파싱 코드 6곳 중복.
|
||||
|
||||
const TOKEN_KEY = "tasteby_token";
|
||||
|
||||
export function getAdminToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function authHeaders(): Record<string, string> {
|
||||
const token = getAdminToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE(Server-Sent Events) 스트림을 라인 단위로 파싱하여 onEvent 콜백을 호출.
|
||||
* 호환 패턴: `data: { ...json... }` 한 줄 = 한 이벤트.
|
||||
* 비어있는 줄은 무시. JSON 파싱 실패 시 콜백 skip.
|
||||
*/
|
||||
export async function consumeSseStream(
|
||||
response: Response,
|
||||
onEvent: (event: unknown) => void,
|
||||
onError?: (err: unknown) => void,
|
||||
): Promise<void> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) return;
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("data:")) continue;
|
||||
const payload = trimmed.slice(5).trim();
|
||||
if (!payload) continue;
|
||||
try {
|
||||
onEvent(JSON.parse(payload));
|
||||
} catch {
|
||||
// 무시: 일부 SSE 줄이 JSON이 아닐 수도 있음
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
onError?.(err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user