Compare commits
2 Commits
branch-2.2
...
branch-2.4
| Author | SHA1 | Date | |
|---|---|---|---|
| a27c99a267 | |||
| 03f774c8f4 |
@@ -16,7 +16,7 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top shadow-sm">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="index.html">
|
||||
<img src="logo.png" alt="Логотип" class="logo me-2" />
|
||||
<img src="../server/src/main/resources/static/assets/logo.png" alt="Логотип" class="logo me-2" />
|
||||
<span class="d-none d-lg-block">Книжный магазин "Тома"</span>
|
||||
<span class="d-lg-none">"Тома"</span>
|
||||
</a>
|
||||
|
||||
@@ -1,76 +1,182 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Button, Form } from 'react-bootstrap';
|
||||
import { Modal, Button, Form, Alert } from 'react-bootstrap';
|
||||
import api from '../services/api';
|
||||
|
||||
const BookModal = ({ show, onHide, bookId, genres, onSave }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
author: '',
|
||||
genreId: '',
|
||||
price: '',
|
||||
description: '',
|
||||
image: ''
|
||||
});
|
||||
const [selectedGenreIds, setSelectedGenreIds] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (bookId) {
|
||||
// Загружаем данные книги для редактирования
|
||||
api.fetchBook(bookId).then(response => {
|
||||
const book = response.data;
|
||||
setFormData({
|
||||
title: book.title || '',
|
||||
author: book.author || '',
|
||||
genreId: book.genre?.id || genres[0]?.id || '',
|
||||
price: book.price || '',
|
||||
description: book.description || '',
|
||||
image: book.image || ''
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('Ошибка загрузки книги:', error);
|
||||
});
|
||||
if (bookId && show) {
|
||||
loadBookData();
|
||||
} else {
|
||||
// Сбрасываем форму для новой книги
|
||||
setFormData({
|
||||
title: '',
|
||||
author: '',
|
||||
genreId: genres[0]?.id || '',
|
||||
price: '',
|
||||
description: '',
|
||||
image: ''
|
||||
});
|
||||
resetForm();
|
||||
}
|
||||
}, [bookId, genres, show]); // Добавил show в зависимости
|
||||
}, [bookId, show]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const bookData = {
|
||||
...formData,
|
||||
price: Number(formData.price) || 0,
|
||||
genreId: Number(formData.genreId),
|
||||
// Убираем автоматическое добавление 'images/' - сохраняем как есть
|
||||
image: formData.image
|
||||
};
|
||||
onSave(bookData);
|
||||
const loadBookData = async () => {
|
||||
if (!bookId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Загружаем данные книги
|
||||
const bookResponse = await api.fetchBook(bookId);
|
||||
const book = bookResponse.data;
|
||||
|
||||
setFormData({
|
||||
title: book.title || '',
|
||||
author: book.author || '',
|
||||
price: book.price || '',
|
||||
description: book.description || '',
|
||||
image: book.image || ''
|
||||
});
|
||||
|
||||
// Загружаем жанры книги, если есть ID
|
||||
if (bookId) {
|
||||
try {
|
||||
const genresResponse = await api.fetchBookGenres(bookId);
|
||||
const bookGenres = genresResponse.data || [];
|
||||
setSelectedGenreIds(bookGenres.map(gb => gb.genre.id));
|
||||
} catch (genreError) {
|
||||
console.error('Ошибка загрузки жанров книги:', genreError);
|
||||
setSelectedGenreIds([]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки книги:', error);
|
||||
setError('Не удалось загрузить данные книги');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
title: '',
|
||||
author: '',
|
||||
genreId: '',
|
||||
price: '',
|
||||
description: '',
|
||||
image: ''
|
||||
});
|
||||
setSelectedGenreIds([]);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleGenreChange = (genreId) => {
|
||||
setSelectedGenreIds(prev => {
|
||||
if (prev.includes(genreId)) {
|
||||
return prev.filter(id => id !== genreId);
|
||||
} else {
|
||||
return [...prev, genreId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const bookData = {
|
||||
title: formData.title,
|
||||
author: formData.author,
|
||||
price: Number(formData.price) || 0,
|
||||
description: formData.description,
|
||||
image: formData.image
|
||||
};
|
||||
|
||||
let savedBook;
|
||||
if (bookId) {
|
||||
// Обновляем книгу
|
||||
savedBook = await api.updateBook(bookId, bookData);
|
||||
|
||||
// Обновляем жанры книги
|
||||
const currentGenresResponse = await api.fetchBookGenres(bookId);
|
||||
const currentGenreIds = (currentGenresResponse.data || []).map(gb => gb.genre.id);
|
||||
|
||||
// Удаляем жанры, которые были убраны
|
||||
for (const currentGenreId of currentGenreIds) {
|
||||
if (!selectedGenreIds.includes(currentGenreId)) {
|
||||
await api.deleteBookGenre(bookId, currentGenreId);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем новые жанры
|
||||
for (const newGenreId of selectedGenreIds) {
|
||||
if (!currentGenreIds.includes(newGenreId)) {
|
||||
await api.addBookGenre(bookId, {
|
||||
genreId: newGenreId,
|
||||
date: new Date().toISOString().split('T')[0] // Текущая дата в формате YYYY-MM-DD
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Создаем новую книгу
|
||||
savedBook = await api.createBook(bookData);
|
||||
const newBookId = savedBook.data.id;
|
||||
|
||||
// Добавляем жанры для новой книги
|
||||
for (const genreId of selectedGenreIds) {
|
||||
await api.addBookGenre(newBookId, {
|
||||
genreId: genreId,
|
||||
date: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSave(bookData);
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения книги:', error);
|
||||
setError('Не удалось сохранить книгу');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onHide();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Modal show={show} onHide={handleClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{bookId ? 'Редактировать книгу' : 'Добавить книгу'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="text-center">
|
||||
<div className="py-4">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
<p className="mt-2">Загрузка данных...</p>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={handleClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{bookId ? 'Редактировать книгу' : 'Добавить книгу'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{error && (
|
||||
<Alert variant="danger" className="mb-3">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Название книги *</Form.Label>
|
||||
@@ -95,17 +201,27 @@ const BookModal = ({ show, onHide, bookId, genres, onSave }) => {
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Жанр *</Form.Label>
|
||||
<Form.Select
|
||||
value={formData.genreId}
|
||||
onChange={(e) => setFormData({...formData, genreId: e.target.value})}
|
||||
required
|
||||
>
|
||||
<option value="">Выберите жанр</option>
|
||||
{genres.map(genre => (
|
||||
<option key={genre.id} value={genre.id}>{genre.name}</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
<Form.Label>Жанры</Form.Label>
|
||||
<div className="border rounded p-3" style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{genres.length === 0 ? (
|
||||
<p className="text-muted mb-0">Нет доступных жанров</p>
|
||||
) : (
|
||||
genres.map(genre => (
|
||||
<Form.Check
|
||||
key={genre.id}
|
||||
type="checkbox"
|
||||
id={`genre-${genre.id}`}
|
||||
label={genre.name}
|
||||
checked={selectedGenreIds.includes(genre.id)}
|
||||
onChange={() => handleGenreChange(genre.id)}
|
||||
className="mb-2"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<Form.Text className="text-muted">
|
||||
Можно выбрать несколько жанров
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
@@ -122,13 +238,12 @@ const BookModal = ({ show, onHide, bookId, genres, onSave }) => {
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Описание *</Form.Label>
|
||||
<Form.Label>Описание</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={3}
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
||||
required
|
||||
placeholder="Введите описание книги"
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -147,7 +262,9 @@ const BookModal = ({ show, onHide, bookId, genres, onSave }) => {
|
||||
</Form.Group>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleClose}>Отмена</Button>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
{bookId ? 'Обновить' : 'Добавить'}
|
||||
</Button>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import logoImage from '../images/logo.png';
|
||||
|
||||
const Navbar = ({ cartCount }) => {
|
||||
return (
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-light fixed-top shadow-sm">
|
||||
<div className="container">
|
||||
<Link className="navbar-brand d-flex align-items-center" to="/">
|
||||
<img src="/logo.png" alt="Логотип" className="logo me-2" />
|
||||
<img src={logoImage} alt="Логотип" className="logo me-2" />
|
||||
<span className="d-none d-lg-block">Книжный магазин "Тома"</span>
|
||||
<span className="d-lg-none">"Тома"</span>
|
||||
</Link>
|
||||
|
||||
50
MyWebSite/components/PageSizeSelector.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
|
||||
const PageSizeSelector = ({
|
||||
pageSize,
|
||||
onPageSizeChange,
|
||||
// Меняем значения в выпадающем списке
|
||||
options = [5, 10, 20]
|
||||
}) => {
|
||||
// Маппинг: что пользователь видит -> что на самом деле запрашиваем
|
||||
const sizeMapping = {
|
||||
3: 5, // Пользователь видит "3", запрашиваем 5
|
||||
5: 10, // Пользователь видит "5", запрашиваем 10
|
||||
10: 20, // Пользователь видит "10", запрашиваем 20
|
||||
};
|
||||
|
||||
// Находим отображаемое значение для текущего реального
|
||||
const getDisplayValue = (realValue) => {
|
||||
for (const [display, real] of Object.entries(sizeMapping)) {
|
||||
if (real === realValue) return Number(display);
|
||||
}
|
||||
return realValue;
|
||||
};
|
||||
|
||||
const displayValue = getDisplayValue(pageSize);
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="me-2">Книг на странице:</span>
|
||||
<Form.Select
|
||||
size="sm"
|
||||
style={{ width: 'auto' }}
|
||||
value={displayValue}
|
||||
onChange={(e) => {
|
||||
const selectedDisplayValue = Number(e.target.value);
|
||||
const realValue = sizeMapping[selectedDisplayValue] || selectedDisplayValue;
|
||||
onPageSizeChange(realValue);
|
||||
}}
|
||||
>
|
||||
{options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSizeSelector;
|
||||
93
MyWebSite/components/Pagination.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { Pagination as BootstrapPagination } from 'react-bootstrap';
|
||||
|
||||
const Pagination = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
maxVisiblePages = 5
|
||||
}) => {
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
const items = [];
|
||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
|
||||
// Корректируем, если не хватает страниц
|
||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
// Кнопка "Назад"
|
||||
items.push(
|
||||
<BootstrapPagination.Prev
|
||||
key="prev"
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
|
||||
/>
|
||||
);
|
||||
|
||||
// Первая страница
|
||||
if (startPage > 1) {
|
||||
items.push(
|
||||
<BootstrapPagination.Item
|
||||
key={1}
|
||||
active={1 === currentPage}
|
||||
onClick={() => onPageChange(1)}
|
||||
>
|
||||
1
|
||||
</BootstrapPagination.Item>
|
||||
);
|
||||
|
||||
if (startPage > 2) {
|
||||
items.push(<BootstrapPagination.Ellipsis key="start-ellipsis" />);
|
||||
}
|
||||
}
|
||||
|
||||
// Видимые страницы
|
||||
for (let page = startPage; page <= endPage; page++) {
|
||||
items.push(
|
||||
<BootstrapPagination.Item
|
||||
key={page}
|
||||
active={page === currentPage}
|
||||
onClick={() => onPageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</BootstrapPagination.Item>
|
||||
);
|
||||
}
|
||||
|
||||
// Последняя страница
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
items.push(<BootstrapPagination.Ellipsis key="end-ellipsis" />);
|
||||
}
|
||||
|
||||
items.push(
|
||||
<BootstrapPagination.Item
|
||||
key={totalPages}
|
||||
active={totalPages === currentPage}
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
>
|
||||
{totalPages}
|
||||
</BootstrapPagination.Item>
|
||||
);
|
||||
}
|
||||
|
||||
// Кнопка "Вперед"
|
||||
items.push(
|
||||
<BootstrapPagination.Next
|
||||
key="next"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<BootstrapPagination className="justify-content-center mt-4">
|
||||
{items}
|
||||
</BootstrapPagination>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
5
MyWebSite/dist/assets/css/index-DvDKDXQB.css
vendored
Normal file
BIN
MyWebSite/dist/assets/images/Book1-BdJql_-B.jpg
vendored
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
MyWebSite/dist/assets/images/Book2-BEB7Ih2u.jpg
vendored
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
MyWebSite/dist/assets/images/Book3-bPojlso8.jpg
vendored
Normal file
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
BIN
MyWebSite/dist/assets/images/dune-Co1F1vkB.jpg
vendored
Normal file
|
After Width: | Height: | Size: 817 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
BIN
MyWebSite/dist/assets/images/the_girl_with_the_dragon_tattoo-CgrgasX2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 370 KiB |
64
MyWebSite/dist/assets/js/index-D_bqwX26.js
vendored
Normal file
64
MyWebSite/dist/assets/main-9YKRXbpb.js
vendored
BIN
MyWebSite/dist/assets/the_hobbit-CkJ8H01T.webp
vendored
Normal file
|
After Width: | Height: | Size: 172 KiB |
4
MyWebSite/dist/index.html
vendored
@@ -6,8 +6,8 @@
|
||||
<title>Книжный интернет-магазин "Тома"</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
|
||||
<script type="module" crossorigin src="/assets/main-9YKRXbpb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BLg3Q5Rn.css">
|
||||
<script type="module" crossorigin src="./assets/js/index-D_bqwX26.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/css/index-DvDKDXQB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
BIN
MyWebSite/images/default-book.jpg
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
MyWebSite/images/logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -5,12 +5,9 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "vite build",
|
||||
"serve": "http-server -p 3000 ./dist/",
|
||||
"prod": "npm-run-all build serve",
|
||||
"lint": "eslint . --ext js --report-unused-disable-directives --max-warnings 0",
|
||||
"server": "json-server --watch db.json --port 3001",
|
||||
"dev": "vite",
|
||||
"start": "npm-run-all -p server dev"
|
||||
"start": "npm-run-all -p dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Container, Row, Spinner } from 'react-bootstrap';
|
||||
import { Button, Container, Row, Spinner, Form, Alert } from 'react-bootstrap';
|
||||
import BookComponent from '../components/BookComponent';
|
||||
import BookModal from '../components/BookModal';
|
||||
import CartModal from '../components/CartModal';
|
||||
import Footer from '../components/Footer';
|
||||
import GenreModal from '../components/GenreModal';
|
||||
import Navbar from '../components/Navbar';
|
||||
import Pagination from '../components/Pagination';
|
||||
import PageSizeSelector from '../components/PageSizeSelector';
|
||||
import api from '../services/api';
|
||||
|
||||
const CatalogPage = () => {
|
||||
@@ -17,40 +19,176 @@ const CatalogPage = () => {
|
||||
const [currentBookId, setCurrentBookId] = useState(null);
|
||||
const [cartCount, setCartCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Состояние для пагинации книг
|
||||
const [bookPagination, setBookPagination] = useState({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
totalPages: 1,
|
||||
totalItems: 0
|
||||
});
|
||||
|
||||
// Состояние для фильтрации
|
||||
const [selectedGenre, setSelectedGenre] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
// Функция загрузки данных с пагинацией
|
||||
const fetchData = async (
|
||||
bookPage = bookPagination.currentPage,
|
||||
bookSize = bookPagination.pageSize
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [genresResponse, booksResponse, cartResponse] = await Promise.all([
|
||||
api.fetchGenres(),
|
||||
api.fetchBooks(),
|
||||
api.fetchCartItems()
|
||||
]);
|
||||
setError(null);
|
||||
|
||||
console.log('Fetching data...');
|
||||
|
||||
// Загружаем жанры (без пагинации для фильтров)
|
||||
const genresResponse = await api.fetchGenres();
|
||||
console.log('Genres response:', genresResponse);
|
||||
|
||||
let genresData = [];
|
||||
if (genresResponse.data && genresResponse.data.items) {
|
||||
// Если ответ содержит пагинацию, берем items
|
||||
genresData = genresResponse.data.items;
|
||||
} else if (Array.isArray(genresResponse.data)) {
|
||||
// Если это просто массив
|
||||
genresData = genresResponse.data;
|
||||
}
|
||||
|
||||
console.log('Genres data:', genresData);
|
||||
setGenres(genresData);
|
||||
|
||||
// Загружаем книги в зависимости от фильтров
|
||||
let booksResponse;
|
||||
if (selectedGenre) {
|
||||
// Фильтруем по жанру через бэкенд
|
||||
console.log('Fetching books by genre:', selectedGenre);
|
||||
booksResponse = await api.getBooksByGenre(selectedGenre, bookPage, bookSize);
|
||||
} else if (searchQuery.trim()) {
|
||||
// Ищем книги через бэкенд
|
||||
console.log('Searching books:', searchQuery);
|
||||
booksResponse = await api.searchBooks(searchQuery.trim(), bookPage, bookSize);
|
||||
} else {
|
||||
// Загружаем все книги с пагинацией
|
||||
console.log('Fetching all books');
|
||||
booksResponse = await api.fetchBooksPaginated(bookPage, bookSize);
|
||||
}
|
||||
|
||||
console.log('Books response:', booksResponse);
|
||||
const booksData = booksResponse.data || {};
|
||||
|
||||
// Загружаем корзину
|
||||
const cartResponse = await api.fetchCartItems();
|
||||
console.log('Cart response:', cartResponse);
|
||||
|
||||
// Обновляем состояние книг
|
||||
setBooks(booksData.items || []);
|
||||
setBookPagination({
|
||||
currentPage: booksData.currentPage || 1,
|
||||
pageSize: booksData.currentSize || bookSize, // Используем текущий размер страницы
|
||||
totalPages: booksData.totalPages || 1,
|
||||
totalItems: booksData.totalItems || 0
|
||||
});
|
||||
|
||||
setGenres(genresResponse.data || []);
|
||||
setBooks(booksResponse.data || []);
|
||||
updateCartCount(cartResponse.data || []);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных:', error);
|
||||
setError('Не удалось загрузить данные. Проверьте подключение к серверу.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCartCount = (cartItems) => {
|
||||
if (!Array.isArray(cartItems)) {
|
||||
console.error('Cart items is not an array:', cartItems);
|
||||
setCartCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const count = cartItems.reduce((sum, item) => sum + (item.quantity || 0), 0);
|
||||
setCartCount(count);
|
||||
};
|
||||
|
||||
// Обработчики для пагинации книг
|
||||
const handleBookPageChange = (page) => {
|
||||
setBookPagination(prev => ({ ...prev, currentPage: page }));
|
||||
fetchData(page, bookPagination.pageSize);
|
||||
};
|
||||
|
||||
const handleBookPageSizeChange = (size) => {
|
||||
setBookPagination(prev => ({ ...prev, pageSize: size, currentPage: 1 }));
|
||||
fetchData(1, size);
|
||||
};
|
||||
|
||||
// Обработчик фильтрации по жанру
|
||||
const handleGenreFilter = (genreId) => {
|
||||
const newSelectedGenre = genreId === selectedGenre ? null : genreId;
|
||||
setSelectedGenre(newSelectedGenre);
|
||||
setSearchQuery(''); // Сбрасываем поиск
|
||||
setBookPagination(prev => ({ ...prev, currentPage: 1 }));
|
||||
fetchData(1, bookPagination.pageSize);
|
||||
};
|
||||
|
||||
// Обработчик поиска
|
||||
const handleSearch = () => {
|
||||
setSelectedGenre(null); // Сбрасываем фильтр по жанру
|
||||
setBookPagination(prev => ({ ...prev, currentPage: 1 }));
|
||||
fetchData(1, bookPagination.pageSize);
|
||||
};
|
||||
|
||||
// Обработчик сброса фильтров
|
||||
const handleResetFilters = () => {
|
||||
setSelectedGenre(null);
|
||||
setSearchQuery('');
|
||||
setBookPagination(prev => ({ ...prev, currentPage: 1 }));
|
||||
fetchData(1, bookPagination.pageSize);
|
||||
};
|
||||
|
||||
// Функция для группировки книг по жанрам (используется только когда НЕ выбран конкретный жанр)
|
||||
const getBooksByGenre = (genreId) => {
|
||||
if (!Array.isArray(books) || selectedGenre) {
|
||||
return []; // Не группируем при выбранном жанре
|
||||
}
|
||||
|
||||
return books.filter(book =>
|
||||
book.genres?.some(bookGenre => bookGenre.genre?.id === genreId)
|
||||
);
|
||||
};
|
||||
|
||||
// Получаем жанры для отображения (с книгами)
|
||||
const getGenresWithBooks = () => {
|
||||
if (!Array.isArray(genres) || !Array.isArray(books)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (selectedGenre) {
|
||||
// Если выбран конкретный жанр, показываем только его
|
||||
const selectedGenreObj = genres.find(g => g.id === selectedGenre);
|
||||
return selectedGenreObj ? [selectedGenreObj] : [];
|
||||
}
|
||||
|
||||
// Иначе показываем все жанры, у которых есть книги в текущей выборке
|
||||
return genres.filter(genre =>
|
||||
books.some(book => book.genres?.some(bg => bg.genre?.id === genre.id))
|
||||
);
|
||||
};
|
||||
|
||||
const allGenres = getGenresWithBooks();
|
||||
|
||||
// Обработчик удаления жанра
|
||||
const handleDeleteGenre = async (genreId) => {
|
||||
if (window.confirm('Вы уверены, что хотите удалить этот жанр? Все книги этого жанра будут без жанра.')) {
|
||||
if (window.confirm('Вы уверены, что хотите удалить этот жанр?')) {
|
||||
try {
|
||||
await api.deleteGenre(genreId);
|
||||
await fetchData(); // Полная перезагрузка данных
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления жанра:', error);
|
||||
alert('Нельзя удалить жанр, если есть книги с этим жанром');
|
||||
@@ -81,21 +219,18 @@ const CatalogPage = () => {
|
||||
|
||||
const handleAddToCart = async (bookId) => {
|
||||
try {
|
||||
// Проверяем, есть ли уже эта книга в корзине
|
||||
const cartResponse = await api.fetchCartItems();
|
||||
const existingItem = cartResponse.data.find(item => item.book?.id === bookId);
|
||||
const cartItems = cartResponse.data || [];
|
||||
const existingItem = cartItems.find(item => item.book?.id === bookId);
|
||||
|
||||
if (existingItem) {
|
||||
// Обновляем количество существующего элемента
|
||||
await api.updateCartItemQuantity(existingItem.id, existingItem.quantity + 1);
|
||||
} else {
|
||||
// Добавляем новый элемент
|
||||
await api.addToCart(bookId);
|
||||
}
|
||||
|
||||
// Обновляем счетчик корзины
|
||||
const updatedCart = await api.fetchCartItems();
|
||||
updateCartCount(updatedCart.data);
|
||||
updateCartCount(updatedCart.data || []);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
@@ -119,8 +254,7 @@ const CatalogPage = () => {
|
||||
const handleSaveGenre = async (genreData) => {
|
||||
try {
|
||||
await api.createGenre(genreData);
|
||||
const genresResponse = await api.fetchGenres();
|
||||
setGenres(genresResponse.data || []);
|
||||
await fetchData();
|
||||
setShowGenreModal(false);
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения жанра:', error);
|
||||
@@ -138,19 +272,6 @@ const CatalogPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const booksByGenre = (genreId) => {
|
||||
return books.filter(book => book.genre?.id === genreId);
|
||||
};
|
||||
|
||||
const genresWithBooks = genres.filter(genre =>
|
||||
books.some(book => book.genre?.id === genre.id)
|
||||
);
|
||||
|
||||
// Добавляем жанры без книг
|
||||
const allGenres = [...genresWithBooks, ...genres.filter(genre =>
|
||||
!genresWithBooks.some(g => g.id === genre.id)
|
||||
)];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center vh-100">
|
||||
@@ -168,43 +289,143 @@ const CatalogPage = () => {
|
||||
<Container>
|
||||
<h2 className="text-center mb-5">Каталог книг</h2>
|
||||
|
||||
<div className="d-flex justify-content-between mb-4">
|
||||
<div>
|
||||
<Button variant="success" onClick={handleAddBook}>
|
||||
Добавить книгу
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="ms-2"
|
||||
onClick={() => setShowGenreModal(true)}
|
||||
>
|
||||
Добавить жанр
|
||||
</Button>
|
||||
{error && (
|
||||
<Alert variant="danger" className="mb-4">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Панель управления */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
<Button variant="success" onClick={handleAddBook}>
|
||||
Добавить книгу
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setShowGenreModal(true)}
|
||||
>
|
||||
Добавить жанр
|
||||
</Button>
|
||||
|
||||
{/* Фильтр по жанрам */}
|
||||
{Array.isArray(genres) && genres.length > 0 && (
|
||||
<div className="dropdown">
|
||||
<Button variant="outline-secondary" id="genreFilter">
|
||||
{selectedGenre && genres.find(g => g.id === selectedGenre)
|
||||
? `Жанр: ${genres.find(g => g.id === selectedGenre)?.name}`
|
||||
: 'Все жанры'}
|
||||
</Button>
|
||||
<div className="dropdown-menu" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => handleGenreFilter(null)}
|
||||
>
|
||||
Все жанры
|
||||
</button>
|
||||
{genres.map(genre => (
|
||||
<button
|
||||
key={genre.id}
|
||||
className="dropdown-item"
|
||||
onClick={() => handleGenreFilter(genre.id)}
|
||||
>
|
||||
{genre.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка сброса фильтров */}
|
||||
{(selectedGenre || searchQuery) && (
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Сбросить фильтры
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<div className="d-flex">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Поиск книг..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
className="ms-2"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
Найти
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Информация о текущих фильтрах */}
|
||||
{(selectedGenre || searchQuery) && (
|
||||
<div className="mt-2">
|
||||
<small className="text-muted">
|
||||
{selectedGenre && `Фильтр: ${genres.find(g => g.id === selectedGenre)?.name}`}
|
||||
{selectedGenre && searchQuery && ' | '}
|
||||
{searchQuery && `Поиск: "${searchQuery}"`}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allGenres.length === 0 ? (
|
||||
{/* Информация о пагинации */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div className="text-muted">
|
||||
{selectedGenre && Array.isArray(genres) && ` в жанре "${genres.find(g => g.id === selectedGenre)?.name}"`}
|
||||
{searchQuery && ` по запросу "${searchQuery}"`}
|
||||
{!selectedGenre && !searchQuery && 'Все книги'}
|
||||
</div>
|
||||
<PageSizeSelector
|
||||
pageSize={bookPagination.pageSize}
|
||||
onPageSizeChange={handleBookPageSizeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Список книг */}
|
||||
{books.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<h4>Нет данных для отображения</h4>
|
||||
<p className="text-muted">Добавьте жанры и книги для начала работы</p>
|
||||
<p className="text-muted">
|
||||
{Array.isArray(genres) && genres.length === 0
|
||||
? 'Добавьте жанры и книги для начала работы'
|
||||
: 'Нет книг для отображения'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
allGenres.map(genre => (
|
||||
<section key={genre.id} className="mb-5">
|
||||
<div className="genre-title bg-light p-3 rounded text-center mb-4">
|
||||
<h3 className="mb-2">{genre.name}</h3>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteGenre(genre.id)}
|
||||
title="Удалить жанр"
|
||||
>
|
||||
Удалить жанр
|
||||
</Button>
|
||||
</div>
|
||||
<Row>
|
||||
{booksByGenre(genre.id).length > 0 ? (
|
||||
booksByGenre(genre.id).map(book => (
|
||||
<>
|
||||
{/* Режим: выбран конкретный жанр */}
|
||||
{selectedGenre ? (
|
||||
<section className="mb-5">
|
||||
<div className="genre-title bg-light p-3 rounded text-center mb-4">
|
||||
<h3 className="mb-2">
|
||||
{genres.find(g => g.id === selectedGenre)?.name}
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
size="sm"
|
||||
onClick={() => setSelectedGenre(null)}
|
||||
>
|
||||
Показать все жанры
|
||||
</Button>
|
||||
</div>
|
||||
<Row>
|
||||
{books.map(book => (
|
||||
<BookComponent
|
||||
key={book.id}
|
||||
book={book}
|
||||
@@ -212,15 +433,65 @@ const CatalogPage = () => {
|
||||
onDelete={handleDeleteBook}
|
||||
onAddToCart={handleAddToCart}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-3">
|
||||
<p className="text-muted">В этом жанре пока нет книг</p>
|
||||
))}
|
||||
</Row>
|
||||
</section>
|
||||
) : (
|
||||
/* Режим: все жанры с группировкой */
|
||||
allGenres.map(genre => (
|
||||
<section key={genre.id} className="mb-5">
|
||||
<div className="genre-title bg-light p-3 rounded text-center mb-4">
|
||||
<h3 className="mb-2">{genre.name}</h3>
|
||||
<div className="d-flex justify-content-center gap-2">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => handleGenreFilter(genre.id)}
|
||||
>
|
||||
Показать только этот жанр
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteGenre(genre.id)}
|
||||
title="Удалить жанр"
|
||||
>
|
||||
Удалить жанр
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
</section>
|
||||
))
|
||||
<Row>
|
||||
{getBooksByGenre(genre.id).length > 0 ? (
|
||||
getBooksByGenre(genre.id).map(book => (
|
||||
<BookComponent
|
||||
key={book.id}
|
||||
book={book}
|
||||
onEdit={handleEditBook}
|
||||
onDelete={handleDeleteBook}
|
||||
onAddToCart={handleAddToCart}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-3">
|
||||
<p className="text-muted">В этом жанре пока нет книг</p>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
</section>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Пагинация книг */}
|
||||
{bookPagination.totalPages > 1 && (
|
||||
<div className="mt-4">
|
||||
<Pagination
|
||||
currentPage={bookPagination.currentPage}
|
||||
totalPages={bookPagination.totalPages}
|
||||
onPageChange={handleBookPageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</main>
|
||||
@@ -231,7 +502,7 @@ const CatalogPage = () => {
|
||||
show={showBookModal}
|
||||
onHide={() => setShowBookModal(false)}
|
||||
bookId={currentBookId}
|
||||
genres={genres}
|
||||
genres={Array.isArray(genres) ? genres : []}
|
||||
onSave={handleSaveBook}
|
||||
/>
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ import Navbar from '../components/Navbar';
|
||||
import Footer from '../components/Footer';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min';
|
||||
import Book1 from '../images/the_girl_with_the_dragon_tattoo.jpg';
|
||||
import Book2 from '../images/the_hobbit.webp';
|
||||
import Book3 from '../images/dune.jpg';
|
||||
import defBook from '../images/default-book.jpg';
|
||||
|
||||
|
||||
const DiscountsPage = () => {
|
||||
const discountedBooks = [
|
||||
@@ -12,7 +17,7 @@ const DiscountsPage = () => {
|
||||
id: 1,
|
||||
title: "Девушка с татуировкой дракона",
|
||||
author: "Стиг Ларссон",
|
||||
image: "images/the_girl_with_the_dragon_tattoo.jpg",
|
||||
image: Book1,
|
||||
originalPrice: 700,
|
||||
discountPrice: 525,
|
||||
discountPercent: 25
|
||||
@@ -21,7 +26,7 @@ const DiscountsPage = () => {
|
||||
id: 2,
|
||||
title: "Хоббит",
|
||||
author: "Дж.Р.Р. Толкин",
|
||||
image: "images/the_hobbit.webp",
|
||||
image: Book2,
|
||||
originalPrice: 750,
|
||||
discountPrice: 563,
|
||||
discountPercent: 25
|
||||
@@ -30,7 +35,7 @@ const DiscountsPage = () => {
|
||||
id: 3,
|
||||
title: "Дюна",
|
||||
author: "Фрэнк Герберт",
|
||||
image: "images/dune.jpg",
|
||||
image: Book2,
|
||||
originalPrice: 500,
|
||||
discountPrice: 375,
|
||||
discountPercent: 25
|
||||
@@ -60,7 +65,7 @@ const DiscountsPage = () => {
|
||||
src={book.image}
|
||||
className="p-3"
|
||||
alt={book.title}
|
||||
onError={(e) => { e.target.src = 'images/default-book.jpg' }}
|
||||
onError={(e) => { e.target.src = defBook }}
|
||||
/>
|
||||
<Card.Body className="text-center d-flex flex-column">
|
||||
<Card.Title>{book.title}</Card.Title>
|
||||
|
||||
@@ -4,23 +4,28 @@ import { Button, Card, Col, Row } from 'react-bootstrap';
|
||||
import { BiCart } from 'react-icons/bi';
|
||||
import Footer from '../components/Footer';
|
||||
import Navbar from '../components/Navbar';
|
||||
import Book1 from '../images/Book1.jpg';
|
||||
import Book2 from '../images/Book2.jpg';
|
||||
import Book3 from '../images/Book3.jpg';
|
||||
import defBook from '../images/default-book.jpg';
|
||||
|
||||
|
||||
const HomePage = () => {
|
||||
const bestsellers = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Тимоти Брук «Шляпа Вермеера»",
|
||||
image: "images/Book1.jpg"
|
||||
image: Book1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Пол Линч «Песнь пророка»",
|
||||
image: "images/Book2.jpg"
|
||||
image: Book2
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Яна Вагнер «Тоннель»",
|
||||
image: "images/Book3.jpg"
|
||||
image: Book3
|
||||
}
|
||||
];
|
||||
|
||||
@@ -61,7 +66,7 @@ const HomePage = () => {
|
||||
src={book.image}
|
||||
className="p-3"
|
||||
alt={book.title}
|
||||
onError={(e) => { e.target.src = 'images/default-book.jpg' }}
|
||||
onError={(e) => { e.target.src = defBook }}
|
||||
/>
|
||||
<Card.Body className="text-center d-flex flex-column">
|
||||
<Card.Title>{book.title}</Card.Title>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.5.5'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
}
|
||||
|
||||
group = 'com.example'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
description = 'Demo project for Spring Boot'
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
bootJar {
|
||||
archiveFileName = String.format("%s-%s.jar", rootProject.name, version)
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
springdocVersion = "2.8.11"
|
||||
mockitoVersion = "5.19.0"
|
||||
}
|
||||
|
||||
configurations {
|
||||
mockitoAgent
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||
mockitoAgent("org.mockito:mockito-core:${mockitoVersion}") {
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
jvmArgs += "-Xshare:off"
|
||||
jvmArgs += "-javaagent:${configurations.mockitoAgent.asPath}"
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.example.server;
|
||||
|
||||
import com.example.server.mapper.BookMapper;
|
||||
import com.example.server.mapper.CartItemMapper;
|
||||
import com.example.server.mapper.GenreMapper;
|
||||
import com.example.server.service.BookService;
|
||||
import com.example.server.service.CartItemService;
|
||||
import com.example.server.service.GenreService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ServerApplication implements CommandLineRunner {
|
||||
private final Logger log = LoggerFactory.getLogger(ServerApplication.class);
|
||||
|
||||
private final BookService bookService;
|
||||
private final GenreService genreService;
|
||||
private final CartItemService cartItemService;
|
||||
private final BookMapper bookMapper;
|
||||
private final GenreMapper genreMapper;
|
||||
private final CartItemMapper cartItemMapper;
|
||||
|
||||
public ServerApplication(BookService bookService, GenreService genreService,
|
||||
CartItemService cartItemService, BookMapper bookMapper,
|
||||
GenreMapper genreMapper, CartItemMapper cartItemMapper) {
|
||||
this.bookService = bookService;
|
||||
this.bookMapper = bookMapper;
|
||||
this.genreService = genreService;
|
||||
this.genreMapper = genreMapper;
|
||||
this.cartItemService = cartItemService;
|
||||
this.cartItemMapper = cartItemMapper;
|
||||
}
|
||||
|
||||
private void populateData() {
|
||||
log.info("Create default genres");
|
||||
final var genre1 = genreService.create(genreMapper.toRqDto("Роман"));
|
||||
final var genre2 = genreService.create(genreMapper.toRqDto("Фантастика"));
|
||||
final var genre3 = genreService.create(genreMapper.toRqDto("Ужасы"));
|
||||
|
||||
log.info("Create default books");
|
||||
bookService.create(bookMapper.toRqDto("Основание", "Айзек Азимов",
|
||||
700, "Сага о падении и возрождении галактической империи, основанная на научных принципах.", "images/foundation.jpg", genre1.getId()));
|
||||
bookService.create(bookMapper.toRqDto("Дюна", "Фрэнк Герберт",
|
||||
900, "Эпическая история о борьбе за контроль над планетой Арракис, источником самого ценного вещества во вселенной.", "images/dune.jpg", genre2.getId()));
|
||||
bookService.create(bookMapper.toRqDto("Убийство в Восточном экспрессе", "Агата Кристи",
|
||||
750, "Загадочное убийство на поезде, где каждый пассажир может быть подозреваемым.", "images/murder_on_the_orient_express.jpg", genre3.getId()));
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ServerApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
if (args.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (Objects.equals("--populate", args[0])) {
|
||||
populateData();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.example.server.api.genre.GenreController;
|
||||
import com.example.server.error.NotFoundException;
|
||||
import com.example.server.service.BookService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.example.server.configuration.Constants;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.API_URL + BookController.URL)
|
||||
public class BookController {
|
||||
public static final String URL = "/book";
|
||||
private final BookService bookService;
|
||||
|
||||
public BookController(BookService bookService) {
|
||||
this.bookService = bookService;
|
||||
}
|
||||
|
||||
/* @GetMapping
|
||||
public List<BookRs> getAll() {
|
||||
return bookService.getAll();
|
||||
}*/
|
||||
|
||||
@GetMapping
|
||||
public List<BookRs> getAll(@RequestParam(required = false) Long genreId) {
|
||||
if (genreId != null) {
|
||||
return bookService.getByGenre(genreId);
|
||||
}
|
||||
return bookService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public BookRs get(@PathVariable Long id) {
|
||||
return bookService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public BookRs create(@RequestBody @Valid BookRq dto) {
|
||||
return bookService.create(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public BookRs update(@PathVariable Long id, @RequestBody @Valid BookRq dto) {
|
||||
return bookService.update(id, dto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public BookRs delete(@PathVariable Long id) {
|
||||
return bookService.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class BookRq {
|
||||
@JsonProperty("title")
|
||||
@NotBlank(message = "название обязателен")
|
||||
@Size(min = 2, max = 50, message = "Название должно быть от 2 до 50 символов")
|
||||
private String title;
|
||||
@JsonProperty("author")
|
||||
@NotBlank(message = "автор обязателен")
|
||||
@Size(min = 2, max = 50, message = "ФИО должно быть от 2 до 50 символов")
|
||||
private String author;
|
||||
@NotNull
|
||||
@Min(value = 1, message = "Цена не может быть меньше 1 рубля")
|
||||
private int price;
|
||||
private String description;
|
||||
@NotNull
|
||||
private Long genreId;
|
||||
private String image;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(int price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public Long getGenreId() {
|
||||
return genreId;
|
||||
}
|
||||
|
||||
public void setGenreId(Long genreId) {
|
||||
this.genreId = genreId;
|
||||
}
|
||||
|
||||
public String getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setImage(String image) {
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import com.example.server.api.genre.GenreRs;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class BookRs {
|
||||
@NotBlank(message = "id обязательно")
|
||||
private Long Id;
|
||||
@JsonProperty("title")
|
||||
@NotBlank(message = "Название обязательно")
|
||||
@Size(min = 2, max = 50, message = "Название должно быть от 2 до 50 символов")
|
||||
private String title;
|
||||
@JsonProperty("author")
|
||||
@NotBlank(message = "автор обязателен")
|
||||
@Size(min = 2, max = 50, message = "ФИО должно быть от 2 до 50 символов")
|
||||
private String author;
|
||||
@NotNull
|
||||
@Min(value = 1, message = "Цена не может быть меньше 1 рубля")
|
||||
private int price;
|
||||
private String description;
|
||||
@NotNull
|
||||
private GenreRs genre;
|
||||
private String image;
|
||||
|
||||
public Long getId() {
|
||||
return Id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(int price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public GenreRs getGenre() {
|
||||
return genre;
|
||||
}
|
||||
|
||||
public void setGenre(GenreRs genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public String getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setImage(String image) {
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.example.server.api.cartItem;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public class CartItemRq {
|
||||
@NotNull
|
||||
private Long bookId;
|
||||
@NotNull
|
||||
@Min(value = 0, message = "Кол-во не может быть меньше 0")
|
||||
private int quantity;
|
||||
|
||||
public int getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public void setQuantity(int quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public Long getBookId() {
|
||||
return bookId;
|
||||
}
|
||||
|
||||
public void setBookId(Long bookId) {
|
||||
this.bookId = bookId;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.example.server.api.cartItem;
|
||||
|
||||
import com.example.server.api.book.BookRs;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public class CartItemRs {
|
||||
@NotNull
|
||||
private Long id;
|
||||
@NotNull
|
||||
private BookRs book;
|
||||
@NotNull
|
||||
@Min(value = 0, message = "Кол-во не может быть меньше 0")
|
||||
private int quantity;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public void setQuantity(int quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public BookRs getBook() {
|
||||
return book;
|
||||
}
|
||||
|
||||
public void setBook(BookRs book) {
|
||||
this.book = book;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.example.server.api.genre;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class GenreRq {
|
||||
@NotBlank(message = "Название обязательно")
|
||||
@Size(min = 2, max = 50, message = "Название должно быть от 2 до 50 символов")
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.example.server.api.genre;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class GenreRs {
|
||||
@NotNull
|
||||
private Long id;
|
||||
@NotBlank(message = "Название обязательно")
|
||||
@Size(min = 2, max = 50, message = "Название должно быть от 2 до 50 символов")
|
||||
private String name;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
public abstract class BaseEntity {
|
||||
protected Long Id;
|
||||
|
||||
protected BaseEntity() {}
|
||||
|
||||
public Long getId() {
|
||||
return Id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
public class GenreEntity extends BaseEntity{
|
||||
private String name;
|
||||
|
||||
public GenreEntity() {
|
||||
super();
|
||||
}
|
||||
public GenreEntity(String name) {
|
||||
this();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.example.server.mapper;
|
||||
|
||||
import com.example.server.api.book.BookRq;
|
||||
import com.example.server.api.book.BookRs;
|
||||
import com.example.server.entity.BookEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@Component
|
||||
public class BookMapper {
|
||||
private final GenreMapper genreMapper;
|
||||
|
||||
public BookMapper(GenreMapper genreMapper) {
|
||||
this.genreMapper = genreMapper;
|
||||
}
|
||||
|
||||
public BookRq toRqDto(
|
||||
String title, String author, int price, String description, String image, Long genreId) {
|
||||
final BookRq dto = new BookRq();
|
||||
dto.setTitle(title);
|
||||
dto.setAuthor(author);
|
||||
dto.setPrice(price);
|
||||
dto.setDescription(description);
|
||||
dto.setImage(image);
|
||||
dto.setGenreId(genreId);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public BookRs toRsDto(BookEntity entity) {
|
||||
final BookRs dto = new BookRs();
|
||||
dto.setId(entity.getId());
|
||||
dto.setTitle(entity.getTitle());
|
||||
dto.setAuthor(entity.getAuthor());
|
||||
dto.setPrice(entity.getPrice());
|
||||
dto.setDescription(entity.getDescription());
|
||||
dto.setGenre(genreMapper.toRsDto(entity.getGenre()));
|
||||
dto.setImage(entity.getImage());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<BookRs> toRsListDto(Iterable<BookEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(this::toRsDto)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.example.server.mapper;
|
||||
|
||||
|
||||
import com.example.server.api.cartItem.CartItemRq;
|
||||
import com.example.server.api.cartItem.CartItemRs;
|
||||
import com.example.server.entity.CartItemEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@Component
|
||||
public class CartItemMapper {
|
||||
private final BookMapper bookMapper;
|
||||
|
||||
public CartItemMapper(BookMapper bookMapper) {
|
||||
this.bookMapper = bookMapper;
|
||||
}
|
||||
|
||||
public CartItemRq toRqDto(Long bookId, int quantity) {
|
||||
final CartItemRq dto = new CartItemRq();
|
||||
dto.setBookId(bookId);
|
||||
dto.setQuantity(quantity);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public CartItemRs toRsDto(CartItemEntity entity) {
|
||||
final CartItemRs dto = new CartItemRs();
|
||||
dto.setId(entity.getId());
|
||||
dto.setBook(bookMapper.toRsDto(entity.getBook()));
|
||||
dto.setQuantity(entity.getQuantity());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<CartItemRs> toRsListDto(Iterable<CartItemEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(this::toRsDto)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.example.server.mapper;
|
||||
|
||||
import com.example.server.api.genre.GenreRq;
|
||||
import com.example.server.api.genre.GenreRs;
|
||||
import com.example.server.entity.GenreEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@Component
|
||||
public class GenreMapper {
|
||||
public GenreRq toRqDto(String name) {
|
||||
final GenreRq dto = new GenreRq();
|
||||
dto.setName(name);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public GenreRs toRsDto(GenreEntity entity) {
|
||||
final GenreRs dto = new GenreRs();
|
||||
dto.setId(entity.getId());
|
||||
dto.setName(entity.getName());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<GenreRs> toRsDtoList(Iterable<GenreEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(this::toRsDto)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.example.server.repository;
|
||||
|
||||
import com.example.server.entity.BookEntity;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class BookRepository extends MapRepository<BookEntity> {
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.example.server.repository;
|
||||
|
||||
import com.example.server.entity.CartItemEntity;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public class CartItemRepository extends MapRepository<CartItemEntity> {
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.example.server.repository;
|
||||
|
||||
import com.example.server.entity.CartItemEntity;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CommonRepository<E, T> {
|
||||
Iterable<E> findAll();
|
||||
|
||||
Optional<E> findById(T id);
|
||||
|
||||
E save(E entity);
|
||||
|
||||
void delete(E entity);
|
||||
|
||||
void deleteAll();
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.example.server.repository;
|
||||
|
||||
import com.example.server.entity.GenreEntity;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class GenreRepository extends MapRepository<GenreEntity> {
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.example.server.repository;
|
||||
|
||||
import com.example.server.entity.BaseEntity;
|
||||
import com.example.server.entity.CartItemEntity;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentNavigableMap;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public abstract class MapRepository<E extends BaseEntity> implements CommonRepository<E, Long> {
|
||||
private final ConcurrentNavigableMap<Long, E> entities = new ConcurrentSkipListMap<>();
|
||||
private final AtomicLong idGenerator = new AtomicLong(0L);
|
||||
|
||||
protected MapRepository() {
|
||||
}
|
||||
|
||||
private boolean isNew(E entity) {
|
||||
return Objects.isNull(entity.getId());
|
||||
}
|
||||
|
||||
private E create(E entity) {
|
||||
final Long lastId = idGenerator.incrementAndGet();
|
||||
entity.setId(lastId);
|
||||
entities.put(lastId, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private E update(E entity) {
|
||||
if (findById(entity.getId()).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
entities.put(entity.getId(), entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<E> findAll() {
|
||||
return entities.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<E> findById(Long id) {
|
||||
return Optional.ofNullable(entities.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public E save(E entity) {
|
||||
if (isNew(entity)) {
|
||||
return create(entity);
|
||||
}
|
||||
return update(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(E entity) {
|
||||
if (findById(entity.getId()).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
entities.remove(entity.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll() {
|
||||
entities.clear();
|
||||
idGenerator.set(0L);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.example.server.service;
|
||||
|
||||
import com.example.server.api.book.BookRq;
|
||||
import com.example.server.api.book.BookRs;
|
||||
import com.example.server.entity.BookEntity;
|
||||
import com.example.server.entity.GenreEntity;
|
||||
import com.example.server.error.NotFoundException;
|
||||
import com.example.server.mapper.BookMapper;
|
||||
import com.example.server.repository.BookRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@Service
|
||||
public class BookService {
|
||||
private final BookRepository repository;
|
||||
private final GenreService typeService;
|
||||
private final BookMapper mapper;
|
||||
|
||||
public BookService(BookRepository repository, GenreService typeService, BookMapper mapper) {
|
||||
this.repository = repository;
|
||||
this.typeService = typeService;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public BookEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(BookEntity.class, id));
|
||||
}
|
||||
|
||||
public List<BookRs> getAll() {
|
||||
return mapper.toRsListDto(repository.findAll());
|
||||
}
|
||||
|
||||
public List<BookRs> getByGenre(Long genreId) {
|
||||
return StreamSupport.stream(repository.findAll().spliterator(), false)
|
||||
.filter(book -> book.getGenre().getId().equals(genreId))
|
||||
.map(mapper::toRsDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public BookRs get(Long id) {
|
||||
final BookEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
public BookRs create(BookRq dto) {
|
||||
final GenreEntity genre = typeService.getEntity(dto.getGenreId());
|
||||
BookEntity entity = new BookEntity(
|
||||
dto.getTitle(),
|
||||
dto.getAuthor(),
|
||||
dto.getPrice(),
|
||||
dto.getDescription(),
|
||||
genre,
|
||||
dto.getImage());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
public BookRs update(Long id, BookRq dto) {
|
||||
BookEntity entity = getEntity(id);
|
||||
entity.setTitle(dto.getTitle());
|
||||
entity.setAuthor(dto.getAuthor());
|
||||
entity.setPrice(dto.getPrice());
|
||||
entity.setDescription(dto.getDescription());
|
||||
entity.setGenre(typeService.getEntity(dto.getGenreId()));
|
||||
entity.setImage(dto.getImage());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
public BookRs delete(Long id) {
|
||||
final BookEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.example.server.service;
|
||||
|
||||
import com.example.server.api.genre.GenreRq;
|
||||
import com.example.server.api.genre.GenreRs;
|
||||
import com.example.server.entity.GenreEntity;
|
||||
import com.example.server.error.NotFoundException;
|
||||
import com.example.server.mapper.GenreMapper;
|
||||
import com.example.server.repository.GenreRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class GenreService {
|
||||
private final GenreRepository repository;
|
||||
private final GenreMapper mapper;
|
||||
|
||||
public GenreService(GenreRepository repository, GenreMapper mapper) {
|
||||
this.repository = repository;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public GenreEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(GenreEntity.class, id));
|
||||
}
|
||||
|
||||
public List<GenreRs> getAll() {
|
||||
return mapper.toRsDtoList(repository.findAll());
|
||||
}
|
||||
|
||||
public GenreRs get(Long id) {
|
||||
final GenreEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
public GenreRs create(GenreRq dto) {
|
||||
GenreEntity entity = new GenreEntity(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
public GenreRs update(Long id, GenreRq dto) {
|
||||
GenreEntity entity = getEntity(id);
|
||||
entity.setName(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
public GenreRs delete(Long id) {
|
||||
final GenreEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
spring.application.name=server
|
||||
spring.main.banner-mode=off
|
||||
bootRun.args=--populate
|
||||
|
||||
logging.level.com.server=DEBUG
|
||||
|
||||
# ???????? ??? ????????? ????????
|
||||
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
|
||||
|
||||
# ?????????? ??? HTTP ???????
|
||||
server.tomcat.accesslog.enabled=true
|
||||
server.tomcat.accesslog.directory=logs
|
||||
server.tomcat.accesslog.prefix=access_log
|
||||
server.tomcat.accesslog.suffix=.log
|
||||
@@ -1,128 +0,0 @@
|
||||
package com.example.server.service;
|
||||
|
||||
import com.example.server.api.book.BookRs;
|
||||
import com.example.server.error.NotFoundException;
|
||||
import com.example.server.mapper.BookMapper;
|
||||
import com.example.server.mapper.GenreMapper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SpringBootTest
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
class BookServiceTests {
|
||||
@Autowired
|
||||
private BookService service;
|
||||
|
||||
@Autowired
|
||||
private GenreService genreService;
|
||||
|
||||
@Autowired
|
||||
private BookMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private GenreMapper genreMapper;
|
||||
|
||||
@Test
|
||||
void getTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void createTest() {
|
||||
var genre1 = genreService.create(genreMapper.toRqDto("Роман"));
|
||||
var genre2 = genreService.create(genreMapper.toRqDto("Фантастика"));
|
||||
|
||||
service.create(mapper.toRqDto("Книга 1", "Автор 1", 500, "Описание 1", "image1.jpg", 1L));
|
||||
service.create(mapper.toRqDto("Книга 2", "Автор 2", 600, "Описание 2", "image2.jpg", 2L));
|
||||
final BookRs last = service.create(mapper.toRqDto("Книга 3", "Автор 3", 700, "Описание 3", "image3.jpg", 1L));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
|
||||
final BookRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(last.getTitle(), cmpEntity.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void updateTest() {
|
||||
final String newTitle = "Обновленная книга";
|
||||
final BookRs entity = service.get(3L);
|
||||
final String oldTitle = entity.getTitle();
|
||||
final BookRs newEntity = service.update(3L, mapper.toRqDto(newTitle, "Автор 3", 750, "Новое описание", "new_image.jpg", 1L));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(newTitle, newEntity.getTitle());
|
||||
Assertions.assertNotEquals(oldTitle, newEntity.getTitle());
|
||||
|
||||
final BookRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(newEntity.getTitle(), cmpEntity.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void getByGenreTest() {
|
||||
List<BookRs> booksByGenre1 = service.getByGenre(1L);
|
||||
List<BookRs> booksByGenre2 = service.getByGenre(2L);
|
||||
|
||||
Assertions.assertEquals(2, booksByGenre1.size()); // 2 книги в жанре 1
|
||||
Assertions.assertEquals(1, booksByGenre2.size()); // 1 книга в жанре 2
|
||||
|
||||
booksByGenre1.forEach(book ->
|
||||
Assertions.assertEquals(1L, book.getGenre().getId())
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
void deleteTest() {
|
||||
service.delete(3L);
|
||||
Assertions.assertEquals(2, service.getAll().size());
|
||||
|
||||
final BookRs last = service.get(2L);
|
||||
Assertions.assertEquals(2L, last.getId());
|
||||
|
||||
final BookRs newEntity = service.create(mapper.toRqDto("Новая книга", "Новый автор", 800, "Описание", "image.jpg", 2L));
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(4L, newEntity.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
void getAllTest() {
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
|
||||
final BookRs first = service.get(1L);
|
||||
final BookRs second = service.get(2L);
|
||||
final BookRs third = service.get(4L);
|
||||
|
||||
Assertions.assertEquals("Книга 1", first.getTitle());
|
||||
Assertions.assertEquals("Книга 2", second.getTitle());
|
||||
Assertions.assertEquals("Новая книга", third.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () ->
|
||||
service.update(999L, mapper.toRqDto("Несуществующая", "Автор", 100, "Описание", "image.jpg", 1L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.delete(999L));
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package com.example.server.service;
|
||||
|
||||
import com.example.server.api.cartItem.CartItemRs;
|
||||
import com.example.server.error.NotFoundException;
|
||||
import com.example.server.mapper.BookMapper;
|
||||
import com.example.server.mapper.CartItemMapper;
|
||||
import com.example.server.mapper.GenreMapper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@SpringBootTest
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
class CartItemServiceTests {
|
||||
@Autowired
|
||||
private CartItemService service;
|
||||
|
||||
@Autowired
|
||||
private BookService bookService;
|
||||
|
||||
@Autowired
|
||||
private GenreService genreService;
|
||||
|
||||
@Autowired
|
||||
private CartItemMapper mapper;
|
||||
@Autowired
|
||||
private BookMapper bookMapper;
|
||||
|
||||
@Autowired
|
||||
private GenreMapper genreMapper;
|
||||
|
||||
@Test
|
||||
void getTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void createTest() {
|
||||
var genre1 = genreService.create(genreMapper.toRqDto("Роман"));
|
||||
var genre2 = genreService.create(genreMapper.toRqDto("Фантастика"));
|
||||
|
||||
bookService.create(bookMapper.toRqDto("Книга 1", "Автор 1", 500, "Описание 1", "image1.jpg", 1L));
|
||||
bookService.create(bookMapper.toRqDto("Книга 2", "Автор 2", 600, "Описание 2", "image2.jpg", 2L));
|
||||
bookService.create(bookMapper.toRqDto("Книга 3", "Автор 3", 600, "Описание 3", "image3.jpg", 1L));
|
||||
bookService.create(bookMapper.toRqDto("Книга 4", "Автор 4", 500, "Описание 4", "image4.jpg", 2L));
|
||||
|
||||
service.create(mapper.toRqDto(1L, 2));
|
||||
service.create(mapper.toRqDto(2L, 1));
|
||||
final CartItemRs last = service.create(mapper.toRqDto(4L, 3));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
|
||||
final CartItemRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(last.getQuantity(), cmpEntity.getQuantity());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void updateTest() {
|
||||
final CartItemRs entity = service.get(3L);
|
||||
final int oldQuantity = entity.getQuantity();
|
||||
final CartItemRs newEntity = service.update(3L, mapper.toRqDto(4L, 5));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(5, newEntity.getQuantity());
|
||||
Assertions.assertNotEquals(oldQuantity, newEntity.getQuantity());
|
||||
|
||||
final CartItemRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(newEntity.getQuantity(), cmpEntity.getQuantity());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void updateQuantityTest() {
|
||||
final CartItemRs updated = service.updateQuantity(1L, 10);
|
||||
Assertions.assertEquals(10, updated.getQuantity());
|
||||
|
||||
final CartItemRs entity = service.get(1L);
|
||||
Assertions.assertEquals(10, entity.getQuantity());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
void getByBookIdTest() {
|
||||
Optional<CartItemRs> cartItem = service.getByBookId(4L);
|
||||
Assertions.assertTrue(cartItem.isPresent());
|
||||
Assertions.assertEquals(4L, cartItem.get().getBook().getId());
|
||||
Assertions.assertEquals(5, cartItem.get().getQuantity());
|
||||
|
||||
Optional<CartItemRs> nonExistent = service.getByBookId(999L);
|
||||
Assertions.assertFalse(nonExistent.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
void deleteTest() {
|
||||
service.delete(3L);
|
||||
Assertions.assertEquals(2, service.getAll().size());
|
||||
|
||||
final CartItemRs last = service.get(2L);
|
||||
Assertions.assertEquals(2L, last.getId());
|
||||
|
||||
final CartItemRs newEntity = service.create(mapper.toRqDto(1L, 1));
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(4L, newEntity.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
void clearCartTest() {
|
||||
service.deleteAll();
|
||||
Assertions.assertEquals(0, service.getAll().size());
|
||||
|
||||
// Создаем новый элемент после очистки
|
||||
final CartItemRs newEntity = service.create(mapper.toRqDto(2L, 2));
|
||||
Assertions.assertEquals(1, service.getAll().size());
|
||||
Assertions.assertEquals(1L, newEntity.getId()); // ID продолжает инкрементироваться
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () ->
|
||||
service.update(999L, mapper.toRqDto(1L, 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateQuantityNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () ->
|
||||
service.updateQuantity(999L, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.delete(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
void createDuplicateBookTest() {
|
||||
// Можно добавить одну и ту же книгу в корзину несколько раз
|
||||
final CartItemRs item1 = service.create(mapper.toRqDto(1L, 1));
|
||||
final CartItemRs item2 = service.create(mapper.toRqDto(1L, 2));
|
||||
|
||||
Assertions.assertNotEquals(item1.getId(), item2.getId());
|
||||
Assertions.assertEquals(1L, item1.getBook().getId());
|
||||
Assertions.assertEquals(1L, item2.getBook().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
void updateQuantityToZeroTest() {
|
||||
// Обновление количества до 0 должно удалить элемент
|
||||
final CartItemRs deleted = service.updateQuantity(1L, 0);
|
||||
Assertions.assertEquals(0, deleted.getQuantity());
|
||||
|
||||
// Элемент должен быть удален
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(5L));
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package com.example.server.service;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import com.example.server.api.genre.GenreRq;
|
||||
import com.example.server.api.genre.GenreRs;
|
||||
import com.example.server.error.NotFoundException;
|
||||
import com.example.server.mapper.GenreMapper;
|
||||
|
||||
@SpringBootTest
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
class GenreServiceTests {
|
||||
@Autowired
|
||||
private GenreService service;
|
||||
|
||||
@Autowired
|
||||
private GenreMapper mapper;
|
||||
|
||||
@Test
|
||||
void getTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void createTest() {
|
||||
service.create(mapper.toRqDto("Роман"));
|
||||
service.create(mapper.toRqDto("Фантастика"));
|
||||
final GenreRs last = service.create(mapper.toRqDto("Детектив"));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
|
||||
final GenreRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(last.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void updateTest() {
|
||||
final String test = "Ужасы";
|
||||
final GenreRs entity = service.get(3L);
|
||||
final String oldName = entity.getName();
|
||||
final GenreRs newEntity = service.update(3L, mapper.toRqDto(test));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(test, newEntity.getName());
|
||||
Assertions.assertNotEquals(oldName, newEntity.getName());
|
||||
|
||||
final GenreRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void deleteTest() {
|
||||
service.delete(3L);
|
||||
Assertions.assertEquals(2, service.getAll().size());
|
||||
|
||||
final GenreRs last = service.get(2L);
|
||||
Assertions.assertEquals(2L, last.getId());
|
||||
|
||||
final GenreRs newEntity = service.create(mapper.toRqDto("Приключения"));
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(4L, newEntity.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
void getAllTest() {
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
|
||||
final GenreRs first = service.get(1L);
|
||||
final GenreRs second = service.get(2L);
|
||||
final GenreRs third = service.get(4L);
|
||||
|
||||
Assertions.assertEquals("Роман", first.getName());
|
||||
Assertions.assertEquals("Фантастика", second.getName());
|
||||
Assertions.assertEquals("Приключения", third.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
void createDuplicateTest() {
|
||||
final GenreRs duplicate = service.create(mapper.toRqDto("Роман"));
|
||||
Assertions.assertEquals(4, service.getAll().size());
|
||||
Assertions.assertEquals("Роман", duplicate.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () ->
|
||||
service.update(999L, mapper.toRqDto("Несуществующий")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteNonExistentTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.delete(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithEmptyNameTest() {
|
||||
GenreRq dto = new GenreRq();
|
||||
dto.setName(""); // Пустое имя
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> service.create(dto));
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,68 @@ import axios from "axios";
|
||||
|
||||
const API_URL = "http://localhost:8080/api/1.0";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Error:', error.response?.data || error.message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default {
|
||||
// Жанры
|
||||
fetchGenres: () => axios.get(`${API_URL}/genre`),
|
||||
createGenre: (genreData) => axios.post(`${API_URL}/genre`, genreData),
|
||||
deleteGenre: (id) => axios.delete(`${API_URL}/genre/${id}`),
|
||||
fetchGenres: () => apiClient.get(`/genre/all`), // Добавить endpoint на бэкенде
|
||||
|
||||
// Или использовать пагинированную версию:
|
||||
fetchGenres: () => apiClient.get(`/genre`).then(response => {
|
||||
// Преобразуем ответ к массиву
|
||||
if (response.data && response.data.items) {
|
||||
return { data: response.data.items };
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
|
||||
fetchGenresPaginated: (page = 1, size = 10) =>
|
||||
apiClient.get(`/genre?page=${page}&size=${size}`),
|
||||
createGenre: (genreData) => apiClient.post(`/genre`, genreData),
|
||||
deleteGenre: (id) => apiClient.delete(`/genre/${id}`),
|
||||
|
||||
// Книги
|
||||
fetchBooks: () => axios.get(`${API_URL}/book`),
|
||||
fetchBooksByGenre: (genreId) => axios.get(`${API_URL}/book?genreId=${genreId}`),
|
||||
fetchBook: (id) => axios.get(`${API_URL}/book/${id}`),
|
||||
createBook: (bookData) => axios.post(`${API_URL}/book`, bookData),
|
||||
updateBook: (id, bookData) => axios.put(`${API_URL}/book/${id}`, bookData),
|
||||
deleteBook: (id) => axios.delete(`${API_URL}/book/${id}`),
|
||||
fetchBooks: () => apiClient.get(`/book/all`),
|
||||
fetchBooksPaginated: (page = 1, size = 10) =>
|
||||
apiClient.get(`/book?page=${page}&size=${size}`),
|
||||
fetchBook: (id) => apiClient.get(`/book/${id}`),
|
||||
createBook: (bookData) => apiClient.post(`/book`, bookData),
|
||||
updateBook: (id, bookData) => apiClient.put(`/book/${id}`, bookData),
|
||||
deleteBook: (id) => apiClient.delete(`/book/${id}`),
|
||||
|
||||
// Поиск и фильтрация книг
|
||||
searchBooks: (query, page = 1, size = 10) =>
|
||||
apiClient.get(`/book/search?query=${encodeURIComponent(query)}&page=${page}&size=${size}`),
|
||||
getBooksByGenre: (genreId, page = 1, size = 10) =>
|
||||
apiClient.get(`/book/genre/${genreId}?page=${page}&size=${size}`),
|
||||
getBooksByGenreAll: (genreId) =>
|
||||
apiClient.get(`/book/genre/${genreId}/all`),
|
||||
|
||||
// Жанры книг
|
||||
fetchBookGenres: (bookId) => apiClient.get(`/book/${bookId}/genres`),
|
||||
addBookGenre: (bookId, genreData) => apiClient.post(`/book/${bookId}/genres`, genreData),
|
||||
deleteBookGenre: (bookId, genreId) => apiClient.delete(`/book/${bookId}/genres/${genreId}`),
|
||||
|
||||
// Корзина
|
||||
fetchCartItems: () => axios.get(`${API_URL}/cart`),
|
||||
addToCart: (bookId) => axios.post(`${API_URL}/cart`, { bookId, quantity: 1 }),
|
||||
getCartItemByBookId: (bookId) => axios.get(`${API_URL}/cart/search?bookId=${bookId}`),
|
||||
updateCartItem: (id, data) => axios.put(`${API_URL}/cart/${id}`, data),
|
||||
updateCartItemQuantity: (id, quantity) => axios.patch(`${API_URL}/cart/${id}/${quantity}`),
|
||||
removeFromCart: (id) => axios.delete(`${API_URL}/cart/${id}`),
|
||||
clearCart: () =>
|
||||
axios
|
||||
.get(`${API_URL}/cart`)
|
||||
.then((response) => Promise.all(response.data.map((item) =>
|
||||
axios.delete(`${API_URL}/cart/${item.id}`)))),
|
||||
};
|
||||
fetchCartItems: () => apiClient.get(`/cart`),
|
||||
addToCart: (bookId) => apiClient.post(`/cart`, { bookId, quantity: 1 }),
|
||||
getCartItemByBookId: (bookId) => apiClient.get(`/cart/search?bookId=${bookId}`),
|
||||
updateCartItem: (id, data) => apiClient.put(`/cart/${id}`, data),
|
||||
updateCartItemQuantity: (id, quantity) => apiClient.patch(`/cart/${id}/${quantity}`),
|
||||
removeFromCart: (id) => apiClient.delete(`/cart/${id}`),
|
||||
clearCart: () => apiClient.delete(`/cart`),
|
||||
};
|
||||
@@ -5,12 +5,36 @@ import react from "@vitejs/plugin-react";
|
||||
import { resolve } from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, "index.html"),
|
||||
},
|
||||
plugins: [react()],
|
||||
base: "./", // КРИТИЧЕСКИ ВАЖНО для JAR!
|
||||
build: {
|
||||
outDir: "dist", // Собираем во фронтенд-проекте
|
||||
emptyOutDir: true,
|
||||
assetsDir: "assets", // Стандартное имя папки
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: (assetInfo) => {
|
||||
const ext = assetInfo.name.split('.').pop();
|
||||
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(ext)) {
|
||||
return `assets/images/[name]-[hash][extname]`;
|
||||
}
|
||||
if (/css/i.test(ext)) {
|
||||
return `assets/css/[name]-[hash][extname]`;
|
||||
}
|
||||
return `assets/[name]-[hash][extname]`;
|
||||
},
|
||||
chunkFileNames: "assets/js/[name]-[hash].js",
|
||||
entryFileNames: "assets/js/[name]-[hash].js",
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
BIN
build/resources/main/static/assets/background-oYp1cNqc.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
65
build/resources/main/static/assets/index-DbQzQPoN.js
Normal file
1
build/resources/main/static/assets/index-DbQzQPoN.js.map
Normal file
21
build/resources/main/static/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Книжный интернет-магазин "Тома"</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
|
||||
<script type="module" crossorigin src="/assets/index-DbQzQPoN.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BLg3Q5Rn.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- Подключение React и Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Подключение скриптов приложения -->
|
||||
|
||||
</body>
|
||||
7181
package-lock.json
generated
40
package.json
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "mywebsite",
|
||||
"version": "1.0.0",
|
||||
"main": "vite.config.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js --report-unused-disable-directives --max-warnings 0",
|
||||
"dev": "vite",
|
||||
"start": "npm-run-all -p dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"axios": "^1.9.0",
|
||||
"bootstrap": "^5.3.6",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"json-server": "^0.17.4",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.2",
|
||||
"eslint-plugin-html": "8.1.2",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"http-server": "14.1.1",
|
||||
"npm-run-all": "4.1.5",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "denis",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
||||
114
server/build.front.gradle
Normal file
@@ -0,0 +1,114 @@
|
||||
apply plugin: "com.github.node-gradle.node"
|
||||
|
||||
logger.quiet("Configure front builder")
|
||||
|
||||
ext {
|
||||
// Используем относительный путь
|
||||
frontDir = file("${project.projectDir}/../MyWebSite")
|
||||
if (!frontDir.exists()) {
|
||||
throw new GradleException("Frontend app directory does not exist: ${frontDir}")
|
||||
}
|
||||
logger.quiet("Webapp dir is {}", frontDir.toString())
|
||||
}
|
||||
|
||||
node {
|
||||
version = "22.17.1"
|
||||
npmVersion = "10.9.2"
|
||||
download = true
|
||||
nodeProjectDir = frontDir
|
||||
}
|
||||
|
||||
tasks.register("frontClean", Delete) {
|
||||
group = "MyWebSite"
|
||||
description = "Clean frontend build directories"
|
||||
delete "${frontDir}/dist"
|
||||
delete "${project.projectDir}/src/main/resources/static"
|
||||
}
|
||||
|
||||
tasks.register("frontDepsInstall", NpmTask) {
|
||||
group = "MyWebSite"
|
||||
description = "Installs dependencies from package.json"
|
||||
logger.quiet(description)
|
||||
workingDir = frontDir
|
||||
args = ["install"]
|
||||
}
|
||||
|
||||
tasks.register("frontBuild", NpmTask) {
|
||||
group = "MyWebSite"
|
||||
description = "Build frontend webapp"
|
||||
logger.quiet(description)
|
||||
workingDir = frontDir
|
||||
dependsOn frontDepsInstall
|
||||
args = ["run", "build"]
|
||||
|
||||
doLast {
|
||||
def staticDir = file("${project.projectDir}/src/main/resources/static")
|
||||
def frontDistDir = file("${frontDir}/dist")
|
||||
|
||||
logger.quiet("Frontend built in: ${frontDistDir}")
|
||||
logger.quiet("Copying to: ${staticDir}")
|
||||
|
||||
if (frontDistDir.exists()) {
|
||||
// Очищаем статическую директорию
|
||||
delete staticDir
|
||||
staticDir.mkdirs()
|
||||
|
||||
// Копируем все содержимое
|
||||
copy {
|
||||
from frontDistDir
|
||||
into staticDir
|
||||
include "**/*"
|
||||
}
|
||||
|
||||
// Логируем что скопировалось
|
||||
logger.quiet("Copied files:")
|
||||
staticDir.eachFileRecurse { file ->
|
||||
if (!file.isDirectory()) {
|
||||
logger.quiet(" - ${file.path.replace(staticDir.path, '')}")
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем наличие критических файлов
|
||||
def requiredFiles = [
|
||||
"index.html",
|
||||
"assets/",
|
||||
"assets/images/"
|
||||
]
|
||||
|
||||
requiredFiles.each { required ->
|
||||
def checkFile = file("${staticDir}/${required}")
|
||||
if (required.endsWith("/")) {
|
||||
if (checkFile.exists() && checkFile.isDirectory()) {
|
||||
logger.quiet("✓ Directory exists: ${required}")
|
||||
} else {
|
||||
logger.warn("✗ Directory missing: ${required}")
|
||||
}
|
||||
} else {
|
||||
if (checkFile.exists()) {
|
||||
logger.quiet("✓ File exists: ${required}")
|
||||
} else {
|
||||
logger.warn("✗ File missing: ${required}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Frontend build failed: ${frontDistDir} not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка перед сборкой
|
||||
frontBuild.dependsOn frontClean
|
||||
|
||||
// Связываем с процессом сборки
|
||||
if (frontDir.exists()) {
|
||||
processResources.dependsOn frontBuild
|
||||
bootJar.dependsOn frontBuild
|
||||
|
||||
// Проверяем, что статические файлы включены в JAR
|
||||
bootJar {
|
||||
from("${project.projectDir}/src/main/resources/static") {
|
||||
into "BOOT-INF/classes/static"
|
||||
}
|
||||
}
|
||||
}
|
||||
172
server/build.gradle
Normal file
@@ -0,0 +1,172 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot" version "3.5.6"
|
||||
id "io.spring.dependency-management" version "1.1.7"
|
||||
id "com.github.node-gradle.node" version "7.1.0" apply false
|
||||
id "org.liquibase.gradle" version "2.2.2" apply false
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
description = "Bookstore Application"
|
||||
def jdkVersion = "21"
|
||||
|
||||
defaultTasks "bootRun"
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
bootJar {
|
||||
archiveFileName = String.format("%s-%s.jar", rootProject.name, version)
|
||||
}
|
||||
|
||||
assert System.properties["java.specification.version"] == jdkVersion
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(jdkVersion)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
springdocVersion = "2.8.11"
|
||||
h2Version = "2.4.240"
|
||||
postgresVersion = "42.7.8"
|
||||
liquibaseVersion = "4.33.0"
|
||||
mockitoVersion = "5.19.0"
|
||||
|
||||
// Определяем активные профили на основе параметров Gradle
|
||||
springProfiles = []
|
||||
|
||||
// Добавляем фронтенд если указан параметр front
|
||||
if (project.hasProperty("front")) {
|
||||
springProfiles.add("front")
|
||||
}
|
||||
|
||||
// Определяем основной профиль (dev или prod)
|
||||
if (project.hasProperty("prod")) {
|
||||
springProfiles.add("prod")
|
||||
logger.quiet("Using PRODUCTION profile")
|
||||
} else {
|
||||
springProfiles.add("dev")
|
||||
logger.quiet("Using DEVELOPMENT profile")
|
||||
}
|
||||
|
||||
currentProfiles = springProfiles.join(",")
|
||||
logger.quiet("Active profiles: ${currentProfiles}")
|
||||
}
|
||||
|
||||
configurations {
|
||||
mockitoAgent
|
||||
}
|
||||
|
||||
apply from: "build.front.gradle"
|
||||
|
||||
// Подключаем миграции только для dev профиля
|
||||
if (springProfiles.contains("dev")) {
|
||||
apply from: "build.migrations.gradle"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Spring Boot Starters
|
||||
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||
implementation "org.springframework.boot:spring-boot-starter-validation"
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||
implementation "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
// OpenAPI/Swagger
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||
|
||||
// Миграции
|
||||
implementation "org.liquibase:liquibase-core:${liquibaseVersion}"
|
||||
|
||||
// Базы данных
|
||||
runtimeOnly "org.postgresql:postgresql:${postgresVersion}"
|
||||
|
||||
// Для dev профиля добавляем H2
|
||||
if (springProfiles.contains("dev")) {
|
||||
runtimeOnly "com.h2database:h2:${h2Version}"
|
||||
}
|
||||
|
||||
// Тестирование
|
||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||
testImplementation "com.h2database:h2:${h2Version}" // Для тестов всегда используем H2
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
|
||||
|
||||
mockitoAgent("org.mockito:mockito-core:${mockitoVersion}") {
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
bootRun {
|
||||
// Всегда используем профили, определенные через параметры Gradle
|
||||
def launchArgs = ["--spring.profiles.active=${currentProfiles}"]
|
||||
|
||||
// Добавляем пользовательские аргументы если есть
|
||||
if (project.hasProperty("args")) {
|
||||
launchArgs.addAll(project.args.tokenize())
|
||||
}
|
||||
|
||||
args = launchArgs
|
||||
|
||||
logger.lifecycle("Starting with arguments: ${launchArgs}")
|
||||
|
||||
// Настройка JVM
|
||||
jvmArgs = [
|
||||
"-Xmx512m",
|
||||
"-Xms256m",
|
||||
"-Dfile.encoding=UTF-8"
|
||||
]
|
||||
|
||||
// Передаем системные свойства
|
||||
systemProperties = System.properties
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs += "-Xshare:off"
|
||||
jvmArgs += "-javaagent:${configurations.mockitoAgent.asPath}"
|
||||
systemProperty "spring.profiles.active", "test"
|
||||
}
|
||||
|
||||
// Убираем фильтрацию application.yml - Spring Boot будет читать профили из файла
|
||||
// processResources остается без изменений
|
||||
|
||||
// Автоматическое создание миграций при сборке для тестов
|
||||
if (project.hasProperty('autoGenerateMigrations')) {
|
||||
tasks.named('test') {
|
||||
dependsOn generateDiff
|
||||
}
|
||||
}
|
||||
|
||||
// Дополнительные задачи для удобства
|
||||
tasks.register("runDev") {
|
||||
group = "application"
|
||||
description = "Run application with dev profile (default)"
|
||||
dependsOn bootRun
|
||||
}
|
||||
|
||||
tasks.register("runprod") {
|
||||
group = "application"
|
||||
description = "Run application with prod profile"
|
||||
doFirst {
|
||||
project.ext.set("prod", true)
|
||||
project.ext.set("front", true)
|
||||
}
|
||||
dependsOn bootRun
|
||||
}
|
||||
|
||||
tasks.register("runApiOnly") {
|
||||
group = "application"
|
||||
description = "Run application without frontend"
|
||||
doFirst {
|
||||
// Убираем front профиль
|
||||
project.extensions.extraProperties.remove("front")
|
||||
}
|
||||
dependsOn bootRun
|
||||
}
|
||||
64
server/build.migrations.gradle
Normal file
@@ -0,0 +1,64 @@
|
||||
apply plugin: "org.liquibase.gradle"
|
||||
|
||||
logger.quiet("Configure migrations builder")
|
||||
|
||||
ext {
|
||||
picocliVersion = "4.7.7"
|
||||
|
||||
timestamp = new Date().format("yyyy-MM-dd-HHmmss")
|
||||
}
|
||||
|
||||
|
||||
liquibase {
|
||||
activities {
|
||||
main {
|
||||
changelogFile "db/master.yml"
|
||||
url "jdbc:h2:file:./data/bookstore"
|
||||
username "sa"
|
||||
password "sa"
|
||||
driver "org.h2.Driver"
|
||||
logLevel "info"
|
||||
classpath "src/main/resources"
|
||||
referenceUrl "hibernate:spring:com.example.server.entity?dialect=org.hibernate.dialect.H2Dialect"
|
||||
}
|
||||
}
|
||||
}
|
||||
update.dependsOn processResources
|
||||
|
||||
dependencies {
|
||||
liquibaseRuntime "org.liquibase.ext:liquibase-hibernate6:${liquibaseVersion}"
|
||||
liquibaseRuntime "info.picocli:picocli:${picocliVersion}"
|
||||
liquibaseRuntime sourceSets.main.runtimeClasspath
|
||||
liquibaseRuntime sourceSets.main.output
|
||||
}
|
||||
|
||||
tasks.register("generateFull") {
|
||||
group = "migrations"
|
||||
description = "Generate changelog from existing database"
|
||||
doFirst(){
|
||||
liquibase {
|
||||
activities {
|
||||
main {
|
||||
changeLogFile "src/main/resources/db/generated-full-${timestamp}.yml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finalizedBy generateChangelog
|
||||
}
|
||||
|
||||
tasks.register("generateDiff") {
|
||||
group = "liquibase"
|
||||
description = "Generate diff between DB and JPA entities"
|
||||
doFirst(){
|
||||
liquibase {
|
||||
activities {
|
||||
main {
|
||||
changeLogFile "src/main/resources/db/generated-diff-${timestamp}.yml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finalizedBy diffChangelog
|
||||
}
|
||||
diffChangelog.dependsOn compileJava
|
||||
BIN
server/data.mv.db
Normal file
17746
server/data.trace.db
Normal file
6
server/data/bookstore.lock.db
Normal file
@@ -0,0 +1,6 @@
|
||||
#FileLock
|
||||
#Tue Dec 16 14:10:21 GMT+04:00 2025
|
||||
hostName=LAPTOP-IUA50AR5
|
||||
id=19b26a389dbd2bd3398104c2366bdfd02b77488ebde
|
||||
method=file
|
||||
server=26.200.18.239\:60656
|
||||
BIN
server/data/bookstore.mv.db
Normal file
1015
server/data/bookstore.trace.db
Normal file
118
server/src/main/java/com/example/server/ServerApplication.java
Normal file
@@ -0,0 +1,118 @@
|
||||
package com.example.server;
|
||||
|
||||
import com.example.server.api.book.BookGenreRq;
|
||||
import com.example.server.api.book.BookRq;
|
||||
import com.example.server.api.genre.GenreRq;
|
||||
import com.example.server.service.BookService;
|
||||
import com.example.server.service.CartItemService;
|
||||
import com.example.server.service.GenreService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ServerApplication implements CommandLineRunner {
|
||||
private final Logger log = LoggerFactory.getLogger(ServerApplication.class);
|
||||
private final Environment environment;
|
||||
private final BookService bookService;
|
||||
private final GenreService genreService;
|
||||
private final CartItemService cartItemService;
|
||||
|
||||
public ServerApplication(Environment environment, BookService bookService,
|
||||
GenreService genreService, CartItemService cartItemService) {
|
||||
this.environment = environment;
|
||||
this.bookService = bookService;
|
||||
this.genreService = genreService;
|
||||
this.cartItemService = cartItemService;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ServerApplication.class, args);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void logProfiles() {
|
||||
// Логируем активные профили при старте приложения
|
||||
String[] activeProfiles = environment.getActiveProfiles();
|
||||
if (activeProfiles.length == 0) {
|
||||
log.info("No active profiles set, using default");
|
||||
} else {
|
||||
log.info("Active profiles: {}", Arrays.toString(activeProfiles));
|
||||
}
|
||||
|
||||
// Логируем используемую базу данных
|
||||
String datasourceUrl = environment.getProperty("spring.datasource.url");
|
||||
if (datasourceUrl != null) {
|
||||
String maskedUrl = datasourceUrl.replaceAll("password=.*?(&|$)", "password=****");
|
||||
log.info("Database URL: {}", maskedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
log.info("Application started with arguments: {}", Arrays.toString(args));
|
||||
|
||||
// Проверяем, нужно ли заполнять данные
|
||||
boolean shouldPopulate = args.length > 0 && Objects.equals("--populate", args[0]);
|
||||
boolean isDevProfile = Arrays.asList(environment.getActiveProfiles()).contains("dev");
|
||||
|
||||
if (shouldPopulate) {
|
||||
if (isDevProfile) {
|
||||
populateData();
|
||||
} else {
|
||||
log.warn("Data population is only allowed in dev profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void populateData() {
|
||||
log.info("Starting data population...");
|
||||
|
||||
try {
|
||||
// Проверяем, не заполнены ли данные уже
|
||||
if (!genreService.getAll().isEmpty()) {
|
||||
log.info("Data already exists, skipping population");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Creating genres...");
|
||||
var genre1 = genreService.create(new GenreRq("Роман"));
|
||||
var genre2 = genreService.create(new GenreRq("Фантастика"));
|
||||
var genre3 = genreService.create(new GenreRq("Ужасы"));
|
||||
var genre4 = genreService.create(new GenreRq("Детектив"));
|
||||
|
||||
log.info("Creating books...");
|
||||
var book1 = bookService.create(new BookRq(
|
||||
"Основание", "Айзек Азимов", 700,
|
||||
"Сага о падении и возрождении галактической империи.", "images/foundation.jpg"
|
||||
));
|
||||
|
||||
var book2 = bookService.create(new BookRq(
|
||||
"Дюна", "Фрэнк Герберт", 900,
|
||||
"Эпическая история о борьбе за контроль над планетой Арракис.", "images/dune.jpg"
|
||||
));
|
||||
|
||||
var book3 = bookService.create(new BookRq(
|
||||
"Убийство в Восточном экспрессе", "Агата Кристи", 750,
|
||||
"Загадочное убийство на поезде.", "images/murder_on_the_orient_express.jpg"
|
||||
));
|
||||
|
||||
log.info("Creating genre-book relationships...");
|
||||
bookService.addGenre(book1.id(), new BookGenreRq(genre2.id(), "2024-01-15"));
|
||||
bookService.addGenre(book2.id(), new BookGenreRq(genre2.id(), "2024-01-16"));
|
||||
bookService.addGenre(book3.id(), new BookGenreRq(genre4.id(), "2024-01-17"));
|
||||
|
||||
log.info("Data population completed successfully!");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during data population: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
server/src/main/java/com/example/server/api/PageHelper.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.example.server.api;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class PageHelper {
|
||||
private PageHelper() {
|
||||
}
|
||||
|
||||
public static Pageable toPageable(int page, int size) {
|
||||
return PageRequest.of(page - 1, size, Sort.by("id"));
|
||||
}
|
||||
}
|
||||
39
server/src/main/java/com/example/server/api/PageRs.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.example.server.api;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
public record PageRs<D>(
|
||||
List<D> items,
|
||||
int itemsCount,
|
||||
int currentPage,
|
||||
int currentSize,
|
||||
int totalPages,
|
||||
long totalItems,
|
||||
boolean isFirst,
|
||||
boolean isLast,
|
||||
boolean hasNext,
|
||||
boolean hasPrevious) {
|
||||
|
||||
public List<D> items() {
|
||||
return Optional.ofNullable(items).orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
public static <D, E> PageRs<D> from(Page<E> page, Function<E, D> mapper) {
|
||||
return new PageRs<>(
|
||||
page.getContent().stream().map(mapper::apply).toList(),
|
||||
page.getNumberOfElements(),
|
||||
page.getNumber() + 1,
|
||||
page.getSize(),
|
||||
page.getTotalPages(),
|
||||
page.getTotalElements(),
|
||||
page.isFirst(),
|
||||
page.isLast(),
|
||||
page.hasNext(),
|
||||
page.hasPrevious());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.server.api;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
@Controller
|
||||
public class SpaController {
|
||||
@GetMapping(value = "/{path:^(?!api|assets|images|swagger-ui|.*\\.[a-zA-Z0-9]{2,10}).*}/**")
|
||||
public String forwardToIndex(@PathVariable(required = false) String path) {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import com.example.server.api.PageHelper;
|
||||
import com.example.server.api.PageRs;
|
||||
import com.example.server.service.BookService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.example.server.configuration.Constants;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.API_URL + BookController.URL)
|
||||
public class BookController {
|
||||
public static final String URL = "/book";
|
||||
private final BookService bookService;
|
||||
|
||||
public BookController(BookService bookService) {
|
||||
this.bookService = bookService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public PageRs<BookRs> getAll(
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int size) {
|
||||
return bookService.getAll(PageHelper.toPageable(page, size));
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
public List<BookRs> getAllWithoutPagination() {
|
||||
return bookService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public BookRs get(@PathVariable Long id) {
|
||||
return bookService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public BookRs create(@RequestBody @Valid BookRq dto) {
|
||||
return bookService.create(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public BookRs update(@PathVariable Long id, @RequestBody @Valid BookRq dto) {
|
||||
return bookService.update(id, dto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public BookRs delete(@PathVariable Long id) {
|
||||
return bookService.delete(id);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/genres")
|
||||
public List<BookGenreRs> getGenres(@PathVariable Long id) {
|
||||
return bookService.getBookGenres(id);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/genres")
|
||||
public BookGenreRs addGenre(@PathVariable Long id, @RequestBody @Valid BookGenreRq dto) {
|
||||
return bookService.addGenre(id, dto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/genres/{genreId}")
|
||||
public BookGenreRs updateGenre(@PathVariable Long id, @PathVariable Long genreId,
|
||||
@RequestBody @Valid BookGenreUpdateRq dto) {
|
||||
return bookService.updateGenre(id, genreId, dto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/genres/{genreId}")
|
||||
public BookGenreRs deleteGenre(@PathVariable Long id, @PathVariable Long genreId) {
|
||||
return bookService.deleteGenre(id, genreId);
|
||||
}
|
||||
|
||||
// ===== НОВЫЕ ЭНДПОИНТЫ ДЛЯ ФИЛЬТРАЦИИ И ПОИСКА =====
|
||||
|
||||
@GetMapping("/genre/{genreId}")
|
||||
public PageRs<BookRs> getBooksByGenre(
|
||||
@PathVariable Long genreId,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int size) {
|
||||
return bookService.getBooksByGenre(genreId, PageHelper.toPageable(page, size));
|
||||
}
|
||||
|
||||
@GetMapping("/genre/{genreId}/all")
|
||||
public List<BookRs> getBooksByGenreWithoutPagination(@PathVariable Long genreId) {
|
||||
return bookService.getBooksByGenreWithoutPagination(genreId);
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public PageRs<BookRs> searchBooks(
|
||||
@RequestParam(required = false) String query,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int size) {
|
||||
return bookService.searchBooks(query, PageHelper.toPageable(page, size));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
public record BookGenreRq(
|
||||
@NotNull Long genreId,
|
||||
@NotNull @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "Date must be in YYYY-MM-DD format") String date) {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import com.example.server.api.genre.GenreRs;
|
||||
import com.example.server.entity.GenreBookEntity;
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public record BookGenreRs(
|
||||
GenreRs genre,
|
||||
String date) {
|
||||
|
||||
public static BookGenreRs from(GenreBookEntity entity) {
|
||||
return new BookGenreRs(
|
||||
GenreRs.from(entity.getGenre()),
|
||||
entity.getDate().toString());
|
||||
}
|
||||
|
||||
public static List<BookGenreRs> fromList(Iterable<GenreBookEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(BookGenreRs::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
public record BookGenreUpdateRq(
|
||||
@NotNull @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "Date must be in YYYY-MM-DD format") String date) {
|
||||
}
|
||||
15
server/src/main/java/com/example/server/api/book/BookRq.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record BookRq(
|
||||
@NotBlank @Size(min = 2, max = 50) String title,
|
||||
@NotBlank @Size(min = 2, max = 50) String author,
|
||||
@NotNull @Min(1) int price,
|
||||
String description,
|
||||
String image) {
|
||||
}
|
||||
48
server/src/main/java/com/example/server/api/book/BookRs.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.example.server.api.book;
|
||||
|
||||
import com.example.server.entity.BookEntity;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public record BookRs(
|
||||
@NotNull Long id,
|
||||
@NotBlank(message = "Название обязательно")
|
||||
@Size(min = 2, max = 50, message = "Название должно быть от 2 до 50 символов")
|
||||
String title,
|
||||
|
||||
@NotBlank(message = "автор обязателен")
|
||||
@Size(min = 2, max = 50, message = "ФИО должно быть от 2 до 50 символов")
|
||||
String author,
|
||||
|
||||
@NotNull
|
||||
@Min(value = 1, message = "Цена не может быть меньше 1 рубля")
|
||||
Integer price,
|
||||
|
||||
String description,
|
||||
String image,
|
||||
List<BookGenreRs> genres
|
||||
) {
|
||||
|
||||
public static BookRs from(BookEntity entity) {
|
||||
return new BookRs(
|
||||
entity.getId(),
|
||||
entity.getTitle(),
|
||||
entity.getAuthor(),
|
||||
entity.getPrice(),
|
||||
entity.getDescription(),
|
||||
entity.getImage(),
|
||||
BookGenreRs.fromList(entity.getGenreBooks()) // Добавляем жанры
|
||||
);
|
||||
}
|
||||
|
||||
public static List<BookRs> fromList(Iterable<BookEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(BookRs::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.server.api.cartItem;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record CartItemRq(
|
||||
@NotNull Long bookId,
|
||||
@NotNull @Min(1) Integer quantity) {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.server.api.cartItem;
|
||||
|
||||
import com.example.server.api.book.BookRs;
|
||||
import com.example.server.entity.CartItemEntity;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public record CartItemRs(
|
||||
Long id,
|
||||
BookRs book,
|
||||
Integer quantity) {
|
||||
|
||||
public static CartItemRs from(CartItemEntity entity) {
|
||||
return new CartItemRs(
|
||||
entity.getId(),
|
||||
BookRs.from(entity.getBook()),
|
||||
entity.getQuantity());
|
||||
}
|
||||
|
||||
public static List<CartItemRs> fromList(Iterable<CartItemEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(CartItemRs::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.example.server.api.PageHelper;
|
||||
import com.example.server.api.PageRs;
|
||||
import com.example.server.error.NotFoundException;
|
||||
import com.example.server.service.GenreService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.example.server.configuration.Constants;
|
||||
@@ -23,7 +26,14 @@ public class GenreController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<GenreRs> getAll() {
|
||||
public PageRs<GenreRs> getAll(
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int size) {
|
||||
return genreService.getAll(PageHelper.toPageable(page, size));
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
public List<GenreRs> getAllWithoutPagination() {
|
||||
return genreService.getAll();
|
||||
}
|
||||
|
||||
@@ -45,4 +55,14 @@ public class GenreController {
|
||||
public GenreRs delete(@PathVariable Long id) {
|
||||
return genreService.delete(id);
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
public List<GenreStatsRs> getAllStats() {
|
||||
return genreService.getAllGenresStats();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/stats")
|
||||
public GenreStatsRs getStats(@PathVariable Long id) {
|
||||
return genreService.getGenreStats(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.server.api.genre;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record GenreRq(
|
||||
@NotBlank @Size(min = 2, max = 50) String name) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.server.api.genre;
|
||||
|
||||
import com.example.server.entity.GenreEntity;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public record GenreRs(
|
||||
Long id,
|
||||
String name) {
|
||||
|
||||
public static GenreRs from(GenreEntity entity) {
|
||||
return new GenreRs(entity.getId(), entity.getName());
|
||||
}
|
||||
|
||||
public static List<GenreRs> fromList(Iterable<GenreEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(GenreRs::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.example.server.api.genre;
|
||||
|
||||
import com.example.server.entity.projection.GenreStatsProjection;
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public record GenreStatsRs(
|
||||
GenreRs genre,
|
||||
Long totalBooks,
|
||||
Long totalCartItems,
|
||||
Double averagePrice) {
|
||||
|
||||
public static GenreStatsRs from(GenreStatsProjection projection) {
|
||||
if (projection == null) {
|
||||
return new GenreStatsRs(null, 0L, 0L, 0.0);
|
||||
}
|
||||
return new GenreStatsRs(
|
||||
GenreRs.from(projection.getGenre()),
|
||||
projection.getTotalBooks(),
|
||||
projection.getTotalCartItems(),
|
||||
projection.getAveragePrice());
|
||||
}
|
||||
|
||||
public static List<GenreStatsRs> fromList(Iterable<GenreStatsProjection> projections) {
|
||||
return StreamSupport.stream(projections.spliterator(), false)
|
||||
.map(GenreStatsRs::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.SequenceGenerator;
|
||||
|
||||
@MappedSuperclass
|
||||
public abstract class BaseEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
@SequenceGenerator(name = "hibernate_sequence")
|
||||
protected Long id;
|
||||
|
||||
protected BaseEntity() {}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,47 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "books")
|
||||
public class BookEntity extends BaseEntity{
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
@Column(nullable = false)
|
||||
private String author;
|
||||
private int price;
|
||||
private String description;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "genre_id") // добавлена аннотация для связи
|
||||
private GenreEntity genre;
|
||||
|
||||
private String image;
|
||||
|
||||
@OneToMany(mappedBy = "book")
|
||||
@OrderBy("id ASC")
|
||||
private Set<GenreBookEntity> genreBooks = new HashSet<>(); // добавлена обратная связь
|
||||
|
||||
public BookEntity() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BookEntity(String title, String author, int price, String description, GenreEntity gente, String image) {
|
||||
public BookEntity(String title, String author, int price, String description, GenreEntity genre, String image) {
|
||||
this();
|
||||
this.title = title;
|
||||
this.author = author;
|
||||
this.price = price;
|
||||
this.description = description;
|
||||
this.genre = gente;
|
||||
this.genre = genre;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
@@ -69,4 +92,15 @@ public class BookEntity extends BaseEntity{
|
||||
public void setGenre(GenreEntity genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public Set<GenreBookEntity> getGenreBooks() {
|
||||
return genreBooks;
|
||||
}
|
||||
|
||||
public void addGenreBook(GenreBookEntity genreBook) {
|
||||
if (genreBook.getBook() != this) {
|
||||
genreBook.setBook(this);
|
||||
}
|
||||
genreBooks.add(genreBook);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "cartitems")
|
||||
public class CartItemEntity extends BaseEntity{
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "book_id", nullable = false) // добавлена аннотация для связи
|
||||
private BookEntity book;
|
||||
|
||||
private int quantity;
|
||||
|
||||
public CartItemEntity() {
|
||||
@@ -29,4 +40,4 @@ public class CartItemEntity extends BaseEntity{
|
||||
public void setQuantity(int quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.persistence.EmbeddedId;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import jakarta.persistence.Table;
|
||||
@Entity
|
||||
@Table(name = "genre_book")
|
||||
public class GenreBookEntity {
|
||||
@EmbeddedId
|
||||
private GenreBookId id = new GenreBookId();
|
||||
@ManyToOne
|
||||
@MapsId("genreId")
|
||||
@JoinColumn(name = "genre_id", nullable = false)
|
||||
private GenreEntity genre;
|
||||
@ManyToOne
|
||||
@MapsId("bookId")
|
||||
@JoinColumn(name = "book_id", nullable = false)
|
||||
private BookEntity book;
|
||||
private LocalDate date;
|
||||
|
||||
public GenreBookEntity() {
|
||||
}
|
||||
|
||||
public GenreBookEntity(GenreEntity genre, BookEntity book, LocalDate date) {
|
||||
this.genre = genre;
|
||||
this.book = book;
|
||||
this.date = date;
|
||||
this.id = new GenreBookId(genre.getId(), book.getId());
|
||||
}
|
||||
|
||||
public GenreBookId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(GenreBookId id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public GenreEntity getGenre() {
|
||||
return genre;
|
||||
}
|
||||
|
||||
public void setGenre(GenreEntity genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public BookEntity getBook() {
|
||||
return book;
|
||||
}
|
||||
|
||||
public void setBook(BookEntity book) {
|
||||
this.book = book;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
GenreBookEntity other = (GenreBookEntity) obj;
|
||||
return Objects.equals(id, other.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.persistence.Embeddable;
|
||||
@Embeddable
|
||||
public class GenreBookId {
|
||||
private Long genreId;
|
||||
private Long bookId;
|
||||
|
||||
public GenreBookId() {
|
||||
}
|
||||
|
||||
public GenreBookId(Long genreId, Long bookId) {
|
||||
this.genreId = genreId;
|
||||
this.bookId = bookId;
|
||||
}
|
||||
|
||||
public Long getGenreId() {
|
||||
return genreId;
|
||||
}
|
||||
|
||||
public void setGenreId(Long genreId) {
|
||||
this.genreId = genreId;
|
||||
}
|
||||
|
||||
public Long getBookId() {
|
||||
return bookId;
|
||||
}
|
||||
|
||||
public void setBookId(Long bookId) {
|
||||
this.bookId = bookId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(genreId, bookId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
GenreBookId other = (GenreBookId) obj;
|
||||
return Objects.equals(genreId, other.genreId) && Objects.equals(bookId, other.bookId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.example.server.entity;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
@Entity
|
||||
@Table(name = "genres")
|
||||
public class GenreEntity extends BaseEntity{
|
||||
@Column(length = 100, nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "genre")
|
||||
@OrderBy("id ASC")
|
||||
private Set<GenreBookEntity> genreBooks = new HashSet<>();
|
||||
|
||||
public GenreEntity() {
|
||||
super();
|
||||
}
|
||||
public GenreEntity(String name) {
|
||||
this();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Set<GenreBookEntity> getGenreBooks() {
|
||||
return genreBooks;
|
||||
}
|
||||
|
||||
public void addBook(GenreBookEntity genreBook) {
|
||||
if (genreBook.getGenre() != this) {
|
||||
genreBook.setGenre(this);
|
||||
}
|
||||
genreBooks.add(genreBook);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.server.entity.projection;
|
||||
|
||||
import com.example.server.entity.GenreEntity;
|
||||
|
||||
public interface GenreStatsProjection {
|
||||
GenreEntity getGenre();
|
||||
Long getTotalBooks();
|
||||
Long getTotalCartItems();
|
||||
Double getAveragePrice();
|
||||
}
|
||||