Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 태그 생성, 조회, 업데이트 구현 #17

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Tag, Long> {

List<Tag> findTagsByMemberIdAndDeletedIsFalse(Long memberId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 쿼리 메서드로 소프트 딜리트를 이렇게 핸들링할 수 있군여! 따봉

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오우 애초에 저렇게 쓰는법도있군요...

}
Original file line number Diff line number Diff line change
@@ -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<TagFindResponseDto> findTagsByMember(Long memberId) {
List<Tag> tags = tagRepository.findTagsByMemberIdAndDeletedIsFalse(memberId);
return tags.stream()
.map(TagFindResponseDto::from)
.toList();
}

public void deleteTagById(Long tagId) {
tagRepository.deleteById(tagId);
}
Comment on lines +39 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

소프트 딜리트이면 deleteById가 아니라, 해당 태그를 가져와서 isDeleted 부분을 변경 감지를 통해 수정하여, update 쿼리를 날려야할 것 같은데 맞을까요!??!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 아마 Tag에 달려있는 deleteSql 때문에 이렇게 해도 업데이트 쿼리 나가는 걸로 알고있어요 !

레오씨의 대답기다리겠습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag엔티티에 선언한 어노테이션으로 deleted쿼리가 나가면 자동으로 update쿼리가 나갑니다!

}
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기본 생성자를 private으로 막아주는 것도 좋을 것 같아요!


private Long tagId;
private String tagName;
private String tagColor;

public static TagFindResponseDto from(Tag tag) {
return new TagFindResponseDto(
tag.getId(),
tag.getTagNameValue(),
tag.getTagColorValue()
);
}
}
12 changes: 6 additions & 6 deletions our-memory/src/main/java/keepsake/ourmemory/domain/tag/Tag.java
Original file line number Diff line number Diff line change
@@ -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.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인텔리제이 포맷팅 설정이 잘못된 것 같아요!!

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
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/tags에 대한 uri가 반복되는데, 클래스단에 @RequestMapping을 붙여 공통된 부분을 처리하는 것은 어떨까요?!?!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공감합니다

public ResponseEntity<Void> createTag(@RequestBody TagCreateRequest request) {
tagService.createTag(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@GetMapping("/tags/members/{memberId}")
public ResponseEntity<TagFindResponse> findTagsByMember(@PathVariable Long memberId) {
List<TagFindResponseDto> tags = tagService.findTagsByMember(memberId);
return ResponseEntity.ok(new TagFindResponse(tags));
}

@DeleteMapping("tags/{tagId}")
public ResponseEntity<Void> deleteTag(@PathVariable Long tagId) {
tagService.deleteTagById(tagId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<TagFindResponseDto> tags;
}
Original file line number Diff line number Diff line change
@@ -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<Tag> 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<Tag> tagsByMember = tagRepository.findTagsByMemberIdAndDeletedIsFalse(memberId);
// then
assertThat(tagsByMember).usingRecursiveFieldByFieldElementComparatorIgnoringFields("id")
.containsExactlyInAnyOrder(savedTag1);
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}

}
Original file line number Diff line number Diff line change
@@ -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());
}
}