Compare commits

...

2 Commits

Author SHA1 Message Date
RM
5e77a63aff add dummy front 2026-10-15 00:29:42 +03:00
RM
97d6d9adc6 2 stage build 2026-10-15 00:29:21 +03:00
15 changed files with 352 additions and 4 deletions

View File

@@ -1,3 +1,15 @@
FROM eclipse-temurin:26-jre-alpine
COPY torgi-0.0.1-SNAPSHOT.jar /home/torgi-0.0.1-SNAPSHOT.jar
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-XX:InitialRAMPercentage=50", "-Xms512m", "-Xmx4096m", "-jar", "/home/torgi-0.0.1-SNAPSHOT.jar"]
# Этап 1: сборка jar внутри Docker (maven package отдельно запускать не нужно)
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN chmod +x mvnw && ./mvnw -B dependency:go-offline
COPY src ./src
RUN ./mvnw -B package -DskipTests
# Этап 2: запуск (JRE 17 — та же версия, что java.version в pom.xml)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /home
COPY --from=build /app/target/torgi-0.0.1-SNAPSHOT.jar /home/torgi-0.0.1-SNAPSHOT.jar
EXPOSE 8181
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-XX:InitialRAMPercentage=50", "-Xms512m", "-Xmx4096m", "-jar", "/home/torgi-0.0.1-SNAPSHOT.jar"]

View File

@@ -1,7 +1,7 @@
version: '3.8'
services:
client-backend:
image: client:0.0.1
image: gitea.rmevs.vip/rm/torgi2-backend:latest
build:
context: .
dockerfile: Dockerfile
@@ -44,6 +44,19 @@ services:
- "5050:80"
volumes:
- pgadmin-data:/var/lib/pgadmin
profiles:
- tools
frontend:
image: gitea.rmevs.vip/rm/torgi2-frontend:latest
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
depends_on:
- client-backend
restart: always
volumes:
db-data:

2
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Торги — события</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

22
frontend/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "torgi-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.8"
}
}

10
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,10 @@
import EventsPage from './pages/EventsPage';
export default function App() {
return (
<main className="container">
<h1>Торги — события</h1>
<EventsPage />
</main>
);
}

24
frontend/src/api.ts Normal file
View File

@@ -0,0 +1,24 @@
import type { Page, TorgiEvent } from './types';
// В dev-режиме запросы идут через vite-proxy (см. vite.config.ts),
// поэтому BASE по умолчанию пустой. Для продакшна задать VITE_API_URL,
// например https://torgi.example.com
const BASE = import.meta.env.VITE_API_URL ?? '';
export interface EventFilter {
cadastral?: string;
status?: string;
page: number;
size: number;
}
export async function fetchEvents(filter: EventFilter): Promise<Page<TorgiEvent>> {
const q = new URLSearchParams();
if (filter.cadastral) q.set('cadastral', filter.cadastral);
if (filter.status) q.set('status', filter.status);
q.set('page', String(filter.page));
q.set('size', String(filter.size));
const res = await fetch(`${BASE}/api/events?${q.toString()}`);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return (await res.json()) as Page<TorgiEvent>;
}

54
frontend/src/index.css Normal file
View File

@@ -0,0 +1,54 @@
body {
font-family: system-ui, sans-serif;
margin: 0;
background: #f6f6f6;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
.filters {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
input, button {
padding: 6px 10px;
font-size: 14px;
}
table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
th, td {
border: 1px solid #ddd;
padding: 6px 8px;
text-align: left;
font-size: 14px;
}
th {
background: #eee;
}
.pagination {
display: flex;
gap: 12px;
align-items: center;
margin-top: 12px;
}
.total {
color: #555;
}
.error {
color: #b00020;
}

13
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const root = document.getElementById('root');
if (!root) throw new Error('Root element not found');
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1,116 @@
import { useCallback, useEffect, useState } from 'react';
import { fetchEvents } from '../api';
import type { Page, TorgiEvent } from '../types';
const PAGE_SIZE = 20;
function cell(value: string | number | null | undefined): string {
if (value === null || value === undefined || value === '') return '—';
return String(value);
}
export default function EventsPage() {
const [cadastral, setCadastral] = useState('');
const [status, setStatus] = useState('');
const [applied, setApplied] = useState({ cadastral: '', status: '' });
const [page, setPage] = useState(0);
const [data, setData] = useState<Page<TorgiEvent> | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await fetchEvents({ ...applied, page, size: PAGE_SIZE });
setData(result);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setLoading(false);
}
}, [applied, page]);
useEffect(() => {
void load();
}, [load]);
const onSearch = () => {
setPage(0);
setApplied({ cadastral: cadastral.trim(), status: status.trim() });
};
return (
<section>
<div className="filters">
<input
placeholder="Кадастровый номер"
value={cadastral}
onChange={(e) => setCadastral(e.target.value)}
/>
<input
placeholder="Статус"
value={status}
onChange={(e) => setStatus(e.target.value)}
/>
<button onClick={onSearch} disabled={loading}>
Найти
</button>
</div>
{loading && <p>Загрузка…</p>}
{error && <p className="error">Ошибка: {error}</p>}
{data && (
<>
<p className="total">Всего: {data.totalElements}</p>
<table>
<thead>
<tr>
<th>Извещение</th>
<th>Лот</th>
<th>Кадастр</th>
<th>Статус</th>
<th>Нач. цена</th>
<th>Итог. цена</th>
<th>Дата торгов</th>
</tr>
</thead>
<tbody>
{data.content.map((ev) => (
<tr key={ev.id}>
<td>{cell(ev.notice)}</td>
<td>{cell(ev.lotNumber)}</td>
<td>{cell(ev.cadastral)}</td>
<td>{cell(ev.status)}</td>
<td>{cell(ev.startPrice)}</td>
<td>{cell(ev.finalPrice)}</td>
<td>{cell(ev.tradesDate)}</td>
</tr>
))}
{data.content.length === 0 && (
<tr>
<td colSpan={7}>Ничего не найдено</td>
</tr>
)}
</tbody>
</table>
<div className="pagination">
<button disabled={page === 0 || loading} onClick={() => setPage((p) => p - 1)}>
← Назад
</button>
<span>
Стр. {data.number + 1} из {Math.max(data.totalPages, 1)}
</span>
<button
disabled={data.number + 1 >= data.totalPages || loading}
onClick={() => setPage((p) => p + 1)}
>
Вперед →
</button>
</div>
</>
)}
</section>
);
}

26
frontend/src/types.ts Normal file
View File

@@ -0,0 +1,26 @@
export interface TorgiEvent {
id: number;
key: string | null;
status: string | null;
bidStart: string | null;
bidEnd: string | null;
tradesDate: string | null;
startPrice: string | null;
step: string | null;
initMoney: string | null;
finalPrice: string | null;
notice: string | null;
lotNumber: number | null;
cadastral: string | null;
type: string | null;
fullInfo: string | null;
createdAt: string | null;
}
export interface Page<T> {
content: T[];
totalElements: number;
totalPages: number;
number: number;
size: number;
}

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

20
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

18
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// Dev-proxy на бэкенд, чтобы не зависеть от CORS при локальной разработке.
// VITE_API_URL имеет приоритет, если задан (например, для продакшн-сборки).
export default defineConfig({
plugins: [react()],
server: {
host: '127.0.0.1',
port: 5173,
proxy: {
'/api': {
target: process.env.VITE_API_URL ?? 'http://localhost:8080',
changeOrigin: true,
},
},
},
});

View File

@@ -83,6 +83,11 @@
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.17</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>