From ac1189bc03d14f30b5eb57c7c0d0549d02f71e5f Mon Sep 17 00:00:00 2001 From: katejhee Date: Sun, 18 May 2025 18:36:37 +0900 Subject: [PATCH 1/3] update --- build.gradle | 5 + .../com/todolist/config/OpenApiConfig.java | 19 ++++ .../todolist/controller/UserController.java | 62 ++++++++++++ src/main/java/com/todolist/entity/Todo.java | 28 ++++++ src/main/java/com/todolist/entity/User.java | 70 +++++++++++++ .../todolist/repository/UserRepository.java | 8 ++ .../com/todolist/service/TodoService.java | 17 ++++ .../com/todolist/service/UserService.java | 99 +++++++++++++++++++ src/main/resources/application.yml | 9 +- 9 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/todolist/config/OpenApiConfig.java create mode 100644 src/main/java/com/todolist/controller/UserController.java create mode 100644 src/main/java/com/todolist/entity/User.java create mode 100644 src/main/java/com/todolist/repository/UserRepository.java create mode 100644 src/main/java/com/todolist/service/UserService.java diff --git a/build.gradle b/build.gradle index 3d4ef2e..b950e44 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' + implementation 'org.hibernate.orm:hibernate-core:6.2.7.Final' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + } tasks.named('test') { diff --git a/src/main/java/com/todolist/config/OpenApiConfig.java b/src/main/java/com/todolist/config/OpenApiConfig.java new file mode 100644 index 0000000..bd5d15d --- /dev/null +++ b/src/main/java/com/todolist/config/OpenApiConfig.java @@ -0,0 +1,19 @@ +package com.todolist.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Todo List API") + .version("1.0.0") + .description("공유 투두 리스트 프로젝트용 Swagger 명세서")); + } +} diff --git a/src/main/java/com/todolist/controller/UserController.java b/src/main/java/com/todolist/controller/UserController.java new file mode 100644 index 0000000..30f47fc --- /dev/null +++ b/src/main/java/com/todolist/controller/UserController.java @@ -0,0 +1,62 @@ +package com.todolist.controller; + +import com.todolist.entity.User; +import com.todolist.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import com.todolist.entity.Todo; +import java.util.Map; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @PostMapping + public ResponseEntity createUser(@RequestBody Map body) { + String username = body.get("username"); + return ResponseEntity.status(201).body(userService.createUser(username)); + } + + @GetMapping + public ResponseEntity> getAllUsers() { + return ResponseEntity.ok(userService.findAllUsers()); + } + + @PostMapping("/{followerId}/follow/{followeeId}") + public ResponseEntity followUser(@PathVariable Long followerId, @PathVariable Long followeeId) { + userService.followUser(followerId, followeeId); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{followerId}/unfollow/{followeeId}") + public ResponseEntity unfollowUser(@PathVariable Long followerId, @PathVariable Long followeeId) { + userService.unfollowUser(followerId, followeeId); + return ResponseEntity.ok().build(); + } + + @GetMapping("/{id}/following") + public ResponseEntity> getFollowing(@PathVariable Long id) { + return ResponseEntity.ok(userService.getFollowing(id)); + } + + @GetMapping("/{id}/followers") + public ResponseEntity> getFollowers(@PathVariable Long id) { + return ResponseEntity.ok(userService.getFollowers(id)); + } + + @GetMapping("/{id}/todos") + public ResponseEntity> getUserTodos(@PathVariable Long id, @RequestParam(required = false, defaultValue = "false") boolean increaseView) { + return ResponseEntity.ok(userService.getUserTodos(id, increaseView)); + } + + @GetMapping("/{id}/my-todos") + public ResponseEntity> getMyTodos(@PathVariable Long id) { + return ResponseEntity.ok(userService.getMyTodos(id)); + } +} diff --git a/src/main/java/com/todolist/entity/Todo.java b/src/main/java/com/todolist/entity/Todo.java index 8660978..134cf90 100644 --- a/src/main/java/com/todolist/entity/Todo.java +++ b/src/main/java/com/todolist/entity/Todo.java @@ -3,6 +3,9 @@ import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.JoinColumn; +import com.todolist.entity.User; @Entity public class Todo { @@ -16,6 +19,12 @@ public class Todo { @Enumerated(EnumType.STRING) private TodoStatus status = TodoStatus.PENDING; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + private int viewCount = 0; + @OneToMany(mappedBy = "todo", cascade = CascadeType.ALL, orphanRemoval = true) private List subTasks = new ArrayList<>(); @@ -42,4 +51,23 @@ public String getTitle() { public TodoStatus getStatus() { return status; } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public int getViewCount() { + return viewCount; + } + + public void increaseViewCount() { + this.viewCount++; + } + public List getSubTasks() { + return subTasks; + } } \ No newline at end of file diff --git a/src/main/java/com/todolist/entity/User.java b/src/main/java/com/todolist/entity/User.java new file mode 100644 index 0000000..c50223a --- /dev/null +++ b/src/main/java/com/todolist/entity/User.java @@ -0,0 +1,70 @@ +package com.todolist.entity; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List todos = new ArrayList<>(); + + @ManyToMany + @JoinTable( + name = "user_following", + joinColumns = @JoinColumn(name = "follower_id"), + inverseJoinColumns = @JoinColumn(name = "followee_id") + ) + private List following = new ArrayList<>(); + + @ManyToMany(mappedBy = "following") + private List followers = new ArrayList<>(); + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public List getTodos() { + return todos; + } + + public void setTodos(List todos) { + this.todos = todos; + } + + public List getFollowing() { + return following; + } + + public void setFollowing(List following) { + this.following = following; + } + + public List getFollowers() { + return followers; + } + + public void setFollowers(List followers) { + this.followers = followers; + } +} diff --git a/src/main/java/com/todolist/repository/UserRepository.java b/src/main/java/com/todolist/repository/UserRepository.java new file mode 100644 index 0000000..ac8cdc4 --- /dev/null +++ b/src/main/java/com/todolist/repository/UserRepository.java @@ -0,0 +1,8 @@ +package com.todolist.repository; + +import com.todolist.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + User findByUsername(String username); +} diff --git a/src/main/java/com/todolist/service/TodoService.java b/src/main/java/com/todolist/service/TodoService.java index 76fc3c3..99d9bc4 100644 --- a/src/main/java/com/todolist/service/TodoService.java +++ b/src/main/java/com/todolist/service/TodoService.java @@ -6,6 +6,7 @@ import com.todolist.entity.Todo; import com.todolist.entity.TodoStatus; import com.todolist.repository.TodoRepository; +import com.todolist.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -16,6 +17,7 @@ public class TodoService { private final TodoRepository todoRepository; + private final UserRepository userRepository; public Todo createTodo(TodoCreateRequest request) { Todo todo = new Todo(); @@ -59,4 +61,19 @@ public void deleteTodos(java.util.List ids) { } todoRepository.deleteAll(todos); } + + public java.util.List getTodosByUserId(Long userId, boolean increaseViewCount) { + var user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("해당 유저가 존재하지 않습니다.")); + + java.util.List todos = user.getTodos(); + if (increaseViewCount) { + for (Todo todo : todos) { + todo.increaseViewCount(); + } + todoRepository.saveAll(todos); + } + + return todos; + } } diff --git a/src/main/java/com/todolist/service/UserService.java b/src/main/java/com/todolist/service/UserService.java new file mode 100644 index 0000000..e7cac25 --- /dev/null +++ b/src/main/java/com/todolist/service/UserService.java @@ -0,0 +1,99 @@ +package com.todolist.service; + +import com.todolist.entity.Todo; + +import com.todolist.entity.User; +import com.todolist.repository.UserRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public List findAllUsers() { + return userRepository.findAll(); + } + + public User findUserById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + id)); + } + + @Transactional + public void followUser(Long followerId, Long followeeId) { + if (followerId.equals(followeeId)) { + throw new IllegalArgumentException("You cannot follow yourself."); + } + + User follower = findUserById(followerId); + User followee = findUserById(followeeId); + + if (!follower.getFollowing().contains(followee)) { + follower.getFollowing().add(followee); + userRepository.save(follower); + } + } + + @Transactional + public void unfollowUser(Long followerId, Long followeeId) { + User follower = findUserById(followerId); + User followee = findUserById(followeeId); + + if (follower.getFollowing().contains(followee)) { + follower.getFollowing().remove(followee); + userRepository.save(follower); + } + } + + public List getFollowers(Long userId) { + return findUserById(userId).getFollowers(); + } + + public List getFollowing(Long userId) { + return findUserById(userId).getFollowing(); + } + + public boolean isFollowing(Long followerId, Long followeeId) { + User follower = findUserById(followerId); + User followee = findUserById(followeeId); + return follower.getFollowing().contains(followee); + } + public boolean isMutualFollow(Long userId1, Long userId2) { + User user1 = findUserById(userId1); + User user2 = findUserById(userId2); + return user1.getFollowing().contains(user2) && user2.getFollowing().contains(user1); + } + public User createUser(String username) { + if (username == null || username.isBlank()) { + throw new IllegalArgumentException("Username must not be empty."); + } + + User user = new User(); + user.setUsername(username); + return userRepository.save(user); + } + public List getUserTodos(Long id, boolean increaseView) { + User user = findUserById(id); + List todos = user.getTodos(); + + if (increaseView) { + for (Todo todo : todos) { + todo.increaseViewCount(); + if (todo.getSubTasks() != null) { + todo.getSubTasks().size(); + } + } + } + + return todos; + } + public List getMyTodos(Long myId) { + return getUserTodos(myId, false); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index adaeec5..3c380f7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,14 @@ spring: datasource: - url: jdbc:mysql://db:3306/todo + url: jdbc:mysql://localhost:3306/todo username: root password: root1234 jpa: hibernate: - ddl-auto: update \ No newline at end of file + ddl-auto: update + dialect: org.hibernate.dialect.MySQL8Dialect + +springdoc: + swagger-ui: + path: /swagger-ui.html \ No newline at end of file From a8aab00b0dd4e7d455cf7a2a50beec629343ee60 Mon Sep 17 00:00:00 2001 From: katejhee Date: Sun, 18 May 2025 18:40:44 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=20update=206=EC=A3=BC=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todo-list | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todo-list b/todo-list index e5f7b5d..2b54989 160000 --- a/todo-list +++ b/todo-list @@ -1 +1 @@ -Subproject commit e5f7b5dfa876205f62150f4b5424764f69115d99 +Subproject commit 2b549897d98f5cf032cc5ec79d2efa8810f2de7e From 57b1650488278fa90ffec707519ace77edc8fea5 Mon Sep 17 00:00:00 2001 From: katejhee Date: Tue, 20 May 2025 16:20:48 +0900 Subject: [PATCH 3/3] Add GitHub Actions workflow for CI/CD --- .github/workflows/deploy.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..43fccdf --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,31 @@ +name: CI/CD Build and Deploy + +on: + push: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v3 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew clean build + + - name: Docker build + run: docker build -t todo-upgrade-app . + + - name: Docker image 확인 + run: docker images \ No newline at end of file