Skip to content

Commit

Permalink
Merge pull request #200 from ShanePark/#199
Browse files Browse the repository at this point in the history
#199 TODO feature
  • Loading branch information
ShanePark authored Oct 7, 2024
2 parents 311fe9c + 483cb1c commit 1d13c65
Show file tree
Hide file tree
Showing 15 changed files with 1,029 additions and 4 deletions.
24 changes: 24 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,27 @@ operation::holiday/reset[]
=== friend-info

operation::friends/friend-info[]

== TODO

=== GET

operation::todos/get-list[]

=== CREATE

operation::todos/create[]

=== UPDATE

operation::todos/update[]

=== UPDATE-POSITION

operation::todos/update-position[]

=== DELETE

operation::todos/delete[]


Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.tistory.shanepark.dutypark.todo.controller

import com.tistory.shanepark.dutypark.member.domain.annotation.Login
import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember
import com.tistory.shanepark.dutypark.todo.domain.dto.TodoRequest
import com.tistory.shanepark.dutypark.todo.domain.dto.TodoResponse
import com.tistory.shanepark.dutypark.todo.service.TodoService
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import java.util.*

@RestController
@RequestMapping("/api/todos")
class TodoController(
private val todoService: TodoService
) {

@GetMapping
fun todoList(
@Login loginMember: LoginMember
): List<TodoResponse> {
return todoService.todoList(loginMember)
}

@PostMapping
fun addTodo(
@Login loginMember: LoginMember,
@RequestBody @Validated todoRequest: TodoRequest
): TodoResponse {
return todoService.addTodo(loginMember, todoRequest.title, todoRequest.content)
}

@PutMapping("/{id}")
fun editTodo(
@Login loginMember: LoginMember,
@PathVariable id: UUID,
@RequestBody @Validated todoRequest: TodoRequest
): TodoResponse {
return todoService.editTodo(loginMember, id, todoRequest.title, todoRequest.content)
}

@PatchMapping("/position")
fun updatePosition(
@Login loginMember: LoginMember,
@RequestBody ids: List<UUID>
) {
todoService.updatePosition(loginMember, ids)
}

@DeleteMapping("/{id}")
fun deleteTodo(
@Login loginMember: LoginMember,
@PathVariable id: UUID
) {
todoService.deleteTodo(loginMember, id)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.tistory.shanepark.dutypark.todo.domain.dto

import jakarta.validation.constraints.NotBlank
import org.hibernate.validator.constraints.Length
import org.springframework.validation.annotation.Validated

@Validated
data class TodoRequest(

@field: NotBlank
@field: Length(min = 1, max = 100)
val title: String,

val content: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.tistory.shanepark.dutypark.todo.domain.dto

import com.tistory.shanepark.dutypark.todo.domain.entity.Todo
import java.time.LocalDateTime

data class TodoResponse(
val id: String,
val title: String,
val content: String,
val position: Int,
val createdDate: LocalDateTime,
) {

companion object {
fun from(todo: Todo): TodoResponse {
return TodoResponse(
id = todo.id.toString(),
title = todo.title,
content = todo.content,
position = todo.position,
createdDate = todo.createdDate
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.tistory.shanepark.dutypark.todo.domain.entity

import com.tistory.shanepark.dutypark.common.domain.entity.EntityBase
import com.tistory.shanepark.dutypark.member.domain.entity.Member
import jakarta.persistence.*
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime

@Entity
@EntityListeners(AuditingEntityListener::class)
class Todo(
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
val member: Member,

@Column(name = "title", nullable = false, length = 50)
var title: String,

@Column(name = "content", nullable = false, length = 50)
var content: String,

@Column(name = "position", nullable = false)
var position: Int,

) : EntityBase() {

fun update(title: String, content: String) {
this.title = title
this.content = content
this.modifiedDate = LocalDateTime.now()
}

@Column(name = "modified_date", updatable = true)
var modifiedDate: LocalDateTime = LocalDateTime.now()

@Column(name = "created_date", updatable = false)
val createdDate: LocalDateTime = LocalDateTime.now()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tistory.shanepark.dutypark.todo.repository

import com.tistory.shanepark.dutypark.member.domain.entity.Member
import com.tistory.shanepark.dutypark.todo.domain.entity.Todo
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import java.util.*

interface TodoRepository : JpaRepository<Todo, UUID> {

@Query(
"SELECT COALESCE(MAX(t.position), 0) " +
"FROM Todo t " +
"WHERE t.member = :member"
)
fun findMaxPositionByMember(@Param("member") member: Member): Int

fun findAllByMemberOrderByPosition(member: Member): List<Todo>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.tistory.shanepark.dutypark.todo.service

import com.tistory.shanepark.dutypark.member.domain.entity.Member
import com.tistory.shanepark.dutypark.member.repository.MemberRepository
import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember
import com.tistory.shanepark.dutypark.todo.domain.dto.TodoResponse
import com.tistory.shanepark.dutypark.todo.domain.entity.Todo
import com.tistory.shanepark.dutypark.todo.repository.TodoRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*

@Service
@Transactional
class TodoService(
private val memberRepository: MemberRepository,
private val todoRepository: TodoRepository
) {

@Transactional(readOnly = true)
fun todoList(loginMember: LoginMember): List<TodoResponse> {
val member = findMember(loginMember)
return todoRepository.findAllByMemberOrderByPosition(member)
.map { TodoResponse.from(it) }
}

fun addTodo(loginMember: LoginMember, title: String, content: String): TodoResponse {
val member = findMember(loginMember)
val todoLastPosition = todoRepository.findMaxPositionByMember(member)

val todo = Todo(member = member, title = title, content = content, position = todoLastPosition + 1)
todoRepository.save(todo)

return TodoResponse.from(todo)
}

fun editTodo(loginMember: LoginMember, id: UUID, title: String, content: String): TodoResponse {
val member = findMember(loginMember)

val todo = todoRepository.findById(id)
.orElseThrow { IllegalArgumentException("Todo not found") }

verifyOwnership(todo, member)

todo.update(title, content)
return TodoResponse.from(todo)
}

fun updatePosition(loginMember: LoginMember, ids: List<UUID>) {
val member = findMember(loginMember)

val indexMap = ids.mapIndexed { index, id -> id to index }.toMap()
val todos = todoRepository.findAllById(ids).sortedBy { indexMap.getValue(it.id) }

todos.forEachIndexed { index, todo ->
verifyOwnership(todo, member)
todo.position = index
}
}

fun deleteTodo(loginMember: LoginMember, id: UUID) {
val member = findMember(loginMember)
val todo = todoRepository.findById(id).orElseThrow { IllegalArgumentException("Todo not found") }
verifyOwnership(todo, member)

todoRepository.delete(todo)
}

private fun verifyOwnership(todo: Todo, member: Member) {
if (todo.member.id != member.id) {
throw IllegalArgumentException("Todo is not yours")
}
}

private fun findMember(loginMember: LoginMember): Member =
memberRepository.findById(loginMember.id)
.orElseThrow { IllegalArgumentException("Member not found") }

}
12 changes: 12 additions & 0 deletions src/main/resources/db/migration/v2/V2.0.13__todo_list.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE `todo`
(
`id` char(36) PRIMARY KEY,
`member_id` bigint NOT NULL,
`title` varchar(50) NOT NULL,
`content` varchar(50) NOT NULL,
`position` int NOT NULL,
`created_date` datetime NOT NULL,
`modified_date` datetime NOT NULL,

index `idx_member_id` (`member_id`)
);
63 changes: 63 additions & 0 deletions src/main/resources/static/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,66 @@ a.homeButton:hover {
.member-info th {
background-color: #eee;
}

.todo-container {
border: 1px solid black;
border-radius: 10px;
padding: 2px 10px;
margin-bottom: 15px;
}

#add-todo-btn {
background-color: #22c55e;
color: #ffffff;
padding: 0.5rem 1rem;
border-radius: 9999px;
border: 0;
font-size: 1.125rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
white-space: nowrap;
}

#todo-list-container {
display: flex;
overflow-x: auto;
padding-top: 0.5rem;
}

.todo-item {
cursor: grab;
user-select: none;
flex: 0 0 auto;
display: flex;
align-items: center;
padding: 1rem;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-right: 1rem;
min-width: 200px;
transition: background-color 0.3s;
position: relative;
}

.todo-item:hover {
background-color: #e2e8f0;
}

.todo-item:active {
cursor: grabbing;
}

.todo-item.dragging {
opacity: 0.5;
}

.todo-text {
flex-grow: 1;
cursor: pointer;
}

input[readonly], textarea[readonly] {
background-color: #f8f9fa;
cursor: not-allowed;
}
25 changes: 25 additions & 0 deletions src/main/resources/static/css/mquery.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,29 @@
font-size: 1.8em;
}

#add-todo-modal .modal-dialog,
#add-todo-modal .modal-header,
#todo-details-modal .modal-dialog,
#todo-details-modal .modal-header {
max-width: 90%;
font-size: 1.5rem;
}

#add-todo-modal h5,
#todo-details-modal h5 {
font-size: 2em;
}

#add-todo-modal input,
#add-todo-modal textarea,
#add-todo-modal button,
#add-todo-modal span,
#todo-details-modal input,
#todo-details-modal textarea,
#todo-details-modal button,
#todo-details-modal span {
font-size: 1.5em;
}


}
2 changes: 2 additions & 0 deletions src/main/resources/static/lib/sortable/Sortable.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit 1d13c65

Please sign in to comment.