From a42d193508adeab4a9617e4ec5287fe498b071bf Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Fri, 22 Aug 2025 06:42:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=99=84=EC=A0=84=ED=95=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백엔드 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) --- backend/src/api/dependencies.py | 6 +- backend/src/api/routes/auth.py | 16 +- backend/src/api/routes/bookmarks.py | 10 +- backend/src/api/routes/documents.py | 114 +++- backend/src/api/routes/highlights.py | 12 +- backend/src/api/routes/notes.py | 12 +- backend/src/api/routes/search.py | 12 +- backend/src/api/routes/users.py | 8 +- backend/src/core/database.py | 8 +- backend/src/core/security.py | 2 +- backend/src/main.py | 6 +- backend/src/models/__init__.py | 10 +- backend/src/models/bookmark.py | 2 +- backend/src/models/document.py | 2 +- backend/src/models/highlight.py | 2 +- backend/src/models/note.py | 2 +- backend/src/models/user.py | 2 +- backend/src/schemas/auth.py | 3 +- .../0ea7a871-9716-410f-af1f-bbc16f174558.html | 504 ++++++++++++++++++ database/init/01_init.sql | 11 + frontend/index.html | 193 +++++-- frontend/static/js/api.js | 2 +- frontend/static/js/auth.js | 13 +- frontend/static/js/main.js | 183 ++++++- frontend/static/js/viewer.js | 56 +- frontend/test-upload.html | 100 ++++ frontend/viewer.html | 1 + test-document.html | 73 +++ 28 files changed, 1213 insertions(+), 152 deletions(-) create mode 100644 backend/uploads/documents/0ea7a871-9716-410f-af1f-bbc16f174558.html create mode 100644 database/init/01_init.sql create mode 100644 frontend/test-upload.html create mode 100644 test-document.html diff --git a/backend/src/api/dependencies.py b/backend/src/api/dependencies.py index 9972ca5..19fe7a7 100644 --- a/backend/src/api/dependencies.py +++ b/backend/src/api/dependencies.py @@ -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 토큰 스키마 diff --git a/backend/src/api/routes/auth.py b/backend/src/api/routes/auth.py index 30b727e..5dffe83 100644 --- a/backend/src/api/routes/auth.py +++ b/backend/src/api/routes/auth.py @@ -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") diff --git a/backend/src/api/routes/bookmarks.py b/backend/src/api/routes/bookmarks.py index 4bfd4b9..bd6e8c8 100644 --- a/backend/src/api/routes/bookmarks.py +++ b/backend/src/api/routes/bookmarks.py @@ -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 diff --git a/backend/src/api/routes/documents.py b/backend/src/api/routes/documents.py index 299ee17..5ac2639 100644 --- a/backend/src/api/routes/documents.py +++ b/backend/src/api/routes/documents.py @@ -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 + ) diff --git a/backend/src/api/routes/highlights.py b/backend/src/api/routes/highlights.py index 03efbc2..c45b868 100644 --- a/backend/src/api/routes/highlights.py +++ b/backend/src/api/routes/highlights.py @@ -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 diff --git a/backend/src/api/routes/notes.py b/backend/src/api/routes/notes.py index 74dce17..8b7b877 100644 --- a/backend/src/api/routes/notes.py +++ b/backend/src/api/routes/notes.py @@ -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 diff --git a/backend/src/api/routes/search.py b/backend/src/api/routes/search.py index beff629..9c11df1 100644 --- a/backend/src/api/routes/search.py +++ b/backend/src/api/routes/search.py @@ -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 diff --git a/backend/src/api/routes/users.py b/backend/src/api/routes/users.py index 2afbdf2..b43868b 100644 --- a/backend/src/api/routes/users.py +++ b/backend/src/api/routes/users.py @@ -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 diff --git a/backend/src/core/database.py b/backend/src/core/database.py index 1dfb7fc..7f19bdf 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -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: diff --git a/backend/src/core/security.py b/backend/src/core/security.py index 8de4260..4677dff 100644 --- a/backend/src/core/security.py +++ b/backend/src/core/security.py @@ -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 # 비밀번호 해싱 컨텍스트 diff --git a/backend/src/main.py b/backend/src/main.py index b616309..e62252e 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -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 diff --git a/backend/src/models/__init__.py b/backend/src/models/__init__.py index dba4270..a7ca151 100644 --- a/backend/src/models/__init__.py +++ b/backend/src/models/__init__.py @@ -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", diff --git a/backend/src/models/bookmark.py b/backend/src/models/bookmark.py index 0b42fe0..329f95c 100644 --- a/backend/src/models/bookmark.py +++ b/backend/src/models/bookmark.py @@ -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): diff --git a/backend/src/models/document.py b/backend/src/models/document.py index c9f423c..68252f3 100644 --- a/backend/src/models/document.py +++ b/backend/src/models/document.py @@ -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 # 문서-태그 다대다 관계 테이블 diff --git a/backend/src/models/highlight.py b/backend/src/models/highlight.py index 3e73da5..5b49149 100644 --- a/backend/src/models/highlight.py +++ b/backend/src/models/highlight.py @@ -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): diff --git a/backend/src/models/note.py b/backend/src/models/note.py index fba4067..508726f 100644 --- a/backend/src/models/note.py +++ b/backend/src/models/note.py @@ -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): diff --git a/backend/src/models/user.py b/backend/src/models/user.py index de2bbe0..bfc408c 100644 --- a/backend/src/models/user.py +++ b/backend/src/models/user.py @@ -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): diff --git a/backend/src/schemas/auth.py b/backend/src/schemas/auth.py index 5488feb..88fceb4 100644 --- a/backend/src/schemas/auth.py +++ b/backend/src/schemas/auth.py @@ -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 diff --git a/backend/uploads/documents/0ea7a871-9716-410f-af1f-bbc16f174558.html b/backend/uploads/documents/0ea7a871-9716-410f-af1f-bbc16f174558.html new file mode 100644 index 0000000..8538ae3 --- /dev/null +++ b/backend/uploads/documents/0ea7a871-9716-410f-af1f-bbc16f174558.html @@ -0,0 +1,504 @@ + + + + + + 압력용기 설계 매뉴얼 - Pressure Vessel Design Manual + + + + + +
+ +
+

