diff --git a/.gitignore b/.gitignore
index 7ed0d6b6..ff5649be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ target/
*.iws
*.iml
*.ipr
+amplicode.xml
### NetBeans ###
/nbproject/private/
diff --git a/README.md b/README.md
index bf775361..dc11619f 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
Требования к приложению
-
+
- База данных: Использовать
PostgreSQL.
- Миграции: Настроить миграции с помощью
Liquibase или Flyway. Миграции должны автоматически применяться при старте приложения и в тестах.
- Работа с БД: Не использовать
Hibernate/JPA, вместо этого использовать JdbcTemplate.
@@ -113,3 +113,49 @@
- Пакеты следует логически организовать:
controller, service, repository, dto, config, exception и т.д.
- Бизнес-ошибки должны обрабатываться централизованно через единый ExceptionHandler.
+
+## Модель данных Задачи (TODO item):
+
+* `id` (Long, Primary Key, автоинкремент)
+* `userId` (Long, Foreign Key, не null, ссылка на пользователя)
+* `title` (String, не null, макс. длина, например, 255)
+* `description` (String, опционально, text)
+* `completed` (boolean, по умолчанию false)
+* `createdAt` (Timestamp, не null, по умолчанию текущее время)
+* `updatedAt` (Timestamp, не null, по умолчанию текущее время, обновляется при изменении)
+* `dueDate` (Date/Timestamp, опционально, для инновационной функции)
+
+## Функции приложения:
+
+### Необходимые функции (базовый CRUD):
+
+1. **Создание новой задачи:**
+ * **Эндпоинт:** `POST /api/v1/todos`
+ * **Вход:** DTO с `title` (обязательно), `description` (опционально).
+ * **Выход:** DTO созданной задачи с присвоенным `id`, `createdAt`, `updatedAt` и `completed=false`.
+ * **Действия:** Сохранение новой задачи в БД. Валидация входных данных.
+
+2. **Получение списка всех задач (с пагинацией):**
+ * **Эндпоинт:** `GET /api/v1/todos`
+ * **Параметры запроса:** `limit` (int, по умолчанию, например, 10), `offset` (int, по умолчанию 0).
+ * **Выход:** Список DTO задач, обернутый, возможно, в объект, содержащий также информацию о пагинации (`totalCount`, `limit`, `offset`).
+ * **Действия:** Извлечение задач из БД с учетом пагинации.
+
+3. **Получение задачи по ID:**
+ * **Эндпоинт:** `GET /api/v1/todos/{id}`
+ * **Вход (path variable):** `id` задачи.
+ * **Выход:** DTO задачи или 404, если задача не найдена.
+ * **Действия:** Поиск задачи в БД по ID.
+
+4. **Обновление существующей задачи:**
+ * **Эндпоинт:** `PUT /api/v1/todos/{id}`
+ * **Вход (path variable):** `id` задачи.
+ * **Вход (body):** DTO с полями для обновления (`title`, `description`, `completed`).
+ * **Выход:** DTO обновленной задачи или 404.
+ * **Действия:** Обновление полей задачи в БД. Валидация входных данных. Обновление `updatedAt`.
+
+5. **Удаление задачи:**
+ * **Эндпоинт:** `DELETE /api/v1/todos/{id}`
+ * **Вход (path variable):** `id` задачи.
+ * **Выход:** Статус 204 No Content или 404.
+ * **Действия:** Удаление задачи из БД.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 03bc446d..16f06906 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,8 +5,8 @@
org.springframework.boot
spring-boot-starter-parent
- 3.3.5
-
+ 3.5.0
+
com.emobile
SpringToDo
@@ -15,7 +15,7 @@
SpringToDo
- 21
+ 17
@@ -24,11 +24,105 @@
spring-boot-starter
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+ org.liquibase
+ liquibase-core
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+ runtime
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.mapstruct
+ mapstruct
+ 1.6.0
+ true
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ true
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.8.8
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-api
+ 2.8.8
+
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
@@ -36,6 +130,38 @@
org.springframework.boot
spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.6.0
+
+
+
diff --git a/src/main/java/com/emobile/springtodo/api/TagApi.java b/src/main/java/com/emobile/springtodo/api/TagApi.java
new file mode 100644
index 00000000..b57dc3d3
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/api/TagApi.java
@@ -0,0 +1,41 @@
+package com.emobile.springtodo.api;
+
+import com.emobile.springtodo.model.dto.*;
+import io.swagger.v3.oas.annotations.*;
+import jakarta.validation.*;
+import org.springframework.web.bind.annotation.*;
+
+import static org.springframework.http.HttpStatus.*;
+
+
+@RequestMapping("/api/v1/tags")
+public interface TagApi {
+
+ @PostMapping
+ @ResponseStatus(CREATED)
+ TagDto createTag(@Valid @RequestBody TagCreateDto createTodoDto);
+
+ @PutMapping("/{id}")
+ TagDto updateTag(@Parameter(description = "ID of the tag to update") @PathVariable Long id,
+ @Valid @RequestBody TagUpdateDto requestDto);
+
+ @DeleteMapping("/{id}")
+ @ResponseStatus(NO_CONTENT)
+ void deleteTagById(@Parameter(description = "ID of the tag item to delete") @PathVariable Long id);
+
+ @GetMapping("/{id}")
+ TagDto getTagById(@Parameter(description = "ID of the tag to retrieve") @PathVariable Long id);
+
+ @GetMapping("/")
+ TagListResponseDto getAllTags();
+
+ @PutMapping("/{tagId}/tag/{todoId}")
+ void appendTagForTodo(
+ @PathVariable @Parameter(description = "ID of the task to append") Long tagId,
+ @PathVariable @Parameter(description = "ID of the tag to append to the task") Long todoId);
+
+ @DeleteMapping("/{tagId}/tag/{todoId}")
+ void removeTagForTodo(
+ @PathVariable @Parameter(description = "ID of the tag to delete from the task") Long tagId,
+ @PathVariable @Parameter(description = "ID of the task") Long todoId);
+}
diff --git a/src/main/java/com/emobile/springtodo/api/TodoApi.java b/src/main/java/com/emobile/springtodo/api/TodoApi.java
new file mode 100644
index 00000000..25e87a3d
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/api/TodoApi.java
@@ -0,0 +1,44 @@
+package com.emobile.springtodo.api;
+
+import com.emobile.springtodo.model.dto.*;
+import io.swagger.v3.oas.annotations.*;
+import jakarta.validation.*;
+import org.springframework.web.bind.annotation.*;
+
+import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.NO_CONTENT;
+
+
+@RequestMapping("/api/v1/todos")
+public interface TodoApi {
+
+ @PostMapping
+ @ResponseStatus(CREATED)
+ TodoDto createTodo(@Valid @RequestBody TodoCreateDto createTodoDto);
+
+ @PutMapping("/{id}")
+ TodoDto updateTodo(@Parameter(description = "ID of the task to update") @PathVariable Long id,
+ @Valid @RequestBody TodoUpdateDto requestDto);
+
+ @GetMapping("/{id}")
+ TodoDto getTodoById(@Parameter(description = "ID of the task to retrieve") @PathVariable (value = "id") Long id);
+
+ @DeleteMapping("/{id}")
+ @ResponseStatus(NO_CONTENT)
+ void deleteTodoById(@Parameter(description = "ID of the TODO item to delete") @PathVariable Long id);
+
+ @GetMapping("/user/{userId}")
+ TodoListResponseDto getAllTodosByUserId(
+ @PathVariable (value = "userId")
+ Long userId
+ );
+
+ @PatchMapping("/complete/{id}")
+ @ResponseStatus(NO_CONTENT)
+ void completeTodo(@Parameter(description = "ID of the task to complete") @PathVariable Long id);
+
+ @PatchMapping("/incomplete/{id}")
+ @ResponseStatus(NO_CONTENT)
+ void incompleteTodo(@Parameter(description = "ID of the task to mark incomplete") @PathVariable Long id);
+
+}
diff --git a/src/main/java/com/emobile/springtodo/aspect/TodoMetricsAspect.java b/src/main/java/com/emobile/springtodo/aspect/TodoMetricsAspect.java
new file mode 100644
index 00000000..ce0c2e38
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/aspect/TodoMetricsAspect.java
@@ -0,0 +1,123 @@
+package com.emobile.springtodo.aspect;
+
+import com.emobile.springtodo.model.entity.*;
+import com.emobile.springtodo.repository.*;
+import io.micrometer.core.instrument.*;
+import io.micrometer.core.instrument.Timer;
+import lombok.extern.slf4j.*;
+import org.aspectj.lang.*;
+import org.aspectj.lang.annotation.*;
+import org.springframework.stereotype.*;
+
+import java.time.*;
+import java.util.*;
+
+@Aspect
+@Component
+@Slf4j
+public class TodoMetricsAspect {
+
+ private final MeterRegistry meterRegistry;
+ private final TodoRepository todoRepository;
+
+ private final Counter todosCreatedCounter;
+ private final Counter todosCompletedCounter;
+ private final Counter todosDeletedCounter;
+ private final Counter tagsAppendedCounter;
+
+ public TodoMetricsAspect(MeterRegistry meterRegistry, TodoRepository todoRepository) {
+ this.meterRegistry = meterRegistry;
+ this.todoRepository = todoRepository;
+
+ this.todosCreatedCounter = Counter.builder("todos.created.total")
+ .description("Total number of created todos")
+ .register(meterRegistry);
+
+ this.todosCompletedCounter = Counter.builder("todos.completed.total")
+ .description("Total number of completed todos")
+ .register(meterRegistry);
+
+ this.todosDeletedCounter = Counter.builder("todos.deleted.total")
+ .description("Total number of deleted todos")
+ .register(meterRegistry);
+
+ this.tagsAppendedCounter = Counter.builder("todos.tags.appended.total")
+ .description("Total number of tags appended to todos")
+ .register(meterRegistry);
+
+ Gauge.builder("todos.active.count", todoRepository, repo -> repo.countByCompleted(false))
+ .description("Current number of active (incomplete) todos")
+ .register(meterRegistry);
+
+ Gauge.builder("todos.completed.count", todoRepository, repo -> repo.countByCompleted(true))
+ .description("Current number of completed todos")
+ .register(meterRegistry);
+ }
+
+ // --- Определения срезов (Pointcuts) ---
+
+ @Pointcut("execution(* com.emobile.springtodo.service.impl.TodoServiceImpl.createTodo(..))")
+ public void createTodoPointcut() {}
+
+ @Pointcut("execution(* com.emobile.springtodo.service.impl.TodoServiceImpl.completeTodo(..))")
+ public void completeTodoPointcut() {}
+
+ @Pointcut("execution(* com.emobile.springtodo.service.impl.TodoServiceImpl.deleteTodo(..))")
+ public void deleteTodoPointcut() {}
+
+ @Pointcut("execution(* com.emobile.springtodo.service.impl.TodoServiceImpl.appendTagTodo(..))")
+ public void appendTagTodoPointcut() {}
+
+
+ // --- Советы (Advices) ---
+
+ @AfterReturning("createTodoPointcut()")
+ public void afterTodoCreated() {
+ todosCreatedCounter.increment();
+ }
+
+ @AfterReturning("deleteTodoPointcut()")
+ public void afterTodoDeleted() {
+ todosDeletedCounter.increment();
+ }
+
+ @AfterReturning("appendTagTodoPointcut()")
+ public void afterTagAppended() {
+ tagsAppendedCounter.increment();
+ }
+
+ /**
+ * Самая интересная метрика: измеряем время от создания до завершения задачи.
+ * Используем @Around, чтобы получить доступ к аргументам метода и выполнить
+ * действия до и после его вызова.
+ */
+ @Around("completeTodoPointcut()")
+ public Object measureCompletionTime(ProceedingJoinPoint joinPoint) throws Throwable {
+ Long todoId = (Long) joinPoint.getArgs()[0];
+
+ // Получаем задачу ДО ее завершения, чтобы узнать время создания
+ Optional todoOptional = todoRepository.findByID(todoId);
+
+ // Выполняем оригинальный метод completeTodo()
+ Object result = joinPoint.proceed();
+
+ // ПОСЛЕ успешного выполнения метода...
+ if (todoOptional.isPresent()) {
+ Todo todo = todoOptional.get();
+ if (todo.getCreatedAt() != null) {
+ // Увеличиваем счетчик завершенных задач
+ todosCompletedCounter.increment();
+
+ // Рассчитываем и записываем время выполнения
+ Duration timeToComplete = Duration.between(todo.getCreatedAt(), LocalDateTime.now());
+ Timer.builder("todos.completion.time")
+ .description("Time taken to complete a todo from its creation")
+ .register(meterRegistry)
+ .record(timeToComplete);
+ log.info("Todo {} completed in {}", todoId, timeToComplete);
+ }
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
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..91d852c3
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/config/RedisConfig.java
@@ -0,0 +1,51 @@
+package com.emobile.springtodo.config;
+
+import com.emobile.springtodo.model.dto.*;
+import org.springframework.beans.factory.annotation.*;
+import org.springframework.boot.autoconfigure.cache.*;
+import org.springframework.cache.*;
+import org.springframework.context.annotation.*;
+
+import java.time.*;
+import org.springframework.data.redis.cache.*;
+import org.springframework.data.redis.connection.*;
+import org.springframework.data.redis.connection.lettuce.*;
+import org.springframework.data.redis.serializer.*;
+
+@Configuration
+public class RedisConfig {
+
+ @Value("${spring.data.redis.host}")
+ private String redisHost;
+ @Value("${spring.data.redis.port}")
+ private int redisPort;
+
+ @Bean
+ public RedisConnectionFactory redisConnectionFactory() {
+ return new LettuceConnectionFactory(redisHost, redisPort);
+ }
+
+ @Bean
+ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
+ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+ .entryTtl(Duration.ofMinutes(10))
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair
+ .fromSerializer(new Jackson2JsonRedisSerializer<>(TodoDto.class)))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair
+ .fromSerializer(new Jackson2JsonRedisSerializer<>(TodoListResponseDto.class)));
+
+ return RedisCacheManager.builder(redisConnectionFactory)
+ .cacheDefaults(config)
+ .build();
+ }
+
+ @Bean
+ public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
+ return builder -> builder
+ .withCacheConfiguration("todo",
+ RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)))
+ .withCacheConfiguration("todoList",
+ RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(60)));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/emobile/springtodo/config/WebConfig.java b/src/main/java/com/emobile/springtodo/config/WebConfig.java
new file mode 100644
index 00000000..59fa24de
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/config/WebConfig.java
@@ -0,0 +1,16 @@
+package com.emobile.springtodo.config;
+
+import org.springframework.context.annotation.*;
+import org.springframework.web.servlet.config.annotation.*;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/api/**")
+ .allowedOrigins("http://localhost:3000")
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+ .allowedHeaders("*")
+ .allowCredentials(true);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/emobile/springtodo/controller/TagController.java b/src/main/java/com/emobile/springtodo/controller/TagController.java
new file mode 100644
index 00000000..234f2593
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controller/TagController.java
@@ -0,0 +1,50 @@
+package com.emobile.springtodo.controller;
+
+import com.emobile.springtodo.api.*;
+import com.emobile.springtodo.model.dto.*;
+import com.emobile.springtodo.service.*;
+import jakarta.validation.*;
+import lombok.*;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@AllArgsConstructor
+public class TagController implements TagApi {
+
+ private final TagService tagService;
+
+ @Override
+ public TagDto createTag(TagCreateDto createTodoDto) {
+ return tagService.createTodo(createTodoDto);
+ }
+
+ @Override
+ public TagDto updateTag(Long id, @Valid TagUpdateDto requestDto) {
+ return tagService.updateTodo(id, requestDto);
+ }
+
+ @Override
+ public void deleteTagById(Long id) {
+ tagService.removeTodo(id);
+ }
+
+ @Override
+ public TagDto getTagById(Long id) {
+ return tagService.getById(id);
+ }
+
+ @Override
+ public TagListResponseDto getAllTags() {
+ return tagService.getAllTags();
+ }
+
+ @Override
+ public void appendTagForTodo(Long tagId, Long todoId) {
+ tagService.appendTagTodo(todoId, tagId);
+ }
+
+ @Override
+ public void removeTagForTodo(Long tagId, Long todoId) {
+ tagService.removeTagTodo(tagId, todoId);
+ }
+}
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..0cf4d5d7
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controller/TodoController.java
@@ -0,0 +1,49 @@
+package com.emobile.springtodo.controller;
+
+import com.emobile.springtodo.api.*;
+import com.emobile.springtodo.model.dto.*;
+import com.emobile.springtodo.service.*;
+import lombok.*;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+public class TodoController implements TodoApi {
+
+ private final TodoService todoService;
+
+ @Override
+ public TodoDto createTodo(TodoCreateDto createTodoDto) {
+ return todoService.createTodo(createTodoDto);
+ }
+
+ @Override
+ public TodoListResponseDto getAllTodosByUserId(Long userId) {
+ return todoService.getAllTodosByUserId(userId);
+ }
+
+ @Override
+ public TodoDto updateTodo(Long id, TodoUpdateDto requestDto) {
+ return todoService.updateTodo(id, requestDto);
+ }
+
+ @Override
+ public void completeTodo(Long id) {
+ todoService.completeTodo(id);
+ }
+
+ @Override
+ public void incompleteTodo(Long id) {
+ todoService.incompleteTodo(id);
+ }
+
+ @Override
+ public TodoDto getTodoById(Long id) {
+ return todoService.getTodoById(id);
+ }
+
+ @Override
+ public void deleteTodoById(Long id) {
+ todoService.deleteTodo(id);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exception/TagRepositoryException.java b/src/main/java/com/emobile/springtodo/exception/TagRepositoryException.java
new file mode 100644
index 00000000..234e6059
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exception/TagRepositoryException.java
@@ -0,0 +1,8 @@
+package com.emobile.springtodo.exception;
+
+public class TagRepositoryException extends RuntimeException {
+
+ public TagRepositoryException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exception/TodoNotFoundException.java b/src/main/java/com/emobile/springtodo/exception/TodoNotFoundException.java
new file mode 100644
index 00000000..7ae6134e
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exception/TodoNotFoundException.java
@@ -0,0 +1,8 @@
+package com.emobile.springtodo.exception;
+
+public class TodoNotFoundException extends RuntimeException {
+
+ public TodoNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exception/TodoRepositoryException.java b/src/main/java/com/emobile/springtodo/exception/TodoRepositoryException.java
new file mode 100644
index 00000000..d84d45aa
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exception/TodoRepositoryException.java
@@ -0,0 +1,8 @@
+package com.emobile.springtodo.exception;
+
+public class TodoRepositoryException extends RuntimeException {
+
+ public TodoRepositoryException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/filter/BasicFilter.java b/src/main/java/com/emobile/springtodo/filter/BasicFilter.java
new file mode 100644
index 00000000..8431319e
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/filter/BasicFilter.java
@@ -0,0 +1,4 @@
+package com.emobile.springtodo.filter;
+
+public class BasicFilter {
+}
diff --git a/src/main/java/com/emobile/springtodo/handler/GlobalExceptionHandler.java b/src/main/java/com/emobile/springtodo/handler/GlobalExceptionHandler.java
new file mode 100644
index 00000000..d45f23f3
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/handler/GlobalExceptionHandler.java
@@ -0,0 +1,47 @@
+package com.emobile.springtodo.handler;
+
+import com.emobile.springtodo.exception.*;
+import lombok.extern.slf4j.*;
+import org.springframework.http.*;
+import org.springframework.validation.*;
+import org.springframework.web.bind.*;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.*;
+import org.springframework.web.servlet.mvc.method.annotation.*;
+
+import java.util.*;
+
+@ControllerAdvice
+@Slf4j
+public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
+
+ @ExceptionHandler(TodoRepositoryException.class)
+ public ResponseEntity handleTodoRepositoryException(TodoRepositoryException ex) {
+ log.error(ex.getMessage());
+ return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(TagRepositoryException.class)
+ public ResponseEntity handleTagRepositoryException(TagRepositoryException ex) {
+ log.error(ex.getMessage());
+ return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(TodoNotFoundException.class)
+ public ResponseEntity handleTodoNotFoundException(TodoNotFoundException ex) {
+ log.error(ex.getMessage());
+ return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
+ }
+
+ @Override
+ protected ResponseEntity