diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..d3dc7a63
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,32 @@
+services:
+ postgres:
+ image: postgres:16
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: zenbook14
+ ports:
+ - "5555:5556"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+ redis:
+ image: redis:7
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+volumes:
+ postgres_data:
+ redis_data:
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 03bc446d..8e3831cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,43 +1,170 @@
-
4.0.0
+
+ com.emobile
+ SpringToDo
+ 0.0.1-SNAPSHOT
+ SpringToDo
+ Spring Boot ToDo Application
+
org.springframework.boot
spring-boot-starter-parent
3.3.5
-
+
- com.emobile
- SpringToDo
- 0.0.1-SNAPSHOT
- SpringToDo
- SpringToDo
21
+ 1.5.5.Final
+ 2.5.0
+ 1.20.2
+
+
+ UTF-8
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+
+ org.flywaydb
+ flyway-core
+
+
+
+ org.flywaydb
+ flyway-database-postgresql
+
+
+
org.springframework.boot
- spring-boot-starter
+ spring-boot-starter-data-redis
+
+
+
+ org.apache.commons
+ commons-pool2
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc.version}
+
+
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.testcontainers
+ postgresql
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+
+
+ src/main/resources
+ false
+
+
+
org.springframework.boot
spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${java.version}
+ ${java.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+ test,hibernate
+
+
+
-
diff --git a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
index 41980514..d1f04a64 100644
--- a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
+++ b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
@@ -2,7 +2,9 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+@EnableCaching
@SpringBootApplication
public class SpringToDoApplication {
diff --git a/src/main/java/com/emobile/springtodo/config/HibernateConfig.java b/src/main/java/com/emobile/springtodo/config/HibernateConfig.java
new file mode 100644
index 00000000..4ae8367d
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/config/HibernateConfig.java
@@ -0,0 +1,10 @@
+package com.emobile.springtodo.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+
+@Configuration
+@Profile("hibernate")
+public class HibernateConfig {
+ // пусто — убрали SessionFactory- бин, чтобы не было циклов
+}
diff --git a/src/main/java/com/emobile/springtodo/config/RedisConfig.java b/src/main/java/com/emobile/springtodo/config/RedisConfig.java
new file mode 100644
index 00000000..3441b371
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/config/RedisConfig.java
@@ -0,0 +1,22 @@
+package com.emobile.springtodo.config;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+@Configuration
+@Profile("redis")
+public class RedisConfig {
+
+ @Bean
+ public RedisCacheConfiguration cacheConfiguration() {
+ return RedisCacheConfiguration.defaultCacheConfig()
+ .serializeValuesWith(RedisSerializationContext.SerializationPair
+ .fromSerializer(new GenericJackson2JsonRedisSerializer()));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/emobile/springtodo/controller/ToDoAPI.java b/src/main/java/com/emobile/springtodo/controller/ToDoAPI.java
new file mode 100644
index 00000000..b2e98fdb
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controller/ToDoAPI.java
@@ -0,0 +1,48 @@
+package com.emobile.springtodo.controller;
+
+import com.emobile.springtodo.dto.ToDoDto;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+import java.util.List;
+
+@Tag(name = "ToDoAPI", description = "API for managing TODO items")
+public interface ToDoAPI {
+
+ @Operation(summary = "Create a new TODO item")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "TODO created"),
+ @ApiResponse(responseCode = "404", description = "invalid input")
+ })
+ ToDoDto create(ToDoDto dto);
+
+ @Operation(summary = "Get TODO item by ID")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "TODO found"),
+ @ApiResponse(responseCode = "404", description = "TODO not found")
+ })
+ ToDoDto getById(Long id);
+
+ @Operation(summary = "Get all TODO items with pagination")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "List of TODOs")
+ })
+ List getAll(int limit, int offset);
+
+ @Operation(summary = "Update TODO item")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "TODO updated"),
+ @ApiResponse(responseCode = "404", description = "TODO not found"),
+ @ApiResponse(responseCode = "400", description = "Invalid input")
+ })
+ ToDoDto update(Long id, ToDoDto dto);
+
+ @Operation(summary = "Delete TODO item")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "TODO deleted"),
+ @ApiResponse(responseCode = "404", description = "TODO not found")
+ })
+ void delete(Long id);
+}
diff --git a/src/main/java/com/emobile/springtodo/controller/ToDoController.java b/src/main/java/com/emobile/springtodo/controller/ToDoController.java
new file mode 100644
index 00000000..45ed6ef4
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controller/ToDoController.java
@@ -0,0 +1,45 @@
+package com.emobile.springtodo.controller;
+
+import com.emobile.springtodo.dto.ToDoDto;
+import com.emobile.springtodo.service.ToDoService;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("api/todos")
+public class ToDoController implements ToDoAPI {
+
+ private final ToDoService service;
+
+ public ToDoController(ToDoService service) {
+ this.service = service;
+ }
+
+ @PostMapping
+ public ToDoDto create(@Valid @RequestBody ToDoDto dto){
+ return service.create(dto);
+ }
+
+ @GetMapping("/{id}")
+ public ToDoDto getById(@PathVariable Long id){
+ return service.findById(id);
+ }
+
+ @GetMapping
+ public List getAll(@RequestParam(defaultValue = "10") int limit,
+ @RequestParam(defaultValue = "0") int offset){
+ return service.findAll(limit, offset);
+ }
+
+ @PutMapping("/{id}")
+ public ToDoDto update(@PathVariable Long id, @Valid @RequestBody ToDoDto dto){
+ return service.update(id, dto);
+ }
+
+ @DeleteMapping("/{id}")
+ public void delete(@PathVariable Long id){
+ service.delete(id);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/dto/ToDoDto.java b/src/main/java/com/emobile/springtodo/dto/ToDoDto.java
new file mode 100644
index 00000000..99028151
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/dto/ToDoDto.java
@@ -0,0 +1,96 @@
+//package com.emobile.springtodo.dto;
+//
+//import jakarta.validation.constraints.NotBlank;
+//import jakarta.validation.constraints.Size;
+//
+//import java.io.Serializable;
+//
+//public class ToDoDto implements Serializable {
+// private static final long serialVersionUID = 1L;
+//
+// private Long id;
+//
+// @NotBlank(message = "Title is mandatory")
+// @Size(max = 100, message = "Title must not exceed 100 characters")
+// private String title;
+//
+// @Size(max = 500, message = "Description must not exceed 500 characters")
+// private String description;
+//
+// private boolean completed; // был Boolean
+//
+// public ToDoDto() {
+// }
+//
+// public ToDoDto(Long id, String title, String description, boolean completed) {
+// this.id = id;
+// this.title = title;
+// this.description = description;
+// this.completed = completed;
+// }
+//
+// public Long getId() {
+// return id;
+// }
+//
+// public void setId(Long id) {
+// this.id = id;
+// }
+//
+// public boolean isCompleted() {
+// return completed;
+// }
+//
+// public void setCompleted(boolean completed) {
+// this.completed = completed;
+// }
+//
+// public String getDescription() {
+// return description;
+// }
+//
+// public void setDescription(String description) {
+// this.description = description;
+// }
+//
+// public String getTitle() {
+// return title;
+// }
+//
+// public void setTitle(String title) {
+// this.title = title;
+// }
+//}
+package com.emobile.springtodo.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import java.io.Serializable;
+
+public class ToDoDto implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private Long id;
+
+ @NotBlank(message = "Title is mandatory")
+ @Size(max = 100, message = "Title must not exceed 100 characters")
+ private String title;
+
+ @Size(max = 500, message = "Description must not exceed 500 characters")
+ private String description;
+
+ private Boolean completed;
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+
+ public String getTitle() { return title; }
+ public void setTitle(String title) { this.title = title; }
+
+ public String getDescription() { return description; }
+ public void setDescription(String description) { this.description = description; }
+
+ public Boolean getCompleted() { return completed; }
+ public void setCompleted(Boolean completed) { this.completed = completed; }
+}
+
diff --git a/src/main/java/com/emobile/springtodo/entity/ToDoEntity.java b/src/main/java/com/emobile/springtodo/entity/ToDoEntity.java
new file mode 100644
index 00000000..0e6f9a6a
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/entity/ToDoEntity.java
@@ -0,0 +1,59 @@
+package com.emobile.springtodo.entity;
+
+import jakarta.persistence.*;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "todos")
+public class ToDoEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 255)
+ private String title;
+
+ @Column(columnDefinition = "text")
+ private String description;
+
+ @Column(nullable = false)
+ private boolean completed = false;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at", nullable = false)
+ private LocalDateTime updatedAt;
+
+ @PrePersist
+ protected void onCreate() {
+ LocalDateTime now = LocalDateTime.now();
+ this.createdAt = now;
+ this.updatedAt = now;
+ }
+
+ @PreUpdate
+ protected void onUpdate() {
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+
+ public String getTitle() { return title; }
+ public void setTitle(String title) { this.title = title; }
+
+ public String getDescription() { return description; }
+ public void setDescription(String description) { this.description = description; }
+
+ public boolean isCompleted() { return completed; }
+ public Boolean getCompleted() { return completed; }
+ public void setCompleted(boolean completed) { this.completed = completed; }
+
+ public LocalDateTime getCreatedAt() { return createdAt; }
+ public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+
+ public LocalDateTime getUpdatedAt() { return updatedAt; }
+ public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}
diff --git a/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java b/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java
new file mode 100644
index 00000000..fa4015c1
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java
@@ -0,0 +1,39 @@
+package com.emobile.springtodo.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(ToDoNotFoundException.class)
+ public ResponseEntity