Compare commits

..

27 Commits

Author SHA1 Message Date
9422439a51 fix: improve mobile responsive design for file list table
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Add comprehensive responsive design for multiple screen sizes (1024px, 768px, 480px, 320px, 280px)
- Implement horizontal scrolling with touch support for table overflow
- Optimize column widths and font sizes for each breakpoint
- Add visual scroll hints for mobile users
- Ensure proper viewport utilization with calc() functions
- Fix table layout issues on extreme small screens (280px)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 16:45:31 +09:00
7796d9b7d5 fix: improve category cancel button functionality
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Enhanced resetCategoryForm() to properly reset editing state
- Clear currentEditCategoryId when cancelling
- Reset button text and form title to default state
- Add console logging for better debugging

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-16 14:08:04 +09:00
d6a0656f12 cleanup: remove unnecessary files and dependencies
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Remove duplicate database files (database.sqlite, database/jaryo.db)
- Remove test files (test-login.html)
- Remove redundant deployment scripts (deploy.sh, start-simple.bat)
- Remove vercel dev dependency and fix security vulnerabilities
- Clean up project structure for better maintainability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-16 13:28:35 +09:00
04d92b7842 feat: update Claude Code settings
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-16 13:21:43 +09:00
d3d8aa48b6 cleanup: remove MariaDB dependencies and files
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Remove MariaDB helper, schema, and initialization script
- Remove mysql2 dependency from package.json
- Update reset-admin.js to use SQLite DatabaseHelper
- Simplify .env.example to remove MariaDB configuration
- Update start-service.sh to use SQLite initialization
- Clean up all MariaDB references across codebase

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 14:08:44 +09:00
80f147731e refactor: simplify to use SQLite for all environments
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Change all environments (local and NAS) to use SQLite database
- Remove MariaDB dependency and complexity
- Make database initialization optional in deployment script
- Simplify deployment by using single database type across all environments

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 13:54:59 +09:00
ed5fa15814 feat: enhance database environment handling and cleanup project
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Add environment-specific database selection (SQLite for local, MariaDB for NAS)
- Remove edit button from admin detail view modal for cleaner UX
- Clean up project files: remove redundant docs and test files
- Update deployment script with improved SSH handling
- Maintain backward compatibility while supporting both database types

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 13:13:19 +09:00
896e42d9cc fix(admin): include credentials on auth requests and adapt login response
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
2025-08-22 16:42:30 +09:00
bda299a6c3 Fix download functionality and attachment display
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Fixed MariaDB compatible download API for NAS deployment
- Updated SQLite schema to remove deprecated file_data column
- Enhanced attachment display consistency between admin and public pages
- Resolved category ordering issues in SQLite environment
- Added NAS MariaDB remote connection configuration
- Improved file upload and download functionality for both environments

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 15:44:54 +09:00
7be1f2ed07 MariaDB 완전 마이그레이션 및 NAS 배포 최적화
- MariaDB 환경별 자동 감지 (Windows/NAS/Linux)
- Unix Socket 및 TCP 연결 지원
- 완전한 UTF8MB4 스키마 적용
- 자동 초기화 스크립트 개선
- NAS 배포 스크립트 MariaDB 지원
- 환경변수 기반 설정 시스템
- 상세한 배포 가이드 문서화

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 13:38:25 +09:00
ced3fd03e4 Add admin password reset script with mysql2 dependency
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
2025-08-22 12:21:24 +09:00
1a053ca047 Fix MariaDB connection to use Unix socket 2025-08-22 11:23:39 +09:00
92391aa2c0 Add MariaDB support for Jaryo File Manager
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Add MariaDB schema with UTF8MB4 charset
- Create MariaDB helper class with connection pooling
- Update application to use MariaDB instead of SQLite
- Configure database user and permissions
- Set service to run on port 3007

