Compare commits

...

8 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
21 changed files with 615 additions and 3766 deletions

View File

@@ -54,7 +54,32 @@
"mcp__context7__get-library-docs",
"Bash(npm run init-mariadb:*)",
"Bash(npm test)",
"Bash(npm run build:*)"
"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": [],

View File

@@ -1,10 +1,5 @@
# 개발 환경 설정 예시
# Windows 개발환경용 MariaDB/MySQL 연결 설정
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_NAME=jaryo
# SQLite 데이터베이스 사용 (설정 불필요)
# NAS 배포 환경
NODE_ENV=development

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

View File

@@ -1,139 +0,0 @@
# Jaryo File Manager - MariaDB 배포 가이드
## 개요
Jaryo File Manager를 시놀로지 NAS의 MariaDB와 함께 배포하는 가이드입니다.
## 시스템 요구사항
### NAS 환경
- 시놀로지 DSM 7.0+
- Node.js 18+ (DSM 패키지 센터에서 설치)
- MariaDB 10+ (DSM 패키지 센터에서 설치)
- Git Server (DSM 패키지 센터에서 설치)
### 개발 환경
- Node.js 18+
- MariaDB/MySQL 5.7+
- Git
## NAS 배포 단계
### 1. 빠른 배포 (권장)
```bash
# 자동 배포 스크립트 실행
./deploy-to-nas.sh [NAS-IP] jaryo [PASSWORD]
# 예시:
./deploy-to-nas.sh 192.168.1.100 jaryo mypassword
```
### 2. 수동 배포
#### 2.1 MariaDB 설정
NAS SSH 접속 후:
```sql
mysql -u root -p
CREATE DATABASE IF NOT EXISTS jaryo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'jaryo_user'@'localhost' IDENTIFIED BY 'JaryoPass2024!@#';
GRANT ALL PRIVILEGES ON jaryo.* TO 'jaryo_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
#### 2.2 스키마 설정
```bash
mysql -u jaryo_user -p jaryo < database/mariadb-schema.sql
```
#### 2.3 서비스 배포
```bash
# 소스 코드 클론
cd /volume1/web
git clone [GITEA_URL] jaryo
cd jaryo
# 의존성 설치
npm install
# 데이터베이스 초기화
npm run init-mariadb
# 서비스 시작
./start-service.sh
```
## 환경별 설정
### Windows 개발 환경
`.env` 파일 생성:
```env
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=jaryo
HOST=0.0.0.0
PORT=3000
```
### NAS 운영 환경
환경 변수는 자동으로 설정됩니다:
- `NODE_ENV=production`
- `DEPLOY_ENV=nas`
- Unix Socket 연결: `/run/mysqld/mysqld10.sock`
## 관리 명령어
### 서비스 관리
```bash
# 시작
./start-service.sh
# 중지
kill $(cat jaryo-nas.pid)
# 로그 확인
tail -f logs/app.log
```
### 데이터베이스 관리
```bash
# 초기화
npm run init-mariadb
# 직접 연결
mysql -u jaryo_user -p -S /run/mysqld/mysqld10.sock jaryo
```
## 접속 정보
### 기본 관리자 계정
- **이메일**: admin@jaryo.com
- **비밀번호**: Hee150603!
### 서비스 URL
- **메인**: http://[NAS-IP]:3005
- **관리자**: http://[NAS-IP]:3005/admin
## 문제 해결
### 연결 오류
1. MariaDB 서비스 상태 확인
2. 데이터베이스 및 사용자 권한 확인
3. 소켓 파일 경로 확인
4. 방화벽 설정 확인
### 상세 가이드
더 자세한 설정 방법은 `mariadb-setup.md` 파일을 참조하세요.
## 기술 스택
- **Backend**: Node.js + Express
- **Database**: MariaDB
- **Frontend**: Vanilla JavaScript
- **Deployment**: Shell Scripts
## 지원
- 로그 파일: `/volume1/web/jaryo/logs/app.log`
- 설정 파일: `database/mariadb-helper.js`
- 스키마: `database/mariadb-schema.sql`

View File

@@ -1,101 +0,0 @@
# Jaryo File Manager 자동 시작 설정 가이드
## 🚀 자동 시작 설정 방법
### 방법 1: 배치 파일 실행 (권장)
1. **관리자 권한으로 실행**
- `install-auto-startup.bat` 파일을 마우스 우클릭
- "관리자 권한으로 실행" 선택
- 안내에 따라 진행
2. **설정 완료 후**
- 컴퓨터 재시작 시 자동으로 서비스 시작
- 서비스 URL: http://99.1.110.50:3005
### 방법 2: 수동 작업 스케줄러 설정
1. **작업 스케줄러 열기**
- Windows 키 + R → `taskschd.msc` 입력 → 엔터
2. **기본 작업 만들기**
- 오른쪽 패널에서 "기본 작업 만들기" 클릭
- 이름: `JaryoFileManagerAutoStart`
- 설명: `Jaryo File Manager 자동 시작`
3. **트리거 설정**
- "컴퓨터를 시작할 때" 선택
4. **동작 설정**
- "프로그램 시작" 선택
- 프로그램/스크립트: `C:\Users\COMTREE\claude_code\jaryo\start-jaryo-service.bat`
- 시작 위치: `C:\Users\COMTREE\claude_code\jaryo`
5. **고급 설정**
- "가장 높은 권한으로 실행" 체크
- "작업이 이미 실행 중인 경우 규칙": "새 인스턴스 시작 안 함"
## 🔧 서비스 관리 명령어
### 수동 서비스 제어
```batch
# 서비스 시작
start-jaryo-service.bat
# 서비스 중지
stop-jaryo-service.bat
```
### 자동 시작 관리
```batch
# 자동 시작 설정
install-auto-startup.bat (관리자 권한 필요)
# 자동 시작 해제
uninstall-auto-startup.bat (관리자 권한 필요)
```
### 작업 스케줄러 명령어
```cmd
# 작업 상태 확인
schtasks /query /tn "JaryoFileManagerAutoStart"
# 작업 수동 실행
schtasks /run /tn "JaryoFileManagerAutoStart"
# 작업 삭제
schtasks /delete /tn "JaryoFileManagerAutoStart" /f
```
## 🌐 접속 정보
- **관리자 페이지**: http://99.1.110.50:3005/admin/index.html
- **메인 페이지**: http://99.1.110.50:3005/index.html
- **API**: http://99.1.110.50:3005/api/files
- **상태 확인**: http://99.1.110.50:3005/health
## 📁 로그 확인
- **로그 파일**: `C:\Users\COMTREE\claude_code\jaryo\logs\app.log`
- **로그 보기**: `type "C:\Users\COMTREE\claude_code\jaryo\logs\app.log"`
## ⚠️ 문제 해결
### 서비스가 시작되지 않는 경우
1. Node.js가 설치되어 있는지 확인: `node --version`
2. 프로젝트 디렉토리가 올바른지 확인
3. 포트 3005가 사용 중인지 확인: `netstat -an | findstr :3005`
4. 로그 파일 확인
### 자동 시작이 작동하지 않는 경우
1. 작업 스케줄러에서 작업 상태 확인
2. 관리자 권한으로 설정했는지 확인
3. 배치 파일 경로가 올바른지 확인
## 📞 지원
문제가 발생하면 다음을 확인해주세요:
1. 로그 파일 내용
2. 작업 스케줄러 작업 상태
3. 포트 사용 현황
4. Node.js 설치 상태

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

@@ -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 1755922561 connect.sid s%3AmN1rJMxZAee73L5t1g-59UL0tXQP36Vr.6GJ1M%2FUaWj68J9z6Ua9MBxCG54Sl8OuOsRUQNQFlLZE

View File

@@ -1,238 +0,0 @@
const mysql = require('mysql2/promise');
const { v4: uuidv4 } = require('uuid');
class MariaDBHelper {
constructor() {
this.connection = null;
// 환경별 설정 지원
const isWindows = process.platform === 'win32';
const isNAS = process.env.NODE_ENV === 'production' || process.env.DEPLOY_ENV === 'nas';
if (isWindows) {
// Windows 개발 환경 (NAS MariaDB 원격 접속)
this.config = {
host: process.env.DB_HOST || '119.64.1.86',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'jaryo_user',
password: process.env.DB_PASSWORD || 'JaryoPass2024!@#',
database: process.env.DB_NAME || 'jaryo',
charset: 'utf8mb4'
};
} else if (isNAS) {
// 시놀로지 NAS 환경 (Unix Socket)
this.config = {
socketPath: '/run/mysqld/mysqld10.sock',
user: 'jaryo_user',
password: 'JaryoPass2024!@#',
database: 'jaryo',
charset: 'utf8mb4'
};
} else {
// 기타 Linux 환경
this.config = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'jaryo_user',
password: process.env.DB_PASSWORD || 'JaryoPass2024!@#',
database: process.env.DB_NAME || 'jaryo',
charset: 'utf8mb4'
};
}
}
async connect() {
try {
if (!this.connection || this.connection.connection._closing) {
this.connection = await mysql.createConnection(this.config);
const connectionType = this.config.socketPath ? 'Unix Socket' : `TCP ${this.config.host}:${this.config.port}`;
console.log(`✅ MariaDB 연결 성공 (${connectionType})`);
}
return this.connection;
} catch (error) {
console.error('❌ MariaDB 연결 실패:', error);
console.error('연결 설정:', { ...this.config, password: '***' });
throw error;
}
}
async close() {
if (this.connection) {
await this.connection.end();
this.connection = null;
console.log('📝 MariaDB 연결 종료');
}
}
generateId() {
return uuidv4();
}
// 사용자 관리
async createUser(userData) {
const conn = await this.connect();
const id = this.generateId();
const [result] = await conn.execute(
'INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)',
[id, userData.email, userData.password_hash, userData.name, userData.role || 'user']
);
return { id, ...result };
}
async getUserByEmail(email) {
const conn = await this.connect();
const [rows] = await conn.execute(
'SELECT * FROM users WHERE email = ?',
[email]
);
return rows[0] || null;
}
async getUserById(id) {
const conn = await this.connect();
const [rows] = await conn.execute(
'SELECT * FROM users WHERE id = ?',
[id]
);
return rows[0] || null;
}
async updateUserLastLogin(id) {
const conn = await this.connect();
const [result] = await conn.execute(
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
[id]
);
return result;
}
// 파일 관리
async addFile(fileData) {
const conn = await this.connect();
const [result] = await conn.execute(
'INSERT INTO files (id, title, description, category, tags, user_id) VALUES (?, ?, ?, ?, ?, ?)',
[fileData.id, fileData.title, fileData.description, fileData.category, JSON.stringify(fileData.tags), fileData.user_id]
);
return result;
}
async getAllFiles(limit = 100, offset = 0) {
const conn = await this.connect();
const [rows] = await conn.execute(
'SELECT f.*, u.name as user_name FROM files f LEFT JOIN users u ON f.user_id = u.id ORDER BY f.created_at DESC LIMIT ? OFFSET ?',
[limit, offset]
);
// Parse tags from JSON
return rows.map(row => ({
...row,
tags: row.tags ? JSON.parse(row.tags) : []
}));
}
async searchFiles(searchTerm, category = null, limit = 100) {
const conn = await this.connect();
let query = 'SELECT f.*, u.name as user_name FROM files f LEFT JOIN users u ON f.user_id = u.id WHERE (f.title LIKE ? OR f.description LIKE ?)';
let params = [`%${searchTerm}%`, `%${searchTerm}%`];
if (category) {
query += ' AND f.category = ?';
params.push(category);
}
query += ' ORDER BY f.created_at DESC LIMIT ?';
params.push(limit);
const [rows] = await conn.execute(query, params);
return rows.map(row => ({
...row,
tags: row.tags ? JSON.parse(row.tags) : []
}));
}
async updateFile(id, updates) {
const conn = await this.connect();
const [result] = await conn.execute(
'UPDATE files SET title = ?, description = ?, category = ?, tags = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[updates.title, updates.description, updates.category, updates.tags, id]
);
return result;
}
async deleteFile(id) {
const conn = await this.connect();
const [result] = await conn.execute('DELETE FROM files WHERE id = ?', [id]);
return result;
}
// 파일 첨부 관리
async addFileAttachment(fileId, attachmentData) {
const conn = await this.connect();
const [result] = await conn.execute(
'INSERT INTO file_attachments (file_id, original_name, file_name, file_path, file_size, mime_type) VALUES (?, ?, ?, ?, ?, ?)',
[fileId, attachmentData.original_name, attachmentData.file_name, attachmentData.file_path, attachmentData.file_size, attachmentData.mime_type]
);
return result;
}
async getFileAttachments(fileId) {
const conn = await this.connect();
const [rows] = await conn.execute(
'SELECT * FROM file_attachments WHERE file_id = ? ORDER BY created_at',
[fileId]
);
return rows;
}
async deleteFileAttachment(attachmentId) {
const conn = await this.connect();
const [result] = await conn.execute('DELETE FROM file_attachments WHERE id = ?', [attachmentId]);
return result;
}
// 카테고리 관리
async getCategories() {
const conn = await this.connect();
const [rows] = await conn.execute('SELECT * FROM categories ORDER BY name');
return rows;
}
async addCategory(name) {
const conn = await this.connect();
const [result] = await conn.execute('INSERT INTO categories (name) VALUES (?)', [name]);
return result;
}
async updateCategory(id, name) {
const conn = await this.connect();
const [result] = await conn.execute('UPDATE categories SET name = ? WHERE id = ?', [name, id]);
return result;
}
async deleteCategory(id) {
const conn = await this.connect();
const [result] = await conn.execute('DELETE FROM categories WHERE id = ?', [id]);
return result;
}
// 통계
async getStats() {
const conn = await this.connect();
const [userCount] = await conn.execute('SELECT COUNT(*) as count FROM users');
const [fileCount] = await conn.execute('SELECT COUNT(*) as count FROM files');
const [categoryCount] = await conn.execute('SELECT COUNT(*) as count FROM categories');
const [attachmentCount] = await conn.execute('SELECT COUNT(*) as count FROM file_attachments');
return {
users: userCount[0].count,
files: fileCount[0].count,
categories: categoryCount[0].count,
attachments: attachmentCount[0].count
};
}
}
module.exports = MariaDBHelper;

View File

@@ -1,87 +0,0 @@
-- 자료실 MariaDB 데이터베이스 스키마
-- 파일: database/mariadb-schema.sql
-- 데이터베이스 생성
CREATE DATABASE IF NOT EXISTS jaryo
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE jaryo;
-- 사용자 테이블
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
role ENUM('admin', 'user') DEFAULT 'user',
is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_login TIMESTAMP NULL,
INDEX idx_email (email),
INDEX idx_role (role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 카테고리 테이블
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
is_default TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 파일 정보 테이블
CREATE TABLE IF NOT EXISTS files (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
category VARCHAR(100) NOT NULL DEFAULT '기타',
tags JSON,
user_id VARCHAR(36),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_category (category),
INDEX idx_created_at (created_at),
INDEX idx_user_id (user_id),
INDEX idx_title (title),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 파일 첨부 정보 테이블
CREATE TABLE IF NOT EXISTS file_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
file_id VARCHAR(36) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
file_size BIGINT DEFAULT 0,
mime_type VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_file_id (file_id),
INDEX idx_original_name (original_name),
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 세션 테이블 (선택사항)
CREATE TABLE IF NOT EXISTS user_sessions (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_expires_at (expires_at),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 기본 카테고리 데이터 삽입
INSERT IGNORE INTO categories (name, is_default) VALUES
('문서', 1),
('이미지', 1),
('동영상', 1),
('프레젠테이션', 1),
('기타', 1);
-- 기본 관리자 계정 생성 (비밀번호: Hee150603!)
-- 실제 해시는 애플리케이션에서 생성됩니다

View File

@@ -9,13 +9,22 @@
NAS_IP="${1:-119.64.1.86}"
PROJECT_NAME="${2:-jaryo}"
NAS_USER="vibsin9322"
NAS_PASS="${3:-vibsin9322}" # 기본 비밀번호, 환경변수 NAS_PASS로 오버라이드 가능
# NAS_PASS 우선순위: 환경변수 > 스크립트 3번째 인자 > 프롬프트 방식
if [ -n "$3" ]; then
NAS_PASS="$3"
else
NAS_PASS="${NAS_PASS:-}"
fi
DEPLOY_DIR="/volume1/web/$PROJECT_NAME"
SERVICE_PORT="3005"
GITEA_URL="http://$NAS_IP:3000/vibsin9322/jaryo.git"
# SSH 명령어 준비
# SSH 명령어 준비 (NAS_PASS가 있으면 plink로 비대화식, 없으면 ssh 프롬프트)
if [ -n "$NAS_PASS" ]; then
SSH_CMD="plink -P 2222 -batch -pw \"$NAS_PASS\" $NAS_USER@$NAS_IP"
else
SSH_CMD="ssh -p 2222 -o ConnectTimeout=10 -o StrictHostKeyChecking=no $NAS_USER@$NAS_IP"
fi
echo "=========================================="
echo "🚀 시놀로지 NAS 자료실 배포 시작"
@@ -31,8 +40,12 @@ echo "=========================================="
echo "📋 1단계: 사전 요구사항 확인"
# SSH 방식 확인
if [ -n "$NAS_PASS" ]; then
echo "🔧 SSH 접속 방식: 비밀번호 비대화식(plink)"
else
echo "🔧 SSH 접속 방식: 비밀번호 프롬프트 방식"
echo "📝 SSH 연결 시 비밀번호 입력이 필요합니다."
fi
# SSH 연결 테스트 (포트 2222)
echo "🔗 SSH 연결 테스트 중... (사용자: $NAS_USER, 포트: 2222)"
@@ -135,30 +148,22 @@ if [ \$? -ne 0 ]; then
fi
echo '✅ 의존성 설치 완료'
# MariaDB 데이터베이스 초기화
if [ -f 'scripts/init-mariadb.js' ]; then
echo '🗄️ MariaDB 데이터베이스 초기화 중...'
echo ' MariaDB 연결 설정:'
echo ' - 데이터베이스: jaryo'
echo ' - 사용자: jaryo_user'
echo ' - 소켓: /run/mysqld/mysqld10.sock'
# 데이터베이스 초기화 (선택사항)
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-mariadb; then
echo '✅ MariaDB 초기화 완료'
if '$NODE_PATH'/npm run init-db; then
echo '✅ SQLite 초기화 완료'
else
echo '❌ MariaDB 초기화 실패'
echo '💡 MariaDB 설정을 확인하고 다음 명령어를 실행하세요:'
echo ' mysql -u root -p << EOF'
echo ' CREATE DATABASE IF NOT EXISTS jaryo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
echo ' CREATE USER IF NOT EXISTS '\''jaryo_user'\''@'\''localhost'\'' IDENTIFIED BY '\''JaryoPass2024!@#'\'';'
echo ' GRANT ALL PRIVILEGES ON jaryo.* TO '\''jaryo_user'\''@'\''localhost'\'';'
echo ' FLUSH PRIVILEGES;'
echo ' EOF'
echo '❌ SQLite 초기화 실패'
echo '💡 수동으로 초기화하려면:'
echo ' npm run init-db'
exit 1
fi
else
echo ' MariaDB 초기화 스크립트를 찾을 수 없습니다.'
echo ' 데이터베이스 초기화 건너뜀 (INIT_DB=true로 설정시 초기화)'
fi
"
@@ -204,9 +209,8 @@ fi
# 서비스 시작
echo '🚀 자료실 서비스 시작 중...'
cd '\$PROJECT_DIR'
# NAS 환경 변수 설정
# NAS 환경 변수 설정 (SQLite 사용)
export NODE_ENV=production
export DEPLOY_ENV=nas
export HOST=0.0.0.0
export PORT='$SERVICE_PORT'
nohup \$NODE_PATH/node server.js > '\$LOG_FILE' 2>&1 &
@@ -274,7 +278,7 @@ chmod +x '$DEPLOY_DIR/stop-nas-service.sh'
echo ""
echo "🎬 5단계: 서비스 시작"
eval "$SSH_CMD '$DEPLOY_DIR/start-nas-service.sh"
eval "$SSH_CMD '$DEPLOY_DIR/start-nas-service.sh'"
# 6단계: 접속 테스트
echo ""
@@ -303,5 +307,5 @@ if curl -s "http://$NAS_IP:$SERVICE_PORT" >/dev/null; then
else
echo "❌ 서비스 접속 실패"
echo "로그 확인:"
eval "$SSH_CMD 'tail -20 $DEPLOY_DIR/logs/app.log"
eval "$SSH_CMD 'tail -20 $DEPLOY_DIR/logs/app.log'"
fi

104
deploy.sh
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

@@ -1,130 +0,0 @@
# 시놀로지 NAS MariaDB 설정 가이드
## 1. MariaDB 패키지 설치
1. **DSM 패키지 센터** 열기
2. **MariaDB 10** 검색 및 설치
3. 설치 완료 후 **실행**
## 2. MariaDB 초기 설정
### 2.1 MariaDB 관리 도구 접속
- DSM에서 **MariaDB 10** 앱 실행
- 또는 웹 브라우저에서 `http://NAS-IP:3307` 접속
### 2.2 관리자 계정으로 로그인
- 사용자: `root`
- 비밀번호: 설치 시 설정한 비밀번호
## 3. 자료실 데이터베이스 설정
### 3.1 데이터베이스 생성
```sql
CREATE DATABASE IF NOT EXISTS jaryo
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
```
### 3.2 전용 사용자 생성
```sql
CREATE USER 'jaryo_user'@'localhost' IDENTIFIED BY 'JaryoPass2024!@#';
GRANT ALL PRIVILEGES ON jaryo.* TO 'jaryo_user'@'localhost';
FLUSH PRIVILEGES;
```
### 3.3 테이블 생성 (스키마 적용)
```bash
# NAS SSH 접속 후
cd /volume1/web/jaryo
mysql -u jaryo_user -p jaryo < database/mariadb-schema.sql
```
## 4. 연결 설정 확인
### 4.1 Unix Socket 경로 확인
```bash
sudo find /run -name "*.sock" | grep mysql
# 일반적인 경로: /run/mysqld/mysqld10.sock
```
### 4.2 연결 테스트
```bash
mysql -u jaryo_user -p -S /run/mysqld/mysqld10.sock jaryo
```
## 5. NAS 자료실 서비스 배포
### 5.1 자동 배포
```bash
./deploy-to-nas.sh [NAS-IP] jaryo [PASSWORD]
```
### 5.2 수동 초기화 (필요시)
```bash
cd /volume1/web/jaryo
npm run init-mariadb
```
## 6. 문제 해결
### 6.1 연결 오류
- MariaDB 서비스 상태 확인: `sudo systemctl status mariadb`
- 소켓 파일 권한 확인: `ls -la /run/mysqld/`
- 방화벽 설정 확인
### 6.2 권한 오류
```sql
-- 권한 재설정
GRANT ALL PRIVILEGES ON jaryo.* TO 'jaryo_user'@'localhost';
FLUSH PRIVILEGES;
```
### 6.3 한글 데이터 문제
```sql
-- 문자셋 확인
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
```
## 7. 성능 최적화
### 7.1 인덱스 확인
```sql
USE jaryo;
SHOW INDEX FROM files;
SHOW INDEX FROM file_attachments;
```
### 7.2 쿼리 최적화
```sql
-- 슬로우 쿼리 로그 활성화
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
```
## 8. 백업 및 복원
### 8.1 데이터베이스 백업
```bash
mysqldump -u jaryo_user -p jaryo > jaryo_backup_$(date +%Y%m%d).sql
```
### 8.2 데이터베이스 복원
```bash
mysql -u jaryo_user -p jaryo < jaryo_backup_YYYYMMDD.sql
```
## 9. 보안 설정
### 9.1 비밀번호 변경
```sql
ALTER USER 'jaryo_user'@'localhost' IDENTIFIED BY 'NEW_SECURE_PASSWORD';
```
### 9.2 불필요한 권한 제거
```sql
-- 테스트 데이터베이스 제거
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.user WHERE User='';
FLUSH PRIVILEGES;
```

