Compare commits
17 Commits
ec2e9abcad
...
master
Author | SHA1 | Date | |
---|---|---|---|
9422439a51 | |||
7796d9b7d5 | |||
d6a0656f12 | |||
04d92b7842 | |||
d3d8aa48b6 | |||
80f147731e | |||
ed5fa15814 | |||
896e42d9cc | |||
bda299a6c3 | |||
7be1f2ed07 | |||
ced3fd03e4 | |||
1a053ca047 | |||
92391aa2c0 | |||
6c41f5d883 | |||
49d16f0512 | |||
2195cdf1b9 | |||
bbf1ec10ef |
@@ -43,7 +43,43 @@
|
||||
"Bash(ssh:*)",
|
||||
"Bash(scp:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(./deploy-manual.sh)"
|
||||
"Bash(./deploy-manual.sh)",
|
||||
"Bash(npm install)",
|
||||
"Bash(powershell:*)",
|
||||
"Bash(schtasks:*)",
|
||||
"Bash(cmd //c:*)",
|
||||
"Bash(npm install:*)",
|
||||
"mcp__playwright__browser_wait_for",
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__context7__get-library-docs",
|
||||
"Bash(npm run init-mariadb:*)",
|
||||
"Bash(npm test)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(npm run stop:*)",
|
||||
"Bash(PORT=3007 node server.js)",
|
||||
"Bash(HOST=119.64.1.86 PORT=3007 node server.js)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"Bash(where claude)",
|
||||
"Read(/C:\\Users\\COMTREE\\.claude/**)",
|
||||
"Read(/C:\\Users\\COMTREE/**)",
|
||||
"Bash(md:*)",
|
||||
"Bash(.mcp_install.bat)",
|
||||
"Bash(npm search mcp)",
|
||||
"Read(/C:\\Users\\COMTREE\\.claude/**)",
|
||||
"Bash(claude mcp add:*)",
|
||||
"Bash(claude mcp:*)",
|
||||
"WebSearch",
|
||||
"Bash(uvx:*)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(npm audit:*)",
|
||||
"Bash(git config:*)",
|
||||
"Bash(git remote:*)",
|
||||
"Bash(findstr:*)",
|
||||
"mcp__serena__activate_project",
|
||||
"mcp__serena__list_dir",
|
||||
"mcp__playwright__browser_resize",
|
||||
"mcp__playwright__browser_take_screenshot"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": [],
|
||||
|
13
.env.example
Normal file
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# 개발 환경 설정 예시
|
||||
# SQLite 데이터베이스 사용 (설정 불필요)
|
||||
|
||||
# NAS 배포 환경
|
||||
NODE_ENV=development
|
||||
DEPLOY_ENV=local
|
||||
|
||||
# 서버 설정
|
||||
HOST=0.0.0.0
|
||||
PORT=3000
|
||||
|
||||
# 세션 설정
|
||||
SESSION_SECRET=your-session-secret-here
|
164
CLAUDE.md
164
CLAUDE.md
@@ -1,146 +1,34 @@
|
||||
# CLAUDE.md
|
||||
---
|
||||
allowed-tools: [Read, Grep, Glob, Bash, Edit, MultiEdit]
|
||||
description: "Clean up code, remove dead code, and optimize project structure"
|
||||
---
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
# /sc:cleanup - Code and Project Cleanup
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a web-based file management system (자료실) with full CRUD functionality. It's a hybrid application built with vanilla HTML, CSS, and JavaScript that supports both Supabase cloud database and localStorage for data persistence, providing seamless offline/online capabilities.
|
||||
|
||||
### Key Features
|
||||
- Create, Read, Update, Delete operations for file records
|
||||
- File upload with multiple file support (local + cloud storage)
|
||||
- User authentication and authorization
|
||||
- Search and filtering by title, description, tags, and category
|
||||
- Categorization system (문서, 이미지, 동영상, 프레젠테이션, 기타)
|
||||
- Tag-based organization
|
||||
- Responsive design for mobile and desktop
|
||||
- Modal-based editing interface
|
||||
- Cloud database with real-time synchronization
|
||||
- Offline support with localStorage fallback
|
||||
- Cross-device data synchronization
|
||||
|
||||
## File Structure
|
||||
## Purpose
|
||||
Systematically clean up code, remove dead code, optimize imports, and improve project structure.
|
||||
|
||||
## Usage
|
||||
```
|
||||
자료실/
|
||||
├── index.html # Main HTML file with UI structure and auth components
|
||||
├── styles.css # Complete styling with responsive design and auth styles
|
||||
├── script.js # Core JavaScript with FileManager class and Supabase integration
|
||||
├── supabase-config.js # Supabase configuration and helper functions
|
||||
├── supabase-schema.sql # Database schema for Supabase setup
|
||||
├── setup-guide.md # Comprehensive Supabase setup guide
|
||||
└── CLAUDE.md # This documentation file
|
||||
/sc:cleanup [target] [--type code|imports|files|all] [--safe|--aggressive] [--dry-run]
|
||||
```
|
||||
|
||||
## Architecture
|
||||
## Arguments
|
||||
- `target` - Files, directories, or entire project to clean
|
||||
- `--type` - Cleanup type (code, imports, files, all)
|
||||
- `--safe` - Conservative cleanup (default)
|
||||
- `--aggressive` - More thorough cleanup with higher risk
|
||||
- `--dry-run` - Preview changes without applying them
|
||||
|
||||
### Core Components
|
||||
## Execution
|
||||
1. Analyze target for cleanup opportunities
|
||||
2. Identify dead code, unused imports, and redundant files
|
||||
3. Create cleanup plan with risk assessment
|
||||
4. Execute cleanup operations with appropriate safety measures
|
||||
5. Validate changes and report cleanup results
|
||||
|
||||
1. **FileManager Class** (`script.js`)
|
||||
- Main application controller with hybrid storage support
|
||||
- Handles all CRUD operations (Supabase + localStorage fallback)
|
||||
- Manages user authentication and session state
|
||||
- Contains event handling and UI updates
|
||||
- Real-time synchronization capabilities
|
||||
|
||||
2. **Supabase Integration** (`supabase-config.js`)
|
||||
- Database configuration and connection management
|
||||
- Authentication helper functions (signup, login, logout)
|
||||
- CRUD helper functions for files and attachments
|
||||
- Storage helper functions for file uploads/downloads
|
||||
- Real-time subscription management
|
||||
|
||||
3. **Data Model**
|
||||
```javascript
|
||||
// Files table
|
||||
{
|
||||
id: UUID, // Primary key
|
||||
title: string, // File title (required)
|
||||
description: string, // Optional description
|
||||
category: string, // Required category
|
||||
tags: string[], // Array of tags
|
||||
user_id: UUID, // Foreign key to auth.users
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp
|
||||
}
|
||||
|
||||
// File attachments table
|
||||
{
|
||||
id: UUID,
|
||||
file_id: UUID, // Foreign key to files
|
||||
original_name: string,
|
||||
storage_path: string, // Supabase Storage path
|
||||
file_size: integer,
|
||||
mime_type: string,
|
||||
created_at: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
4. **UI Components**
|
||||
- Authentication section (login/signup/logout)
|
||||
- Sync status indicator
|
||||
- Search and filter section
|
||||
- Add new file form with cloud upload
|
||||
- File list display with sorting
|
||||
- Edit modal for updates
|
||||
- Responsive card-based layout
|
||||
- Offline mode notifications
|
||||
|
||||
### Development Commands
|
||||
|
||||
This is a hybrid web application supporting both online and offline modes:
|
||||
|
||||
1. **Local Development**
|
||||
```bash
|
||||
# Open index.html in a web browser
|
||||
# OR use a simple HTTP server:
|
||||
python -m http.server 8000
|
||||
# OR
|
||||
npx serve .
|
||||
```
|
||||
|
||||
2. **Supabase Setup (Required for online features)**
|
||||
```bash
|
||||
# 1. Follow setup-guide.md for complete setup
|
||||
# 2. Create Supabase project and database
|
||||
# 3. Run supabase-schema.sql in SQL Editor
|
||||
# 4. Create Storage bucket named 'files'
|
||||
# 5. Update supabase-config.js with your credentials
|
||||
```
|
||||
|
||||
3. **File Access**
|
||||
- Open `index.html` directly in browser
|
||||
- Works offline with localStorage (limited functionality)
|
||||
- Full features available with Supabase configuration
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
- **Database**: Supabase PostgreSQL with Row Level Security (RLS)
|
||||
- **Storage**: Supabase Storage for files + localStorage fallback
|
||||
- **Authentication**: Supabase Auth with email/password
|
||||
- **Real-time**: Supabase Realtime for live synchronization
|
||||
- **File Handling**: FileReader API + Supabase Storage API
|
||||
- **UI Updates**: Vanilla JavaScript DOM manipulation
|
||||
- **Styling**: CSS Grid and Flexbox for responsive layouts
|
||||
- **Animations**: CSS transitions and keyframe animations
|
||||
- **Offline Support**: Automatic fallback to localStorage when offline
|
||||
|
||||
### Data Management
|
||||
|
||||
- **Online Mode**: Files stored in Supabase PostgreSQL + Storage
|
||||
- **Offline Mode**: Files stored as base64 strings in localStorage
|
||||
- **Hybrid Sync**: Automatic synchronization when connection restored
|
||||
- User-specific data isolation with RLS policies
|
||||
- Search works across title, description, and tags
|
||||
- Sorting available by date, title, or category
|
||||
- Categories are predefined but can be extended
|
||||
- Real-time updates across devices for same user
|
||||
|
||||
### Browser Compatibility
|
||||
|
||||
- Modern browsers with ES6+ support
|
||||
- localStorage API support required
|
||||
- FileReader API for file uploads
|
||||
- Fetch API for Supabase communication
|
||||
- WebSocket support for real-time features
|
||||
- External dependency: Supabase JavaScript client library
|
||||
## Claude Code Integration
|
||||
- Uses Glob for systematic file discovery
|
||||
- Leverages Grep for dead code detection
|
||||
- Applies MultiEdit for batch cleanup operations
|
||||
- Maintains backup and rollback capabilities
|
45
JaryoAutoStart.xml
Normal file
45
JaryoAutoStart.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Date>2025-08-22T01:20:00</Date>
|
||||
<Author>COMTREE</Author>
|
||||
<Description>Jaryo File Manager 자동 시작 작업</Description>
|
||||
</RegistrationInfo>
|
||||
<Triggers>
|
||||
<BootTrigger>
|
||||
<Enabled>true</Enabled>
|
||||
<Delay>PT30S</Delay>
|
||||
</BootTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<UserId>S-1-5-18</UserId>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>true</AllowHardTerminate>
|
||||
<StartWhenAvailable>true</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>C:\Users\COMTREE\claude_code\jaryo\start-simple.bat</Command>
|
||||
<WorkingDirectory>C:\Users\COMTREE\claude_code\jaryo</WorkingDirectory>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
@@ -3,8 +3,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>자료실 - 관리자</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="styles.css?v=20250821194500">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@@ -461,7 +461,7 @@ class AdminFileManager {
|
||||
|
||||
async checkSession() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/session');
|
||||
const response = await fetch('/api/auth/session', { credentials: 'include' });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.user) {
|
||||
@@ -494,20 +494,21 @@ class AdminFileManager {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.currentUser = data.user;
|
||||
this.currentUser = data.user || data.data || null;
|
||||
this.isLoggedIn = true;
|
||||
this.showNotification('로그인되었습니다!', 'success');
|
||||
|
||||
await this.loadData();
|
||||
this.updateUI();
|
||||
} else {
|
||||
throw new Error(data.message || '로그인에 실패했습니다.');
|
||||
throw new Error(data.error || data.message || '로그인에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('로그인 오류:', error);
|
||||
@@ -520,7 +521,7 @@ class AdminFileManager {
|
||||
|
||||
async handleLogout() {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
||||
|
||||
this.currentUser = null;
|
||||
this.isLoggedIn = false;
|
||||
@@ -1329,7 +1330,6 @@ class AdminFileManager {
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">닫기</button>
|
||||
<button class="btn btn-primary" onclick="adminManager.editFile('${file.id}')">수정</button>
|
||||
<button class="btn btn-danger" onclick="adminManager.deleteFile('${file.id}')">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1367,6 +1367,22 @@ class AdminFileManager {
|
||||
|
||||
resetCategoryForm() {
|
||||
document.getElementById('categoryName').value = '';
|
||||
this.currentEditCategoryId = null;
|
||||
|
||||
// 버튼 텍스트를 기본 상태로 복원
|
||||
const submitBtn = document.getElementById('addCategoryBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.textContent = '➕ 카테고리 추가';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 폼 제목을 기본 상태로 복원
|
||||
const formTitle = document.querySelector('#categoryTab h2');
|
||||
if (formTitle) {
|
||||
formTitle.textContent = '🏷️ 카테고리 관리';
|
||||
}
|
||||
|
||||
console.log('카테고리 폼이 초기화되었습니다.');
|
||||
}
|
||||
|
||||
renderCategoryList() {
|
||||
|
@@ -29,7 +29,7 @@ header {
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #667eea;
|
||||
color: #667eea !important;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
@@ -1,298 +0,0 @@
|
||||
# 시놀로지 NAS 대안 Git 서버 설치 방안
|
||||
|
||||
## 🚀 개요
|
||||
|
||||
시놀로지 Git Server 패키지가 작동하지 않을 때 사용할 수 있는 대안적인 Git 서버 설치 방법들을 제공합니다.
|
||||
|
||||
## 🐳 방법 1: Docker를 이용한 Gitea 설치 (권장)
|
||||
|
||||
### 1.1 장점
|
||||
- 웹 기반 Git 관리 인터페이스
|
||||
- GitHub와 유사한 사용자 경험
|
||||
- 이슈 관리, 위키, 프로젝트 관리 기능
|
||||
- 가벼움 (Go 언어 기반)
|
||||
|
||||
### 1.2 설치 과정
|
||||
|
||||
#### Docker 설치 확인
|
||||
```bash
|
||||
# DSM > 패키지 센터 > Docker 설치
|
||||
# 또는 SSH에서 확인
|
||||
docker --version
|
||||
```
|
||||
|
||||
#### Gitea 컨테이너 실행
|
||||
```bash
|
||||
# SSH로 NAS 접속
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# Gitea 데이터 디렉토리 생성
|
||||
sudo mkdir -p /volume1/docker/gitea
|
||||
|
||||
# Gitea 컨테이너 실행
|
||||
docker run -d \
|
||||
--name gitea \
|
||||
-p 3000:3000 \
|
||||
-p 222:22 \
|
||||
-v /volume1/docker/gitea:/data \
|
||||
-e USER_UID=1000 \
|
||||
-e USER_GID=1000 \
|
||||
gitea/gitea:latest
|
||||
```
|
||||
|
||||
#### 웹 설정
|
||||
1. 브라우저에서 `http://your-nas-ip:3000` 접속
|
||||
2. 초기 설정 완료:
|
||||
- 데이터베이스: SQLite3 (기본)
|
||||
- 관리자 계정 생성
|
||||
- 저장소 루트 경로: `/data/git/repositories`
|
||||
|
||||
### 1.3 저장소 생성 및 연결
|
||||
```bash
|
||||
# 웹 인터페이스에서 새 저장소 'jaryo-file-manager' 생성
|
||||
# 로컬에서 연결
|
||||
git remote add gitea http://your-nas-ip:3000/username/jaryo-file-manager.git
|
||||
git push gitea master
|
||||
```
|
||||
|
||||
## 📦 방법 2: 순수 Git 서버 설치
|
||||
|
||||
### 2.1 수동 Git 설치
|
||||
```bash
|
||||
# SSH로 NAS 접속
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# 패키지 관리자 업데이트
|
||||
sudo apt update
|
||||
|
||||
# Git 설치
|
||||
sudo apt install git git-daemon-run
|
||||
|
||||
# 버전 확인
|
||||
git --version
|
||||
```
|
||||
|
||||
### 2.2 Git 서비스 설정
|
||||
```bash
|
||||
# Git 사용자 생성
|
||||
sudo adduser git
|
||||
sudo su git
|
||||
cd /home/git
|
||||
|
||||
# SSH 키 디렉토리 생성
|
||||
mkdir .ssh && chmod 700 .ssh
|
||||
touch .ssh/authorized_keys && chmod 600 .ssh/authorized_keys
|
||||
|
||||
# Git 저장소 디렉토리 생성
|
||||
mkdir /home/git/repositories
|
||||
```
|
||||
|
||||
### 2.3 systemd 서비스 설정
|
||||
```bash
|
||||
# Git daemon 서비스 파일 생성
|
||||
sudo tee /etc/systemd/system/git-daemon.service << EOF
|
||||
[Unit]
|
||||
Description=Git Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/home/git/repositories --export-all --verbose --enable=receive-pack
|
||||
Restart=always
|
||||
User=git
|
||||
Group=git
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# 서비스 활성화
|
||||
sudo systemctl enable git-daemon
|
||||
sudo systemctl start git-daemon
|
||||
sudo systemctl status git-daemon
|
||||
```
|
||||
|
||||
### 2.4 저장소 생성
|
||||
```bash
|
||||
# git 사용자로 전환
|
||||
sudo su git
|
||||
cd /home/git/repositories
|
||||
|
||||
# bare 저장소 생성
|
||||
git init --bare jaryo-file-manager.git
|
||||
```
|
||||
|
||||
## 🌐 방법 3: GitLab CE Docker 설치
|
||||
|
||||
### 3.1 특징
|
||||
- 기업급 Git 관리 플랫폼
|
||||
- CI/CD 파이프라인 지원
|
||||
- 이슈 추적, 위키, 프로젝트 관리
|
||||
- 더 많은 리소스 필요
|
||||
|
||||
### 3.2 설치 과정
|
||||
```bash
|
||||
# GitLab 데이터 디렉토리 생성
|
||||
sudo mkdir -p /volume1/docker/gitlab/{config,logs,data}
|
||||
|
||||
# GitLab 컨테이너 실행 (최소 4GB RAM 권장)
|
||||
docker run -d \
|
||||
--hostname your-nas-ip \
|
||||
--name gitlab \
|
||||
-p 8080:80 \
|
||||
-p 8443:443 \
|
||||
-p 8022:22 \
|
||||
-v /volume1/docker/gitlab/config:/etc/gitlab \
|
||||
-v /volume1/docker/gitlab/logs:/var/log/gitlab \
|
||||
-v /volume1/docker/gitlab/data:/var/opt/gitlab \
|
||||
gitlab/gitlab-ce:latest
|
||||
```
|
||||
|
||||
### 3.3 초기 설정
|
||||
```bash
|
||||
# 컨테이너 시작 대기 (2-3분)
|
||||
docker logs -f gitlab
|
||||
|
||||
# 브라우저에서 http://your-nas-ip:8080 접속
|
||||
# 초기 root 비밀번호 확인
|
||||
docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password
|
||||
```
|
||||
|
||||
## 🔧 방법 4: Forgejo (Gitea Fork) 설치
|
||||
|
||||
### 4.1 특징
|
||||
- Gitea의 커뮤니티 중심 포크
|
||||
- 더 빠른 개발 주기
|
||||
- 오픈소스 중심
|
||||
|
||||
### 4.2 설치 과정
|
||||
```bash
|
||||
# Forgejo 데이터 디렉토리 생성
|
||||
sudo mkdir -p /volume1/docker/forgejo
|
||||
|
||||
# Forgejo 컨테이너 실행
|
||||
docker run -d \
|
||||
--name forgejo \
|
||||
-p 3000:3000 \
|
||||
-p 222:22 \
|
||||
-v /volume1/docker/forgejo:/data \
|
||||
-e USER_UID=1000 \
|
||||
-e USER_GID=1000 \
|
||||
codeberg.org/forgejo/forgejo:latest
|
||||
```
|
||||
|
||||
## 📱 방법 5: 간단한 HTTP Git 서버
|
||||
|
||||
### 5.1 Python 기반 간단 서버
|
||||
```bash
|
||||
# Python Git HTTP 서버 스크립트 생성
|
||||
cat > /volume1/web/git-http-server.py << 'EOF'
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import http.server
|
||||
import socketserver
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
class GitHTTPHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path.endswith('.git/info/refs'):
|
||||
# Git 정보 요청 처리
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
self.end_headers()
|
||||
|
||||
repo_path = self.path.split('/')[1]
|
||||
git_dir = f'/volume1/git/{repo_path}'
|
||||
|
||||
if os.path.exists(git_dir):
|
||||
proc = Popen(['git', 'upload-pack', '--advertise-refs', git_dir],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
output, _ = proc.communicate()
|
||||
self.wfile.write(output)
|
||||
else:
|
||||
self.wfile.write(b'Repository not found')
|
||||
else:
|
||||
super().do_GET()
|
||||
|
||||
PORT = 8000
|
||||
with socketserver.TCPServer(("", PORT), GitHTTPHandler) as httpd:
|
||||
print(f"Git HTTP Server running on port {PORT}")
|
||||
httpd.serve_forever()
|
||||
EOF
|
||||
|
||||
# 실행 권한 부여
|
||||
chmod +x /volume1/web/git-http-server.py
|
||||
|
||||
# 서버 실행
|
||||
python3 /volume1/web/git-http-server.py
|
||||
```
|
||||
|
||||
## 🔀 방법별 비교표
|
||||
|
||||
| 방법 | 난이도 | 리소스 사용량 | 기능 | 웹 UI | 권장도 |
|
||||
|------|--------|---------------|------|-------|--------|
|
||||
| Gitea | 쉬움 | 낮음 | 풍부 | ✅ | ⭐⭐⭐⭐⭐ |
|
||||
| 순수 Git | 보통 | 매우 낮음 | 기본 | ❌ | ⭐⭐⭐ |
|
||||
| GitLab CE | 어려움 | 높음 | 매우 풍부 | ✅ | ⭐⭐⭐⭐ |
|
||||
| Forgejo | 쉬움 | 낮음 | 풍부 | ✅ | ⭐⭐⭐⭐ |
|
||||
| Python 서버 | 보통 | 낮음 | 제한적 | ❌ | ⭐⭐ |
|
||||
|
||||
## 🚀 빠른 시작 가이드 (Gitea 권장)
|
||||
|
||||
### 1단계: Docker 설치 확인
|
||||
```bash
|
||||
# DSM 패키지 센터에서 Docker 설치
|
||||
```
|
||||
|
||||
### 2단계: Gitea 설치
|
||||
```bash
|
||||
ssh admin@your-nas-ip
|
||||
sudo mkdir -p /volume1/docker/gitea
|
||||
docker run -d --name gitea -p 3000:3000 -p 222:22 -v /volume1/docker/gitea:/data gitea/gitea:latest
|
||||
```
|
||||
|
||||
### 3단계: 웹 설정
|
||||
- `http://your-nas-ip:3000` 접속
|
||||
- 초기 설정 완료
|
||||
- 관리자 계정 생성
|
||||
|
||||
### 4단계: 저장소 생성 및 연결
|
||||
```bash
|
||||
# 웹에서 새 저장소 생성
|
||||
# 로컬에서 연결
|
||||
cd /c/Users/COMTREE/claude_code/jaryo
|
||||
git remote add gitea http://your-nas-ip:3000/admin/jaryo-file-manager.git
|
||||
git push gitea master
|
||||
```
|
||||
|
||||
## 🛠️ 문제 해결
|
||||
|
||||
### Docker 관련 문제
|
||||
```bash
|
||||
# 컨테이너 상태 확인
|
||||
docker ps -a
|
||||
|
||||
# 로그 확인
|
||||
docker logs gitea
|
||||
|
||||
# 컨테이너 재시작
|
||||
docker restart gitea
|
||||
```
|
||||
|
||||
### 포트 충돌 문제
|
||||
```bash
|
||||
# 사용 중인 포트 확인
|
||||
sudo netstat -tulpn | grep :3000
|
||||
|
||||
# 다른 포트 사용
|
||||
docker run -d --name gitea -p 3001:3000 -p 223:22 -v /volume1/docker/gitea:/data gitea/gitea:latest
|
||||
```
|
||||
|
||||
### 권한 문제
|
||||
```bash
|
||||
# 데이터 디렉토리 권한 수정
|
||||
sudo chown -R 1000:1000 /volume1/docker/gitea
|
||||
```
|
||||
|
||||
이 가이드를 통해 시놀로지 NAS Git Server 패키지 문제를 우회하여 안정적인 Git 서버를 구축할 수 있습니다.
|
40
api/files.js
40
api/files.js
@@ -1,40 +0,0 @@
|
||||
// 파일 API
|
||||
module.exports = (req, res) => {
|
||||
// CORS 헤더 설정
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// 샘플 파일 데이터
|
||||
const files = [
|
||||
{
|
||||
id: 1,
|
||||
title: '샘플 문서',
|
||||
description: '데모용 파일입니다',
|
||||
category: '문서',
|
||||
tags: ['샘플', '테스트'],
|
||||
created_at: new Date().toISOString(),
|
||||
file_url: '#'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '이미지 파일',
|
||||
description: '예시 이미지',
|
||||
category: '이미지',
|
||||
tags: ['샘플'],
|
||||
created_at: new Date().toISOString(),
|
||||
file_url: '#'
|
||||
}
|
||||
];
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: files,
|
||||
message: '파일 목록을 성공적으로 불러왔습니다.'
|
||||
});
|
||||
};
|
@@ -1,4 +0,0 @@
|
||||
const app = require('../server');
|
||||
|
||||
// Vercel 서버리스 함수로 export
|
||||
module.exports = app;
|
124
api/simple.js
124
api/simple.js
@@ -1,124 +0,0 @@
|
||||
// Vercel Serverless 함수 핸들러
|
||||
module.exports = function handler(req, res) {
|
||||
// CORS 헤더 설정
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, method } = req;
|
||||
|
||||
// 헬스 체크
|
||||
if (url === '/health' || url === '/api/health') {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Jaryo File Manager is running',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || 'production',
|
||||
path: url
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 루트 경로
|
||||
if (url === '/' || url === '/api') {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Jaryo File Manager</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; }
|
||||
.container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; text-align: center; }
|
||||
.status { background: #e8f5e8; border: 1px solid #4caf50; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||
.feature { background: #f0f8ff; border: 1px solid #2196f3; padding: 15px; border-radius: 5px; margin: 10px 0; }
|
||||
.button { background: #4caf50; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; text-decoration: none; display: inline-block; margin: 5px; }
|
||||
.button:hover { background: #45a049; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 Jaryo File Manager</h1>
|
||||
<div class="status">
|
||||
<h3>✅ 배포 성공!</h3>
|
||||
<p>Vercel Serverless에서 성공적으로 실행 중입니다.</p>
|
||||
<p><strong>시간:</strong> ${new Date().toLocaleString('ko-KR')}</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🔧 API 엔드포인트</h3>
|
||||
<p><a href="/health" class="button">헬스 체크</a></p>
|
||||
<p><a href="/api/files" class="button">파일 목록 API</a></p>
|
||||
<p><a href="/api/files/public" class="button">공개 파일 API</a></p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>📱 페이지</h3>
|
||||
<p><a href="/index.html" class="button">메인 페이지</a></p>
|
||||
<p><a href="/admin/index.html" class="button">관리자 페이지</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// API 라우트들
|
||||
if (url === '/api/files' || url === '/api/files/public') {
|
||||
res.json({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
title: '샘플 문서',
|
||||
description: '데모용 파일입니다',
|
||||
category: '문서',
|
||||
tags: ['샘플', '테스트'],
|
||||
created_at: new Date().toISOString(),
|
||||
file_url: '#'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '이미지 파일',
|
||||
description: '예시 이미지',
|
||||
category: '이미지',
|
||||
tags: ['샘플'],
|
||||
created_at: new Date().toISOString(),
|
||||
file_url: '#'
|
||||
}
|
||||
],
|
||||
message: url.includes('public') ? '공개 파일 목록' : '파일 목록 API (데모 모드)'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url === '/api/categories') {
|
||||
res.json({
|
||||
success: true,
|
||||
data: [
|
||||
{ id: 1, name: '문서', description: '문서 파일' },
|
||||
{ id: 2, name: '이미지', description: '이미지 파일' },
|
||||
{ id: 3, name: '동영상', description: '동영상 파일' },
|
||||
{ id: 4, name: '프레젠테이션', description: '프레젠테이션 파일' },
|
||||
{ id: 5, name: '기타', description: '기타 파일' }
|
||||
],
|
||||
message: '카테고리 목록'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 404 핸들러
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: '요청한 리소스를 찾을 수 없습니다.',
|
||||
path: url,
|
||||
method: method
|
||||
});
|
||||
}
|
21
api/test.js
21
api/test.js
@@ -1,21 +0,0 @@
|
||||
// 테스트용 간단한 API
|
||||
module.exports = (req, res) => {
|
||||
// CORS 헤더 설정
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// 간단한 응답
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'API가 정상 작동합니다!',
|
||||
timestamp: new Date().toISOString(),
|
||||
url: req.url,
|
||||
method: req.method
|
||||
});
|
||||
};
|
@@ -1,119 +0,0 @@
|
||||
// Supabase configuration (오프라인 모드)
|
||||
// ⚠️ 오프라인 모드로 강제 설정됨
|
||||
const SUPABASE_CONFIG = {
|
||||
url: 'https://kncudtzthmjegowbgnto.supabase.co',
|
||||
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtuY3VkdHp0aG1qZWdvd2JnbnRvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU1Njc5OTksImV4cCI6MjA3MTE0Mzk5OX0.NlJN2vdgM96RvyVJE6ILQeDVUOU9X2F9vUn-jr_xlKc'
|
||||
};
|
||||
|
||||
// Supabase 클라이언트 초기화 (강제 비활성화)
|
||||
let supabase = null;
|
||||
|
||||
// 설정이 유효한지 확인
|
||||
function isSupabaseConfigured() {
|
||||
return false; // 강제로 false 반환
|
||||
}
|
||||
|
||||
// Supabase 클라이언트 초기화 함수 (오프라인 모드 강제)
|
||||
function initializeSupabase() {
|
||||
console.log('⚠️ 오프라인 모드로 강제 설정되었습니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 인증 상태 변경 리스너 (오프라인 모드용 - 빈 함수)
|
||||
function setupAuthListener(callback) {
|
||||
// 오프라인 모드에서는 아무것도 하지 않음
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 사용자 가져오기 (오프라인 모드용 - null 반환)
|
||||
async function getCurrentUser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 로그인 (오프라인 모드용 - 빈 함수)
|
||||
async function signIn(email, password) {
|
||||
throw new Error('오프라인 모드에서는 로그인할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 회원가입 (오프라인 모드용 - 빈 함수)
|
||||
async function signUp(email, password, metadata = {}) {
|
||||
throw new Error('오프라인 모드에서는 회원가입할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 로그아웃 (오프라인 모드용 - 빈 함수)
|
||||
async function signOut() {
|
||||
throw new Error('오프라인 모드에서는 로그아웃할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 데이터베이스 헬퍼 함수들 (오프라인 모드용)
|
||||
const SupabaseHelper = {
|
||||
// 파일 목록 가져오기 (오프라인 모드용)
|
||||
async getFiles(userId) {
|
||||
console.log('🔍 SupabaseHelper.getFiles 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// 파일 추가 (오프라인 모드용)
|
||||
async addFile(fileData, userId) {
|
||||
console.log('🔍 SupabaseHelper.addFile 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// 파일 수정 (오프라인 모드용)
|
||||
async updateFile(id, updates, userId) {
|
||||
console.log('🔍 SupabaseHelper.updateFile 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// 파일 삭제 (오프라인 모드용)
|
||||
async deleteFile(id, userId) {
|
||||
console.log('🔍 SupabaseHelper.deleteFile 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// 실시간 구독 설정 (오프라인 모드용)
|
||||
subscribeToFiles(userId, callback) {
|
||||
console.log('🔍 SupabaseHelper.subscribeToFiles 호출됨 (오프라인 모드)');
|
||||
return null;
|
||||
},
|
||||
|
||||
// 파일 업로드 (오프라인 모드용)
|
||||
async uploadFile(file, filePath) {
|
||||
console.log('🔍 SupabaseHelper.uploadFile 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase Storage를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// 파일 다운로드 URL 가져오기 (오프라인 모드용)
|
||||
async getFileUrl(filePath) {
|
||||
console.log('🔍 SupabaseHelper.getFileUrl 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase Storage를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// 파일 삭제 (Storage) (오프라인 모드용)
|
||||
async deleteStorageFile(filePath) {
|
||||
console.log('🔍 SupabaseHelper.deleteStorageFile 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase Storage를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// 첨부파일 정보 추가 (오프라인 모드용)
|
||||
async addFileAttachment(fileId, attachmentData) {
|
||||
console.log('🔍 SupabaseHelper.addFileAttachment 호출됨 (오프라인 모드)');
|
||||
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
|
||||
},
|
||||
|
||||
// Storage 버킷 확인 및 생성 (오프라인 모드용)
|
||||
async checkOrCreateBucket() {
|
||||
console.log('🔍 SupabaseHelper.checkOrCreateBucket 호출됨 (오프라인 모드)');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 전역으로 내보내기
|
||||
window.SupabaseHelper = SupabaseHelper;
|
||||
window.initializeSupabase = initializeSupabase;
|
||||
window.isSupabaseConfigured = isSupabaseConfigured;
|
||||
window.setupAuthListener = setupAuthListener;
|
||||
window.getCurrentUser = getCurrentUser;
|
||||
window.signIn = signIn;
|
||||
window.signUp = signUp;
|
||||
window.signOut = signOut;
|
@@ -1,62 +0,0 @@
|
||||
-- 완전 초기화 후 Storage 정책 재설정
|
||||
|
||||
-- 1단계: 모든 기존 Storage 정책 삭제
|
||||
DROP POLICY IF EXISTS "Users can upload to own folder" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Users can view own files" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Users can update own files" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Users can delete own files" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Public upload for testing" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Public read for testing" ON storage.objects;
|
||||
|
||||
-- 혹시 다른 이름으로 생성된 정책들도 삭제
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Enable select for authenticated users only" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Enable update for authenticated users only" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Enable delete for authenticated users only" ON storage.objects;
|
||||
|
||||
-- 2단계: RLS 활성화 확인 (보통 이미 활성화되어 있음)
|
||||
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 3단계: 새 정책 생성
|
||||
-- 업로드 정책
|
||||
CREATE POLICY "Users can upload to own folder"
|
||||
ON storage.objects
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 조회 정책
|
||||
CREATE POLICY "Users can view own files"
|
||||
ON storage.objects
|
||||
FOR SELECT
|
||||
USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 업데이트 정책
|
||||
CREATE POLICY "Users can update own files"
|
||||
ON storage.objects
|
||||
FOR UPDATE
|
||||
USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 삭제 정책
|
||||
CREATE POLICY "Users can delete own files"
|
||||
ON storage.objects
|
||||
FOR DELETE
|
||||
USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 4단계: 정책 생성 확인
|
||||
SELECT
|
||||
'Storage policies created successfully!' as message,
|
||||
COUNT(*) as policy_count
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'storage' AND tablename = 'objects';
|
@@ -1,150 +0,0 @@
|
||||
# Supabase 설정 가이드
|
||||
|
||||
이 문서는 자료실 시스템을 Supabase와 연동하기 위한 설정 가이드입니다.
|
||||
|
||||
## 1. Supabase 프로젝트 생성
|
||||
|
||||
1. [Supabase](https://supabase.com)에 접속하여 계정을 생성합니다.
|
||||
2. 새 프로젝트를 생성합니다.
|
||||
3. 프로젝트 이름과 비밀번호를 설정합니다.
|
||||
4. 리전은 `ap-northeast-1` (Asia Pacific - Tokyo)를 선택하는 것을 권장합니다.
|
||||
|
||||
## 2. 데이터베이스 스키마 설정
|
||||
|
||||
1. Supabase 대시보드에서 **SQL Editor**로 이동합니다.
|
||||
2. `supabase-schema.sql` 파일의 내용을 복사하여 실행합니다.
|
||||
3. 스키마가 성공적으로 생성되었는지 **Table Editor**에서 확인합니다.
|
||||
|
||||
### 생성되는 테이블
|
||||
- `files`: 파일 메타데이터 저장
|
||||
- `file_attachments`: 첨부파일 정보 저장
|
||||
|
||||
## 3. Storage 버킷 설정
|
||||
|
||||
1. Supabase 대시보드에서 **Storage**로 이동합니다.
|
||||
2. **New bucket** 버튼을 클릭합니다.
|
||||
3. 버킷 이름을 `files`로 설정합니다.
|
||||
4. **Public bucket** 체크박스는 해제합니다 (보안상 권장).
|
||||
5. 버킷을 생성합니다.
|
||||
|
||||
### Storage 정책 설정
|
||||
버킷 생성 후 **Policies** 탭에서 다음 정책들을 추가합니다:
|
||||
|
||||
#### SELECT 정책 (파일 조회)
|
||||
```sql
|
||||
CREATE POLICY "Users can view their own files" ON storage.objects
|
||||
FOR SELECT USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
```
|
||||
|
||||
#### INSERT 정책 (파일 업로드)
|
||||
```sql
|
||||
CREATE POLICY "Users can upload their own files" ON storage.objects
|
||||
FOR INSERT WITH CHECK (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
```
|
||||
|
||||
#### DELETE 정책 (파일 삭제)
|
||||
```sql
|
||||
CREATE POLICY "Users can delete their own files" ON storage.objects
|
||||
FOR DELETE USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
```
|
||||
|
||||
## 4. API 키 및 URL 설정
|
||||
|
||||
1. Supabase 대시보드에서 **Settings** > **API**로 이동합니다.
|
||||
2. 다음 정보를 확인합니다:
|
||||
- **Project URL**: `https://your-project-id.supabase.co`
|
||||
- **Project API keys** > **anon public**: `eyJ...`
|
||||
|
||||
3. `supabase-config.js` 파일을 수정합니다:
|
||||
```javascript
|
||||
const SUPABASE_CONFIG = {
|
||||
url: 'https://your-project-id.supabase.co', // 실제 Project URL로 교체
|
||||
anonKey: 'eyJ...' // 실제 anon public key로 교체
|
||||
};
|
||||
```
|
||||
|
||||
## 5. 인증 설정 (선택사항)
|
||||
|
||||
### 이메일 인증 비활성화 (개발용)
|
||||
개발 환경에서 빠른 테스트를 위해 이메일 인증을 비활성화할 수 있습니다:
|
||||
|
||||
1. **Authentication** > **Settings**로 이동
|
||||
2. **Enable email confirmations** 체크박스 해제
|
||||
3. **Save** 클릭
|
||||
|
||||
⚠️ **주의**: 프로덕션 환경에서는 이메일 인증을 활성화하는 것을 강력히 권장합니다.
|
||||
|
||||
### 이메일 템플릿 설정 (프로덕션용)
|
||||
1. **Authentication** > **Email Templates**에서 이메일 템플릿을 커스터마이징할 수 있습니다.
|
||||
2. 회사 브랜드에 맞게 이메일 디자인을 수정하세요.
|
||||
|
||||
## 6. 보안 설정
|
||||
|
||||
### Row Level Security (RLS)
|
||||
스키마 실행 시 자동으로 설정되지만, 다음 사항을 확인하세요:
|
||||
|
||||
1. **Authentication** > **Policies**에서 정책이 올바르게 설정되었는지 확인
|
||||
2. 각 테이블에 사용자별 접근 제한이 적용되어 있는지 확인
|
||||
|
||||
### 환경변수 보안
|
||||
프로덕션 환경에서는 API 키를 환경변수로 관리하세요:
|
||||
|
||||
```javascript
|
||||
const SUPABASE_CONFIG = {
|
||||
url: process.env.SUPABASE_URL || 'YOUR_SUPABASE_PROJECT_URL',
|
||||
anonKey: process.env.SUPABASE_ANON_KEY || 'YOUR_SUPABASE_ANON_KEY'
|
||||
};
|
||||
```
|
||||
|
||||
## 7. 테스트
|
||||
|
||||
설정 완료 후 다음 기능들을 테스트하세요:
|
||||
|
||||
1. **회원가입/로그인** - 새 계정 생성 및 로그인
|
||||
2. **파일 추가** - 새 자료 추가 (첨부파일 포함)
|
||||
3. **파일 수정** - 기존 자료 수정
|
||||
4. **파일 삭제** - 자료 삭제 (첨부파일도 함께 삭제되는지 확인)
|
||||
5. **파일 다운로드** - 첨부파일 다운로드
|
||||
6. **실시간 동기화** - 다른 브라우저에서 같은 계정으로 로그인하여 실시간 동기화 확인
|
||||
|
||||
## 8. 문제 해결
|
||||
|
||||
### 연결 오류
|
||||
- Supabase URL과 API 키가 올바른지 확인
|
||||
- 브라우저 콘솔에서 오류 메시지 확인
|
||||
- CORS 설정 확인 (대부분 자동으로 설정됨)
|
||||
|
||||
### 권한 오류
|
||||
- RLS 정책이 올바르게 설정되었는지 확인
|
||||
- 사용자가 올바르게 인증되었는지 확인
|
||||
|
||||
### 파일 업로드 오류
|
||||
- Storage 버킷이 올바르게 생성되었는지 확인
|
||||
- Storage 정책이 올바르게 설정되었는지 확인
|
||||
- 파일 크기 제한 확인 (Supabase 기본값: 50MB)
|
||||
|
||||
## 9. 추가 개선사항
|
||||
|
||||
### 성능 최적화
|
||||
- 대용량 파일 처리를 위한 chunk 업로드 구현
|
||||
- 이미지 최적화 및 썸네일 생성
|
||||
- CDN 연동 고려
|
||||
|
||||
### 기능 확장
|
||||
- 파일 공유 기능
|
||||
- 버전 관리
|
||||
- 협업 기능
|
||||
- 백업 및 복원 기능
|
||||
|
||||
---
|
||||
|
||||
설정 중 문제가 발생하면 [Supabase 공식 문서](https://supabase.com/docs)를 참고하거나 이슈를 등록해주세요.
|
@@ -1,39 +0,0 @@
|
||||
-- Storage 전용 정책 (Supabase Dashboard → Storage → Policies에서 실행)
|
||||
|
||||
-- 1. 파일 업로드 정책
|
||||
CREATE POLICY "Users can upload to own folder"
|
||||
ON storage.objects
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 2. 파일 조회 정책
|
||||
CREATE POLICY "Users can view own files"
|
||||
ON storage.objects
|
||||
FOR SELECT
|
||||
USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 3. 파일 업데이트 정책
|
||||
CREATE POLICY "Users can update own files"
|
||||
ON storage.objects
|
||||
FOR UPDATE
|
||||
USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 4. 파일 삭제 정책
|
||||
CREATE POLICY "Users can delete own files"
|
||||
ON storage.objects
|
||||
FOR DELETE
|
||||
USING (
|
||||
bucket_id = 'files' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
-- 참고: storage.foldername(name)[1]은 'user_id/filename.txt'에서 'user_id' 부분을 추출합니다.
|
@@ -1,309 +0,0 @@
|
||||
// Supabase configuration
|
||||
// ⚠️ 실제 사용 시에는 이 값들을 환경변수나 설정 파일로 관리하세요
|
||||
const SUPABASE_CONFIG = {
|
||||
// 실제 Supabase 프로젝트 URL로 교체하세요
|
||||
url: 'https://kncudtzthmjegowbgnto.supabase.co',
|
||||
// 실제 Supabase anon key로 교체하세요
|
||||
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtuY3VkdHp0aG1qZWdvd2JnbnRvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU1Njc5OTksImV4cCI6MjA3MTE0Mzk5OX0.NlJN2vdgM96RvyVJE6ILQeDVUOU9X2F9vUn-jr_xlKc'
|
||||
};
|
||||
|
||||
// Supabase 클라이언트 초기화
|
||||
let supabase;
|
||||
|
||||
// 설정이 유효한지 확인
|
||||
function isSupabaseConfigured() {
|
||||
return SUPABASE_CONFIG.url !== 'YOUR_SUPABASE_PROJECT_URL' &&
|
||||
SUPABASE_CONFIG.anonKey !== 'YOUR_SUPABASE_ANON_KEY';
|
||||
}
|
||||
|
||||
// Supabase 클라이언트 초기화 함수
|
||||
function initializeSupabase() {
|
||||
if (!isSupabaseConfigured()) {
|
||||
console.warn('⚠️ Supabase가 설정되지 않았습니다. localStorage를 사용합니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
supabase = window.supabase.createClient(SUPABASE_CONFIG.url, SUPABASE_CONFIG.anonKey);
|
||||
console.log('✅ Supabase 클라이언트가 초기화되었습니다.');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Supabase 초기화 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 인증 상태 변경 리스너
|
||||
function setupAuthListener(callback) {
|
||||
if (!supabase) return;
|
||||
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
console.log('Auth state changed:', event, session);
|
||||
if (callback) callback(event, session);
|
||||
});
|
||||
}
|
||||
|
||||
// 현재 사용자 가져오기
|
||||
async function getCurrentUser() {
|
||||
if (!supabase) return null;
|
||||
|
||||
try {
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
if (error) throw error;
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 가져오기 오류:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 로그인
|
||||
async function signIn(email, password) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
// 회원가입
|
||||
async function signUp(email, password, metadata = {}) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: metadata
|
||||
}
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
// 로그아웃
|
||||
async function signOut() {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// 데이터베이스 헬퍼 함수들
|
||||
const SupabaseHelper = {
|
||||
// 파일 목록 가져오기
|
||||
async getFiles(userId) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
let query = supabase
|
||||
.from('files')
|
||||
.select(`
|
||||
*,
|
||||
file_attachments (*)
|
||||
`);
|
||||
|
||||
// 공개 파일 요청이 아닌 경우에만 사용자 ID로 필터링
|
||||
if (userId !== 'public') {
|
||||
query = query.eq('user_id', userId);
|
||||
}
|
||||
|
||||
const { data, error } = await query.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
// 파일 추가
|
||||
async addFile(fileData, userId) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
// 데이터베이스 스키마에 맞는 필드만 추출
|
||||
const dbFileData = {
|
||||
title: fileData.title,
|
||||
description: fileData.description || '',
|
||||
category: fileData.category,
|
||||
tags: fileData.tags || [],
|
||||
user_id: userId
|
||||
// created_at, updated_at은 데이터베이스에서 자동 생성
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('files')
|
||||
.insert([dbFileData])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
// 파일 수정
|
||||
async updateFile(id, updates, userId) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
// 데이터베이스 스키마에 맞는 필드만 추출
|
||||
const dbUpdates = {
|
||||
title: updates.title,
|
||||
description: updates.description,
|
||||
category: updates.category,
|
||||
tags: updates.tags || []
|
||||
// updated_at은 트리거에 의해 자동 업데이트됨
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('files')
|
||||
.update(dbUpdates)
|
||||
.eq('id', id)
|
||||
.eq('user_id', userId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
// 파일 삭제
|
||||
async deleteFile(id, userId) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
const { error } = await supabase
|
||||
.from('files')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (error) throw error;
|
||||
},
|
||||
|
||||
// 실시간 구독 설정
|
||||
subscribeToFiles(userId, callback) {
|
||||
if (!supabase) return null;
|
||||
|
||||
return supabase
|
||||
.channel('files')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'files',
|
||||
filter: `user_id=eq.${userId}`
|
||||
}, callback)
|
||||
.subscribe();
|
||||
},
|
||||
|
||||
// 파일 업로드 (Storage)
|
||||
async uploadFile(file, filePath) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from('files')
|
||||
.upload(filePath, file);
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
// 파일 다운로드 URL 가져오기
|
||||
async getFileUrl(filePath) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
try {
|
||||
// 먼저 파일이 존재하는지 확인
|
||||
const { data: fileExists, error: checkError } = await supabase.storage
|
||||
.from('files')
|
||||
.list(filePath.substring(0, filePath.lastIndexOf('/')), {
|
||||
search: filePath.substring(filePath.lastIndexOf('/') + 1)
|
||||
});
|
||||
|
||||
if (checkError) {
|
||||
throw new Error(`Storage 버킷 오류: ${checkError.message}`);
|
||||
}
|
||||
|
||||
if (!fileExists || fileExists.length === 0) {
|
||||
throw new Error('파일을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 파일이 존재하면 URL 생성
|
||||
const { data } = supabase.storage
|
||||
.from('files')
|
||||
.getPublicUrl(filePath);
|
||||
|
||||
return data.publicUrl;
|
||||
} catch (error) {
|
||||
console.error('파일 URL 생성 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 파일 삭제 (Storage)
|
||||
async deleteStorageFile(filePath) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
const { error } = await supabase.storage
|
||||
.from('files')
|
||||
.remove([filePath]);
|
||||
|
||||
if (error) throw error;
|
||||
},
|
||||
|
||||
// 첨부파일 정보 추가
|
||||
async addFileAttachment(fileId, attachmentData) {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('file_attachments')
|
||||
.insert([{
|
||||
file_id: fileId,
|
||||
...attachmentData
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
// Storage 버킷 확인 및 생성
|
||||
async checkOrCreateBucket() {
|
||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||
|
||||
try {
|
||||
// 버킷 목록 확인
|
||||
const { data: buckets, error: listError } = await supabase.storage.listBuckets();
|
||||
|
||||
if (listError) {
|
||||
console.error('버킷 목록 조회 오류:', listError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 'files' 버킷이 있는지 확인
|
||||
const filesBucket = buckets.find(bucket => bucket.name === 'files');
|
||||
|
||||
if (filesBucket) {
|
||||
console.log('✅ files 버킷이 존재합니다.');
|
||||
return true;
|
||||
} else {
|
||||
console.warn('⚠️ files 버킷이 존재하지 않습니다.');
|
||||
console.log('Supabase Dashboard에서 files 버킷을 생성해주세요.');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('버킷 확인 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 전역으로 내보내기
|
||||
window.SupabaseHelper = SupabaseHelper;
|
||||
window.initializeSupabase = initializeSupabase;
|
||||
window.isSupabaseConfigured = isSupabaseConfigured;
|
||||
window.setupAuthListener = setupAuthListener;
|
||||
window.getCurrentUser = getCurrentUser;
|
||||
window.signIn = signIn;
|
||||
window.signUp = signUp;
|
||||
window.signOut = signOut;
|
@@ -1,128 +0,0 @@
|
||||
-- Supabase 데이터베이스 스키마
|
||||
-- 이 파일을 Supabase SQL 에디터에서 실행하세요
|
||||
|
||||
-- 1. files 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS public.files (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT NOT NULL,
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- 2. file_attachments 테이블 생성 (파일 첨부 정보)
|
||||
CREATE TABLE IF NOT EXISTS public.file_attachments (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
file_id UUID REFERENCES public.files(id) ON DELETE CASCADE NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
storage_path TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- 3. Row Level Security (RLS) 정책 활성화
|
||||
ALTER TABLE public.files ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.file_attachments ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 4. files 테이블 RLS 정책
|
||||
-- 사용자는 자신의 파일만 조회할 수 있음
|
||||
CREATE POLICY "Users can view their own files" ON public.files
|
||||
FOR SELECT USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자는 자신의 파일만 생성할 수 있음
|
||||
CREATE POLICY "Users can create their own files" ON public.files
|
||||
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- 사용자는 자신의 파일만 수정할 수 있음
|
||||
CREATE POLICY "Users can update their own files" ON public.files
|
||||
FOR UPDATE USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자는 자신의 파일만 삭제할 수 있음
|
||||
CREATE POLICY "Users can delete their own files" ON public.files
|
||||
FOR DELETE USING (auth.uid() = user_id);
|
||||
|
||||
-- 5. file_attachments 테이블 RLS 정책
|
||||
-- 사용자는 자신의 파일 첨부만 조회할 수 있음
|
||||
CREATE POLICY "Users can view their own file attachments" ON public.file_attachments
|
||||
FOR SELECT USING (
|
||||
auth.uid() = (
|
||||
SELECT user_id FROM public.files WHERE id = file_attachments.file_id
|
||||
)
|
||||
);
|
||||
|
||||
-- 사용자는 자신의 파일에만 첨부를 생성할 수 있음
|
||||
CREATE POLICY "Users can create attachments for their own files" ON public.file_attachments
|
||||
FOR INSERT WITH CHECK (
|
||||
auth.uid() = (
|
||||
SELECT user_id FROM public.files WHERE id = file_attachments.file_id
|
||||
)
|
||||
);
|
||||
|
||||
-- 사용자는 자신의 파일 첨부만 삭제할 수 있음
|
||||
CREATE POLICY "Users can delete their own file attachments" ON public.file_attachments
|
||||
FOR DELETE USING (
|
||||
auth.uid() = (
|
||||
SELECT user_id FROM public.files WHERE id = file_attachments.file_id
|
||||
)
|
||||
);
|
||||
|
||||
-- 6. 인덱스 생성 (성능 최적화)
|
||||
CREATE INDEX IF NOT EXISTS idx_files_user_id ON public.files(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_created_at ON public.files(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_category ON public.files(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_tags ON public.files USING GIN(tags);
|
||||
CREATE INDEX IF NOT EXISTS idx_file_attachments_file_id ON public.file_attachments(file_id);
|
||||
|
||||
-- 7. 업데이트 트리거 함수 (updated_at 자동 갱신)
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 8. updated_at 자동 갱신 트리거
|
||||
CREATE TRIGGER update_files_updated_at
|
||||
BEFORE UPDATE ON public.files
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 9. Storage 버킷 생성 (실제로는 Supabase Dashboard에서 생성)
|
||||
-- 버킷 이름: 'files'
|
||||
-- 공개 액세스: false (인증된 사용자만 접근)
|
||||
--
|
||||
-- Storage 정책은 Supabase Dashboard에서 다음과 같이 설정:
|
||||
-- SELECT: 사용자는 자신의 파일만 조회 가능
|
||||
-- INSERT: 사용자는 자신의 폴더에만 업로드 가능
|
||||
-- UPDATE: 사용자는 자신의 파일만 수정 가능
|
||||
-- DELETE: 사용자는 자신의 파일만 삭제 가능
|
||||
|
||||
-- 10. 유용한 뷰 생성 (파일과 첨부 정보 조인)
|
||||
-- 주의: 뷰는 자동으로 기본 테이블의 RLS 정책을 상속받으므로 별도 정책 설정 불필요
|
||||
CREATE OR REPLACE VIEW public.files_with_attachments AS
|
||||
SELECT
|
||||
f.*,
|
||||
COALESCE(
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', fa.id,
|
||||
'original_name', fa.original_name,
|
||||
'storage_path', fa.storage_path,
|
||||
'file_size', fa.file_size,
|
||||
'mime_type', fa.mime_type,
|
||||
'created_at', fa.created_at
|
||||
)
|
||||
) FILTER (WHERE fa.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS attachments
|
||||
FROM public.files f
|
||||
LEFT JOIN public.file_attachments fa ON f.id = fa.file_id
|
||||
GROUP BY f.id, f.title, f.description, f.category, f.tags, f.user_id, f.created_at, f.updated_at;
|
||||
|
||||
-- 설정 완료 메시지
|
||||
SELECT 'Supabase 스키마 설정이 완료되었습니다!' as message;
|
@@ -1,16 +0,0 @@
|
||||
-- 임시 공개 접근 정책 (테스트용만 사용!)
|
||||
-- 보안상 권장하지 않음 - 운영환경에서는 사용하지 마세요
|
||||
|
||||
-- 모든 사용자가 files 버킷에 업로드 가능 (임시)
|
||||
CREATE POLICY "Public upload for testing"
|
||||
ON storage.objects
|
||||
FOR INSERT
|
||||
WITH CHECK (bucket_id = 'files');
|
||||
|
||||
-- 모든 사용자가 files 버킷 파일 조회 가능 (임시)
|
||||
CREATE POLICY "Public read for testing"
|
||||
ON storage.objects
|
||||
FOR SELECT
|
||||
USING (bucket_id = 'files');
|
||||
|
||||
-- 주의: 이 정책들은 테스트 후 반드시 삭제하고 위의 사용자별 정책으로 교체하세요!
|
@@ -1,5 +0,0 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 1755826306 connect.sid s%3Alkct8oX-9zlTMoD6Mu3BP0RwCz0CFR-X.Mad5GaxzMugYjbnKEOxUq9mnbJkkJY3O79f3q%2BaE%2BJ4
|
@@ -1,141 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 시놀로지 NAS Git 저장소 생성 스크립트 (개선 버전)
|
||||
# 사용법: ./create-git-repo.sh [repo-name] [git-dir]
|
||||
|
||||
# 기본 설정
|
||||
DEFAULT_GIT_DIR="/volume1/git"
|
||||
DEFAULT_REPO_NAME="jaryo-file-manager"
|
||||
|
||||
# 매개변수 처리
|
||||
REPO_NAME="${1:-$DEFAULT_REPO_NAME}"
|
||||
GIT_DIR="${2:-$DEFAULT_GIT_DIR}"
|
||||
REPO_PATH="$GIT_DIR/$REPO_NAME.git"
|
||||
|
||||
echo "=== 시놀로지 NAS Git 저장소 생성 ==="
|
||||
echo "저장소 이름: $REPO_NAME"
|
||||
echo "Git 디렉토리: $GIT_DIR"
|
||||
echo "저장소 경로: $REPO_PATH"
|
||||
echo "=========================================="
|
||||
|
||||
# 권한 확인
|
||||
if [ "$EUID" -ne 0 ] && [ "$(whoami)" != "admin" ]; then
|
||||
echo "⚠️ 경고: 관리자 권한이 필요할 수 있습니다."
|
||||
echo "sudo 또는 admin 계정으로 실행하세요."
|
||||
fi
|
||||
|
||||
# Git 설치 확인
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "❌ Git이 설치되지 않았습니다."
|
||||
echo "패키지 센터에서 Git Server를 설치하거나 다음 명령어를 실행하세요:"
|
||||
echo "sudo apt update && sudo apt install git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Git 디렉토리 확인 및 생성
|
||||
echo "📁 Git 디렉토리 확인 중..."
|
||||
if [ ! -d "$GIT_DIR" ]; then
|
||||
echo "Git 디렉토리가 없습니다. 생성 중..."
|
||||
mkdir -p "$GIT_DIR" || {
|
||||
echo "❌ Git 디렉토리 생성 실패. 권한을 확인하세요."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 권한 설정
|
||||
if command -v chown &> /dev/null; then
|
||||
chown admin:users "$GIT_DIR" 2>/dev/null || echo "⚠️ chown 권한 부족"
|
||||
fi
|
||||
chmod 755 "$GIT_DIR" 2>/dev/null || echo "⚠️ chmod 권한 부족"
|
||||
echo "✅ Git 디렉토리 생성 완료: $GIT_DIR"
|
||||
else
|
||||
echo "✅ Git 디렉토리 존재 확인: $GIT_DIR"
|
||||
fi
|
||||
|
||||
# 기존 저장소 확인
|
||||
if [ -d "$REPO_PATH" ]; then
|
||||
echo "⚠️ 저장소가 이미 존재합니다: $REPO_PATH"
|
||||
read -p "삭제 후 재생성하시겠습니까? (y/N): " confirm
|
||||
if [[ $confirm =~ ^[Yy]$ ]]; then
|
||||
rm -rf "$REPO_PATH"
|
||||
echo "🗑️ 기존 저장소 삭제 완료"
|
||||
else
|
||||
echo "❌ 작업을 취소합니다."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 저장소 디렉토리 생성
|
||||
echo "📂 저장소 디렉토리 생성 중..."
|
||||
mkdir -p "$REPO_PATH" || {
|
||||
echo "❌ 저장소 디렉토리 생성 실패"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Git 저장소 초기화
|
||||
echo "🔧 Git 저장소 초기화 중..."
|
||||
cd "$REPO_PATH" || exit 1
|
||||
git init --bare || {
|
||||
echo "❌ Git 저장소 초기화 실패"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 권한 설정
|
||||
echo "🔐 권한 설정 중..."
|
||||
if command -v chown &> /dev/null; then
|
||||
chown -R admin:users "$REPO_PATH" 2>/dev/null || echo "⚠️ chown 권한 부족"
|
||||
fi
|
||||
chmod -R 755 "$REPO_PATH" 2>/dev/null || echo "⚠️ chmod 권한 부족"
|
||||
|
||||
# Git hooks 설정 (선택사항)
|
||||
echo "🪝 Git hooks 설정 중..."
|
||||
cat > "$REPO_PATH/hooks/post-receive" << 'EOF'
|
||||
#!/bin/bash
|
||||
# 자동 배포 hook (선택사항)
|
||||
echo "푸시 완료: $(date)"
|
||||
echo "저장소: $PWD"
|
||||
EOF
|
||||
chmod +x "$REPO_PATH/hooks/post-receive" 2>/dev/null
|
||||
|
||||
# 저장소 설명 파일 생성
|
||||
echo "📄 저장소 설명 파일 생성 중..."
|
||||
cat > "$REPO_PATH/description" << EOF
|
||||
Jaryo File Manager - 시놀로지 NAS 자료실 파일 관리 시스템
|
||||
EOF
|
||||
|
||||
# Git 서비스 확인 및 시작
|
||||
echo "🔄 Git 서비스 상태 확인 중..."
|
||||
NAS_IP=$(hostname -I | awk '{print $1}' | tr -d ' ')
|
||||
|
||||
# 다양한 방법으로 IP 확인
|
||||
if [ -z "$NAS_IP" ]; then
|
||||
NAS_IP=$(ip route get 1 | awk '{print $7; exit}' 2>/dev/null)
|
||||
fi
|
||||
if [ -z "$NAS_IP" ]; then
|
||||
NAS_IP="your-nas-ip"
|
||||
fi
|
||||
|
||||
echo "✅ Git 저장소 생성 완료!"
|
||||
echo "=========================================="
|
||||
echo "📋 저장소 정보:"
|
||||
echo " - 이름: $REPO_NAME"
|
||||
echo " - 경로: $REPO_PATH"
|
||||
echo " - 설명: 자료실 파일 관리 시스템"
|
||||
echo ""
|
||||
echo "🌐 연결 URL:"
|
||||
echo " SSH: ssh://admin@$NAS_IP$REPO_PATH"
|
||||
echo " HTTP: http://$NAS_IP:3000/git/$REPO_NAME.git"
|
||||
echo ""
|
||||
echo "🔗 로컬에서 연결하는 방법:"
|
||||
echo " git remote add nas ssh://admin@$NAS_IP$REPO_PATH"
|
||||
echo " git push nas master"
|
||||
echo ""
|
||||
echo "📝 다음 단계:"
|
||||
echo " 1. 로컬 프로젝트에서 원격 저장소 추가"
|
||||
echo " 2. 첫 번째 push 실행"
|
||||
echo " 3. Git 서비스 동작 확인"
|
||||
echo ""
|
||||
echo "🔧 Git 서비스 수동 시작 (필요시):"
|
||||
echo " sudo systemctl start git-daemon"
|
||||
echo " sudo git daemon --base-path=$GIT_DIR --export-all --reuseaddr &"
|
||||
echo ""
|
||||
echo "📖 자세한 설정은 synology-git-diagnostic.md 파일을 참조하세요."
|
@@ -4,7 +4,9 @@ const fs = require('fs');
|
||||
|
||||
class DatabaseHelper {
|
||||
constructor() {
|
||||
this.dbPath = path.join(__dirname, 'jaryo.db');
|
||||
// 프로젝트 루트의 data 디렉토리에 데이터베이스 저장
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
this.dbPath = path.join(projectRoot, 'data', 'jaryo.db');
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
@@ -16,13 +18,26 @@ class DatabaseHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
this.db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READWRITE, (err) => {
|
||||
// 데이터베이스 디렉토리 생성
|
||||
const dbDir = path.dirname(this.dbPath);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 데이터베이스 파일이 없으면 생성
|
||||
const flags = fs.existsSync(this.dbPath) ?
|
||||
sqlite3.OPEN_READWRITE :
|
||||
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE;
|
||||
|
||||
this.db = new sqlite3.Database(this.dbPath, flags, (err) => {
|
||||
if (err) {
|
||||
console.error('데이터베이스 연결 오류:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('✅ SQLite 데이터베이스 연결됨');
|
||||
resolve(this.db);
|
||||
console.log('✅ SQLite 데이터베이스 연결됨:', this.dbPath);
|
||||
this.initializeTables().then(() => {
|
||||
resolve(this.db);
|
||||
}).catch(reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -46,6 +61,78 @@ class DatabaseHelper {
|
||||
});
|
||||
}
|
||||
|
||||
// 테이블 초기화
|
||||
initializeTables() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const createTables = `
|
||||
-- 사용자 테이블
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
role TEXT DEFAULT 'user',
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME
|
||||
);
|
||||
|
||||
-- 카테고리 테이블
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 파일 테이블
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT NOT NULL,
|
||||
tags TEXT DEFAULT '[]',
|
||||
user_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- 첨부파일 테이블
|
||||
CREATE TABLE IF NOT EXISTS file_attachments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_id TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
mime_type TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 사용자 세션 테이블 (옵션)
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`;
|
||||
|
||||
this.db.exec(createTables, (err) => {
|
||||
if (err) {
|
||||
console.error('테이블 생성 오류:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('✅ 데이터베이스 테이블 초기화 완료');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 모든 파일 목록 가져오기
|
||||
async getAllFiles(limit = 100, offset = 0) {
|
||||
await this.connect();
|
||||
@@ -277,8 +364,8 @@ class DatabaseHelper {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = `
|
||||
INSERT INTO file_attachments (file_id, original_name, file_name, file_path, file_size, mime_type, file_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO file_attachments (file_id, original_name, file_name, file_path, file_size, mime_type)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const params = [
|
||||
@@ -287,8 +374,7 @@ class DatabaseHelper {
|
||||
attachmentData.file_name || attachmentData.original_name,
|
||||
attachmentData.file_path || '',
|
||||
attachmentData.file_size || 0,
|
||||
attachmentData.mime_type || '',
|
||||
attachmentData.file_data || null
|
||||
attachmentData.mime_type || ''
|
||||
];
|
||||
|
||||
this.db.run(query, params, function(err) {
|
||||
@@ -323,7 +409,7 @@ class DatabaseHelper {
|
||||
await this.connect();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = 'SELECT * FROM categories ORDER BY is_default DESC, name ASC';
|
||||
const query = 'SELECT * FROM categories ORDER BY name ASC';
|
||||
|
||||
this.db.all(query, [], (err, rows) => {
|
||||
if (err) {
|
||||
|
@@ -1,68 +0,0 @@
|
||||
const DatabaseHelper = require('./database/db-helper');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function debugFiles() {
|
||||
const db = new DatabaseHelper();
|
||||
|
||||
try {
|
||||
await db.connect();
|
||||
|
||||
console.log('\n📋 데이터베이스의 모든 파일:');
|
||||
const files = await db.getAllFiles();
|
||||
|
||||
files.forEach((file, index) => {
|
||||
console.log(`\n${index + 1}. ${file.title} (ID: ${file.id})`);
|
||||
console.log(` 카테고리: ${file.category}`);
|
||||
console.log(` 첨부파일: ${file.files?.length || 0}개`);
|
||||
|
||||
if (file.files && file.files.length > 0) {
|
||||
file.files.forEach((attachment, idx) => {
|
||||
console.log(` ${idx + 1}) ${attachment.original_name}`);
|
||||
console.log(` - ID: ${attachment.id}`);
|
||||
console.log(` - 경로: ${attachment.file_path}`);
|
||||
console.log(` - 파일명: ${attachment.file_name}`);
|
||||
console.log(` - 크기: ${attachment.file_size}`);
|
||||
|
||||
// 실제 파일 존재 확인
|
||||
const fullPath = path.join(__dirname, attachment.file_path);
|
||||
const exists = fs.existsSync(fullPath);
|
||||
console.log(` - 실제 파일 존재: ${exists ? '✅' : '❌'} (${fullPath})`);
|
||||
|
||||
if (!exists) {
|
||||
// 다른 경로들 시도
|
||||
const paths = [
|
||||
path.join(__dirname, 'uploads', attachment.file_name),
|
||||
path.join(__dirname, 'uploads', attachment.original_name),
|
||||
attachment.file_path,
|
||||
];
|
||||
|
||||
console.log(` - 시도할 경로들:`);
|
||||
paths.forEach(p => {
|
||||
const pathExists = fs.existsSync(p);
|
||||
console.log(` ${pathExists ? '✅' : '❌'} ${p}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n📁 uploads 폴더의 실제 파일들:');
|
||||
const uploadsDir = path.join(__dirname, 'uploads');
|
||||
if (fs.existsSync(uploadsDir)) {
|
||||
const actualFiles = fs.readdirSync(uploadsDir);
|
||||
actualFiles.forEach(file => {
|
||||
const filePath = path.join(uploadsDir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
console.log(` - ${file} (크기: ${stats.size})`);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 오류:', error.message);
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
}
|
||||
|
||||
debugFiles();
|
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 수동 배포 스크립트 - 각 단계를 개별 실행
|
||||
NAS_IP="119.64.1.86"
|
||||
NAS_USER="vibsin9322"
|
||||
DEPLOY_DIR="/volume1/web/jaryo"
|
||||
GITEA_URL="http://119.64.1.86:3000/vibsin9322/jaryo.git"
|
||||
|
||||
echo "=========================================="
|
||||
echo "🔧 수동 배포 가이드"
|
||||
echo "=========================================="
|
||||
echo "다음 명령들을 하나씩 실행하세요:"
|
||||
echo ""
|
||||
|
||||
echo "1️⃣ SSH 연결 테스트:"
|
||||
echo "ssh -p 2222 $NAS_USER@$NAS_IP"
|
||||
echo ""
|
||||
|
||||
echo "2️⃣ 기존 배포 백업 (있는 경우):"
|
||||
echo "ssh -p 2222 $NAS_USER@$NAS_IP 'sudo cp -r $DEPLOY_DIR ${DEPLOY_DIR}_backup_\$(date +%Y%m%d_%H%M%S) 2>/dev/null || true'"
|
||||
echo ""
|
||||
|
||||
echo "3️⃣ 배포 디렉토리 준비:"
|
||||
echo "ssh -p 2222 $NAS_USER@$NAS_IP 'sudo rm -rf $DEPLOY_DIR && sudo mkdir -p $DEPLOY_DIR && sudo chown $NAS_USER:users $DEPLOY_DIR'"
|
||||
echo ""
|
||||
|
||||
echo "4️⃣ Git 클론:"
|
||||
echo "ssh -p 2222 $NAS_USER@$NAS_IP 'cd $DEPLOY_DIR && git clone $GITEA_URL .'"
|
||||
echo ""
|
||||
|
||||
echo "5️⃣ 의존성 설치:"
|
||||
echo "ssh -p 2222 $NAS_USER@$NAS_IP 'cd $DEPLOY_DIR && npm install'"
|
||||
echo ""
|
||||
|
||||
echo "6️⃣ 데이터베이스 처리:"
|
||||
echo "ssh -p 2222 $NAS_USER@$NAS_IP 'cd $DEPLOY_DIR && if [ -f data/database.db ]; then echo \"기존 DB 유지\"; else npm run init-db; fi'"
|
||||
echo ""
|
||||
|
||||
echo "7️⃣ 서비스 시작:"
|
||||
echo "ssh -p 2222 $NAS_USER@$NAS_IP 'cd $DEPLOY_DIR && PORT=3005 nohup node server.js > logs/app.log 2>&1 & echo \$! > jaryo.pid'"
|
||||
echo ""
|
||||
|
||||
echo "8️⃣ 서비스 확인:"
|
||||
echo "curl http://$NAS_IP:3005"
|
||||
echo ""
|
||||
echo "=========================================="
|
@@ -9,13 +9,22 @@
|
||||
NAS_IP="${1:-119.64.1.86}"
|
||||
PROJECT_NAME="${2:-jaryo}"
|
||||
NAS_USER="vibsin9322"
|
||||
NAS_PASS="${3:-vibsin9322}" # 기본 비밀번호, 환경변수 NAS_PASS로 오버라이드 가능
|
||||
# NAS_PASS 우선순위: 환경변수 > 스크립트 3번째 인자 > 프롬프트 방식
|
||||
if [ -n "$3" ]; then
|
||||
NAS_PASS="$3"
|
||||
else
|
||||
NAS_PASS="${NAS_PASS:-}"
|
||||
fi
|
||||
DEPLOY_DIR="/volume1/web/$PROJECT_NAME"
|
||||
SERVICE_PORT="3005"
|
||||
GITEA_URL="http://$NAS_IP:3000/vibsin9322/jaryo.git"
|
||||
|
||||
# SSH 명령어 준비
|
||||
SSH_CMD="ssh -p 2222 -o ConnectTimeout=10 -o StrictHostKeyChecking=no $NAS_USER@$NAS_IP"
|
||||
# SSH 명령어 준비 (NAS_PASS가 있으면 plink로 비대화식, 없으면 ssh 프롬프트)
|
||||
if [ -n "$NAS_PASS" ]; then
|
||||
SSH_CMD="plink -P 2222 -batch -pw \"$NAS_PASS\" $NAS_USER@$NAS_IP"
|
||||
else
|
||||
SSH_CMD="ssh -p 2222 -o ConnectTimeout=10 -o StrictHostKeyChecking=no $NAS_USER@$NAS_IP"
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "🚀 시놀로지 NAS 자료실 배포 시작"
|
||||
@@ -31,8 +40,12 @@ echo "=========================================="
|
||||
echo "📋 1단계: 사전 요구사항 확인"
|
||||
|
||||
# SSH 방식 확인
|
||||
echo "🔧 SSH 접속 방식: 비밀번호 프롬프트 방식"
|
||||
echo "📝 SSH 연결 시 비밀번호 입력이 필요합니다."
|
||||
if [ -n "$NAS_PASS" ]; then
|
||||
echo "🔧 SSH 접속 방식: 비밀번호 비대화식(plink)"
|
||||
else
|
||||
echo "🔧 SSH 접속 방식: 비밀번호 프롬프트 방식"
|
||||
echo "📝 SSH 연결 시 비밀번호 입력이 필요합니다."
|
||||
fi
|
||||
|
||||
# SSH 연결 테스트 (포트 2222)
|
||||
echo "🔗 SSH 연결 테스트 중... (사용자: $NAS_USER, 포트: 2222)"
|
||||
@@ -135,27 +148,22 @@ if [ \$? -ne 0 ]; then
|
||||
fi
|
||||
echo '✅ 의존성 설치 완료'
|
||||
|
||||
# 데이터베이스 백업 및 초기화
|
||||
if [ -f 'scripts/init-database.js' ]; then
|
||||
# 기존 데이터베이스 백업
|
||||
DB_FILE='data/database.db'
|
||||
BACKUP_FILE='data/database_backup_$(date +%Y%m%d_%H%M%S).db'
|
||||
# 데이터베이스 초기화 (선택사항)
|
||||
if [ "\$INIT_DB" = "true" ] && [ -f 'scripts/init-database.js' ]; then
|
||||
echo '🗄️ SQLite 데이터베이스 초기화 중...'
|
||||
echo 'ℹ️ SQLite 데이터베이스: data/jaryo.db'
|
||||
|
||||
if [ -f '\$DB_FILE' ]; then
|
||||
echo '💾 기존 데이터베이스 백업 중...'
|
||||
cp '\$DB_FILE' '\$BACKUP_FILE'
|
||||
echo '✅ 백업 완료: \$BACKUP_FILE'
|
||||
|
||||
# 기존 데이터 유지 - 초기화 건너뛰기
|
||||
echo 'ℹ️ 기존 데이터베이스 발견 - 초기화 건너뛰기'
|
||||
echo '💡 새 데이터베이스가 필요하면 수동으로 실행: npm run init-db'
|
||||
export PATH='$NODE_PATH':\$PATH
|
||||
if '$NODE_PATH'/npm run init-db; then
|
||||
echo '✅ SQLite 초기화 완료'
|
||||
else
|
||||
# 새 설치 - 데이터베이스 초기화
|
||||
echo '🗄️ 새 데이터베이스 초기화 중...'
|
||||
export PATH='$NODE_PATH':\$PATH
|
||||
'$NODE_PATH'/npm run init-db
|
||||
echo '✅ 데이터베이스 초기화 완료'
|
||||
echo '❌ SQLite 초기화 실패'
|
||||
echo '💡 수동으로 초기화하려면:'
|
||||
echo ' npm run init-db'
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo 'ℹ️ 데이터베이스 초기화 건너뜀 (INIT_DB=true로 설정시 초기화)'
|
||||
fi
|
||||
"
|
||||
|
||||
@@ -201,7 +209,11 @@ fi
|
||||
# 서비스 시작
|
||||
echo '🚀 자료실 서비스 시작 중...'
|
||||
cd '\$PROJECT_DIR'
|
||||
PORT='$SERVICE_PORT' nohup \$NODE_PATH/node server.js > '\$LOG_FILE' 2>&1 &
|
||||
# NAS 환경 변수 설정 (SQLite 사용)
|
||||
export NODE_ENV=production
|
||||
export HOST=0.0.0.0
|
||||
export PORT='$SERVICE_PORT'
|
||||
nohup \$NODE_PATH/node server.js > '\$LOG_FILE' 2>&1 &
|
||||
echo \$! > '\$PID_FILE'
|
||||
|
||||
sleep 2
|
||||
@@ -266,7 +278,7 @@ chmod +x '$DEPLOY_DIR/stop-nas-service.sh'
|
||||
echo ""
|
||||
echo "🎬 5단계: 서비스 시작"
|
||||
|
||||
eval "$SSH_CMD '$DEPLOY_DIR/start-nas-service.sh"
|
||||
eval "$SSH_CMD '$DEPLOY_DIR/start-nas-service.sh'"
|
||||
|
||||
# 6단계: 접속 테스트
|
||||
echo ""
|
||||
@@ -295,5 +307,5 @@ if curl -s "http://$NAS_IP:$SERVICE_PORT" >/dev/null; then
|
||||
else
|
||||
echo "❌ 서비스 접속 실패"
|
||||
echo "로그 확인:"
|
||||
eval "$SSH_CMD 'tail -20 $DEPLOY_DIR/logs/app.log"
|
||||
eval "$SSH_CMD 'tail -20 $DEPLOY_DIR/logs/app.log'"
|
||||
fi
|
104
deploy.sh
104
deploy.sh
@@ -1,104 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Git을 통한 자동 배포 스크립트
|
||||
# 사용법: ./deploy.sh [branch_name]
|
||||
|
||||
# 설정
|
||||
PROJECT_DIR="/volume1/web/jaryo"
|
||||
GIT_REPO="/volume1/git/jaryo-file-manager.git"
|
||||
BACKUP_DIR="/volume1/web/jaryo-backup"
|
||||
LOG_FILE="/volume1/web/jaryo/logs/deploy.log"
|
||||
BRANCH=${1:-main}
|
||||
|
||||
# 로그 함수
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# 로그 디렉토리 생성
|
||||
mkdir -p "$(dirname $LOG_FILE)"
|
||||
|
||||
log "=== 배포 시작 ==="
|
||||
log "브랜치: $BRANCH"
|
||||
log "프로젝트 디렉토리: $PROJECT_DIR"
|
||||
|
||||
# 1. 현재 서비스 중지
|
||||
log "기존 서비스 중지 중..."
|
||||
if [ -f "$PROJECT_DIR/app.pid" ]; then
|
||||
PID=$(cat "$PROJECT_DIR/app.pid")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
kill "$PID"
|
||||
sleep 3
|
||||
log "서비스 중지 완료 (PID: $PID)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. 백업 생성
|
||||
log "현재 버전 백업 중..."
|
||||
BACKUP_NAME="backup-$(date +%Y%m%d-%H%M%S)"
|
||||
if [ -d "$PROJECT_DIR" ]; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -r "$PROJECT_DIR" "$BACKUP_DIR/$BACKUP_NAME"
|
||||
log "백업 완료: $BACKUP_DIR/$BACKUP_NAME"
|
||||
fi
|
||||
|
||||
# 3. Git에서 최신 코드 가져오기
|
||||
log "Git에서 최신 코드 가져오는 중..."
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
git clone "$GIT_REPO" .
|
||||
else
|
||||
cd "$PROJECT_DIR"
|
||||
# 현재 변경사항 백업
|
||||
git stash push -m "Auto backup before deploy $(date)"
|
||||
|
||||
# 원격 저장소에서 최신 정보 가져오기
|
||||
git fetch origin
|
||||
|
||||
# 지정된 브랜치로 체크아웃
|
||||
git checkout "$BRANCH"
|
||||
|
||||
# 원격 브랜치와 동기화
|
||||
git pull origin "$BRANCH"
|
||||
fi
|
||||
|
||||
# 4. 의존성 설치
|
||||
log "의존성 설치 중..."
|
||||
npm install --production
|
||||
|
||||
# 5. 데이터베이스 마이그레이션 (필요한 경우)
|
||||
log "데이터베이스 초기화 중..."
|
||||
node scripts/init-database.js
|
||||
|
||||
# 6. 권한 설정
|
||||
log "권한 설정 중..."
|
||||
chmod +x *.sh
|
||||
chown -R admin:users "$PROJECT_DIR"
|
||||
|
||||
# 7. 서비스 시작
|
||||
log "새로운 서비스 시작 중..."
|
||||
./start-service.sh
|
||||
|
||||
# 8. 서비스 상태 확인
|
||||
sleep 5
|
||||
if [ -f "$PROJECT_DIR/app.pid" ]; then
|
||||
PID=$(cat "$PROJECT_DIR/app.pid")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
log "✅ 배포 성공! 서비스가 정상적으로 시작되었습니다. (PID: $PID)"
|
||||
else
|
||||
log "❌ 배포 실패! 서비스가 시작되지 않았습니다."
|
||||
log "로그 확인: tail -f $PROJECT_DIR/logs/app.log"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log "❌ 배포 실패! PID 파일이 생성되지 않았습니다."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 9. 이전 백업 정리 (30일 이상 된 백업 삭제)
|
||||
log "오래된 백업 정리 중..."
|
||||
find "$BACKUP_DIR" -name "backup-*" -type d -mtime +30 -exec rm -rf {} \; 2>/dev/null
|
||||
|
||||
log "=== 배포 완료 ==="
|
||||
log "서비스 URL: http://$(hostname -I | awk '{print $1}'):3005"
|
@@ -1,234 +0,0 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const url = require("url");
|
||||
|
||||
const PORT = 3005;
|
||||
const DATA_FILE = path.join(__dirname, 'data.json');
|
||||
const UPLOAD_DIR = path.join(__dirname, 'uploads');
|
||||
|
||||
// 데이터 파일 초기화
|
||||
function initializeData() {
|
||||
const defaultData = {
|
||||
users: [
|
||||
{
|
||||
id: '1',
|
||||
email: 'admin@jaryo.com',
|
||||
password: 'admin123',
|
||||
name: '관리자',
|
||||
role: 'admin'
|
||||
}
|
||||
],
|
||||
files: [
|
||||
{
|
||||
id: '1',
|
||||
title: '샘플 문서',
|
||||
description: '자료실 테스트용 샘플 파일입니다.',
|
||||
category: '문서',
|
||||
tags: ['샘플', '테스트'],
|
||||
user_id: '1',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
attachments: []
|
||||
}
|
||||
],
|
||||
categories: ['문서', '이미지', '동영상', '프레젠테이션', '기타']
|
||||
};
|
||||
|
||||
if (!fs.existsSync(DATA_FILE)) {
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(defaultData, null, 2));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(UPLOAD_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터 읽기/쓰기
|
||||
function readData() {
|
||||
try {
|
||||
const data = fs.readFileSync(DATA_FILE, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error('데이터 읽기 오류:', error);
|
||||
return { users: [], files: [], categories: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function writeData(data) {
|
||||
try {
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('데이터 쓰기 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// MIME 타입
|
||||
const mimeTypes = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".css": "text/css",
|
||||
".js": "application/javascript",
|
||||
".json": "application/json",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg"
|
||||
};
|
||||
|
||||
// API 요청 처리
|
||||
async function handleApiRequest(req, res, pathname, query) {
|
||||
const data = readData();
|
||||
|
||||
if (pathname === "/api/files/public" && req.method === "GET") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: data.files,
|
||||
total: data.files.length
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/api/files" && req.method === "GET") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: data.files,
|
||||
total: data.files.length
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/api/auth/login" && req.method === "POST") {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { email, password } = JSON.parse(body);
|
||||
const user = data.users.find(u => u.email === email && u.password === password);
|
||||
|
||||
if (user) {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(401, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: '이메일 또는 비밀번호가 올바르지 않습니다.'
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
res.writeHead(400, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: '잘못된 요청입니다.'
|
||||
}));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 기본 응답
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: "자료실 API 서버 실행 중",
|
||||
timestamp: new Date().toISOString(),
|
||||
path: pathname
|
||||
}));
|
||||
}
|
||||
|
||||
// 정적 파일 서빙
|
||||
async function serveStaticFile(req, res, pathname) {
|
||||
const filePath = path.join(__dirname, pathname);
|
||||
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>404 Not Found</title></head>
|
||||
<body>
|
||||
<h1>404 - 파일을 찾을 수 없습니다</h1>
|
||||
<p>요청한 파일: ${pathname}</p>
|
||||
<p><a href="/">홈으로 돌아가기</a></p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath);
|
||||
const contentType = mimeTypes[ext] || "text/plain";
|
||||
|
||||
res.writeHead(200, { "Content-Type": contentType });
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
// HTTP 서버
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
let pathname = parsedUrl.pathname;
|
||||
const query = parsedUrl.query;
|
||||
|
||||
console.log(`📨 ${req.method} ${pathname}`);
|
||||
|
||||
if (pathname.startsWith("/api/")) {
|
||||
try {
|
||||
await handleApiRequest(req, res, pathname, query);
|
||||
} catch (error) {
|
||||
console.error('API 처리 오류:', error);
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: '서버 내부 오류가 발생했습니다.'
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/" || pathname === "/index.html") {
|
||||
pathname = "/index.html";
|
||||
} else if (pathname === "/admin" || pathname === "/admin/") {
|
||||
pathname = "/admin/index.html";
|
||||
}
|
||||
|
||||
await serveStaticFile(req, res, pathname);
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🚀 향상된 자료실 서버가 포트 ${PORT}에서 실행 중입니다`);
|
||||
console.log(`📍 접속 URL: http://119.64.1.86:${PORT}`);
|
||||
console.log(`🔧 관리자 URL: http://119.64.1.86:${PORT}/admin`);
|
||||
console.log(`⏰ 시작 시간: ${new Date().toLocaleString("ko-KR")}`);
|
||||
|
||||
initializeData();
|
||||
console.log(`✅ 데이터 파일 초기화 완료`);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\n🛑 서버를 종료합니다...");
|
||||
server.close(() => {
|
||||
console.log("✅ 서버가 정상적으로 종료되었습니다");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
@@ -3,8 +3,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>자료실 - 파일 보기</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="styles.css?v=20250821194500">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
66
install-auto-startup.bat
Normal file
66
install-auto-startup.bat
Normal file
@@ -0,0 +1,66 @@
|
||||
@echo off
|
||||
REM Windows 작업 스케줄러를 사용한 자동 시작 설정 스크립트
|
||||
REM 관리자 권한으로 실행 필요
|
||||
|
||||
echo === Jaryo File Manager 자동 시작 설정 ===
|
||||
echo.
|
||||
|
||||
REM 관리자 권한 확인
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 오류: 이 스크립트는 관리자 권한으로 실행해야 합니다.
|
||||
echo 마우스 우클릭 후 "관리자 권한으로 실행"을 선택해주세요.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set PROJECT_DIR=C:\Users\COMTREE\claude_code\jaryo
|
||||
set TASK_NAME=JaryoFileManagerAutoStart
|
||||
|
||||
echo 프로젝트 디렉토리: %PROJECT_DIR%
|
||||
echo 작업 이름: %TASK_NAME%
|
||||
echo.
|
||||
|
||||
REM 기존 작업이 있으면 삭제
|
||||
schtasks /query /tn "%TASK_NAME%" >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo 기존 자동 시작 작업을 제거합니다...
|
||||
schtasks /delete /tn "%TASK_NAME%" /f
|
||||
)
|
||||
|
||||
REM 새로운 자동 시작 작업 생성
|
||||
echo 새로운 자동 시작 작업을 생성합니다...
|
||||
|
||||
REM 시스템 시작 시 실행되는 작업 생성
|
||||
schtasks /create /tn "%TASK_NAME%" ^
|
||||
/tr "\"%PROJECT_DIR%\start-jaryo-service.bat\"" ^
|
||||
/sc onstart ^
|
||||
/ru "SYSTEM" ^
|
||||
/rl highest ^
|
||||
/f
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo.
|
||||
echo ✅ 자동 시작 작업이 성공적으로 생성되었습니다!
|
||||
echo.
|
||||
echo === 설정 정보 ===
|
||||
echo 작업 이름: %TASK_NAME%
|
||||
echo 실행 파일: %PROJECT_DIR%\start-jaryo-service.bat
|
||||
echo 트리거: 시스템 시작 시
|
||||
echo 실행 계정: SYSTEM
|
||||
echo.
|
||||
echo === 관리 명령어 ===
|
||||
echo 작업 확인: schtasks /query /tn "%TASK_NAME%"
|
||||
echo 수동 실행: schtasks /run /tn "%TASK_NAME%"
|
||||
echo 작업 삭제: schtasks /delete /tn "%TASK_NAME%" /f
|
||||
echo.
|
||||
echo 이제 컴퓨터를 재시작하면 Jaryo File Manager가 자동으로 시작됩니다.
|
||||
echo 서비스 URL: http://99.1.110.50:3005
|
||||
) else (
|
||||
echo.
|
||||
echo ❌ 자동 시작 작업 생성에 실패했습니다.
|
||||
echo 관리자 권한으로 실행했는지 확인해주세요.
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
53
install-task.bat
Normal file
53
install-task.bat
Normal file
@@ -0,0 +1,53 @@
|
||||
@echo off
|
||||
REM 작업 스케줄러에 XML 파일로 작업 등록
|
||||
echo === Jaryo File Manager 자동 시작 설정 ===
|
||||
echo.
|
||||
|
||||
REM 관리자 권한 확인
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo This script requires administrator privileges.
|
||||
echo Right-click and select "Run as administrator"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set TASK_NAME=JaryoFileManagerAutoStart
|
||||
set XML_FILE=%~dp0JaryoAutoStart.xml
|
||||
|
||||
echo Task Name: %TASK_NAME%
|
||||
echo XML File: %XML_FILE%
|
||||
echo.
|
||||
|
||||
REM 기존 작업 삭제 (있는 경우)
|
||||
schtasks /query /tn "%TASK_NAME%" >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo Removing existing task...
|
||||
schtasks /delete /tn "%TASK_NAME%" /f
|
||||
)
|
||||
|
||||
REM XML 파일로 작업 생성
|
||||
echo Creating new auto-start task...
|
||||
schtasks /create /tn "%TASK_NAME%" /xml "%XML_FILE%"
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo.
|
||||
echo SUCCESS: Auto-start task created successfully!
|
||||
echo.
|
||||
echo Task Name: %TASK_NAME%
|
||||
echo Service URL: http://99.1.110.50:3005
|
||||
echo.
|
||||
echo The service will start automatically on system boot.
|
||||
) else (
|
||||
echo.
|
||||
echo ERROR: Failed to create auto-start task.
|
||||
echo Please run this script as administrator.
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Management commands:
|
||||
echo Check task: schtasks /query /tn "%TASK_NAME%"
|
||||
echo Run task: schtasks /run /tn "%TASK_NAME%"
|
||||
echo Delete task: schtasks /delete /tn "%TASK_NAME%" /f
|
||||
echo.
|
||||
pause
|
@@ -1,328 +0,0 @@
|
||||
# 시놀로지 NAS 수동 배포 가이드
|
||||
|
||||
## 🚀 시놀로지 NAS에서 자료실 서비스 배포하기
|
||||
|
||||
### 사전 준비사항 ✅
|
||||
|
||||
1. **DSM 패키지 설치 확인**
|
||||
- Node.js v16+ (패키지 센터에서 설치)
|
||||
- Git Server (패키지 센터에서 설치)
|
||||
- SSH 서비스 활성화 (제어판 > 터미널 및 SNMP)
|
||||
|
||||
2. **방화벽 설정**
|
||||
- SSH 포트: 2222 허용
|
||||
- 서비스 포트: 3005 허용
|
||||
- Gitea 포트: 3000 허용
|
||||
|
||||
### 1단계: SSH 접속 🔗
|
||||
|
||||
```bash
|
||||
# Windows에서 NAS 접속
|
||||
ssh -p 2222 admin@119.64.1.86
|
||||
|
||||
# 접속 후 관리자 권한 확인
|
||||
sudo whoami
|
||||
```
|
||||
|
||||
### 2단계: 배포 디렉토리 준비 📁
|
||||
|
||||
```bash
|
||||
# 웹 디렉토리로 이동
|
||||
cd /volume1/web
|
||||
|
||||
# 기존 jaryo 폴더가 있다면 백업
|
||||
if [ -d "jaryo" ]; then
|
||||
sudo mv jaryo jaryo_backup_$(date +%Y%m%d_%H%M%S)
|
||||
fi
|
||||
|
||||
# 새 디렉토리 생성
|
||||
sudo mkdir -p jaryo
|
||||
sudo chown admin:users jaryo
|
||||
cd jaryo
|
||||
```
|
||||
|
||||
### 3단계: 소스 코드 클론 📥
|
||||
|
||||
```bash
|
||||
# Gitea에서 소스 코드 클론
|
||||
git clone http://119.64.1.86:3000/vibsin9322/jaryo.git .
|
||||
|
||||
# 클론 성공 확인
|
||||
ls -la
|
||||
```
|
||||
|
||||
### 4단계: 의존성 설치 📦
|
||||
|
||||
```bash
|
||||
# 기존 node_modules 제거 (있다면)
|
||||
rm -rf node_modules package-lock.json
|
||||
|
||||
# npm 의존성 설치
|
||||
npm install
|
||||
|
||||
# 설치 확인
|
||||
ls -la node_modules
|
||||
```
|
||||
|
||||
### 5단계: 데이터베이스 초기화 🗄️
|
||||
|
||||
```bash
|
||||
# SQLite 데이터베이스 초기화
|
||||
npm run init-db
|
||||
|
||||
# 데이터베이스 파일 확인
|
||||
ls -la *.db
|
||||
```
|
||||
|
||||
### 6단계: 서비스 시작 스크립트 생성 📝
|
||||
|
||||
```bash
|
||||
# 시작 스크립트 생성
|
||||
cat > start-jaryo.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# 자료실 서비스 시작 스크립트
|
||||
PROJECT_DIR="/volume1/web/jaryo"
|
||||
SERVICE_PORT="3005"
|
||||
PID_FILE="$PROJECT_DIR/jaryo.pid"
|
||||
LOG_FILE="$PROJECT_DIR/jaryo.log"
|
||||
|
||||
# 로그 디렉토리 생성
|
||||
mkdir -p "$PROJECT_DIR/logs"
|
||||
|
||||
# 기존 프로세스 확인 및 종료
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
OLD_PID=$(cat "$PID_FILE")
|
||||
if ps -p "$OLD_PID" > /dev/null; then
|
||||
echo "기존 서비스 종료 중... (PID: $OLD_PID)"
|
||||
kill "$OLD_PID"
|
||||
sleep 2
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
# 포트 사용 확인
|
||||
if netstat -tulpn | grep ":$SERVICE_PORT " > /dev/null; then
|
||||
echo "⚠️ 포트 $SERVICE_PORT가 이미 사용 중입니다."
|
||||
echo "사용 중인 프로세스:"
|
||||
netstat -tulpn | grep ":$SERVICE_PORT "
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 서비스 시작
|
||||
echo "🚀 자료실 서비스 시작 중..."
|
||||
cd "$PROJECT_DIR"
|
||||
PORT="$SERVICE_PORT" nohup node server.js > "$LOG_FILE" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
|
||||
sleep 2
|
||||
|
||||
# 시작 확인
|
||||
if ps -p $(cat "$PID_FILE") > /dev/null; then
|
||||
echo "✅ 자료실 서비스 시작 완료!"
|
||||
echo "📍 접속 URL: http://119.64.1.86:$SERVICE_PORT"
|
||||
echo "📋 PID: $(cat "$PID_FILE")"
|
||||
echo "📄 로그: $LOG_FILE"
|
||||
else
|
||||
echo "❌ 서비스 시작 실패"
|
||||
echo "로그 확인:"
|
||||
tail -20 "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
|
||||
# 실행 권한 부여
|
||||
chmod +x start-jaryo.sh
|
||||
```
|
||||
|
||||
### 7단계: 중지 스크립트 생성 🛑
|
||||
|
||||
```bash
|
||||
# 중지 스크립트 생성
|
||||
cat > stop-jaryo.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# 자료실 서비스 중지 스크립트
|
||||
PROJECT_DIR="/volume1/web/jaryo"
|
||||
PID_FILE="$PROJECT_DIR/jaryo.pid"
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null; then
|
||||
echo "🛑 자료실 서비스 중지 중... (PID: $PID)"
|
||||
kill "$PID"
|
||||
sleep 2
|
||||
|
||||
# 강제 종료 확인
|
||||
if ps -p "$PID" > /dev/null; then
|
||||
echo "⚡ 강제 종료 중..."
|
||||
kill -9 "$PID"
|
||||
fi
|
||||
|
||||
rm -f "$PID_FILE"
|
||||
echo "✅ 자료실 서비스 중지 완료"
|
||||
else
|
||||
echo "⚠️ 프로세스가 이미 종료됨"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ PID 파일이 없습니다. 수동으로 프로세스를 확인하세요."
|
||||
echo "실행 중인 Node.js 프로세스:"
|
||||
ps aux | grep 'node.*server.js' | grep -v grep
|
||||
fi
|
||||
EOF
|
||||
|
||||
# 실행 권한 부여
|
||||
chmod +x stop-jaryo.sh
|
||||
```
|
||||
|
||||
### 8단계: 서비스 시작 🎬
|
||||
|
||||
```bash
|
||||
# 서비스 시작
|
||||
./start-jaryo.sh
|
||||
|
||||
# 로그 확인 (별도 터미널에서)
|
||||
tail -f jaryo.log
|
||||
```
|
||||
|
||||
### 9단계: 접속 테스트 🧪
|
||||
|
||||
**브라우저에서 접속:**
|
||||
- **메인 페이지**: `http://119.64.1.86:3005`
|
||||
- **관리자 페이지**: `http://119.64.1.86:3005/admin`
|
||||
|
||||
**명령줄에서 테스트:**
|
||||
```bash
|
||||
# 서비스 상태 확인
|
||||
curl -s http://119.64.1.86:3005
|
||||
|
||||
# 프로세스 확인
|
||||
ps aux | grep 'node.*server.js' | grep -v grep
|
||||
|
||||
# 포트 확인
|
||||
netstat -tulpn | grep :3005
|
||||
```
|
||||
|
||||
## 🔧 서비스 관리
|
||||
|
||||
### 일상적인 관리 명령어
|
||||
|
||||
```bash
|
||||
# 서비스 시작
|
||||
/volume1/web/jaryo/start-jaryo.sh
|
||||
|
||||
# 서비스 중지
|
||||
/volume1/web/jaryo/stop-jaryo.sh
|
||||
|
||||
# 서비스 재시작
|
||||
/volume1/web/jaryo/stop-jaryo.sh && /volume1/web/jaryo/start-jaryo.sh
|
||||
|
||||
# 로그 확인
|
||||
tail -f /volume1/web/jaryo/jaryo.log
|
||||
|
||||
# 실시간 로그 보기
|
||||
ssh -p 2222 admin@119.64.1.86 'tail -f /volume1/web/jaryo/jaryo.log'
|
||||
```
|
||||
|
||||
### 상태 모니터링
|
||||
|
||||
```bash
|
||||
# 서비스 상태 스크립트
|
||||
cat > status-jaryo.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_DIR="/volume1/web/jaryo"
|
||||
PID_FILE="$PROJECT_DIR/jaryo.pid"
|
||||
LOG_FILE="$PROJECT_DIR/jaryo.log"
|
||||
|
||||
echo "=== 자료실 서비스 상태 ==="
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null; then
|
||||
echo "✅ 서비스 실행 중 (PID: $PID)"
|
||||
echo "📊 메모리 사용량:"
|
||||
ps -o pid,ppid,cmd,%mem,%cpu -p "$PID"
|
||||
echo ""
|
||||
echo "🌐 포트 상태:"
|
||||
netstat -tulpn | grep :3005
|
||||
else
|
||||
echo "❌ 서비스 중지됨 (PID 파일 존재하지만 프로세스 없음)"
|
||||
fi
|
||||
else
|
||||
echo "❌ 서비스 중지됨 (PID 파일 없음)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📄 최근 로그 (최근 5줄):"
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
tail -5 "$LOG_FILE"
|
||||
else
|
||||
echo "로그 파일이 없습니다."
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x status-jaryo.sh
|
||||
```
|
||||
|
||||
## 🔄 자동 시작 설정 (선택사항)
|
||||
|
||||
### DSM 작업 스케줄러 사용
|
||||
|
||||
1. **DSM 로그인** → **제어판** → **작업 스케줄러**
|
||||
2. **생성** → **예약된 작업** → **사용자 정의 스크립트**
|
||||
3. 설정:
|
||||
- **작업 이름**: Jaryo 자료실 자동 시작
|
||||
- **사용자**: root
|
||||
- **스케줄**: 시스템 부팅 시
|
||||
- **스크립트**: `/volume1/web/jaryo/start-jaryo.sh`
|
||||
|
||||
## 🚨 문제 해결
|
||||
|
||||
### 일반적인 문제들
|
||||
|
||||
**1. 포트 충돌**
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
netstat -tulpn | grep :3005
|
||||
|
||||
# 다른 포트 사용시 (예: 3006)
|
||||
PORT=3006 node server.js
|
||||
```
|
||||
|
||||
**2. 권한 문제**
|
||||
```bash
|
||||
# 디렉토리 권한 수정
|
||||
sudo chown -R admin:users /volume1/web/jaryo
|
||||
chmod -R 755 /volume1/web/jaryo
|
||||
```
|
||||
|
||||
**3. Node.js 버전 문제**
|
||||
```bash
|
||||
# Node.js 버전 확인
|
||||
node --version
|
||||
|
||||
# 최소 요구 버전: v16.0.0
|
||||
```
|
||||
|
||||
**4. 데이터베이스 문제**
|
||||
```bash
|
||||
# 데이터베이스 재초기화
|
||||
rm -f jaryo.db
|
||||
npm run init-db
|
||||
```
|
||||
|
||||
## 📱 최종 확인
|
||||
|
||||
배포 완료 후 다음 URL들이 정상 작동하는지 확인:
|
||||
|
||||
- ✅ **메인 페이지**: http://119.64.1.86:3005
|
||||
- ✅ **관리자 페이지**: http://119.64.1.86:3005/admin
|
||||
- ✅ **API 상태**: http://119.64.1.86:3005/api/files
|
||||
- ✅ **Gitea 저장소**: http://119.64.1.86:3000/vibsin9322/jaryo
|
||||
|
||||
## 🎉 배포 완료!
|
||||
|
||||
시놀로지 NAS에서 자료실 서비스가 성공적으로 실행되고 있습니다.
|
||||
서비스 관리는 위의 스크립트들을 사용하여 쉽게 할 수 있습니다.
|
@@ -1,257 +0,0 @@
|
||||
# 시놀로지 NAS Git 서버 연결 테스트 가이드
|
||||
|
||||
## 🧪 연결 테스트 단계별 가이드
|
||||
|
||||
### 1단계: 기본 연결 테스트
|
||||
|
||||
#### 1.1 SSH 연결 확인
|
||||
```bash
|
||||
# NAS SSH 연결 테스트
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# 성공시 NAS 터미널에 접속됨
|
||||
# 실패시 확인사항:
|
||||
# - SSH 서비스 활성화 여부
|
||||
# - 방화벽 설정
|
||||
# - IP 주소 정확성
|
||||
```
|
||||
|
||||
#### 1.2 Git 설치 확인
|
||||
```bash
|
||||
# NAS에서 Git 명령어 확인
|
||||
which git
|
||||
git --version
|
||||
|
||||
# Git 서비스 상태 확인
|
||||
sudo systemctl status git-daemon
|
||||
ps aux | grep git
|
||||
```
|
||||
|
||||
### 2단계: 저장소 생성 및 설정
|
||||
|
||||
#### 2.1 자동 스크립트 실행
|
||||
```bash
|
||||
# 로컬에서 NAS로 스크립트 복사
|
||||
scp create-git-repo.sh admin@your-nas-ip:/tmp/
|
||||
|
||||
# NAS에서 스크립트 실행
|
||||
ssh admin@your-nas-ip
|
||||
cd /tmp
|
||||
chmod +x create-git-repo.sh
|
||||
./create-git-repo.sh jaryo-file-manager
|
||||
```
|
||||
|
||||
#### 2.2 수동 저장소 생성 (스크립트 실패시)
|
||||
```bash
|
||||
# NAS에서 직접 실행
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# Git 디렉토리 생성
|
||||
sudo mkdir -p /volume1/git
|
||||
sudo chown admin:users /volume1/git
|
||||
cd /volume1/git
|
||||
|
||||
# Bare 저장소 생성
|
||||
mkdir jaryo-file-manager.git
|
||||
cd jaryo-file-manager.git
|
||||
git init --bare
|
||||
sudo chown -R admin:users .
|
||||
```
|
||||
|
||||
### 3단계: 로컬에서 연결 테스트
|
||||
|
||||
#### 3.1 기존 프로젝트에 원격 저장소 추가
|
||||
```bash
|
||||
# 현재 jaryo 프로젝트 디렉토리에서
|
||||
cd /c/Users/COMTREE/claude_code/jaryo
|
||||
|
||||
# NAS Git 원격 저장소 추가
|
||||
git remote add nas ssh://admin@your-nas-ip/volume1/git/jaryo-file-manager.git
|
||||
|
||||
# 원격 저장소 확인
|
||||
git remote -v
|
||||
```
|
||||
|
||||
#### 3.2 첫 번째 Push 테스트
|
||||
```bash
|
||||
# 모든 변경사항 커밋 (필요시)
|
||||
git add .
|
||||
git commit -m "Initial commit for NAS deployment"
|
||||
|
||||
# NAS로 푸시
|
||||
git push nas master
|
||||
|
||||
# 성공시 출력 예시:
|
||||
# Enumerating objects: X, done.
|
||||
# Counting objects: 100% (X/X), done.
|
||||
# Delta compression using up to Y threads
|
||||
# Compressing objects: 100% (X/X), done.
|
||||
# Writing objects: 100% (X/X), X.XX KiB | X.XX MiB/s, done.
|
||||
# Total X (delta X), reused X (delta X), pack-reused 0
|
||||
# To ssh://admin@your-nas-ip/volume1/git/jaryo-file-manager.git
|
||||
# * [new branch] master -> master
|
||||
```
|
||||
|
||||
### 4단계: 클론 테스트
|
||||
|
||||
#### 4.1 다른 디렉토리에서 클론 테스트
|
||||
```bash
|
||||
# 테스트용 디렉토리 생성
|
||||
mkdir /tmp/git-test
|
||||
cd /tmp/git-test
|
||||
|
||||
# NAS에서 클론
|
||||
git clone ssh://admin@your-nas-ip/volume1/git/jaryo-file-manager.git
|
||||
|
||||
# 성공시 프로젝트 파일들이 다운로드됨
|
||||
cd jaryo-file-manager
|
||||
ls -la
|
||||
```
|
||||
|
||||
#### 4.2 HTTP 클론 테스트 (Git HTTP 서버 실행시)
|
||||
```bash
|
||||
# Git HTTP 서버가 실행 중인 경우
|
||||
git clone http://your-nas-ip:3000/jaryo-file-manager.git
|
||||
```
|
||||
|
||||
### 5단계: 웹 인터페이스 테스트
|
||||
|
||||
#### 5.1 GitWeb 접속 테스트
|
||||
- 브라우저에서 `http://your-nas-ip/git` 접속
|
||||
- 또는 `http://your-nas-ip:3000` 접속
|
||||
- 저장소 목록에서 `jaryo-file-manager` 확인
|
||||
|
||||
#### 5.2 Git HTTP 서버 상태 확인
|
||||
```bash
|
||||
# NAS에서 Git HTTP 서버 실행 확인
|
||||
sudo netstat -tulpn | grep :3000
|
||||
ps aux | grep git-daemon
|
||||
```
|
||||
|
||||
## 🚨 문제 해결
|
||||
|
||||
### 연결 실패 시 체크리스트
|
||||
|
||||
#### ❌ "Connection refused" 오류
|
||||
```bash
|
||||
# 1. SSH 서비스 확인
|
||||
ssh -v admin@your-nas-ip
|
||||
|
||||
# 2. 포트 확인 (기본: 22)
|
||||
ssh -p 22 admin@your-nas-ip
|
||||
|
||||
# 3. 방화벽 확인
|
||||
# DSM > 제어판 > 보안 > 방화벽
|
||||
```
|
||||
|
||||
#### ❌ "Permission denied" 오류
|
||||
```bash
|
||||
# 1. 사용자 권한 확인
|
||||
# DSM > 제어판 > 사용자 및 그룹 > admin > 애플리케이션
|
||||
|
||||
# 2. SSH 키 설정 (선택사항)
|
||||
ssh-keygen -t rsa
|
||||
ssh-copy-id admin@your-nas-ip
|
||||
```
|
||||
|
||||
#### ❌ "Repository not found" 오류
|
||||
```bash
|
||||
# 1. 저장소 경로 확인
|
||||
ssh admin@your-nas-ip
|
||||
ls -la /volume1/git/
|
||||
ls -la /volume1/git/jaryo-file-manager.git/
|
||||
|
||||
# 2. 권한 확인
|
||||
sudo chown -R admin:users /volume1/git/jaryo-file-manager.git
|
||||
chmod -R 755 /volume1/git/jaryo-file-manager.git
|
||||
```
|
||||
|
||||
#### ❌ Git 명령어 없음
|
||||
```bash
|
||||
# Git 설치 확인
|
||||
which git
|
||||
|
||||
# 패키지 센터에서 Git Server 설치
|
||||
# 또는 수동 설치:
|
||||
sudo apt update
|
||||
sudo apt install git
|
||||
```
|
||||
|
||||
### 네트워크 설정 문제
|
||||
|
||||
#### 내부 네트워크 접속 실패
|
||||
```bash
|
||||
# IP 주소 확인
|
||||
ping your-nas-ip
|
||||
nslookup your-nas-ip
|
||||
|
||||
# 포트 스캔
|
||||
nmap -p 22,3000 your-nas-ip
|
||||
```
|
||||
|
||||
#### 외부 네트워크 접속 설정
|
||||
```bash
|
||||
# 라우터 포트 포워딩 설정
|
||||
# 22 (SSH) -> NAS_IP:22
|
||||
# 3000 (Git HTTP) -> NAS_IP:3000
|
||||
|
||||
# 동적 DNS 설정 (선택사항)
|
||||
# your-domain.dyndns.org -> your-public-ip
|
||||
```
|
||||
|
||||
## 📊 연결 성공 확인
|
||||
|
||||
### ✅ 성공 지표들
|
||||
|
||||
1. **SSH 연결**: `ssh admin@your-nas-ip` 성공
|
||||
2. **저장소 존재**: `/volume1/git/jaryo-file-manager.git` 확인
|
||||
3. **Push 성공**: `git push nas master` 완료
|
||||
4. **Clone 성공**: 다른 위치에서 클론 가능
|
||||
5. **웹 접속**: 브라우저에서 Git 저장소 확인
|
||||
|
||||
### 📈 성능 테스트
|
||||
|
||||
```bash
|
||||
# 대용량 파일 Push 테스트
|
||||
dd if=/dev/zero of=test-large-file.bin bs=1M count=10
|
||||
git add test-large-file.bin
|
||||
git commit -m "Large file test"
|
||||
time git push nas master
|
||||
|
||||
# 클론 속도 테스트
|
||||
time git clone ssh://admin@your-nas-ip/volume1/git/jaryo-file-manager.git test-clone
|
||||
```
|
||||
|
||||
## 🔧 고급 설정
|
||||
|
||||
### Git Hooks 활용
|
||||
```bash
|
||||
# NAS에서 post-receive hook 설정
|
||||
ssh admin@your-nas-ip
|
||||
cd /volume1/git/jaryo-file-manager.git/hooks
|
||||
|
||||
# 자동 배포 hook
|
||||
cat > post-receive << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "코드 푸시 완료: $(date)"
|
||||
# 자동 배포 로직 추가 가능
|
||||
# cd /volume1/web/jaryo && git pull
|
||||
EOF
|
||||
|
||||
chmod +x post-receive
|
||||
```
|
||||
|
||||
### 백업 설정
|
||||
```bash
|
||||
# 저장소 백업 스크립트
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/volume1/backup/git"
|
||||
REPO_DIR="/volume1/git"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
tar -czf "$BACKUP_DIR/git-repos-$DATE.tar.gz" -C "$REPO_DIR" .
|
||||
echo "백업 완료: $BACKUP_DIR/git-repos-$DATE.tar.gz"
|
||||
```
|
||||
|
||||
이 가이드를 따라하면 시놀로지 NAS Git 서버와의 연결을 성공적으로 테스트하고 설정할 수 있습니다.
|
372
package-lock.json
generated
372
package-lock.json
generated
@@ -1,26 +1,24 @@
|
||||
{
|
||||
"name": "jaryo-file-manager",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jaryo-file-manager",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"fs": "^0.0.1-security",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"path": "^0.12.7",
|
||||
"sqlite3": "^5.1.6",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@gar/promisify": {
|
||||
@@ -251,20 +249,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
@@ -359,19 +343,6 @@
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
@@ -440,19 +411,6 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
@@ -575,31 +533,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
@@ -1001,19 +934,6 @@
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
@@ -1050,12 +970,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs": {
|
||||
"version": "0.0.1-security",
|
||||
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
|
||||
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
@@ -1080,21 +994,6 @@
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -1189,19 +1088,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -1221,16 +1107,6 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -1402,13 +1278,6 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
@@ -1478,29 +1347,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -1510,19 +1356,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-lambda": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
|
||||
@@ -1530,16 +1363,6 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -1936,60 +1759,6 @@
|
||||
"node": ">= 10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
||||
"integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^4",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^7.5.3",
|
||||
"simple-update-notifier": "^2.0.0",
|
||||
"supports-color": "^5.5.0",
|
||||
"touch": "^3.1.0",
|
||||
"undefsafe": "^2.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"nodemon": "bin/nodemon.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nodemon"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
@@ -2005,16 +1774,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
|
||||
@@ -2108,16 +1867,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path": {
|
||||
"version": "0.12.7",
|
||||
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"process": "^0.11.1",
|
||||
"util": "^0.10.3"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -2133,19 +1882,6 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
@@ -2172,15 +1908,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -2221,13 +1948,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
@@ -2322,19 +2042,6 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
@@ -2588,19 +2295,6 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
@@ -2771,19 +2465,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
@@ -2870,19 +2551,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -2892,16 +2560,6 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/touch": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@@ -2951,13 +2609,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unique-filename": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
|
||||
@@ -2987,27 +2638,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util/node_modules/inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
28
package.json
28
package.json
@@ -1,20 +1,30 @@
|
||||
{
|
||||
"name": "jaryo-file-manager",
|
||||
"version": "2.0.0",
|
||||
"description": "자료실 파일 관리 시스템 - Vercel Serverless",
|
||||
"description": "자료실 파일 관리 시스템",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"dev": "vercel dev",
|
||||
"build": "echo 'Build complete'",
|
||||
"start": "vercel dev"
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js",
|
||||
"init-db": "node scripts/init-database.js",
|
||||
"build": "echo 'Build complete'"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"vercel": "^32.0.0"
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"keywords": ["file-manager", "vercel", "serverless", "admin"],
|
||||
"keywords": [
|
||||
"file-manager",
|
||||
"admin"
|
||||
],
|
||||
"author": "Claude Code",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +0,0 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'jaryo-file-manager',
|
||||
script: 'server.js',
|
||||
cwd: '/volume1/web/jaryo',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3005
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3005
|
||||
},
|
||||
log_file: '/volume1/web/jaryo/logs/combined.log',
|
||||
out_file: '/volume1/web/jaryo/logs/out.log',
|
||||
error_file: '/volume1/web/jaryo/logs/error.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
||||
merge_logs: true,
|
||||
time: true
|
||||
}]
|
||||
};
|
51
pm2-start.sh
51
pm2-start.sh
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# PM2를 사용한 시놀로지 NAS 서비스 시작 스크립트
|
||||
# 사용법: ./pm2-start.sh
|
||||
|
||||
PROJECT_DIR="/volume1/web/jaryo"
|
||||
LOG_DIR="/volume1/web/jaryo/logs"
|
||||
|
||||
echo "=== PM2로 Jaryo File Manager 서비스 시작 ==="
|
||||
|
||||
# 로그 디렉토리 생성
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# 프로젝트 디렉토리로 이동
|
||||
cd "$PROJECT_DIR" || {
|
||||
echo "오류: 프로젝트 디렉토리를 찾을 수 없습니다: $PROJECT_DIR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# PM2 설치 확인 및 설치
|
||||
if ! command -v pm2 &> /dev/null; then
|
||||
echo "PM2 설치 중..."
|
||||
npm install -g pm2
|
||||
fi
|
||||
|
||||
# 의존성 설치 확인
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "의존성 설치 중..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# 데이터베이스 초기화
|
||||
echo "데이터베이스 초기화 중..."
|
||||
node scripts/init-database.js
|
||||
|
||||
# 기존 PM2 프로세스 중지
|
||||
echo "기존 프로세스 정리 중..."
|
||||
pm2 delete jaryo-file-manager 2>/dev/null || true
|
||||
|
||||
# PM2로 서비스 시작
|
||||
echo "PM2로 서비스 시작 중..."
|
||||
pm2 start pm2-ecosystem.config.js --env production
|
||||
|
||||
# PM2 시작 스크립트 생성
|
||||
pm2 startup
|
||||
pm2 save
|
||||
|
||||
echo "서비스가 PM2로 시작되었습니다."
|
||||
echo "상태 확인: pm2 status"
|
||||
echo "로그 확인: pm2 logs jaryo-file-manager"
|
||||
echo "서비스 중지: pm2 stop jaryo-file-manager"
|
12
render.yaml
12
render.yaml
@@ -1,12 +0,0 @@
|
||||
services:
|
||||
- type: web
|
||||
name: jaryo-file-manager
|
||||
env: node
|
||||
plan: free
|
||||
buildCommand: npm install
|
||||
startCommand: npm start
|
||||
envVars:
|
||||
- key: NODE_ENV
|
||||
value: production
|
||||
- key: PORT
|
||||
value: 10000
|
64
reset-admin.js
Normal file
64
reset-admin.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const DatabaseHelper = require('./database/db-helper');
|
||||
|
||||
async function resetAdminPassword() {
|
||||
const dbHelper = new DatabaseHelper();
|
||||
|
||||
try {
|
||||
console.log('🔄 관리자 비밀번호 초기화 시작...');
|
||||
|
||||
const password = 'Hee150603!';
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
console.log('🔐 해시된 비밀번호:', hashedPassword);
|
||||
|
||||
// 관리자 사용자 확인
|
||||
const existingUser = await dbHelper.getUserByEmail('admin@jaryo.com');
|
||||
|
||||
if (existingUser) {
|
||||
// 기존 사용자 비밀번호 업데이트 (SQLite 용)
|
||||
await dbHelper.connect();
|
||||
const query = 'UPDATE users SET password_hash = ? WHERE email = ?';
|
||||
dbHelper.db.run(query, [hashedPassword, 'admin@jaryo.com'], function(err) {
|
||||
if (err) {
|
||||
console.error('비밀번호 업데이트 실패:', err);
|
||||
} else {
|
||||
console.log('✅ 기존 관리자 비밀번호가 업데이트되었습니다.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 새 관리자 사용자 생성
|
||||
const adminData = {
|
||||
email: 'admin@jaryo.com',
|
||||
password_hash: hashedPassword,
|
||||
name: '관리자',
|
||||
role: 'admin'
|
||||
};
|
||||
|
||||
const result = await dbHelper.createUser(adminData);
|
||||
console.log('✅ 새 관리자 사용자가 생성되었습니다.');
|
||||
}
|
||||
|
||||
// 로그인 테스트
|
||||
const user = await dbHelper.getUserByEmail('admin@jaryo.com');
|
||||
const isValid = await bcrypt.compare(password, user.password_hash);
|
||||
|
||||
console.log('🧪 로그인 테스트 결과:', isValid ? '성공' : '실패');
|
||||
|
||||
if (isValid) {
|
||||
console.log('🎉 관리자 계정 설정 완료!');
|
||||
console.log('📧 이메일: admin@jaryo.com');
|
||||
console.log('🔑 비밀번호: Hee150603!');
|
||||
}
|
||||
|
||||
await dbHelper.close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 오류 발생:', error);
|
||||
await dbHelper.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
resetAdminPassword();
|
67
script.js
67
script.js
@@ -343,9 +343,6 @@ class PublicFileViewer {
|
||||
|
||||
async downloadSingleFile(fileId, attachmentIndex) {
|
||||
try {
|
||||
// 다운로드 시작 로딩 표시
|
||||
this.showLoading(true);
|
||||
|
||||
console.log('downloadSingleFile 호출됨:', fileId, attachmentIndex);
|
||||
const file = this.files.find(f => f.id === fileId);
|
||||
console.log('찾은 파일:', file);
|
||||
@@ -359,70 +356,28 @@ class PublicFileViewer {
|
||||
const downloadUrl = `/api/download/${fileId}/${attachmentId}`;
|
||||
console.log('다운로드 URL:', downloadUrl);
|
||||
|
||||
const response = await fetch(downloadUrl, {
|
||||
credentials: 'include'
|
||||
});
|
||||
console.log('응답 상태:', response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.log('응답 오류:', errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
console.log('다운로드 시작...');
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// 파일명을 서버에서 전송된 정보에서 추출 (개선된 방식)
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`;
|
||||
|
||||
console.log('📁 다운로드 파일명 처리:', {
|
||||
original_name: file.files[attachmentIndex].original_name,
|
||||
content_disposition: contentDisposition,
|
||||
default_filename: filename
|
||||
});
|
||||
|
||||
if (contentDisposition) {
|
||||
// RFC 5987 filename* 파라미터를 우선 처리 (UTF-8 지원)
|
||||
const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
|
||||
if (filenameStarMatch) {
|
||||
filename = decodeURIComponent(filenameStarMatch[1]);
|
||||
console.log('📁 UTF-8 파일명 추출:', filename);
|
||||
} else {
|
||||
// 일반 filename 파라미터 처리
|
||||
const filenameMatch = contentDisposition.match(/filename="?([^";\r\n]+)"?/);
|
||||
if (filenameMatch) {
|
||||
filename = filenameMatch[1];
|
||||
console.log('📁 기본 파일명 추출:', filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 파일명이 여전히 비어있다면 기본값 사용
|
||||
if (!filename || filename.trim() === '') {
|
||||
filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`;
|
||||
console.log('📁 기본 파일명 사용:', filename);
|
||||
}
|
||||
|
||||
// 대용량 파일을 위해 직접 링크로 다운로드 (blob 사용하지 않음)
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.href = downloadUrl;
|
||||
link.target = '_blank'; // 새 탭에서 열어 다운로드
|
||||
|
||||
// 파일명 설정 (서버에서 Content-Disposition 헤더로 처리됨)
|
||||
const filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`;
|
||||
link.download = filename;
|
||||
|
||||
// 숨겨진 링크 생성 후 클릭
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
console.log('다운로드 완료');
|
||||
this.showLoading(false);
|
||||
console.log('📁 대용량 파일 다운로드 시작:', filename);
|
||||
|
||||
if (arguments.length === 2) { // 단일 파일 다운로드인 경우만 알림 표시
|
||||
this.showNotification(`파일 다운로드 완료: ${filename}`, 'success');
|
||||
this.showNotification(`파일 다운로드 시작: ${filename}`, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('downloadSingleFile 오류:', error);
|
||||
this.showLoading(false);
|
||||
this.showNotification('파일 다운로드 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
@@ -1,73 +1,79 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const bcrypt = require('bcrypt');
|
||||
const DatabaseHelper = require('../database/db-helper');
|
||||
|
||||
// 데이터베이스 파일 경로
|
||||
const dbPath = path.join(__dirname, '../database/jaryo.db');
|
||||
const schemaPath = path.join(__dirname, '../database/schema.sql');
|
||||
|
||||
// database 폴더가 없으면 생성
|
||||
const dbDir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
// uploads 폴더도 생성
|
||||
const uploadsDir = path.join(__dirname, '../uploads');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log('🔧 SQLite 데이터베이스 초기화 시작...');
|
||||
|
||||
// 데이터베이스 연결
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('❌ 데이터베이스 연결 오류:', err.message);
|
||||
return;
|
||||
}
|
||||
console.log('✅ SQLite 데이터베이스 연결 성공');
|
||||
});
|
||||
|
||||
// 스키마 파일 읽기 및 실행
|
||||
fs.readFile(schemaPath, 'utf8', (err, schema) => {
|
||||
if (err) {
|
||||
console.error('❌ 스키마 파일 읽기 오류:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 여러 SQL 문을 분리하여 실행
|
||||
const statements = schema.split(';').filter(stmt => stmt.trim().length > 0);
|
||||
async function initializeDatabase() {
|
||||
console.log('🗄️ 데이터베이스 초기화 시작...');
|
||||
|
||||
db.serialize(() => {
|
||||
statements.forEach((statement, index) => {
|
||||
if (statement.trim()) {
|
||||
db.run(statement + ';', (err) => {
|
||||
if (err) {
|
||||
console.error(`❌ SQL 실행 오류 (${index + 1}):`, err.message);
|
||||
console.error('실행하려던 SQL:', statement);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
const dbHelper = new DatabaseHelper();
|
||||
|
||||
try {
|
||||
// 데이터 디렉토리 생성
|
||||
const dataDir = path.join(__dirname, '..', 'data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
console.log('📁 데이터 디렉토리 생성됨:', dataDir);
|
||||
}
|
||||
|
||||
console.log('✅ 데이터베이스 스키마 생성 완료');
|
||||
// 데이터베이스 연결 및 테이블 생성
|
||||
await dbHelper.connect();
|
||||
console.log('✅ 데이터베이스 연결 성공');
|
||||
|
||||
// 데이터 확인
|
||||
db.all('SELECT COUNT(*) as count FROM files', (err, rows) => {
|
||||
if (err) {
|
||||
console.error('❌ 데이터 확인 오류:', err.message);
|
||||
} else {
|
||||
console.log(`📊 파일 테이블 레코드 수: ${rows[0].count}`);
|
||||
}
|
||||
// 기본 관리자 계정 생성
|
||||
const adminEmail = 'admin@jaryo.com';
|
||||
const adminPassword = 'Hee150603!';
|
||||
|
||||
const existingUser = await dbHelper.getUserByEmail(adminEmail);
|
||||
|
||||
if (!existingUser) {
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(adminPassword, saltRounds);
|
||||
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('❌ 데이터베이스 종료 오류:', err.message);
|
||||
const adminData = {
|
||||
email: adminEmail,
|
||||
password_hash: hashedPassword,
|
||||
name: '관리자',
|
||||
role: 'admin'
|
||||
};
|
||||
|
||||
await dbHelper.createUser(adminData);
|
||||
console.log('👤 기본 관리자 계정 생성됨');
|
||||
console.log('📧 이메일:', adminEmail);
|
||||
console.log('🔑 비밀번호:', adminPassword);
|
||||
} else {
|
||||
console.log('👤 관리자 계정이 이미 존재합니다.');
|
||||
}
|
||||
|
||||
// 기본 카테고리 생성
|
||||
const defaultCategories = ['문서', '이미지', '동영상', '프레젠테이션', '기타'];
|
||||
|
||||
for (const categoryName of defaultCategories) {
|
||||
try {
|
||||
await dbHelper.addCategory(categoryName);
|
||||
console.log(`📂 카테고리 생성됨: ${categoryName}`);
|
||||
} catch (error) {
|
||||
if (error.message.includes('UNIQUE constraint failed')) {
|
||||
console.log(`📂 카테고리 이미 존재: ${categoryName}`);
|
||||
} else {
|
||||
console.log('🏁 데이터베이스 초기화 완료');
|
||||
console.error(`카테고리 생성 오류 (${categoryName}):`, error.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 데이터베이스 초기화 완료!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 데이터베이스 초기화 실패:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await dbHelper.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 스크립트 직접 실행 시에만 초기화 실행
|
||||
if (require.main === module) {
|
||||
initializeDatabase().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = initializeDatabase;
|
119
server.js
119
server.js
@@ -6,7 +6,9 @@ const fs = require('fs');
|
||||
const bcrypt = require('bcrypt');
|
||||
const session = require('express-session');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
// 모든 환경에서 SQLite 사용
|
||||
const DatabaseHelper = require('./database/db-helper');
|
||||
console.log('🗄️ SQLite 데이터베이스 사용');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3005;
|
||||
@@ -26,9 +28,9 @@ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
app.use(session({
|
||||
secret: 'jaryo-file-manager-secret-key-2024',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
saveUninitialized: true, // 세션 초기화 허용
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production', // Vercel에서는 HTTPS
|
||||
secure: false, // 개발 환경에서도 HTTP로 작동하도록 수정
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24시간
|
||||
}
|
||||
@@ -44,6 +46,14 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// CSS 파일에 대한 캐시 무효화 헤더 설정
|
||||
app.get('*.css', (req, res, next) => {
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Expires', '0');
|
||||
next();
|
||||
});
|
||||
|
||||
// 정적 파일 서빙
|
||||
app.use(express.static(__dirname));
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
@@ -691,12 +701,12 @@ app.get('/api/stats', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 파일 다운로드
|
||||
// 파일 다운로드 (SQLite 호환)
|
||||
app.get('/api/download/:id/:attachmentId', async (req, res) => {
|
||||
try {
|
||||
const { id, attachmentId } = req.params;
|
||||
|
||||
// 첨부파일 정보 조회 (간단한 쿼리로 대체)
|
||||
// SQLite에서 첨부파일 정보 조회
|
||||
await db.connect();
|
||||
const query = 'SELECT * FROM file_attachments WHERE id = ? AND file_id = ?';
|
||||
|
||||
@@ -720,29 +730,67 @@ app.get('/api/download/:id/:attachmentId', async (req, res) => {
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
// 한글 파일명을 위한 개선된 헤더 설정
|
||||
console.log('📁 다운로드 파일 정보:', {
|
||||
original_name: row.original_name,
|
||||
file_path: row.file_path,
|
||||
storage_path: filePath
|
||||
});
|
||||
console.log('📁 다운로드 파일 정보:', {
|
||||
original_name: row.original_name,
|
||||
file_path: row.file_path,
|
||||
storage_path: filePath
|
||||
});
|
||||
|
||||
const originalName = row.original_name || 'download';
|
||||
const encodedName = encodeURIComponent(originalName);
|
||||
|
||||
// RFC 5987을 준수하는 헤더 설정 (한글 파일명 지원)
|
||||
const stat = fs.statSync(filePath);
|
||||
const fileSize = stat.size;
|
||||
|
||||
// Range 요청 처리
|
||||
const range = req.headers.range;
|
||||
let start = 0;
|
||||
let end = fileSize - 1;
|
||||
let statusCode = 200;
|
||||
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-");
|
||||
start = parseInt(parts[0], 10);
|
||||
end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||
statusCode = 206; // Partial Content
|
||||
|
||||
const originalName = row.original_name || 'download';
|
||||
const encodedName = encodeURIComponent(originalName);
|
||||
|
||||
// RFC 5987을 준수하는 헤더 설정 (한글 파일명 지원)
|
||||
res.setHeader('Content-Disposition',
|
||||
`attachment; filename*=UTF-8''${encodedName}`);
|
||||
res.setHeader('Content-Type', row.mime_type || 'application/octet-stream');
|
||||
res.setHeader('Content-Length', row.file_size || fs.statSync(filePath).size);
|
||||
|
||||
// 원본 파일명으로 다운로드
|
||||
res.download(filePath, originalName, (err) => {
|
||||
if (err) {
|
||||
console.error('📁 다운로드 오류:', err);
|
||||
} else {
|
||||
console.log('📁 다운로드 완료:', originalName);
|
||||
}
|
||||
});
|
||||
res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
|
||||
res.setHeader('Content-Length', (end - start + 1));
|
||||
} else {
|
||||
res.setHeader('Content-Length', fileSize);
|
||||
}
|
||||
|
||||
res.status(statusCode);
|
||||
res.setHeader('Content-Disposition',
|
||||
`attachment; filename*=UTF-8''${encodedName}`);
|
||||
res.setHeader('Content-Type', row.mime_type || 'application/octet-stream');
|
||||
res.setHeader('Accept-Ranges', 'bytes');
|
||||
res.setHeader('Cache-Control', 'public, max-age=0');
|
||||
|
||||
// 클라이언트 연결 끊김 감지
|
||||
res.on('close', () => {
|
||||
if (!res.headersSent) {
|
||||
console.log('📁 다운로드 취소됨:', originalName);
|
||||
}
|
||||
});
|
||||
|
||||
// 스트림 기반 다운로드로 대용량 파일 지원 (Range 요청 지원)
|
||||
const readStream = fs.createReadStream(filePath, { start, end });
|
||||
|
||||
readStream.on('error', (err) => {
|
||||
console.error('📁 파일 읽기 오류:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: '파일 읽기 실패' });
|
||||
}
|
||||
});
|
||||
|
||||
readStream.on('end', () => {
|
||||
console.log('📁 다운로드 완료:', originalName);
|
||||
});
|
||||
|
||||
// 스트림을 응답에 연결
|
||||
readStream.pipe(res);
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
@@ -781,12 +829,23 @@ module.exports = app;
|
||||
|
||||
// 로컬 개발 환경에서만 서버 시작
|
||||
if (process.env.NODE_ENV !== 'production' || process.env.VERCEL !== '1') {
|
||||
app.listen(PORT, () => {
|
||||
const HOST = process.env.HOST || '0.0.0.0'; // NAS 호환성을 위해 모든 인터페이스에서 수신
|
||||
const server = app.listen(PORT, HOST, () => {
|
||||
const serverAddress = server.address();
|
||||
const host = serverAddress.address === '::' ? 'localhost' :
|
||||
serverAddress.address === '0.0.0.0' ? 'localhost' :
|
||||
serverAddress.address;
|
||||
|
||||
console.log(`🚀 자료실 서버가 포트 ${PORT}에서 실행중입니다.`);
|
||||
console.log(`📱 Admin 페이지: http://localhost:${PORT}/admin/index.html`);
|
||||
console.log(`🌐 Main 페이지: http://localhost:${PORT}/index.html`);
|
||||
console.log(`📊 API: http://localhost:${PORT}/api/files`);
|
||||
console.log(`📍 서버 주소: ${HOST}:${PORT}`);
|
||||
console.log(`📱 Admin 페이지: http://${host}:${PORT}/admin/index.html`);
|
||||
console.log(`🌐 Main 페이지: http://${host}:${PORT}/index.html`);
|
||||
console.log(`📊 API: http://${host}:${PORT}/api/files`);
|
||||
console.log(`🔧 NAS 접속: http://[NAS-IP]:${PORT}`);
|
||||
});
|
||||
|
||||
// 대용량 파일 다운로드를 위해 서버 타임아웃을 30분으로 설정
|
||||
server.timeout = 1800000; // 30분 (30 * 60 * 1000ms)
|
||||
|
||||
// 프로세스 종료 시 데이터베이스 연결 종료
|
||||
process.on('SIGINT', async () => {
|
||||
|
110
simple.html
110
simple.html
@@ -1,110 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Jaryo File Manager - 테스트</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
.status {
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #4caf50;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.button {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
}
|
||||
.button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: #f0f8ff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 Jaryo File Manager</h1>
|
||||
|
||||
<div class="status">
|
||||
<h3>✅ 정적 페이지 테스트</h3>
|
||||
<p>이 페이지가 보인다면 Vercel 배포가 성공한 것입니다!</p>
|
||||
<p><strong>시간:</strong> <span id="currentTime"></span></p>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>🔧 테스트 버튼들</h3>
|
||||
<a href="/index.html" class="button">메인 페이지</a>
|
||||
<a href="/api/test" class="button">API 테스트</a>
|
||||
<a href="/api/files" class="button">파일 API</a>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>📡 AJAX 테스트</h3>
|
||||
<button onclick="testAPI()" class="button">API 연결 테스트</button>
|
||||
<div id="apiResult" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 현재 시간 표시
|
||||
document.getElementById('currentTime').textContent = new Date().toLocaleString('ko-KR');
|
||||
|
||||
// API 테스트 함수
|
||||
async function testAPI() {
|
||||
const resultDiv = document.getElementById('apiResult');
|
||||
resultDiv.innerHTML = '🔄 API 테스트 중...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/test');
|
||||
const data = await response.json();
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div style="background: #e8f5e8; padding: 10px; border-radius: 5px;">
|
||||
<strong>✅ API 연결 성공!</strong><br>
|
||||
메시지: ${data.message}<br>
|
||||
시간: ${data.timestamp}
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `
|
||||
<div style="background: #ffe8e8; padding: 10px; border-radius: 5px;">
|
||||
<strong>❌ API 연결 실패</strong><br>
|
||||
오류: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# SSH 연결 헬퍼 스크립트
|
||||
NAS_IP="${1:-119.64.1.86}"
|
||||
NAS_USER="${2:-vibsin9322}"
|
||||
COMMAND="${3:-echo 'SSH 연결 성공'}"
|
||||
|
||||
# 비밀번호를 입력받아 SSH 연결
|
||||
echo "🔑 SSH 연결 시도: $NAS_USER@$NAS_IP:2222"
|
||||
echo "📝 비밀번호를 입력하세요:"
|
||||
|
||||
ssh -p 2222 -o StrictHostKeyChecking=no "$NAS_USER@$NAS_IP" "$COMMAND"
|
84
start-jaryo-service.bat
Normal file
84
start-jaryo-service.bat
Normal file
@@ -0,0 +1,84 @@
|
||||
@echo off
|
||||
REM Windows용 Jaryo File Manager 자동 시작 스크립트
|
||||
REM 사용법: start-jaryo-service.bat
|
||||
|
||||
echo === Jaryo File Manager 서비스 시작 ===
|
||||
echo 시작 시간: %date% %time%
|
||||
|
||||
REM 프로젝트 디렉토리 설정
|
||||
set PROJECT_DIR=C:\Users\COMTREE\claude_code\jaryo
|
||||
set LOG_FILE=%PROJECT_DIR%\logs\app.log
|
||||
set PID_FILE=%PROJECT_DIR%\app.pid
|
||||
|
||||
echo 프로젝트 디렉토리: %PROJECT_DIR%
|
||||
echo 로그 파일: %LOG_FILE%
|
||||
|
||||
REM 로그 디렉토리 생성
|
||||
if not exist "%PROJECT_DIR%\logs" mkdir "%PROJECT_DIR%\logs"
|
||||
|
||||
REM 프로젝트 디렉토리로 이동
|
||||
cd /d "%PROJECT_DIR%" || (
|
||||
echo 오류: 프로젝트 디렉토리를 찾을 수 없습니다: %PROJECT_DIR%
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Node.js와 npm 경로 확인
|
||||
where node >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 오류: Node.js가 설치되지 않았거나 PATH에 없습니다.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 의존성 설치 확인
|
||||
if not exist "node_modules" (
|
||||
echo 의존성 설치 중...
|
||||
npm install
|
||||
)
|
||||
|
||||
REM 기존 프로세스 종료 (PID 파일이 있으면)
|
||||
if exist "%PID_FILE%" (
|
||||
echo 기존 프로세스 종료 중...
|
||||
call stop-jaryo-service.bat
|
||||
timeout /t 3 /nobreak >nul
|
||||
)
|
||||
|
||||
REM 서비스 시작 (백그라운드에서 실행)
|
||||
echo 서비스 시작 중...
|
||||
start "" /min cmd /c "node server.js >> "%LOG_FILE%" 2>&1"
|
||||
|
||||
REM 프로세스 ID 저장을 위해 잠시 대기
|
||||
timeout /t 2 /nobreak >nul
|
||||
|
||||
REM 실행 중인 프로세스 확인
|
||||
for /f "tokens=2" %%i in ('tasklist /fi "imagename eq node.exe" /fo csv /nh ^| findstr server.js') do (
|
||||
echo %%i > "%PID_FILE%"
|
||||
echo 서비스가 시작되었습니다. PID: %%i
|
||||
goto :found
|
||||
)
|
||||
|
||||
REM PID를 찾지 못한 경우 (다른 방법으로 확인)
|
||||
wmic process where "name='node.exe' and commandline like '%%server.js%%'" get processid /value 2>nul | findstr "ProcessId" > temp_pid.txt
|
||||
if exist temp_pid.txt (
|
||||
for /f "tokens=2 delims==" %%i in (temp_pid.txt) do (
|
||||
echo %%i > "%PID_FILE%"
|
||||
echo 서비스가 시작되었습니다. PID: %%i
|
||||
del temp_pid.txt
|
||||
goto :found
|
||||
)
|
||||
del temp_pid.txt
|
||||
)
|
||||
|
||||
echo 프로세스 ID를 확인할 수 없습니다. 수동으로 확인해주세요.
|
||||
|
||||
:found
|
||||
echo.
|
||||
echo === 서비스 정보 ===
|
||||
echo 관리자 페이지: http://99.1.110.50:3005/admin/index.html
|
||||
echo 메인 페이지: http://99.1.110.50:3005/index.html
|
||||
echo API: http://99.1.110.50:3005/api/files
|
||||
echo 로그 확인: type "%LOG_FILE%"
|
||||
echo 서비스 중지: stop-jaryo-service.bat
|
||||
echo.
|
||||
echo 서비스가 성공적으로 시작되었습니다.
|
@@ -40,9 +40,17 @@ if [ ! -d "node_modules" ]; then
|
||||
$NPM_PATH install
|
||||
fi
|
||||
|
||||
# 데이터베이스 초기화
|
||||
echo "데이터베이스 초기화 중..."
|
||||
$NODE_PATH scripts/init-database.js
|
||||
# SQLite 데이터베이스 초기화 (선택적)
|
||||
if [ "$INIT_DB" = "true" ] && [ -f "scripts/init-database.js" ]; then
|
||||
echo "SQLite 데이터베이스 초기화 중..."
|
||||
if $NPM_PATH run init-db; then
|
||||
echo "✅ SQLite 초기화 완료"
|
||||
else
|
||||
echo "⚠️ SQLite 초기화 실패"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ 데이터베이스 초기화 건너뜀 (INIT_DB=true로 설정시 초기화)"
|
||||
fi
|
||||
|
||||
# 기존 프로세스 종료
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
@@ -57,6 +65,11 @@ fi
|
||||
|
||||
# 서비스 시작
|
||||
echo "서비스 시작 중..."
|
||||
# NAS 환경 변수 설정 (SQLite 사용)
|
||||
export NODE_ENV=production
|
||||
export HOST=0.0.0.0
|
||||
export PORT=3005
|
||||
|
||||
nohup $NODE_PATH server.js > "$LOG_FILE" 2>&1 &
|
||||
NEW_PID=$!
|
||||
|
||||
|
65
stop-jaryo-service.bat
Normal file
65
stop-jaryo-service.bat
Normal file
@@ -0,0 +1,65 @@
|
||||
@echo off
|
||||
REM Windows용 Jaryo File Manager 서비스 중지 스크립트
|
||||
REM 사용법: stop-jaryo-service.bat
|
||||
|
||||
echo === Jaryo File Manager 서비스 중지 ===
|
||||
echo 중지 시간: %date% %time%
|
||||
|
||||
set PROJECT_DIR=C:\Users\COMTREE\claude_code\jaryo
|
||||
set PID_FILE=%PROJECT_DIR%\app.pid
|
||||
|
||||
REM PID 파일이 있는 경우
|
||||
if exist "%PID_FILE%" (
|
||||
for /f %%i in (%PID_FILE%) do (
|
||||
echo 프로세스 ID: %%i
|
||||
echo 서비스 중지 중...
|
||||
|
||||
REM 프로세스 종료
|
||||
taskkill /pid %%i /f >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo 서비스가 중지되었습니다.
|
||||
) else (
|
||||
echo 프로세스가 이미 종료되었거나 종료할 수 없습니다.
|
||||
)
|
||||
)
|
||||
|
||||
REM PID 파일 삭제
|
||||
del "%PID_FILE%"
|
||||
) else (
|
||||
echo PID 파일을 찾을 수 없습니다. Node.js 프로세스를 직접 확인합니다.
|
||||
)
|
||||
|
||||
REM 실행 중인 모든 관련 Node.js 프로세스 확인 및 종료
|
||||
echo.
|
||||
echo 실행 중인 Jaryo 관련 Node.js 프로세스를 확인합니다...
|
||||
|
||||
REM server.js를 실행하는 모든 node.exe 프로세스 종료
|
||||
for /f "tokens=2" %%i in ('tasklist /fi "imagename eq node.exe" /fo table /nh 2^>nul ^| findstr /i "node.exe"') do (
|
||||
wmic process where "processid=%%i and commandline like '%%server.js%%'" get commandline /value 2>nul | findstr "server.js" >nul
|
||||
if not errorlevel 1 (
|
||||
echo Jaryo 서비스 프로세스 발견 - PID: %%i
|
||||
taskkill /pid %%i /f >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
echo 프로세스 %%i가 종료되었습니다.
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM cmd 프로세스 중에서 node server.js를 실행하는 것도 확인
|
||||
for /f "tokens=2" %%i in ('tasklist /fi "imagename eq cmd.exe" /fo table /nh 2^>nul ^| findstr /i "cmd.exe"') do (
|
||||
wmic process where "processid=%%i and commandline like '%%server.js%%'" get commandline /value 2>nul | findstr "server.js" >nul
|
||||
if not errorlevel 1 (
|
||||
echo Jaryo 관련 CMD 프로세스 발견 - PID: %%i
|
||||
taskkill /pid %%i /f >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
echo CMD 프로세스 %%i가 종료되었습니다.
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo === 현재 실행 중인 Node.js 프로세스 ===
|
||||
tasklist /fi "imagename eq node.exe" 2>nul | findstr "node.exe" || echo Node.js 프로세스가 실행되지 않음
|
||||
|
||||
echo.
|
||||
echo 서비스 중지가 완료되었습니다.
|
488
styles.css
488
styles.css
@@ -16,6 +16,8 @@ body {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -29,7 +31,7 @@ header {
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #667eea;
|
||||
color: #667eea !important;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -559,6 +561,8 @@ header p {
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.board-table {
|
||||
@@ -618,8 +622,8 @@ header p {
|
||||
|
||||
/* 제목 스타일 */
|
||||
.board-title {
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
color: #667eea;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
@@ -1468,4 +1472,482 @@ header p {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 테이블 반응형 스타일 */
|
||||
@media (max-width: 1024px) {
|
||||
.board-container {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.board-table {
|
||||
min-width: 650px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.col-title {
|
||||
min-width: 150px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.col-attachment {
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
padding: 20px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.board-container {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -10px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.board-table {
|
||||
min-width: 580px;
|
||||
font-size: 0.8rem;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.board-table th,
|
||||
.board-table td {
|
||||
padding: 6px 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.col-no { width: 35px; min-width: 35px; }
|
||||
.col-category { width: 60px; min-width: 60px; }
|
||||
.col-title { min-width: 120px; max-width: 160px; }
|
||||
.col-attachment { width: 140px; min-width: 140px; }
|
||||
.col-date { width: 80px; min-width: 80px; }
|
||||
.col-actions { width: 80px; min-width: 80px; }
|
||||
|
||||
.board-title {
|
||||
font-size: 0.85rem;
|
||||
padding: 2px 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
max-height: 50px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.attachment-item-public {
|
||||
padding: 2px 4px;
|
||||
font-size: 0.7rem;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.download-single-btn {
|
||||
padding: 2px 5px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 3px 6px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 8px 5px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
padding: 15px 5px;
|
||||
margin: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.board-container {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -5px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
border: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
width: calc(100vw - 10px);
|
||||
max-width: calc(100vw - 10px);
|
||||
}
|
||||
|
||||
.board-table {
|
||||
min-width: 480px;
|
||||
font-size: 0.7rem;
|
||||
border-radius: 4px;
|
||||
width: max-content;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.board-table th,
|
||||
.board-table td {
|
||||
padding: 4px 2px;
|
||||
font-size: 0.7rem;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.col-no { width: 25px; min-width: 25px; max-width: 25px; }
|
||||
.col-category { width: 40px; min-width: 40px; max-width: 40px; }
|
||||
.col-title { width: 120px; min-width: 120px; max-width: 120px; text-align: left; }
|
||||
.col-attachment { width: 100px; min-width: 100px; max-width: 100px; }
|
||||
.col-date { width: 60px; min-width: 60px; max-width: 60px; }
|
||||
.col-actions { width: 50px; min-width: 50px; max-width: 50px; }
|
||||
|
||||
.board-title {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
max-height: 40px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.attachment-item-public {
|
||||
padding: 1px 3px;
|
||||
font-size: 0.65rem;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.download-single-btn {
|
||||
padding: 1px 4px;
|
||||
font-size: 0.65rem;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 2px 4px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 페이지네이션 반응형 */
|
||||
.pagination {
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 0 -10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 6px 10px;
|
||||
min-width: 50px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#pageInfo {
|
||||
font-size: 0.9rem;
|
||||
order: -1;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
/* 스크롤 힌트 추가 */
|
||||
.board-container::after {
|
||||
content: "← 좌우로 스와이프하세요 →";
|
||||
position: sticky;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.7rem;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* 320px 이하 극소형 화면 대응 */
|
||||
@media (max-width: 320px) {
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2px 0;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
padding: 8px 2px;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.board-container {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -2px;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
border: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
width: calc(100vw - 4px);
|
||||
max-width: calc(100vw - 4px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.board-table {
|
||||
min-width: 350px;
|
||||
font-size: 0.6rem;
|
||||
border-radius: 2px;
|
||||
width: 350px;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.board-table th,
|
||||
.board-table td {
|
||||
padding: 2px 1px;
|
||||
font-size: 0.6rem;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.1;
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.col-no { width: 18px; min-width: 18px; max-width: 18px; }
|
||||
.col-category { width: 30px; min-width: 30px; max-width: 30px; }
|
||||
.col-title { width: 90px; min-width: 90px; max-width: 90px; text-align: left; }
|
||||
.col-attachment { width: 70px; min-width: 70px; max-width: 70px; }
|
||||
.col-date { width: 45px; min-width: 45px; max-width: 45px; }
|
||||
.col-actions { width: 35px; min-width: 35px; max-width: 35px; }
|
||||
|
||||
.board-title {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.1;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
font-size: 0.6rem;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
max-height: 30px;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.attachment-item-public {
|
||||
padding: 1px 2px;
|
||||
font-size: 0.6rem;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.download-single-btn {
|
||||
padding: 1px 3px;
|
||||
font-size: 0.6rem;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 1px 3px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
gap: 5px;
|
||||
padding: 8px;
|
||||
margin: 0 -2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 4px 8px;
|
||||
min-width: 40px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
#pageInfo {
|
||||
font-size: 0.8rem;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.board-container::after {
|
||||
content: "← 스와이프 →";
|
||||
font-size: 0.6rem;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 280px 이하 초극소형 화면 대응 */
|
||||
@media (max-width: 280px) {
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1px 0;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
padding: 5px 1px;
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.board-container {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -1px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
width: calc(100vw - 2px);
|
||||
max-width: calc(100vw - 2px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.board-table {
|
||||
min-width: 320px;
|
||||
font-size: 0.55rem;
|
||||
border-radius: 2px;
|
||||
width: 320px;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.board-table th,
|
||||
.board-table td {
|
||||
padding: 1px 0;
|
||||
font-size: 0.55rem;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.0;
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.col-no { width: 15px; min-width: 15px; max-width: 15px; }
|
||||
.col-category { width: 25px; min-width: 25px; max-width: 25px; }
|
||||
.col-title { width: 80px; min-width: 80px; max-width: 80px; text-align: left; }
|
||||
.col-attachment { width: 60px; min-width: 60px; max-width: 60px; }
|
||||
.col-date { width: 40px; min-width: 40px; max-width: 40px; }
|
||||
.col-actions { width: 30px; min-width: 30px; max-width: 30px; }
|
||||
|
||||
.board-title {
|
||||
font-size: 0.6rem;
|
||||
line-height: 1.0;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
font-size: 0.5rem;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
max-height: 25px;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.attachment-item-public {
|
||||
padding: 0 1px;
|
||||
font-size: 0.5rem;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.download-single-btn {
|
||||
padding: 1px 2px;
|
||||
font-size: 0.5rem;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 1px 2px;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.board-container::after {
|
||||
content: "← →";
|
||||
font-size: 0.5rem;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
}
|
@@ -1,203 +0,0 @@
|
||||
# 시놀로지 NAS Git Server 진단 및 해결 가이드
|
||||
|
||||
## 🔍 1단계: Git Server 패키지 상태 확인
|
||||
|
||||
### 1.1 DSM 패키지 센터 점검
|
||||
1. **DSM 로그인** → **패키지 센터**
|
||||
2. **설치됨** 탭에서 "Git Server" 검색
|
||||
3. 상태 확인:
|
||||
- ✅ **실행 중**: 정상 동작
|
||||
- ⚠️ **중지됨**: 서비스 시작 필요
|
||||
- ❌ **미설치**: 패키지 설치 필요
|
||||
|
||||
### 1.2 Git Server 서비스 시작
|
||||
```bash
|
||||
# SSH로 NAS 접속 후
|
||||
sudo systemctl status git-daemon
|
||||
sudo systemctl start git-daemon
|
||||
sudo systemctl enable git-daemon
|
||||
```
|
||||
|
||||
## 🛠️ 2단계: 기본 설정 확인
|
||||
|
||||
### 2.1 SSH 서비스 활성화
|
||||
1. **DSM 제어판** → **터미널 및 SNMP**
|
||||
2. **SSH 서비스 활성화** 체크
|
||||
3. 포트 번호 확인 (기본: 22)
|
||||
|
||||
### 2.2 사용자 권한 설정
|
||||
1. **DSM 제어판** → **사용자 및 그룹**
|
||||
2. 사용자 선택 → **편집** → **애플리케이션**
|
||||
3. **Git Server** 권한 부여
|
||||
|
||||
### 2.3 방화벽 설정
|
||||
1. **DSM 제어판** → **보안** → **방화벽**
|
||||
2. 다음 포트 허용:
|
||||
- SSH: 22
|
||||
- Git HTTP: 3000
|
||||
- Git HTTPS: 3001
|
||||
|
||||
## 📁 3단계: Git 디렉토리 구조 확인
|
||||
|
||||
### 3.1 기본 경로 확인
|
||||
```bash
|
||||
# SSH 접속 후 확인
|
||||
ls -la /volume1/
|
||||
ls -la /volume1/git/
|
||||
|
||||
# Git 설정 디렉토리 확인
|
||||
ls -la /usr/local/git/
|
||||
```
|
||||
|
||||
### 3.2 권한 문제 해결
|
||||
```bash
|
||||
# Git 디렉토리 생성
|
||||
sudo mkdir -p /volume1/git
|
||||
sudo chown -R admin:users /volume1/git
|
||||
sudo chmod 755 /volume1/git
|
||||
|
||||
# Git Server 사용자 추가 (필요시)
|
||||
sudo adduser git
|
||||
sudo usermod -a -G users git
|
||||
```
|
||||
|
||||
## 🔧 4단계: 레포지토리 수동 생성
|
||||
|
||||
### 4.1 Bare 레포지토리 생성
|
||||
```bash
|
||||
# SSH로 NAS 접속
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# 프로젝트 디렉토리 생성
|
||||
cd /volume1/git
|
||||
sudo mkdir jaryo-file-manager.git
|
||||
cd jaryo-file-manager.git
|
||||
|
||||
# Bare 레포지토리 초기화
|
||||
sudo git init --bare
|
||||
sudo chown -R admin:users .
|
||||
```
|
||||
|
||||
### 4.2 웹 인터페이스 활성화
|
||||
```bash
|
||||
# Git HTTP 서버 시작
|
||||
cd /volume1/git
|
||||
sudo git daemon --reuseaddr --base-path=. --export-all --verbose --enable=receive-pack
|
||||
```
|
||||
|
||||
## 🌐 5단계: 웹 인터페이스 설정
|
||||
|
||||
### 5.1 Git Web 설정
|
||||
```bash
|
||||
# CGit 또는 GitWeb 설치
|
||||
sudo apt update
|
||||
sudo apt install gitweb
|
||||
|
||||
# Apache 설정 (Web Station 사용시)
|
||||
sudo ln -s /usr/share/gitweb /volume1/web/git
|
||||
```
|
||||
|
||||
### 5.2 브라우저에서 접속
|
||||
- URL: `http://your-nas-ip/git`
|
||||
- 또는: `http://your-nas-ip:3000`
|
||||
|
||||
## 🚨 6단계: 문제 해결
|
||||
|
||||
### 6.1 "레포지토리 설정이 안보임" 해결
|
||||
**원인 1: Git Server 패키지 미설치**
|
||||
```bash
|
||||
# 패키지 센터에서 Git Server 재설치
|
||||
# 또는 수동 Git 설치
|
||||
sudo apt update
|
||||
sudo apt install git git-daemon-run
|
||||
```
|
||||
|
||||
**원인 2: 서비스 시작 실패**
|
||||
```bash
|
||||
# 서비스 상태 확인
|
||||
sudo systemctl status git-daemon
|
||||
sudo journalctl -u git-daemon
|
||||
|
||||
# 수동 재시작
|
||||
sudo systemctl restart git-daemon
|
||||
```
|
||||
|
||||
**원인 3: 권한 문제**
|
||||
```bash
|
||||
# 권한 재설정
|
||||
sudo chown -R www-data:www-data /volume1/git
|
||||
sudo chmod -R 755 /volume1/git
|
||||
```
|
||||
|
||||
### 6.2 포트 충돌 해결
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
sudo netstat -tulpn | grep :3000
|
||||
sudo netstat -tulpn | grep :22
|
||||
|
||||
# 다른 포트로 변경
|
||||
sudo git daemon --port=3001 --reuseaddr --base-path=/volume1/git --export-all
|
||||
```
|
||||
|
||||
## 📋 7단계: 연결 테스트
|
||||
|
||||
### 7.1 로컬에서 연결 테스트
|
||||
```bash
|
||||
# SSH 연결 테스트
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# Git 클론 테스트
|
||||
git clone ssh://admin@your-nas-ip/volume1/git/jaryo-file-manager.git
|
||||
|
||||
# 또는 HTTP 연결
|
||||
git clone http://your-nas-ip:3000/jaryo-file-manager.git
|
||||
```
|
||||
|
||||
### 7.2 기존 프로젝트 푸시
|
||||
```bash
|
||||
# 기존 프로젝트에서
|
||||
git remote add nas ssh://admin@your-nas-ip/volume1/git/jaryo-file-manager.git
|
||||
git push nas master
|
||||
```
|
||||
|
||||
## 🔄 8단계: 자동화 설정
|
||||
|
||||
### 8.1 systemd 서비스 생성
|
||||
```bash
|
||||
# /etc/systemd/system/git-daemon.service
|
||||
sudo tee /etc/systemd/system/git-daemon.service << EOF
|
||||
[Unit]
|
||||
Description=Git Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/volume1/git --export-all --verbose --enable=receive-pack
|
||||
Restart=always
|
||||
User=git
|
||||
Group=git
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl enable git-daemon
|
||||
sudo systemctl start git-daemon
|
||||
```
|
||||
|
||||
## 📊 요약
|
||||
|
||||
레포지토리 설정이 보이지 않는 주요 원인:
|
||||
1. ❌ Git Server 패키지 미설치/미실행
|
||||
2. ❌ SSH 서비스 비활성화
|
||||
3. ❌ 사용자 권한 부족
|
||||
4. ❌ 방화벽 차단
|
||||
5. ❌ Git 디렉토리 부재
|
||||
|
||||
해결 순서:
|
||||
1. 패키지 설치/재시작
|
||||
2. SSH 및 권한 설정
|
||||
3. 수동 레포지토리 생성
|
||||
4. 연결 테스트
|
||||
5. 자동화 설정
|
||||
|
||||
이 가이드를 따라하면 시놀로지 NAS에서 Git 레포지토리를 성공적으로 설정할 수 있습니다.
|
@@ -1,248 +0,0 @@
|
||||
# 시놀로지 NAS에서 Jaryo File Manager 서비스 실행 가이드
|
||||
|
||||
## 1. 사전 준비사항
|
||||
|
||||
### 1.1 DSM 패키지 설치
|
||||
1. **DSM 제어판** → **패키지 센터** 접속
|
||||
2. 다음 패키지들을 설치:
|
||||
- **Node.js** (최신 LTS 버전 권장)
|
||||
- **Git Server** (선택사항, 소스코드 관리용)
|
||||
- **Web Station** (선택사항, 웹 서버 프록시용)
|
||||
|
||||
### 1.2 SSH 활성화
|
||||
1. **DSM 제어판** → **터미널 및 SNMP** → **SSH 서비스 활성화**
|
||||
2. 포트 번호 확인 (기본: 22)
|
||||
|
||||
## 2. 프로젝트 배포
|
||||
|
||||
### 2.1 방법 1: 직접 파일 업로드 (간단한 방법)
|
||||
|
||||
1. **File Station**에서 `/volume1/web/` 폴더 생성
|
||||
2. 프로젝트 파일들을 `jaryo` 폴더에 업로드
|
||||
3. SSH로 접속하여 설정
|
||||
|
||||
### 2.2 방법 2: Git을 통한 배포 (권장)
|
||||
|
||||
```bash
|
||||
# NAS에 SSH 접속
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# 프로젝트 디렉토리 생성
|
||||
mkdir -p /volume1/web/jaryo
|
||||
cd /volume1/web/jaryo
|
||||
|
||||
# Git 저장소 클론 (로컬에서 push한 경우)
|
||||
git clone [your-repository-url] .
|
||||
|
||||
# 또는 로컬에서 직접 파일 복사
|
||||
# scp -r ./jaryo/* admin@your-nas-ip:/volume1/web/jaryo/
|
||||
```
|
||||
|
||||
## 3. 서비스 설정 및 실행
|
||||
|
||||
### 3.1 스크립트 권한 설정
|
||||
|
||||
```bash
|
||||
# SSH로 NAS 접속
|
||||
ssh admin@your-nas-ip
|
||||
|
||||
# 프로젝트 디렉토리로 이동
|
||||
cd /volume1/web/jaryo
|
||||
|
||||
# 스크립트 실행 권한 부여
|
||||
chmod +x start-service.sh
|
||||
chmod +x stop-service.sh
|
||||
```
|
||||
|
||||
### 3.2 서비스 시작
|
||||
|
||||
```bash
|
||||
# 서비스 시작
|
||||
./start-service.sh
|
||||
|
||||
# 로그 확인
|
||||
tail -f logs/app.log
|
||||
|
||||
# 프로세스 상태 확인
|
||||
ps aux | grep "node server.js"
|
||||
```
|
||||
|
||||
### 3.3 서비스 중지
|
||||
|
||||
```bash
|
||||
# 서비스 중지
|
||||
./stop-service.sh
|
||||
```
|
||||
|
||||
## 4. 자동 시작 설정 (선택사항)
|
||||
|
||||
### 4.1 Task Scheduler 사용
|
||||
|
||||
1. **DSM 제어판** → **작업 스케줄러**
|
||||
2. **작업 생성** → **사용자 정의 스크립트**
|
||||
3. 설정:
|
||||
- **작업 이름**: Jaryo Service Start
|
||||
- **사용자**: root
|
||||
- **스케줄**: 시스템 부팅 시
|
||||
- **작업 설정**: `/volume1/web/jaryo/start-service.sh`
|
||||
|
||||
### 4.2 rc.local 사용 (고급 사용자)
|
||||
|
||||
```bash
|
||||
# /etc/rc.local 파일 편집
|
||||
sudo vi /etc/rc.local
|
||||
|
||||
# 다음 라인 추가
|
||||
/volume1/web/jaryo/start-service.sh &
|
||||
|
||||
# 파일 저장 후 권한 설정
|
||||
chmod +x /etc/rc.local
|
||||
```
|
||||
|
||||
## 5. 방화벽 및 포트 설정
|
||||
|
||||
### 5.1 DSM 방화벽 설정
|
||||
|
||||
1. **DSM 제어판** → **보안** → **방화벽**
|
||||
2. **방화벽 규칙 편집** → **규칙 생성**
|
||||
3. 설정:
|
||||
- **포트**: 3005 (애플리케이션 포트)
|
||||
- **프로토콜**: TCP
|
||||
- **소스**: 허용할 IP 범위
|
||||
|
||||
### 5.2 라우터 포트 포워딩 (외부 접속용)
|
||||
|
||||
라우터에서 포트 3005를 NAS의 IP로 포워딩 설정
|
||||
|
||||
## 6. 웹 서버 프록시 설정 (Web Station 사용)
|
||||
|
||||
### 6.1 Web Station 설정
|
||||
|
||||
1. **Web Station** → **가상 호스트** → **생성**
|
||||
2. 설정:
|
||||
- **도메인 이름**: your-domain.com (또는 IP)
|
||||
- **포트**: 80 (또는 443 for HTTPS)
|
||||
- **문서 루트**: `/volume1/web/jaryo`
|
||||
- **HTTP 백엔드 서버**: `http://localhost:3005`
|
||||
|
||||
### 6.2 Apache 설정 (고급)
|
||||
|
||||
```apache
|
||||
# /volume1/web/apache/conf/vhost/VirtualHost.conf
|
||||
<VirtualHost *:80>
|
||||
ServerName your-domain.com
|
||||
DocumentRoot /volume1/web/jaryo
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:3005/
|
||||
ProxyPassReverse / http://localhost:3005/
|
||||
|
||||
<Directory /volume1/web/jaryo>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
## 7. 모니터링 및 유지보수
|
||||
|
||||
### 7.1 로그 모니터링
|
||||
|
||||
```bash
|
||||
# 실시간 로그 확인
|
||||
tail -f /volume1/web/jaryo/logs/app.log
|
||||
|
||||
# 로그 파일 크기 확인
|
||||
du -h /volume1/web/jaryo/logs/app.log
|
||||
|
||||
# 로그 로테이션 설정 (logrotate 사용)
|
||||
```
|
||||
|
||||
### 7.2 서비스 상태 확인
|
||||
|
||||
```bash
|
||||
# 프로세스 확인
|
||||
ps aux | grep "node server.js"
|
||||
|
||||
# 포트 사용 확인
|
||||
netstat -tlnp | grep :3005
|
||||
|
||||
# 메모리 사용량 확인
|
||||
top -p $(cat /volume1/web/jaryo/app.pid)
|
||||
```
|
||||
|
||||
### 7.3 백업 설정
|
||||
|
||||
1. **Hyper Backup** 패키지 설치
|
||||
2. `/volume1/web/jaryo` 폴더 백업 스케줄 설정
|
||||
3. 데이터베이스 파일 (`jaryo.db`) 별도 백업 권장
|
||||
|
||||
## 8. 문제 해결
|
||||
|
||||
### 8.1 일반적인 문제들
|
||||
|
||||
**서비스가 시작되지 않는 경우:**
|
||||
```bash
|
||||
# Node.js 설치 확인
|
||||
which node
|
||||
node --version
|
||||
|
||||
# 의존성 재설치
|
||||
cd /volume1/web/jaryo
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
|
||||
# 권한 문제 확인
|
||||
ls -la /volume1/web/jaryo/
|
||||
chown -R admin:users /volume1/web/jaryo/
|
||||
```
|
||||
|
||||
**포트 충돌 문제:**
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
netstat -tlnp | grep :3005
|
||||
|
||||
# 다른 포트로 변경 (server.js 수정)
|
||||
# const PORT = process.env.PORT || 3006;
|
||||
```
|
||||
|
||||
**메모리 부족 문제:**
|
||||
```bash
|
||||
# 메모리 사용량 확인
|
||||
free -h
|
||||
|
||||
# Node.js 메모리 제한 설정
|
||||
# node --max-old-space-size=512 server.js
|
||||
```
|
||||
|
||||
### 8.2 로그 분석
|
||||
|
||||
```bash
|
||||
# 에러 로그만 확인
|
||||
grep -i error /volume1/web/jaryo/logs/app.log
|
||||
|
||||
# 최근 100줄 확인
|
||||
tail -100 /volume1/web/jaryo/logs/app.log
|
||||
|
||||
# 특정 시간대 로그 확인
|
||||
grep "2024-01-15" /volume1/web/jaryo/logs/app.log
|
||||
```
|
||||
|
||||
## 9. 보안 고려사항
|
||||
|
||||
1. **HTTPS 설정**: Let's Encrypt 인증서 사용
|
||||
2. **방화벽 강화**: 필요한 포트만 개방
|
||||
3. **정기 업데이트**: Node.js 및 패키지 업데이트
|
||||
4. **백업**: 정기적인 데이터 백업
|
||||
5. **모니터링**: 로그 모니터링 및 알림 설정
|
||||
|
||||
## 10. 성능 최적화
|
||||
|
||||
1. **PM2 사용**: 프로세스 관리자로 PM2 사용 고려
|
||||
2. **캐싱**: 정적 파일 캐싱 설정
|
||||
3. **압축**: gzip 압축 활성화
|
||||
4. **CDN**: 정적 파일 CDN 사용 고려
|
||||
|
||||
---
|
||||
|
||||
**참고**: 이 가이드는 시놀로지 DSM 7.x 기준으로 작성되었습니다. 버전에 따라 일부 설정이 다를 수 있습니다.
|
54
uninstall-auto-startup.bat
Normal file
54
uninstall-auto-startup.bat
Normal file
@@ -0,0 +1,54 @@
|
||||
@echo off
|
||||
REM Windows 작업 스케줄러에서 자동 시작 설정 제거 스크립트
|
||||
REM 관리자 권한으로 실행 필요
|
||||
|
||||
echo === Jaryo File Manager 자동 시작 설정 제거 ===
|
||||
echo.
|
||||
|
||||
REM 관리자 권한 확인
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 오류: 이 스크립트는 관리자 권한으로 실행해야 합니다.
|
||||
echo 마우스 우클릭 후 "관리자 권한으로 실행"을 선택해주세요.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set TASK_NAME=JaryoFileManagerAutoStart
|
||||
|
||||
echo 작업 이름: %TASK_NAME%
|
||||
echo.
|
||||
|
||||
REM 작업 존재 여부 확인
|
||||
schtasks /query /tn "%TASK_NAME%" >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ⚠️ 자동 시작 작업을 찾을 수 없습니다.
|
||||
echo 이미 제거되었거나 설치되지 않았습니다.
|
||||
goto :end
|
||||
)
|
||||
|
||||
REM 현재 실행 중인 서비스 중지
|
||||
echo 현재 실행 중인 서비스를 중지합니다...
|
||||
call "%~dp0stop-jaryo-service.bat"
|
||||
|
||||
echo.
|
||||
echo 자동 시작 작업을 제거합니다...
|
||||
|
||||
REM 작업 삭제
|
||||
schtasks /delete /tn "%TASK_NAME%" /f
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo.
|
||||
echo ✅ 자동 시작 작업이 성공적으로 제거되었습니다!
|
||||
echo.
|
||||
echo 이제 컴퓨터를 재시작해도 Jaryo File Manager가 자동으로 시작되지 않습니다.
|
||||
echo 수동으로 서비스를 시작하려면 start-jaryo-service.bat를 실행하세요.
|
||||
) else (
|
||||
echo.
|
||||
echo ❌ 자동 시작 작업 제거에 실패했습니다.
|
||||
echo 관리자 권한으로 실행했는지 확인해주세요.
|
||||
)
|
||||
|
||||
:end
|
||||
echo.
|
||||
pause
|
33
vercel.json
33
vercel.json
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{
|
||||
"src": "/simple",
|
||||
"dest": "/simple.html"
|
||||
},
|
||||
{
|
||||
"src": "/api/test",
|
||||
"dest": "/api/test.js"
|
||||
},
|
||||
{
|
||||
"src": "/api/files/public",
|
||||
"dest": "/api/files.js"
|
||||
},
|
||||
{
|
||||
"src": "/api/files",
|
||||
"dest": "/api/files.js"
|
||||
},
|
||||
{
|
||||
"src": "/(.*\\.(css|js|json|svg|png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|html))",
|
||||
"dest": "/$1"
|
||||
},
|
||||
{
|
||||
"src": "/admin/(.*)",
|
||||
"dest": "/admin/$1"
|
||||
},
|
||||
{
|
||||
"src": "/",
|
||||
"dest": "/simple.html"
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user