commit 150891b29ddce6a9ceb6404c49ae6efdb6149560 Author: Andrew K. Choi Date: Sun Oct 19 18:27:00 2025 +0900 init commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bbf9a40 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# Environment Configuration +NODE_ENV=development +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# JWT Secret +JWT_SECRET=your_super_secret_jwt_key_here_change_in_production + +# Session Secret +SESSION_SECRET=your_session_secret_here_change_in_production + +# Telegram Bot +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here +TELEGRAM_CHAT_ID=your_telegram_chat_id_here + +# Email Configuration (for contact forms) +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your_email@gmail.com +EMAIL_PASS=your_email_password + +# Admin Credentials (default) +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=admin123456 + +# File Upload +MAX_FILE_SIZE=10485760 +UPLOAD_PATH=./public/uploads + +# Site Configuration +SITE_URL=https://smartsoltech.kr +SITE_NAME=SmartSolTech \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..239c253 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules/ +.env +.env.local +.env.production +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +dist/ +build/ +uploads/ +*.log +.vscode/ +.idea/ +coverage/ +.nyc_output/ +*.tgz +*.tar.gz +.cache/ +.parcel-cache/ \ No newline at end of file diff --git a/.history/.env_20251019160502.example b/.history/.env_20251019160502.example new file mode 100644 index 0000000..bbf9a40 --- /dev/null +++ b/.history/.env_20251019160502.example @@ -0,0 +1,34 @@ +# Environment Configuration +NODE_ENV=development +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# JWT Secret +JWT_SECRET=your_super_secret_jwt_key_here_change_in_production + +# Session Secret +SESSION_SECRET=your_session_secret_here_change_in_production + +# Telegram Bot +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here +TELEGRAM_CHAT_ID=your_telegram_chat_id_here + +# Email Configuration (for contact forms) +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your_email@gmail.com +EMAIL_PASS=your_email_password + +# Admin Credentials (default) +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=admin123456 + +# File Upload +MAX_FILE_SIZE=10485760 +UPLOAD_PATH=./public/uploads + +# Site Configuration +SITE_URL=https://smartsoltech.kr +SITE_NAME=SmartSolTech \ No newline at end of file diff --git a/.history/.env_20251019162544.example b/.history/.env_20251019162544.example new file mode 100644 index 0000000..bbf9a40 --- /dev/null +++ b/.history/.env_20251019162544.example @@ -0,0 +1,34 @@ +# Environment Configuration +NODE_ENV=development +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# JWT Secret +JWT_SECRET=your_super_secret_jwt_key_here_change_in_production + +# Session Secret +SESSION_SECRET=your_session_secret_here_change_in_production + +# Telegram Bot +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here +TELEGRAM_CHAT_ID=your_telegram_chat_id_here + +# Email Configuration (for contact forms) +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your_email@gmail.com +EMAIL_PASS=your_email_password + +# Admin Credentials (default) +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=admin123456 + +# File Upload +MAX_FILE_SIZE=10485760 +UPLOAD_PATH=./public/uploads + +# Site Configuration +SITE_URL=https://smartsoltech.kr +SITE_NAME=SmartSolTech \ No newline at end of file diff --git a/.history/.gitignore_20251019160509 b/.history/.gitignore_20251019160509 new file mode 100644 index 0000000..239c253 --- /dev/null +++ b/.history/.gitignore_20251019160509 @@ -0,0 +1,20 @@ +node_modules/ +.env +.env.local +.env.production +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +dist/ +build/ +uploads/ +*.log +.vscode/ +.idea/ +coverage/ +.nyc_output/ +*.tgz +*.tar.gz +.cache/ +.parcel-cache/ \ No newline at end of file diff --git a/.history/.gitignore_20251019162544 b/.history/.gitignore_20251019162544 new file mode 100644 index 0000000..239c253 --- /dev/null +++ b/.history/.gitignore_20251019162544 @@ -0,0 +1,20 @@ +node_modules/ +.env +.env.local +.env.production +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +dist/ +build/ +uploads/ +*.log +.vscode/ +.idea/ +coverage/ +.nyc_output/ +*.tgz +*.tar.gz +.cache/ +.parcel-cache/ \ No newline at end of file diff --git a/.history/README_20251019162701.md b/.history/README_20251019162701.md new file mode 100644 index 0000000..e831520 --- /dev/null +++ b/.history/README_20251019162701.md @@ -0,0 +1,280 @@ +# SmartSolTech Website + +Современный PWA сайт для SmartSolTech с админ-панелью, управлением портфолио, интеграцией с Telegram ботом и калькулятором стоимости услуг. + +## 🚀 Особенности + +- **Современный дизайн**: Отзывчивый интерфейс с анимациями и современным UI/UX +- **PWA**: Прогрессивное веб-приложение с поддержкой офлайн-режима +- **Админ-панель**: Управление портфолио, услугами, медиа-контентом и настройками сайта +- **Интеграция с Telegram**: Подключение к Telegram боту для уведомлений +- **Калькулятор стоимости**: Интерактивный калькулятор расчета стоимости услуг +- **Система аутентификации**: Безопасная авторизация с JWT токенами +- **Загрузка файлов**: Оптимизация изображений и управление медиа-контентом +- **SEO оптимизация**: Настроенные мета-теги и структурированные данные + +## 🛠️ Технологии + +### Backend +- **Node.js** - Серверная платформа +- **Express.js** - Веб-фреймворк +- **MongoDB** - База данных +- **Mongoose** - ODM для MongoDB +- **JWT** - Токены аутентификации +- **bcrypt** - Хеширование паролей +- **Multer** - Загрузка файлов +- **Sharp** - Обработка изображений + +### Frontend +- **EJS** - Шаблонизатор +- **Tailwind CSS** - CSS фреймворк +- **AOS** - Библиотека анимаций +- **Font Awesome** - Иконки +- **Service Worker** - PWA функциональность + +### Дополнительно +- **node-telegram-bot-api** - Интеграция с Telegram +- **Nodemailer** - Отправка email +- **Helmet** - Безопасность +- **express-rate-limit** - Ограничение запросов + +## 📦 Установка + +1. **Клонирование репозитория** +```bash +git clone +cd sst_site +``` + +2. **Установка зависимостей** +```bash +npm install +``` + +3. **Настройка переменных окружения** +```bash +cp .env.example .env +``` + +Отредактируйте файл `.env` с вашими настройками: +```env +NODE_ENV=development +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# Security +SESSION_SECRET=your-super-secret-session-key +JWT_SECRET=your-super-secret-jwt-key + +# File Upload +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password + +# Telegram Bot (Optional) +TELEGRAM_BOT_TOKEN=your-telegram-bot-token + +# Admin Account +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=change-this-password +``` + +4. **Инициализация базы данных** +```bash +npm run init-db +``` + +5. **Запуск в режиме разработки** +```bash +npm run dev +``` + +Сайт будет доступен по адресу: `http://localhost:3000` + +## 🗂️ Структура проекта + +``` +sst_site/ +├── models/ # Модели данных (MongoDB) +│ ├── User.js +│ ├── Portfolio.js +│ ├── Service.js +│ ├── Contact.js +│ └── SiteSettings.js +├── routes/ # Маршруты API +│ ├── index.js +│ ├── auth.js +│ ├── contact.js +│ ├── calculator.js +│ ├── portfolio.js +│ ├── services.js +│ ├── media.js +│ └── admin.js +├── views/ # Шаблоны EJS +│ ├── layout.ejs +│ ├── index.ejs +│ ├── calculator.ejs +│ └── partials/ +├── public/ # Статические файлы +│ ├── css/ +│ ├── js/ +│ ├── images/ +│ ├── manifest.json +│ └── sw.js +├── middleware/ # Промежуточное ПО +├── scripts/ # Служебные скрипты +│ ├── init-db.js +│ ├── dev.js +│ └── build.js +├── uploads/ # Загруженные файлы +├── server.js # Главный файл сервера +├── package.json +└── README.md +``` + +## 📋 Доступные команды + +```bash +# Разработка +npm run dev # Запуск в режиме разработки с hot reload + +# Продакшн +npm start # Запуск в продакшн режиме +npm run build # Сборка для продакшна + +# База данных +npm run init-db # Инициализация БД с тестовыми данными + +# Тестирование +npm test # Запуск тестов (пока не реализовано) +``` + +## 🔐 Админ-панель + +После инициализации базы данных вы можете войти в админ-панель: + +- **URL**: `http://localhost:3000/admin` +- **Email**: `admin@smartsoltech.kr` (или из .env) +- **Пароль**: Указанный в .env файле + +### Возможности админ-панели: +- Управление портфолио проектами +- Редактирование услуг и их стоимости +- Загрузка и управление медиа-файлами +- Просмотр и управление контактными формами +- Настройки сайта и SEO +- Управление пользователями + +## 🤖 Интеграция с Telegram + +Для настройки Telegram бота: + +1. Создайте бота через [@BotFather](https://t.me/BotFather) +2. Получите токен бота +3. Добавьте токен в `.env` файл: `TELEGRAM_BOT_TOKEN=your-token` +4. Перезапустите сервер + +Бот будет отправлять уведомления о: +- Новых контактных формах +- Заказах через калькулятор +- Новых комментариях + +## 💰 Калькулятор стоимости + +Интерактивный калькулятор позволяет клиентам: +- Выбрать тип услуги +- Указать дополнительные параметры +- Получить примерную стоимость +- Отправить заявку на расчет + +Настройки калькулятора можно изменить в админ-панели. + +## 🔒 Безопасность + +Проект включает следующие меры безопасности: +- Хеширование паролей с bcrypt +- JWT токены для аутентификации +- Защита от CSRF атак +- Ограничение количества запросов +- Валидация входных данных +- Безопасные HTTP заголовки + +## 📱 PWA функции + +- Установка на устройство +- Офлайн работа +- Push уведомления +- Фоновая синхронизация +- Адаптивные иконки +- Splash screen + +## 🚀 Деплой + +### Для продакшна: + +1. **Сборка приложения** +```bash +npm run build +``` + +2. **Настройка сервера** +- Установите Node.js и MongoDB +- Настройте переменные окружения +- Настройте прокси-сервер (nginx) + +3. **Запуск** +```bash +cd dist +npm install --production +npm start +``` + +### Docker деплой: + +```dockerfile +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +EXPOSE 3000 +CMD ["npm", "start"] +``` + +## 🤝 Участие в разработке + +1. Форкните репозиторий +2. Создайте ветку для фичи (`git checkout -b feature/AmazingFeature`) +3. Сделайте коммит (`git commit -m 'Add some AmazingFeature'`) +4. Отправьте в ветку (`git push origin feature/AmazingFeature`) +5. Откройте Pull Request + +## 📝 Лицензия + +Этот проект лицензирован под MIT License - подробности в файле [LICENSE](LICENSE). + +## 📞 Поддержка + +Если у вас есть вопросы или проблемы: + +- Email: info@smartsoltech.kr +- GitHub Issues: [Create Issue](../../issues) +- Telegram: @smartsoltech + +## 🙏 Благодарности + +- [Express.js](https://expressjs.com/) - За отличный веб-фреймворк +- [MongoDB](https://www.mongodb.com/) - За гибкую базу данных +- [Tailwind CSS](https://tailwindcss.com/) - За удобный CSS фреймворк +- [AOS](https://michalsnik.github.io/aos/) - За красивые анимации + +--- + +**SmartSolTech** - Умные решения для вашего бизнеса 🚀 \ No newline at end of file diff --git a/.history/README_20251019163806.md b/.history/README_20251019163806.md new file mode 100644 index 0000000..e831520 --- /dev/null +++ b/.history/README_20251019163806.md @@ -0,0 +1,280 @@ +# SmartSolTech Website + +Современный PWA сайт для SmartSolTech с админ-панелью, управлением портфолио, интеграцией с Telegram ботом и калькулятором стоимости услуг. + +## 🚀 Особенности + +- **Современный дизайн**: Отзывчивый интерфейс с анимациями и современным UI/UX +- **PWA**: Прогрессивное веб-приложение с поддержкой офлайн-режима +- **Админ-панель**: Управление портфолио, услугами, медиа-контентом и настройками сайта +- **Интеграция с Telegram**: Подключение к Telegram боту для уведомлений +- **Калькулятор стоимости**: Интерактивный калькулятор расчета стоимости услуг +- **Система аутентификации**: Безопасная авторизация с JWT токенами +- **Загрузка файлов**: Оптимизация изображений и управление медиа-контентом +- **SEO оптимизация**: Настроенные мета-теги и структурированные данные + +## 🛠️ Технологии + +### Backend +- **Node.js** - Серверная платформа +- **Express.js** - Веб-фреймворк +- **MongoDB** - База данных +- **Mongoose** - ODM для MongoDB +- **JWT** - Токены аутентификации +- **bcrypt** - Хеширование паролей +- **Multer** - Загрузка файлов +- **Sharp** - Обработка изображений + +### Frontend +- **EJS** - Шаблонизатор +- **Tailwind CSS** - CSS фреймворк +- **AOS** - Библиотека анимаций +- **Font Awesome** - Иконки +- **Service Worker** - PWA функциональность + +### Дополнительно +- **node-telegram-bot-api** - Интеграция с Telegram +- **Nodemailer** - Отправка email +- **Helmet** - Безопасность +- **express-rate-limit** - Ограничение запросов + +## 📦 Установка + +1. **Клонирование репозитория** +```bash +git clone +cd sst_site +``` + +2. **Установка зависимостей** +```bash +npm install +``` + +3. **Настройка переменных окружения** +```bash +cp .env.example .env +``` + +Отредактируйте файл `.env` с вашими настройками: +```env +NODE_ENV=development +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# Security +SESSION_SECRET=your-super-secret-session-key +JWT_SECRET=your-super-secret-jwt-key + +# File Upload +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password + +# Telegram Bot (Optional) +TELEGRAM_BOT_TOKEN=your-telegram-bot-token + +# Admin Account +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=change-this-password +``` + +4. **Инициализация базы данных** +```bash +npm run init-db +``` + +5. **Запуск в режиме разработки** +```bash +npm run dev +``` + +Сайт будет доступен по адресу: `http://localhost:3000` + +## 🗂️ Структура проекта + +``` +sst_site/ +├── models/ # Модели данных (MongoDB) +│ ├── User.js +│ ├── Portfolio.js +│ ├── Service.js +│ ├── Contact.js +│ └── SiteSettings.js +├── routes/ # Маршруты API +│ ├── index.js +│ ├── auth.js +│ ├── contact.js +│ ├── calculator.js +│ ├── portfolio.js +│ ├── services.js +│ ├── media.js +│ └── admin.js +├── views/ # Шаблоны EJS +│ ├── layout.ejs +│ ├── index.ejs +│ ├── calculator.ejs +│ └── partials/ +├── public/ # Статические файлы +│ ├── css/ +│ ├── js/ +│ ├── images/ +│ ├── manifest.json +│ └── sw.js +├── middleware/ # Промежуточное ПО +├── scripts/ # Служебные скрипты +│ ├── init-db.js +│ ├── dev.js +│ └── build.js +├── uploads/ # Загруженные файлы +├── server.js # Главный файл сервера +├── package.json +└── README.md +``` + +## 📋 Доступные команды + +```bash +# Разработка +npm run dev # Запуск в режиме разработки с hot reload + +# Продакшн +npm start # Запуск в продакшн режиме +npm run build # Сборка для продакшна + +# База данных +npm run init-db # Инициализация БД с тестовыми данными + +# Тестирование +npm test # Запуск тестов (пока не реализовано) +``` + +## 🔐 Админ-панель + +После инициализации базы данных вы можете войти в админ-панель: + +- **URL**: `http://localhost:3000/admin` +- **Email**: `admin@smartsoltech.kr` (или из .env) +- **Пароль**: Указанный в .env файле + +### Возможности админ-панели: +- Управление портфолио проектами +- Редактирование услуг и их стоимости +- Загрузка и управление медиа-файлами +- Просмотр и управление контактными формами +- Настройки сайта и SEO +- Управление пользователями + +## 🤖 Интеграция с Telegram + +Для настройки Telegram бота: + +1. Создайте бота через [@BotFather](https://t.me/BotFather) +2. Получите токен бота +3. Добавьте токен в `.env` файл: `TELEGRAM_BOT_TOKEN=your-token` +4. Перезапустите сервер + +Бот будет отправлять уведомления о: +- Новых контактных формах +- Заказах через калькулятор +- Новых комментариях + +## 💰 Калькулятор стоимости + +Интерактивный калькулятор позволяет клиентам: +- Выбрать тип услуги +- Указать дополнительные параметры +- Получить примерную стоимость +- Отправить заявку на расчет + +Настройки калькулятора можно изменить в админ-панели. + +## 🔒 Безопасность + +Проект включает следующие меры безопасности: +- Хеширование паролей с bcrypt +- JWT токены для аутентификации +- Защита от CSRF атак +- Ограничение количества запросов +- Валидация входных данных +- Безопасные HTTP заголовки + +## 📱 PWA функции + +- Установка на устройство +- Офлайн работа +- Push уведомления +- Фоновая синхронизация +- Адаптивные иконки +- Splash screen + +## 🚀 Деплой + +### Для продакшна: + +1. **Сборка приложения** +```bash +npm run build +``` + +2. **Настройка сервера** +- Установите Node.js и MongoDB +- Настройте переменные окружения +- Настройте прокси-сервер (nginx) + +3. **Запуск** +```bash +cd dist +npm install --production +npm start +``` + +### Docker деплой: + +```dockerfile +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +EXPOSE 3000 +CMD ["npm", "start"] +``` + +## 🤝 Участие в разработке + +1. Форкните репозиторий +2. Создайте ветку для фичи (`git checkout -b feature/AmazingFeature`) +3. Сделайте коммит (`git commit -m 'Add some AmazingFeature'`) +4. Отправьте в ветку (`git push origin feature/AmazingFeature`) +5. Откройте Pull Request + +## 📝 Лицензия + +Этот проект лицензирован под MIT License - подробности в файле [LICENSE](LICENSE). + +## 📞 Поддержка + +Если у вас есть вопросы или проблемы: + +- Email: info@smartsoltech.kr +- GitHub Issues: [Create Issue](../../issues) +- Telegram: @smartsoltech + +## 🙏 Благодарности + +- [Express.js](https://expressjs.com/) - За отличный веб-фреймворк +- [MongoDB](https://www.mongodb.com/) - За гибкую базу данных +- [Tailwind CSS](https://tailwindcss.com/) - За удобный CSS фреймворк +- [AOS](https://michalsnik.github.io/aos/) - За красивые анимации + +--- + +**SmartSolTech** - Умные решения для вашего бизнеса 🚀 \ No newline at end of file diff --git a/.history/locales/en_20251019171415.json b/.history/locales/en_20251019171415.json new file mode 100644 index 0000000..36e700e --- /dev/null +++ b/.history/locales/en_20251019171415.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta_primary": "Start Project", + "cta_secondary": "View Portfolio" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "web_development": { + "title": "Web Development", + "description": "Modern and responsive websites and web applications development", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Mobile App", + "description": "Native and cross-platform apps for iOS and Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX Design", + "description": "User-centered intuitive and beautiful interface design", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Digital Marketing", + "description": "Digital marketing through SEO, social media, online advertising", + "price": "$2,000~" + }, + "view_all": "View All Services" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio" + }, + "calculator": { + "title": "Check Your Project Estimate", + "description": "Select your desired services and requirements to calculate estimates in real time", + "cta": "Use Estimate Calculator" + }, + "contact": { + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "phone_consultation": "Phone Consultation", + "email_inquiry": "Email Inquiry", + "telegram_chat": "Telegram Chat", + "instant_response": "Instant response available", + "free_consultation": "Free Consultation Application", + "form": { + "name": "Name", + "email": "Email", + "phone": "Phone", + "service_interest": "Service Interest", + "service_options": { + "select": "Select Service Interest", + "web_development": "Web Development", + "mobile_app": "Mobile App", + "ui_ux_design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + }, + "message": "Please briefly describe your project", + "submit": "Apply for Consultation" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "Digital solution specialist leading customer success with innovative technology", + "overview": { + "title": "Creating Future with Innovation and Creativity", + "description_1": "SmartSolTech is a digital solution specialist established in 2020, supporting customer business success with innovative technology and creative ideas in web development, mobile apps, and UI/UX design.", + "description_2": "We don't just provide technology, but understand customer goals and propose optimal solutions to become partners growing together.", + "stats": { + "projects": "100+", + "projects_label": "Completed Projects", + "clients": "50+", + "clients_label": "Satisfied Customers", + "experience": "4 years", + "experience_label": "Industry Experience" + }, + "mission": "Our Mission", + "mission_text": "Helping all businesses succeed in the digital age through technology", + "vision": "Our Vision", + "vision_text": "Growing as a global digital solution company representing Korea to lead digital innovation for customers worldwide" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "Core values pursued by SmartSolTech", + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "collaboration": { + "title": "Collaboration", + "description": "We create the best results through close communication and collaboration with customers." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "growth": { + "title": "Growth", + "description": "We grow together with customers and pursue continuous learning and development." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "Introducing the SmartSolTech team with expertise and passion" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "We provide the best solutions with cutting-edge technology and proven tools", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "Become a Partner for Success Together", + "description": "Take your business to the next level with SmartSolTech", + "partnership": "Partnership Inquiry", + "portfolio": "View Portfolio" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "quick_links": "Quick Links", + "services": "Services", + "contact_info": "Contact Information", + "follow_us": "Follow Us", + "rights": "All rights reserved." + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous" + } +} \ No newline at end of file diff --git a/.history/locales/en_20251019171645.json b/.history/locales/en_20251019171645.json new file mode 100644 index 0000000..36e700e --- /dev/null +++ b/.history/locales/en_20251019171645.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta_primary": "Start Project", + "cta_secondary": "View Portfolio" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "web_development": { + "title": "Web Development", + "description": "Modern and responsive websites and web applications development", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Mobile App", + "description": "Native and cross-platform apps for iOS and Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX Design", + "description": "User-centered intuitive and beautiful interface design", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Digital Marketing", + "description": "Digital marketing through SEO, social media, online advertising", + "price": "$2,000~" + }, + "view_all": "View All Services" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio" + }, + "calculator": { + "title": "Check Your Project Estimate", + "description": "Select your desired services and requirements to calculate estimates in real time", + "cta": "Use Estimate Calculator" + }, + "contact": { + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "phone_consultation": "Phone Consultation", + "email_inquiry": "Email Inquiry", + "telegram_chat": "Telegram Chat", + "instant_response": "Instant response available", + "free_consultation": "Free Consultation Application", + "form": { + "name": "Name", + "email": "Email", + "phone": "Phone", + "service_interest": "Service Interest", + "service_options": { + "select": "Select Service Interest", + "web_development": "Web Development", + "mobile_app": "Mobile App", + "ui_ux_design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + }, + "message": "Please briefly describe your project", + "submit": "Apply for Consultation" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "Digital solution specialist leading customer success with innovative technology", + "overview": { + "title": "Creating Future with Innovation and Creativity", + "description_1": "SmartSolTech is a digital solution specialist established in 2020, supporting customer business success with innovative technology and creative ideas in web development, mobile apps, and UI/UX design.", + "description_2": "We don't just provide technology, but understand customer goals and propose optimal solutions to become partners growing together.", + "stats": { + "projects": "100+", + "projects_label": "Completed Projects", + "clients": "50+", + "clients_label": "Satisfied Customers", + "experience": "4 years", + "experience_label": "Industry Experience" + }, + "mission": "Our Mission", + "mission_text": "Helping all businesses succeed in the digital age through technology", + "vision": "Our Vision", + "vision_text": "Growing as a global digital solution company representing Korea to lead digital innovation for customers worldwide" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "Core values pursued by SmartSolTech", + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "collaboration": { + "title": "Collaboration", + "description": "We create the best results through close communication and collaboration with customers." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "growth": { + "title": "Growth", + "description": "We grow together with customers and pursue continuous learning and development." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "Introducing the SmartSolTech team with expertise and passion" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "We provide the best solutions with cutting-edge technology and proven tools", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "Become a Partner for Success Together", + "description": "Take your business to the next level with SmartSolTech", + "partnership": "Partnership Inquiry", + "portfolio": "View Portfolio" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "quick_links": "Quick Links", + "services": "Services", + "contact_info": "Contact Information", + "follow_us": "Follow Us", + "rights": "All rights reserved." + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous" + } +} \ No newline at end of file diff --git a/.history/locales/en_20251019181515.json b/.history/locales/en_20251019181515.json new file mode 100644 index 0000000..d0209de --- /dev/null +++ b/.history/locales/en_20251019181515.json @@ -0,0 +1,223 @@ +{ + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta_primary": "Start Project", + "cta_secondary": "View Portfolio" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "web_development": { + "title": "Web Development", + "description": "Modern and responsive websites and web applications development", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Mobile App", + "description": "Native and cross-platform apps for iOS and Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX Design", + "description": "User-centered intuitive and beautiful interface design", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Digital Marketing", + "description": "Digital marketing through SEO, social media, online advertising", + "price": "$2,000~" + }, + "view_all": "View All Services" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio" + }, + "calculator": { + "title": "Project Cost Calculator", + "subtitle": "Select your desired services and requirements to get accurate cost estimates in real time", + "meta": { + "title": "Project Cost Calculator", + "description": "Calculate the cost of your web development, mobile app, or design project with our interactive calculator" + }, + "cta": { + "title": "Check Your Project Estimate", + "subtitle": "Select your desired services and requirements to calculate costs in real time", + "button": "Use Cost Calculator" + }, + "step1": { + "title": "Step 1: Service Selection", + "subtitle": "Please select the services you need (multiple selection allowed)" + }, + "step2": { + "title": "Step 2: Project Details", + "subtitle": "Select project complexity and timeline" + }, + "complexity": { + "title": "Project Complexity", + "simple": "Simple", + "simple_desc": "Basic features, standard design", + "medium": "Medium", + "medium_desc": "Additional features, custom design", + "complex": "Complex", + "complex_desc": "Advanced features, complex integrations" + }, + "timeline": { + "title": "Development Timeline", + "standard": "Standard", + "standard_desc": "Normal development timeframe", + "rush": "Rush", + "rush_desc": "Fast development (+50%)", + "extended": "Extended", + "extended_desc": "Flexible development timeline (-20%)" + }, + "result": { + "title": "Estimate Results", + "subtitle": "Here's your preliminary project cost estimate", + "estimated_price": "Estimated Price", + "price_note": "* Final cost may vary based on project details", + "summary": "Project Summary", + "selected_services": "Selected Services", + "complexity": "Complexity", + "timeline": "Timeline", + "get_quote": "Get Accurate Quote", + "recalculate": "Recalculate", + "contact_note": "Contact us for an accurate quote and to discuss project details" + }, + "next_step": "Next Step", + "prev_step": "Previous", + "calculate": "Calculate" + }, + "contact": { + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "phone_consultation": "Phone Consultation", + "email_inquiry": "Email Inquiry", + "telegram_chat": "Telegram Chat", + "instant_response": "Instant response available", + "free_consultation": "Free Consultation Application", + "form": { + "name": "Name", + "email": "Email", + "phone": "Phone", + "service_interest": "Service Interest", + "service_options": { + "select": "Select Service Interest", + "web_development": "Web Development", + "mobile_app": "Mobile App", + "ui_ux_design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + }, + "message": "Please briefly describe your project", + "submit": "Apply for Consultation" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "Digital solution specialist leading customer success with innovative technology", + "overview": { + "title": "Creating Future with Innovation and Creativity", + "description_1": "SmartSolTech is a digital solution specialist established in 2020, supporting customer business success with innovative technology and creative ideas in web development, mobile apps, and UI/UX design.", + "description_2": "We don't just provide technology, but understand customer goals and propose optimal solutions to become partners growing together.", + "stats": { + "projects": "100+", + "projects_label": "Completed Projects", + "clients": "50+", + "clients_label": "Satisfied Customers", + "experience": "4 years", + "experience_label": "Industry Experience" + }, + "mission": "Our Mission", + "mission_text": "Helping all businesses succeed in the digital age through technology", + "vision": "Our Vision", + "vision_text": "Growing as a global digital solution company representing Korea to lead digital innovation for customers worldwide" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "Core values pursued by SmartSolTech", + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "collaboration": { + "title": "Collaboration", + "description": "We create the best results through close communication and collaboration with customers." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "growth": { + "title": "Growth", + "description": "We grow together with customers and pursue continuous learning and development." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "Introducing the SmartSolTech team with expertise and passion" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "We provide the best solutions with cutting-edge technology and proven tools", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "Become a Partner for Success Together", + "description": "Take your business to the next level with SmartSolTech", + "partnership": "Partnership Inquiry", + "portfolio": "View Portfolio" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "quick_links": "Quick Links", + "services": "Services", + "contact_info": "Contact Information", + "follow_us": "Follow Us", + "rights": "All rights reserved." + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous" + } +} \ No newline at end of file diff --git a/.history/locales/en_20251019181629.json b/.history/locales/en_20251019181629.json new file mode 100644 index 0000000..d0209de --- /dev/null +++ b/.history/locales/en_20251019181629.json @@ -0,0 +1,223 @@ +{ + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta_primary": "Start Project", + "cta_secondary": "View Portfolio" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "web_development": { + "title": "Web Development", + "description": "Modern and responsive websites and web applications development", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Mobile App", + "description": "Native and cross-platform apps for iOS and Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX Design", + "description": "User-centered intuitive and beautiful interface design", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Digital Marketing", + "description": "Digital marketing through SEO, social media, online advertising", + "price": "$2,000~" + }, + "view_all": "View All Services" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio" + }, + "calculator": { + "title": "Project Cost Calculator", + "subtitle": "Select your desired services and requirements to get accurate cost estimates in real time", + "meta": { + "title": "Project Cost Calculator", + "description": "Calculate the cost of your web development, mobile app, or design project with our interactive calculator" + }, + "cta": { + "title": "Check Your Project Estimate", + "subtitle": "Select your desired services and requirements to calculate costs in real time", + "button": "Use Cost Calculator" + }, + "step1": { + "title": "Step 1: Service Selection", + "subtitle": "Please select the services you need (multiple selection allowed)" + }, + "step2": { + "title": "Step 2: Project Details", + "subtitle": "Select project complexity and timeline" + }, + "complexity": { + "title": "Project Complexity", + "simple": "Simple", + "simple_desc": "Basic features, standard design", + "medium": "Medium", + "medium_desc": "Additional features, custom design", + "complex": "Complex", + "complex_desc": "Advanced features, complex integrations" + }, + "timeline": { + "title": "Development Timeline", + "standard": "Standard", + "standard_desc": "Normal development timeframe", + "rush": "Rush", + "rush_desc": "Fast development (+50%)", + "extended": "Extended", + "extended_desc": "Flexible development timeline (-20%)" + }, + "result": { + "title": "Estimate Results", + "subtitle": "Here's your preliminary project cost estimate", + "estimated_price": "Estimated Price", + "price_note": "* Final cost may vary based on project details", + "summary": "Project Summary", + "selected_services": "Selected Services", + "complexity": "Complexity", + "timeline": "Timeline", + "get_quote": "Get Accurate Quote", + "recalculate": "Recalculate", + "contact_note": "Contact us for an accurate quote and to discuss project details" + }, + "next_step": "Next Step", + "prev_step": "Previous", + "calculate": "Calculate" + }, + "contact": { + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "phone_consultation": "Phone Consultation", + "email_inquiry": "Email Inquiry", + "telegram_chat": "Telegram Chat", + "instant_response": "Instant response available", + "free_consultation": "Free Consultation Application", + "form": { + "name": "Name", + "email": "Email", + "phone": "Phone", + "service_interest": "Service Interest", + "service_options": { + "select": "Select Service Interest", + "web_development": "Web Development", + "mobile_app": "Mobile App", + "ui_ux_design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + }, + "message": "Please briefly describe your project", + "submit": "Apply for Consultation" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "Digital solution specialist leading customer success with innovative technology", + "overview": { + "title": "Creating Future with Innovation and Creativity", + "description_1": "SmartSolTech is a digital solution specialist established in 2020, supporting customer business success with innovative technology and creative ideas in web development, mobile apps, and UI/UX design.", + "description_2": "We don't just provide technology, but understand customer goals and propose optimal solutions to become partners growing together.", + "stats": { + "projects": "100+", + "projects_label": "Completed Projects", + "clients": "50+", + "clients_label": "Satisfied Customers", + "experience": "4 years", + "experience_label": "Industry Experience" + }, + "mission": "Our Mission", + "mission_text": "Helping all businesses succeed in the digital age through technology", + "vision": "Our Vision", + "vision_text": "Growing as a global digital solution company representing Korea to lead digital innovation for customers worldwide" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "Core values pursued by SmartSolTech", + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "collaboration": { + "title": "Collaboration", + "description": "We create the best results through close communication and collaboration with customers." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "growth": { + "title": "Growth", + "description": "We grow together with customers and pursue continuous learning and development." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "Introducing the SmartSolTech team with expertise and passion" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "We provide the best solutions with cutting-edge technology and proven tools", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "Become a Partner for Success Together", + "description": "Take your business to the next level with SmartSolTech", + "partnership": "Partnership Inquiry", + "portfolio": "View Portfolio" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "quick_links": "Quick Links", + "services": "Services", + "contact_info": "Contact Information", + "follow_us": "Follow Us", + "rights": "All rights reserved." + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous" + } +} \ No newline at end of file diff --git a/.history/locales/kk_20251019171626.json b/.history/locales/kk_20251019171626.json new file mode 100644 index 0000000..632b696 --- /dev/null +++ b/.history/locales/kk_20251019171626.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "Басты бет", + "about": "Біз туралы", + "services": "Қызметтер", + "portfolio": "Портфолио", + "contact": "Байланыс", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Ақылды Технологиялық", + "subtitle": "Шешімдер", + "description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз", + "cta_primary": "Жобаны бастау", + "cta_secondary": "Портфолионы көру" + }, + "services": { + "title": "Біздің", + "title_highlight": "Қызметтер", + "description": "Заманауи технология және шығармашылық идеялармен жасалған цифрлық шешімдер", + "web_development": { + "title": "Веб-әзірлеу", + "description": "Заманауи және бейімделгіш веб-сайттар мен веб-қосымшаларды әзірлеу", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильді қосымшалар", + "description": "iOS және Android үшін нативті және кросс-платформалық қосымшалар", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Пайдаланушы-орталықты интуитивті және әдемі интерфейс дизайны", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифрлық маркетинг", + "description": "SEO, әлеуметтік медиа, онлайн жарнама арқылы цифрлық маркетинг", + "price": "$2,000~" + }, + "view_all": "Барлық қызметтерді көру" + }, + "portfolio": { + "title": "Соңғы", + "title_highlight": "Жобалар", + "description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз", + "view_details": "Толығырақ", + "view_all": "Барлық портфолионы көру" + }, + "calculator": { + "title": "Жобаңыздың бағасын тексеріңіз", + "description": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептеңіз", + "cta": "Баға калькуляторын пайдалану" + }, + "contact": { + "ready_title": "Жобаңызды бастауға дайынсыз ба?", + "ready_description": "Идеяларыңызды шындыққа айналдырыңыз. Сарапшылар ең жақсы шешімдерді ұсынады.", + "phone_consultation": "Телефон кеңесі", + "email_inquiry": "Электрондық пошта сұрауы", + "telegram_chat": "Telegram чаты", + "instant_response": "Лезде жауап беру мүмкін", + "free_consultation": "Тегін кеңес беру өтініші", + "form": { + "name": "Аты", + "email": "Электрондық пошта", + "phone": "Телефон", + "service_interest": "Қызығатын қызмет", + "service_options": { + "select": "Қызығатын қызметті таңдаңыз", + "web_development": "Веб-әзірлеу", + "mobile_app": "Мобильді қосымша", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Кеңес беру", + "other": "Басқа" + }, + "message": "Жобаңыз туралы қысқаша сипаттаңыз", + "submit": "Кеңес беру үшін өтініш беру" + } + }, + "about": { + "hero_title": "Туралы", + "hero_highlight": "SmartSolTech", + "hero_description": "Инновациялық технологиямен тұтынушылардың табысына жетелейтін цифрлық шешімдер маманы", + "overview": { + "title": "Инновация мен шығармашылықпен болашақты құру", + "description_1": "SmartSolTech - 2020 жылы құрылған цифрлық шешімдер маманы, веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн салаларында инновациялық технология мен шығармашылық идеялар негізінде тұтынушылардың бизнес табысын қолдайды.", + "description_2": "Біз жай ғана технологияны ұсынып қана қоймаймыз, тұтынушылардың мақсаттарын түсініп, оларға сәйкес оңтайлы шешімдерді ұсынып, бірге өсетін серіктестер болуды мақсат етеміз.", + "stats": { + "projects": "100+", + "projects_label": "Аяқталған жобалар", + "clients": "50+", + "clients_label": "Қанағаттанған тұтынушылар", + "experience": "4 жыл", + "experience_label": "Саладағы тәжірибе" + }, + "mission": "Біздің миссия", + "mission_text": "Технология арқылы барлық бизнестердің цифрлық дәуірде табысқа жетуіне көмектесу", + "vision": "Біздің көзқарас", + "vision_text": "Қазақстанды білдіретін жаһандық цифрлық шешімдер компаниясы ретінде өсіп, бүкіл әлемдегі тұтынушылардың цифрлық инновацияларын басқару" + }, + "values": { + "title": "Негізгі", + "title_highlight": "Құндылықтар", + "description": "SmartSolTech ұстанатын негізгі құндылықтар", + "innovation": { + "title": "Инновация", + "description": "Үздіксіз зерттеу-әзірлеу және заманауи технологияларды енгізу арқылы инновациялық шешімдерді ұсынамыз." + }, + "collaboration": { + "title": "Ынтымақтастық", + "description": "Тұтынушылармен тығыз қарым-қатынас пен ынтымақтастық арқылы ең жақсы нәтижелерді жасаймыз." + }, + "quality": { + "title": "Сапа", + "description": "Жоғары сапа стандарттарын сақтап, тұтынушылар қанағаттана алатын жоғары сапалы өнімдерді ұсынамыз." + }, + "growth": { + "title": "Өсу", + "description": "Тұтынушылармен бірге өсіп, үздіксіз оқу мен дамуды мақсат етеміз." + } + }, + "team": { + "title": "Біздің", + "title_highlight": "Команда", + "description": "Сараптама мен құлшынысты SmartSolTech командасын таныстырамыз" + }, + "tech_stack": { + "title": "Технологиялық", + "title_highlight": "Стек", + "description": "Заманауи технология мен дәлелденген құралдармен ең жақсы шешімдерді ұсынамыз", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильді" + }, + "cta": { + "title": "Бірге табысқа жететін серіктес болыңыз", + "description": "SmartSolTech-пен бизнесіңізді келесі деңгейге дамытыңыз", + "partnership": "Серіктестік сұрауы", + "portfolio": "Портфолионы көру" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Инновацияны басқаратын цифрлық шешімдер маманы", + "quick_links": "Жылдам сілтемелер", + "services": "Қызметтер", + "contact_info": "Байланыс ақпараты", + "follow_us": "Бізді іздеңіз", + "rights": "Барлық құқықтар сақталған." + }, + "theme": { + "light": "Ашық тема", + "dark": "Қараңғы тема", + "toggle": "Теманы ауыстыру" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Жүктелуде...", + "error": "Қате орын алды", + "success": "Сәтті", + "view_more": "Көбірек көру", + "back": "Артқа", + "next": "Келесі", + "previous": "Алдыңғы" + } +} \ No newline at end of file diff --git a/.history/locales/kk_20251019171645.json b/.history/locales/kk_20251019171645.json new file mode 100644 index 0000000..632b696 --- /dev/null +++ b/.history/locales/kk_20251019171645.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "Басты бет", + "about": "Біз туралы", + "services": "Қызметтер", + "portfolio": "Портфолио", + "contact": "Байланыс", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Ақылды Технологиялық", + "subtitle": "Шешімдер", + "description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз", + "cta_primary": "Жобаны бастау", + "cta_secondary": "Портфолионы көру" + }, + "services": { + "title": "Біздің", + "title_highlight": "Қызметтер", + "description": "Заманауи технология және шығармашылық идеялармен жасалған цифрлық шешімдер", + "web_development": { + "title": "Веб-әзірлеу", + "description": "Заманауи және бейімделгіш веб-сайттар мен веб-қосымшаларды әзірлеу", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильді қосымшалар", + "description": "iOS және Android үшін нативті және кросс-платформалық қосымшалар", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Пайдаланушы-орталықты интуитивті және әдемі интерфейс дизайны", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифрлық маркетинг", + "description": "SEO, әлеуметтік медиа, онлайн жарнама арқылы цифрлық маркетинг", + "price": "$2,000~" + }, + "view_all": "Барлық қызметтерді көру" + }, + "portfolio": { + "title": "Соңғы", + "title_highlight": "Жобалар", + "description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз", + "view_details": "Толығырақ", + "view_all": "Барлық портфолионы көру" + }, + "calculator": { + "title": "Жобаңыздың бағасын тексеріңіз", + "description": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептеңіз", + "cta": "Баға калькуляторын пайдалану" + }, + "contact": { + "ready_title": "Жобаңызды бастауға дайынсыз ба?", + "ready_description": "Идеяларыңызды шындыққа айналдырыңыз. Сарапшылар ең жақсы шешімдерді ұсынады.", + "phone_consultation": "Телефон кеңесі", + "email_inquiry": "Электрондық пошта сұрауы", + "telegram_chat": "Telegram чаты", + "instant_response": "Лезде жауап беру мүмкін", + "free_consultation": "Тегін кеңес беру өтініші", + "form": { + "name": "Аты", + "email": "Электрондық пошта", + "phone": "Телефон", + "service_interest": "Қызығатын қызмет", + "service_options": { + "select": "Қызығатын қызметті таңдаңыз", + "web_development": "Веб-әзірлеу", + "mobile_app": "Мобильді қосымша", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Кеңес беру", + "other": "Басқа" + }, + "message": "Жобаңыз туралы қысқаша сипаттаңыз", + "submit": "Кеңес беру үшін өтініш беру" + } + }, + "about": { + "hero_title": "Туралы", + "hero_highlight": "SmartSolTech", + "hero_description": "Инновациялық технологиямен тұтынушылардың табысына жетелейтін цифрлық шешімдер маманы", + "overview": { + "title": "Инновация мен шығармашылықпен болашақты құру", + "description_1": "SmartSolTech - 2020 жылы құрылған цифрлық шешімдер маманы, веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн салаларында инновациялық технология мен шығармашылық идеялар негізінде тұтынушылардың бизнес табысын қолдайды.", + "description_2": "Біз жай ғана технологияны ұсынып қана қоймаймыз, тұтынушылардың мақсаттарын түсініп, оларға сәйкес оңтайлы шешімдерді ұсынып, бірге өсетін серіктестер болуды мақсат етеміз.", + "stats": { + "projects": "100+", + "projects_label": "Аяқталған жобалар", + "clients": "50+", + "clients_label": "Қанағаттанған тұтынушылар", + "experience": "4 жыл", + "experience_label": "Саладағы тәжірибе" + }, + "mission": "Біздің миссия", + "mission_text": "Технология арқылы барлық бизнестердің цифрлық дәуірде табысқа жетуіне көмектесу", + "vision": "Біздің көзқарас", + "vision_text": "Қазақстанды білдіретін жаһандық цифрлық шешімдер компаниясы ретінде өсіп, бүкіл әлемдегі тұтынушылардың цифрлық инновацияларын басқару" + }, + "values": { + "title": "Негізгі", + "title_highlight": "Құндылықтар", + "description": "SmartSolTech ұстанатын негізгі құндылықтар", + "innovation": { + "title": "Инновация", + "description": "Үздіксіз зерттеу-әзірлеу және заманауи технологияларды енгізу арқылы инновациялық шешімдерді ұсынамыз." + }, + "collaboration": { + "title": "Ынтымақтастық", + "description": "Тұтынушылармен тығыз қарым-қатынас пен ынтымақтастық арқылы ең жақсы нәтижелерді жасаймыз." + }, + "quality": { + "title": "Сапа", + "description": "Жоғары сапа стандарттарын сақтап, тұтынушылар қанағаттана алатын жоғары сапалы өнімдерді ұсынамыз." + }, + "growth": { + "title": "Өсу", + "description": "Тұтынушылармен бірге өсіп, үздіксіз оқу мен дамуды мақсат етеміз." + } + }, + "team": { + "title": "Біздің", + "title_highlight": "Команда", + "description": "Сараптама мен құлшынысты SmartSolTech командасын таныстырамыз" + }, + "tech_stack": { + "title": "Технологиялық", + "title_highlight": "Стек", + "description": "Заманауи технология мен дәлелденген құралдармен ең жақсы шешімдерді ұсынамыз", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильді" + }, + "cta": { + "title": "Бірге табысқа жететін серіктес болыңыз", + "description": "SmartSolTech-пен бизнесіңізді келесі деңгейге дамытыңыз", + "partnership": "Серіктестік сұрауы", + "portfolio": "Портфолионы көру" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Инновацияны басқаратын цифрлық шешімдер маманы", + "quick_links": "Жылдам сілтемелер", + "services": "Қызметтер", + "contact_info": "Байланыс ақпараты", + "follow_us": "Бізді іздеңіз", + "rights": "Барлық құқықтар сақталған." + }, + "theme": { + "light": "Ашық тема", + "dark": "Қараңғы тема", + "toggle": "Теманы ауыстыру" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Жүктелуде...", + "error": "Қате орын алды", + "success": "Сәтті", + "view_more": "Көбірек көру", + "back": "Артқа", + "next": "Келесі", + "previous": "Алдыңғы" + } +} \ No newline at end of file diff --git a/.history/locales/kk_20251019181611.json b/.history/locales/kk_20251019181611.json new file mode 100644 index 0000000..493faeb --- /dev/null +++ b/.history/locales/kk_20251019181611.json @@ -0,0 +1,223 @@ +{ + "navigation": { + "home": "Басты бет", + "about": "Біз туралы", + "services": "Қызметтер", + "portfolio": "Портфолио", + "contact": "Байланыс", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Ақылды Технологиялық", + "subtitle": "Шешімдер", + "description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз", + "cta_primary": "Жобаны бастау", + "cta_secondary": "Портфолионы көру" + }, + "services": { + "title": "Біздің", + "title_highlight": "Қызметтер", + "description": "Заманауи технология және шығармашылық идеялармен жасалған цифрлық шешімдер", + "web_development": { + "title": "Веб-әзірлеу", + "description": "Заманауи және бейімделгіш веб-сайттар мен веб-қосымшаларды әзірлеу", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильді қосымшалар", + "description": "iOS және Android үшін нативті және кросс-платформалық қосымшалар", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Пайдаланушы-орталықты интуитивті және әдемі интерфейс дизайны", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифрлық маркетинг", + "description": "SEO, әлеуметтік медиа, онлайн жарнама арқылы цифрлық маркетинг", + "price": "$2,000~" + }, + "view_all": "Барлық қызметтерді көру" + }, + "portfolio": { + "title": "Соңғы", + "title_highlight": "Жобалар", + "description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз", + "view_details": "Толығырақ", + "view_all": "Барлық портфолионы көру" + }, + "calculator": { + "title": "Жоба Құнының Калькуляторы", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта дәл бағаны алыңыз", + "meta": { + "title": "Жоба құнының калькуляторы", + "description": "Веб-әзірлеу, мобильді қосымша немесе дизайн жобасының құнын біздің интерактивті калькулятормен есептеңіз" + }, + "cta": { + "title": "Жобаның бағасын тексеріңіз", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептейміз", + "button": "Құн калькуляторын пайдалану" + }, + "step1": { + "title": "1-қадам: Қызмет таңдау", + "subtitle": "Қажетті қызметтерді таңдаңыз (бірнеше таңдауға болады)" + }, + "step2": { + "title": "2-қадам: Жоба мәліметтері", + "subtitle": "Жобаның күрделілігі мен мерзімін таңдаңыз" + }, + "complexity": { + "title": "Жобаның күрделілігі", + "simple": "Қарапайым", + "simple_desc": "Негізгі функциялар, стандартты дизайн", + "medium": "Орташа", + "medium_desc": "Қосымша функциялар, жеке дизайн", + "complex": "Күрделі", + "complex_desc": "Кеңейтілген функциялар, күрделі интеграциялар" + }, + "timeline": { + "title": "Әзірлеу мерзімі", + "standard": "Стандартты", + "standard_desc": "Қалыпты әзірлеу мерзімі", + "rush": "Асығыс", + "rush_desc": "Жылдам әзірлеу (+50%)", + "extended": "Кеңейтілген", + "extended_desc": "Икемді әзірлеу мерзімі (-20%)" + }, + "result": { + "title": "Есептеу нәтижесі", + "subtitle": "Міне, сіздің алдын ала жоба құнының бағасы", + "estimated_price": "Алдын ала баға", + "price_note": "* Соңғы құн жоба мәліметтеріне байланысты өзгеруі мүмкін", + "summary": "Жоба қорытындысы", + "selected_services": "Таңдалған қызметтер", + "complexity": "Күрделілік", + "timeline": "Мерзім", + "get_quote": "Дәл ұсыныс алу", + "recalculate": "Қайта есептеу", + "contact_note": "Дәл ұсыныс алу және жоба мәліметтерін талқылау үшін бізбен байланысыңыз" + }, + "next_step": "Келесі қадам", + "prev_step": "Артқа", + "calculate": "Есептеу" + }, + "contact": { + "ready_title": "Жобаңызды бастауға дайынсыз ба?", + "ready_description": "Идеяларыңызды шындыққа айналдырыңыз. Сарапшылар ең жақсы шешімдерді ұсынады.", + "phone_consultation": "Телефон кеңесі", + "email_inquiry": "Электрондық пошта сұрауы", + "telegram_chat": "Telegram чаты", + "instant_response": "Лезде жауап беру мүмкін", + "free_consultation": "Тегін кеңес беру өтініші", + "form": { + "name": "Аты", + "email": "Электрондық пошта", + "phone": "Телефон", + "service_interest": "Қызығатын қызмет", + "service_options": { + "select": "Қызығатын қызметті таңдаңыз", + "web_development": "Веб-әзірлеу", + "mobile_app": "Мобильді қосымша", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Кеңес беру", + "other": "Басқа" + }, + "message": "Жобаңыз туралы қысқаша сипаттаңыз", + "submit": "Кеңес беру үшін өтініш беру" + } + }, + "about": { + "hero_title": "Туралы", + "hero_highlight": "SmartSolTech", + "hero_description": "Инновациялық технологиямен тұтынушылардың табысына жетелейтін цифрлық шешімдер маманы", + "overview": { + "title": "Инновация мен шығармашылықпен болашақты құру", + "description_1": "SmartSolTech - 2020 жылы құрылған цифрлық шешімдер маманы, веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн салаларында инновациялық технология мен шығармашылық идеялар негізінде тұтынушылардың бизнес табысын қолдайды.", + "description_2": "Біз жай ғана технологияны ұсынып қана қоймаймыз, тұтынушылардың мақсаттарын түсініп, оларға сәйкес оңтайлы шешімдерді ұсынып, бірге өсетін серіктестер болуды мақсат етеміз.", + "stats": { + "projects": "100+", + "projects_label": "Аяқталған жобалар", + "clients": "50+", + "clients_label": "Қанағаттанған тұтынушылар", + "experience": "4 жыл", + "experience_label": "Саладағы тәжірибе" + }, + "mission": "Біздің миссия", + "mission_text": "Технология арқылы барлық бизнестердің цифрлық дәуірде табысқа жетуіне көмектесу", + "vision": "Біздің көзқарас", + "vision_text": "Қазақстанды білдіретін жаһандық цифрлық шешімдер компаниясы ретінде өсіп, бүкіл әлемдегі тұтынушылардың цифрлық инновацияларын басқару" + }, + "values": { + "title": "Негізгі", + "title_highlight": "Құндылықтар", + "description": "SmartSolTech ұстанатын негізгі құндылықтар", + "innovation": { + "title": "Инновация", + "description": "Үздіксіз зерттеу-әзірлеу және заманауи технологияларды енгізу арқылы инновациялық шешімдерді ұсынамыз." + }, + "collaboration": { + "title": "Ынтымақтастық", + "description": "Тұтынушылармен тығыз қарым-қатынас пен ынтымақтастық арқылы ең жақсы нәтижелерді жасаймыз." + }, + "quality": { + "title": "Сапа", + "description": "Жоғары сапа стандарттарын сақтап, тұтынушылар қанағаттана алатын жоғары сапалы өнімдерді ұсынамыз." + }, + "growth": { + "title": "Өсу", + "description": "Тұтынушылармен бірге өсіп, үздіксіз оқу мен дамуды мақсат етеміз." + } + }, + "team": { + "title": "Біздің", + "title_highlight": "Команда", + "description": "Сараптама мен құлшынысты SmartSolTech командасын таныстырамыз" + }, + "tech_stack": { + "title": "Технологиялық", + "title_highlight": "Стек", + "description": "Заманауи технология мен дәлелденген құралдармен ең жақсы шешімдерді ұсынамыз", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильді" + }, + "cta": { + "title": "Бірге табысқа жететін серіктес болыңыз", + "description": "SmartSolTech-пен бизнесіңізді келесі деңгейге дамытыңыз", + "partnership": "Серіктестік сұрауы", + "portfolio": "Портфолионы көру" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Инновацияны басқаратын цифрлық шешімдер маманы", + "quick_links": "Жылдам сілтемелер", + "services": "Қызметтер", + "contact_info": "Байланыс ақпараты", + "follow_us": "Бізді іздеңіз", + "rights": "Барлық құқықтар сақталған." + }, + "theme": { + "light": "Ашық тема", + "dark": "Қараңғы тема", + "toggle": "Теманы ауыстыру" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Жүктелуде...", + "error": "Қате орын алды", + "success": "Сәтті", + "view_more": "Көбірек көру", + "back": "Артқа", + "next": "Келесі", + "previous": "Алдыңғы" + } +} \ No newline at end of file diff --git a/.history/locales/kk_20251019181629.json b/.history/locales/kk_20251019181629.json new file mode 100644 index 0000000..493faeb --- /dev/null +++ b/.history/locales/kk_20251019181629.json @@ -0,0 +1,223 @@ +{ + "navigation": { + "home": "Басты бет", + "about": "Біз туралы", + "services": "Қызметтер", + "portfolio": "Портфолио", + "contact": "Байланыс", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Ақылды Технологиялық", + "subtitle": "Шешімдер", + "description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз", + "cta_primary": "Жобаны бастау", + "cta_secondary": "Портфолионы көру" + }, + "services": { + "title": "Біздің", + "title_highlight": "Қызметтер", + "description": "Заманауи технология және шығармашылық идеялармен жасалған цифрлық шешімдер", + "web_development": { + "title": "Веб-әзірлеу", + "description": "Заманауи және бейімделгіш веб-сайттар мен веб-қосымшаларды әзірлеу", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильді қосымшалар", + "description": "iOS және Android үшін нативті және кросс-платформалық қосымшалар", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Пайдаланушы-орталықты интуитивті және әдемі интерфейс дизайны", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифрлық маркетинг", + "description": "SEO, әлеуметтік медиа, онлайн жарнама арқылы цифрлық маркетинг", + "price": "$2,000~" + }, + "view_all": "Барлық қызметтерді көру" + }, + "portfolio": { + "title": "Соңғы", + "title_highlight": "Жобалар", + "description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз", + "view_details": "Толығырақ", + "view_all": "Барлық портфолионы көру" + }, + "calculator": { + "title": "Жоба Құнының Калькуляторы", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта дәл бағаны алыңыз", + "meta": { + "title": "Жоба құнының калькуляторы", + "description": "Веб-әзірлеу, мобильді қосымша немесе дизайн жобасының құнын біздің интерактивті калькулятормен есептеңіз" + }, + "cta": { + "title": "Жобаның бағасын тексеріңіз", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептейміз", + "button": "Құн калькуляторын пайдалану" + }, + "step1": { + "title": "1-қадам: Қызмет таңдау", + "subtitle": "Қажетті қызметтерді таңдаңыз (бірнеше таңдауға болады)" + }, + "step2": { + "title": "2-қадам: Жоба мәліметтері", + "subtitle": "Жобаның күрделілігі мен мерзімін таңдаңыз" + }, + "complexity": { + "title": "Жобаның күрделілігі", + "simple": "Қарапайым", + "simple_desc": "Негізгі функциялар, стандартты дизайн", + "medium": "Орташа", + "medium_desc": "Қосымша функциялар, жеке дизайн", + "complex": "Күрделі", + "complex_desc": "Кеңейтілген функциялар, күрделі интеграциялар" + }, + "timeline": { + "title": "Әзірлеу мерзімі", + "standard": "Стандартты", + "standard_desc": "Қалыпты әзірлеу мерзімі", + "rush": "Асығыс", + "rush_desc": "Жылдам әзірлеу (+50%)", + "extended": "Кеңейтілген", + "extended_desc": "Икемді әзірлеу мерзімі (-20%)" + }, + "result": { + "title": "Есептеу нәтижесі", + "subtitle": "Міне, сіздің алдын ала жоба құнының бағасы", + "estimated_price": "Алдын ала баға", + "price_note": "* Соңғы құн жоба мәліметтеріне байланысты өзгеруі мүмкін", + "summary": "Жоба қорытындысы", + "selected_services": "Таңдалған қызметтер", + "complexity": "Күрделілік", + "timeline": "Мерзім", + "get_quote": "Дәл ұсыныс алу", + "recalculate": "Қайта есептеу", + "contact_note": "Дәл ұсыныс алу және жоба мәліметтерін талқылау үшін бізбен байланысыңыз" + }, + "next_step": "Келесі қадам", + "prev_step": "Артқа", + "calculate": "Есептеу" + }, + "contact": { + "ready_title": "Жобаңызды бастауға дайынсыз ба?", + "ready_description": "Идеяларыңызды шындыққа айналдырыңыз. Сарапшылар ең жақсы шешімдерді ұсынады.", + "phone_consultation": "Телефон кеңесі", + "email_inquiry": "Электрондық пошта сұрауы", + "telegram_chat": "Telegram чаты", + "instant_response": "Лезде жауап беру мүмкін", + "free_consultation": "Тегін кеңес беру өтініші", + "form": { + "name": "Аты", + "email": "Электрондық пошта", + "phone": "Телефон", + "service_interest": "Қызығатын қызмет", + "service_options": { + "select": "Қызығатын қызметті таңдаңыз", + "web_development": "Веб-әзірлеу", + "mobile_app": "Мобильді қосымша", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Кеңес беру", + "other": "Басқа" + }, + "message": "Жобаңыз туралы қысқаша сипаттаңыз", + "submit": "Кеңес беру үшін өтініш беру" + } + }, + "about": { + "hero_title": "Туралы", + "hero_highlight": "SmartSolTech", + "hero_description": "Инновациялық технологиямен тұтынушылардың табысына жетелейтін цифрлық шешімдер маманы", + "overview": { + "title": "Инновация мен шығармашылықпен болашақты құру", + "description_1": "SmartSolTech - 2020 жылы құрылған цифрлық шешімдер маманы, веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн салаларында инновациялық технология мен шығармашылық идеялар негізінде тұтынушылардың бизнес табысын қолдайды.", + "description_2": "Біз жай ғана технологияны ұсынып қана қоймаймыз, тұтынушылардың мақсаттарын түсініп, оларға сәйкес оңтайлы шешімдерді ұсынып, бірге өсетін серіктестер болуды мақсат етеміз.", + "stats": { + "projects": "100+", + "projects_label": "Аяқталған жобалар", + "clients": "50+", + "clients_label": "Қанағаттанған тұтынушылар", + "experience": "4 жыл", + "experience_label": "Саладағы тәжірибе" + }, + "mission": "Біздің миссия", + "mission_text": "Технология арқылы барлық бизнестердің цифрлық дәуірде табысқа жетуіне көмектесу", + "vision": "Біздің көзқарас", + "vision_text": "Қазақстанды білдіретін жаһандық цифрлық шешімдер компаниясы ретінде өсіп, бүкіл әлемдегі тұтынушылардың цифрлық инновацияларын басқару" + }, + "values": { + "title": "Негізгі", + "title_highlight": "Құндылықтар", + "description": "SmartSolTech ұстанатын негізгі құндылықтар", + "innovation": { + "title": "Инновация", + "description": "Үздіксіз зерттеу-әзірлеу және заманауи технологияларды енгізу арқылы инновациялық шешімдерді ұсынамыз." + }, + "collaboration": { + "title": "Ынтымақтастық", + "description": "Тұтынушылармен тығыз қарым-қатынас пен ынтымақтастық арқылы ең жақсы нәтижелерді жасаймыз." + }, + "quality": { + "title": "Сапа", + "description": "Жоғары сапа стандарттарын сақтап, тұтынушылар қанағаттана алатын жоғары сапалы өнімдерді ұсынамыз." + }, + "growth": { + "title": "Өсу", + "description": "Тұтынушылармен бірге өсіп, үздіксіз оқу мен дамуды мақсат етеміз." + } + }, + "team": { + "title": "Біздің", + "title_highlight": "Команда", + "description": "Сараптама мен құлшынысты SmartSolTech командасын таныстырамыз" + }, + "tech_stack": { + "title": "Технологиялық", + "title_highlight": "Стек", + "description": "Заманауи технология мен дәлелденген құралдармен ең жақсы шешімдерді ұсынамыз", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильді" + }, + "cta": { + "title": "Бірге табысқа жететін серіктес болыңыз", + "description": "SmartSolTech-пен бизнесіңізді келесі деңгейге дамытыңыз", + "partnership": "Серіктестік сұрауы", + "portfolio": "Портфолионы көру" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Инновацияны басқаратын цифрлық шешімдер маманы", + "quick_links": "Жылдам сілтемелер", + "services": "Қызметтер", + "contact_info": "Байланыс ақпараты", + "follow_us": "Бізді іздеңіз", + "rights": "Барлық құқықтар сақталған." + }, + "theme": { + "light": "Ашық тема", + "dark": "Қараңғы тема", + "toggle": "Теманы ауыстыру" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Жүктелуде...", + "error": "Қате орын алды", + "success": "Сәтті", + "view_more": "Көбірек көру", + "back": "Артқа", + "next": "Келесі", + "previous": "Алдыңғы" + } +} \ No newline at end of file diff --git a/.history/locales/ko_20251019171450.json b/.history/locales/ko_20251019171450.json new file mode 100644 index 0000000..60583db --- /dev/null +++ b/.history/locales/ko_20251019171450.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "홈", + "about": "회사소개", + "services": "서비스", + "portfolio": "포트폴리오", + "contact": "연락처", + "calculator": "견적계산기", + "admin": "관리자" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다", + "cta_primary": "프로젝트 시작하기", + "cta_secondary": "포트폴리오 보기" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션", + "web_development": { + "title": "웹 개발", + "description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발", + "price": "₩500,000~" + }, + "mobile_app": { + "title": "모바일 앱", + "description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱", + "price": "₩800,000~" + }, + "ui_ux_design": { + "title": "UI/UX 디자인", + "description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인", + "price": "₩300,000~" + }, + "digital_marketing": { + "title": "디지털 마케팅", + "description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅", + "price": "₩200,000~" + }, + "view_all": "모든 서비스 보기" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요", + "view_details": "자세히 보기", + "view_all": "전체 포트폴리오 보기" + }, + "calculator": { + "title": "프로젝트 견적을 확인해보세요", + "description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "cta": "견적 계산기 사용하기" + }, + "contact": { + "ready_title": "프로젝트를 시작할 준비가 되셨나요?", + "ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.", + "phone_consultation": "전화 상담", + "email_inquiry": "이메일 문의", + "telegram_chat": "텔레그램 채팅", + "instant_response": "즉시 답변 가능", + "free_consultation": "무료 상담 신청", + "form": { + "name": "이름", + "email": "이메일", + "phone": "연락처", + "service_interest": "관심 서비스", + "service_options": { + "select": "관심 서비스 선택", + "web_development": "웹 개발", + "mobile_app": "모바일 앱", + "ui_ux_design": "UI/UX 디자인", + "branding": "브랜딩", + "consulting": "컨설팅", + "other": "기타" + }, + "message": "프로젝트에 대해 간단히 설명해주세요", + "submit": "상담 신청하기" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업", + "overview": { + "title": "혁신과 창의로 만드는 미래", + "description_1": "SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다.", + "description_2": "우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 제안하여 함께 성장하는 파트너가 되고자 합니다.", + "stats": { + "projects": "100+", + "projects_label": "완료 프로젝트", + "clients": "50+", + "clients_label": "만족한 고객", + "experience": "4년", + "experience_label": "업계 경험" + }, + "mission": "우리의 미션", + "mission_text": "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것", + "vision": "우리의 비전", + "vision_text": "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 전 세계 고객들의 디지털 혁신을 주도하는 것" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "SmartSolTech가 추구하는 핵심 가치들", + "innovation": { + "title": "혁신 (Innovation)", + "description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다." + }, + "collaboration": { + "title": "협력 (Collaboration)", + "description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다." + }, + "quality": { + "title": "품질 (Quality)", + "description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다." + }, + "growth": { + "title": "성장 (Growth)", + "description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "함께 성공하는 파트너가 되어보세요", + "description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요", + "partnership": "파트너십 문의", + "portfolio": "포트폴리오 보기" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "혁신을 이끌어가는 디지털 솔루션 전문 기업", + "quick_links": "빠른 링크", + "services": "서비스", + "contact_info": "연락처 정보", + "follow_us": "팔로우하기", + "rights": "모든 권리 보유." + }, + "theme": { + "light": "라이트 테마", + "dark": "다크 테마", + "toggle": "테마 전환" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "로딩 중...", + "error": "오류가 발생했습니다", + "success": "성공", + "view_more": "더 보기", + "back": "뒤로", + "next": "다음", + "previous": "이전" + } +} \ No newline at end of file diff --git a/.history/locales/ko_20251019171645.json b/.history/locales/ko_20251019171645.json new file mode 100644 index 0000000..60583db --- /dev/null +++ b/.history/locales/ko_20251019171645.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "홈", + "about": "회사소개", + "services": "서비스", + "portfolio": "포트폴리오", + "contact": "연락처", + "calculator": "견적계산기", + "admin": "관리자" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다", + "cta_primary": "프로젝트 시작하기", + "cta_secondary": "포트폴리오 보기" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션", + "web_development": { + "title": "웹 개발", + "description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발", + "price": "₩500,000~" + }, + "mobile_app": { + "title": "모바일 앱", + "description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱", + "price": "₩800,000~" + }, + "ui_ux_design": { + "title": "UI/UX 디자인", + "description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인", + "price": "₩300,000~" + }, + "digital_marketing": { + "title": "디지털 마케팅", + "description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅", + "price": "₩200,000~" + }, + "view_all": "모든 서비스 보기" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요", + "view_details": "자세히 보기", + "view_all": "전체 포트폴리오 보기" + }, + "calculator": { + "title": "프로젝트 견적을 확인해보세요", + "description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "cta": "견적 계산기 사용하기" + }, + "contact": { + "ready_title": "프로젝트를 시작할 준비가 되셨나요?", + "ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.", + "phone_consultation": "전화 상담", + "email_inquiry": "이메일 문의", + "telegram_chat": "텔레그램 채팅", + "instant_response": "즉시 답변 가능", + "free_consultation": "무료 상담 신청", + "form": { + "name": "이름", + "email": "이메일", + "phone": "연락처", + "service_interest": "관심 서비스", + "service_options": { + "select": "관심 서비스 선택", + "web_development": "웹 개발", + "mobile_app": "모바일 앱", + "ui_ux_design": "UI/UX 디자인", + "branding": "브랜딩", + "consulting": "컨설팅", + "other": "기타" + }, + "message": "프로젝트에 대해 간단히 설명해주세요", + "submit": "상담 신청하기" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업", + "overview": { + "title": "혁신과 창의로 만드는 미래", + "description_1": "SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다.", + "description_2": "우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 제안하여 함께 성장하는 파트너가 되고자 합니다.", + "stats": { + "projects": "100+", + "projects_label": "완료 프로젝트", + "clients": "50+", + "clients_label": "만족한 고객", + "experience": "4년", + "experience_label": "업계 경험" + }, + "mission": "우리의 미션", + "mission_text": "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것", + "vision": "우리의 비전", + "vision_text": "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 전 세계 고객들의 디지털 혁신을 주도하는 것" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "SmartSolTech가 추구하는 핵심 가치들", + "innovation": { + "title": "혁신 (Innovation)", + "description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다." + }, + "collaboration": { + "title": "협력 (Collaboration)", + "description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다." + }, + "quality": { + "title": "품질 (Quality)", + "description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다." + }, + "growth": { + "title": "성장 (Growth)", + "description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "함께 성공하는 파트너가 되어보세요", + "description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요", + "partnership": "파트너십 문의", + "portfolio": "포트폴리오 보기" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "혁신을 이끌어가는 디지털 솔루션 전문 기업", + "quick_links": "빠른 링크", + "services": "서비스", + "contact_info": "연락처 정보", + "follow_us": "팔로우하기", + "rights": "모든 권리 보유." + }, + "theme": { + "light": "라이트 테마", + "dark": "다크 테마", + "toggle": "테마 전환" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "로딩 중...", + "error": "오류가 발생했습니다", + "success": "성공", + "view_more": "더 보기", + "back": "뒤로", + "next": "다음", + "previous": "이전" + } +} \ No newline at end of file diff --git a/.history/locales/ko_20251019181428.json b/.history/locales/ko_20251019181428.json new file mode 100644 index 0000000..15e58cf --- /dev/null +++ b/.history/locales/ko_20251019181428.json @@ -0,0 +1,227 @@ +{ + "navigation": { + "home": "홈", + "about": "calculator": { + "title": "프로젝트 견적 계산기", + "subtitle": "원하는 서비스와 요구사항을 선택하면 실시간으로 정확한 견적을 계산해드립니다", + "meta": { + "title": "프로젝트 견적 계산기", + "description": "웹 개발, 모바일 앱, 디자인 프로젝트 비용을 실시간으로 계산해보세요" + }, + "cta": { + "title": "프로젝트 견적을 확인해보세요", + "subtitle": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "button": "견적 계산기 사용하기" + }, + "step1": { + "title": "1단계: 서비스 선택", + "subtitle": "필요한 서비스를 선택해주세요 (복수 선택 가능)" + }, + "step2": { + "title": "2단계: 프로젝트 세부사항", + "subtitle": "프로젝트의 복잡도와 일정을 선택해주세요" + }, + "complexity": { + "title": "프로젝트 복잡도", + "simple": "단순", + "simple_desc": "기본 기능, 표준 디자인", + "medium": "보통", + "medium_desc": "추가 기능, 커스텀 디자인", + "complex": "복잡", + "complex_desc": "고급 기능, 복합 통합" + }, + "timeline": { + "title": "개발 일정", + "standard": "표준", + "standard_desc": "일반적인 개발 기간", + "rush": "급한", + "rush_desc": "빠른 개발 (+50%)", + "extended": "여유", + "extended_desc": "넉넉한 개발 기간 (-20%)" + }, + "result": { + "title": "견적 결과", + "subtitle": "프로젝트 예상 견적입니다", + "estimated_price": "예상 견적", + "price_note": "* 최종 견적은 프로젝트 세부사항에 따라 달라질 수 있습니다", + "summary": "프로젝트 요약", + "selected_services": "선택한 서비스", + "complexity": "복잡도", + "timeline": "일정", + "get_quote": "정확한 견적 받기", + "recalculate": "다시 계산", + "contact_note": "정확한 견적과 프로젝트 상담을 위해 연락주세요" + }, + "next_step": "다음 단계", + "prev_step": "이전", + "calculate": "계산하기" + },", + "services": "서비스", + "portfolio": "포트폴리오", + "contact": "연락처", + "calculator": "견적계산기", + "admin": "관리자" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다", + "cta_primary": "프로젝트 시작하기", + "cta_secondary": "포트폴리오 보기" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션", + "web_development": { + "title": "웹 개발", + "description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발", + "price": "₩500,000~" + }, + "mobile_app": { + "title": "모바일 앱", + "description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱", + "price": "₩800,000~" + }, + "ui_ux_design": { + "title": "UI/UX 디자인", + "description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인", + "price": "₩300,000~" + }, + "digital_marketing": { + "title": "디지털 마케팅", + "description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅", + "price": "₩200,000~" + }, + "view_all": "모든 서비스 보기" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요", + "view_details": "자세히 보기", + "view_all": "전체 포트폴리오 보기" + }, + "calculator": { + "title": "프로젝트 견적을 확인해보세요", + "description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "cta": "견적 계산기 사용하기" + }, + "contact": { + "ready_title": "프로젝트를 시작할 준비가 되셨나요?", + "ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.", + "phone_consultation": "전화 상담", + "email_inquiry": "이메일 문의", + "telegram_chat": "텔레그램 채팅", + "instant_response": "즉시 답변 가능", + "free_consultation": "무료 상담 신청", + "form": { + "name": "이름", + "email": "이메일", + "phone": "연락처", + "service_interest": "관심 서비스", + "service_options": { + "select": "관심 서비스 선택", + "web_development": "웹 개발", + "mobile_app": "모바일 앱", + "ui_ux_design": "UI/UX 디자인", + "branding": "브랜딩", + "consulting": "컨설팅", + "other": "기타" + }, + "message": "프로젝트에 대해 간단히 설명해주세요", + "submit": "상담 신청하기" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업", + "overview": { + "title": "혁신과 창의로 만드는 미래", + "description_1": "SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다.", + "description_2": "우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 제안하여 함께 성장하는 파트너가 되고자 합니다.", + "stats": { + "projects": "100+", + "projects_label": "완료 프로젝트", + "clients": "50+", + "clients_label": "만족한 고객", + "experience": "4년", + "experience_label": "업계 경험" + }, + "mission": "우리의 미션", + "mission_text": "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것", + "vision": "우리의 비전", + "vision_text": "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 전 세계 고객들의 디지털 혁신을 주도하는 것" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "SmartSolTech가 추구하는 핵심 가치들", + "innovation": { + "title": "혁신 (Innovation)", + "description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다." + }, + "collaboration": { + "title": "협력 (Collaboration)", + "description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다." + }, + "quality": { + "title": "품질 (Quality)", + "description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다." + }, + "growth": { + "title": "성장 (Growth)", + "description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "함께 성공하는 파트너가 되어보세요", + "description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요", + "partnership": "파트너십 문의", + "portfolio": "포트폴리오 보기" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "혁신을 이끌어가는 디지털 솔루션 전문 기업", + "quick_links": "빠른 링크", + "services": "서비스", + "contact_info": "연락처 정보", + "follow_us": "팔로우하기", + "rights": "모든 권리 보유." + }, + "theme": { + "light": "라이트 테마", + "dark": "다크 테마", + "toggle": "테마 전환" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "로딩 중...", + "error": "오류가 발생했습니다", + "success": "성공", + "view_more": "더 보기", + "back": "뒤로", + "next": "다음", + "previous": "이전" + } +} \ No newline at end of file diff --git a/.history/locales/ko_20251019181450.json b/.history/locales/ko_20251019181450.json new file mode 100644 index 0000000..9e5e2a9 --- /dev/null +++ b/.history/locales/ko_20251019181450.json @@ -0,0 +1,230 @@ +{ + "navigation": { + "home": "홈", + "about": "calculator": { + "title": "프로젝트 견적 계산기", + "subtitle": "원하는 서비스와 요구사항을 선택하면 실시간으로 정확한 견적을 계산해드립니다", + "meta": { + "title": "프로젝트 견적 계산기", + "description": "웹 개발, 모바일 앱, 디자인 프로젝트 비용을 실시간으로 계산해보세요" + }, + "cta": { + "title": "프로젝트 견적을 확인해보세요", + "subtitle": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "button": "견적 계산기 사용하기" + }, + "step1": { + "title": "1단계: 서비스 선택", + "subtitle": "필요한 서비스를 선택해주세요 (복수 선택 가능)" + }, + "step2": { + "title": "2단계: 프로젝트 세부사항", + "subtitle": "프로젝트의 복잡도와 일정을 선택해주세요" + }, + "complexity": { + "title": "프로젝트 복잡도", + "simple": "단순", + "simple_desc": "기본 기능, 표준 디자인", + "medium": "보통", + "medium_desc": "추가 기능, 커스텀 디자인", + "complex": "복잡", + "complex_desc": "고급 기능, 복합 통합" + }, + "timeline": { + "title": "개발 일정", + "standard": "표준", + "standard_desc": "일반적인 개발 기간", + "rush": "급한", + "rush_desc": "빠른 개발 (+50%)", + "extended": "여유", + "extended_desc": "넉넉한 개발 기간 (-20%)" + }, + "result": { + "title": "견적 결과", + "subtitle": "프로젝트 예상 견적입니다", + "estimated_price": "예상 견적", + "price_note": "* 최종 견적은 프로젝트 세부사항에 따라 달라질 수 있습니다", + "summary": "프로젝트 요약", + "selected_services": "선택한 서비스", + "complexity": "복잡도", + "timeline": "일정", + "get_quote": "정확한 견적 받기", + "recalculate": "다시 계산", + "contact_note": "정확한 견적과 프로젝트 상담을 위해 연락주세요" + }, + "next_step": "다음 단계", + "prev_step": "이전", + "calculate": "계산하기" + }, + "nav": { + "home": "홈", + "about": "회사소개", + "services": "서비스", + "portfolio": "포트폴리오", + "contact": "연락처", + "calculator": "견적계산기", + "admin": "관리자" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다", + "cta_primary": "프로젝트 시작하기", + "cta_secondary": "포트폴리오 보기" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션", + "web_development": { + "title": "웹 개발", + "description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발", + "price": "₩500,000~" + }, + "mobile_app": { + "title": "모바일 앱", + "description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱", + "price": "₩800,000~" + }, + "ui_ux_design": { + "title": "UI/UX 디자인", + "description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인", + "price": "₩300,000~" + }, + "digital_marketing": { + "title": "디지털 마케팅", + "description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅", + "price": "₩200,000~" + }, + "view_all": "모든 서비스 보기" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요", + "view_details": "자세히 보기", + "view_all": "전체 포트폴리오 보기" + }, + "calculator": { + "title": "프로젝트 견적을 확인해보세요", + "description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "cta": "견적 계산기 사용하기" + }, + "contact": { + "ready_title": "프로젝트를 시작할 준비가 되셨나요?", + "ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.", + "phone_consultation": "전화 상담", + "email_inquiry": "이메일 문의", + "telegram_chat": "텔레그램 채팅", + "instant_response": "즉시 답변 가능", + "free_consultation": "무료 상담 신청", + "form": { + "name": "이름", + "email": "이메일", + "phone": "연락처", + "service_interest": "관심 서비스", + "service_options": { + "select": "관심 서비스 선택", + "web_development": "웹 개발", + "mobile_app": "모바일 앱", + "ui_ux_design": "UI/UX 디자인", + "branding": "브랜딩", + "consulting": "컨설팅", + "other": "기타" + }, + "message": "프로젝트에 대해 간단히 설명해주세요", + "submit": "상담 신청하기" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업", + "overview": { + "title": "혁신과 창의로 만드는 미래", + "description_1": "SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다.", + "description_2": "우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 제안하여 함께 성장하는 파트너가 되고자 합니다.", + "stats": { + "projects": "100+", + "projects_label": "완료 프로젝트", + "clients": "50+", + "clients_label": "만족한 고객", + "experience": "4년", + "experience_label": "업계 경험" + }, + "mission": "우리의 미션", + "mission_text": "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것", + "vision": "우리의 비전", + "vision_text": "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 전 세계 고객들의 디지털 혁신을 주도하는 것" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "SmartSolTech가 추구하는 핵심 가치들", + "innovation": { + "title": "혁신 (Innovation)", + "description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다." + }, + "collaboration": { + "title": "협력 (Collaboration)", + "description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다." + }, + "quality": { + "title": "품질 (Quality)", + "description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다." + }, + "growth": { + "title": "성장 (Growth)", + "description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "함께 성공하는 파트너가 되어보세요", + "description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요", + "partnership": "파트너십 문의", + "portfolio": "포트폴리오 보기" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "혁신을 이끌어가는 디지털 솔루션 전문 기업", + "quick_links": "빠른 링크", + "services": "서비스", + "contact_info": "연락처 정보", + "follow_us": "팔로우하기", + "rights": "모든 권리 보유." + }, + "theme": { + "light": "라이트 테마", + "dark": "다크 테마", + "toggle": "테마 전환" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "로딩 중...", + "error": "오류가 발생했습니다", + "success": "성공", + "view_more": "더 보기", + "back": "뒤로", + "next": "다음", + "previous": "이전" + } +} \ No newline at end of file diff --git a/.history/locales/ko_20251019181629.json b/.history/locales/ko_20251019181629.json new file mode 100644 index 0000000..9e5e2a9 --- /dev/null +++ b/.history/locales/ko_20251019181629.json @@ -0,0 +1,230 @@ +{ + "navigation": { + "home": "홈", + "about": "calculator": { + "title": "프로젝트 견적 계산기", + "subtitle": "원하는 서비스와 요구사항을 선택하면 실시간으로 정확한 견적을 계산해드립니다", + "meta": { + "title": "프로젝트 견적 계산기", + "description": "웹 개발, 모바일 앱, 디자인 프로젝트 비용을 실시간으로 계산해보세요" + }, + "cta": { + "title": "프로젝트 견적을 확인해보세요", + "subtitle": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "button": "견적 계산기 사용하기" + }, + "step1": { + "title": "1단계: 서비스 선택", + "subtitle": "필요한 서비스를 선택해주세요 (복수 선택 가능)" + }, + "step2": { + "title": "2단계: 프로젝트 세부사항", + "subtitle": "프로젝트의 복잡도와 일정을 선택해주세요" + }, + "complexity": { + "title": "프로젝트 복잡도", + "simple": "단순", + "simple_desc": "기본 기능, 표준 디자인", + "medium": "보통", + "medium_desc": "추가 기능, 커스텀 디자인", + "complex": "복잡", + "complex_desc": "고급 기능, 복합 통합" + }, + "timeline": { + "title": "개발 일정", + "standard": "표준", + "standard_desc": "일반적인 개발 기간", + "rush": "급한", + "rush_desc": "빠른 개발 (+50%)", + "extended": "여유", + "extended_desc": "넉넉한 개발 기간 (-20%)" + }, + "result": { + "title": "견적 결과", + "subtitle": "프로젝트 예상 견적입니다", + "estimated_price": "예상 견적", + "price_note": "* 최종 견적은 프로젝트 세부사항에 따라 달라질 수 있습니다", + "summary": "프로젝트 요약", + "selected_services": "선택한 서비스", + "complexity": "복잡도", + "timeline": "일정", + "get_quote": "정확한 견적 받기", + "recalculate": "다시 계산", + "contact_note": "정확한 견적과 프로젝트 상담을 위해 연락주세요" + }, + "next_step": "다음 단계", + "prev_step": "이전", + "calculate": "계산하기" + }, + "nav": { + "home": "홈", + "about": "회사소개", + "services": "서비스", + "portfolio": "포트폴리오", + "contact": "연락처", + "calculator": "견적계산기", + "admin": "관리자" + }, + "hero": { + "title": "Smart Technology", + "subtitle": "Solutions", + "description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다", + "cta_primary": "프로젝트 시작하기", + "cta_secondary": "포트폴리오 보기" + }, + "services": { + "title": "Our", + "title_highlight": "Services", + "description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션", + "web_development": { + "title": "웹 개발", + "description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발", + "price": "₩500,000~" + }, + "mobile_app": { + "title": "모바일 앱", + "description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱", + "price": "₩800,000~" + }, + "ui_ux_design": { + "title": "UI/UX 디자인", + "description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인", + "price": "₩300,000~" + }, + "digital_marketing": { + "title": "디지털 마케팅", + "description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅", + "price": "₩200,000~" + }, + "view_all": "모든 서비스 보기" + }, + "portfolio": { + "title": "Recent", + "title_highlight": "Projects", + "description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요", + "view_details": "자세히 보기", + "view_all": "전체 포트폴리오 보기" + }, + "calculator": { + "title": "프로젝트 견적을 확인해보세요", + "description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다", + "cta": "견적 계산기 사용하기" + }, + "contact": { + "ready_title": "프로젝트를 시작할 준비가 되셨나요?", + "ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.", + "phone_consultation": "전화 상담", + "email_inquiry": "이메일 문의", + "telegram_chat": "텔레그램 채팅", + "instant_response": "즉시 답변 가능", + "free_consultation": "무료 상담 신청", + "form": { + "name": "이름", + "email": "이메일", + "phone": "연락처", + "service_interest": "관심 서비스", + "service_options": { + "select": "관심 서비스 선택", + "web_development": "웹 개발", + "mobile_app": "모바일 앱", + "ui_ux_design": "UI/UX 디자인", + "branding": "브랜딩", + "consulting": "컨설팅", + "other": "기타" + }, + "message": "프로젝트에 대해 간단히 설명해주세요", + "submit": "상담 신청하기" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업", + "overview": { + "title": "혁신과 창의로 만드는 미래", + "description_1": "SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다.", + "description_2": "우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 제안하여 함께 성장하는 파트너가 되고자 합니다.", + "stats": { + "projects": "100+", + "projects_label": "완료 프로젝트", + "clients": "50+", + "clients_label": "만족한 고객", + "experience": "4년", + "experience_label": "업계 경험" + }, + "mission": "우리의 미션", + "mission_text": "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것", + "vision": "우리의 비전", + "vision_text": "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 전 세계 고객들의 디지털 혁신을 주도하는 것" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "SmartSolTech가 추구하는 핵심 가치들", + "innovation": { + "title": "혁신 (Innovation)", + "description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다." + }, + "collaboration": { + "title": "협력 (Collaboration)", + "description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다." + }, + "quality": { + "title": "품질 (Quality)", + "description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다." + }, + "growth": { + "title": "성장 (Growth)", + "description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "함께 성공하는 파트너가 되어보세요", + "description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요", + "partnership": "파트너십 문의", + "portfolio": "포트폴리오 보기" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "혁신을 이끌어가는 디지털 솔루션 전문 기업", + "quick_links": "빠른 링크", + "services": "서비스", + "contact_info": "연락처 정보", + "follow_us": "팔로우하기", + "rights": "모든 권리 보유." + }, + "theme": { + "light": "라이트 테마", + "dark": "다크 테마", + "toggle": "테마 전환" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "로딩 중...", + "error": "오류가 발생했습니다", + "success": "성공", + "view_more": "더 보기", + "back": "뒤로", + "next": "다음", + "previous": "이전" + } +} \ No newline at end of file diff --git a/.history/locales/ko_20251019182536.json b/.history/locales/ko_20251019182536.json new file mode 100644 index 0000000..2d12a27 --- /dev/null +++ b/.history/locales/ko_20251019182536.json @@ -0,0 +1,196 @@ +{ + "undefined - SmartSolTech": "undefined - SmartSolTech", + "meta": { + "description": "meta.description", + "keywords": "meta.keywords", + "title": "meta.title" + }, + "navigation": { + "home": "navigation.home", + "about": "navigation.about", + "services": "navigation.services", + "portfolio": "navigation.portfolio", + "calculator": "navigation.calculator", + "contact": "navigation.contact", + "home - SmartSolTech": "navigation.home - SmartSolTech" + }, + "language": { + "ko": "language.ko", + "korean": "language.korean", + "english": "language.english", + "russian": "language.russian", + "kazakh": "language.kazakh" + }, + "theme": { + "toggle": "theme.toggle" + }, + "hero": { + "cta_primary": "hero.cta_primary", + "title": { + "smart": "hero.title.smart", + "solutions": "hero.title.solutions" + }, + "subtitle": "hero.subtitle", + "cta": { + "start": "hero.cta.start", + "portfolio": "hero.cta.portfolio" + } + }, + "services": { + "title": { + "our": "services.title.our", + "services": "services.title.services" + }, + "subtitle": "services.subtitle", + "web": { + "title": "services.web.title", + "description": "services.web.description", + "price": "services.web.price" + }, + "mobile": { + "title": "services.mobile.title", + "description": "services.mobile.description", + "price": "services.mobile.price" + }, + "design": { + "title": "services.design.title", + "description": "services.design.description", + "price": "services.design.price" + }, + "marketing": { + "title": "services.marketing.title", + "description": "services.marketing.description", + "price": "services.marketing.price" + }, + "view_all": "services.view_all" + }, + "portfolio": { + "title": { + "recent": "portfolio.title.recent", + "projects": "portfolio.title.projects" + }, + "subtitle": "portfolio.subtitle", + "view_all": "portfolio.view_all" + }, + "common": { + "view_details": "common.view_details" + }, + "calculator": { + "cta": { + "title": "calculator.cta.title", + "subtitle": "calculator.cta.subtitle", + "button": "calculator.cta.button" + }, + "meta": { + "title": "calculator.meta.title", + "description": "calculator.meta.description" + }, + "title": "calculator.title", + "subtitle": "calculator.subtitle", + "step1": { + "title": "calculator.step1.title", + "subtitle": "calculator.step1.subtitle" + }, + "next_step": "calculator.next_step", + "step2": { + "title": "calculator.step2.title", + "subtitle": "calculator.step2.subtitle" + }, + "complexity": { + "title": "calculator.complexity.title", + "simple": "calculator.complexity.simple", + "simple_desc": "calculator.complexity.simple_desc", + "medium": "calculator.complexity.medium", + "medium_desc": "calculator.complexity.medium_desc", + "complex": "calculator.complexity.complex", + "complex_desc": "calculator.complexity.complex_desc" + }, + "timeline": { + "title": "calculator.timeline.title", + "standard": "calculator.timeline.standard", + "standard_desc": "calculator.timeline.standard_desc", + "rush": "calculator.timeline.rush", + "rush_desc": "calculator.timeline.rush_desc", + "extended": "calculator.timeline.extended", + "extended_desc": "calculator.timeline.extended_desc" + }, + "prev_step": "calculator.prev_step", + "calculate": "calculator.calculate", + "result": { + "title": "calculator.result.title", + "subtitle": "calculator.result.subtitle", + "estimated_price": "calculator.result.estimated_price", + "price_note": "calculator.result.price_note", + "summary": "calculator.result.summary", + "get_quote": "calculator.result.get_quote", + "recalculate": "calculator.result.recalculate", + "contact_note": "calculator.result.contact_note", + "selected_services": "선택된 서비스", + "complexity": "복잡도", + "timeline": "개발 기간" + } + }, + "contact": { + "cta": { + "ready": "contact.cta.ready", + "start": "contact.cta.start", + "question": "contact.cta.question", + "subtitle": "contact.cta.subtitle" + }, + "phone": { + "title": "contact.phone.title", + "number": "contact.phone.number" + }, + "email": { + "title": "contact.email.title", + "address": "contact.email.address" + }, + "telegram": { + "title": "contact.telegram.title", + "subtitle": "contact.telegram.subtitle" + }, + "form": { + "title": "contact.form.title", + "name": "contact.form.name", + "email": "contact.form.email", + "phone": "contact.form.phone", + "service": { + "select": "contact.form.service.select", + "web": "contact.form.service.web", + "mobile": "contact.form.service.mobile", + "design": "contact.form.service.design", + "branding": "contact.form.service.branding", + "consulting": "contact.form.service.consulting", + "other": "contact.form.service.other" + }, + "message": "contact.form.message", + "submit": "contact.form.submit", + "success": "contact.form.success", + "error": "contact.form.error" + } + }, + "footer": { + "company": { + "description": "footer.company.description" + }, + "links": { + "title": "footer.links.title" + }, + "contact": { + "title": "footer.contact.title", + "email": "footer.contact.email", + "phone": "footer.contact.phone", + "address": "footer.contact.address" + }, + "copyright": "footer.copyright", + "privacy": "footer.privacy", + "terms": "footer.terms" + }, + "nav": { + "home": "nav.home", + "about": "nav.about", + "services": "nav.services", + "portfolio": "nav.portfolio", + "calculator": "nav.calculator" + } +} \ No newline at end of file diff --git a/.history/locales/ko_20251019182628.json b/.history/locales/ko_20251019182628.json new file mode 100644 index 0000000..2d12a27 --- /dev/null +++ b/.history/locales/ko_20251019182628.json @@ -0,0 +1,196 @@ +{ + "undefined - SmartSolTech": "undefined - SmartSolTech", + "meta": { + "description": "meta.description", + "keywords": "meta.keywords", + "title": "meta.title" + }, + "navigation": { + "home": "navigation.home", + "about": "navigation.about", + "services": "navigation.services", + "portfolio": "navigation.portfolio", + "calculator": "navigation.calculator", + "contact": "navigation.contact", + "home - SmartSolTech": "navigation.home - SmartSolTech" + }, + "language": { + "ko": "language.ko", + "korean": "language.korean", + "english": "language.english", + "russian": "language.russian", + "kazakh": "language.kazakh" + }, + "theme": { + "toggle": "theme.toggle" + }, + "hero": { + "cta_primary": "hero.cta_primary", + "title": { + "smart": "hero.title.smart", + "solutions": "hero.title.solutions" + }, + "subtitle": "hero.subtitle", + "cta": { + "start": "hero.cta.start", + "portfolio": "hero.cta.portfolio" + } + }, + "services": { + "title": { + "our": "services.title.our", + "services": "services.title.services" + }, + "subtitle": "services.subtitle", + "web": { + "title": "services.web.title", + "description": "services.web.description", + "price": "services.web.price" + }, + "mobile": { + "title": "services.mobile.title", + "description": "services.mobile.description", + "price": "services.mobile.price" + }, + "design": { + "title": "services.design.title", + "description": "services.design.description", + "price": "services.design.price" + }, + "marketing": { + "title": "services.marketing.title", + "description": "services.marketing.description", + "price": "services.marketing.price" + }, + "view_all": "services.view_all" + }, + "portfolio": { + "title": { + "recent": "portfolio.title.recent", + "projects": "portfolio.title.projects" + }, + "subtitle": "portfolio.subtitle", + "view_all": "portfolio.view_all" + }, + "common": { + "view_details": "common.view_details" + }, + "calculator": { + "cta": { + "title": "calculator.cta.title", + "subtitle": "calculator.cta.subtitle", + "button": "calculator.cta.button" + }, + "meta": { + "title": "calculator.meta.title", + "description": "calculator.meta.description" + }, + "title": "calculator.title", + "subtitle": "calculator.subtitle", + "step1": { + "title": "calculator.step1.title", + "subtitle": "calculator.step1.subtitle" + }, + "next_step": "calculator.next_step", + "step2": { + "title": "calculator.step2.title", + "subtitle": "calculator.step2.subtitle" + }, + "complexity": { + "title": "calculator.complexity.title", + "simple": "calculator.complexity.simple", + "simple_desc": "calculator.complexity.simple_desc", + "medium": "calculator.complexity.medium", + "medium_desc": "calculator.complexity.medium_desc", + "complex": "calculator.complexity.complex", + "complex_desc": "calculator.complexity.complex_desc" + }, + "timeline": { + "title": "calculator.timeline.title", + "standard": "calculator.timeline.standard", + "standard_desc": "calculator.timeline.standard_desc", + "rush": "calculator.timeline.rush", + "rush_desc": "calculator.timeline.rush_desc", + "extended": "calculator.timeline.extended", + "extended_desc": "calculator.timeline.extended_desc" + }, + "prev_step": "calculator.prev_step", + "calculate": "calculator.calculate", + "result": { + "title": "calculator.result.title", + "subtitle": "calculator.result.subtitle", + "estimated_price": "calculator.result.estimated_price", + "price_note": "calculator.result.price_note", + "summary": "calculator.result.summary", + "get_quote": "calculator.result.get_quote", + "recalculate": "calculator.result.recalculate", + "contact_note": "calculator.result.contact_note", + "selected_services": "선택된 서비스", + "complexity": "복잡도", + "timeline": "개발 기간" + } + }, + "contact": { + "cta": { + "ready": "contact.cta.ready", + "start": "contact.cta.start", + "question": "contact.cta.question", + "subtitle": "contact.cta.subtitle" + }, + "phone": { + "title": "contact.phone.title", + "number": "contact.phone.number" + }, + "email": { + "title": "contact.email.title", + "address": "contact.email.address" + }, + "telegram": { + "title": "contact.telegram.title", + "subtitle": "contact.telegram.subtitle" + }, + "form": { + "title": "contact.form.title", + "name": "contact.form.name", + "email": "contact.form.email", + "phone": "contact.form.phone", + "service": { + "select": "contact.form.service.select", + "web": "contact.form.service.web", + "mobile": "contact.form.service.mobile", + "design": "contact.form.service.design", + "branding": "contact.form.service.branding", + "consulting": "contact.form.service.consulting", + "other": "contact.form.service.other" + }, + "message": "contact.form.message", + "submit": "contact.form.submit", + "success": "contact.form.success", + "error": "contact.form.error" + } + }, + "footer": { + "company": { + "description": "footer.company.description" + }, + "links": { + "title": "footer.links.title" + }, + "contact": { + "title": "footer.contact.title", + "email": "footer.contact.email", + "phone": "footer.contact.phone", + "address": "footer.contact.address" + }, + "copyright": "footer.copyright", + "privacy": "footer.privacy", + "terms": "footer.terms" + }, + "nav": { + "home": "nav.home", + "about": "nav.about", + "services": "nav.services", + "portfolio": "nav.portfolio", + "calculator": "nav.calculator" + } +} \ No newline at end of file diff --git a/.history/locales/ru_20251019171530.json b/.history/locales/ru_20251019171530.json new file mode 100644 index 0000000..64cd338 --- /dev/null +++ b/.history/locales/ru_20251019171530.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "Главная", + "about": "О нас", + "services": "Услуги", + "portfolio": "Портфолио", + "contact": "Контакты", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Умные Технологические", + "subtitle": "Решения", + "description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса", + "cta_primary": "Начать проект", + "cta_secondary": "Посмотреть портфолио" + }, + "services": { + "title": "Наши", + "title_highlight": "Услуги", + "description": "Цифровые решения с использованием передовых технологий и творческих идей", + "web_development": { + "title": "Веб-разработка", + "description": "Современные и адаптивные веб-сайты и веб-приложения", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильные приложения", + "description": "Нативные и кроссплатформенные приложения для iOS и Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Ориентированный на пользователя интуитивный и красивый дизайн интерфейса", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифровой маркетинг", + "description": "Цифровой маркетинг через SEO, социальные сети, онлайн-рекламу", + "price": "$2,000~" + }, + "view_all": "Посмотреть все услуги" + }, + "portfolio": { + "title": "Недавние", + "title_highlight": "Проекты", + "description": "Ознакомьтесь с проектами, выполненными для успеха клиентов", + "view_details": "Подробнее", + "view_all": "Посмотреть все портфолио" + }, + "calculator": { + "title": "Проверьте стоимость вашего проекта", + "description": "Выберите нужные услуги и требования для расчета стоимости в реальном времени", + "cta": "Использовать калькулятор стоимости" + }, + "contact": { + "ready_title": "Готовы начать свой проект?", + "ready_description": "Превратите свои идеи в реальность. Эксперты предоставят лучшие решения.", + "phone_consultation": "Телефонная консультация", + "email_inquiry": "Запрос по электронной почте", + "telegram_chat": "Чат в Telegram", + "instant_response": "Мгновенный ответ доступен", + "free_consultation": "Заявка на бесплатную консультацию", + "form": { + "name": "Имя", + "email": "Электронная почта", + "phone": "Телефон", + "service_interest": "Интересующая услуга", + "service_options": { + "select": "Выберите интересующую услугу", + "web_development": "Веб-разработка", + "mobile_app": "Мобильное приложение", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Консалтинг", + "other": "Другое" + }, + "message": "Кратко опишите ваш проект", + "submit": "Подать заявку на консультацию" + } + }, + "about": { + "hero_title": "О", + "hero_highlight": "SmartSolTech", + "hero_description": "Специалист по цифровым решениям, ведущий к успеху клиентов с помощью инновационных технологий", + "overview": { + "title": "Создавая будущее с инновациями и креативностью", + "description_1": "SmartSolTech - это специалист по цифровым решениям, основанный в 2020 году, поддерживающий успех клиентского бизнеса с помощью инновационных технологий и творческих идей в области веб-разработки, мобильных приложений и UI/UX дизайна.", + "description_2": "Мы не просто предоставляем технологии, но понимаем цели клиентов и предлагаем оптимальные решения, чтобы стать партнерами, растущими вместе.", + "stats": { + "projects": "100+", + "projects_label": "Завершенные проекты", + "clients": "50+", + "clients_label": "Довольные клиенты", + "experience": "4 года", + "experience_label": "Опыт в отрасли" + }, + "mission": "Наша миссия", + "mission_text": "Помощь всем предприятиям в достижении успеха в цифровую эпоху с помощью технологий", + "vision": "Наше видение", + "vision_text": "Рост как глобальной компании цифровых решений, представляющей Корею, для ведения цифровых инноваций для клиентов по всему миру" + }, + "values": { + "title": "Основные", + "title_highlight": "Ценности", + "description": "Основные ценности, которых придерживается SmartSolTech", + "innovation": { + "title": "Инновации", + "description": "Мы предоставляем инновационные решения через непрерывные исследования и внедрение передовых технологий." + }, + "collaboration": { + "title": "Сотрудничество", + "description": "Мы создаем лучшие результаты через тесное общение и сотрудничество с клиентами." + }, + "quality": { + "title": "Качество", + "description": "Мы поддерживаем высокие стандарты качества и предоставляем высококачественные продукты, которыми клиенты могут быть довольны." + }, + "growth": { + "title": "Рост", + "description": "Мы растем вместе с клиентами и стремимся к непрерывному обучению и развитию." + } + }, + "team": { + "title": "Наша", + "title_highlight": "Команда", + "description": "Представляем команду SmartSolTech с экспертизой и страстью" + }, + "tech_stack": { + "title": "Технологический", + "title_highlight": "Стек", + "description": "Мы предоставляем лучшие решения с передовыми технологиями и проверенными инструментами", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильные" + }, + "cta": { + "title": "Станьте партнером для совместного успеха", + "description": "Выведите свой бизнес на следующий уровень с SmartSolTech", + "partnership": "Запрос о партнерстве", + "portfolio": "Посмотреть портфолио" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Специалист по цифровым решениям, ведущий инновации", + "quick_links": "Быстрые ссылки", + "services": "Услуги", + "contact_info": "Контактная информация", + "follow_us": "Подписывайтесь", + "rights": "Все права защищены." + }, + "theme": { + "light": "Светлая тема", + "dark": "Темная тема", + "toggle": "Переключить тему" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Загрузка...", + "error": "Произошла ошибка", + "success": "Успешно", + "view_more": "Посмотреть еще", + "back": "Назад", + "next": "Далее", + "previous": "Предыдущий" + } +} \ No newline at end of file diff --git a/.history/locales/ru_20251019171645.json b/.history/locales/ru_20251019171645.json new file mode 100644 index 0000000..64cd338 --- /dev/null +++ b/.history/locales/ru_20251019171645.json @@ -0,0 +1,173 @@ +{ + "navigation": { + "home": "Главная", + "about": "О нас", + "services": "Услуги", + "portfolio": "Портфолио", + "contact": "Контакты", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Умные Технологические", + "subtitle": "Решения", + "description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса", + "cta_primary": "Начать проект", + "cta_secondary": "Посмотреть портфолио" + }, + "services": { + "title": "Наши", + "title_highlight": "Услуги", + "description": "Цифровые решения с использованием передовых технологий и творческих идей", + "web_development": { + "title": "Веб-разработка", + "description": "Современные и адаптивные веб-сайты и веб-приложения", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильные приложения", + "description": "Нативные и кроссплатформенные приложения для iOS и Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Ориентированный на пользователя интуитивный и красивый дизайн интерфейса", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифровой маркетинг", + "description": "Цифровой маркетинг через SEO, социальные сети, онлайн-рекламу", + "price": "$2,000~" + }, + "view_all": "Посмотреть все услуги" + }, + "portfolio": { + "title": "Недавние", + "title_highlight": "Проекты", + "description": "Ознакомьтесь с проектами, выполненными для успеха клиентов", + "view_details": "Подробнее", + "view_all": "Посмотреть все портфолио" + }, + "calculator": { + "title": "Проверьте стоимость вашего проекта", + "description": "Выберите нужные услуги и требования для расчета стоимости в реальном времени", + "cta": "Использовать калькулятор стоимости" + }, + "contact": { + "ready_title": "Готовы начать свой проект?", + "ready_description": "Превратите свои идеи в реальность. Эксперты предоставят лучшие решения.", + "phone_consultation": "Телефонная консультация", + "email_inquiry": "Запрос по электронной почте", + "telegram_chat": "Чат в Telegram", + "instant_response": "Мгновенный ответ доступен", + "free_consultation": "Заявка на бесплатную консультацию", + "form": { + "name": "Имя", + "email": "Электронная почта", + "phone": "Телефон", + "service_interest": "Интересующая услуга", + "service_options": { + "select": "Выберите интересующую услугу", + "web_development": "Веб-разработка", + "mobile_app": "Мобильное приложение", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Консалтинг", + "other": "Другое" + }, + "message": "Кратко опишите ваш проект", + "submit": "Подать заявку на консультацию" + } + }, + "about": { + "hero_title": "О", + "hero_highlight": "SmartSolTech", + "hero_description": "Специалист по цифровым решениям, ведущий к успеху клиентов с помощью инновационных технологий", + "overview": { + "title": "Создавая будущее с инновациями и креативностью", + "description_1": "SmartSolTech - это специалист по цифровым решениям, основанный в 2020 году, поддерживающий успех клиентского бизнеса с помощью инновационных технологий и творческих идей в области веб-разработки, мобильных приложений и UI/UX дизайна.", + "description_2": "Мы не просто предоставляем технологии, но понимаем цели клиентов и предлагаем оптимальные решения, чтобы стать партнерами, растущими вместе.", + "stats": { + "projects": "100+", + "projects_label": "Завершенные проекты", + "clients": "50+", + "clients_label": "Довольные клиенты", + "experience": "4 года", + "experience_label": "Опыт в отрасли" + }, + "mission": "Наша миссия", + "mission_text": "Помощь всем предприятиям в достижении успеха в цифровую эпоху с помощью технологий", + "vision": "Наше видение", + "vision_text": "Рост как глобальной компании цифровых решений, представляющей Корею, для ведения цифровых инноваций для клиентов по всему миру" + }, + "values": { + "title": "Основные", + "title_highlight": "Ценности", + "description": "Основные ценности, которых придерживается SmartSolTech", + "innovation": { + "title": "Инновации", + "description": "Мы предоставляем инновационные решения через непрерывные исследования и внедрение передовых технологий." + }, + "collaboration": { + "title": "Сотрудничество", + "description": "Мы создаем лучшие результаты через тесное общение и сотрудничество с клиентами." + }, + "quality": { + "title": "Качество", + "description": "Мы поддерживаем высокие стандарты качества и предоставляем высококачественные продукты, которыми клиенты могут быть довольны." + }, + "growth": { + "title": "Рост", + "description": "Мы растем вместе с клиентами и стремимся к непрерывному обучению и развитию." + } + }, + "team": { + "title": "Наша", + "title_highlight": "Команда", + "description": "Представляем команду SmartSolTech с экспертизой и страстью" + }, + "tech_stack": { + "title": "Технологический", + "title_highlight": "Стек", + "description": "Мы предоставляем лучшие решения с передовыми технологиями и проверенными инструментами", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильные" + }, + "cta": { + "title": "Станьте партнером для совместного успеха", + "description": "Выведите свой бизнес на следующий уровень с SmartSolTech", + "partnership": "Запрос о партнерстве", + "portfolio": "Посмотреть портфолио" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Специалист по цифровым решениям, ведущий инновации", + "quick_links": "Быстрые ссылки", + "services": "Услуги", + "contact_info": "Контактная информация", + "follow_us": "Подписывайтесь", + "rights": "Все права защищены." + }, + "theme": { + "light": "Светлая тема", + "dark": "Темная тема", + "toggle": "Переключить тему" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Загрузка...", + "error": "Произошла ошибка", + "success": "Успешно", + "view_more": "Посмотреть еще", + "back": "Назад", + "next": "Далее", + "previous": "Предыдущий" + } +} \ No newline at end of file diff --git a/.history/locales/ru_20251019181401.json b/.history/locales/ru_20251019181401.json new file mode 100644 index 0000000..edcfda9 --- /dev/null +++ b/.history/locales/ru_20251019181401.json @@ -0,0 +1,223 @@ +{ + "navigation": { + "home": "Главная", + "about": "О нас", + "services": "Услуги", + "portfolio": "Портфолио", + "contact": "Контакты", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Умные Технологические", + "subtitle": "Решения", + "description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса", + "cta_primary": "Начать проект", + "cta_secondary": "Посмотреть портфолио" + }, + "services": { + "title": "Наши", + "title_highlight": "Услуги", + "description": "Цифровые решения с использованием передовых технологий и творческих идей", + "web_development": { + "title": "Веб-разработка", + "description": "Современные и адаптивные веб-сайты и веб-приложения", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильные приложения", + "description": "Нативные и кроссплатформенные приложения для iOS и Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Ориентированный на пользователя интуитивный и красивый дизайн интерфейса", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифровой маркетинг", + "description": "Цифровой маркетинг через SEO, социальные сети, онлайн-рекламу", + "price": "$2,000~" + }, + "view_all": "Посмотреть все услуги" + }, + "portfolio": { + "title": "Недавние", + "title_highlight": "Проекты", + "description": "Ознакомьтесь с проектами, выполненными для успеха клиентов", + "view_details": "Подробнее", + "view_all": "Посмотреть все портфолио" + }, + "calculator": { + "title": "Калькулятор Стоимости Проекта", + "subtitle": "Выберите нужные услуги и требования для получения точной оценки стоимости в режиме реального времени", + "meta": { + "title": "Калькулятор стоимости проекта", + "description": "Рассчитайте стоимость вашего проекта веб-разработки, мобильного приложения или дизайна с помощью нашего интерактивного калькулятора" + }, + "cta": { + "title": "Узнайте стоимость вашего проекта", + "subtitle": "Выберите необходимые услуги и требования, и мы рассчитаем стоимость в режиме реального времени", + "button": "Использовать калькулятор стоимости" + }, + "step1": { + "title": "Шаг 1: Выбор услуг", + "subtitle": "Выберите необходимые услуги (можно выбрать несколько)" + }, + "step2": { + "title": "Шаг 2: Детали проекта", + "subtitle": "Выберите сложность проекта и сроки" + }, + "complexity": { + "title": "Сложность проекта", + "simple": "Простой", + "simple_desc": "Базовый функционал, стандартный дизайн", + "medium": "Средний", + "medium_desc": "Дополнительные функции, кастомный дизайн", + "complex": "Сложный", + "complex_desc": "Расширенный функционал, интеграции" + }, + "timeline": { + "title": "Временные рамки", + "standard": "Стандартные", + "standard_desc": "Обычные сроки разработки", + "rush": "Срочно", + "rush_desc": "Ускоренная разработка (+50%)", + "extended": "Расширенные", + "extended_desc": "Длительная разработка (-20%)" + }, + "result": { + "title": "Результат расчета", + "subtitle": "Вот ваша предварительная оценка стоимости проекта", + "estimated_price": "Предварительная стоимость", + "price_note": "* Окончательная стоимость может варьироваться в зависимости от деталей проекта", + "summary": "Сводка проекта", + "selected_services": "Выбранные услуги", + "complexity": "Сложность", + "timeline": "Временные рамки", + "get_quote": "Получить точное предложение", + "recalculate": "Пересчитать", + "contact_note": "Свяжитесь с нами для получения точного предложения и обсуждения деталей проекта" + }, + "next_step": "Следующий шаг", + "prev_step": "Назад", + "calculate": "Рассчитать" + }, + "contact": { + "ready_title": "Готовы начать свой проект?", + "ready_description": "Превратите свои идеи в реальность. Эксперты предоставят лучшие решения.", + "phone_consultation": "Телефонная консультация", + "email_inquiry": "Запрос по электронной почте", + "telegram_chat": "Чат в Telegram", + "instant_response": "Мгновенный ответ доступен", + "free_consultation": "Заявка на бесплатную консультацию", + "form": { + "name": "Имя", + "email": "Электронная почта", + "phone": "Телефон", + "service_interest": "Интересующая услуга", + "service_options": { + "select": "Выберите интересующую услугу", + "web_development": "Веб-разработка", + "mobile_app": "Мобильное приложение", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Консалтинг", + "other": "Другое" + }, + "message": "Кратко опишите ваш проект", + "submit": "Подать заявку на консультацию" + } + }, + "about": { + "hero_title": "О", + "hero_highlight": "SmartSolTech", + "hero_description": "Специалист по цифровым решениям, ведущий к успеху клиентов с помощью инновационных технологий", + "overview": { + "title": "Создавая будущее с инновациями и креативностью", + "description_1": "SmartSolTech - это специалист по цифровым решениям, основанный в 2020 году, поддерживающий успех клиентского бизнеса с помощью инновационных технологий и творческих идей в области веб-разработки, мобильных приложений и UI/UX дизайна.", + "description_2": "Мы не просто предоставляем технологии, но понимаем цели клиентов и предлагаем оптимальные решения, чтобы стать партнерами, растущими вместе.", + "stats": { + "projects": "100+", + "projects_label": "Завершенные проекты", + "clients": "50+", + "clients_label": "Довольные клиенты", + "experience": "4 года", + "experience_label": "Опыт в отрасли" + }, + "mission": "Наша миссия", + "mission_text": "Помощь всем предприятиям в достижении успеха в цифровую эпоху с помощью технологий", + "vision": "Наше видение", + "vision_text": "Рост как глобальной компании цифровых решений, представляющей Корею, для ведения цифровых инноваций для клиентов по всему миру" + }, + "values": { + "title": "Основные", + "title_highlight": "Ценности", + "description": "Основные ценности, которых придерживается SmartSolTech", + "innovation": { + "title": "Инновации", + "description": "Мы предоставляем инновационные решения через непрерывные исследования и внедрение передовых технологий." + }, + "collaboration": { + "title": "Сотрудничество", + "description": "Мы создаем лучшие результаты через тесное общение и сотрудничество с клиентами." + }, + "quality": { + "title": "Качество", + "description": "Мы поддерживаем высокие стандарты качества и предоставляем высококачественные продукты, которыми клиенты могут быть довольны." + }, + "growth": { + "title": "Рост", + "description": "Мы растем вместе с клиентами и стремимся к непрерывному обучению и развитию." + } + }, + "team": { + "title": "Наша", + "title_highlight": "Команда", + "description": "Представляем команду SmartSolTech с экспертизой и страстью" + }, + "tech_stack": { + "title": "Технологический", + "title_highlight": "Стек", + "description": "Мы предоставляем лучшие решения с передовыми технологиями и проверенными инструментами", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильные" + }, + "cta": { + "title": "Станьте партнером для совместного успеха", + "description": "Выведите свой бизнес на следующий уровень с SmartSolTech", + "partnership": "Запрос о партнерстве", + "portfolio": "Посмотреть портфолио" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Специалист по цифровым решениям, ведущий инновации", + "quick_links": "Быстрые ссылки", + "services": "Услуги", + "contact_info": "Контактная информация", + "follow_us": "Подписывайтесь", + "rights": "Все права защищены." + }, + "theme": { + "light": "Светлая тема", + "dark": "Темная тема", + "toggle": "Переключить тему" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Загрузка...", + "error": "Произошла ошибка", + "success": "Успешно", + "view_more": "Посмотреть еще", + "back": "Назад", + "next": "Далее", + "previous": "Предыдущий" + } +} \ No newline at end of file diff --git a/.history/locales/ru_20251019181629.json b/.history/locales/ru_20251019181629.json new file mode 100644 index 0000000..edcfda9 --- /dev/null +++ b/.history/locales/ru_20251019181629.json @@ -0,0 +1,223 @@ +{ + "navigation": { + "home": "Главная", + "about": "О нас", + "services": "Услуги", + "portfolio": "Портфолио", + "contact": "Контакты", + "calculator": "Калькулятор", + "admin": "Админ" + }, + "hero": { + "title": "Умные Технологические", + "subtitle": "Решения", + "description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса", + "cta_primary": "Начать проект", + "cta_secondary": "Посмотреть портфолио" + }, + "services": { + "title": "Наши", + "title_highlight": "Услуги", + "description": "Цифровые решения с использованием передовых технологий и творческих идей", + "web_development": { + "title": "Веб-разработка", + "description": "Современные и адаптивные веб-сайты и веб-приложения", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильные приложения", + "description": "Нативные и кроссплатформенные приложения для iOS и Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Ориентированный на пользователя интуитивный и красивый дизайн интерфейса", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифровой маркетинг", + "description": "Цифровой маркетинг через SEO, социальные сети, онлайн-рекламу", + "price": "$2,000~" + }, + "view_all": "Посмотреть все услуги" + }, + "portfolio": { + "title": "Недавние", + "title_highlight": "Проекты", + "description": "Ознакомьтесь с проектами, выполненными для успеха клиентов", + "view_details": "Подробнее", + "view_all": "Посмотреть все портфолио" + }, + "calculator": { + "title": "Калькулятор Стоимости Проекта", + "subtitle": "Выберите нужные услуги и требования для получения точной оценки стоимости в режиме реального времени", + "meta": { + "title": "Калькулятор стоимости проекта", + "description": "Рассчитайте стоимость вашего проекта веб-разработки, мобильного приложения или дизайна с помощью нашего интерактивного калькулятора" + }, + "cta": { + "title": "Узнайте стоимость вашего проекта", + "subtitle": "Выберите необходимые услуги и требования, и мы рассчитаем стоимость в режиме реального времени", + "button": "Использовать калькулятор стоимости" + }, + "step1": { + "title": "Шаг 1: Выбор услуг", + "subtitle": "Выберите необходимые услуги (можно выбрать несколько)" + }, + "step2": { + "title": "Шаг 2: Детали проекта", + "subtitle": "Выберите сложность проекта и сроки" + }, + "complexity": { + "title": "Сложность проекта", + "simple": "Простой", + "simple_desc": "Базовый функционал, стандартный дизайн", + "medium": "Средний", + "medium_desc": "Дополнительные функции, кастомный дизайн", + "complex": "Сложный", + "complex_desc": "Расширенный функционал, интеграции" + }, + "timeline": { + "title": "Временные рамки", + "standard": "Стандартные", + "standard_desc": "Обычные сроки разработки", + "rush": "Срочно", + "rush_desc": "Ускоренная разработка (+50%)", + "extended": "Расширенные", + "extended_desc": "Длительная разработка (-20%)" + }, + "result": { + "title": "Результат расчета", + "subtitle": "Вот ваша предварительная оценка стоимости проекта", + "estimated_price": "Предварительная стоимость", + "price_note": "* Окончательная стоимость может варьироваться в зависимости от деталей проекта", + "summary": "Сводка проекта", + "selected_services": "Выбранные услуги", + "complexity": "Сложность", + "timeline": "Временные рамки", + "get_quote": "Получить точное предложение", + "recalculate": "Пересчитать", + "contact_note": "Свяжитесь с нами для получения точного предложения и обсуждения деталей проекта" + }, + "next_step": "Следующий шаг", + "prev_step": "Назад", + "calculate": "Рассчитать" + }, + "contact": { + "ready_title": "Готовы начать свой проект?", + "ready_description": "Превратите свои идеи в реальность. Эксперты предоставят лучшие решения.", + "phone_consultation": "Телефонная консультация", + "email_inquiry": "Запрос по электронной почте", + "telegram_chat": "Чат в Telegram", + "instant_response": "Мгновенный ответ доступен", + "free_consultation": "Заявка на бесплатную консультацию", + "form": { + "name": "Имя", + "email": "Электронная почта", + "phone": "Телефон", + "service_interest": "Интересующая услуга", + "service_options": { + "select": "Выберите интересующую услугу", + "web_development": "Веб-разработка", + "mobile_app": "Мобильное приложение", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Консалтинг", + "other": "Другое" + }, + "message": "Кратко опишите ваш проект", + "submit": "Подать заявку на консультацию" + } + }, + "about": { + "hero_title": "О", + "hero_highlight": "SmartSolTech", + "hero_description": "Специалист по цифровым решениям, ведущий к успеху клиентов с помощью инновационных технологий", + "overview": { + "title": "Создавая будущее с инновациями и креативностью", + "description_1": "SmartSolTech - это специалист по цифровым решениям, основанный в 2020 году, поддерживающий успех клиентского бизнеса с помощью инновационных технологий и творческих идей в области веб-разработки, мобильных приложений и UI/UX дизайна.", + "description_2": "Мы не просто предоставляем технологии, но понимаем цели клиентов и предлагаем оптимальные решения, чтобы стать партнерами, растущими вместе.", + "stats": { + "projects": "100+", + "projects_label": "Завершенные проекты", + "clients": "50+", + "clients_label": "Довольные клиенты", + "experience": "4 года", + "experience_label": "Опыт в отрасли" + }, + "mission": "Наша миссия", + "mission_text": "Помощь всем предприятиям в достижении успеха в цифровую эпоху с помощью технологий", + "vision": "Наше видение", + "vision_text": "Рост как глобальной компании цифровых решений, представляющей Корею, для ведения цифровых инноваций для клиентов по всему миру" + }, + "values": { + "title": "Основные", + "title_highlight": "Ценности", + "description": "Основные ценности, которых придерживается SmartSolTech", + "innovation": { + "title": "Инновации", + "description": "Мы предоставляем инновационные решения через непрерывные исследования и внедрение передовых технологий." + }, + "collaboration": { + "title": "Сотрудничество", + "description": "Мы создаем лучшие результаты через тесное общение и сотрудничество с клиентами." + }, + "quality": { + "title": "Качество", + "description": "Мы поддерживаем высокие стандарты качества и предоставляем высококачественные продукты, которыми клиенты могут быть довольны." + }, + "growth": { + "title": "Рост", + "description": "Мы растем вместе с клиентами и стремимся к непрерывному обучению и развитию." + } + }, + "team": { + "title": "Наша", + "title_highlight": "Команда", + "description": "Представляем команду SmartSolTech с экспертизой и страстью" + }, + "tech_stack": { + "title": "Технологический", + "title_highlight": "Стек", + "description": "Мы предоставляем лучшие решения с передовыми технологиями и проверенными инструментами", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильные" + }, + "cta": { + "title": "Станьте партнером для совместного успеха", + "description": "Выведите свой бизнес на следующий уровень с SmartSolTech", + "partnership": "Запрос о партнерстве", + "portfolio": "Посмотреть портфолио" + } + }, + "footer": { + "company": "SmartSolTech", + "description": "Специалист по цифровым решениям, ведущий инновации", + "quick_links": "Быстрые ссылки", + "services": "Услуги", + "contact_info": "Контактная информация", + "follow_us": "Подписывайтесь", + "rights": "Все права защищены." + }, + "theme": { + "light": "Светлая тема", + "dark": "Темная тема", + "toggle": "Переключить тему" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Загрузка...", + "error": "Произошла ошибка", + "success": "Успешно", + "view_more": "Посмотреть еще", + "back": "Назад", + "next": "Далее", + "previous": "Предыдущий" + } +} \ No newline at end of file diff --git a/.history/middleware/auth_20251019163222.js b/.history/middleware/auth_20251019163222.js new file mode 100644 index 0000000..e1aa95c --- /dev/null +++ b/.history/middleware/auth_20251019163222.js @@ -0,0 +1,154 @@ +const jwt = require('jsonwebtoken'); +const User = require('../models/User'); + +/** + * Authentication middleware + * Verifies JWT token and attaches user to request + */ +const authenticateToken = async (req, res, next) => { + try { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + return res.status(401).json({ + success: false, + message: 'Access token required' + }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.userId).select('-password'); + + if (!user || !user.isActive) { + return res.status(401).json({ + success: false, + message: 'Invalid or inactive user' + }); + } + + req.user = user; + next(); + } catch (error) { + console.error('Token verification error:', error); + return res.status(403).json({ + success: false, + message: 'Invalid token' + }); + } +}; + +/** + * Session-based authentication middleware + * For web pages using sessions + */ +const authenticateSession = async (req, res, next) => { + try { + if (!req.session.userId) { + req.flash('error', '로그인이 필요합니다.'); + return res.redirect('/auth/login'); + } + + const user = await User.findById(req.session.userId).select('-password'); + + if (!user || !user.isActive) { + req.session.destroy(); + req.flash('error', '유효하지 않은 사용자입니다.'); + return res.redirect('/auth/login'); + } + + req.user = user; + res.locals.user = user; + next(); + } catch (error) { + console.error('Session authentication error:', error); + req.session.destroy(); + req.flash('error', '인증 오류가 발생했습니다.'); + return res.redirect('/auth/login'); + } +}; + +/** + * Admin role middleware + * Requires user to be authenticated and have admin role + */ +const requireAdmin = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + success: false, + message: 'Authentication required' + }); + } + + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + message: 'Admin access required' + }); + } + + next(); +}; + +/** + * Admin session middleware for web pages + */ +const requireAdminSession = (req, res, next) => { + if (!req.user) { + req.flash('error', '로그인이 필요합니다.'); + return res.redirect('/auth/login'); + } + + if (req.user.role !== 'admin') { + req.flash('error', '관리자 권한이 필요합니다.'); + return res.redirect('/'); + } + + next(); +}; + +/** + * Optional authentication middleware + * Attaches user if token exists but doesn't require it + */ +const optionalAuth = async (req, res, next) => { + try { + // Check session first + if (req.session.userId) { + const user = await User.findById(req.session.userId).select('-password'); + if (user && user.isActive) { + req.user = user; + res.locals.user = user; + } + } + + // Check JWT token if no session + if (!req.user) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (token) { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.userId).select('-password'); + + if (user && user.isActive) { + req.user = user; + res.locals.user = user; + } + } + } + + next(); + } catch (error) { + // Continue without authentication if token is invalid + next(); + } +}; + +module.exports = { + authenticateToken, + authenticateSession, + requireAdmin, + requireAdminSession, + optionalAuth +}; \ No newline at end of file diff --git a/.history/middleware/auth_20251019163807.js b/.history/middleware/auth_20251019163807.js new file mode 100644 index 0000000..e1aa95c --- /dev/null +++ b/.history/middleware/auth_20251019163807.js @@ -0,0 +1,154 @@ +const jwt = require('jsonwebtoken'); +const User = require('../models/User'); + +/** + * Authentication middleware + * Verifies JWT token and attaches user to request + */ +const authenticateToken = async (req, res, next) => { + try { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + return res.status(401).json({ + success: false, + message: 'Access token required' + }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.userId).select('-password'); + + if (!user || !user.isActive) { + return res.status(401).json({ + success: false, + message: 'Invalid or inactive user' + }); + } + + req.user = user; + next(); + } catch (error) { + console.error('Token verification error:', error); + return res.status(403).json({ + success: false, + message: 'Invalid token' + }); + } +}; + +/** + * Session-based authentication middleware + * For web pages using sessions + */ +const authenticateSession = async (req, res, next) => { + try { + if (!req.session.userId) { + req.flash('error', '로그인이 필요합니다.'); + return res.redirect('/auth/login'); + } + + const user = await User.findById(req.session.userId).select('-password'); + + if (!user || !user.isActive) { + req.session.destroy(); + req.flash('error', '유효하지 않은 사용자입니다.'); + return res.redirect('/auth/login'); + } + + req.user = user; + res.locals.user = user; + next(); + } catch (error) { + console.error('Session authentication error:', error); + req.session.destroy(); + req.flash('error', '인증 오류가 발생했습니다.'); + return res.redirect('/auth/login'); + } +}; + +/** + * Admin role middleware + * Requires user to be authenticated and have admin role + */ +const requireAdmin = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + success: false, + message: 'Authentication required' + }); + } + + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + message: 'Admin access required' + }); + } + + next(); +}; + +/** + * Admin session middleware for web pages + */ +const requireAdminSession = (req, res, next) => { + if (!req.user) { + req.flash('error', '로그인이 필요합니다.'); + return res.redirect('/auth/login'); + } + + if (req.user.role !== 'admin') { + req.flash('error', '관리자 권한이 필요합니다.'); + return res.redirect('/'); + } + + next(); +}; + +/** + * Optional authentication middleware + * Attaches user if token exists but doesn't require it + */ +const optionalAuth = async (req, res, next) => { + try { + // Check session first + if (req.session.userId) { + const user = await User.findById(req.session.userId).select('-password'); + if (user && user.isActive) { + req.user = user; + res.locals.user = user; + } + } + + // Check JWT token if no session + if (!req.user) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (token) { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.userId).select('-password'); + + if (user && user.isActive) { + req.user = user; + res.locals.user = user; + } + } + } + + next(); + } catch (error) { + // Continue without authentication if token is invalid + next(); + } +}; + +module.exports = { + authenticateToken, + authenticateSession, + requireAdmin, + requireAdminSession, + optionalAuth +}; \ No newline at end of file diff --git a/.history/middleware/validation_20251019163300.js b/.history/middleware/validation_20251019163300.js new file mode 100644 index 0000000..b19ca74 --- /dev/null +++ b/.history/middleware/validation_20251019163300.js @@ -0,0 +1,310 @@ +/** + * Validation middleware for various data types + */ + +const { body, validationResult } = require('express-validator'); + +/** + * Validation error handler + */ +const handleValidationErrors = (req, res, next) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + // For API requests + if (req.xhr || req.headers.accept?.includes('application/json')) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: errors.array() + }); + } + + // For web requests + const errorMessages = errors.array().map(error => error.msg); + req.flash('error', errorMessages); + return res.redirect('back'); + } + + next(); +}; + +/** + * Contact form validation + */ +const validateContactForm = [ + body('name') + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.') + .matches(/^[a-zA-Z가-힣\s]+$/) + .withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'), + + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + body('company') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('회사명은 100자 이하여야 합니다.'), + + body('service') + .isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'digital-marketing', 'consulting', 'other']) + .withMessage('유효한 서비스를 선택해주세요.'), + + body('budget') + .optional() + .isIn(['under-500', '500-1000', '1000-3000', '3000-5000', '5000-10000', 'over-10000', 'discuss']) + .withMessage('유효한 예산 범위를 선택해주세요.'), + + body('message') + .trim() + .isLength({ min: 10, max: 2000 }) + .withMessage('메시지는 10-2000자 사이여야 합니다.'), + + handleValidationErrors +]; + +/** + * User registration validation + */ +const validateRegistration = [ + body('name') + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.') + .matches(/^[a-zA-Z가-힣\s]+$/) + .withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'), + + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('password') + .isLength({ min: 8 }) + .withMessage('비밀번호는 최소 8자 이상이어야 합니다.') + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) + .withMessage('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'), + + body('confirmPassword') + .custom((value, { req }) => { + if (value !== req.body.password) { + throw new Error('비밀번호 확인이 일치하지 않습니다.'); + } + return true; + }), + + handleValidationErrors +]; + +/** + * User login validation + */ +const validateLogin = [ + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('password') + .isLength({ min: 1 }) + .withMessage('비밀번호를 입력해주세요.'), + + handleValidationErrors +]; + +/** + * Portfolio validation + */ +const validatePortfolio = [ + body('title') + .trim() + .isLength({ min: 2, max: 100 }) + .withMessage('제목은 2-100자 사이여야 합니다.'), + + body('description') + .trim() + .isLength({ min: 10, max: 5000 }) + .withMessage('설명은 10-5000자 사이여야 합니다.'), + + body('shortDescription') + .optional() + .trim() + .isLength({ max: 200 }) + .withMessage('간단한 설명은 200자 이하여야 합니다.'), + + body('category') + .isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'marketing']) + .withMessage('유효한 카테고리를 선택해주세요.'), + + body('technologies') + .optional() + .isArray() + .withMessage('기술 스택은 배열이어야 합니다.'), + + body('clientName') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('클라이언트 이름은 100자 이하여야 합니다.'), + + body('projectUrl') + .optional() + .isURL() + .withMessage('유효한 URL을 입력해주세요.'), + + body('status') + .optional() + .isIn(['planning', 'in-progress', 'completed', 'on-hold']) + .withMessage('유효한 상태를 선택해주세요.'), + + handleValidationErrors +]; + +/** + * Service validation + */ +const validateService = [ + body('name') + .trim() + .isLength({ min: 2, max: 100 }) + .withMessage('서비스명은 2-100자 사이여야 합니다.'), + + body('description') + .trim() + .isLength({ min: 10, max: 5000 }) + .withMessage('설명은 10-5000자 사이여야 합니다.'), + + body('shortDescription') + .optional() + .trim() + .isLength({ max: 200 }) + .withMessage('간단한 설명은 200자 이하여야 합니다.'), + + body('category') + .isIn(['development', 'design', 'marketing', 'consulting']) + .withMessage('유효한 카테고리를 선택해주세요.'), + + body('pricing.basePrice') + .optional() + .isNumeric() + .withMessage('기본 가격은 숫자여야 합니다.'), + + body('pricing.priceType') + .optional() + .isIn(['project', 'hourly', 'monthly']) + .withMessage('유효한 가격 유형을 선택해주세요.'), + + handleValidationErrors +]; + +/** + * Calculator validation + */ +const validateCalculator = [ + body('service') + .isMongoId() + .withMessage('유효한 서비스를 선택해주세요.'), + + body('projectType') + .optional() + .isIn(['simple', 'medium', 'complex', 'enterprise']) + .withMessage('유효한 프로젝트 유형을 선택해주세요.'), + + body('timeline') + .optional() + .isIn(['urgent', 'normal', 'flexible']) + .withMessage('유효한 타임라인을 선택해주세요.'), + + body('features') + .optional() + .isArray() + .withMessage('기능은 배열이어야 합니다.'), + + body('contactInfo.name') + .optional() + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.'), + + body('contactInfo.email') + .optional() + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.'), + + body('contactInfo.phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + handleValidationErrors +]; + +/** + * Settings validation + */ +const validateSettings = [ + body('siteName') + .optional() + .trim() + .isLength({ min: 1, max: 100 }) + .withMessage('사이트명은 1-100자 사이여야 합니다.'), + + body('siteDescription') + .optional() + .trim() + .isLength({ max: 500 }) + .withMessage('사이트 설명은 500자 이하여야 합니다.'), + + body('contact.email') + .optional() + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.'), + + body('contact.phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + body('social.facebook') + .optional() + .isURL() + .withMessage('유효한 Facebook URL을 입력해주세요.'), + + body('social.twitter') + .optional() + .isURL() + .withMessage('유효한 Twitter URL을 입력해주세요.'), + + body('social.linkedin') + .optional() + .isURL() + .withMessage('유효한 LinkedIn URL을 입력해주세요.'), + + body('social.instagram') + .optional() + .isURL() + .withMessage('유효한 Instagram URL을 입력해주세요.'), + + handleValidationErrors +]; + +module.exports = { + handleValidationErrors, + validateContactForm, + validateRegistration, + validateLogin, + validatePortfolio, + validateService, + validateCalculator, + validateSettings +}; \ No newline at end of file diff --git a/.history/middleware/validation_20251019163807.js b/.history/middleware/validation_20251019163807.js new file mode 100644 index 0000000..b19ca74 --- /dev/null +++ b/.history/middleware/validation_20251019163807.js @@ -0,0 +1,310 @@ +/** + * Validation middleware for various data types + */ + +const { body, validationResult } = require('express-validator'); + +/** + * Validation error handler + */ +const handleValidationErrors = (req, res, next) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + // For API requests + if (req.xhr || req.headers.accept?.includes('application/json')) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: errors.array() + }); + } + + // For web requests + const errorMessages = errors.array().map(error => error.msg); + req.flash('error', errorMessages); + return res.redirect('back'); + } + + next(); +}; + +/** + * Contact form validation + */ +const validateContactForm = [ + body('name') + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.') + .matches(/^[a-zA-Z가-힣\s]+$/) + .withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'), + + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + body('company') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('회사명은 100자 이하여야 합니다.'), + + body('service') + .isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'digital-marketing', 'consulting', 'other']) + .withMessage('유효한 서비스를 선택해주세요.'), + + body('budget') + .optional() + .isIn(['under-500', '500-1000', '1000-3000', '3000-5000', '5000-10000', 'over-10000', 'discuss']) + .withMessage('유효한 예산 범위를 선택해주세요.'), + + body('message') + .trim() + .isLength({ min: 10, max: 2000 }) + .withMessage('메시지는 10-2000자 사이여야 합니다.'), + + handleValidationErrors +]; + +/** + * User registration validation + */ +const validateRegistration = [ + body('name') + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.') + .matches(/^[a-zA-Z가-힣\s]+$/) + .withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'), + + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('password') + .isLength({ min: 8 }) + .withMessage('비밀번호는 최소 8자 이상이어야 합니다.') + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) + .withMessage('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'), + + body('confirmPassword') + .custom((value, { req }) => { + if (value !== req.body.password) { + throw new Error('비밀번호 확인이 일치하지 않습니다.'); + } + return true; + }), + + handleValidationErrors +]; + +/** + * User login validation + */ +const validateLogin = [ + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('password') + .isLength({ min: 1 }) + .withMessage('비밀번호를 입력해주세요.'), + + handleValidationErrors +]; + +/** + * Portfolio validation + */ +const validatePortfolio = [ + body('title') + .trim() + .isLength({ min: 2, max: 100 }) + .withMessage('제목은 2-100자 사이여야 합니다.'), + + body('description') + .trim() + .isLength({ min: 10, max: 5000 }) + .withMessage('설명은 10-5000자 사이여야 합니다.'), + + body('shortDescription') + .optional() + .trim() + .isLength({ max: 200 }) + .withMessage('간단한 설명은 200자 이하여야 합니다.'), + + body('category') + .isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'marketing']) + .withMessage('유효한 카테고리를 선택해주세요.'), + + body('technologies') + .optional() + .isArray() + .withMessage('기술 스택은 배열이어야 합니다.'), + + body('clientName') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('클라이언트 이름은 100자 이하여야 합니다.'), + + body('projectUrl') + .optional() + .isURL() + .withMessage('유효한 URL을 입력해주세요.'), + + body('status') + .optional() + .isIn(['planning', 'in-progress', 'completed', 'on-hold']) + .withMessage('유효한 상태를 선택해주세요.'), + + handleValidationErrors +]; + +/** + * Service validation + */ +const validateService = [ + body('name') + .trim() + .isLength({ min: 2, max: 100 }) + .withMessage('서비스명은 2-100자 사이여야 합니다.'), + + body('description') + .trim() + .isLength({ min: 10, max: 5000 }) + .withMessage('설명은 10-5000자 사이여야 합니다.'), + + body('shortDescription') + .optional() + .trim() + .isLength({ max: 200 }) + .withMessage('간단한 설명은 200자 이하여야 합니다.'), + + body('category') + .isIn(['development', 'design', 'marketing', 'consulting']) + .withMessage('유효한 카테고리를 선택해주세요.'), + + body('pricing.basePrice') + .optional() + .isNumeric() + .withMessage('기본 가격은 숫자여야 합니다.'), + + body('pricing.priceType') + .optional() + .isIn(['project', 'hourly', 'monthly']) + .withMessage('유효한 가격 유형을 선택해주세요.'), + + handleValidationErrors +]; + +/** + * Calculator validation + */ +const validateCalculator = [ + body('service') + .isMongoId() + .withMessage('유효한 서비스를 선택해주세요.'), + + body('projectType') + .optional() + .isIn(['simple', 'medium', 'complex', 'enterprise']) + .withMessage('유효한 프로젝트 유형을 선택해주세요.'), + + body('timeline') + .optional() + .isIn(['urgent', 'normal', 'flexible']) + .withMessage('유효한 타임라인을 선택해주세요.'), + + body('features') + .optional() + .isArray() + .withMessage('기능은 배열이어야 합니다.'), + + body('contactInfo.name') + .optional() + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.'), + + body('contactInfo.email') + .optional() + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.'), + + body('contactInfo.phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + handleValidationErrors +]; + +/** + * Settings validation + */ +const validateSettings = [ + body('siteName') + .optional() + .trim() + .isLength({ min: 1, max: 100 }) + .withMessage('사이트명은 1-100자 사이여야 합니다.'), + + body('siteDescription') + .optional() + .trim() + .isLength({ max: 500 }) + .withMessage('사이트 설명은 500자 이하여야 합니다.'), + + body('contact.email') + .optional() + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.'), + + body('contact.phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + body('social.facebook') + .optional() + .isURL() + .withMessage('유효한 Facebook URL을 입력해주세요.'), + + body('social.twitter') + .optional() + .isURL() + .withMessage('유효한 Twitter URL을 입력해주세요.'), + + body('social.linkedin') + .optional() + .isURL() + .withMessage('유효한 LinkedIn URL을 입력해주세요.'), + + body('social.instagram') + .optional() + .isURL() + .withMessage('유효한 Instagram URL을 입력해주세요.'), + + handleValidationErrors +]; + +module.exports = { + handleValidationErrors, + validateContactForm, + validateRegistration, + validateLogin, + validatePortfolio, + validateService, + validateCalculator, + validateSettings +}; \ No newline at end of file diff --git a/.history/models/Contact_20251019160612.js b/.history/models/Contact_20251019160612.js new file mode 100644 index 0000000..c7a79ff --- /dev/null +++ b/.history/models/Contact_20251019160612.js @@ -0,0 +1,80 @@ +const mongoose = require('mongoose'); + +const contactSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + email: { + type: String, + required: true, + lowercase: true, + trim: true + }, + phone: { + type: String, + trim: true + }, + company: { + type: String, + trim: true + }, + subject: { + type: String, + required: true, + trim: true + }, + message: { + type: String, + required: true + }, + serviceInterest: { + type: String, + enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'consulting', 'other'] + }, + budget: { + type: String, + enum: ['under-1m', '1m-5m', '5m-10m', '10m-20m', '20m-50m', 'over-50m'] + }, + timeline: { + type: String, + enum: ['asap', '1-month', '1-3-months', '3-6-months', 'flexible'] + }, + status: { + type: String, + enum: ['new', 'in-progress', 'replied', 'closed'], + default: 'new' + }, + priority: { + type: String, + enum: ['low', 'medium', 'high', 'urgent'], + default: 'medium' + }, + source: { + type: String, + enum: ['website', 'telegram', 'email', 'phone', 'referral'], + default: 'website' + }, + isRead: { + type: Boolean, + default: false + }, + adminNotes: { + type: String + }, + ipAddress: { + type: String + }, + userAgent: { + type: String + } +}, { + timestamps: true +}); + +contactSchema.index({ status: 1, createdAt: -1 }); +contactSchema.index({ isRead: 1, createdAt: -1 }); +contactSchema.index({ email: 1 }); + +module.exports = mongoose.model('Contact', contactSchema); \ No newline at end of file diff --git a/.history/models/Contact_20251019162544.js b/.history/models/Contact_20251019162544.js new file mode 100644 index 0000000..c7a79ff --- /dev/null +++ b/.history/models/Contact_20251019162544.js @@ -0,0 +1,80 @@ +const mongoose = require('mongoose'); + +const contactSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + email: { + type: String, + required: true, + lowercase: true, + trim: true + }, + phone: { + type: String, + trim: true + }, + company: { + type: String, + trim: true + }, + subject: { + type: String, + required: true, + trim: true + }, + message: { + type: String, + required: true + }, + serviceInterest: { + type: String, + enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'consulting', 'other'] + }, + budget: { + type: String, + enum: ['under-1m', '1m-5m', '5m-10m', '10m-20m', '20m-50m', 'over-50m'] + }, + timeline: { + type: String, + enum: ['asap', '1-month', '1-3-months', '3-6-months', 'flexible'] + }, + status: { + type: String, + enum: ['new', 'in-progress', 'replied', 'closed'], + default: 'new' + }, + priority: { + type: String, + enum: ['low', 'medium', 'high', 'urgent'], + default: 'medium' + }, + source: { + type: String, + enum: ['website', 'telegram', 'email', 'phone', 'referral'], + default: 'website' + }, + isRead: { + type: Boolean, + default: false + }, + adminNotes: { + type: String + }, + ipAddress: { + type: String + }, + userAgent: { + type: String + } +}, { + timestamps: true +}); + +contactSchema.index({ status: 1, createdAt: -1 }); +contactSchema.index({ isRead: 1, createdAt: -1 }); +contactSchema.index({ email: 1 }); + +module.exports = mongoose.model('Contact', contactSchema); \ No newline at end of file diff --git a/.history/models/Portfolio_20251019160550.js b/.history/models/Portfolio_20251019160550.js new file mode 100644 index 0000000..d86985a --- /dev/null +++ b/.history/models/Portfolio_20251019160550.js @@ -0,0 +1,107 @@ +const mongoose = require('mongoose'); + +const portfolioSchema = new mongoose.Schema({ + title: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: true + }, + shortDescription: { + type: String, + required: true, + maxlength: 200 + }, + category: { + type: String, + required: true, + enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'e-commerce', 'other'] + }, + technologies: [{ + type: String, + trim: true + }], + images: [{ + url: { + type: String, + required: true + }, + alt: { + type: String, + default: '' + }, + isPrimary: { + type: Boolean, + default: false + } + }], + clientName: { + type: String, + trim: true + }, + projectUrl: { + type: String, + trim: true + }, + githubUrl: { + type: String, + trim: true + }, + status: { + type: String, + enum: ['completed', 'in-progress', 'planning'], + default: 'completed' + }, + featured: { + type: Boolean, + default: false + }, + publishedAt: { + type: Date, + default: Date.now + }, + completedAt: { + type: Date + }, + isPublished: { + type: Boolean, + default: true + }, + viewCount: { + type: Number, + default: 0 + }, + likes: { + type: Number, + default: 0 + }, + order: { + type: Number, + default: 0 + }, + seo: { + metaTitle: String, + metaDescription: String, + keywords: [String] + } +}, { + timestamps: true +}); + +// Index for search and sorting +portfolioSchema.index({ title: 'text', description: 'text', technologies: 'text' }); +portfolioSchema.index({ category: 1, publishedAt: -1 }); +portfolioSchema.index({ featured: -1, publishedAt: -1 }); + +// Virtual for primary image +portfolioSchema.virtual('primaryImage').get(function() { + const primary = this.images.find(img => img.isPrimary); + return primary || (this.images.length > 0 ? this.images[0] : null); +}); + +portfolioSchema.set('toJSON', { virtuals: true }); + +module.exports = mongoose.model('Portfolio', portfolioSchema); \ No newline at end of file diff --git a/.history/models/Portfolio_20251019162544.js b/.history/models/Portfolio_20251019162544.js new file mode 100644 index 0000000..d86985a --- /dev/null +++ b/.history/models/Portfolio_20251019162544.js @@ -0,0 +1,107 @@ +const mongoose = require('mongoose'); + +const portfolioSchema = new mongoose.Schema({ + title: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: true + }, + shortDescription: { + type: String, + required: true, + maxlength: 200 + }, + category: { + type: String, + required: true, + enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'e-commerce', 'other'] + }, + technologies: [{ + type: String, + trim: true + }], + images: [{ + url: { + type: String, + required: true + }, + alt: { + type: String, + default: '' + }, + isPrimary: { + type: Boolean, + default: false + } + }], + clientName: { + type: String, + trim: true + }, + projectUrl: { + type: String, + trim: true + }, + githubUrl: { + type: String, + trim: true + }, + status: { + type: String, + enum: ['completed', 'in-progress', 'planning'], + default: 'completed' + }, + featured: { + type: Boolean, + default: false + }, + publishedAt: { + type: Date, + default: Date.now + }, + completedAt: { + type: Date + }, + isPublished: { + type: Boolean, + default: true + }, + viewCount: { + type: Number, + default: 0 + }, + likes: { + type: Number, + default: 0 + }, + order: { + type: Number, + default: 0 + }, + seo: { + metaTitle: String, + metaDescription: String, + keywords: [String] + } +}, { + timestamps: true +}); + +// Index for search and sorting +portfolioSchema.index({ title: 'text', description: 'text', technologies: 'text' }); +portfolioSchema.index({ category: 1, publishedAt: -1 }); +portfolioSchema.index({ featured: -1, publishedAt: -1 }); + +// Virtual for primary image +portfolioSchema.virtual('primaryImage').get(function() { + const primary = this.images.find(img => img.isPrimary); + return primary || (this.images.length > 0 ? this.images[0] : null); +}); + +portfolioSchema.set('toJSON', { virtuals: true }); + +module.exports = mongoose.model('Portfolio', portfolioSchema); \ No newline at end of file diff --git a/.history/models/Service_20251019160601.js b/.history/models/Service_20251019160601.js new file mode 100644 index 0000000..d8f37e6 --- /dev/null +++ b/.history/models/Service_20251019160601.js @@ -0,0 +1,102 @@ +const mongoose = require('mongoose'); + +const serviceSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: true + }, + shortDescription: { + type: String, + required: true, + maxlength: 150 + }, + icon: { + type: String, + required: true + }, + category: { + type: String, + required: true, + enum: ['development', 'design', 'consulting', 'marketing', 'maintenance'] + }, + features: [{ + name: String, + description: String, + included: { + type: Boolean, + default: true + } + }], + pricing: { + basePrice: { + type: Number, + required: true, + min: 0 + }, + currency: { + type: String, + default: 'KRW' + }, + priceType: { + type: String, + enum: ['fixed', 'hourly', 'project'], + default: 'project' + }, + priceRange: { + min: Number, + max: Number + } + }, + estimatedTime: { + min: { + type: Number, + required: true + }, + max: { + type: Number, + required: true + }, + unit: { + type: String, + enum: ['hours', 'days', 'weeks', 'months'], + default: 'days' + } + }, + isActive: { + type: Boolean, + default: true + }, + featured: { + type: Boolean, + default: false + }, + order: { + type: Number, + default: 0 + }, + portfolio: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'Portfolio' + }], + tags: [{ + type: String, + trim: true + }], + seo: { + metaTitle: String, + metaDescription: String, + keywords: [String] + } +}, { + timestamps: true +}); + +serviceSchema.index({ name: 'text', description: 'text', tags: 'text' }); +serviceSchema.index({ category: 1, featured: -1, order: 1 }); + +module.exports = mongoose.model('Service', serviceSchema); \ No newline at end of file diff --git a/.history/models/Service_20251019162544.js b/.history/models/Service_20251019162544.js new file mode 100644 index 0000000..d8f37e6 --- /dev/null +++ b/.history/models/Service_20251019162544.js @@ -0,0 +1,102 @@ +const mongoose = require('mongoose'); + +const serviceSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: true + }, + shortDescription: { + type: String, + required: true, + maxlength: 150 + }, + icon: { + type: String, + required: true + }, + category: { + type: String, + required: true, + enum: ['development', 'design', 'consulting', 'marketing', 'maintenance'] + }, + features: [{ + name: String, + description: String, + included: { + type: Boolean, + default: true + } + }], + pricing: { + basePrice: { + type: Number, + required: true, + min: 0 + }, + currency: { + type: String, + default: 'KRW' + }, + priceType: { + type: String, + enum: ['fixed', 'hourly', 'project'], + default: 'project' + }, + priceRange: { + min: Number, + max: Number + } + }, + estimatedTime: { + min: { + type: Number, + required: true + }, + max: { + type: Number, + required: true + }, + unit: { + type: String, + enum: ['hours', 'days', 'weeks', 'months'], + default: 'days' + } + }, + isActive: { + type: Boolean, + default: true + }, + featured: { + type: Boolean, + default: false + }, + order: { + type: Number, + default: 0 + }, + portfolio: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'Portfolio' + }], + tags: [{ + type: String, + trim: true + }], + seo: { + metaTitle: String, + metaDescription: String, + keywords: [String] + } +}, { + timestamps: true +}); + +serviceSchema.index({ name: 'text', description: 'text', tags: 'text' }); +serviceSchema.index({ category: 1, featured: -1, order: 1 }); + +module.exports = mongoose.model('Service', serviceSchema); \ No newline at end of file diff --git a/.history/models/SiteSettings_20251019160626.js b/.history/models/SiteSettings_20251019160626.js new file mode 100644 index 0000000..0156ce7 --- /dev/null +++ b/.history/models/SiteSettings_20251019160626.js @@ -0,0 +1,116 @@ +const mongoose = require('mongoose'); + +const siteSettingsSchema = new mongoose.Schema({ + siteName: { + type: String, + default: 'SmartSolTech' + }, + siteDescription: { + type: String, + default: 'Innovative technology solutions for modern businesses' + }, + logo: { + type: String, + default: '/images/logo.png' + }, + favicon: { + type: String, + default: '/images/favicon.ico' + }, + contact: { + email: { + type: String, + default: 'info@smartsoltech.kr' + }, + phone: { + type: String, + default: '+82-10-0000-0000' + }, + address: { + type: String, + default: 'Seoul, South Korea' + } + }, + social: { + facebook: String, + twitter: String, + linkedin: String, + instagram: String, + github: String, + telegram: String + }, + telegram: { + botToken: String, + chatId: String, + isEnabled: { + type: Boolean, + default: false + } + }, + seo: { + metaTitle: { + type: String, + default: 'SmartSolTech - Technology Solutions' + }, + metaDescription: { + type: String, + default: 'Professional web development, mobile apps, and digital solutions in Korea' + }, + keywords: { + type: String, + default: 'web development, mobile apps, UI/UX design, Korea, technology' + }, + googleAnalytics: String, + googleTagManager: String + }, + hero: { + title: { + type: String, + default: 'Smart Technology Solutions' + }, + subtitle: { + type: String, + default: 'We create innovative digital experiences that drive business growth' + }, + backgroundImage: { + type: String, + default: '/images/hero-bg.jpg' + }, + ctaText: { + type: String, + default: 'Get Started' + }, + ctaLink: { + type: String, + default: '#contact' + } + }, + about: { + title: { + type: String, + default: 'About SmartSolTech' + }, + description: { + type: String, + default: 'We are a team of passionate developers and designers creating cutting-edge technology solutions.' + }, + image: { + type: String, + default: '/images/about.jpg' + } + }, + maintenance: { + isEnabled: { + type: Boolean, + default: false + }, + message: { + type: String, + default: 'We are currently performing maintenance. Please check back soon.' + } + } +}, { + timestamps: true +}); + +module.exports = mongoose.model('SiteSettings', siteSettingsSchema); \ No newline at end of file diff --git a/.history/models/SiteSettings_20251019162544.js b/.history/models/SiteSettings_20251019162544.js new file mode 100644 index 0000000..0156ce7 --- /dev/null +++ b/.history/models/SiteSettings_20251019162544.js @@ -0,0 +1,116 @@ +const mongoose = require('mongoose'); + +const siteSettingsSchema = new mongoose.Schema({ + siteName: { + type: String, + default: 'SmartSolTech' + }, + siteDescription: { + type: String, + default: 'Innovative technology solutions for modern businesses' + }, + logo: { + type: String, + default: '/images/logo.png' + }, + favicon: { + type: String, + default: '/images/favicon.ico' + }, + contact: { + email: { + type: String, + default: 'info@smartsoltech.kr' + }, + phone: { + type: String, + default: '+82-10-0000-0000' + }, + address: { + type: String, + default: 'Seoul, South Korea' + } + }, + social: { + facebook: String, + twitter: String, + linkedin: String, + instagram: String, + github: String, + telegram: String + }, + telegram: { + botToken: String, + chatId: String, + isEnabled: { + type: Boolean, + default: false + } + }, + seo: { + metaTitle: { + type: String, + default: 'SmartSolTech - Technology Solutions' + }, + metaDescription: { + type: String, + default: 'Professional web development, mobile apps, and digital solutions in Korea' + }, + keywords: { + type: String, + default: 'web development, mobile apps, UI/UX design, Korea, technology' + }, + googleAnalytics: String, + googleTagManager: String + }, + hero: { + title: { + type: String, + default: 'Smart Technology Solutions' + }, + subtitle: { + type: String, + default: 'We create innovative digital experiences that drive business growth' + }, + backgroundImage: { + type: String, + default: '/images/hero-bg.jpg' + }, + ctaText: { + type: String, + default: 'Get Started' + }, + ctaLink: { + type: String, + default: '#contact' + } + }, + about: { + title: { + type: String, + default: 'About SmartSolTech' + }, + description: { + type: String, + default: 'We are a team of passionate developers and designers creating cutting-edge technology solutions.' + }, + image: { + type: String, + default: '/images/about.jpg' + } + }, + maintenance: { + isEnabled: { + type: Boolean, + default: false + }, + message: { + type: String, + default: 'We are currently performing maintenance. Please check back soon.' + } + } +}, { + timestamps: true +}); + +module.exports = mongoose.model('SiteSettings', siteSettingsSchema); \ No newline at end of file diff --git a/.history/models/User_20251019160538.js b/.history/models/User_20251019160538.js new file mode 100644 index 0000000..27dc646 --- /dev/null +++ b/.history/models/User_20251019160538.js @@ -0,0 +1,75 @@ +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); + +const userSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true + }, + password: { + type: String, + required: true, + minlength: 6 + }, + name: { + type: String, + required: true, + trim: true + }, + role: { + type: String, + enum: ['admin', 'moderator'], + default: 'admin' + }, + avatar: { + type: String, + default: null + }, + isActive: { + type: Boolean, + default: true + }, + lastLogin: { + type: Date, + default: null + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +// Hash password before saving +userSchema.pre('save', async function(next) { + if (!this.isModified('password')) return next(); + + try { + const salt = await bcrypt.genSalt(12); + this.password = await bcrypt.hash(this.password, salt); + next(); + } catch (error) { + next(error); + } +}); + +// Compare password method +userSchema.methods.comparePassword = async function(candidatePassword) { + return bcrypt.compare(candidatePassword, this.password); +}; + +// Update last login +userSchema.methods.updateLastLogin = function() { + this.lastLogin = new Date(); + return this.save(); +}; + +module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/.history/models/User_20251019162544.js b/.history/models/User_20251019162544.js new file mode 100644 index 0000000..27dc646 --- /dev/null +++ b/.history/models/User_20251019162544.js @@ -0,0 +1,75 @@ +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); + +const userSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true + }, + password: { + type: String, + required: true, + minlength: 6 + }, + name: { + type: String, + required: true, + trim: true + }, + role: { + type: String, + enum: ['admin', 'moderator'], + default: 'admin' + }, + avatar: { + type: String, + default: null + }, + isActive: { + type: Boolean, + default: true + }, + lastLogin: { + type: Date, + default: null + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +// Hash password before saving +userSchema.pre('save', async function(next) { + if (!this.isModified('password')) return next(); + + try { + const salt = await bcrypt.genSalt(12); + this.password = await bcrypt.hash(this.password, salt); + next(); + } catch (error) { + next(error); + } +}); + +// Compare password method +userSchema.methods.comparePassword = async function(candidatePassword) { + return bcrypt.compare(candidatePassword, this.password); +}; + +// Update last login +userSchema.methods.updateLastLogin = function() { + this.lastLogin = new Date(); + return this.save(); +}; + +module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/.history/package_20251019160453.json b/.history/package_20251019160453.json new file mode 100644 index 0000000..5a9baac --- /dev/null +++ b/.history/package_20251019160453.json @@ -0,0 +1,48 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "build": "webpack --mode production", + "build:dev": "webpack --mode development", + "watch": "webpack --mode development --watch" + }, + "keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"], + "author": "SmartSolTech", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^8.0.3", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "express-session": "^1.17.3", + "connect-mongo": "^5.1.0", + "multer": "^1.4.5-lts.1", + "sharp": "^0.33.0", + "node-telegram-bot-api": "^0.64.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "compression": "^1.7.4", + "morgan": "^1.10.0", + "nodemailer": "^6.9.7", + "express-validator": "^7.0.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "terser-webpack-plugin": "^5.3.9", + "workbox-webpack-plugin": "^7.0.0" + } +} \ No newline at end of file diff --git a/.history/package_20251019162544.json b/.history/package_20251019162544.json new file mode 100644 index 0000000..5a9baac --- /dev/null +++ b/.history/package_20251019162544.json @@ -0,0 +1,48 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "build": "webpack --mode production", + "build:dev": "webpack --mode development", + "watch": "webpack --mode development --watch" + }, + "keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"], + "author": "SmartSolTech", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^8.0.3", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "express-session": "^1.17.3", + "connect-mongo": "^5.1.0", + "multer": "^1.4.5-lts.1", + "sharp": "^0.33.0", + "node-telegram-bot-api": "^0.64.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "compression": "^1.7.4", + "morgan": "^1.10.0", + "nodemailer": "^6.9.7", + "express-validator": "^7.0.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "terser-webpack-plugin": "^5.3.9", + "workbox-webpack-plugin": "^7.0.0" + } +} \ No newline at end of file diff --git a/.history/package_20251019162621.json b/.history/package_20251019162621.json new file mode 100644 index 0000000..83a22c9 --- /dev/null +++ b/.history/package_20251019162621.json @@ -0,0 +1,48 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node scripts/dev.js", + "build": "node scripts/build.js", + "init-db": "node scripts/init-db.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"], + "author": "SmartSolTech", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^8.0.3", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "express-session": "^1.17.3", + "connect-mongo": "^5.1.0", + "multer": "^1.4.5-lts.1", + "sharp": "^0.33.0", + "node-telegram-bot-api": "^0.64.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "compression": "^1.7.4", + "morgan": "^1.10.0", + "nodemailer": "^6.9.7", + "express-validator": "^7.0.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "terser-webpack-plugin": "^5.3.9", + "workbox-webpack-plugin": "^7.0.0" + } +} \ No newline at end of file diff --git a/.history/package_20251019163806.json b/.history/package_20251019163806.json new file mode 100644 index 0000000..83a22c9 --- /dev/null +++ b/.history/package_20251019163806.json @@ -0,0 +1,48 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node scripts/dev.js", + "build": "node scripts/build.js", + "init-db": "node scripts/init-db.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"], + "author": "SmartSolTech", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^8.0.3", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "express-session": "^1.17.3", + "connect-mongo": "^5.1.0", + "multer": "^1.4.5-lts.1", + "sharp": "^0.33.0", + "node-telegram-bot-api": "^0.64.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "compression": "^1.7.4", + "morgan": "^1.10.0", + "nodemailer": "^6.9.7", + "express-validator": "^7.0.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "terser-webpack-plugin": "^5.3.9", + "workbox-webpack-plugin": "^7.0.0" + } +} \ No newline at end of file diff --git a/.history/package_20251019163809.json b/.history/package_20251019163809.json new file mode 100644 index 0000000..1ef301a --- /dev/null +++ b/.history/package_20251019163809.json @@ -0,0 +1,49 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "demo": "node server-demo.js", + "dev": "node scripts/dev.js", + "build": "node scripts/build.js", + "init-db": "node scripts/init-db.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"], + "author": "SmartSolTech", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^8.0.3", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "express-session": "^1.17.3", + "connect-mongo": "^5.1.0", + "multer": "^1.4.5-lts.1", + "sharp": "^0.33.0", + "node-telegram-bot-api": "^0.64.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "compression": "^1.7.4", + "morgan": "^1.10.0", + "nodemailer": "^6.9.7", + "express-validator": "^7.0.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "terser-webpack-plugin": "^5.3.9", + "workbox-webpack-plugin": "^7.0.0" + } +} \ No newline at end of file diff --git a/.history/package_20251019163858.json b/.history/package_20251019163858.json new file mode 100644 index 0000000..1ef301a --- /dev/null +++ b/.history/package_20251019163858.json @@ -0,0 +1,49 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "demo": "node server-demo.js", + "dev": "node scripts/dev.js", + "build": "node scripts/build.js", + "init-db": "node scripts/init-db.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"], + "author": "SmartSolTech", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^8.0.3", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "express-session": "^1.17.3", + "connect-mongo": "^5.1.0", + "multer": "^1.4.5-lts.1", + "sharp": "^0.33.0", + "node-telegram-bot-api": "^0.64.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "compression": "^1.7.4", + "morgan": "^1.10.0", + "nodemailer": "^6.9.7", + "express-validator": "^7.0.1", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "terser-webpack-plugin": "^5.3.9", + "workbox-webpack-plugin": "^7.0.0" + } +} \ No newline at end of file diff --git a/.history/public/css/custom_20251019164526.css b/.history/public/css/custom_20251019164526.css new file mode 100644 index 0000000..cb2943d --- /dev/null +++ b/.history/public/css/custom_20251019164526.css @@ -0,0 +1,88 @@ +/* SmartSolTech - Main Styles */ + +/* Basic reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Additional styles for demo */ +.hero-section { + padding: 6rem 0 4rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + text-align: center; +} + +.section-padding { + padding: 4rem 0; +} + +.card-hover { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card-hover:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); +} + +.btn-gradient { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + transition: all 0.3s ease; +} + +.btn-gradient:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); +} + +/* Loading spinner */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-up { + animation: fadeInUp 0.6s ease-out; +} + +/* Responsive utilities */ +@media (max-width: 768px) { + .hero-section { + padding: 4rem 0 3rem; + } + + .section-padding { + padding: 2rem 0; + } +} \ No newline at end of file diff --git a/.history/public/css/custom_20251019165556.css b/.history/public/css/custom_20251019165556.css new file mode 100644 index 0000000..cb2943d --- /dev/null +++ b/.history/public/css/custom_20251019165556.css @@ -0,0 +1,88 @@ +/* SmartSolTech - Main Styles */ + +/* Basic reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Additional styles for demo */ +.hero-section { + padding: 6rem 0 4rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + text-align: center; +} + +.section-padding { + padding: 4rem 0; +} + +.card-hover { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card-hover:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); +} + +.btn-gradient { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + transition: all 0.3s ease; +} + +.btn-gradient:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); +} + +/* Loading spinner */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-up { + animation: fadeInUp 0.6s ease-out; +} + +/* Responsive utilities */ +@media (max-width: 768px) { + .hero-section { + padding: 4rem 0 3rem; + } + + .section-padding { + padding: 2rem 0; + } +} \ No newline at end of file diff --git a/.history/public/css/dark-theme_20251019173803.css b/.history/public/css/dark-theme_20251019173803.css new file mode 100644 index 0000000..2d0cca4 --- /dev/null +++ b/.history/public/css/dark-theme_20251019173803.css @@ -0,0 +1,315 @@ +/* Dark Theme Support for SmartSolTech */ + +/* Base Dark Theme */ +html.dark { + color-scheme: dark; +} + +.dark body { + background-color: #111827; + color: #f9fafb; +} + +/* Navigation Dark Theme */ +.dark nav { + background-color: #1f2937; + border-color: #374151; +} + +.dark .nav-link { + color: #d1d5db; +} + +.dark .nav-link:hover { + color: #60a5fa; +} + +.dark .nav-link.active { + color: #60a5fa; +} + +/* Sections Dark Theme */ +.dark section { + background-color: #111827; + color: #f9fafb; +} + +.dark .bg-white { + background-color: #1f2937 !important; +} + +.dark .bg-gray-50 { + background-color: #111827 !important; +} + +.dark .bg-gray-100 { + background-color: #374151 !important; +} + +.dark .bg-gray-900 { + background-color: #030712 !important; +} + +/* Text Colors Dark Theme */ +.dark .text-gray-900 { + color: #f9fafb !important; +} + +.dark .text-gray-800 { + color: #e5e7eb !important; +} + +.dark .text-gray-700 { + color: #d1d5db !important; +} + +.dark .text-gray-600 { + color: #9ca3af !important; +} + +.dark .text-gray-500 { + color: #6b7280 !important; +} + +.dark .text-gray-400 { + color: #9ca3af !important; +} + +.dark .text-gray-300 { + color: #d1d5db !important; +} + +/* Cards Dark Theme */ +.dark .card-hover { + background-color: #1f2937; + border-color: #374151; +} + +.dark .card-hover:hover { + background-color: #374151; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); +} + +/* Forms Dark Theme */ +.dark .form-input { + background-color: #374151; + border-color: #4b5563; + color: #f9fafb; +} + +.dark .form-input:focus { + border-color: #60a5fa; + background-color: #1f2937; +} + +.dark .form-input::placeholder { + color: #9ca3af; +} + +.dark input, +.dark textarea, +.dark select { + background-color: #374151; + border-color: #4b5563; + color: #f9fafb; +} + +.dark input:focus, +.dark textarea:focus, +.dark select:focus { + border-color: #60a5fa; + background-color: #1f2937; +} + +/* Borders Dark Theme */ +.dark .border-gray-300 { + border-color: #4b5563 !important; +} + +.dark .border-gray-200 { + border-color: #374151 !important; +} + +.dark .border-t { + border-color: #374151; +} + +/* Contact Form Dark Theme */ +.dark .contact-form { + background: linear-gradient(145deg, rgba(31,41,55,0.9), rgba(31,41,55,0.95)); + border-color: #374151; +} + +/* Service Cards Dark Theme */ +.dark .service-card { + background: linear-gradient(145deg, rgba(31,41,55,0.8), rgba(31,41,55,0.6)); + border-color: #374151; +} + +/* Team Cards Dark Theme */ +.dark .team-card { + background-color: #1f2937; + border-color: #374151; +} + +/* Portfolio Items Dark Theme */ +.dark .portfolio-item { + background-color: #1f2937; +} + +/* Hero Section Dark Theme */ +.dark .hero-section { + background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%); +} + +/* Shadows Dark Theme */ +.dark .shadow-lg { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); +} + +.dark .shadow-xl { + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); +} + +.dark .shadow-2xl { + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); +} + +/* Dropdown Dark Theme */ +.dark .dropdown-menu { + background-color: #1f2937; + border-color: #374151; +} + +.dark .dropdown-menu a { + color: #d1d5db; +} + +.dark .dropdown-menu a:hover { + background-color: #374151; + color: #f9fafb; +} + +/* Icons Dark Theme */ +.dark .text-blue-600 { + color: #60a5fa !important; +} + +.dark .text-purple-600 { + color: #a78bfa !important; +} + +.dark .text-green-600 { + color: #34d399 !important; +} + +.dark .text-yellow-600 { + color: #fbbf24 !important; +} + +/* Buttons Dark Theme */ +.dark .btn-primary { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); +} + +.dark .btn-primary:hover { + background: linear-gradient(135deg, #1d4ed8, #1e40af); +} + +/* Footer Dark Theme */ +.dark footer { + background-color: #030712; + border-color: #1f2937; +} + +.dark .footer-gradient { + background: linear-gradient(135deg, #030712 0%, #111827 100%); +} + +/* Scrollbar Dark Theme */ +.dark ::-webkit-scrollbar-track { + background: #1f2937; +} + +.dark ::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); +} + +/* Technology Stack Dark Theme */ +.dark .tech-stack { + background-color: #030712; +} + +.dark .tech-card { + background-color: #1f2937; + border-color: #374151; +} + +/* CTA Section Dark Theme */ +.dark .cta-section { + background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%); +} + +/* Testimonials Dark Theme */ +.dark .testimonial-card { + background: rgba(31, 41, 55, 0.8); + border-color: #374151; +} + +/* Alert Messages Dark Theme */ +.dark .alert-success { + background: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.3); + color: #10b981; +} + +.dark .alert-error { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +/* Mobile Menu Dark Theme */ +.dark #mobile-menu { + background-color: #1f2937; + border-color: #374151; +} + +/* Language Dropdown Dark Theme */ +.dark #mobile-language-menu { + background-color: #1f2937; + border-color: #374151; +} + +/* Smooth Theme Transition */ +* { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +/* Print styles for dark theme */ +@media print { + .dark * { + background: white !important; + color: black !important; + } +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + .dark { + --tw-bg-opacity: 1; + --tw-text-opacity: 1; + } + + .dark .border { + border-width: 2px; + } +} + +/* Reduced motion for accessibility */ +@media (prefers-reduced-motion: reduce) { + .dark * { + transition: none !important; + animation: none !important; + } +} \ No newline at end of file diff --git a/.history/public/css/dark-theme_20251019174052.css b/.history/public/css/dark-theme_20251019174052.css new file mode 100644 index 0000000..2d0cca4 --- /dev/null +++ b/.history/public/css/dark-theme_20251019174052.css @@ -0,0 +1,315 @@ +/* Dark Theme Support for SmartSolTech */ + +/* Base Dark Theme */ +html.dark { + color-scheme: dark; +} + +.dark body { + background-color: #111827; + color: #f9fafb; +} + +/* Navigation Dark Theme */ +.dark nav { + background-color: #1f2937; + border-color: #374151; +} + +.dark .nav-link { + color: #d1d5db; +} + +.dark .nav-link:hover { + color: #60a5fa; +} + +.dark .nav-link.active { + color: #60a5fa; +} + +/* Sections Dark Theme */ +.dark section { + background-color: #111827; + color: #f9fafb; +} + +.dark .bg-white { + background-color: #1f2937 !important; +} + +.dark .bg-gray-50 { + background-color: #111827 !important; +} + +.dark .bg-gray-100 { + background-color: #374151 !important; +} + +.dark .bg-gray-900 { + background-color: #030712 !important; +} + +/* Text Colors Dark Theme */ +.dark .text-gray-900 { + color: #f9fafb !important; +} + +.dark .text-gray-800 { + color: #e5e7eb !important; +} + +.dark .text-gray-700 { + color: #d1d5db !important; +} + +.dark .text-gray-600 { + color: #9ca3af !important; +} + +.dark .text-gray-500 { + color: #6b7280 !important; +} + +.dark .text-gray-400 { + color: #9ca3af !important; +} + +.dark .text-gray-300 { + color: #d1d5db !important; +} + +/* Cards Dark Theme */ +.dark .card-hover { + background-color: #1f2937; + border-color: #374151; +} + +.dark .card-hover:hover { + background-color: #374151; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); +} + +/* Forms Dark Theme */ +.dark .form-input { + background-color: #374151; + border-color: #4b5563; + color: #f9fafb; +} + +.dark .form-input:focus { + border-color: #60a5fa; + background-color: #1f2937; +} + +.dark .form-input::placeholder { + color: #9ca3af; +} + +.dark input, +.dark textarea, +.dark select { + background-color: #374151; + border-color: #4b5563; + color: #f9fafb; +} + +.dark input:focus, +.dark textarea:focus, +.dark select:focus { + border-color: #60a5fa; + background-color: #1f2937; +} + +/* Borders Dark Theme */ +.dark .border-gray-300 { + border-color: #4b5563 !important; +} + +.dark .border-gray-200 { + border-color: #374151 !important; +} + +.dark .border-t { + border-color: #374151; +} + +/* Contact Form Dark Theme */ +.dark .contact-form { + background: linear-gradient(145deg, rgba(31,41,55,0.9), rgba(31,41,55,0.95)); + border-color: #374151; +} + +/* Service Cards Dark Theme */ +.dark .service-card { + background: linear-gradient(145deg, rgba(31,41,55,0.8), rgba(31,41,55,0.6)); + border-color: #374151; +} + +/* Team Cards Dark Theme */ +.dark .team-card { + background-color: #1f2937; + border-color: #374151; +} + +/* Portfolio Items Dark Theme */ +.dark .portfolio-item { + background-color: #1f2937; +} + +/* Hero Section Dark Theme */ +.dark .hero-section { + background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%); +} + +/* Shadows Dark Theme */ +.dark .shadow-lg { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); +} + +.dark .shadow-xl { + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); +} + +.dark .shadow-2xl { + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); +} + +/* Dropdown Dark Theme */ +.dark .dropdown-menu { + background-color: #1f2937; + border-color: #374151; +} + +.dark .dropdown-menu a { + color: #d1d5db; +} + +.dark .dropdown-menu a:hover { + background-color: #374151; + color: #f9fafb; +} + +/* Icons Dark Theme */ +.dark .text-blue-600 { + color: #60a5fa !important; +} + +.dark .text-purple-600 { + color: #a78bfa !important; +} + +.dark .text-green-600 { + color: #34d399 !important; +} + +.dark .text-yellow-600 { + color: #fbbf24 !important; +} + +/* Buttons Dark Theme */ +.dark .btn-primary { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); +} + +.dark .btn-primary:hover { + background: linear-gradient(135deg, #1d4ed8, #1e40af); +} + +/* Footer Dark Theme */ +.dark footer { + background-color: #030712; + border-color: #1f2937; +} + +.dark .footer-gradient { + background: linear-gradient(135deg, #030712 0%, #111827 100%); +} + +/* Scrollbar Dark Theme */ +.dark ::-webkit-scrollbar-track { + background: #1f2937; +} + +.dark ::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); +} + +/* Technology Stack Dark Theme */ +.dark .tech-stack { + background-color: #030712; +} + +.dark .tech-card { + background-color: #1f2937; + border-color: #374151; +} + +/* CTA Section Dark Theme */ +.dark .cta-section { + background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%); +} + +/* Testimonials Dark Theme */ +.dark .testimonial-card { + background: rgba(31, 41, 55, 0.8); + border-color: #374151; +} + +/* Alert Messages Dark Theme */ +.dark .alert-success { + background: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.3); + color: #10b981; +} + +.dark .alert-error { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +/* Mobile Menu Dark Theme */ +.dark #mobile-menu { + background-color: #1f2937; + border-color: #374151; +} + +/* Language Dropdown Dark Theme */ +.dark #mobile-language-menu { + background-color: #1f2937; + border-color: #374151; +} + +/* Smooth Theme Transition */ +* { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +/* Print styles for dark theme */ +@media print { + .dark * { + background: white !important; + color: black !important; + } +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + .dark { + --tw-bg-opacity: 1; + --tw-text-opacity: 1; + } + + .dark .border { + border-width: 2px; + } +} + +/* Reduced motion for accessibility */ +@media (prefers-reduced-motion: reduce) { + .dark * { + transition: none !important; + animation: none !important; + } +} \ No newline at end of file diff --git a/.history/public/css/fixes_20251019170837.css b/.history/public/css/fixes_20251019170837.css new file mode 100644 index 0000000..432d387 --- /dev/null +++ b/.history/public/css/fixes_20251019170837.css @@ -0,0 +1,281 @@ +/* SmartSolTech - Design Fixes & Enhancements */ + +/* Glass effect improvements */ +.glass-effect { + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + /* Fallback for browsers that don't support backdrop-filter */ +} + +/* Support backdrop-filter for modern browsers */ +@supports (backdrop-filter: blur(10px)) { + .glass-effect { + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + } +} + +/* Hero section improvements */ +.hero-section { + position: relative; + overflow: hidden; + min-height: 100vh; +} + +/* Background blob animations */ +@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; +} + +/* Enhanced card hover effects */ +.card-hover { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + backface-visibility: hidden; +} + +.card-hover:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); +} + +/* Portfolio item enhancements */ +.portfolio-item { + overflow: hidden; + border-radius: 16px; + position: relative; +} + +.portfolio-image { + transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.portfolio-item:hover .portfolio-image { + transform: scale(1.1); +} + +/* Button improvements */ +.btn-primary { + background: linear-gradient(135deg, #3B82F6, #1D4ED8); + border: none; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.5s; +} + +.btn-primary:hover::before { + left: 100%; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #1D4ED8, #1E40AF); + transform: translateY(-3px); + box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4); +} + +/* Navigation improvements */ +.nav-link { + position: relative; + transition: all 0.3s ease; +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 50%; + width: 0; + height: 2px; + background: linear-gradient(90deg, #3B82F6, #8B5CF6); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +.nav-link:hover::after, +.nav-link.active::after { + width: 100%; +} + +/* Form improvements */ +.form-input { + transition: all 0.3s ease; + border: 2px solid #E5E7EB; + background: rgba(255, 255, 255, 0.95); +} + +.form-input:focus { + border-color: #3B82F6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + transform: translateY(-1px); +} + +/* Contact form styling */ +.contact-form { + background: linear-gradient(145deg, rgba(255,255,255,0.9), rgba(255,255,255,0.95)); + border: 1px solid rgba(255,255,255,0.3); + box-shadow: 0 20px 40px rgba(0,0,0,0.1); +} + +/* CTA section improvements */ +.cta-section { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + position: relative; +} + +.cta-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; +} + +/* Service cards */ +.service-card { + background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05)); + border: 1px solid rgba(255,255,255,0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +/* Team member cards */ +.team-card { + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.95); +} + +.team-card:hover { + transform: translateY(-10px); + box-shadow: 0 30px 60px rgba(0,0,0,0.12); +} + +/* Technology icons */ +.tech-icon { + transition: all 0.3s ease; +} + +.tech-icon:hover { + transform: scale(1.1) rotate(5deg); + filter: brightness(1.2); +} + +/* Loading states */ +.loading { + opacity: 0.7; + pointer-events: none; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid #f3f3f3; + border-top: 2px solid #3B82F6; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Mobile optimizations */ +@media (max-width: 768px) { + .card-hover:hover { + transform: translateY(-4px) scale(1.01); + } + + .btn-primary:hover { + transform: translateY(-2px); + } + + .hero-section { + min-height: 80vh; + } + + .portfolio-item:hover .portfolio-image { + transform: scale(1.05); + } +} + +/* Accessibility improvements */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +button:focus, +input:focus, +textarea:focus, +select:focus, +a:focus { + outline: 2px solid #3B82F6; + outline-offset: 2px; +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Print styles */ +@media print { + .no-print { + display: none !important; + } + + body { + color: black !important; + background: white !important; + } +} + +/* Dark mode support (if needed) */ +@media (prefers-color-scheme: dark) { + .auto-dark { + color: #f9fafb; + background-color: #111827; + } +} \ No newline at end of file diff --git a/.history/public/css/fixes_20251019170927.css b/.history/public/css/fixes_20251019170927.css new file mode 100644 index 0000000..432d387 --- /dev/null +++ b/.history/public/css/fixes_20251019170927.css @@ -0,0 +1,281 @@ +/* SmartSolTech - Design Fixes & Enhancements */ + +/* Glass effect improvements */ +.glass-effect { + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + /* Fallback for browsers that don't support backdrop-filter */ +} + +/* Support backdrop-filter for modern browsers */ +@supports (backdrop-filter: blur(10px)) { + .glass-effect { + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + } +} + +/* Hero section improvements */ +.hero-section { + position: relative; + overflow: hidden; + min-height: 100vh; +} + +/* Background blob animations */ +@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; +} + +/* Enhanced card hover effects */ +.card-hover { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + backface-visibility: hidden; +} + +.card-hover:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); +} + +/* Portfolio item enhancements */ +.portfolio-item { + overflow: hidden; + border-radius: 16px; + position: relative; +} + +.portfolio-image { + transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.portfolio-item:hover .portfolio-image { + transform: scale(1.1); +} + +/* Button improvements */ +.btn-primary { + background: linear-gradient(135deg, #3B82F6, #1D4ED8); + border: none; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.5s; +} + +.btn-primary:hover::before { + left: 100%; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #1D4ED8, #1E40AF); + transform: translateY(-3px); + box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4); +} + +/* Navigation improvements */ +.nav-link { + position: relative; + transition: all 0.3s ease; +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 50%; + width: 0; + height: 2px; + background: linear-gradient(90deg, #3B82F6, #8B5CF6); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +.nav-link:hover::after, +.nav-link.active::after { + width: 100%; +} + +/* Form improvements */ +.form-input { + transition: all 0.3s ease; + border: 2px solid #E5E7EB; + background: rgba(255, 255, 255, 0.95); +} + +.form-input:focus { + border-color: #3B82F6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + transform: translateY(-1px); +} + +/* Contact form styling */ +.contact-form { + background: linear-gradient(145deg, rgba(255,255,255,0.9), rgba(255,255,255,0.95)); + border: 1px solid rgba(255,255,255,0.3); + box-shadow: 0 20px 40px rgba(0,0,0,0.1); +} + +/* CTA section improvements */ +.cta-section { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + position: relative; +} + +.cta-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; +} + +/* Service cards */ +.service-card { + background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05)); + border: 1px solid rgba(255,255,255,0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +/* Team member cards */ +.team-card { + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.95); +} + +.team-card:hover { + transform: translateY(-10px); + box-shadow: 0 30px 60px rgba(0,0,0,0.12); +} + +/* Technology icons */ +.tech-icon { + transition: all 0.3s ease; +} + +.tech-icon:hover { + transform: scale(1.1) rotate(5deg); + filter: brightness(1.2); +} + +/* Loading states */ +.loading { + opacity: 0.7; + pointer-events: none; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid #f3f3f3; + border-top: 2px solid #3B82F6; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Mobile optimizations */ +@media (max-width: 768px) { + .card-hover:hover { + transform: translateY(-4px) scale(1.01); + } + + .btn-primary:hover { + transform: translateY(-2px); + } + + .hero-section { + min-height: 80vh; + } + + .portfolio-item:hover .portfolio-image { + transform: scale(1.05); + } +} + +/* Accessibility improvements */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +button:focus, +input:focus, +textarea:focus, +select:focus, +a:focus { + outline: 2px solid #3B82F6; + outline-offset: 2px; +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Print styles */ +@media print { + .no-print { + display: none !important; + } + + body { + color: black !important; + background: white !important; + } +} + +/* Dark mode support (if needed) */ +@media (prefers-color-scheme: dark) { + .auto-dark { + color: #f9fafb; + background-color: #111827; + } +} \ No newline at end of file diff --git a/.history/public/css/main_20251019161505.css b/.history/public/css/main_20251019161505.css new file mode 100644 index 0000000..4ca83ed --- /dev/null +++ b/.history/public/css/main_20251019161505.css @@ -0,0 +1,425 @@ +/* Custom CSS for SmartSolTech */ +:root { + --primary-color: #3b82f6; + --secondary-color: #8b5cf6; + --accent-color: #10b981; + --text-dark: #1f2937; + --text-light: #6b7280; + --bg-light: #f9fafb; + --border-color: #e5e7eb; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: var(--text-dark); + scroll-behavior: smooth; +} + +/* Loading Animation */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Navigation Enhancements */ +.navbar-scrolled { + background-color: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +/* Mobile Menu Animation */ +.mobile-menu { + transition: all 0.3s ease-in-out; + max-height: 0; + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} \ No newline at end of file diff --git a/.history/public/css/main_20251019162545.css b/.history/public/css/main_20251019162545.css new file mode 100644 index 0000000..4ca83ed --- /dev/null +++ b/.history/public/css/main_20251019162545.css @@ -0,0 +1,425 @@ +/* Custom CSS for SmartSolTech */ +:root { + --primary-color: #3b82f6; + --secondary-color: #8b5cf6; + --accent-color: #10b981; + --text-dark: #1f2937; + --text-light: #6b7280; + --bg-light: #f9fafb; + --border-color: #e5e7eb; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: var(--text-dark); + scroll-behavior: smooth; +} + +/* Loading Animation */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Navigation Enhancements */ +.navbar-scrolled { + background-color: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +/* Mobile Menu Animation */ +.mobile-menu { + transition: all 0.3s ease-in-out; + max-height: 0; + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} \ No newline at end of file diff --git a/.history/public/css/main_20251019164257.css b/.history/public/css/main_20251019164257.css new file mode 100644 index 0000000..5e24d2d --- /dev/null +++ b/.history/public/css/main_20251019164257.css @@ -0,0 +1,427 @@ +/* SmartSolTech - Main Styles */ + +/* CSS Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Utility Classes */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.section-padding { + padding: 4rem 0; +} + +/* Loading Animation */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Navigation Enhancements */ +.navbar-scrolled { + background-color: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +/* Mobile Menu Animation */ +.mobile-menu { + transition: all 0.3s ease-in-out; + max-height: 0; + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} \ No newline at end of file diff --git a/.history/public/css/main_20251019164506.css b/.history/public/css/main_20251019164506.css new file mode 100644 index 0000000..8f98e8c --- /dev/null +++ b/.history/public/css/main_20251019164506.css @@ -0,0 +1,442 @@ +/* SmartSolTech - Main Styles */ + +/* CSS Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Utility Classes */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.section-padding { + padding: 4rem 0; +} + +/* Navigation */ +.navbar { + background: rgba(255, 255, 255, 0.95); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + transition: all 0.3s ease; +} + +.navbar-brand { + font-size: 1.5rem; + font-weight: bold; + color: #3b82f6; + text-decoration: none; +} + +.navbar-nav { + display: flex; + gap: 2rem; + list-style: none; +} + +.nav-link { + color: #6b7280; + text-decoration: none; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.3s ease; +} + +.nav-link:hover, +.nav-link.active { + color: #3b82f6; + background-color: #eff6ff; +} + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} \ No newline at end of file diff --git a/.history/public/css/main_20251019165556.css b/.history/public/css/main_20251019165556.css new file mode 100644 index 0000000..8f98e8c --- /dev/null +++ b/.history/public/css/main_20251019165556.css @@ -0,0 +1,442 @@ +/* SmartSolTech - Main Styles */ + +/* CSS Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Utility Classes */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.section-padding { + padding: 4rem 0; +} + +/* Navigation */ +.navbar { + background: rgba(255, 255, 255, 0.95); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + transition: all 0.3s ease; +} + +.navbar-brand { + font-size: 1.5rem; + font-weight: bold; + color: #3b82f6; + text-decoration: none; +} + +.navbar-nav { + display: flex; + gap: 2rem; + list-style: none; +} + +.nav-link { + color: #6b7280; + text-decoration: none; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.3s ease; +} + +.nav-link:hover, +.nav-link.active { + color: #3b82f6; + background-color: #eff6ff; +} + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} \ No newline at end of file diff --git a/.history/public/css/main_20251019182154.css b/.history/public/css/main_20251019182154.css new file mode 100644 index 0000000..e13ca7f --- /dev/null +++ b/.history/public/css/main_20251019182154.css @@ -0,0 +1,552 @@ +/* SmartSolTech - Main Styles */ + +/* CSS Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Utility Classes */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.section-padding { + padding: 4rem 0; +} + +/* Navigation */ +.navbar { + background: rgba(255, 255, 255, 0.95); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + transition: all 0.3s ease; +} + +.navbar-brand { + font-size: 1.5rem; + font-weight: bold; + color: #3b82f6; + text-decoration: none; +} + +.navbar-nav { + display: flex; + gap: 2rem; + list-style: none; +} + +.nav-link { + color: #6b7280; + text-decoration: none; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.3s ease; +} + +.nav-link:hover, +.nav-link.active { + color: #3b82f6; + background-color: #eff6ff; +} + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +/* Calculator Styles */ +.calculator-step { + display: none; +} + +.calculator-step.active { + display: block; +} + +.service-option, +.complexity-option, +.timeline-option { + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.service-option:hover, +.complexity-option:hover, +.timeline-option:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15); +} + +.service-option.selected, +.complexity-option.selected, +.timeline-option.selected { + border-color: #3B82F6 !important; + background-color: #EBF8FF !important; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15); +} + +.service-option.selected::after, +.complexity-option.selected::after, +.timeline-option.selected::after { + content: '✓'; + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 24px; + height: 24px; + background: #3B82F6; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; +} + +/* Price Display Animation */ +#final-price { + animation: priceReveal 0.8s ease-in-out; +} + +@keyframes priceReveal { + 0% { + opacity: 0; + transform: scale(0.8); + } + 50% { + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +/* Calculator Progress Bar */ +.calculator-progress { + width: 100%; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + margin-bottom: 2rem; + overflow: hidden; +} + +.calculator-progress-bar { + height: 100%; + background: linear-gradient(90deg, #3B82F6, #8B5CF6); + border-radius: 2px; + transition: width 0.3s ease; + width: 33.33%; +} + +.calculator-progress-bar.step-2 { + width: 66.66%; +} + +.calculator-progress-bar.step-3 { + width: 100%; +} + +/* Calculator Mobile Improvements */ +@media (max-width: 768px) { + .service-option, + .complexity-option, + .timeline-option { + margin-bottom: 1rem; + } + + #final-price { + font-size: 2.5rem; + } +} \ No newline at end of file diff --git a/.history/public/css/main_20251019182628.css b/.history/public/css/main_20251019182628.css new file mode 100644 index 0000000..e13ca7f --- /dev/null +++ b/.history/public/css/main_20251019182628.css @@ -0,0 +1,552 @@ +/* SmartSolTech - Main Styles */ + +/* CSS Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Utility Classes */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.section-padding { + padding: 4rem 0; +} + +/* Navigation */ +.navbar { + background: rgba(255, 255, 255, 0.95); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + transition: all 0.3s ease; +} + +.navbar-brand { + font-size: 1.5rem; + font-weight: bold; + color: #3b82f6; + text-decoration: none; +} + +.navbar-nav { + display: flex; + gap: 2rem; + list-style: none; +} + +.nav-link { + color: #6b7280; + text-decoration: none; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.3s ease; +} + +.nav-link:hover, +.nav-link.active { + color: #3b82f6; + background-color: #eff6ff; +} + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +/* Calculator Styles */ +.calculator-step { + display: none; +} + +.calculator-step.active { + display: block; +} + +.service-option, +.complexity-option, +.timeline-option { + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.service-option:hover, +.complexity-option:hover, +.timeline-option:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15); +} + +.service-option.selected, +.complexity-option.selected, +.timeline-option.selected { + border-color: #3B82F6 !important; + background-color: #EBF8FF !important; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15); +} + +.service-option.selected::after, +.complexity-option.selected::after, +.timeline-option.selected::after { + content: '✓'; + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 24px; + height: 24px; + background: #3B82F6; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; +} + +/* Price Display Animation */ +#final-price { + animation: priceReveal 0.8s ease-in-out; +} + +@keyframes priceReveal { + 0% { + opacity: 0; + transform: scale(0.8); + } + 50% { + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +/* Calculator Progress Bar */ +.calculator-progress { + width: 100%; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + margin-bottom: 2rem; + overflow: hidden; +} + +.calculator-progress-bar { + height: 100%; + background: linear-gradient(90deg, #3B82F6, #8B5CF6); + border-radius: 2px; + transition: width 0.3s ease; + width: 33.33%; +} + +.calculator-progress-bar.step-2 { + width: 66.66%; +} + +.calculator-progress-bar.step-3 { + width: 100%; +} + +/* Calculator Mobile Improvements */ +@media (max-width: 768px) { + .service-option, + .complexity-option, + .timeline-option { + margin-bottom: 1rem; + } + + #final-price { + font-size: 2.5rem; + } +} \ No newline at end of file diff --git a/.history/public/js/calculator_20251019182236.js b/.history/public/js/calculator_20251019182236.js new file mode 100644 index 0000000..a9f0e01 --- /dev/null +++ b/.history/public/js/calculator_20251019182236.js @@ -0,0 +1,368 @@ +// Calculator Logic +class ProjectCalculator { + constructor() { + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + this.totalSteps = 3; + + this.init(); + } + + init() { + this.setupEventListeners(); + this.updateProgressBar(); + } + + setupEventListeners() { + // Service selection + document.querySelectorAll('.service-option').forEach(option => { + option.addEventListener('click', (e) => this.handleServiceSelection(e)); + }); + + // Complexity selection + document.querySelectorAll('.complexity-option').forEach(option => { + option.addEventListener('click', (e) => this.handleComplexitySelection(e)); + }); + + // Timeline selection + document.querySelectorAll('.timeline-option').forEach(option => { + option.addEventListener('click', (e) => this.handleTimelineSelection(e)); + }); + + // Navigation buttons + document.querySelectorAll('.next-step').forEach(btn => { + btn.addEventListener('click', () => this.nextStep()); + }); + + document.querySelectorAll('.prev-step').forEach(btn => { + btn.addEventListener('click', () => this.prevStep()); + }); + + // Restart button + const restartBtn = document.querySelector('.restart-calculator'); + if (restartBtn) { + restartBtn.addEventListener('click', () => this.restart()); + } + } + + handleServiceSelection(e) { + const option = e.currentTarget; + const service = option.dataset.service; + const price = parseInt(option.dataset.basePrice); + + option.classList.toggle('selected'); + + if (option.classList.contains('selected')) { + this.selectedServices.push({ service, price }); + this.animateSelection(option, true); + } else { + this.selectedServices = this.selectedServices.filter(s => s.service !== service); + this.animateSelection(option, false); + } + + this.updateStepButton(); + } + + handleComplexitySelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.complexity-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedComplexity = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + handleTimelineSelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedTimeline = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + animateSelection(element, selected) { + if (selected) { + element.style.borderColor = '#3B82F6'; + element.style.backgroundColor = '#EBF8FF'; + element.style.transform = 'translateY(-2px)'; + element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)'; + } else { + element.style.borderColor = ''; + element.style.backgroundColor = ''; + element.style.transform = ''; + element.style.boxShadow = ''; + } + } + + updateStepButton() { + const step1Button = document.querySelector('#step-1 .next-step'); + const step2Button = document.querySelector('#step-2 .next-step'); + + if (step1Button) { + step1Button.disabled = this.selectedServices.length === 0; + step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6'; + } + + if (step2Button) { + const isValid = this.selectedComplexity && this.selectedTimeline; + step2Button.disabled = !isValid; + step2Button.style.opacity = isValid ? '1' : '0.6'; + } + } + + updateProgressBar() { + const progressBar = document.querySelector('.calculator-progress-bar'); + if (progressBar) { + const progress = (this.currentStep / this.totalSteps) * 100; + progressBar.style.width = `${progress}%`; + progressBar.className = `calculator-progress-bar step-${this.currentStep}`; + } + } + + nextStep() { + if (this.currentStep >= this.totalSteps) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const nextStepElement = currentStepElement.nextElementSibling; + + if (nextStepElement && nextStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in next step + nextStepElement.classList.add('active'); + nextStepElement.style.display = 'block'; + nextStepElement.style.opacity = '0'; + nextStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + nextStepElement.style.opacity = '1'; + nextStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep++; + this.updateProgressBar(); + + if (this.currentStep === 3) { + setTimeout(() => this.calculateFinalPrice(), 300); + } + }, 200); + } + } + + prevStep() { + if (this.currentStep <= 1) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const prevStepElement = currentStepElement.previousElementSibling; + + if (prevStepElement && prevStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in previous step + prevStepElement.classList.add('active'); + prevStepElement.style.display = 'block'; + prevStepElement.style.opacity = '0'; + prevStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + prevStepElement.style.opacity = '1'; + prevStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep--; + this.updateProgressBar(); + }, 200); + } + } + + calculateFinalPrice() { + let total = 0; + + // Calculate base price from services + this.selectedServices.forEach(service => { + total += service.price; + }); + + // Apply complexity multiplier + if (this.selectedComplexity) { + total *= this.selectedComplexity.multiplier; + } + + // Apply timeline multiplier + if (this.selectedTimeline) { + total *= this.selectedTimeline.multiplier; + } + + // Animate price reveal + const priceElement = document.getElementById('final-price'); + if (priceElement) { + priceElement.style.opacity = '0'; + priceElement.style.transform = 'scale(0.8)'; + + setTimeout(() => { + priceElement.textContent = '₩' + total.toLocaleString(); + priceElement.style.opacity = '1'; + priceElement.style.transform = 'scale(1)'; + }, 300); + } + + // Generate summary + setTimeout(() => this.generateSummary(total), 600); + } + + generateSummary(total) { + const summary = document.getElementById('project-summary'); + if (!summary) return; + + let summaryHTML = ''; + + // Services + summaryHTML += '
Selected Services:
'; + this.selectedServices.forEach(service => { + summaryHTML += `
+ + ${this.getServiceName(service.service)} + ₩${service.price.toLocaleString()} +
`; + }); + + // Complexity + if (this.selectedComplexity) { + summaryHTML += `
+ + Complexity: + ${this.getComplexityName(this.selectedComplexity.value)} + ×${this.selectedComplexity.multiplier} +
`; + } + + // Timeline + if (this.selectedTimeline) { + summaryHTML += `
+ + Timeline: + ${this.getTimelineName(this.selectedTimeline.value)} + ×${this.selectedTimeline.multiplier} +
`; + } + + // Animate summary appearance + summary.style.opacity = '0'; + summary.innerHTML = summaryHTML; + + setTimeout(() => { + summary.style.opacity = '1'; + }, 200); + } + + getServiceName(service) { + const names = { + web: 'Web Development', + mobile: 'Mobile App', + design: 'UI/UX Design', + marketing: 'Digital Marketing' + }; + return names[service] || service; + } + + getComplexityName(complexity) { + const names = { + simple: 'Simple', + medium: 'Medium', + complex: 'Complex' + }; + return names[complexity] || complexity; + } + + getTimelineName(timeline) { + const names = { + standard: 'Standard', + rush: 'Rush', + extended: 'Extended' + }; + return names[timeline] || timeline; + } + + restart() { + // Reset all selections + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + + // Reset UI + document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Reset steps + document.querySelectorAll('.calculator-step').forEach(step => { + step.classList.remove('active'); + step.style.display = 'none'; + step.style.opacity = '1'; + step.style.transform = 'translateX(0)'; + }); + + // Show first step + const firstStep = document.getElementById('step-1'); + if (firstStep) { + firstStep.classList.add('active'); + firstStep.style.display = 'block'; + } + + this.updateProgressBar(); + this.updateStepButton(); + } +} + +// Initialize calculator when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Theme initialization + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.className = theme === 'dark' ? 'dark' : ''; + + // Initialize calculator + if (document.querySelector('.calculator-step')) { + new ProjectCalculator(); + } +}); \ No newline at end of file diff --git a/.history/public/js/calculator_20251019182338.js b/.history/public/js/calculator_20251019182338.js new file mode 100644 index 0000000..888cde0 --- /dev/null +++ b/.history/public/js/calculator_20251019182338.js @@ -0,0 +1,380 @@ +// Calculator Logic +class ProjectCalculator { + constructor() { + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + this.totalSteps = 3; + + this.init(); + } + + init() { + this.setupEventListeners(); + this.updateProgressBar(); + } + + setupEventListeners() { + // Service selection + document.querySelectorAll('.service-option').forEach(option => { + option.addEventListener('click', (e) => this.handleServiceSelection(e)); + }); + + // Complexity selection + document.querySelectorAll('.complexity-option').forEach(option => { + option.addEventListener('click', (e) => this.handleComplexitySelection(e)); + }); + + // Timeline selection + document.querySelectorAll('.timeline-option').forEach(option => { + option.addEventListener('click', (e) => this.handleTimelineSelection(e)); + }); + + // Navigation buttons + document.querySelectorAll('.next-step').forEach(btn => { + btn.addEventListener('click', () => this.nextStep()); + }); + + document.querySelectorAll('.prev-step').forEach(btn => { + btn.addEventListener('click', () => this.prevStep()); + }); + + // Restart button + const restartBtn = document.querySelector('.restart-calculator'); + if (restartBtn) { + restartBtn.addEventListener('click', () => this.restart()); + } + } + + handleServiceSelection(e) { + const option = e.currentTarget; + const service = option.dataset.service; + const price = parseInt(option.dataset.basePrice); + + option.classList.toggle('selected'); + + if (option.classList.contains('selected')) { + this.selectedServices.push({ service, price }); + this.animateSelection(option, true); + } else { + this.selectedServices = this.selectedServices.filter(s => s.service !== service); + this.animateSelection(option, false); + } + + this.updateStepButton(); + } + + handleComplexitySelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.complexity-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedComplexity = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + handleTimelineSelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedTimeline = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + animateSelection(element, selected) { + if (selected) { + element.style.borderColor = '#3B82F6'; + element.style.backgroundColor = '#EBF8FF'; + element.style.transform = 'translateY(-2px)'; + element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)'; + } else { + element.style.borderColor = ''; + element.style.backgroundColor = ''; + element.style.transform = ''; + element.style.boxShadow = ''; + } + } + + updateStepButton() { + const step1Button = document.querySelector('#step-1 .next-step'); + const step2Button = document.querySelector('#step-2 .next-step'); + + if (step1Button) { + step1Button.disabled = this.selectedServices.length === 0; + step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6'; + } + + if (step2Button) { + const isValid = this.selectedComplexity && this.selectedTimeline; + step2Button.disabled = !isValid; + step2Button.style.opacity = isValid ? '1' : '0.6'; + } + } + + updateProgressBar() { + const progressBar = document.querySelector('.calculator-progress-bar'); + if (progressBar) { + const progress = (this.currentStep / this.totalSteps) * 100; + progressBar.style.width = `${progress}%`; + progressBar.className = `calculator-progress-bar step-${this.currentStep}`; + } + } + + nextStep() { + if (this.currentStep >= this.totalSteps) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const nextStepElement = currentStepElement.nextElementSibling; + + if (nextStepElement && nextStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in next step + nextStepElement.classList.add('active'); + nextStepElement.style.display = 'block'; + nextStepElement.style.opacity = '0'; + nextStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + nextStepElement.style.opacity = '1'; + nextStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep++; + this.updateProgressBar(); + + if (this.currentStep === 3) { + setTimeout(() => this.calculateFinalPrice(), 300); + } + }, 200); + } + } + + prevStep() { + if (this.currentStep <= 1) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const prevStepElement = currentStepElement.previousElementSibling; + + if (prevStepElement && prevStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in previous step + prevStepElement.classList.add('active'); + prevStepElement.style.display = 'block'; + prevStepElement.style.opacity = '0'; + prevStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + prevStepElement.style.opacity = '1'; + prevStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep--; + this.updateProgressBar(); + }, 200); + } + } + + calculateFinalPrice() { + let total = 0; + + // Calculate base price from services + this.selectedServices.forEach(service => { + total += service.price; + }); + + // Apply complexity multiplier + if (this.selectedComplexity) { + total *= this.selectedComplexity.multiplier; + } + + // Apply timeline multiplier + if (this.selectedTimeline) { + total *= this.selectedTimeline.multiplier; + } + + // Animate price reveal + const priceElement = document.getElementById('final-price'); + if (priceElement) { + priceElement.style.opacity = '0'; + priceElement.style.transform = 'scale(0.8)'; + + setTimeout(() => { + priceElement.textContent = '₩' + total.toLocaleString(); + priceElement.style.opacity = '1'; + priceElement.style.transform = 'scale(1)'; + }, 300); + } + + // Generate summary + setTimeout(() => this.generateSummary(total), 600); + } + + generateSummary(total) { + const summary = document.getElementById('project-summary'); + if (!summary) return; + + let summaryHTML = ''; + + // Services + summaryHTML += '
Selected Services:
'; + this.selectedServices.forEach(service => { + summaryHTML += `
+ + ${this.getServiceName(service.service)} + ₩${service.price.toLocaleString()} +
`; + }); + + // Complexity + if (this.selectedComplexity) { + summaryHTML += `
+ + Complexity: + ${this.getComplexityName(this.selectedComplexity.value)} + ×${this.selectedComplexity.multiplier} +
`; + } + + // Timeline + if (this.selectedTimeline) { + summaryHTML += `
+ + Timeline: + ${this.getTimelineName(this.selectedTimeline.value)} + ×${this.selectedTimeline.multiplier} +
`; + } + + // Animate summary appearance + summary.style.opacity = '0'; + summary.innerHTML = summaryHTML; + + setTimeout(() => { + summary.style.opacity = '1'; + }, 200); + } + + getServiceName(service) { + if (window.calculatorTranslations && window.calculatorTranslations.services) { + return window.calculatorTranslations.services[service] || service; + } + + const names = { + web: 'Web Development', + mobile: 'Mobile App', + design: 'UI/UX Design', + marketing: 'Digital Marketing' + }; + return names[service] || service; + } + + getComplexityName(complexity) { + if (window.calculatorTranslations && window.calculatorTranslations.complexity) { + return window.calculatorTranslations.complexity[complexity] || complexity; + } + + const names = { + simple: 'Simple', + medium: 'Medium', + complex: 'Complex' + }; + return names[complexity] || complexity; + } + + getTimelineName(timeline) { + if (window.calculatorTranslations && window.calculatorTranslations.timeline) { + return window.calculatorTranslations.timeline[timeline] || timeline; + } + + const names = { + standard: 'Standard', + rush: 'Rush', + extended: 'Extended' + }; + return names[timeline] || timeline; + } + + restart() { + // Reset all selections + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + + // Reset UI + document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Reset steps + document.querySelectorAll('.calculator-step').forEach(step => { + step.classList.remove('active'); + step.style.display = 'none'; + step.style.opacity = '1'; + step.style.transform = 'translateX(0)'; + }); + + // Show first step + const firstStep = document.getElementById('step-1'); + if (firstStep) { + firstStep.classList.add('active'); + firstStep.style.display = 'block'; + } + + this.updateProgressBar(); + this.updateStepButton(); + } +} + +// Initialize calculator when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Theme initialization + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.className = theme === 'dark' ? 'dark' : ''; + + // Initialize calculator + if (document.querySelector('.calculator-step')) { + new ProjectCalculator(); + } +}); \ No newline at end of file diff --git a/.history/public/js/calculator_20251019182400.js b/.history/public/js/calculator_20251019182400.js new file mode 100644 index 0000000..5618dd2 --- /dev/null +++ b/.history/public/js/calculator_20251019182400.js @@ -0,0 +1,384 @@ +// Calculator Logic +class ProjectCalculator { + constructor() { + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + this.totalSteps = 3; + + this.init(); + } + + init() { + this.setupEventListeners(); + this.updateProgressBar(); + } + + setupEventListeners() { + // Service selection + document.querySelectorAll('.service-option').forEach(option => { + option.addEventListener('click', (e) => this.handleServiceSelection(e)); + }); + + // Complexity selection + document.querySelectorAll('.complexity-option').forEach(option => { + option.addEventListener('click', (e) => this.handleComplexitySelection(e)); + }); + + // Timeline selection + document.querySelectorAll('.timeline-option').forEach(option => { + option.addEventListener('click', (e) => this.handleTimelineSelection(e)); + }); + + // Navigation buttons + document.querySelectorAll('.next-step').forEach(btn => { + btn.addEventListener('click', () => this.nextStep()); + }); + + document.querySelectorAll('.prev-step').forEach(btn => { + btn.addEventListener('click', () => this.prevStep()); + }); + + // Restart button + const restartBtn = document.querySelector('.restart-calculator'); + if (restartBtn) { + restartBtn.addEventListener('click', () => this.restart()); + } + } + + handleServiceSelection(e) { + const option = e.currentTarget; + const service = option.dataset.service; + const price = parseInt(option.dataset.basePrice); + + option.classList.toggle('selected'); + + if (option.classList.contains('selected')) { + this.selectedServices.push({ service, price }); + this.animateSelection(option, true); + } else { + this.selectedServices = this.selectedServices.filter(s => s.service !== service); + this.animateSelection(option, false); + } + + this.updateStepButton(); + } + + handleComplexitySelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.complexity-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedComplexity = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + handleTimelineSelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedTimeline = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + animateSelection(element, selected) { + if (selected) { + element.style.borderColor = '#3B82F6'; + element.style.backgroundColor = '#EBF8FF'; + element.style.transform = 'translateY(-2px)'; + element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)'; + } else { + element.style.borderColor = ''; + element.style.backgroundColor = ''; + element.style.transform = ''; + element.style.boxShadow = ''; + } + } + + updateStepButton() { + const step1Button = document.querySelector('#step-1 .next-step'); + const step2Button = document.querySelector('#step-2 .next-step'); + + if (step1Button) { + step1Button.disabled = this.selectedServices.length === 0; + step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6'; + } + + if (step2Button) { + const isValid = this.selectedComplexity && this.selectedTimeline; + step2Button.disabled = !isValid; + step2Button.style.opacity = isValid ? '1' : '0.6'; + } + } + + updateProgressBar() { + const progressBar = document.querySelector('.calculator-progress-bar'); + if (progressBar) { + const progress = (this.currentStep / this.totalSteps) * 100; + progressBar.style.width = `${progress}%`; + progressBar.className = `calculator-progress-bar step-${this.currentStep}`; + } + } + + nextStep() { + if (this.currentStep >= this.totalSteps) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const nextStepElement = currentStepElement.nextElementSibling; + + if (nextStepElement && nextStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in next step + nextStepElement.classList.add('active'); + nextStepElement.style.display = 'block'; + nextStepElement.style.opacity = '0'; + nextStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + nextStepElement.style.opacity = '1'; + nextStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep++; + this.updateProgressBar(); + + if (this.currentStep === 3) { + setTimeout(() => this.calculateFinalPrice(), 300); + } + }, 200); + } + } + + prevStep() { + if (this.currentStep <= 1) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const prevStepElement = currentStepElement.previousElementSibling; + + if (prevStepElement && prevStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in previous step + prevStepElement.classList.add('active'); + prevStepElement.style.display = 'block'; + prevStepElement.style.opacity = '0'; + prevStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + prevStepElement.style.opacity = '1'; + prevStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep--; + this.updateProgressBar(); + }, 200); + } + } + + calculateFinalPrice() { + let total = 0; + + // Calculate base price from services + this.selectedServices.forEach(service => { + total += service.price; + }); + + // Apply complexity multiplier + if (this.selectedComplexity) { + total *= this.selectedComplexity.multiplier; + } + + // Apply timeline multiplier + if (this.selectedTimeline) { + total *= this.selectedTimeline.multiplier; + } + + // Animate price reveal + const priceElement = document.getElementById('final-price'); + if (priceElement) { + priceElement.style.opacity = '0'; + priceElement.style.transform = 'scale(0.8)'; + + setTimeout(() => { + priceElement.textContent = '₩' + total.toLocaleString(); + priceElement.style.opacity = '1'; + priceElement.style.transform = 'scale(1)'; + }, 300); + } + + // Generate summary + setTimeout(() => this.generateSummary(total), 600); + } + + generateSummary(total) { + const summary = document.getElementById('project-summary'); + if (!summary) return; + + let summaryHTML = ''; + + // Services + const servicesLabel = window.calculatorTranslations?.labels?.selected_services || 'Selected Services'; + const complexityLabel = window.calculatorTranslations?.labels?.complexity || 'Complexity'; + const timelineLabel = window.calculatorTranslations?.labels?.timeline || 'Timeline'; + + summaryHTML += `
${servicesLabel}:
`; + this.selectedServices.forEach(service => { + summaryHTML += `
+ + ${this.getServiceName(service.service)} + ₩${service.price.toLocaleString()} +
`; + }); + + // Complexity + if (this.selectedComplexity) { + summaryHTML += `
+ + ${complexityLabel}: + ${this.getComplexityName(this.selectedComplexity.value)} + ×${this.selectedComplexity.multiplier} +
`; + } + + // Timeline + if (this.selectedTimeline) { + summaryHTML += `
+ + ${timelineLabel}: + ${this.getTimelineName(this.selectedTimeline.value)} + ×${this.selectedTimeline.multiplier} +
`; + } + + // Animate summary appearance + summary.style.opacity = '0'; + summary.innerHTML = summaryHTML; + + setTimeout(() => { + summary.style.opacity = '1'; + }, 200); + } + + getServiceName(service) { + if (window.calculatorTranslations && window.calculatorTranslations.services) { + return window.calculatorTranslations.services[service] || service; + } + + const names = { + web: 'Web Development', + mobile: 'Mobile App', + design: 'UI/UX Design', + marketing: 'Digital Marketing' + }; + return names[service] || service; + } + + getComplexityName(complexity) { + if (window.calculatorTranslations && window.calculatorTranslations.complexity) { + return window.calculatorTranslations.complexity[complexity] || complexity; + } + + const names = { + simple: 'Simple', + medium: 'Medium', + complex: 'Complex' + }; + return names[complexity] || complexity; + } + + getTimelineName(timeline) { + if (window.calculatorTranslations && window.calculatorTranslations.timeline) { + return window.calculatorTranslations.timeline[timeline] || timeline; + } + + const names = { + standard: 'Standard', + rush: 'Rush', + extended: 'Extended' + }; + return names[timeline] || timeline; + } + + restart() { + // Reset all selections + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + + // Reset UI + document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Reset steps + document.querySelectorAll('.calculator-step').forEach(step => { + step.classList.remove('active'); + step.style.display = 'none'; + step.style.opacity = '1'; + step.style.transform = 'translateX(0)'; + }); + + // Show first step + const firstStep = document.getElementById('step-1'); + if (firstStep) { + firstStep.classList.add('active'); + firstStep.style.display = 'block'; + } + + this.updateProgressBar(); + this.updateStepButton(); + } +} + +// Initialize calculator when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Theme initialization + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.className = theme === 'dark' ? 'dark' : ''; + + // Initialize calculator + if (document.querySelector('.calculator-step')) { + new ProjectCalculator(); + } +}); \ No newline at end of file diff --git a/.history/public/js/calculator_20251019182510.js b/.history/public/js/calculator_20251019182510.js new file mode 100644 index 0000000..8bbbfcb --- /dev/null +++ b/.history/public/js/calculator_20251019182510.js @@ -0,0 +1,384 @@ +// Calculator Logic +class ProjectCalculator { + constructor() { + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + this.totalSteps = 3; + + this.init(); + } + + init() { + this.setupEventListeners(); + this.updateProgressBar(); + } + + setupEventListeners() { + // Service selection + document.querySelectorAll('.service-option').forEach(option => { + option.addEventListener('click', (e) => this.handleServiceSelection(e)); + }); + + // Complexity selection + document.querySelectorAll('.complexity-option').forEach(option => { + option.addEventListener('click', (e) => this.handleComplexitySelection(e)); + }); + + // Timeline selection + document.querySelectorAll('.timeline-option').forEach(option => { + option.addEventListener('click', (e) => this.handleTimelineSelection(e)); + }); + + // Navigation buttons + document.querySelectorAll('.next-step').forEach(btn => { + btn.addEventListener('click', () => this.nextStep()); + }); + + document.querySelectorAll('.prev-step').forEach(btn => { + btn.addEventListener('click', () => this.prevStep()); + }); + + // Restart button + const restartBtn = document.querySelector('.restart-calculator'); + if (restartBtn) { + restartBtn.addEventListener('click', () => this.restart()); + } + } + + handleServiceSelection(e) { + const option = e.currentTarget; + const service = option.dataset.service; + const price = parseInt(option.dataset.basePrice); + + option.classList.toggle('selected'); + + if (option.classList.contains('selected')) { + this.selectedServices.push({ service, price }); + this.animateSelection(option, true); + } else { + this.selectedServices = this.selectedServices.filter(s => s.service !== service); + this.animateSelection(option, false); + } + + this.updateStepButton(); + } + + handleComplexitySelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.complexity-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedComplexity = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + handleTimelineSelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedTimeline = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + animateSelection(element, selected) { + if (selected) { + element.style.borderColor = '#3B82F6'; + element.style.backgroundColor = '#EBF8FF'; + element.style.transform = 'translateY(-2px)'; + element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)'; + } else { + element.style.borderColor = ''; + element.style.backgroundColor = ''; + element.style.transform = ''; + element.style.boxShadow = ''; + } + } + + updateStepButton() { + const step1Button = document.querySelector('#step-1 .next-step'); + const step2Button = document.querySelector('#step-2 .next-step'); + + if (step1Button) { + step1Button.disabled = this.selectedServices.length === 0; + step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6'; + } + + if (step2Button) { + const isValid = this.selectedComplexity && this.selectedTimeline; + step2Button.disabled = !isValid; + step2Button.style.opacity = isValid ? '1' : '0.6'; + } + } + + updateProgressBar() { + const progressBar = document.querySelector('.calculator-progress-bar'); + if (progressBar) { + const progress = (this.currentStep / this.totalSteps) * 100; + progressBar.style.width = `${progress}%`; + progressBar.className = `calculator-progress-bar step-${this.currentStep}`; + } + } + + nextStep() { + if (this.currentStep >= this.totalSteps) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const nextStepElement = currentStepElement.nextElementSibling; + + if (nextStepElement && nextStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in next step + nextStepElement.classList.add('active'); + nextStepElement.style.display = 'block'; + nextStepElement.style.opacity = '0'; + nextStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + nextStepElement.style.opacity = '1'; + nextStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep++; + this.updateProgressBar(); + + if (this.currentStep === 3) { + setTimeout(() => this.calculateFinalPrice(), 300); + } + }, 200); + } + } + + prevStep() { + if (this.currentStep <= 1) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const prevStepElement = currentStepElement.previousElementSibling; + + if (prevStepElement && prevStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in previous step + prevStepElement.classList.add('active'); + prevStepElement.style.display = 'block'; + prevStepElement.style.opacity = '0'; + prevStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + prevStepElement.style.opacity = '1'; + prevStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep--; + this.updateProgressBar(); + }, 200); + } + } + + calculateFinalPrice() { + let total = 0; + + // Calculate base price from services + this.selectedServices.forEach(service => { + total += service.price; + }); + + // Apply complexity multiplier + if (this.selectedComplexity) { + total *= this.selectedComplexity.multiplier; + } + + // Apply timeline multiplier + if (this.selectedTimeline) { + total *= this.selectedTimeline.multiplier; + } + + // Animate price reveal + const priceElement = document.getElementById('final-price'); + if (priceElement) { + priceElement.style.opacity = '0'; + priceElement.style.transform = 'scale(0.8)'; + + setTimeout(() => { + priceElement.textContent = '₩' + total.toLocaleString(); + priceElement.style.opacity = '1'; + priceElement.style.transform = 'scale(1)'; + }, 300); + } + + // Generate summary + setTimeout(() => this.generateSummary(total), 600); + } + + generateSummary(total) { + const summary = document.getElementById('project-summary'); + if (!summary) return; + + let summaryHTML = ''; + + // Services + const servicesLabel = window.calculatorTranslations?.result?.selected_services || 'Selected Services'; + const complexityLabel = window.calculatorTranslations?.result?.complexity || 'Complexity'; + const timelineLabel = window.calculatorTranslations?.result?.timeline || 'Timeline'; + + summaryHTML += `
${servicesLabel}:
`; + this.selectedServices.forEach(service => { + summaryHTML += `
+ + ${this.getServiceName(service.service)} + ₩${service.price.toLocaleString()} +
`; + }); + + // Complexity + if (this.selectedComplexity) { + summaryHTML += `
+ + ${complexityLabel}: + ${this.getComplexityName(this.selectedComplexity.value)} + ×${this.selectedComplexity.multiplier} +
`; + } + + // Timeline + if (this.selectedTimeline) { + summaryHTML += `
+ + ${timelineLabel}: + ${this.getTimelineName(this.selectedTimeline.value)} + ×${this.selectedTimeline.multiplier} +
`; + } + + // Animate summary appearance + summary.style.opacity = '0'; + summary.innerHTML = summaryHTML; + + setTimeout(() => { + summary.style.opacity = '1'; + }, 200); + } + + getServiceName(service) { + if (window.calculatorTranslations && window.calculatorTranslations.services) { + return window.calculatorTranslations.services[service] || service; + } + + const names = { + web: 'Web Development', + mobile: 'Mobile App', + design: 'UI/UX Design', + marketing: 'Digital Marketing' + }; + return names[service] || service; + } + + getComplexityName(complexity) { + if (window.calculatorTranslations && window.calculatorTranslations.complexity) { + return window.calculatorTranslations.complexity[complexity] || complexity; + } + + const names = { + simple: 'Simple', + medium: 'Medium', + complex: 'Complex' + }; + return names[complexity] || complexity; + } + + getTimelineName(timeline) { + if (window.calculatorTranslations && window.calculatorTranslations.timeline) { + return window.calculatorTranslations.timeline[timeline] || timeline; + } + + const names = { + standard: 'Standard', + rush: 'Rush', + extended: 'Extended' + }; + return names[timeline] || timeline; + } + + restart() { + // Reset all selections + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + + // Reset UI + document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Reset steps + document.querySelectorAll('.calculator-step').forEach(step => { + step.classList.remove('active'); + step.style.display = 'none'; + step.style.opacity = '1'; + step.style.transform = 'translateX(0)'; + }); + + // Show first step + const firstStep = document.getElementById('step-1'); + if (firstStep) { + firstStep.classList.add('active'); + firstStep.style.display = 'block'; + } + + this.updateProgressBar(); + this.updateStepButton(); + } +} + +// Initialize calculator when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Theme initialization + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.className = theme === 'dark' ? 'dark' : ''; + + // Initialize calculator + if (document.querySelector('.calculator-step')) { + new ProjectCalculator(); + } +}); \ No newline at end of file diff --git a/.history/public/js/calculator_20251019182628.js b/.history/public/js/calculator_20251019182628.js new file mode 100644 index 0000000..8bbbfcb --- /dev/null +++ b/.history/public/js/calculator_20251019182628.js @@ -0,0 +1,384 @@ +// Calculator Logic +class ProjectCalculator { + constructor() { + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + this.totalSteps = 3; + + this.init(); + } + + init() { + this.setupEventListeners(); + this.updateProgressBar(); + } + + setupEventListeners() { + // Service selection + document.querySelectorAll('.service-option').forEach(option => { + option.addEventListener('click', (e) => this.handleServiceSelection(e)); + }); + + // Complexity selection + document.querySelectorAll('.complexity-option').forEach(option => { + option.addEventListener('click', (e) => this.handleComplexitySelection(e)); + }); + + // Timeline selection + document.querySelectorAll('.timeline-option').forEach(option => { + option.addEventListener('click', (e) => this.handleTimelineSelection(e)); + }); + + // Navigation buttons + document.querySelectorAll('.next-step').forEach(btn => { + btn.addEventListener('click', () => this.nextStep()); + }); + + document.querySelectorAll('.prev-step').forEach(btn => { + btn.addEventListener('click', () => this.prevStep()); + }); + + // Restart button + const restartBtn = document.querySelector('.restart-calculator'); + if (restartBtn) { + restartBtn.addEventListener('click', () => this.restart()); + } + } + + handleServiceSelection(e) { + const option = e.currentTarget; + const service = option.dataset.service; + const price = parseInt(option.dataset.basePrice); + + option.classList.toggle('selected'); + + if (option.classList.contains('selected')) { + this.selectedServices.push({ service, price }); + this.animateSelection(option, true); + } else { + this.selectedServices = this.selectedServices.filter(s => s.service !== service); + this.animateSelection(option, false); + } + + this.updateStepButton(); + } + + handleComplexitySelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.complexity-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedComplexity = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + handleTimelineSelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedTimeline = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + animateSelection(element, selected) { + if (selected) { + element.style.borderColor = '#3B82F6'; + element.style.backgroundColor = '#EBF8FF'; + element.style.transform = 'translateY(-2px)'; + element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)'; + } else { + element.style.borderColor = ''; + element.style.backgroundColor = ''; + element.style.transform = ''; + element.style.boxShadow = ''; + } + } + + updateStepButton() { + const step1Button = document.querySelector('#step-1 .next-step'); + const step2Button = document.querySelector('#step-2 .next-step'); + + if (step1Button) { + step1Button.disabled = this.selectedServices.length === 0; + step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6'; + } + + if (step2Button) { + const isValid = this.selectedComplexity && this.selectedTimeline; + step2Button.disabled = !isValid; + step2Button.style.opacity = isValid ? '1' : '0.6'; + } + } + + updateProgressBar() { + const progressBar = document.querySelector('.calculator-progress-bar'); + if (progressBar) { + const progress = (this.currentStep / this.totalSteps) * 100; + progressBar.style.width = `${progress}%`; + progressBar.className = `calculator-progress-bar step-${this.currentStep}`; + } + } + + nextStep() { + if (this.currentStep >= this.totalSteps) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const nextStepElement = currentStepElement.nextElementSibling; + + if (nextStepElement && nextStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in next step + nextStepElement.classList.add('active'); + nextStepElement.style.display = 'block'; + nextStepElement.style.opacity = '0'; + nextStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + nextStepElement.style.opacity = '1'; + nextStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep++; + this.updateProgressBar(); + + if (this.currentStep === 3) { + setTimeout(() => this.calculateFinalPrice(), 300); + } + }, 200); + } + } + + prevStep() { + if (this.currentStep <= 1) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const prevStepElement = currentStepElement.previousElementSibling; + + if (prevStepElement && prevStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in previous step + prevStepElement.classList.add('active'); + prevStepElement.style.display = 'block'; + prevStepElement.style.opacity = '0'; + prevStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + prevStepElement.style.opacity = '1'; + prevStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep--; + this.updateProgressBar(); + }, 200); + } + } + + calculateFinalPrice() { + let total = 0; + + // Calculate base price from services + this.selectedServices.forEach(service => { + total += service.price; + }); + + // Apply complexity multiplier + if (this.selectedComplexity) { + total *= this.selectedComplexity.multiplier; + } + + // Apply timeline multiplier + if (this.selectedTimeline) { + total *= this.selectedTimeline.multiplier; + } + + // Animate price reveal + const priceElement = document.getElementById('final-price'); + if (priceElement) { + priceElement.style.opacity = '0'; + priceElement.style.transform = 'scale(0.8)'; + + setTimeout(() => { + priceElement.textContent = '₩' + total.toLocaleString(); + priceElement.style.opacity = '1'; + priceElement.style.transform = 'scale(1)'; + }, 300); + } + + // Generate summary + setTimeout(() => this.generateSummary(total), 600); + } + + generateSummary(total) { + const summary = document.getElementById('project-summary'); + if (!summary) return; + + let summaryHTML = ''; + + // Services + const servicesLabel = window.calculatorTranslations?.result?.selected_services || 'Selected Services'; + const complexityLabel = window.calculatorTranslations?.result?.complexity || 'Complexity'; + const timelineLabel = window.calculatorTranslations?.result?.timeline || 'Timeline'; + + summaryHTML += `
${servicesLabel}:
`; + this.selectedServices.forEach(service => { + summaryHTML += `
+ + ${this.getServiceName(service.service)} + ₩${service.price.toLocaleString()} +
`; + }); + + // Complexity + if (this.selectedComplexity) { + summaryHTML += `
+ + ${complexityLabel}: + ${this.getComplexityName(this.selectedComplexity.value)} + ×${this.selectedComplexity.multiplier} +
`; + } + + // Timeline + if (this.selectedTimeline) { + summaryHTML += `
+ + ${timelineLabel}: + ${this.getTimelineName(this.selectedTimeline.value)} + ×${this.selectedTimeline.multiplier} +
`; + } + + // Animate summary appearance + summary.style.opacity = '0'; + summary.innerHTML = summaryHTML; + + setTimeout(() => { + summary.style.opacity = '1'; + }, 200); + } + + getServiceName(service) { + if (window.calculatorTranslations && window.calculatorTranslations.services) { + return window.calculatorTranslations.services[service] || service; + } + + const names = { + web: 'Web Development', + mobile: 'Mobile App', + design: 'UI/UX Design', + marketing: 'Digital Marketing' + }; + return names[service] || service; + } + + getComplexityName(complexity) { + if (window.calculatorTranslations && window.calculatorTranslations.complexity) { + return window.calculatorTranslations.complexity[complexity] || complexity; + } + + const names = { + simple: 'Simple', + medium: 'Medium', + complex: 'Complex' + }; + return names[complexity] || complexity; + } + + getTimelineName(timeline) { + if (window.calculatorTranslations && window.calculatorTranslations.timeline) { + return window.calculatorTranslations.timeline[timeline] || timeline; + } + + const names = { + standard: 'Standard', + rush: 'Rush', + extended: 'Extended' + }; + return names[timeline] || timeline; + } + + restart() { + // Reset all selections + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + + // Reset UI + document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Reset steps + document.querySelectorAll('.calculator-step').forEach(step => { + step.classList.remove('active'); + step.style.display = 'none'; + step.style.opacity = '1'; + step.style.transform = 'translateX(0)'; + }); + + // Show first step + const firstStep = document.getElementById('step-1'); + if (firstStep) { + firstStep.classList.add('active'); + firstStep.style.display = 'block'; + } + + this.updateProgressBar(); + this.updateStepButton(); + } +} + +// Initialize calculator when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Theme initialization + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.className = theme === 'dark' ? 'dark' : ''; + + // Initialize calculator + if (document.querySelector('.calculator-step')) { + new ProjectCalculator(); + } +}); \ No newline at end of file diff --git a/.history/public/js/main_20251019161602.js b/.history/public/js/main_20251019161602.js new file mode 100644 index 0000000..0f44325 --- /dev/null +++ b/.history/public/js/main_20251019161602.js @@ -0,0 +1,544 @@ +// Main JavaScript for SmartSolTech Website +document.addEventListener('DOMContentLoaded', function() { + // Initialize AOS (Animate On Scroll) + if (typeof AOS !== 'undefined') { + AOS.init({ + duration: 800, + easing: 'ease-in-out', + once: true, + offset: 100 + }); + } + + // Mobile Navigation Toggle + const mobileMenuButton = document.querySelector('.mobile-menu-button'); + const mobileMenu = document.querySelector('.mobile-menu'); + + if (mobileMenuButton && mobileMenu) { + mobileMenuButton.addEventListener('click', function() { + mobileMenu.classList.toggle('show'); + const isOpen = mobileMenu.classList.contains('show'); + + // Toggle button icon + const icon = mobileMenuButton.querySelector('svg'); + if (icon) { + icon.innerHTML = isOpen + ? '' + : ''; + } + + // Accessibility + mobileMenuButton.setAttribute('aria-expanded', isOpen); + }); + + // Close mobile menu when clicking outside + document.addEventListener('click', function(e) { + if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) { + mobileMenu.classList.remove('show'); + mobileMenuButton.setAttribute('aria-expanded', 'false'); + } + }); + } + + // Navbar Scroll Effect + const navbar = document.querySelector('nav'); + let lastScrollY = window.scrollY; + + window.addEventListener('scroll', function() { + const currentScrollY = window.scrollY; + + if (navbar) { + if (currentScrollY > 100) { + navbar.classList.add('navbar-scrolled'); + } else { + navbar.classList.remove('navbar-scrolled'); + } + + // Hide/show navbar on scroll + if (currentScrollY > lastScrollY && currentScrollY > 200) { + navbar.style.transform = 'translateY(-100%)'; + } else { + navbar.style.transform = 'translateY(0)'; + } + } + + lastScrollY = currentScrollY; + }); + + // Smooth Scrolling for Anchor Links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + const offsetTop = target.offsetTop - (navbar ? navbar.offsetHeight : 0); + window.scrollTo({ + top: offsetTop, + behavior: 'smooth' + }); + } + }); + }); + + // Contact Form Handler + const quickContactForm = document.getElementById('quick-contact-form'); + if (quickContactForm) { + quickContactForm.addEventListener('submit', handleContactSubmit); + } + + const mainContactForm = document.getElementById('contact-form'); + if (mainContactForm) { + mainContactForm.addEventListener('submit', handleContactSubmit); + } + + async function handleContactSubmit(e) { + e.preventDefault(); + + const form = e.target; + const submitButton = form.querySelector('button[type="submit"]'); + const originalText = submitButton.textContent; + + // Show loading state + submitButton.disabled = true; + submitButton.classList.add('btn-loading'); + submitButton.textContent = '전송 중...'; + + try { + const formData = new FormData(form); + const data = Object.fromEntries(formData.entries()); + + const response = await fetch('/api/contact/submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + showNotification('메시지가 성공적으로 전송되었습니다! 곧 연락드리겠습니다.', 'success'); + form.reset(); + } else { + showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); + } + } catch (error) { + console.error('Contact form error:', error); + showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); + } finally { + // Reset button state + submitButton.disabled = false; + submitButton.classList.remove('btn-loading'); + submitButton.textContent = originalText; + } + } + + // Notification System + function showNotification(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` +
+ ${message} + +
+ `; + + // Styles + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + max-width: 400px; + padding: 1rem; + border-radius: 0.5rem; + color: white; + font-weight: 500; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + transform: translateX(100%); + transition: transform 0.3s ease; + ${type === 'success' ? 'background: #10b981;' : ''} + ${type === 'error' ? 'background: #ef4444;' : ''} + ${type === 'info' ? 'background: #3b82f6;' : ''} + `; + + document.body.appendChild(notification); + + // Animate in + setTimeout(() => { + notification.style.transform = 'translateX(0)'; + }, 100); + + // Close button handler + const closeButton = notification.querySelector('.notification-close'); + closeButton.addEventListener('click', () => { + closeNotification(notification); + }); + + // Auto close after 5 seconds + setTimeout(() => { + closeNotification(notification); + }, 5000); + } + + function closeNotification(notification) { + notification.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + } + + // Portfolio Filter (if on portfolio page) + const portfolioFilters = document.querySelectorAll('.portfolio-filter'); + const portfolioItems = document.querySelectorAll('.portfolio-item'); + + portfolioFilters.forEach(filter => { + filter.addEventListener('click', function() { + const category = this.dataset.category; + + // Update active filter + portfolioFilters.forEach(f => f.classList.remove('active')); + this.classList.add('active'); + + // Filter items + portfolioItems.forEach(item => { + if (category === 'all' || item.dataset.category === category) { + item.style.display = 'block'; + item.style.animation = 'fadeIn 0.5s ease'; + } else { + item.style.display = 'none'; + } + }); + }); + }); + + // Image Lazy Loading + const images = document.querySelectorAll('img[data-src]'); + const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src; + img.classList.remove('lazy'); + imageObserver.unobserve(img); + } + }); + }); + + images.forEach(img => imageObserver.observe(img)); + + // Service Worker Registration for PWA + if ('serviceWorker' in navigator && 'PushManager' in window) { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('Service Worker registered successfully:', registration); + }) + .catch(error => { + console.log('Service Worker registration failed:', error); + }); + } + + // Performance Monitoring + if ('performance' in window) { + window.addEventListener('load', () => { + setTimeout(() => { + const perfData = performance.getEntriesByType('navigation')[0]; + const loadTime = perfData.loadEventEnd - perfData.loadEventStart; + + if (loadTime > 3000) { + console.warn('Page load time is slow:', loadTime + 'ms'); + } + }, 1000); + }); + } + + // Cookie Consent (if needed) + function initCookieConsent() { + const consent = localStorage.getItem('cookieConsent'); + if (!consent) { + showCookieConsent(); + } + } + + function showCookieConsent() { + const banner = document.createElement('div'); + banner.className = 'cookie-consent'; + banner.innerHTML = ` + + `; + + banner.style.cssText = ` + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 1rem; + z-index: 9999; + transform: translateY(100%); + transition: transform 0.3s ease; + `; + + document.body.appendChild(banner); + + setTimeout(() => { + banner.style.transform = 'translateY(0)'; + }, 100); + + document.getElementById('accept-cookies').addEventListener('click', () => { + localStorage.setItem('cookieConsent', 'accepted'); + banner.style.transform = 'translateY(100%)'; + setTimeout(() => banner.remove(), 300); + }); + + document.getElementById('decline-cookies').addEventListener('click', () => { + localStorage.setItem('cookieConsent', 'declined'); + banner.style.transform = 'translateY(100%)'; + setTimeout(() => banner.remove(), 300); + }); + } + + // Initialize cookie consent + // initCookieConsent(); + + // Parallax Effect + const parallaxElements = document.querySelectorAll('.parallax'); + + function updateParallax() { + const scrollY = window.pageYOffset; + + parallaxElements.forEach(element => { + const speed = element.dataset.speed || 0.5; + const yPos = -(scrollY * speed); + element.style.transform = `translateY(${yPos}px)`; + }); + } + + if (parallaxElements.length > 0) { + window.addEventListener('scroll', updateParallax); + } + + // Counter Animation + const counters = document.querySelectorAll('.counter'); + const counterObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + animateCounter(entry.target); + counterObserver.unobserve(entry.target); + } + }); + }); + + counters.forEach(counter => counterObserver.observe(counter)); + + function animateCounter(element) { + const target = parseInt(element.dataset.count); + const duration = 2000; + const start = performance.now(); + + function updateCounter(currentTime) { + const elapsed = currentTime - start; + const progress = Math.min(elapsed / duration, 1); + + const current = Math.floor(progress * target); + element.textContent = current.toLocaleString(); + + if (progress < 1) { + requestAnimationFrame(updateCounter); + } + } + + requestAnimationFrame(updateCounter); + } + + // Typing Effect for Hero Text + const typingElements = document.querySelectorAll('.typing-effect'); + + typingElements.forEach(element => { + const text = element.textContent; + element.textContent = ''; + element.style.borderRight = '2px solid'; + + let i = 0; + const timer = setInterval(() => { + element.textContent += text[i]; + i++; + + if (i >= text.length) { + clearInterval(timer); + element.style.borderRight = 'none'; + } + }, 100); + }); + + // Handle form validation + const forms = document.querySelectorAll('form[data-validate]'); + forms.forEach(form => { + form.addEventListener('submit', function(e) { + if (!validateForm(this)) { + e.preventDefault(); + } + }); + + // Real-time validation + const inputs = form.querySelectorAll('input, textarea'); + inputs.forEach(input => { + input.addEventListener('blur', () => validateField(input)); + input.addEventListener('input', () => clearFieldError(input)); + }); + }); + + function validateForm(form) { + let isValid = true; + const inputs = form.querySelectorAll('input[required], textarea[required]'); + + inputs.forEach(input => { + if (!validateField(input)) { + isValid = false; + } + }); + + return isValid; + } + + function validateField(field) { + const value = field.value.trim(); + const type = field.type; + let isValid = true; + let message = ''; + + // Required field check + if (field.hasAttribute('required') && !value) { + isValid = false; + message = '이 필드는 필수입니다.'; + } + + // Email validation + if (type === 'email' && value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + isValid = false; + message = '올바른 이메일 형식을 입력해주세요.'; + } + } + + // Phone validation + if (type === 'tel' && value) { + const phoneRegex = /^[0-9-+\s()]+$/; + if (!phoneRegex.test(value)) { + isValid = false; + message = '올바른 전화번호 형식을 입력해주세요.'; + } + } + + // Show/hide error + if (!isValid) { + showFieldError(field, message); + } else { + clearFieldError(field); + } + + return isValid; + } + + function showFieldError(field, message) { + clearFieldError(field); + + field.classList.add('error'); + const errorDiv = document.createElement('div'); + errorDiv.className = 'field-error'; + errorDiv.textContent = message; + errorDiv.style.cssText = 'color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;'; + + field.parentNode.appendChild(errorDiv); + } + + function clearFieldError(field) { + field.classList.remove('error'); + const errorDiv = field.parentNode.querySelector('.field-error'); + if (errorDiv) { + errorDiv.remove(); + } + } +}); + +// Utility Functions +const utils = { + // Debounce function + debounce: function(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + // Throttle function + throttle: function(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + }, + + // Format currency + formatCurrency: function(amount, currency = 'KRW') { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: currency, + minimumFractionDigits: 0 + }).format(amount); + }, + + // Format date + formatDate: function(date, options = {}) { + const defaultOptions = { + year: 'numeric', + month: 'long', + day: 'numeric' + }; + return new Intl.DateTimeFormat('ko-KR', { ...defaultOptions, ...options }).format(new Date(date)); + } +}; + +// Global error handler +window.addEventListener('error', function(e) { + console.error('Global error:', e.error); + // Could send error to analytics service +}); + +// Unhandled promise rejection handler +window.addEventListener('unhandledrejection', function(e) { + console.error('Unhandled promise rejection:', e.reason); + // Could send error to analytics service +}); + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = utils; +} \ No newline at end of file diff --git a/.history/public/js/main_20251019162545.js b/.history/public/js/main_20251019162545.js new file mode 100644 index 0000000..0f44325 --- /dev/null +++ b/.history/public/js/main_20251019162545.js @@ -0,0 +1,544 @@ +// Main JavaScript for SmartSolTech Website +document.addEventListener('DOMContentLoaded', function() { + // Initialize AOS (Animate On Scroll) + if (typeof AOS !== 'undefined') { + AOS.init({ + duration: 800, + easing: 'ease-in-out', + once: true, + offset: 100 + }); + } + + // Mobile Navigation Toggle + const mobileMenuButton = document.querySelector('.mobile-menu-button'); + const mobileMenu = document.querySelector('.mobile-menu'); + + if (mobileMenuButton && mobileMenu) { + mobileMenuButton.addEventListener('click', function() { + mobileMenu.classList.toggle('show'); + const isOpen = mobileMenu.classList.contains('show'); + + // Toggle button icon + const icon = mobileMenuButton.querySelector('svg'); + if (icon) { + icon.innerHTML = isOpen + ? '' + : ''; + } + + // Accessibility + mobileMenuButton.setAttribute('aria-expanded', isOpen); + }); + + // Close mobile menu when clicking outside + document.addEventListener('click', function(e) { + if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) { + mobileMenu.classList.remove('show'); + mobileMenuButton.setAttribute('aria-expanded', 'false'); + } + }); + } + + // Navbar Scroll Effect + const navbar = document.querySelector('nav'); + let lastScrollY = window.scrollY; + + window.addEventListener('scroll', function() { + const currentScrollY = window.scrollY; + + if (navbar) { + if (currentScrollY > 100) { + navbar.classList.add('navbar-scrolled'); + } else { + navbar.classList.remove('navbar-scrolled'); + } + + // Hide/show navbar on scroll + if (currentScrollY > lastScrollY && currentScrollY > 200) { + navbar.style.transform = 'translateY(-100%)'; + } else { + navbar.style.transform = 'translateY(0)'; + } + } + + lastScrollY = currentScrollY; + }); + + // Smooth Scrolling for Anchor Links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + const offsetTop = target.offsetTop - (navbar ? navbar.offsetHeight : 0); + window.scrollTo({ + top: offsetTop, + behavior: 'smooth' + }); + } + }); + }); + + // Contact Form Handler + const quickContactForm = document.getElementById('quick-contact-form'); + if (quickContactForm) { + quickContactForm.addEventListener('submit', handleContactSubmit); + } + + const mainContactForm = document.getElementById('contact-form'); + if (mainContactForm) { + mainContactForm.addEventListener('submit', handleContactSubmit); + } + + async function handleContactSubmit(e) { + e.preventDefault(); + + const form = e.target; + const submitButton = form.querySelector('button[type="submit"]'); + const originalText = submitButton.textContent; + + // Show loading state + submitButton.disabled = true; + submitButton.classList.add('btn-loading'); + submitButton.textContent = '전송 중...'; + + try { + const formData = new FormData(form); + const data = Object.fromEntries(formData.entries()); + + const response = await fetch('/api/contact/submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + showNotification('메시지가 성공적으로 전송되었습니다! 곧 연락드리겠습니다.', 'success'); + form.reset(); + } else { + showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); + } + } catch (error) { + console.error('Contact form error:', error); + showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); + } finally { + // Reset button state + submitButton.disabled = false; + submitButton.classList.remove('btn-loading'); + submitButton.textContent = originalText; + } + } + + // Notification System + function showNotification(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` +
+ ${message} + +
+ `; + + // Styles + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + max-width: 400px; + padding: 1rem; + border-radius: 0.5rem; + color: white; + font-weight: 500; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + transform: translateX(100%); + transition: transform 0.3s ease; + ${type === 'success' ? 'background: #10b981;' : ''} + ${type === 'error' ? 'background: #ef4444;' : ''} + ${type === 'info' ? 'background: #3b82f6;' : ''} + `; + + document.body.appendChild(notification); + + // Animate in + setTimeout(() => { + notification.style.transform = 'translateX(0)'; + }, 100); + + // Close button handler + const closeButton = notification.querySelector('.notification-close'); + closeButton.addEventListener('click', () => { + closeNotification(notification); + }); + + // Auto close after 5 seconds + setTimeout(() => { + closeNotification(notification); + }, 5000); + } + + function closeNotification(notification) { + notification.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + } + + // Portfolio Filter (if on portfolio page) + const portfolioFilters = document.querySelectorAll('.portfolio-filter'); + const portfolioItems = document.querySelectorAll('.portfolio-item'); + + portfolioFilters.forEach(filter => { + filter.addEventListener('click', function() { + const category = this.dataset.category; + + // Update active filter + portfolioFilters.forEach(f => f.classList.remove('active')); + this.classList.add('active'); + + // Filter items + portfolioItems.forEach(item => { + if (category === 'all' || item.dataset.category === category) { + item.style.display = 'block'; + item.style.animation = 'fadeIn 0.5s ease'; + } else { + item.style.display = 'none'; + } + }); + }); + }); + + // Image Lazy Loading + const images = document.querySelectorAll('img[data-src]'); + const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src; + img.classList.remove('lazy'); + imageObserver.unobserve(img); + } + }); + }); + + images.forEach(img => imageObserver.observe(img)); + + // Service Worker Registration for PWA + if ('serviceWorker' in navigator && 'PushManager' in window) { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('Service Worker registered successfully:', registration); + }) + .catch(error => { + console.log('Service Worker registration failed:', error); + }); + } + + // Performance Monitoring + if ('performance' in window) { + window.addEventListener('load', () => { + setTimeout(() => { + const perfData = performance.getEntriesByType('navigation')[0]; + const loadTime = perfData.loadEventEnd - perfData.loadEventStart; + + if (loadTime > 3000) { + console.warn('Page load time is slow:', loadTime + 'ms'); + } + }, 1000); + }); + } + + // Cookie Consent (if needed) + function initCookieConsent() { + const consent = localStorage.getItem('cookieConsent'); + if (!consent) { + showCookieConsent(); + } + } + + function showCookieConsent() { + const banner = document.createElement('div'); + banner.className = 'cookie-consent'; + banner.innerHTML = ` + + `; + + banner.style.cssText = ` + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 1rem; + z-index: 9999; + transform: translateY(100%); + transition: transform 0.3s ease; + `; + + document.body.appendChild(banner); + + setTimeout(() => { + banner.style.transform = 'translateY(0)'; + }, 100); + + document.getElementById('accept-cookies').addEventListener('click', () => { + localStorage.setItem('cookieConsent', 'accepted'); + banner.style.transform = 'translateY(100%)'; + setTimeout(() => banner.remove(), 300); + }); + + document.getElementById('decline-cookies').addEventListener('click', () => { + localStorage.setItem('cookieConsent', 'declined'); + banner.style.transform = 'translateY(100%)'; + setTimeout(() => banner.remove(), 300); + }); + } + + // Initialize cookie consent + // initCookieConsent(); + + // Parallax Effect + const parallaxElements = document.querySelectorAll('.parallax'); + + function updateParallax() { + const scrollY = window.pageYOffset; + + parallaxElements.forEach(element => { + const speed = element.dataset.speed || 0.5; + const yPos = -(scrollY * speed); + element.style.transform = `translateY(${yPos}px)`; + }); + } + + if (parallaxElements.length > 0) { + window.addEventListener('scroll', updateParallax); + } + + // Counter Animation + const counters = document.querySelectorAll('.counter'); + const counterObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + animateCounter(entry.target); + counterObserver.unobserve(entry.target); + } + }); + }); + + counters.forEach(counter => counterObserver.observe(counter)); + + function animateCounter(element) { + const target = parseInt(element.dataset.count); + const duration = 2000; + const start = performance.now(); + + function updateCounter(currentTime) { + const elapsed = currentTime - start; + const progress = Math.min(elapsed / duration, 1); + + const current = Math.floor(progress * target); + element.textContent = current.toLocaleString(); + + if (progress < 1) { + requestAnimationFrame(updateCounter); + } + } + + requestAnimationFrame(updateCounter); + } + + // Typing Effect for Hero Text + const typingElements = document.querySelectorAll('.typing-effect'); + + typingElements.forEach(element => { + const text = element.textContent; + element.textContent = ''; + element.style.borderRight = '2px solid'; + + let i = 0; + const timer = setInterval(() => { + element.textContent += text[i]; + i++; + + if (i >= text.length) { + clearInterval(timer); + element.style.borderRight = 'none'; + } + }, 100); + }); + + // Handle form validation + const forms = document.querySelectorAll('form[data-validate]'); + forms.forEach(form => { + form.addEventListener('submit', function(e) { + if (!validateForm(this)) { + e.preventDefault(); + } + }); + + // Real-time validation + const inputs = form.querySelectorAll('input, textarea'); + inputs.forEach(input => { + input.addEventListener('blur', () => validateField(input)); + input.addEventListener('input', () => clearFieldError(input)); + }); + }); + + function validateForm(form) { + let isValid = true; + const inputs = form.querySelectorAll('input[required], textarea[required]'); + + inputs.forEach(input => { + if (!validateField(input)) { + isValid = false; + } + }); + + return isValid; + } + + function validateField(field) { + const value = field.value.trim(); + const type = field.type; + let isValid = true; + let message = ''; + + // Required field check + if (field.hasAttribute('required') && !value) { + isValid = false; + message = '이 필드는 필수입니다.'; + } + + // Email validation + if (type === 'email' && value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + isValid = false; + message = '올바른 이메일 형식을 입력해주세요.'; + } + } + + // Phone validation + if (type === 'tel' && value) { + const phoneRegex = /^[0-9-+\s()]+$/; + if (!phoneRegex.test(value)) { + isValid = false; + message = '올바른 전화번호 형식을 입력해주세요.'; + } + } + + // Show/hide error + if (!isValid) { + showFieldError(field, message); + } else { + clearFieldError(field); + } + + return isValid; + } + + function showFieldError(field, message) { + clearFieldError(field); + + field.classList.add('error'); + const errorDiv = document.createElement('div'); + errorDiv.className = 'field-error'; + errorDiv.textContent = message; + errorDiv.style.cssText = 'color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;'; + + field.parentNode.appendChild(errorDiv); + } + + function clearFieldError(field) { + field.classList.remove('error'); + const errorDiv = field.parentNode.querySelector('.field-error'); + if (errorDiv) { + errorDiv.remove(); + } + } +}); + +// Utility Functions +const utils = { + // Debounce function + debounce: function(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + // Throttle function + throttle: function(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + }, + + // Format currency + formatCurrency: function(amount, currency = 'KRW') { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: currency, + minimumFractionDigits: 0 + }).format(amount); + }, + + // Format date + formatDate: function(date, options = {}) { + const defaultOptions = { + year: 'numeric', + month: 'long', + day: 'numeric' + }; + return new Intl.DateTimeFormat('ko-KR', { ...defaultOptions, ...options }).format(new Date(date)); + } +}; + +// Global error handler +window.addEventListener('error', function(e) { + console.error('Global error:', e.error); + // Could send error to analytics service +}); + +// Unhandled promise rejection handler +window.addEventListener('unhandledrejection', function(e) { + console.error('Unhandled promise rejection:', e.reason); + // Could send error to analytics service +}); + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = utils; +} \ No newline at end of file diff --git a/.history/public/manifest_20251019161703.json b/.history/public/manifest_20251019161703.json new file mode 100644 index 0000000..c493fcf --- /dev/null +++ b/.history/public/manifest_20251019161703.json @@ -0,0 +1,161 @@ +{ + "name": "SmartSolTech - Technology Solutions", + "short_name": "SmartSolTech", + "description": "Professional web development, mobile apps, and digital solutions in Korea", + "start_url": "/", + "display": "standalone", + "orientation": "portrait-primary", + "theme_color": "#3b82f6", + "background_color": "#ffffff", + "lang": "ko", + "scope": "/", + "categories": ["business", "productivity", "technology"], + "icons": [ + { + "src": "/images/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "screenshots": [ + { + "src": "/images/screenshot-desktop.png", + "sizes": "1280x720", + "type": "image/png", + "form_factor": "wide", + "label": "SmartSolTech Desktop View" + }, + { + "src": "/images/screenshot-mobile.png", + "sizes": "375x812", + "type": "image/png", + "form_factor": "narrow", + "label": "SmartSolTech Mobile View" + } + ], + "shortcuts": [ + { + "name": "Portfolio", + "short_name": "Portfolio", + "description": "View our latest projects", + "url": "/portfolio", + "icons": [ + { + "src": "/images/shortcut-portfolio.png", + "sizes": "96x96", + "type": "image/png" + } + ] + }, + { + "name": "Calculator", + "short_name": "Calculator", + "description": "Calculate project costs", + "url": "/calculator", + "icons": [ + { + "src": "/images/shortcut-calculator.png", + "sizes": "96x96", + "type": "image/png" + } + ] + }, + { + "name": "Contact", + "short_name": "Contact", + "description": "Get in touch with us", + "url": "/contact", + "icons": [ + { + "src": "/images/shortcut-contact.png", + "sizes": "96x96", + "type": "image/png" + } + ] + } + ], + "related_applications": [ + { + "platform": "webapp", + "url": "https://smartsoltech.kr/manifest.json" + } + ], + "prefer_related_applications": false, + "edge_side_panel": { + "preferred_width": 400 + }, + "protocol_handlers": [ + { + "protocol": "mailto", + "url": "/contact?email=%s" + } + ], + "file_handlers": [ + { + "action": "/open-file", + "accept": { + "image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"], + "application/pdf": [".pdf"] + } + } + ], + "share_target": { + "action": "/share", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [ + { + "name": "images", + "accept": ["image/*"] + } + ] + } + } +} \ No newline at end of file diff --git a/.history/public/manifest_20251019162545.json b/.history/public/manifest_20251019162545.json new file mode 100644 index 0000000..c493fcf --- /dev/null +++ b/.history/public/manifest_20251019162545.json @@ -0,0 +1,161 @@ +{ + "name": "SmartSolTech - Technology Solutions", + "short_name": "SmartSolTech", + "description": "Professional web development, mobile apps, and digital solutions in Korea", + "start_url": "/", + "display": "standalone", + "orientation": "portrait-primary", + "theme_color": "#3b82f6", + "background_color": "#ffffff", + "lang": "ko", + "scope": "/", + "categories": ["business", "productivity", "technology"], + "icons": [ + { + "src": "/images/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "screenshots": [ + { + "src": "/images/screenshot-desktop.png", + "sizes": "1280x720", + "type": "image/png", + "form_factor": "wide", + "label": "SmartSolTech Desktop View" + }, + { + "src": "/images/screenshot-mobile.png", + "sizes": "375x812", + "type": "image/png", + "form_factor": "narrow", + "label": "SmartSolTech Mobile View" + } + ], + "shortcuts": [ + { + "name": "Portfolio", + "short_name": "Portfolio", + "description": "View our latest projects", + "url": "/portfolio", + "icons": [ + { + "src": "/images/shortcut-portfolio.png", + "sizes": "96x96", + "type": "image/png" + } + ] + }, + { + "name": "Calculator", + "short_name": "Calculator", + "description": "Calculate project costs", + "url": "/calculator", + "icons": [ + { + "src": "/images/shortcut-calculator.png", + "sizes": "96x96", + "type": "image/png" + } + ] + }, + { + "name": "Contact", + "short_name": "Contact", + "description": "Get in touch with us", + "url": "/contact", + "icons": [ + { + "src": "/images/shortcut-contact.png", + "sizes": "96x96", + "type": "image/png" + } + ] + } + ], + "related_applications": [ + { + "platform": "webapp", + "url": "https://smartsoltech.kr/manifest.json" + } + ], + "prefer_related_applications": false, + "edge_side_panel": { + "preferred_width": 400 + }, + "protocol_handlers": [ + { + "protocol": "mailto", + "url": "/contact?email=%s" + } + ], + "file_handlers": [ + { + "action": "/open-file", + "accept": { + "image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"], + "application/pdf": [".pdf"] + } + } + ], + "share_target": { + "action": "/share", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [ + { + "name": "images", + "accept": ["image/*"] + } + ] + } + } +} \ No newline at end of file diff --git a/.history/public/sw_20251019161644.js b/.history/public/sw_20251019161644.js new file mode 100644 index 0000000..4cc2eef --- /dev/null +++ b/.history/public/sw_20251019161644.js @@ -0,0 +1,396 @@ +// Service Worker for SmartSolTech PWA +const CACHE_NAME = 'smartsoltech-v1.0.0'; +const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.0'; +const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.0'; + +// Files to cache immediately +const STATIC_FILES = [ + '/', + '/css/main.css', + '/js/main.js', + '/images/logo.png', + '/images/icon-192x192.png', + '/images/icon-512x512.png', + '/manifest.json', + 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap', + 'https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css', + 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css', + 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/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)) { + event.respondWith(staleWhileRevalidate(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) { + const cache = await caches.open(DYNAMIC_CACHE_NAME); + const cachedResponse = await cache.match(request); + + const fetchPromise = fetch(request).then(networkResponse => { + if (networkResponse.ok) { + cache.put(request, networkResponse.clone()); + } + return networkResponse; + }); + + return cachedResponse || fetchPromise; +} + +// 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_20251019162545.js b/.history/public/sw_20251019162545.js new file mode 100644 index 0000000..4cc2eef --- /dev/null +++ b/.history/public/sw_20251019162545.js @@ -0,0 +1,396 @@ +// Service Worker for SmartSolTech PWA +const CACHE_NAME = 'smartsoltech-v1.0.0'; +const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.0'; +const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.0'; + +// Files to cache immediately +const STATIC_FILES = [ + '/', + '/css/main.css', + '/js/main.js', + '/images/logo.png', + '/images/icon-192x192.png', + '/images/icon-512x512.png', + '/manifest.json', + 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap', + 'https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css', + 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css', + 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/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)) { + event.respondWith(staleWhileRevalidate(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) { + const cache = await caches.open(DYNAMIC_CACHE_NAME); + const cachedResponse = await cache.match(request); + + const fetchPromise = fetch(request).then(networkResponse => { + if (networkResponse.ok) { + cache.put(request, networkResponse.clone()); + } + return networkResponse; + }); + + return cachedResponse || fetchPromise; +} + +// 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_20251019160958.js b/.history/routes/admin_20251019160958.js new file mode 100644 index 0000000..b4c8e82 --- /dev/null +++ b/.history/routes/admin_20251019160958.js @@ -0,0 +1,387 @@ +const express = require('express'); +const router = express.Router(); +const { body, validationResult } = require('express-validator'); +const User = require('../models/User'); +const Portfolio = require('../models/Portfolio'); +const Service = require('../models/Service'); +const Contact = require('../models/Contact'); +const SiteSettings = require('../models/SiteSettings'); + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.redirect('/admin/login'); + } + 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 - SmartSolTech', + layout: 'admin/layout', + error: null + }); +}); + +// Admin login POST +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + const user = await User.findOne({ email, isActive: true }); + if (!user || !(await user.comparePassword(password))) { + return res.render('admin/login', { + title: 'Admin Login - SmartSolTech', + layout: 'admin/layout', + error: 'Invalid email or password' + }); + } + + 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 - SmartSolTech', + layout: 'admin/layout', + error: 'An error occurred. Please try again.' + }); + } +}); + +// Admin logout +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + console.error('Logout error:', err); + } + res.redirect('/admin/login'); + }); +}); + +// Dashboard +router.get('/dashboard', requireAuth, async (req, res) => { + try { + const [ + portfolioCount, + servicesCount, + contactsCount, + recentContacts, + recentPortfolio + ] = await Promise.all([ + Portfolio.countDocuments({ isPublished: true }), + Service.countDocuments({ isActive: true }), + Contact.countDocuments(), + Contact.find().sort({ createdAt: -1 }).limit(5), + Portfolio.find({ isPublished: true }).sort({ createdAt: -1 }).limit(5) + ]); + + const stats = { + portfolio: portfolioCount, + services: servicesCount, + contacts: contactsCount, + unreadContacts: await Contact.countDocuments({ isRead: false }) + }; + + res.render('admin/dashboard', { + title: 'Dashboard - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + stats, + recentContacts, + recentPortfolio + }); + } catch (error) { + console.error('Dashboard error:', error); + res.status(500).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Error loading dashboard' + }); + } +}); + +// 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.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Portfolio.countDocuments() + ]); + + 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' + }); + } +}); + +// 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 + }); +}); + +// Edit portfolio item +router.get('/portfolio/edit/:id', requireAuth, async (req, res) => { + try { + const portfolio = await Portfolio.findById(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 + }); + } 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' + }); + } +}); + +// 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.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Service.countDocuments() + ]); + + 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 + }); +}); + +// Edit service +router.get('/services/edit/:id', requireAuth, async (req, res) => { + try { + const service = await Service.findById(req.params.id) + .populate('portfolio', 'title'); + + if (!service) { + return res.status(404).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Service not found' + }); + } + + const availablePortfolio = await Portfolio.find({ isPublished: true }) + .select('title category'); + + res.render('admin/services/edit', { + title: 'Edit Service - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + service, + availablePortfolio + }); + } 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' + }); + } +}); + +// 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 query = {}; + if (status && status !== 'all') { + query.status = status; + } + + const [contacts, total] = await Promise.all([ + Contact.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Contact.countDocuments(query) + ]); + + 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.findById(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() || new SiteSettings(); + + 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 + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/admin_20251019162544.js b/.history/routes/admin_20251019162544.js new file mode 100644 index 0000000..b4c8e82 --- /dev/null +++ b/.history/routes/admin_20251019162544.js @@ -0,0 +1,387 @@ +const express = require('express'); +const router = express.Router(); +const { body, validationResult } = require('express-validator'); +const User = require('../models/User'); +const Portfolio = require('../models/Portfolio'); +const Service = require('../models/Service'); +const Contact = require('../models/Contact'); +const SiteSettings = require('../models/SiteSettings'); + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.redirect('/admin/login'); + } + 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 - SmartSolTech', + layout: 'admin/layout', + error: null + }); +}); + +// Admin login POST +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + const user = await User.findOne({ email, isActive: true }); + if (!user || !(await user.comparePassword(password))) { + return res.render('admin/login', { + title: 'Admin Login - SmartSolTech', + layout: 'admin/layout', + error: 'Invalid email or password' + }); + } + + 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 - SmartSolTech', + layout: 'admin/layout', + error: 'An error occurred. Please try again.' + }); + } +}); + +// Admin logout +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + console.error('Logout error:', err); + } + res.redirect('/admin/login'); + }); +}); + +// Dashboard +router.get('/dashboard', requireAuth, async (req, res) => { + try { + const [ + portfolioCount, + servicesCount, + contactsCount, + recentContacts, + recentPortfolio + ] = await Promise.all([ + Portfolio.countDocuments({ isPublished: true }), + Service.countDocuments({ isActive: true }), + Contact.countDocuments(), + Contact.find().sort({ createdAt: -1 }).limit(5), + Portfolio.find({ isPublished: true }).sort({ createdAt: -1 }).limit(5) + ]); + + const stats = { + portfolio: portfolioCount, + services: servicesCount, + contacts: contactsCount, + unreadContacts: await Contact.countDocuments({ isRead: false }) + }; + + res.render('admin/dashboard', { + title: 'Dashboard - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + stats, + recentContacts, + recentPortfolio + }); + } catch (error) { + console.error('Dashboard error:', error); + res.status(500).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Error loading dashboard' + }); + } +}); + +// 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.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Portfolio.countDocuments() + ]); + + 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' + }); + } +}); + +// 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 + }); +}); + +// Edit portfolio item +router.get('/portfolio/edit/:id', requireAuth, async (req, res) => { + try { + const portfolio = await Portfolio.findById(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 + }); + } 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' + }); + } +}); + +// 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.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Service.countDocuments() + ]); + + 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 + }); +}); + +// Edit service +router.get('/services/edit/:id', requireAuth, async (req, res) => { + try { + const service = await Service.findById(req.params.id) + .populate('portfolio', 'title'); + + if (!service) { + return res.status(404).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Service not found' + }); + } + + const availablePortfolio = await Portfolio.find({ isPublished: true }) + .select('title category'); + + res.render('admin/services/edit', { + title: 'Edit Service - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + service, + availablePortfolio + }); + } 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' + }); + } +}); + +// 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 query = {}; + if (status && status !== 'all') { + query.status = status; + } + + const [contacts, total] = await Promise.all([ + Contact.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Contact.countDocuments(query) + ]); + + 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.findById(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() || new SiteSettings(); + + 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 + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/auth_20251019160707.js b/.history/routes/auth_20251019160707.js new file mode 100644 index 0000000..de4d91e --- /dev/null +++ b/.history/routes/auth_20251019160707.js @@ -0,0 +1,201 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const { body, validationResult } = require('express-validator'); +const User = require('../models/User'); + +// Login validation rules +const loginValidation = [ + body('email').isEmail().normalizeEmail(), + body('password').isLength({ min: 6 }) +]; + +// Login +router.post('/login', loginValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { email, password } = req.body; + + // Find user + const user = await User.findOne({ email, isActive: true }); + if (!user) { + return res.status(401).json({ + success: false, + message: 'Invalid credentials' + }); + } + + // Check password + const isValidPassword = await user.comparePassword(password); + if (!isValidPassword) { + return res.status(401).json({ + success: false, + message: 'Invalid credentials' + }); + } + + // Update last login + await user.updateLastLogin(); + + // Create JWT token + const token = jwt.sign( + { userId: user._id, email: user.email, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + // Set session + req.session.user = { + id: user._id, + email: user.email, + name: user.name, + role: user.role + }; + + res.json({ + success: true, + message: 'Login successful', + token, + user: { + id: user._id, + email: user.email, + name: user.name, + role: user.role, + avatar: user.avatar + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +// Logout +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + return res.status(500).json({ + success: false, + message: 'Could not log out' + }); + } + + res.clearCookie('connect.sid'); + res.json({ + success: true, + message: 'Logout successful' + }); + }); +}); + +// Check authentication status +router.get('/me', async (req, res) => { + try { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Not authenticated' + }); + } + + const user = await User.findById(req.session.user.id) + .select('-password'); + + if (!user || !user.isActive) { + req.session.destroy(); + return res.status(401).json({ + success: false, + message: 'User not found or inactive' + }); + } + + res.json({ + success: true, + user: { + id: user._id, + email: user.email, + name: user.name, + role: user.role, + avatar: user.avatar, + lastLogin: user.lastLogin + } + }); + } catch (error) { + console.error('Auth check error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +// Change password +router.put('/change-password', [ + body('currentPassword').isLength({ min: 6 }), + body('newPassword').isLength({ min: 6 }) +], async (req, res) => { + try { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Not authenticated' + }); + } + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { currentPassword, newPassword } = req.body; + const user = await User.findById(req.session.user.id); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + // Verify current password + const isValidPassword = await user.comparePassword(currentPassword); + if (!isValidPassword) { + return res.status(400).json({ + success: false, + message: 'Current password is incorrect' + }); + } + + // Update password + user.password = newPassword; + await user.save(); + + res.json({ + success: true, + message: 'Password updated successfully' + }); + } catch (error) { + console.error('Change password error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/auth_20251019162544.js b/.history/routes/auth_20251019162544.js new file mode 100644 index 0000000..de4d91e --- /dev/null +++ b/.history/routes/auth_20251019162544.js @@ -0,0 +1,201 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const { body, validationResult } = require('express-validator'); +const User = require('../models/User'); + +// Login validation rules +const loginValidation = [ + body('email').isEmail().normalizeEmail(), + body('password').isLength({ min: 6 }) +]; + +// Login +router.post('/login', loginValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { email, password } = req.body; + + // Find user + const user = await User.findOne({ email, isActive: true }); + if (!user) { + return res.status(401).json({ + success: false, + message: 'Invalid credentials' + }); + } + + // Check password + const isValidPassword = await user.comparePassword(password); + if (!isValidPassword) { + return res.status(401).json({ + success: false, + message: 'Invalid credentials' + }); + } + + // Update last login + await user.updateLastLogin(); + + // Create JWT token + const token = jwt.sign( + { userId: user._id, email: user.email, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + // Set session + req.session.user = { + id: user._id, + email: user.email, + name: user.name, + role: user.role + }; + + res.json({ + success: true, + message: 'Login successful', + token, + user: { + id: user._id, + email: user.email, + name: user.name, + role: user.role, + avatar: user.avatar + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +// Logout +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + return res.status(500).json({ + success: false, + message: 'Could not log out' + }); + } + + res.clearCookie('connect.sid'); + res.json({ + success: true, + message: 'Logout successful' + }); + }); +}); + +// Check authentication status +router.get('/me', async (req, res) => { + try { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Not authenticated' + }); + } + + const user = await User.findById(req.session.user.id) + .select('-password'); + + if (!user || !user.isActive) { + req.session.destroy(); + return res.status(401).json({ + success: false, + message: 'User not found or inactive' + }); + } + + res.json({ + success: true, + user: { + id: user._id, + email: user.email, + name: user.name, + role: user.role, + avatar: user.avatar, + lastLogin: user.lastLogin + } + }); + } catch (error) { + console.error('Auth check error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +// Change password +router.put('/change-password', [ + body('currentPassword').isLength({ min: 6 }), + body('newPassword').isLength({ min: 6 }) +], async (req, res) => { + try { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Not authenticated' + }); + } + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { currentPassword, newPassword } = req.body; + const user = await User.findById(req.session.user.id); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + // Verify current password + const isValidPassword = await user.comparePassword(currentPassword); + if (!isValidPassword) { + return res.status(400).json({ + success: false, + message: 'Current password is incorrect' + }); + } + + // Update password + user.password = newPassword; + await user.save(); + + res.json({ + success: true, + message: 'Password updated successfully' + }); + } catch (error) { + console.error('Change password error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/calculator_20251019160816.js b/.history/routes/calculator_20251019160816.js new file mode 100644 index 0000000..90559e4 --- /dev/null +++ b/.history/routes/calculator_20251019160816.js @@ -0,0 +1,312 @@ +const express = require('express'); +const router = express.Router(); +const Service = require('../models/Service'); + +// Get all services for calculator +router.get('/services', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .select('name pricing category features estimatedTime') + .sort({ category: 1, name: 1 }); + + const servicesByCategory = services.reduce((acc, service) => { + if (!acc[service.category]) { + acc[service.category] = []; + } + acc[service.category].push(service); + return acc; + }, {}); + + res.json({ + success: true, + services: servicesByCategory, + allServices: services + }); + } catch (error) { + console.error('Calculator services error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching services' + }); + } +}); + +// Calculate project estimate +router.post('/calculate', async (req, res) => { + try { + const { + selectedServices = [], + projectComplexity = 'medium', + timeline = 'standard', + additionalFeatures = [], + customRequirements = '' + } = req.body; + + if (!selectedServices.length) { + return res.status(400).json({ + success: false, + message: 'Please select at least one service' + }); + } + + // Get selected services details + const services = await Service.find({ + _id: { $in: selectedServices }, + isActive: true + }); + + if (services.length !== selectedServices.length) { + return res.status(400).json({ + success: false, + message: 'Some selected services are not available' + }); + } + + // Calculate base cost + let baseCost = 0; + let totalTime = 0; // in days + + services.forEach(service => { + baseCost += service.pricing.basePrice; + totalTime += (service.estimatedTime.min + service.estimatedTime.max) / 2; + }); + + // Apply complexity multiplier + const complexityMultipliers = { + 'simple': 0.7, + 'medium': 1.0, + 'complex': 1.5, + 'enterprise': 2.0 + }; + + const complexityMultiplier = complexityMultipliers[projectComplexity] || 1.0; + + // Apply timeline multiplier + const timelineMultipliers = { + 'rush': 1.8, // Less than 2 weeks + 'fast': 1.4, // 2-4 weeks + 'standard': 1.0, // 1-3 months + 'flexible': 0.8 // 3+ months + }; + + const timelineMultiplier = timelineMultipliers[timeline] || 1.0; + + // Additional features cost + const featureCosts = { + 'seo-optimization': 300000, + 'analytics-setup': 150000, + 'social-integration': 200000, + 'payment-gateway': 500000, + 'multilingual': 400000, + 'admin-panel': 600000, + 'api-integration': 350000, + 'mobile-responsive': 250000, + 'ssl-certificate': 100000, + 'backup-system': 200000 + }; + + let additionalCost = 0; + additionalFeatures.forEach(feature => { + additionalCost += featureCosts[feature] || 0; + }); + + // Custom requirements cost (estimated based on description length and complexity) + let customCost = 0; + if (customRequirements && customRequirements.length > 50) { + customCost = Math.min(customRequirements.length * 1000, 1000000); // Max 1M KRW + } + + // Calculate final estimate + const subtotal = baseCost * complexityMultiplier * timelineMultiplier; + const total = subtotal + additionalCost + customCost; + + // Calculate time estimate + const timeMultiplier = complexityMultiplier * timelineMultiplier; + const estimatedDays = Math.ceil(totalTime * timeMultiplier); + const estimatedWeeks = Math.ceil(estimatedDays / 7); + + // Create price ranges (±20%) + const minPrice = Math.round(total * 0.8); + const maxPrice = Math.round(total * 1.2); + + // Breakdown + const breakdown = { + baseServices: Math.round(baseCost), + complexityAdjustment: Math.round(baseCost * (complexityMultiplier - 1)), + timelineAdjustment: Math.round(baseCost * complexityMultiplier * (timelineMultiplier - 1)), + additionalFeatures: additionalCost, + customRequirements: customCost, + subtotal: Math.round(subtotal), + total: Math.round(total) + }; + + const result = { + success: true, + estimate: { + total: Math.round(total), + range: { + min: minPrice, + max: maxPrice, + formatted: `₩${minPrice.toLocaleString()} - ₩${maxPrice.toLocaleString()}` + }, + breakdown, + timeline: { + days: estimatedDays, + weeks: estimatedWeeks, + formatted: estimatedWeeks === 1 ? '1 week' : `${estimatedWeeks} weeks` + }, + currency: 'KRW', + confidence: calculateConfidence(services.length, projectComplexity, customRequirements), + recommendations: generateRecommendations(projectComplexity, timeline, services) + }, + selectedServices: services.map(s => ({ + id: s._id, + name: s.name, + category: s.category, + basePrice: s.pricing.basePrice + })), + calculatedAt: new Date() + }; + + res.json(result); + } catch (error) { + console.error('Calculator calculation error:', error); + res.status(500).json({ + success: false, + message: 'Error calculating estimate' + }); + } +}); + +// Helper function to calculate confidence level +function calculateConfidence(serviceCount, complexity, customRequirements) { + let confidence = 85; // Base confidence + + // Adjust based on service count + if (serviceCount === 1) confidence += 10; + else if (serviceCount > 5) confidence -= 10; + + // Adjust based on complexity + if (complexity === 'simple') confidence += 10; + else if (complexity === 'enterprise') confidence -= 15; + + // Adjust based on custom requirements + if (customRequirements && customRequirements.length > 200) { + confidence -= 20; + } + + return Math.max(60, Math.min(95, confidence)); +} + +// Helper function to generate recommendations +function generateRecommendations(complexity, timeline, services) { + const recommendations = []; + + if (complexity === 'enterprise' && timeline === 'rush') { + recommendations.push('Consider extending timeline for enterprise-level projects to ensure quality'); + } + + if (services.length > 6) { + recommendations.push('Consider breaking down into phases for better project management'); + } + + if (timeline === 'flexible') { + recommendations.push('Flexible timeline allows for better cost optimization and quality assurance'); + } + + const hasDesign = services.some(s => s.category === 'design'); + const hasDevelopment = services.some(s => s.category === 'development'); + + if (hasDevelopment && !hasDesign) { + recommendations.push('Consider adding UI/UX design services for better user experience'); + } + + return recommendations; +} + +// Get pricing guidelines +router.get('/pricing-guide', async (req, res) => { + try { + const pricingGuide = { + serviceCategories: { + 'development': { + name: 'Web Development', + priceRange: '₩500,000 - ₩5,000,000', + description: 'Custom web applications and websites' + }, + 'design': { + name: 'UI/UX Design', + priceRange: '₩300,000 - ₩2,000,000', + description: 'User interface and experience design' + }, + 'marketing': { + name: 'Digital Marketing', + priceRange: '₩200,000 - ₩1,500,000', + description: 'SEO, social media, and online marketing' + }, + 'consulting': { + name: 'Technical Consulting', + priceRange: '₩150,000 - ₩1,000,000', + description: 'Technology strategy and consultation' + } + }, + complexityFactors: { + 'simple': { + name: 'Simple Project', + multiplier: '0.7x', + description: 'Basic functionality, standard design' + }, + 'medium': { + name: 'Medium Project', + multiplier: '1.0x', + description: 'Moderate complexity, custom features' + }, + 'complex': { + name: 'Complex Project', + multiplier: '1.5x', + description: 'Advanced features, integrations' + }, + 'enterprise': { + name: 'Enterprise Project', + multiplier: '2.0x', + description: 'Large scale, high complexity' + } + }, + timelineImpact: { + 'rush': { + name: 'Rush (< 2 weeks)', + multiplier: '+80%', + description: 'Requires overtime and priority handling' + }, + 'fast': { + name: 'Fast (2-4 weeks)', + multiplier: '+40%', + description: 'Accelerated timeline' + }, + 'standard': { + name: 'Standard (1-3 months)', + multiplier: 'Standard', + description: 'Normal project timeline' + }, + 'flexible': { + name: 'Flexible (3+ months)', + multiplier: '-20%', + description: 'Extended timeline allows optimization' + } + } + }; + + res.json({ + success: true, + pricingGuide + }); + } catch (error) { + console.error('Pricing guide error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching pricing guide' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/calculator_20251019162544.js b/.history/routes/calculator_20251019162544.js new file mode 100644 index 0000000..90559e4 --- /dev/null +++ b/.history/routes/calculator_20251019162544.js @@ -0,0 +1,312 @@ +const express = require('express'); +const router = express.Router(); +const Service = require('../models/Service'); + +// Get all services for calculator +router.get('/services', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .select('name pricing category features estimatedTime') + .sort({ category: 1, name: 1 }); + + const servicesByCategory = services.reduce((acc, service) => { + if (!acc[service.category]) { + acc[service.category] = []; + } + acc[service.category].push(service); + return acc; + }, {}); + + res.json({ + success: true, + services: servicesByCategory, + allServices: services + }); + } catch (error) { + console.error('Calculator services error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching services' + }); + } +}); + +// Calculate project estimate +router.post('/calculate', async (req, res) => { + try { + const { + selectedServices = [], + projectComplexity = 'medium', + timeline = 'standard', + additionalFeatures = [], + customRequirements = '' + } = req.body; + + if (!selectedServices.length) { + return res.status(400).json({ + success: false, + message: 'Please select at least one service' + }); + } + + // Get selected services details + const services = await Service.find({ + _id: { $in: selectedServices }, + isActive: true + }); + + if (services.length !== selectedServices.length) { + return res.status(400).json({ + success: false, + message: 'Some selected services are not available' + }); + } + + // Calculate base cost + let baseCost = 0; + let totalTime = 0; // in days + + services.forEach(service => { + baseCost += service.pricing.basePrice; + totalTime += (service.estimatedTime.min + service.estimatedTime.max) / 2; + }); + + // Apply complexity multiplier + const complexityMultipliers = { + 'simple': 0.7, + 'medium': 1.0, + 'complex': 1.5, + 'enterprise': 2.0 + }; + + const complexityMultiplier = complexityMultipliers[projectComplexity] || 1.0; + + // Apply timeline multiplier + const timelineMultipliers = { + 'rush': 1.8, // Less than 2 weeks + 'fast': 1.4, // 2-4 weeks + 'standard': 1.0, // 1-3 months + 'flexible': 0.8 // 3+ months + }; + + const timelineMultiplier = timelineMultipliers[timeline] || 1.0; + + // Additional features cost + const featureCosts = { + 'seo-optimization': 300000, + 'analytics-setup': 150000, + 'social-integration': 200000, + 'payment-gateway': 500000, + 'multilingual': 400000, + 'admin-panel': 600000, + 'api-integration': 350000, + 'mobile-responsive': 250000, + 'ssl-certificate': 100000, + 'backup-system': 200000 + }; + + let additionalCost = 0; + additionalFeatures.forEach(feature => { + additionalCost += featureCosts[feature] || 0; + }); + + // Custom requirements cost (estimated based on description length and complexity) + let customCost = 0; + if (customRequirements && customRequirements.length > 50) { + customCost = Math.min(customRequirements.length * 1000, 1000000); // Max 1M KRW + } + + // Calculate final estimate + const subtotal = baseCost * complexityMultiplier * timelineMultiplier; + const total = subtotal + additionalCost + customCost; + + // Calculate time estimate + const timeMultiplier = complexityMultiplier * timelineMultiplier; + const estimatedDays = Math.ceil(totalTime * timeMultiplier); + const estimatedWeeks = Math.ceil(estimatedDays / 7); + + // Create price ranges (±20%) + const minPrice = Math.round(total * 0.8); + const maxPrice = Math.round(total * 1.2); + + // Breakdown + const breakdown = { + baseServices: Math.round(baseCost), + complexityAdjustment: Math.round(baseCost * (complexityMultiplier - 1)), + timelineAdjustment: Math.round(baseCost * complexityMultiplier * (timelineMultiplier - 1)), + additionalFeatures: additionalCost, + customRequirements: customCost, + subtotal: Math.round(subtotal), + total: Math.round(total) + }; + + const result = { + success: true, + estimate: { + total: Math.round(total), + range: { + min: minPrice, + max: maxPrice, + formatted: `₩${minPrice.toLocaleString()} - ₩${maxPrice.toLocaleString()}` + }, + breakdown, + timeline: { + days: estimatedDays, + weeks: estimatedWeeks, + formatted: estimatedWeeks === 1 ? '1 week' : `${estimatedWeeks} weeks` + }, + currency: 'KRW', + confidence: calculateConfidence(services.length, projectComplexity, customRequirements), + recommendations: generateRecommendations(projectComplexity, timeline, services) + }, + selectedServices: services.map(s => ({ + id: s._id, + name: s.name, + category: s.category, + basePrice: s.pricing.basePrice + })), + calculatedAt: new Date() + }; + + res.json(result); + } catch (error) { + console.error('Calculator calculation error:', error); + res.status(500).json({ + success: false, + message: 'Error calculating estimate' + }); + } +}); + +// Helper function to calculate confidence level +function calculateConfidence(serviceCount, complexity, customRequirements) { + let confidence = 85; // Base confidence + + // Adjust based on service count + if (serviceCount === 1) confidence += 10; + else if (serviceCount > 5) confidence -= 10; + + // Adjust based on complexity + if (complexity === 'simple') confidence += 10; + else if (complexity === 'enterprise') confidence -= 15; + + // Adjust based on custom requirements + if (customRequirements && customRequirements.length > 200) { + confidence -= 20; + } + + return Math.max(60, Math.min(95, confidence)); +} + +// Helper function to generate recommendations +function generateRecommendations(complexity, timeline, services) { + const recommendations = []; + + if (complexity === 'enterprise' && timeline === 'rush') { + recommendations.push('Consider extending timeline for enterprise-level projects to ensure quality'); + } + + if (services.length > 6) { + recommendations.push('Consider breaking down into phases for better project management'); + } + + if (timeline === 'flexible') { + recommendations.push('Flexible timeline allows for better cost optimization and quality assurance'); + } + + const hasDesign = services.some(s => s.category === 'design'); + const hasDevelopment = services.some(s => s.category === 'development'); + + if (hasDevelopment && !hasDesign) { + recommendations.push('Consider adding UI/UX design services for better user experience'); + } + + return recommendations; +} + +// Get pricing guidelines +router.get('/pricing-guide', async (req, res) => { + try { + const pricingGuide = { + serviceCategories: { + 'development': { + name: 'Web Development', + priceRange: '₩500,000 - ₩5,000,000', + description: 'Custom web applications and websites' + }, + 'design': { + name: 'UI/UX Design', + priceRange: '₩300,000 - ₩2,000,000', + description: 'User interface and experience design' + }, + 'marketing': { + name: 'Digital Marketing', + priceRange: '₩200,000 - ₩1,500,000', + description: 'SEO, social media, and online marketing' + }, + 'consulting': { + name: 'Technical Consulting', + priceRange: '₩150,000 - ₩1,000,000', + description: 'Technology strategy and consultation' + } + }, + complexityFactors: { + 'simple': { + name: 'Simple Project', + multiplier: '0.7x', + description: 'Basic functionality, standard design' + }, + 'medium': { + name: 'Medium Project', + multiplier: '1.0x', + description: 'Moderate complexity, custom features' + }, + 'complex': { + name: 'Complex Project', + multiplier: '1.5x', + description: 'Advanced features, integrations' + }, + 'enterprise': { + name: 'Enterprise Project', + multiplier: '2.0x', + description: 'Large scale, high complexity' + } + }, + timelineImpact: { + 'rush': { + name: 'Rush (< 2 weeks)', + multiplier: '+80%', + description: 'Requires overtime and priority handling' + }, + 'fast': { + name: 'Fast (2-4 weeks)', + multiplier: '+40%', + description: 'Accelerated timeline' + }, + 'standard': { + name: 'Standard (1-3 months)', + multiplier: 'Standard', + description: 'Normal project timeline' + }, + 'flexible': { + name: 'Flexible (3+ months)', + multiplier: '-20%', + description: 'Extended timeline allows optimization' + } + } + }; + + res.json({ + success: true, + pricingGuide + }); + } catch (error) { + console.error('Pricing guide error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching pricing guide' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/contact_20251019160738.js b/.history/routes/contact_20251019160738.js new file mode 100644 index 0000000..3ffc6e6 --- /dev/null +++ b/.history/routes/contact_20251019160738.js @@ -0,0 +1,250 @@ +const express = require('express'); +const router = express.Router(); +const { body, validationResult } = require('express-validator'); +const nodemailer = require('nodemailer'); +const Contact = require('../models/Contact'); +const TelegramBot = require('node-telegram-bot-api'); + +// Initialize Telegram bot if token is provided +let bot = null; +if (process.env.TELEGRAM_BOT_TOKEN) { + bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false }); +} + +// Contact form validation +const contactValidation = [ + body('name').trim().isLength({ min: 2, max: 100 }), + body('email').isEmail().normalizeEmail(), + body('subject').trim().isLength({ min: 5, max: 200 }), + body('message').trim().isLength({ min: 10, max: 2000 }), + body('phone').optional().isMobilePhone(), + body('company').optional().trim().isLength({ max: 100 }) +]; + +// Submit contact form +router.post('/submit', contactValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const contactData = { + ...req.body, + ipAddress: req.ip, + userAgent: req.get('User-Agent'), + source: 'website' + }; + + // Save to database + const contact = new Contact(contactData); + await contact.save(); + + // Send email notification + await sendEmailNotification(contact); + + // Send Telegram notification + await sendTelegramNotification(contact); + + res.json({ + success: true, + message: 'Your message has been sent successfully. We will get back to you soon!', + contactId: contact._id + }); + } catch (error) { + console.error('Contact form error:', error); + res.status(500).json({ + success: false, + message: 'Sorry, there was an error sending your message. Please try again.' + }); + } +}); + +// Get project estimate +router.post('/estimate', [ + body('services').isArray().notEmpty(), + body('projectType').notEmpty(), + body('timeline').notEmpty(), + body('budget').notEmpty(), + body('description').trim().isLength({ min: 10, max: 1000 }) +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { services, projectType, timeline, budget, description, contactInfo } = req.body; + + // Calculate estimate (simplified) + const estimate = calculateProjectEstimate(services, projectType, timeline); + + // Save inquiry to database + const contactData = { + name: contactInfo.name, + email: contactInfo.email, + phone: contactInfo.phone, + company: contactInfo.company, + subject: `Project Estimate Request - ${projectType}`, + message: `Project Description: ${description}\n\nServices: ${services.join(', ')}\nTimeline: ${timeline}\nBudget: ${budget}\n\nCalculated Estimate: ${estimate.formatted}`, + serviceInterest: projectType, + budget: budget, + timeline: timeline, + source: 'calculator', + ipAddress: req.ip, + userAgent: req.get('User-Agent') + }; + + const contact = new Contact(contactData); + await contact.save(); + + // Send notifications + await sendEmailNotification(contact); + await sendTelegramNotification(contact); + + res.json({ + success: true, + message: 'Your project estimate request has been submitted successfully!', + estimate: estimate, + contactId: contact._id + }); + } catch (error) { + console.error('Estimate request error:', error); + res.status(500).json({ + success: false, + message: 'Sorry, there was an error processing your request. Please try again.' + }); + } +}); + +// Helper function to send email notification +async function sendEmailNotification(contact) { + if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER) { + console.log('Email configuration not provided, skipping email notification'); + return; + } + + try { + const transporter = nodemailer.createTransporter({ + host: process.env.EMAIL_HOST, + port: process.env.EMAIL_PORT || 587, + secure: false, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } + }); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: process.env.ADMIN_EMAIL || process.env.EMAIL_USER, + subject: `New Contact Form Submission: ${contact.subject}`, + html: ` +

New Contact Form Submission

+

Name: ${contact.name}

+

Email: ${contact.email}

+

Phone: ${contact.phone || 'Not provided'}

+

Company: ${contact.company || 'Not provided'}

+

Subject: ${contact.subject}

+

Message:

+

${contact.message.replace(/\n/g, '
')}

+

Source: ${contact.source}

+

Submitted: ${contact.createdAt}

+
+

View in Admin Panel

+ ` + }; + + await transporter.sendMail(mailOptions); + console.log('Email notification sent successfully'); + } catch (error) { + console.error('Email notification error:', error); + } +} + +// Helper function to send Telegram notification +async function sendTelegramNotification(contact) { + if (!bot || !process.env.TELEGRAM_CHAT_ID) { + console.log('Telegram configuration not provided, skipping Telegram notification'); + return; + } + + try { + const message = ` +🔔 *New Contact Form Submission* + +👤 *Name:* ${contact.name} +📧 *Email:* ${contact.email} +📱 *Phone:* ${contact.phone || 'Not provided'} +🏢 *Company:* ${contact.company || 'Not provided'} +📝 *Subject:* ${contact.subject} + +💬 *Message:* +${contact.message} + +📍 *Source:* ${contact.source} +🕐 *Time:* ${contact.createdAt.toLocaleString()} + +[View in Admin Panel](${process.env.SITE_URL}/admin/contacts/${contact._id}) + `; + + await bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message, { + parse_mode: 'Markdown', + disable_web_page_preview: true + }); + + console.log('Telegram notification sent successfully'); + } catch (error) { + console.error('Telegram notification error:', error); + } +} + +// Helper function to calculate project estimate +function calculateProjectEstimate(services, projectType, timeline) { + const baseRates = { + 'web-development': 50000, + 'mobile-app': 80000, + 'ui-ux-design': 30000, + 'branding': 20000, + 'e-commerce': 70000, + 'consulting': 40000 + }; + + const timelineMultipliers = { + 'asap': 1.5, + '1-month': 1.2, + '1-3-months': 1.0, + '3-6-months': 0.9, + 'flexible': 0.8 + }; + + let basePrice = baseRates[projectType] || 50000; + let multiplier = timelineMultipliers[timeline] || 1.0; + + // Add service modifiers + let serviceModifier = 1.0; + if (services.length > 3) serviceModifier += 0.3; + if (services.length > 5) serviceModifier += 0.5; + + const totalEstimate = Math.round(basePrice * multiplier * serviceModifier); + const rangeMin = Math.round(totalEstimate * 0.8); + const rangeMax = Math.round(totalEstimate * 1.3); + + return { + base: totalEstimate, + min: rangeMin, + max: rangeMax, + formatted: `₩${rangeMin.toLocaleString()} - ₩${rangeMax.toLocaleString()}`, + currency: 'KRW' + }; +} + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/contact_20251019162544.js b/.history/routes/contact_20251019162544.js new file mode 100644 index 0000000..3ffc6e6 --- /dev/null +++ b/.history/routes/contact_20251019162544.js @@ -0,0 +1,250 @@ +const express = require('express'); +const router = express.Router(); +const { body, validationResult } = require('express-validator'); +const nodemailer = require('nodemailer'); +const Contact = require('../models/Contact'); +const TelegramBot = require('node-telegram-bot-api'); + +// Initialize Telegram bot if token is provided +let bot = null; +if (process.env.TELEGRAM_BOT_TOKEN) { + bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false }); +} + +// Contact form validation +const contactValidation = [ + body('name').trim().isLength({ min: 2, max: 100 }), + body('email').isEmail().normalizeEmail(), + body('subject').trim().isLength({ min: 5, max: 200 }), + body('message').trim().isLength({ min: 10, max: 2000 }), + body('phone').optional().isMobilePhone(), + body('company').optional().trim().isLength({ max: 100 }) +]; + +// Submit contact form +router.post('/submit', contactValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const contactData = { + ...req.body, + ipAddress: req.ip, + userAgent: req.get('User-Agent'), + source: 'website' + }; + + // Save to database + const contact = new Contact(contactData); + await contact.save(); + + // Send email notification + await sendEmailNotification(contact); + + // Send Telegram notification + await sendTelegramNotification(contact); + + res.json({ + success: true, + message: 'Your message has been sent successfully. We will get back to you soon!', + contactId: contact._id + }); + } catch (error) { + console.error('Contact form error:', error); + res.status(500).json({ + success: false, + message: 'Sorry, there was an error sending your message. Please try again.' + }); + } +}); + +// Get project estimate +router.post('/estimate', [ + body('services').isArray().notEmpty(), + body('projectType').notEmpty(), + body('timeline').notEmpty(), + body('budget').notEmpty(), + body('description').trim().isLength({ min: 10, max: 1000 }) +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { services, projectType, timeline, budget, description, contactInfo } = req.body; + + // Calculate estimate (simplified) + const estimate = calculateProjectEstimate(services, projectType, timeline); + + // Save inquiry to database + const contactData = { + name: contactInfo.name, + email: contactInfo.email, + phone: contactInfo.phone, + company: contactInfo.company, + subject: `Project Estimate Request - ${projectType}`, + message: `Project Description: ${description}\n\nServices: ${services.join(', ')}\nTimeline: ${timeline}\nBudget: ${budget}\n\nCalculated Estimate: ${estimate.formatted}`, + serviceInterest: projectType, + budget: budget, + timeline: timeline, + source: 'calculator', + ipAddress: req.ip, + userAgent: req.get('User-Agent') + }; + + const contact = new Contact(contactData); + await contact.save(); + + // Send notifications + await sendEmailNotification(contact); + await sendTelegramNotification(contact); + + res.json({ + success: true, + message: 'Your project estimate request has been submitted successfully!', + estimate: estimate, + contactId: contact._id + }); + } catch (error) { + console.error('Estimate request error:', error); + res.status(500).json({ + success: false, + message: 'Sorry, there was an error processing your request. Please try again.' + }); + } +}); + +// Helper function to send email notification +async function sendEmailNotification(contact) { + if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER) { + console.log('Email configuration not provided, skipping email notification'); + return; + } + + try { + const transporter = nodemailer.createTransporter({ + host: process.env.EMAIL_HOST, + port: process.env.EMAIL_PORT || 587, + secure: false, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } + }); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: process.env.ADMIN_EMAIL || process.env.EMAIL_USER, + subject: `New Contact Form Submission: ${contact.subject}`, + html: ` +

New Contact Form Submission

+

Name: ${contact.name}

+

Email: ${contact.email}

+

Phone: ${contact.phone || 'Not provided'}

+

Company: ${contact.company || 'Not provided'}

+

Subject: ${contact.subject}

+

Message:

+

${contact.message.replace(/\n/g, '
')}

+

Source: ${contact.source}

+

Submitted: ${contact.createdAt}

+
+

View in Admin Panel

+ ` + }; + + await transporter.sendMail(mailOptions); + console.log('Email notification sent successfully'); + } catch (error) { + console.error('Email notification error:', error); + } +} + +// Helper function to send Telegram notification +async function sendTelegramNotification(contact) { + if (!bot || !process.env.TELEGRAM_CHAT_ID) { + console.log('Telegram configuration not provided, skipping Telegram notification'); + return; + } + + try { + const message = ` +🔔 *New Contact Form Submission* + +👤 *Name:* ${contact.name} +📧 *Email:* ${contact.email} +📱 *Phone:* ${contact.phone || 'Not provided'} +🏢 *Company:* ${contact.company || 'Not provided'} +📝 *Subject:* ${contact.subject} + +💬 *Message:* +${contact.message} + +📍 *Source:* ${contact.source} +🕐 *Time:* ${contact.createdAt.toLocaleString()} + +[View in Admin Panel](${process.env.SITE_URL}/admin/contacts/${contact._id}) + `; + + await bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message, { + parse_mode: 'Markdown', + disable_web_page_preview: true + }); + + console.log('Telegram notification sent successfully'); + } catch (error) { + console.error('Telegram notification error:', error); + } +} + +// Helper function to calculate project estimate +function calculateProjectEstimate(services, projectType, timeline) { + const baseRates = { + 'web-development': 50000, + 'mobile-app': 80000, + 'ui-ux-design': 30000, + 'branding': 20000, + 'e-commerce': 70000, + 'consulting': 40000 + }; + + const timelineMultipliers = { + 'asap': 1.5, + '1-month': 1.2, + '1-3-months': 1.0, + '3-6-months': 0.9, + 'flexible': 0.8 + }; + + let basePrice = baseRates[projectType] || 50000; + let multiplier = timelineMultipliers[timeline] || 1.0; + + // Add service modifiers + let serviceModifier = 1.0; + if (services.length > 3) serviceModifier += 0.3; + if (services.length > 5) serviceModifier += 0.5; + + const totalEstimate = Math.round(basePrice * multiplier * serviceModifier); + const rangeMin = Math.round(totalEstimate * 0.8); + const rangeMax = Math.round(totalEstimate * 1.3); + + return { + base: totalEstimate, + min: rangeMin, + max: rangeMax, + formatted: `₩${rangeMin.toLocaleString()} - ₩${rangeMax.toLocaleString()}`, + currency: 'KRW' + }; +} + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/index_20251019160650.js b/.history/routes/index_20251019160650.js new file mode 100644 index 0000000..65d2ed0 --- /dev/null +++ b/.history/routes/index_20251019160650.js @@ -0,0 +1,203 @@ +const express = require('express'); +const router = express.Router(); +const Portfolio = require('../models/Portfolio'); +const Service = require('../models/Service'); +const SiteSettings = require('../models/SiteSettings'); + +// Home page +router.get('/', async (req, res) => { + try { + const [settings, featuredPortfolio, featuredServices] = await Promise.all([ + SiteSettings.findOne() || {}, + Portfolio.find({ featured: true, isPublished: true }) + .sort({ order: 1, createdAt: -1 }) + .limit(6), + Service.find({ featured: true, isActive: true }) + .sort({ order: 1 }) + .limit(4) + ]); + + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: settings || {}, + featuredPortfolio, + featuredServices, + currentPage: 'home' + }); + } catch (error) { + console.error('Home page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// About page +router.get('/about', async (req, res) => { + try { + const settings = await SiteSettings.findOne() || {}; + + res.render('about', { + title: 'About Us - SmartSolTech', + settings, + currentPage: 'about' + }); + } catch (error) { + console.error('About page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Portfolio page +router.get('/portfolio', async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = 12; + const skip = (page - 1) * limit; + const category = req.query.category; + + let query = { isPublished: true }; + if (category && category !== 'all') { + query.category = category; + } + + const [portfolio, total, categories] = await Promise.all([ + Portfolio.find(query) + .sort({ featured: -1, publishedAt: -1 }) + .skip(skip) + .limit(limit), + Portfolio.countDocuments(query), + Portfolio.distinct('category', { isPublished: true }) + ]); + + const totalPages = Math.ceil(total / limit); + + res.render('portfolio', { + title: 'Portfolio - SmartSolTech', + portfolio, + categories, + currentCategory: category || 'all', + pagination: { + current: page, + total: totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + }, + currentPage: 'portfolio' + }); + } catch (error) { + console.error('Portfolio page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Portfolio detail page +router.get('/portfolio/:id', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).render('404', { + title: '404 - Project Not Found', + message: 'The requested project was not found' + }); + } + + // Increment view count + portfolio.viewCount += 1; + await portfolio.save(); + + // Get related projects + const relatedProjects = await Portfolio.find({ + _id: { $ne: portfolio._id }, + category: portfolio.category, + isPublished: true + }).limit(3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - Portfolio - SmartSolTech`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); + } catch (error) { + console.error('Portfolio detail error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Services page +router.get('/services', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .sort({ featured: -1, order: 1 }) + .populate('portfolio', 'title images'); + + const categories = await Service.distinct('category', { isActive: true }); + + res.render('services', { + title: 'Services - SmartSolTech', + services, + categories, + currentPage: 'services' + }); + } catch (error) { + console.error('Services page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Calculator page +router.get('/calculator', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .select('name pricing category') + .sort({ category: 1, name: 1 }); + + res.render('calculator', { + title: 'Project Calculator - SmartSolTech', + services, + currentPage: 'calculator' + }); + } catch (error) { + console.error('Calculator page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Contact page +router.get('/contact', async (req, res) => { + try { + const settings = await SiteSettings.findOne() || {}; + + res.render('contact', { + title: 'Contact Us - SmartSolTech', + settings, + currentPage: 'contact' + }); + } catch (error) { + console.error('Contact page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/index_20251019162544.js b/.history/routes/index_20251019162544.js new file mode 100644 index 0000000..65d2ed0 --- /dev/null +++ b/.history/routes/index_20251019162544.js @@ -0,0 +1,203 @@ +const express = require('express'); +const router = express.Router(); +const Portfolio = require('../models/Portfolio'); +const Service = require('../models/Service'); +const SiteSettings = require('../models/SiteSettings'); + +// Home page +router.get('/', async (req, res) => { + try { + const [settings, featuredPortfolio, featuredServices] = await Promise.all([ + SiteSettings.findOne() || {}, + Portfolio.find({ featured: true, isPublished: true }) + .sort({ order: 1, createdAt: -1 }) + .limit(6), + Service.find({ featured: true, isActive: true }) + .sort({ order: 1 }) + .limit(4) + ]); + + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: settings || {}, + featuredPortfolio, + featuredServices, + currentPage: 'home' + }); + } catch (error) { + console.error('Home page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// About page +router.get('/about', async (req, res) => { + try { + const settings = await SiteSettings.findOne() || {}; + + res.render('about', { + title: 'About Us - SmartSolTech', + settings, + currentPage: 'about' + }); + } catch (error) { + console.error('About page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Portfolio page +router.get('/portfolio', async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = 12; + const skip = (page - 1) * limit; + const category = req.query.category; + + let query = { isPublished: true }; + if (category && category !== 'all') { + query.category = category; + } + + const [portfolio, total, categories] = await Promise.all([ + Portfolio.find(query) + .sort({ featured: -1, publishedAt: -1 }) + .skip(skip) + .limit(limit), + Portfolio.countDocuments(query), + Portfolio.distinct('category', { isPublished: true }) + ]); + + const totalPages = Math.ceil(total / limit); + + res.render('portfolio', { + title: 'Portfolio - SmartSolTech', + portfolio, + categories, + currentCategory: category || 'all', + pagination: { + current: page, + total: totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + }, + currentPage: 'portfolio' + }); + } catch (error) { + console.error('Portfolio page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Portfolio detail page +router.get('/portfolio/:id', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).render('404', { + title: '404 - Project Not Found', + message: 'The requested project was not found' + }); + } + + // Increment view count + portfolio.viewCount += 1; + await portfolio.save(); + + // Get related projects + const relatedProjects = await Portfolio.find({ + _id: { $ne: portfolio._id }, + category: portfolio.category, + isPublished: true + }).limit(3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - Portfolio - SmartSolTech`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); + } catch (error) { + console.error('Portfolio detail error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Services page +router.get('/services', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .sort({ featured: -1, order: 1 }) + .populate('portfolio', 'title images'); + + const categories = await Service.distinct('category', { isActive: true }); + + res.render('services', { + title: 'Services - SmartSolTech', + services, + categories, + currentPage: 'services' + }); + } catch (error) { + console.error('Services page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Calculator page +router.get('/calculator', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .select('name pricing category') + .sort({ category: 1, name: 1 }); + + res.render('calculator', { + title: 'Project Calculator - SmartSolTech', + services, + currentPage: 'calculator' + }); + } catch (error) { + console.error('Calculator page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Contact page +router.get('/contact', async (req, res) => { + try { + const settings = await SiteSettings.findOne() || {}; + + res.render('contact', { + title: 'Contact Us - SmartSolTech', + settings, + currentPage: 'contact' + }); + } catch (error) { + console.error('Contact page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/media_20251019160925.js b/.history/routes/media_20251019160925.js new file mode 100644 index 0000000..d5b7100 --- /dev/null +++ b/.history/routes/media_20251019160925.js @@ -0,0 +1,345 @@ +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; + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Authentication required' + }); + } + next(); +}; + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: async (req, file, cb) => { + const uploadPath = path.join(__dirname, '../public/uploads'); + try { + await fs.mkdir(uploadPath, { recursive: true }); + cb(null, uploadPath); + } catch (error) { + cb(error); + } + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + cb(null, file.fieldname + '-' + uniqueSuffix + ext); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB + files: 10 + }, + fileFilter: (req, file, cb) => { + // Allow images only + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed'), false); + } + } +}); + +// Upload single image +router.post('/upload', requireAuth, upload.single('image'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: 'No file uploaded' + }); + } + + const originalPath = req.file.path; + const filename = req.file.filename; + const nameWithoutExt = path.parse(filename).name; + + // Create optimized versions + const sizes = { + thumbnail: { width: 300, height: 200 }, + medium: { width: 800, height: 600 }, + large: { width: 1200, height: 900 } + }; + + const optimizedImages = {}; + + for (const [sizeName, dimensions] of Object.entries(sizes)) { + const outputPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-${sizeName}.webp` + ); + + await sharp(originalPath) + .resize(dimensions.width, dimensions.height, { + fit: 'inside', + withoutEnlargement: true + }) + .webp({ quality: 85 }) + .toFile(outputPath); + + optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; + } + + // Keep original as well (converted to webp for better compression) + const originalWebpPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-original.webp` + ); + + await sharp(originalPath) + .webp({ quality: 90 }) + .toFile(originalWebpPath); + + optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; + + // Remove the original uploaded file + await fs.unlink(originalPath); + + res.json({ + success: true, + message: 'Image uploaded and optimized successfully', + images: optimizedImages, + metadata: { + originalName: req.file.originalname, + mimeType: req.file.mimetype, + size: req.file.size + } + }); + } catch (error) { + console.error('Image upload error:', error); + + // Clean up files on error + if (req.file && req.file.path) { + try { + await fs.unlink(req.file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + + res.status(500).json({ + success: false, + message: 'Error uploading image' + }); + } +}); + +// Upload multiple images +router.post('/upload-multiple', requireAuth, upload.array('images', 10), async (req, res) => { + try { + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + success: false, + message: 'No files uploaded' + }); + } + + const uploadedImages = []; + + for (const file of req.files) { + try { + const originalPath = file.path; + const filename = file.filename; + const nameWithoutExt = path.parse(filename).name; + + // Create optimized versions + const sizes = { + thumbnail: { width: 300, height: 200 }, + medium: { width: 800, height: 600 }, + large: { width: 1200, height: 900 } + }; + + const optimizedImages = {}; + + for (const [sizeName, dimensions] of Object.entries(sizes)) { + const outputPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-${sizeName}.webp` + ); + + await sharp(originalPath) + .resize(dimensions.width, dimensions.height, { + fit: 'inside', + withoutEnlargement: true + }) + .webp({ quality: 85 }) + .toFile(outputPath); + + optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; + } + + // Original as webp + const originalWebpPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-original.webp` + ); + + await sharp(originalPath) + .webp({ quality: 90 }) + .toFile(originalWebpPath); + + optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; + + uploadedImages.push({ + originalName: file.originalname, + images: optimizedImages, + metadata: { + mimeType: file.mimetype, + size: file.size + } + }); + + // Remove original file + await fs.unlink(originalPath); + } catch (fileError) { + console.error(`Error processing file ${file.originalname}:`, fileError); + // Clean up this file and continue with others + try { + await fs.unlink(file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + } + + res.json({ + success: true, + message: `${uploadedImages.length} images uploaded and optimized successfully`, + images: uploadedImages + }); + } catch (error) { + console.error('Multiple images upload error:', error); + + // Clean up files on error + if (req.files) { + for (const file of req.files) { + try { + await fs.unlink(file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + } + + res.status(500).json({ + success: false, + message: 'Error uploading images' + }); + } +}); + +// Delete image +router.delete('/:filename', requireAuth, async (req, res) => { + try { + const filename = req.params.filename; + const uploadPath = path.join(__dirname, '../public/uploads'); + + // Security check - ensure filename doesn't contain path traversal + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return res.status(400).json({ + success: false, + message: 'Invalid filename' + }); + } + + const filePath = path.join(uploadPath, filename); + + try { + await fs.access(filePath); + await fs.unlink(filePath); + + res.json({ + success: true, + message: 'Image deleted successfully' + }); + } catch (error) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: 'Image not found' + }); + } + throw error; + } + } catch (error) { + console.error('Image deletion error:', error); + res.status(500).json({ + success: false, + message: 'Error deleting image' + }); + } +}); + +// List uploaded images +router.get('/list', requireAuth, async (req, res) => { + try { + const uploadPath = path.join(__dirname, '../public/uploads'); + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + + const files = await fs.readdir(uploadPath); + const imageFiles = files.filter(file => + /\.(jpg|jpeg|png|gif|webp)$/i.test(file) + ); + + const total = imageFiles.length; + const totalPages = Math.ceil(total / limit); + const start = (page - 1) * limit; + const end = start + limit; + + const paginatedFiles = imageFiles.slice(start, end); + + const imagesWithStats = await Promise.all( + paginatedFiles.map(async (file) => { + try { + const filePath = path.join(uploadPath, file); + const stats = await fs.stat(filePath); + + return { + filename: file, + url: `/uploads/${file}`, + size: stats.size, + modified: stats.mtime, + isImage: true + }; + } catch (error) { + console.error(`Error getting stats for ${file}:`, error); + return null; + } + }) + ); + + const validImages = imagesWithStats.filter(img => img !== null); + + res.json({ + success: true, + images: validImages, + pagination: { + current: page, + total: totalPages, + limit, + totalItems: total, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('List images error:', error); + res.status(500).json({ + success: false, + message: 'Error listing images' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/media_20251019162544.js b/.history/routes/media_20251019162544.js new file mode 100644 index 0000000..d5b7100 --- /dev/null +++ b/.history/routes/media_20251019162544.js @@ -0,0 +1,345 @@ +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; + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Authentication required' + }); + } + next(); +}; + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: async (req, file, cb) => { + const uploadPath = path.join(__dirname, '../public/uploads'); + try { + await fs.mkdir(uploadPath, { recursive: true }); + cb(null, uploadPath); + } catch (error) { + cb(error); + } + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + cb(null, file.fieldname + '-' + uniqueSuffix + ext); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB + files: 10 + }, + fileFilter: (req, file, cb) => { + // Allow images only + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed'), false); + } + } +}); + +// Upload single image +router.post('/upload', requireAuth, upload.single('image'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: 'No file uploaded' + }); + } + + const originalPath = req.file.path; + const filename = req.file.filename; + const nameWithoutExt = path.parse(filename).name; + + // Create optimized versions + const sizes = { + thumbnail: { width: 300, height: 200 }, + medium: { width: 800, height: 600 }, + large: { width: 1200, height: 900 } + }; + + const optimizedImages = {}; + + for (const [sizeName, dimensions] of Object.entries(sizes)) { + const outputPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-${sizeName}.webp` + ); + + await sharp(originalPath) + .resize(dimensions.width, dimensions.height, { + fit: 'inside', + withoutEnlargement: true + }) + .webp({ quality: 85 }) + .toFile(outputPath); + + optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; + } + + // Keep original as well (converted to webp for better compression) + const originalWebpPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-original.webp` + ); + + await sharp(originalPath) + .webp({ quality: 90 }) + .toFile(originalWebpPath); + + optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; + + // Remove the original uploaded file + await fs.unlink(originalPath); + + res.json({ + success: true, + message: 'Image uploaded and optimized successfully', + images: optimizedImages, + metadata: { + originalName: req.file.originalname, + mimeType: req.file.mimetype, + size: req.file.size + } + }); + } catch (error) { + console.error('Image upload error:', error); + + // Clean up files on error + if (req.file && req.file.path) { + try { + await fs.unlink(req.file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + + res.status(500).json({ + success: false, + message: 'Error uploading image' + }); + } +}); + +// Upload multiple images +router.post('/upload-multiple', requireAuth, upload.array('images', 10), async (req, res) => { + try { + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + success: false, + message: 'No files uploaded' + }); + } + + const uploadedImages = []; + + for (const file of req.files) { + try { + const originalPath = file.path; + const filename = file.filename; + const nameWithoutExt = path.parse(filename).name; + + // Create optimized versions + const sizes = { + thumbnail: { width: 300, height: 200 }, + medium: { width: 800, height: 600 }, + large: { width: 1200, height: 900 } + }; + + const optimizedImages = {}; + + for (const [sizeName, dimensions] of Object.entries(sizes)) { + const outputPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-${sizeName}.webp` + ); + + await sharp(originalPath) + .resize(dimensions.width, dimensions.height, { + fit: 'inside', + withoutEnlargement: true + }) + .webp({ quality: 85 }) + .toFile(outputPath); + + optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; + } + + // Original as webp + const originalWebpPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-original.webp` + ); + + await sharp(originalPath) + .webp({ quality: 90 }) + .toFile(originalWebpPath); + + optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; + + uploadedImages.push({ + originalName: file.originalname, + images: optimizedImages, + metadata: { + mimeType: file.mimetype, + size: file.size + } + }); + + // Remove original file + await fs.unlink(originalPath); + } catch (fileError) { + console.error(`Error processing file ${file.originalname}:`, fileError); + // Clean up this file and continue with others + try { + await fs.unlink(file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + } + + res.json({ + success: true, + message: `${uploadedImages.length} images uploaded and optimized successfully`, + images: uploadedImages + }); + } catch (error) { + console.error('Multiple images upload error:', error); + + // Clean up files on error + if (req.files) { + for (const file of req.files) { + try { + await fs.unlink(file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + } + + res.status(500).json({ + success: false, + message: 'Error uploading images' + }); + } +}); + +// Delete image +router.delete('/:filename', requireAuth, async (req, res) => { + try { + const filename = req.params.filename; + const uploadPath = path.join(__dirname, '../public/uploads'); + + // Security check - ensure filename doesn't contain path traversal + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return res.status(400).json({ + success: false, + message: 'Invalid filename' + }); + } + + const filePath = path.join(uploadPath, filename); + + try { + await fs.access(filePath); + await fs.unlink(filePath); + + res.json({ + success: true, + message: 'Image deleted successfully' + }); + } catch (error) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: 'Image not found' + }); + } + throw error; + } + } catch (error) { + console.error('Image deletion error:', error); + res.status(500).json({ + success: false, + message: 'Error deleting image' + }); + } +}); + +// List uploaded images +router.get('/list', requireAuth, async (req, res) => { + try { + const uploadPath = path.join(__dirname, '../public/uploads'); + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + + const files = await fs.readdir(uploadPath); + const imageFiles = files.filter(file => + /\.(jpg|jpeg|png|gif|webp)$/i.test(file) + ); + + const total = imageFiles.length; + const totalPages = Math.ceil(total / limit); + const start = (page - 1) * limit; + const end = start + limit; + + const paginatedFiles = imageFiles.slice(start, end); + + const imagesWithStats = await Promise.all( + paginatedFiles.map(async (file) => { + try { + const filePath = path.join(uploadPath, file); + const stats = await fs.stat(filePath); + + return { + filename: file, + url: `/uploads/${file}`, + size: stats.size, + modified: stats.mtime, + isImage: true + }; + } catch (error) { + console.error(`Error getting stats for ${file}:`, error); + return null; + } + }) + ); + + const validImages = imagesWithStats.filter(img => img !== null); + + res.json({ + success: true, + images: validImages, + pagination: { + current: page, + total: totalPages, + limit, + totalItems: total, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('List images error:', error); + res.status(500).json({ + success: false, + message: 'Error listing images' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/portfolio_20251019160838.js b/.history/routes/portfolio_20251019160838.js new file mode 100644 index 0000000..0164537 --- /dev/null +++ b/.history/routes/portfolio_20251019160838.js @@ -0,0 +1,196 @@ +const express = require('express'); +const router = express.Router(); +const Portfolio = require('../models/Portfolio'); + +// Get all portfolio items +router.get('/', async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 12; + const skip = (page - 1) * limit; + const category = req.query.category; + const search = req.query.search; + const featured = req.query.featured; + + // Build query + let query = { isPublished: true }; + + if (category && category !== 'all') { + query.category = category; + } + + if (featured === 'true') { + query.featured = true; + } + + if (search) { + query.$text = { $search: search }; + } + + // Get portfolio items + const [portfolio, total] = await Promise.all([ + Portfolio.find(query) + .sort({ featured: -1, publishedAt: -1 }) + .skip(skip) + .limit(limit) + .select('title shortDescription category technologies images status publishedAt viewCount'), + Portfolio.countDocuments(query) + ]); + + const totalPages = Math.ceil(total / limit); + + res.json({ + success: true, + portfolio, + pagination: { + current: page, + total: totalPages, + limit, + totalItems: total, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('Portfolio API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching portfolio' + }); + } +}); + +// Get single portfolio item +router.get('/:id', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).json({ + success: false, + message: 'Portfolio item not found' + }); + } + + // Increment view count + portfolio.viewCount += 1; + await portfolio.save(); + + // Get related projects + const relatedProjects = await Portfolio.find({ + _id: { $ne: portfolio._id }, + category: portfolio.category, + isPublished: true + }) + .select('title shortDescription images') + .limit(4); + + res.json({ + success: true, + portfolio, + relatedProjects + }); + } catch (error) { + console.error('Portfolio detail API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching portfolio item' + }); + } +}); + +// Get portfolio categories +router.get('/meta/categories', async (req, res) => { + try { + const categories = await Portfolio.distinct('category', { isPublished: true }); + + // Get count for each category + const categoriesWithCount = await Promise.all( + categories.map(async (category) => { + const count = await Portfolio.countDocuments({ + category, + isPublished: true + }); + return { category, count }; + }) + ); + + res.json({ + success: true, + categories: categoriesWithCount + }); + } catch (error) { + console.error('Portfolio categories API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching categories' + }); + } +}); + +// Like portfolio item +router.post('/:id/like', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).json({ + success: false, + message: 'Portfolio item not found' + }); + } + + portfolio.likes += 1; + await portfolio.save(); + + res.json({ + success: true, + likes: portfolio.likes + }); + } catch (error) { + console.error('Portfolio like API error:', error); + res.status(500).json({ + success: false, + message: 'Error liking portfolio item' + }); + } +}); + +// Search portfolio +router.get('/search/:term', async (req, res) => { + try { + const searchTerm = req.params.term; + const limit = parseInt(req.query.limit) || 10; + + const portfolio = await Portfolio.find({ + $and: [ + { isPublished: true }, + { + $or: [ + { title: { $regex: searchTerm, $options: 'i' } }, + { description: { $regex: searchTerm, $options: 'i' } }, + { technologies: { $in: [new RegExp(searchTerm, 'i')] } } + ] + } + ] + }) + .select('title shortDescription category images') + .sort({ featured: -1, publishedAt: -1 }) + .limit(limit); + + res.json({ + success: true, + portfolio, + searchTerm, + count: portfolio.length + }); + } catch (error) { + console.error('Portfolio search API error:', error); + res.status(500).json({ + success: false, + message: 'Error searching portfolio' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/portfolio_20251019162544.js b/.history/routes/portfolio_20251019162544.js new file mode 100644 index 0000000..0164537 --- /dev/null +++ b/.history/routes/portfolio_20251019162544.js @@ -0,0 +1,196 @@ +const express = require('express'); +const router = express.Router(); +const Portfolio = require('../models/Portfolio'); + +// Get all portfolio items +router.get('/', async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 12; + const skip = (page - 1) * limit; + const category = req.query.category; + const search = req.query.search; + const featured = req.query.featured; + + // Build query + let query = { isPublished: true }; + + if (category && category !== 'all') { + query.category = category; + } + + if (featured === 'true') { + query.featured = true; + } + + if (search) { + query.$text = { $search: search }; + } + + // Get portfolio items + const [portfolio, total] = await Promise.all([ + Portfolio.find(query) + .sort({ featured: -1, publishedAt: -1 }) + .skip(skip) + .limit(limit) + .select('title shortDescription category technologies images status publishedAt viewCount'), + Portfolio.countDocuments(query) + ]); + + const totalPages = Math.ceil(total / limit); + + res.json({ + success: true, + portfolio, + pagination: { + current: page, + total: totalPages, + limit, + totalItems: total, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('Portfolio API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching portfolio' + }); + } +}); + +// Get single portfolio item +router.get('/:id', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).json({ + success: false, + message: 'Portfolio item not found' + }); + } + + // Increment view count + portfolio.viewCount += 1; + await portfolio.save(); + + // Get related projects + const relatedProjects = await Portfolio.find({ + _id: { $ne: portfolio._id }, + category: portfolio.category, + isPublished: true + }) + .select('title shortDescription images') + .limit(4); + + res.json({ + success: true, + portfolio, + relatedProjects + }); + } catch (error) { + console.error('Portfolio detail API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching portfolio item' + }); + } +}); + +// Get portfolio categories +router.get('/meta/categories', async (req, res) => { + try { + const categories = await Portfolio.distinct('category', { isPublished: true }); + + // Get count for each category + const categoriesWithCount = await Promise.all( + categories.map(async (category) => { + const count = await Portfolio.countDocuments({ + category, + isPublished: true + }); + return { category, count }; + }) + ); + + res.json({ + success: true, + categories: categoriesWithCount + }); + } catch (error) { + console.error('Portfolio categories API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching categories' + }); + } +}); + +// Like portfolio item +router.post('/:id/like', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).json({ + success: false, + message: 'Portfolio item not found' + }); + } + + portfolio.likes += 1; + await portfolio.save(); + + res.json({ + success: true, + likes: portfolio.likes + }); + } catch (error) { + console.error('Portfolio like API error:', error); + res.status(500).json({ + success: false, + message: 'Error liking portfolio item' + }); + } +}); + +// Search portfolio +router.get('/search/:term', async (req, res) => { + try { + const searchTerm = req.params.term; + const limit = parseInt(req.query.limit) || 10; + + const portfolio = await Portfolio.find({ + $and: [ + { isPublished: true }, + { + $or: [ + { title: { $regex: searchTerm, $options: 'i' } }, + { description: { $regex: searchTerm, $options: 'i' } }, + { technologies: { $in: [new RegExp(searchTerm, 'i')] } } + ] + } + ] + }) + .select('title shortDescription category images') + .sort({ featured: -1, publishedAt: -1 }) + .limit(limit); + + res.json({ + success: true, + portfolio, + searchTerm, + count: portfolio.length + }); + } catch (error) { + console.error('Portfolio search API error:', error); + res.status(500).json({ + success: false, + message: 'Error searching portfolio' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/services_20251019160852.js b/.history/routes/services_20251019160852.js new file mode 100644 index 0000000..d6ae97c --- /dev/null +++ b/.history/routes/services_20251019160852.js @@ -0,0 +1,141 @@ +const express = require('express'); +const router = express.Router(); +const Service = require('../models/Service'); +const Portfolio = require('../models/Portfolio'); + +// Get all services +router.get('/', async (req, res) => { + try { + const category = req.query.category; + const featured = req.query.featured; + + let query = { isActive: true }; + + if (category && category !== 'all') { + query.category = category; + } + + if (featured === 'true') { + query.featured = true; + } + + const services = await Service.find(query) + .populate('portfolio', 'title images') + .sort({ featured: -1, order: 1 }); + + res.json({ + success: true, + services + }); + } catch (error) { + console.error('Services API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching services' + }); + } +}); + +// Get single service +router.get('/:id', async (req, res) => { + try { + const service = await Service.findById(req.params.id) + .populate('portfolio', 'title shortDescription images category'); + + if (!service || !service.isActive) { + return res.status(404).json({ + success: false, + message: 'Service not found' + }); + } + + // Get related services + const relatedServices = await Service.find({ + _id: { $ne: service._id }, + category: service.category, + isActive: true + }) + .select('name shortDescription icon pricing') + .limit(3); + + res.json({ + success: true, + service, + relatedServices + }); + } catch (error) { + console.error('Service detail API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching service' + }); + } +}); + +// Get service categories +router.get('/meta/categories', async (req, res) => { + try { + const categories = await Service.distinct('category', { isActive: true }); + + // Get count for each category + const categoriesWithCount = await Promise.all( + categories.map(async (category) => { + const count = await Service.countDocuments({ + category, + isActive: true + }); + return { category, count }; + }) + ); + + res.json({ + success: true, + categories: categoriesWithCount + }); + } catch (error) { + console.error('Service categories API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching categories' + }); + } +}); + +// Search services +router.get('/search/:term', async (req, res) => { + try { + const searchTerm = req.params.term; + const limit = parseInt(req.query.limit) || 10; + + const services = await Service.find({ + $and: [ + { isActive: true }, + { + $or: [ + { name: { $regex: searchTerm, $options: 'i' } }, + { description: { $regex: searchTerm, $options: 'i' } }, + { tags: { $in: [new RegExp(searchTerm, 'i')] } } + ] + } + ] + }) + .select('name shortDescription icon pricing category') + .sort({ featured: -1, order: 1 }) + .limit(limit); + + res.json({ + success: true, + services, + searchTerm, + count: services.length + }); + } catch (error) { + console.error('Service search API error:', error); + res.status(500).json({ + success: false, + message: 'Error searching services' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/routes/services_20251019162544.js b/.history/routes/services_20251019162544.js new file mode 100644 index 0000000..d6ae97c --- /dev/null +++ b/.history/routes/services_20251019162544.js @@ -0,0 +1,141 @@ +const express = require('express'); +const router = express.Router(); +const Service = require('../models/Service'); +const Portfolio = require('../models/Portfolio'); + +// Get all services +router.get('/', async (req, res) => { + try { + const category = req.query.category; + const featured = req.query.featured; + + let query = { isActive: true }; + + if (category && category !== 'all') { + query.category = category; + } + + if (featured === 'true') { + query.featured = true; + } + + const services = await Service.find(query) + .populate('portfolio', 'title images') + .sort({ featured: -1, order: 1 }); + + res.json({ + success: true, + services + }); + } catch (error) { + console.error('Services API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching services' + }); + } +}); + +// Get single service +router.get('/:id', async (req, res) => { + try { + const service = await Service.findById(req.params.id) + .populate('portfolio', 'title shortDescription images category'); + + if (!service || !service.isActive) { + return res.status(404).json({ + success: false, + message: 'Service not found' + }); + } + + // Get related services + const relatedServices = await Service.find({ + _id: { $ne: service._id }, + category: service.category, + isActive: true + }) + .select('name shortDescription icon pricing') + .limit(3); + + res.json({ + success: true, + service, + relatedServices + }); + } catch (error) { + console.error('Service detail API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching service' + }); + } +}); + +// Get service categories +router.get('/meta/categories', async (req, res) => { + try { + const categories = await Service.distinct('category', { isActive: true }); + + // Get count for each category + const categoriesWithCount = await Promise.all( + categories.map(async (category) => { + const count = await Service.countDocuments({ + category, + isActive: true + }); + return { category, count }; + }) + ); + + res.json({ + success: true, + categories: categoriesWithCount + }); + } catch (error) { + console.error('Service categories API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching categories' + }); + } +}); + +// Search services +router.get('/search/:term', async (req, res) => { + try { + const searchTerm = req.params.term; + const limit = parseInt(req.query.limit) || 10; + + const services = await Service.find({ + $and: [ + { isActive: true }, + { + $or: [ + { name: { $regex: searchTerm, $options: 'i' } }, + { description: { $regex: searchTerm, $options: 'i' } }, + { tags: { $in: [new RegExp(searchTerm, 'i')] } } + ] + } + ] + }) + .select('name shortDescription icon pricing category') + .sort({ featured: -1, order: 1 }) + .limit(limit); + + res.json({ + success: true, + services, + searchTerm, + count: services.length + }); + } catch (error) { + console.error('Service search API error:', error); + res.status(500).json({ + success: false, + message: 'Error searching services' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/.history/scripts/build_20251019162023.js b/.history/scripts/build_20251019162023.js new file mode 100644 index 0000000..2b54f55 --- /dev/null +++ b/.history/scripts/build_20251019162023.js @@ -0,0 +1,279 @@ +#!/usr/bin/env node + +/** + * Build script for production deployment + * Handles CSS compilation, asset optimization, and production setup + */ + +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { promisify } = require('util'); + +const execAsync = promisify(exec); + +// Configuration +const BUILD_DIR = path.join(__dirname, '..', 'dist'); +const PUBLIC_DIR = path.join(__dirname, '..', 'public'); +const VIEWS_DIR = path.join(__dirname, '..', 'views'); + +async function buildForProduction() { + try { + console.log('🏗️ Starting production build...'); + + // Create build directory + await createBuildDirectory(); + + // Install production dependencies + await installProductionDependencies(); + + // Optimize assets + await optimizeAssets(); + + // Generate service worker with workbox + await generateServiceWorker(); + + // Create production environment file + await createProductionEnv(); + + // Copy necessary files + await copyProductionFiles(); + + console.log('✅ Production build completed successfully!'); + console.log('📦 Build output available in:', BUILD_DIR); + console.log('🚀 Ready for deployment!'); + + } catch (error) { + console.error('❌ Production build failed:', error); + process.exit(1); + } +} + +async function createBuildDirectory() { + try { + if (fs.existsSync(BUILD_DIR)) { + console.log('🗑️ Cleaning existing build directory...'); + await execAsync(`rm -rf ${BUILD_DIR}`); + } + + fs.mkdirSync(BUILD_DIR, { recursive: true }); + console.log('📁 Created build directory'); + } catch (error) { + console.error('❌ Error creating build directory:', error); + throw error; + } +} + +async function installProductionDependencies() { + try { + console.log('📦 Installing production dependencies...'); + await execAsync('npm ci --only=production'); + console.log('✅ Production dependencies installed'); + } catch (error) { + console.error('❌ Error installing dependencies:', error); + throw error; + } +} + +async function optimizeAssets() { + try { + console.log('🎨 Optimizing CSS and JavaScript...'); + + // Create optimized CSS + const cssContent = fs.readFileSync(path.join(PUBLIC_DIR, 'css', 'main.css'), 'utf8'); + const optimizedCSS = await optimizeCSS(cssContent); + + const buildCSSDir = path.join(BUILD_DIR, 'public', 'css'); + fs.mkdirSync(buildCSSDir, { recursive: true }); + fs.writeFileSync(path.join(buildCSSDir, 'main.min.css'), optimizedCSS); + + // Create optimized JavaScript + const jsContent = fs.readFileSync(path.join(PUBLIC_DIR, 'js', 'main.js'), 'utf8'); + const optimizedJS = await optimizeJS(jsContent); + + const buildJSDir = path.join(BUILD_DIR, 'public', 'js'); + fs.mkdirSync(buildJSDir, { recursive: true }); + fs.writeFileSync(path.join(buildJSDir, 'main.min.js'), optimizedJS); + + // Copy other assets + await copyDirectory(path.join(PUBLIC_DIR, 'images'), path.join(BUILD_DIR, 'public', 'images')); + + console.log('✅ Assets optimized'); + } catch (error) { + console.error('❌ Error optimizing assets:', error); + throw error; + } +} + +async function optimizeCSS(css) { + // Simple CSS minification (remove comments, extra whitespace) + return css + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments + .replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space + .replace(/;\s*}/g, '}') // Remove semicolon before closing brace + .replace(/\s*{\s*/g, '{') // Remove spaces around opening brace + .replace(/;\s*/g, ';') // Remove spaces after semicolons + .replace(/,\s*/g, ',') // Remove spaces after commas + .trim(); +} + +async function optimizeJS(js) { + // Simple JS minification (remove comments, extra whitespace) + return js + .replace(/\/\/.*$/gm, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments + .replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space + .replace(/\n\s*/g, '') // Remove newlines and indentation + .trim(); +} + +async function generateServiceWorker() { + try { + console.log('⚙️ Generating optimized service worker...'); + + const swContent = fs.readFileSync(path.join(PUBLIC_DIR, 'sw.js'), 'utf8'); + const optimizedSW = await optimizeJS(swContent); + + fs.writeFileSync(path.join(BUILD_DIR, 'public', 'sw.js'), optimizedSW); + + // Copy manifest + fs.copyFileSync( + path.join(PUBLIC_DIR, 'manifest.json'), + path.join(BUILD_DIR, 'public', 'manifest.json') + ); + + console.log('✅ Service worker generated'); + } catch (error) { + console.error('❌ Error generating service worker:', error); + throw error; + } +} + +async function createProductionEnv() { + try { + console.log('🔧 Creating production environment configuration...'); + + const prodEnv = ` +# Production Environment Configuration +NODE_ENV=production +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# Security +SESSION_SECRET=your-super-secret-session-key-change-this-in-production +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production + +# File Upload +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password + +# Telegram Bot (Optional) +TELEGRAM_BOT_TOKEN=your-telegram-bot-token + +# Admin Account +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=change-this-password + +# Security Headers +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 +`.trim(); + + fs.writeFileSync(path.join(BUILD_DIR, '.env.production'), prodEnv); + console.log('✅ Production environment file created'); + } catch (error) { + console.error('❌ Error creating production environment:', error); + throw error; + } +} + +async function copyProductionFiles() { + try { + console.log('📋 Copying production files...'); + + // Copy main application files + const filesToCopy = [ + 'server.js', + 'package.json', + 'package-lock.json' + ]; + + for (const file of filesToCopy) { + fs.copyFileSync( + path.join(__dirname, '..', file), + path.join(BUILD_DIR, file) + ); + } + + // Copy directories + await copyDirectory( + path.join(__dirname, '..', 'models'), + path.join(BUILD_DIR, 'models') + ); + + await copyDirectory( + path.join(__dirname, '..', 'routes'), + path.join(BUILD_DIR, 'routes') + ); + + await copyDirectory( + path.join(__dirname, '..', 'views'), + path.join(BUILD_DIR, 'views') + ); + + await copyDirectory( + path.join(__dirname, '..', 'middleware'), + path.join(BUILD_DIR, 'middleware') + ); + + // Create uploads directory + fs.mkdirSync(path.join(BUILD_DIR, 'uploads'), { recursive: true }); + + console.log('✅ Production files copied'); + } catch (error) { + console.error('❌ Error copying production files:', error); + throw error; + } +} + +async function copyDirectory(src, dest) { + try { + if (!fs.existsSync(src)) { + return; + } + + fs.mkdirSync(dest, { recursive: true }); + + const items = fs.readdirSync(src); + + for (const item of items) { + const srcPath = path.join(src, item); + const destPath = path.join(dest, item); + + const stat = fs.statSync(srcPath); + + if (stat.isDirectory()) { + await copyDirectory(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } catch (error) { + console.error(`❌ Error copying directory ${src}:`, error); + throw error; + } +} + +if (require.main === module) { + buildForProduction(); +} + +module.exports = { buildForProduction }; \ No newline at end of file diff --git a/.history/scripts/build_20251019162545.js b/.history/scripts/build_20251019162545.js new file mode 100644 index 0000000..2b54f55 --- /dev/null +++ b/.history/scripts/build_20251019162545.js @@ -0,0 +1,279 @@ +#!/usr/bin/env node + +/** + * Build script for production deployment + * Handles CSS compilation, asset optimization, and production setup + */ + +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { promisify } = require('util'); + +const execAsync = promisify(exec); + +// Configuration +const BUILD_DIR = path.join(__dirname, '..', 'dist'); +const PUBLIC_DIR = path.join(__dirname, '..', 'public'); +const VIEWS_DIR = path.join(__dirname, '..', 'views'); + +async function buildForProduction() { + try { + console.log('🏗️ Starting production build...'); + + // Create build directory + await createBuildDirectory(); + + // Install production dependencies + await installProductionDependencies(); + + // Optimize assets + await optimizeAssets(); + + // Generate service worker with workbox + await generateServiceWorker(); + + // Create production environment file + await createProductionEnv(); + + // Copy necessary files + await copyProductionFiles(); + + console.log('✅ Production build completed successfully!'); + console.log('📦 Build output available in:', BUILD_DIR); + console.log('🚀 Ready for deployment!'); + + } catch (error) { + console.error('❌ Production build failed:', error); + process.exit(1); + } +} + +async function createBuildDirectory() { + try { + if (fs.existsSync(BUILD_DIR)) { + console.log('🗑️ Cleaning existing build directory...'); + await execAsync(`rm -rf ${BUILD_DIR}`); + } + + fs.mkdirSync(BUILD_DIR, { recursive: true }); + console.log('📁 Created build directory'); + } catch (error) { + console.error('❌ Error creating build directory:', error); + throw error; + } +} + +async function installProductionDependencies() { + try { + console.log('📦 Installing production dependencies...'); + await execAsync('npm ci --only=production'); + console.log('✅ Production dependencies installed'); + } catch (error) { + console.error('❌ Error installing dependencies:', error); + throw error; + } +} + +async function optimizeAssets() { + try { + console.log('🎨 Optimizing CSS and JavaScript...'); + + // Create optimized CSS + const cssContent = fs.readFileSync(path.join(PUBLIC_DIR, 'css', 'main.css'), 'utf8'); + const optimizedCSS = await optimizeCSS(cssContent); + + const buildCSSDir = path.join(BUILD_DIR, 'public', 'css'); + fs.mkdirSync(buildCSSDir, { recursive: true }); + fs.writeFileSync(path.join(buildCSSDir, 'main.min.css'), optimizedCSS); + + // Create optimized JavaScript + const jsContent = fs.readFileSync(path.join(PUBLIC_DIR, 'js', 'main.js'), 'utf8'); + const optimizedJS = await optimizeJS(jsContent); + + const buildJSDir = path.join(BUILD_DIR, 'public', 'js'); + fs.mkdirSync(buildJSDir, { recursive: true }); + fs.writeFileSync(path.join(buildJSDir, 'main.min.js'), optimizedJS); + + // Copy other assets + await copyDirectory(path.join(PUBLIC_DIR, 'images'), path.join(BUILD_DIR, 'public', 'images')); + + console.log('✅ Assets optimized'); + } catch (error) { + console.error('❌ Error optimizing assets:', error); + throw error; + } +} + +async function optimizeCSS(css) { + // Simple CSS minification (remove comments, extra whitespace) + return css + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments + .replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space + .replace(/;\s*}/g, '}') // Remove semicolon before closing brace + .replace(/\s*{\s*/g, '{') // Remove spaces around opening brace + .replace(/;\s*/g, ';') // Remove spaces after semicolons + .replace(/,\s*/g, ',') // Remove spaces after commas + .trim(); +} + +async function optimizeJS(js) { + // Simple JS minification (remove comments, extra whitespace) + return js + .replace(/\/\/.*$/gm, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments + .replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space + .replace(/\n\s*/g, '') // Remove newlines and indentation + .trim(); +} + +async function generateServiceWorker() { + try { + console.log('⚙️ Generating optimized service worker...'); + + const swContent = fs.readFileSync(path.join(PUBLIC_DIR, 'sw.js'), 'utf8'); + const optimizedSW = await optimizeJS(swContent); + + fs.writeFileSync(path.join(BUILD_DIR, 'public', 'sw.js'), optimizedSW); + + // Copy manifest + fs.copyFileSync( + path.join(PUBLIC_DIR, 'manifest.json'), + path.join(BUILD_DIR, 'public', 'manifest.json') + ); + + console.log('✅ Service worker generated'); + } catch (error) { + console.error('❌ Error generating service worker:', error); + throw error; + } +} + +async function createProductionEnv() { + try { + console.log('🔧 Creating production environment configuration...'); + + const prodEnv = ` +# Production Environment Configuration +NODE_ENV=production +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# Security +SESSION_SECRET=your-super-secret-session-key-change-this-in-production +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production + +# File Upload +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password + +# Telegram Bot (Optional) +TELEGRAM_BOT_TOKEN=your-telegram-bot-token + +# Admin Account +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=change-this-password + +# Security Headers +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 +`.trim(); + + fs.writeFileSync(path.join(BUILD_DIR, '.env.production'), prodEnv); + console.log('✅ Production environment file created'); + } catch (error) { + console.error('❌ Error creating production environment:', error); + throw error; + } +} + +async function copyProductionFiles() { + try { + console.log('📋 Copying production files...'); + + // Copy main application files + const filesToCopy = [ + 'server.js', + 'package.json', + 'package-lock.json' + ]; + + for (const file of filesToCopy) { + fs.copyFileSync( + path.join(__dirname, '..', file), + path.join(BUILD_DIR, file) + ); + } + + // Copy directories + await copyDirectory( + path.join(__dirname, '..', 'models'), + path.join(BUILD_DIR, 'models') + ); + + await copyDirectory( + path.join(__dirname, '..', 'routes'), + path.join(BUILD_DIR, 'routes') + ); + + await copyDirectory( + path.join(__dirname, '..', 'views'), + path.join(BUILD_DIR, 'views') + ); + + await copyDirectory( + path.join(__dirname, '..', 'middleware'), + path.join(BUILD_DIR, 'middleware') + ); + + // Create uploads directory + fs.mkdirSync(path.join(BUILD_DIR, 'uploads'), { recursive: true }); + + console.log('✅ Production files copied'); + } catch (error) { + console.error('❌ Error copying production files:', error); + throw error; + } +} + +async function copyDirectory(src, dest) { + try { + if (!fs.existsSync(src)) { + return; + } + + fs.mkdirSync(dest, { recursive: true }); + + const items = fs.readdirSync(src); + + for (const item of items) { + const srcPath = path.join(src, item); + const destPath = path.join(dest, item); + + const stat = fs.statSync(srcPath); + + if (stat.isDirectory()) { + await copyDirectory(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } catch (error) { + console.error(`❌ Error copying directory ${src}:`, error); + throw error; + } +} + +if (require.main === module) { + buildForProduction(); +} + +module.exports = { buildForProduction }; \ No newline at end of file diff --git a/.history/scripts/dev_20251019161952.js b/.history/scripts/dev_20251019161952.js new file mode 100644 index 0000000..0a9d859 --- /dev/null +++ b/.history/scripts/dev_20251019161952.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +/** + * Development server with hot reload for SmartSolTech + * Uses nodemon for automatic server restart on file changes + */ + +const path = require('path'); +const { spawn } = require('child_process'); + +// Configuration +const SERVER_FILE = path.join(__dirname, '..', 'server.js'); +const NODEMON_CONFIG = { + script: SERVER_FILE, + ext: 'js,json,ejs', + ignore: ['node_modules/', 'public/', '.git/'], + watch: ['models/', 'routes/', 'views/', 'server.js'], + env: { + NODE_ENV: 'development', + DEBUG: 'app:*' + }, + delay: 1000 +}; + +function startDevelopmentServer() { + console.log('🚀 Starting SmartSolTech development server...'); + console.log('📁 Watching files for changes...'); + console.log('🔄 Server will automatically restart on file changes'); + console.log(''); + + const nodemonArgs = [ + '--script', NODEMON_CONFIG.script, + '--ext', NODEMON_CONFIG.ext, + '--ignore', NODEMON_CONFIG.ignore.join(','), + '--watch', NODEMON_CONFIG.watch.join(','), + '--delay', NODEMON_CONFIG.delay.toString() + ]; + + const nodemon = spawn('npx', ['nodemon', ...nodemonArgs], { + stdio: 'inherit', + env: { + ...process.env, + ...NODEMON_CONFIG.env + } + }); + + nodemon.on('close', (code) => { + console.log(`\n🛑 Development server exited with code ${code}`); + process.exit(code); + }); + + nodemon.on('error', (error) => { + console.error('❌ Error starting development server:', error); + process.exit(1); + }); + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('\n🛑 Shutting down development server...'); + nodemon.kill('SIGINT'); + }); + + process.on('SIGTERM', () => { + console.log('\n🛑 Shutting down development server...'); + nodemon.kill('SIGTERM'); + }); +} + +if (require.main === module) { + startDevelopmentServer(); +} + +module.exports = { startDevelopmentServer }; \ No newline at end of file diff --git a/.history/scripts/dev_20251019162545.js b/.history/scripts/dev_20251019162545.js new file mode 100644 index 0000000..0a9d859 --- /dev/null +++ b/.history/scripts/dev_20251019162545.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +/** + * Development server with hot reload for SmartSolTech + * Uses nodemon for automatic server restart on file changes + */ + +const path = require('path'); +const { spawn } = require('child_process'); + +// Configuration +const SERVER_FILE = path.join(__dirname, '..', 'server.js'); +const NODEMON_CONFIG = { + script: SERVER_FILE, + ext: 'js,json,ejs', + ignore: ['node_modules/', 'public/', '.git/'], + watch: ['models/', 'routes/', 'views/', 'server.js'], + env: { + NODE_ENV: 'development', + DEBUG: 'app:*' + }, + delay: 1000 +}; + +function startDevelopmentServer() { + console.log('🚀 Starting SmartSolTech development server...'); + console.log('📁 Watching files for changes...'); + console.log('🔄 Server will automatically restart on file changes'); + console.log(''); + + const nodemonArgs = [ + '--script', NODEMON_CONFIG.script, + '--ext', NODEMON_CONFIG.ext, + '--ignore', NODEMON_CONFIG.ignore.join(','), + '--watch', NODEMON_CONFIG.watch.join(','), + '--delay', NODEMON_CONFIG.delay.toString() + ]; + + const nodemon = spawn('npx', ['nodemon', ...nodemonArgs], { + stdio: 'inherit', + env: { + ...process.env, + ...NODEMON_CONFIG.env + } + }); + + nodemon.on('close', (code) => { + console.log(`\n🛑 Development server exited with code ${code}`); + process.exit(code); + }); + + nodemon.on('error', (error) => { + console.error('❌ Error starting development server:', error); + process.exit(1); + }); + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('\n🛑 Shutting down development server...'); + nodemon.kill('SIGINT'); + }); + + process.on('SIGTERM', () => { + console.log('\n🛑 Shutting down development server...'); + nodemon.kill('SIGTERM'); + }); +} + +if (require.main === module) { + startDevelopmentServer(); +} + +module.exports = { startDevelopmentServer }; \ No newline at end of file diff --git a/.history/scripts/init-db_20251019161840.js b/.history/scripts/init-db_20251019161840.js new file mode 100644 index 0000000..5c4d272 --- /dev/null +++ b/.history/scripts/init-db_20251019161840.js @@ -0,0 +1,498 @@ +#!/usr/bin/env node + +/** + * Database initialization script for SmartSolTech + * Creates initial admin user and sample data + */ + +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); +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'); + +// 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'); + + // Create admin user + await createAdminUser(); + + // Create sample services + await createSampleServices(); + + // Create sample portfolio items + await createSamplePortfolio(); + + // Create site settings + await createSiteSettings(); + + console.log('🎉 Database initialization completed successfully!'); + console.log(`📧 Admin login: ${ADMIN_EMAIL}`); + console.log(`🔑 Admin password: ${ADMIN_PASSWORD}`); + console.log('⚠️ Please change the admin password after first login!'); + + } catch (error) { + console.error('❌ Database initialization failed:', error); + process.exit(1); + } finally { + await mongoose.connection.close(); + console.log('🔌 Database connection closed'); + process.exit(0); + } +} + +async function createAdminUser() { + try { + const existingAdmin = await User.findOne({ email: ADMIN_EMAIL }); + + if (existingAdmin) { + console.log('👤 Admin user already exists, skipping...'); + return; + } + + const adminUser = new User({ + name: 'Administrator', + email: ADMIN_EMAIL, + password: ADMIN_PASSWORD, + role: 'admin', + isActive: true + }); + + await adminUser.save(); + console.log('✅ Admin user created successfully'); + } catch (error) { + console.error('❌ Error creating admin user:', error); + throw error; + } +} + +async function createSampleServices() { + try { + const existingServices = await Service.countDocuments(); + + if (existingServices > 0) { + console.log('🛠️ Services already exist, skipping...'); + return; + } + + const services = [ + { + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + features: [ + { name: '반응형 디자인', description: '모든 디바이스에서 완벽하게 작동', included: true }, + { name: 'SEO 최적화', description: '검색엔진 최적화로 더 많은 방문자 유치', included: true }, + { name: '성능 최적화', description: '빠른 로딩 속도와 원활한 사용자 경험', included: true }, + { name: '보안 강화', description: '최신 보안 기술로 안전한 웹사이트', included: true }, + { name: '유지보수', description: '6개월 무료 유지보수 지원', included: true } + ], + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + estimatedTime: { min: 2, max: 8, unit: 'weeks' }, + featured: true, + isActive: true, + order: 1, + tags: ['React', 'Node.js', 'MongoDB', 'Express', 'JavaScript', 'HTML', 'CSS'] + }, + { + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + features: [ + { name: '크로스플랫폼 개발', description: 'iOS와 Android 동시 지원', included: true }, + { name: '네이티브 성능', description: '최적화된 성능과 사용자 경험', included: true }, + { name: '푸시 알림', description: '실시간 푸시 알림 시스템', included: true }, + { name: '오프라인 지원', description: '인터넷 연결 없이도 기본 기능 사용 가능', included: false }, + { name: '앱스토어 등록', description: '앱스토어 및 플레이스토어 등록 지원', included: true } + ], + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + estimatedTime: { min: 4, max: 16, unit: 'weeks' }, + featured: true, + isActive: true, + order: 2, + tags: ['React Native', 'Flutter', 'iOS', 'Android', 'Swift', 'Kotlin', 'JavaScript'] + }, + { + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + features: [ + { name: '사용자 경험 연구', description: '타겟 사용자 분석 및 페르소나 설정', included: true }, + { name: '와이어프레임', description: '정보 구조와 레이아웃 설계', included: true }, + { name: '프로토타이핑', description: '인터랙티브 프로토타입 제작', included: true }, + { name: '비주얼 디자인', description: '브랜드에 맞는 시각적 디자인', included: true }, + { name: '디자인 시스템', description: '일관된 디자인을 위한 가이드라인', included: false } + ], + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + estimatedTime: { min: 1, max: 6, unit: 'weeks' }, + featured: true, + isActive: true, + order: 3, + tags: ['Figma', 'Sketch', 'Adobe XD', 'Photoshop', 'Illustrator', 'Prototyping'] + }, + { + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + features: [ + { name: 'SEO 최적화', description: '검색엔진 상위 노출을 위한 최적화', included: true }, + { name: '소셜미디어 관리', description: '페이스북, 인스타그램, 유튜브 관리', included: true }, + { name: '구글 광고', description: '구글 애즈를 통한 타겟 광고', included: true }, + { name: '콘텐츠 마케팅', description: '블로그 및 콘텐츠 제작', included: false }, + { name: '분석 및 리포팅', description: '마케팅 성과 분석 및 보고서', included: true } + ], + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + estimatedTime: { min: 2, max: 12, unit: 'weeks' }, + featured: true, + isActive: true, + order: 4, + tags: ['SEO', 'Google Ads', 'Facebook Ads', 'Analytics', 'Social Media', 'Content Marketing'] + }, + { + name: '브랜딩', + description: '기업의 정체성을 반영하는 로고, 브랜드 가이드라인, 마케팅 자료를 디자인합니다. 일관된 브랜드 이미지로 브랜드 가치를 높입니다.', + shortDescription: '로고, 브랜드 가이드라인, 마케팅 자료 디자인', + icon: 'fas fa-copyright', + category: 'design', + features: [ + { name: '로고 디자인', description: '기업 정체성을 반영한 로고 제작', included: true }, + { name: '브랜드 가이드라인', description: '색상, 폰트, 사용법 가이드', included: true }, + { name: '명함 디자인', description: '브랜드에 맞는 명함 디자인', included: true }, + { name: '브로슈어 디자인', description: '회사 소개 브로슈어 제작', included: false }, + { name: '웹 브랜딩', description: '웹사이트 브랜딩 요소 적용', included: false } + ], + pricing: { + basePrice: 400000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 400000, max: 2500000 } + }, + estimatedTime: { min: 2, max: 8, unit: 'weeks' }, + featured: false, + isActive: true, + order: 5, + tags: ['Logo Design', 'Brand Identity', 'Graphic Design', 'Corporate Design'] + }, + { + name: '기술 컨설팅', + description: '기업의 디지털 전환과 기술 도입을 위한 전문적인 컨설팅을 제공합니다. 기술 아키텍처 설계부터 구현 전략까지 포괄적으로 지원합니다.', + shortDescription: '디지털 전환 및 기술 도입을 위한 전문 컨설팅', + icon: 'fas fa-lightbulb', + category: 'consulting', + features: [ + { name: '기술 분석', description: '현재 기술 스택 분석 및 개선안 제시', included: true }, + { name: '아키텍처 설계', description: '확장 가능한 시스템 아키텍처 설계', included: true }, + { name: '개발 프로세스 개선', description: '효율적인 개발 워크플로우 구축', included: true }, + { name: '팀 교육', description: '개발팀 대상 기술 교육', included: false }, + { name: '지속적인 지원', description: '프로젝트 완료 후 지속적인 기술 지원', included: false } + ], + pricing: { + basePrice: 150000, + currency: 'KRW', + priceType: 'hourly', + priceRange: { min: 150000, max: 500000 } + }, + estimatedTime: { min: 1, max: 4, unit: 'weeks' }, + featured: false, + isActive: true, + order: 6, + tags: ['Architecture', 'Strategy', 'Process', 'Training', 'Consultation'] + } + ]; + + await Service.insertMany(services); + console.log('✅ Sample services created successfully'); + } catch (error) { + console.error('❌ Error creating sample services:', error); + throw error; + } +} + +async function createSamplePortfolio() { + try { + const existingPortfolio = await Portfolio.countDocuments(); + + if (existingPortfolio > 0) { + console.log('🎨 Portfolio items already exist, skipping...'); + return; + } + + const portfolioItems = [ + { + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }, + { url: '/images/portfolio/ecommerce-2.jpg', alt: '상품 상세페이지', isPrimary: false }, + { url: '/images/portfolio/ecommerce-3.jpg', alt: '장바구니 페이지', isPrimary: false } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.com', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25, + order: 1, + seo: { + metaTitle: 'E-commerce 플랫폼 개발 프로젝트', + metaDescription: '현대적인 온라인 쇼핑몰 플랫폼 개발 사례', + keywords: ['E-commerce', 'React', 'Node.js', '온라인쇼핑몰'] + } + }, + { + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }, + { url: '/images/portfolio/fitness-2.jpg', alt: '운동 계획 화면', isPrimary: false }, + { url: '/images/portfolio/fitness-3.jpg', alt: '통계 화면', isPrimary: false } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35, + order: 2, + seo: { + metaTitle: '모바일 피트니스 앱 개발 프로젝트', + metaDescription: 'React Native로 개발한 크로스플랫폼 피트니스 앱', + keywords: ['Mobile App', 'React Native', 'Fitness', '헬스케어'] + } + }, + { + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }, + { url: '/images/portfolio/corporate-2.jpg', alt: '회사소개 페이지', isPrimary: false }, + { url: '/images/portfolio/corporate-3.jpg', alt: '서비스 페이지', isPrimary: false } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.com', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18, + order: 3, + seo: { + metaTitle: '기업 웹사이트 리뉴얼 프로젝트', + metaDescription: '모던한 디자인과 향상된 UX의 기업 웹사이트', + keywords: ['Web Design', 'Corporate', 'UI/UX', '웹사이트리뉴얼'] + } + }, + { + title: '레스토랑 예약 시스템', + description: '레스토랑을 위한 온라인 예약 시스템을 개발했습니다. 실시간 테이블 현황, 예약 관리, 고객 관리 기능을 포함하여 레스토랑 운영 효율성을 높였습니다.', + shortDescription: '실시간 테이블 예약 및 관리 시스템', + category: 'web-development', + technologies: ['Vue.js', 'Laravel', 'MySQL', 'Socket.io', 'Bootstrap'], + images: [ + { url: '/images/portfolio/restaurant-1.jpg', alt: '예약 시스템 메인', isPrimary: true }, + { url: '/images/portfolio/restaurant-2.jpg', alt: '예약 현황 관리', isPrimary: false } + ], + clientName: '레스토랑 델리셔스', + status: 'completed', + featured: false, + publishedAt: new Date('2024-04-05'), + completedAt: new Date('2024-04-01'), + isPublished: true, + viewCount: 85, + likes: 12, + order: 4, + seo: { + metaTitle: '레스토랑 예약 시스템 개발', + metaDescription: '실시간 테이블 예약 및 관리 웹 시스템', + keywords: ['Reservation System', 'Vue.js', 'Laravel', '예약시스템'] + } + }, + { + title: '교육 플랫폼 앱', + description: '온라인 교육을 위한 모바일 애플리케이션입니다. 동영상 강의, 퀴즈, 진도 관리 기능을 포함하여 효과적인 학습 환경을 제공합니다.', + shortDescription: '온라인 학습을 위한 교육 플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['Flutter', 'Dart', 'Firebase', 'FFmpeg', 'AWS S3'], + images: [ + { url: '/images/portfolio/education-1.jpg', alt: '교육 앱 메인화면', isPrimary: true }, + { url: '/images/portfolio/education-2.jpg', alt: '강의 재생 화면', isPrimary: false } + ], + clientName: '온라인 교육 기업 EduTech', + status: 'completed', + featured: false, + publishedAt: new Date('2024-05-12'), + completedAt: new Date('2024-05-08'), + isPublished: true, + viewCount: 95, + likes: 20, + order: 5, + seo: { + metaTitle: '교육 플랫폼 모바일 앱 개발', + metaDescription: 'Flutter로 개발한 온라인 교육 모바일 앱', + keywords: ['Education App', 'Flutter', 'E-learning', '교육앱'] + } + }, + { + title: 'IoT 대시보드', + description: 'IoT 디바이스들을 모니터링하고 제어할 수 있는 웹 대시보드를 개발했습니다. 실시간 데이터 시각화와 알림 기능을 포함합니다.', + shortDescription: 'IoT 디바이스 모니터링 및 제어 웹 대시보드', + category: 'web-development', + technologies: ['React', 'D3.js', 'Node.js', 'MQTT', 'InfluxDB', 'Docker'], + images: [ + { url: '/images/portfolio/iot-1.jpg', alt: 'IoT 대시보드 메인', isPrimary: true } + ], + clientName: 'IoT 솔루션 기업 SmartDevice', + status: 'in-progress', + featured: false, + publishedAt: new Date('2024-06-01'), + isPublished: true, + viewCount: 45, + likes: 8, + order: 6, + seo: { + metaTitle: 'IoT 대시보드 개발 프로젝트', + metaDescription: '실시간 IoT 디바이스 모니터링 웹 대시보드', + keywords: ['IoT', 'Dashboard', 'Real-time', 'Monitoring'] + } + } + ]; + + await Portfolio.insertMany(portfolioItems); + console.log('✅ Sample portfolio items created successfully'); + } catch (error) { + console.error('❌ Error creating sample portfolio:', error); + throw error; + } +} + +async function createSiteSettings() { + try { + const existingSettings = await SiteSettings.findOne(); + + if (existingSettings) { + console.log('⚙️ Site settings already exist, skipping...'); + return; + } + + const settings = new SiteSettings({ + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + logo: '/images/logo.png', + favicon: '/images/favicon.ico', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech', + github: 'https://github.com/smartsoltech' + }, + telegram: { + isEnabled: false + }, + seo: { + metaTitle: 'SmartSolTech - 혁신적인 기술 솔루션', + metaDescription: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅 전문 기업. 한국에서 최고의 기술 솔루션을 제공합니다.', + keywords: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅, 기술 솔루션, 한국, SmartSolTech' + }, + hero: { + title: 'Smart Technology Solutions', + subtitle: '혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다', + backgroundImage: '/images/hero-bg.jpg', + ctaText: '프로젝트 시작하기', + ctaLink: '/contact' + }, + about: { + title: 'SmartSolTech 소개', + description: '우리는 최신 기술과 창의적인 아이디어로 고객의 비즈니스 성장을 지원하는 전문 개발팀입니다. 웹 개발부터 모바일 앱, UI/UX 디자인까지 포괄적인 디지털 솔루션을 제공합니다.', + image: '/images/about.jpg' + }, + maintenance: { + isEnabled: false, + message: '현재 시스템 점검 중입니다. 잠시 후 다시 접속해 주세요.' + } + }); + + await settings.save(); + console.log('✅ Site settings created successfully'); + } catch (error) { + console.error('❌ Error creating site settings:', error); + throw error; + } +} + +// Run the initialization +if (require.main === module) { + initializeDatabase(); +} + +module.exports = { + initializeDatabase, + createAdminUser, + createSampleServices, + createSamplePortfolio, + createSiteSettings +}; \ No newline at end of file diff --git a/.history/scripts/init-db_20251019162545.js b/.history/scripts/init-db_20251019162545.js new file mode 100644 index 0000000..5c4d272 --- /dev/null +++ b/.history/scripts/init-db_20251019162545.js @@ -0,0 +1,498 @@ +#!/usr/bin/env node + +/** + * Database initialization script for SmartSolTech + * Creates initial admin user and sample data + */ + +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); +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'); + +// 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'); + + // Create admin user + await createAdminUser(); + + // Create sample services + await createSampleServices(); + + // Create sample portfolio items + await createSamplePortfolio(); + + // Create site settings + await createSiteSettings(); + + console.log('🎉 Database initialization completed successfully!'); + console.log(`📧 Admin login: ${ADMIN_EMAIL}`); + console.log(`🔑 Admin password: ${ADMIN_PASSWORD}`); + console.log('⚠️ Please change the admin password after first login!'); + + } catch (error) { + console.error('❌ Database initialization failed:', error); + process.exit(1); + } finally { + await mongoose.connection.close(); + console.log('🔌 Database connection closed'); + process.exit(0); + } +} + +async function createAdminUser() { + try { + const existingAdmin = await User.findOne({ email: ADMIN_EMAIL }); + + if (existingAdmin) { + console.log('👤 Admin user already exists, skipping...'); + return; + } + + const adminUser = new User({ + name: 'Administrator', + email: ADMIN_EMAIL, + password: ADMIN_PASSWORD, + role: 'admin', + isActive: true + }); + + await adminUser.save(); + console.log('✅ Admin user created successfully'); + } catch (error) { + console.error('❌ Error creating admin user:', error); + throw error; + } +} + +async function createSampleServices() { + try { + const existingServices = await Service.countDocuments(); + + if (existingServices > 0) { + console.log('🛠️ Services already exist, skipping...'); + return; + } + + const services = [ + { + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + features: [ + { name: '반응형 디자인', description: '모든 디바이스에서 완벽하게 작동', included: true }, + { name: 'SEO 최적화', description: '검색엔진 최적화로 더 많은 방문자 유치', included: true }, + { name: '성능 최적화', description: '빠른 로딩 속도와 원활한 사용자 경험', included: true }, + { name: '보안 강화', description: '최신 보안 기술로 안전한 웹사이트', included: true }, + { name: '유지보수', description: '6개월 무료 유지보수 지원', included: true } + ], + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + estimatedTime: { min: 2, max: 8, unit: 'weeks' }, + featured: true, + isActive: true, + order: 1, + tags: ['React', 'Node.js', 'MongoDB', 'Express', 'JavaScript', 'HTML', 'CSS'] + }, + { + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + features: [ + { name: '크로스플랫폼 개발', description: 'iOS와 Android 동시 지원', included: true }, + { name: '네이티브 성능', description: '최적화된 성능과 사용자 경험', included: true }, + { name: '푸시 알림', description: '실시간 푸시 알림 시스템', included: true }, + { name: '오프라인 지원', description: '인터넷 연결 없이도 기본 기능 사용 가능', included: false }, + { name: '앱스토어 등록', description: '앱스토어 및 플레이스토어 등록 지원', included: true } + ], + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + estimatedTime: { min: 4, max: 16, unit: 'weeks' }, + featured: true, + isActive: true, + order: 2, + tags: ['React Native', 'Flutter', 'iOS', 'Android', 'Swift', 'Kotlin', 'JavaScript'] + }, + { + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + features: [ + { name: '사용자 경험 연구', description: '타겟 사용자 분석 및 페르소나 설정', included: true }, + { name: '와이어프레임', description: '정보 구조와 레이아웃 설계', included: true }, + { name: '프로토타이핑', description: '인터랙티브 프로토타입 제작', included: true }, + { name: '비주얼 디자인', description: '브랜드에 맞는 시각적 디자인', included: true }, + { name: '디자인 시스템', description: '일관된 디자인을 위한 가이드라인', included: false } + ], + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + estimatedTime: { min: 1, max: 6, unit: 'weeks' }, + featured: true, + isActive: true, + order: 3, + tags: ['Figma', 'Sketch', 'Adobe XD', 'Photoshop', 'Illustrator', 'Prototyping'] + }, + { + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + features: [ + { name: 'SEO 최적화', description: '검색엔진 상위 노출을 위한 최적화', included: true }, + { name: '소셜미디어 관리', description: '페이스북, 인스타그램, 유튜브 관리', included: true }, + { name: '구글 광고', description: '구글 애즈를 통한 타겟 광고', included: true }, + { name: '콘텐츠 마케팅', description: '블로그 및 콘텐츠 제작', included: false }, + { name: '분석 및 리포팅', description: '마케팅 성과 분석 및 보고서', included: true } + ], + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + estimatedTime: { min: 2, max: 12, unit: 'weeks' }, + featured: true, + isActive: true, + order: 4, + tags: ['SEO', 'Google Ads', 'Facebook Ads', 'Analytics', 'Social Media', 'Content Marketing'] + }, + { + name: '브랜딩', + description: '기업의 정체성을 반영하는 로고, 브랜드 가이드라인, 마케팅 자료를 디자인합니다. 일관된 브랜드 이미지로 브랜드 가치를 높입니다.', + shortDescription: '로고, 브랜드 가이드라인, 마케팅 자료 디자인', + icon: 'fas fa-copyright', + category: 'design', + features: [ + { name: '로고 디자인', description: '기업 정체성을 반영한 로고 제작', included: true }, + { name: '브랜드 가이드라인', description: '색상, 폰트, 사용법 가이드', included: true }, + { name: '명함 디자인', description: '브랜드에 맞는 명함 디자인', included: true }, + { name: '브로슈어 디자인', description: '회사 소개 브로슈어 제작', included: false }, + { name: '웹 브랜딩', description: '웹사이트 브랜딩 요소 적용', included: false } + ], + pricing: { + basePrice: 400000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 400000, max: 2500000 } + }, + estimatedTime: { min: 2, max: 8, unit: 'weeks' }, + featured: false, + isActive: true, + order: 5, + tags: ['Logo Design', 'Brand Identity', 'Graphic Design', 'Corporate Design'] + }, + { + name: '기술 컨설팅', + description: '기업의 디지털 전환과 기술 도입을 위한 전문적인 컨설팅을 제공합니다. 기술 아키텍처 설계부터 구현 전략까지 포괄적으로 지원합니다.', + shortDescription: '디지털 전환 및 기술 도입을 위한 전문 컨설팅', + icon: 'fas fa-lightbulb', + category: 'consulting', + features: [ + { name: '기술 분석', description: '현재 기술 스택 분석 및 개선안 제시', included: true }, + { name: '아키텍처 설계', description: '확장 가능한 시스템 아키텍처 설계', included: true }, + { name: '개발 프로세스 개선', description: '효율적인 개발 워크플로우 구축', included: true }, + { name: '팀 교육', description: '개발팀 대상 기술 교육', included: false }, + { name: '지속적인 지원', description: '프로젝트 완료 후 지속적인 기술 지원', included: false } + ], + pricing: { + basePrice: 150000, + currency: 'KRW', + priceType: 'hourly', + priceRange: { min: 150000, max: 500000 } + }, + estimatedTime: { min: 1, max: 4, unit: 'weeks' }, + featured: false, + isActive: true, + order: 6, + tags: ['Architecture', 'Strategy', 'Process', 'Training', 'Consultation'] + } + ]; + + await Service.insertMany(services); + console.log('✅ Sample services created successfully'); + } catch (error) { + console.error('❌ Error creating sample services:', error); + throw error; + } +} + +async function createSamplePortfolio() { + try { + const existingPortfolio = await Portfolio.countDocuments(); + + if (existingPortfolio > 0) { + console.log('🎨 Portfolio items already exist, skipping...'); + return; + } + + const portfolioItems = [ + { + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }, + { url: '/images/portfolio/ecommerce-2.jpg', alt: '상품 상세페이지', isPrimary: false }, + { url: '/images/portfolio/ecommerce-3.jpg', alt: '장바구니 페이지', isPrimary: false } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.com', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25, + order: 1, + seo: { + metaTitle: 'E-commerce 플랫폼 개발 프로젝트', + metaDescription: '현대적인 온라인 쇼핑몰 플랫폼 개발 사례', + keywords: ['E-commerce', 'React', 'Node.js', '온라인쇼핑몰'] + } + }, + { + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }, + { url: '/images/portfolio/fitness-2.jpg', alt: '운동 계획 화면', isPrimary: false }, + { url: '/images/portfolio/fitness-3.jpg', alt: '통계 화면', isPrimary: false } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35, + order: 2, + seo: { + metaTitle: '모바일 피트니스 앱 개발 프로젝트', + metaDescription: 'React Native로 개발한 크로스플랫폼 피트니스 앱', + keywords: ['Mobile App', 'React Native', 'Fitness', '헬스케어'] + } + }, + { + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }, + { url: '/images/portfolio/corporate-2.jpg', alt: '회사소개 페이지', isPrimary: false }, + { url: '/images/portfolio/corporate-3.jpg', alt: '서비스 페이지', isPrimary: false } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.com', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18, + order: 3, + seo: { + metaTitle: '기업 웹사이트 리뉴얼 프로젝트', + metaDescription: '모던한 디자인과 향상된 UX의 기업 웹사이트', + keywords: ['Web Design', 'Corporate', 'UI/UX', '웹사이트리뉴얼'] + } + }, + { + title: '레스토랑 예약 시스템', + description: '레스토랑을 위한 온라인 예약 시스템을 개발했습니다. 실시간 테이블 현황, 예약 관리, 고객 관리 기능을 포함하여 레스토랑 운영 효율성을 높였습니다.', + shortDescription: '실시간 테이블 예약 및 관리 시스템', + category: 'web-development', + technologies: ['Vue.js', 'Laravel', 'MySQL', 'Socket.io', 'Bootstrap'], + images: [ + { url: '/images/portfolio/restaurant-1.jpg', alt: '예약 시스템 메인', isPrimary: true }, + { url: '/images/portfolio/restaurant-2.jpg', alt: '예약 현황 관리', isPrimary: false } + ], + clientName: '레스토랑 델리셔스', + status: 'completed', + featured: false, + publishedAt: new Date('2024-04-05'), + completedAt: new Date('2024-04-01'), + isPublished: true, + viewCount: 85, + likes: 12, + order: 4, + seo: { + metaTitle: '레스토랑 예약 시스템 개발', + metaDescription: '실시간 테이블 예약 및 관리 웹 시스템', + keywords: ['Reservation System', 'Vue.js', 'Laravel', '예약시스템'] + } + }, + { + title: '교육 플랫폼 앱', + description: '온라인 교육을 위한 모바일 애플리케이션입니다. 동영상 강의, 퀴즈, 진도 관리 기능을 포함하여 효과적인 학습 환경을 제공합니다.', + shortDescription: '온라인 학습을 위한 교육 플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['Flutter', 'Dart', 'Firebase', 'FFmpeg', 'AWS S3'], + images: [ + { url: '/images/portfolio/education-1.jpg', alt: '교육 앱 메인화면', isPrimary: true }, + { url: '/images/portfolio/education-2.jpg', alt: '강의 재생 화면', isPrimary: false } + ], + clientName: '온라인 교육 기업 EduTech', + status: 'completed', + featured: false, + publishedAt: new Date('2024-05-12'), + completedAt: new Date('2024-05-08'), + isPublished: true, + viewCount: 95, + likes: 20, + order: 5, + seo: { + metaTitle: '교육 플랫폼 모바일 앱 개발', + metaDescription: 'Flutter로 개발한 온라인 교육 모바일 앱', + keywords: ['Education App', 'Flutter', 'E-learning', '교육앱'] + } + }, + { + title: 'IoT 대시보드', + description: 'IoT 디바이스들을 모니터링하고 제어할 수 있는 웹 대시보드를 개발했습니다. 실시간 데이터 시각화와 알림 기능을 포함합니다.', + shortDescription: 'IoT 디바이스 모니터링 및 제어 웹 대시보드', + category: 'web-development', + technologies: ['React', 'D3.js', 'Node.js', 'MQTT', 'InfluxDB', 'Docker'], + images: [ + { url: '/images/portfolio/iot-1.jpg', alt: 'IoT 대시보드 메인', isPrimary: true } + ], + clientName: 'IoT 솔루션 기업 SmartDevice', + status: 'in-progress', + featured: false, + publishedAt: new Date('2024-06-01'), + isPublished: true, + viewCount: 45, + likes: 8, + order: 6, + seo: { + metaTitle: 'IoT 대시보드 개발 프로젝트', + metaDescription: '실시간 IoT 디바이스 모니터링 웹 대시보드', + keywords: ['IoT', 'Dashboard', 'Real-time', 'Monitoring'] + } + } + ]; + + await Portfolio.insertMany(portfolioItems); + console.log('✅ Sample portfolio items created successfully'); + } catch (error) { + console.error('❌ Error creating sample portfolio:', error); + throw error; + } +} + +async function createSiteSettings() { + try { + const existingSettings = await SiteSettings.findOne(); + + if (existingSettings) { + console.log('⚙️ Site settings already exist, skipping...'); + return; + } + + const settings = new SiteSettings({ + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + logo: '/images/logo.png', + favicon: '/images/favicon.ico', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech', + github: 'https://github.com/smartsoltech' + }, + telegram: { + isEnabled: false + }, + seo: { + metaTitle: 'SmartSolTech - 혁신적인 기술 솔루션', + metaDescription: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅 전문 기업. 한국에서 최고의 기술 솔루션을 제공합니다.', + keywords: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅, 기술 솔루션, 한국, SmartSolTech' + }, + hero: { + title: 'Smart Technology Solutions', + subtitle: '혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다', + backgroundImage: '/images/hero-bg.jpg', + ctaText: '프로젝트 시작하기', + ctaLink: '/contact' + }, + about: { + title: 'SmartSolTech 소개', + description: '우리는 최신 기술과 창의적인 아이디어로 고객의 비즈니스 성장을 지원하는 전문 개발팀입니다. 웹 개발부터 모바일 앱, UI/UX 디자인까지 포괄적인 디지털 솔루션을 제공합니다.', + image: '/images/about.jpg' + }, + maintenance: { + isEnabled: false, + message: '현재 시스템 점검 중입니다. 잠시 후 다시 접속해 주세요.' + } + }); + + await settings.save(); + console.log('✅ Site settings created successfully'); + } catch (error) { + console.error('❌ Error creating site settings:', error); + throw error; + } +} + +// Run the initialization +if (require.main === module) { + initializeDatabase(); +} + +module.exports = { + initializeDatabase, + createAdminUser, + createSampleServices, + createSamplePortfolio, + createSiteSettings +}; \ No newline at end of file diff --git a/.history/server-demo_20251019163550.js b/.history/server-demo_20251019163550.js new file mode 100644 index 0000000..63a6c75 --- /dev/null +++ b/.history/server-demo_20251019163550.js @@ -0,0 +1,416 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019163807.js b/.history/server-demo_20251019163807.js new file mode 100644 index 0000000..63a6c75 --- /dev/null +++ b/.history/server-demo_20251019163807.js @@ -0,0 +1,416 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019164141.js b/.history/server-demo_20251019164141.js new file mode 100644 index 0000000..2dbb84e --- /dev/null +++ b/.history/server-demo_20251019164141.js @@ -0,0 +1,418 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019165556.js b/.history/server-demo_20251019165556.js new file mode 100644 index 0000000..2dbb84e --- /dev/null +++ b/.history/server-demo_20251019165556.js @@ -0,0 +1,418 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019165723.js b/.history/server-demo_20251019165723.js new file mode 100644 index 0000000..d23037b --- /dev/null +++ b/.history/server-demo_20251019165723.js @@ -0,0 +1,419 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019170013.js b/.history/server-demo_20251019170013.js new file mode 100644 index 0000000..d23037b --- /dev/null +++ b/.history/server-demo_20251019170013.js @@ -0,0 +1,419 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019170431.js b/.history/server-demo_20251019170431.js new file mode 100644 index 0000000..f5af5a0 --- /dev/null +++ b/.history/server-demo_20251019170431.js @@ -0,0 +1,426 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019170533.js b/.history/server-demo_20251019170533.js new file mode 100644 index 0000000..f5af5a0 --- /dev/null +++ b/.history/server-demo_20251019170533.js @@ -0,0 +1,426 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019170746.js b/.history/server-demo_20251019170746.js new file mode 100644 index 0000000..1d0e977 --- /dev/null +++ b/.history/server-demo_20251019170746.js @@ -0,0 +1,427 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019170927.js b/.history/server-demo_20251019170927.js new file mode 100644 index 0000000..1d0e977 --- /dev/null +++ b/.history/server-demo_20251019170927.js @@ -0,0 +1,427 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171746.js b/.history/server-demo_20251019171746.js new file mode 100644 index 0000000..b6527bb --- /dev/null +++ b/.history/server-demo_20251019171746.js @@ -0,0 +1,440 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171805.js b/.history/server-demo_20251019171805.js new file mode 100644 index 0000000..bb22ce6 --- /dev/null +++ b/.history/server-demo_20251019171805.js @@ -0,0 +1,443 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171824.js b/.history/server-demo_20251019171824.js new file mode 100644 index 0000000..895e52e --- /dev/null +++ b/.history/server-demo_20251019171824.js @@ -0,0 +1,474 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171840.js b/.history/server-demo_20251019171840.js new file mode 100644 index 0000000..6640ff5 --- /dev/null +++ b/.history/server-demo_20251019171840.js @@ -0,0 +1,485 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Theme switching route +app.get('/theme/:theme', (req, res) => { + const theme = req.params.theme; + if (['light', 'dark'].includes(theme)) { + res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: res.__('navigation.home') + ' - SmartSolTech', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: '포트폴리오 - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171853.js b/.history/server-demo_20251019171853.js new file mode 100644 index 0000000..c14e838 --- /dev/null +++ b/.history/server-demo_20251019171853.js @@ -0,0 +1,485 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Theme switching route +app.get('/theme/:theme', (req, res) => { + const theme = req.params.theme; + if (['light', 'dark'].includes(theme)) { + res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: res.__('navigation.home') + ' - SmartSolTech', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: res.__('navigation.portfolio') + ' - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - 포트폴리오`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171910.js b/.history/server-demo_20251019171910.js new file mode 100644 index 0000000..8840932 --- /dev/null +++ b/.history/server-demo_20251019171910.js @@ -0,0 +1,486 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Theme switching route +app.get('/theme/:theme', (req, res) => { + const theme = req.params.theme; + if (['light', 'dark'].includes(theme)) { + res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: res.__('navigation.home') + ' - SmartSolTech', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: res.__('navigation.portfolio') + ' - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - ' + res.__('common.error'), + message: res.__('common.error'), + currentPage: 'error' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - ${res.__('navigation.portfolio')}`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: '서비스 - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: '회사 소개 - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: '연락처 - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171926.js b/.history/server-demo_20251019171926.js new file mode 100644 index 0000000..34a0f02 --- /dev/null +++ b/.history/server-demo_20251019171926.js @@ -0,0 +1,486 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Theme switching route +app.get('/theme/:theme', (req, res) => { + const theme = req.params.theme; + if (['light', 'dark'].includes(theme)) { + res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: res.__('navigation.home') + ' - SmartSolTech', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: res.__('navigation.portfolio') + ' - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - ' + res.__('common.error'), + message: res.__('common.error'), + currentPage: 'error' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - ${res.__('navigation.portfolio')}`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: res.__('navigation.services') + ' - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: res.__('navigation.about') + ' - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: res.__('navigation.contact') + ' - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.'); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: '비용 계산기 - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019171941.js b/.history/server-demo_20251019171941.js new file mode 100644 index 0000000..0a61a18 --- /dev/null +++ b/.history/server-demo_20251019171941.js @@ -0,0 +1,486 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Theme switching route +app.get('/theme/:theme', (req, res) => { + const theme = req.params.theme; + if (['light', 'dark'].includes(theme)) { + res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: res.__('navigation.home') + ' - SmartSolTech', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: res.__('navigation.portfolio') + ' - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - ' + res.__('common.error'), + message: res.__('common.error'), + currentPage: 'error' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - ${res.__('navigation.portfolio')}`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: res.__('navigation.services') + ' - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: res.__('navigation.about') + ' - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: res.__('navigation.contact') + ' - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', res.__('common.success')); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: res.__('navigation.calculator') + ' - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server-demo_20251019173728.js b/.history/server-demo_20251019173728.js new file mode 100644 index 0000000..0a61a18 --- /dev/null +++ b/.history/server-demo_20251019173728.js @@ -0,0 +1,486 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Theme switching route +app.get('/theme/:theme', (req, res) => { + const theme = req.params.theme; + if (['light', 'dark'].includes(theme)) { + res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: res.__('navigation.home') + ' - SmartSolTech', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: res.__('navigation.portfolio') + ' - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - ' + res.__('common.error'), + message: res.__('common.error'), + currentPage: 'error' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - ${res.__('navigation.portfolio')}`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: res.__('navigation.services') + ' - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: res.__('navigation.about') + ' - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: res.__('navigation.contact') + ' - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', res.__('common.success')); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: res.__('navigation.calculator') + ' - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server_20251019160526.js b/.history/server_20251019160526.js new file mode 100644 index 0000000..d17df23 --- /dev/null +++ b/.history/server_20251019160526.js @@ -0,0 +1,121 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const session = require('express-session'); +const MongoStore = require('connect-mongo'); +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'); +require('dotenv').config(); + +const app = express(); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com"], + fontSrc: ["'self'", "https://fonts.gstatic.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "ws:", "wss:"] + } + } +})); + +// 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'))); + +// View engine +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// Database connection +mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', { + useNewUrlParser: true, + useUnifiedTopology: true, +}) +.then(() => console.log('✓ MongoDB connected')) +.catch(err => console.error('✗ MongoDB connection error:', err)); + +// Session configuration +app.use(session({ + secret: process.env.SESSION_SECRET || 'your-secret-key', + resave: false, + saveUninitialized: false, + store: MongoStore.create({ + mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', + touchAfter: 24 * 3600 // lazy session update + }), + cookie: { + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days + } +})); + +// 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('/admin', require('./routes/admin')); + +// 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).json({ + success: false, + message: process.env.NODE_ENV === 'production' + ? 'Something went wrong!' + : err.message + }); +}); + +// 404 handler +app.use((req, res) => { + res.status(404).render('404', { + title: '404 - Страница не найдена', + message: 'Запрашиваемая страница не найдена' + }); +}); + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`🚀 Server running on port ${PORT}`); + console.log(`🌐 Visit: http://localhost:${PORT}`); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/server_20251019162544.js b/.history/server_20251019162544.js new file mode 100644 index 0000000..d17df23 --- /dev/null +++ b/.history/server_20251019162544.js @@ -0,0 +1,121 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const session = require('express-session'); +const MongoStore = require('connect-mongo'); +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'); +require('dotenv').config(); + +const app = express(); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com"], + fontSrc: ["'self'", "https://fonts.gstatic.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "ws:", "wss:"] + } + } +})); + +// 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'))); + +// View engine +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// Database connection +mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', { + useNewUrlParser: true, + useUnifiedTopology: true, +}) +.then(() => console.log('✓ MongoDB connected')) +.catch(err => console.error('✗ MongoDB connection error:', err)); + +// Session configuration +app.use(session({ + secret: process.env.SESSION_SECRET || 'your-secret-key', + resave: false, + saveUninitialized: false, + store: MongoStore.create({ + mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', + touchAfter: 24 * 3600 // lazy session update + }), + cookie: { + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days + } +})); + +// 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('/admin', require('./routes/admin')); + +// 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).json({ + success: false, + message: process.env.NODE_ENV === 'production' + ? 'Something went wrong!' + : err.message + }); +}); + +// 404 handler +app.use((req, res) => { + res.status(404).render('404', { + title: '404 - Страница не найдена', + message: 'Запрашиваемая страница не найдена' + }); +}); + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`🚀 Server running on port ${PORT}`); + console.log(`🌐 Visit: http://localhost:${PORT}`); +}); + +module.exports = app; \ No newline at end of file diff --git a/.history/views/about-new_20251019174332.ejs b/.history/views/about-new_20251019174332.ejs new file mode 100644 index 0000000..6954758 --- /dev/null +++ b/.history/views/about-new_20251019174332.ejs @@ -0,0 +1,160 @@ + + + + + + <%- __('about.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('about.hero.title') %> +

+

+ <%- __('about.hero.subtitle') %> +

+
+
+ + +
+
+
+
+

+ <%- __('about.company.title') %> +

+

+ <%- __('about.company.description1') %> +

+

+ <%- __('about.company.description2') %> +

+ + +
+
+
50+
+
<%- __('about.stats.projects') %>
+
+
+
3+
+
<%- __('about.stats.experience') %>
+
+
+
30+
+
<%- __('about.stats.clients') %>
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+

+ <%- __('about.mission.title') %> +

+

+ <%- __('about.mission.description') %> +

+
+ +
+ +
+
+ +
+

+ <%- __('about.values.innovation.title') %> +

+

+ <%- __('about.values.innovation.description') %> +

+
+ + +
+
+ +
+

+ <%- __('about.values.quality.title') %> +

+

+ <%- __('about.values.quality.description') %> +

+
+ + +
+
+ +
+

+ <%- __('about.values.partnership.title') %> +

+

+ <%- __('about.values.partnership.description') %> +

+
+
+
+
+ + +
+
+

+ <%- __('about.cta.title') %> +

+

+ <%- __('about.cta.subtitle') %> +

+ + <%- __('about.cta.button') %> + + + + +
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/.history/views/about-new_20251019174544.ejs b/.history/views/about-new_20251019174544.ejs new file mode 100644 index 0000000..6954758 --- /dev/null +++ b/.history/views/about-new_20251019174544.ejs @@ -0,0 +1,160 @@ + + + + + + <%- __('about.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('about.hero.title') %> +

+

+ <%- __('about.hero.subtitle') %> +

+
+
+ + +
+
+
+
+

+ <%- __('about.company.title') %> +

+

+ <%- __('about.company.description1') %> +

+

+ <%- __('about.company.description2') %> +

+ + +
+
+
50+
+
<%- __('about.stats.projects') %>
+
+
+
3+
+
<%- __('about.stats.experience') %>
+
+
+
30+
+
<%- __('about.stats.clients') %>
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+

+ <%- __('about.mission.title') %> +

+

+ <%- __('about.mission.description') %> +

+
+ +
+ +
+
+ +
+

+ <%- __('about.values.innovation.title') %> +

+

+ <%- __('about.values.innovation.description') %> +

+
+ + +
+
+ +
+

+ <%- __('about.values.quality.title') %> +

+

+ <%- __('about.values.quality.description') %> +

+
+ + +
+
+ +
+

+ <%- __('about.values.partnership.title') %> +

+

+ <%- __('about.values.partnership.description') %> +

+
+
+
+
+ + +
+
+

+ <%- __('about.cta.title') %> +

+

+ <%- __('about.cta.subtitle') %> +

+ + <%- __('about.cta.button') %> + + + + +
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/.history/views/about_20251019170417.ejs b/.history/views/about_20251019170417.ejs new file mode 100644 index 0000000..9dfbf5b --- /dev/null +++ b/.history/views/about_20251019170417.ejs @@ -0,0 +1,336 @@ + + + + + + 회사 소개 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ About SmartSolTech +

+

+ 혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업 +

+
+
+ + +
+
+
+
+

+ 혁신창의로 만드는 미래 +

+

+ SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 + 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다. +

+

+ 우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 + 제안하여 함께 성장하는 파트너가 되고자 합니다. +

+
+
+
100+
+
완료 프로젝트
+
+
+
50+
+
만족한 고객
+
+
+
4년
+
업계 경험
+
+
+
+
+
+
+

우리의 미션

+

+ "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것" +

+

우리의 비전

+

+ "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 + 전 세계 고객들의 디지털 혁신을 주도하는 것" +

+
+
+
+
+
+
+
+
+ + +
+
+
+

+ Core Values +

+

+ SmartSolTech가 추구하는 핵심 가치들 +

+
+ +
+
+
+ +
+

혁신 (Innovation)

+

+ 끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다. +

+
+ +
+
+ +
+

협력 (Collaboration)

+

+ 고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다. +

+
+ +
+
+ +
+

품질 (Quality)

+

+ 높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다. +

+
+ +
+
+ +
+

성장 (Growth)

+

+ 고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다. +

+
+
+
+
+ + +
+
+
+

+ Our Team +

+

+ 전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다 +

+
+ +
+
+
+ KH +
+

김현우

+

CEO & Founder

+

+ 10년 이상의 웹 개발 경험을 바탕으로 SmartSolTech를 설립하여 + 혁신적인 디지털 솔루션을 제공하고 있습니다. +

+ +
+ +
+
+ LJ +
+

이정민

+

CTO & Lead Developer

+

+ 풀스택 개발자로서 최신 기술 트렌드를 적용하여 + 확장 가능하고 안정적인 시스템을 구축합니다. +

+ +
+ +
+
+ PS +
+

박서연

+

UI/UX Designer

+

+ 사용자 중심의 디자인 철학을 바탕으로 직관적이고 + 아름다운 인터페이스를 설계합니다. +

+ +
+
+
+
+ + +
+
+
+

+ Technology Stack +

+

+ 최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다 +

+
+ +
+
+

Frontend

+
+
+ +
React
+
+
+ +
Vue.js
+
+
+ +
Angular
+
+
+
+ +
+

Backend

+
+
+ +
Node.js
+
+
+ +
Python
+
+
+ +
Java
+
+
+
+ +
+

Mobile

+
+
+ +
React Native
+
+
+ +
Flutter
+
+
+ +
Swift
+
+
+
+
+
+
+ + +
+
+
+

+ 함께 성공하는 파트너가 되어보세요 +

+

+ SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요 +

+ +
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/about_20251019170533.ejs b/.history/views/about_20251019170533.ejs new file mode 100644 index 0000000..9dfbf5b --- /dev/null +++ b/.history/views/about_20251019170533.ejs @@ -0,0 +1,336 @@ + + + + + + 회사 소개 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ About SmartSolTech +

+

+ 혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업 +

+
+
+ + +
+
+
+
+

+ 혁신창의로 만드는 미래 +

+

+ SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 + 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다. +

+

+ 우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 + 제안하여 함께 성장하는 파트너가 되고자 합니다. +

+
+
+
100+
+
완료 프로젝트
+
+
+
50+
+
만족한 고객
+
+
+
4년
+
업계 경험
+
+
+
+
+
+
+

우리의 미션

+

+ "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것" +

+

우리의 비전

+

+ "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 + 전 세계 고객들의 디지털 혁신을 주도하는 것" +

+
+
+
+
+
+
+
+
+ + +
+
+
+

+ Core Values +

+

+ SmartSolTech가 추구하는 핵심 가치들 +

+
+ +
+
+
+ +
+

혁신 (Innovation)

+

+ 끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다. +

+
+ +
+
+ +
+

협력 (Collaboration)

+

+ 고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다. +

+
+ +
+
+ +
+

품질 (Quality)

+

+ 높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다. +

+
+ +
+
+ +
+

성장 (Growth)

+

+ 고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다. +

+
+
+
+
+ + +
+
+
+

+ Our Team +

+

+ 전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다 +

+
+ +
+
+
+ KH +
+

김현우

+

CEO & Founder

+

+ 10년 이상의 웹 개발 경험을 바탕으로 SmartSolTech를 설립하여 + 혁신적인 디지털 솔루션을 제공하고 있습니다. +

+ +
+ +
+
+ LJ +
+

이정민

+

CTO & Lead Developer

+

+ 풀스택 개발자로서 최신 기술 트렌드를 적용하여 + 확장 가능하고 안정적인 시스템을 구축합니다. +

+ +
+ +
+
+ PS +
+

박서연

+

UI/UX Designer

+

+ 사용자 중심의 디자인 철학을 바탕으로 직관적이고 + 아름다운 인터페이스를 설계합니다. +

+ +
+
+
+
+ + +
+
+
+

+ Technology Stack +

+

+ 최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다 +

+
+ +
+
+

Frontend

+
+
+ +
React
+
+
+ +
Vue.js
+
+
+ +
Angular
+
+
+
+ +
+

Backend

+
+
+ +
Node.js
+
+
+ +
Python
+
+
+ +
Java
+
+
+
+ +
+

Mobile

+
+
+ +
React Native
+
+
+ +
Flutter
+
+
+ +
Swift
+
+
+
+
+
+
+ + +
+
+
+

+ 함께 성공하는 파트너가 되어보세요 +

+

+ SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요 +

+ +
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/about_20251019171021.ejs b/.history/views/about_20251019171021.ejs new file mode 100644 index 0000000..0974833 --- /dev/null +++ b/.history/views/about_20251019171021.ejs @@ -0,0 +1,337 @@ + + + + + + 회사 소개 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ About SmartSolTech +

+

+ 혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업 +

+
+
+ + +
+
+
+
+

+ 혁신창의로 만드는 미래 +

+

+ SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 + 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다. +

+

+ 우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 + 제안하여 함께 성장하는 파트너가 되고자 합니다. +

+
+
+
100+
+
완료 프로젝트
+
+
+
50+
+
만족한 고객
+
+
+
4년
+
업계 경험
+
+
+
+
+
+
+

우리의 미션

+

+ "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것" +

+

우리의 비전

+

+ "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 + 전 세계 고객들의 디지털 혁신을 주도하는 것" +

+
+
+
+
+
+
+
+
+ + +
+
+
+

+ Core Values +

+

+ SmartSolTech가 추구하는 핵심 가치들 +

+
+ +
+
+
+ +
+

혁신 (Innovation)

+

+ 끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다. +

+
+ +
+
+ +
+

협력 (Collaboration)

+

+ 고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다. +

+
+ +
+
+ +
+

품질 (Quality)

+

+ 높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다. +

+
+ +
+
+ +
+

성장 (Growth)

+

+ 고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다. +

+
+
+
+
+ + +
+
+
+

+ Our Team +

+

+ 전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다 +

+
+ +
+
+
+ KH +
+

김현우

+

CEO & Founder

+

+ 10년 이상의 웹 개발 경험을 바탕으로 SmartSolTech를 설립하여 + 혁신적인 디지털 솔루션을 제공하고 있습니다. +

+ +
+ +
+
+ LJ +
+

이정민

+

CTO & Lead Developer

+

+ 풀스택 개발자로서 최신 기술 트렌드를 적용하여 + 확장 가능하고 안정적인 시스템을 구축합니다. +

+ +
+ +
+
+ PS +
+

박서연

+

UI/UX Designer

+

+ 사용자 중심의 디자인 철학을 바탕으로 직관적이고 + 아름다운 인터페이스를 설계합니다. +

+ +
+
+
+
+ + +
+
+
+

+ Technology Stack +

+

+ 최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다 +

+
+ +
+
+

Frontend

+
+
+ +
React
+
+
+ +
Vue.js
+
+
+ +
Angular
+
+
+
+ +
+

Backend

+
+
+ +
Node.js
+
+
+ +
Python
+
+
+ +
Java
+
+
+
+ +
+

Mobile

+
+
+ +
React Native
+
+
+ +
Flutter
+
+
+ +
Swift
+
+
+
+
+
+
+ + +
+
+
+

+ 함께 성공하는 파트너가 되어보세요 +

+

+ SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요 +

+ +
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/about_20251019171203.ejs b/.history/views/about_20251019171203.ejs new file mode 100644 index 0000000..0974833 --- /dev/null +++ b/.history/views/about_20251019171203.ejs @@ -0,0 +1,337 @@ + + + + + + 회사 소개 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ About SmartSolTech +

+

+ 혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업 +

+
+
+ + +
+
+
+
+

+ 혁신창의로 만드는 미래 +

+

+ SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 + 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다. +

+

+ 우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 + 제안하여 함께 성장하는 파트너가 되고자 합니다. +

+
+
+
100+
+
완료 프로젝트
+
+
+
50+
+
만족한 고객
+
+
+
4년
+
업계 경험
+
+
+
+
+
+
+

우리의 미션

+

+ "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것" +

+

우리의 비전

+

+ "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 + 전 세계 고객들의 디지털 혁신을 주도하는 것" +

+
+
+
+
+
+
+
+
+ + +
+
+
+

+ Core Values +

+

+ SmartSolTech가 추구하는 핵심 가치들 +

+
+ +
+
+
+ +
+

혁신 (Innovation)

+

+ 끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다. +

+
+ +
+
+ +
+

협력 (Collaboration)

+

+ 고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다. +

+
+ +
+
+ +
+

품질 (Quality)

+

+ 높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다. +

+
+ +
+
+ +
+

성장 (Growth)

+

+ 고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다. +

+
+
+
+
+ + +
+
+
+

+ Our Team +

+

+ 전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다 +

+
+ +
+
+
+ KH +
+

김현우

+

CEO & Founder

+

+ 10년 이상의 웹 개발 경험을 바탕으로 SmartSolTech를 설립하여 + 혁신적인 디지털 솔루션을 제공하고 있습니다. +

+ +
+ +
+
+ LJ +
+

이정민

+

CTO & Lead Developer

+

+ 풀스택 개발자로서 최신 기술 트렌드를 적용하여 + 확장 가능하고 안정적인 시스템을 구축합니다. +

+ +
+ +
+
+ PS +
+

박서연

+

UI/UX Designer

+

+ 사용자 중심의 디자인 철학을 바탕으로 직관적이고 + 아름다운 인터페이스를 설계합니다. +

+ +
+
+
+
+ + +
+
+
+

+ Technology Stack +

+

+ 최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다 +

+
+ +
+
+

Frontend

+
+
+ +
React
+
+
+ +
Vue.js
+
+
+ +
Angular
+
+
+
+ +
+

Backend

+
+
+ +
Node.js
+
+
+ +
Python
+
+
+ +
Java
+
+
+
+ +
+

Mobile

+
+
+ +
React Native
+
+
+ +
Flutter
+
+
+ +
Swift
+
+
+
+
+
+
+ + +
+
+
+

+ 함께 성공하는 파트너가 되어보세요 +

+

+ SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요 +

+ +
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/calculator-new_20251019181325.ejs b/.history/views/calculator-new_20251019181325.ejs new file mode 100644 index 0000000..e7cb97b --- /dev/null +++ b/.history/views/calculator-new_20251019181325.ejs @@ -0,0 +1,489 @@ + + + + + + <%- __('calculator.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('calculator.title') %> +

+

+ <%- __('calculator.subtitle') %> +

+
+
+ + +
+
+
+
+
+ +
+
+

+ <%- __('calculator.step1.title') %> +

+

+ <%- __('calculator.step1.subtitle') %> +

+
+ +
+ +
+
+
+ +
+
+

+ <%- __('services.web.title') %> +

+
<%- __('services.web.price') %>
+
+
+

+ <%- __('services.web.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.mobile.title') %> +

+
<%- __('services.mobile.price') %>
+
+
+

+ <%- __('services.mobile.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.design.title') %> +

+
<%- __('services.design.price') %>
+
+
+

+ <%- __('services.design.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.marketing.title') %> +

+
<%- __('services.marketing.price') %>
+
+
+

+ <%- __('services.marketing.description') %> +

+
+
+ +
+ +
+
+ + + + + + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/.history/views/calculator-new_20251019181629.ejs b/.history/views/calculator-new_20251019181629.ejs new file mode 100644 index 0000000..e7cb97b --- /dev/null +++ b/.history/views/calculator-new_20251019181629.ejs @@ -0,0 +1,489 @@ + + + + + + <%- __('calculator.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('calculator.title') %> +

+

+ <%- __('calculator.subtitle') %> +

+
+
+ + +
+
+
+
+
+ +
+
+

+ <%- __('calculator.step1.title') %> +

+

+ <%- __('calculator.step1.subtitle') %> +

+
+ +
+ +
+
+
+ +
+
+

+ <%- __('services.web.title') %> +

+
<%- __('services.web.price') %>
+
+
+

+ <%- __('services.web.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.mobile.title') %> +

+
<%- __('services.mobile.price') %>
+
+
+

+ <%- __('services.mobile.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.design.title') %> +

+
<%- __('services.design.price') %>
+
+
+

+ <%- __('services.design.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.marketing.title') %> +

+
<%- __('services.marketing.price') %>
+
+
+

+ <%- __('services.marketing.description') %> +

+
+
+ +
+ +
+
+ + + + + + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/.history/views/calculator_20251019161408.ejs b/.history/views/calculator_20251019161408.ejs new file mode 100644 index 0000000..69ee69f --- /dev/null +++ b/.history/views/calculator_20251019161408.ejs @@ -0,0 +1,677 @@ + +
+
+

+ 프로젝트 견적 계산기 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 정확한 견적을 계산해드립니다 +

+
+
+ + +
+
+
+
+
+ +
+
+

1단계: 서비스 선택

+

필요한 서비스를 선택해주세요 (복수 선택 가능)

+
+ +
+ +
+ +
+ +
+
+ + +
+
+

2단계: 프로젝트 세부사항

+

프로젝트의 복잡도와 일정을 선택해주세요

+
+ +
+ +
+ +
+
+
+
🌟
+
간단함
+
기본 기능, 표준 디자인
+
-30% 할인
+
+
+
+
+
+
보통
+
중간 복잡도, 커스텀 기능
+
표준 가격
+
+
+
+
+
🚀
+
복잡함
+
고급 기능, 통합 시스템
+
+50% 추가
+
+
+
+
+
💎
+
엔터프라이즈
+
대규모, 높은 복잡도
+
+100% 추가
+
+
+
+
+ + +
+ +
+
+
+
+
긴급 (2주 이내)
+
+80% 추가
+
+
+
+
+
🔥
+
빠름 (2-4주)
+
+40% 추가
+
+
+
+
+
+
표준 (1-3개월)
+
표준 가격
+
+
+
+
+
🌱
+
여유 (3개월+)
+
-20% 할인
+
+
+
+
+
+ +
+ + +
+
+ + +
+
+

3단계: 추가 기능

+

필요한 추가 기능을 선택해주세요 (선택사항)

+
+ +
+
+
+
+ SEO 최적화 + ₩300,000 +
+

검색엔진 최적화 및 메타 태그 설정

+
+
+ +
+
+
+ 분석 도구 설정 + ₩150,000 +
+

Google Analytics, 태그 매니저 설정

+
+
+ +
+
+
+ 소셜 미디어 연동 + ₩200,000 +
+

Facebook, Instagram, 카카오톡 연동

+
+
+ +
+
+
+ 결제 시스템 + ₩500,000 +
+

신용카드, 간편결제 시스템 연동

+
+
+ +
+
+
+ 다국어 지원 + ₩400,000 +
+

한국어, 영어, 중국어 등 다국어

+
+
+ +
+
+
+ 관리자 패널 + ₩600,000 +
+

콘텐츠 관리, 사용자 관리 시스템

+
+
+
+ + +
+ + +
+ +
+ + +
+
+ + +
+
+

견적 결과

+

계산된 프로젝트 견적을 확인하세요

+
+ +
+ +
+ + +
+

견적 요청하기

+

정확한 견적을 받으시려면 연락처를 남겨주세요

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/.history/views/calculator_20251019162545.ejs b/.history/views/calculator_20251019162545.ejs new file mode 100644 index 0000000..69ee69f --- /dev/null +++ b/.history/views/calculator_20251019162545.ejs @@ -0,0 +1,677 @@ + +
+
+

+ 프로젝트 견적 계산기 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 정확한 견적을 계산해드립니다 +

+
+
+ + +
+
+
+
+
+ +
+
+

1단계: 서비스 선택

+

필요한 서비스를 선택해주세요 (복수 선택 가능)

+
+ +
+ +
+ +
+ +
+
+ + +
+
+

2단계: 프로젝트 세부사항

+

프로젝트의 복잡도와 일정을 선택해주세요

+
+ +
+ +
+ +
+
+
+
🌟
+
간단함
+
기본 기능, 표준 디자인
+
-30% 할인
+
+
+
+
+
+
보통
+
중간 복잡도, 커스텀 기능
+
표준 가격
+
+
+
+
+
🚀
+
복잡함
+
고급 기능, 통합 시스템
+
+50% 추가
+
+
+
+
+
💎
+
엔터프라이즈
+
대규모, 높은 복잡도
+
+100% 추가
+
+
+
+
+ + +
+ +
+
+
+
+
긴급 (2주 이내)
+
+80% 추가
+
+
+
+
+
🔥
+
빠름 (2-4주)
+
+40% 추가
+
+
+
+
+
+
표준 (1-3개월)
+
표준 가격
+
+
+
+
+
🌱
+
여유 (3개월+)
+
-20% 할인
+
+
+
+
+
+ +
+ + +
+
+ + +
+
+

3단계: 추가 기능

+

필요한 추가 기능을 선택해주세요 (선택사항)

+
+ +
+
+
+
+ SEO 최적화 + ₩300,000 +
+

검색엔진 최적화 및 메타 태그 설정

+
+
+ +
+
+
+ 분석 도구 설정 + ₩150,000 +
+

Google Analytics, 태그 매니저 설정

+
+
+ +
+
+
+ 소셜 미디어 연동 + ₩200,000 +
+

Facebook, Instagram, 카카오톡 연동

+
+
+ +
+
+
+ 결제 시스템 + ₩500,000 +
+

신용카드, 간편결제 시스템 연동

+
+
+ +
+
+
+ 다국어 지원 + ₩400,000 +
+

한국어, 영어, 중국어 등 다국어

+
+
+ +
+
+
+ 관리자 패널 + ₩600,000 +
+

콘텐츠 관리, 사용자 관리 시스템

+
+
+
+ + +
+ + +
+ +
+ + +
+
+ + +
+
+

견적 결과

+

계산된 프로젝트 견적을 확인하세요

+
+ +
+ +
+ + +
+

견적 요청하기

+

정확한 견적을 받으시려면 연락처를 남겨주세요

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/.history/views/calculator_20251019182247.ejs b/.history/views/calculator_20251019182247.ejs new file mode 100644 index 0000000..c93022f --- /dev/null +++ b/.history/views/calculator_20251019182247.ejs @@ -0,0 +1,494 @@ + + + + + + <%- __('calculator.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('calculator.title') %> +

+

+ <%- __('calculator.subtitle') %> +

+
+
+ + +
+
+
+
+ +
+
+
+ +
+ +
+
+

+ <%- __('calculator.step1.title') %> +

+

+ <%- __('calculator.step1.subtitle') %> +

+
+ +
+ +
+
+
+ +
+
+

+ <%- __('services.web.title') %> +

+
<%- __('services.web.price') %>
+
+
+

+ <%- __('services.web.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.mobile.title') %> +

+
<%- __('services.mobile.price') %>
+
+
+

+ <%- __('services.mobile.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.design.title') %> +

+
<%- __('services.design.price') %>
+
+
+

+ <%- __('services.design.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.marketing.title') %> +

+
<%- __('services.marketing.price') %>
+
+
+

+ <%- __('services.marketing.description') %> +

+
+
+ +
+ +
+
+ + + + + + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/.history/views/calculator_20251019182320.ejs b/.history/views/calculator_20251019182320.ejs new file mode 100644 index 0000000..f876cf5 --- /dev/null +++ b/.history/views/calculator_20251019182320.ejs @@ -0,0 +1,302 @@ + + + + + + <%- __('calculator.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('calculator.title') %> +

+

+ <%- __('calculator.subtitle') %> +

+
+
+ + +
+
+
+
+ +
+
+
+ +
+ +
+
+

+ <%- __('calculator.step1.title') %> +

+

+ <%- __('calculator.step1.subtitle') %> +

+
+ +
+ +
+
+
+ +
+
+

+ <%- __('services.web.title') %> +

+
<%- __('services.web.price') %>
+
+
+

+ <%- __('services.web.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.mobile.title') %> +

+
<%- __('services.mobile.price') %>
+
+
+

+ <%- __('services.mobile.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.design.title') %> +

+
<%- __('services.design.price') %>
+
+
+

+ <%- __('services.design.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.marketing.title') %> +

+
<%- __('services.marketing.price') %>
+
+
+

+ <%- __('services.marketing.description') %> +

+
+
+ +
+ +
+
+ + + + + + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + \ No newline at end of file diff --git a/.history/views/calculator_20251019182628.ejs b/.history/views/calculator_20251019182628.ejs new file mode 100644 index 0000000..f876cf5 --- /dev/null +++ b/.history/views/calculator_20251019182628.ejs @@ -0,0 +1,302 @@ + + + + + + <%- __('calculator.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('calculator.title') %> +

+

+ <%- __('calculator.subtitle') %> +

+
+
+ + +
+
+
+
+ +
+
+
+ +
+ +
+
+

+ <%- __('calculator.step1.title') %> +

+

+ <%- __('calculator.step1.subtitle') %> +

+
+ +
+ +
+
+
+ +
+
+

+ <%- __('services.web.title') %> +

+
<%- __('services.web.price') %>
+
+
+

+ <%- __('services.web.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.mobile.title') %> +

+
<%- __('services.mobile.price') %>
+
+
+

+ <%- __('services.mobile.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.design.title') %> +

+
<%- __('services.design.price') %>
+
+
+

+ <%- __('services.design.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.marketing.title') %> +

+
<%- __('services.marketing.price') %>
+
+
+

+ <%- __('services.marketing.description') %> +

+
+
+ +
+ +
+
+ + + + + + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + \ No newline at end of file diff --git a/.history/views/contact-new_20251019174651.ejs b/.history/views/contact-new_20251019174651.ejs new file mode 100644 index 0000000..b7efc4e --- /dev/null +++ b/.history/views/contact-new_20251019174651.ejs @@ -0,0 +1,211 @@ + + + + + + <%- __('contact.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('contact.hero.title') %> +

+

+ <%- __('contact.hero.subtitle') %> +

+
+
+ + +
+
+
+ +
+

+ <%- __('contact.form.title') %> +

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+

+ <%- __('contact.info.title') %> +

+ +
+ +
+
+ +
+
+

+ <%- __('contact.phone.title') %> +

+

+ <%- __('contact.phone.number') %> +

+

+ <%- __('contact.phone.hours') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.email.title') %> +

+

+ <%- __('contact.email.address') %> +

+

+ <%- __('contact.email.response') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.telegram.title') %> +

+

+ @smartsoltech +

+

+ <%- __('contact.telegram.subtitle') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.address.title') %> +

+

+ <%- __('contact.address.line1') %>
+ <%- __('contact.address.line2') %> +

+
+
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/.history/views/contact-new_20251019174704.ejs b/.history/views/contact-new_20251019174704.ejs new file mode 100644 index 0000000..b7efc4e --- /dev/null +++ b/.history/views/contact-new_20251019174704.ejs @@ -0,0 +1,211 @@ + + + + + + <%- __('contact.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('contact.hero.title') %> +

+

+ <%- __('contact.hero.subtitle') %> +

+
+
+ + +
+
+
+ +
+

+ <%- __('contact.form.title') %> +

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+

+ <%- __('contact.info.title') %> +

+ +
+ +
+
+ +
+
+

+ <%- __('contact.phone.title') %> +

+

+ <%- __('contact.phone.number') %> +

+

+ <%- __('contact.phone.hours') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.email.title') %> +

+

+ <%- __('contact.email.address') %> +

+

+ <%- __('contact.email.response') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.telegram.title') %> +

+

+ @smartsoltech +

+

+ <%- __('contact.telegram.subtitle') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.address.title') %> +

+

+ <%- __('contact.address.line1') %>
+ <%- __('contact.address.line2') %> +

+
+
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/.history/views/contact_20251019162940.ejs b/.history/views/contact_20251019162940.ejs new file mode 100644 index 0000000..ca869f8 --- /dev/null +++ b/.history/views/contact_20251019162940.ejs @@ -0,0 +1,491 @@ + + + + + + 연락처 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 연락처 +

+

+ 프로젝트 문의부터 기술 상담까지, 언제든 연락주세요 +

+
+
+ + +
+
+
+ + +
+
+

+ 프로젝트 문의하기 +

+

+ 아래 양식을 작성해 주시면 24시간 내에 답변드리겠습니다. +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + +
+ + +
+

+ 연락처 정보 +

+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+

전화번호

+ + +82-10-1234-5678 + +
+
+ + +
+
+ +
+
+

주소

+

Seoul, South Korea

+
+
+ + +
+
+ +
+
+

운영시간

+

평일 09:00 - 18:00

+

주말 및 공휴일 휴무

+
+
+
+
+ + +
+

+ 소셜 미디어 +

+ + +
+ + + +
+
+
+
+ + +
+
+
+

+ 자주 묻는 질문 +

+

+ 프로젝트와 관련된 궁금한 점들을 확인해보세요 +

+
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + + \ No newline at end of file diff --git a/.history/views/contact_20251019163807.ejs b/.history/views/contact_20251019163807.ejs new file mode 100644 index 0000000..ca869f8 --- /dev/null +++ b/.history/views/contact_20251019163807.ejs @@ -0,0 +1,491 @@ + + + + + + 연락처 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 연락처 +

+

+ 프로젝트 문의부터 기술 상담까지, 언제든 연락주세요 +

+
+
+ + +
+
+
+ + +
+
+

+ 프로젝트 문의하기 +

+

+ 아래 양식을 작성해 주시면 24시간 내에 답변드리겠습니다. +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + +
+ + +
+

+ 연락처 정보 +

+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+

전화번호

+ + +82-10-1234-5678 + +
+
+ + +
+
+ +
+
+

주소

+

Seoul, South Korea

+
+
+ + +
+
+ +
+
+

운영시간

+

평일 09:00 - 18:00

+

주말 및 공휴일 휴무

+
+
+
+
+ + +
+

+ 소셜 미디어 +

+ + +
+ + + +
+
+
+
+ + +
+
+
+

+ 자주 묻는 질문 +

+

+ 프로젝트와 관련된 궁금한 점들을 확인해보세요 +

+
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + + \ No newline at end of file diff --git a/.history/views/contact_20251019164437.ejs b/.history/views/contact_20251019164437.ejs new file mode 100644 index 0000000..e3c18f4 --- /dev/null +++ b/.history/views/contact_20251019164437.ejs @@ -0,0 +1,490 @@ + + + + + + 연락처 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 연락처 +

+

+ 프로젝트 문의부터 기술 상담까지, 언제든 연락주세요 +

+
+
+ + +
+
+
+ + +
+
+

+ 프로젝트 문의하기 +

+

+ 아래 양식을 작성해 주시면 24시간 내에 답변드리겠습니다. +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + +
+ + +
+

+ 연락처 정보 +

+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+

전화번호

+ + +82-10-1234-5678 + +
+
+ + +
+
+ +
+
+

주소

+

Seoul, South Korea

+
+
+ + +
+
+ +
+
+

운영시간

+

평일 09:00 - 18:00

+

주말 및 공휴일 휴무

+
+
+
+
+ + +
+

+ 소셜 미디어 +

+ + +
+ + + +
+
+
+
+ + +
+
+
+

+ 자주 묻는 질문 +

+

+ 프로젝트와 관련된 궁금한 점들을 확인해보세요 +

+
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + + \ No newline at end of file diff --git a/.history/views/contact_20251019165556.ejs b/.history/views/contact_20251019165556.ejs new file mode 100644 index 0000000..e3c18f4 --- /dev/null +++ b/.history/views/contact_20251019165556.ejs @@ -0,0 +1,490 @@ + + + + + + 연락처 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 연락처 +

+

+ 프로젝트 문의부터 기술 상담까지, 언제든 연락주세요 +

+
+
+ + +
+
+
+ + +
+
+

+ 프로젝트 문의하기 +

+

+ 아래 양식을 작성해 주시면 24시간 내에 답변드리겠습니다. +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + +
+ + +
+

+ 연락처 정보 +

+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+

전화번호

+ + +82-10-1234-5678 + +
+
+ + +
+
+ +
+
+

주소

+

Seoul, South Korea

+
+
+ + +
+
+ +
+
+

운영시간

+

평일 09:00 - 18:00

+

주말 및 공휴일 휴무

+
+
+
+
+ + +
+

+ 소셜 미디어 +

+ + +
+ + + +
+
+
+
+ + +
+
+
+

+ 자주 묻는 질문 +

+

+ 프로젝트와 관련된 궁금한 점들을 확인해보세요 +

+
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + + \ No newline at end of file diff --git a/.history/views/contact_20251019171039.ejs b/.history/views/contact_20251019171039.ejs new file mode 100644 index 0000000..2a11d23 --- /dev/null +++ b/.history/views/contact_20251019171039.ejs @@ -0,0 +1,491 @@ + + + + + + 연락처 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 연락처 +

+

+ 프로젝트 문의부터 기술 상담까지, 언제든 연락주세요 +

+
+
+ + +
+
+
+ + +
+
+

+ 프로젝트 문의하기 +

+

+ 아래 양식을 작성해 주시면 24시간 내에 답변드리겠습니다. +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + +
+ + +
+

+ 연락처 정보 +

+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+

전화번호

+ + +82-10-1234-5678 + +
+
+ + +
+
+ +
+
+

주소

+

Seoul, South Korea

+
+
+ + +
+
+ +
+
+

운영시간

+

평일 09:00 - 18:00

+

주말 및 공휴일 휴무

+
+
+
+
+ + +
+

+ 소셜 미디어 +

+ + +
+ + + +
+
+
+
+ + +
+
+
+

+ 자주 묻는 질문 +

+

+ 프로젝트와 관련된 궁금한 점들을 확인해보세요 +

+
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + + \ No newline at end of file diff --git a/.history/views/contact_20251019171203.ejs b/.history/views/contact_20251019171203.ejs new file mode 100644 index 0000000..2a11d23 --- /dev/null +++ b/.history/views/contact_20251019171203.ejs @@ -0,0 +1,491 @@ + + + + + + 연락처 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 연락처 +

+

+ 프로젝트 문의부터 기술 상담까지, 언제든 연락주세요 +

+
+
+ + +
+
+
+ + +
+
+

+ 프로젝트 문의하기 +

+

+ 아래 양식을 작성해 주시면 24시간 내에 답변드리겠습니다. +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + +
+ + +
+

+ 연락처 정보 +

+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+

전화번호

+ + +82-10-1234-5678 + +
+
+ + +
+
+ +
+
+

주소

+

Seoul, South Korea

+
+
+ + +
+
+ +
+
+

운영시간

+

평일 09:00 - 18:00

+

주말 및 공휴일 휴무

+
+
+
+
+ + +
+

+ 소셜 미디어 +

+ + +
+ + + +
+
+
+
+ + +
+
+
+

+ 자주 묻는 질문 +

+

+ 프로젝트와 관련된 궁금한 점들을 확인해보세요 +

+
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + + \ No newline at end of file diff --git a/.history/views/error_20251019163708.ejs b/.history/views/error_20251019163708.ejs new file mode 100644 index 0000000..39b8900 --- /dev/null +++ b/.history/views/error_20251019163708.ejs @@ -0,0 +1,86 @@ + + + + + + 오류 - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+ +
+ + +

+ <%= title || '오류가 발생했습니다' %> +

+ + +

+ <%= message || '요청을 처리하는 중 문제가 발생했습니다.' %> +

+ + +
+ + + 홈으로 돌아가기 + + +
+ + +
+

도움이 필요하신가요?

+

+ 문제가 지속되면 언제든지 저희에게 연락해 주세요. +

+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/.history/views/error_20251019163807.ejs b/.history/views/error_20251019163807.ejs new file mode 100644 index 0000000..39b8900 --- /dev/null +++ b/.history/views/error_20251019163807.ejs @@ -0,0 +1,86 @@ + + + + + + 오류 - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+ +
+ + +

+ <%= title || '오류가 발생했습니다' %> +

+ + +

+ <%= message || '요청을 처리하는 중 문제가 발생했습니다.' %> +

+ + +
+ + + 홈으로 돌아가기 + + +
+ + +
+

도움이 필요하신가요?

+

+ 문제가 지속되면 언제든지 저희에게 연락해 주세요. +

+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/.history/views/index-new_20251019170311.ejs b/.history/views/index-new_20251019170311.ejs new file mode 100644 index 0000000..8559f2e --- /dev/null +++ b/.history/views/index-new_20251019170311.ejs @@ -0,0 +1,389 @@ + + + + + + <%= title || 'SmartSolTech - 혁신적인 기술 솔루션' %> + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ Smart Technology + Solutions +

+

+ 혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다 +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ Our Services +

+

+ 최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션 +

+
+ +
+ +
+
+ +
+

웹 개발

+

현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발

+
₩500,000~
+
+ +
+
+ +
+

모바일 앱

+

iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱

+
₩800,000~
+
+ +
+
+ +
+

UI/UX 디자인

+

사용자 중심의 직관적이고 아름다운 인터페이스 디자인

+
₩300,000~
+
+ +
+
+ +
+

디지털 마케팅

+

SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅

+
₩200,000~
+
+
+ + +
+
+ + +
+
+
+

+ Recent Projects +

+

+ 고객의 성공을 위해 완성한 프로젝트들을 확인해보세요 +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + 자세히 보기 + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ E-commerce + + 1,234 + +
+

온라인 쇼핑몰 플랫폼

+

현대적인 UI/UX와 완벽한 결제 시스템을 갖춘 전자상거래 솔루션

+ + 자세히 보기 + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ 프로젝트 견적을 확인해보세요 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다 +

+ + + 견적 계산기 사용하기 + +
+
+
+ + +
+
+
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다. +

+
+
+
+ +
+
+
전화 상담
+
+82-10-0000-0000
+
+
+
+
+ +
+
+
이메일 문의
+
info@smartsoltech.kr
+
+
+
+
+ +
+
+
텔레그램 채팅
+
즉시 답변 가능
+
+
+
+
+
+
+

무료 상담 신청

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/index-new_20251019170533.ejs b/.history/views/index-new_20251019170533.ejs new file mode 100644 index 0000000..8559f2e --- /dev/null +++ b/.history/views/index-new_20251019170533.ejs @@ -0,0 +1,389 @@ + + + + + + <%= title || 'SmartSolTech - 혁신적인 기술 솔루션' %> + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ Smart Technology + Solutions +

+

+ 혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다 +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ Our Services +

+

+ 최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션 +

+
+ +
+ +
+
+ +
+

웹 개발

+

현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발

+
₩500,000~
+
+ +
+
+ +
+

모바일 앱

+

iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱

+
₩800,000~
+
+ +
+
+ +
+

UI/UX 디자인

+

사용자 중심의 직관적이고 아름다운 인터페이스 디자인

+
₩300,000~
+
+ +
+
+ +
+

디지털 마케팅

+

SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅

+
₩200,000~
+
+
+ + +
+
+ + +
+
+
+

+ Recent Projects +

+

+ 고객의 성공을 위해 완성한 프로젝트들을 확인해보세요 +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + 자세히 보기 + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ E-commerce + + 1,234 + +
+

온라인 쇼핑몰 플랫폼

+

현대적인 UI/UX와 완벽한 결제 시스템을 갖춘 전자상거래 솔루션

+ + 자세히 보기 + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ 프로젝트 견적을 확인해보세요 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다 +

+ + + 견적 계산기 사용하기 + +
+
+
+ + +
+
+
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다. +

+
+
+
+ +
+
+
전화 상담
+
+82-10-0000-0000
+
+
+
+
+ +
+
+
이메일 문의
+
info@smartsoltech.kr
+
+
+
+
+ +
+
+
텔레그램 채팅
+
즉시 답변 가능
+
+
+
+
+
+
+

무료 상담 신청

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/index-new_20251019174028.ejs b/.history/views/index-new_20251019174028.ejs new file mode 100644 index 0000000..2a60a82 --- /dev/null +++ b/.history/views/index-new_20251019174028.ejs @@ -0,0 +1,400 @@ + + + + + + <%- __(title || 'meta.title') %> + + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ <%- __('hero.title.smart') %> + <%- __('hero.title.solutions') %> +

+

+ <%- __('hero.subtitle') %> +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ <%- __('services.title.our') %> <%- __('services.title.services') %> +

+

+ <%- __('services.subtitle') %> +

+
+ +
+ +
+
+ +
+

<%- __('services.web.title') %>

+

<%- __('services.web.description') %>

+
<%- __('services.web.price') %>
+
+ + +
+
+ +
+

<%- __('services.mobile.title') %>

+

<%- __('services.mobile.description') %>

+
<%- __('services.mobile.price') %>
+
+ + +
+
+ +
+

<%- __('services.design.title') %>

+

<%- __('services.design.description') %>

+
<%- __('services.design.price') %>
+
+ + +
+
+ +
+

<%- __('services.marketing.title') %>

+

<%- __('services.marketing.description') %>

+
<%- __('services.marketing.price') %>
+
+
+ + +
+
+ + +
+
+
+

+ <%- __('portfolio.title.recent') %> <%- __('portfolio.title.projects') %> +

+

+ <%- __('portfolio.subtitle') %> +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + <%- __('common.view_details') %> + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ <%- __('portfolio.default.ecommerce') %> + + 1,234 + +
+

<%- __('portfolio.default.title') %>

+

<%- __('portfolio.default.description') %>

+ + <%- __('common.view_details') %> + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ <%- __('calculator.cta.title') %> +

+

+ <%- __('calculator.cta.subtitle') %> +

+ + + <%- __('calculator.cta.button') %> + +
+
+
+ + +
+
+
+
+

+ <%- __('contact.cta.ready') %> <%- __('contact.cta.start') %><%- __('contact.cta.question') %> +

+

+ <%- __('contact.cta.subtitle') %> +

+
+
+
+ +
+
+
<%- __('contact.phone.title') %>
+
<%- __('contact.phone.number') %>
+
+
+
+
+ +
+
+
<%- __('contact.email.title') %>
+
<%- __('contact.email.address') %>
+
+
+
+
+ +
+
+
<%- __('contact.telegram.title') %>
+
<%- __('contact.telegram.subtitle') %>
+
+
+
+
+
+
+

<%- __('contact.form.title') %>

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/index-new_20251019174051.ejs b/.history/views/index-new_20251019174051.ejs new file mode 100644 index 0000000..2a60a82 --- /dev/null +++ b/.history/views/index-new_20251019174051.ejs @@ -0,0 +1,400 @@ + + + + + + <%- __(title || 'meta.title') %> + + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ <%- __('hero.title.smart') %> + <%- __('hero.title.solutions') %> +

+

+ <%- __('hero.subtitle') %> +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ <%- __('services.title.our') %> <%- __('services.title.services') %> +

+

+ <%- __('services.subtitle') %> +

+
+ +
+ +
+
+ +
+

<%- __('services.web.title') %>

+

<%- __('services.web.description') %>

+
<%- __('services.web.price') %>
+
+ + +
+
+ +
+

<%- __('services.mobile.title') %>

+

<%- __('services.mobile.description') %>

+
<%- __('services.mobile.price') %>
+
+ + +
+
+ +
+

<%- __('services.design.title') %>

+

<%- __('services.design.description') %>

+
<%- __('services.design.price') %>
+
+ + +
+
+ +
+

<%- __('services.marketing.title') %>

+

<%- __('services.marketing.description') %>

+
<%- __('services.marketing.price') %>
+
+
+ + +
+
+ + +
+
+
+

+ <%- __('portfolio.title.recent') %> <%- __('portfolio.title.projects') %> +

+

+ <%- __('portfolio.subtitle') %> +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + <%- __('common.view_details') %> + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ <%- __('portfolio.default.ecommerce') %> + + 1,234 + +
+

<%- __('portfolio.default.title') %>

+

<%- __('portfolio.default.description') %>

+ + <%- __('common.view_details') %> + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ <%- __('calculator.cta.title') %> +

+

+ <%- __('calculator.cta.subtitle') %> +

+ + + <%- __('calculator.cta.button') %> + +
+
+
+ + +
+
+
+
+

+ <%- __('contact.cta.ready') %> <%- __('contact.cta.start') %><%- __('contact.cta.question') %> +

+

+ <%- __('contact.cta.subtitle') %> +

+
+
+
+ +
+
+
<%- __('contact.phone.title') %>
+
<%- __('contact.phone.number') %>
+
+
+
+
+ +
+
+
<%- __('contact.email.title') %>
+
<%- __('contact.email.address') %>
+
+
+
+
+ +
+
+
<%- __('contact.telegram.title') %>
+
<%- __('contact.telegram.subtitle') %>
+
+
+
+
+
+
+

<%- __('contact.form.title') %>

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/index_20251019161225.ejs b/.history/views/index_20251019161225.ejs new file mode 100644 index 0000000..e1d69e2 --- /dev/null +++ b/.history/views/index_20251019161225.ejs @@ -0,0 +1,310 @@ + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ Smart Technology + Solutions +

+

+ 혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다 +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ Our Services +

+

+ 최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션 +

+
+ +
+ <% if (featuredServices && featuredServices.length > 0) { %> + <% featuredServices.forEach((service, index) => { %> +
+
+ +
+

<%= service.name %>

+

<%= service.shortDescription %>

+
+ ₩<%= service.pricing.basePrice.toLocaleString() %>~ +
+
+ <% }) %> + <% } else { %> + +
+
+ +
+

웹 개발

+

현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발

+
₩500,000~
+
+ +
+
+ +
+

모바일 앱

+

iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱

+
₩800,000~
+
+ +
+
+ +
+

UI/UX 디자인

+

사용자 중심의 직관적이고 아름다운 인터페이스 디자인

+
₩300,000~
+
+ +
+
+ +
+

디지털 마케팅

+

SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅

+
₩200,000~
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ Recent Projects +

+

+ 고객의 성공을 위해 완성한 프로젝트들을 확인해보세요 +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.primaryImage) { %> + <%= project.primaryImage.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount %> + +
+

<%= project.title %>

+

<%= project.shortDescription %>

+ + 자세히 보기 + + + + +
+
+ <% }) %> + <% } %> +
+ + +
+
+ + +
+
+
+

+ 프로젝트 견적을 확인해보세요 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다 +

+ + + 견적 계산기 사용하기 + +
+
+
+ + +
+
+
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다. +

+
+
+
+ +
+
+
전화 상담
+
<%= settings && settings.contact ? settings.contact.phone : '+82-10-0000-0000' %>
+
+
+
+
+ +
+
+
이메일 문의
+
<%= settings && settings.contact ? settings.contact.email : 'info@smartsoltech.kr' %>
+
+
+
+
+ +
+
+
텔레그램 채팅
+
즉시 답변 가능
+
+
+
+
+
+
+

무료 상담 신청

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/.history/views/index_20251019162544.ejs b/.history/views/index_20251019162544.ejs new file mode 100644 index 0000000..e1d69e2 --- /dev/null +++ b/.history/views/index_20251019162544.ejs @@ -0,0 +1,310 @@ + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ Smart Technology + Solutions +

+

+ 혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다 +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ Our Services +

+

+ 최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션 +

+
+ +
+ <% if (featuredServices && featuredServices.length > 0) { %> + <% featuredServices.forEach((service, index) => { %> +
+
+ +
+

<%= service.name %>

+

<%= service.shortDescription %>

+
+ ₩<%= service.pricing.basePrice.toLocaleString() %>~ +
+
+ <% }) %> + <% } else { %> + +
+
+ +
+

웹 개발

+

현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발

+
₩500,000~
+
+ +
+
+ +
+

모바일 앱

+

iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱

+
₩800,000~
+
+ +
+
+ +
+

UI/UX 디자인

+

사용자 중심의 직관적이고 아름다운 인터페이스 디자인

+
₩300,000~
+
+ +
+
+ +
+

디지털 마케팅

+

SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅

+
₩200,000~
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ Recent Projects +

+

+ 고객의 성공을 위해 완성한 프로젝트들을 확인해보세요 +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.primaryImage) { %> + <%= project.primaryImage.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount %> + +
+

<%= project.title %>

+

<%= project.shortDescription %>

+ + 자세히 보기 + + + + +
+
+ <% }) %> + <% } %> +
+ + +
+
+ + +
+
+
+

+ 프로젝트 견적을 확인해보세요 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다 +

+ + + 견적 계산기 사용하기 + +
+
+
+ + +
+
+
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다. +

+
+
+
+ +
+
+
전화 상담
+
<%= settings && settings.contact ? settings.contact.phone : '+82-10-0000-0000' %>
+
+
+
+
+ +
+
+
이메일 문의
+
<%= settings && settings.contact ? settings.contact.email : 'info@smartsoltech.kr' %>
+
+
+
+
+ +
+
+
텔레그램 채팅
+
즉시 답변 가능
+
+
+
+
+
+
+

무료 상담 신청

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/.history/views/index_20251019171013.ejs b/.history/views/index_20251019171013.ejs new file mode 100644 index 0000000..a95ab66 --- /dev/null +++ b/.history/views/index_20251019171013.ejs @@ -0,0 +1,390 @@ + + + + + + <%= title || 'SmartSolTech - 혁신적인 기술 솔루션' %> + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ Smart Technology + Solutions +

+

+ 혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다 +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ Our Services +

+

+ 최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션 +

+
+ +
+ +
+
+ +
+

웹 개발

+

현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발

+
₩500,000~
+
+ +
+
+ +
+

모바일 앱

+

iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱

+
₩800,000~
+
+ +
+
+ +
+

UI/UX 디자인

+

사용자 중심의 직관적이고 아름다운 인터페이스 디자인

+
₩300,000~
+
+ +
+
+ +
+

디지털 마케팅

+

SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅

+
₩200,000~
+
+
+ + +
+
+ + +
+
+
+

+ Recent Projects +

+

+ 고객의 성공을 위해 완성한 프로젝트들을 확인해보세요 +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + 자세히 보기 + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ E-commerce + + 1,234 + +
+

온라인 쇼핑몰 플랫폼

+

현대적인 UI/UX와 완벽한 결제 시스템을 갖춘 전자상거래 솔루션

+ + 자세히 보기 + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ 프로젝트 견적을 확인해보세요 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다 +

+ + + 견적 계산기 사용하기 + +
+
+
+ + +
+
+
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다. +

+
+
+
+ +
+
+
전화 상담
+
+82-10-0000-0000
+
+
+
+
+ +
+
+
이메일 문의
+
info@smartsoltech.kr
+
+
+
+
+ +
+
+
텔레그램 채팅
+
즉시 답변 가능
+
+
+
+
+
+
+

무료 상담 신청

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/index_20251019171203.ejs b/.history/views/index_20251019171203.ejs new file mode 100644 index 0000000..a95ab66 --- /dev/null +++ b/.history/views/index_20251019171203.ejs @@ -0,0 +1,390 @@ + + + + + + <%= title || 'SmartSolTech - 혁신적인 기술 솔루션' %> + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ Smart Technology + Solutions +

+

+ 혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다 +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ Our Services +

+

+ 최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션 +

+
+ +
+ +
+
+ +
+

웹 개발

+

현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발

+
₩500,000~
+
+ +
+
+ +
+

모바일 앱

+

iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱

+
₩800,000~
+
+ +
+
+ +
+

UI/UX 디자인

+

사용자 중심의 직관적이고 아름다운 인터페이스 디자인

+
₩300,000~
+
+ +
+
+ +
+

디지털 마케팅

+

SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅

+
₩200,000~
+
+
+ + +
+
+ + +
+
+
+

+ Recent Projects +

+

+ 고객의 성공을 위해 완성한 프로젝트들을 확인해보세요 +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + 자세히 보기 + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ E-commerce + + 1,234 + +
+

온라인 쇼핑몰 플랫폼

+

현대적인 UI/UX와 완벽한 결제 시스템을 갖춘 전자상거래 솔루션

+ + 자세히 보기 + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ 프로젝트 견적을 확인해보세요 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다 +

+ + + 견적 계산기 사용하기 + +
+
+
+ + +
+
+
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다. +

+
+
+
+ +
+
+
전화 상담
+
+82-10-0000-0000
+
+
+
+
+ +
+
+
이메일 문의
+
info@smartsoltech.kr
+
+
+
+
+ +
+
+
텔레그램 채팅
+
즉시 답변 가능
+
+
+
+
+
+
+

무료 상담 신청

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/.history/views/layout_20251019161025.ejs b/.history/views/layout_20251019161025.ejs new file mode 100644 index 0000000..a1a4954 --- /dev/null +++ b/.history/views/layout_20251019161025.ejs @@ -0,0 +1,88 @@ + + + + + + <%= title %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+ <%- body %> +
+ + + <%- include('partials/footer') %> + + + + + + + + + + <% if (settings && settings.seo && settings.seo.googleAnalytics) { %> + + + + <% } %> + + \ No newline at end of file diff --git a/.history/views/layout_20251019162544.ejs b/.history/views/layout_20251019162544.ejs new file mode 100644 index 0000000..a1a4954 --- /dev/null +++ b/.history/views/layout_20251019162544.ejs @@ -0,0 +1,88 @@ + + + + + + <%= title %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+ <%- body %> +
+ + + <%- include('partials/footer') %> + + + + + + + + + + <% if (settings && settings.seo && settings.seo.googleAnalytics) { %> + + + + <% } %> + + \ No newline at end of file diff --git a/.history/views/partials/footer-new_20251019174149.ejs b/.history/views/partials/footer-new_20251019174149.ejs new file mode 100644 index 0000000..7200dad --- /dev/null +++ b/.history/views/partials/footer-new_20251019174149.ejs @@ -0,0 +1,125 @@ + \ No newline at end of file diff --git a/.history/views/partials/footer-new_20251019174206.ejs b/.history/views/partials/footer-new_20251019174206.ejs new file mode 100644 index 0000000..7200dad --- /dev/null +++ b/.history/views/partials/footer-new_20251019174206.ejs @@ -0,0 +1,125 @@ + \ No newline at end of file diff --git a/.history/views/partials/footer_20251019161120.ejs b/.history/views/partials/footer_20251019161120.ejs new file mode 100644 index 0000000..fb5f61d --- /dev/null +++ b/.history/views/partials/footer_20251019161120.ejs @@ -0,0 +1,103 @@ + \ No newline at end of file diff --git a/.history/views/partials/footer_20251019162544.ejs b/.history/views/partials/footer_20251019162544.ejs new file mode 100644 index 0000000..fb5f61d --- /dev/null +++ b/.history/views/partials/footer_20251019162544.ejs @@ -0,0 +1,103 @@ + \ No newline at end of file diff --git a/.history/views/partials/navigation-new_20251019172101.ejs b/.history/views/partials/navigation-new_20251019172101.ejs new file mode 100644 index 0000000..fc18f45 --- /dev/null +++ b/.history/views/partials/navigation-new_20251019172101.ejs @@ -0,0 +1,207 @@ + + + \ No newline at end of file diff --git a/.history/views/partials/navigation-new_20251019173728.ejs b/.history/views/partials/navigation-new_20251019173728.ejs new file mode 100644 index 0000000..fc18f45 --- /dev/null +++ b/.history/views/partials/navigation-new_20251019173728.ejs @@ -0,0 +1,207 @@ + + + \ No newline at end of file diff --git a/.history/views/partials/navigation_20251019161046.ejs b/.history/views/partials/navigation_20251019161046.ejs new file mode 100644 index 0000000..4bf6bde --- /dev/null +++ b/.history/views/partials/navigation_20251019161046.ejs @@ -0,0 +1,80 @@ + + + +
\ No newline at end of file diff --git a/.history/views/partials/navigation_20251019162544.ejs b/.history/views/partials/navigation_20251019162544.ejs new file mode 100644 index 0000000..4bf6bde --- /dev/null +++ b/.history/views/partials/navigation_20251019162544.ejs @@ -0,0 +1,80 @@ + + + +
\ No newline at end of file diff --git a/.history/views/portfolio-detail_20251019163054.ejs b/.history/views/portfolio-detail_20251019163054.ejs new file mode 100644 index 0000000..4b3a40b --- /dev/null +++ b/.history/views/portfolio-detail_20251019163054.ejs @@ -0,0 +1,473 @@ + + + + + + <%= portfolio.title %> - SmartSolTech 포트폴리오 + + + + + + + + + + + <% if (portfolio.images && portfolio.images.length > 0) { %> + + <% } %> + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+ + + +
+ +
+
+ + <%= getCategoryName(portfolio.category) %> + + <% if (portfolio.featured) { %> + + FEATURED + + <% } %> + <% if (portfolio.status === 'completed') { %> + + 완료 + + <% } else if (portfolio.status === 'in-progress') { %> + + 진행 중 + + <% } %> +
+ +

+ <%= portfolio.title %> +

+ +

+ <%= portfolio.description %> +

+ + +
+
+
<%= portfolio.viewCount || 0 %>
+
조회수
+
+
+
<%= portfolio.likes || 0 %>
+
좋아요
+
+
+
+ <%= portfolio.completedAt ? new Date(portfolio.completedAt).getFullYear() : new Date().getFullYear() %> +
+
완료년도
+
+
+ + +
+ <% if (portfolio.projectUrl) { %> + + + 프로젝트 보기 + + <% } %> + +
+
+ + +
+ <% if (portfolio.images && portfolio.images.length > 0) { %> +
+ <%= portfolio.title %> +
+
+ <% } else { %> +
+ +
+ <% } %> +
+
+
+
+ + +
+
+
+ + +
+ + + <% if (portfolio.images && portfolio.images.length > 1) { %> +
+

프로젝트 갤러리

+ + +
+
+ <% portfolio.images.forEach(image => { %> +
+
+ <%= image.alt || portfolio.title %> +
+

<%= image.alt || portfolio.title %>

+
+
+
+ <% }) %> +
+
+
+
+
+
+ <% } %> + + +
+

프로젝트 개요

+
+ <%= portfolio.description %> +
+
+ + + <% if (portfolio.technologies && portfolio.technologies.length > 0) { %> +
+

사용된 기술

+
+ <% portfolio.technologies.forEach(tech => { %> +
+
<%= tech %>
+
+ <% }) %> +
+
+ <% } %> + + +
+

이 프로젝트 공유하기

+
+ + + + +
+
+
+ + +
+ + +
+

프로젝트 정보

+ +
+ <% if (portfolio.clientName) { %> +
+
클라이언트
+
<%= portfolio.clientName %>
+
+ <% } %> + +
+
카테고리
+
<%= getCategoryName(portfolio.category) %>
+
+ + <% if (portfolio.completedAt) { %> +
+
완료일
+
+ <%= new Date(portfolio.completedAt).toLocaleDateString('ko-KR') %> +
+
+ <% } %> + + <% if (portfolio.projectUrl) { %> +
+
프로젝트 URL
+ + <%= portfolio.projectUrl %> + +
+ <% } %> +
+
+ + +
+

비슷한 프로젝트가 필요하신가요?

+

+ 이런 프로젝트에 관심이 있으시다면 언제든 연락주세요. + 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다. +

+ +
+ +
+
+
+
+ + +
+
+
+

+ 관련 프로젝트 +

+

+ 비슷한 카테고리의 다른 프로젝트들도 확인해보세요 +

+
+ +
+ <% if (relatedProjects && relatedProjects.length > 0) { %> + <% relatedProjects.forEach((project, index) => { %> +
+ +
+ <% if (project.images && project.images.length > 0) { %> + <%= project.title %> + <% } else { %> +
+ +
+ <% } %> +
+ +
+

+ <%= project.title %> +

+

+ <%= project.shortDescription || project.description %> +

+ + 자세히 보기 → + +
+
+ <% }) %> + <% } else { %> +
+

관련 프로젝트가 없습니다.

+
+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio-detail_20251019163806.ejs b/.history/views/portfolio-detail_20251019163806.ejs new file mode 100644 index 0000000..4b3a40b --- /dev/null +++ b/.history/views/portfolio-detail_20251019163806.ejs @@ -0,0 +1,473 @@ + + + + + + <%= portfolio.title %> - SmartSolTech 포트폴리오 + + + + + + + + + + + <% if (portfolio.images && portfolio.images.length > 0) { %> + + <% } %> + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+ + + +
+ +
+
+ + <%= getCategoryName(portfolio.category) %> + + <% if (portfolio.featured) { %> + + FEATURED + + <% } %> + <% if (portfolio.status === 'completed') { %> + + 완료 + + <% } else if (portfolio.status === 'in-progress') { %> + + 진행 중 + + <% } %> +
+ +

+ <%= portfolio.title %> +

+ +

+ <%= portfolio.description %> +

+ + +
+
+
<%= portfolio.viewCount || 0 %>
+
조회수
+
+
+
<%= portfolio.likes || 0 %>
+
좋아요
+
+
+
+ <%= portfolio.completedAt ? new Date(portfolio.completedAt).getFullYear() : new Date().getFullYear() %> +
+
완료년도
+
+
+ + +
+ <% if (portfolio.projectUrl) { %> + + + 프로젝트 보기 + + <% } %> + +
+
+ + +
+ <% if (portfolio.images && portfolio.images.length > 0) { %> +
+ <%= portfolio.title %> +
+
+ <% } else { %> +
+ +
+ <% } %> +
+
+
+
+ + +
+
+
+ + +
+ + + <% if (portfolio.images && portfolio.images.length > 1) { %> +
+

프로젝트 갤러리

+ + +
+
+ <% portfolio.images.forEach(image => { %> +
+
+ <%= image.alt || portfolio.title %> +
+

<%= image.alt || portfolio.title %>

+
+
+
+ <% }) %> +
+
+
+
+
+
+ <% } %> + + +
+

프로젝트 개요

+
+ <%= portfolio.description %> +
+
+ + + <% if (portfolio.technologies && portfolio.technologies.length > 0) { %> +
+

사용된 기술

+
+ <% portfolio.technologies.forEach(tech => { %> +
+
<%= tech %>
+
+ <% }) %> +
+
+ <% } %> + + +
+

이 프로젝트 공유하기

+
+ + + + +
+
+
+ + +
+ + +
+

프로젝트 정보

+ +
+ <% if (portfolio.clientName) { %> +
+
클라이언트
+
<%= portfolio.clientName %>
+
+ <% } %> + +
+
카테고리
+
<%= getCategoryName(portfolio.category) %>
+
+ + <% if (portfolio.completedAt) { %> +
+
완료일
+
+ <%= new Date(portfolio.completedAt).toLocaleDateString('ko-KR') %> +
+
+ <% } %> + + <% if (portfolio.projectUrl) { %> +
+
프로젝트 URL
+ + <%= portfolio.projectUrl %> + +
+ <% } %> +
+
+ + +
+

비슷한 프로젝트가 필요하신가요?

+

+ 이런 프로젝트에 관심이 있으시다면 언제든 연락주세요. + 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다. +

+ +
+ +
+
+
+
+ + +
+
+
+

+ 관련 프로젝트 +

+

+ 비슷한 카테고리의 다른 프로젝트들도 확인해보세요 +

+
+ +
+ <% if (relatedProjects && relatedProjects.length > 0) { %> + <% relatedProjects.forEach((project, index) => { %> +
+ +
+ <% if (project.images && project.images.length > 0) { %> + <%= project.title %> + <% } else { %> +
+ +
+ <% } %> +
+ +
+

+ <%= project.title %> +

+

+ <%= project.shortDescription || project.description %> +

+ + 자세히 보기 → + +
+
+ <% }) %> + <% } else { %> +
+

관련 프로젝트가 없습니다.

+
+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio-detail_20251019164427.ejs b/.history/views/portfolio-detail_20251019164427.ejs new file mode 100644 index 0000000..b98c672 --- /dev/null +++ b/.history/views/portfolio-detail_20251019164427.ejs @@ -0,0 +1,472 @@ + + + + + + <%= portfolio.title %> - SmartSolTech 포트폴리오 + + + + + + + + + + <% if (portfolio.images && portfolio.images.length > 0) { %> + + <% } %> + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+ + + +
+ +
+
+ + <%= getCategoryName(portfolio.category) %> + + <% if (portfolio.featured) { %> + + FEATURED + + <% } %> + <% if (portfolio.status === 'completed') { %> + + 완료 + + <% } else if (portfolio.status === 'in-progress') { %> + + 진행 중 + + <% } %> +
+ +

+ <%= portfolio.title %> +

+ +

+ <%= portfolio.description %> +

+ + +
+
+
<%= portfolio.viewCount || 0 %>
+
조회수
+
+
+
<%= portfolio.likes || 0 %>
+
좋아요
+
+
+
+ <%= portfolio.completedAt ? new Date(portfolio.completedAt).getFullYear() : new Date().getFullYear() %> +
+
완료년도
+
+
+ + +
+ <% if (portfolio.projectUrl) { %> + + + 프로젝트 보기 + + <% } %> + +
+
+ + +
+ <% if (portfolio.images && portfolio.images.length > 0) { %> +
+ <%= portfolio.title %> +
+
+ <% } else { %> +
+ +
+ <% } %> +
+
+
+
+ + +
+
+
+ + +
+ + + <% if (portfolio.images && portfolio.images.length > 1) { %> +
+

프로젝트 갤러리

+ + +
+
+ <% portfolio.images.forEach(image => { %> +
+
+ <%= image.alt || portfolio.title %> +
+

<%= image.alt || portfolio.title %>

+
+
+
+ <% }) %> +
+
+
+
+
+
+ <% } %> + + +
+

프로젝트 개요

+
+ <%= portfolio.description %> +
+
+ + + <% if (portfolio.technologies && portfolio.technologies.length > 0) { %> +
+

사용된 기술

+
+ <% portfolio.technologies.forEach(tech => { %> +
+
<%= tech %>
+
+ <% }) %> +
+
+ <% } %> + + +
+

이 프로젝트 공유하기

+
+ + + + +
+
+
+ + +
+ + +
+

프로젝트 정보

+ +
+ <% if (portfolio.clientName) { %> +
+
클라이언트
+
<%= portfolio.clientName %>
+
+ <% } %> + +
+
카테고리
+
<%= getCategoryName(portfolio.category) %>
+
+ + <% if (portfolio.completedAt) { %> +
+
완료일
+
+ <%= new Date(portfolio.completedAt).toLocaleDateString('ko-KR') %> +
+
+ <% } %> + + <% if (portfolio.projectUrl) { %> +
+
프로젝트 URL
+ + <%= portfolio.projectUrl %> + +
+ <% } %> +
+
+ + +
+

비슷한 프로젝트가 필요하신가요?

+

+ 이런 프로젝트에 관심이 있으시다면 언제든 연락주세요. + 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다. +

+ +
+ +
+
+
+
+ + +
+
+
+

+ 관련 프로젝트 +

+

+ 비슷한 카테고리의 다른 프로젝트들도 확인해보세요 +

+
+ +
+ <% if (relatedProjects && relatedProjects.length > 0) { %> + <% relatedProjects.forEach((project, index) => { %> +
+ +
+ <% if (project.images && project.images.length > 0) { %> + <%= project.title %> + <% } else { %> +
+ +
+ <% } %> +
+ +
+

+ <%= project.title %> +

+

+ <%= project.shortDescription || project.description %> +

+ + 자세히 보기 → + +
+
+ <% }) %> + <% } else { %> +
+

관련 프로젝트가 없습니다.

+
+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio-detail_20251019165556.ejs b/.history/views/portfolio-detail_20251019165556.ejs new file mode 100644 index 0000000..b98c672 --- /dev/null +++ b/.history/views/portfolio-detail_20251019165556.ejs @@ -0,0 +1,472 @@ + + + + + + <%= portfolio.title %> - SmartSolTech 포트폴리오 + + + + + + + + + + <% if (portfolio.images && portfolio.images.length > 0) { %> + + <% } %> + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+ + + +
+ +
+
+ + <%= getCategoryName(portfolio.category) %> + + <% if (portfolio.featured) { %> + + FEATURED + + <% } %> + <% if (portfolio.status === 'completed') { %> + + 완료 + + <% } else if (portfolio.status === 'in-progress') { %> + + 진행 중 + + <% } %> +
+ +

+ <%= portfolio.title %> +

+ +

+ <%= portfolio.description %> +

+ + +
+
+
<%= portfolio.viewCount || 0 %>
+
조회수
+
+
+
<%= portfolio.likes || 0 %>
+
좋아요
+
+
+
+ <%= portfolio.completedAt ? new Date(portfolio.completedAt).getFullYear() : new Date().getFullYear() %> +
+
완료년도
+
+
+ + +
+ <% if (portfolio.projectUrl) { %> + + + 프로젝트 보기 + + <% } %> + +
+
+ + +
+ <% if (portfolio.images && portfolio.images.length > 0) { %> +
+ <%= portfolio.title %> +
+
+ <% } else { %> +
+ +
+ <% } %> +
+
+
+
+ + +
+
+
+ + +
+ + + <% if (portfolio.images && portfolio.images.length > 1) { %> +
+

프로젝트 갤러리

+ + +
+
+ <% portfolio.images.forEach(image => { %> +
+
+ <%= image.alt || portfolio.title %> +
+

<%= image.alt || portfolio.title %>

+
+
+
+ <% }) %> +
+
+
+
+
+
+ <% } %> + + +
+

프로젝트 개요

+
+ <%= portfolio.description %> +
+
+ + + <% if (portfolio.technologies && portfolio.technologies.length > 0) { %> +
+

사용된 기술

+
+ <% portfolio.technologies.forEach(tech => { %> +
+
<%= tech %>
+
+ <% }) %> +
+
+ <% } %> + + +
+

이 프로젝트 공유하기

+
+ + + + +
+
+
+ + +
+ + +
+

프로젝트 정보

+ +
+ <% if (portfolio.clientName) { %> +
+
클라이언트
+
<%= portfolio.clientName %>
+
+ <% } %> + +
+
카테고리
+
<%= getCategoryName(portfolio.category) %>
+
+ + <% if (portfolio.completedAt) { %> +
+
완료일
+
+ <%= new Date(portfolio.completedAt).toLocaleDateString('ko-KR') %> +
+
+ <% } %> + + <% if (portfolio.projectUrl) { %> +
+
프로젝트 URL
+ + <%= portfolio.projectUrl %> + +
+ <% } %> +
+
+ + +
+

비슷한 프로젝트가 필요하신가요?

+

+ 이런 프로젝트에 관심이 있으시다면 언제든 연락주세요. + 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다. +

+ +
+ +
+
+
+
+ + +
+
+
+

+ 관련 프로젝트 +

+

+ 비슷한 카테고리의 다른 프로젝트들도 확인해보세요 +

+
+ +
+ <% if (relatedProjects && relatedProjects.length > 0) { %> + <% relatedProjects.forEach((project, index) => { %> +
+ +
+ <% if (project.images && project.images.length > 0) { %> + <%= project.title %> + <% } else { %> +
+ +
+ <% } %> +
+ +
+

+ <%= project.title %> +

+

+ <%= project.shortDescription || project.description %> +

+ + 자세히 보기 → + +
+
+ <% }) %> + <% } else { %> +
+

관련 프로젝트가 없습니다.

+
+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio-detail_20251019171031.ejs b/.history/views/portfolio-detail_20251019171031.ejs new file mode 100644 index 0000000..9cdc864 --- /dev/null +++ b/.history/views/portfolio-detail_20251019171031.ejs @@ -0,0 +1,473 @@ + + + + + + <%= portfolio.title %> - SmartSolTech 포트폴리오 + + + + + + + + + + <% if (portfolio.images && portfolio.images.length > 0) { %> + + <% } %> + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+ + + +
+ +
+
+ + <%= getCategoryName(portfolio.category) %> + + <% if (portfolio.featured) { %> + + FEATURED + + <% } %> + <% if (portfolio.status === 'completed') { %> + + 완료 + + <% } else if (portfolio.status === 'in-progress') { %> + + 진행 중 + + <% } %> +
+ +

+ <%= portfolio.title %> +

+ +

+ <%= portfolio.description %> +

+ + +
+
+
<%= portfolio.viewCount || 0 %>
+
조회수
+
+
+
<%= portfolio.likes || 0 %>
+
좋아요
+
+
+
+ <%= portfolio.completedAt ? new Date(portfolio.completedAt).getFullYear() : new Date().getFullYear() %> +
+
완료년도
+
+
+ + +
+ <% if (portfolio.projectUrl) { %> + + + 프로젝트 보기 + + <% } %> + +
+
+ + +
+ <% if (portfolio.images && portfolio.images.length > 0) { %> +
+ <%= portfolio.title %> +
+
+ <% } else { %> +
+ +
+ <% } %> +
+
+
+
+ + +
+
+
+ + +
+ + + <% if (portfolio.images && portfolio.images.length > 1) { %> +
+

프로젝트 갤러리

+ + +
+
+ <% portfolio.images.forEach(image => { %> +
+
+ <%= image.alt || portfolio.title %> +
+

<%= image.alt || portfolio.title %>

+
+
+
+ <% }) %> +
+
+
+
+
+
+ <% } %> + + +
+

프로젝트 개요

+
+ <%= portfolio.description %> +
+
+ + + <% if (portfolio.technologies && portfolio.technologies.length > 0) { %> +
+

사용된 기술

+
+ <% portfolio.technologies.forEach(tech => { %> +
+
<%= tech %>
+
+ <% }) %> +
+
+ <% } %> + + +
+

이 프로젝트 공유하기

+
+ + + + +
+
+
+ + +
+ + +
+

프로젝트 정보

+ +
+ <% if (portfolio.clientName) { %> +
+
클라이언트
+
<%= portfolio.clientName %>
+
+ <% } %> + +
+
카테고리
+
<%= getCategoryName(portfolio.category) %>
+
+ + <% if (portfolio.completedAt) { %> +
+
완료일
+
+ <%= new Date(portfolio.completedAt).toLocaleDateString('ko-KR') %> +
+
+ <% } %> + + <% if (portfolio.projectUrl) { %> +
+
프로젝트 URL
+ + <%= portfolio.projectUrl %> + +
+ <% } %> +
+
+ + +
+

비슷한 프로젝트가 필요하신가요?

+

+ 이런 프로젝트에 관심이 있으시다면 언제든 연락주세요. + 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다. +

+ +
+ +
+
+
+
+ + +
+
+
+

+ 관련 프로젝트 +

+

+ 비슷한 카테고리의 다른 프로젝트들도 확인해보세요 +

+
+ +
+ <% if (relatedProjects && relatedProjects.length > 0) { %> + <% relatedProjects.forEach((project, index) => { %> +
+ +
+ <% if (project.images && project.images.length > 0) { %> + <%= project.title %> + <% } else { %> +
+ +
+ <% } %> +
+ +
+

+ <%= project.title %> +

+

+ <%= project.shortDescription || project.description %> +

+ + 자세히 보기 → + +
+
+ <% }) %> + <% } else { %> +
+

관련 프로젝트가 없습니다.

+
+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio-detail_20251019171203.ejs b/.history/views/portfolio-detail_20251019171203.ejs new file mode 100644 index 0000000..9cdc864 --- /dev/null +++ b/.history/views/portfolio-detail_20251019171203.ejs @@ -0,0 +1,473 @@ + + + + + + <%= portfolio.title %> - SmartSolTech 포트폴리오 + + + + + + + + + + <% if (portfolio.images && portfolio.images.length > 0) { %> + + <% } %> + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+ + + +
+ +
+
+ + <%= getCategoryName(portfolio.category) %> + + <% if (portfolio.featured) { %> + + FEATURED + + <% } %> + <% if (portfolio.status === 'completed') { %> + + 완료 + + <% } else if (portfolio.status === 'in-progress') { %> + + 진행 중 + + <% } %> +
+ +

+ <%= portfolio.title %> +

+ +

+ <%= portfolio.description %> +

+ + +
+
+
<%= portfolio.viewCount || 0 %>
+
조회수
+
+
+
<%= portfolio.likes || 0 %>
+
좋아요
+
+
+
+ <%= portfolio.completedAt ? new Date(portfolio.completedAt).getFullYear() : new Date().getFullYear() %> +
+
완료년도
+
+
+ + +
+ <% if (portfolio.projectUrl) { %> + + + 프로젝트 보기 + + <% } %> + +
+
+ + +
+ <% if (portfolio.images && portfolio.images.length > 0) { %> +
+ <%= portfolio.title %> +
+
+ <% } else { %> +
+ +
+ <% } %> +
+
+
+
+ + +
+
+
+ + +
+ + + <% if (portfolio.images && portfolio.images.length > 1) { %> +
+

프로젝트 갤러리

+ + +
+
+ <% portfolio.images.forEach(image => { %> +
+
+ <%= image.alt || portfolio.title %> +
+

<%= image.alt || portfolio.title %>

+
+
+
+ <% }) %> +
+
+
+
+
+
+ <% } %> + + +
+

프로젝트 개요

+
+ <%= portfolio.description %> +
+
+ + + <% if (portfolio.technologies && portfolio.technologies.length > 0) { %> +
+

사용된 기술

+
+ <% portfolio.technologies.forEach(tech => { %> +
+
<%= tech %>
+
+ <% }) %> +
+
+ <% } %> + + +
+

이 프로젝트 공유하기

+
+ + + + +
+
+
+ + +
+ + +
+

프로젝트 정보

+ +
+ <% if (portfolio.clientName) { %> +
+
클라이언트
+
<%= portfolio.clientName %>
+
+ <% } %> + +
+
카테고리
+
<%= getCategoryName(portfolio.category) %>
+
+ + <% if (portfolio.completedAt) { %> +
+
완료일
+
+ <%= new Date(portfolio.completedAt).toLocaleDateString('ko-KR') %> +
+
+ <% } %> + + <% if (portfolio.projectUrl) { %> +
+
프로젝트 URL
+ + <%= portfolio.projectUrl %> + +
+ <% } %> +
+
+ + +
+

비슷한 프로젝트가 필요하신가요?

+

+ 이런 프로젝트에 관심이 있으시다면 언제든 연락주세요. + 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다. +

+ +
+ +
+
+
+
+ + +
+
+
+

+ 관련 프로젝트 +

+

+ 비슷한 카테고리의 다른 프로젝트들도 확인해보세요 +

+
+ +
+ <% if (relatedProjects && relatedProjects.length > 0) { %> + <% relatedProjects.forEach((project, index) => { %> +
+ +
+ <% if (project.images && project.images.length > 0) { %> + <%= project.title %> + <% } else { %> +
+ +
+ <% } %> +
+ +
+

+ <%= project.title %> +

+

+ <%= project.shortDescription || project.description %> +

+ + 자세히 보기 → + +
+
+ <% }) %> + <% } else { %> +
+

관련 프로젝트가 없습니다.

+
+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio_20251019162820.ejs b/.history/views/portfolio_20251019162820.ejs new file mode 100644 index 0000000..5d868dc --- /dev/null +++ b/.history/views/portfolio_20251019162820.ejs @@ -0,0 +1,297 @@ + + + + + + 포트폴리오 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 포트폴리오 +

+

+ 혁신적인 프로젝트와 창의적인 솔루션들을 만나보세요 +

+
+ + + + +
+
+
+ + +
+
+
+ <% if (portfolioItems && portfolioItems.length > 0) { %> + <% portfolioItems.forEach((item, index) => { %> +
+ + +
+ <% if (item.images && item.images.length > 0) { %> + <%= item.title %> + <% } else { %> +
+ +
+ <% } %> + + +
+ + <%= getCategoryName(item.category) %> + +
+ + + <% if (item.featured) { %> +
+ + FEATURED + +
+ <% } %> + + + +
+ + +
+

+ <%= item.title %> +

+

+ <%= item.shortDescription || item.description %> +

+ + +
+ <% if (item.technologies && item.technologies.length > 0) { %> + <% item.technologies.slice(0, 3).forEach(tech => { %> + + <%= tech %> + + <% }) %> + <% if (item.technologies.length > 3) { %> + + +<%= item.technologies.length - 3 %> + + <% } %> + <% } %> +
+ + +
+
+ <% if (item.clientName) { %> + + <%= item.clientName %> + <% } %> +
+
+ + + <%= item.viewCount || 0 %> + + + + <%= item.likes || 0 %> + +
+
+ + + +
+
+ <% }) %> + <% } else { %> +
+ +

아직 포트폴리오가 없습니다

+

곧 멋진 프로젝트들을 공개할 예정입니다!

+
+ <% } %> +
+ + + <% if (portfolioItems && portfolioItems.length >= 9) { %> +
+ +
+ <% } %> +
+
+ + +
+
+

+ 다음 프로젝트의 주인공이 되어보세요 +

+

+ 우리와 함께 혁신적인 디지털 솔루션을 만들어보세요 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio_20251019163806.ejs b/.history/views/portfolio_20251019163806.ejs new file mode 100644 index 0000000..5d868dc --- /dev/null +++ b/.history/views/portfolio_20251019163806.ejs @@ -0,0 +1,297 @@ + + + + + + 포트폴리오 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 포트폴리오 +

+

+ 혁신적인 프로젝트와 창의적인 솔루션들을 만나보세요 +

+
+ + + + +
+
+
+ + +
+
+
+ <% if (portfolioItems && portfolioItems.length > 0) { %> + <% portfolioItems.forEach((item, index) => { %> +
+ + +
+ <% if (item.images && item.images.length > 0) { %> + <%= item.title %> + <% } else { %> +
+ +
+ <% } %> + + +
+ + <%= getCategoryName(item.category) %> + +
+ + + <% if (item.featured) { %> +
+ + FEATURED + +
+ <% } %> + + + +
+ + +
+

+ <%= item.title %> +

+

+ <%= item.shortDescription || item.description %> +

+ + +
+ <% if (item.technologies && item.technologies.length > 0) { %> + <% item.technologies.slice(0, 3).forEach(tech => { %> + + <%= tech %> + + <% }) %> + <% if (item.technologies.length > 3) { %> + + +<%= item.technologies.length - 3 %> + + <% } %> + <% } %> +
+ + +
+
+ <% if (item.clientName) { %> + + <%= item.clientName %> + <% } %> +
+
+ + + <%= item.viewCount || 0 %> + + + + <%= item.likes || 0 %> + +
+
+ + + +
+
+ <% }) %> + <% } else { %> +
+ +

아직 포트폴리오가 없습니다

+

곧 멋진 프로젝트들을 공개할 예정입니다!

+
+ <% } %> +
+ + + <% if (portfolioItems && portfolioItems.length >= 9) { %> +
+ +
+ <% } %> +
+
+ + +
+
+

+ 다음 프로젝트의 주인공이 되어보세요 +

+

+ 우리와 함께 혁신적인 디지털 솔루션을 만들어보세요 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio_20251019164448.ejs b/.history/views/portfolio_20251019164448.ejs new file mode 100644 index 0000000..8173622 --- /dev/null +++ b/.history/views/portfolio_20251019164448.ejs @@ -0,0 +1,296 @@ + + + + + + 포트폴리오 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 포트폴리오 +

+

+ 혁신적인 프로젝트와 창의적인 솔루션들을 만나보세요 +

+
+ + + + +
+
+
+ + +
+
+
+ <% if (portfolioItems && portfolioItems.length > 0) { %> + <% portfolioItems.forEach((item, index) => { %> +
+ + +
+ <% if (item.images && item.images.length > 0) { %> + <%= item.title %> + <% } else { %> +
+ +
+ <% } %> + + +
+ + <%= getCategoryName(item.category) %> + +
+ + + <% if (item.featured) { %> +
+ + FEATURED + +
+ <% } %> + + + +
+ + +
+

+ <%= item.title %> +

+

+ <%= item.shortDescription || item.description %> +

+ + +
+ <% if (item.technologies && item.technologies.length > 0) { %> + <% item.technologies.slice(0, 3).forEach(tech => { %> + + <%= tech %> + + <% }) %> + <% if (item.technologies.length > 3) { %> + + +<%= item.technologies.length - 3 %> + + <% } %> + <% } %> +
+ + +
+
+ <% if (item.clientName) { %> + + <%= item.clientName %> + <% } %> +
+
+ + + <%= item.viewCount || 0 %> + + + + <%= item.likes || 0 %> + +
+
+ + + +
+
+ <% }) %> + <% } else { %> +
+ +

아직 포트폴리오가 없습니다

+

곧 멋진 프로젝트들을 공개할 예정입니다!

+
+ <% } %> +
+ + + <% if (portfolioItems && portfolioItems.length >= 9) { %> +
+ +
+ <% } %> +
+
+ + +
+
+

+ 다음 프로젝트의 주인공이 되어보세요 +

+

+ 우리와 함께 혁신적인 디지털 솔루션을 만들어보세요 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/portfolio_20251019165556.ejs b/.history/views/portfolio_20251019165556.ejs new file mode 100644 index 0000000..8173622 --- /dev/null +++ b/.history/views/portfolio_20251019165556.ejs @@ -0,0 +1,296 @@ + + + + + + 포트폴리오 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 포트폴리오 +

+

+ 혁신적인 프로젝트와 창의적인 솔루션들을 만나보세요 +

+
+ + + + +
+
+
+ + +
+
+
+ <% if (portfolioItems && portfolioItems.length > 0) { %> + <% portfolioItems.forEach((item, index) => { %> +
+ + +
+ <% if (item.images && item.images.length > 0) { %> + <%= item.title %> + <% } else { %> +
+ +
+ <% } %> + + +
+ + <%= getCategoryName(item.category) %> + +
+ + + <% if (item.featured) { %> +
+ + FEATURED + +
+ <% } %> + + + +
+ + +
+

+ <%= item.title %> +

+

+ <%= item.shortDescription || item.description %> +

+ + +
+ <% if (item.technologies && item.technologies.length > 0) { %> + <% item.technologies.slice(0, 3).forEach(tech => { %> + + <%= tech %> + + <% }) %> + <% if (item.technologies.length > 3) { %> + + +<%= item.technologies.length - 3 %> + + <% } %> + <% } %> +
+ + +
+
+ <% if (item.clientName) { %> + + <%= item.clientName %> + <% } %> +
+
+ + + <%= item.viewCount || 0 %> + + + + <%= item.likes || 0 %> + +
+
+ + + +
+
+ <% }) %> + <% } else { %> +
+ +

아직 포트폴리오가 없습니다

+

곧 멋진 프로젝트들을 공개할 예정입니다!

+
+ <% } %> +
+ + + <% if (portfolioItems && portfolioItems.length >= 9) { %> +
+ +
+ <% } %> +
+
+ + +
+
+

+ 다음 프로젝트의 주인공이 되어보세요 +

+

+ 우리와 함께 혁신적인 디지털 솔루션을 만들어보세요 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/.history/views/services_20251019163647.ejs b/.history/views/services_20251019163647.ejs new file mode 100644 index 0000000..02f0191 --- /dev/null +++ b/.history/views/services_20251019163647.ejs @@ -0,0 +1,293 @@ + + + + + + 서비스 - SmartSolTech + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 서비스 +

+

+ 혁신적인 기술로 비즈니스의 성장을 지원합니다 +

+
+
+ + +
+
+
+ <% if (services && services.length > 0) { %> + <% services.forEach((service, index) => { %> +
+ + +
+ +
+ + +

+ <%= service.name %> +

+ +

+ <%= service.shortDescription || service.description %> +

+ + + <% if (service.pricing) { %> +
+
시작가격
+
+ <%= service.pricing.basePrice ? service.pricing.basePrice.toLocaleString() : '상담' %> + <% if (service.pricing.basePrice) { %> + 원~ + <% } %> +
+ <% if (service.pricing.priceRange) { %> +
+ <%= service.pricing.priceRange.min.toLocaleString() %>원 - + <%= service.pricing.priceRange.max.toLocaleString() %>원 +
+ <% } %> +
+ <% } %> + + + + + + <% if (service.featured) { %> +
+ + 인기 + +
+ <% } %> +
+ <% }) %> + <% } else { %> +
+ +

서비스 준비 중

+

곧 다양한 서비스를 제공할 예정입니다!

+
+ <% } %> +
+
+
+ + +
+
+
+

+ 프로젝트 진행 과정 +

+

+ 체계적이고 전문적인 프로세스로 프로젝트를 진행합니다 +

+
+ +
+ +
+
+ 1 +
+

상담 및 기획

+

+ 고객의 요구사항을 정확히 파악하고 + 최적의 솔루션을 기획합니다 +

+
+ + +
+
+ 2 +
+

디자인 및 설계

+

+ 사용자 중심의 직관적인 디자인과 + 견고한 시스템 아키텍처를 설계합니다 +

+
+ + +
+
+ 3 +
+

개발 및 구현

+

+ 최신 기술과 모범 사례를 활용하여 + 효율적이고 확장 가능한 솔루션을 개발합니다 +

+
+ + +
+
+ 4 +
+

테스트 및 배포

+

+ 철저한 테스트를 통해 품질을 보장하고 + 안정적인 배포를 진행합니다 +

+
+
+
+
+ + +
+
+
+ +
+

+ 왜 SmartSolTech를 선택해야 할까요? +

+ +
+ +
+
+ +
+
+

최신 기술 활용

+

+ 항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 + 미래 지향적인 솔루션을 제공합니다. +

+
+
+ + +
+
+ +
+
+

전문가 팀

+

+ 각 분야의 전문가들로 구성된 팀이 협력하여 + 최고 품질의 결과물을 보장합니다. +

+
+
+ + +
+
+ +
+
+

빠른 대응

+

+ 신속한 커뮤니케이션과 효율적인 프로젝트 관리로 + 정해진 일정 내에 프로젝트를 완료합니다. +

+
+
+ + +
+
+ +
+
+

지속적인 지원

+

+ 프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 + 통해 장기적인 파트너십을 유지합니다. +

+
+
+
+
+ + +
+
+
+ +

품질 보장

+

+ 고객 만족을 위한
+ 최고 품질의 서비스 +

+
+
+
+
+
+
+ + +
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + \ No newline at end of file diff --git a/.history/views/services_20251019163807.ejs b/.history/views/services_20251019163807.ejs new file mode 100644 index 0000000..02f0191 --- /dev/null +++ b/.history/views/services_20251019163807.ejs @@ -0,0 +1,293 @@ + + + + + + 서비스 - SmartSolTech + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 서비스 +

+

+ 혁신적인 기술로 비즈니스의 성장을 지원합니다 +

+
+
+ + +
+
+
+ <% if (services && services.length > 0) { %> + <% services.forEach((service, index) => { %> +
+ + +
+ +
+ + +

+ <%= service.name %> +

+ +

+ <%= service.shortDescription || service.description %> +

+ + + <% if (service.pricing) { %> +
+
시작가격
+
+ <%= service.pricing.basePrice ? service.pricing.basePrice.toLocaleString() : '상담' %> + <% if (service.pricing.basePrice) { %> + 원~ + <% } %> +
+ <% if (service.pricing.priceRange) { %> +
+ <%= service.pricing.priceRange.min.toLocaleString() %>원 - + <%= service.pricing.priceRange.max.toLocaleString() %>원 +
+ <% } %> +
+ <% } %> + + + + + + <% if (service.featured) { %> +
+ + 인기 + +
+ <% } %> +
+ <% }) %> + <% } else { %> +
+ +

서비스 준비 중

+

곧 다양한 서비스를 제공할 예정입니다!

+
+ <% } %> +
+
+
+ + +
+
+
+

+ 프로젝트 진행 과정 +

+

+ 체계적이고 전문적인 프로세스로 프로젝트를 진행합니다 +

+
+ +
+ +
+
+ 1 +
+

상담 및 기획

+

+ 고객의 요구사항을 정확히 파악하고 + 최적의 솔루션을 기획합니다 +

+
+ + +
+
+ 2 +
+

디자인 및 설계

+

+ 사용자 중심의 직관적인 디자인과 + 견고한 시스템 아키텍처를 설계합니다 +

+
+ + +
+
+ 3 +
+

개발 및 구현

+

+ 최신 기술과 모범 사례를 활용하여 + 효율적이고 확장 가능한 솔루션을 개발합니다 +

+
+ + +
+
+ 4 +
+

테스트 및 배포

+

+ 철저한 테스트를 통해 품질을 보장하고 + 안정적인 배포를 진행합니다 +

+
+
+
+
+ + +
+
+
+ +
+

+ 왜 SmartSolTech를 선택해야 할까요? +

+ +
+ +
+
+ +
+
+

최신 기술 활용

+

+ 항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 + 미래 지향적인 솔루션을 제공합니다. +

+
+
+ + +
+
+ +
+
+

전문가 팀

+

+ 각 분야의 전문가들로 구성된 팀이 협력하여 + 최고 품질의 결과물을 보장합니다. +

+
+
+ + +
+
+ +
+
+

빠른 대응

+

+ 신속한 커뮤니케이션과 효율적인 프로젝트 관리로 + 정해진 일정 내에 프로젝트를 완료합니다. +

+
+
+ + +
+
+ +
+
+

지속적인 지원

+

+ 프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 + 통해 장기적인 파트너십을 유지합니다. +

+
+
+
+
+ + +
+
+
+ +

품질 보장

+

+ 고객 만족을 위한
+ 최고 품질의 서비스 +

+
+
+
+
+
+
+ + +
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e831520 --- /dev/null +++ b/README.md @@ -0,0 +1,280 @@ +# SmartSolTech Website + +Современный PWA сайт для SmartSolTech с админ-панелью, управлением портфолио, интеграцией с Telegram ботом и калькулятором стоимости услуг. + +## 🚀 Особенности + +- **Современный дизайн**: Отзывчивый интерфейс с анимациями и современным UI/UX +- **PWA**: Прогрессивное веб-приложение с поддержкой офлайн-режима +- **Админ-панель**: Управление портфолио, услугами, медиа-контентом и настройками сайта +- **Интеграция с Telegram**: Подключение к Telegram боту для уведомлений +- **Калькулятор стоимости**: Интерактивный калькулятор расчета стоимости услуг +- **Система аутентификации**: Безопасная авторизация с JWT токенами +- **Загрузка файлов**: Оптимизация изображений и управление медиа-контентом +- **SEO оптимизация**: Настроенные мета-теги и структурированные данные + +## 🛠️ Технологии + +### Backend +- **Node.js** - Серверная платформа +- **Express.js** - Веб-фреймворк +- **MongoDB** - База данных +- **Mongoose** - ODM для MongoDB +- **JWT** - Токены аутентификации +- **bcrypt** - Хеширование паролей +- **Multer** - Загрузка файлов +- **Sharp** - Обработка изображений + +### Frontend +- **EJS** - Шаблонизатор +- **Tailwind CSS** - CSS фреймворк +- **AOS** - Библиотека анимаций +- **Font Awesome** - Иконки +- **Service Worker** - PWA функциональность + +### Дополнительно +- **node-telegram-bot-api** - Интеграция с Telegram +- **Nodemailer** - Отправка email +- **Helmet** - Безопасность +- **express-rate-limit** - Ограничение запросов + +## 📦 Установка + +1. **Клонирование репозитория** +```bash +git clone +cd sst_site +``` + +2. **Установка зависимостей** +```bash +npm install +``` + +3. **Настройка переменных окружения** +```bash +cp .env.example .env +``` + +Отредактируйте файл `.env` с вашими настройками: +```env +NODE_ENV=development +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# Security +SESSION_SECRET=your-super-secret-session-key +JWT_SECRET=your-super-secret-jwt-key + +# File Upload +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password + +# Telegram Bot (Optional) +TELEGRAM_BOT_TOKEN=your-telegram-bot-token + +# Admin Account +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=change-this-password +``` + +4. **Инициализация базы данных** +```bash +npm run init-db +``` + +5. **Запуск в режиме разработки** +```bash +npm run dev +``` + +Сайт будет доступен по адресу: `http://localhost:3000` + +## 🗂️ Структура проекта + +``` +sst_site/ +├── models/ # Модели данных (MongoDB) +│ ├── User.js +│ ├── Portfolio.js +│ ├── Service.js +│ ├── Contact.js +│ └── SiteSettings.js +├── routes/ # Маршруты API +│ ├── index.js +│ ├── auth.js +│ ├── contact.js +│ ├── calculator.js +│ ├── portfolio.js +│ ├── services.js +│ ├── media.js +│ └── admin.js +├── views/ # Шаблоны EJS +│ ├── layout.ejs +│ ├── index.ejs +│ ├── calculator.ejs +│ └── partials/ +├── public/ # Статические файлы +│ ├── css/ +│ ├── js/ +│ ├── images/ +│ ├── manifest.json +│ └── sw.js +├── middleware/ # Промежуточное ПО +├── scripts/ # Служебные скрипты +│ ├── init-db.js +│ ├── dev.js +│ └── build.js +├── uploads/ # Загруженные файлы +├── server.js # Главный файл сервера +├── package.json +└── README.md +``` + +## 📋 Доступные команды + +```bash +# Разработка +npm run dev # Запуск в режиме разработки с hot reload + +# Продакшн +npm start # Запуск в продакшн режиме +npm run build # Сборка для продакшна + +# База данных +npm run init-db # Инициализация БД с тестовыми данными + +# Тестирование +npm test # Запуск тестов (пока не реализовано) +``` + +## 🔐 Админ-панель + +После инициализации базы данных вы можете войти в админ-панель: + +- **URL**: `http://localhost:3000/admin` +- **Email**: `admin@smartsoltech.kr` (или из .env) +- **Пароль**: Указанный в .env файле + +### Возможности админ-панели: +- Управление портфолио проектами +- Редактирование услуг и их стоимости +- Загрузка и управление медиа-файлами +- Просмотр и управление контактными формами +- Настройки сайта и SEO +- Управление пользователями + +## 🤖 Интеграция с Telegram + +Для настройки Telegram бота: + +1. Создайте бота через [@BotFather](https://t.me/BotFather) +2. Получите токен бота +3. Добавьте токен в `.env` файл: `TELEGRAM_BOT_TOKEN=your-token` +4. Перезапустите сервер + +Бот будет отправлять уведомления о: +- Новых контактных формах +- Заказах через калькулятор +- Новых комментариях + +## 💰 Калькулятор стоимости + +Интерактивный калькулятор позволяет клиентам: +- Выбрать тип услуги +- Указать дополнительные параметры +- Получить примерную стоимость +- Отправить заявку на расчет + +Настройки калькулятора можно изменить в админ-панели. + +## 🔒 Безопасность + +Проект включает следующие меры безопасности: +- Хеширование паролей с bcrypt +- JWT токены для аутентификации +- Защита от CSRF атак +- Ограничение количества запросов +- Валидация входных данных +- Безопасные HTTP заголовки + +## 📱 PWA функции + +- Установка на устройство +- Офлайн работа +- Push уведомления +- Фоновая синхронизация +- Адаптивные иконки +- Splash screen + +## 🚀 Деплой + +### Для продакшна: + +1. **Сборка приложения** +```bash +npm run build +``` + +2. **Настройка сервера** +- Установите Node.js и MongoDB +- Настройте переменные окружения +- Настройте прокси-сервер (nginx) + +3. **Запуск** +```bash +cd dist +npm install --production +npm start +``` + +### Docker деплой: + +```dockerfile +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +EXPOSE 3000 +CMD ["npm", "start"] +``` + +## 🤝 Участие в разработке + +1. Форкните репозиторий +2. Создайте ветку для фичи (`git checkout -b feature/AmazingFeature`) +3. Сделайте коммит (`git commit -m 'Add some AmazingFeature'`) +4. Отправьте в ветку (`git push origin feature/AmazingFeature`) +5. Откройте Pull Request + +## 📝 Лицензия + +Этот проект лицензирован под MIT License - подробности в файле [LICENSE](LICENSE). + +## 📞 Поддержка + +Если у вас есть вопросы или проблемы: + +- Email: info@smartsoltech.kr +- GitHub Issues: [Create Issue](../../issues) +- Telegram: @smartsoltech + +## 🙏 Благодарности + +- [Express.js](https://expressjs.com/) - За отличный веб-фреймворк +- [MongoDB](https://www.mongodb.com/) - За гибкую базу данных +- [Tailwind CSS](https://tailwindcss.com/) - За удобный CSS фреймворк +- [AOS](https://michalsnik.github.io/aos/) - За красивые анимации + +--- + +**SmartSolTech** - Умные решения для вашего бизнеса 🚀 \ No newline at end of file diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..5f08cf5 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,318 @@ +{ + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin", + "home - SmartSolTech": "navigation.home - SmartSolTech" + }, + "hero": { + "title": { + "smart": "hero.title.smart", + "solutions": "hero.title.solutions" + }, + "subtitle": "Solutions", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta_primary": "Start Project", + "cta_secondary": "View Portfolio", + "cta": { + "start": "hero.cta.start", + "portfolio": "hero.cta.portfolio" + } + }, + "services": { + "title": { + "our": "services.title.our", + "services": "services.title.services" + }, + "title_highlight": "Services", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "web_development": { + "title": "Web Development", + "description": "Modern and responsive websites and web applications development", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Mobile App", + "description": "Native and cross-platform apps for iOS and Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX Design", + "description": "User-centered intuitive and beautiful interface design", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Digital Marketing", + "description": "Digital marketing through SEO, social media, online advertising", + "price": "$2,000~" + }, + "view_all": "View All Services", + "subtitle": "services.subtitle", + "web": { + "title": "services.web.title", + "description": "services.web.description", + "price": "services.web.price" + }, + "mobile": { + "title": "services.mobile.title", + "description": "services.mobile.description", + "price": "services.mobile.price" + }, + "design": { + "title": "services.design.title", + "description": "services.design.description", + "price": "services.design.price" + }, + "marketing": { + "title": "services.marketing.title", + "description": "services.marketing.description", + "price": "services.marketing.price" + } + }, + "portfolio": { + "title": { + "recent": "portfolio.title.recent", + "projects": "portfolio.title.projects" + }, + "title_highlight": "Projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio", + "subtitle": "portfolio.subtitle" + }, + "calculator": { + "title": "Project Cost Calculator", + "subtitle": "Select your desired services and requirements to get accurate cost estimates in real time", + "meta": { + "title": "Project Cost Calculator", + "description": "Calculate the cost of your web development, mobile app, or design project with our interactive calculator" + }, + "cta": { + "title": "Check Your Project Estimate", + "subtitle": "Select your desired services and requirements to calculate costs in real time", + "button": "Use Cost Calculator" + }, + "step1": { + "title": "Step 1: Service Selection", + "subtitle": "Please select the services you need (multiple selection allowed)" + }, + "step2": { + "title": "Step 2: Project Details", + "subtitle": "Select project complexity and timeline" + }, + "complexity": { + "title": "Project Complexity", + "simple": "Simple", + "simple_desc": "Basic features, standard design", + "medium": "Medium", + "medium_desc": "Additional features, custom design", + "complex": "Complex", + "complex_desc": "Advanced features, complex integrations" + }, + "timeline": { + "title": "Development Timeline", + "standard": "Standard", + "standard_desc": "Normal development timeframe", + "rush": "Rush", + "rush_desc": "Fast development (+50%)", + "extended": "Extended", + "extended_desc": "Flexible development timeline (-20%)" + }, + "result": { + "title": "Estimate Results", + "subtitle": "Here's your preliminary project cost estimate", + "estimated_price": "Estimated Price", + "price_note": "* Final cost may vary based on project details", + "summary": "Project Summary", + "selected_services": "Selected Services", + "complexity": "Complexity", + "timeline": "Timeline", + "get_quote": "Get Accurate Quote", + "recalculate": "Recalculate", + "contact_note": "Contact us for an accurate quote and to discuss project details" + }, + "next_step": "Next Step", + "prev_step": "Previous", + "calculate": "Calculate" + }, + "contact": { + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "phone_consultation": "Phone Consultation", + "email_inquiry": "Email Inquiry", + "telegram_chat": "Telegram Chat", + "instant_response": "Instant response available", + "free_consultation": "Free Consultation Application", + "form": { + "name": "Name", + "email": "Email", + "phone": "Phone", + "service_interest": "Service Interest", + "service_options": { + "select": "Select Service Interest", + "web_development": "Web Development", + "mobile_app": "Mobile App", + "ui_ux_design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + }, + "message": "Please briefly describe your project", + "submit": "Apply for Consultation", + "title": "contact.form.title", + "service": { + "select": "contact.form.service.select", + "web": "contact.form.service.web", + "mobile": "contact.form.service.mobile", + "design": "contact.form.service.design", + "branding": "contact.form.service.branding", + "consulting": "contact.form.service.consulting", + "other": "contact.form.service.other" + }, + "success": "contact.form.success", + "error": "contact.form.error" + }, + "cta": { + "ready": "contact.cta.ready", + "start": "contact.cta.start", + "question": "contact.cta.question", + "subtitle": "contact.cta.subtitle" + }, + "phone": { + "title": "contact.phone.title", + "number": "contact.phone.number" + }, + "email": { + "title": "contact.email.title", + "address": "contact.email.address" + }, + "telegram": { + "title": "contact.telegram.title", + "subtitle": "contact.telegram.subtitle" + } + }, + "about": { + "hero_title": "About", + "hero_highlight": "SmartSolTech", + "hero_description": "Digital solution specialist leading customer success with innovative technology", + "overview": { + "title": "Creating Future with Innovation and Creativity", + "description_1": "SmartSolTech is a digital solution specialist established in 2020, supporting customer business success with innovative technology and creative ideas in web development, mobile apps, and UI/UX design.", + "description_2": "We don't just provide technology, but understand customer goals and propose optimal solutions to become partners growing together.", + "stats": { + "projects": "100+", + "projects_label": "Completed Projects", + "clients": "50+", + "clients_label": "Satisfied Customers", + "experience": "4 years", + "experience_label": "Industry Experience" + }, + "mission": "Our Mission", + "mission_text": "Helping all businesses succeed in the digital age through technology", + "vision": "Our Vision", + "vision_text": "Growing as a global digital solution company representing Korea to lead digital innovation for customers worldwide" + }, + "values": { + "title": "Core", + "title_highlight": "Values", + "description": "Core values pursued by SmartSolTech", + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "collaboration": { + "title": "Collaboration", + "description": "We create the best results through close communication and collaboration with customers." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "growth": { + "title": "Growth", + "description": "We grow together with customers and pursue continuous learning and development." + } + }, + "team": { + "title": "Our", + "title_highlight": "Team", + "description": "Introducing the SmartSolTech team with expertise and passion" + }, + "tech_stack": { + "title": "Technology", + "title_highlight": "Stack", + "description": "We provide the best solutions with cutting-edge technology and proven tools", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Mobile" + }, + "cta": { + "title": "Become a Partner for Success Together", + "description": "Take your business to the next level with SmartSolTech", + "partnership": "Partnership Inquiry", + "portfolio": "View Portfolio" + } + }, + "footer": { + "company": { + "description": "footer.company.description" + }, + "description": "Digital solution specialist leading innovation", + "quick_links": "Quick Links", + "services": "Services", + "contact_info": "Contact Information", + "follow_us": "Follow Us", + "rights": "All rights reserved.", + "links": { + "title": "footer.links.title" + }, + "contact": { + "title": "footer.contact.title", + "email": "footer.contact.email", + "phone": "footer.contact.phone", + "address": "footer.contact.address" + }, + "copyright": "footer.copyright", + "privacy": "footer.privacy", + "terms": "footer.terms" + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша", + "ko": "language.ko" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous", + "view_details": "common.view_details" + }, + "undefined - SmartSolTech": "undefined - SmartSolTech", + "meta": { + "description": "meta.description", + "keywords": "meta.keywords", + "title": "meta.title" + }, + "nav": { + "home": "nav.home", + "about": "nav.about", + "services": "nav.services", + "portfolio": "nav.portfolio", + "calculator": "nav.calculator" + } +} \ No newline at end of file diff --git a/locales/kk.json b/locales/kk.json new file mode 100644 index 0000000..b607320 --- /dev/null +++ b/locales/kk.json @@ -0,0 +1,318 @@ +{ + "navigation": { + "home": "Басты бет", + "about": "Біз туралы", + "services": "Қызметтер", + "portfolio": "Портфолио", + "contact": "Байланыс", + "calculator": "Калькулятор", + "admin": "Админ", + "home - SmartSolTech": "navigation.home - SmartSolTech" + }, + "hero": { + "title": { + "smart": "hero.title.smart", + "solutions": "hero.title.solutions" + }, + "subtitle": "Шешімдер", + "description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз", + "cta_primary": "Жобаны бастау", + "cta_secondary": "Портфолионы көру", + "cta": { + "start": "hero.cta.start", + "portfolio": "hero.cta.portfolio" + } + }, + "services": { + "title": { + "our": "services.title.our", + "services": "services.title.services" + }, + "title_highlight": "Қызметтер", + "description": "Заманауи технология және шығармашылық идеялармен жасалған цифрлық шешімдер", + "web_development": { + "title": "Веб-әзірлеу", + "description": "Заманауи және бейімделгіш веб-сайттар мен веб-қосымшаларды әзірлеу", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильді қосымшалар", + "description": "iOS және Android үшін нативті және кросс-платформалық қосымшалар", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Пайдаланушы-орталықты интуитивті және әдемі интерфейс дизайны", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифрлық маркетинг", + "description": "SEO, әлеуметтік медиа, онлайн жарнама арқылы цифрлық маркетинг", + "price": "$2,000~" + }, + "view_all": "Барлық қызметтерді көру", + "subtitle": "services.subtitle", + "web": { + "title": "services.web.title", + "description": "services.web.description", + "price": "services.web.price" + }, + "mobile": { + "title": "services.mobile.title", + "description": "services.mobile.description", + "price": "services.mobile.price" + }, + "design": { + "title": "services.design.title", + "description": "services.design.description", + "price": "services.design.price" + }, + "marketing": { + "title": "services.marketing.title", + "description": "services.marketing.description", + "price": "services.marketing.price" + } + }, + "portfolio": { + "title": { + "recent": "portfolio.title.recent", + "projects": "portfolio.title.projects" + }, + "title_highlight": "Жобалар", + "description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз", + "view_details": "Толығырақ", + "view_all": "Барлық портфолионы көру", + "subtitle": "portfolio.subtitle" + }, + "calculator": { + "title": "Жоба Құнының Калькуляторы", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта дәл бағаны алыңыз", + "meta": { + "title": "Жоба құнының калькуляторы", + "description": "Веб-әзірлеу, мобильді қосымша немесе дизайн жобасының құнын біздің интерактивті калькулятормен есептеңіз" + }, + "cta": { + "title": "Жобаның бағасын тексеріңіз", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептейміз", + "button": "Құн калькуляторын пайдалану" + }, + "step1": { + "title": "1-қадам: Қызмет таңдау", + "subtitle": "Қажетті қызметтерді таңдаңыз (бірнеше таңдауға болады)" + }, + "step2": { + "title": "2-қадам: Жоба мәліметтері", + "subtitle": "Жобаның күрделілігі мен мерзімін таңдаңыз" + }, + "complexity": { + "title": "Жобаның күрделілігі", + "simple": "Қарапайым", + "simple_desc": "Негізгі функциялар, стандартты дизайн", + "medium": "Орташа", + "medium_desc": "Қосымша функциялар, жеке дизайн", + "complex": "Күрделі", + "complex_desc": "Кеңейтілген функциялар, күрделі интеграциялар" + }, + "timeline": { + "title": "Әзірлеу мерзімі", + "standard": "Стандартты", + "standard_desc": "Қалыпты әзірлеу мерзімі", + "rush": "Асығыс", + "rush_desc": "Жылдам әзірлеу (+50%)", + "extended": "Кеңейтілген", + "extended_desc": "Икемді әзірлеу мерзімі (-20%)" + }, + "result": { + "title": "Есептеу нәтижесі", + "subtitle": "Міне, сіздің алдын ала жоба құнының бағасы", + "estimated_price": "Алдын ала баға", + "price_note": "* Соңғы құн жоба мәліметтеріне байланысты өзгеруі мүмкін", + "summary": "Жоба қорытындысы", + "selected_services": "Таңдалған қызметтер", + "complexity": "Күрделілік", + "timeline": "Мерзім", + "get_quote": "Дәл ұсыныс алу", + "recalculate": "Қайта есептеу", + "contact_note": "Дәл ұсыныс алу және жоба мәліметтерін талқылау үшін бізбен байланысыңыз" + }, + "next_step": "Келесі қадам", + "prev_step": "Артқа", + "calculate": "Есептеу" + }, + "contact": { + "ready_title": "Жобаңызды бастауға дайынсыз ба?", + "ready_description": "Идеяларыңызды шындыққа айналдырыңыз. Сарапшылар ең жақсы шешімдерді ұсынады.", + "phone_consultation": "Телефон кеңесі", + "email_inquiry": "Электрондық пошта сұрауы", + "telegram_chat": "Telegram чаты", + "instant_response": "Лезде жауап беру мүмкін", + "free_consultation": "Тегін кеңес беру өтініші", + "form": { + "name": "Аты", + "email": "Электрондық пошта", + "phone": "Телефон", + "service_interest": "Қызығатын қызмет", + "service_options": { + "select": "Қызығатын қызметті таңдаңыз", + "web_development": "Веб-әзірлеу", + "mobile_app": "Мобильді қосымша", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Кеңес беру", + "other": "Басқа" + }, + "message": "Жобаңыз туралы қысқаша сипаттаңыз", + "submit": "Кеңес беру үшін өтініш беру", + "title": "contact.form.title", + "service": { + "select": "contact.form.service.select", + "web": "contact.form.service.web", + "mobile": "contact.form.service.mobile", + "design": "contact.form.service.design", + "branding": "contact.form.service.branding", + "consulting": "contact.form.service.consulting", + "other": "contact.form.service.other" + }, + "success": "contact.form.success", + "error": "contact.form.error" + }, + "cta": { + "ready": "contact.cta.ready", + "start": "contact.cta.start", + "question": "contact.cta.question", + "subtitle": "contact.cta.subtitle" + }, + "phone": { + "title": "contact.phone.title", + "number": "contact.phone.number" + }, + "email": { + "title": "contact.email.title", + "address": "contact.email.address" + }, + "telegram": { + "title": "contact.telegram.title", + "subtitle": "contact.telegram.subtitle" + } + }, + "about": { + "hero_title": "Туралы", + "hero_highlight": "SmartSolTech", + "hero_description": "Инновациялық технологиямен тұтынушылардың табысына жетелейтін цифрлық шешімдер маманы", + "overview": { + "title": "Инновация мен шығармашылықпен болашақты құру", + "description_1": "SmartSolTech - 2020 жылы құрылған цифрлық шешімдер маманы, веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн салаларында инновациялық технология мен шығармашылық идеялар негізінде тұтынушылардың бизнес табысын қолдайды.", + "description_2": "Біз жай ғана технологияны ұсынып қана қоймаймыз, тұтынушылардың мақсаттарын түсініп, оларға сәйкес оңтайлы шешімдерді ұсынып, бірге өсетін серіктестер болуды мақсат етеміз.", + "stats": { + "projects": "100+", + "projects_label": "Аяқталған жобалар", + "clients": "50+", + "clients_label": "Қанағаттанған тұтынушылар", + "experience": "4 жыл", + "experience_label": "Саладағы тәжірибе" + }, + "mission": "Біздің миссия", + "mission_text": "Технология арқылы барлық бизнестердің цифрлық дәуірде табысқа жетуіне көмектесу", + "vision": "Біздің көзқарас", + "vision_text": "Қазақстанды білдіретін жаһандық цифрлық шешімдер компаниясы ретінде өсіп, бүкіл әлемдегі тұтынушылардың цифрлық инновацияларын басқару" + }, + "values": { + "title": "Негізгі", + "title_highlight": "Құндылықтар", + "description": "SmartSolTech ұстанатын негізгі құндылықтар", + "innovation": { + "title": "Инновация", + "description": "Үздіксіз зерттеу-әзірлеу және заманауи технологияларды енгізу арқылы инновациялық шешімдерді ұсынамыз." + }, + "collaboration": { + "title": "Ынтымақтастық", + "description": "Тұтынушылармен тығыз қарым-қатынас пен ынтымақтастық арқылы ең жақсы нәтижелерді жасаймыз." + }, + "quality": { + "title": "Сапа", + "description": "Жоғары сапа стандарттарын сақтап, тұтынушылар қанағаттана алатын жоғары сапалы өнімдерді ұсынамыз." + }, + "growth": { + "title": "Өсу", + "description": "Тұтынушылармен бірге өсіп, үздіксіз оқу мен дамуды мақсат етеміз." + } + }, + "team": { + "title": "Біздің", + "title_highlight": "Команда", + "description": "Сараптама мен құлшынысты SmartSolTech командасын таныстырамыз" + }, + "tech_stack": { + "title": "Технологиялық", + "title_highlight": "Стек", + "description": "Заманауи технология мен дәлелденген құралдармен ең жақсы шешімдерді ұсынамыз", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильді" + }, + "cta": { + "title": "Бірге табысқа жететін серіктес болыңыз", + "description": "SmartSolTech-пен бизнесіңізді келесі деңгейге дамытыңыз", + "partnership": "Серіктестік сұрауы", + "portfolio": "Портфолионы көру" + } + }, + "footer": { + "company": { + "description": "footer.company.description" + }, + "description": "Инновацияны басқаратын цифрлық шешімдер маманы", + "quick_links": "Жылдам сілтемелер", + "services": "Қызметтер", + "contact_info": "Байланыс ақпараты", + "follow_us": "Бізді іздеңіз", + "rights": "Барлық құқықтар сақталған.", + "links": { + "title": "footer.links.title" + }, + "contact": { + "title": "footer.contact.title", + "email": "footer.contact.email", + "phone": "footer.contact.phone", + "address": "footer.contact.address" + }, + "copyright": "footer.copyright", + "privacy": "footer.privacy", + "terms": "footer.terms" + }, + "theme": { + "light": "Ашық тема", + "dark": "Қараңғы тема", + "toggle": "Теманы ауыстыру" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша", + "ko": "language.ko" + }, + "common": { + "loading": "Жүктелуде...", + "error": "Қате орын алды", + "success": "Сәтті", + "view_more": "Көбірек көру", + "back": "Артқа", + "next": "Келесі", + "previous": "Алдыңғы", + "view_details": "common.view_details" + }, + "undefined - SmartSolTech": "undefined - SmartSolTech", + "meta": { + "description": "meta.description", + "keywords": "meta.keywords", + "title": "meta.title" + }, + "nav": { + "home": "nav.home", + "about": "nav.about", + "services": "nav.services", + "portfolio": "nav.portfolio", + "calculator": "nav.calculator" + } +} \ No newline at end of file diff --git a/locales/ko.json b/locales/ko.json new file mode 100644 index 0000000..2d12a27 --- /dev/null +++ b/locales/ko.json @@ -0,0 +1,196 @@ +{ + "undefined - SmartSolTech": "undefined - SmartSolTech", + "meta": { + "description": "meta.description", + "keywords": "meta.keywords", + "title": "meta.title" + }, + "navigation": { + "home": "navigation.home", + "about": "navigation.about", + "services": "navigation.services", + "portfolio": "navigation.portfolio", + "calculator": "navigation.calculator", + "contact": "navigation.contact", + "home - SmartSolTech": "navigation.home - SmartSolTech" + }, + "language": { + "ko": "language.ko", + "korean": "language.korean", + "english": "language.english", + "russian": "language.russian", + "kazakh": "language.kazakh" + }, + "theme": { + "toggle": "theme.toggle" + }, + "hero": { + "cta_primary": "hero.cta_primary", + "title": { + "smart": "hero.title.smart", + "solutions": "hero.title.solutions" + }, + "subtitle": "hero.subtitle", + "cta": { + "start": "hero.cta.start", + "portfolio": "hero.cta.portfolio" + } + }, + "services": { + "title": { + "our": "services.title.our", + "services": "services.title.services" + }, + "subtitle": "services.subtitle", + "web": { + "title": "services.web.title", + "description": "services.web.description", + "price": "services.web.price" + }, + "mobile": { + "title": "services.mobile.title", + "description": "services.mobile.description", + "price": "services.mobile.price" + }, + "design": { + "title": "services.design.title", + "description": "services.design.description", + "price": "services.design.price" + }, + "marketing": { + "title": "services.marketing.title", + "description": "services.marketing.description", + "price": "services.marketing.price" + }, + "view_all": "services.view_all" + }, + "portfolio": { + "title": { + "recent": "portfolio.title.recent", + "projects": "portfolio.title.projects" + }, + "subtitle": "portfolio.subtitle", + "view_all": "portfolio.view_all" + }, + "common": { + "view_details": "common.view_details" + }, + "calculator": { + "cta": { + "title": "calculator.cta.title", + "subtitle": "calculator.cta.subtitle", + "button": "calculator.cta.button" + }, + "meta": { + "title": "calculator.meta.title", + "description": "calculator.meta.description" + }, + "title": "calculator.title", + "subtitle": "calculator.subtitle", + "step1": { + "title": "calculator.step1.title", + "subtitle": "calculator.step1.subtitle" + }, + "next_step": "calculator.next_step", + "step2": { + "title": "calculator.step2.title", + "subtitle": "calculator.step2.subtitle" + }, + "complexity": { + "title": "calculator.complexity.title", + "simple": "calculator.complexity.simple", + "simple_desc": "calculator.complexity.simple_desc", + "medium": "calculator.complexity.medium", + "medium_desc": "calculator.complexity.medium_desc", + "complex": "calculator.complexity.complex", + "complex_desc": "calculator.complexity.complex_desc" + }, + "timeline": { + "title": "calculator.timeline.title", + "standard": "calculator.timeline.standard", + "standard_desc": "calculator.timeline.standard_desc", + "rush": "calculator.timeline.rush", + "rush_desc": "calculator.timeline.rush_desc", + "extended": "calculator.timeline.extended", + "extended_desc": "calculator.timeline.extended_desc" + }, + "prev_step": "calculator.prev_step", + "calculate": "calculator.calculate", + "result": { + "title": "calculator.result.title", + "subtitle": "calculator.result.subtitle", + "estimated_price": "calculator.result.estimated_price", + "price_note": "calculator.result.price_note", + "summary": "calculator.result.summary", + "get_quote": "calculator.result.get_quote", + "recalculate": "calculator.result.recalculate", + "contact_note": "calculator.result.contact_note", + "selected_services": "선택된 서비스", + "complexity": "복잡도", + "timeline": "개발 기간" + } + }, + "contact": { + "cta": { + "ready": "contact.cta.ready", + "start": "contact.cta.start", + "question": "contact.cta.question", + "subtitle": "contact.cta.subtitle" + }, + "phone": { + "title": "contact.phone.title", + "number": "contact.phone.number" + }, + "email": { + "title": "contact.email.title", + "address": "contact.email.address" + }, + "telegram": { + "title": "contact.telegram.title", + "subtitle": "contact.telegram.subtitle" + }, + "form": { + "title": "contact.form.title", + "name": "contact.form.name", + "email": "contact.form.email", + "phone": "contact.form.phone", + "service": { + "select": "contact.form.service.select", + "web": "contact.form.service.web", + "mobile": "contact.form.service.mobile", + "design": "contact.form.service.design", + "branding": "contact.form.service.branding", + "consulting": "contact.form.service.consulting", + "other": "contact.form.service.other" + }, + "message": "contact.form.message", + "submit": "contact.form.submit", + "success": "contact.form.success", + "error": "contact.form.error" + } + }, + "footer": { + "company": { + "description": "footer.company.description" + }, + "links": { + "title": "footer.links.title" + }, + "contact": { + "title": "footer.contact.title", + "email": "footer.contact.email", + "phone": "footer.contact.phone", + "address": "footer.contact.address" + }, + "copyright": "footer.copyright", + "privacy": "footer.privacy", + "terms": "footer.terms" + }, + "nav": { + "home": "nav.home", + "about": "nav.about", + "services": "nav.services", + "portfolio": "nav.portfolio", + "calculator": "nav.calculator" + } +} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json new file mode 100644 index 0000000..0cdb277 --- /dev/null +++ b/locales/ru.json @@ -0,0 +1,318 @@ +{ + "navigation": { + "home": "Главная", + "about": "О нас", + "services": "Услуги", + "portfolio": "Портфолио", + "contact": "Контакты", + "calculator": "Калькулятор", + "admin": "Админ", + "home - SmartSolTech": "navigation.home - SmartSolTech" + }, + "hero": { + "title": { + "smart": "hero.title.smart", + "solutions": "hero.title.solutions" + }, + "subtitle": "Решения", + "description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса", + "cta_primary": "Начать проект", + "cta_secondary": "Посмотреть портфолио", + "cta": { + "start": "hero.cta.start", + "portfolio": "hero.cta.portfolio" + } + }, + "services": { + "title": { + "our": "services.title.our", + "services": "services.title.services" + }, + "title_highlight": "Услуги", + "description": "Цифровые решения с использованием передовых технологий и творческих идей", + "web_development": { + "title": "Веб-разработка", + "description": "Современные и адаптивные веб-сайты и веб-приложения", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильные приложения", + "description": "Нативные и кроссплатформенные приложения для iOS и Android", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Ориентированный на пользователя интуитивный и красивый дизайн интерфейса", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифровой маркетинг", + "description": "Цифровой маркетинг через SEO, социальные сети, онлайн-рекламу", + "price": "$2,000~" + }, + "view_all": "Посмотреть все услуги", + "subtitle": "services.subtitle", + "web": { + "title": "services.web.title", + "description": "services.web.description", + "price": "services.web.price" + }, + "mobile": { + "title": "services.mobile.title", + "description": "services.mobile.description", + "price": "services.mobile.price" + }, + "design": { + "title": "services.design.title", + "description": "services.design.description", + "price": "services.design.price" + }, + "marketing": { + "title": "services.marketing.title", + "description": "services.marketing.description", + "price": "services.marketing.price" + } + }, + "portfolio": { + "title": { + "recent": "portfolio.title.recent", + "projects": "portfolio.title.projects" + }, + "title_highlight": "Проекты", + "description": "Ознакомьтесь с проектами, выполненными для успеха клиентов", + "view_details": "Подробнее", + "view_all": "Посмотреть все портфолио", + "subtitle": "portfolio.subtitle" + }, + "calculator": { + "title": "Калькулятор Стоимости Проекта", + "subtitle": "Выберите нужные услуги и требования для получения точной оценки стоимости в режиме реального времени", + "meta": { + "title": "Калькулятор стоимости проекта", + "description": "Рассчитайте стоимость вашего проекта веб-разработки, мобильного приложения или дизайна с помощью нашего интерактивного калькулятора" + }, + "cta": { + "title": "Узнайте стоимость вашего проекта", + "subtitle": "Выберите необходимые услуги и требования, и мы рассчитаем стоимость в режиме реального времени", + "button": "Использовать калькулятор стоимости" + }, + "step1": { + "title": "Шаг 1: Выбор услуг", + "subtitle": "Выберите необходимые услуги (можно выбрать несколько)" + }, + "step2": { + "title": "Шаг 2: Детали проекта", + "subtitle": "Выберите сложность проекта и сроки" + }, + "complexity": { + "title": "Сложность проекта", + "simple": "Простой", + "simple_desc": "Базовый функционал, стандартный дизайн", + "medium": "Средний", + "medium_desc": "Дополнительные функции, кастомный дизайн", + "complex": "Сложный", + "complex_desc": "Расширенный функционал, интеграции" + }, + "timeline": { + "title": "Временные рамки", + "standard": "Стандартные", + "standard_desc": "Обычные сроки разработки", + "rush": "Срочно", + "rush_desc": "Ускоренная разработка (+50%)", + "extended": "Расширенные", + "extended_desc": "Длительная разработка (-20%)" + }, + "result": { + "title": "Результат расчета", + "subtitle": "Вот ваша предварительная оценка стоимости проекта", + "estimated_price": "Предварительная стоимость", + "price_note": "* Окончательная стоимость может варьироваться в зависимости от деталей проекта", + "summary": "Сводка проекта", + "selected_services": "Выбранные услуги", + "complexity": "Сложность", + "timeline": "Временные рамки", + "get_quote": "Получить точное предложение", + "recalculate": "Пересчитать", + "contact_note": "Свяжитесь с нами для получения точного предложения и обсуждения деталей проекта" + }, + "next_step": "Следующий шаг", + "prev_step": "Назад", + "calculate": "Рассчитать" + }, + "contact": { + "ready_title": "Готовы начать свой проект?", + "ready_description": "Превратите свои идеи в реальность. Эксперты предоставят лучшие решения.", + "phone_consultation": "Телефонная консультация", + "email_inquiry": "Запрос по электронной почте", + "telegram_chat": "Чат в Telegram", + "instant_response": "Мгновенный ответ доступен", + "free_consultation": "Заявка на бесплатную консультацию", + "form": { + "name": "Имя", + "email": "Электронная почта", + "phone": "Телефон", + "service_interest": "Интересующая услуга", + "service_options": { + "select": "Выберите интересующую услугу", + "web_development": "Веб-разработка", + "mobile_app": "Мобильное приложение", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Консалтинг", + "other": "Другое" + }, + "message": "Кратко опишите ваш проект", + "submit": "Подать заявку на консультацию", + "title": "contact.form.title", + "service": { + "select": "contact.form.service.select", + "web": "contact.form.service.web", + "mobile": "contact.form.service.mobile", + "design": "contact.form.service.design", + "branding": "contact.form.service.branding", + "consulting": "contact.form.service.consulting", + "other": "contact.form.service.other" + }, + "success": "contact.form.success", + "error": "contact.form.error" + }, + "cta": { + "ready": "contact.cta.ready", + "start": "contact.cta.start", + "question": "contact.cta.question", + "subtitle": "contact.cta.subtitle" + }, + "phone": { + "title": "contact.phone.title", + "number": "contact.phone.number" + }, + "email": { + "title": "contact.email.title", + "address": "contact.email.address" + }, + "telegram": { + "title": "contact.telegram.title", + "subtitle": "contact.telegram.subtitle" + } + }, + "about": { + "hero_title": "О", + "hero_highlight": "SmartSolTech", + "hero_description": "Специалист по цифровым решениям, ведущий к успеху клиентов с помощью инновационных технологий", + "overview": { + "title": "Создавая будущее с инновациями и креативностью", + "description_1": "SmartSolTech - это специалист по цифровым решениям, основанный в 2020 году, поддерживающий успех клиентского бизнеса с помощью инновационных технологий и творческих идей в области веб-разработки, мобильных приложений и UI/UX дизайна.", + "description_2": "Мы не просто предоставляем технологии, но понимаем цели клиентов и предлагаем оптимальные решения, чтобы стать партнерами, растущими вместе.", + "stats": { + "projects": "100+", + "projects_label": "Завершенные проекты", + "clients": "50+", + "clients_label": "Довольные клиенты", + "experience": "4 года", + "experience_label": "Опыт в отрасли" + }, + "mission": "Наша миссия", + "mission_text": "Помощь всем предприятиям в достижении успеха в цифровую эпоху с помощью технологий", + "vision": "Наше видение", + "vision_text": "Рост как глобальной компании цифровых решений, представляющей Корею, для ведения цифровых инноваций для клиентов по всему миру" + }, + "values": { + "title": "Основные", + "title_highlight": "Ценности", + "description": "Основные ценности, которых придерживается SmartSolTech", + "innovation": { + "title": "Инновации", + "description": "Мы предоставляем инновационные решения через непрерывные исследования и внедрение передовых технологий." + }, + "collaboration": { + "title": "Сотрудничество", + "description": "Мы создаем лучшие результаты через тесное общение и сотрудничество с клиентами." + }, + "quality": { + "title": "Качество", + "description": "Мы поддерживаем высокие стандарты качества и предоставляем высококачественные продукты, которыми клиенты могут быть довольны." + }, + "growth": { + "title": "Рост", + "description": "Мы растем вместе с клиентами и стремимся к непрерывному обучению и развитию." + } + }, + "team": { + "title": "Наша", + "title_highlight": "Команда", + "description": "Представляем команду SmartSolTech с экспертизой и страстью" + }, + "tech_stack": { + "title": "Технологический", + "title_highlight": "Стек", + "description": "Мы предоставляем лучшие решения с передовыми технологиями и проверенными инструментами", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильные" + }, + "cta": { + "title": "Станьте партнером для совместного успеха", + "description": "Выведите свой бизнес на следующий уровень с SmartSolTech", + "partnership": "Запрос о партнерстве", + "portfolio": "Посмотреть портфолио" + } + }, + "footer": { + "company": { + "description": "footer.company.description" + }, + "description": "Специалист по цифровым решениям, ведущий инновации", + "quick_links": "Быстрые ссылки", + "services": "Услуги", + "contact_info": "Контактная информация", + "follow_us": "Подписывайтесь", + "rights": "Все права защищены.", + "links": { + "title": "footer.links.title" + }, + "contact": { + "title": "footer.contact.title", + "email": "footer.contact.email", + "phone": "footer.contact.phone", + "address": "footer.contact.address" + }, + "copyright": "footer.copyright", + "privacy": "footer.privacy", + "terms": "footer.terms" + }, + "theme": { + "light": "Светлая тема", + "dark": "Темная тема", + "toggle": "Переключить тему" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша", + "ko": "language.ko" + }, + "common": { + "loading": "Загрузка...", + "error": "Произошла ошибка", + "success": "Успешно", + "view_more": "Посмотреть еще", + "back": "Назад", + "next": "Далее", + "previous": "Предыдущий", + "view_details": "common.view_details" + }, + "undefined - SmartSolTech": "undefined - SmartSolTech", + "meta": { + "description": "meta.description", + "keywords": "meta.keywords", + "title": "meta.title" + }, + "nav": { + "home": "nav.home", + "about": "nav.about", + "services": "nav.services", + "portfolio": "nav.portfolio", + "calculator": "nav.calculator" + } +} \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..e1aa95c --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,154 @@ +const jwt = require('jsonwebtoken'); +const User = require('../models/User'); + +/** + * Authentication middleware + * Verifies JWT token and attaches user to request + */ +const authenticateToken = async (req, res, next) => { + try { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + return res.status(401).json({ + success: false, + message: 'Access token required' + }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.userId).select('-password'); + + if (!user || !user.isActive) { + return res.status(401).json({ + success: false, + message: 'Invalid or inactive user' + }); + } + + req.user = user; + next(); + } catch (error) { + console.error('Token verification error:', error); + return res.status(403).json({ + success: false, + message: 'Invalid token' + }); + } +}; + +/** + * Session-based authentication middleware + * For web pages using sessions + */ +const authenticateSession = async (req, res, next) => { + try { + if (!req.session.userId) { + req.flash('error', '로그인이 필요합니다.'); + return res.redirect('/auth/login'); + } + + const user = await User.findById(req.session.userId).select('-password'); + + if (!user || !user.isActive) { + req.session.destroy(); + req.flash('error', '유효하지 않은 사용자입니다.'); + return res.redirect('/auth/login'); + } + + req.user = user; + res.locals.user = user; + next(); + } catch (error) { + console.error('Session authentication error:', error); + req.session.destroy(); + req.flash('error', '인증 오류가 발생했습니다.'); + return res.redirect('/auth/login'); + } +}; + +/** + * Admin role middleware + * Requires user to be authenticated and have admin role + */ +const requireAdmin = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + success: false, + message: 'Authentication required' + }); + } + + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + message: 'Admin access required' + }); + } + + next(); +}; + +/** + * Admin session middleware for web pages + */ +const requireAdminSession = (req, res, next) => { + if (!req.user) { + req.flash('error', '로그인이 필요합니다.'); + return res.redirect('/auth/login'); + } + + if (req.user.role !== 'admin') { + req.flash('error', '관리자 권한이 필요합니다.'); + return res.redirect('/'); + } + + next(); +}; + +/** + * Optional authentication middleware + * Attaches user if token exists but doesn't require it + */ +const optionalAuth = async (req, res, next) => { + try { + // Check session first + if (req.session.userId) { + const user = await User.findById(req.session.userId).select('-password'); + if (user && user.isActive) { + req.user = user; + res.locals.user = user; + } + } + + // Check JWT token if no session + if (!req.user) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (token) { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.userId).select('-password'); + + if (user && user.isActive) { + req.user = user; + res.locals.user = user; + } + } + } + + next(); + } catch (error) { + // Continue without authentication if token is invalid + next(); + } +}; + +module.exports = { + authenticateToken, + authenticateSession, + requireAdmin, + requireAdminSession, + optionalAuth +}; \ No newline at end of file diff --git a/middleware/validation.js b/middleware/validation.js new file mode 100644 index 0000000..b19ca74 --- /dev/null +++ b/middleware/validation.js @@ -0,0 +1,310 @@ +/** + * Validation middleware for various data types + */ + +const { body, validationResult } = require('express-validator'); + +/** + * Validation error handler + */ +const handleValidationErrors = (req, res, next) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + // For API requests + if (req.xhr || req.headers.accept?.includes('application/json')) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: errors.array() + }); + } + + // For web requests + const errorMessages = errors.array().map(error => error.msg); + req.flash('error', errorMessages); + return res.redirect('back'); + } + + next(); +}; + +/** + * Contact form validation + */ +const validateContactForm = [ + body('name') + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.') + .matches(/^[a-zA-Z가-힣\s]+$/) + .withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'), + + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + body('company') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('회사명은 100자 이하여야 합니다.'), + + body('service') + .isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'digital-marketing', 'consulting', 'other']) + .withMessage('유효한 서비스를 선택해주세요.'), + + body('budget') + .optional() + .isIn(['under-500', '500-1000', '1000-3000', '3000-5000', '5000-10000', 'over-10000', 'discuss']) + .withMessage('유효한 예산 범위를 선택해주세요.'), + + body('message') + .trim() + .isLength({ min: 10, max: 2000 }) + .withMessage('메시지는 10-2000자 사이여야 합니다.'), + + handleValidationErrors +]; + +/** + * User registration validation + */ +const validateRegistration = [ + body('name') + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.') + .matches(/^[a-zA-Z가-힣\s]+$/) + .withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'), + + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('password') + .isLength({ min: 8 }) + .withMessage('비밀번호는 최소 8자 이상이어야 합니다.') + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) + .withMessage('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'), + + body('confirmPassword') + .custom((value, { req }) => { + if (value !== req.body.password) { + throw new Error('비밀번호 확인이 일치하지 않습니다.'); + } + return true; + }), + + handleValidationErrors +]; + +/** + * User login validation + */ +const validateLogin = [ + body('email') + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.') + .normalizeEmail(), + + body('password') + .isLength({ min: 1 }) + .withMessage('비밀번호를 입력해주세요.'), + + handleValidationErrors +]; + +/** + * Portfolio validation + */ +const validatePortfolio = [ + body('title') + .trim() + .isLength({ min: 2, max: 100 }) + .withMessage('제목은 2-100자 사이여야 합니다.'), + + body('description') + .trim() + .isLength({ min: 10, max: 5000 }) + .withMessage('설명은 10-5000자 사이여야 합니다.'), + + body('shortDescription') + .optional() + .trim() + .isLength({ max: 200 }) + .withMessage('간단한 설명은 200자 이하여야 합니다.'), + + body('category') + .isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'marketing']) + .withMessage('유효한 카테고리를 선택해주세요.'), + + body('technologies') + .optional() + .isArray() + .withMessage('기술 스택은 배열이어야 합니다.'), + + body('clientName') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('클라이언트 이름은 100자 이하여야 합니다.'), + + body('projectUrl') + .optional() + .isURL() + .withMessage('유효한 URL을 입력해주세요.'), + + body('status') + .optional() + .isIn(['planning', 'in-progress', 'completed', 'on-hold']) + .withMessage('유효한 상태를 선택해주세요.'), + + handleValidationErrors +]; + +/** + * Service validation + */ +const validateService = [ + body('name') + .trim() + .isLength({ min: 2, max: 100 }) + .withMessage('서비스명은 2-100자 사이여야 합니다.'), + + body('description') + .trim() + .isLength({ min: 10, max: 5000 }) + .withMessage('설명은 10-5000자 사이여야 합니다.'), + + body('shortDescription') + .optional() + .trim() + .isLength({ max: 200 }) + .withMessage('간단한 설명은 200자 이하여야 합니다.'), + + body('category') + .isIn(['development', 'design', 'marketing', 'consulting']) + .withMessage('유효한 카테고리를 선택해주세요.'), + + body('pricing.basePrice') + .optional() + .isNumeric() + .withMessage('기본 가격은 숫자여야 합니다.'), + + body('pricing.priceType') + .optional() + .isIn(['project', 'hourly', 'monthly']) + .withMessage('유효한 가격 유형을 선택해주세요.'), + + handleValidationErrors +]; + +/** + * Calculator validation + */ +const validateCalculator = [ + body('service') + .isMongoId() + .withMessage('유효한 서비스를 선택해주세요.'), + + body('projectType') + .optional() + .isIn(['simple', 'medium', 'complex', 'enterprise']) + .withMessage('유효한 프로젝트 유형을 선택해주세요.'), + + body('timeline') + .optional() + .isIn(['urgent', 'normal', 'flexible']) + .withMessage('유효한 타임라인을 선택해주세요.'), + + body('features') + .optional() + .isArray() + .withMessage('기능은 배열이어야 합니다.'), + + body('contactInfo.name') + .optional() + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage('이름은 2-50자 사이여야 합니다.'), + + body('contactInfo.email') + .optional() + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.'), + + body('contactInfo.phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + handleValidationErrors +]; + +/** + * Settings validation + */ +const validateSettings = [ + body('siteName') + .optional() + .trim() + .isLength({ min: 1, max: 100 }) + .withMessage('사이트명은 1-100자 사이여야 합니다.'), + + body('siteDescription') + .optional() + .trim() + .isLength({ max: 500 }) + .withMessage('사이트 설명은 500자 이하여야 합니다.'), + + body('contact.email') + .optional() + .isEmail() + .withMessage('유효한 이메일 주소를 입력해주세요.'), + + body('contact.phone') + .optional() + .matches(/^[0-9\-\+\(\)\s]+$/) + .withMessage('유효한 전화번호를 입력해주세요.'), + + body('social.facebook') + .optional() + .isURL() + .withMessage('유효한 Facebook URL을 입력해주세요.'), + + body('social.twitter') + .optional() + .isURL() + .withMessage('유효한 Twitter URL을 입력해주세요.'), + + body('social.linkedin') + .optional() + .isURL() + .withMessage('유효한 LinkedIn URL을 입력해주세요.'), + + body('social.instagram') + .optional() + .isURL() + .withMessage('유효한 Instagram URL을 입력해주세요.'), + + handleValidationErrors +]; + +module.exports = { + handleValidationErrors, + validateContactForm, + validateRegistration, + validateLogin, + validatePortfolio, + validateService, + validateCalculator, + validateSettings +}; \ No newline at end of file diff --git a/models/Contact.js b/models/Contact.js new file mode 100644 index 0000000..c7a79ff --- /dev/null +++ b/models/Contact.js @@ -0,0 +1,80 @@ +const mongoose = require('mongoose'); + +const contactSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + email: { + type: String, + required: true, + lowercase: true, + trim: true + }, + phone: { + type: String, + trim: true + }, + company: { + type: String, + trim: true + }, + subject: { + type: String, + required: true, + trim: true + }, + message: { + type: String, + required: true + }, + serviceInterest: { + type: String, + enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'consulting', 'other'] + }, + budget: { + type: String, + enum: ['under-1m', '1m-5m', '5m-10m', '10m-20m', '20m-50m', 'over-50m'] + }, + timeline: { + type: String, + enum: ['asap', '1-month', '1-3-months', '3-6-months', 'flexible'] + }, + status: { + type: String, + enum: ['new', 'in-progress', 'replied', 'closed'], + default: 'new' + }, + priority: { + type: String, + enum: ['low', 'medium', 'high', 'urgent'], + default: 'medium' + }, + source: { + type: String, + enum: ['website', 'telegram', 'email', 'phone', 'referral'], + default: 'website' + }, + isRead: { + type: Boolean, + default: false + }, + adminNotes: { + type: String + }, + ipAddress: { + type: String + }, + userAgent: { + type: String + } +}, { + timestamps: true +}); + +contactSchema.index({ status: 1, createdAt: -1 }); +contactSchema.index({ isRead: 1, createdAt: -1 }); +contactSchema.index({ email: 1 }); + +module.exports = mongoose.model('Contact', contactSchema); \ No newline at end of file diff --git a/models/Portfolio.js b/models/Portfolio.js new file mode 100644 index 0000000..d86985a --- /dev/null +++ b/models/Portfolio.js @@ -0,0 +1,107 @@ +const mongoose = require('mongoose'); + +const portfolioSchema = new mongoose.Schema({ + title: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: true + }, + shortDescription: { + type: String, + required: true, + maxlength: 200 + }, + category: { + type: String, + required: true, + enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'e-commerce', 'other'] + }, + technologies: [{ + type: String, + trim: true + }], + images: [{ + url: { + type: String, + required: true + }, + alt: { + type: String, + default: '' + }, + isPrimary: { + type: Boolean, + default: false + } + }], + clientName: { + type: String, + trim: true + }, + projectUrl: { + type: String, + trim: true + }, + githubUrl: { + type: String, + trim: true + }, + status: { + type: String, + enum: ['completed', 'in-progress', 'planning'], + default: 'completed' + }, + featured: { + type: Boolean, + default: false + }, + publishedAt: { + type: Date, + default: Date.now + }, + completedAt: { + type: Date + }, + isPublished: { + type: Boolean, + default: true + }, + viewCount: { + type: Number, + default: 0 + }, + likes: { + type: Number, + default: 0 + }, + order: { + type: Number, + default: 0 + }, + seo: { + metaTitle: String, + metaDescription: String, + keywords: [String] + } +}, { + timestamps: true +}); + +// Index for search and sorting +portfolioSchema.index({ title: 'text', description: 'text', technologies: 'text' }); +portfolioSchema.index({ category: 1, publishedAt: -1 }); +portfolioSchema.index({ featured: -1, publishedAt: -1 }); + +// Virtual for primary image +portfolioSchema.virtual('primaryImage').get(function() { + const primary = this.images.find(img => img.isPrimary); + return primary || (this.images.length > 0 ? this.images[0] : null); +}); + +portfolioSchema.set('toJSON', { virtuals: true }); + +module.exports = mongoose.model('Portfolio', portfolioSchema); \ No newline at end of file diff --git a/models/Service.js b/models/Service.js new file mode 100644 index 0000000..d8f37e6 --- /dev/null +++ b/models/Service.js @@ -0,0 +1,102 @@ +const mongoose = require('mongoose'); + +const serviceSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: true + }, + shortDescription: { + type: String, + required: true, + maxlength: 150 + }, + icon: { + type: String, + required: true + }, + category: { + type: String, + required: true, + enum: ['development', 'design', 'consulting', 'marketing', 'maintenance'] + }, + features: [{ + name: String, + description: String, + included: { + type: Boolean, + default: true + } + }], + pricing: { + basePrice: { + type: Number, + required: true, + min: 0 + }, + currency: { + type: String, + default: 'KRW' + }, + priceType: { + type: String, + enum: ['fixed', 'hourly', 'project'], + default: 'project' + }, + priceRange: { + min: Number, + max: Number + } + }, + estimatedTime: { + min: { + type: Number, + required: true + }, + max: { + type: Number, + required: true + }, + unit: { + type: String, + enum: ['hours', 'days', 'weeks', 'months'], + default: 'days' + } + }, + isActive: { + type: Boolean, + default: true + }, + featured: { + type: Boolean, + default: false + }, + order: { + type: Number, + default: 0 + }, + portfolio: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'Portfolio' + }], + tags: [{ + type: String, + trim: true + }], + seo: { + metaTitle: String, + metaDescription: String, + keywords: [String] + } +}, { + timestamps: true +}); + +serviceSchema.index({ name: 'text', description: 'text', tags: 'text' }); +serviceSchema.index({ category: 1, featured: -1, order: 1 }); + +module.exports = mongoose.model('Service', serviceSchema); \ No newline at end of file diff --git a/models/SiteSettings.js b/models/SiteSettings.js new file mode 100644 index 0000000..0156ce7 --- /dev/null +++ b/models/SiteSettings.js @@ -0,0 +1,116 @@ +const mongoose = require('mongoose'); + +const siteSettingsSchema = new mongoose.Schema({ + siteName: { + type: String, + default: 'SmartSolTech' + }, + siteDescription: { + type: String, + default: 'Innovative technology solutions for modern businesses' + }, + logo: { + type: String, + default: '/images/logo.png' + }, + favicon: { + type: String, + default: '/images/favicon.ico' + }, + contact: { + email: { + type: String, + default: 'info@smartsoltech.kr' + }, + phone: { + type: String, + default: '+82-10-0000-0000' + }, + address: { + type: String, + default: 'Seoul, South Korea' + } + }, + social: { + facebook: String, + twitter: String, + linkedin: String, + instagram: String, + github: String, + telegram: String + }, + telegram: { + botToken: String, + chatId: String, + isEnabled: { + type: Boolean, + default: false + } + }, + seo: { + metaTitle: { + type: String, + default: 'SmartSolTech - Technology Solutions' + }, + metaDescription: { + type: String, + default: 'Professional web development, mobile apps, and digital solutions in Korea' + }, + keywords: { + type: String, + default: 'web development, mobile apps, UI/UX design, Korea, technology' + }, + googleAnalytics: String, + googleTagManager: String + }, + hero: { + title: { + type: String, + default: 'Smart Technology Solutions' + }, + subtitle: { + type: String, + default: 'We create innovative digital experiences that drive business growth' + }, + backgroundImage: { + type: String, + default: '/images/hero-bg.jpg' + }, + ctaText: { + type: String, + default: 'Get Started' + }, + ctaLink: { + type: String, + default: '#contact' + } + }, + about: { + title: { + type: String, + default: 'About SmartSolTech' + }, + description: { + type: String, + default: 'We are a team of passionate developers and designers creating cutting-edge technology solutions.' + }, + image: { + type: String, + default: '/images/about.jpg' + } + }, + maintenance: { + isEnabled: { + type: Boolean, + default: false + }, + message: { + type: String, + default: 'We are currently performing maintenance. Please check back soon.' + } + } +}, { + timestamps: true +}); + +module.exports = mongoose.model('SiteSettings', siteSettingsSchema); \ No newline at end of file diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..27dc646 --- /dev/null +++ b/models/User.js @@ -0,0 +1,75 @@ +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); + +const userSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true + }, + password: { + type: String, + required: true, + minlength: 6 + }, + name: { + type: String, + required: true, + trim: true + }, + role: { + type: String, + enum: ['admin', 'moderator'], + default: 'admin' + }, + avatar: { + type: String, + default: null + }, + isActive: { + type: Boolean, + default: true + }, + lastLogin: { + type: Date, + default: null + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +// Hash password before saving +userSchema.pre('save', async function(next) { + if (!this.isModified('password')) return next(); + + try { + const salt = await bcrypt.genSalt(12); + this.password = await bcrypt.hash(this.password, salt); + next(); + } catch (error) { + next(error); + } +}); + +// Compare password method +userSchema.methods.comparePassword = async function(candidatePassword) { + return bcrypt.compare(candidatePassword, this.password); +}; + +// Update last login +userSchema.methods.updateLastLogin = function() { + this.lastLogin = new Date(); + return this.save(); +}; + +module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5b04506 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9294 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smartsoltech-website", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "connect-flash": "^0.1.1", + "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-session": "^1.17.3", + "express-validator": "^7.0.1", + "helmet": "^7.1.0", + "i18n": "^0.15.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.0.3", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "node-telegram-bot-api": "^0.64.0", + "nodemailer": "^6.9.7", + "sharp": "^0.33.0", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "css-loader": "^6.8.1", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "nodemon": "^3.0.2", + "style-loader": "^3.3.3", + "terser-webpack-plugin": "^5.3.9", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "workbox-webpack-plugin": "^7.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", + "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.14.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request-promise": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@cypress/request-promise/-/request-promise-5.0.0.tgz", + "integrity": "sha512-eKdYVpa9cBEw2kTBlHeu1PP16Blwtum6QHg/u9s/MoHkZfuo1pRGka1VlUHXF5kdew82BvOJVVGk0x8X0nbp+w==", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "@cypress/request": "^3.0.0" + } + }, + "node_modules/@cypress/request-promise/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@cypress/request-promise/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@messageformat/core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz", + "integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==", + "dependencies": { + "@messageformat/date-skeleton": "^1.0.0", + "@messageformat/number-skeleton": "^1.0.0", + "@messageformat/parser": "^5.1.0", + "@messageformat/runtime": "^3.0.1", + "make-plural": "^7.0.0", + "safe-identifier": "^0.4.1" + } + }, + "node_modules/@messageformat/date-skeleton": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz", + "integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==" + }, + "node_modules/@messageformat/number-skeleton": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", + "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==" + }, + "node_modules/@messageformat/parser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz", + "integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==", + "dependencies": { + "moo": "^0.5.1" + } + }, + "node_modules/@messageformat/runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", + "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", + "dependencies": { + "make-plural": "^7.0.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array.prototype.findindex": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findindex/-/array.prototype.findindex-2.2.4.tgz", + "integrity": "sha512-LLm4mhxa9v8j0A/RPnpQAP4svXToJFh+Hp1pNYl5ZD5qpB4zdx/D4YjpVcETkhFbUKWO3iGMVLvrOnnmkAJT6A==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", + "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/connect-flash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", + "integrity": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, + "node_modules/connect-mongo/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/connect-mongo/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-js-compat": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", + "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", + "dev": true, + "dependencies": { + "browserslist": "^4.26.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.19.0.tgz", + "integrity": "sha512-DoSM9VyG6O3vqBf+p3Gjgr/Q52HYBBtO3v+4koAxt1MnWr+zEnxE+nke/yXS4lt2P4SYCHQ4V3f1i88LQVOpAw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-printf": { + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz", + "integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==", + "engines": { + "node": ">=10.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "peer": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", + "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/i18n": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.15.2.tgz", + "integrity": "sha512-mdBxCfC651UL/hNizIQgB1NHwbBKjlrPcsoTzd/X8rNbJlS1FMF//TOyHEVFg9Dxo0RcrI2ZKt1AFTNe3Q40og==", + "dependencies": { + "@messageformat/core": "^3.4.0", + "debug": "^4.4.3", + "fast-printf": "^1.6.10", + "make-plural": "^7.4.0", + "math-interval-parser": "^2.0.1", + "mustache": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/mashpie" + } + }, + "node_modules/i18n/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/i18n/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kruptein": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.8.tgz", + "integrity": "sha512-0CyalFA0Cjp3jnziMp0u1uLZW2/ouhQ0mEMfYlroBXNe86na1RwAuwBcdRAegeWZNMfQy/G5fN47g/Axjtqrfw==", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">8" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-plural": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", + "integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==" + }, + "node_modules/math-interval-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz", + "integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", + "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", + "dev": true + }, + "node_modules/node-telegram-bot-api": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.64.0.tgz", + "integrity": "sha512-/gxCuaEDUyWMBiHInP0ufopUkaaKprXiv3lyP9MMZdPy2KPfYKNYNKfd1Ph7o9KhfURDtOYowPZCi4UCr+2caw==", + "dependencies": { + "@cypress/request": "^3.0.1", + "@cypress/request-promise": "^5.0.0", + "array.prototype.findindex": "^2.0.2", + "bl": "^1.2.3", + "debug": "^3.2.7", + "eventemitter3": "^3.0.0", + "file-type": "^3.9.0", + "mime": "^1.6.0", + "pump": "^2.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/node-telegram-bot-api/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/node-telegram-bot-api/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "peer": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "peer": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/request/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "peer": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "peer": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "peer": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/workbox-background-sync": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", + "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz", + "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-build": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.3.0.tgz", + "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==", + "dev": true, + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.3.0", + "workbox-broadcast-update": "7.3.0", + "workbox-cacheable-response": "7.3.0", + "workbox-core": "7.3.0", + "workbox-expiration": "7.3.0", + "workbox-google-analytics": "7.3.0", + "workbox-navigation-preload": "7.3.0", + "workbox-precaching": "7.3.0", + "workbox-range-requests": "7.3.0", + "workbox-recipes": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0", + "workbox-streams": "7.3.0", + "workbox-sw": "7.3.0", + "workbox-window": "7.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/workbox-build/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/workbox-build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/workbox-build/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/workbox-build/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz", + "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-core": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.3.0.tgz", + "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==", + "dev": true + }, + "node_modules/workbox-expiration": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.3.0.tgz", + "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz", + "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==", + "dev": true, + "dependencies": { + "workbox-background-sync": "7.3.0", + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz", + "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-precaching": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz", + "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz", + "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-recipes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.3.0.tgz", + "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==", + "dev": true, + "dependencies": { + "workbox-cacheable-response": "7.3.0", + "workbox-core": "7.3.0", + "workbox-expiration": "7.3.0", + "workbox-precaching": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" + } + }, + "node_modules/workbox-routing": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.3.0.tgz", + "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-strategies": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.3.0.tgz", + "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0" + } + }, + "node_modules/workbox-streams": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.3.0.tgz", + "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==", + "dev": true, + "dependencies": { + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0" + } + }, + "node_modules/workbox-sw": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.3.0.tgz", + "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==", + "dev": true + }, + "node_modules/workbox-webpack-plugin": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-7.3.0.tgz", + "integrity": "sha512-EC8lmSAuNmPli04+a5r5lTgv8ab+f5l+XjdYuYpbGnxDT15kH6DBeBazVslpffqTDHt+wkdBMnBCu8GdkKrTSA==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "7.3.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.91.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/workbox-window": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.3.0.tgz", + "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.3.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..de4abf4 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "smartsoltech-website", + "version": "1.0.0", + "description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "demo": "node server-demo.js", + "dev": "node scripts/dev.js", + "build": "node scripts/build.js", + "init-db": "node scripts/init-db.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "pwa", + "nodejs", + "express", + "telegram", + "portfolio", + "calculator" + ], + "author": "SmartSolTech", + "license": "MIT", + "dependencies": { + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "connect-flash": "^0.1.1", + "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-session": "^1.17.3", + "express-validator": "^7.0.1", + "helmet": "^7.1.0", + "i18n": "^0.15.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.0.3", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "node-telegram-bot-api": "^0.64.0", + "nodemailer": "^6.9.7", + "sharp": "^0.33.0", + "socket.io": "^4.7.4" + }, + "devDependencies": { + "css-loader": "^6.8.1", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.3", + "mini-css-extract-plugin": "^2.7.6", + "nodemon": "^3.0.2", + "style-loader": "^3.3.3", + "terser-webpack-plugin": "^5.3.9", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "workbox-webpack-plugin": "^7.0.0" + } +} diff --git a/public/css/custom.css b/public/css/custom.css new file mode 100644 index 0000000..cb2943d --- /dev/null +++ b/public/css/custom.css @@ -0,0 +1,88 @@ +/* SmartSolTech - Main Styles */ + +/* Basic reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Additional styles for demo */ +.hero-section { + padding: 6rem 0 4rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + text-align: center; +} + +.section-padding { + padding: 4rem 0; +} + +.card-hover { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card-hover:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); +} + +.btn-gradient { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + transition: all 0.3s ease; +} + +.btn-gradient:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); +} + +/* Loading spinner */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-up { + animation: fadeInUp 0.6s ease-out; +} + +/* Responsive utilities */ +@media (max-width: 768px) { + .hero-section { + padding: 4rem 0 3rem; + } + + .section-padding { + padding: 2rem 0; + } +} \ No newline at end of file diff --git a/public/css/dark-theme.css b/public/css/dark-theme.css new file mode 100644 index 0000000..2d0cca4 --- /dev/null +++ b/public/css/dark-theme.css @@ -0,0 +1,315 @@ +/* Dark Theme Support for SmartSolTech */ + +/* Base Dark Theme */ +html.dark { + color-scheme: dark; +} + +.dark body { + background-color: #111827; + color: #f9fafb; +} + +/* Navigation Dark Theme */ +.dark nav { + background-color: #1f2937; + border-color: #374151; +} + +.dark .nav-link { + color: #d1d5db; +} + +.dark .nav-link:hover { + color: #60a5fa; +} + +.dark .nav-link.active { + color: #60a5fa; +} + +/* Sections Dark Theme */ +.dark section { + background-color: #111827; + color: #f9fafb; +} + +.dark .bg-white { + background-color: #1f2937 !important; +} + +.dark .bg-gray-50 { + background-color: #111827 !important; +} + +.dark .bg-gray-100 { + background-color: #374151 !important; +} + +.dark .bg-gray-900 { + background-color: #030712 !important; +} + +/* Text Colors Dark Theme */ +.dark .text-gray-900 { + color: #f9fafb !important; +} + +.dark .text-gray-800 { + color: #e5e7eb !important; +} + +.dark .text-gray-700 { + color: #d1d5db !important; +} + +.dark .text-gray-600 { + color: #9ca3af !important; +} + +.dark .text-gray-500 { + color: #6b7280 !important; +} + +.dark .text-gray-400 { + color: #9ca3af !important; +} + +.dark .text-gray-300 { + color: #d1d5db !important; +} + +/* Cards Dark Theme */ +.dark .card-hover { + background-color: #1f2937; + border-color: #374151; +} + +.dark .card-hover:hover { + background-color: #374151; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); +} + +/* Forms Dark Theme */ +.dark .form-input { + background-color: #374151; + border-color: #4b5563; + color: #f9fafb; +} + +.dark .form-input:focus { + border-color: #60a5fa; + background-color: #1f2937; +} + +.dark .form-input::placeholder { + color: #9ca3af; +} + +.dark input, +.dark textarea, +.dark select { + background-color: #374151; + border-color: #4b5563; + color: #f9fafb; +} + +.dark input:focus, +.dark textarea:focus, +.dark select:focus { + border-color: #60a5fa; + background-color: #1f2937; +} + +/* Borders Dark Theme */ +.dark .border-gray-300 { + border-color: #4b5563 !important; +} + +.dark .border-gray-200 { + border-color: #374151 !important; +} + +.dark .border-t { + border-color: #374151; +} + +/* Contact Form Dark Theme */ +.dark .contact-form { + background: linear-gradient(145deg, rgba(31,41,55,0.9), rgba(31,41,55,0.95)); + border-color: #374151; +} + +/* Service Cards Dark Theme */ +.dark .service-card { + background: linear-gradient(145deg, rgba(31,41,55,0.8), rgba(31,41,55,0.6)); + border-color: #374151; +} + +/* Team Cards Dark Theme */ +.dark .team-card { + background-color: #1f2937; + border-color: #374151; +} + +/* Portfolio Items Dark Theme */ +.dark .portfolio-item { + background-color: #1f2937; +} + +/* Hero Section Dark Theme */ +.dark .hero-section { + background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%); +} + +/* Shadows Dark Theme */ +.dark .shadow-lg { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); +} + +.dark .shadow-xl { + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); +} + +.dark .shadow-2xl { + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); +} + +/* Dropdown Dark Theme */ +.dark .dropdown-menu { + background-color: #1f2937; + border-color: #374151; +} + +.dark .dropdown-menu a { + color: #d1d5db; +} + +.dark .dropdown-menu a:hover { + background-color: #374151; + color: #f9fafb; +} + +/* Icons Dark Theme */ +.dark .text-blue-600 { + color: #60a5fa !important; +} + +.dark .text-purple-600 { + color: #a78bfa !important; +} + +.dark .text-green-600 { + color: #34d399 !important; +} + +.dark .text-yellow-600 { + color: #fbbf24 !important; +} + +/* Buttons Dark Theme */ +.dark .btn-primary { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); +} + +.dark .btn-primary:hover { + background: linear-gradient(135deg, #1d4ed8, #1e40af); +} + +/* Footer Dark Theme */ +.dark footer { + background-color: #030712; + border-color: #1f2937; +} + +.dark .footer-gradient { + background: linear-gradient(135deg, #030712 0%, #111827 100%); +} + +/* Scrollbar Dark Theme */ +.dark ::-webkit-scrollbar-track { + background: #1f2937; +} + +.dark ::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); +} + +/* Technology Stack Dark Theme */ +.dark .tech-stack { + background-color: #030712; +} + +.dark .tech-card { + background-color: #1f2937; + border-color: #374151; +} + +/* CTA Section Dark Theme */ +.dark .cta-section { + background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%); +} + +/* Testimonials Dark Theme */ +.dark .testimonial-card { + background: rgba(31, 41, 55, 0.8); + border-color: #374151; +} + +/* Alert Messages Dark Theme */ +.dark .alert-success { + background: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.3); + color: #10b981; +} + +.dark .alert-error { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +/* Mobile Menu Dark Theme */ +.dark #mobile-menu { + background-color: #1f2937; + border-color: #374151; +} + +/* Language Dropdown Dark Theme */ +.dark #mobile-language-menu { + background-color: #1f2937; + border-color: #374151; +} + +/* Smooth Theme Transition */ +* { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +/* Print styles for dark theme */ +@media print { + .dark * { + background: white !important; + color: black !important; + } +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + .dark { + --tw-bg-opacity: 1; + --tw-text-opacity: 1; + } + + .dark .border { + border-width: 2px; + } +} + +/* Reduced motion for accessibility */ +@media (prefers-reduced-motion: reduce) { + .dark * { + transition: none !important; + animation: none !important; + } +} \ No newline at end of file diff --git a/public/css/fixes.css b/public/css/fixes.css new file mode 100644 index 0000000..432d387 --- /dev/null +++ b/public/css/fixes.css @@ -0,0 +1,281 @@ +/* SmartSolTech - Design Fixes & Enhancements */ + +/* Glass effect improvements */ +.glass-effect { + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + /* Fallback for browsers that don't support backdrop-filter */ +} + +/* Support backdrop-filter for modern browsers */ +@supports (backdrop-filter: blur(10px)) { + .glass-effect { + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + } +} + +/* Hero section improvements */ +.hero-section { + position: relative; + overflow: hidden; + min-height: 100vh; +} + +/* Background blob animations */ +@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; +} + +/* Enhanced card hover effects */ +.card-hover { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + backface-visibility: hidden; +} + +.card-hover:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); +} + +/* Portfolio item enhancements */ +.portfolio-item { + overflow: hidden; + border-radius: 16px; + position: relative; +} + +.portfolio-image { + transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.portfolio-item:hover .portfolio-image { + transform: scale(1.1); +} + +/* Button improvements */ +.btn-primary { + background: linear-gradient(135deg, #3B82F6, #1D4ED8); + border: none; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.5s; +} + +.btn-primary:hover::before { + left: 100%; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #1D4ED8, #1E40AF); + transform: translateY(-3px); + box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4); +} + +/* Navigation improvements */ +.nav-link { + position: relative; + transition: all 0.3s ease; +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 50%; + width: 0; + height: 2px; + background: linear-gradient(90deg, #3B82F6, #8B5CF6); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +.nav-link:hover::after, +.nav-link.active::after { + width: 100%; +} + +/* Form improvements */ +.form-input { + transition: all 0.3s ease; + border: 2px solid #E5E7EB; + background: rgba(255, 255, 255, 0.95); +} + +.form-input:focus { + border-color: #3B82F6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + transform: translateY(-1px); +} + +/* Contact form styling */ +.contact-form { + background: linear-gradient(145deg, rgba(255,255,255,0.9), rgba(255,255,255,0.95)); + border: 1px solid rgba(255,255,255,0.3); + box-shadow: 0 20px 40px rgba(0,0,0,0.1); +} + +/* CTA section improvements */ +.cta-section { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + position: relative; +} + +.cta-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; +} + +/* Service cards */ +.service-card { + background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05)); + border: 1px solid rgba(255,255,255,0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +/* Team member cards */ +.team-card { + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.95); +} + +.team-card:hover { + transform: translateY(-10px); + box-shadow: 0 30px 60px rgba(0,0,0,0.12); +} + +/* Technology icons */ +.tech-icon { + transition: all 0.3s ease; +} + +.tech-icon:hover { + transform: scale(1.1) rotate(5deg); + filter: brightness(1.2); +} + +/* Loading states */ +.loading { + opacity: 0.7; + pointer-events: none; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid #f3f3f3; + border-top: 2px solid #3B82F6; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Mobile optimizations */ +@media (max-width: 768px) { + .card-hover:hover { + transform: translateY(-4px) scale(1.01); + } + + .btn-primary:hover { + transform: translateY(-2px); + } + + .hero-section { + min-height: 80vh; + } + + .portfolio-item:hover .portfolio-image { + transform: scale(1.05); + } +} + +/* Accessibility improvements */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +button:focus, +input:focus, +textarea:focus, +select:focus, +a:focus { + outline: 2px solid #3B82F6; + outline-offset: 2px; +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Print styles */ +@media print { + .no-print { + display: none !important; + } + + body { + color: black !important; + background: white !important; + } +} + +/* Dark mode support (if needed) */ +@media (prefers-color-scheme: dark) { + .auto-dark { + color: #f9fafb; + background-color: #111827; + } +} \ No newline at end of file diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000..e13ca7f --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,552 @@ +/* SmartSolTech - Main Styles */ + +/* CSS Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #1f2937; +} + +/* Utility Classes */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.section-padding { + padding: 4rem 0; +} + +/* Navigation */ +.navbar { + background: rgba(255, 255, 255, 0.95); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + transition: all 0.3s ease; +} + +.navbar-brand { + font-size: 1.5rem; + font-weight: bold; + color: #3b82f6; + text-decoration: none; +} + +.navbar-nav { + display: flex; + gap: 2rem; + list-style: none; +} + +.nav-link { + color: #6b7280; + text-decoration: none; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.3s ease; +} + +.nav-link:hover, +.nav-link.active { + color: #3b82f6; + background-color: #eff6ff; +} + overflow: hidden; +} + +.mobile-menu.show { + max-height: 500px; +} + +/* Button Hover Effects */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + transition: all 0.3s ease; + transform: translateY(0); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card Hover Effects */ +.card-hover { + transition: all 0.3s ease; + transform: translateY(0); +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Portfolio Grid */ +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + border-radius: 1rem; + overflow: hidden; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.portfolio-item:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.portfolio-image { + position: relative; + overflow: hidden; + aspect-ratio: 16/10; +} + +.portfolio-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.portfolio-item:hover .portfolio-image img { + transform: scale(1.05); +} + +.portfolio-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8)); + opacity: 0; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.portfolio-item:hover .portfolio-overlay { + opacity: 1; +} + +/* Service Cards */ +.service-card { + background: white; + border-radius: 1rem; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.service-card:hover { + border-color: var(--primary-color); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + font-size: 2rem; + color: white; + transition: all 0.3s ease; +} + +.service-card:hover .service-icon { + transform: scale(1.1) rotate(5deg); +} + +/* Contact Form */ +.contact-form { + background: white; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Calculator Styles */ +.calculator-step { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.option-card { + border: 2px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.option-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.option-card.selected { + border-color: var(--primary-color); + background: rgba(59, 130, 246, 0.05); +} + +/* Progress Bar */ +.progress-bar { + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + margin-bottom: 2rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + transition: width 0.3s ease; +} + +/* Hero Section Animations */ +.hero-content { + animation: heroFadeIn 1s ease-out; +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Parallax Effect */ +.parallax { + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +/* Scroll Animations */ +.fade-in-up { + opacity: 0; + transform: translateY(30px); + transition: all 0.8s ease; +} + +.fade-in-up.animate { + opacity: 1; + transform: translateY(0); +} + +/* Typography */ +.gradient-text { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-new { + background: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.status-in-progress { + background: rgba(245, 158, 11, 0.1); + color: #d97706; +} + +.status-completed { + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .portfolio-grid { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .service-card { + padding: 1.5rem; + } + + .calculator-step { + padding: 1rem; + } +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + :root { + --text-dark: #f9fafb; + --text-light: #d1d5db; + --bg-light: #1f2937; + --border-color: #374151; + } + + body { + background-color: #111827; + color: var(--text-dark); + } + + .card-hover, .service-card, .contact-form, .option-card { + background: #1f2937; + border-color: var(--border-color); + } + + .form-control { + background: #374151; + border-color: #4b5563; + color: white; + } + + .form-control:focus { + border-color: var(--primary-color); + } +} + +/* Print Styles */ +@media print { + .navbar, .footer, .contact-form, .mobile-menu { + display: none !important; + } + + body { + font-size: 12pt; + line-height: 1.5; + } + + .portfolio-item, .service-card { + break-inside: avoid; + margin-bottom: 1rem; + } +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Focus styles for better accessibility */ +.form-control:focus, +.btn:focus, +.option-card:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Loading States */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +/* Calculator Styles */ +.calculator-step { + display: none; +} + +.calculator-step.active { + display: block; +} + +.service-option, +.complexity-option, +.timeline-option { + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.service-option:hover, +.complexity-option:hover, +.timeline-option:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15); +} + +.service-option.selected, +.complexity-option.selected, +.timeline-option.selected { + border-color: #3B82F6 !important; + background-color: #EBF8FF !important; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15); +} + +.service-option.selected::after, +.complexity-option.selected::after, +.timeline-option.selected::after { + content: '✓'; + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 24px; + height: 24px; + background: #3B82F6; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; +} + +/* Price Display Animation */ +#final-price { + animation: priceReveal 0.8s ease-in-out; +} + +@keyframes priceReveal { + 0% { + opacity: 0; + transform: scale(0.8); + } + 50% { + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +/* Calculator Progress Bar */ +.calculator-progress { + width: 100%; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + margin-bottom: 2rem; + overflow: hidden; +} + +.calculator-progress-bar { + height: 100%; + background: linear-gradient(90deg, #3B82F6, #8B5CF6); + border-radius: 2px; + transition: width 0.3s ease; + width: 33.33%; +} + +.calculator-progress-bar.step-2 { + width: 66.66%; +} + +.calculator-progress-bar.step-3 { + width: 100%; +} + +/* Calculator Mobile Improvements */ +@media (max-width: 768px) { + .service-option, + .complexity-option, + .timeline-option { + margin-bottom: 1rem; + } + + #final-price { + font-size: 2.5rem; + } +} \ No newline at end of file diff --git a/public/js/calculator.js b/public/js/calculator.js new file mode 100644 index 0000000..8bbbfcb --- /dev/null +++ b/public/js/calculator.js @@ -0,0 +1,384 @@ +// Calculator Logic +class ProjectCalculator { + constructor() { + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + this.totalSteps = 3; + + this.init(); + } + + init() { + this.setupEventListeners(); + this.updateProgressBar(); + } + + setupEventListeners() { + // Service selection + document.querySelectorAll('.service-option').forEach(option => { + option.addEventListener('click', (e) => this.handleServiceSelection(e)); + }); + + // Complexity selection + document.querySelectorAll('.complexity-option').forEach(option => { + option.addEventListener('click', (e) => this.handleComplexitySelection(e)); + }); + + // Timeline selection + document.querySelectorAll('.timeline-option').forEach(option => { + option.addEventListener('click', (e) => this.handleTimelineSelection(e)); + }); + + // Navigation buttons + document.querySelectorAll('.next-step').forEach(btn => { + btn.addEventListener('click', () => this.nextStep()); + }); + + document.querySelectorAll('.prev-step').forEach(btn => { + btn.addEventListener('click', () => this.prevStep()); + }); + + // Restart button + const restartBtn = document.querySelector('.restart-calculator'); + if (restartBtn) { + restartBtn.addEventListener('click', () => this.restart()); + } + } + + handleServiceSelection(e) { + const option = e.currentTarget; + const service = option.dataset.service; + const price = parseInt(option.dataset.basePrice); + + option.classList.toggle('selected'); + + if (option.classList.contains('selected')) { + this.selectedServices.push({ service, price }); + this.animateSelection(option, true); + } else { + this.selectedServices = this.selectedServices.filter(s => s.service !== service); + this.animateSelection(option, false); + } + + this.updateStepButton(); + } + + handleComplexitySelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.complexity-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedComplexity = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + handleTimelineSelection(e) { + const option = e.currentTarget; + + // Clear previous selections + document.querySelectorAll('.timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Select current option + option.classList.add('selected'); + this.animateSelection(option, true); + + this.selectedTimeline = { + value: option.dataset.value, + multiplier: parseFloat(option.dataset.multiplier) + }; + + this.updateStepButton(); + } + + animateSelection(element, selected) { + if (selected) { + element.style.borderColor = '#3B82F6'; + element.style.backgroundColor = '#EBF8FF'; + element.style.transform = 'translateY(-2px)'; + element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)'; + } else { + element.style.borderColor = ''; + element.style.backgroundColor = ''; + element.style.transform = ''; + element.style.boxShadow = ''; + } + } + + updateStepButton() { + const step1Button = document.querySelector('#step-1 .next-step'); + const step2Button = document.querySelector('#step-2 .next-step'); + + if (step1Button) { + step1Button.disabled = this.selectedServices.length === 0; + step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6'; + } + + if (step2Button) { + const isValid = this.selectedComplexity && this.selectedTimeline; + step2Button.disabled = !isValid; + step2Button.style.opacity = isValid ? '1' : '0.6'; + } + } + + updateProgressBar() { + const progressBar = document.querySelector('.calculator-progress-bar'); + if (progressBar) { + const progress = (this.currentStep / this.totalSteps) * 100; + progressBar.style.width = `${progress}%`; + progressBar.className = `calculator-progress-bar step-${this.currentStep}`; + } + } + + nextStep() { + if (this.currentStep >= this.totalSteps) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const nextStepElement = currentStepElement.nextElementSibling; + + if (nextStepElement && nextStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in next step + nextStepElement.classList.add('active'); + nextStepElement.style.display = 'block'; + nextStepElement.style.opacity = '0'; + nextStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + nextStepElement.style.opacity = '1'; + nextStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep++; + this.updateProgressBar(); + + if (this.currentStep === 3) { + setTimeout(() => this.calculateFinalPrice(), 300); + } + }, 200); + } + } + + prevStep() { + if (this.currentStep <= 1) return; + + const currentStepElement = document.querySelector('.calculator-step.active'); + const prevStepElement = currentStepElement.previousElementSibling; + + if (prevStepElement && prevStepElement.classList.contains('calculator-step')) { + // Animate out current step + currentStepElement.style.opacity = '0'; + currentStepElement.style.transform = 'translateX(20px)'; + + setTimeout(() => { + currentStepElement.classList.remove('active'); + currentStepElement.style.display = 'none'; + + // Animate in previous step + prevStepElement.classList.add('active'); + prevStepElement.style.display = 'block'; + prevStepElement.style.opacity = '0'; + prevStepElement.style.transform = 'translateX(-20px)'; + + setTimeout(() => { + prevStepElement.style.opacity = '1'; + prevStepElement.style.transform = 'translateX(0)'; + }, 50); + + this.currentStep--; + this.updateProgressBar(); + }, 200); + } + } + + calculateFinalPrice() { + let total = 0; + + // Calculate base price from services + this.selectedServices.forEach(service => { + total += service.price; + }); + + // Apply complexity multiplier + if (this.selectedComplexity) { + total *= this.selectedComplexity.multiplier; + } + + // Apply timeline multiplier + if (this.selectedTimeline) { + total *= this.selectedTimeline.multiplier; + } + + // Animate price reveal + const priceElement = document.getElementById('final-price'); + if (priceElement) { + priceElement.style.opacity = '0'; + priceElement.style.transform = 'scale(0.8)'; + + setTimeout(() => { + priceElement.textContent = '₩' + total.toLocaleString(); + priceElement.style.opacity = '1'; + priceElement.style.transform = 'scale(1)'; + }, 300); + } + + // Generate summary + setTimeout(() => this.generateSummary(total), 600); + } + + generateSummary(total) { + const summary = document.getElementById('project-summary'); + if (!summary) return; + + let summaryHTML = ''; + + // Services + const servicesLabel = window.calculatorTranslations?.result?.selected_services || 'Selected Services'; + const complexityLabel = window.calculatorTranslations?.result?.complexity || 'Complexity'; + const timelineLabel = window.calculatorTranslations?.result?.timeline || 'Timeline'; + + summaryHTML += `
${servicesLabel}:
`; + this.selectedServices.forEach(service => { + summaryHTML += `
+ + ${this.getServiceName(service.service)} + ₩${service.price.toLocaleString()} +
`; + }); + + // Complexity + if (this.selectedComplexity) { + summaryHTML += `
+ + ${complexityLabel}: + ${this.getComplexityName(this.selectedComplexity.value)} + ×${this.selectedComplexity.multiplier} +
`; + } + + // Timeline + if (this.selectedTimeline) { + summaryHTML += `
+ + ${timelineLabel}: + ${this.getTimelineName(this.selectedTimeline.value)} + ×${this.selectedTimeline.multiplier} +
`; + } + + // Animate summary appearance + summary.style.opacity = '0'; + summary.innerHTML = summaryHTML; + + setTimeout(() => { + summary.style.opacity = '1'; + }, 200); + } + + getServiceName(service) { + if (window.calculatorTranslations && window.calculatorTranslations.services) { + return window.calculatorTranslations.services[service] || service; + } + + const names = { + web: 'Web Development', + mobile: 'Mobile App', + design: 'UI/UX Design', + marketing: 'Digital Marketing' + }; + return names[service] || service; + } + + getComplexityName(complexity) { + if (window.calculatorTranslations && window.calculatorTranslations.complexity) { + return window.calculatorTranslations.complexity[complexity] || complexity; + } + + const names = { + simple: 'Simple', + medium: 'Medium', + complex: 'Complex' + }; + return names[complexity] || complexity; + } + + getTimelineName(timeline) { + if (window.calculatorTranslations && window.calculatorTranslations.timeline) { + return window.calculatorTranslations.timeline[timeline] || timeline; + } + + const names = { + standard: 'Standard', + rush: 'Rush', + extended: 'Extended' + }; + return names[timeline] || timeline; + } + + restart() { + // Reset all selections + this.selectedServices = []; + this.selectedComplexity = null; + this.selectedTimeline = null; + this.currentStep = 1; + + // Reset UI + document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => { + opt.classList.remove('selected'); + this.animateSelection(opt, false); + }); + + // Reset steps + document.querySelectorAll('.calculator-step').forEach(step => { + step.classList.remove('active'); + step.style.display = 'none'; + step.style.opacity = '1'; + step.style.transform = 'translateX(0)'; + }); + + // Show first step + const firstStep = document.getElementById('step-1'); + if (firstStep) { + firstStep.classList.add('active'); + firstStep.style.display = 'block'; + } + + this.updateProgressBar(); + this.updateStepButton(); + } +} + +// Initialize calculator when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Theme initialization + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.className = theme === 'dark' ? 'dark' : ''; + + // Initialize calculator + if (document.querySelector('.calculator-step')) { + new ProjectCalculator(); + } +}); \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..0f44325 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,544 @@ +// Main JavaScript for SmartSolTech Website +document.addEventListener('DOMContentLoaded', function() { + // Initialize AOS (Animate On Scroll) + if (typeof AOS !== 'undefined') { + AOS.init({ + duration: 800, + easing: 'ease-in-out', + once: true, + offset: 100 + }); + } + + // Mobile Navigation Toggle + const mobileMenuButton = document.querySelector('.mobile-menu-button'); + const mobileMenu = document.querySelector('.mobile-menu'); + + if (mobileMenuButton && mobileMenu) { + mobileMenuButton.addEventListener('click', function() { + mobileMenu.classList.toggle('show'); + const isOpen = mobileMenu.classList.contains('show'); + + // Toggle button icon + const icon = mobileMenuButton.querySelector('svg'); + if (icon) { + icon.innerHTML = isOpen + ? '' + : ''; + } + + // Accessibility + mobileMenuButton.setAttribute('aria-expanded', isOpen); + }); + + // Close mobile menu when clicking outside + document.addEventListener('click', function(e) { + if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) { + mobileMenu.classList.remove('show'); + mobileMenuButton.setAttribute('aria-expanded', 'false'); + } + }); + } + + // Navbar Scroll Effect + const navbar = document.querySelector('nav'); + let lastScrollY = window.scrollY; + + window.addEventListener('scroll', function() { + const currentScrollY = window.scrollY; + + if (navbar) { + if (currentScrollY > 100) { + navbar.classList.add('navbar-scrolled'); + } else { + navbar.classList.remove('navbar-scrolled'); + } + + // Hide/show navbar on scroll + if (currentScrollY > lastScrollY && currentScrollY > 200) { + navbar.style.transform = 'translateY(-100%)'; + } else { + navbar.style.transform = 'translateY(0)'; + } + } + + lastScrollY = currentScrollY; + }); + + // Smooth Scrolling for Anchor Links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + const offsetTop = target.offsetTop - (navbar ? navbar.offsetHeight : 0); + window.scrollTo({ + top: offsetTop, + behavior: 'smooth' + }); + } + }); + }); + + // Contact Form Handler + const quickContactForm = document.getElementById('quick-contact-form'); + if (quickContactForm) { + quickContactForm.addEventListener('submit', handleContactSubmit); + } + + const mainContactForm = document.getElementById('contact-form'); + if (mainContactForm) { + mainContactForm.addEventListener('submit', handleContactSubmit); + } + + async function handleContactSubmit(e) { + e.preventDefault(); + + const form = e.target; + const submitButton = form.querySelector('button[type="submit"]'); + const originalText = submitButton.textContent; + + // Show loading state + submitButton.disabled = true; + submitButton.classList.add('btn-loading'); + submitButton.textContent = '전송 중...'; + + try { + const formData = new FormData(form); + const data = Object.fromEntries(formData.entries()); + + const response = await fetch('/api/contact/submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + showNotification('메시지가 성공적으로 전송되었습니다! 곧 연락드리겠습니다.', 'success'); + form.reset(); + } else { + showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); + } + } catch (error) { + console.error('Contact form error:', error); + showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error'); + } finally { + // Reset button state + submitButton.disabled = false; + submitButton.classList.remove('btn-loading'); + submitButton.textContent = originalText; + } + } + + // Notification System + function showNotification(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` +
+ ${message} + +
+ `; + + // Styles + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + max-width: 400px; + padding: 1rem; + border-radius: 0.5rem; + color: white; + font-weight: 500; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + transform: translateX(100%); + transition: transform 0.3s ease; + ${type === 'success' ? 'background: #10b981;' : ''} + ${type === 'error' ? 'background: #ef4444;' : ''} + ${type === 'info' ? 'background: #3b82f6;' : ''} + `; + + document.body.appendChild(notification); + + // Animate in + setTimeout(() => { + notification.style.transform = 'translateX(0)'; + }, 100); + + // Close button handler + const closeButton = notification.querySelector('.notification-close'); + closeButton.addEventListener('click', () => { + closeNotification(notification); + }); + + // Auto close after 5 seconds + setTimeout(() => { + closeNotification(notification); + }, 5000); + } + + function closeNotification(notification) { + notification.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + } + + // Portfolio Filter (if on portfolio page) + const portfolioFilters = document.querySelectorAll('.portfolio-filter'); + const portfolioItems = document.querySelectorAll('.portfolio-item'); + + portfolioFilters.forEach(filter => { + filter.addEventListener('click', function() { + const category = this.dataset.category; + + // Update active filter + portfolioFilters.forEach(f => f.classList.remove('active')); + this.classList.add('active'); + + // Filter items + portfolioItems.forEach(item => { + if (category === 'all' || item.dataset.category === category) { + item.style.display = 'block'; + item.style.animation = 'fadeIn 0.5s ease'; + } else { + item.style.display = 'none'; + } + }); + }); + }); + + // Image Lazy Loading + const images = document.querySelectorAll('img[data-src]'); + const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src; + img.classList.remove('lazy'); + imageObserver.unobserve(img); + } + }); + }); + + images.forEach(img => imageObserver.observe(img)); + + // Service Worker Registration for PWA + if ('serviceWorker' in navigator && 'PushManager' in window) { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('Service Worker registered successfully:', registration); + }) + .catch(error => { + console.log('Service Worker registration failed:', error); + }); + } + + // Performance Monitoring + if ('performance' in window) { + window.addEventListener('load', () => { + setTimeout(() => { + const perfData = performance.getEntriesByType('navigation')[0]; + const loadTime = perfData.loadEventEnd - perfData.loadEventStart; + + if (loadTime > 3000) { + console.warn('Page load time is slow:', loadTime + 'ms'); + } + }, 1000); + }); + } + + // Cookie Consent (if needed) + function initCookieConsent() { + const consent = localStorage.getItem('cookieConsent'); + if (!consent) { + showCookieConsent(); + } + } + + function showCookieConsent() { + const banner = document.createElement('div'); + banner.className = 'cookie-consent'; + banner.innerHTML = ` + + `; + + banner.style.cssText = ` + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 1rem; + z-index: 9999; + transform: translateY(100%); + transition: transform 0.3s ease; + `; + + document.body.appendChild(banner); + + setTimeout(() => { + banner.style.transform = 'translateY(0)'; + }, 100); + + document.getElementById('accept-cookies').addEventListener('click', () => { + localStorage.setItem('cookieConsent', 'accepted'); + banner.style.transform = 'translateY(100%)'; + setTimeout(() => banner.remove(), 300); + }); + + document.getElementById('decline-cookies').addEventListener('click', () => { + localStorage.setItem('cookieConsent', 'declined'); + banner.style.transform = 'translateY(100%)'; + setTimeout(() => banner.remove(), 300); + }); + } + + // Initialize cookie consent + // initCookieConsent(); + + // Parallax Effect + const parallaxElements = document.querySelectorAll('.parallax'); + + function updateParallax() { + const scrollY = window.pageYOffset; + + parallaxElements.forEach(element => { + const speed = element.dataset.speed || 0.5; + const yPos = -(scrollY * speed); + element.style.transform = `translateY(${yPos}px)`; + }); + } + + if (parallaxElements.length > 0) { + window.addEventListener('scroll', updateParallax); + } + + // Counter Animation + const counters = document.querySelectorAll('.counter'); + const counterObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + animateCounter(entry.target); + counterObserver.unobserve(entry.target); + } + }); + }); + + counters.forEach(counter => counterObserver.observe(counter)); + + function animateCounter(element) { + const target = parseInt(element.dataset.count); + const duration = 2000; + const start = performance.now(); + + function updateCounter(currentTime) { + const elapsed = currentTime - start; + const progress = Math.min(elapsed / duration, 1); + + const current = Math.floor(progress * target); + element.textContent = current.toLocaleString(); + + if (progress < 1) { + requestAnimationFrame(updateCounter); + } + } + + requestAnimationFrame(updateCounter); + } + + // Typing Effect for Hero Text + const typingElements = document.querySelectorAll('.typing-effect'); + + typingElements.forEach(element => { + const text = element.textContent; + element.textContent = ''; + element.style.borderRight = '2px solid'; + + let i = 0; + const timer = setInterval(() => { + element.textContent += text[i]; + i++; + + if (i >= text.length) { + clearInterval(timer); + element.style.borderRight = 'none'; + } + }, 100); + }); + + // Handle form validation + const forms = document.querySelectorAll('form[data-validate]'); + forms.forEach(form => { + form.addEventListener('submit', function(e) { + if (!validateForm(this)) { + e.preventDefault(); + } + }); + + // Real-time validation + const inputs = form.querySelectorAll('input, textarea'); + inputs.forEach(input => { + input.addEventListener('blur', () => validateField(input)); + input.addEventListener('input', () => clearFieldError(input)); + }); + }); + + function validateForm(form) { + let isValid = true; + const inputs = form.querySelectorAll('input[required], textarea[required]'); + + inputs.forEach(input => { + if (!validateField(input)) { + isValid = false; + } + }); + + return isValid; + } + + function validateField(field) { + const value = field.value.trim(); + const type = field.type; + let isValid = true; + let message = ''; + + // Required field check + if (field.hasAttribute('required') && !value) { + isValid = false; + message = '이 필드는 필수입니다.'; + } + + // Email validation + if (type === 'email' && value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + isValid = false; + message = '올바른 이메일 형식을 입력해주세요.'; + } + } + + // Phone validation + if (type === 'tel' && value) { + const phoneRegex = /^[0-9-+\s()]+$/; + if (!phoneRegex.test(value)) { + isValid = false; + message = '올바른 전화번호 형식을 입력해주세요.'; + } + } + + // Show/hide error + if (!isValid) { + showFieldError(field, message); + } else { + clearFieldError(field); + } + + return isValid; + } + + function showFieldError(field, message) { + clearFieldError(field); + + field.classList.add('error'); + const errorDiv = document.createElement('div'); + errorDiv.className = 'field-error'; + errorDiv.textContent = message; + errorDiv.style.cssText = 'color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;'; + + field.parentNode.appendChild(errorDiv); + } + + function clearFieldError(field) { + field.classList.remove('error'); + const errorDiv = field.parentNode.querySelector('.field-error'); + if (errorDiv) { + errorDiv.remove(); + } + } +}); + +// Utility Functions +const utils = { + // Debounce function + debounce: function(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + // Throttle function + throttle: function(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + }, + + // Format currency + formatCurrency: function(amount, currency = 'KRW') { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: currency, + minimumFractionDigits: 0 + }).format(amount); + }, + + // Format date + formatDate: function(date, options = {}) { + const defaultOptions = { + year: 'numeric', + month: 'long', + day: 'numeric' + }; + return new Intl.DateTimeFormat('ko-KR', { ...defaultOptions, ...options }).format(new Date(date)); + } +}; + +// Global error handler +window.addEventListener('error', function(e) { + console.error('Global error:', e.error); + // Could send error to analytics service +}); + +// Unhandled promise rejection handler +window.addEventListener('unhandledrejection', function(e) { + console.error('Unhandled promise rejection:', e.reason); + // Could send error to analytics service +}); + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = utils; +} \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..c493fcf --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,161 @@ +{ + "name": "SmartSolTech - Technology Solutions", + "short_name": "SmartSolTech", + "description": "Professional web development, mobile apps, and digital solutions in Korea", + "start_url": "/", + "display": "standalone", + "orientation": "portrait-primary", + "theme_color": "#3b82f6", + "background_color": "#ffffff", + "lang": "ko", + "scope": "/", + "categories": ["business", "productivity", "technology"], + "icons": [ + { + "src": "/images/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/images/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "screenshots": [ + { + "src": "/images/screenshot-desktop.png", + "sizes": "1280x720", + "type": "image/png", + "form_factor": "wide", + "label": "SmartSolTech Desktop View" + }, + { + "src": "/images/screenshot-mobile.png", + "sizes": "375x812", + "type": "image/png", + "form_factor": "narrow", + "label": "SmartSolTech Mobile View" + } + ], + "shortcuts": [ + { + "name": "Portfolio", + "short_name": "Portfolio", + "description": "View our latest projects", + "url": "/portfolio", + "icons": [ + { + "src": "/images/shortcut-portfolio.png", + "sizes": "96x96", + "type": "image/png" + } + ] + }, + { + "name": "Calculator", + "short_name": "Calculator", + "description": "Calculate project costs", + "url": "/calculator", + "icons": [ + { + "src": "/images/shortcut-calculator.png", + "sizes": "96x96", + "type": "image/png" + } + ] + }, + { + "name": "Contact", + "short_name": "Contact", + "description": "Get in touch with us", + "url": "/contact", + "icons": [ + { + "src": "/images/shortcut-contact.png", + "sizes": "96x96", + "type": "image/png" + } + ] + } + ], + "related_applications": [ + { + "platform": "webapp", + "url": "https://smartsoltech.kr/manifest.json" + } + ], + "prefer_related_applications": false, + "edge_side_panel": { + "preferred_width": 400 + }, + "protocol_handlers": [ + { + "protocol": "mailto", + "url": "/contact?email=%s" + } + ], + "file_handlers": [ + { + "action": "/open-file", + "accept": { + "image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"], + "application/pdf": [".pdf"] + } + } + ], + "share_target": { + "action": "/share", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [ + { + "name": "images", + "accept": ["image/*"] + } + ] + } + } +} \ No newline at end of file diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..4cc2eef --- /dev/null +++ b/public/sw.js @@ -0,0 +1,396 @@ +// Service Worker for SmartSolTech PWA +const CACHE_NAME = 'smartsoltech-v1.0.0'; +const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.0'; +const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.0'; + +// Files to cache immediately +const STATIC_FILES = [ + '/', + '/css/main.css', + '/js/main.js', + '/images/logo.png', + '/images/icon-192x192.png', + '/images/icon-512x512.png', + '/manifest.json', + 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap', + 'https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css', + 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css', + 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/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)) { + event.respondWith(staleWhileRevalidate(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) { + const cache = await caches.open(DYNAMIC_CACHE_NAME); + const cachedResponse = await cache.match(request); + + const fetchPromise = fetch(request).then(networkResponse => { + if (networkResponse.ok) { + cache.put(request, networkResponse.clone()); + } + return networkResponse; + }); + + return cachedResponse || fetchPromise; +} + +// 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/routes/admin.js b/routes/admin.js new file mode 100644 index 0000000..b4c8e82 --- /dev/null +++ b/routes/admin.js @@ -0,0 +1,387 @@ +const express = require('express'); +const router = express.Router(); +const { body, validationResult } = require('express-validator'); +const User = require('../models/User'); +const Portfolio = require('../models/Portfolio'); +const Service = require('../models/Service'); +const Contact = require('../models/Contact'); +const SiteSettings = require('../models/SiteSettings'); + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.redirect('/admin/login'); + } + 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 - SmartSolTech', + layout: 'admin/layout', + error: null + }); +}); + +// Admin login POST +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + const user = await User.findOne({ email, isActive: true }); + if (!user || !(await user.comparePassword(password))) { + return res.render('admin/login', { + title: 'Admin Login - SmartSolTech', + layout: 'admin/layout', + error: 'Invalid email or password' + }); + } + + 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 - SmartSolTech', + layout: 'admin/layout', + error: 'An error occurred. Please try again.' + }); + } +}); + +// Admin logout +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + console.error('Logout error:', err); + } + res.redirect('/admin/login'); + }); +}); + +// Dashboard +router.get('/dashboard', requireAuth, async (req, res) => { + try { + const [ + portfolioCount, + servicesCount, + contactsCount, + recentContacts, + recentPortfolio + ] = await Promise.all([ + Portfolio.countDocuments({ isPublished: true }), + Service.countDocuments({ isActive: true }), + Contact.countDocuments(), + Contact.find().sort({ createdAt: -1 }).limit(5), + Portfolio.find({ isPublished: true }).sort({ createdAt: -1 }).limit(5) + ]); + + const stats = { + portfolio: portfolioCount, + services: servicesCount, + contacts: contactsCount, + unreadContacts: await Contact.countDocuments({ isRead: false }) + }; + + res.render('admin/dashboard', { + title: 'Dashboard - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + stats, + recentContacts, + recentPortfolio + }); + } catch (error) { + console.error('Dashboard error:', error); + res.status(500).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Error loading dashboard' + }); + } +}); + +// 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.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Portfolio.countDocuments() + ]); + + 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' + }); + } +}); + +// 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 + }); +}); + +// Edit portfolio item +router.get('/portfolio/edit/:id', requireAuth, async (req, res) => { + try { + const portfolio = await Portfolio.findById(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 + }); + } 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' + }); + } +}); + +// 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.find() + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Service.countDocuments() + ]); + + 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 + }); +}); + +// Edit service +router.get('/services/edit/:id', requireAuth, async (req, res) => { + try { + const service = await Service.findById(req.params.id) + .populate('portfolio', 'title'); + + if (!service) { + return res.status(404).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Service not found' + }); + } + + const availablePortfolio = await Portfolio.find({ isPublished: true }) + .select('title category'); + + res.render('admin/services/edit', { + title: 'Edit Service - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + service, + availablePortfolio + }); + } 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' + }); + } +}); + +// 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 query = {}; + if (status && status !== 'all') { + query.status = status; + } + + const [contacts, total] = await Promise.all([ + Contact.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + Contact.countDocuments(query) + ]); + + 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.findById(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() || new SiteSettings(); + + 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 + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..de4d91e --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,201 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const { body, validationResult } = require('express-validator'); +const User = require('../models/User'); + +// Login validation rules +const loginValidation = [ + body('email').isEmail().normalizeEmail(), + body('password').isLength({ min: 6 }) +]; + +// Login +router.post('/login', loginValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { email, password } = req.body; + + // Find user + const user = await User.findOne({ email, isActive: true }); + if (!user) { + return res.status(401).json({ + success: false, + message: 'Invalid credentials' + }); + } + + // Check password + const isValidPassword = await user.comparePassword(password); + if (!isValidPassword) { + return res.status(401).json({ + success: false, + message: 'Invalid credentials' + }); + } + + // Update last login + await user.updateLastLogin(); + + // Create JWT token + const token = jwt.sign( + { userId: user._id, email: user.email, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + // Set session + req.session.user = { + id: user._id, + email: user.email, + name: user.name, + role: user.role + }; + + res.json({ + success: true, + message: 'Login successful', + token, + user: { + id: user._id, + email: user.email, + name: user.name, + role: user.role, + avatar: user.avatar + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +// Logout +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + return res.status(500).json({ + success: false, + message: 'Could not log out' + }); + } + + res.clearCookie('connect.sid'); + res.json({ + success: true, + message: 'Logout successful' + }); + }); +}); + +// Check authentication status +router.get('/me', async (req, res) => { + try { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Not authenticated' + }); + } + + const user = await User.findById(req.session.user.id) + .select('-password'); + + if (!user || !user.isActive) { + req.session.destroy(); + return res.status(401).json({ + success: false, + message: 'User not found or inactive' + }); + } + + res.json({ + success: true, + user: { + id: user._id, + email: user.email, + name: user.name, + role: user.role, + avatar: user.avatar, + lastLogin: user.lastLogin + } + }); + } catch (error) { + console.error('Auth check error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +// Change password +router.put('/change-password', [ + body('currentPassword').isLength({ min: 6 }), + body('newPassword').isLength({ min: 6 }) +], async (req, res) => { + try { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Not authenticated' + }); + } + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { currentPassword, newPassword } = req.body; + const user = await User.findById(req.session.user.id); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + // Verify current password + const isValidPassword = await user.comparePassword(currentPassword); + if (!isValidPassword) { + return res.status(400).json({ + success: false, + message: 'Current password is incorrect' + }); + } + + // Update password + user.password = newPassword; + await user.save(); + + res.json({ + success: true, + message: 'Password updated successfully' + }); + } catch (error) { + console.error('Change password error:', error); + res.status(500).json({ + success: false, + message: 'Server error' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/calculator.js b/routes/calculator.js new file mode 100644 index 0000000..90559e4 --- /dev/null +++ b/routes/calculator.js @@ -0,0 +1,312 @@ +const express = require('express'); +const router = express.Router(); +const Service = require('../models/Service'); + +// Get all services for calculator +router.get('/services', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .select('name pricing category features estimatedTime') + .sort({ category: 1, name: 1 }); + + const servicesByCategory = services.reduce((acc, service) => { + if (!acc[service.category]) { + acc[service.category] = []; + } + acc[service.category].push(service); + return acc; + }, {}); + + res.json({ + success: true, + services: servicesByCategory, + allServices: services + }); + } catch (error) { + console.error('Calculator services error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching services' + }); + } +}); + +// Calculate project estimate +router.post('/calculate', async (req, res) => { + try { + const { + selectedServices = [], + projectComplexity = 'medium', + timeline = 'standard', + additionalFeatures = [], + customRequirements = '' + } = req.body; + + if (!selectedServices.length) { + return res.status(400).json({ + success: false, + message: 'Please select at least one service' + }); + } + + // Get selected services details + const services = await Service.find({ + _id: { $in: selectedServices }, + isActive: true + }); + + if (services.length !== selectedServices.length) { + return res.status(400).json({ + success: false, + message: 'Some selected services are not available' + }); + } + + // Calculate base cost + let baseCost = 0; + let totalTime = 0; // in days + + services.forEach(service => { + baseCost += service.pricing.basePrice; + totalTime += (service.estimatedTime.min + service.estimatedTime.max) / 2; + }); + + // Apply complexity multiplier + const complexityMultipliers = { + 'simple': 0.7, + 'medium': 1.0, + 'complex': 1.5, + 'enterprise': 2.0 + }; + + const complexityMultiplier = complexityMultipliers[projectComplexity] || 1.0; + + // Apply timeline multiplier + const timelineMultipliers = { + 'rush': 1.8, // Less than 2 weeks + 'fast': 1.4, // 2-4 weeks + 'standard': 1.0, // 1-3 months + 'flexible': 0.8 // 3+ months + }; + + const timelineMultiplier = timelineMultipliers[timeline] || 1.0; + + // Additional features cost + const featureCosts = { + 'seo-optimization': 300000, + 'analytics-setup': 150000, + 'social-integration': 200000, + 'payment-gateway': 500000, + 'multilingual': 400000, + 'admin-panel': 600000, + 'api-integration': 350000, + 'mobile-responsive': 250000, + 'ssl-certificate': 100000, + 'backup-system': 200000 + }; + + let additionalCost = 0; + additionalFeatures.forEach(feature => { + additionalCost += featureCosts[feature] || 0; + }); + + // Custom requirements cost (estimated based on description length and complexity) + let customCost = 0; + if (customRequirements && customRequirements.length > 50) { + customCost = Math.min(customRequirements.length * 1000, 1000000); // Max 1M KRW + } + + // Calculate final estimate + const subtotal = baseCost * complexityMultiplier * timelineMultiplier; + const total = subtotal + additionalCost + customCost; + + // Calculate time estimate + const timeMultiplier = complexityMultiplier * timelineMultiplier; + const estimatedDays = Math.ceil(totalTime * timeMultiplier); + const estimatedWeeks = Math.ceil(estimatedDays / 7); + + // Create price ranges (±20%) + const minPrice = Math.round(total * 0.8); + const maxPrice = Math.round(total * 1.2); + + // Breakdown + const breakdown = { + baseServices: Math.round(baseCost), + complexityAdjustment: Math.round(baseCost * (complexityMultiplier - 1)), + timelineAdjustment: Math.round(baseCost * complexityMultiplier * (timelineMultiplier - 1)), + additionalFeatures: additionalCost, + customRequirements: customCost, + subtotal: Math.round(subtotal), + total: Math.round(total) + }; + + const result = { + success: true, + estimate: { + total: Math.round(total), + range: { + min: minPrice, + max: maxPrice, + formatted: `₩${minPrice.toLocaleString()} - ₩${maxPrice.toLocaleString()}` + }, + breakdown, + timeline: { + days: estimatedDays, + weeks: estimatedWeeks, + formatted: estimatedWeeks === 1 ? '1 week' : `${estimatedWeeks} weeks` + }, + currency: 'KRW', + confidence: calculateConfidence(services.length, projectComplexity, customRequirements), + recommendations: generateRecommendations(projectComplexity, timeline, services) + }, + selectedServices: services.map(s => ({ + id: s._id, + name: s.name, + category: s.category, + basePrice: s.pricing.basePrice + })), + calculatedAt: new Date() + }; + + res.json(result); + } catch (error) { + console.error('Calculator calculation error:', error); + res.status(500).json({ + success: false, + message: 'Error calculating estimate' + }); + } +}); + +// Helper function to calculate confidence level +function calculateConfidence(serviceCount, complexity, customRequirements) { + let confidence = 85; // Base confidence + + // Adjust based on service count + if (serviceCount === 1) confidence += 10; + else if (serviceCount > 5) confidence -= 10; + + // Adjust based on complexity + if (complexity === 'simple') confidence += 10; + else if (complexity === 'enterprise') confidence -= 15; + + // Adjust based on custom requirements + if (customRequirements && customRequirements.length > 200) { + confidence -= 20; + } + + return Math.max(60, Math.min(95, confidence)); +} + +// Helper function to generate recommendations +function generateRecommendations(complexity, timeline, services) { + const recommendations = []; + + if (complexity === 'enterprise' && timeline === 'rush') { + recommendations.push('Consider extending timeline for enterprise-level projects to ensure quality'); + } + + if (services.length > 6) { + recommendations.push('Consider breaking down into phases for better project management'); + } + + if (timeline === 'flexible') { + recommendations.push('Flexible timeline allows for better cost optimization and quality assurance'); + } + + const hasDesign = services.some(s => s.category === 'design'); + const hasDevelopment = services.some(s => s.category === 'development'); + + if (hasDevelopment && !hasDesign) { + recommendations.push('Consider adding UI/UX design services for better user experience'); + } + + return recommendations; +} + +// Get pricing guidelines +router.get('/pricing-guide', async (req, res) => { + try { + const pricingGuide = { + serviceCategories: { + 'development': { + name: 'Web Development', + priceRange: '₩500,000 - ₩5,000,000', + description: 'Custom web applications and websites' + }, + 'design': { + name: 'UI/UX Design', + priceRange: '₩300,000 - ₩2,000,000', + description: 'User interface and experience design' + }, + 'marketing': { + name: 'Digital Marketing', + priceRange: '₩200,000 - ₩1,500,000', + description: 'SEO, social media, and online marketing' + }, + 'consulting': { + name: 'Technical Consulting', + priceRange: '₩150,000 - ₩1,000,000', + description: 'Technology strategy and consultation' + } + }, + complexityFactors: { + 'simple': { + name: 'Simple Project', + multiplier: '0.7x', + description: 'Basic functionality, standard design' + }, + 'medium': { + name: 'Medium Project', + multiplier: '1.0x', + description: 'Moderate complexity, custom features' + }, + 'complex': { + name: 'Complex Project', + multiplier: '1.5x', + description: 'Advanced features, integrations' + }, + 'enterprise': { + name: 'Enterprise Project', + multiplier: '2.0x', + description: 'Large scale, high complexity' + } + }, + timelineImpact: { + 'rush': { + name: 'Rush (< 2 weeks)', + multiplier: '+80%', + description: 'Requires overtime and priority handling' + }, + 'fast': { + name: 'Fast (2-4 weeks)', + multiplier: '+40%', + description: 'Accelerated timeline' + }, + 'standard': { + name: 'Standard (1-3 months)', + multiplier: 'Standard', + description: 'Normal project timeline' + }, + 'flexible': { + name: 'Flexible (3+ months)', + multiplier: '-20%', + description: 'Extended timeline allows optimization' + } + } + }; + + res.json({ + success: true, + pricingGuide + }); + } catch (error) { + console.error('Pricing guide error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching pricing guide' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/contact.js b/routes/contact.js new file mode 100644 index 0000000..3ffc6e6 --- /dev/null +++ b/routes/contact.js @@ -0,0 +1,250 @@ +const express = require('express'); +const router = express.Router(); +const { body, validationResult } = require('express-validator'); +const nodemailer = require('nodemailer'); +const Contact = require('../models/Contact'); +const TelegramBot = require('node-telegram-bot-api'); + +// Initialize Telegram bot if token is provided +let bot = null; +if (process.env.TELEGRAM_BOT_TOKEN) { + bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false }); +} + +// Contact form validation +const contactValidation = [ + body('name').trim().isLength({ min: 2, max: 100 }), + body('email').isEmail().normalizeEmail(), + body('subject').trim().isLength({ min: 5, max: 200 }), + body('message').trim().isLength({ min: 10, max: 2000 }), + body('phone').optional().isMobilePhone(), + body('company').optional().trim().isLength({ max: 100 }) +]; + +// Submit contact form +router.post('/submit', contactValidation, async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const contactData = { + ...req.body, + ipAddress: req.ip, + userAgent: req.get('User-Agent'), + source: 'website' + }; + + // Save to database + const contact = new Contact(contactData); + await contact.save(); + + // Send email notification + await sendEmailNotification(contact); + + // Send Telegram notification + await sendTelegramNotification(contact); + + res.json({ + success: true, + message: 'Your message has been sent successfully. We will get back to you soon!', + contactId: contact._id + }); + } catch (error) { + console.error('Contact form error:', error); + res.status(500).json({ + success: false, + message: 'Sorry, there was an error sending your message. Please try again.' + }); + } +}); + +// Get project estimate +router.post('/estimate', [ + body('services').isArray().notEmpty(), + body('projectType').notEmpty(), + body('timeline').notEmpty(), + body('budget').notEmpty(), + body('description').trim().isLength({ min: 10, max: 1000 }) +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Invalid input data', + errors: errors.array() + }); + } + + const { services, projectType, timeline, budget, description, contactInfo } = req.body; + + // Calculate estimate (simplified) + const estimate = calculateProjectEstimate(services, projectType, timeline); + + // Save inquiry to database + const contactData = { + name: contactInfo.name, + email: contactInfo.email, + phone: contactInfo.phone, + company: contactInfo.company, + subject: `Project Estimate Request - ${projectType}`, + message: `Project Description: ${description}\n\nServices: ${services.join(', ')}\nTimeline: ${timeline}\nBudget: ${budget}\n\nCalculated Estimate: ${estimate.formatted}`, + serviceInterest: projectType, + budget: budget, + timeline: timeline, + source: 'calculator', + ipAddress: req.ip, + userAgent: req.get('User-Agent') + }; + + const contact = new Contact(contactData); + await contact.save(); + + // Send notifications + await sendEmailNotification(contact); + await sendTelegramNotification(contact); + + res.json({ + success: true, + message: 'Your project estimate request has been submitted successfully!', + estimate: estimate, + contactId: contact._id + }); + } catch (error) { + console.error('Estimate request error:', error); + res.status(500).json({ + success: false, + message: 'Sorry, there was an error processing your request. Please try again.' + }); + } +}); + +// Helper function to send email notification +async function sendEmailNotification(contact) { + if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER) { + console.log('Email configuration not provided, skipping email notification'); + return; + } + + try { + const transporter = nodemailer.createTransporter({ + host: process.env.EMAIL_HOST, + port: process.env.EMAIL_PORT || 587, + secure: false, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } + }); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: process.env.ADMIN_EMAIL || process.env.EMAIL_USER, + subject: `New Contact Form Submission: ${contact.subject}`, + html: ` +

New Contact Form Submission

+

Name: ${contact.name}

+

Email: ${contact.email}

+

Phone: ${contact.phone || 'Not provided'}

+

Company: ${contact.company || 'Not provided'}

+

Subject: ${contact.subject}

+

Message:

+

${contact.message.replace(/\n/g, '
')}

+

Source: ${contact.source}

+

Submitted: ${contact.createdAt}

+
+

View in Admin Panel

+ ` + }; + + await transporter.sendMail(mailOptions); + console.log('Email notification sent successfully'); + } catch (error) { + console.error('Email notification error:', error); + } +} + +// Helper function to send Telegram notification +async function sendTelegramNotification(contact) { + if (!bot || !process.env.TELEGRAM_CHAT_ID) { + console.log('Telegram configuration not provided, skipping Telegram notification'); + return; + } + + try { + const message = ` +🔔 *New Contact Form Submission* + +👤 *Name:* ${contact.name} +📧 *Email:* ${contact.email} +📱 *Phone:* ${contact.phone || 'Not provided'} +🏢 *Company:* ${contact.company || 'Not provided'} +📝 *Subject:* ${contact.subject} + +💬 *Message:* +${contact.message} + +📍 *Source:* ${contact.source} +🕐 *Time:* ${contact.createdAt.toLocaleString()} + +[View in Admin Panel](${process.env.SITE_URL}/admin/contacts/${contact._id}) + `; + + await bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message, { + parse_mode: 'Markdown', + disable_web_page_preview: true + }); + + console.log('Telegram notification sent successfully'); + } catch (error) { + console.error('Telegram notification error:', error); + } +} + +// Helper function to calculate project estimate +function calculateProjectEstimate(services, projectType, timeline) { + const baseRates = { + 'web-development': 50000, + 'mobile-app': 80000, + 'ui-ux-design': 30000, + 'branding': 20000, + 'e-commerce': 70000, + 'consulting': 40000 + }; + + const timelineMultipliers = { + 'asap': 1.5, + '1-month': 1.2, + '1-3-months': 1.0, + '3-6-months': 0.9, + 'flexible': 0.8 + }; + + let basePrice = baseRates[projectType] || 50000; + let multiplier = timelineMultipliers[timeline] || 1.0; + + // Add service modifiers + let serviceModifier = 1.0; + if (services.length > 3) serviceModifier += 0.3; + if (services.length > 5) serviceModifier += 0.5; + + const totalEstimate = Math.round(basePrice * multiplier * serviceModifier); + const rangeMin = Math.round(totalEstimate * 0.8); + const rangeMax = Math.round(totalEstimate * 1.3); + + return { + base: totalEstimate, + min: rangeMin, + max: rangeMax, + formatted: `₩${rangeMin.toLocaleString()} - ₩${rangeMax.toLocaleString()}`, + currency: 'KRW' + }; +} + +module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..65d2ed0 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,203 @@ +const express = require('express'); +const router = express.Router(); +const Portfolio = require('../models/Portfolio'); +const Service = require('../models/Service'); +const SiteSettings = require('../models/SiteSettings'); + +// Home page +router.get('/', async (req, res) => { + try { + const [settings, featuredPortfolio, featuredServices] = await Promise.all([ + SiteSettings.findOne() || {}, + Portfolio.find({ featured: true, isPublished: true }) + .sort({ order: 1, createdAt: -1 }) + .limit(6), + Service.find({ featured: true, isActive: true }) + .sort({ order: 1 }) + .limit(4) + ]); + + res.render('index', { + title: 'SmartSolTech - Innovative Technology Solutions', + settings: settings || {}, + featuredPortfolio, + featuredServices, + currentPage: 'home' + }); + } catch (error) { + console.error('Home page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// About page +router.get('/about', async (req, res) => { + try { + const settings = await SiteSettings.findOne() || {}; + + res.render('about', { + title: 'About Us - SmartSolTech', + settings, + currentPage: 'about' + }); + } catch (error) { + console.error('About page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Portfolio page +router.get('/portfolio', async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = 12; + const skip = (page - 1) * limit; + const category = req.query.category; + + let query = { isPublished: true }; + if (category && category !== 'all') { + query.category = category; + } + + const [portfolio, total, categories] = await Promise.all([ + Portfolio.find(query) + .sort({ featured: -1, publishedAt: -1 }) + .skip(skip) + .limit(limit), + Portfolio.countDocuments(query), + Portfolio.distinct('category', { isPublished: true }) + ]); + + const totalPages = Math.ceil(total / limit); + + res.render('portfolio', { + title: 'Portfolio - SmartSolTech', + portfolio, + categories, + currentCategory: category || 'all', + pagination: { + current: page, + total: totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + }, + currentPage: 'portfolio' + }); + } catch (error) { + console.error('Portfolio page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Portfolio detail page +router.get('/portfolio/:id', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).render('404', { + title: '404 - Project Not Found', + message: 'The requested project was not found' + }); + } + + // Increment view count + portfolio.viewCount += 1; + await portfolio.save(); + + // Get related projects + const relatedProjects = await Portfolio.find({ + _id: { $ne: portfolio._id }, + category: portfolio.category, + isPublished: true + }).limit(3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - Portfolio - SmartSolTech`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); + } catch (error) { + console.error('Portfolio detail error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Services page +router.get('/services', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .sort({ featured: -1, order: 1 }) + .populate('portfolio', 'title images'); + + const categories = await Service.distinct('category', { isActive: true }); + + res.render('services', { + title: 'Services - SmartSolTech', + services, + categories, + currentPage: 'services' + }); + } catch (error) { + console.error('Services page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Calculator page +router.get('/calculator', async (req, res) => { + try { + const services = await Service.find({ isActive: true }) + .select('name pricing category') + .sort({ category: 1, name: 1 }); + + res.render('calculator', { + title: 'Project Calculator - SmartSolTech', + services, + currentPage: 'calculator' + }); + } catch (error) { + console.error('Calculator page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +// Contact page +router.get('/contact', async (req, res) => { + try { + const settings = await SiteSettings.findOne() || {}; + + res.render('contact', { + title: 'Contact Us - SmartSolTech', + settings, + currentPage: 'contact' + }); + } catch (error) { + console.error('Contact page error:', error); + res.status(500).render('error', { + title: 'Error', + message: 'Something went wrong' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/media.js b/routes/media.js new file mode 100644 index 0000000..d5b7100 --- /dev/null +++ b/routes/media.js @@ -0,0 +1,345 @@ +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; + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: 'Authentication required' + }); + } + next(); +}; + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: async (req, file, cb) => { + const uploadPath = path.join(__dirname, '../public/uploads'); + try { + await fs.mkdir(uploadPath, { recursive: true }); + cb(null, uploadPath); + } catch (error) { + cb(error); + } + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + cb(null, file.fieldname + '-' + uniqueSuffix + ext); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB + files: 10 + }, + fileFilter: (req, file, cb) => { + // Allow images only + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed'), false); + } + } +}); + +// Upload single image +router.post('/upload', requireAuth, upload.single('image'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: 'No file uploaded' + }); + } + + const originalPath = req.file.path; + const filename = req.file.filename; + const nameWithoutExt = path.parse(filename).name; + + // Create optimized versions + const sizes = { + thumbnail: { width: 300, height: 200 }, + medium: { width: 800, height: 600 }, + large: { width: 1200, height: 900 } + }; + + const optimizedImages = {}; + + for (const [sizeName, dimensions] of Object.entries(sizes)) { + const outputPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-${sizeName}.webp` + ); + + await sharp(originalPath) + .resize(dimensions.width, dimensions.height, { + fit: 'inside', + withoutEnlargement: true + }) + .webp({ quality: 85 }) + .toFile(outputPath); + + optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; + } + + // Keep original as well (converted to webp for better compression) + const originalWebpPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-original.webp` + ); + + await sharp(originalPath) + .webp({ quality: 90 }) + .toFile(originalWebpPath); + + optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; + + // Remove the original uploaded file + await fs.unlink(originalPath); + + res.json({ + success: true, + message: 'Image uploaded and optimized successfully', + images: optimizedImages, + metadata: { + originalName: req.file.originalname, + mimeType: req.file.mimetype, + size: req.file.size + } + }); + } catch (error) { + console.error('Image upload error:', error); + + // Clean up files on error + if (req.file && req.file.path) { + try { + await fs.unlink(req.file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + + res.status(500).json({ + success: false, + message: 'Error uploading image' + }); + } +}); + +// Upload multiple images +router.post('/upload-multiple', requireAuth, upload.array('images', 10), async (req, res) => { + try { + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + success: false, + message: 'No files uploaded' + }); + } + + const uploadedImages = []; + + for (const file of req.files) { + try { + const originalPath = file.path; + const filename = file.filename; + const nameWithoutExt = path.parse(filename).name; + + // Create optimized versions + const sizes = { + thumbnail: { width: 300, height: 200 }, + medium: { width: 800, height: 600 }, + large: { width: 1200, height: 900 } + }; + + const optimizedImages = {}; + + for (const [sizeName, dimensions] of Object.entries(sizes)) { + const outputPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-${sizeName}.webp` + ); + + await sharp(originalPath) + .resize(dimensions.width, dimensions.height, { + fit: 'inside', + withoutEnlargement: true + }) + .webp({ quality: 85 }) + .toFile(outputPath); + + optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; + } + + // Original as webp + const originalWebpPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-original.webp` + ); + + await sharp(originalPath) + .webp({ quality: 90 }) + .toFile(originalWebpPath); + + optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; + + uploadedImages.push({ + originalName: file.originalname, + images: optimizedImages, + metadata: { + mimeType: file.mimetype, + size: file.size + } + }); + + // Remove original file + await fs.unlink(originalPath); + } catch (fileError) { + console.error(`Error processing file ${file.originalname}:`, fileError); + // Clean up this file and continue with others + try { + await fs.unlink(file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + } + + res.json({ + success: true, + message: `${uploadedImages.length} images uploaded and optimized successfully`, + images: uploadedImages + }); + } catch (error) { + console.error('Multiple images upload error:', error); + + // Clean up files on error + if (req.files) { + for (const file of req.files) { + try { + await fs.unlink(file.path); + } catch (unlinkError) { + console.error('Error removing file:', unlinkError); + } + } + } + + res.status(500).json({ + success: false, + message: 'Error uploading images' + }); + } +}); + +// Delete image +router.delete('/:filename', requireAuth, async (req, res) => { + try { + const filename = req.params.filename; + const uploadPath = path.join(__dirname, '../public/uploads'); + + // Security check - ensure filename doesn't contain path traversal + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return res.status(400).json({ + success: false, + message: 'Invalid filename' + }); + } + + const filePath = path.join(uploadPath, filename); + + try { + await fs.access(filePath); + await fs.unlink(filePath); + + res.json({ + success: true, + message: 'Image deleted successfully' + }); + } catch (error) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: 'Image not found' + }); + } + throw error; + } + } catch (error) { + console.error('Image deletion error:', error); + res.status(500).json({ + success: false, + message: 'Error deleting image' + }); + } +}); + +// List uploaded images +router.get('/list', requireAuth, async (req, res) => { + try { + const uploadPath = path.join(__dirname, '../public/uploads'); + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + + const files = await fs.readdir(uploadPath); + const imageFiles = files.filter(file => + /\.(jpg|jpeg|png|gif|webp)$/i.test(file) + ); + + const total = imageFiles.length; + const totalPages = Math.ceil(total / limit); + const start = (page - 1) * limit; + const end = start + limit; + + const paginatedFiles = imageFiles.slice(start, end); + + const imagesWithStats = await Promise.all( + paginatedFiles.map(async (file) => { + try { + const filePath = path.join(uploadPath, file); + const stats = await fs.stat(filePath); + + return { + filename: file, + url: `/uploads/${file}`, + size: stats.size, + modified: stats.mtime, + isImage: true + }; + } catch (error) { + console.error(`Error getting stats for ${file}:`, error); + return null; + } + }) + ); + + const validImages = imagesWithStats.filter(img => img !== null); + + res.json({ + success: true, + images: validImages, + pagination: { + current: page, + total: totalPages, + limit, + totalItems: total, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('List images error:', error); + res.status(500).json({ + success: false, + message: 'Error listing images' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/portfolio.js b/routes/portfolio.js new file mode 100644 index 0000000..0164537 --- /dev/null +++ b/routes/portfolio.js @@ -0,0 +1,196 @@ +const express = require('express'); +const router = express.Router(); +const Portfolio = require('../models/Portfolio'); + +// Get all portfolio items +router.get('/', async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 12; + const skip = (page - 1) * limit; + const category = req.query.category; + const search = req.query.search; + const featured = req.query.featured; + + // Build query + let query = { isPublished: true }; + + if (category && category !== 'all') { + query.category = category; + } + + if (featured === 'true') { + query.featured = true; + } + + if (search) { + query.$text = { $search: search }; + } + + // Get portfolio items + const [portfolio, total] = await Promise.all([ + Portfolio.find(query) + .sort({ featured: -1, publishedAt: -1 }) + .skip(skip) + .limit(limit) + .select('title shortDescription category technologies images status publishedAt viewCount'), + Portfolio.countDocuments(query) + ]); + + const totalPages = Math.ceil(total / limit); + + res.json({ + success: true, + portfolio, + pagination: { + current: page, + total: totalPages, + limit, + totalItems: total, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('Portfolio API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching portfolio' + }); + } +}); + +// Get single portfolio item +router.get('/:id', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).json({ + success: false, + message: 'Portfolio item not found' + }); + } + + // Increment view count + portfolio.viewCount += 1; + await portfolio.save(); + + // Get related projects + const relatedProjects = await Portfolio.find({ + _id: { $ne: portfolio._id }, + category: portfolio.category, + isPublished: true + }) + .select('title shortDescription images') + .limit(4); + + res.json({ + success: true, + portfolio, + relatedProjects + }); + } catch (error) { + console.error('Portfolio detail API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching portfolio item' + }); + } +}); + +// Get portfolio categories +router.get('/meta/categories', async (req, res) => { + try { + const categories = await Portfolio.distinct('category', { isPublished: true }); + + // Get count for each category + const categoriesWithCount = await Promise.all( + categories.map(async (category) => { + const count = await Portfolio.countDocuments({ + category, + isPublished: true + }); + return { category, count }; + }) + ); + + res.json({ + success: true, + categories: categoriesWithCount + }); + } catch (error) { + console.error('Portfolio categories API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching categories' + }); + } +}); + +// Like portfolio item +router.post('/:id/like', async (req, res) => { + try { + const portfolio = await Portfolio.findById(req.params.id); + + if (!portfolio || !portfolio.isPublished) { + return res.status(404).json({ + success: false, + message: 'Portfolio item not found' + }); + } + + portfolio.likes += 1; + await portfolio.save(); + + res.json({ + success: true, + likes: portfolio.likes + }); + } catch (error) { + console.error('Portfolio like API error:', error); + res.status(500).json({ + success: false, + message: 'Error liking portfolio item' + }); + } +}); + +// Search portfolio +router.get('/search/:term', async (req, res) => { + try { + const searchTerm = req.params.term; + const limit = parseInt(req.query.limit) || 10; + + const portfolio = await Portfolio.find({ + $and: [ + { isPublished: true }, + { + $or: [ + { title: { $regex: searchTerm, $options: 'i' } }, + { description: { $regex: searchTerm, $options: 'i' } }, + { technologies: { $in: [new RegExp(searchTerm, 'i')] } } + ] + } + ] + }) + .select('title shortDescription category images') + .sort({ featured: -1, publishedAt: -1 }) + .limit(limit); + + res.json({ + success: true, + portfolio, + searchTerm, + count: portfolio.length + }); + } catch (error) { + console.error('Portfolio search API error:', error); + res.status(500).json({ + success: false, + message: 'Error searching portfolio' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/services.js b/routes/services.js new file mode 100644 index 0000000..d6ae97c --- /dev/null +++ b/routes/services.js @@ -0,0 +1,141 @@ +const express = require('express'); +const router = express.Router(); +const Service = require('../models/Service'); +const Portfolio = require('../models/Portfolio'); + +// Get all services +router.get('/', async (req, res) => { + try { + const category = req.query.category; + const featured = req.query.featured; + + let query = { isActive: true }; + + if (category && category !== 'all') { + query.category = category; + } + + if (featured === 'true') { + query.featured = true; + } + + const services = await Service.find(query) + .populate('portfolio', 'title images') + .sort({ featured: -1, order: 1 }); + + res.json({ + success: true, + services + }); + } catch (error) { + console.error('Services API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching services' + }); + } +}); + +// Get single service +router.get('/:id', async (req, res) => { + try { + const service = await Service.findById(req.params.id) + .populate('portfolio', 'title shortDescription images category'); + + if (!service || !service.isActive) { + return res.status(404).json({ + success: false, + message: 'Service not found' + }); + } + + // Get related services + const relatedServices = await Service.find({ + _id: { $ne: service._id }, + category: service.category, + isActive: true + }) + .select('name shortDescription icon pricing') + .limit(3); + + res.json({ + success: true, + service, + relatedServices + }); + } catch (error) { + console.error('Service detail API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching service' + }); + } +}); + +// Get service categories +router.get('/meta/categories', async (req, res) => { + try { + const categories = await Service.distinct('category', { isActive: true }); + + // Get count for each category + const categoriesWithCount = await Promise.all( + categories.map(async (category) => { + const count = await Service.countDocuments({ + category, + isActive: true + }); + return { category, count }; + }) + ); + + res.json({ + success: true, + categories: categoriesWithCount + }); + } catch (error) { + console.error('Service categories API error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching categories' + }); + } +}); + +// Search services +router.get('/search/:term', async (req, res) => { + try { + const searchTerm = req.params.term; + const limit = parseInt(req.query.limit) || 10; + + const services = await Service.find({ + $and: [ + { isActive: true }, + { + $or: [ + { name: { $regex: searchTerm, $options: 'i' } }, + { description: { $regex: searchTerm, $options: 'i' } }, + { tags: { $in: [new RegExp(searchTerm, 'i')] } } + ] + } + ] + }) + .select('name shortDescription icon pricing category') + .sort({ featured: -1, order: 1 }) + .limit(limit); + + res.json({ + success: true, + services, + searchTerm, + count: services.length + }); + } catch (error) { + console.error('Service search API error:', error); + res.status(500).json({ + success: false, + message: 'Error searching services' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..2b54f55 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,279 @@ +#!/usr/bin/env node + +/** + * Build script for production deployment + * Handles CSS compilation, asset optimization, and production setup + */ + +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { promisify } = require('util'); + +const execAsync = promisify(exec); + +// Configuration +const BUILD_DIR = path.join(__dirname, '..', 'dist'); +const PUBLIC_DIR = path.join(__dirname, '..', 'public'); +const VIEWS_DIR = path.join(__dirname, '..', 'views'); + +async function buildForProduction() { + try { + console.log('🏗️ Starting production build...'); + + // Create build directory + await createBuildDirectory(); + + // Install production dependencies + await installProductionDependencies(); + + // Optimize assets + await optimizeAssets(); + + // Generate service worker with workbox + await generateServiceWorker(); + + // Create production environment file + await createProductionEnv(); + + // Copy necessary files + await copyProductionFiles(); + + console.log('✅ Production build completed successfully!'); + console.log('📦 Build output available in:', BUILD_DIR); + console.log('🚀 Ready for deployment!'); + + } catch (error) { + console.error('❌ Production build failed:', error); + process.exit(1); + } +} + +async function createBuildDirectory() { + try { + if (fs.existsSync(BUILD_DIR)) { + console.log('🗑️ Cleaning existing build directory...'); + await execAsync(`rm -rf ${BUILD_DIR}`); + } + + fs.mkdirSync(BUILD_DIR, { recursive: true }); + console.log('📁 Created build directory'); + } catch (error) { + console.error('❌ Error creating build directory:', error); + throw error; + } +} + +async function installProductionDependencies() { + try { + console.log('📦 Installing production dependencies...'); + await execAsync('npm ci --only=production'); + console.log('✅ Production dependencies installed'); + } catch (error) { + console.error('❌ Error installing dependencies:', error); + throw error; + } +} + +async function optimizeAssets() { + try { + console.log('🎨 Optimizing CSS and JavaScript...'); + + // Create optimized CSS + const cssContent = fs.readFileSync(path.join(PUBLIC_DIR, 'css', 'main.css'), 'utf8'); + const optimizedCSS = await optimizeCSS(cssContent); + + const buildCSSDir = path.join(BUILD_DIR, 'public', 'css'); + fs.mkdirSync(buildCSSDir, { recursive: true }); + fs.writeFileSync(path.join(buildCSSDir, 'main.min.css'), optimizedCSS); + + // Create optimized JavaScript + const jsContent = fs.readFileSync(path.join(PUBLIC_DIR, 'js', 'main.js'), 'utf8'); + const optimizedJS = await optimizeJS(jsContent); + + const buildJSDir = path.join(BUILD_DIR, 'public', 'js'); + fs.mkdirSync(buildJSDir, { recursive: true }); + fs.writeFileSync(path.join(buildJSDir, 'main.min.js'), optimizedJS); + + // Copy other assets + await copyDirectory(path.join(PUBLIC_DIR, 'images'), path.join(BUILD_DIR, 'public', 'images')); + + console.log('✅ Assets optimized'); + } catch (error) { + console.error('❌ Error optimizing assets:', error); + throw error; + } +} + +async function optimizeCSS(css) { + // Simple CSS minification (remove comments, extra whitespace) + return css + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments + .replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space + .replace(/;\s*}/g, '}') // Remove semicolon before closing brace + .replace(/\s*{\s*/g, '{') // Remove spaces around opening brace + .replace(/;\s*/g, ';') // Remove spaces after semicolons + .replace(/,\s*/g, ',') // Remove spaces after commas + .trim(); +} + +async function optimizeJS(js) { + // Simple JS minification (remove comments, extra whitespace) + return js + .replace(/\/\/.*$/gm, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments + .replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space + .replace(/\n\s*/g, '') // Remove newlines and indentation + .trim(); +} + +async function generateServiceWorker() { + try { + console.log('⚙️ Generating optimized service worker...'); + + const swContent = fs.readFileSync(path.join(PUBLIC_DIR, 'sw.js'), 'utf8'); + const optimizedSW = await optimizeJS(swContent); + + fs.writeFileSync(path.join(BUILD_DIR, 'public', 'sw.js'), optimizedSW); + + // Copy manifest + fs.copyFileSync( + path.join(PUBLIC_DIR, 'manifest.json'), + path.join(BUILD_DIR, 'public', 'manifest.json') + ); + + console.log('✅ Service worker generated'); + } catch (error) { + console.error('❌ Error generating service worker:', error); + throw error; + } +} + +async function createProductionEnv() { + try { + console.log('🔧 Creating production environment configuration...'); + + const prodEnv = ` +# Production Environment Configuration +NODE_ENV=production +PORT=3000 + +# Database +MONGODB_URI=mongodb://localhost:27017/smartsoltech + +# Security +SESSION_SECRET=your-super-secret-session-key-change-this-in-production +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production + +# File Upload +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password + +# Telegram Bot (Optional) +TELEGRAM_BOT_TOKEN=your-telegram-bot-token + +# Admin Account +ADMIN_EMAIL=admin@smartsoltech.kr +ADMIN_PASSWORD=change-this-password + +# Security Headers +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 +`.trim(); + + fs.writeFileSync(path.join(BUILD_DIR, '.env.production'), prodEnv); + console.log('✅ Production environment file created'); + } catch (error) { + console.error('❌ Error creating production environment:', error); + throw error; + } +} + +async function copyProductionFiles() { + try { + console.log('📋 Copying production files...'); + + // Copy main application files + const filesToCopy = [ + 'server.js', + 'package.json', + 'package-lock.json' + ]; + + for (const file of filesToCopy) { + fs.copyFileSync( + path.join(__dirname, '..', file), + path.join(BUILD_DIR, file) + ); + } + + // Copy directories + await copyDirectory( + path.join(__dirname, '..', 'models'), + path.join(BUILD_DIR, 'models') + ); + + await copyDirectory( + path.join(__dirname, '..', 'routes'), + path.join(BUILD_DIR, 'routes') + ); + + await copyDirectory( + path.join(__dirname, '..', 'views'), + path.join(BUILD_DIR, 'views') + ); + + await copyDirectory( + path.join(__dirname, '..', 'middleware'), + path.join(BUILD_DIR, 'middleware') + ); + + // Create uploads directory + fs.mkdirSync(path.join(BUILD_DIR, 'uploads'), { recursive: true }); + + console.log('✅ Production files copied'); + } catch (error) { + console.error('❌ Error copying production files:', error); + throw error; + } +} + +async function copyDirectory(src, dest) { + try { + if (!fs.existsSync(src)) { + return; + } + + fs.mkdirSync(dest, { recursive: true }); + + const items = fs.readdirSync(src); + + for (const item of items) { + const srcPath = path.join(src, item); + const destPath = path.join(dest, item); + + const stat = fs.statSync(srcPath); + + if (stat.isDirectory()) { + await copyDirectory(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } catch (error) { + console.error(`❌ Error copying directory ${src}:`, error); + throw error; + } +} + +if (require.main === module) { + buildForProduction(); +} + +module.exports = { buildForProduction }; \ No newline at end of file diff --git a/scripts/dev.js b/scripts/dev.js new file mode 100644 index 0000000..0a9d859 --- /dev/null +++ b/scripts/dev.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +/** + * Development server with hot reload for SmartSolTech + * Uses nodemon for automatic server restart on file changes + */ + +const path = require('path'); +const { spawn } = require('child_process'); + +// Configuration +const SERVER_FILE = path.join(__dirname, '..', 'server.js'); +const NODEMON_CONFIG = { + script: SERVER_FILE, + ext: 'js,json,ejs', + ignore: ['node_modules/', 'public/', '.git/'], + watch: ['models/', 'routes/', 'views/', 'server.js'], + env: { + NODE_ENV: 'development', + DEBUG: 'app:*' + }, + delay: 1000 +}; + +function startDevelopmentServer() { + console.log('🚀 Starting SmartSolTech development server...'); + console.log('📁 Watching files for changes...'); + console.log('🔄 Server will automatically restart on file changes'); + console.log(''); + + const nodemonArgs = [ + '--script', NODEMON_CONFIG.script, + '--ext', NODEMON_CONFIG.ext, + '--ignore', NODEMON_CONFIG.ignore.join(','), + '--watch', NODEMON_CONFIG.watch.join(','), + '--delay', NODEMON_CONFIG.delay.toString() + ]; + + const nodemon = spawn('npx', ['nodemon', ...nodemonArgs], { + stdio: 'inherit', + env: { + ...process.env, + ...NODEMON_CONFIG.env + } + }); + + nodemon.on('close', (code) => { + console.log(`\n🛑 Development server exited with code ${code}`); + process.exit(code); + }); + + nodemon.on('error', (error) => { + console.error('❌ Error starting development server:', error); + process.exit(1); + }); + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('\n🛑 Shutting down development server...'); + nodemon.kill('SIGINT'); + }); + + process.on('SIGTERM', () => { + console.log('\n🛑 Shutting down development server...'); + nodemon.kill('SIGTERM'); + }); +} + +if (require.main === module) { + startDevelopmentServer(); +} + +module.exports = { startDevelopmentServer }; \ No newline at end of file diff --git a/scripts/init-db.js b/scripts/init-db.js new file mode 100644 index 0000000..5c4d272 --- /dev/null +++ b/scripts/init-db.js @@ -0,0 +1,498 @@ +#!/usr/bin/env node + +/** + * Database initialization script for SmartSolTech + * Creates initial admin user and sample data + */ + +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); +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'); + +// 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'); + + // Create admin user + await createAdminUser(); + + // Create sample services + await createSampleServices(); + + // Create sample portfolio items + await createSamplePortfolio(); + + // Create site settings + await createSiteSettings(); + + console.log('🎉 Database initialization completed successfully!'); + console.log(`📧 Admin login: ${ADMIN_EMAIL}`); + console.log(`🔑 Admin password: ${ADMIN_PASSWORD}`); + console.log('⚠️ Please change the admin password after first login!'); + + } catch (error) { + console.error('❌ Database initialization failed:', error); + process.exit(1); + } finally { + await mongoose.connection.close(); + console.log('🔌 Database connection closed'); + process.exit(0); + } +} + +async function createAdminUser() { + try { + const existingAdmin = await User.findOne({ email: ADMIN_EMAIL }); + + if (existingAdmin) { + console.log('👤 Admin user already exists, skipping...'); + return; + } + + const adminUser = new User({ + name: 'Administrator', + email: ADMIN_EMAIL, + password: ADMIN_PASSWORD, + role: 'admin', + isActive: true + }); + + await adminUser.save(); + console.log('✅ Admin user created successfully'); + } catch (error) { + console.error('❌ Error creating admin user:', error); + throw error; + } +} + +async function createSampleServices() { + try { + const existingServices = await Service.countDocuments(); + + if (existingServices > 0) { + console.log('🛠️ Services already exist, skipping...'); + return; + } + + const services = [ + { + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + features: [ + { name: '반응형 디자인', description: '모든 디바이스에서 완벽하게 작동', included: true }, + { name: 'SEO 최적화', description: '검색엔진 최적화로 더 많은 방문자 유치', included: true }, + { name: '성능 최적화', description: '빠른 로딩 속도와 원활한 사용자 경험', included: true }, + { name: '보안 강화', description: '최신 보안 기술로 안전한 웹사이트', included: true }, + { name: '유지보수', description: '6개월 무료 유지보수 지원', included: true } + ], + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + estimatedTime: { min: 2, max: 8, unit: 'weeks' }, + featured: true, + isActive: true, + order: 1, + tags: ['React', 'Node.js', 'MongoDB', 'Express', 'JavaScript', 'HTML', 'CSS'] + }, + { + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + features: [ + { name: '크로스플랫폼 개발', description: 'iOS와 Android 동시 지원', included: true }, + { name: '네이티브 성능', description: '최적화된 성능과 사용자 경험', included: true }, + { name: '푸시 알림', description: '실시간 푸시 알림 시스템', included: true }, + { name: '오프라인 지원', description: '인터넷 연결 없이도 기본 기능 사용 가능', included: false }, + { name: '앱스토어 등록', description: '앱스토어 및 플레이스토어 등록 지원', included: true } + ], + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + estimatedTime: { min: 4, max: 16, unit: 'weeks' }, + featured: true, + isActive: true, + order: 2, + tags: ['React Native', 'Flutter', 'iOS', 'Android', 'Swift', 'Kotlin', 'JavaScript'] + }, + { + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + features: [ + { name: '사용자 경험 연구', description: '타겟 사용자 분석 및 페르소나 설정', included: true }, + { name: '와이어프레임', description: '정보 구조와 레이아웃 설계', included: true }, + { name: '프로토타이핑', description: '인터랙티브 프로토타입 제작', included: true }, + { name: '비주얼 디자인', description: '브랜드에 맞는 시각적 디자인', included: true }, + { name: '디자인 시스템', description: '일관된 디자인을 위한 가이드라인', included: false } + ], + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + estimatedTime: { min: 1, max: 6, unit: 'weeks' }, + featured: true, + isActive: true, + order: 3, + tags: ['Figma', 'Sketch', 'Adobe XD', 'Photoshop', 'Illustrator', 'Prototyping'] + }, + { + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + features: [ + { name: 'SEO 최적화', description: '검색엔진 상위 노출을 위한 최적화', included: true }, + { name: '소셜미디어 관리', description: '페이스북, 인스타그램, 유튜브 관리', included: true }, + { name: '구글 광고', description: '구글 애즈를 통한 타겟 광고', included: true }, + { name: '콘텐츠 마케팅', description: '블로그 및 콘텐츠 제작', included: false }, + { name: '분석 및 리포팅', description: '마케팅 성과 분석 및 보고서', included: true } + ], + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + estimatedTime: { min: 2, max: 12, unit: 'weeks' }, + featured: true, + isActive: true, + order: 4, + tags: ['SEO', 'Google Ads', 'Facebook Ads', 'Analytics', 'Social Media', 'Content Marketing'] + }, + { + name: '브랜딩', + description: '기업의 정체성을 반영하는 로고, 브랜드 가이드라인, 마케팅 자료를 디자인합니다. 일관된 브랜드 이미지로 브랜드 가치를 높입니다.', + shortDescription: '로고, 브랜드 가이드라인, 마케팅 자료 디자인', + icon: 'fas fa-copyright', + category: 'design', + features: [ + { name: '로고 디자인', description: '기업 정체성을 반영한 로고 제작', included: true }, + { name: '브랜드 가이드라인', description: '색상, 폰트, 사용법 가이드', included: true }, + { name: '명함 디자인', description: '브랜드에 맞는 명함 디자인', included: true }, + { name: '브로슈어 디자인', description: '회사 소개 브로슈어 제작', included: false }, + { name: '웹 브랜딩', description: '웹사이트 브랜딩 요소 적용', included: false } + ], + pricing: { + basePrice: 400000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 400000, max: 2500000 } + }, + estimatedTime: { min: 2, max: 8, unit: 'weeks' }, + featured: false, + isActive: true, + order: 5, + tags: ['Logo Design', 'Brand Identity', 'Graphic Design', 'Corporate Design'] + }, + { + name: '기술 컨설팅', + description: '기업의 디지털 전환과 기술 도입을 위한 전문적인 컨설팅을 제공합니다. 기술 아키텍처 설계부터 구현 전략까지 포괄적으로 지원합니다.', + shortDescription: '디지털 전환 및 기술 도입을 위한 전문 컨설팅', + icon: 'fas fa-lightbulb', + category: 'consulting', + features: [ + { name: '기술 분석', description: '현재 기술 스택 분석 및 개선안 제시', included: true }, + { name: '아키텍처 설계', description: '확장 가능한 시스템 아키텍처 설계', included: true }, + { name: '개발 프로세스 개선', description: '효율적인 개발 워크플로우 구축', included: true }, + { name: '팀 교육', description: '개발팀 대상 기술 교육', included: false }, + { name: '지속적인 지원', description: '프로젝트 완료 후 지속적인 기술 지원', included: false } + ], + pricing: { + basePrice: 150000, + currency: 'KRW', + priceType: 'hourly', + priceRange: { min: 150000, max: 500000 } + }, + estimatedTime: { min: 1, max: 4, unit: 'weeks' }, + featured: false, + isActive: true, + order: 6, + tags: ['Architecture', 'Strategy', 'Process', 'Training', 'Consultation'] + } + ]; + + await Service.insertMany(services); + console.log('✅ Sample services created successfully'); + } catch (error) { + console.error('❌ Error creating sample services:', error); + throw error; + } +} + +async function createSamplePortfolio() { + try { + const existingPortfolio = await Portfolio.countDocuments(); + + if (existingPortfolio > 0) { + console.log('🎨 Portfolio items already exist, skipping...'); + return; + } + + const portfolioItems = [ + { + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }, + { url: '/images/portfolio/ecommerce-2.jpg', alt: '상품 상세페이지', isPrimary: false }, + { url: '/images/portfolio/ecommerce-3.jpg', alt: '장바구니 페이지', isPrimary: false } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.com', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25, + order: 1, + seo: { + metaTitle: 'E-commerce 플랫폼 개발 프로젝트', + metaDescription: '현대적인 온라인 쇼핑몰 플랫폼 개발 사례', + keywords: ['E-commerce', 'React', 'Node.js', '온라인쇼핑몰'] + } + }, + { + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }, + { url: '/images/portfolio/fitness-2.jpg', alt: '운동 계획 화면', isPrimary: false }, + { url: '/images/portfolio/fitness-3.jpg', alt: '통계 화면', isPrimary: false } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35, + order: 2, + seo: { + metaTitle: '모바일 피트니스 앱 개발 프로젝트', + metaDescription: 'React Native로 개발한 크로스플랫폼 피트니스 앱', + keywords: ['Mobile App', 'React Native', 'Fitness', '헬스케어'] + } + }, + { + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }, + { url: '/images/portfolio/corporate-2.jpg', alt: '회사소개 페이지', isPrimary: false }, + { url: '/images/portfolio/corporate-3.jpg', alt: '서비스 페이지', isPrimary: false } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.com', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18, + order: 3, + seo: { + metaTitle: '기업 웹사이트 리뉴얼 프로젝트', + metaDescription: '모던한 디자인과 향상된 UX의 기업 웹사이트', + keywords: ['Web Design', 'Corporate', 'UI/UX', '웹사이트리뉴얼'] + } + }, + { + title: '레스토랑 예약 시스템', + description: '레스토랑을 위한 온라인 예약 시스템을 개발했습니다. 실시간 테이블 현황, 예약 관리, 고객 관리 기능을 포함하여 레스토랑 운영 효율성을 높였습니다.', + shortDescription: '실시간 테이블 예약 및 관리 시스템', + category: 'web-development', + technologies: ['Vue.js', 'Laravel', 'MySQL', 'Socket.io', 'Bootstrap'], + images: [ + { url: '/images/portfolio/restaurant-1.jpg', alt: '예약 시스템 메인', isPrimary: true }, + { url: '/images/portfolio/restaurant-2.jpg', alt: '예약 현황 관리', isPrimary: false } + ], + clientName: '레스토랑 델리셔스', + status: 'completed', + featured: false, + publishedAt: new Date('2024-04-05'), + completedAt: new Date('2024-04-01'), + isPublished: true, + viewCount: 85, + likes: 12, + order: 4, + seo: { + metaTitle: '레스토랑 예약 시스템 개발', + metaDescription: '실시간 테이블 예약 및 관리 웹 시스템', + keywords: ['Reservation System', 'Vue.js', 'Laravel', '예약시스템'] + } + }, + { + title: '교육 플랫폼 앱', + description: '온라인 교육을 위한 모바일 애플리케이션입니다. 동영상 강의, 퀴즈, 진도 관리 기능을 포함하여 효과적인 학습 환경을 제공합니다.', + shortDescription: '온라인 학습을 위한 교육 플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['Flutter', 'Dart', 'Firebase', 'FFmpeg', 'AWS S3'], + images: [ + { url: '/images/portfolio/education-1.jpg', alt: '교육 앱 메인화면', isPrimary: true }, + { url: '/images/portfolio/education-2.jpg', alt: '강의 재생 화면', isPrimary: false } + ], + clientName: '온라인 교육 기업 EduTech', + status: 'completed', + featured: false, + publishedAt: new Date('2024-05-12'), + completedAt: new Date('2024-05-08'), + isPublished: true, + viewCount: 95, + likes: 20, + order: 5, + seo: { + metaTitle: '교육 플랫폼 모바일 앱 개발', + metaDescription: 'Flutter로 개발한 온라인 교육 모바일 앱', + keywords: ['Education App', 'Flutter', 'E-learning', '교육앱'] + } + }, + { + title: 'IoT 대시보드', + description: 'IoT 디바이스들을 모니터링하고 제어할 수 있는 웹 대시보드를 개발했습니다. 실시간 데이터 시각화와 알림 기능을 포함합니다.', + shortDescription: 'IoT 디바이스 모니터링 및 제어 웹 대시보드', + category: 'web-development', + technologies: ['React', 'D3.js', 'Node.js', 'MQTT', 'InfluxDB', 'Docker'], + images: [ + { url: '/images/portfolio/iot-1.jpg', alt: 'IoT 대시보드 메인', isPrimary: true } + ], + clientName: 'IoT 솔루션 기업 SmartDevice', + status: 'in-progress', + featured: false, + publishedAt: new Date('2024-06-01'), + isPublished: true, + viewCount: 45, + likes: 8, + order: 6, + seo: { + metaTitle: 'IoT 대시보드 개발 프로젝트', + metaDescription: '실시간 IoT 디바이스 모니터링 웹 대시보드', + keywords: ['IoT', 'Dashboard', 'Real-time', 'Monitoring'] + } + } + ]; + + await Portfolio.insertMany(portfolioItems); + console.log('✅ Sample portfolio items created successfully'); + } catch (error) { + console.error('❌ Error creating sample portfolio:', error); + throw error; + } +} + +async function createSiteSettings() { + try { + const existingSettings = await SiteSettings.findOne(); + + if (existingSettings) { + console.log('⚙️ Site settings already exist, skipping...'); + return; + } + + const settings = new SiteSettings({ + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + logo: '/images/logo.png', + favicon: '/images/favicon.ico', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech', + github: 'https://github.com/smartsoltech' + }, + telegram: { + isEnabled: false + }, + seo: { + metaTitle: 'SmartSolTech - 혁신적인 기술 솔루션', + metaDescription: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅 전문 기업. 한국에서 최고의 기술 솔루션을 제공합니다.', + keywords: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅, 기술 솔루션, 한국, SmartSolTech' + }, + hero: { + title: 'Smart Technology Solutions', + subtitle: '혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다', + backgroundImage: '/images/hero-bg.jpg', + ctaText: '프로젝트 시작하기', + ctaLink: '/contact' + }, + about: { + title: 'SmartSolTech 소개', + description: '우리는 최신 기술과 창의적인 아이디어로 고객의 비즈니스 성장을 지원하는 전문 개발팀입니다. 웹 개발부터 모바일 앱, UI/UX 디자인까지 포괄적인 디지털 솔루션을 제공합니다.', + image: '/images/about.jpg' + }, + maintenance: { + isEnabled: false, + message: '현재 시스템 점검 중입니다. 잠시 후 다시 접속해 주세요.' + } + }); + + await settings.save(); + console.log('✅ Site settings created successfully'); + } catch (error) { + console.error('❌ Error creating site settings:', error); + throw error; + } +} + +// Run the initialization +if (require.main === module) { + initializeDatabase(); +} + +module.exports = { + initializeDatabase, + createAdminUser, + createSampleServices, + createSamplePortfolio, + createSiteSettings +}; \ No newline at end of file diff --git a/server-demo.js b/server-demo.js new file mode 100644 index 0000000..0a61a18 --- /dev/null +++ b/server-demo.js @@ -0,0 +1,486 @@ +/** + * Demo server for SmartSolTech website + * Uses mock data instead of MongoDB for demonstration + */ + +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const flash = require('connect-flash'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configure i18n +i18n.configure({ + locales: ['en', 'ko', 'ru', 'kk'], + directory: path.join(__dirname, 'locales'), + defaultLocale: 'ko', + cookie: 'language', + queryParameter: 'lang', + autoReload: true, + syncFiles: true, + objectNotation: true +}); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://cdnjs.cloudflare.com"], + connectSrc: ["'self'", "https:"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"] + } + } +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.' +}); +app.use(limiter); + +// Compression +app.use(compression()); + +// View engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Initialize i18n +app.use(i18n.init); + +// Session configuration +app.use(session({ + secret: 'demo-secret-key', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Flash messages +app.use(flash()); + +// Global variables for templates +app.use((req, res, next) => { + res.locals.success_msg = req.flash('success'); + res.locals.error_msg = req.flash('error'); + res.locals.error = req.flash('error'); + res.locals.currentYear = new Date().getFullYear(); + next(); +}); + +// Mock data +const mockPortfolio = [ + { + _id: '1', + title: 'E-commerce 플랫폼', + description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.', + shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발', + category: 'web-development', + technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'], + images: [ + { url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true } + ], + clientName: '패션 브랜드 ABC', + projectUrl: 'https://example-ecommerce.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-01-15'), + completedAt: new Date('2024-01-10'), + isPublished: true, + viewCount: 150, + likes: 25 + }, + { + _id: '2', + title: '모바일 피트니스 앱', + description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.', + shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱', + category: 'mobile-app', + technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'], + images: [ + { url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true } + ], + clientName: '헬스케어 스타트업 FIT', + status: 'completed', + featured: true, + publishedAt: new Date('2024-02-20'), + completedAt: new Date('2024-02-15'), + isPublished: true, + viewCount: 200, + likes: 35 + }, + { + _id: '3', + title: '기업 웹사이트 리뉴얼', + description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.', + shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼', + category: 'ui-ux-design', + technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'], + images: [ + { url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true } + ], + clientName: '기술 기업 TechCorp', + projectUrl: 'https://example-corp.demo', + status: 'completed', + featured: true, + publishedAt: new Date('2024-03-10'), + completedAt: new Date('2024-03-05'), + isPublished: true, + viewCount: 120, + likes: 18 + } +]; + +const mockServices = [ + { + _id: '1', + name: '웹 개발', + description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.', + shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발', + icon: 'fas fa-code', + category: 'development', + pricing: { + basePrice: 500000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 500000, max: 5000000 } + }, + featured: true, + isActive: true + }, + { + _id: '2', + name: '모바일 앱 개발', + description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.', + shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발', + icon: 'fas fa-mobile-alt', + category: 'development', + pricing: { + basePrice: 800000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 800000, max: 8000000 } + }, + featured: true, + isActive: true + }, + { + _id: '3', + name: 'UI/UX 디자인', + description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.', + shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인', + icon: 'fas fa-palette', + category: 'design', + pricing: { + basePrice: 300000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 300000, max: 2000000 } + }, + featured: true, + isActive: true + }, + { + _id: '4', + name: '디지털 마케팅', + description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.', + shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅', + icon: 'fas fa-chart-line', + category: 'marketing', + pricing: { + basePrice: 200000, + currency: 'KRW', + priceType: 'project', + priceRange: { min: 200000, max: 1500000 } + }, + featured: true, + isActive: true + } +]; + +const mockSettings = { + siteName: 'SmartSolTech', + siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', + contact: { + email: 'info@smartsoltech.kr', + phone: '+82-10-1234-5678', + address: 'Seoul, South Korea' + }, + social: { + facebook: 'https://facebook.com/smartsoltech', + twitter: 'https://twitter.com/smartsoltech', + linkedin: 'https://linkedin.com/company/smartsoltech', + instagram: 'https://instagram.com/smartsoltech' + } +}; + +// Helper function for category names +function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; +} + +// Language switching route +app.get('/lang/:language', (req, res) => { + const language = req.params.language; + const supportedLanguages = ['en', 'ko', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + res.cookie('language', language, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + req.setLocale(language); + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Middleware to set language and theme preferences +app.use((req, res, next) => { + // Set language + const language = req.cookies.language || req.query.lang || 'ko'; + if (['en', 'ko', 'ru', 'kk'].includes(language)) { + req.setLocale(language); + } + + // Set theme preference + const theme = req.cookies.theme || 'light'; + res.locals.theme = theme; + res.locals.currentLanguage = req.getLocale(); + res.locals.__ = res.__; + + next(); +}); + +// Theme switching route +app.get('/theme/:theme', (req, res) => { + const theme = req.params.theme; + if (['light', 'dark'].includes(theme)) { + res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year + } + + const redirectUrl = req.get('Referer') || '/'; + res.redirect(redirectUrl); +}); + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: res.__('navigation.home') + ' - SmartSolTech', + settings: mockSettings, + featuredPortfolio: mockPortfolio.filter(p => p.featured), + featuredServices: mockServices.filter(s => s.featured), + currentPage: 'home' + }); +}); + +app.get('/portfolio', (req, res) => { + const category = req.query.category; + let filteredPortfolio = mockPortfolio; + + if (category && category !== 'all') { + filteredPortfolio = mockPortfolio.filter(p => p.category === category); + } + + res.render('portfolio', { + title: res.__('navigation.portfolio') + ' - SmartSolTech', + portfolioItems: filteredPortfolio, + currentPage: 'portfolio' + }); +}); + +app.get('/portfolio/:id', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + + if (!portfolio) { + return res.status(404).render('error', { + title: '404 - ' + res.__('common.error'), + message: res.__('common.error'), + currentPage: 'error' + }); + } + + const relatedProjects = mockPortfolio.filter(p => + p._id !== portfolio._id && p.category === portfolio.category + ).slice(0, 3); + + res.render('portfolio-detail', { + title: `${portfolio.title} - ${res.__('navigation.portfolio')}`, + portfolio, + relatedProjects, + currentPage: 'portfolio' + }); +}); + +app.get('/services', (req, res) => { + res.render('services', { + title: res.__('navigation.services') + ' - SmartSolTech', + services: mockServices, + currentPage: 'services' + }); +}); + +app.get('/about', (req, res) => { + res.render('about', { + title: res.__('navigation.about') + ' - SmartSolTech', + currentPage: 'about' + }); +}); + +app.get('/contact', (req, res) => { + res.render('contact', { + title: res.__('navigation.contact') + ' - SmartSolTech', + settings: mockSettings, + currentPage: 'contact' + }); +}); + +app.post('/contact', (req, res) => { + // Simulate contact form processing + console.log('Contact form submission:', req.body); + + req.flash('success', res.__('common.success')); + res.redirect('/contact'); +}); + +app.get('/calculator', (req, res) => { + res.render('calculator', { + title: res.__('navigation.calculator') + ' - SmartSolTech', + services: mockServices, + currentPage: 'calculator' + }); +}); + +// API Routes for calculator +app.post('/api/calculator/estimate', (req, res) => { + const { service, projectType, timeline, features } = req.body; + + // Simple calculation logic + let basePrice = 500000; + const selectedService = mockServices.find(s => s._id === service); + + if (selectedService) { + basePrice = selectedService.pricing.basePrice; + } + + // Apply multipliers based on project complexity + const typeMultipliers = { + 'simple': 1, + 'medium': 1.5, + 'complex': 2.5, + 'enterprise': 4 + }; + + const timelineMultipliers = { + 'urgent': 1.5, + 'normal': 1, + 'flexible': 0.9 + }; + + const typeMultiplier = typeMultipliers[projectType] || 1; + const timelineMultiplier = timelineMultipliers[timeline] || 1; + const featuresMultiplier = 1 + (features?.length || 0) * 0.2; + + const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier); + + res.json({ + success: true, + estimate: { + basePrice, + estimatedPrice, + breakdown: { + basePrice, + projectType: projectType, + typeMultiplier, + timeline, + timelineMultiplier, + features: features || [], + featuresMultiplier + } + } + }); +}); + +// API Routes for portfolio interactions +app.post('/api/portfolio/:id/like', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.likes = (portfolio.likes || 0) + 1; + res.json({ success: true, likes: portfolio.likes }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +app.post('/api/portfolio/:id/view', (req, res) => { + const portfolio = mockPortfolio.find(p => p._id === req.params.id); + if (portfolio) { + portfolio.viewCount = (portfolio.viewCount || 0) + 1; + res.json({ success: true, viewCount: portfolio.viewCount }); + } else { + res.status(404).json({ success: false, message: 'Portfolio not found' }); + } +}); + +// Error handling +app.use((req, res) => { + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + message: '요청하신 페이지를 찾을 수 없습니다.', + currentPage: '404' + }); +}); + +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { + title: '500 - 서버 오류', + message: '서버에서 오류가 발생했습니다.', + currentPage: 'error' + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`); + console.log('📋 Available pages:'); + console.log(' • Home: http://localhost:3000/'); + console.log(' • About: http://localhost:3000/about'); + console.log(' • Portfolio: http://localhost:3000/portfolio'); + console.log(' • Services: http://localhost:3000/services'); + console.log(' • Contact: http://localhost:3000/contact'); + console.log(' • Calculator: http://localhost:3000/calculator'); + console.log(''); + console.log('💡 This is a demo version using mock data'); + console.log('💾 To use with MongoDB, run: npm start'); +}); + +module.exports = app; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..d17df23 --- /dev/null +++ b/server.js @@ -0,0 +1,121 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const session = require('express-session'); +const MongoStore = require('connect-mongo'); +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'); +require('dotenv').config(); + +const app = express(); + +// Security middleware +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com"], + fontSrc: ["'self'", "https://fonts.gstatic.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "ws:", "wss:"] + } + } +})); + +// 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'))); + +// View engine +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// Database connection +mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', { + useNewUrlParser: true, + useUnifiedTopology: true, +}) +.then(() => console.log('✓ MongoDB connected')) +.catch(err => console.error('✗ MongoDB connection error:', err)); + +// Session configuration +app.use(session({ + secret: process.env.SESSION_SECRET || 'your-secret-key', + resave: false, + saveUninitialized: false, + store: MongoStore.create({ + mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', + touchAfter: 24 * 3600 // lazy session update + }), + cookie: { + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days + } +})); + +// 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('/admin', require('./routes/admin')); + +// 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).json({ + success: false, + message: process.env.NODE_ENV === 'production' + ? 'Something went wrong!' + : err.message + }); +}); + +// 404 handler +app.use((req, res) => { + res.status(404).render('404', { + title: '404 - Страница не найдена', + message: 'Запрашиваемая страница не найдена' + }); +}); + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`🚀 Server running on port ${PORT}`); + console.log(`🌐 Visit: http://localhost:${PORT}`); +}); + +module.exports = app; \ No newline at end of file diff --git a/views/about-old.ejs b/views/about-old.ejs new file mode 100644 index 0000000..0974833 --- /dev/null +++ b/views/about-old.ejs @@ -0,0 +1,337 @@ + + + + + + 회사 소개 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ About SmartSolTech +

+

+ 혁신적인 기술로 고객의 성공을 이끌어가는 디지털 솔루션 전문 기업 +

+
+
+ + +
+
+
+
+

+ 혁신창의로 만드는 미래 +

+

+ SmartSolTech는 2020년 설립된 디지털 솔루션 전문 기업으로, 웹 개발, 모바일 앱, UI/UX 디자인 분야에서 + 혁신적인 기술과 창의적인 아이디어를 바탕으로 고객의 비즈니스 성공을 지원합니다. +

+

+ 우리는 단순히 기술을 제공하는 것이 아니라, 고객의 목표를 이해하고 그에 맞는 최적의 솔루션을 + 제안하여 함께 성장하는 파트너가 되고자 합니다. +

+
+
+
100+
+
완료 프로젝트
+
+
+
50+
+
만족한 고객
+
+
+
4년
+
업계 경험
+
+
+
+
+
+
+

우리의 미션

+

+ "기술을 통해 모든 비즈니스가 디지털 시대에서 성공할 수 있도록 돕는 것" +

+

우리의 비전

+

+ "한국을 대표하는 글로벌 디지털 솔루션 기업으로 성장하여 + 전 세계 고객들의 디지털 혁신을 주도하는 것" +

+
+
+
+
+
+
+
+
+ + +
+
+
+

+ Core Values +

+

+ SmartSolTech가 추구하는 핵심 가치들 +

+
+ +
+
+
+ +
+

혁신 (Innovation)

+

+ 끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다. +

+
+ +
+
+ +
+

협력 (Collaboration)

+

+ 고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다. +

+
+ +
+
+ +
+

품질 (Quality)

+

+ 높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다. +

+
+ +
+
+ +
+

성장 (Growth)

+

+ 고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다. +

+
+
+
+
+ + +
+
+
+

+ Our Team +

+

+ 전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다 +

+
+ +
+
+
+ KH +
+

김현우

+

CEO & Founder

+

+ 10년 이상의 웹 개발 경험을 바탕으로 SmartSolTech를 설립하여 + 혁신적인 디지털 솔루션을 제공하고 있습니다. +

+ +
+ +
+
+ LJ +
+

이정민

+

CTO & Lead Developer

+

+ 풀스택 개발자로서 최신 기술 트렌드를 적용하여 + 확장 가능하고 안정적인 시스템을 구축합니다. +

+ +
+ +
+
+ PS +
+

박서연

+

UI/UX Designer

+

+ 사용자 중심의 디자인 철학을 바탕으로 직관적이고 + 아름다운 인터페이스를 설계합니다. +

+ +
+
+
+
+ + +
+
+
+

+ Technology Stack +

+

+ 최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다 +

+
+ +
+
+

Frontend

+
+
+ +
React
+
+
+ +
Vue.js
+
+
+ +
Angular
+
+
+
+ +
+

Backend

+
+
+ +
Node.js
+
+
+ +
Python
+
+
+ +
Java
+
+
+
+ +
+

Mobile

+
+
+ +
React Native
+
+
+ +
Flutter
+
+
+ +
Swift
+
+
+
+
+
+
+ + +
+
+
+

+ 함께 성공하는 파트너가 되어보세요 +

+

+ SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요 +

+ +
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/views/about.ejs b/views/about.ejs new file mode 100644 index 0000000..6954758 --- /dev/null +++ b/views/about.ejs @@ -0,0 +1,160 @@ + + + + + + <%- __('about.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('about.hero.title') %> +

+

+ <%- __('about.hero.subtitle') %> +

+
+
+ + +
+
+
+
+

+ <%- __('about.company.title') %> +

+

+ <%- __('about.company.description1') %> +

+

+ <%- __('about.company.description2') %> +

+ + +
+
+
50+
+
<%- __('about.stats.projects') %>
+
+
+
3+
+
<%- __('about.stats.experience') %>
+
+
+
30+
+
<%- __('about.stats.clients') %>
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+

+ <%- __('about.mission.title') %> +

+

+ <%- __('about.mission.description') %> +

+
+ +
+ +
+
+ +
+

+ <%- __('about.values.innovation.title') %> +

+

+ <%- __('about.values.innovation.description') %> +

+
+ + +
+
+ +
+

+ <%- __('about.values.quality.title') %> +

+

+ <%- __('about.values.quality.description') %> +

+
+ + +
+
+ +
+

+ <%- __('about.values.partnership.title') %> +

+

+ <%- __('about.values.partnership.description') %> +

+
+
+
+
+ + +
+
+

+ <%- __('about.cta.title') %> +

+

+ <%- __('about.cta.subtitle') %> +

+ + <%- __('about.cta.button') %> + + + + +
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/views/calculator-old.ejs b/views/calculator-old.ejs new file mode 100644 index 0000000..69ee69f --- /dev/null +++ b/views/calculator-old.ejs @@ -0,0 +1,677 @@ + +
+
+

+ 프로젝트 견적 계산기 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 정확한 견적을 계산해드립니다 +

+
+
+ + +
+
+
+
+
+ +
+
+

1단계: 서비스 선택

+

필요한 서비스를 선택해주세요 (복수 선택 가능)

+
+ +
+ +
+ +
+ +
+
+ + +
+
+

2단계: 프로젝트 세부사항

+

프로젝트의 복잡도와 일정을 선택해주세요

+
+ +
+ +
+ +
+
+
+
🌟
+
간단함
+
기본 기능, 표준 디자인
+
-30% 할인
+
+
+
+
+
+
보통
+
중간 복잡도, 커스텀 기능
+
표준 가격
+
+
+
+
+
🚀
+
복잡함
+
고급 기능, 통합 시스템
+
+50% 추가
+
+
+
+
+
💎
+
엔터프라이즈
+
대규모, 높은 복잡도
+
+100% 추가
+
+
+
+
+ + +
+ +
+
+
+
+
긴급 (2주 이내)
+
+80% 추가
+
+
+
+
+
🔥
+
빠름 (2-4주)
+
+40% 추가
+
+
+
+
+
+
표준 (1-3개월)
+
표준 가격
+
+
+
+
+
🌱
+
여유 (3개월+)
+
-20% 할인
+
+
+
+
+
+ +
+ + +
+
+ + +
+
+

3단계: 추가 기능

+

필요한 추가 기능을 선택해주세요 (선택사항)

+
+ +
+
+
+
+ SEO 최적화 + ₩300,000 +
+

검색엔진 최적화 및 메타 태그 설정

+
+
+ +
+
+
+ 분석 도구 설정 + ₩150,000 +
+

Google Analytics, 태그 매니저 설정

+
+
+ +
+
+
+ 소셜 미디어 연동 + ₩200,000 +
+

Facebook, Instagram, 카카오톡 연동

+
+
+ +
+
+
+ 결제 시스템 + ₩500,000 +
+

신용카드, 간편결제 시스템 연동

+
+
+ +
+
+
+ 다국어 지원 + ₩400,000 +
+

한국어, 영어, 중국어 등 다국어

+
+
+ +
+
+
+ 관리자 패널 + ₩600,000 +
+

콘텐츠 관리, 사용자 관리 시스템

+
+
+
+ + +
+ + +
+ +
+ + +
+
+ + +
+
+

견적 결과

+

계산된 프로젝트 견적을 확인하세요

+
+ +
+ +
+ + +
+

견적 요청하기

+

정확한 견적을 받으시려면 연락처를 남겨주세요

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/views/calculator.ejs b/views/calculator.ejs new file mode 100644 index 0000000..f876cf5 --- /dev/null +++ b/views/calculator.ejs @@ -0,0 +1,302 @@ + + + + + + <%- __('calculator.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('calculator.title') %> +

+

+ <%- __('calculator.subtitle') %> +

+
+
+ + +
+
+
+
+ +
+
+
+ +
+ +
+
+

+ <%- __('calculator.step1.title') %> +

+

+ <%- __('calculator.step1.subtitle') %> +

+
+ +
+ +
+
+
+ +
+
+

+ <%- __('services.web.title') %> +

+
<%- __('services.web.price') %>
+
+
+

+ <%- __('services.web.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.mobile.title') %> +

+
<%- __('services.mobile.price') %>
+
+
+

+ <%- __('services.mobile.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.design.title') %> +

+
<%- __('services.design.price') %>
+
+
+

+ <%- __('services.design.description') %> +

+
+ + +
+
+
+ +
+
+

+ <%- __('services.marketing.title') %> +

+
<%- __('services.marketing.price') %>
+
+
+

+ <%- __('services.marketing.description') %> +

+
+
+ +
+ +
+
+ + + + + + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + \ No newline at end of file diff --git a/views/contact-old.ejs b/views/contact-old.ejs new file mode 100644 index 0000000..2a11d23 --- /dev/null +++ b/views/contact-old.ejs @@ -0,0 +1,491 @@ + + + + + + 연락처 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 연락처 +

+

+ 프로젝트 문의부터 기술 상담까지, 언제든 연락주세요 +

+
+
+ + +
+
+
+ + +
+
+

+ 프로젝트 문의하기 +

+

+ 아래 양식을 작성해 주시면 24시간 내에 답변드리겠습니다. +

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + +
+ + +
+

+ 연락처 정보 +

+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+

전화번호

+ + +82-10-1234-5678 + +
+
+ + +
+
+ +
+
+

주소

+

Seoul, South Korea

+
+
+ + +
+
+ +
+
+

운영시간

+

평일 09:00 - 18:00

+

주말 및 공휴일 휴무

+
+
+
+
+ + +
+

+ 소셜 미디어 +

+ + +
+ + + +
+
+
+
+ + +
+
+
+

+ 자주 묻는 질문 +

+

+ 프로젝트와 관련된 궁금한 점들을 확인해보세요 +

+
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + + \ No newline at end of file diff --git a/views/contact.ejs b/views/contact.ejs new file mode 100644 index 0000000..b7efc4e --- /dev/null +++ b/views/contact.ejs @@ -0,0 +1,211 @@ + + + + + + <%- __('contact.meta.title') %> - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+

+ <%- __('contact.hero.title') %> +

+

+ <%- __('contact.hero.subtitle') %> +

+
+
+ + +
+
+
+ +
+

+ <%- __('contact.form.title') %> +

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+

+ <%- __('contact.info.title') %> +

+ +
+ +
+
+ +
+
+

+ <%- __('contact.phone.title') %> +

+

+ <%- __('contact.phone.number') %> +

+

+ <%- __('contact.phone.hours') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.email.title') %> +

+

+ <%- __('contact.email.address') %> +

+

+ <%- __('contact.email.response') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.telegram.title') %> +

+

+ @smartsoltech +

+

+ <%- __('contact.telegram.subtitle') %> +

+
+
+ + +
+
+ +
+
+

+ <%- __('contact.address.title') %> +

+

+ <%- __('contact.address.line1') %>
+ <%- __('contact.address.line2') %> +

+
+
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + \ No newline at end of file diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 0000000..39b8900 --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,86 @@ + + + + + + 오류 - SmartSolTech + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+ +
+ + +

+ <%= title || '오류가 발생했습니다' %> +

+ + +

+ <%= message || '요청을 처리하는 중 문제가 발생했습니다.' %> +

+ + +
+ + + 홈으로 돌아가기 + + +
+ + +
+

도움이 필요하신가요?

+

+ 문제가 지속되면 언제든지 저희에게 연락해 주세요. +

+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/views/index-old.ejs b/views/index-old.ejs new file mode 100644 index 0000000..a95ab66 --- /dev/null +++ b/views/index-old.ejs @@ -0,0 +1,390 @@ + + + + + + <%= title || 'SmartSolTech - 혁신적인 기술 솔루션' %> + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ Smart Technology + Solutions +

+

+ 혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다 +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ Our Services +

+

+ 최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션 +

+
+ +
+ +
+
+ +
+

웹 개발

+

현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발

+
₩500,000~
+
+ +
+
+ +
+

모바일 앱

+

iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱

+
₩800,000~
+
+ +
+
+ +
+

UI/UX 디자인

+

사용자 중심의 직관적이고 아름다운 인터페이스 디자인

+
₩300,000~
+
+ +
+
+ +
+

디지털 마케팅

+

SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅

+
₩200,000~
+
+
+ + +
+
+ + +
+
+
+

+ Recent Projects +

+

+ 고객의 성공을 위해 완성한 프로젝트들을 확인해보세요 +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + 자세히 보기 + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ E-commerce + + 1,234 + +
+

온라인 쇼핑몰 플랫폼

+

현대적인 UI/UX와 완벽한 결제 시스템을 갖춘 전자상거래 솔루션

+ + 자세히 보기 + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ 프로젝트 견적을 확인해보세요 +

+

+ 원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다 +

+ + + 견적 계산기 사용하기 + +
+
+
+ + +
+
+
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다. +

+
+
+
+ +
+
+
전화 상담
+
+82-10-0000-0000
+
+
+
+
+ +
+
+
이메일 문의
+
info@smartsoltech.kr
+
+
+
+
+ +
+
+
텔레그램 채팅
+
즉시 답변 가능
+
+
+
+
+
+
+

무료 상담 신청

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..2a60a82 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,400 @@ + + + + + + <%- __(title || 'meta.title') %> + + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+
+ + +
+
+
+
+
+ +
+
+

+ <%- __('hero.title.smart') %> + <%- __('hero.title.solutions') %> +

+

+ <%- __('hero.subtitle') %> +

+ +
+ + +
+ + + +
+
+
+ + +
+
+
+

+ <%- __('services.title.our') %> <%- __('services.title.services') %> +

+

+ <%- __('services.subtitle') %> +

+
+ +
+ +
+
+ +
+

<%- __('services.web.title') %>

+

<%- __('services.web.description') %>

+
<%- __('services.web.price') %>
+
+ + +
+
+ +
+

<%- __('services.mobile.title') %>

+

<%- __('services.mobile.description') %>

+
<%- __('services.mobile.price') %>
+
+ + +
+
+ +
+

<%- __('services.design.title') %>

+

<%- __('services.design.description') %>

+
<%- __('services.design.price') %>
+
+ + +
+
+ +
+

<%- __('services.marketing.title') %>

+

<%- __('services.marketing.description') %>

+
<%- __('services.marketing.price') %>
+
+
+ + +
+
+ + +
+
+
+

+ <%- __('portfolio.title.recent') %> <%- __('portfolio.title.projects') %> +

+

+ <%- __('portfolio.subtitle') %> +

+
+ +
+ <% if (featuredPortfolio && featuredPortfolio.length > 0) { %> + <% featuredPortfolio.forEach((project, index) => { %> +
+
+ <% if (project.images && project.images.length > 0) { %> + <%= project.images.find(img => img.isPrimary)?.alt || project.title %> + <% } else { %> +
+ <%= project.title.charAt(0) %> +
+ <% } %> +
+
+
+ <% if (project.technologies && project.technologies.length > 0) { %> + <% project.technologies.slice(0, 3).forEach(tech => { %> + <%= tech %> + <% }) %> + <% } %> +
+
+
+
+
+ <%= project.category %> + + <%= project.viewCount || 0 %> + +
+

<%= project.title %>

+

<%= project.shortDescription || project.description %>

+ + <%- __('common.view_details') %> + + + + +
+
+ <% }) %> + <% } else { %> + +
+
+
+ E +
+
+
+
+ React + Node.js +
+
+
+
+
+ <%- __('portfolio.default.ecommerce') %> + + 1,234 + +
+

<%- __('portfolio.default.title') %>

+

<%- __('portfolio.default.description') %>

+ + <%- __('common.view_details') %> + + + + +
+
+ <% } %> +
+ + +
+
+ + +
+
+
+

+ <%- __('calculator.cta.title') %> +

+

+ <%- __('calculator.cta.subtitle') %> +

+ + + <%- __('calculator.cta.button') %> + +
+
+
+ + +
+
+
+
+

+ <%- __('contact.cta.ready') %> <%- __('contact.cta.start') %><%- __('contact.cta.question') %> +

+

+ <%- __('contact.cta.subtitle') %> +

+
+
+
+ +
+
+
<%- __('contact.phone.title') %>
+
<%- __('contact.phone.number') %>
+
+
+
+
+ +
+
+
<%- __('contact.email.title') %>
+
<%- __('contact.email.address') %>
+
+
+
+
+ +
+
+
<%- __('contact.telegram.title') %>
+
<%- __('contact.telegram.subtitle') %>
+
+
+
+
+
+
+

<%- __('contact.form.title') %>

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + <%- include('partials/footer') %> + + + + + + + \ No newline at end of file diff --git a/views/layout.ejs b/views/layout.ejs new file mode 100644 index 0000000..a1a4954 --- /dev/null +++ b/views/layout.ejs @@ -0,0 +1,88 @@ + + + + + + <%= title %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+ <%- body %> +
+ + + <%- include('partials/footer') %> + + + + + + + + + + <% if (settings && settings.seo && settings.seo.googleAnalytics) { %> + + + + <% } %> + + \ No newline at end of file diff --git a/views/partials/footer-old.ejs b/views/partials/footer-old.ejs new file mode 100644 index 0000000..fb5f61d --- /dev/null +++ b/views/partials/footer-old.ejs @@ -0,0 +1,103 @@ + \ No newline at end of file diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs new file mode 100644 index 0000000..7200dad --- /dev/null +++ b/views/partials/footer.ejs @@ -0,0 +1,125 @@ + \ No newline at end of file diff --git a/views/partials/navigation-old.ejs b/views/partials/navigation-old.ejs new file mode 100644 index 0000000..4bf6bde --- /dev/null +++ b/views/partials/navigation-old.ejs @@ -0,0 +1,80 @@ + + + +
\ No newline at end of file diff --git a/views/partials/navigation.ejs b/views/partials/navigation.ejs new file mode 100644 index 0000000..fc18f45 --- /dev/null +++ b/views/partials/navigation.ejs @@ -0,0 +1,207 @@ + + + \ No newline at end of file diff --git a/views/portfolio-detail.ejs b/views/portfolio-detail.ejs new file mode 100644 index 0000000..9cdc864 --- /dev/null +++ b/views/portfolio-detail.ejs @@ -0,0 +1,473 @@ + + + + + + <%= portfolio.title %> - SmartSolTech 포트폴리오 + + + + + + + + + + <% if (portfolio.images && portfolio.images.length > 0) { %> + + <% } %> + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+ + + +
+ +
+
+ + <%= getCategoryName(portfolio.category) %> + + <% if (portfolio.featured) { %> + + FEATURED + + <% } %> + <% if (portfolio.status === 'completed') { %> + + 완료 + + <% } else if (portfolio.status === 'in-progress') { %> + + 진행 중 + + <% } %> +
+ +

+ <%= portfolio.title %> +

+ +

+ <%= portfolio.description %> +

+ + +
+
+
<%= portfolio.viewCount || 0 %>
+
조회수
+
+
+
<%= portfolio.likes || 0 %>
+
좋아요
+
+
+
+ <%= portfolio.completedAt ? new Date(portfolio.completedAt).getFullYear() : new Date().getFullYear() %> +
+
완료년도
+
+
+ + +
+ <% if (portfolio.projectUrl) { %> + + + 프로젝트 보기 + + <% } %> + +
+
+ + +
+ <% if (portfolio.images && portfolio.images.length > 0) { %> +
+ <%= portfolio.title %> +
+
+ <% } else { %> +
+ +
+ <% } %> +
+
+
+
+ + +
+
+
+ + +
+ + + <% if (portfolio.images && portfolio.images.length > 1) { %> +
+

프로젝트 갤러리

+ + +
+
+ <% portfolio.images.forEach(image => { %> +
+
+ <%= image.alt || portfolio.title %> +
+

<%= image.alt || portfolio.title %>

+
+
+
+ <% }) %> +
+
+
+
+
+
+ <% } %> + + +
+

프로젝트 개요

+
+ <%= portfolio.description %> +
+
+ + + <% if (portfolio.technologies && portfolio.technologies.length > 0) { %> +
+

사용된 기술

+
+ <% portfolio.technologies.forEach(tech => { %> +
+
<%= tech %>
+
+ <% }) %> +
+
+ <% } %> + + +
+

이 프로젝트 공유하기

+
+ + + + +
+
+
+ + +
+ + +
+

프로젝트 정보

+ +
+ <% if (portfolio.clientName) { %> +
+
클라이언트
+
<%= portfolio.clientName %>
+
+ <% } %> + +
+
카테고리
+
<%= getCategoryName(portfolio.category) %>
+
+ + <% if (portfolio.completedAt) { %> +
+
완료일
+
+ <%= new Date(portfolio.completedAt).toLocaleDateString('ko-KR') %> +
+
+ <% } %> + + <% if (portfolio.projectUrl) { %> +
+
프로젝트 URL
+ + <%= portfolio.projectUrl %> + +
+ <% } %> +
+
+ + +
+

비슷한 프로젝트가 필요하신가요?

+

+ 이런 프로젝트에 관심이 있으시다면 언제든 연락주세요. + 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다. +

+ +
+ +
+
+
+
+ + +
+
+
+

+ 관련 프로젝트 +

+

+ 비슷한 카테고리의 다른 프로젝트들도 확인해보세요 +

+
+ +
+ <% if (relatedProjects && relatedProjects.length > 0) { %> + <% relatedProjects.forEach((project, index) => { %> +
+ +
+ <% if (project.images && project.images.length > 0) { %> + <%= project.title %> + <% } else { %> +
+ +
+ <% } %> +
+ +
+

+ <%= project.title %> +

+

+ <%= project.shortDescription || project.description %> +

+ + 자세히 보기 → + +
+
+ <% }) %> + <% } else { %> +
+

관련 프로젝트가 없습니다.

+
+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/views/portfolio.ejs b/views/portfolio.ejs new file mode 100644 index 0000000..8173622 --- /dev/null +++ b/views/portfolio.ejs @@ -0,0 +1,296 @@ + + + + + + 포트폴리오 - SmartSolTech + + + + + + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 포트폴리오 +

+

+ 혁신적인 프로젝트와 창의적인 솔루션들을 만나보세요 +

+
+ + + + +
+
+
+ + +
+
+
+ <% if (portfolioItems && portfolioItems.length > 0) { %> + <% portfolioItems.forEach((item, index) => { %> +
+ + +
+ <% if (item.images && item.images.length > 0) { %> + <%= item.title %> + <% } else { %> +
+ +
+ <% } %> + + +
+ + <%= getCategoryName(item.category) %> + +
+ + + <% if (item.featured) { %> +
+ + FEATURED + +
+ <% } %> + + + +
+ + +
+

+ <%= item.title %> +

+

+ <%= item.shortDescription || item.description %> +

+ + +
+ <% if (item.technologies && item.technologies.length > 0) { %> + <% item.technologies.slice(0, 3).forEach(tech => { %> + + <%= tech %> + + <% }) %> + <% if (item.technologies.length > 3) { %> + + +<%= item.technologies.length - 3 %> + + <% } %> + <% } %> +
+ + +
+
+ <% if (item.clientName) { %> + + <%= item.clientName %> + <% } %> +
+
+ + + <%= item.viewCount || 0 %> + + + + <%= item.likes || 0 %> + +
+
+ + + +
+
+ <% }) %> + <% } else { %> +
+ +

아직 포트폴리오가 없습니다

+

곧 멋진 프로젝트들을 공개할 예정입니다!

+
+ <% } %> +
+ + + <% if (portfolioItems && portfolioItems.length >= 9) { %> +
+ +
+ <% } %> +
+
+ + +
+
+

+ 다음 프로젝트의 주인공이 되어보세요 +

+

+ 우리와 함께 혁신적인 디지털 솔루션을 만들어보세요 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + + + <% + // Helper function for category names + function getCategoryName(category) { + const categoryNames = { + 'web-development': '웹 개발', + 'mobile-app': '모바일 앱', + 'ui-ux-design': 'UI/UX 디자인', + 'branding': '브랜딩', + 'marketing': '디지털 마케팅' + }; + return categoryNames[category] || category; + } + %> + + \ No newline at end of file diff --git a/views/services.ejs b/views/services.ejs new file mode 100644 index 0000000..02f0191 --- /dev/null +++ b/views/services.ejs @@ -0,0 +1,293 @@ + + + + + + 서비스 - SmartSolTech + + + + + + + + + + + + + + + + + + <%- include('partials/navigation') %> + + +
+
+

+ 우리의 서비스 +

+

+ 혁신적인 기술로 비즈니스의 성장을 지원합니다 +

+
+
+ + +
+
+
+ <% if (services && services.length > 0) { %> + <% services.forEach((service, index) => { %> +
+ + +
+ +
+ + +

+ <%= service.name %> +

+ +

+ <%= service.shortDescription || service.description %> +

+ + + <% if (service.pricing) { %> +
+
시작가격
+
+ <%= service.pricing.basePrice ? service.pricing.basePrice.toLocaleString() : '상담' %> + <% if (service.pricing.basePrice) { %> + 원~ + <% } %> +
+ <% if (service.pricing.priceRange) { %> +
+ <%= service.pricing.priceRange.min.toLocaleString() %>원 - + <%= service.pricing.priceRange.max.toLocaleString() %>원 +
+ <% } %> +
+ <% } %> + + + + + + <% if (service.featured) { %> +
+ + 인기 + +
+ <% } %> +
+ <% }) %> + <% } else { %> +
+ +

서비스 준비 중

+

곧 다양한 서비스를 제공할 예정입니다!

+
+ <% } %> +
+
+
+ + +
+
+
+

+ 프로젝트 진행 과정 +

+

+ 체계적이고 전문적인 프로세스로 프로젝트를 진행합니다 +

+
+ +
+ +
+
+ 1 +
+

상담 및 기획

+

+ 고객의 요구사항을 정확히 파악하고 + 최적의 솔루션을 기획합니다 +

+
+ + +
+
+ 2 +
+

디자인 및 설계

+

+ 사용자 중심의 직관적인 디자인과 + 견고한 시스템 아키텍처를 설계합니다 +

+
+ + +
+
+ 3 +
+

개발 및 구현

+

+ 최신 기술과 모범 사례를 활용하여 + 효율적이고 확장 가능한 솔루션을 개발합니다 +

+
+ + +
+
+ 4 +
+

테스트 및 배포

+

+ 철저한 테스트를 통해 품질을 보장하고 + 안정적인 배포를 진행합니다 +

+
+
+
+
+ + +
+
+
+ +
+

+ 왜 SmartSolTech를 선택해야 할까요? +

+ +
+ +
+
+ +
+
+

최신 기술 활용

+

+ 항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 + 미래 지향적인 솔루션을 제공합니다. +

+
+
+ + +
+
+ +
+
+

전문가 팀

+

+ 각 분야의 전문가들로 구성된 팀이 협력하여 + 최고 품질의 결과물을 보장합니다. +

+
+
+ + +
+
+ +
+
+

빠른 대응

+

+ 신속한 커뮤니케이션과 효율적인 프로젝트 관리로 + 정해진 일정 내에 프로젝트를 완료합니다. +

+
+
+ + +
+
+ +
+
+

지속적인 지원

+

+ 프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 + 통해 장기적인 파트너십을 유지합니다. +

+
+
+
+
+ + +
+
+
+ +

품질 보장

+

+ 고객 만족을 위한
+ 최고 품질의 서비스 +

+
+
+
+
+
+
+ + +
+
+

+ 프로젝트를 시작할 준비가 되셨나요? +

+

+ 무료 상담을 통해 최적의 솔루션을 제안해드리겠습니다 +

+ +
+
+ + <%- include('partials/footer') %> + + + + + + \ No newline at end of file