fix: Исправлена функциональность создания и редактирования портфолио
- Добавлена поддержка загрузки изображений через multer - Исправлена обработка FormData в маршрутах создания и обновления портфолио - Добавлена обработка изображений с помощью Sharp (оптимизация в WebP) - Добавлен маршрут предпросмотра проекта - Исправлена валидация и парсинг технологий из JSON - Поддержка сохранения черновиков и публикации проектов
This commit is contained in:
276
routes/admin.js
276
routes/admin.js
@@ -1,8 +1,26 @@
|
||||
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) {
|
||||
@@ -536,23 +554,8 @@ router.get('/portfolio/add', requireAuth, (req, res) => {
|
||||
});
|
||||
|
||||
// Create portfolio item
|
||||
router.post('/portfolio/add', requireAuth, [
|
||||
body('title').notEmpty().withMessage('제목을 입력해주세요'),
|
||||
body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
|
||||
body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
|
||||
body('category').notEmpty().withMessage('카테고리를 선택해주세요'),
|
||||
body('technologies').isArray({ min: 1 }).withMessage('최소 한 개의 기술을 입력해주세요'),
|
||||
], async (req, res) => {
|
||||
router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '입력 데이터를 확인해주세요',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
shortDescription,
|
||||
@@ -563,40 +566,101 @@ router.post('/portfolio/add', requireAuth, [
|
||||
githubUrl,
|
||||
clientName,
|
||||
duration,
|
||||
isPublished = false,
|
||||
featured = false
|
||||
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: Array.isArray(technologies) ? technologies : technologies.split(',').map(t => t.trim()),
|
||||
demoUrl: demoUrl || null,
|
||||
technologies: parsedTechnologies,
|
||||
images: processedImages,
|
||||
projectUrl: demoUrl || null,
|
||||
githubUrl: githubUrl || null,
|
||||
clientName: clientName || null,
|
||||
duration: duration ? parseInt(duration) : null,
|
||||
isPublished: Boolean(isPublished),
|
||||
featured: Boolean(featured),
|
||||
publishedAt: Boolean(isPublished) ? new Date() : null,
|
||||
status: Boolean(isPublished) ? 'published' : 'draft'
|
||||
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: '포트폴리오가 성공적으로 생성되었습니다',
|
||||
message: 'Проект успешно создан!',
|
||||
portfolio: {
|
||||
id: portfolio.id,
|
||||
title: portfolio.title,
|
||||
category: portfolio.category
|
||||
category: portfolio.category,
|
||||
isPublished: portfolio.isPublished
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio creation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '포트폴리오 생성 중 오류가 발생했습니다'
|
||||
message: 'Ошибка при создании проекта: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -639,27 +703,13 @@ router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// Update portfolio item
|
||||
router.put('/portfolio/:id', requireAuth, [
|
||||
body('title').notEmpty().withMessage('제목을 입력해주세요'),
|
||||
body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'),
|
||||
body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'),
|
||||
body('category').notEmpty().withMessage('카테고리를 선택해주세요')
|
||||
], async (req, res) => {
|
||||
router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '입력 데이터를 확인해주세요',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const portfolio = await Portfolio.findByPk(req.params.id);
|
||||
if (!portfolio) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '포트폴리오를 찾을 수 없습니다'
|
||||
message: 'Проект не найден'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -673,41 +723,103 @@ router.put('/portfolio/:id', requireAuth, [
|
||||
githubUrl,
|
||||
clientName,
|
||||
duration,
|
||||
seoTitle,
|
||||
seoDescription,
|
||||
isPublished,
|
||||
featured
|
||||
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: Array.isArray(technologies) ? technologies : technologies.split(',').map(t => t.trim()),
|
||||
demoUrl: demoUrl || null,
|
||||
technologies: parsedTechnologies,
|
||||
images: finalImages,
|
||||
projectUrl: demoUrl || null,
|
||||
githubUrl: githubUrl || null,
|
||||
clientName: clientName || null,
|
||||
duration: duration ? parseInt(duration) : null,
|
||||
isPublished: Boolean(isPublished),
|
||||
featured: Boolean(featured),
|
||||
publishedAt: Boolean(isPublished) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt,
|
||||
status: Boolean(isPublished) ? 'published' : 'draft'
|
||||
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: '포트폴리오가 성공적으로 업데이트되었습니다',
|
||||
message: 'Проект успешно обновлен!',
|
||||
portfolio: {
|
||||
id: portfolio.id,
|
||||
title: portfolio.title,
|
||||
category: portfolio.category
|
||||
category: portfolio.category,
|
||||
isPublished: portfolio.isPublished
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio update error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '포트폴리오 업데이트 중 오류가 발생했습니다'
|
||||
message: 'Ошибка при обновлении проекта: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -772,6 +884,64 @@ router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user