Загрузить новый баннер
+Перетащите изображения сюда или нажмите для выбора
+Поддерживаются: JPG, PNG, GIF (максимум 10MB)
+ + +diff --git a/.history/ADMINLTE_CSP_FIX_20251026213607.md b/.history/ADMINLTE_CSP_FIX_20251026213607.md
new file mode 100644
index 0000000..2d4045c
--- /dev/null
+++ b/.history/ADMINLTE_CSP_FIX_20251026213607.md
@@ -0,0 +1,63 @@
+# AdminLTE CSP Fix - Исправление Content Security Policy
+
+## Проблема
+Content Security Policy (CSP) блокировал загрузку jQuery и Bootstrap из CDN, что приводило к ошибкам:
+
+```
+Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://code.jquery.com/jquery-3.6.0.min.js
+Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js
+```
+
+## Причина
+В server.js в настройках helmet CSP для `scriptSrc` отсутствовали домены:
+- `https://code.jquery.com` (для jQuery)
+- `https://cdn.jsdelivr.net` (для Bootstrap)
+
+## Решение
+Обновлены CSP настройки в `/server.js`:
+
+### ДО:
+```javascript
+scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+```
+
+### ПОСЛЕ:
+```javascript
+scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
+```
+
+## Полная CSP конфигурация
+```javascript
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+```
+
+## Результат
+✅ AdminLTE 3 теперь работает полностью:
+- ✅ CSS стили загружаются
+- ✅ jQuery загружается без ошибок CSP
+- ✅ Bootstrap загружается без ошибок CSP
+- ✅ AdminLTE JavaScript функции работают
+- ✅ Интерактивные элементы функциональны
+
+## Тестирование
+После исправления CSP:
+1. Перезапустить сервер: `npm start`
+2. Открыть админку: `http://localhost:3000/admin`
+3. Проверить консоль браузера - ошибки CSP исчезли
+4. Проверить функциональность: навигация, выпадающие меню, модальные окна
+
+## Примечания
+- Исправление обеспечивает безопасность через CSP при разрешении необходимых CDN
+- AdminLTE теперь полностью функциональна с корейской локализацией
+- Все зависимости загружаются правильно
\ No newline at end of file
diff --git a/.history/ADMINLTE_CSP_FIX_20251026213623.md b/.history/ADMINLTE_CSP_FIX_20251026213623.md
new file mode 100644
index 0000000..2d4045c
--- /dev/null
+++ b/.history/ADMINLTE_CSP_FIX_20251026213623.md
@@ -0,0 +1,63 @@
+# AdminLTE CSP Fix - Исправление Content Security Policy
+
+## Проблема
+Content Security Policy (CSP) блокировал загрузку jQuery и Bootstrap из CDN, что приводило к ошибкам:
+
+```
+Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://code.jquery.com/jquery-3.6.0.min.js
+Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js
+```
+
+## Причина
+В server.js в настройках helmet CSP для `scriptSrc` отсутствовали домены:
+- `https://code.jquery.com` (для jQuery)
+- `https://cdn.jsdelivr.net` (для Bootstrap)
+
+## Решение
+Обновлены CSP настройки в `/server.js`:
+
+### ДО:
+```javascript
+scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+```
+
+### ПОСЛЕ:
+```javascript
+scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
+```
+
+## Полная CSP конфигурация
+```javascript
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+```
+
+## Результат
+✅ AdminLTE 3 теперь работает полностью:
+- ✅ CSS стили загружаются
+- ✅ jQuery загружается без ошибок CSP
+- ✅ Bootstrap загружается без ошибок CSP
+- ✅ AdminLTE JavaScript функции работают
+- ✅ Интерактивные элементы функциональны
+
+## Тестирование
+После исправления CSP:
+1. Перезапустить сервер: `npm start`
+2. Открыть админку: `http://localhost:3000/admin`
+3. Проверить консоль браузера - ошибки CSP исчезли
+4. Проверить функциональность: навигация, выпадающие меню, модальные окна
+
+## Примечания
+- Исправление обеспечивает безопасность через CSP при разрешении необходимых CDN
+- AdminLTE теперь полностью функциональна с корейской локализацией
+- Все зависимости загружаются правильно
\ No newline at end of file
diff --git a/.history/ADMINLTE_SETUP_COMPLETE_20251026212843.md b/.history/ADMINLTE_SETUP_COMPLETE_20251026212843.md
new file mode 100644
index 0000000..2caca7a
--- /dev/null
+++ b/.history/ADMINLTE_SETUP_COMPLETE_20251026212843.md
@@ -0,0 +1,109 @@
+# SmartSolTech AdminLTE 3 - Документация
+
+## ✅ Успешно настроен AdminLTE 3!
+
+AdminLTE 3 теперь полностью интегрирован в ваш сайт SmartSolTech как основная админ-панель.
+
+## 🎯 Что было сделано:
+
+### 1. **Полная замена старой админки**
+- ❌ Удалена старая Tailwind-админка
+- ❌ Удалены все демо-файлы и сравнения
+- ❌ Удален Tabler и связанные файлы
+- ✅ AdminLTE 3 стал основной админкой
+
+### 2. **Новый дизайн и функциональность**
+- 🎨 **Корейская локализация** - все элементы переведены
+- 📊 **Современный dashboard** с информативными карточками
+- 🔢 **Статистика в sidebar** - счетчики портфолио, услуг, сообщений
+- 📱 **Адаптивный дизайн** для всех устройств
+- 🎭 **Профессиональная цветовая схема**
+
+### 3. **Интеграция с существующей системой**
+- 🔗 **Полная совместимость** с текущими маршрутами
+- 📊 **Middleware для статистики** на всех страницах
+- 🔐 **Сохранена вся авторизация** и безопасность
+- 💾 **Все данные остались** без изменений
+
+## 🎨 Основные возможности AdminLTE:
+
+### Dashboard (대시보드)
+- 📈 **Статистические карточки** с progress bar
+- 📋 **Последние проекты** с статусами (완료/진행중/계획)
+- 📧 **Последние сообщения** с цветными статусами
+- ⚡ **Быстрые действия** с градиентными кнопками
+- 📊 **Системный статус** и метрики
+
+### Навигация
+- 🎯 **Активные индикаторы** текущей страницы
+- 🔢 **Счетчики в sidebar** для каждого раздела
+- 🌟 **Группировка разделов** (основные + системные)
+- 📱 **Складное меню** для мобильных
+
+### Дизайн
+- 🇰🇷 **Шрифт Noto Sans KR** для корейского текста
+- 🎨 **Градиентные элементы** и современные цвета
+- 💎 **Закругленные карточки** с тенями
+- ⚡ **Плавные анимации** и переходы
+
+## 🔧 Техническая информация:
+
+### Файлы
+- `views/admin/layout.ejs` - Основной layout AdminLTE
+- `views/admin/dashboard.ejs` - Новый dashboard
+- `routes/admin.js` - Обновленные маршруты с middleware
+
+### CSS & JS
+- AdminLTE 3 CSS из `node_modules/admin-lte/dist/css/`
+- jQuery + Bootstrap 4 для функциональности
+- Font Awesome 6 для иконок
+- Custom CSS для корейских шрифтов
+
+### Middleware
+```javascript
+// Автоматически добавляет статистику на все админ-страницы
+const addStats = async (req, res, next) => {
+ // portfolioCount, servicesCount, contactsCount
+}
+```
+
+## 🚀 Доступные URL:
+
+- **Админ-панель:** http://localhost:3000/admin
+- **Dashboard:** http://localhost:3000/admin/dashboard
+- **Портфолио:** http://localhost:3000/admin/portfolio
+- **Услуги:** http://localhost:3000/admin/services
+- **Сообщения:** http://localhost:3000/admin/contacts
+- **Медиа:** http://localhost:3000/admin/media
+- **Настройки:** http://localhost:3000/admin/settings
+- **Телеграм:** http://localhost:3000/admin/telegram
+- **Баннеры:** http://localhost:3000/admin/banner-editor
+
+## 💡 Преимущества новой админки:
+
+1. **Профессиональный вид** - соответствует корейским стандартам
+2. **Удобство использования** - интуитивный интерфейс
+3. **Производительность** - быстрая загрузка
+4. **Мобильность** - отлично работает на телефонах
+5. **Стабильность** - проверенное решение
+6. **Бесплатность** - никаких лицензионных ограничений
+
+## 🎯 Что дальше?
+
+AdminLTE 3 полностью готов к работе! Можете:
+
+1. **Войти в админку** по адресу `/admin`
+2. **Управлять контентом** через новый интерфейс
+3. **Добавлять проекты** и услуги
+4. **Настраивать сайт** через удобные формы
+5. **Следить за статистикой** в реальном времени
+
+## 🔑 Логин:
+- **Email:** admin@smartsoltech.kr
+- **Пароль:** как настроено в .env
+
+---
+
+**🎉 AdminLTE 3 успешно настроен и готов к использованию!**
+
+Старая админка полностью удалена, новая AdminLTE интегрирована и работает со всеми существующими функциями сайта.
\ No newline at end of file
diff --git a/.history/ADMINLTE_SETUP_COMPLETE_20251026212848.md b/.history/ADMINLTE_SETUP_COMPLETE_20251026212848.md
new file mode 100644
index 0000000..2caca7a
--- /dev/null
+++ b/.history/ADMINLTE_SETUP_COMPLETE_20251026212848.md
@@ -0,0 +1,109 @@
+# SmartSolTech AdminLTE 3 - Документация
+
+## ✅ Успешно настроен AdminLTE 3!
+
+AdminLTE 3 теперь полностью интегрирован в ваш сайт SmartSolTech как основная админ-панель.
+
+## 🎯 Что было сделано:
+
+### 1. **Полная замена старой админки**
+- ❌ Удалена старая Tailwind-админка
+- ❌ Удалены все демо-файлы и сравнения
+- ❌ Удален Tabler и связанные файлы
+- ✅ AdminLTE 3 стал основной админкой
+
+### 2. **Новый дизайн и функциональность**
+- 🎨 **Корейская локализация** - все элементы переведены
+- 📊 **Современный dashboard** с информативными карточками
+- 🔢 **Статистика в sidebar** - счетчики портфолио, услуг, сообщений
+- 📱 **Адаптивный дизайн** для всех устройств
+- 🎭 **Профессиональная цветовая схема**
+
+### 3. **Интеграция с существующей системой**
+- 🔗 **Полная совместимость** с текущими маршрутами
+- 📊 **Middleware для статистики** на всех страницах
+- 🔐 **Сохранена вся авторизация** и безопасность
+- 💾 **Все данные остались** без изменений
+
+## 🎨 Основные возможности AdminLTE:
+
+### Dashboard (대시보드)
+- 📈 **Статистические карточки** с progress bar
+- 📋 **Последние проекты** с статусами (완료/진행중/계획)
+- 📧 **Последние сообщения** с цветными статусами
+- ⚡ **Быстрые действия** с градиентными кнопками
+- 📊 **Системный статус** и метрики
+
+### Навигация
+- 🎯 **Активные индикаторы** текущей страницы
+- 🔢 **Счетчики в sidebar** для каждого раздела
+- 🌟 **Группировка разделов** (основные + системные)
+- 📱 **Складное меню** для мобильных
+
+### Дизайн
+- 🇰🇷 **Шрифт Noto Sans KR** для корейского текста
+- 🎨 **Градиентные элементы** и современные цвета
+- 💎 **Закругленные карточки** с тенями
+- ⚡ **Плавные анимации** и переходы
+
+## 🔧 Техническая информация:
+
+### Файлы
+- `views/admin/layout.ejs` - Основной layout AdminLTE
+- `views/admin/dashboard.ejs` - Новый dashboard
+- `routes/admin.js` - Обновленные маршруты с middleware
+
+### CSS & JS
+- AdminLTE 3 CSS из `node_modules/admin-lte/dist/css/`
+- jQuery + Bootstrap 4 для функциональности
+- Font Awesome 6 для иконок
+- Custom CSS для корейских шрифтов
+
+### Middleware
+```javascript
+// Автоматически добавляет статистику на все админ-страницы
+const addStats = async (req, res, next) => {
+ // portfolioCount, servicesCount, contactsCount
+}
+```
+
+## 🚀 Доступные URL:
+
+- **Админ-панель:** http://localhost:3000/admin
+- **Dashboard:** http://localhost:3000/admin/dashboard
+- **Портфолио:** http://localhost:3000/admin/portfolio
+- **Услуги:** http://localhost:3000/admin/services
+- **Сообщения:** http://localhost:3000/admin/contacts
+- **Медиа:** http://localhost:3000/admin/media
+- **Настройки:** http://localhost:3000/admin/settings
+- **Телеграм:** http://localhost:3000/admin/telegram
+- **Баннеры:** http://localhost:3000/admin/banner-editor
+
+## 💡 Преимущества новой админки:
+
+1. **Профессиональный вид** - соответствует корейским стандартам
+2. **Удобство использования** - интуитивный интерфейс
+3. **Производительность** - быстрая загрузка
+4. **Мобильность** - отлично работает на телефонах
+5. **Стабильность** - проверенное решение
+6. **Бесплатность** - никаких лицензионных ограничений
+
+## 🎯 Что дальше?
+
+AdminLTE 3 полностью готов к работе! Можете:
+
+1. **Войти в админку** по адресу `/admin`
+2. **Управлять контентом** через новый интерфейс
+3. **Добавлять проекты** и услуги
+4. **Настраивать сайт** через удобные формы
+5. **Следить за статистикой** в реальном времени
+
+## 🔑 Логин:
+- **Email:** admin@smartsoltech.kr
+- **Пароль:** как настроено в .env
+
+---
+
+**🎉 AdminLTE 3 успешно настроен и готов к использованию!**
+
+Старая админка полностью удалена, новая AdminLTE интегрирована и работает со всеми существующими функциями сайта.
\ No newline at end of file
diff --git a/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026211919.md b/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026211919.md
new file mode 100644
index 0000000..e69de29
diff --git a/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026211921.md b/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026211921.md
new file mode 100644
index 0000000..f1de9f1
--- /dev/null
+++ b/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026211921.md
@@ -0,0 +1,145 @@
+# SmartSolTech Admin Bundle Рекомендации
+
+## 🎯 Итоговый Выбор: AdminLTE 3
+
+После детального анализа и тестирования обеих админ-панелей, **AdminLTE 3** является оптимальным выбором для SmartSolTech.
+
+## 📊 Анализ Установленных Решений
+
+### 1. AdminLTE 3 ✅ (Рекомендуется)
+
+**Установка:** `npm install admin-lte`
+
+**Преимущества:**
+- ✅ **Полностью бесплатен** - нет скрытых платежей
+- ✅ **Отличная поддержка корейского языка** - идеально для вашей аудитории
+- ✅ **Богатая экосистема компонентов** - все необходимые элементы UI
+- ✅ **Стабильность** - проверенное временем решение
+- ✅ **Большое сообщество** - легко найти помощь и примеры
+- ✅ **jQuery совместимость** - работает с существующим кодом
+
+**Интеграция:**
+```javascript
+// Уже создано:
+/views/admin/layout-adminlte.ejs // Основной layout
+/views/admin/dashboard-adminlte.ejs // Dashboard с корейской локализацией
+/demo/demo-adminlte // Демо URL
+```
+
+### 2. Tabler ⚠️ (Альтернатива)
+
+**Установка:** `npm install @tabler/core @tabler/icons`
+
+**Преимущества:**
+- ✅ **Современный дизайн** - более свежий внешний вид
+- ✅ **Высокая производительность** - быстрая загрузка
+- ✅ **3000+ SVG иконок** - богатая иконография
+- ✅ **Bootstrap 5** - современные стандарты
+
+**Недостатки:**
+- ⚠️ **Требует Node.js 20+** (у вас 18.19.1)
+- ⚠️ **Менее зрелое решение** - меньше документации на корейском
+- ⚠️ **Меньше готовых компонентов** - больше кастомизации
+
+## 🔧 Готовые к Использованию URL
+
+После запуска сервера доступны:
+
+1. **Страница сравнения:** http://localhost:3000/demo/admin-comparison
+2. **AdminLTE Demo:** http://localhost:3000/demo/demo-adminlte
+3. **Tabler Demo:** http://localhost:3000/demo/demo-tabler
+
+## 🚀 Интеграция с Существующей Админкой
+
+### Вариант 1: Постепенная Миграция (Рекомендуется)
+
+1. **Обновить существующий layout:**
+```bash
+# Заменить существующий layout на AdminLTE
+cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
+```
+
+2. **Обновить dashboard:**
+```bash
+# Заменить dashboard на AdminLTE версию
+cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
+```
+
+### Вариант 2: Параллельная Система
+
+Оставить существующую админку и добавить новую через роуты:
+```javascript
+// В routes/admin.js добавить:
+router.get('/modern', (req, res) => {
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte'
+ // ... остальные данные
+ });
+});
+```
+
+## 🎨 Кастомизация под SmartSolTech
+
+### Цветовая Схема
+```css
+/* Добавлено в layout-adminlte.ejs: */
+.nav-sidebar .nav-link.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.brand-text {
+ color: #007bff !important;
+}
+```
+
+### Корейская Локализация
+- ✅ Все меню переведены на корейский
+- ✅ Даты в корейском формате
+- ✅ Статусы и сообщения на корейском
+- ✅ Шрифт Noto Sans KR для корректного отображения
+
+## 📋 Следующие Шаги
+
+1. **Тестирование:**
+ ```bash
+ # Откройте демо:
+ http://localhost:3000/demo/admin-comparison
+ ```
+
+2. **Интеграция:**
+ ```bash
+ # Если решили использовать AdminLTE:
+ cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
+ cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
+ ```
+
+3. **Создание дополнительных страниц:**
+ - Список портфолио в стиле AdminLTE
+ - Управление услугами
+ - Страница контактов
+ - Настройки системы
+
+## 💡 Дополнительные Рекомендации
+
+### Для Улучшения UX:
+1. **Темная тема** - AdminLTE поддерживает переключение тем
+2. **Мобильная оптимизация** - уже включена
+3. **Уведомления** - можно добавить toast-уведомления
+4. **Виджеты** - использовать info-box для статистики
+
+### Производительность:
+- AdminLTE загружается быстро (~500KB)
+- Минимальные зависимости
+- Отличная совместимость с вашим стеком
+
+## 🎯 Финальная Рекомендация
+
+**Используйте AdminLTE 3** для SmartSolTech потому что:
+
+1. **Готов к production** - стабилен и протестирован
+2. **Корейская аудитория** - отличная поддержка языка
+3. **Бесплатный** - нет лицензионных ограничений
+4. **Совместимость** - работает с вашей текущей архитектурой
+5. **Поддержка** - большое сообщество и документация
+
+Начните с демо-версии по адресу `/demo/demo-adminlte` и при желании интегрируйте в основную админку!
\ No newline at end of file
diff --git a/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026212002.md b/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026212002.md
new file mode 100644
index 0000000..f1de9f1
--- /dev/null
+++ b/.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026212002.md
@@ -0,0 +1,145 @@
+# SmartSolTech Admin Bundle Рекомендации
+
+## 🎯 Итоговый Выбор: AdminLTE 3
+
+После детального анализа и тестирования обеих админ-панелей, **AdminLTE 3** является оптимальным выбором для SmartSolTech.
+
+## 📊 Анализ Установленных Решений
+
+### 1. AdminLTE 3 ✅ (Рекомендуется)
+
+**Установка:** `npm install admin-lte`
+
+**Преимущества:**
+- ✅ **Полностью бесплатен** - нет скрытых платежей
+- ✅ **Отличная поддержка корейского языка** - идеально для вашей аудитории
+- ✅ **Богатая экосистема компонентов** - все необходимые элементы UI
+- ✅ **Стабильность** - проверенное временем решение
+- ✅ **Большое сообщество** - легко найти помощь и примеры
+- ✅ **jQuery совместимость** - работает с существующим кодом
+
+**Интеграция:**
+```javascript
+// Уже создано:
+/views/admin/layout-adminlte.ejs // Основной layout
+/views/admin/dashboard-adminlte.ejs // Dashboard с корейской локализацией
+/demo/demo-adminlte // Демо URL
+```
+
+### 2. Tabler ⚠️ (Альтернатива)
+
+**Установка:** `npm install @tabler/core @tabler/icons`
+
+**Преимущества:**
+- ✅ **Современный дизайн** - более свежий внешний вид
+- ✅ **Высокая производительность** - быстрая загрузка
+- ✅ **3000+ SVG иконок** - богатая иконография
+- ✅ **Bootstrap 5** - современные стандарты
+
+**Недостатки:**
+- ⚠️ **Требует Node.js 20+** (у вас 18.19.1)
+- ⚠️ **Менее зрелое решение** - меньше документации на корейском
+- ⚠️ **Меньше готовых компонентов** - больше кастомизации
+
+## 🔧 Готовые к Использованию URL
+
+После запуска сервера доступны:
+
+1. **Страница сравнения:** http://localhost:3000/demo/admin-comparison
+2. **AdminLTE Demo:** http://localhost:3000/demo/demo-adminlte
+3. **Tabler Demo:** http://localhost:3000/demo/demo-tabler
+
+## 🚀 Интеграция с Существующей Админкой
+
+### Вариант 1: Постепенная Миграция (Рекомендуется)
+
+1. **Обновить существующий layout:**
+```bash
+# Заменить существующий layout на AdminLTE
+cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
+```
+
+2. **Обновить dashboard:**
+```bash
+# Заменить dashboard на AdminLTE версию
+cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
+```
+
+### Вариант 2: Параллельная Система
+
+Оставить существующую админку и добавить новую через роуты:
+```javascript
+// В routes/admin.js добавить:
+router.get('/modern', (req, res) => {
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte'
+ // ... остальные данные
+ });
+});
+```
+
+## 🎨 Кастомизация под SmartSolTech
+
+### Цветовая Схема
+```css
+/* Добавлено в layout-adminlte.ejs: */
+.nav-sidebar .nav-link.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.brand-text {
+ color: #007bff !important;
+}
+```
+
+### Корейская Локализация
+- ✅ Все меню переведены на корейский
+- ✅ Даты в корейском формате
+- ✅ Статусы и сообщения на корейском
+- ✅ Шрифт Noto Sans KR для корректного отображения
+
+## 📋 Следующие Шаги
+
+1. **Тестирование:**
+ ```bash
+ # Откройте демо:
+ http://localhost:3000/demo/admin-comparison
+ ```
+
+2. **Интеграция:**
+ ```bash
+ # Если решили использовать AdminLTE:
+ cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
+ cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
+ ```
+
+3. **Создание дополнительных страниц:**
+ - Список портфолио в стиле AdminLTE
+ - Управление услугами
+ - Страница контактов
+ - Настройки системы
+
+## 💡 Дополнительные Рекомендации
+
+### Для Улучшения UX:
+1. **Темная тема** - AdminLTE поддерживает переключение тем
+2. **Мобильная оптимизация** - уже включена
+3. **Уведомления** - можно добавить toast-уведомления
+4. **Виджеты** - использовать info-box для статистики
+
+### Производительность:
+- AdminLTE загружается быстро (~500KB)
+- Минимальные зависимости
+- Отличная совместимость с вашим стеком
+
+## 🎯 Финальная Рекомендация
+
+**Используйте AdminLTE 3** для SmartSolTech потому что:
+
+1. **Готов к production** - стабилен и протестирован
+2. **Корейская аудитория** - отличная поддержка языка
+3. **Бесплатный** - нет лицензионных ограничений
+4. **Совместимость** - работает с вашей текущей архитектурой
+5. **Поддержка** - большое сообщество и документация
+
+Начните с демо-версии по адресу `/demo/demo-adminlte` и при желании интегрируйте в основную админку!
\ No newline at end of file
diff --git a/.history/models/Service_20251026221128.js b/.history/models/Service_20251026221128.js
new file mode 100644
index 0000000..366500e
--- /dev/null
+++ b/.history/models/Service_20251026221128.js
@@ -0,0 +1,103 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: false
+ },
+ shortDescription: {
+ type: DataTypes.STRING(150),
+ allowNull: false
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: false,
+ validate: {
+ isValidPricing(value) {
+ if (!value.basePrice || value.basePrice < 0) {
+ throw new Error('Base price must be a positive number');
+ }
+ }
+ }
+ },
+ estimatedTime: {
+ type: DataTypes.JSONB,
+ allowNull: false,
+ validate: {
+ isValidTime(value) {
+ if (!value.min || !value.max || value.min > value.max) {
+ throw new Error('Invalid estimated time range');
+ }
+ }
+ }
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.ARRAY(DataTypes.STRING),
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/models/Service_20251026221134.js b/.history/models/Service_20251026221134.js
new file mode 100644
index 0000000..c8baaa7
--- /dev/null
+++ b/.history/models/Service_20251026221134.js
@@ -0,0 +1,96 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: false
+ },
+ shortDescription: {
+ type: DataTypes.STRING(150),
+ allowNull: false
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: false,
+ validate: {
+ isValidPricing(value) {
+ if (!value.basePrice || value.basePrice < 0) {
+ throw new Error('Base price must be a positive number');
+ }
+ }
+ }
+ },
+ estimatedTime: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.ARRAY(DataTypes.STRING),
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/models/Service_20251026221140.js b/.history/models/Service_20251026221140.js
new file mode 100644
index 0000000..2e29fdc
--- /dev/null
+++ b/.history/models/Service_20251026221140.js
@@ -0,0 +1,96 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: false
+ },
+ shortDescription: {
+ type: DataTypes.STRING(150),
+ allowNull: false
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: false,
+ validate: {
+ isValidPricing(value) {
+ if (!value.basePrice || value.basePrice < 0) {
+ throw new Error('Base price must be a positive number');
+ }
+ }
+ }
+ },
+ estimatedTime: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/models/Service_20251026221146.js b/.history/models/Service_20251026221146.js
new file mode 100644
index 0000000..b9a2c8d
--- /dev/null
+++ b/.history/models/Service_20251026221146.js
@@ -0,0 +1,90 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: false
+ },
+ shortDescription: {
+ type: DataTypes.STRING(150),
+ allowNull: false
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ defaultValue: {}
+ },
+ estimatedTime: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/models/Service_20251026221152.js b/.history/models/Service_20251026221152.js
new file mode 100644
index 0000000..ceed2ae
--- /dev/null
+++ b/.history/models/Service_20251026221152.js
@@ -0,0 +1,90 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: false
+ },
+ shortDescription: {
+ type: DataTypes.STRING(500),
+ allowNull: true
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ defaultValue: {}
+ },
+ estimatedTime: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/models/Service_20251026221158.js b/.history/models/Service_20251026221158.js
new file mode 100644
index 0000000..bf0cdd7
--- /dev/null
+++ b/.history/models/Service_20251026221158.js
@@ -0,0 +1,90 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
+ shortDescription: {
+ type: DataTypes.STRING(500),
+ allowNull: true
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ defaultValue: {}
+ },
+ estimatedTime: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/models/Service_20251026221203.js b/.history/models/Service_20251026221203.js
new file mode 100644
index 0000000..ddf34a6
--- /dev/null
+++ b/.history/models/Service_20251026221203.js
@@ -0,0 +1,91 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
+ shortDescription: {
+ type: DataTypes.STRING(500),
+ allowNull: true
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ defaultValue: 'fas fa-cog'
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ defaultValue: {}
+ },
+ estimatedTime: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/models/Service_20251026221317.js b/.history/models/Service_20251026221317.js
new file mode 100644
index 0000000..ddf34a6
--- /dev/null
+++ b/.history/models/Service_20251026221317.js
@@ -0,0 +1,91 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Service = sequelize.define('Service', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ len: [1, 255]
+ },
+ set(value) {
+ this.setDataValue('name', value.trim());
+ }
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
+ shortDescription: {
+ type: DataTypes.STRING(500),
+ allowNull: true
+ },
+ icon: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ defaultValue: 'fas fa-cog'
+ },
+ category: {
+ type: DataTypes.ENUM(
+ 'web-development',
+ 'mobile-development',
+ 'ui-ux-design',
+ 'consulting',
+ 'support',
+ 'other'
+ ),
+ allowNull: false
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ pricing: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ defaultValue: {}
+ },
+ estimatedTime: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ isActive: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true
+ },
+ featured: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false
+ },
+ order: {
+ type: DataTypes.INTEGER,
+ defaultValue: 0
+ },
+ tags: {
+ type: DataTypes.JSONB,
+ defaultValue: []
+ },
+ seo: {
+ type: DataTypes.JSONB,
+ defaultValue: {}
+ }
+}, {
+ tableName: 'services',
+ timestamps: true,
+ indexes: [
+ {
+ fields: ['category', 'featured', 'order']
+ },
+ {
+ type: 'gin',
+ fields: ['tags']
+ }
+ ]
+});
+
+module.exports = Service;
\ No newline at end of file
diff --git a/.history/public/css/enhanced-animations_20251026210710.css b/.history/public/css/enhanced-animations_20251026210710.css
new file mode 100644
index 0000000..e239a26
--- /dev/null
+++ b/.history/public/css/enhanced-animations_20251026210710.css
@@ -0,0 +1,203 @@
+/* Enhanced Animations for SmartSolTech */
+
+/* Gradient Animation */
+@keyframes gradient-x {
+ 0%, 100% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+}
+
+.animate-gradient-x {
+ background-size: 200% 200%;
+ animation: gradient-x 4s ease infinite;
+}
+
+/* Enhanced Blob Animation */
+@keyframes blob {
+ 0% {
+ transform: translate(0px, 0px) scale(1);
+ }
+ 33% {
+ transform: translate(30px, -50px) scale(1.1);
+ }
+ 66% {
+ transform: translate(-20px, 20px) scale(0.9);
+ }
+ 100% {
+ transform: translate(0px, 0px) scale(1);
+ }
+}
+
+.animate-blob {
+ animation: blob 7s infinite;
+}
+
+.animation-delay-2000 {
+ animation-delay: 2s;
+}
+
+.animation-delay-4000 {
+ animation-delay: 4s;
+}
+
+.animation-delay-6000 {
+ animation-delay: 6s;
+}
+
+/* Pulse Animation with Delay */
+@keyframes pulse-delayed {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.animate-pulse-delayed {
+ animation: pulse-delayed 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+/* Floating Animation */
+@keyframes float {
+ 0%, 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+}
+
+.animate-float {
+ animation: float 3s ease-in-out infinite;
+}
+
+/* Card Hover Effects */
+.card-hover {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.card-hover:hover {
+ transform: translateY(-8px);
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+/* Background Pattern */
+.bg-pattern {
+ background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.15) 1px, transparent 0);
+ background-size: 20px 20px;
+}
+
+/* Glowing Effect */
+.glow {
+ box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
+}
+
+.glow-purple {
+ box-shadow: 0 0 20px rgba(139, 92, 246, 0.5);
+}
+
+.glow-green {
+ box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
+}
+
+.glow-orange {
+ box-shadow: 0 0 20px rgba(251, 146, 60, 0.5);
+}
+
+/* Hero Section Specific */
+.hero-section {
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(1200px 600px at 10% -10%, rgba(99, 102, 241, 0.35), transparent 60%),
+ radial-gradient(1000px 500px at 110% 10%, rgba(168, 85, 247, 0.35), transparent 60%);
+ pointer-events: none;
+ z-index: 1;
+}
+
+/* Glass Effect */
+.glass-effect {
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* Smooth Transitions */
+.smooth-transition {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Text Shimmer Effect */
+@keyframes shimmer {
+ 0% {
+ background-position: -200% center;
+ }
+ 100% {
+ background-position: 200% center;
+ }
+}
+
+.text-shimmer {
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
+ background-size: 200% 100%;
+ animation: shimmer 2s infinite;
+ background-clip: text;
+ -webkit-background-clip: text;
+}
+
+/* Button Hover Effects */
+.btn-enhanced {
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.btn-enhanced::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+ transition: left 0.5s;
+}
+
+.btn-enhanced:hover::before {
+ left: 100%;
+}
+
+/* Responsive Adjustments */
+@media (max-width: 768px) {
+ .animate-blob {
+ animation-duration: 10s;
+ }
+
+ .card-hover:hover {
+ transform: translateY(-4px);
+ }
+}
+
+/* Dark Mode Adjustments */
+.dark .glass-effect {
+ background: rgba(0, 0, 0, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.dark .bg-pattern {
+ background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.1) 1px, transparent 0);
+}
\ No newline at end of file
diff --git a/.history/public/css/enhanced-animations_20251026210722.css b/.history/public/css/enhanced-animations_20251026210722.css
new file mode 100644
index 0000000..e239a26
--- /dev/null
+++ b/.history/public/css/enhanced-animations_20251026210722.css
@@ -0,0 +1,203 @@
+/* Enhanced Animations for SmartSolTech */
+
+/* Gradient Animation */
+@keyframes gradient-x {
+ 0%, 100% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+}
+
+.animate-gradient-x {
+ background-size: 200% 200%;
+ animation: gradient-x 4s ease infinite;
+}
+
+/* Enhanced Blob Animation */
+@keyframes blob {
+ 0% {
+ transform: translate(0px, 0px) scale(1);
+ }
+ 33% {
+ transform: translate(30px, -50px) scale(1.1);
+ }
+ 66% {
+ transform: translate(-20px, 20px) scale(0.9);
+ }
+ 100% {
+ transform: translate(0px, 0px) scale(1);
+ }
+}
+
+.animate-blob {
+ animation: blob 7s infinite;
+}
+
+.animation-delay-2000 {
+ animation-delay: 2s;
+}
+
+.animation-delay-4000 {
+ animation-delay: 4s;
+}
+
+.animation-delay-6000 {
+ animation-delay: 6s;
+}
+
+/* Pulse Animation with Delay */
+@keyframes pulse-delayed {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.animate-pulse-delayed {
+ animation: pulse-delayed 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+/* Floating Animation */
+@keyframes float {
+ 0%, 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+}
+
+.animate-float {
+ animation: float 3s ease-in-out infinite;
+}
+
+/* Card Hover Effects */
+.card-hover {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.card-hover:hover {
+ transform: translateY(-8px);
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+/* Background Pattern */
+.bg-pattern {
+ background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.15) 1px, transparent 0);
+ background-size: 20px 20px;
+}
+
+/* Glowing Effect */
+.glow {
+ box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
+}
+
+.glow-purple {
+ box-shadow: 0 0 20px rgba(139, 92, 246, 0.5);
+}
+
+.glow-green {
+ box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
+}
+
+.glow-orange {
+ box-shadow: 0 0 20px rgba(251, 146, 60, 0.5);
+}
+
+/* Hero Section Specific */
+.hero-section {
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(1200px 600px at 10% -10%, rgba(99, 102, 241, 0.35), transparent 60%),
+ radial-gradient(1000px 500px at 110% 10%, rgba(168, 85, 247, 0.35), transparent 60%);
+ pointer-events: none;
+ z-index: 1;
+}
+
+/* Glass Effect */
+.glass-effect {
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* Smooth Transitions */
+.smooth-transition {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Text Shimmer Effect */
+@keyframes shimmer {
+ 0% {
+ background-position: -200% center;
+ }
+ 100% {
+ background-position: 200% center;
+ }
+}
+
+.text-shimmer {
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
+ background-size: 200% 100%;
+ animation: shimmer 2s infinite;
+ background-clip: text;
+ -webkit-background-clip: text;
+}
+
+/* Button Hover Effects */
+.btn-enhanced {
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.btn-enhanced::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+ transition: left 0.5s;
+}
+
+.btn-enhanced:hover::before {
+ left: 100%;
+}
+
+/* Responsive Adjustments */
+@media (max-width: 768px) {
+ .animate-blob {
+ animation-duration: 10s;
+ }
+
+ .card-hover:hover {
+ transform: translateY(-4px);
+ }
+}
+
+/* Dark Mode Adjustments */
+.dark .glass-effect {
+ background: rgba(0, 0, 0, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.dark .bg-pattern {
+ background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.1) 1px, transparent 0);
+}
\ No newline at end of file
diff --git a/.history/public/images/services/branding_20251026180829.svg b/.history/public/images/services/branding_20251026180829.svg
new file mode 100644
index 0000000..1468c63
--- /dev/null
+++ b/.history/public/images/services/branding_20251026180829.svg
@@ -0,0 +1,55 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/branding_20251026181004.svg b/.history/public/images/services/branding_20251026181004.svg
new file mode 100644
index 0000000..1468c63
--- /dev/null
+++ b/.history/public/images/services/branding_20251026181004.svg
@@ -0,0 +1,55 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/consulting_20251026180856.svg b/.history/public/images/services/consulting_20251026180856.svg
new file mode 100644
index 0000000..546322d
--- /dev/null
+++ b/.history/public/images/services/consulting_20251026180856.svg
@@ -0,0 +1,69 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/consulting_20251026181004.svg b/.history/public/images/services/consulting_20251026181004.svg
new file mode 100644
index 0000000..546322d
--- /dev/null
+++ b/.history/public/images/services/consulting_20251026181004.svg
@@ -0,0 +1,69 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/default_20251026180915.svg b/.history/public/images/services/default_20251026180915.svg
new file mode 100644
index 0000000..1cffcbd
--- /dev/null
+++ b/.history/public/images/services/default_20251026180915.svg
@@ -0,0 +1,52 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/default_20251026181004.svg b/.history/public/images/services/default_20251026181004.svg
new file mode 100644
index 0000000..1cffcbd
--- /dev/null
+++ b/.history/public/images/services/default_20251026181004.svg
@@ -0,0 +1,52 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/digital-marketing_20251026180806.svg b/.history/public/images/services/digital-marketing_20251026180806.svg
new file mode 100644
index 0000000..d0e3861
--- /dev/null
+++ b/.history/public/images/services/digital-marketing_20251026180806.svg
@@ -0,0 +1,56 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/digital-marketing_20251026181004.svg b/.history/public/images/services/digital-marketing_20251026181004.svg
new file mode 100644
index 0000000..d0e3861
--- /dev/null
+++ b/.history/public/images/services/digital-marketing_20251026181004.svg
@@ -0,0 +1,56 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/mobile-app_20251026180728.svg b/.history/public/images/services/mobile-app_20251026180728.svg
new file mode 100644
index 0000000..a357dce
--- /dev/null
+++ b/.history/public/images/services/mobile-app_20251026180728.svg
@@ -0,0 +1,43 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/mobile-app_20251026181004.svg b/.history/public/images/services/mobile-app_20251026181004.svg
new file mode 100644
index 0000000..a357dce
--- /dev/null
+++ b/.history/public/images/services/mobile-app_20251026181004.svg
@@ -0,0 +1,43 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/ui-ux-design_20251026180746.svg b/.history/public/images/services/ui-ux-design_20251026180746.svg
new file mode 100644
index 0000000..899c22e
--- /dev/null
+++ b/.history/public/images/services/ui-ux-design_20251026180746.svg
@@ -0,0 +1,47 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/ui-ux-design_20251026181004.svg b/.history/public/images/services/ui-ux-design_20251026181004.svg
new file mode 100644
index 0000000..899c22e
--- /dev/null
+++ b/.history/public/images/services/ui-ux-design_20251026181004.svg
@@ -0,0 +1,47 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/web-development_20251026180712.svg b/.history/public/images/services/web-development_20251026180712.svg
new file mode 100644
index 0000000..da73055
--- /dev/null
+++ b/.history/public/images/services/web-development_20251026180712.svg
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/.history/public/images/services/web-development_20251026181004.svg b/.history/public/images/services/web-development_20251026181004.svg
new file mode 100644
index 0000000..da73055
--- /dev/null
+++ b/.history/public/images/services/web-development_20251026181004.svg
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/.history/public/sw_20251026215851.js b/.history/public/sw_20251026215851.js
new file mode 100644
index 0000000..f37048c
--- /dev/null
+++ b/.history/public/sw_20251026215851.js
@@ -0,0 +1,414 @@
+// Service Worker for SmartSolTech PWA
+const CACHE_NAME = 'smartsoltech-v1.0.2';
+const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.2';
+const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.2';
+
+// Files to cache immediately
+const STATIC_FILES = [
+ '/',
+ '/css/main.css',
+ '/css/fixes.css',
+ '/css/dark-theme.css',
+ '/js/main.js',
+ '/vendor/jquery/jquery-3.6.0.min.js',
+ '/vendor/bootstrap/bootstrap.bundle.min.js',
+ '/vendor/bootstrap/bootstrap.min.css',
+ '/vendor/adminlte/adminlte.min.js',
+ '/vendor/adminlte/adminlte.min.css',
+ '/vendor/adminlte/fontawesome.min.css',
+ '/images/logo.png',
+ '/images/icon-192x192.png',
+ '/images/icon-144x144.png',
+ '/manifest.json',
+ 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
+ 'https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap',
+ 'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css',
+ 'https://unpkg.com/aos@2.3.1/dist/aos.css',
+ 'https://unpkg.com/aos@2.3.1/dist/aos.js'
+];
+
+// Routes to cache dynamically
+const DYNAMIC_ROUTES = [
+ '/about',
+ '/services',
+ '/portfolio',
+ '/calculator',
+ '/contact'
+];
+
+// API endpoints to cache
+const API_CACHE_PATTERNS = [
+ /^\/api\/portfolio/,
+ /^\/api\/services/,
+ /^\/api\/calculator\/services/
+];
+
+// Install event - cache static files
+self.addEventListener('install', event => {
+ console.log('Service Worker: Installing...');
+
+ event.waitUntil(
+ caches.open(STATIC_CACHE_NAME)
+ .then(cache => {
+ console.log('Service Worker: Caching static files');
+ return cache.addAll(STATIC_FILES);
+ })
+ .then(() => {
+ console.log('Service Worker: Static files cached');
+ return self.skipWaiting();
+ })
+ .catch(error => {
+ console.error('Service Worker: Error caching static files', error);
+ })
+ );
+});
+
+// Activate event - clean up old caches
+self.addEventListener('activate', event => {
+ console.log('Service Worker: Activating...');
+
+ event.waitUntil(
+ caches.keys()
+ .then(cacheNames => {
+ return Promise.all(
+ cacheNames.map(cacheName => {
+ if (cacheName !== STATIC_CACHE_NAME &&
+ cacheName !== DYNAMIC_CACHE_NAME) {
+ console.log('Service Worker: Deleting old cache', cacheName);
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ })
+ .then(() => {
+ console.log('Service Worker: Activated');
+ return self.clients.claim();
+ })
+ );
+});
+
+// Fetch event - serve cached files or fetch from network
+self.addEventListener('fetch', event => {
+ const request = event.request;
+ const url = new URL(request.url);
+
+ // Skip non-GET requests
+ if (request.method !== 'GET') {
+ return;
+ }
+
+ // Skip Chrome extension requests
+ if (url.protocol === 'chrome-extension:') {
+ return;
+ }
+
+ // Handle different types of requests
+ if (isStaticFile(request.url)) {
+ event.respondWith(cacheFirst(request));
+ } else if (isAPIRequest(request.url)) {
+ event.respondWith(networkFirst(request));
+ } else if (isDynamicRoute(request.url) || url.pathname === '/') {
+ // For main pages, always check network first to ensure language consistency
+ event.respondWith(networkFirst(request));
+ } else {
+ event.respondWith(networkFirst(request));
+ }
+});
+
+// Cache strategies
+async function cacheFirst(request) {
+ try {
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ const networkResponse = await fetch(request);
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ return networkResponse;
+ } catch (error) {
+ console.error('Cache first strategy failed:', error);
+ return new Response('Offline', { status: 503 });
+ }
+}
+
+async function networkFirst(request) {
+ try {
+ const networkResponse = await fetch(request);
+
+ // Cache successful responses
+ if (networkResponse.ok) {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ }
+
+ return networkResponse;
+ } catch (error) {
+ console.log('Network first: Falling back to cache for', request.url);
+
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ // Return offline page for navigation requests
+ if (request.mode === 'navigate') {
+ return caches.match('/offline.html') || new Response('Offline', {
+ status: 503,
+ headers: { 'Content-Type': 'text/html' }
+ });
+ }
+
+ return new Response('Network Error', { status: 503 });
+ }
+}
+
+async function staleWhileRevalidate(request) {
+ try {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ const cachedResponse = await cache.match(request);
+
+ const fetchPromise = fetch(request).then(networkResponse => {
+ if (networkResponse && networkResponse.ok) {
+ cache.put(request, networkResponse.clone());
+ }
+ return networkResponse;
+ }).catch(error => {
+ console.log('staleWhileRevalidate fetch failed:', error);
+ return null;
+ });
+
+ return cachedResponse || fetchPromise || new Response('Not available', { status: 503 });
+ } catch (error) {
+ console.error('staleWhileRevalidate error:', error);
+ return new Response('Service unavailable', { status: 503 });
+ }
+}
+
+// Helper functions
+function isStaticFile(url) {
+ return url.includes('/css/') ||
+ url.includes('/js/') ||
+ url.includes('/images/') ||
+ url.includes('/fonts/') ||
+ url.includes('googleapis.com') ||
+ url.includes('cdnjs.cloudflare.com');
+}
+
+function isAPIRequest(url) {
+ return url.includes('/api/') ||
+ API_CACHE_PATTERNS.some(pattern => pattern.test(url));
+}
+
+function isDynamicRoute(url) {
+ const pathname = new URL(url).pathname;
+ return DYNAMIC_ROUTES.includes(pathname) ||
+ pathname.startsWith('/portfolio/') ||
+ pathname.startsWith('/services/');
+}
+
+// Background sync for form submissions
+self.addEventListener('sync', event => {
+ console.log('Service Worker: Background sync triggered', event.tag);
+
+ if (event.tag === 'contact-form-sync') {
+ event.waitUntil(syncContactForms());
+ }
+});
+
+async function syncContactForms() {
+ try {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ const requests = await cache.keys();
+
+ const contactRequests = requests.filter(request =>
+ request.url.includes('/api/contact/submit')
+ );
+
+ for (const request of contactRequests) {
+ try {
+ await fetch(request);
+ await cache.delete(request);
+ console.log('Contact form synced successfully');
+ } catch (error) {
+ console.error('Failed to sync contact form:', error);
+ }
+ }
+ } catch (error) {
+ console.error('Background sync failed:', error);
+ }
+}
+
+// Push notification handling
+self.addEventListener('push', event => {
+ console.log('Service Worker: Push received', event);
+
+ let data = {};
+ if (event.data) {
+ data = event.data.json();
+ }
+
+ const title = data.title || 'SmartSolTech';
+ const options = {
+ body: data.body || 'You have a new notification',
+ icon: '/images/icon-192x192.png',
+ badge: '/images/icon-72x72.png',
+ tag: data.tag || 'default',
+ data: data.url || '/',
+ actions: [
+ {
+ action: 'open',
+ title: '열기',
+ icon: '/images/icon-open.png'
+ },
+ {
+ action: 'close',
+ title: '닫기',
+ icon: '/images/icon-close.png'
+ }
+ ],
+ requireInteraction: data.requireInteraction || false,
+ silent: data.silent || false,
+ vibrate: data.vibrate || [200, 100, 200]
+ };
+
+ event.waitUntil(
+ self.registration.showNotification(title, options)
+ );
+});
+
+// Notification click handling
+self.addEventListener('notificationclick', event => {
+ console.log('Service Worker: Notification clicked', event);
+
+ event.notification.close();
+
+ if (event.action === 'close') {
+ return;
+ }
+
+ const url = event.notification.data || '/';
+
+ event.waitUntil(
+ clients.matchAll({ type: 'window' }).then(clientList => {
+ // Check if window is already open
+ for (const client of clientList) {
+ if (client.url === url && 'focus' in client) {
+ return client.focus();
+ }
+ }
+
+ // Open new window
+ if (clients.openWindow) {
+ return clients.openWindow(url);
+ }
+ })
+ );
+});
+
+// Handle messages from main thread
+self.addEventListener('message', event => {
+ console.log('Service Worker: Message received', event.data);
+
+ if (event.data && event.data.type === 'SKIP_WAITING') {
+ self.skipWaiting();
+ }
+
+ if (event.data && event.data.type === 'CACHE_URLS') {
+ cacheUrls(event.data.urls);
+ }
+});
+
+async function cacheUrls(urls) {
+ try {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ await cache.addAll(urls);
+ console.log('URLs cached successfully:', urls);
+ } catch (error) {
+ console.error('Failed to cache URLs:', error);
+ }
+}
+
+// Periodic background sync (if supported)
+self.addEventListener('periodicsync', event => {
+ console.log('Service Worker: Periodic sync triggered', event.tag);
+
+ if (event.tag === 'content-sync') {
+ event.waitUntil(syncContent());
+ }
+});
+
+async function syncContent() {
+ try {
+ // Fetch fresh portfolio and services data
+ const portfolioResponse = await fetch('/api/portfolio?featured=true');
+ const servicesResponse = await fetch('/api/services?featured=true');
+
+ if (portfolioResponse.ok && servicesResponse.ok) {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ cache.put('/api/portfolio?featured=true', portfolioResponse.clone());
+ cache.put('/api/services?featured=true', servicesResponse.clone());
+ console.log('Content synced successfully');
+ }
+ } catch (error) {
+ console.error('Content sync failed:', error);
+ }
+}
+
+// Cache management utilities
+async function cleanupCaches() {
+ const cacheNames = await caches.keys();
+ const currentCaches = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];
+
+ return Promise.all(
+ cacheNames.map(cacheName => {
+ if (!currentCaches.includes(cacheName)) {
+ console.log('Deleting old cache:', cacheName);
+ return caches.delete(cacheName);
+ }
+ })
+ );
+}
+
+// Limit cache size
+async function limitCacheSize(cacheName, maxItems) {
+ const cache = await caches.open(cacheName);
+ const keys = await cache.keys();
+
+ if (keys.length > maxItems) {
+ const keysToDelete = keys.slice(0, keys.length - maxItems);
+ return Promise.all(keysToDelete.map(key => cache.delete(key)));
+ }
+}
+
+// Performance monitoring
+self.addEventListener('fetch', event => {
+ if (event.request.url.includes('/api/')) {
+ const start = performance.now();
+
+ event.respondWith(
+ fetch(event.request).then(response => {
+ const duration = performance.now() - start;
+
+ // Log slow API requests
+ if (duration > 2000) {
+ console.warn('Slow API request:', event.request.url, duration + 'ms');
+ }
+
+ return response;
+ })
+ );
+ }
+});
+
+// Error tracking
+self.addEventListener('error', event => {
+ console.error('Service Worker error:', event.error);
+ // Could send to analytics service
+});
+
+self.addEventListener('unhandledrejection', event => {
+ console.error('Service Worker unhandled rejection:', event.reason);
+ // Could send to analytics service
+});
\ No newline at end of file
diff --git a/.history/public/sw_20251026215934.js b/.history/public/sw_20251026215934.js
new file mode 100644
index 0000000..f37048c
--- /dev/null
+++ b/.history/public/sw_20251026215934.js
@@ -0,0 +1,414 @@
+// Service Worker for SmartSolTech PWA
+const CACHE_NAME = 'smartsoltech-v1.0.2';
+const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.2';
+const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.2';
+
+// Files to cache immediately
+const STATIC_FILES = [
+ '/',
+ '/css/main.css',
+ '/css/fixes.css',
+ '/css/dark-theme.css',
+ '/js/main.js',
+ '/vendor/jquery/jquery-3.6.0.min.js',
+ '/vendor/bootstrap/bootstrap.bundle.min.js',
+ '/vendor/bootstrap/bootstrap.min.css',
+ '/vendor/adminlte/adminlte.min.js',
+ '/vendor/adminlte/adminlte.min.css',
+ '/vendor/adminlte/fontawesome.min.css',
+ '/images/logo.png',
+ '/images/icon-192x192.png',
+ '/images/icon-144x144.png',
+ '/manifest.json',
+ 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
+ 'https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap',
+ 'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css',
+ 'https://unpkg.com/aos@2.3.1/dist/aos.css',
+ 'https://unpkg.com/aos@2.3.1/dist/aos.js'
+];
+
+// Routes to cache dynamically
+const DYNAMIC_ROUTES = [
+ '/about',
+ '/services',
+ '/portfolio',
+ '/calculator',
+ '/contact'
+];
+
+// API endpoints to cache
+const API_CACHE_PATTERNS = [
+ /^\/api\/portfolio/,
+ /^\/api\/services/,
+ /^\/api\/calculator\/services/
+];
+
+// Install event - cache static files
+self.addEventListener('install', event => {
+ console.log('Service Worker: Installing...');
+
+ event.waitUntil(
+ caches.open(STATIC_CACHE_NAME)
+ .then(cache => {
+ console.log('Service Worker: Caching static files');
+ return cache.addAll(STATIC_FILES);
+ })
+ .then(() => {
+ console.log('Service Worker: Static files cached');
+ return self.skipWaiting();
+ })
+ .catch(error => {
+ console.error('Service Worker: Error caching static files', error);
+ })
+ );
+});
+
+// Activate event - clean up old caches
+self.addEventListener('activate', event => {
+ console.log('Service Worker: Activating...');
+
+ event.waitUntil(
+ caches.keys()
+ .then(cacheNames => {
+ return Promise.all(
+ cacheNames.map(cacheName => {
+ if (cacheName !== STATIC_CACHE_NAME &&
+ cacheName !== DYNAMIC_CACHE_NAME) {
+ console.log('Service Worker: Deleting old cache', cacheName);
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ })
+ .then(() => {
+ console.log('Service Worker: Activated');
+ return self.clients.claim();
+ })
+ );
+});
+
+// Fetch event - serve cached files or fetch from network
+self.addEventListener('fetch', event => {
+ const request = event.request;
+ const url = new URL(request.url);
+
+ // Skip non-GET requests
+ if (request.method !== 'GET') {
+ return;
+ }
+
+ // Skip Chrome extension requests
+ if (url.protocol === 'chrome-extension:') {
+ return;
+ }
+
+ // Handle different types of requests
+ if (isStaticFile(request.url)) {
+ event.respondWith(cacheFirst(request));
+ } else if (isAPIRequest(request.url)) {
+ event.respondWith(networkFirst(request));
+ } else if (isDynamicRoute(request.url) || url.pathname === '/') {
+ // For main pages, always check network first to ensure language consistency
+ event.respondWith(networkFirst(request));
+ } else {
+ event.respondWith(networkFirst(request));
+ }
+});
+
+// Cache strategies
+async function cacheFirst(request) {
+ try {
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ const networkResponse = await fetch(request);
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ return networkResponse;
+ } catch (error) {
+ console.error('Cache first strategy failed:', error);
+ return new Response('Offline', { status: 503 });
+ }
+}
+
+async function networkFirst(request) {
+ try {
+ const networkResponse = await fetch(request);
+
+ // Cache successful responses
+ if (networkResponse.ok) {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ }
+
+ return networkResponse;
+ } catch (error) {
+ console.log('Network first: Falling back to cache for', request.url);
+
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ // Return offline page for navigation requests
+ if (request.mode === 'navigate') {
+ return caches.match('/offline.html') || new Response('Offline', {
+ status: 503,
+ headers: { 'Content-Type': 'text/html' }
+ });
+ }
+
+ return new Response('Network Error', { status: 503 });
+ }
+}
+
+async function staleWhileRevalidate(request) {
+ try {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ const cachedResponse = await cache.match(request);
+
+ const fetchPromise = fetch(request).then(networkResponse => {
+ if (networkResponse && networkResponse.ok) {
+ cache.put(request, networkResponse.clone());
+ }
+ return networkResponse;
+ }).catch(error => {
+ console.log('staleWhileRevalidate fetch failed:', error);
+ return null;
+ });
+
+ return cachedResponse || fetchPromise || new Response('Not available', { status: 503 });
+ } catch (error) {
+ console.error('staleWhileRevalidate error:', error);
+ return new Response('Service unavailable', { status: 503 });
+ }
+}
+
+// Helper functions
+function isStaticFile(url) {
+ return url.includes('/css/') ||
+ url.includes('/js/') ||
+ url.includes('/images/') ||
+ url.includes('/fonts/') ||
+ url.includes('googleapis.com') ||
+ url.includes('cdnjs.cloudflare.com');
+}
+
+function isAPIRequest(url) {
+ return url.includes('/api/') ||
+ API_CACHE_PATTERNS.some(pattern => pattern.test(url));
+}
+
+function isDynamicRoute(url) {
+ const pathname = new URL(url).pathname;
+ return DYNAMIC_ROUTES.includes(pathname) ||
+ pathname.startsWith('/portfolio/') ||
+ pathname.startsWith('/services/');
+}
+
+// Background sync for form submissions
+self.addEventListener('sync', event => {
+ console.log('Service Worker: Background sync triggered', event.tag);
+
+ if (event.tag === 'contact-form-sync') {
+ event.waitUntil(syncContactForms());
+ }
+});
+
+async function syncContactForms() {
+ try {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ const requests = await cache.keys();
+
+ const contactRequests = requests.filter(request =>
+ request.url.includes('/api/contact/submit')
+ );
+
+ for (const request of contactRequests) {
+ try {
+ await fetch(request);
+ await cache.delete(request);
+ console.log('Contact form synced successfully');
+ } catch (error) {
+ console.error('Failed to sync contact form:', error);
+ }
+ }
+ } catch (error) {
+ console.error('Background sync failed:', error);
+ }
+}
+
+// Push notification handling
+self.addEventListener('push', event => {
+ console.log('Service Worker: Push received', event);
+
+ let data = {};
+ if (event.data) {
+ data = event.data.json();
+ }
+
+ const title = data.title || 'SmartSolTech';
+ const options = {
+ body: data.body || 'You have a new notification',
+ icon: '/images/icon-192x192.png',
+ badge: '/images/icon-72x72.png',
+ tag: data.tag || 'default',
+ data: data.url || '/',
+ actions: [
+ {
+ action: 'open',
+ title: '열기',
+ icon: '/images/icon-open.png'
+ },
+ {
+ action: 'close',
+ title: '닫기',
+ icon: '/images/icon-close.png'
+ }
+ ],
+ requireInteraction: data.requireInteraction || false,
+ silent: data.silent || false,
+ vibrate: data.vibrate || [200, 100, 200]
+ };
+
+ event.waitUntil(
+ self.registration.showNotification(title, options)
+ );
+});
+
+// Notification click handling
+self.addEventListener('notificationclick', event => {
+ console.log('Service Worker: Notification clicked', event);
+
+ event.notification.close();
+
+ if (event.action === 'close') {
+ return;
+ }
+
+ const url = event.notification.data || '/';
+
+ event.waitUntil(
+ clients.matchAll({ type: 'window' }).then(clientList => {
+ // Check if window is already open
+ for (const client of clientList) {
+ if (client.url === url && 'focus' in client) {
+ return client.focus();
+ }
+ }
+
+ // Open new window
+ if (clients.openWindow) {
+ return clients.openWindow(url);
+ }
+ })
+ );
+});
+
+// Handle messages from main thread
+self.addEventListener('message', event => {
+ console.log('Service Worker: Message received', event.data);
+
+ if (event.data && event.data.type === 'SKIP_WAITING') {
+ self.skipWaiting();
+ }
+
+ if (event.data && event.data.type === 'CACHE_URLS') {
+ cacheUrls(event.data.urls);
+ }
+});
+
+async function cacheUrls(urls) {
+ try {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ await cache.addAll(urls);
+ console.log('URLs cached successfully:', urls);
+ } catch (error) {
+ console.error('Failed to cache URLs:', error);
+ }
+}
+
+// Periodic background sync (if supported)
+self.addEventListener('periodicsync', event => {
+ console.log('Service Worker: Periodic sync triggered', event.tag);
+
+ if (event.tag === 'content-sync') {
+ event.waitUntil(syncContent());
+ }
+});
+
+async function syncContent() {
+ try {
+ // Fetch fresh portfolio and services data
+ const portfolioResponse = await fetch('/api/portfolio?featured=true');
+ const servicesResponse = await fetch('/api/services?featured=true');
+
+ if (portfolioResponse.ok && servicesResponse.ok) {
+ const cache = await caches.open(DYNAMIC_CACHE_NAME);
+ cache.put('/api/portfolio?featured=true', portfolioResponse.clone());
+ cache.put('/api/services?featured=true', servicesResponse.clone());
+ console.log('Content synced successfully');
+ }
+ } catch (error) {
+ console.error('Content sync failed:', error);
+ }
+}
+
+// Cache management utilities
+async function cleanupCaches() {
+ const cacheNames = await caches.keys();
+ const currentCaches = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];
+
+ return Promise.all(
+ cacheNames.map(cacheName => {
+ if (!currentCaches.includes(cacheName)) {
+ console.log('Deleting old cache:', cacheName);
+ return caches.delete(cacheName);
+ }
+ })
+ );
+}
+
+// Limit cache size
+async function limitCacheSize(cacheName, maxItems) {
+ const cache = await caches.open(cacheName);
+ const keys = await cache.keys();
+
+ if (keys.length > maxItems) {
+ const keysToDelete = keys.slice(0, keys.length - maxItems);
+ return Promise.all(keysToDelete.map(key => cache.delete(key)));
+ }
+}
+
+// Performance monitoring
+self.addEventListener('fetch', event => {
+ if (event.request.url.includes('/api/')) {
+ const start = performance.now();
+
+ event.respondWith(
+ fetch(event.request).then(response => {
+ const duration = performance.now() - start;
+
+ // Log slow API requests
+ if (duration > 2000) {
+ console.warn('Slow API request:', event.request.url, duration + 'ms');
+ }
+
+ return response;
+ })
+ );
+ }
+});
+
+// Error tracking
+self.addEventListener('error', event => {
+ console.error('Service Worker error:', event.error);
+ // Could send to analytics service
+});
+
+self.addEventListener('unhandledrejection', event => {
+ console.error('Service Worker unhandled rejection:', event.reason);
+ // Could send to analytics service
+});
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212605.js b/.history/routes/admin_20251026212605.js
new file mode 100644
index 0000000..ee24cac
--- /dev/null
+++ b/.history/routes/admin_20251026212605.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212607.js b/.history/routes/admin_20251026212607.js
new file mode 100644
index 0000000..ee24cac
--- /dev/null
+++ b/.history/routes/admin_20251026212607.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212617.js b/.history/routes/admin_20251026212617.js
new file mode 100644
index 0000000..d9303ba
--- /dev/null
+++ b/.history/routes/admin_20251026212617.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212628.js b/.history/routes/admin_20251026212628.js
new file mode 100644
index 0000000..c583496
--- /dev/null
+++ b/.history/routes/admin_20251026212628.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212638.js b/.history/routes/admin_20251026212638.js
new file mode 100644
index 0000000..0b636c3
--- /dev/null
+++ b/.history/routes/admin_20251026212638.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212647.js b/.history/routes/admin_20251026212647.js
new file mode 100644
index 0000000..10c8f63
--- /dev/null
+++ b/.history/routes/admin_20251026212647.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212658.js b/.history/routes/admin_20251026212658.js
new file mode 100644
index 0000000..28f8c05
--- /dev/null
+++ b/.history/routes/admin_20251026212658.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212708.js b/.history/routes/admin_20251026212708.js
new file mode 100644
index 0000000..03db60b
--- /dev/null
+++ b/.history/routes/admin_20251026212708.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212718.js b/.history/routes/admin_20251026212718.js
new file mode 100644
index 0000000..8b5ee41
--- /dev/null
+++ b/.history/routes/admin_20251026212718.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026212756.js b/.history/routes/admin_20251026212756.js
new file mode 100644
index 0000000..8b5ee41
--- /dev/null
+++ b/.history/routes/admin_20251026212756.js
@@ -0,0 +1,1634 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026215040.js b/.history/routes/admin_20251026215040.js
new file mode 100644
index 0000000..744e882
--- /dev/null
+++ b/.history/routes/admin_20251026215040.js
@@ -0,0 +1,1652 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026215051.js b/.history/routes/admin_20251026215051.js
new file mode 100644
index 0000000..744e882
--- /dev/null
+++ b/.history/routes/admin_20251026215051.js
@@ -0,0 +1,1652 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220319.js b/.history/routes/admin_20251026220319.js
new file mode 100644
index 0000000..70ea60c
--- /dev/null
+++ b/.history/routes/admin_20251026220319.js
@@ -0,0 +1,1653 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220340.js b/.history/routes/admin_20251026220340.js
new file mode 100644
index 0000000..76a9a5b
--- /dev/null
+++ b/.history/routes/admin_20251026220340.js
@@ -0,0 +1,1654 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'telegram',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220351.js b/.history/routes/admin_20251026220351.js
new file mode 100644
index 0000000..6a9a7e5
--- /dev/null
+++ b/.history/routes/admin_20251026220351.js
@@ -0,0 +1,1655 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'portfolio',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'telegram',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220402.js b/.history/routes/admin_20251026220402.js
new file mode 100644
index 0000000..483c566
--- /dev/null
+++ b/.history/routes/admin_20251026220402.js
@@ -0,0 +1,1656 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'portfolio',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'services',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'telegram',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220414.js b/.history/routes/admin_20251026220414.js
new file mode 100644
index 0000000..6e4ae57
--- /dev/null
+++ b/.history/routes/admin_20251026220414.js
@@ -0,0 +1,1657 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'portfolio',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'services',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'contacts',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'telegram',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220429.js b/.history/routes/admin_20251026220429.js
new file mode 100644
index 0000000..0cedea5
--- /dev/null
+++ b/.history/routes/admin_20251026220429.js
@@ -0,0 +1,1658 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'portfolio',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'services',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'contacts',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'settings',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'telegram',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220439.js b/.history/routes/admin_20251026220439.js
new file mode 100644
index 0000000..877d594
--- /dev/null
+++ b/.history/routes/admin_20251026220439.js
@@ -0,0 +1,1659 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'portfolio',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'services',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'contacts',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'settings',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'telegram',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'banner-editor',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/admin_20251026220452.js b/.history/routes/admin_20251026220452.js
new file mode 100644
index 0000000..877d594
--- /dev/null
+++ b/.history/routes/admin_20251026220452.js
@@ -0,0 +1,1659 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const sharp = require('sharp');
+const path = require('path');
+const fs = require('fs').promises;
+const { body, validationResult } = require('express-validator');
+const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models');
+
+// Configure multer for file uploads
+const storage = multer.memoryStorage(); // Use memory storage to process with sharp
+const upload = multer({
+ storage,
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed'), false);
+ }
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.redirect('/admin/login');
+ }
+ next();
+};
+
+// Add stats middleware
+const addStats = async (req, res, next) => {
+ try {
+ if (req.session.user) {
+ const [portfolioCount, servicesCount, contactsCount] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count()
+ ]);
+
+ res.locals.stats = {
+ portfolioCount,
+ servicesCount,
+ contactsCount
+ };
+ }
+ next();
+ } catch (error) {
+ console.error('Stats middleware error:', error);
+ next();
+ }
+};
+
+// Admin login page
+router.get('/login', (req, res) => {
+ if (req.session.user) {
+ return res.redirect('/admin/dashboard');
+ }
+
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: null
+ });
+});
+
+// Admin login POST
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({
+ where: {
+ email: email,
+ isActive: true
+ }
+ });
+ if (!user || !(await user.comparePassword(password))) {
+ return res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Invalid credentials'
+ });
+ }
+
+ await user.updateLastLogin();
+
+ req.session.user = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role
+ };
+
+ res.redirect('/admin/dashboard');
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.render('admin/login', {
+ title: 'Admin Login',
+ error: 'Server error'
+ });
+ }
+});
+
+// Admin logout
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ console.error('Logout error:', err);
+ }
+ res.redirect('/admin/login');
+ });
+});
+
+// Dashboard (default route)
+router.get('/', requireAuth, async (req, res) => {
+ res.redirect('/admin/dashboard');
+});
+
+// Dashboard
+router.get('/dashboard', requireAuth, async (req, res) => {
+ try {
+ const [
+ portfolioCount,
+ servicesCount,
+ contactsCount,
+ recentContacts,
+ recentPortfolio
+ ] = await Promise.all([
+ Portfolio.count({ where: { isPublished: true } }),
+ Service.count({ where: { isActive: true } }),
+ Contact.count(),
+ Contact.findAll({
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ }),
+ Portfolio.findAll({
+ where: { isPublished: true },
+ order: [['createdAt', 'DESC']],
+ limit: 5
+ })
+ ]);
+
+ const stats = {
+ portfolioCount: portfolioCount,
+ servicesCount: servicesCount,
+ contactsCount: contactsCount,
+ usersCount: await User.count()
+ };
+
+ res.render('admin/dashboard', {
+ title: 'Admin Dashboard',
+ layout: 'admin/layout',
+ user: req.session.user,
+ stats,
+ recentContacts,
+ recentPortfolio,
+ currentPage: 'dashboard'
+ });
+ } catch (error) {
+ console.error('Dashboard error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading dashboard'
+ });
+ }
+});
+
+// Banner management
+router.get('/banners', requireAuth, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [banners, total] = await Promise.all([
+ Banner.findAll({
+ order: [['order', 'ASC'], ['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Banner.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/banners/list', {
+ title: 'Banner Management - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banners,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Banner list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banners'
+ });
+ }
+});
+
+// Add banner
+router.get('/banners/add', requireAuth, (req, res) => {
+ res.render('admin/banners/add', {
+ title: 'Add Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+});
+
+// Create banner
+router.post('/banners/add', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive = true,
+ startDate,
+ endDate,
+ targetAudience = 'all',
+ backgroundColor,
+ textColor,
+ animation = 'none'
+ } = req.body;
+
+ const banner = await Banner.create({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience,
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 생성되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit banner
+router.get('/banners/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Banner not found'
+ });
+ }
+
+ res.render('admin/banners/edit', {
+ title: 'Edit Banner - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ banner,
+ positions: [
+ { value: 'hero', label: '메인 히어로' },
+ { value: 'secondary', label: '보조 배너' },
+ { value: 'footer', label: '푸터 배너' }
+ ],
+ animations: [
+ { value: 'none', label: '없음' },
+ { value: 'fade', label: '페이드' },
+ { value: 'slide', label: '슬라이드' },
+ { value: 'zoom', label: '줌' }
+ ]
+ });
+ } catch (error) {
+ console.error('Banner edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading banner'
+ });
+ }
+});
+
+// Update banner
+router.put('/banners/:id', requireAuth, [
+ body('title').notEmpty().withMessage('제목을 입력해주세요'),
+ body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'),
+ body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const banner = await Banner.findByPk(req.params.id);
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ title,
+ subtitle,
+ description,
+ buttonText,
+ buttonUrl,
+ image,
+ mobileImage,
+ position,
+ order,
+ isActive,
+ startDate,
+ endDate,
+ targetAudience,
+ backgroundColor,
+ textColor,
+ animation
+ } = req.body;
+
+ await banner.update({
+ title,
+ subtitle: subtitle || null,
+ description: description || null,
+ buttonText: buttonText || null,
+ buttonUrl: buttonUrl || null,
+ image: image || null,
+ mobileImage: mobileImage || null,
+ position,
+ order: parseInt(order),
+ isActive: Boolean(isActive),
+ startDate: startDate || null,
+ endDate: endDate || null,
+ targetAudience: targetAudience || 'all',
+ backgroundColor: backgroundColor || null,
+ textColor: textColor || null,
+ animation: animation || 'none'
+ });
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 업데이트되었습니다',
+ banner: {
+ id: banner.id,
+ title: banner.title,
+ position: banner.position
+ }
+ });
+ } catch (error) {
+ console.error('Banner update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete banner
+router.delete('/banners/:id', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.destroy();
+
+ res.json({
+ success: true,
+ message: '배너가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Banner deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '배너 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle banner active status
+router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !banner.isActive;
+ await banner.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Banner toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Record banner click
+router.post('/banners/:id/click', async (req, res) => {
+ try {
+ const banner = await Banner.findByPk(req.params.id);
+
+ if (!banner) {
+ return res.status(404).json({
+ success: false,
+ message: '배너를 찾을 수 없습니다'
+ });
+ }
+
+ await banner.recordClick();
+
+ res.json({
+ success: true,
+ clickCount: banner.clickCount
+ });
+ } catch (error) {
+ console.error('Banner click record error:', error);
+ res.status(500).json({
+ success: false,
+ message: '클릭 기록 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Banner Editor (legacy route)
+router.get('/banner-editor', requireAuth, addStats, async (req, res) => {
+ res.redirect('/admin/banners');
+});
+
+// Portfolio management
+router.get('/portfolio', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [portfolio, total] = await Promise.all([
+ Portfolio.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Portfolio.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/portfolio/list', {
+ title: 'Portfolio Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'portfolio',
+ user: req.session.user,
+ portfolio,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio'
+ });
+ }
+});
+
+// Utility function for category names
+const getCategoryName = (category) => {
+ const categoryNames = {
+ 'web-development': 'Веб-разработка',
+ 'mobile-app': 'Мобильные приложения',
+ 'ui-ux-design': 'UI/UX дизайн',
+ 'e-commerce': 'Электронная коммерция',
+ 'enterprise': 'Корпоративное ПО',
+ 'other': 'Другое'
+ };
+ return categoryNames[category] || category;
+};
+
+// Add portfolio item
+router.get('/portfolio/add', requireAuth, (req, res) => {
+ res.render('admin/portfolio/add', {
+ title: 'Add Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ],
+ getCategoryName: getCategoryName
+ });
+});
+
+// Create portfolio item
+router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ if (parsedTechnologies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Добавьте хотя бы одну технологию'
+ });
+ }
+
+ // Process uploaded images
+ const processedImages = [];
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ processedImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Create portfolio item
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: processedImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно создан!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании проекта: ' + error.message
+ });
+ }
+});
+
+// Edit portfolio item
+router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Portfolio item not found'
+ });
+ }
+
+ res.render('admin/portfolio/edit', {
+ title: 'Edit Portfolio Item - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ portfolio,
+ categories: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'enterprise',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Portfolio edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading portfolio item'
+ });
+ }
+});
+
+// Update portfolio item
+router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: 'Проект не найден'
+ });
+ }
+
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName,
+ duration,
+ seoTitle,
+ seoDescription,
+ isPublished,
+ featured,
+ existingImages
+ } = req.body;
+
+ // Validate required fields
+ if (!title || !shortDescription || !description || !category) {
+ return res.status(400).json({
+ success: false,
+ message: 'Заполните все обязательные поля'
+ });
+ }
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Handle existing images
+ let finalImages = [];
+ try {
+ const existing = JSON.parse(existingImages || '[]');
+ finalImages = Array.isArray(existing) ? existing : [];
+ } catch (e) {
+ finalImages = portfolio.images || [];
+ }
+
+ // Process new uploaded images
+ if (req.files && req.files.length > 0) {
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio');
+
+ // Ensure directory exists
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ for (const file of req.files) {
+ const timestamp = Date.now();
+ const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`;
+ const filepath = path.join(uploadDir, filename);
+
+ // Process image with Sharp
+ await sharp(file.buffer)
+ .webp({ quality: 85 })
+ .resize(1200, 800, { fit: 'inside', withoutEnlargement: true })
+ .toFile(filepath);
+
+ finalImages.push(`/uploads/portfolio/${filename}`);
+ }
+ }
+
+ // Create SEO data
+ const seo = portfolio.seo || {};
+ if (seoTitle) seo.title = seoTitle;
+ if (seoDescription) seo.description = seoDescription;
+
+ // Update portfolio
+ await portfolio.update({
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: finalImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ duration: duration ? parseInt(duration) : null,
+ isPublished: isPublished === 'true' || isPublished === true,
+ featured: featured === 'true' || featured === true,
+ publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft',
+ seo: Object.keys(seo).length > 0 ? seo : null
+ });
+
+ res.json({
+ success: true,
+ message: 'Проект успешно обновлен!',
+ portfolio: {
+ id: portfolio.id,
+ title: portfolio.title,
+ category: portfolio.category,
+ isPublished: portfolio.isPublished
+ }
+ });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при обновлении проекта: ' + error.message
+ });
+ }
+});
+
+// Delete portfolio item
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ await portfolio.destroy();
+
+ res.json({
+ success: true,
+ message: '포트폴리오가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '포트폴리오 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle portfolio publish status
+router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+
+ if (!portfolio) {
+ return res.status(404).json({
+ success: false,
+ message: '포트폴리오를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !portfolio.isPublished;
+ await portfolio.update({
+ isPublished: newStatus,
+ publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
+ status: newStatus ? 'published' : 'draft'
+ });
+
+ res.json({
+ success: true,
+ message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`,
+ isPublished: newStatus
+ });
+ } catch (error) {
+ console.error('Portfolio toggle publish error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Portfolio preview
+router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies,
+ demoUrl,
+ githubUrl,
+ clientName
+ } = req.body;
+
+ // Parse technologies from JSON string
+ let parsedTechnologies = [];
+ try {
+ parsedTechnologies = JSON.parse(technologies || '[]');
+ } catch (e) {
+ parsedTechnologies = typeof technologies === 'string' ? [technologies] : [];
+ }
+
+ // Process uploaded images for preview
+ const previewImages = [];
+ if (req.files && req.files.length > 0) {
+ for (const file of req.files) {
+ // Convert to base64 for preview
+ const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`;
+ previewImages.push(base64);
+ }
+ }
+
+ const previewData = {
+ title,
+ shortDescription,
+ description,
+ category,
+ technologies: parsedTechnologies,
+ images: previewImages,
+ projectUrl: demoUrl || null,
+ githubUrl: githubUrl || null,
+ clientName: clientName || null,
+ createdAt: new Date()
+ };
+
+ res.json({
+ success: true,
+ preview: previewData
+ });
+ } catch (error) {
+ console.error('Portfolio preview error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Ошибка при создании предпросмотра: ' + error.message
+ });
+ }
+});
+
+// Services management
+router.get('/services', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+
+ const [services, total] = await Promise.all([
+ Service.findAll({
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Service.count()
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/services/list', {
+ title: 'Services Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'services',
+ user: req.session.user,
+ services,
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Services list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading services'
+ });
+ }
+});
+
+// Add service
+router.get('/services/add', requireAuth, (req, res) => {
+ res.render('admin/services/add', {
+ title: 'Add Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+});
+
+// Create service
+router.post('/services/add', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive = true,
+ featured = false
+ } = req.body;
+
+ const service = await Service.create({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 생성되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 생성 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Edit service
+router.get('/services/edit/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Service not found'
+ });
+ }
+
+ const availablePortfolio = await Portfolio.findAll({
+ where: { isPublished: true },
+ attributes: ['id', 'title', 'category']
+ });
+
+ res.render('admin/services/edit', {
+ title: 'Edit Service - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ service,
+ availablePortfolio,
+ serviceTypes: [
+ 'web-development',
+ 'mobile-app',
+ 'ui-ux-design',
+ 'e-commerce',
+ 'seo',
+ 'maintenance',
+ 'consultation',
+ 'other'
+ ]
+ });
+ } catch (error) {
+ console.error('Service edit error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading service'
+ });
+ }
+});
+
+// Update service
+router.put('/services/:id', requireAuth, [
+ body('name').notEmpty().withMessage('서비스명을 입력해주세요'),
+ body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
+ body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
+ body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
+ body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: '입력 데이터를 확인해주세요',
+ errors: errors.array()
+ });
+ }
+
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const {
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice,
+ features,
+ duration,
+ isActive,
+ featured
+ } = req.body;
+
+ await service.update({
+ name,
+ shortDescription,
+ description,
+ category,
+ basePrice: parseFloat(basePrice),
+ features: features || [],
+ duration: duration ? parseInt(duration) : null,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured)
+ });
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 업데이트되었습니다',
+ service: {
+ id: service.id,
+ name: service.name,
+ category: service.category
+ }
+ });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 업데이트 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Delete service
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ await service.destroy();
+
+ res.json({
+ success: true,
+ message: '서비스가 성공적으로 삭제되었습니다'
+ });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({
+ success: false,
+ message: '서비스 삭제 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Toggle service active status
+router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+
+ if (!service) {
+ return res.status(404).json({
+ success: false,
+ message: '서비스를 찾을 수 없습니다'
+ });
+ }
+
+ const newStatus = !service.isActive;
+ await service.update({ isActive: newStatus });
+
+ res.json({
+ success: true,
+ message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`,
+ isActive: newStatus
+ });
+ } catch (error) {
+ console.error('Service toggle active error:', error);
+ res.status(500).json({
+ success: false,
+ message: '상태 변경 중 오류가 발생했습니다'
+ });
+ }
+});
+
+// Contacts management
+router.get('/contacts', requireAuth, addStats, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = 20;
+ const skip = (page - 1) * limit;
+ const status = req.query.status;
+
+ let whereClause = {};
+ if (status && status !== 'all') {
+ whereClause.status = status;
+ }
+
+ const [contacts, total] = await Promise.all([
+ Contact.findAll({
+ where: whereClause,
+ order: [['createdAt', 'DESC']],
+ offset: skip,
+ limit: limit
+ }),
+ Contact.count({ where: whereClause })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ res.render('admin/contacts/list', {
+ title: 'Contacts Management - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'contacts',
+ user: req.session.user,
+ contacts,
+ currentStatus: status || 'all',
+ pagination: {
+ current: page,
+ total: totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ });
+ } catch (error) {
+ console.error('Contacts list error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contacts'
+ });
+ }
+});
+
+// View contact details
+router.get('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+
+ if (!contact) {
+ return res.status(404).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Contact not found'
+ });
+ }
+
+ // Mark as read
+ if (!contact.isRead) {
+ contact.isRead = true;
+ await contact.save();
+ }
+
+ res.render('admin/contacts/view', {
+ title: 'Contact Details - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user,
+ contact
+ });
+ } catch (error) {
+ console.error('Contact view error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading contact'
+ });
+ }
+});
+
+// Settings
+router.get('/settings', requireAuth, addStats, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || await SiteSettings.create({});
+
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'settings',
+ user: req.session.user,
+ settings
+ });
+ } catch (error) {
+ console.error('Settings error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading settings'
+ });
+ }
+});
+
+// Media gallery
+router.get('/media', requireAuth, addStats, (req, res) => {
+ res.render('admin/media', {
+ title: 'Media Gallery - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'media',
+ user: req.session.user
+ });
+});
+
+// Telegram bot configuration and testing
+router.get('/telegram', requireAuth, addStats, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+
+ // Get bot info and available chats if token is configured
+ let botInfo = null;
+ let availableChats = [];
+
+ if (telegramService.botToken) {
+ const result = await telegramService.getBotInfo();
+ if (result.success) {
+ botInfo = result.bot;
+ availableChats = telegramService.getAvailableChats();
+ }
+ }
+
+ res.render('admin/telegram', {
+ title: 'Telegram Bot - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'telegram',
+ user: req.session.user,
+ botConfigured: telegramService.isEnabled,
+ botToken: telegramService.botToken || '',
+ chatId: telegramService.chatId || '',
+ botInfo,
+ availableChats
+ });
+ } catch (error) {
+ console.error('Telegram page error:', error);
+ res.status(500).render('admin/error', {
+ title: 'Error - Admin Panel',
+ layout: 'admin/layout',
+ message: 'Error loading Telegram settings'
+ });
+ }
+});
+
+// Update bot token
+router.post('/telegram/configure', requireAuth, [
+ body('botToken').notEmpty().withMessage('Bot token is required'),
+ body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const { botToken, chatId } = req.body;
+ const telegramService = require('../services/telegram');
+
+ // Update bot token
+ const result = await telegramService.updateBotToken(botToken);
+
+ if (result.success) {
+ // Update chat ID if provided
+ if (chatId) {
+ telegramService.updateChatId(parseInt(chatId));
+ }
+
+ // Update environment variables (in production, this should update a config file)
+ process.env.TELEGRAM_BOT_TOKEN = botToken;
+ if (chatId) {
+ process.env.TELEGRAM_CHAT_ID = chatId;
+ }
+
+ res.json({
+ success: true,
+ message: 'Telegram bot configured successfully',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to configure bot'
+ });
+ }
+ } catch (error) {
+ console.error('Configure Telegram bot error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error configuring Telegram bot'
+ });
+ }
+});
+
+// Get bot info and discover chats
+router.get('/telegram/info', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ botInfo: result.bot,
+ availableChats: result.availableChats || [],
+ isConfigured: telegramService.isEnabled
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to get bot info'
+ });
+ }
+ } catch (error) {
+ console.error('Get Telegram info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting bot information'
+ });
+ }
+});
+
+// Get chat information
+router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.getChat(req.params.chatId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ chat: result.chat
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || 'Failed to get chat info'
+ });
+ }
+ } catch (error) {
+ console.error('Get chat info error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error getting chat information'
+ });
+ }
+});
+
+// Test connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const telegramService = require('../services/telegram');
+ const result = await telegramService.testConnection();
+
+ if (result.success) {
+ const testMessage = `🤖 Тест Telegram бота\n\n` +
+ `✅ Соединение успешно установлено!\n` +
+ `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` +
+ `🆔 ID бота: ${result.bot.id}\n` +
+ `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` +
+ `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` +
+ `Бот готов к отправке уведомлений! 🚀`;
+
+ const sendResult = await telegramService.sendMessage(testMessage);
+
+ if (sendResult.success) {
+ res.json({
+ success: true,
+ message: 'Test message sent successfully!',
+ botInfo: result.bot,
+ availableChats: result.availableChats || []
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message)
+ });
+ }
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || result.error || 'Failed to connect to Telegram bot'
+ });
+ }
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error testing Telegram bot'
+ });
+ }
+});
+
+// Send custom message
+router.post('/telegram/send', requireAuth, [
+ body('message').notEmpty().withMessage('Message is required'),
+ body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'),
+ body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode')
+], async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Validation failed',
+ errors: errors.array()
+ });
+ }
+
+ const {
+ message,
+ chatIds = [],
+ parseMode = 'HTML',
+ disableWebPagePreview = false,
+ disableNotification = false
+ } = req.body;
+
+ const telegramService = require('../services/telegram');
+
+ let result;
+ if (chatIds.length > 0) {
+ // Send to multiple chats
+ result = await telegramService.sendCustomMessage({
+ text: message,
+ chatIds: chatIds.map(id => parseInt(id)),
+ parseMode,
+ disableWebPagePreview,
+ disableNotification
+ });
+
+ res.json({
+ success: result.success,
+ message: result.success ?
+ `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` :
+ 'Failed to send message',
+ results: result.results || [],
+ errors: result.errors || []
+ });
+ } else {
+ // Send to default chat
+ result = await telegramService.sendMessage(message, {
+ parse_mode: parseMode,
+ disable_web_page_preview: disableWebPagePreview,
+ disable_notification: disableNotification
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Message sent successfully!'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.error || result.message || 'Failed to send message'
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Send Telegram message error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Error sending message'
+ });
+ }
+});
+
+// Settings page
+router.get('/settings', requireAuth, addStats, (req, res) => {
+ res.render('admin/settings', {
+ title: 'Site Settings - Admin Panel',
+ layout: 'admin/layout',
+ user: req.session.user
+ });
+});
+
+// Banner editor page
+router.get('/banner-editor', requireAuth, addStats, (req, res) => {
+ res.render('admin/banner-editor', {
+ title: 'Banner Editor - Admin Panel',
+ layout: 'admin/layout',
+ currentPage: 'banner-editor',
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/api/admin_20251026221106.js b/.history/routes/api/admin_20251026221106.js
new file mode 100644
index 0000000..41ded91
--- /dev/null
+++ b/.history/routes/api/admin_20251026221106.js
@@ -0,0 +1,496 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const path = require('path');
+const { Portfolio, Service, Contact, User } = require('../../models');
+
+// Multer configuration for file uploads
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ cb(null, 'public/uploads/');
+ },
+ filename: (req, file, cb) => {
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
+ cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
+ }
+});
+
+const upload = multer({
+ storage: storage,
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed!'), false);
+ }
+ },
+ limits: {
+ fileSize: 10 * 1024 * 1024 // 10MB
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.status(401).json({ success: false, message: 'Authentication required' });
+ }
+ next();
+};
+
+// Portfolio API Routes
+router.post('/portfolio', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ clientName,
+ projectUrl,
+ githubUrl,
+ technologies,
+ featured,
+ isPublished
+ } = req.body;
+
+ // Process uploaded images
+ const images = req.files ? req.files.map((file, index) => ({
+ url: `/uploads/${file.filename}`,
+ alt: `${title} image ${index + 1}`,
+ isPrimary: index === 0
+ })) : [];
+
+ // Parse technologies
+ let techArray = [];
+ if (technologies) {
+ try {
+ techArray = JSON.parse(technologies);
+ } catch (e) {
+ techArray = technologies.split(',').map(t => t.trim());
+ }
+ }
+
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ clientName,
+ projectUrl: projectUrl || null,
+ githubUrl: githubUrl || null,
+ technologies: techArray,
+ images,
+ featured: featured === 'on',
+ isPublished: isPublished === 'on',
+ status: 'completed',
+ publishedAt: isPublished === 'on' ? new Date() : null
+ });
+
+ res.json({ success: true, portfolio });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.patch('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({ success: false, message: 'Portfolio not found' });
+ }
+
+ const updates = { ...req.body };
+
+ // Handle checkboxes
+ updates.featured = updates.featured === 'on';
+ updates.isPublished = updates.isPublished === 'on';
+
+ // Process technologies
+ if (updates.technologies) {
+ try {
+ updates.technologies = JSON.parse(updates.technologies);
+ } catch (e) {
+ updates.technologies = updates.technologies.split(',').map(t => t.trim());
+ }
+ }
+
+ // Process new images
+ if (req.files && req.files.length > 0) {
+ const newImages = req.files.map((file, index) => ({
+ url: `/uploads/${file.filename}`,
+ alt: `${updates.title || portfolio.title} image ${index + 1}`,
+ isPrimary: index === 0 && (!portfolio.images || portfolio.images.length === 0)
+ }));
+
+ updates.images = [...(portfolio.images || []), ...newImages];
+ }
+
+ await portfolio.update(updates);
+ res.json({ success: true, portfolio });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({ success: false, message: 'Portfolio not found' });
+ }
+
+ await portfolio.destroy();
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Services API Routes
+router.post('/services', requireAuth, async (req, res) => {
+ try {
+ const {
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features,
+ pricing,
+ estimatedTime,
+ isActive,
+ featured,
+ tags
+ } = req.body;
+
+ // Parse arrays
+ let featuresArray = [];
+ let tagsArray = [];
+ let pricingObj = {};
+
+ if (features) {
+ try {
+ featuresArray = JSON.parse(features);
+ } catch (e) {
+ featuresArray = features.split(',').map(f => f.trim());
+ }
+ }
+
+ if (tags) {
+ try {
+ tagsArray = JSON.parse(tags);
+ } catch (e) {
+ tagsArray = tags.split(',').map(t => t.trim());
+ }
+ }
+
+ if (pricing) {
+ try {
+ pricingObj = JSON.parse(pricing);
+ } catch (e) {
+ pricingObj = { basePrice: pricing };
+ }
+ }
+
+ const service = await Service.create({
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features: featuresArray,
+ pricing: pricingObj,
+ estimatedTime,
+ isActive: isActive === 'on',
+ featured: featured === 'on',
+ tags: tagsArray
+ });
+
+ res.json({ success: true, service });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.put('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({ success: false, message: 'Service not found' });
+ }
+
+ const {
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features,
+ pricing,
+ estimatedTime,
+ isActive,
+ featured,
+ tags,
+ order,
+ seo
+ } = req.body;
+
+ // Parse arrays and objects
+ let featuresArray = [];
+ let tagsArray = [];
+ let pricingObj = {};
+ let seoObj = {};
+
+ if (features) {
+ if (Array.isArray(features)) {
+ featuresArray = features;
+ } else {
+ try {
+ featuresArray = JSON.parse(features);
+ } catch (e) {
+ featuresArray = features.split(',').map(f => f.trim());
+ }
+ }
+ }
+
+ if (tags) {
+ if (Array.isArray(tags)) {
+ tagsArray = tags;
+ } else {
+ try {
+ tagsArray = JSON.parse(tags);
+ } catch (e) {
+ tagsArray = tags.split(',').map(t => t.trim());
+ }
+ }
+ }
+
+ if (pricing) {
+ if (typeof pricing === 'object') {
+ pricingObj = pricing;
+ } else {
+ try {
+ pricingObj = JSON.parse(pricing);
+ } catch (e) {
+ pricingObj = { basePrice: pricing };
+ }
+ }
+ }
+
+ if (seo) {
+ if (typeof seo === 'object') {
+ seoObj = seo;
+ } else {
+ try {
+ seoObj = JSON.parse(seo);
+ } catch (e) {
+ seoObj = {};
+ }
+ }
+ }
+
+ const updateData = {
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features: featuresArray,
+ pricing: pricingObj,
+ estimatedTime,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured),
+ tags: tagsArray,
+ order: order ? parseInt(order) : 0,
+ seo: seoObj
+ };
+
+ // Remove undefined values
+ Object.keys(updateData).forEach(key => {
+ if (updateData[key] === undefined) {
+ delete updateData[key];
+ }
+ });
+
+ await service.update(updateData);
+ res.json({ success: true, service });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({ success: false, message: 'Service not found' });
+ }
+
+ await service.destroy();
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Contacts API Routes
+router.patch('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+ if (!contact) {
+ return res.status(404).json({ success: false, message: 'Contact not found' });
+ }
+
+ await contact.update(req.body);
+ res.json({ success: true, contact });
+ } catch (error) {
+ console.error('Contact update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.delete('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+ if (!contact) {
+ return res.status(404).json({ success: false, message: 'Contact not found' });
+ }
+
+ await contact.destroy();
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Contact deletion error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Telegram notification for contact
+router.post('/contacts/:id/telegram', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+ if (!contact) {
+ return res.status(404).json({ success: false, message: 'Contact not found' });
+ }
+
+ // Send Telegram notification
+ const telegramService = require('../../services/telegram');
+ const result = await telegramService.sendContactNotification(contact);
+
+ if (result.success) {
+ res.json({ success: true });
+ } else {
+ res.status(500).json({ success: false, message: result.message || result.error });
+ }
+ } catch (error) {
+ console.error('Telegram notification error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Test Telegram connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const { botToken, chatId } = req.body;
+
+ // Temporarily set up telegram service with provided credentials
+ const axios = require('axios');
+
+ // Test bot info
+ const botResponse = await axios.get(`https://api.telegram.org/bot${botToken}/getMe`);
+
+ // Test sending a message
+ const testMessage = '✅ Telegram bot подключен успешно!\n\nЭто тестовое сообщение от SmartSolTech Admin Panel.';
+ await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, {
+ chat_id: chatId,
+ text: testMessage,
+ parse_mode: 'Markdown'
+ });
+
+ res.json({
+ success: true,
+ bot: botResponse.data.result,
+ message: 'Test message sent successfully'
+ });
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ let message = 'Connection failed';
+
+ if (error.response?.data?.description) {
+ message = error.response.data.description;
+ } else if (error.message) {
+ message = error.message;
+ }
+
+ res.status(400).json({ success: false, message });
+ }
+});
+
+// Settings API
+const { SiteSettings } = require('../../models');
+
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || {};
+ res.json({ success: true, settings });
+ } catch (error) {
+ console.error('Settings fetch error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.post('/settings', requireAuth, upload.fields([
+ { name: 'logo', maxCount: 1 },
+ { name: 'favicon', maxCount: 1 }
+]), async (req, res) => {
+ try {
+ let settings = await SiteSettings.findOne();
+ if (!settings) {
+ settings = await SiteSettings.create({});
+ }
+
+ const updates = {};
+
+ // Handle nested objects
+ Object.keys(req.body).forEach(key => {
+ if (key.includes('.')) {
+ const [parent, child] = key.split('.');
+ if (!updates[parent]) updates[parent] = {};
+ updates[parent][child] = req.body[key];
+ } else {
+ updates[key] = req.body[key];
+ }
+ });
+
+ // Handle file uploads
+ if (req.files.logo) {
+ updates.logo = `/uploads/${req.files.logo[0].filename}`;
+ }
+ if (req.files.favicon) {
+ updates.favicon = `/uploads/${req.files.favicon[0].filename}`;
+ }
+
+ // Update existing settings with new values
+ Object.keys(updates).forEach(key => {
+ if (typeof updates[key] === 'object' && updates[key] !== null) {
+ settings[key] = { ...settings[key], ...updates[key] };
+ } else {
+ settings[key] = updates[key];
+ }
+ });
+
+ await settings.save();
+
+ res.json({ success: true, settings });
+ } catch (error) {
+ console.error('Settings update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/api/admin_20251026221118.js b/.history/routes/api/admin_20251026221118.js
new file mode 100644
index 0000000..41ded91
--- /dev/null
+++ b/.history/routes/api/admin_20251026221118.js
@@ -0,0 +1,496 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const path = require('path');
+const { Portfolio, Service, Contact, User } = require('../../models');
+
+// Multer configuration for file uploads
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ cb(null, 'public/uploads/');
+ },
+ filename: (req, file, cb) => {
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
+ cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
+ }
+});
+
+const upload = multer({
+ storage: storage,
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype.startsWith('image/')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Only image files are allowed!'), false);
+ }
+ },
+ limits: {
+ fileSize: 10 * 1024 * 1024 // 10MB
+ }
+});
+
+// Authentication middleware
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.status(401).json({ success: false, message: 'Authentication required' });
+ }
+ next();
+};
+
+// Portfolio API Routes
+router.post('/portfolio', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const {
+ title,
+ shortDescription,
+ description,
+ category,
+ clientName,
+ projectUrl,
+ githubUrl,
+ technologies,
+ featured,
+ isPublished
+ } = req.body;
+
+ // Process uploaded images
+ const images = req.files ? req.files.map((file, index) => ({
+ url: `/uploads/${file.filename}`,
+ alt: `${title} image ${index + 1}`,
+ isPrimary: index === 0
+ })) : [];
+
+ // Parse technologies
+ let techArray = [];
+ if (technologies) {
+ try {
+ techArray = JSON.parse(technologies);
+ } catch (e) {
+ techArray = technologies.split(',').map(t => t.trim());
+ }
+ }
+
+ const portfolio = await Portfolio.create({
+ title,
+ shortDescription,
+ description,
+ category,
+ clientName,
+ projectUrl: projectUrl || null,
+ githubUrl: githubUrl || null,
+ technologies: techArray,
+ images,
+ featured: featured === 'on',
+ isPublished: isPublished === 'on',
+ status: 'completed',
+ publishedAt: isPublished === 'on' ? new Date() : null
+ });
+
+ res.json({ success: true, portfolio });
+ } catch (error) {
+ console.error('Portfolio creation error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.patch('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({ success: false, message: 'Portfolio not found' });
+ }
+
+ const updates = { ...req.body };
+
+ // Handle checkboxes
+ updates.featured = updates.featured === 'on';
+ updates.isPublished = updates.isPublished === 'on';
+
+ // Process technologies
+ if (updates.technologies) {
+ try {
+ updates.technologies = JSON.parse(updates.technologies);
+ } catch (e) {
+ updates.technologies = updates.technologies.split(',').map(t => t.trim());
+ }
+ }
+
+ // Process new images
+ if (req.files && req.files.length > 0) {
+ const newImages = req.files.map((file, index) => ({
+ url: `/uploads/${file.filename}`,
+ alt: `${updates.title || portfolio.title} image ${index + 1}`,
+ isPrimary: index === 0 && (!portfolio.images || portfolio.images.length === 0)
+ }));
+
+ updates.images = [...(portfolio.images || []), ...newImages];
+ }
+
+ await portfolio.update(updates);
+ res.json({ success: true, portfolio });
+ } catch (error) {
+ console.error('Portfolio update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.delete('/portfolio/:id', requireAuth, async (req, res) => {
+ try {
+ const portfolio = await Portfolio.findByPk(req.params.id);
+ if (!portfolio) {
+ return res.status(404).json({ success: false, message: 'Portfolio not found' });
+ }
+
+ await portfolio.destroy();
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Portfolio deletion error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Services API Routes
+router.post('/services', requireAuth, async (req, res) => {
+ try {
+ const {
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features,
+ pricing,
+ estimatedTime,
+ isActive,
+ featured,
+ tags
+ } = req.body;
+
+ // Parse arrays
+ let featuresArray = [];
+ let tagsArray = [];
+ let pricingObj = {};
+
+ if (features) {
+ try {
+ featuresArray = JSON.parse(features);
+ } catch (e) {
+ featuresArray = features.split(',').map(f => f.trim());
+ }
+ }
+
+ if (tags) {
+ try {
+ tagsArray = JSON.parse(tags);
+ } catch (e) {
+ tagsArray = tags.split(',').map(t => t.trim());
+ }
+ }
+
+ if (pricing) {
+ try {
+ pricingObj = JSON.parse(pricing);
+ } catch (e) {
+ pricingObj = { basePrice: pricing };
+ }
+ }
+
+ const service = await Service.create({
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features: featuresArray,
+ pricing: pricingObj,
+ estimatedTime,
+ isActive: isActive === 'on',
+ featured: featured === 'on',
+ tags: tagsArray
+ });
+
+ res.json({ success: true, service });
+ } catch (error) {
+ console.error('Service creation error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.put('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({ success: false, message: 'Service not found' });
+ }
+
+ const {
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features,
+ pricing,
+ estimatedTime,
+ isActive,
+ featured,
+ tags,
+ order,
+ seo
+ } = req.body;
+
+ // Parse arrays and objects
+ let featuresArray = [];
+ let tagsArray = [];
+ let pricingObj = {};
+ let seoObj = {};
+
+ if (features) {
+ if (Array.isArray(features)) {
+ featuresArray = features;
+ } else {
+ try {
+ featuresArray = JSON.parse(features);
+ } catch (e) {
+ featuresArray = features.split(',').map(f => f.trim());
+ }
+ }
+ }
+
+ if (tags) {
+ if (Array.isArray(tags)) {
+ tagsArray = tags;
+ } else {
+ try {
+ tagsArray = JSON.parse(tags);
+ } catch (e) {
+ tagsArray = tags.split(',').map(t => t.trim());
+ }
+ }
+ }
+
+ if (pricing) {
+ if (typeof pricing === 'object') {
+ pricingObj = pricing;
+ } else {
+ try {
+ pricingObj = JSON.parse(pricing);
+ } catch (e) {
+ pricingObj = { basePrice: pricing };
+ }
+ }
+ }
+
+ if (seo) {
+ if (typeof seo === 'object') {
+ seoObj = seo;
+ } else {
+ try {
+ seoObj = JSON.parse(seo);
+ } catch (e) {
+ seoObj = {};
+ }
+ }
+ }
+
+ const updateData = {
+ name,
+ description,
+ shortDescription,
+ icon,
+ category,
+ features: featuresArray,
+ pricing: pricingObj,
+ estimatedTime,
+ isActive: Boolean(isActive),
+ featured: Boolean(featured),
+ tags: tagsArray,
+ order: order ? parseInt(order) : 0,
+ seo: seoObj
+ };
+
+ // Remove undefined values
+ Object.keys(updateData).forEach(key => {
+ if (updateData[key] === undefined) {
+ delete updateData[key];
+ }
+ });
+
+ await service.update(updateData);
+ res.json({ success: true, service });
+ } catch (error) {
+ console.error('Service update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.delete('/services/:id', requireAuth, async (req, res) => {
+ try {
+ const service = await Service.findByPk(req.params.id);
+ if (!service) {
+ return res.status(404).json({ success: false, message: 'Service not found' });
+ }
+
+ await service.destroy();
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Service deletion error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Contacts API Routes
+router.patch('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+ if (!contact) {
+ return res.status(404).json({ success: false, message: 'Contact not found' });
+ }
+
+ await contact.update(req.body);
+ res.json({ success: true, contact });
+ } catch (error) {
+ console.error('Contact update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.delete('/contacts/:id', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+ if (!contact) {
+ return res.status(404).json({ success: false, message: 'Contact not found' });
+ }
+
+ await contact.destroy();
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Contact deletion error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Telegram notification for contact
+router.post('/contacts/:id/telegram', requireAuth, async (req, res) => {
+ try {
+ const contact = await Contact.findByPk(req.params.id);
+ if (!contact) {
+ return res.status(404).json({ success: false, message: 'Contact not found' });
+ }
+
+ // Send Telegram notification
+ const telegramService = require('../../services/telegram');
+ const result = await telegramService.sendContactNotification(contact);
+
+ if (result.success) {
+ res.json({ success: true });
+ } else {
+ res.status(500).json({ success: false, message: result.message || result.error });
+ }
+ } catch (error) {
+ console.error('Telegram notification error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+// Test Telegram connection
+router.post('/telegram/test', requireAuth, async (req, res) => {
+ try {
+ const { botToken, chatId } = req.body;
+
+ // Temporarily set up telegram service with provided credentials
+ const axios = require('axios');
+
+ // Test bot info
+ const botResponse = await axios.get(`https://api.telegram.org/bot${botToken}/getMe`);
+
+ // Test sending a message
+ const testMessage = '✅ Telegram bot подключен успешно!\n\nЭто тестовое сообщение от SmartSolTech Admin Panel.';
+ await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, {
+ chat_id: chatId,
+ text: testMessage,
+ parse_mode: 'Markdown'
+ });
+
+ res.json({
+ success: true,
+ bot: botResponse.data.result,
+ message: 'Test message sent successfully'
+ });
+ } catch (error) {
+ console.error('Telegram test error:', error);
+ let message = 'Connection failed';
+
+ if (error.response?.data?.description) {
+ message = error.response.data.description;
+ } else if (error.message) {
+ message = error.message;
+ }
+
+ res.status(400).json({ success: false, message });
+ }
+});
+
+// Settings API
+const { SiteSettings } = require('../../models');
+
+router.get('/settings', requireAuth, async (req, res) => {
+ try {
+ const settings = await SiteSettings.findOne() || {};
+ res.json({ success: true, settings });
+ } catch (error) {
+ console.error('Settings fetch error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+router.post('/settings', requireAuth, upload.fields([
+ { name: 'logo', maxCount: 1 },
+ { name: 'favicon', maxCount: 1 }
+]), async (req, res) => {
+ try {
+ let settings = await SiteSettings.findOne();
+ if (!settings) {
+ settings = await SiteSettings.create({});
+ }
+
+ const updates = {};
+
+ // Handle nested objects
+ Object.keys(req.body).forEach(key => {
+ if (key.includes('.')) {
+ const [parent, child] = key.split('.');
+ if (!updates[parent]) updates[parent] = {};
+ updates[parent][child] = req.body[key];
+ } else {
+ updates[key] = req.body[key];
+ }
+ });
+
+ // Handle file uploads
+ if (req.files.logo) {
+ updates.logo = `/uploads/${req.files.logo[0].filename}`;
+ }
+ if (req.files.favicon) {
+ updates.favicon = `/uploads/${req.files.favicon[0].filename}`;
+ }
+
+ // Update existing settings with new values
+ Object.keys(updates).forEach(key => {
+ if (typeof updates[key] === 'object' && updates[key] !== null) {
+ settings[key] = { ...settings[key], ...updates[key] };
+ } else {
+ settings[key] = updates[key];
+ }
+ });
+
+ await settings.save();
+
+ res.json({ success: true, settings });
+ } catch (error) {
+ console.error('Settings update error:', error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/demo-adminlte_20251026211505.js b/.history/routes/demo-adminlte_20251026211505.js
new file mode 100644
index 0000000..8dbde1e
--- /dev/null
+++ b/.history/routes/demo-adminlte_20251026211505.js
@@ -0,0 +1,77 @@
+const express = require('express');
+const router = express.Router();
+
+// Демо-роут для AdminLTE админки
+router.get('/demo-adminlte', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('AdminLTE Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/demo-adminlte_20251026211507.js b/.history/routes/demo-adminlte_20251026211507.js
new file mode 100644
index 0000000..8dbde1e
--- /dev/null
+++ b/.history/routes/demo-adminlte_20251026211507.js
@@ -0,0 +1,77 @@
+const express = require('express');
+const router = express.Router();
+
+// Демо-роут для AdminLTE админки
+router.get('/demo-adminlte', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('AdminLTE Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/demo-adminlte_20251026211708.js b/.history/routes/demo-adminlte_20251026211708.js
new file mode 100644
index 0000000..43aa1d4
--- /dev/null
+++ b/.history/routes/demo-adminlte_20251026211708.js
@@ -0,0 +1,150 @@
+const express = require('express');
+const router = express.Router();
+
+// Демо-роут для AdminLTE админки
+router.get('/demo-adminlte', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('AdminLTE Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+// Демо-роут для Tabler админки
+router.get('/demo-tabler', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-tabler', {
+ layout: 'admin/layout-tabler',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('Tabler Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/demo-adminlte_20251026211716.js b/.history/routes/demo-adminlte_20251026211716.js
new file mode 100644
index 0000000..43aa1d4
--- /dev/null
+++ b/.history/routes/demo-adminlte_20251026211716.js
@@ -0,0 +1,150 @@
+const express = require('express');
+const router = express.Router();
+
+// Демо-роут для AdminLTE админки
+router.get('/demo-adminlte', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('AdminLTE Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+// Демо-роут для Tabler админки
+router.get('/demo-tabler', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-tabler', {
+ layout: 'admin/layout-tabler',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('Tabler Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/demo-adminlte_20251026211846.js b/.history/routes/demo-adminlte_20251026211846.js
new file mode 100644
index 0000000..db28812
--- /dev/null
+++ b/.history/routes/demo-adminlte_20251026211846.js
@@ -0,0 +1,162 @@
+const express = require('express');
+const router = express.Router();
+
+// Главная страница сравнения admin bundles
+router.get('/admin-comparison', async (req, res) => {
+ try {
+ res.render('admin-bundle-comparison', {
+ layout: false // Не используем layout для этой страницы
+ });
+ } catch (error) {
+ console.error('Admin Comparison Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+// Демо-роут для AdminLTE админки
+router.get('/demo-adminlte', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('AdminLTE Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+// Демо-роут для Tabler админки
+router.get('/demo-tabler', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-tabler', {
+ layout: 'admin/layout-tabler',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('Tabler Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/routes/demo-adminlte_20251026211849.js b/.history/routes/demo-adminlte_20251026211849.js
new file mode 100644
index 0000000..db28812
--- /dev/null
+++ b/.history/routes/demo-adminlte_20251026211849.js
@@ -0,0 +1,162 @@
+const express = require('express');
+const router = express.Router();
+
+// Главная страница сравнения admin bundles
+router.get('/admin-comparison', async (req, res) => {
+ try {
+ res.render('admin-bundle-comparison', {
+ layout: false // Не используем layout для этой страницы
+ });
+ } catch (error) {
+ console.error('Admin Comparison Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+// Демо-роут для AdminLTE админки
+router.get('/demo-adminlte', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-adminlte', {
+ layout: 'admin/layout-adminlte',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('AdminLTE Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+// Демо-роут для Tabler админки
+router.get('/demo-tabler', async (req, res) => {
+ try {
+ // Мок-данные для демонстрации
+ const stats = {
+ portfolioCount: 12,
+ servicesCount: 6,
+ contactsCount: 24,
+ usersCount: 3
+ };
+
+ const recentPortfolio = [
+ {
+ title: '삼성 모바일 앱 개발',
+ category: 'mobile-app',
+ status: 'completed',
+ createdAt: new Date('2024-10-20')
+ },
+ {
+ title: 'LG 웹사이트 리뉴얼',
+ category: 'web-development',
+ status: 'in-progress',
+ createdAt: new Date('2024-10-18')
+ },
+ {
+ title: '현대차 UI/UX 디자인',
+ category: 'ui-ux-design',
+ status: 'planning',
+ createdAt: new Date('2024-10-15')
+ }
+ ];
+
+ const recentContacts = [
+ {
+ name: '김철수',
+ email: 'kim@example.com',
+ status: 'pending',
+ createdAt: new Date('2024-10-25')
+ },
+ {
+ name: '이영희',
+ email: 'lee@company.kr',
+ status: 'replied',
+ createdAt: new Date('2024-10-24')
+ },
+ {
+ name: '박민수',
+ email: 'park@startup.co.kr',
+ status: 'new',
+ createdAt: new Date('2024-10-23')
+ }
+ ];
+
+ const user = {
+ name: '관리자'
+ };
+
+ res.render('admin/dashboard-tabler', {
+ layout: 'admin/layout-tabler',
+ title: '대시보드',
+ currentPage: 'dashboard',
+ currentLanguage: 'ko',
+ stats,
+ recentPortfolio,
+ recentContacts,
+ user
+ });
+ } catch (error) {
+ console.error('Tabler Demo Error:', error);
+ res.status(500).send('Server Error');
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/.history/server_20251026211452.js b/.history/server_20251026211452.js
new file mode 100644
index 0000000..0f7c8d1
--- /dev/null
+++ b/.history/server_20251026211452.js
@@ -0,0 +1,220 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026211459.js b/.history/server_20251026211459.js
new file mode 100644
index 0000000..0f7c8d1
--- /dev/null
+++ b/.history/server_20251026211459.js
@@ -0,0 +1,220 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026211521.js b/.history/server_20251026211521.js
new file mode 100644
index 0000000..4c1d0ef
--- /dev/null
+++ b/.history/server_20251026211521.js
@@ -0,0 +1,223 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// AdminLTE Demo route
+app.use('/demo', require('./routes/demo-adminlte'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026211716.js b/.history/server_20251026211716.js
new file mode 100644
index 0000000..4c1d0ef
--- /dev/null
+++ b/.history/server_20251026211716.js
@@ -0,0 +1,223 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// AdminLTE Demo route
+app.use('/demo', require('./routes/demo-adminlte'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026212324.js b/.history/server_20251026212324.js
new file mode 100644
index 0000000..0f7c8d1
--- /dev/null
+++ b/.history/server_20251026212324.js
@@ -0,0 +1,220 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026212427.js b/.history/server_20251026212427.js
new file mode 100644
index 0000000..0f7c8d1
--- /dev/null
+++ b/.history/server_20251026212427.js
@@ -0,0 +1,220 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026213505.js b/.history/server_20251026213505.js
new file mode 100644
index 0000000..912888b
--- /dev/null
+++ b/.history/server_20251026213505.js
@@ -0,0 +1,220 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026213507.js b/.history/server_20251026213507.js
new file mode 100644
index 0000000..912888b
--- /dev/null
+++ b/.history/server_20251026213507.js
@@ -0,0 +1,220 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026215908.js b/.history/server_20251026215908.js
new file mode 100644
index 0000000..b657098
--- /dev/null
+++ b/.history/server_20251026215908.js
@@ -0,0 +1,220 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026215916.js b/.history/server_20251026215916.js
new file mode 100644
index 0000000..1249567
--- /dev/null
+++ b/.history/server_20251026215916.js
@@ -0,0 +1,223 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// Vendor files (local copies of external libraries)
+app.use('/vendor', express.static(path.join(__dirname, 'public/vendor')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/server_20251026215934.js b/.history/server_20251026215934.js
new file mode 100644
index 0000000..1249567
--- /dev/null
+++ b/.history/server_20251026215934.js
@@ -0,0 +1,223 @@
+const express = require('express');
+const { sequelize, testConnection } = require('./config/database');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const path = require('path');
+const helmet = require('helmet');
+const compression = require('compression');
+const cors = require('cors');
+const morgan = require('morgan');
+const rateLimit = require('express-rate-limit');
+const i18n = require('i18n');
+require('dotenv').config();
+
+const app = express();
+
+// Настройка i18n
+i18n.configure({
+ locales: ['ko', 'en', 'ru', 'kk'],
+ defaultLocale: 'ru',
+ directory: path.join(__dirname, 'locales'),
+ objectNotation: true,
+ updateFiles: false,
+ syncFiles: false
+});
+
+// i18n middleware
+app.use(i18n.init);
+
+// Security middleware
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.tailwindcss.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.tailwindcss.com"],
+ imgSrc: ["'self'", "data:", "https:"],
+ connectSrc: ["'self'", "ws:", "wss:", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
+ }
+ }
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
+app.use('/api/', limiter);
+
+// Middleware
+app.use(compression());
+app.use(cors());
+app.use(morgan('combined'));
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Static files
+app.use(express.static(path.join(__dirname, 'public')));
+app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
+
+// Vendor files (local copies of external libraries)
+app.use('/vendor', express.static(path.join(__dirname, 'public/vendor')));
+
+// AdminLTE static files
+app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+// Layout engine
+const expressLayouts = require('express-ejs-layouts');
+app.use(expressLayouts);
+app.set('layout', 'layout'); // Default layout for main site
+app.set('layout extractScripts', true);
+app.set('layout extractStyles', true);
+
+// Database connection and testing
+testConnection();
+
+// Session store configuration
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+ tableName: 'sessions',
+ checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
+ expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
+});
+
+// Session configuration
+app.use(session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ resave: false,
+ saveUninitialized: false,
+ store: sessionStore,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ }
+}));
+
+// Middleware для передачи переменных в шаблоны
+app.use((req, res, next) => {
+ const currentLang = req.session?.language || req.getLocale() || 'ru';
+ req.setLocale(currentLang);
+
+ res.locals.locale = currentLang;
+ res.locals.__ = res.__;
+ res.locals.theme = req.session?.theme || 'light';
+ res.locals.currentLanguage = currentLang;
+ res.locals.currentPage = req.path.split('/')[1] || 'home';
+
+ // Debug logging for theme
+ if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
+ }
+
+ // Устанавливаем заголовки для предотвращения кеширования языкового контента
+ if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.setHeader('Vary', 'Accept-Language');
+ }
+
+ // Отладочная информация
+ console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
+
+ next();
+});
+
+// Routes
+app.use('/', require('./routes/index'));
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/portfolio', require('./routes/portfolio'));
+app.use('/api/services', require('./routes/services'));
+app.use('/api/calculator', require('./routes/calculator'));
+app.use('/api/contact', require('./routes/contact'));
+app.use('/api/media', require('./routes/media'));
+app.use('/api/admin', require('./routes/api/admin'));
+app.use('/admin', require('./routes/admin'));
+
+// Language switching routes
+app.get('/lang/:language', (req, res) => {
+ const { language } = req.params;
+ const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
+
+ if (supportedLanguages.includes(language)) {
+ req.setLocale(language);
+ req.session.language = language;
+ console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
+ }
+
+ const referer = req.get('Referer') || '/';
+ res.redirect(referer);
+});
+
+// Theme switching routes
+app.get('/theme/:theme', (req, res) => {
+ const { theme } = req.params;
+ if (['light', 'dark'].includes(theme)) {
+ req.session.theme = theme;
+ }
+ res.json({ success: true, theme: req.session.theme });
+});
+
+// PWA Service Worker
+app.get('/sw.js', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'sw.js'));
+});
+
+// PWA Manifest
+app.get('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).render('error', {
+ title: 'Error',
+ settings: {},
+ message: process.env.NODE_ENV === 'production'
+ ? 'Something went wrong!'
+ : err.message,
+ currentPage: 'error'
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).render('error', {
+ title: '404 - 페이지를 찾을 수 없습니다',
+ settings: {},
+ message: '요청하신 페이지를 찾을 수 없습니다',
+ currentPage: 'error'
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Sync database and start server
+async function startServer() {
+ try {
+ // Sync all models with database
+ await sequelize.sync({ force: false });
+ console.log('✓ Database synchronized');
+
+ // Create session table
+ await sessionStore.sync();
+ console.log('✓ Session store synchronized');
+
+ app.listen(PORT, () => {
+ console.log(`🚀 Server running on port ${PORT}`);
+ console.log(`🌐 Visit: http://localhost:${PORT}`);
+ });
+ } catch (error) {
+ console.error('✗ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+startServer();
\ No newline at end of file
diff --git a/.history/views/LandingDemo_20251026205940.tsx b/.history/views/LandingDemo_20251026205940.tsx
new file mode 100644
index 0000000..74a4718
--- /dev/null
+++ b/.history/views/LandingDemo_20251026205940.tsx
@@ -0,0 +1,374 @@
+import React, { useEffect, useState } from "react";
+import {
+ ArrowRight, Calculator, Code2, Smartphone, Palette, LineChart, Eye, Menu, X,
+ Sun, Moon, ChevronDown, Phone, Mail, MessageCircle, Rocket, Users, Clock,
+ Headphones, Award
+} from "lucide-react";
+import AOS from "aos";
+import "aos/dist/aos.css";
+
+// Korean i18n strings to replace <%- __('...') %>
+const t = {
+ navigation: {
+ home: "홈",
+ about: "회사소개",
+ services: "서비스",
+ portfolio: "포트폴리오",
+ contact: "연락처",
+ calculator: "비용계산기"
+ },
+ language: {
+ korean: "한국어",
+ english: "English",
+ russian: "Русский",
+ kazakh: "Қазақша"
+ },
+ hero: {
+ title: { smart: "스마트", solutions: "솔루션" },
+ subtitle: "혁신적인 기술로 비즈니스를 성장시키세요",
+ description: "비즈니스의 디지털 전환을 이끄는 혁신적인 웹개발, 모바일 앱, UI/UX 디자인",
+ cta: { start: "시작하기", portfolio: "포트폴리오 보기" },
+ },
+ services: {
+ title: { our: "우리의", services: "서비스" },
+ subtitle: "아이디어를 현실로 만드는 전문 개발 서비스",
+ description: "최첨단 기술과 창의적 아이디어로 완성하는 디지털 솔루션",
+ view_all: "모든 서비스 보기",
+ web: { title: "웹 개발", description: "반응형 웹사이트 및 웹 애플리케이션 개발", price: "500,000원부터" },
+ mobile: { title: "모바일 앱", description: "iOS 및 Android 네이티브 앱 개발", price: "1,000,000원부터" },
+ design: { title: "UI/UX 디자인", description: "사용자 중심의 인터페이스 및 경험 디자인", price: "300,000원부터" },
+ marketing: { title: "디지털 마케팅", description: "SEO, 소셜미디어 마케팅, 광고 관리", price: "200,000원부터" },
+ hero: {
+ title: "우리의",
+ title_highlight: "서비스",
+ subtitle: "혁신적인 기술로 비즈니스 성장 지원"
+ },
+ cards: {
+ starting_price: "시작 가격",
+ consultation: "상담",
+ calculate_cost: "비용 계산",
+ popular: "인기"
+ },
+ process: {
+ title: "프로젝트 수행 과정",
+ subtitle: "체계적이고 전문적인 프로세스로 프로젝트를 진행합니다",
+ step1: { title: "상담 및 기획", description: "고객의 요구사항을 정확히 파악하고 최적의 솔루션을 기획합니다" },
+ step2: { title: "디자인 및 설계", description: "사용자 중심의 직관적인 디자인과 견고한 시스템 아키텍처를 설계합니다" },
+ step3: { title: "개발 및 구현", description: "최신 기술과 모범 사례를 활용하여 효율적이고 확장 가능한 솔루션을 개발합니다" },
+ step4: { title: "테스트 및 배포", description: "철저한 테스트를 통해 품질을 보장하고 안정적인 배포를 진행합니다" }
+ },
+ why_choose: {
+ title: "왜 SmartSolTech를 선택해야 할까요?",
+ modern_tech: { title: "최신 기술 활용", description: "항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 미래 지향적인 솔루션을 제공합니다." },
+ expert_team: { title: "전문가 팀", description: "각 분야의 전문가들로 구성된 팀이 협력하여 최고 품질의 결과물을 보장합니다." },
+ fast_response: { title: "빠른 대응", description: "신속한 의사소통과 빠른 피드백을 통해 프로젝트를 효율적으로 진행합니다." },
+ continuous_support: { title: "지속적인 지원", description: "프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 제공합니다." },
+ quality_guarantee: { title: "품질 보장", subtitle: "최고 품질의 결과물을 약속드립니다" }
+ },
+ cta: {
+ title: "프로젝트를 시작할 준비가 되셨나요?",
+ subtitle: "지금 바로 무료 상담을 받아보세요",
+ calculate_cost: "비용 계산하기",
+ view_portfolio: "포트폴리오 보기"
+ }
+ },
+ portfolio: {
+ title: { recent: "최근", projects: "프로젝트" },
+ subtitle: "웹, 모바일, 브랜딩 분야의 선별된 작업들",
+ view_all: "전체 포트폴리오 보기",
+ },
+ calculator: {
+ cta: { title: "프로젝트 비용 계산기", subtitle: "몇 가지 질문에 답하여 빠른 견적을 받아보세요", button: "계산기 열기" },
+ },
+ contact: {
+ cta: { ready: "시작할", start: "준비가", question: " 되셨나요?", subtitle: "프로젝트에 대해 알려주세요 - 하루 안에 답변드리겠습니다" },
+ phone: { title: "전화", number: "+82 10-1234-5678" },
+ email: { title: "이메일", address: "hello@smartsoltech.kr" },
+ telegram: { title: "텔레그램", subtitle: "@smartsoltech" },
+ form: {
+ title: "빠른 연락",
+ name: "성함",
+ email: "이메일",
+ phone: "전화번호 (선택사항)",
+ service: {
+ select: "어떤 서비스에 관심이 있으신가요?",
+ web: "웹 개발",
+ mobile: "모바일 앱",
+ design: "UI/UX 디자인",
+ branding: "브랜딩",
+ consulting: "컨설팅",
+ other: "기타",
+ },
+ message: "프로젝트에 대해 알려주세요",
+ submit: "메시지 보내기",
+ },
+ },
+ common: { view_details: "자세히 보기" },
+ theme: { toggle: "테마 변경" }
+};
+
+const services = [
+ { icon:
+ {t.hero.subtitle} +
+ ++ {t.services.subtitle} +
+{s.description}
++ {t.portfolio.subtitle} +
+{project.shortDescription ?? project.description}
+ + {t.common.view_details} +{t.contact.cta.subtitle}
++ {t.hero.subtitle} +
+ ++ {t.services.subtitle} +
+{s.description}
++ {t.portfolio.subtitle} +
+{project.shortDescription ?? project.description}
+ + {t.common.view_details} +{t.contact.cta.subtitle}
++ {t.hero.subtitle} +
+ ++ {t.hero.subtitle} +
+ ++ {t.services.hero.subtitle} +
++ {service.description} +
+ + {/* Pricing */} ++ {t.services.process.subtitle} +
++ {t.services.process.step1.description} +
++ {t.services.process.step2.description} +
++ {t.services.process.step3.description} +
++ {t.services.process.step4.description} +
++ {t.services.why_choose.modern_tech.description} +
++ {t.services.why_choose.expert_team.description} +
++ {t.services.why_choose.fast_response.description} +
++ {t.services.why_choose.continuous_support.description} +
++ {t.services.why_choose.quality_guarantee.subtitle} +
++ {t.portfolio.subtitle} +
+{project.shortDescription ?? project.description}
+ + {t.common.view_details} +{t.contact.cta.subtitle}
++ {t.hero.subtitle} +
+ ++ {t.hero.subtitle} +
+ ++ {t.services.hero.subtitle} +
++ {service.description} +
+ + {/* Pricing */} ++ {t.services.process.subtitle} +
++ {t.services.process.step1.description} +
++ {t.services.process.step2.description} +
++ {t.services.process.step3.description} +
++ {t.services.process.step4.description} +
++ {t.services.why_choose.modern_tech.description} +
++ {t.services.why_choose.expert_team.description} +
++ {t.services.why_choose.fast_response.description} +
++ {t.services.why_choose.continuous_support.description} +
++ {t.services.why_choose.quality_guarantee.subtitle} +
++ {t.portfolio.subtitle} +
+{project.shortDescription ?? project.description}
+ + {t.common.view_details} ++ {t.services.cta.subtitle} +
+ +{t.contact.cta.subtitle}
++ {t.hero.subtitle} +
+ ++ {t.hero.subtitle} +
+ ++ {t.services.hero.subtitle} +
++ {service.description} +
+ + {/* Pricing */} ++ {t.services.process.subtitle} +
++ {t.services.process.step1.description} +
++ {t.services.process.step2.description} +
++ {t.services.process.step3.description} +
++ {t.services.process.step4.description} +
++ {t.services.why_choose.modern_tech.description} +
++ {t.services.why_choose.expert_team.description} +
++ {t.services.why_choose.fast_response.description} +
++ {t.services.why_choose.continuous_support.description} +
++ {t.services.why_choose.quality_guarantee.subtitle} +
++ {t.portfolio.subtitle} +
+{project.shortDescription ?? project.description}
+ + {t.common.view_details} ++ {t.services.cta.subtitle} +
+ +{t.contact.cta.subtitle}
++ 우리 웹사이트에 가장 적합한 관리자 패널을 선택해보세요. 각 솔루션의 기능과 디자인을 비교해보실 수 있습니다. +
+무료 오픈소스 관리자 템플릿
+모던 관리자 대시보드 템플릿
+| 기능 | +AdminLTE | +Tabler | +
|---|---|---|
| 가격 | +완전 무료 | +무료 + Pro 버전 | +
| 한국어 지원 | +우수 | +보통 | +
| 디자인 | +클래식 | +모던 | +
| 컴포넌트 수 | +매우 많음 | +적당함 | +
| 성능 | +양호 | +우수 | +
| 커뮤니티 | +대규모 | +성장중 | +
| 학습 곡선 | +쉬움 | +보통 | +
+ SmartSolTech 웹사이트의 특성을 고려한 맞춤 추천 +
+ +
+ 추천 이유:
+ • 한국 고객을 위한 완벽한 한국어 지원
+ • 무료로 모든 기능 사용 가능
+ • 기존 시스템과의 호환성 우수
+ • 풍부한 문서화와 커뮤니티 지원
+ • 기업용 웹사이트에 적합한 안정적인 디자인
+
+ 우리 웹사이트에 가장 적합한 관리자 패널을 선택해보세요. 각 솔루션의 기능과 디자인을 비교해보실 수 있습니다. +
+무료 오픈소스 관리자 템플릿
+모던 관리자 대시보드 템플릿
+| 기능 | +AdminLTE | +Tabler | +
|---|---|---|
| 가격 | +완전 무료 | +무료 + Pro 버전 | +
| 한국어 지원 | +우수 | +보통 | +
| 디자인 | +클래식 | +모던 | +
| 컴포넌트 수 | +매우 많음 | +적당함 | +
| 성능 | +양호 | +우수 | +
| 커뮤니티 | +대규모 | +성장중 | +
| 학습 곡선 | +쉬움 | +보통 | +
+ SmartSolTech 웹사이트의 특성을 고려한 맞춤 추천 +
+ +
+ 추천 이유:
+ • 한국 고객을 위한 완벽한 한국어 지원
+ • 무료로 모든 기능 사용 가능
+ • 기존 시스템과의 호환성 우수
+ • 풍부한 문서화와 커뮤니티 지원
+ • 기업용 웹사이트에 적합한 안정적인 디자인
+
Перетащите изображения сюда или нажмите для выбора
+Поддерживаются: JPG, PNG, GIF (максимум 10MB)
+ + +Перетащите изображения сюда или нажмите для выбора
+Поддерживаются: JPG, PNG, GIF (максимум 10MB)
+ + +<%= project.category %>
+ + + <%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +아직 포트폴리오 프로젝트가 없습니다.
+ + + 첫 번째 프로젝트 추가 + +<%= contact.email %>
+ + + <%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +새로운 문의가 없습니다.
+ 고객 문의가 들어오면 여기에 표시됩니다. +<%= project.category %>
+ + + <%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +아직 포트폴리오 프로젝트가 없습니다.
+ + + 첫 번째 프로젝트 추가 + +<%= contact.email %>
+ + + <%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +새로운 문의가 없습니다.
+ 고객 문의가 들어오면 여기에 표시됩니다. +새로운 문의가 없습니다
++ 고객 문의가 들어오면 여기에 표시됩니다 +
+새로운 문의가 없습니다
++ 고객 문의가 들어오면 여기에 표시됩니다 +
+<%= project.category %>
+ + + <%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +아직 포트폴리오 프로젝트가 없습니다.
+ + + 첫 번째 프로젝트 추가 + +<%= contact.email %>
+ + + <%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +새로운 문의가 없습니다.
+ 고객 문의가 들어오면 여기에 표시됩니다. +<%= project.category %>
+ + + <%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +아직 포트폴리오 프로젝트가 없습니다.
+ + + 첫 번째 프로젝트 추가 + +<%= contact.email %>
+ + + <%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %> + +새로운 문의가 없습니다.
+ 고객 문의가 들어오면 여기에 표시됩니다. +Загрузка медиа файлов...
+