Pressure Vessel Design Manual

+

Fourth Edition

+ +
+

Dennis R. Moss
Michael Basic

+

AMSTERDAM • BOSTON • HEIDELBERG • LONDON • NEW YORK • OXFORD
+ PARIS • SAN DIEGO • SAN FRANCISCO • SINGAPORE • SYDNEY • TOKYO

+

Butterworth-Heinemann is an imprint of Elsevier

+
+ + + +

Contents

+
+
Preface to the 4th Edition ix
+ +
1: General Topics 1
+
Design Philosophy 1
+
Stress Analysis 2
+
Stress/Failure Theories 3
+
Failures in Pressure Vessels 7
+
Loadings 8
+
Stress 10
+
Thermal Stresses 13
+
Discontinuity Stresses 14
+
Fatigue Analysis for Cyclic Service 15
+
Creep 24
+
Cryogenic Applications 32
+
Service Considerations 34
+
Miscellaneous Design Considerations 35
+
Items to be Included in a User's Design Specification (UDS) for ASME VIII-2 Vessels 35
+
References 36
+ +
2: General Design 37
+
Procedure 2-1: General Vessel Formulas 38
+
Procedure 2-2: External Pressure Design 42
+
Procedure 2-3: Properties of Stiffening Rings 51
+
Procedure 2-4: Code Case 2286 54
+
Procedure 2-5: Design of Cones 58
+
Procedure 2-6: Design of Toriconical Transitions 67
+
Procedure 2-7: Stresses in Heads Due to Internal Pressure 70
+
Procedure 2-8: Design of Intermediate Heads 74
+
Procedure 2-9: Design of Flat Heads 76
+
Procedure 2-10: Design of Large Openings in Flat Heads 81
+
Procedure 2-11: Calculate MAP, MAWP, and Test Pressures 83
+
Procedure 2-12: Nozzle Reinforcement 85
+
Procedure 2-13: Find or Revise the Center of Gravity of a Vessel 90
+
Procedure 2-14: Minimum Design Metal Temperature (MDMT) 90
+
Procedure 2-15: Buckling of Thin Wall Cylindrical Shells 95
+
Procedure 2-16: Optimum Vessel Proportions 96
+
Procedure 2-17: Estimating Weights of Vessels and Vessel Components 102
+
Procedure 2-18: Design of Jacketed Vessels 124
+
Procedure 2-19: Forming Strains/Fiber Elongation 134
+
References 138
+ +
3: Flange Design 139
+
Introduction 140
+
Procedure 3-1: Design of Flanges 148
+
Procedure 3-2: Design of Spherically Dished Covers 165
+
Procedure 3-3: Design of Blind Flanges with Openings 167
+
Procedure 3-4: Bolt Torque Required for Sealing Flanges 169
+
Procedure 3-5: Design of Studding Outlets 172
+
Procedure 3-6: Reinforcement for Studding Outlets 175
+
Procedure 3-7: Studding Flanges 176
+
Procedure 3-8: Design of Elliptical, Internal Manways 181
+
Procedure 3-9: Through Nozzles 182
+
References 183
+ +
4: Design of Vessel Supports 185
+
Introduction: Support Structures 186
+
Procedure 4-1: Wind Design Per ASCE 189
+
Procedure 4-2: Seismic Design - General 199
+
Procedure 4-3: Seismic Design for Vessels 204
+
Procedure 4-4: Seismic Design - Vessel on Unbraced Legs 208
+
Procedure 4-5: Seismic Design - Vessel on Braced Legs 217
+
Procedure 4-6: Seismic Design - Vessel on Rings 223
+
Procedure 4-7: Seismic Design - Vessel on Lugs 229
+
Procedure 4-8: Seismic Design - Vessel on Skirt 239
+
Procedure 4-9: Seismic Design - Vessel on Conical Skirt 248
+
Procedure 4-10: Design of Horizontal Vessel on Saddles 253
+
Procedure 4-11: Design of Saddle Supports for Large Vessels 267
+
Procedure 4-12: Design of Base Plates for Legs 275
+
Procedure 4-13: Design of Lug Supports 278
+
Procedure 4-14: Design of Base Details for Vertical Vessels-Shifted Neutral Axis Method 281
+
Procedure 4-15: Design of Base Details for Vertical Vessels - Centered Neutral Axis Method 291
+
Procedure 4-16: Design of Anchor Bolts for Vertical Vessels 293
+
Procedure 4-17: Properties of Concrete 295
+
References 296
+ +
5: Vessel Internals 297
+
Procedure 5-1: Design of Internal Support Beds 298
+
Procedure 5-2: Design of Lattice Beams 310
+
Procedure 5-3: Shell Stresses due to Loadings at Support Beam Locations 316
+
Procedure 5-4: Design of Support Blocks 319
+
Procedure 5-5: Hub Rings used for Bed Supports 321
+
Procedure 5-6: Design of Pipe Coils for Heat Transfer 326
+
Procedure 5-7: Agitators/Mixers for Vessels and Tanks 345
+
Procedure 5-8: Design of Internal Pipe Distributors 353
+
Procedure 5-9: Design of Trays 366
+
Procedure 5-10: Flow Over Weirs 375
+
Procedure 5-11: Design of Demisters 376
+
Procedure 5-12: Design of Baffles 381
+
Procedure 5-13: Design of Impingement Plates 391
+
References 392
+ +
6: Special Designs 393
+
Procedure 6-1: Design of Large-Diameter Nozzle Openings 394
+
Large Openings—Membrane and Bending Analysis 397
+
Procedure 6-2: Tower Deflection 397
+
Procedure 6-3: Design of Ring Girders 401
+
Procedure 6-4: Design of Vessels with Refractory Linings 406
+
Procedure 6-5: Vibration of Tall Towers and Stacks 418
+
Procedure 6-6: Underground Tanks & Vessels 428
+
Procedure 6-7: Local Thin Area (LTA) 432
+
References 433
+ +
7: Local Loads 435
+
Procedure 7-1: Stresses in Circular Rings 437
+
Procedure 7-2: Design of Partial Ring Stiffeners 446
+
Procedure 7-3: Attachment Parameters 448
+
Procedure 7-4: Stresses in Cylindrical Shells from External Local Loads 449
+
Procedure 7-5: Stresses in Spherical Shells from External Local Loads 465
+
References 472
+ +
8: High Pressure Vessels 473
+
1.0. General 474
+
2.0. Shell Design 496
+
3.0. Design of Closures 502
+
4.0. Nozzles 551
+
5.0. References 556
+ +
9: Related Equipment 557
+
Procedure 9-1: Design of Davits 558
+
Procedure 9-2: Design of Circular Platforms 563
+
Procedure 9-3: Design of Square and Rectangular Platforms 571
+
Procedure 9-4: Design of Pipe Supports 576
+
Procedure 9-5: Shear Loads in Bolted Connections 584
+
Procedure 9-6: Design of Bins and Elevated Tanks 586
+
Procedure 9-7: Field-Fabricated Spheres 594
+
References 630
+ +
10: Transportation and Erection of Pressure Vessels 631
+
Procedure 10-1: Transportation of Pressure Vessels 632
+
Procedure 10-2: Erection of Pressure Vessels 660
+
Procedure 10-3: Lifting Attachments and Terminology 666
+
Procedure 10-4: Lifting Loads and Forces 675
+
Procedure 10-5: Design of Tail Beams, Lugs, and Base Ring Details 681
+
Procedure 10-6: Design of Top Head and Cone Lifting Lugs 691
+
Procedure 10-7: Design of Flange Lugs 695
+
Procedure 10-8: Design of Trunnions 706
+
Procedure 10-9: Local Loads in Shell Due to Erection Forces 710
+
Procedure 10-10: Miscellaneous 713
+ +
11: Materials 719
+
11.1. Types of Materials 720
+
11.2. Properties of Materials 723
+
11.3. Bolting 728
+
11.4. Testing & Examination 732
+
11.5. Heat Treatment 738
+ +
Appendices 743
+
Index 803
+
+ +

