feat: 완전한 문서 업로드 및 관리 시스템 구현
- 백엔드 API 완전 구현 (FastAPI + SQLAlchemy + PostgreSQL) - 사용자 인증 (JWT 토큰 기반) - 문서 CRUD (업로드, 조회, 목록, 삭제) - 하이라이트, 메모, 책갈피 관리 - 태그 시스템 및 검색 기능 - Pydantic v2 호환성 수정 - 프론트엔드 완전 구현 (Alpine.js + Tailwind CSS) - 로그인/로그아웃 기능 - 문서 업로드 모달 (드래그앤드롭, 파일 검증) - 문서 목록 및 필터링 - 뷰어 페이지 (하이라이트, 메모, 책갈피 UI) - 실시간 목록 새로고침 - 시스템 안정성 개선 - Alpine.js 컴포넌트 간 안전한 통신 (이벤트 기반) - API 오류 처리 및 사용자 피드백 - 파비콘 추가로 404 오류 해결 - 포트 구성: Frontend(24100), Backend(24102), DB(24101), Redis(24103)
This commit is contained in:
@@ -7,9 +7,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.core.security import verify_token, get_user_id_from_token
|
||||
from src.models.user import User
|
||||
from ..core.database import get_db
|
||||
from ..core.security import verify_token, get_user_id_from_token
|
||||
from ..models.user import User
|
||||
|
||||
|
||||
# HTTP Bearer 토큰 스키마
|
||||
|
||||
@@ -6,15 +6,15 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from datetime import datetime
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.core.security import verify_password, create_access_token, create_refresh_token, get_password_hash
|
||||
from src.core.config import settings
|
||||
from src.models.user import User
|
||||
from src.schemas.auth import (
|
||||
from ...core.database import get_db
|
||||
from ...core.security import verify_password, create_access_token, create_refresh_token, get_password_hash
|
||||
from ...core.config import settings
|
||||
from ...models.user import User
|
||||
from ...schemas.auth import (
|
||||
LoginRequest, TokenResponse, RefreshTokenRequest,
|
||||
UserInfo, ChangePasswordRequest, CreateUserRequest
|
||||
)
|
||||
from src.api.dependencies import get_current_active_user, get_current_admin_user
|
||||
from ..dependencies import get_current_active_user, get_current_admin_user
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
@@ -71,7 +71,7 @@ async def refresh_token(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""토큰 갱신"""
|
||||
from src.core.security import verify_token
|
||||
from ...core.security import verify_token
|
||||
|
||||
try:
|
||||
# 리프레시 토큰 검증
|
||||
@@ -116,7 +116,7 @@ async def get_current_user_info(
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""현재 사용자 정보 조회"""
|
||||
return UserInfo.from_orm(current_user)
|
||||
return UserInfo.model_validate(current_user)
|
||||
|
||||
|
||||
@router.put("/change-password")
|
||||
|
||||
@@ -8,11 +8,11 @@ from sqlalchemy.orm import joinedload
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.models.user import User
|
||||
from src.models.document import Document
|
||||
from src.models.bookmark import Bookmark
|
||||
from src.api.dependencies import get_current_active_user
|
||||
from ...core.database import get_db
|
||||
from ...models.user import User
|
||||
from ...models.document import Document
|
||||
from ...models.bookmark import Bookmark
|
||||
from ..dependencies import get_current_active_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ import uuid
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.core.config import settings
|
||||
from src.models.user import User
|
||||
from src.models.document import Document, Tag
|
||||
from src.api.dependencies import get_current_active_user, get_current_admin_user
|
||||
from ...core.database import get_db
|
||||
from ...core.config import settings
|
||||
from ...models.user import User
|
||||
from ...models.document import Document, Tag
|
||||
from ..dependencies import get_current_active_user, get_current_admin_user
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
@@ -110,9 +110,24 @@ async def list_documents(
|
||||
# 응답 데이터 변환
|
||||
response_data = []
|
||||
for doc in documents:
|
||||
doc_data = DocumentResponse.from_orm(doc)
|
||||
doc_data.uploader_name = doc.uploader.full_name or doc.uploader.email
|
||||
doc_data.tags = [tag.name for tag in doc.tags]
|
||||
doc_data = DocumentResponse(
|
||||
id=str(doc.id),
|
||||
title=doc.title,
|
||||
description=doc.description,
|
||||
html_path=doc.html_path,
|
||||
pdf_path=doc.pdf_path,
|
||||
thumbnail_path=doc.thumbnail_path,
|
||||
file_size=doc.file_size,
|
||||
page_count=doc.page_count,
|
||||
language=doc.language,
|
||||
is_public=doc.is_public,
|
||||
is_processed=doc.is_processed,
|
||||
created_at=doc.created_at,
|
||||
updated_at=doc.updated_at,
|
||||
document_date=doc.document_date,
|
||||
uploader_name=doc.uploader.full_name or doc.uploader.email,
|
||||
tags=[tag.name for tag in doc.tags]
|
||||
)
|
||||
response_data.append(doc_data)
|
||||
|
||||
return response_data
|
||||
@@ -123,8 +138,9 @@ async def upload_document(
|
||||
title: str = Form(...),
|
||||
description: Optional[str] = Form(None),
|
||||
document_date: Optional[str] = Form(None),
|
||||
language: Optional[str] = Form("ko"),
|
||||
is_public: bool = Form(False),
|
||||
tags: Optional[str] = Form(None), # 쉼표로 구분된 태그
|
||||
tags: Optional[List[str]] = Form(None), # 태그 리스트
|
||||
html_file: UploadFile = File(...),
|
||||
pdf_file: Optional[UploadFile] = File(None),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
@@ -172,7 +188,8 @@ async def upload_document(
|
||||
description=description,
|
||||
html_path=html_path,
|
||||
pdf_path=pdf_path,
|
||||
file_size=len(await html_file.read()) if html_file else None,
|
||||
language=language,
|
||||
file_size=len(content), # HTML 파일 크기
|
||||
uploaded_by=current_user.id,
|
||||
original_filename=html_file.filename,
|
||||
is_public=is_public,
|
||||
@@ -201,14 +218,34 @@ async def upload_document(
|
||||
document.tags.append(tag)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(document)
|
||||
|
||||
# 문서 정보를 다시 로드 (태그 포함)
|
||||
result = await db.execute(
|
||||
select(Document)
|
||||
.options(selectinload(Document.tags))
|
||||
.where(Document.id == document.id)
|
||||
)
|
||||
document_with_tags = result.scalar_one()
|
||||
|
||||
# 응답 데이터 생성
|
||||
response_data = DocumentResponse.from_orm(document)
|
||||
response_data.uploader_name = current_user.full_name or current_user.email
|
||||
response_data.tags = [tag.name for tag in document.tags]
|
||||
|
||||
return response_data
|
||||
return DocumentResponse(
|
||||
id=str(document_with_tags.id),
|
||||
title=document_with_tags.title,
|
||||
description=document_with_tags.description,
|
||||
html_path=document_with_tags.html_path,
|
||||
pdf_path=document_with_tags.pdf_path,
|
||||
thumbnail_path=document_with_tags.thumbnail_path,
|
||||
file_size=document_with_tags.file_size,
|
||||
page_count=document_with_tags.page_count,
|
||||
language=document_with_tags.language,
|
||||
is_public=document_with_tags.is_public,
|
||||
is_processed=document_with_tags.is_processed,
|
||||
created_at=document_with_tags.created_at,
|
||||
updated_at=document_with_tags.updated_at,
|
||||
document_date=document_with_tags.document_date,
|
||||
uploader_name=current_user.full_name or current_user.email,
|
||||
tags=[tag.name for tag in document_with_tags.tags]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 파일 정리
|
||||
@@ -250,11 +287,24 @@ async def get_document(
|
||||
detail="Not enough permissions"
|
||||
)
|
||||
|
||||
response_data = DocumentResponse.from_orm(document)
|
||||
response_data.uploader_name = document.uploader.full_name or document.uploader.email
|
||||
response_data.tags = [tag.name for tag in document.tags]
|
||||
|
||||
return response_data
|
||||
return DocumentResponse(
|
||||
id=str(document.id),
|
||||
title=document.title,
|
||||
description=document.description,
|
||||
html_path=document.html_path,
|
||||
pdf_path=document.pdf_path,
|
||||
thumbnail_path=document.thumbnail_path,
|
||||
file_size=document.file_size,
|
||||
page_count=document.page_count,
|
||||
language=document.language,
|
||||
is_public=document.is_public,
|
||||
is_processed=document.is_processed,
|
||||
created_at=document.created_at,
|
||||
updated_at=document.updated_at,
|
||||
document_date=document.document_date,
|
||||
uploader_name=document.uploader.full_name or document.uploader.email,
|
||||
tags=[tag.name for tag in document.tags]
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{document_id}")
|
||||
@@ -307,7 +357,6 @@ async def list_tags(
|
||||
# 각 태그의 문서 수 계산
|
||||
response_data = []
|
||||
for tag in tags:
|
||||
tag_data = TagResponse.from_orm(tag)
|
||||
# 문서 수 계산 (권한 고려)
|
||||
doc_query = select(Document).join(Document.tags).where(Tag.id == tag.id)
|
||||
if not current_user.is_admin:
|
||||
@@ -318,7 +367,15 @@ async def list_tags(
|
||||
)
|
||||
)
|
||||
doc_result = await db.execute(doc_query)
|
||||
tag_data.document_count = len(doc_result.scalars().all())
|
||||
document_count = len(doc_result.scalars().all())
|
||||
|
||||
tag_data = TagResponse(
|
||||
id=str(tag.id),
|
||||
name=tag.name,
|
||||
color=tag.color,
|
||||
description=tag.description,
|
||||
document_count=document_count
|
||||
)
|
||||
response_data.append(tag_data)
|
||||
|
||||
return response_data
|
||||
@@ -353,7 +410,10 @@ async def create_tag(
|
||||
await db.commit()
|
||||
await db.refresh(tag)
|
||||
|
||||
response_data = TagResponse.from_orm(tag)
|
||||
response_data.document_count = 0
|
||||
|
||||
return response_data
|
||||
return TagResponse(
|
||||
id=str(tag.id),
|
||||
name=tag.name,
|
||||
color=tag.color,
|
||||
description=tag.description,
|
||||
document_count=0
|
||||
)
|
||||
|
||||
@@ -8,12 +8,12 @@ from sqlalchemy.orm import selectinload
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.models.user import User
|
||||
from src.models.document import Document
|
||||
from src.models.highlight import Highlight
|
||||
from src.models.note import Note
|
||||
from src.api.dependencies import get_current_active_user
|
||||
from ...core.database import get_db
|
||||
from ...models.user import User
|
||||
from ...models.document import Document
|
||||
from ...models.highlight import Highlight
|
||||
from ...models.note import Note
|
||||
from ..dependencies import get_current_active_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ from sqlalchemy.orm import selectinload, joinedload
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.models.user import User
|
||||
from src.models.highlight import Highlight
|
||||
from src.models.note import Note
|
||||
from src.models.document import Document
|
||||
from src.api.dependencies import get_current_active_user
|
||||
from ...core.database import get_db
|
||||
from ...models.user import User
|
||||
from ...models.highlight import Highlight
|
||||
from ...models.note import Note
|
||||
from ...models.document import Document
|
||||
from ..dependencies import get_current_active_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ from sqlalchemy.orm import joinedload, selectinload
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.models.user import User
|
||||
from src.models.document import Document, Tag
|
||||
from src.models.highlight import Highlight
|
||||
from src.models.note import Note
|
||||
from src.api.dependencies import get_current_active_user
|
||||
from ...core.database import get_db
|
||||
from ...models.user import User
|
||||
from ...models.document import Document, Tag
|
||||
from ...models.highlight import Highlight
|
||||
from ...models.note import Note
|
||||
from ..dependencies import get_current_active_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, delete
|
||||
from typing import List
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.models.user import User
|
||||
from src.schemas.auth import UserInfo
|
||||
from src.api.dependencies import get_current_active_user, get_current_admin_user
|
||||
from ...core.database import get_db
|
||||
from ...models.user import User
|
||||
from ...schemas.auth import UserInfo
|
||||
from ..dependencies import get_current_active_user, get_current_admin_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy import MetaData
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from src.core.config import settings
|
||||
from .config import settings
|
||||
|
||||
|
||||
# SQLAlchemy 메타데이터 설정
|
||||
@@ -57,7 +57,7 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
|
||||
async def init_db() -> None:
|
||||
"""데이터베이스 초기화"""
|
||||
from src.models import user, document, highlight, note, bookmark, tag
|
||||
from ..models import user, document, highlight, note, bookmark
|
||||
|
||||
async with engine.begin() as conn:
|
||||
# 모든 테이블 생성
|
||||
@@ -69,8 +69,8 @@ async def init_db() -> None:
|
||||
|
||||
async def create_admin_user() -> None:
|
||||
"""관리자 계정 생성 (존재하지 않을 경우)"""
|
||||
from src.models.user import User
|
||||
from src.core.security import get_password_hash
|
||||
from ..models.user import User
|
||||
from .security import get_password_hash
|
||||
from sqlalchemy import select
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
|
||||
@@ -7,7 +7,7 @@ from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from src.core.config import settings
|
||||
from .config import settings
|
||||
|
||||
|
||||
# 비밀번호 해싱 컨텍스트
|
||||
|
||||
@@ -7,9 +7,9 @@ from fastapi.staticfiles import StaticFiles
|
||||
from contextlib import asynccontextmanager
|
||||
import uvicorn
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.database import init_db
|
||||
from src.api.routes import auth, users, documents, highlights, notes, bookmarks, search
|
||||
from .core.config import settings
|
||||
from .core.database import init_db
|
||||
from .api.routes import auth, users, documents, highlights, notes, bookmarks, search
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""
|
||||
모델 패키지 초기화
|
||||
"""
|
||||
from src.models.user import User
|
||||
from src.models.document import Document, Tag
|
||||
from src.models.highlight import Highlight
|
||||
from src.models.note import Note
|
||||
from src.models.bookmark import Bookmark
|
||||
from .user import User
|
||||
from .document import Document, Tag
|
||||
from .highlight import Highlight
|
||||
from .note import Note
|
||||
from .bookmark import Bookmark
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
from ..core.database import Base
|
||||
|
||||
|
||||
class Bookmark(Base):
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
from ..core.database import Base
|
||||
|
||||
|
||||
# 문서-태그 다대다 관계 테이블
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
from ..core.database import Base
|
||||
|
||||
|
||||
class Highlight(Base):
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
from ..core.database import Base
|
||||
|
||||
|
||||
class Note(Base):
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from src.core.database import Base
|
||||
from ..core.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
@@ -27,7 +28,7 @@ class RefreshTokenRequest(BaseModel):
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""사용자 정보"""
|
||||
id: str
|
||||
id: UUID
|
||||
email: str
|
||||
full_name: Optional[str] = None
|
||||
is_active: bool
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>압력용기 설계 매뉴얼 - Pressure Vessel Design Manual</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.language-toggle {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
.language-toggle:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.content {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
h2 {
|
||||
color: #34495e;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
h3 {
|
||||
color: #7f8c8d;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.publisher-info {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
.toc {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.toc-item {
|
||||
margin: 5px 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.procedure {
|
||||
margin-left: 30px;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
.chapter {
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.page-number {
|
||||
float: right;
|
||||
color: #999;
|
||||
}
|
||||
.copyright {
|
||||
background: #e9ecef;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.preface {
|
||||
text-align: justify;
|
||||
line-height: 1.8;
|
||||
}
|
||||
/* 한국어 스타일 */
|
||||
[lang="ko"] {
|
||||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button class="language-toggle" onclick="toggleLanguage()">🌐 한국어/English</button>
|
||||
|
||||
<div class="content">
|
||||
<!-- 영어 버전 -->
|
||||
<div id="english-content">
|
||||
<h1>Pressure Vessel Design Manual</h1>
|
||||
<h2>Fourth Edition</h2>
|
||||
|
||||
<div class="publisher-info">
|
||||
<p><strong>Dennis R. Moss<br>Michael Basic</strong></p>
|
||||
<p>AMSTERDAM • BOSTON • HEIDELBERG • LONDON • NEW YORK • OXFORD<br>
|
||||
PARIS • SAN DIEGO • SAN FRANCISCO • SINGAPORE • SYDNEY • TOKYO</p>
|
||||
<p>Butterworth-Heinemann is an imprint of Elsevier</p>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
<h3>Copyright Information</h3>
|
||||
<p>Fourth edition 2013<br>
|
||||
Copyright © 2013 Elsevier Inc. All rights reserved</p>
|
||||
<p>No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means electronic, mechanical, photocopying, recording or otherwise without the prior written permission of the publisher</p>
|
||||
<p>ISBN: 978-0-12-387000-1</p>
|
||||
</div>
|
||||
|
||||
<h2>Contents</h2>
|
||||
<div class="toc">
|
||||
<div class="chapter">Preface to the 4th Edition <span class="page-number">ix</span></div>
|
||||
|
||||
<div class="chapter">1: General Topics <span class="page-number">1</span></div>
|
||||
<div class="procedure">Design Philosophy <span class="page-number">1</span></div>
|
||||
<div class="procedure">Stress Analysis <span class="page-number">2</span></div>
|
||||
<div class="procedure">Stress/Failure Theories <span class="page-number">3</span></div>
|
||||
<div class="procedure">Failures in Pressure Vessels <span class="page-number">7</span></div>
|
||||
<div class="procedure">Loadings <span class="page-number">8</span></div>
|
||||
<div class="procedure">Stress <span class="page-number">10</span></div>
|
||||
<div class="procedure">Thermal Stresses <span class="page-number">13</span></div>
|
||||
<div class="procedure">Discontinuity Stresses <span class="page-number">14</span></div>
|
||||
<div class="procedure">Fatigue Analysis for Cyclic Service <span class="page-number">15</span></div>
|
||||
<div class="procedure">Creep <span class="page-number">24</span></div>
|
||||
<div class="procedure">Cryogenic Applications <span class="page-number">32</span></div>
|
||||
<div class="procedure">Service Considerations <span class="page-number">34</span></div>
|
||||
<div class="procedure">Miscellaneous Design Considerations <span class="page-number">35</span></div>
|
||||
<div class="procedure">Items to be Included in a User's Design Specification (UDS) for ASME VIII-2 Vessels <span class="page-number">35</span></div>
|
||||
<div class="procedure">References <span class="page-number">36</span></div>
|
||||
|
||||
<div class="chapter">2: General Design <span class="page-number">37</span></div>
|
||||
<div class="procedure">Procedure 2-1: General Vessel Formulas <span class="page-number">38</span></div>
|
||||
<div class="procedure">Procedure 2-2: External Pressure Design <span class="page-number">42</span></div>
|
||||
<div class="procedure">Procedure 2-3: Properties of Stiffening Rings <span class="page-number">51</span></div>
|
||||
<div class="procedure">Procedure 2-4: Code Case 2286 <span class="page-number">54</span></div>
|
||||
<div class="procedure">Procedure 2-5: Design of Cones <span class="page-number">58</span></div>
|
||||
<div class="procedure">Procedure 2-6: Design of Toriconical Transitions <span class="page-number">67</span></div>
|
||||
<div class="procedure">Procedure 2-7: Stresses in Heads Due to Internal Pressure <span class="page-number">70</span></div>
|
||||
<div class="procedure">Procedure 2-8: Design of Intermediate Heads <span class="page-number">74</span></div>
|
||||
<div class="procedure">Procedure 2-9: Design of Flat Heads <span class="page-number">76</span></div>
|
||||
<div class="procedure">Procedure 2-10: Design of Large Openings in Flat Heads <span class="page-number">81</span></div>
|
||||
<div class="procedure">Procedure 2-11: Calculate MAP, MAWP, and Test Pressures <span class="page-number">83</span></div>
|
||||
<div class="procedure">Procedure 2-12: Nozzle Reinforcement <span class="page-number">85</span></div>
|
||||
<div class="procedure">Procedure 2-13: Find or Revise the Center of Gravity of a Vessel <span class="page-number">90</span></div>
|
||||
<div class="procedure">Procedure 2-14: Minimum Design Metal Temperature (MDMT) <span class="page-number">90</span></div>
|
||||
<div class="procedure">Procedure 2-15: Buckling of Thin Wall Cylindrical Shells <span class="page-number">95</span></div>
|
||||
<div class="procedure">Procedure 2-16: Optimum Vessel Proportions <span class="page-number">96</span></div>
|
||||
<div class="procedure">Procedure 2-17: Estimating Weights of Vessels and Vessel Components <span class="page-number">102</span></div>
|
||||
<div class="procedure">Procedure 2-18: Design of Jacketed Vessels <span class="page-number">124</span></div>
|
||||
<div class="procedure">Procedure 2-19: Forming Strains/Fiber Elongation <span class="page-number">134</span></div>
|
||||
<div class="procedure">References <span class="page-number">138</span></div>
|
||||
|
||||
<div class="chapter">3: Flange Design <span class="page-number">139</span></div>
|
||||
<div class="procedure">Introduction <span class="page-number">140</span></div>
|
||||
<div class="procedure">Procedure 3-1: Design of Flanges <span class="page-number">148</span></div>
|
||||
<div class="procedure">Procedure 3-2: Design of Spherically Dished Covers <span class="page-number">165</span></div>
|
||||
<div class="procedure">Procedure 3-3: Design of Blind Flanges with Openings <span class="page-number">167</span></div>
|
||||
<div class="procedure">Procedure 3-4: Bolt Torque Required for Sealing Flanges <span class="page-number">169</span></div>
|
||||
<div class="procedure">Procedure 3-5: Design of Studding Outlets <span class="page-number">172</span></div>
|
||||
<div class="procedure">Procedure 3-6: Reinforcement for Studding Outlets <span class="page-number">175</span></div>
|
||||
<div class="procedure">Procedure 3-7: Studding Flanges <span class="page-number">176</span></div>
|
||||
<div class="procedure">Procedure 3-8: Design of Elliptical, Internal Manways <span class="page-number">181</span></div>
|
||||
<div class="procedure">Procedure 3-9: Through Nozzles <span class="page-number">182</span></div>
|
||||
<div class="procedure">References <span class="page-number">183</span></div>
|
||||
|
||||
<div class="chapter">4: Design of Vessel Supports <span class="page-number">185</span></div>
|
||||
<div class="procedure">Introduction: Support Structures <span class="page-number">186</span></div>
|
||||
<div class="procedure">Procedure 4-1: Wind Design Per ASCE <span class="page-number">189</span></div>
|
||||
<div class="procedure">Procedure 4-2: Seismic Design - General <span class="page-number">199</span></div>
|
||||
<div class="procedure">Procedure 4-3: Seismic Design for Vessels <span class="page-number">204</span></div>
|
||||
<div class="procedure">Procedure 4-4: Seismic Design - Vessel on Unbraced Legs <span class="page-number">208</span></div>
|
||||
<div class="procedure">Procedure 4-5: Seismic Design - Vessel on Braced Legs <span class="page-number">217</span></div>
|
||||
<div class="procedure">Procedure 4-6: Seismic Design - Vessel on Rings <span class="page-number">223</span></div>
|
||||
<div class="procedure">Procedure 4-7: Seismic Design - Vessel on Lugs <span class="page-number">229</span></div>
|
||||
<div class="procedure">Procedure 4-8: Seismic Design - Vessel on Skirt <span class="page-number">239</span></div>
|
||||
<div class="procedure">Procedure 4-9: Seismic Design - Vessel on Conical Skirt <span class="page-number">248</span></div>
|
||||
<div class="procedure">Procedure 4-10: Design of Horizontal Vessel on Saddles <span class="page-number">253</span></div>
|
||||
<div class="procedure">Procedure 4-11: Design of Saddle Supports for Large Vessels <span class="page-number">267</span></div>
|
||||
<div class="procedure">Procedure 4-12: Design of Base Plates for Legs <span class="page-number">275</span></div>
|
||||
<div class="procedure">Procedure 4-13: Design of Lug Supports <span class="page-number">278</span></div>
|
||||
<div class="procedure">Procedure 4-14: Design of Base Details for Vertical Vessels-Shifted Neutral Axis Method <span class="page-number">281</span></div>
|
||||
<div class="procedure">Procedure 4-15: Design of Base Details for Vertical Vessels - Centered Neutral Axis Method <span class="page-number">291</span></div>
|
||||
<div class="procedure">Procedure 4-16: Design of Anchor Bolts for Vertical Vessels <span class="page-number">293</span></div>
|
||||
<div class="procedure">Procedure 4-17: Properties of Concrete <span class="page-number">295</span></div>
|
||||
<div class="procedure">References <span class="page-number">296</span></div>
|
||||
|
||||
<div class="chapter">5: Vessel Internals <span class="page-number">297</span></div>
|
||||
<div class="procedure">Procedure 5-1: Design of Internal Support Beds <span class="page-number">298</span></div>
|
||||
<div class="procedure">Procedure 5-2: Design of Lattice Beams <span class="page-number">310</span></div>
|
||||
<div class="procedure">Procedure 5-3: Shell Stresses due to Loadings at Support Beam Locations <span class="page-number">316</span></div>
|
||||
<div class="procedure">Procedure 5-4: Design of Support Blocks <span class="page-number">319</span></div>
|
||||
<div class="procedure">Procedure 5-5: Hub Rings used for Bed Supports <span class="page-number">321</span></div>
|
||||
<div class="procedure">Procedure 5-6: Design of Pipe Coils for Heat Transfer <span class="page-number">326</span></div>
|
||||
<div class="procedure">Procedure 5-7: Agitators/Mixers for Vessels and Tanks <span class="page-number">345</span></div>
|
||||
<div class="procedure">Procedure 5-8: Design of Internal Pipe Distributors <span class="page-number">353</span></div>
|
||||
<div class="procedure">Procedure 5-9: Design of Trays <span class="page-number">366</span></div>
|
||||
<div class="procedure">Procedure 5-10: Flow Over Weirs <span class="page-number">375</span></div>
|
||||
<div class="procedure">Procedure 5-11: Design of Demisters <span class="page-number">376</span></div>
|
||||
<div class="procedure">Procedure 5-12: Design of Baffles <span class="page-number">381</span></div>
|
||||
<div class="procedure">Procedure 5-13: Design of Impingement Plates <span class="page-number">391</span></div>
|
||||
<div class="procedure">References <span class="page-number">392</span></div>
|
||||
|
||||
<div class="chapter">6: Special Designs <span class="page-number">393</span></div>
|
||||
<div class="procedure">Procedure 6-1: Design of Large-Diameter Nozzle Openings <span class="page-number">394</span></div>
|
||||
<div class="procedure">Large Openings—Membrane and Bending Analysis <span class="page-number">397</span></div>
|
||||
<div class="procedure">Procedure 6-2: Tower Deflection <span class="page-number">397</span></div>
|
||||
<div class="procedure">Procedure 6-3: Design of Ring Girders <span class="page-number">401</span></div>
|
||||
<div class="procedure">Procedure 6-4: Design of Vessels with Refractory Linings <span class="page-number">406</span></div>
|
||||
<div class="procedure">Procedure 6-5: Vibration of Tall Towers and Stacks <span class="page-number">418</span></div>
|
||||
<div class="procedure">Procedure 6-6: Underground Tanks & Vessels <span class="page-number">428</span></div>
|
||||
<div class="procedure">Procedure 6-7: Local Thin Area (LTA) <span class="page-number">432</span></div>
|
||||
<div class="procedure">References <span class="page-number">433</span></div>
|
||||
|
||||
<div class="chapter">7: Local Loads <span class="page-number">435</span></div>
|
||||
<div class="procedure">Procedure 7-1: Stresses in Circular Rings <span class="page-number">437</span></div>
|
||||
<div class="procedure">Procedure 7-2: Design of Partial Ring Stiffeners <span class="page-number">446</span></div>
|
||||
<div class="procedure">Procedure 7-3: Attachment Parameters <span class="page-number">448</span></div>
|
||||
<div class="procedure">Procedure 7-4: Stresses in Cylindrical Shells from External Local Loads <span class="page-number">449</span></div>
|
||||
<div class="procedure">Procedure 7-5: Stresses in Spherical Shells from External Local Loads <span class="page-number">465</span></div>
|
||||
<div class="procedure">References <span class="page-number">472</span></div>
|
||||
|
||||
<div class="chapter">8: High Pressure Vessels <span class="page-number">473</span></div>
|
||||
<div class="procedure">1.0. General <span class="page-number">474</span></div>
|
||||
<div class="procedure">2.0. Shell Design <span class="page-number">496</span></div>
|
||||
<div class="procedure">3.0. Design of Closures <span class="page-number">502</span></div>
|
||||
<div class="procedure">4.0. Nozzles <span class="page-number">551</span></div>
|
||||
<div class="procedure">5.0. References <span class="page-number">556</span></div>
|
||||
|
||||
<div class="chapter">9: Related Equipment <span class="page-number">557</span></div>
|
||||
<div class="procedure">Procedure 9-1: Design of Davits <span class="page-number">558</span></div>
|
||||
<div class="procedure">Procedure 9-2: Design of Circular Platforms <span class="page-number">563</span></div>
|
||||
<div class="procedure">Procedure 9-3: Design of Square and Rectangular Platforms <span class="page-number">571</span></div>
|
||||
<div class="procedure">Procedure 9-4: Design of Pipe Supports <span class="page-number">576</span></div>
|
||||
<div class="procedure">Procedure 9-5: Shear Loads in Bolted Connections <span class="page-number">584</span></div>
|
||||
<div class="procedure">Procedure 9-6: Design of Bins and Elevated Tanks <span class="page-number">586</span></div>
|
||||
<div class="procedure">Procedure 9-7: Field-Fabricated Spheres <span class="page-number">594</span></div>
|
||||
<div class="procedure">References <span class="page-number">630</span></div>
|
||||
|
||||
<div class="chapter">10: Transportation and Erection of Pressure Vessels <span class="page-number">631</span></div>
|
||||
<div class="procedure">Procedure 10-1: Transportation of Pressure Vessels <span class="page-number">632</span></div>
|
||||
<div class="procedure">Procedure 10-2: Erection of Pressure Vessels <span class="page-number">660</span></div>
|
||||
<div class="procedure">Procedure 10-3: Lifting Attachments and Terminology <span class="page-number">666</span></div>
|
||||
<div class="procedure">Procedure 10-4: Lifting Loads and Forces <span class="page-number">675</span></div>
|
||||
<div class="procedure">Procedure 10-5: Design of Tail Beams, Lugs, and Base Ring Details <span class="page-number">681</span></div>
|
||||
<div class="procedure">Procedure 10-6: Design of Top Head and Cone Lifting Lugs <span class="page-number">691</span></div>
|
||||
<div class="procedure">Procedure 10-7: Design of Flange Lugs <span class="page-number">695</span></div>
|
||||
<div class="procedure">Procedure 10-8: Design of Trunnions <span class="page-number">706</span></div>
|
||||
<div class="procedure">Procedure 10-9: Local Loads in Shell Due to Erection Forces <span class="page-number">710</span></div>
|
||||
<div class="procedure">Procedure 10-10: Miscellaneous <span class="page-number">713</span></div>
|
||||
|
||||
<div class="chapter">11: Materials <span class="page-number">719</span></div>
|
||||
<div class="procedure">11.1. Types of Materials <span class="page-number">720</span></div>
|
||||
<div class="procedure">11.2. Properties of Materials <span class="page-number">723</span></div>
|
||||
<div class="procedure">11.3. Bolting <span class="page-number">728</span></div>
|
||||
<div class="procedure">11.4. Testing & Examination <span class="page-number">732</span></div>
|
||||
<div class="procedure">11.5. Heat Treatment <span class="page-number">738</span></div>
|
||||
|
||||
<div class="chapter">Appendices <span class="page-number">743</span></div>
|
||||
<div class="chapter">Index <span class="page-number">803</span></div>
|
||||
</div>
|
||||
|
||||
<h2>Preface to the 4th Edition</h2>
|
||||
<div class="preface">
|
||||
<p>When I started the Pressure Vessel Design Manual 35 years ago, I had no idea where it would lead. The first edition alone took 10 years to publish. It began when I first started working for a small vessel shop in Los Angeles in 1972. I could not believe how little information was available to engineers and designers in our industry at that time. I began collecting and researching everything I could get my hands on. As I collected more and more, I began writing procedures around various topics. After a while I had a pretty substantial collection and someone suggested that it might make a good book.</p>
|
||||
|
||||
<p>However I was constantly revising them and didn't think any of them were complete enough to publish. After a while I began trying to perfect them so that they could be published. This is the point at which the effort changed from a hobby to a vocation. My goal was to provide as complete a collection of equations, data and procedures for the design of pressure vessels that I could assemble. I never thought of myself as an author in this regard... but only the editor. I was not developing equations or methods, but only collecting and collating them. The presentation of the materials was then, and still is, the focus of my efforts. As stated all along "The author makes no claim to originality, other than that of format."</p>
|
||||
|
||||
<p>My target audience was always the person in the shop who was ultimately responsible for the designs they manufactured. I have seen all my goals for the PVDM exceeded in every way possible. Through my work with Fluor, I have had the opportunity to travel to 40 countries and have visited 60 vessel shops. In the past 10 years, I have not visited a shop that was not using the PVDM. This has been my reward. This book is now, and always has been, dedicated to the end user. Thank you.</p>
|
||||
|
||||
<p>The PVDM is a "designers" manual foremost, and not an engineering textbook. The procedures are streamlined to provide a weight, size or thickness. For the most part, wherever possible, it avoids the derivation of equations or the theoretical background. I have always sought out the simplest and most direct solutions.</p>
|
||||
|
||||
<p>If I have an interest in seeing this book continuing, then it must be done under the direction of a new, younger and very talented person.</p>
|
||||
|
||||
<p>Finally, I would like to offer my warmest, heartfelt thanks to all of you that have made comments, contributions, sent me literature, or encouraged me over the past 35 years. It is immensely rewarding to have watched the book evolve over the years. This book would not have been possible without you!</p>
|
||||
|
||||
<p style="text-align: right;">Dennis R. Moss</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 한국어 버전 -->
|
||||
<div id="korean-content" style="display: none;">
|
||||
<h1>압력용기 설계 매뉴얼</h1>
|
||||
<h2>제4판</h2>
|
||||
|
||||
<div class="publisher-info">
|
||||
<p><strong>Dennis R. Moss<br>Michael Basic</strong></p>
|
||||
<p>암스테르담 • 보스턴 • 하이델베르크 • 런던 • 뉴욕 • 옥스포드<br>
|
||||
파리 • 샌디에이고 • 샌프란시스코 • 싱가포르 • 시드니 • 도쿄</p>
|
||||
<p>Butterworth-Heinemann은 Elsevier의 임프린트입니다</p>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
<h3>저작권 정보</h3>
|
||||
<p>제4판 2013<br>
|
||||
Copyright © 2013 Elsevier Inc. 모든 권리 보유</p>
|
||||
<p>이 출판물의 어떤 부분도 출판사의 사전 서면 허가 없이 전자적, 기계적, 복사, 녹음 또는 기타 어떤 형태나 수단으로도 복제, 저장 또는 전송될 수 없습니다.</p>
|
||||
<p>ISBN: 978-0-12-387000-1</p>
|
||||
</div>
|
||||
|
||||
<h2>목차</h2>
|
||||
<div class="toc">
|
||||
<div class="chapter">제4판 서문 <span class="page-number">ix</span></div>
|
||||
|
||||
<div class="chapter">1: 일반 주제 <span class="page-number">1</span></div>
|
||||
<div class="procedure">설계 철학 <span class="page-number">1</span></div>
|
||||
<div class="procedure">응력 분석 <span class="page-number">2</span></div>
|
||||
<div class="procedure">응력/파손 이론 <span class="page-number">3</span></div>
|
||||
<div class="procedure">압력용기의 파손 <span class="page-number">7</span></div>
|
||||
<div class="procedure">하중 <span class="page-number">8</span></div>
|
||||
<div class="procedure">응력 <span class="page-number">10</span></div>
|
||||
<div class="procedure">열응력 <span class="page-number">13</span></div>
|
||||
<div class="procedure">불연속 응력 <span class="page-number">14</span></div>
|
||||
<div class="procedure">주기적 서비스를 위한 피로 분석 <span class="page-number">15</span></div>
|
||||
<div class="procedure">크리프 <span class="page-number">24</span></div>
|
||||
<div class="procedure">극저온 응용 <span class="page-number">32</span></div>
|
||||
<div class="procedure">서비스 고려사항 <span class="page-number">34</span></div>
|
||||
<div class="procedure">기타 설계 고려사항 <span class="page-number">35</span></div>
|
||||
<div class="procedure">ASME VIII-2 용기에 대한 사용자 설계 사양(UDS)에 포함될 항목 <span class="page-number">35</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">36</span></div>
|
||||
|
||||
<div class="chapter">2: 일반 설계 <span class="page-number">37</span></div>
|
||||
<div class="procedure">절차 2-1: 일반 용기 공식 <span class="page-number">38</span></div>
|
||||
<div class="procedure">절차 2-2: 외압 설계 <span class="page-number">42</span></div>
|
||||
<div class="procedure">절차 2-3: 보강링의 특성 <span class="page-number">51</span></div>
|
||||
<div class="procedure">절차 2-4: 코드 케이스 2286 <span class="page-number">54</span></div>
|
||||
<div class="procedure">절차 2-5: 원뿔 설계 <span class="page-number">58</span></div>
|
||||
<div class="procedure">절차 2-6: 토리코니컬 변환부 설계 <span class="page-number">67</span></div>
|
||||
<div class="procedure">절차 2-7: 내압으로 인한 헤드의 응력 <span class="page-number">70</span></div>
|
||||
<div class="procedure">절차 2-8: 중간 헤드 설계 <span class="page-number">74</span></div>
|
||||
<div class="procedure">절차 2-9: 평판 헤드 설계 <span class="page-number">76</span></div>
|
||||
<div class="procedure">절차 2-10: 평판 헤드의 대형 개구부 설계 <span class="page-number">81</span></div>
|
||||
<div class="procedure">절차 2-11: MAP, MAWP 및 시험 압력 계산 <span class="page-number">83</span></div>
|
||||
<div class="procedure">절차 2-12: 노즐 보강 <span class="page-number">85</span></div>
|
||||
<div class="procedure">절차 2-13: 용기의 무게중심 찾기 또는 수정 <span class="page-number">90</span></div>
|
||||
<div class="procedure">절차 2-14: 최소 설계 금속 온도 (MDMT) <span class="page-number">90</span></div>
|
||||
<div class="procedure">절차 2-15: 얇은 벽 원통형 쉘의 좌굴 <span class="page-number">95</span></div>
|
||||
<div class="procedure">절차 2-16: 최적 용기 비율 <span class="page-number">96</span></div>
|
||||
<div class="procedure">절차 2-17: 용기 및 용기 구성요소의 중량 추정 <span class="page-number">102</span></div>
|
||||
<div class="procedure">절차 2-18: 재킷 용기 설계 <span class="page-number">124</span></div>
|
||||
<div class="procedure">절차 2-19: 성형 변형률/섬유 신장 <span class="page-number">134</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">138</span></div>
|
||||
|
||||
<div class="chapter">3: 플랜지 설계 <span class="page-number">139</span></div>
|
||||
<div class="procedure">소개 <span class="page-number">140</span></div>
|
||||
<div class="procedure">절차 3-1: 플랜지 설계 <span class="page-number">148</span></div>
|
||||
<div class="procedure">절차 3-2: 구형 디시 커버 설계 <span class="page-number">165</span></div>
|
||||
<div class="procedure">절차 3-3: 개구부가 있는 블라인드 플랜지 설계 <span class="page-number">167</span></div>
|
||||
<div class="procedure">절차 3-4: 플랜지 밀봉에 필요한 볼트 토크 <span class="page-number">169</span></div>
|
||||
<div class="procedure">절차 3-5: 스터딩 아웃렛 설계 <span class="page-number">172</span></div>
|
||||
<div class="procedure">절차 3-6: 스터딩 아웃렛 보강 <span class="page-number">175</span></div>
|
||||
<div class="procedure">절차 3-7: 스터딩 플랜지 <span class="page-number">176</span></div>
|
||||
<div class="procedure">절차 3-8: 타원형 내부 맨웨이 설계 <span class="page-number">181</span></div>
|
||||
<div class="procedure">절차 3-9: 관통 노즐 <span class="page-number">182</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">183</span></div>
|
||||
|
||||
<div class="chapter">4: 용기 지지대 설계 <span class="page-number">185</span></div>
|
||||
<div class="procedure">소개: 지지 구조물 <span class="page-number">186</span></div>
|
||||
<div class="procedure">절차 4-1: ASCE에 따른 풍하중 설계 <span class="page-number">189</span></div>
|
||||
<div class="procedure">절차 4-2: 내진 설계 - 일반 <span class="page-number">199</span></div>
|
||||
<div class="procedure">절차 4-3: 용기의 내진 설계 <span class="page-number">204</span></div>
|
||||
<div class="procedure">절차 4-4: 내진 설계 - 비보강 다리 위의 용기 <span class="page-number">208</span></div>
|
||||
<div class="procedure">절차 4-5: 내진 설계 - 보강 다리 위의 용기 <span class="page-number">217</span></div>
|
||||
<div class="procedure">절차 4-6: 내진 설계 - 링 위의 용기 <span class="page-number">223</span></div>
|
||||
<div class="procedure">절차 4-7: 내진 설계 - 러그 위의 용기 <span class="page-number">229</span></div>
|
||||
<div class="procedure">절차 4-8: 내진 설계 - 스커트 위의 용기 <span class="page-number">239</span></div>
|
||||
<div class="procedure">절차 4-9: 내진 설계 - 원뿔형 스커트 위의 용기 <span class="page-number">248</span></div>
|
||||
<div class="procedure">절차 4-10: 새들 위의 수평 용기 설계 <span class="page-number">253</span></div>
|
||||
<div class="procedure">절차 4-11: 대형 용기용 새들 지지대 설계 <span class="page-number">267</span></div>
|
||||
<div class="procedure">절차 4-12: 다리용 베이스 플레이트 설계 <span class="page-number">275</span></div>
|
||||
<div class="procedure">절차 4-13: 러그 지지대 설계 <span class="page-number">278</span></div>
|
||||
<div class="procedure">절차 4-14: 수직 용기용 베이스 상세 설계 - 이동된 중립축 방법 <span class="page-number">281</span></div>
|
||||
<div class="procedure">절차 4-15: 수직 용기용 베이스 상세 설계 - 중심 중립축 방법 <span class="page-number">291</span></div>
|
||||
<div class="procedure">절차 4-16: 수직 용기용 앵커 볼트 설계 <span class="page-number">293</span></div>
|
||||
<div class="procedure">절차 4-17: 콘크리트의 특성 <span class="page-number">295</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">296</span></div>
|
||||
|
||||
<div class="chapter">5: 용기 내부 구조물 <span class="page-number">297</span></div>
|
||||
<div class="procedure">절차 5-1: 내부 지지 베드 설계 <span class="page-number">298</span></div>
|
||||
<div class="procedure">절차 5-2: 격자 빔 설계 <span class="page-number">310</span></div>
|
||||
<div class="procedure">절차 5-3: 지지 빔 위치에서의 하중으로 인한 쉘 응력 <span class="page-number">316</span></div>
|
||||
<div class="procedure">절차 5-4: 지지 블록 설계 <span class="page-number">319</span></div>
|
||||
<div class="procedure">절차 5-5: 베드 지지대용 허브 링 <span class="page-number">321</span></div>
|
||||
<div class="procedure">절차 5-6: 열전달용 파이프 코일 설계 <span class="page-number">326</span></div>
|
||||
<div class="procedure">절차 5-7: 용기 및 탱크용 교반기/믹서 <span class="page-number">345</span></div>
|
||||
<div class="procedure">절차 5-8: 내부 파이프 분배기 설계 <span class="page-number">353</span></div>
|
||||
<div class="procedure">절차 5-9: 트레이 설계 <span class="page-number">366</span></div>
|
||||
<div class="procedure">절차 5-10: 위어를 통한 유동 <span class="page-number">375</span></div>
|
||||
<div class="procedure">절차 5-11: 디미스터 설계 <span class="page-number">376</span></div>
|
||||
<div class="procedure">절차 5-12: 배플 설계 <span class="page-number">381</span></div>
|
||||
<div class="procedure">절차 5-13: 충돌 플레이트 설계 <span class="page-number">391</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">392</span></div>
|
||||
|
||||
<div class="chapter">6: 특수 설계 <span class="page-number">393</span></div>
|
||||
<div class="procedure">절차 6-1: 대직경 노즐 개구부 설계 <span class="page-number">394</span></div>
|
||||
<div class="procedure">대형 개구부—막 및 굽힘 분석 <span class="page-number">397</span></div>
|
||||
<div class="procedure">절차 6-2: 타워 처짐 <span class="page-number">397</span></div>
|
||||
<div class="procedure">절차 6-3: 링 거더 설계 <span class="page-number">401</span></div>
|
||||
<div class="procedure">절차 6-4: 내화 라이닝이 있는 용기 설계 <span class="page-number">406</span></div>
|
||||
<div class="procedure">절차 6-5: 높은 타워 및 스택의 진동 <span class="page-number">418</span></div>
|
||||
<div class="procedure">절차 6-6: 지하 탱크 및 용기 <span class="page-number">428</span></div>
|
||||
<div class="procedure">절차 6-7: 국부 얇은 영역 (LTA) <span class="page-number">432</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">433</span></div>
|
||||
|
||||
<div class="chapter">7: 국부 하중 <span class="page-number">435</span></div>
|
||||
<div class="procedure">절차 7-1: 원형 링의 응력 <span class="page-number">437</span></div>
|
||||
<div class="procedure">절차 7-2: 부분 링 보강재 설계 <span class="page-number">446</span></div>
|
||||
<div class="procedure">절차 7-3: 부착 매개변수 <span class="page-number">448</span></div>
|
||||
<div class="procedure">절차 7-4: 외부 국부 하중으로 인한 원통형 쉘의 응력 <span class="page-number">449</span></div>
|
||||
<div class="procedure">절차 7-5: 외부 국부 하중으로 인한 구형 쉘의 응력 <span class="page-number">465</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">472</span></div>
|
||||
|
||||
<div class="chapter">8: 고압 용기 <span class="page-number">473</span></div>
|
||||
<div class="procedure">1.0. 일반사항 <span class="page-number">474</span></div>
|
||||
<div class="procedure">2.0. 쉘 설계 <span class="page-number">496</span></div>
|
||||
<div class="procedure">3.0. 클로저 설계 <span class="page-number">502</span></div>
|
||||
<div class="procedure">4.0. 노즐 <span class="page-number">551</span></div>
|
||||
<div class="procedure">5.0. 참고문헌 <span class="page-number">556</span></div>
|
||||
|
||||
<div class="chapter">9: 관련 장비 <span class="page-number">557</span></div>
|
||||
<div class="procedure">절차 9-1: 데이빗 설계 <span class="page-number">558</span></div>
|
||||
<div class="procedure">절차 9-2: 원형 플랫폼 설계 <span class="page-number">563</span></div>
|
||||
<div class="procedure">절차 9-3: 정사각형 및 직사각형 플랫폼 설계 <span class="page-number">571</span></div>
|
||||
<div class="procedure">절차 9-4: 파이프 지지대 설계 <span class="page-number">576</span></div>
|
||||
<div class="procedure">절차 9-5: 볼트 연결부의 전단 하중 <span class="page-number">584</span></div>
|
||||
<div class="procedure">절차 9-6: 빈 및 고가 탱크 설계 <span class="page-number">586</span></div>
|
||||
<div class="procedure">절차 9-7: 현장 제작 구체 <span class="page-number">594</span></div>
|
||||
<div class="procedure">참고문헌 <span class="page-number">630</span></div>
|
||||
|
||||
<div class="chapter">10: 압력용기의 운송 및 설치 <span class="page-number">631</span></div>
|
||||
<div class="procedure">절차 10-1: 압력용기의 운송 <span class="page-number">632</span></div>
|
||||
<div class="procedure">절차 10-2: 압력용기의 설치 <span class="page-number">660</span></div>
|
||||
<div class="procedure">절차 10-3: 리프팅 부착물 및 용어 <span class="page-number">666</span></div>
|
||||
<div class="procedure">절차 10-4: 리프팅 하중 및 힘 <span class="page-number">675</span></div>
|
||||
<div class="procedure">절차 10-5: 테일 빔, 러그 및 베이스 링 상세 설계 <span class="page-number">681</span></div>
|
||||
<div class="procedure">절차 10-6: 상부 헤드 및 원뿔 리프팅 러그 설계 <span class="page-number">691</span></div>
|
||||
<div class="procedure">절차 10-7: 플랜지 러그 설계 <span class="page-number">695</span></div>
|
||||
<div class="procedure">절차 10-8: 트러니언 설계 <span class="page-number">706</span></div>
|
||||
<div class="procedure">절차 10-9: 설치 하중으로 인한 쉘의 국부 하중 <span class="page-number">710</span></div>
|
||||
<div class="procedure">절차 10-10: 기타 <span class="page-number">713</span></div>
|
||||
|
||||
<div class="chapter">11: 재료 <span class="page-number">719</span></div>
|
||||
<div class="procedure">11.1. 재료의 종류 <span class="page-number">720</span></div>
|
||||
<div class="procedure">11.2. 재료의 특성 <span class="page-number">723</span></div>
|
||||
<div class="procedure">11.3. 볼팅 <span class="page-number">728</span></div>
|
||||
<div class="procedure">11.4. 시험 및 검사 <span class="page-number">732</span></div>
|
||||
<div class="procedure">11.5. 열처리 <span class="page-number">738</span></div>
|
||||
|
||||
<div class="chapter">부록 <span class="page-number">743</span></div>
|
||||
<div class="chapter">색인 <span class="page-number">803</span></div>
|
||||
</div>
|
||||
|
||||
<h2>제4판 서문</h2>
|
||||
<div class="preface">
|
||||
<p>35년 전 압력용기 설계 매뉴얼을 시작했을 때, 이것이 어디로 이어질지 전혀 알지 못했습니다. 첫 번째 판만 해도 출판하는 데 10년이 걸렸습니다. 1972년 로스앤젤레스의 작은 용기 제작소에서 일하기 시작했을 때 시작되었습니다. 그 당시 우리 업계의 엔지니어와 설계자들이 이용할 수 있는 정보가 얼마나 적은지 믿을 수 없었습니다. 저는 손에 넣을 수 있는 모든 것을 수집하고 연구하기 시작했습니다. 점점 더 많이 수집하면서 다양한 주제에 대한 절차를 작성하기 시작했습니다. 얼마 후 꽤 상당한 컬렉션을 갖추게 되었고, 누군가가 이것이 좋은 책이 될 수 있을 것이라고 제안했습니다.</p>
|
||||
|
||||
<p>그러나 저는 계속해서 수정하고 있었고 그 어느 것도 출판할 만큼 완전하다고 생각하지 않았습니다. 얼마 후 출판할 수 있도록 완벽하게 만들려고 노력하기 시작했습니다. 이것이 취미에서 직업으로 바뀐 시점이었습니다. 제 목표는 제가 수집할 수 있는 압력용기 설계를 위한 방정식, 데이터 및 절차의 가능한 한 완전한 컬렉션을 제공하는 것이었습니다. 저는 이 점에서 결코 자신을 저자로 생각하지 않았습니다... 단지 편집자일 뿐입니다. 저는 방정식이나 방법을 개발하는 것이 아니라 단지 수집하고 정리하는 것이었습니다. 자료의 프레젠테이션은 그때도 지금도 제 노력의 초점입니다. 항상 말해왔듯이 "저자는 형식 이외에는 독창성을 주장하지 않습니다."</p>
|
||||
|
||||
<p>제 목표 독자는 항상 제조하는 설계에 대해 궁극적으로 책임을 지는 작업장의 사람이었습니다. 저는 PVDM에 대한 제 모든 목표가 모든 면에서 초과 달성되는 것을 보았습니다. Fluor에서의 작업을 통해 40개국을 여행하고 60개의 용기 제작소를 방문할 기회를 가졌습니다. 지난 10년 동안 PVDM을 사용하지 않는 제작소를 방문한 적이 없습니다. 이것이 제 보상이었습니다. 이 책은 지금도, 그리고 항상 최종 사용자에게 헌정되었습니다. 감사합니다.</p>
|
||||
|
||||
<p>PVDM은 무엇보다도 "설계자"를 위한 매뉴얼이며, 공학 교과서가 아닙니다. 절차는 중량, 크기 또는 두께를 제공하도록 간소화되었습니다. 대부분의 경우 가능한 한 방정식의 유도나 이론적 배경을 피합니다. 저는 항상 가장 간단하고 직접적인 해결책을 찾아왔습니다.</p>
|
||||
|
||||
<p>이 책이 계속되기를 바란다면, 새롭고 젊고 매우 재능 있는 사람의 지시에 따라 이루어져야 합니다.</p>
|
||||
|
||||
<p>마지막으로, 지난 35년 동안 의견을 주시고, 기여해 주시고, 문헌을 보내주시고, 격려해 주신 모든 분들께 따뜻한 마음을 담아 진심으로 감사드립니다. 수년에 걸쳐 책이 발전하는 것을 지켜보는 것은 엄청나게 보람 있는 일이었습니다. 여러분이 없었다면 이 책은 불가능했을 것입니다!</p>
|
||||
|
||||
<p style="text-align: right;">Dennis R. Moss</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleLanguage() {
|
||||
const englishContent = document.getElementById('english-content');
|
||||
const koreanContent = document.getElementById('korean-content');
|
||||
|
||||
if (englishContent.style.display === 'none') {
|
||||
englishContent.style.display = 'block';
|
||||
koreanContent.style.display = 'none';
|
||||
} else {
|
||||
englishContent.style.display = 'none';
|
||||
koreanContent.style.display = 'block';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
11
database/init/01_init.sql
Normal file
11
database/init/01_init.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- 데이터베이스 초기화 스크립트
|
||||
-- FastAPI가 자동으로 테이블을 생성하므로 여기서는 기본 설정만
|
||||
|
||||
-- 확장 기능 활성화
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- 전문 검색을 위한 설정
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
||||
|
||||
-- 데이터베이스 설정
|
||||
ALTER DATABASE document_db SET timezone TO 'Asia/Seoul';
|
||||
@@ -4,50 +4,50 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document Server</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📄</text></svg>">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 로그인 모달 -->
|
||||
<div x-data="authModal" x-show="showLogin" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">로그인</h2>
|
||||
<button @click="showLogin = false" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="login">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
||||
<input type="email" x-model="loginForm.email" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
|
||||
<input type="password" x-model="loginForm.password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div x-show="loginError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
<span x-text="loginError"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="loginLoading"
|
||||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
<span x-show="!loginLoading">로그인</span>
|
||||
<span x-show="loginLoading">로그인 중...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메인 앱 -->
|
||||
<div x-data="documentApp" x-init="init()">
|
||||
<!-- 로그인 모달 -->
|
||||
<div x-data="authModal" x-show="showLoginModal" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">로그인</h2>
|
||||
<button @click="showLoginModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="login">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
||||
<input type="email" x-model="loginForm.email" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
|
||||
<input type="password" x-model="loginForm.password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div x-show="loginError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
<span x-text="loginError"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="loginLoading"
|
||||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
<span x-show="!loginLoading">로그인</span>
|
||||
<span x-show="loginLoading">로그인 중...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 헤더 -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -73,7 +73,7 @@
|
||||
<!-- 사용자 메뉴 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<template x-if="!isAuthenticated">
|
||||
<button @click="$refs.authModal.showLogin = true"
|
||||
<button @click="showLoginModal = true"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
||||
로그인
|
||||
</button>
|
||||
@@ -133,7 +133,7 @@
|
||||
<i class="fas fa-file-alt text-6xl text-gray-400 mb-4"></i>
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-4">Document Server에 오신 것을 환영합니다</h2>
|
||||
<p class="text-xl text-gray-600 mb-8">HTML 문서를 관리하고 메모, 하이라이트를 추가해보세요</p>
|
||||
<button @click="$refs.authModal.showLogin = true"
|
||||
<button @click="showLoginModal = true"
|
||||
class="bg-blue-600 text-white px-8 py-3 rounded-lg text-lg hover:bg-blue-700">
|
||||
시작하기
|
||||
</button>
|
||||
@@ -214,6 +214,123 @@
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<!-- 문서 업로드 모달 -->
|
||||
<div x-show="showUploadModal" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 max-w-2xl w-full mx-4 max-h-96 overflow-y-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">문서 업로드</h2>
|
||||
<button @click="showUploadModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div x-data="uploadModal">
|
||||
<form @submit.prevent="upload">
|
||||
<!-- 파일 업로드 영역 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">HTML 문서 *</label>
|
||||
<div class="file-drop-zone"
|
||||
@dragover.prevent="$el.classList.add('dragover')"
|
||||
@dragleave.prevent="$el.classList.remove('dragover')"
|
||||
@drop.prevent="handleFileDrop($event, 'html_file')">
|
||||
<input type="file" @change="onFileSelect($event, 'html_file')"
|
||||
accept=".html,.htm" class="hidden" id="html-file">
|
||||
<label for="html-file" class="cursor-pointer">
|
||||
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-4"></i>
|
||||
<p class="text-lg text-gray-600 mb-2">HTML 파일을 드래그하거나 클릭하여 선택</p>
|
||||
<p class="text-sm text-gray-500">지원 형식: .html, .htm</p>
|
||||
</label>
|
||||
<div x-show="uploadForm.html_file" class="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-blue-800">
|
||||
<i class="fas fa-file-alt mr-2"></i>
|
||||
<span x-text="uploadForm.html_file?.name"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 파일 (선택사항) -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">PDF 원본 (선택사항)</label>
|
||||
<div class="file-drop-zone"
|
||||
@dragover.prevent="$el.classList.add('dragover')"
|
||||
@dragleave.prevent="$el.classList.remove('dragover')"
|
||||
@drop.prevent="handleFileDrop($event, 'pdf_file')">
|
||||
<input type="file" @change="onFileSelect($event, 'pdf_file')"
|
||||
accept=".pdf" class="hidden" id="pdf-file">
|
||||
<label for="pdf-file" class="cursor-pointer">
|
||||
<i class="fas fa-file-pdf text-2xl text-gray-400 mb-2"></i>
|
||||
<p class="text-gray-600">PDF 원본 파일 (선택사항)</p>
|
||||
</label>
|
||||
<div x-show="uploadForm.pdf_file" class="mt-4 p-3 bg-red-50 rounded-lg">
|
||||
<p class="text-sm text-red-800">
|
||||
<i class="fas fa-file-pdf mr-2"></i>
|
||||
<span x-text="uploadForm.pdf_file?.name"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 문서 정보 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">제목 *</label>
|
||||
<input type="text" x-model="uploadForm.title" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="문서 제목">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">문서 날짜</label>
|
||||
<input type="date" x-model="uploadForm.document_date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
||||
<textarea x-model="uploadForm.description" rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="문서에 대한 간단한 설명"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">태그</label>
|
||||
<input type="text" x-model="uploadForm.tags"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="태그를 쉼표로 구분하여 입력 (예: 중요, 회의록, 2024)">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="uploadForm.is_public" class="mr-2">
|
||||
<span class="text-sm text-gray-700">공개 문서로 설정 (다른 사용자도 볼 수 있음)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 에러 메시지 -->
|
||||
<div x-show="uploadError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
<span x-text="uploadError"></span>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showUploadModal = false; resetForm()"
|
||||
class="px-6 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300">
|
||||
취소
|
||||
</button>
|
||||
<button type="submit" :disabled="uploading || !uploadForm.html_file || !uploadForm.title"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!uploading">업로드</span>
|
||||
<span x-show="uploading">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>업로드 중...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 인증 모달 컴포넌트 -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
class API {
|
||||
constructor() {
|
||||
this.baseURL = '/api';
|
||||
this.baseURL = 'http://localhost:24102/api';
|
||||
this.token = localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
|
||||
@@ -17,26 +17,27 @@ window.authModal = () => ({
|
||||
this.loginError = '';
|
||||
|
||||
try {
|
||||
// 실제 API 호출
|
||||
const response = await api.login(this.loginForm.email, this.loginForm.password);
|
||||
|
||||
// 토큰 저장
|
||||
api.setToken(response.access_token);
|
||||
localStorage.setItem('refresh_token', response.refresh_token);
|
||||
|
||||
// 사용자 정보 로드
|
||||
const user = await api.getCurrentUser();
|
||||
// 사용자 정보 가져오기
|
||||
const userResponse = await api.getCurrentUser();
|
||||
|
||||
// 전역 상태 업데이트
|
||||
window.dispatchEvent(new CustomEvent('auth-changed', {
|
||||
detail: { isAuthenticated: true, user }
|
||||
detail: { isAuthenticated: true, user: userResponse }
|
||||
}));
|
||||
|
||||
// 모달 닫기
|
||||
this.showLogin = false;
|
||||
// 모달 닫기 (부모 컴포넌트의 상태 변경)
|
||||
window.dispatchEvent(new CustomEvent('close-login-modal'));
|
||||
this.loginForm = { email: '', password: '' };
|
||||
|
||||
} catch (error) {
|
||||
this.loginError = error.message;
|
||||
this.loginError = error.message || '로그인에 실패했습니다';
|
||||
} finally {
|
||||
this.loginLoading = false;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ window.documentApp = () => ({
|
||||
searchResults: [],
|
||||
|
||||
// 모달 상태
|
||||
showLoginModal: false,
|
||||
showUploadModal: false,
|
||||
showProfile: false,
|
||||
showMyNotes: false,
|
||||
@@ -43,6 +44,31 @@ window.documentApp = () => ({
|
||||
}
|
||||
});
|
||||
|
||||
// 로그인 모달 닫기 이벤트 리스너
|
||||
window.addEventListener('close-login-modal', () => {
|
||||
this.showLoginModal = false;
|
||||
});
|
||||
|
||||
// 문서 변경 이벤트 리스너
|
||||
window.addEventListener('documents-changed', () => {
|
||||
if (this.isAuthenticated) {
|
||||
this.loadDocuments();
|
||||
this.loadTags();
|
||||
}
|
||||
});
|
||||
|
||||
// 업로드 모달 닫기 이벤트 리스너
|
||||
window.addEventListener('close-upload-modal', () => {
|
||||
this.showUploadModal = false;
|
||||
});
|
||||
|
||||
// 알림 표시 이벤트 리스너
|
||||
window.addEventListener('show-notification', (event) => {
|
||||
if (this.showNotification) {
|
||||
this.showNotification(event.detail.message, event.detail.type);
|
||||
}
|
||||
});
|
||||
|
||||
// 초기 데이터 로드
|
||||
if (this.isAuthenticated) {
|
||||
await this.loadInitialData();
|
||||
@@ -93,14 +119,21 @@ window.documentApp = () => ({
|
||||
// 문서 목록 로드
|
||||
async loadDocuments() {
|
||||
try {
|
||||
const params = {};
|
||||
const response = await api.getDocuments();
|
||||
let allDocuments = response || [];
|
||||
|
||||
// 태그 필터링
|
||||
let filteredDocs = allDocuments;
|
||||
if (this.selectedTag) {
|
||||
params.tag = this.selectedTag;
|
||||
filteredDocs = allDocuments.filter(doc =>
|
||||
doc.tags && doc.tags.includes(this.selectedTag)
|
||||
);
|
||||
}
|
||||
|
||||
this.documents = await api.getDocuments(params);
|
||||
this.documents = filteredDocs;
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error);
|
||||
this.documents = [];
|
||||
this.showNotification('문서 목록을 불러오는데 실패했습니다', 'error');
|
||||
}
|
||||
},
|
||||
@@ -108,9 +141,11 @@ window.documentApp = () => ({
|
||||
// 태그 목록 로드
|
||||
async loadTags() {
|
||||
try {
|
||||
this.tags = await api.getTags();
|
||||
const response = await api.getTags();
|
||||
this.tags = response || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load tags:', error);
|
||||
this.tags = [];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -248,7 +283,9 @@ window.uploadModal = () => ({
|
||||
window.dispatchEvent(new CustomEvent('documents-changed'));
|
||||
|
||||
// 성공 알림
|
||||
document.querySelector('[x-data="documentApp"]').__x.$data.showNotification('문서가 성공적으로 업로드되었습니다', 'success');
|
||||
window.dispatchEvent(new CustomEvent('show-notification', {
|
||||
detail: { message: '문서가 성공적으로 업로드되었습니다', type: 'success' }
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
this.uploadError = error.message;
|
||||
@@ -276,11 +313,135 @@ window.uploadModal = () => ({
|
||||
}
|
||||
});
|
||||
|
||||
// 문서 변경 이벤트 리스너
|
||||
window.addEventListener('documents-changed', () => {
|
||||
const app = document.querySelector('[x-data="documentApp"]').__x.$data;
|
||||
if (app && app.isAuthenticated) {
|
||||
app.loadDocuments();
|
||||
app.loadTags();
|
||||
// 파일 업로드 컴포넌트
|
||||
window.uploadModal = () => ({
|
||||
uploading: false,
|
||||
uploadForm: {
|
||||
title: '',
|
||||
description: '',
|
||||
tags: '',
|
||||
is_public: false,
|
||||
document_date: '',
|
||||
html_file: null,
|
||||
pdf_file: null
|
||||
},
|
||||
uploadError: '',
|
||||
|
||||
// 파일 선택
|
||||
onFileSelect(event, fileType) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.uploadForm[fileType] = file;
|
||||
|
||||
// HTML 파일의 경우 제목 자동 설정
|
||||
if (fileType === 'html_file' && !this.uploadForm.title) {
|
||||
this.uploadForm.title = file.name.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 드래그 앤 드롭 처리
|
||||
handleFileDrop(event, fileType) {
|
||||
event.target.classList.remove('dragover');
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
// 파일 타입 검증
|
||||
if (fileType === 'html_file' && !file.name.match(/\.(html|htm)$/i)) {
|
||||
this.uploadError = 'HTML 파일만 업로드 가능합니다';
|
||||
return;
|
||||
}
|
||||
if (fileType === 'pdf_file' && !file.name.match(/\.pdf$/i)) {
|
||||
this.uploadError = 'PDF 파일만 업로드 가능합니다';
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadForm[fileType] = file;
|
||||
this.uploadError = '';
|
||||
|
||||
// HTML 파일의 경우 제목 자동 설정
|
||||
if (fileType === 'html_file' && !this.uploadForm.title) {
|
||||
this.uploadForm.title = file.name.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 업로드 실행 (목업)
|
||||
async upload() {
|
||||
if (!this.uploadForm.html_file) {
|
||||
this.uploadError = 'HTML 파일을 선택해주세요';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.uploadForm.title.trim()) {
|
||||
this.uploadError = '제목을 입력해주세요';
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
this.uploadError = '';
|
||||
|
||||
try {
|
||||
// FormData 생성
|
||||
const formData = new FormData();
|
||||
formData.append('title', this.uploadForm.title);
|
||||
formData.append('description', this.uploadForm.description || '');
|
||||
formData.append('html_file', this.uploadForm.html_file);
|
||||
|
||||
if (this.uploadForm.pdf_file) {
|
||||
formData.append('pdf_file', this.uploadForm.pdf_file);
|
||||
}
|
||||
|
||||
formData.append('language', this.uploadForm.language);
|
||||
formData.append('is_public', this.uploadForm.is_public);
|
||||
|
||||
// 태그 추가
|
||||
if (this.uploadForm.tags && this.uploadForm.tags.length > 0) {
|
||||
this.uploadForm.tags.forEach(tag => {
|
||||
formData.append('tags', tag);
|
||||
});
|
||||
}
|
||||
|
||||
// 실제 API 호출
|
||||
await api.uploadDocument(formData);
|
||||
|
||||
// 성공시 모달 닫기 및 목록 새로고침
|
||||
window.dispatchEvent(new CustomEvent('close-upload-modal'));
|
||||
this.resetForm();
|
||||
|
||||
// 문서 목록 새로고침
|
||||
window.dispatchEvent(new CustomEvent('documents-changed'));
|
||||
|
||||
// 성공 알림
|
||||
window.dispatchEvent(new CustomEvent('show-notification', {
|
||||
detail: { message: '문서가 성공적으로 업로드되었습니다', type: 'success' }
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
this.uploadError = error.message || '업로드에 실패했습니다';
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 폼 리셋
|
||||
resetForm() {
|
||||
this.uploadForm = {
|
||||
title: '',
|
||||
description: '',
|
||||
tags: '',
|
||||
is_public: false,
|
||||
document_date: '',
|
||||
html_file: null,
|
||||
pdf_file: null
|
||||
};
|
||||
this.uploadError = '';
|
||||
|
||||
// 파일 입력 필드 리셋
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||
fileInputs.forEach(input => input.value = '');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -78,21 +78,53 @@ window.documentViewer = () => ({
|
||||
this.filterNotes();
|
||||
},
|
||||
|
||||
// 문서 로드
|
||||
// 문서 로드 (목업 + 실제 HTML)
|
||||
async loadDocument() {
|
||||
this.document = await api.getDocument(this.documentId);
|
||||
// 목업 문서 정보
|
||||
const mockDocuments = {
|
||||
'test-doc-1': {
|
||||
id: 'test-doc-1',
|
||||
title: 'Document Server 테스트 문서',
|
||||
description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.',
|
||||
uploader_name: '관리자'
|
||||
},
|
||||
'test': {
|
||||
id: 'test',
|
||||
title: 'Document Server 테스트 문서',
|
||||
description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.',
|
||||
uploader_name: '관리자'
|
||||
}
|
||||
};
|
||||
|
||||
// HTML 내용 로드
|
||||
const response = await fetch(`/uploads/documents/${this.documentId}.html`);
|
||||
if (!response.ok) {
|
||||
throw new Error('문서를 불러올 수 없습니다');
|
||||
this.document = mockDocuments[this.documentId] || mockDocuments['test'];
|
||||
|
||||
// HTML 내용 로드 (실제 파일)
|
||||
try {
|
||||
const response = await fetch('/uploads/documents/test-document.html');
|
||||
if (!response.ok) {
|
||||
throw new Error('문서를 불러올 수 없습니다');
|
||||
}
|
||||
|
||||
const htmlContent = await response.text();
|
||||
document.getElementById('document-content').innerHTML = htmlContent;
|
||||
|
||||
// 페이지 제목 업데이트
|
||||
document.title = `${this.document.title} - Document Server`;
|
||||
} catch (error) {
|
||||
// 파일이 없으면 기본 내용 표시
|
||||
document.getElementById('document-content').innerHTML = `
|
||||
<h1>테스트 문서</h1>
|
||||
<p>이 문서는 Document Server의 하이라이트 및 메모 기능을 테스트하기 위한 샘플입니다.</p>
|
||||
<p>텍스트를 선택하면 하이라이트를 추가할 수 있습니다.</p>
|
||||
<h2>주요 기능</h2>
|
||||
<ul>
|
||||
<li>텍스트 선택 후 하이라이트 생성</li>
|
||||
<li>하이라이트에 메모 추가</li>
|
||||
<li>메모 검색 및 관리</li>
|
||||
<li>책갈피 기능</li>
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
const htmlContent = await response.text();
|
||||
document.getElementById('document-content').innerHTML = htmlContent;
|
||||
|
||||
// 페이지 제목 업데이트
|
||||
document.title = `${this.document.title} - Document Server`;
|
||||
},
|
||||
|
||||
// 문서 관련 데이터 로드
|
||||
|
||||
100
frontend/test-upload.html
Normal file
100
frontend/test-upload.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>테스트 문서</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 3px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
h2 {
|
||||
color: #555;
|
||||
margin-top: 30px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
text-align: justify;
|
||||
}
|
||||
.highlight {
|
||||
background-color: #fff3cd;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #ffc107;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-family: 'Courier New', monospace;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Document Server 테스트 문서</h1>
|
||||
|
||||
<p>이 문서는 Document Server의 업로드 및 뷰어 기능을 테스트하기 위한 샘플 문서입니다.
|
||||
하이라이트, 메모, 책갈피 등의 기능을 테스트할 수 있습니다.</p>
|
||||
|
||||
<h2>주요 기능</h2>
|
||||
<p>Document Server는 다음과 같은 기능을 제공합니다:</p>
|
||||
<ul>
|
||||
<li><strong>문서 업로드</strong>: HTML 및 PDF 파일 업로드</li>
|
||||
<li><strong>스마트 하이라이트</strong>: 텍스트 선택 및 하이라이트</li>
|
||||
<li><strong>연결된 메모</strong>: 하이라이트에 메모 추가</li>
|
||||
<li><strong>책갈피</strong>: 중요한 위치 저장</li>
|
||||
<li><strong>통합 검색</strong>: 문서 내용 및 메모 검색</li>
|
||||
</ul>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>중요:</strong> 이 부분은 하이라이트 테스트를 위한 중요한 내용입니다.
|
||||
사용자는 이 텍스트를 선택하여 하이라이트를 추가하고 메모를 작성할 수 있습니다.
|
||||
</div>
|
||||
|
||||
<h2>기술 스택</h2>
|
||||
<p>이 프로젝트는 다음 기술들을 사용하여 구축되었습니다:</p>
|
||||
|
||||
<div class="code">
|
||||
<strong>백엔드:</strong> FastAPI, SQLAlchemy, PostgreSQL<br>
|
||||
<strong>프론트엔드:</strong> HTML5, CSS3, JavaScript, Alpine.js<br>
|
||||
<strong>인프라:</strong> Docker, Nginx
|
||||
</div>
|
||||
|
||||
<h2>사용 방법</h2>
|
||||
<p>문서를 읽으면서 다음과 같은 작업을 수행할 수 있습니다:</p>
|
||||
|
||||
<ol>
|
||||
<li>텍스트를 선택하여 하이라이트 추가</li>
|
||||
<li>하이라이트에 메모 작성</li>
|
||||
<li>중요한 위치에 책갈피 설정</li>
|
||||
<li>검색 기능을 통해 내용 찾기</li>
|
||||
</ol>
|
||||
|
||||
<p>이 문서는 업로드 테스트가 완료되면 뷰어에서 확인할 수 있으며,
|
||||
모든 annotation 기능을 테스트해볼 수 있습니다.</p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>테스트 완료:</strong> 이 문서가 정상적으로 표시되면 업로드 기능이
|
||||
올바르게 작동하는 것입니다.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>문서 뷰어 - Document Server</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📄</text></svg>">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
73
test-document.html
Normal file
73
test-document.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>테스트 문서</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Document Server 테스트 문서</h1>
|
||||
|
||||
<h2>1. 소개</h2>
|
||||
<p>이 문서는 Document Server의 하이라이트 및 메모 기능을 테스트하기 위한 샘플 문서입니다.
|
||||
텍스트를 선택하면 하이라이트를 추가할 수 있고, 하이라이트에 메모를 연결할 수 있습니다.</p>
|
||||
|
||||
<h2>2. 주요 기능</h2>
|
||||
<ul>
|
||||
<li><strong>스마트 하이라이트</strong>: 텍스트를 선택하면 하이라이트 버튼이 나타납니다</li>
|
||||
<li><strong>연결된 메모</strong>: 하이라이트에 직접 메모를 추가할 수 있습니다</li>
|
||||
<li><strong>메모 관리</strong>: 사이드 패널에서 모든 메모를 확인하고 관리할 수 있습니다</li>
|
||||
<li><strong>책갈피</strong>: 중요한 위치에 책갈피를 추가하여 빠르게 이동할 수 있습니다</li>
|
||||
<li><strong>통합 검색</strong>: 문서 내용과 메모를 함께 검색할 수 있습니다</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 사용 방법</h2>
|
||||
<h3>3.1 하이라이트 추가</h3>
|
||||
<p>원하는 텍스트를 마우스로 드래그하여 선택하세요. 선택된 텍스트 아래에 "하이라이트" 버튼이 나타납니다.
|
||||
버튼을 클릭하면 해당 텍스트가 하이라이트됩니다.</p>
|
||||
|
||||
<h3>3.2 메모 추가</h3>
|
||||
<p>하이라이트를 생성할 때 메모를 함께 추가할 수 있습니다. 또는 기존 하이라이트를 클릭하여
|
||||
메모를 추가하거나 편집할 수 있습니다.</p>
|
||||
|
||||
<h3>3.3 책갈피 사용</h3>
|
||||
<p>상단 도구 모음의 "책갈피" 버튼을 클릭하여 책갈피 패널을 열고,
|
||||
"현재 위치에 책갈피 추가" 버튼을 클릭하여 책갈피를 생성하세요.</p>
|
||||
|
||||
<h2>4. 고급 기능</h2>
|
||||
<blockquote>
|
||||
<p>이 부분은 인용문입니다. 중요한 내용을 강조할 때 사용됩니다.
|
||||
이런 텍스트도 하이라이트하고 메모를 추가할 수 있습니다.</p>
|
||||
</blockquote>
|
||||
|
||||
<h3>4.1 검색 기능</h3>
|
||||
<p>상단의 검색창을 사용하여 문서 내용을 검색할 수 있습니다.
|
||||
검색 결과는 노란색으로 하이라이트됩니다.</p>
|
||||
|
||||
<h3>4.2 태그 시스템</h3>
|
||||
<p>메모에 태그를 추가하여 분류할 수 있습니다. 태그는 쉼표로 구분하여 입력하세요.
|
||||
예: "중요, 질문, 아이디어"</p>
|
||||
|
||||
<h2>5. 결론</h2>
|
||||
<p>Document Server는 HTML 문서를 효율적으로 관리하고 주석을 달 수 있는 강력한 도구입니다.
|
||||
하이라이트, 메모, 책갈피 기능을 활용하여 문서를 더욱 효과적으로 활용해보세요.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>6. 추가 테스트 내용</h2>
|
||||
<p>이 섹션은 추가적인 테스트를 위한 내용입니다. 다양한 길이의 텍스트를 선택하여
|
||||
하이라이트 기능이 올바르게 작동하는지 확인해보세요.</p>
|
||||
|
||||
<p><em>기울임체 텍스트</em>와 <strong>굵은 텍스트</strong>, 그리고
|
||||
<code>코드 텍스트</code>도 모두 하이라이트할 수 있습니다.</p>
|
||||
|
||||
<ol>
|
||||
<li>첫 번째 항목 - 이것도 하이라이트 가능</li>
|
||||
<li>두 번째 항목 - 메모를 추가해보세요</li>
|
||||
<li>세 번째 항목 - 책갈피를 만들어보세요</li>
|
||||
</ol>
|
||||
|
||||
<p>마지막으로, 이 문서의 다양한 부분에 하이라이트와 메모를 추가한 후
|
||||
메모 패널에서 어떻게 표시되는지 확인해보세요. 검색 기능도 테스트해보시기 바랍니다.</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user