Database Configuration:
- Host: localhost:3306
- Database: jaryo
- User: jaryo_user
- Tables: users, files, file_attachments, categories, user_sessions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 11:06:54 +09:00
6c41f5d883 Add Windows auto-startup configuration and NAS deployment
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Add Windows service startup scripts (start/stop-jaryo-service.bat)
- Add auto-startup configuration with Task Scheduler
- Add XML task definition for Windows Task Scheduler
- Update server.js to bind to specific IP (99.1.110.50)
- Add comprehensive auto-startup documentation
- Prepare for NAS deployment with existing scripts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 10:24:32 +09:00
49d16f0512 수정
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
2025-08-21 20:43:34 +09:00
2195cdf1b9 Fix large file download and title color issues
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- 대용량 파일 다운로드: blob 방식 대신 직접 링크 방식으로 변경하여 메모리 문제 해결
- 자료 목록 제목 색상: 파란색(#667eea)으로 변경하여 가독성 향상

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 20:04:58 +09:00
bbf1ec10ef Improve large file download handling
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Add better error handling for ECONNABORTED (client disconnect)
- Add Accept-Ranges header for better download resume support
- Add client connection close detection
- Improve logging for download interruptions
- Better file stat handling for large files

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 19:37:52 +09:00
ec2e9abcad Update header title color to blue in both main and admin pages
Some checks failed
Deploy to Vercel / deploy (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
- Changed header h1 color from #4a5568 (gray) to #667eea (blue)
- Applied same blue color to both main page and admin page
- Consistent branding across all pages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 19:12:42 +09:00
922552c30b Update deployment scripts with Node.js path fixes and data preservation
- Modified deploy-to-nas.sh to use vibsin9322 account
- Added Node.js path detection for Synology NAS
- Fixed npm command paths to use full Node.js path
- Added database backup and preservation logic
- Created manual deployment guide and SSH helper scripts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 18:23:27 +09:00
ce34ca7956 Add comprehensive NAS deployment scripts and guides
- Add deploy-to-nas.sh: automated deployment script with SSH port 2222
- Add manual-nas-deployment-guide.md: step-by-step manual deployment guide
- Include service start/stop scripts and monitoring tools
- Add troubleshooting and auto-start configuration
- Support both automated and manual deployment approaches
2025-08-21 14:28:55 +09:00
6477a488c2 Add comprehensive Synology NAS Git server setup guides
- Add synology-git-diagnostic.md: complete troubleshooting guide
- Enhance create-git-repo.sh: improved error handling and user feedback
- Add nas-git-connection-test.md: step-by-step connection testing
- Add alternative-git-servers.md: Docker-based Git server alternatives
- Cover Gitea, GitLab, Forgejo, and manual Git server installation
- Include troubleshooting and performance optimization tips
2025-08-21 13:58:24 +09:00
c6ec1c3720 Add simple test page for 404 debugging
- Create simple.html as fallback test page
- Set root route to simple.html for immediate access
- Include API test functionality in the page
- Verify basic Vercel deployment works
2025-08-21 13:35:38 +09:00
08894eeb66 Fix serverless functions: simplify API structure
- Replace complex Express-style handler with simple module.exports
- Add separate API endpoints for better organization
- Simplify vercel.json routing configuration
- Remove ES modules to use CommonJS for Vercel compatibility
- Add dedicated test API endpoint for debugging
2025-08-21 13:32:53 +09:00
ec5da4db32 Fix Vercel serverless deployment: optimize for fast loading
- Convert Express app to Vercel serverless function
- Add missing /api/files/public endpoint
- Optimize static file routing with proper caching
- Remove unnecessary dependencies for faster cold starts
- Add comprehensive debugging and error handling
- Improve API response times and user experience
2025-08-21 13:25:57 +09:00
ce29d6bc3b Fix Vercel routing: separate API and static file routes 2025-08-21 13:11:42 +09:00
43d0802442 Fix Vercel deployment issues: add CORS fix, session config, and simple API version 2025-08-21 13:06:35 +09:00
9b9eb7f321 Fix Vercel deployment: add serverless compatibility and API routes 2025-08-21 12:43:53 +09:00
41 changed files with 1704 additions and 2079 deletions

View File

@@ -36,12 +36,56 @@
"Bash(tasklist)",
"Bash(start http://localhost:8000)",
"Bash(npm --version)",
"mcp__sequential-thinking__sequentialthinking"
"mcp__sequential-thinking__sequentialthinking",
"Bash(git commit:*)",
"Bash(chmod:*)",
"Bash(./deploy-to-nas.sh:*)",
"Bash(ssh:*)",
"Bash(scp:*)",
"Bash(cat:*)",
"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": [],
"additionalDirectories": [
"C:\\c\\Users\\COMTREE\\claude_code"
"C:\\c\\Users\\COMTREE\\claude_code",
"C:\\Users\\COMTREE\\.ssh"
]
},
"default-mode": "plan"

13
.env.example Normal file
View 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
View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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() {

View File

@@ -29,7 +29,7 @@ header {
}
header h1 {
color: #4a5568;
color: #667eea !important;
font-size: 2.5rem;
margin-bottom: 10px;
}

View File

@@ -5,20 +5,31 @@ const API_BASE_URL = '';
// API 요청 헬퍼 함수
async function apiRequest(url, options = {}) {
const response = await fetch(`${API_BASE_URL}${url}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
console.log(`🔗 API 요청: ${url}`);
if (!response.ok) {
const error = await response.text();
throw new Error(`API Error: ${response.status} - ${error}`);
try {
const response = await fetch(`${API_BASE_URL}${url}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
timeout: 10000, // 10초 타임아웃
...options
});
console.log(`📡 응답 상태: ${response.status} ${response.statusText}`);
if (!response.ok) {
const error = await response.text();
console.error(`❌ API 오류: ${response.status} - ${error}`);
throw new Error(`API Error: ${response.status} - ${error}`);
}
return response;
} catch (error) {
console.error(`🚨 네트워크 오류:`, error);
throw error;
}
return response;
}
// 공개 파일 목록 조회

