From d73d48cba77a26a68e4874545c6609ba8204b931 Mon Sep 17 00:00:00 2001 From: "DESKTOP-JH9P8S7\\taehun" Date: Fri, 13 Feb 2026 01:42:31 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore/SW-50=20:=20build=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jpa 중복 의존성 삭제 --- build.gradle | 1 - 1 file changed, 1 deletion(-) 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' From cb76270f1532c41b05b15015d6c3cac8e5c18bcf Mon Sep 17 00:00:00 2001 From: "DESKTOP-JH9P8S7\\taehun" Date: Fri, 13 Feb 2026 02:29:14 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20SW-50=20Tag=20CRUD=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 도메인, dao(repository), 서비스, 컨트롤러 작성 - dao(repository) : JPA사용하여 구현 - 서비스 : 인터페이스 설계 후, 구현체 작성 - 컨트롤러 : 공통 응답 포맷 만들어서 resposneEntity에 담아서 응답 --- .../tag/controller/MemberTagController.java | 70 ++++++++++++++++++ .../SearchWeb/tag/dao/MemberTagJpaDao.java | 15 ++++ .../web/SearchWeb/tag/domain/MemberTag.java | 37 ++++++++++ .../tag/service/MemberTagService.java | 17 +++++ .../tag/service/MemberTagServiceImpl.java | 72 +++++++++++++++++++ 5 files changed, 211 insertions(+) create mode 100644 src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java create mode 100644 src/main/java/com/web/SearchWeb/tag/dao/MemberTagJpaDao.java create mode 100644 src/main/java/com/web/SearchWeb/tag/domain/MemberTag.java create mode 100644 src/main/java/com/web/SearchWeb/tag/service/MemberTagService.java create mode 100644 src/main/java/com/web/SearchWeb/tag/service/MemberTagServiceImpl.java 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/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/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"); + } + } +} From f538bfebb830badfdf0dd64ebcc71dbcf5366263 Mon Sep 17 00:00:00 2001 From: "DESKTOP-JH9P8S7\\taehun" Date: Fri, 13 Feb 2026 02:31:39 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20SW-51=20Tag=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 커스텀 예외와 에러코드 추가 --- .../web/SearchWeb/tag/error/TagErrorCode.java | 15 +++++++++++++++ .../web/SearchWeb/tag/error/TagException.java | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java create mode 100644 src/main/java/com/web/SearchWeb/tag/error/TagException.java 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); + } + } + +} From 8384887d80e1978a01bba5e009103f59116436b9 Mon Sep 17 00:00:00 2001 From: "DESKTOP-JH9P8S7\\taehun" Date: Fri, 13 Feb 2026 02:34:39 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat=20:=20SW-52=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=8F=AC=EB=A7=B7=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - controller 에 responseEntity로 응답 - dto 를 inner class로 작성 --- .../tag/controller/dto/MemberTagDto.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/com/web/SearchWeb/tag/controller/dto/MemberTagDto.java 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(); + } + } +}