diff --git a/frontend/src/pages/DocViewerPage.jsx b/frontend/src/pages/DocViewerPage.jsx
index ac50e55..291db63 100644
--- a/frontend/src/pages/DocViewerPage.jsx
+++ b/frontend/src/pages/DocViewerPage.jsx
@@ -1,32 +1,99 @@
-// 이 파일이 하는 일: 문서 하나를 iframe으로 크게 띄우는 뷰어.
-// 주소의 :slug로 문서를 찾아 filePath의 HTML을 그대로 보여준다.
-import { useEffect, useState } from 'react';
+// 이 파일이 하는 일: 학습 문서 하나를 iframe 없이 React 안에서 네이티브로 렌더한다.
+//
+// ⚠️ 왜 iframe을 걷어냈나 — iframe은 "페이지 속 또 다른 완성된 페이지"라, 스크롤이 이중으로
+// 생기고(바깥+안쪽), 모바일에선 문서가 좁은 창에 갇혔다. 앱과 완전히 분리된 세계라 로고도
+// 두 번 나왔다. 그렇다고 문서 HTML을 앱 화면에 그냥 부어 넣으면, 문서마다 다른 226줄짜리
+// 자체 스타일(.wrap, .callout …)이 앱 CSS와 뒤엉켜 서로를 망가뜨린다.
+//
+// 해법: Shadow DOM. 문서의 HTML과 자체
${bodyInner}
`);
+ })
+ .catch(() => {
+ if (!cancelled) setError(true);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [doc]);
+
+ // 학습 포인트: 몰입 화면에선 ESC로 빠져나가는 게 관례다. 키보드만 쓰는 사용자를 위한 배려.
+ useEffect(() => {
+ const onKey = (e) => {
+ if (e.key === 'Escape') navigate('/docs');
+ };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [navigate]);
+
+ if (error) {
return (
문서를 찾을 수 없어요.
@@ -37,38 +104,25 @@ export default function DocViewerPage() {
);
}
- if (!doc) {
+ if (!doc || !shadowHtml) {
return
불러오는 중...
;
}
return (
-
-
- {/* whiteSpace: nowrap — 모바일에서 버튼 글자가 "뒤로가/기"로 꺾이는 것 방지 */}
-
navigate(-1)}>
- ← 뒤로가기
+ // position: fixed + inset:0 → 앱 상단바까지 덮어 문서가 화면을 가득 쓴다(몰입).
+ // 앱 크롬이 물러나므로 문서의 자체 헤더가 "중복 로고"가 아니라 "이 페이지의 제목"이 된다.
+
+ {/* 얇은 상단 바: 나가는 길 하나만. 문서 제목은 문서 자체에 있으니 반복하지 않는다. */}
+
+
navigate('/docs')}>
+ 문서 목록
-
{doc.title}
+
{doc.title}
+
+ {/* 문서 본문 — 남은 세로 공간을 채우고 여기'만' 스크롤한다(이중 스크롤 없음). */}
+
+
- {/* 학습 포인트: 기존 HTML 자산 재사용 —
- 학습 문서들은 이미 완성된 정적 HTML(public/docs/*.html)이다.
- React로 다시 만들 필요 없이 iframe으로 "액자에 넣듯" 보여주면
- 문서의 자체 스타일도 그대로 살고 앱 CSS와 충돌하지도 않는다.
- ⚠️ ?v= 캐시 버스터: 예전에 잘못된 보안헤더(X-Frame-Options:DENY)가 붙은 문서가
- 브라우저 캐시에 박혀 새로고침으로도 안 지워지는 문제가 있었다. 주소 뒤에 버전을
- 붙이면 브라우저가 "다른 파일"로 보고 새로 받아온다(캐시 키가 달라짐). 헤더가 또
- 바뀌면 이 숫자만 올리면 된다. */}
-
);
}
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 947a984..e87203c 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -1416,3 +1416,54 @@ inline-code, .icode {
.cert-border { box-shadow: none; border-color: #333; }
body { background: #fff; }
}
+
+/* ── 학습 문서 몰입 뷰어 (DocViewerPage) ──────────────────────────────
+ 학습 포인트: 문서를 볼 때만 뷰포트 전체를 덮어 문서가 화면을 가득 쓰게 한다.
+ position: fixed + inset:0 이 앱 상단바까지 덮는다(그래서 z-index를 상단바보다 높게).
+ 세로 flex로 [얇은 바][문서]를 쌓고, 문서(iframe)에 flex:1을 줘서 남는 공간을 전부
+ 차지하게 한다 — 높이를 calc(100vh - 얼마)처럼 손으로 계산하지 않아도 된다.
+ 이게 예전 방식보다 나은 점: 이중 스크롤이 사라지고(스크롤은 iframe 하나뿐),
+ 모바일에서 문서가 작은 상자에 갇히지 않는다. */
+.doc-immersive {
+ position: fixed;
+ inset: 0;
+ z-index: 50; /* 앱 상단바(z-index:10)보다 위 */
+ display: flex;
+ flex-direction: column;
+ background: var(--bg);
+}
+
+.doc-immersive-bar {
+ flex: none;
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-2) var(--space-3);
+ border-bottom: 1px solid var(--line);
+ background: var(--card);
+}
+
+.doc-immersive-title {
+ font-weight: var(--weight-bold);
+ color: var(--ink);
+ font-size: var(--text-sm);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis; /* 제목이 길면 … 로 줄여 한 줄 유지 */
+}
+
+/* 문서 본문 스크롤 영역 — 예전엔 iframe이 이 자리에 있었다. 지금은 Shadow DOM 호스트를
+ 담고 이 영역'만' 세로 스크롤한다(바깥 페이지는 fixed라 스크롤이 없다 → 이중 스크롤 제거).
+ 학습 문서들은 밝은 배경으로 디자인돼 있어 배경을 흰색으로 둔다(다크 모드에서도 문서는 밝게). */
+.doc-immersive-scroll {
+ flex: 1;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch; /* iOS에서 관성 스크롤 */
+ background: #fff;
+}
+
+/* Shadow DOM 호스트: 문서 스타일이 여기 격리된다. 폭은 문서 자체 CSS(.wrap 등)가 정하므로
+ 호스트는 블록으로만 두면 된다. */
+.doc-shadow-host {
+ display: block;
+}