View File

@@ -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;

View File

@@ -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';

View File

@@ -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)를 참고하거나 이슈를 등록해주세요.

View File

@@ -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' 부분을 추출합니다.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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');
-- 주의: 이 정책들은 테스트 후 반드시 삭제하고 위의 사용자별 정책으로 교체하세요!

View File

@@ -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

View File

@@ -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) {

View File

@@ -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();

311
deploy-to-nas.sh Normal file
View File

@@ -0,0 +1,311 @@
#!/bin/bash
# 시놀로지 NAS 자료실 배포 스크립트
# 사용법: ./deploy-to-nas.sh [nas-ip] [project-name] [password]
# 예시: ./deploy-to-nas.sh 119.64.1.86 jaryo mypassword
# 환경변수: NAS_PASS=mypassword ./deploy-to-nas.sh
# 기본 설정
NAS_IP="${1:-119.64.1.86}"
PROJECT_NAME="${2:-jaryo}"
NAS_USER="vibsin9322"
# 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 명령어 준비 (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 자료실 배포 시작"
echo "=========================================="
echo "NAS IP: $NAS_IP"
echo "프로젝트: $PROJECT_NAME"
echo "배포 경로: $DEPLOY_DIR"
echo "서비스 포트: $SERVICE_PORT"
echo "Gitea URL: $GITEA_URL"
echo "=========================================="
# 사전 요구사항 확인
echo "📋 1단계: 사전 요구사항 확인"
# SSH 방식 확인
if [ -n "$NAS_PASS" ]; then
echo "🔧 SSH 접속 방식: 비밀번호 비대화식(plink)"
else
echo "🔧 SSH 접속 방식: 비밀번호 프롬프트 방식"
echo "📝 SSH 연결 시 비밀번호 입력이 필요합니다."
fi
# SSH 연결 테스트 (포트 2222)
echo "🔗 SSH 연결 테스트 중... (사용자: $NAS_USER, 포트: 2222)"
if ! eval "$SSH_CMD 'echo SSH 연결 성공'"; then
echo "❌ SSH 연결 실패. 다음을 확인하세요:"
echo " - NAS IP 주소: $NAS_IP"
echo " - SSH 포트: 2222"
echo " - SSH 서비스 활성화 (DSM > 제어판 > 터미널 및 SNMP)"
echo " - 방화벽 설정 (포트 2222 허용)"
exit 1
fi
echo "✅ SSH 연결 성공"
# Node.js 설치 확인
echo "📦 Node.js 설치 확인 중..."
NODE_PATH=""
if eval "$SSH_CMD 'test -f /usr/local/bin/node'" 2>/dev/null; then
NODE_PATH="/usr/local/bin"
elif eval "$SSH_CMD 'which node'" >/dev/null 2>&1; then
NODE_PATH=$(eval "$SSH_CMD 'which node'" | dirname)
else
echo "❌ Node.js가 설치되지 않았습니다."
echo "DSM 패키지 센터에서 Node.js를 설치하세요."
exit 1
fi
NODE_VERSION=$(eval "$SSH_CMD '$NODE_PATH/node --version'")
echo "✅ Node.js 설치됨: $NODE_VERSION ($NODE_PATH)"
# Git 설치 확인
echo "📦 Git 설치 확인 중..."
eval "$SSH_CMD 'which git'" >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo "❌ Git이 설치되지 않았습니다."
echo "DSM 패키지 센터에서 Git Server를 설치하거나 다음 명령을 실행하세요:"
echo "ssh -p 2222 $NAS_USER@$NAS_IP 'sudo apt update && sudo apt install git'"
exit 1
fi
GIT_VERSION=$(eval "$SSH_CMD 'git --version'")
echo "✅ Git 설치됨: $GIT_VERSION"
# 2단계: 소스 코드 배포
echo ""
echo "📂 2단계: 소스 코드 배포"
# 기존 배포 디렉토리 확인
echo "🗂️ 배포 디렉토리 확인 중..."
eval "$SSH_CMD '
if [ -d '$DEPLOY_DIR' ]; then
echo '⚠️ 기존 배포가 존재합니다: $DEPLOY_DIR'
echo '백업 생성 중...'
sudo cp -r '$DEPLOY_DIR' '${DEPLOY_DIR}_backup_$(date +%Y%m%d_%H%M%S)'
echo '기존 배포 제거 중...'
sudo rm -rf '$DEPLOY_DIR'
fi
"
# 배포 디렉토리 생성
echo "📁 배포 디렉토리 생성 중..."
eval "$SSH_CMD '
sudo mkdir -p '$DEPLOY_DIR'
sudo chown admin:users '$DEPLOY_DIR'
cd '$DEPLOY_DIR'
"
# Git 클론
echo "📥 Gitea에서 소스 코드 클론 중..."
eval "$SSH_CMD '
cd '$DEPLOY_DIR'
git clone '$GITEA_URL' .
if [ \$? -ne 0 ]; then
echo '❌ Git 클론 실패'
exit 1
fi
echo '✅ 소스 코드 클론 완료'
"
# 3단계: 의존성 설치 및 빌드
echo ""
echo "🔧 3단계: 의존성 설치 및 빌드"
eval "$SSH_CMD '
cd '$DEPLOY_DIR'
# 기존 node_modules 제거
if [ -d 'node_modules' ]; then
echo '🗑️ 기존 node_modules 제거 중...'
rm -rf node_modules package-lock.json
fi
# 의존성 설치
echo '📦 의존성 설치 중...'
export PATH='$NODE_PATH':\$PATH
'$NODE_PATH'/npm install
if [ \$? -ne 0 ]; then
echo '❌ npm install 실패'
exit 1
fi
echo '✅ 의존성 설치 완료'
# 데이터베이스 초기화 (선택사항)
if [ "\$INIT_DB" = "true" ] && [ -f 'scripts/init-database.js' ]; then
echo '🗄️ SQLite 데이터베이스 초기화 중...'
echo ' SQLite 데이터베이스: data/jaryo.db'
export PATH='$NODE_PATH':\$PATH
if '$NODE_PATH'/npm run init-db; then
echo '✅ SQLite 초기화 완료'
else
echo '❌ SQLite 초기화 실패'
echo '💡 수동으로 초기화하려면:'
echo ' npm run init-db'
exit 1
fi
else
echo ' 데이터베이스 초기화 건너뜀 (INIT_DB=true로 설정시 초기화)'
fi
"
# 4단계: 서비스 설정
echo ""
echo "⚙️ 4단계: 서비스 설정"
# 시작 스크립트 생성
echo "📝 시작 스크립트 생성 중..."
eval "$SSH_CMD '
cat > '$DEPLOY_DIR/start-nas-service.sh' << 'EOF'
#!/bin/bash
# 자료실 NAS 서비스 시작 스크립트
PROJECT_DIR='$DEPLOY_DIR'
SERVICE_PORT='$SERVICE_PORT'
NODE_PATH='$NODE_PATH'
PID_FILE='\$PROJECT_DIR/jaryo-nas.pid'
LOG_FILE='\$PROJECT_DIR/logs/app.log'
# 로그 디렉토리 생성
mkdir -p '\$PROJECT_DIR/logs'
# 기존 프로세스 확인 및 종료
if [ -f '\$PID_FILE' ]; then
OLD_PID=\$(cat '\$PID_FILE')
if kill -0 '\$OLD_PID' 2>/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'
# 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
# 시작 확인
if kill -0 \$(cat '\$PID_FILE') 2>/dev/null; then
echo '✅ 자료실 서비스 시작 완료!'
echo '📍 접속 URL: http://$NAS_IP:$SERVICE_PORT'
echo '📋 PID: '\$(cat '\$PID_FILE')
echo '📄 로그: '\$LOG_FILE'
else
echo '❌ 서비스 시작 실패'
echo '로그 확인:'
tail -20 '\$LOG_FILE'
exit 1
fi
EOF
chmod +x '$DEPLOY_DIR/start-nas-service.sh'
"
# 중지 스크립트 생성
echo "📝 중지 스크립트 생성 중..."
eval "$SSH_CMD '
cat > '$DEPLOY_DIR/stop-nas-service.sh' << 'EOF'
#!/bin/bash
# 자료실 NAS 서비스 중지 스크립트
PROJECT_DIR='$DEPLOY_DIR'
PID_FILE='\$PROJECT_DIR/jaryo-nas.pid'
if [ -f '\$PID_FILE' ]; then
PID=\$(cat '\$PID_FILE')
if kill -0 '\$PID' 2>/dev/null; then
echo '🛑 자료실 서비스 중지 중... (PID: '\$PID')'
kill '\$PID'
sleep 2
# 강제 종료 확인
if kill -0 '\$PID' 2>/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 '$DEPLOY_DIR/stop-nas-service.sh'
"
# 5단계: 서비스 시작
echo ""
echo "🎬 5단계: 서비스 시작"
eval "$SSH_CMD '$DEPLOY_DIR/start-nas-service.sh'"
# 6단계: 접속 테스트
echo ""
echo "🧪 6단계: 접속 테스트"
sleep 3
echo "🌐 서비스 상태 확인 중..."
if curl -s "http://$NAS_IP:$SERVICE_PORT" >/dev/null; then
echo "✅ 자료실 서비스 정상 작동!"
echo ""
echo "=========================================="
echo "🎉 배포 완료!"
echo "=========================================="
echo "📍 접속 URL: http://$NAS_IP:$SERVICE_PORT"
echo "🔧 관리자 URL: http://$NAS_IP:$SERVICE_PORT/admin"
echo "📂 배포 경로: $DEPLOY_DIR"
echo "📄 로그 파일: $DEPLOY_DIR/logs/app.log"
echo ""
echo "🔧 서비스 관리:"
echo " 시작: ssh -p 2222 $NAS_USER@$NAS_IP '$DEPLOY_DIR/start-nas-service.sh'"
echo " 중지: ssh -p 2222 $NAS_USER@$NAS_IP '$DEPLOY_DIR/stop-nas-service.sh'"
echo " 로그: ssh -p 2222 $NAS_USER@$NAS_IP 'tail -f $DEPLOY_DIR/logs/app.log'"
echo ""
echo "📱 브라우저에서 http://$NAS_IP:$SERVICE_PORT 접속하세요!"
else
echo "❌ 서비스 접속 실패"
echo "로그 확인:"
eval "$SSH_CMD 'tail -20 $DEPLOY_DIR/logs/app.log'"
fi

104
deploy.sh
View File

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

View File

@@ -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
View 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
View 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

372
package-lock.json generated
View File

@@ -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",

View File

@@ -1,28 +1,30 @@
{
"name": "jaryo-file-manager",
"version": "1.0.0",
"version": "2.0.0",
"description": "자료실 파일 관리 시스템",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"init-db": "node scripts/init-database.js"
"dev": "node server.js",
"init-db": "node scripts/init-database.js",
"build": "echo 'Build complete'"
},
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"cors": "^2.8.5",
"multer": "^1.4.5-lts.1",
"path": "^0.12.7",
"fs": "^0.0.1-security",
"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"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": ["file-manager", "sqlite", "express", "admin"],
"keywords": [
"file-manager",
"admin"
],
"author": "Claude Code",
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -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
}]
};

