ChoiPDFV-for-WebOS23/ChoiPDFv/2index.html

488 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Modern PDF Viewer</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary-bg: #1e1e1e;
--secondary-bg: #2d2d2d;
--tertiary-bg: #3c3c3c;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent-color: #0099ff;
--border-color: #4a4a4a;
}
body, html {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
font-family: 'Noto Sans KR', sans-serif;
background: var(--primary-bg);
color: var(--text-primary);
display: flex;
overflow: hidden;
}
/* --- 레이아웃 --- */
#file-sidebar {
width: 300px;
background: var(--secondary-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
}
main {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
#viewer-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background: var(--primary-bg);
overflow: hidden;
touch-action: none;
user-select: none;
}
#page-sidebar {
width: 250px;
background: var(--secondary-bg);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
}
/* 세로 모드 (Portrait) 레이아웃 */
body.portrait {
flex-direction: column;
}
body.portrait #file-sidebar, body.portrait #page-sidebar {
width: 100%;
border: none;
border-bottom: 1px solid var(--border-color);
max-height: 25vh; /* 세로 모드에서는 사이드바 높이 제한 */
}
body.portrait main {
flex: 1;
}
/* --- 헤더 및 푸터 (파일 정보, 페이지네이션) --- */
#viewer-header, #viewer-footer {
background: rgba(45, 45, 45, 0.9);
padding: 12px 20px;
text-align: center;
position: absolute;
left: 0;
right: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
backdrop-filter: blur(5px);
}
#viewer-header {
top: 0;
border-bottom: 1px solid var(--border-color);
}
#viewer-footer {
bottom: 0;
border-top: 1px solid var(--border-color);
}
#file-name-display {
font-size: 1.1em;
font-weight: 500;
}
#page-info-display {
font-size: 1.1em;
}
.nav-btn {
background: none;
border: none;
color: var(--text-primary);
font-size: 2em; /* 터치를 위해 아이콘 키움 */
cursor: pointer;
padding: 0 15px;
transition: color 0.2s;
}
.nav-btn:hover {
color: var(--accent-color);
}
.nav-btn:disabled {
color: var(--tertiary-bg);
cursor: not-allowed;
}
/* --- 사이드바 콘텐츠 --- */
.sidebar-header {
padding: 20px;
font-size: 1.4em;
font-weight: 700;
background: var(--tertiary-bg);
border-bottom: 1px solid var(--border-color);
}
.sidebar-content {
overflow-y: auto;
flex: 1;
}
.sidebar-content ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-content li {
padding: 15px 20px;
cursor: pointer;
border-bottom: 1px solid var(--border-color);
transition: background-color 0.2s;
font-size: 1.1em;
display: flex;
align-items: center;
}
.sidebar-content li:hover {
background: var(--tertiary-bg);
}
/* 현재 선택된 항목 스타일 */
.sidebar-content li.active {
background-color: var(--accent-color);
color: white;
font-weight: 700;
}
.sidebar-content li.active:hover {
background-color: #007acc;
}
/* 파일 목록 폴더/파일 아이콘 */
#file-list .folder > .item-content::before {
font-family: "Font Awesome 6 Free";
content: '\f07b'; /* folder icon */
margin-right: 15px;
font-weight: 900;
}
#file-list .file > .item-content::before {
font-family: "Font Awesome 6 Free";
content: '\f15c'; /* file-alt icon */
margin-right: 15px;
font-weight: 900;
}
/* 폴더 구조 들여쓰기 */
#file-list ul {
padding-left: 20px;
}
/* --- 캔버스 --- */
canvas {
background: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
transition: transform 0.1s ease-out;
}
</style>
</head>
<body>
<aside id="file-sidebar">
<div class="sidebar-header">PDF 목록</div>
<div class="sidebar-content">
<ul id="file-list"></ul>
</div>
</aside>
<main>
<header id="viewer-header">
<span id="file-name-display">파일을 선택하세요</span>
</header>
<div id="viewer-container">
<canvas id="pdf-canvas"></canvas>
</div>
<footer id="viewer-footer">
<button id="prev-page-btn" class="nav-btn"><i class="fas fa-chevron-left"></i></button>
<span id="page-info-display">- / -</span>
<button id="next-page-btn" class="nav-btn"><i class="fas fa-chevron-right"></i></button>
</footer>
</main>
<aside id="page-sidebar">
<div class="sidebar-header">페이지</div>
<div class="sidebar-content">
<ul id="page-list"></ul>
</div>
</aside>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
<script>
// 전역 변수 및 DOM 요소
const canvas = document.getElementById('pdf-canvas');
const ctx = canvas.getContext('2d');
const viewerContainer = document.getElementById('viewer-container');
const pageListEl = document.getElementById('page-list');
const fileListEl = document.getElementById('file-list');
const fileNameDisplay = document.getElementById('file-name-display');
const pageInfoDisplay = document.getElementById('page-info-display');
const prevPageBtn = document.getElementById('prev-page-btn');
const nextPageBtn = document.getElementById('next-page-btn');
let pdfDoc = null;
let currentPageNum = 1;
let currentFileName = '';
let scale = 1.0;
let posX = 0, posY = 0;
const SCALE_STEPS = [1.0, 1.5, 2.5];
let currentScaleStep = 0;
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js`;
// ===================================================================
// 1. 페이지 이동 시 스케일 100%로 초기화
// ===================================================================
function resetScaleAndPosition() {
scale = 1.0;
currentScaleStep = 0;
posX = 0;
posY = 0;
}
// ===================================================================
// 2. 현재 페이지를 페이지 목록에 가독성 있게 표시
// ===================================================================
function updateActivePageLink() {
const pageLinks = pageListEl.querySelectorAll('li');
pageLinks.forEach((link, index) => {
if (index + 1 === currentPageNum) {
link.classList.add('active');
// 현재 페이지가 보이도록 스크롤
link.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
link.classList.remove('active');
}
});
}
// ===================================================================
// 3. 파일 이름 및 페이지 번호 표시
// ===================================================================
function updateViewerInfo() {
fileNameDisplay.textContent = currentFileName;
if(pdfDoc) {
pageInfoDisplay.textContent = `${currentPageNum} / ${pdfDoc.numPages}`;
} else {
pageInfoDisplay.textContent = `- / -`;
}
}
// PDF 로드 함수
async function loadPdf(url, fileName) {
try {
const loadingTask = pdfjsLib.getDocument({ url });
pdfDoc = await loadingTask.promise;
currentFileName = fileName;
currentPageNum = 1;
resetScaleAndPosition();
generatePageList();
renderPage(currentPageNum);
updateViewerInfo();
} catch (err) {
alert("PDF를 불러올 수 없습니다.");
console.error("PDF 로드 오류:", err);
}
}
// 페이지 렌더링 함수
async function renderPage(num) {
if (!pdfDoc) return;
const page = await pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
await page.render(renderContext).promise;
// 위치 초기화 후 적용
canvas.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
updateActivePageLink();
updateViewerInfo();
// 버튼 상태 업데이트
prevPageBtn.disabled = (currentPageNum <= 1);
nextPageBtn.disabled = (currentPageNum >= pdfDoc.numPages);
}
// 페이지 목록 생성
function generatePageList() {
pageListEl.innerHTML = '';
for (let i = 1; i <= pdfDoc.numPages; i++) {
const li = document.createElement('li');
li.textContent = `페이지 ${i}`;
li.dataset.pageNum = i;
li.onclick = () => {
currentPageNum = i;
resetScaleAndPosition(); // 페이지 목록 클릭 시 스케일 초기화
renderPage(currentPageNum);
};
pageListEl.appendChild(li);
}
}
// 네비게이션 버튼 이벤트
prevPageBtn.onclick = () => {
if (currentPageNum <= 1) return;
currentPageNum--;
resetScaleAndPosition(); // 페이지 이동 시 스케일 초기화
renderPage(currentPageNum);
};
nextPageBtn.onclick = () => {
if (currentPageNum >= pdfDoc.numPages) return;
currentPageNum++;
resetScaleAndPosition(); // 페이지 이동 시 스케일 초기화
renderPage(currentPageNum);
};
// ===================================================================
// 4. PDF 파일 목록을 폴더 구조로 표시 및 현재 파일 강조
// ===================================================================
function createFileList(items, container) {
items.forEach(item => {
const li = document.createElement('li');
// 터치 영역을 넓히기 위해 내부 div 추가
const itemContent = document.createElement('div');
itemContent.className = 'item-content';
itemContent.textContent = item.name;
li.appendChild(itemContent);
if (item.type === 'folder') {
li.classList.add('folder');
if (item.children) {
const sublist = document.createElement('ul');
createFileList(item.children, sublist);
li.appendChild(sublist);
}
// 폴더 클릭 시 하위 목록 토글 (선택적 기능)
itemContent.onclick = (e) => {
e.stopPropagation();
li.querySelector('ul')?.classList.toggle('collapsed');
};
} else {
li.classList.add('file');
itemContent.onclick = (e) => {
e.stopPropagation();
// 기존 active 클래스 제거
document.querySelectorAll('#file-list li.active').forEach(el => el.classList.remove('active'));
// 현재 클릭된 파일에 active 클래스 추가
li.classList.add('active');
loadPdf(item.uri, item.name);
};
}
container.appendChild(li);
});
}
async function loadFileList() {
try {
// pdf-list.json 파일을 폴더 구조로 수정해야 합니다. (아래 예시 참고)
const response = await fetch("assets/pdf-list.json");
const files = await response.json();
fileListEl.innerHTML = '';
createFileList(files, fileListEl);
} catch (err) {
console.error("파일 목록 로드 실패:", err);
fileListEl.innerHTML = '<li>파일 목록을 불러올 수 없습니다.</li>';
}
}
// ===================================================================
// 5. 터치 인터페이스 (Hammer.js)
// ===================================================================
const manager = new Hammer.Manager(viewerContainer, {
inputClass: Hammer.PointerEventInput
});
const pinch = new Hammer.Pinch();
const pan = new Hammer.Pan({ direction: Hammer.DIRECTION_ALL });
const doubleTap = new Hammer.Tap({ event: 'doubletap', taps: 2 });
manager.add([pinch, pan, doubleTap]);
// 더블탭: 확대/축소
manager.on("doubletap", ev => {
currentScaleStep = (currentScaleStep + 1) % SCALE_STEPS.length;
scale = SCALE_STEPS[currentScaleStep];
posX = 0; // 더블탭 시 위치도 초기화하여 중앙에서 확대되도록
posY = 0;
renderPage(currentPageNum);
});
// 핀치: 확대/축소
let startScale = 1.0;
manager.on("pinchstart", () => {
startScale = scale;
});
manager.on("pinchmove", ev => {
scale = Math.min(Math.max(0.5, startScale * ev.scale), 4);
canvas.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
});
// 드래그 (Pan)
let startPosX = 0, startPosY = 0;
manager.on("panstart", () => {
startPosX = posX;
startPosY = posY;
});
manager.on("panmove", ev => {
if(scale <= 1) return; // 100% 스케일 이하에서는 이동 방지
posX = startPosX + ev.deltaX;
posY = startPosY + ev.deltaY;
canvas.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
});
// 방향 감지 및 레이아웃 조정 (webOS 용 코드는 그대로 유지)
function applyOrientationLayout() {
// webOS 환경이 아닐 경우 브라우저 창 크기로 판단
if (window.innerHeight > window.innerWidth) {
document.body.classList.add('portrait');
} else {
document.body.classList.remove('portrait');
}
}
window.addEventListener('resize', applyOrientationLayout);
// 초기화
window.onload = function() {
applyOrientationLayout();
loadFileList();
updateViewerInfo();
// 초기 버튼 상태
prevPageBtn.disabled = true;
nextPageBtn.disabled = true;
};
</script>
</body>
</html>