Preface to the 4th Edition

+
+

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.

+ +

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."

+ +

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.

+ +

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.

+ +

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.

+ +

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!

+ +

Dennis R. Moss

+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/database/init/01_init.sql b/database/init/01_init.sql new file mode 100644 index 0000000..5831ae3 --- /dev/null +++ b/database/init/01_init.sql @@ -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'; diff --git a/frontend/index.html b/frontend/index.html index 36f99a1..bbb4baa 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,50 +4,50 @@ Document Server + - -
-
-
-

로그인

- -
- -
-
- - -
- -
- - -
- -
- -
- - -
-
-
-
+ +
+
+
+

로그인

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
@@ -73,7 +73,7 @@
+ + +
+
+
+

문서 업로드

+ +
+ +
+
+ +
+ +
+ + +
+

+ + +

+
+
+
+ + +
+ +
+ + +
+

+ + +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+
diff --git a/frontend/static/js/api.js b/frontend/static/js/api.js index 0158b87..522c85a 100644 --- a/frontend/static/js/api.js +++ b/frontend/static/js/api.js @@ -3,7 +3,7 @@ */ class API { constructor() { - this.baseURL = '/api'; + this.baseURL = 'http://localhost:24102/api'; this.token = localStorage.getItem('access_token'); } diff --git a/frontend/static/js/auth.js b/frontend/static/js/auth.js index b96ae57..ed2df17 100644 --- a/frontend/static/js/auth.js +++ b/frontend/static/js/auth.js @@ -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; } diff --git a/frontend/static/js/main.js b/frontend/static/js/main.js index d102535..778054b 100644 --- a/frontend/static/js/main.js +++ b/frontend/static/js/main.js @@ -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 = ''); } }); + + diff --git a/frontend/static/js/viewer.js b/frontend/static/js/viewer.js index 99b2609..6e048a1 100644 --- a/frontend/static/js/viewer.js +++ b/frontend/static/js/viewer.js @@ -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 = ` +

테스트 문서

+

이 문서는 Document Server의 하이라이트 및 메모 기능을 테스트하기 위한 샘플입니다.

+

텍스트를 선택하면 하이라이트를 추가할 수 있습니다.

+

주요 기능

+
    +
  • 텍스트 선택 후 하이라이트 생성
  • +
  • 하이라이트에 메모 추가
  • +
  • 메모 검색 및 관리
  • +
  • 책갈피 기능
  • +
+ `; } - - const htmlContent = await response.text(); - document.getElementById('document-content').innerHTML = htmlContent; - - // 페이지 제목 업데이트 - document.title = `${this.document.title} - Document Server`; }, // 문서 관련 데이터 로드 diff --git a/frontend/test-upload.html b/frontend/test-upload.html new file mode 100644 index 0000000..2c62e18 --- /dev/null +++ b/frontend/test-upload.html @@ -0,0 +1,100 @@ + + + + + + 테스트 문서 + + + +
+