2587
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@
"start": "node server.js",
"dev": "node server.js",
"init-db": "node scripts/init-database.js",
"init-mariadb": "node scripts/init-mariadb.js",
"build": "echo 'Build complete'"
},
"dependencies": {
@@ -16,13 +15,9 @@
"express": "^4.18.2",
"express-session": "^1.17.3",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.14.3",
"sqlite3": "^5.1.6",
"uuid": "^9.0.1"
},
"devDependencies": {
"vercel": "^32.0.0"
},
"keywords": [
"file-manager",
"admin"

View File

@@ -1,8 +1,8 @@
const bcrypt = require('bcrypt');
const MariaDBHelper = require('./database/mariadb-helper');
const DatabaseHelper = require('./database/db-helper');
async function resetAdminPassword() {
const dbHelper = new MariaDBHelper();
const dbHelper = new DatabaseHelper();
try {
console.log('🔄 관리자 비밀번호 초기화 시작...');
@@ -17,13 +17,16 @@ async function resetAdminPassword() {
const existingUser = await dbHelper.getUserByEmail('admin@jaryo.com');
if (existingUser) {
// 기존 사용자 비밀번호 업데이트
const conn = await dbHelper.connect();
const [result] = await conn.execute(
'UPDATE users SET password_hash = ? WHERE email = ?',
[hashedPassword, 'admin@jaryo.com']
);
// 기존 사용자 비밀번호 업데이트 (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 = {

View File

@@ -1,108 +0,0 @@
const bcrypt = require('bcrypt');
const MariaDBHelper = require('../database/mariadb-helper');
async function initializeMariaDB() {
console.log('🗄️ MariaDB 데이터베이스 초기화 시작...');
const dbHelper = new MariaDBHelper();
try {
// 데이터베이스 연결 테스트
await dbHelper.connect();
console.log('✅ MariaDB 연결 성공');
// 기본 관리자 계정 생성
const adminEmail = 'admin@jaryo.com';
const adminPassword = 'Hee150603!';
const existingUser = await dbHelper.getUserByEmail(adminEmail);
if (!existingUser) {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(adminPassword, saltRounds);
const adminData = {
email: adminEmail,
password_hash: hashedPassword,
name: '관리자',
role: 'admin'
};
await dbHelper.createUser(adminData);
console.log('👤 기본 관리자 계정 생성됨');
console.log('📧 이메일:', adminEmail);
console.log('🔑 비밀번호:', adminPassword);
} else {
console.log('👤 관리자 계정이 이미 존재합니다.');
}
// 기본 카테고리 확인 및 생성
const categories = await dbHelper.getCategories();
const defaultCategories = ['문서', '이미지', '동영상', '프레젠테이션', '기타'];
for (const categoryName of defaultCategories) {
const existingCategory = categories.find(cat => cat.name === categoryName);
if (!existingCategory) {
try {
await dbHelper.addCategory(categoryName);
console.log(`📂 카테고리 생성됨: ${categoryName}`);
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
console.log(`📂 카테고리 이미 존재: ${categoryName}`);
} else {
console.error(`카테고리 생성 오류 (${categoryName}):`, error.message);
}
}
} else {
console.log(`📂 카테고리 이미 존재: ${categoryName}`);
}
}
console.log('🎉 MariaDB 초기화 완료!');
} catch (error) {
console.error('❌ MariaDB 초기화 실패:', error);
// 연결 오류인 경우 상세한 도움말 제공
if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT') {
console.log('\n📋 MariaDB 연결 확인사항:');
if (process.platform === 'win32') {
console.log('🪟 Windows 개발 환경:');
console.log('1. MariaDB 또는 MySQL이 설치되고 실행 중인지 확인');
console.log('2. 환경변수 또는 .env 파일에 DB 연결 정보 설정');
console.log(' - DB_HOST=localhost');
console.log(' - DB_PORT=3306');
console.log(' - DB_USER=root');
console.log(' - DB_PASSWORD=your_password');
console.log(' - DB_NAME=jaryo');
console.log('\n🔧 Windows MySQL/MariaDB 설정:');
console.log('CREATE DATABASE jaryo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
console.log('USE jaryo;');
console.log('-- 그 다음 database/mariadb-schema.sql 파일 실행');
} else {
console.log('🐧 Linux/NAS 환경:');
console.log('1. MariaDB가 설치되고 실행 중인지 확인');
console.log('2. 데이터베이스 "jaryo"가 생성되어 있는지 확인');
console.log('3. 사용자 "jaryo_user"가 생성되고 권한이 있는지 확인');
console.log('4. Unix 소켓 경로가 올바른지 확인: /run/mysqld/mysqld10.sock');
console.log('\n🔧 NAS MariaDB 설정 명령어:');
console.log('CREATE DATABASE jaryo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
console.log('CREATE USER \'jaryo_user\'@\'localhost\' IDENTIFIED BY \'JaryoPass2024!@#\';');
console.log('GRANT ALL PRIVILEGES ON jaryo.* TO \'jaryo_user\'@\'localhost\';');
console.log('FLUSH PRIVILEGES;');
}
}
process.exit(1);
} finally {
await dbHelper.close();
}
}
// 스크립트 직접 실행 시에만 초기화 실행
if (require.main === module) {
initializeMariaDB().catch(console.error);
}
module.exports = initializeMariaDB;

View File

@@ -6,12 +6,14 @@ 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;
// 데이터베이스 헬퍼 인스턴스 (SQLite - 로컬 테스트용)
// 데이터베이스 헬퍼 인스턴스
const db = new DatabaseHelper();
// 미들웨어 설정

View File

@@ -40,23 +40,16 @@ if [ ! -d "node_modules" ]; then
$NPM_PATH install
fi
# MariaDB 데이터베이스 초기화
echo "MariaDB 데이터베이스 초기화 중..."
if [ -f "scripts/init-mariadb.js" ]; then
# NAS 환경 설정
export NODE_ENV=production
export DEPLOY_ENV=nas
if $NPM_PATH run init-mariadb; then
echo "✅ MariaDB 초기화 완료"
# SQLite 데이터베이스 초기화 (선택적)
if [ "$INIT_DB" = "true" ] && [ -f "scripts/init-database.js" ]; then
echo "SQLite 데이터베이스 초기화 중..."
if $NPM_PATH run init-db; then
echo "✅ SQLite 초기화 완료"
else
echo "⚠️ MariaDB 초기화 실패"
echo "💡 수동으로 MariaDB를 설정해야 할 수 있습니다."
echo "자세한 내용은 mariadb-setup.md를 참조하세요."
echo "⚠️ SQLite 초기화 실패"
fi
else
echo " MariaDB 초기화 스크립트를 찾을 수 없습니다."
echo "💡 수동으로 MariaDB를 설정하세요."
echo " 데이터베이스 초기화 건너뜀 (INIT_DB=true로 설정시 초기화)"
fi
# 기존 프로세스 종료
@@ -72,9 +65,8 @@ fi
# 서비스 시작
echo "서비스 시작 중..."
# NAS 환경 변수 설정
# NAS 환경 변수 설정 (SQLite 사용)
export NODE_ENV=production
export DEPLOY_ENV=nas
export HOST=0.0.0.0
export PORT=3005

View File

@@ -1,4 +0,0 @@
@echo off
cd /d "C:\Users\COMTREE\claude_code\jaryo"
echo Starting Jaryo File Manager...
node server.js

View File

@@ -16,6 +16,8 @@ body {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
width: 100%;
box-sizing: border-box;
}
header {
@@ -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 {
@@ -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,60 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Test</title>
</head>
<body>
<h1>Admin Login Test</h1>
<div>
<input type="email" id="email" value="admin@jaryo.com" placeholder="Email">
<input type="password" id="password" placeholder="Password">
<button onclick="testLogin()">Test Login</button>
</div>
<div id="result"></div>
<script>
async function testLogin() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const result = document.getElementById('result');
console.log('Testing login with:', { email, password });
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ email, password })
});
console.log('Response status:', response.status);
console.log('Response headers:', response.headers);
const data = await response.json();
console.log('Response data:', data);
if (response.ok) {
result.innerHTML = '<p style="color: green;">✅ Login Success: ' + JSON.stringify(data, null, 2) + '</p>';
// Test session
const sessionResponse = await fetch('/api/auth/session', {
credentials: 'include'
});
const sessionData = await sessionResponse.json();
console.log('Session data:', sessionData);
result.innerHTML += '<p style="color: blue;">📋 Session: ' + JSON.stringify(sessionData, null, 2) + '</p>';
} else {
result.innerHTML = '<p style="color: red;">❌ Login Failed: ' + JSON.stringify(data, null, 2) + '</p>';
}
} catch (error) {
console.error('Login error:', error);
result.innerHTML = '<p style="color: red;">❌ Error: ' + error.message + '</p>';
}
}
</script>
</body>
</html>