From ad91226e3f278bf34597cd8bae88fd6170064eb5 Mon Sep 17 00:00:00 2001 From: Godopu Date: Thu, 19 Feb 2026 11:00:53 +0900 Subject: [PATCH] init --- .claude/settings.local.json | 8 + .gitignore | 2 + PROMPT-OLD.md | 25 +++ PROMPT.md | 63 ++++++++ README.md | 15 ++ REPORT.md | 99 ++++++++++++ REQUIREMENTS.md | 69 +++++++++ backup.md | 15 ++ main.py | 187 ++++++++++++++++++++++ metadata.json | 26 ++++ static/index.html | 301 ++++++++++++++++++++++++++++++++++++ 11 files changed, 810 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 PROMPT-OLD.md create mode 100644 PROMPT.md create mode 100644 README.md create mode 100644 REPORT.md create mode 100644 REQUIREMENTS.md create mode 100644 backup.md create mode 100644 main.py create mode 100644 metadata.json create mode 100644 static/index.html diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..66708a9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(python3 -m venv venv)", + "Bash(./venv/bin/pip install:*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd5c9e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +assets/images/* +assets/videos/* \ No newline at end of file diff --git a/PROMPT-OLD.md b/PROMPT-OLD.md new file mode 100644 index 0000000..2e0f5e5 --- /dev/null +++ b/PROMPT-OLD.md @@ -0,0 +1,25 @@ +# 프로젝트 구현 기록 (Prompts) - 원본 + +본 파일은 프로젝트 진행 과정에서 전달된 사용자의 주요 요구 사항들을 시간 순서대로 정리한 기록입니다. + +### 1. 프로젝트 소개 파일 수정 +- **요구 사항**: `README.md` 파일에는 구현할 프로그램의 설명과 요구사항이 작성되어 있어. 견고한 프로그램 구현을 위해 해당 파일의 내용을 수정해서 `REQUIREMENTS.md` 파일에 저장해줘. + +### 2. 프로젝트 구현 +- **요구 사항**: 작성된 `REQUIREMENTS.md` 파일의 내용을 참고해서 프로그램 초안을 구현해줘. + +### 3. 초기 레포트 생성 및 분석 +- **요구 사항**: 구현한 프로그램에 대한 설명, 실행법, curl을 이용한 테스트 방법 등의 내용을 `REPORT.md` 파일에 작성해줘. + +### 4. YouTube 다운로드 기능 추가 +- **요구 사항**: youtube 링크를 전달하면 해당 링크에서 파일을 다운로드 할 수 있도록 youtube 링크 전달 및 다운로드를 위한 API를 만들어줘. 그리고 이를 이용할 수 있도록 web interface를 수정, `REPORT.md` 파일 또한 함께 수정해줘. + +### 5. 실시간 스트리밍 지원 +- **요구 사항**: 다운로드 받은 영상의 경우 실시간 스트리밍이 가능하도록 코드를 변경해줘. + +### 6. 웹 UI 모던화 및 스타일 개선 +- **요구 사항**: web ui가 너무 심플하고, 사용하기 불편해. 모던하고, 파스텔 컬러 스타일의 사용하기 편한 UI로 웹을 전반적으로 수정해줘. + +### 7. 파일 삭제 기능 구현 +- **요구 사항**: 업로드한 파일을 삭제할 수 있는 기능을 작성해줘. + diff --git a/PROMPT.md b/PROMPT.md new file mode 100644 index 0000000..1da3d8d --- /dev/null +++ b/PROMPT.md @@ -0,0 +1,63 @@ +# 효율적인 프롬프트 작성 가이드 및 프로젝트 기록 + +이 문서는 프로젝트 개발 과정에서의 요구사항을 **Prompt Engineering** 관점에서 재구성한 가이드입니다. 각 기능 구현 시 모델에게 전달하면 가장 효과적인 구조를 채택했습니다. + +--- + +### 💡 시스템 기본 페르소나 (System Role) +> "너는 FastAPI와 현대적인 Frontend 기술에 능숙한 시니어 풀스택 개발자야. 보안(경로 탐색 방지), 코드 효율성, 그리고 사용자 친화적인 UI/UX를 최우선으로 고려해서 코드를 작성해줘." + +--- + +### 1. 요구사항 분석 및 구체화 (Requirement Engineering) +**[Prompt]** +- **Context**: `README.md` 파일에 프로그램 설명과 기초 요구사항이 작성되어 있어. +- **Task**: 견고하고 전문적인 프로그램 구현을 위해 이 내용을 분석하고, 상세한 소프트웨어 요구사항 명세서(SRS)를 작성해서 `REQUIREMENTS.md` 파일에 저장해줘. +- **Output**: 기능적/비기능적 요구사항, 기술 스택, 디렉토리 구조가 포함된 명세서. + +### 2. 초기 프로토타입 구현 (Prototype Implementation) +**[Prompt]** +- **Context**: 작성된 `REQUIREMENTS.md` 명세서를 기반으로 프로젝트를 시작하려 해. +- **Task**: 명세서의 모든 요구사항을 충족하는 FastAPI 애플리케이션의 초안 코드를 작성해줘. +- **Constraints**: + - 파일 업로드/목록 조회/다운로드 API 포함. + - 파일 타입별(이미지/비디오) 자동 분류 저장 로직 구현. + - 메타데이터는 `metadata.json`으로 관리할 것. + +### 3. 프로젝트 분석 및 문서화 (Documentation) +**[Prompt]** +- **Task**: 현재 구현된 코드베이스를 분석하고 사용자용 `REPORT.md`를 작성해줘. +- **Instructions**: + 1. 핵심 기능 요약 및 실행 방법 안내. + 2. `curl`을 이용한 API 엔드포인트별 테스트 명령어 예시 포함. + +### 4. YouTube 연동 기능 (External Integration) +**[Prompt]** +- **Task**: YouTube URL을 통해 영상을 서버로 직접 다운로드하는 기능을 추가해줘. +- **Requirements**: + - `yt-dlp` 라이브러리를 사용하고, 결과물을 `assets/videos`에 저장. + - Web UI에 URL 입력 폼과 처리 상태 표시 기능을 통합할 것. + +### 5. 스트리밍 엔진 최적화 (Streaming & Range Requests) +**[Prompt]** +- **Task**: 대용량 영상의 실시간 스트리밍(Seeking)이 가능하도록 개선해줘. +- **Technical Detail**: FastAPI의 `FileResponse`가 HTTP Range Requests를 지원하도록 설정하고, 브라우저 내 재생을 위해 `inline` 콘텐츠 배치를 적용할 것. + +### 6. UI/UX 현대화 (Frontend Redesign) +**[Prompt]** +- **Role**: 전문 UI/UX 디자이너 및 프론트엔드 개발자. +- **Task**: 기존의 기본적인 UI를 현대적인 파스텔 톤 스타일로 전면 개편해줘. +- **Style Guide**: 카드형 레이아웃, Google Fonts (Poppins), 반응형 디자인, 작업 상태 애니메이션(Loader) 적용. + +### 7. 리소스 관리 및 삭제 (Resource Management) +**[Prompt]** +- **Task**: 서버 리소스 관리를 위해 파일 삭제 기능을 추가해줘. +- **Constraints**: 실제 파일 삭제와 메타데이터 제거를 동시 처리하고, UI에서는 삭제 전 사용자 확인 절차를 둘 것. + +--- + +### 🚀 효율적인 프롬프트 전달 팁 +1. **역할 정의 (Role)**: 전문가 페르소나를 지정하세요. +2. **구체적인 제약 조건 (Constraints)**: 기술적 한계나 필수 라이브러리를 명시하세요. +3. **단계별 지시 (Step-by-Step)**: 복잡한 작업은 논리적 순서로 나누어 요청하세요. +4. **출력 형식 지정 (Output Format)**: 결과물의 형태(Markdown, JSON, 특정 코드 스타일 등)를 정의하세요. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9757e9f --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# 프로젝트 개요 +- fastapi를 이용한 파일 업로드/다운로드 프로그램 작성 + +# 프로젝트 요구사항 + +- fastapi를 통해 파일 업로드 및 다운로드를 위한 api 구현 +- 업로드한 파일은 파일의 타입에 따라 assets/videos 또는 assets/images에 저장 +- 저장된 파일의 정보 (파일 이름, 타입, 업로드 날짜)는 별도 json 파일을 이용하여 관리 +- API 인터페이스 정의 + - 파일 업로드: /api/files/upload + - 파일 다운로드: /api/files/download + - static (웹 (html, css, javascript) 서빙) : /web + + + diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..1b05238 --- /dev/null +++ b/REPORT.md @@ -0,0 +1,99 @@ +# 프로젝트 결과 보고서: FastAPI 파일 관리 시스템 + +## 1. 프로그램 개요 +본 프로그램은 **FastAPI**를 기반으로 한 웹 기반 파일 관리 시스템입니다. 사용자는 이미와 동영상 파일을 업로드하고, 서버에서 해당 파일을 다운로드하거나 목록을 확인할 수 있습니다. + +### 주요 기능 +- **파일 업로드 (`POST /api/files/upload`)**: 파일을 업로드하면 MIME 타입에 따라 `assets/images` 또는 `assets/videos` 폴더로 자동 분류하여 저장합니다. +- **파일 목록 조회 (`GET /api/files`)**: 서버에 저장된 모든 파일의 메타데이터(파일명, 타입, 업로드 날짜, 경로 등)를 JSON 형식으로 반환합니다. +- **파일 다운로드 (`GET /api/files/download`)**: 파일명을 파라미터로 전달하여 서버에 저장된 파일을 로컬로 다운로드할 수 있습니다. +- **파일 삭제 (`DELETE /api/files`)**: 서버에서 특정 파일을 삭제하고 메타데이터에서도 제거합니다. +- **메타데이터 관리**: 업로드된 파일의 정보는 `metadata.json` 파일에 기록되어 영구적으로 관리됩니다. +- **YouTube 다운로드 (`POST /api/files/youtube`)**: YouTube 링크를 전달받아 해당 영상을 서버로 다운로드하고 목록에 추가합니다. +- **실시간 스트리밍 지원**: `FileResponse`의 Range Requests 지원을 통해 대용량 영상 파일도 브라우저에서 원하는 시점으로 이동하며 끊김 없이 감상할 수 있습니다. +- **모던 파스텔 UI**: 사용자 친화적인 파스텔 톤의 카드 레이아웃과 반응형 디자인을 적용하여 모바일과 데스크톱 모두에서 쾌적하게 사용할 수 있습니다. +- **웹 인터페이스 서빙 (`/web`)**: `static/` 디렉토리의 HTML/JS 파일을 통해 사용자에게 웹 화면을 제공합니다. + +--- + +## 2. 실행 방법 + +### 환경 준비 +Python 3.9 버전 이상이 필요합니다. + +1. **가상환경 생성 및 활성화** + ```bash + python -m venv .venv + source .venv/bin/activate # Linux/macOS + # 또는 + .venv\Scripts\activate # Windows + ``` + +2. **필수 패키지 설치** + ```bash + pip install fastapi uvicorn python-multipart yt-dlp + ``` + +### 서버 실행 +```bash +python main.py +``` +서버는 기본적으로 `http://0.0.0.0:8000`에서 실행됩니다. + +--- + +## 3. curl을 이용한 테스트 방법 + +### A. 파일 업로드 테스트 +`assets/images/dummy.png` 파일을 업로드하는 예시입니다. + +```bash +curl -X POST "http://localhost:8000/api/files/upload" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "file=@assets/images/dummy.png" \ + -F "description=테스트 이미지입니다" \ + -F "uploader=홍길동" +``` + +### B. 파일 목록 조회 테스트 +서버에 저장된 모든 파일의 정보를 확인합니다. + +```bash +curl -X GET "http://localhost:8000/api/files" +``` + +### C. 파일 다운로드 테스트 +`dummy.png`라는 이름의 파일을 다운로드하여 `downloaded_file.png`로 저장하는 예시입니다. + +```bash +curl -X GET "http://localhost:8000/api/files/download?filename=dummy.png" \ + --output downloaded_file.png +``` + +### D. 파일 삭제 테스트 +서버의 파일을 삭제하는 예시입니다. + +```bash +curl -X DELETE "http://localhost:8000/api/files?filename=dummy.png" +``` + +### E. YouTube 다운로드 테스트 +YouTube 링크를 통해 영상을 다운로드하는 예시입니다. + +```bash +curl -X POST "http://localhost:8000/api/files/youtube" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "url=https://www.youtube.com/watch?v=EXAMPLE_ID" \ + -F "uploader=테스터" +``` + +--- + +## 4. 디렉토리 구조 +- `main.py`: FastAPI 애플리케이션 엔트리 포인트 +- `metadata.json`: 파일 메타데이터 저장소 +- `assets/`: 업로드된 실제 파일 저장 (images/videos 분류) +- `static/`: 웹 프론트엔드 정적 파일 +- `REQUIREMENTS.md`: 프로젝트 요구사항 명세서 diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..b33ebce --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,69 @@ +# Detailed Software Requirements Specification + +## 1. Project Overview +A web-based file management system using FastAPI that allows users to upload and download files. The system categorizes files by type and maintains metadata in a JSON file. + +## 2. Functional Requirements + +### 2.1 File Upload +- **Endpoint**: `POST /api/files/upload` +- **Description**: Accepts a file and its associated metadata. +- **Payload**: Multipart form-data containing: + - `file`: The actual file to upload. + - `description`: (Optional) A brief description of the file. + - `uploader`: (Optional) Name of the person uploading. +- **Storage Logic**: + - Images (e.g., JPG, PNG, GIF) must be saved in `assets/images/`. + - Videos (e.g., MP4, AVI, MKV) must be saved in `assets/videos/`. + - *Optional: Handle other file types or return an error if unsupported.* +- **Metadata Management**: + - Upon successful upload, record the following in `metadata.json`: + - `filename`: Original or sanitized filename. + - `type`: MIME type or category (image/video). + - `upload_date`: ISO 8601 formatted timestamp. + - `file_path`: Relative path to the stored file. + - `description`: Provided description. + - `uploader`: Provided uploader name. + +### 2.2 File List Retrieval +- **Endpoint**: `GET /api/files` +- **Description**: Returns a list of all uploaded files and their metadata. +- **Response**: JSON array containing metadata for each file stored in `metadata.json`. + +### 2.3 File Download +- **Endpoint**: `GET /api/files/download` +- **Parameters**: `filename` (query parameter). +- **Description**: Streams the requested file to the client. +- **Error Handling**: Return 404 if the file is not found in metadata or on disk. + +### 2.3 Static Web Serving +- **Endpoint**: `/web` +- **Description**: Serve static HTML, CSS, and JavaScript files from a `static` directory to provide a user interface. + +## 3. Technical Requirements + +### 3.1 Backend +- **Framework**: FastAPI +- **Language**: Python 3.9+ +- **Dependencies**: `python-multipart` (for file uploads), `uvicorn` (ASGI server). + +### 3.2 Data Storage +- **Filesystem**: Local storage under `assets/` directory. +- **Metadata**: A single `metadata.json` file acting as a lightweight database. + +### 3.3 Directory Structure +``` +. +├── main.py # FastAPI application entry point +├── metadata.json # File metadata storage +├── assets/ +│ ├── images/ # Uploaded image files +│ └── videos/ # Uploaded video files +├── static/ # Web frontend files (HTML/CSS/JS) +└── REQUIREMENTS.md # This document +``` + +## 4. Non-Functional Requirements +- **Security**: Sanitize filenames to prevent path traversal attacks. +- **Performance**: Efficiently handle file streaming for downloads. +- **Maintainability**: Clear separation between API logic and file management. diff --git a/backup.md b/backup.md new file mode 100644 index 0000000..9757e9f --- /dev/null +++ b/backup.md @@ -0,0 +1,15 @@ +# 프로젝트 개요 +- fastapi를 이용한 파일 업로드/다운로드 프로그램 작성 + +# 프로젝트 요구사항 + +- fastapi를 통해 파일 업로드 및 다운로드를 위한 api 구현 +- 업로드한 파일은 파일의 타입에 따라 assets/videos 또는 assets/images에 저장 +- 저장된 파일의 정보 (파일 이름, 타입, 업로드 날짜)는 별도 json 파일을 이용하여 관리 +- API 인터페이스 정의 + - 파일 업로드: /api/files/upload + - 파일 다운로드: /api/files/download + - static (웹 (html, css, javascript) 서빙) : /web + + + diff --git a/main.py b/main.py new file mode 100644 index 0000000..5789041 --- /dev/null +++ b/main.py @@ -0,0 +1,187 @@ +import json +import os +import shutil +import yt_dlp +from datetime import datetime +from pathlib import Path +from typing import List, Optional + +import yt_dlp +from fastapi import FastAPI, File, Form, HTTPException, UploadFile +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles + +app = FastAPI() + +# Configuration +BASE_DIR = Path(__file__).parent +ASSETS_DIR = BASE_DIR / "assets" +IMAGES_DIR = ASSETS_DIR / "images" +VIDEOS_DIR = ASSETS_DIR / "videos" +METADATA_FILE = BASE_DIR / "metadata.json" +STATIC_DIR = BASE_DIR / "static" + +# Ensure directories exist +for directory in [IMAGES_DIR, VIDEOS_DIR, STATIC_DIR]: + directory.mkdir(parents=True, exist_ok=True) + +if not METADATA_FILE.exists(): + METADATA_FILE.write_text("[]") + +# Mount static files +app.mount("/web", StaticFiles(directory=str(STATIC_DIR), html=True), name="static") + +def load_metadata() -> List[dict]: + try: + with open(METADATA_FILE, "r") as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + return [] + +def save_metadata(metadata: List[dict]): + with open(METADATA_FILE, "w") as f: + json.dump(metadata, f, indent=4) + +def get_file_category(content_type: str) -> str: + if content_type.startswith("image/"): + return "images" + elif content_type.startswith("video/"): + return "videos" + return "others" + +def sanitize_filename(filename: str) -> str: + # Basic sanitization: remove path traversal attempts and keep only alphanumeric/dots/underscores + return os.path.basename(filename).replace(" ", "_") + +@app.post("/api/files/upload") +async def upload_file( + file: UploadFile = File(...), + description: Optional[str] = Form(None), + uploader: Optional[str] = Form(None) +): + category = get_file_category(file.content_type) + if category == "others": + raise HTTPException(status_code=400, detail="Unsupported file type. Only images and videos are allowed.") + + filename = sanitize_filename(file.filename) + target_dir = IMAGES_DIR if category == "images" else VIDEOS_DIR + file_path = target_dir / filename + + # Handle duplicate filenames by appending timestamp + if file_path.exists(): + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + name, ext = os.path.splitext(filename) + filename = f"{name}_{timestamp}{ext}" + file_path = target_dir / filename + + try: + with file_path.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}") + + metadata_entry = { + "filename": filename, + "type": file.content_type, + "upload_date": datetime.now().isoformat(), + "file_path": str(file_path.relative_to(BASE_DIR)), + "description": description, + "uploader": uploader + } + + metadata = load_metadata() + metadata.append(metadata_entry) + save_metadata(metadata) + + return metadata_entry + +@app.post("/api/files/youtube") +async def download_youtube( + url: str = Form(...), + description: Optional[str] = Form(None), + uploader: Optional[str] = Form(None) +): + ydl_opts = { + 'format': 'bestvideo+bestaudio/best', + 'outtmpl': str(VIDEOS_DIR / '%(title)s.%(ext)s'), + 'quiet': True, + 'no_warnings': True, + } + + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=True) + file_path_str = ydl.prepare_filename(info) + file_path = Path(file_path_str) + filename = file_path.name + + metadata_entry = { + "filename": filename, + "type": "video/mp4", # Simplified, yt-dlp might download different formats + "upload_date": datetime.now().isoformat(), + "file_path": str(file_path.relative_to(BASE_DIR)), + "description": description or info.get('title'), + "uploader": uploader or info.get('uploader') + } + + metadata = load_metadata() + metadata.append(metadata_entry) + save_metadata(metadata) + + return metadata_entry + except Exception as e: + raise HTTPException(status_code=500, detail=f"YouTube download failed: {str(e)}") + +@app.get("/api/files") +async def list_files(): + return load_metadata() + +@app.get("/api/files/download") +async def download_file(filename: str, stream: bool = False): + metadata = load_metadata() + entry = next((item for item in metadata if item["filename"] == filename), None) + + if not entry: + raise HTTPException(status_code=404, detail="File not found in metadata") + + file_path = BASE_DIR / entry["file_path"] + if not file_path.exists(): + raise HTTPException(status_code=404, detail="File not found on disk") + + # If stream is True, we use 'inline' so the browser can play it + disposition = "inline" if stream else "attachment" + + return FileResponse( + path=file_path, + filename=filename if not stream else None, + media_type=entry["type"], + content_disposition_type=disposition + ) + +@app.delete("/api/files") +async def delete_file(filename: str): + metadata = load_metadata() + entry_index = next((i for i, item in enumerate(metadata) if item["filename"] == filename), None) + + if entry_index is None: + raise HTTPException(status_code=404, detail="File not found in metadata") + + entry = metadata[entry_index] + file_path = BASE_DIR / entry["file_path"] + + # Delete from filesystem + try: + if file_path.exists(): + file_path.unlink() + except Exception as e: + raise HTTPException(status_code=500, detail=f"Could not delete file from disk: {str(e)}") + + # Remove from metadata + metadata.pop(entry_index) + save_metadata(metadata) + + return {"message": f"File {filename} deleted successfully"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..01cbcb2 --- /dev/null +++ b/metadata.json @@ -0,0 +1,26 @@ +[ + { + "filename": "dummy.png", + "type": "image/png", + "upload_date": "2026-02-19T09:45:18.881161", + "file_path": "assets/images/dummy.png", + "description": "Test image", + "uploader": "Claude" + }, + { + "filename": "dummy.mp4", + "type": "video/mp4", + "upload_date": "2026-02-19T09:45:57.924247", + "file_path": "assets/videos/dummy.mp4", + "description": "Test video", + "uploader": "Claude" + }, + { + "filename": "Elegant Jazz \uff5c Sophisticated Melodies in a Luxury Hotel Lobby #1.webm", + "type": "video/mp4", + "upload_date": "2026-02-19T10:41:57.544173", + "file_path": "assets/videos/Elegant Jazz \uff5c Sophisticated Melodies in a Luxury Hotel Lobby #1.webm", + "description": "Elegant Jazz | Sophisticated Melodies in a Luxury Hotel Lobby #1", + "uploader": "Blue in Green" + } +] \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..a6beafd --- /dev/null +++ b/static/index.html @@ -0,0 +1,301 @@ + + + + + + Modern File Manager + + + + +
+

✨ File Manager

+ + +
+ + +
+

Playing...

+
+
+ +
+ +
+

📁 Local Upload

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

📺 YouTube Link

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

📦 Library

+
+ +
+
+ + + +