Document Server 테스트 문서

+ +

이 문서는 Document Server의 업로드 및 뷰어 기능을 테스트하기 위한 샘플 문서입니다. + 하이라이트, 메모, 책갈피 등의 기능을 테스트할 수 있습니다.

+ +

주요 기능

+

Document Server는 다음과 같은 기능을 제공합니다:

+
    +
  • 문서 업로드: HTML 및 PDF 파일 업로드
  • +
  • 스마트 하이라이트: 텍스트 선택 및 하이라이트
  • +
  • 연결된 메모: 하이라이트에 메모 추가
  • +
  • 책갈피: 중요한 위치 저장
  • +
  • 통합 검색: 문서 내용 및 메모 검색
  • +
+ +
+ 중요: 이 부분은 하이라이트 테스트를 위한 중요한 내용입니다. + 사용자는 이 텍스트를 선택하여 하이라이트를 추가하고 메모를 작성할 수 있습니다. +
+ +

기술 스택

+

이 프로젝트는 다음 기술들을 사용하여 구축되었습니다:

+ +
+ 백엔드: FastAPI, SQLAlchemy, PostgreSQL
+ 프론트엔드: HTML5, CSS3, JavaScript, Alpine.js
+ 인프라: Docker, Nginx +
+ +

사용 방법

