작업 기간: 2026.01.15 ~ 2026.01.23 (약 8일)
변경 규모: 27개 파일, +1,579줄 / -259줄
/pdf/guest 페이지로 이동하면 빈 화면URL.createObjectURL() 반환값 확인 → FileUpload에서는 정상URL.createObjectURL()로 생성한 blob URL은 해당 Document 컨텍스트에서만 유효.
React Router의 navigate()로 페이지 이동 시 Document 컨텍스트가 변경되어 blob URL이 무효화됨.
sessionStorage에 저장FileUpload.tsx
// Before: blob URL을 직접 전달
const fileUrl = URL.createObjectURL(file);
navigate(`/pdf/guest?url=${encodeURIComponent(fileUrl)}`);
// After: base64로 변환 후 sessionStorage에 저장
const reader = new FileReader();
reader.onload = () => {
sessionStorage.setItem("guest_pdf_data", reader.result as string);
sessionStorage.setItem("guest_pdf_name", file.name);
navigate("/pdf/guest");
};
reader.readAsDataURL(file);
PDFViewer.tsx
// sessionStorage에서 복원
const guestPdfData = sessionStorage.getItem("guest_pdf_data");
const byteString = atob(guestPdfData.split(',')[1]);
// ... Uint8Array로 변환 후 Blob 생성
const blob = new Blob([ab], { type: mimeType });
const blobUrl = URL.createObjectURL(blob);
position: absolute + getBoundingClientRect() 조합 문제 발견position: fixed로 변경 (viewport 기준 통일)window.scrollX/Y 추가ScreenshotSelector.tsx
// Before
img.style.position = 'absolute';
img.style.left = `${absoluteLeft}px`;
img.style.top = `${absoluteTop}px`;
// After
img.style.position = 'fixed';
img.style.left = `${canvasRect.left}px`; // viewport 기준
img.style.top = `${canvasRect.top}px`;
// 직접 canvas crop 로직 추가
if (hasCanvasSnapshots) {
const outputCanvas = document.createElement('canvas');
ctx.drawImage(
sourceImg,
relativeX * scaleX, relativeY * scaleY, // 소스 좌표
cropWidth * scaleX, cropHeight * scaleY, // 소스 크기
0, 0, outputCanvas.width, outputCanvas.height // 대상
);
onCapture(outputCanvas.toDataURL('image/png', 1.0));
return;
}
// html2canvas fallback
const canvas = await html2canvas(document.body, {
x: left + window.scrollX, // 스크롤 위치 추가
y: top + window.scrollY,
// ...
});
blob: URL은 브라우저 메모리에만 존재하는 리소스.
백엔드(Firebase Functions)에서는 이 URL에 접근 불가.
게스트 모드에서는:
ConceptView.tsx / MemorizeView.tsx
// blob URL 감지 시 텍스트 추출 모드로 전환
if ((fileId === 'guest' || !currentUser) && urlToUse.startsWith('blob:')) {
console.log("[ConceptView] Guest mode: extracting text from blob URL");
const extractedText = await extractTextFromPdf(urlToUse);
// pdfUrl 대신 transcript로 전송
await conceptService.extractConcepts(extractedText, { isTranscript: true });
}
functions/index.ts
// transcript 파라미터 지원 추가
const { pdfUrl, transcript, pageRanges, ... } = req.body;
if (!pdfUrl && !transcript) {
res.status(400).send("PDF URL 또는 transcript가 필요합니다.");
return;
}
// Transcript 모드 처리
if (transcript) {
const stream = await extractConceptsWithGeminiService(
null, // PDF base64 없음
language,
languageInstruction,
transcript, // 텍스트 직접 전달
customPrompt
);
// ... 스트리밍 응답
}
ConceptService.ts / MemorizeService.ts
// URL이면 pdfUrl + pageRanges, 아니면 transcript만
if (isUrl) {
requestBody.pdfUrl = pdfUrl;
requestBody.pageRanges = options?.pageRanges || null;
} else {
requestBody.transcript = pdfUrl; // 추출된 텍스트
// transcript 모드에서는 pageRanges를 전송하지 않음
}
MemorizeView.tsx
// 게스트 모드: 페이지 범위 옵션 없이 바로 전체 생성
if (fileId === "guest") {
setShowOptions(false);
generateMemorize(); // pageRanges 없이 호출
}
React의 stale closure 문제.
useCallback 내부에서 selectedImages 상태를 직접 참조하면 클로저에 캡처된 시점의 값을 사용.
setSelectedImages(prev => [...prev, file])GeminiChatView.tsx
// Before: stale closure 발생
const handleScreenshotCapture = (imageData: string) => {
setSelectedImages([...selectedImages, file]); // selectedImages가 오래된 값
};
// After: 함수형 업데이트로 최신 상태 보장
const handleScreenshotCapture = useCallback((imageData: string) => {
setSelectedImages(prev => [...prev, file]);
setImagePreviews(prev => [...prev, imageData]);
// 부모에게 처리 완료 알림
if (onCapturedImageProcessed) {
onCapturedImageProcessed();
}
}, [onCapturedImageProcessed]);
| 버그 | 핵심 원인 | 해결 난이도 |
|---|---|---|
| PDF 로딩 실패 | blob URL의 Document 컨텍스트 한계 | ⭐⭐⭐⭐⭐ |
| 캡처 영역 불일치 | 좌표 체계 혼란 + iframe 접근 제한 | ⭐⭐⭐⭐ |
| AI 기능 실패 | blob URL 서버 전송 불가 | ⭐⭐⭐ |
| 파라미터 충돌 | 모드별 파라미터 분리 미흡 | ⭐⭐ |
| 미리보기 안됨 | React stale closure | ⭐⭐ |