feat: Реализован полный CRUD для админ-панели и улучшена функциональность
- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации - Services CRUD: полное управление услугами с возможностью активации/деактивации - Banner system: новая модель Banner с CRUD операциями и аналитикой кликов - Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений - Media management: улучшенная загрузка файлов с оптимизацией изображений и превью - UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой - Database: добавлена таблица banners с полями для баннеров и аналитики
This commit is contained in:
@@ -29,7 +29,7 @@ function startDevelopmentServer() {
|
||||
console.log('');
|
||||
|
||||
const nodemonArgs = [
|
||||
'--script', NODEMON_CONFIG.script,
|
||||
NODEMON_CONFIG.script,
|
||||
'--ext', NODEMON_CONFIG.ext,
|
||||
'--ignore', NODEMON_CONFIG.ignore.join(','),
|
||||
'--watch', NODEMON_CONFIG.watch.join(','),
|
||||
|
||||
@@ -2,32 +2,29 @@
|
||||
|
||||
/**
|
||||
* Database initialization script for SmartSolTech
|
||||
* Creates initial admin user and sample data
|
||||
* Creates initial admin user and sample data for PostgreSQL
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { sequelize } = require('../config/database');
|
||||
require('dotenv').config();
|
||||
|
||||
// Import models
|
||||
const User = require('../models/User');
|
||||
const Service = require('../models/Service');
|
||||
const Portfolio = require('../models/Portfolio');
|
||||
const SiteSettings = require('../models/SiteSettings');
|
||||
const { User, Service, Portfolio, SiteSettings } = require('../models');
|
||||
|
||||
// Configuration
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech';
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@smartsoltech.kr';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123456';
|
||||
|
||||
async function initializeDatabase() {
|
||||
try {
|
||||
console.log('🔄 Connecting to MongoDB...');
|
||||
await mongoose.connect(MONGODB_URI, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
});
|
||||
console.log('✅ Connected to MongoDB');
|
||||
console.log('🔄 Connecting to PostgreSQL...');
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Connected to PostgreSQL');
|
||||
|
||||
// Sync database (create tables)
|
||||
console.log('🔄 Syncing database schema...');
|
||||
await sequelize.sync({ force: false });
|
||||
console.log('✅ Database schema synchronized');
|
||||
|
||||
// Create admin user
|
||||
await createAdminUser();
|
||||
@@ -50,7 +47,7 @@ async function initializeDatabase() {
|
||||
console.error('❌ Database initialization failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await mongoose.connection.close();
|
||||
await sequelize.close();
|
||||
console.log('🔌 Database connection closed');
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -58,14 +55,16 @@ async function initializeDatabase() {
|
||||
|
||||
async function createAdminUser() {
|
||||
try {
|
||||
const existingAdmin = await User.findOne({ email: ADMIN_EMAIL });
|
||||
const existingAdmin = await User.findOne({
|
||||
where: { email: ADMIN_EMAIL }
|
||||
});
|
||||
|
||||
if (existingAdmin) {
|
||||
console.log('👤 Admin user already exists, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const adminUser = new User({
|
||||
const adminUser = await User.create({
|
||||
name: 'Administrator',
|
||||
email: ADMIN_EMAIL,
|
||||
password: ADMIN_PASSWORD,
|
||||
@@ -73,7 +72,6 @@ async function createAdminUser() {
|
||||
isActive: true
|
||||
});
|
||||
|
||||
await adminUser.save();
|
||||
console.log('✅ Admin user created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating admin user:', error);
|
||||
@@ -83,7 +81,7 @@ async function createAdminUser() {
|
||||
|
||||
async function createSampleServices() {
|
||||
try {
|
||||
const existingServices = await Service.countDocuments();
|
||||
const existingServices = await Service.count();
|
||||
|
||||
if (existingServices > 0) {
|
||||
console.log('🛠️ Services already exist, skipping...');
|
||||
@@ -243,7 +241,7 @@ async function createSampleServices() {
|
||||
}
|
||||
];
|
||||
|
||||
await Service.insertMany(services);
|
||||
await Service.bulkCreate(services);
|
||||
console.log('✅ Sample services created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating sample services:', error);
|
||||
@@ -253,7 +251,7 @@ async function createSampleServices() {
|
||||
|
||||
async function createSamplePortfolio() {
|
||||
try {
|
||||
const existingPortfolio = await Portfolio.countDocuments();
|
||||
const existingPortfolio = await Portfolio.count();
|
||||
|
||||
if (existingPortfolio > 0) {
|
||||
console.log('🎨 Portfolio items already exist, skipping...');
|
||||
@@ -416,7 +414,7 @@ async function createSamplePortfolio() {
|
||||
}
|
||||
];
|
||||
|
||||
await Portfolio.insertMany(portfolioItems);
|
||||
await Portfolio.bulkCreate(portfolioItems);
|
||||
console.log('✅ Sample portfolio items created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating sample portfolio:', error);
|
||||
@@ -433,7 +431,7 @@ async function createSiteSettings() {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = new SiteSettings({
|
||||
const settings = await SiteSettings.create({
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
logo: '/images/logo.png',
|
||||
@@ -476,7 +474,6 @@ async function createSiteSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
await settings.save();
|
||||
console.log('✅ Site settings created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating site settings:', error);
|
||||
|
||||
115
scripts/sync-locales.js
Normal file
115
scripts/sync-locales.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const localesDir = path.join(__dirname, '..', 'locales');
|
||||
const files = fs.readdirSync(localesDir).filter(f => f.endsWith('.json'));
|
||||
|
||||
// priority order for falling back when filling missing translations
|
||||
const priority = ['en.json', 'ko.json', 'ru.json', 'kk.json'];
|
||||
|
||||
function readJSON(file) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(localesDir, file), 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to read', file, e.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function flatten(obj, prefix = '') {
|
||||
const res = {};
|
||||
for (const k of Object.keys(obj)) {
|
||||
const val = obj[k];
|
||||
const key = prefix ? `${prefix}.${k}` : k;
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
Object.assign(res, flatten(val, key));
|
||||
} else {
|
||||
res[key] = val;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function unflatten(flat) {
|
||||
const res = {};
|
||||
for (const flatKey of Object.keys(flat)) {
|
||||
const parts = flatKey.split('.');
|
||||
let cur = res;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
if (i === parts.length - 1) {
|
||||
cur[p] = flat[flatKey];
|
||||
} else {
|
||||
cur[p] = cur[p] || {};
|
||||
cur = cur[p];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// load all
|
||||
const data = {};
|
||||
for (const f of files) {
|
||||
data[f] = readJSON(f);
|
||||
}
|
||||
|
||||
// use en.json as canonical key set if exists, else merge all keys
|
||||
let canonical = {};
|
||||
if (files.includes('en.json')) {
|
||||
canonical = flatten(data['en.json']);
|
||||
} else {
|
||||
for (const f of files) {
|
||||
canonical = Object.assign(canonical, flatten(data[f]));
|
||||
}
|
||||
}
|
||||
|
||||
function getFallback(key, currentFile) {
|
||||
for (const p of priority) {
|
||||
if (p === currentFile) continue;
|
||||
if (!files.includes(p)) continue;
|
||||
const flat = flatten(data[p]);
|
||||
if (flat[key] && typeof flat[key] === 'string' && flat[key].trim()) return flat[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let report = {};
|
||||
for (const f of files) {
|
||||
const flat = flatten(data[f]);
|
||||
report[f] = { added: 0, marked: 0 };
|
||||
const out = {};
|
||||
for (const key of Object.keys(canonical)) {
|
||||
if (flat.hasOwnProperty(key)) {
|
||||
out[key] = flat[key];
|
||||
} else {
|
||||
const fb = getFallback(key, f);
|
||||
if (fb) {
|
||||
out[key] = fb;
|
||||
} else {
|
||||
out[key] = '[TRANSLATE] ' + canonical[key];
|
||||
report[f].marked++;
|
||||
}
|
||||
report[f].added++;
|
||||
}
|
||||
}
|
||||
// also keep any extra keys present in this file but not in canonical
|
||||
for (const key of Object.keys(flat)) {
|
||||
if (!out.hasOwnProperty(key)) {
|
||||
out[key] = flat[key];
|
||||
}
|
||||
}
|
||||
// write back
|
||||
const nested = unflatten(out);
|
||||
try {
|
||||
fs.writeFileSync(path.join(localesDir, f), JSON.stringify(nested, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
console.error('Failed to write', f, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Locale sync report:');
|
||||
for (const f of files) {
|
||||
console.log(`- ${f}: added ${report[f].added} keys, ${report[f].marked} marked for translation`);
|
||||
}
|
||||
console.log('Done. Review files in locales/ and replace [TRANSLATE] placeholders with correct translations.');
|
||||
Reference in New Issue
Block a user