View File

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

View File

@@ -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
View 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();

View File

@@ -9,16 +9,21 @@ class PublicFileViewer {
}
async init() {
console.log('🚀 PublicFileViewer 초기화 시작');
try {
this.showLoading(true);
console.log('📡 파일 목록 로드 중...');
await this.loadFiles();
this.filteredFiles = [...this.files];
console.log(`${this.files.length}개 파일 로드 완료`);
this.bindEvents();
this.renderFiles();
this.updatePagination();
} catch (error) {
console.error('초기화 오류:', error);
this.showNotification('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
console.error('초기화 오류:', error);
this.showNotification('데이터를 불러오는 중 오류가 발생했습니다. 페이지를 새로고침 해주세요.', 'error');
} finally {
this.showLoading(false);
}
@@ -338,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);
@@ -354,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');
}
}

View File

@@ -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');
async function initializeDatabase() {
console.log('🗄️ 데이터베이스 초기화 시작...');
// database 폴더가 없으면 생성
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
const dbHelper = new DatabaseHelper();
// uploads 폴더도 생성
const uploadsDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
try {
// 데이터 디렉토리 생성
const dataDir = path.join(__dirname, '..', 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
console.log('📁 데이터 디렉토리 생성됨:', dataDir);
}
console.log('🔧 SQLite 데이터베이스 초기화 시작...');
// 데이터베이스 연결 및 테이블 생성
await dbHelper.connect();
console.log('✅ 데이터베이스 연결 성공');
// 데이터베이스 연결
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('❌ 데이터베이스 연결 오류:', err.message);
return;
}
console.log('✅ SQLite 데이터베이스 연결 성공');
});
// 기본 관리자 계정 생성
const adminEmail = 'admin@jaryo.com';
const adminPassword = 'Hee150603!';
// 스키마 파일 읽기 및 실행
fs.readFile(schemaPath, 'utf8', (err, schema) => {
if (err) {
console.error('❌ 스키마 파일 읽기 오류:', err.message);
return;
}
const existingUser = await dbHelper.getUserByEmail(adminEmail);
// 여러 SQL 문을 분리하여 실행
const statements = schema.split(';').filter(stmt => stmt.trim().length > 0);
if (!existingUser) {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(adminPassword, saltRounds);
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 adminData = {
email: adminEmail,
password_hash: hashedPassword,
name: '관리자',
role: 'admin'
};
console.log('✅ 데이터베이스 스키마 생성 완료');
await dbHelper.createUser(adminData);
console.log('👤 기본 관리자 계정 생성됨');
console.log('📧 이메일:', adminEmail);
console.log('🔑 비밀번호:', adminPassword);
} else {
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 defaultCategories = ['문서', '이미지', '동영상', '프레젠테이션', '기타'];
db.close((err) => {
if (err) {
console.error('❌ 데이터베이스 종료 오류:', err.message);
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;

160
server.js
View File

@@ -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;
@@ -16,7 +18,7 @@ const db = new DatabaseHelper();
// 미들웨어 설정
app.use(cors({
origin: ['http://localhost:3001', 'http://127.0.0.1:3001'],
origin: true, // 모든 도메인 허용 (Vercel 배포용)
credentials: true
}));
app.use(express.json({ limit: '50mb' }));
@@ -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: false, // HTTPS에서는 true로 설
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')));
@@ -53,6 +63,16 @@ app.get('/', (req, res) => {
res.redirect('/index.html');
});
// 헬스 체크 엔드포인트
app.get('/health', (req, res) => {
res.json({
success: true,
message: 'Jaryo File Manager is running',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
});
});
// Multer 설정 (파일 업로드)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
@@ -681,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 = ?';
@@ -710,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);
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);
// RFC 5987을 준수하는 헤더 설정 (한글 파일명 지원)
const stat = fs.statSync(filePath);
const fileSize = stat.size;
// 원본 파일명으로 다운로드
res.download(filePath, originalName, (err) => {
if (err) {
console.error('📁 다운로드 오류:', err);
} else {
console.log('📁 다운로드 완료:', originalName);
}
});
// 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
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,
@@ -766,23 +824,39 @@ app.use((req, res) => {
});
});
// 서버 시작
app.listen(PORT, () => {
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`);
});
// Vercel 서버리스 환경을 위한 export
module.exports = app;
// 프로세스 종료 시 데이터베이스 연결 종료
process.on('SIGINT', async () => {
console.log('\n📝 서버를 종료합니다...');
await db.close();
process.exit(0);
});
// 로컬 개발 환경에서만 서버 시작
if (process.env.NODE_ENV !== 'production' || process.env.VERCEL !== '1') {
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;
process.on('SIGTERM', async () => {
console.log('\n📝 서버를 종료합니다...');
await db.close();
process.exit(0);
});
console.log(`🚀 자료실 서버가 포트 ${PORT}에서 실행중입니다.`);
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 () => {
console.log('\n📝 서버를 종료합니다...');
await db.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\n📝 서버를 종료합니다...');
await db.close();
process.exit(0);
});
}

84
start-jaryo-service.bat Normal file
View 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 서비스가 성공적으로 시작되었습니다.

View File

@@ -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
View 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 서비스 중지가 완료되었습니다.

View File

@@ -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: #4a5568;
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;
@@ -1469,3 +1473,481 @@ header p {
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;
}
}

View File

@@ -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 기준으로 작성되었습니다. 버전에 따라 일부 설정이 다를 수 있습니다.

View 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

View File

@@ -1,18 +0,0 @@
{
"version": 2,
"builds": [
{
"src": "server.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/server.js"
}
],
"env": {
"NODE_ENV": "production"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB