diff --git a/build.gradle b/build.gradle index 1497be9..e753fc4 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'me.paulschwarz:spring-dotenv:4.0.0' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' diff --git a/src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java b/src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java new file mode 100644 index 0000000..026c17a --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java @@ -0,0 +1,70 @@ +package com.web.SearchWeb.tag.controller; + +import com.web.SearchWeb.config.ApiResponse; +import com.web.SearchWeb.tag.controller.dto.MemberTagDto; +import com.web.SearchWeb.tag.domain.MemberTag; +import com.web.SearchWeb.tag.service.MemberTagService; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/tags") +public class MemberTagController { + + private final MemberTagService memberTagService; + + // 생성 + @PostMapping + public ResponseEntity> create(@RequestBody MemberTagDto.CreateRequest req) { + Long tagId = memberTagService.create(req.getOwnerMemberId(), req.getTagName()); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(ApiResponse.success(tagId)); + } + + // 단건 조회 + @GetMapping("/{tagId}") + public ResponseEntity> get(@PathVariable Long tagId) { + MemberTag tag = memberTagService.get(tagId); + return ResponseEntity.ok(ApiResponse.success(MemberTagDto.Response.from(tag))); + } + + // 목록 조회 + @GetMapping("/owners/{ownerMemberId}") + public ResponseEntity>> list(@PathVariable Long ownerMemberId) { + List responses = memberTagService.listByOwner(ownerMemberId) + .stream() + .map(MemberTagDto.Response::from) + .collect(Collectors.toList()); + return ResponseEntity.ok(ApiResponse.success(responses)); + } + + // 수정 + @PutMapping("/{tagId}") + public ResponseEntity> update( + @PathVariable Long tagId, + @RequestBody MemberTagDto.UpdateRequest req + ) { + memberTagService.update(tagId, req.getTagName()); + return ResponseEntity.ok(ApiResponse.success(null)); + } + + // 삭제 + @DeleteMapping("/{tagId}") + public ResponseEntity> delete(@PathVariable Long tagId) { + memberTagService.delete(tagId); + return ResponseEntity.ok(ApiResponse.success(null)); + } +} diff --git a/src/main/java/com/web/SearchWeb/tag/controller/dto/MemberTagDto.java b/src/main/java/com/web/SearchWeb/tag/controller/dto/MemberTagDto.java new file mode 100644 index 0000000..2d51770 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/controller/dto/MemberTagDto.java @@ -0,0 +1,46 @@ +package com.web.SearchWeb.tag.controller.dto; + +import com.web.SearchWeb.tag.domain.MemberTag; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class MemberTagDto { + + @Getter + @NoArgsConstructor + public static class CreateRequest { + private Long ownerMemberId; + private String tagName; + } + + @Getter + @NoArgsConstructor + public static class UpdateRequest { + private String tagName; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Response { + private Long tagId; + private Long ownerMemberId; + private String tagName; + + @Builder + private Response(Long tagId, Long ownerMemberId, String tagName) { + this.tagId = tagId; + this.ownerMemberId = ownerMemberId; + this.tagName = tagName; + } + + public static Response from(MemberTag tag) { + return Response.builder() + .tagId(tag.getMemberTagId()) + .ownerMemberId(tag.getOwnerMemberId()) + .tagName(tag.getTagName()) + .build(); + } + } +} diff --git a/src/main/java/com/web/SearchWeb/tag/dao/MemberTagJpaDao.java b/src/main/java/com/web/SearchWeb/tag/dao/MemberTagJpaDao.java new file mode 100644 index 0000000..c3ae372 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/dao/MemberTagJpaDao.java @@ -0,0 +1,15 @@ +package com.web.SearchWeb.tag.dao; + +import com.web.SearchWeb.tag.domain.MemberTag; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberTagJpaDao extends JpaRepository { + + List findAllByOwnerMemberId(Long ownerMemberId); + + Optional findByOwnerMemberIdAndTagName(Long ownerMemberId, String tagName); + + boolean existsByOwnerMemberIdAndTagName(Long ownerMemberId, String tagName); +} diff --git a/src/main/java/com/web/SearchWeb/tag/domain/MemberTag.java b/src/main/java/com/web/SearchWeb/tag/domain/MemberTag.java new file mode 100644 index 0000000..ec9490b --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/domain/MemberTag.java @@ -0,0 +1,37 @@ +package com.web.SearchWeb.tag.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Entity +@Table(name = "member_tag") +public class MemberTag { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_tag_id") + private Long memberTagId; + @Column(name = "owner_member_id") + private Long ownerMemberId; + @Column(name = "tag_name") + private String tagName; + + public void changeName(String tagName) { + if (tagName == null || tagName.isBlank()) { + throw new IllegalArgumentException("tagName must not be blank"); + } + this.tagName = tagName; + } +} diff --git a/src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java b/src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java new file mode 100644 index 0000000..16b7732 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java @@ -0,0 +1,15 @@ +package com.web.SearchWeb.tag.error; + +import com.web.SearchWeb.config.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum TagErrorCode implements ErrorCode { + Tag_NOT_FOUND(HttpStatus.NOT_FOUND, "T001", "태그를 찾을 수 없습니다."); + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/web/SearchWeb/tag/error/TagException.java b/src/main/java/com/web/SearchWeb/tag/error/TagException.java new file mode 100644 index 0000000..79c7b22 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/error/TagException.java @@ -0,0 +1,19 @@ +package com.web.SearchWeb.tag.error; + +import com.web.SearchWeb.config.BusinessException; +import com.web.SearchWeb.config.ErrorCode; +import lombok.Getter; + +@Getter +public class TagException extends BusinessException { + + public TagException(ErrorCode errorCode) { + super(errorCode); + } + public static class NotFound extends TagException { + public NotFound() { + super(TagErrorCode.Tag_NOT_FOUND); + } + } + +} diff --git a/src/main/java/com/web/SearchWeb/tag/service/MemberTagService.java b/src/main/java/com/web/SearchWeb/tag/service/MemberTagService.java new file mode 100644 index 0000000..0bcca54 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/service/MemberTagService.java @@ -0,0 +1,17 @@ +package com.web.SearchWeb.tag.service; + +import com.web.SearchWeb.tag.domain.MemberTag; +import java.util.List; + +public interface MemberTagService { + + Long create(Long ownerMemberId, String tagName); + + MemberTag get(Long memberTagId); + + List listByOwner(Long ownerMemberId); + + void update(Long memberTagId, String tagName); + + void delete(Long memberTagId); +} diff --git a/src/main/java/com/web/SearchWeb/tag/service/MemberTagServiceImpl.java b/src/main/java/com/web/SearchWeb/tag/service/MemberTagServiceImpl.java new file mode 100644 index 0000000..f805a13 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/tag/service/MemberTagServiceImpl.java @@ -0,0 +1,72 @@ +package com.web.SearchWeb.tag.service; + +import com.web.SearchWeb.tag.dao.MemberTagJpaDao; +import com.web.SearchWeb.tag.domain.MemberTag; +import com.web.SearchWeb.tag.error.TagException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MemberTagServiceImpl implements MemberTagService { + + private final MemberTagJpaDao memberTagJpaDao; + + @Override + @Transactional + public Long create(Long ownerMemberId, String tagName) { + validateTagName(tagName); + + if (memberTagJpaDao.existsByOwnerMemberIdAndTagName(ownerMemberId, tagName)) { + throw new IllegalArgumentException("Tag already exists."); + } + + MemberTag tag = MemberTag.builder() + .ownerMemberId(ownerMemberId) + .tagName(tagName) + .build(); + + return memberTagJpaDao.save(tag).getMemberTagId(); + } + + @Override + @Transactional(readOnly = true) + public MemberTag get(Long memberTagId) { + return memberTagJpaDao.findById(memberTagId) + .orElseThrow(TagException.NotFound::new); + } + + @Override + @Transactional(readOnly = true) + public List listByOwner(Long ownerMemberId) { + return memberTagJpaDao.findAllByOwnerMemberId(ownerMemberId); + } + + @Override + @Transactional + public void update(Long memberTagId, String tagName) { + validateTagName(tagName); + + MemberTag tag = memberTagJpaDao.findById(memberTagId) + .orElseThrow(TagException.NotFound::new); + + tag.changeName(tagName); + } + + @Override + @Transactional + public void delete(Long memberTagId) { + if (!memberTagJpaDao.existsById(memberTagId)) { + return; + } + memberTagJpaDao.deleteById(memberTagId); + } + + private void validateTagName(String tagName) { + if (tagName == null || tagName.isBlank()) { + throw new IllegalArgumentException("tagName must not be blank"); + } + } +}