+

문서를 읽으면서 다음과 같은 작업을 수행할 수 있습니다:

+ +
    +
  1. 텍스트를 선택하여 하이라이트 추가
  2. +
  3. 하이라이트에 메모 작성
  4. +
  5. 중요한 위치에 책갈피 설정
  6. +
  7. 검색 기능을 통해 내용 찾기
  8. +
+ +

이 문서는 업로드 테스트가 완료되면 뷰어에서 확인할 수 있으며, + 모든 annotation 기능을 테스트해볼 수 있습니다.

+ +
+ 테스트 완료: 이 문서가 정상적으로 표시되면 업로드 기능이 + 올바르게 작동하는 것입니다. +
+
+ + diff --git a/frontend/viewer.html b/frontend/viewer.html index 53de403..6431984 100644 --- a/frontend/viewer.html +++ b/frontend/viewer.html @@ -4,6 +4,7 @@ 문서 뷰어 - Document Server + diff --git a/test-document.html b/test-document.html new file mode 100644 index 0000000..83af052 --- /dev/null +++ b/test-document.html @@ -0,0 +1,73 @@ + + + + + + 테스트 문서 + + +

Document Server 테스트 문서

+ +

1. 소개

+

이 문서는 Document Server의 하이라이트 및 메모 기능을 테스트하기 위한 샘플 문서입니다. + 텍스트를 선택하면 하이라이트를 추가할 수 있고, 하이라이트에 메모를 연결할 수 있습니다.

