diff --git a/our-memory/src/main/java/keepsake/ourmemory/application/repository/TagRepository.java b/our-memory/src/main/java/keepsake/ourmemory/application/repository/TagRepository.java new file mode 100644 index 0000000..8d5d76d --- /dev/null +++ b/our-memory/src/main/java/keepsake/ourmemory/application/repository/TagRepository.java @@ -0,0 +1,11 @@ +package keepsake.ourmemory.application.repository; + +import keepsake.ourmemory.domain.tag.Tag; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TagRepository extends JpaRepository { + + List findTagsByMemberIdAndDeletedIsFalse(Long memberId); +} diff --git a/our-memory/src/main/java/keepsake/ourmemory/application/tag/TagService.java b/our-memory/src/main/java/keepsake/ourmemory/application/tag/TagService.java new file mode 100644 index 0000000..1f4111a --- /dev/null +++ b/our-memory/src/main/java/keepsake/ourmemory/application/tag/TagService.java @@ -0,0 +1,42 @@ +package keepsake.ourmemory.application.tag; + +import keepsake.ourmemory.application.repository.TagRepository; +import keepsake.ourmemory.application.tag.dto.TagFindResponseDto; +import keepsake.ourmemory.domain.tag.Tag; +import keepsake.ourmemory.domain.tag.TagColor; +import keepsake.ourmemory.domain.tag.TagName; +import keepsake.ourmemory.ui.tag.dto.TagCreateRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Transactional +@RequiredArgsConstructor +@Service +public class TagService { + + private final TagRepository tagRepository; + + public void createTag(TagCreateRequest request) { + TagName tagName = new TagName(request.getTagName()); + TagColor tagColor = new TagColor(request.getTagColor()); + Long memberId = request.getMemberId(); + + Tag tag = new Tag(memberId, tagName, tagColor); + tagRepository.save(tag); + } + + @Transactional(readOnly = true) + public List findTagsByMember(Long memberId) { + List tags = tagRepository.findTagsByMemberIdAndDeletedIsFalse(memberId); + return tags.stream() + .map(TagFindResponseDto::from) + .toList(); + } + + public void deleteTagById(Long tagId) { + tagRepository.deleteById(tagId); + } +} diff --git a/our-memory/src/main/java/keepsake/ourmemory/application/tag/dto/TagFindResponseDto.java b/our-memory/src/main/java/keepsake/ourmemory/application/tag/dto/TagFindResponseDto.java new file mode 100644 index 0000000..f100d0c --- /dev/null +++ b/our-memory/src/main/java/keepsake/ourmemory/application/tag/dto/TagFindResponseDto.java @@ -0,0 +1,22 @@ +package keepsake.ourmemory.application.tag.dto; + +import keepsake.ourmemory.domain.tag.Tag; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class TagFindResponseDto { + + private Long tagId; + private String tagName; + private String tagColor; + + public static TagFindResponseDto from(Tag tag) { + return new TagFindResponseDto( + tag.getId(), + tag.getTagNameValue(), + tag.getTagColorValue() + ); + } +} diff --git a/our-memory/src/main/java/keepsake/ourmemory/domain/tag/Tag.java b/our-memory/src/main/java/keepsake/ourmemory/domain/tag/Tag.java index 699458d..ba85944 100644 --- a/our-memory/src/main/java/keepsake/ourmemory/domain/tag/Tag.java +++ b/our-memory/src/main/java/keepsake/ourmemory/domain/tag/Tag.java @@ -1,20 +1,17 @@ package keepsake.ourmemory.domain.tag; -import jakarta.persistence.Column; -import jakarta.persistence.Embedded; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import keepsake.ourmemory.domain.BaseEntity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; import java.util.Objects; @Entity @Getter +@SQLDelete(sql = "UPDATE tag SET deleted = true WHERE id = ?") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Tag extends BaseEntity { @Id @@ -30,6 +27,9 @@ public class Tag extends BaseEntity { @Embedded private TagColor tagColor; + @Column(nullable = false) + private boolean deleted = false; + public Tag(Long memberId, TagName tagName, TagColor tagColor) { this.memberId = memberId; this.tagName = tagName; diff --git a/our-memory/src/main/java/keepsake/ourmemory/ui/tag/TagController.java b/our-memory/src/main/java/keepsake/ourmemory/ui/tag/TagController.java new file mode 100644 index 0000000..1a8ce72 --- /dev/null +++ b/our-memory/src/main/java/keepsake/ourmemory/ui/tag/TagController.java @@ -0,0 +1,37 @@ +package keepsake.ourmemory.ui.tag; + +import keepsake.ourmemory.application.tag.TagService; +import keepsake.ourmemory.application.tag.dto.TagFindResponseDto; +import keepsake.ourmemory.ui.tag.dto.TagCreateRequest; +import keepsake.ourmemory.ui.tag.dto.TagFindResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +public class TagController { + + private final TagService tagService; + + @PostMapping("/tags") + public ResponseEntity createTag(@RequestBody TagCreateRequest request) { + tagService.createTag(request); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @GetMapping("/tags/members/{memberId}") + public ResponseEntity findTagsByMember(@PathVariable Long memberId) { + List tags = tagService.findTagsByMember(memberId); + return ResponseEntity.ok(new TagFindResponse(tags)); + } + + @DeleteMapping("tags/{tagId}") + public ResponseEntity deleteTag(@PathVariable Long tagId) { + tagService.deleteTagById(tagId); + return ResponseEntity.noContent().build(); + } +} diff --git a/our-memory/src/main/java/keepsake/ourmemory/ui/tag/dto/TagCreateRequest.java b/our-memory/src/main/java/keepsake/ourmemory/ui/tag/dto/TagCreateRequest.java new file mode 100644 index 0000000..782f446 --- /dev/null +++ b/our-memory/src/main/java/keepsake/ourmemory/ui/tag/dto/TagCreateRequest.java @@ -0,0 +1,15 @@ +package keepsake.ourmemory.ui.tag.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class TagCreateRequest { + + private Long memberId; + private String tagName; + private String tagColor; +} diff --git a/our-memory/src/main/java/keepsake/ourmemory/ui/tag/dto/TagFindResponse.java b/our-memory/src/main/java/keepsake/ourmemory/ui/tag/dto/TagFindResponse.java new file mode 100644 index 0000000..0946111 --- /dev/null +++ b/our-memory/src/main/java/keepsake/ourmemory/ui/tag/dto/TagFindResponse.java @@ -0,0 +1,18 @@ +package keepsake.ourmemory.ui.tag.dto; + +import keepsake.ourmemory.application.tag.dto.TagFindResponseDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +import static lombok.AccessLevel.PROTECTED; + +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@Getter +public class TagFindResponse { + + private List tags; +} diff --git a/our-memory/src/test/java/keepsake/ourmemory/application/repository/TagRepositoryTest.java b/our-memory/src/test/java/keepsake/ourmemory/application/repository/TagRepositoryTest.java new file mode 100644 index 0000000..6bfc1b4 --- /dev/null +++ b/our-memory/src/test/java/keepsake/ourmemory/application/repository/TagRepositoryTest.java @@ -0,0 +1,57 @@ +package keepsake.ourmemory.application.repository; + +import keepsake.ourmemory.domain.tag.Tag; +import keepsake.ourmemory.domain.tag.TagColor; +import keepsake.ourmemory.domain.tag.TagName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class TagRepositoryTest { + + @Autowired + private TagRepository tagRepository; + + @Test + void 멤버의_태그를_조회한다() { + // given + Long memberId = 1L; + Tag tag1 = new Tag(memberId, new TagName("tagName1"), new TagColor("tagColor1")); + Tag tag2 = new Tag(memberId, new TagName("tagName2"), new TagColor("tagColor2")); + Tag tag3 = new Tag(2L, new TagName("tagName3"), new TagColor("tagColor3")); + + tagRepository.save(tag1); + tagRepository.save(tag2); + tagRepository.save(tag3); + + // when + List tagsByMember = tagRepository.findTagsByMemberIdAndDeletedIsFalse(memberId); + + // then + assertThat(tagsByMember).usingRecursiveFieldByFieldElementComparatorIgnoringFields("id") + .containsExactlyInAnyOrder(tag1, tag2); + } + + @Test + void 멤버의_태그를_삭제한다() { + // given + Long memberId = 1L; + Tag tag1 = new Tag(memberId, new TagName("tagName1"), new TagColor("tagColor1")); + Tag tag2 = new Tag(memberId, new TagName("tagName2"), new TagColor("tagColor2")); + + Tag savedTag1 = tagRepository.save(tag1); + Tag savedTag2 = tagRepository.save(tag2); + + // when + tagRepository.deleteById(savedTag2.getId()); + List tagsByMember = tagRepository.findTagsByMemberIdAndDeletedIsFalse(memberId); + // then + assertThat(tagsByMember).usingRecursiveFieldByFieldElementComparatorIgnoringFields("id") + .containsExactlyInAnyOrder(savedTag1); + } +} diff --git a/our-memory/src/test/java/keepsake/ourmemory/application/tag/TagServiceTest.java b/our-memory/src/test/java/keepsake/ourmemory/application/tag/TagServiceTest.java new file mode 100644 index 0000000..2d3857e --- /dev/null +++ b/our-memory/src/test/java/keepsake/ourmemory/application/tag/TagServiceTest.java @@ -0,0 +1,41 @@ +package keepsake.ourmemory.application.tag; + +import keepsake.ourmemory.application.repository.TagRepository; +import keepsake.ourmemory.domain.tag.Tag; +import keepsake.ourmemory.domain.tag.TagColor; +import keepsake.ourmemory.domain.tag.TagName; +import keepsake.ourmemory.ui.tag.dto.TagCreateRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class TagServiceTest { + + @InjectMocks + private TagService tagService; + + @Mock + private TagRepository tagRepository; + + @Test + void 태그를_생성한다() { + Tag dummyTag = makeDummyTag(); + given(tagRepository.save(any())) + .willReturn(dummyTag); + + TagCreateRequest request = new TagCreateRequest(1L, "tagName", "tagColor"); + assertDoesNotThrow(() -> tagService.createTag(request)); + } + + private Tag makeDummyTag() { + return new Tag(1L, new TagName("tagNAme"), new TagColor("tagColor")); + } + +} diff --git a/our-memory/src/test/java/keepsake/ourmemory/ui/tag/TagControllerTest.java b/our-memory/src/test/java/keepsake/ourmemory/ui/tag/TagControllerTest.java new file mode 100644 index 0000000..0b6f425 --- /dev/null +++ b/our-memory/src/test/java/keepsake/ourmemory/ui/tag/TagControllerTest.java @@ -0,0 +1,45 @@ +package keepsake.ourmemory.ui.tag; + +import com.fasterxml.jackson.databind.ObjectMapper; +import keepsake.ourmemory.application.tag.TagService; +import keepsake.ourmemory.ui.tag.dto.TagCreateRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(TagController.class) +class TagControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TagService tagService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void 태그_생성_후_201_을_반환한다() throws Exception { + TagCreateRequest request = new TagCreateRequest(1L, "tagName", "tagColor"); + String jsonRequest = objectMapper.writeValueAsString(request); + + mockMvc.perform(post("/tags") + .content(jsonRequest) + .contentType(APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + + @Test + void 멤버의_태그_조회에_성공하면_200을_반환한다() throws Exception { + mockMvc.perform(get("/tags/members/{memberId}", 1L)) + .andExpect(status().isOk()); + } +}