+ +

2. 주요 기능

+
    +
  • 스마트 하이라이트: 텍스트를 선택하면 하이라이트 버튼이 나타납니다
  • +
  • 연결된 메모: 하이라이트에 직접 메모를 추가할 수 있습니다
  • +
  • 메모 관리: 사이드 패널에서 모든 메모를 확인하고 관리할 수 있습니다
  • +
  • 책갈피: 중요한 위치에 책갈피를 추가하여 빠르게 이동할 수 있습니다
  • +
  • 통합 검색: 문서 내용과 메모를 함께 검색할 수 있습니다
  • +
+ +

3. 사용 방법

+

3.1 하이라이트 추가

+

원하는 텍스트를 마우스로 드래그하여 선택하세요. 선택된 텍스트 아래에 "하이라이트" 버튼이 나타납니다. + 버튼을 클릭하면 해당 텍스트가 하이라이트됩니다.

+ +

3.2 메모 추가

+

하이라이트를 생성할 때 메모를 함께 추가할 수 있습니다. 또는 기존 하이라이트를 클릭하여 + 메모를 추가하거나 편집할 수 있습니다.

+ +

3.3 책갈피 사용

+

상단 도구 모음의 "책갈피" 버튼을 클릭하여 책갈피 패널을 열고, + "현재 위치에 책갈피 추가" 버튼을 클릭하여 책갈피를 생성하세요.

+ +

4. 고급 기능

+
+

이 부분은 인용문입니다. 중요한 내용을 강조할 때 사용됩니다. + 이런 텍스트도 하이라이트하고 메모를 추가할 수 있습니다.

+
+ +

4.1 검색 기능

+

상단의 검색창을 사용하여 문서 내용을 검색할 수 있습니다. + 검색 결과는 노란색으로 하이라이트됩니다.

+ +

4.2 태그 시스템

+

메모에 태그를 추가하여 분류할 수 있습니다. 태그는 쉼표로 구분하여 입력하세요. + 예: "중요, 질문, 아이디어"

+ +

5. 결론

+

Document Server는 HTML 문서를 효율적으로 관리하고 주석을 달 수 있는 강력한 도구입니다. + 하이라이트, 메모, 책갈피 기능을 활용하여 문서를 더욱 효과적으로 활용해보세요.

+ +
+ +

6. 추가 테스트 내용

+

이 섹션은 추가적인 테스트를 위한 내용입니다. 다양한 길이의 텍스트를 선택하여 + 하이라이트 기능이 올바르게 작동하는지 확인해보세요.

+ +

기울임체 텍스트굵은 텍스트, 그리고 + 코드 텍스트도 모두 하이라이트할 수 있습니다.

+ +
    +
  1. 첫 번째 항목 - 이것도 하이라이트 가능
  2. +
  3. 두 번째 항목 - 메모를 추가해보세요
  4. +
  5. 세 번째 항목 - 책갈피를 만들어보세요
  6. +
+ +

마지막으로, 이 문서의 다양한 부분에 하이라이트와 메모를 추가한 후 + 메모 패널에서 어떻게 표시되는지 확인해보세요. 검색 기능도 테스트해보시기 바랍니다.

+ +