From 68919c161571b8a0e9bd8312843d328b6ba3860c Mon Sep 17 00:00:00 2001 From: youbeen Date: Mon, 16 Jun 2025 17:00:32 +0900 Subject: [PATCH] store update --- .../com/ktds/hi/member/domain/TasteTag.java | 6 +- .../ktds/hi/store/biz/service/TagService.java | 64 +++++++++ .../hi/store/biz/usecase/in/TagUseCase.java | 25 ++++ .../biz/usecase/out/TagRepositoryPort.java | 47 +++++++ .../java/com/ktds/hi/store/domain/Tag.java | 46 +++++++ .../com/ktds/hi/store/domain/TagCategory.java | 29 ++++ .../store/infra/controller/TagController.java | 52 ++++++++ .../dto/response/TopClickedTagResponse.java | 22 ++++ .../infra/gateway/TagRepositoryAdapter.java | 124 ++++++++++++++++++ .../store/infra/gateway/entity/TagEntity.java | 52 ++++++++ .../gateway/repository/TagJpaRepository.java | 49 +++++++ 11 files changed, 514 insertions(+), 2 deletions(-) create mode 100644 store/src/main/java/com/ktds/hi/store/biz/service/TagService.java create mode 100644 store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java create mode 100644 store/src/main/java/com/ktds/hi/store/biz/usecase/out/TagRepositoryPort.java create mode 100644 store/src/main/java/com/ktds/hi/store/domain/Tag.java create mode 100644 store/src/main/java/com/ktds/hi/store/domain/TagCategory.java create mode 100644 store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java create mode 100644 store/src/main/java/com/ktds/hi/store/infra/dto/response/TopClickedTagResponse.java create mode 100644 store/src/main/java/com/ktds/hi/store/infra/gateway/TagRepositoryAdapter.java create mode 100644 store/src/main/java/com/ktds/hi/store/infra/gateway/entity/TagEntity.java create mode 100644 store/src/main/java/com/ktds/hi/store/infra/gateway/repository/TagJpaRepository.java diff --git a/member/src/main/java/com/ktds/hi/member/domain/TasteTag.java b/member/src/main/java/com/ktds/hi/member/domain/TasteTag.java index faacc90..5929e24 100644 --- a/member/src/main/java/com/ktds/hi/member/domain/TasteTag.java +++ b/member/src/main/java/com/ktds/hi/member/domain/TasteTag.java @@ -1,5 +1,6 @@ package com.ktds.hi.member.domain; +import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -13,11 +14,12 @@ import lombok.NoArgsConstructor; @Builder @NoArgsConstructor @AllArgsConstructor +@Table(name = "taste_tag") public class TasteTag { private Long id; private String tagName; - private TagType tagType; - private String description; + private TagType tagType; //카테고리 + private String description; //매운맛, 짠맛 private Boolean isActive; } diff --git a/store/src/main/java/com/ktds/hi/store/biz/service/TagService.java b/store/src/main/java/com/ktds/hi/store/biz/service/TagService.java new file mode 100644 index 0000000..9fcf1a9 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/biz/service/TagService.java @@ -0,0 +1,64 @@ +package com.ktds.hi.store.biz.service; + +import com.ktds.hi.store.biz.usecase.in.TagUseCase; +import com.ktds.hi.store.biz.usecase.out.TagRepositoryPort; +import com.ktds.hi.store.domain.Tag; +import com.ktds.hi.store.infra.dto.response.TopClickedTagResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * 태그 서비스 클래스 + * 태그 관련 비즈니스 로직을 구현 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class TagService implements TagUseCase { + + private final TagRepositoryPort tagRepositoryPort; + + @Override + public List getTopClickedTags() { + log.info("가장 많이 클릭된 상위 5개 태그 조회 시작"); + + List topTags = tagRepositoryPort.findTopClickedTags(); + + AtomicInteger rank = new AtomicInteger(1); + + List responses = topTags.stream() + .map(tag -> TopClickedTagResponse.builder() + .tagId(tag.getId()) + .tagName(tag.getTagName()) + .tagCategory(tag.getTagCategory().name()) + .tagColor(tag.getTagColor()) + .clickCount(tag.getClickCount()) + .rank(rank.getAndIncrement()) + .build()) + .collect(Collectors.toList()); + + log.info("가장 많이 클릭된 상위 5개 태그 조회 완료: count={}", responses.size()); + return responses; + } + + @Override + @Transactional + public void recordTagClick(Long tagId) { + log.info("태그 클릭 이벤트 처리 시작: tagId={}", tagId); + + Tag updatedTag = tagRepositoryPort.incrementTagClickCount(tagId); + + log.info("태그 클릭 수 증가 완료: tagId={}, clickCount={}", + tagId, updatedTag.getClickCount()); + } +} diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java new file mode 100644 index 0000000..ac15128 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java @@ -0,0 +1,25 @@ +package com.ktds.hi.store.biz.usecase.in; + +import com.ktds.hi.store.infra.dto.response.TopClickedTagResponse; + +import java.util.List; + +/** + * 태그 유스케이스 인터페이스 + * 태그 관련 비즈니스 로직을 정의 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +public interface TagUseCase { + + /** + * 가장 많이 클릭된 상위 5개 태그 조회 + */ + List getTopClickedTags(); + + /** + * 태그 클릭 이벤트 처리 + */ + void recordTagClick(Long tagId); +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/TagRepositoryPort.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/TagRepositoryPort.java new file mode 100644 index 0000000..c511d33 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/TagRepositoryPort.java @@ -0,0 +1,47 @@ +package com.ktds.hi.store.biz.usecase.out; + +// store/src/main/java/com/ktds/hi/store/biz/usecase/out/TagRepositoryPort.java +import com.ktds.hi.store.domain.Tag; + +import java.util.List; +import java.util.Optional; + +/** + * 태그 리포지토리 포트 인터페이스 + * 태그 데이터 영속성 기능을 정의 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +public interface TagRepositoryPort { + + /** + * 활성화된 모든 태그 조회 + */ + List findAllActiveTags(); + + /** + * 태그 ID로 태그 조회 + */ + Optional findTagById(Long tagId); + + /** + * 태그명으로 태그 조회 + */ + Optional findTagByName(String tagName); + + /** + * 가장 많이 클릭된 상위 5개 태그 조회 + */ + List findTopClickedTags(); + + /** + * 태그 클릭 수 증가 + */ + Tag incrementTagClickCount(Long tagId); + + /** + * 태그 저장 + */ + Tag saveTag(Tag tag); +} diff --git a/store/src/main/java/com/ktds/hi/store/domain/Tag.java b/store/src/main/java/com/ktds/hi/store/domain/Tag.java new file mode 100644 index 0000000..ff615b8 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/domain/Tag.java @@ -0,0 +1,46 @@ +package com.ktds.hi.store.domain; + + +import lombok.Builder; +import lombok.Getter; + +/** + * 태그 도메인 클래스 + * 매장 태그 정보를 나타냄 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Getter +@Builder +public class Tag { + private Long id; + private String tagName; + private TagCategory tagCategory; + private String tagColor; + private Integer sortOrder; + private Boolean isActive; + private Long clickCount; + + /** + * 클릭 수 증가 + */ + public Tag incrementClickCount() { + return Tag.builder() + .id(this.id) + .tagName(this.tagName) + .tagCategory(this.tagCategory) + .tagColor(this.tagColor) + .sortOrder(this.sortOrder) + .isActive(this.isActive) + .clickCount(this.clickCount != null ? this.clickCount + 1 : 1L) + .build(); + } + + /** + * 활성 상태 확인 + */ + public boolean isActive() { + return Boolean.TRUE.equals(this.isActive); + } +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/domain/TagCategory.java b/store/src/main/java/com/ktds/hi/store/domain/TagCategory.java new file mode 100644 index 0000000..4edbfaa --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/domain/TagCategory.java @@ -0,0 +1,29 @@ +package com.ktds.hi.store.domain; + + +/** + * 태그 카테고리 열거형 클래스 + * 매장 태그의 분류를 정의 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +public enum TagCategory { + TASTE("맛"), // 매운맛, 단맛, 짠맛 등 + ATMOSPHERE("분위기"), // 깨끗한, 혼밥, 데이트 등 + ALLERGY("알러지"), // 유제품, 견과류, 갑각류 등 + SERVICE("서비스"), // 빠른서비스, 친절한, 조용한 등 + PRICE("가격대"), // 저렴한, 합리적인, 가성비 등 + FOOD_TYPE("음식 종류"), // 한식, 중식, 일식 등 + HEALTH("건강 정보"); // 저염, 저당, 글루텐프리 등 + + private final String description; + + TagCategory(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java b/store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java new file mode 100644 index 0000000..ad467ba --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java @@ -0,0 +1,52 @@ +package com.ktds.hi.store.infra.controller; + +import com.ktds.hi.store.biz.usecase.in.TagUseCase; +import com.ktds.hi.store.infra.dto.response.TopClickedTagResponse; +import com.ktds.hi.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 태그 컨트롤러 클래스 + * 태그 관련 API 엔드포인트를 제공 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@RestController +@RequestMapping("/api/stores/tags") +@RequiredArgsConstructor +@Tag(name = "태그 관리 API", description = "매장 태그 조회 및 통계 관련 API") +public class TagController { + + private final TagUseCase tagUseCase; + + /** + * 가장 많이 클릭된 상위 5개 태그 조회 API + */ + @GetMapping("/top-clicked") + @Operation(summary = "인기 태그 조회", description = "가장 많이 클릭된 상위 5개 태그를 조회합니다.") + public ResponseEntity>> getTopClickedTags() { + + List topTags = tagUseCase.getTopClickedTags(); + + return ResponseEntity.ok(ApiResponse.success(topTags)); + } + + /** + * 태그 클릭 이벤트 기록 API + */ + @PostMapping("/{tagId}/click") + @Operation(summary = "태그 클릭 기록", description = "태그 클릭 이벤트를 기록하고 클릭 수를 증가시킵니다.") + public ResponseEntity> recordTagClick(@PathVariable Long tagId) { + + tagUseCase.recordTagClick(tagId); + + return ResponseEntity.ok(ApiResponse.success()); + } +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/response/TopClickedTagResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/response/TopClickedTagResponse.java new file mode 100644 index 0000000..8105be6 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/response/TopClickedTagResponse.java @@ -0,0 +1,22 @@ +package com.ktds.hi.store.infra.dto.response; + +import lombok.Builder; +import lombok.Getter; + +/** + * 인기 태그 응답 DTO 클래스 + * 가장 많이 클릭된 태그 정보를 전달 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Getter +@Builder +public class TopClickedTagResponse { + private Long tagId; + private String tagName; + private String tagCategory; + private String tagColor; + private Long clickCount; + private Integer rank; +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/gateway/TagRepositoryAdapter.java b/store/src/main/java/com/ktds/hi/store/infra/gateway/TagRepositoryAdapter.java new file mode 100644 index 0000000..d81799d --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/infra/gateway/TagRepositoryAdapter.java @@ -0,0 +1,124 @@ +package com.ktds.hi.store.infra.gateway; + +import com.ktds.hi.store.biz.usecase.out.TagRepositoryPort; +import com.ktds.hi.store.domain.Tag; +import com.ktds.hi.store.domain.TagCategory; +import com.ktds.hi.store.infra.gateway.entity.TagEntity; +import com.ktds.hi.store.infra.gateway.repository.TagJpaRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 태그 리포지토리 어댑터 클래스 + * TagRepositoryPort를 구현하여 태그 데이터 액세스 기능을 제공 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class TagRepositoryAdapter implements TagRepositoryPort { + + private final TagJpaRepository tagJpaRepository; + + @Override + public List findAllActiveTags() { + log.info("활성화된 모든 태그 조회"); + + List entities = tagJpaRepository.findByIsActiveTrueOrderByTagName(); + + return entities.stream() + .map(this::toDomain) + .collect(Collectors.toList()); + } + + @Override + public Optional findTagById(Long tagId) { + log.info("태그 ID로 태그 조회: tagId={}", tagId); + + return tagJpaRepository.findById(tagId) + .filter(entity -> Boolean.TRUE.equals(entity.getIsActive())) + .map(this::toDomain); + } + + @Override + public Optional findTagByName(String tagName) { + log.info("태그명으로 태그 조회: tagName={}", tagName); + + return tagJpaRepository.findByTagNameAndIsActiveTrue(tagName) + .map(this::toDomain); + } + + @Override + public List findTopClickedTags() { + log.info("가장 많이 클릭된 상위 5개 태그 조회"); + + List entities = tagJpaRepository.findTop5ByOrderByClickCountDesc( + PageRequest.of(0, 5) + ); + + return entities.stream() + .map(this::toDomain) + .collect(Collectors.toList()); + } + + @Override + public Tag incrementTagClickCount(Long tagId) { + log.info("태그 클릭 수 증가: tagId={}", tagId); + + TagEntity entity = tagJpaRepository.findById(tagId) + .orElseThrow(() -> new IllegalArgumentException("태그를 찾을 수 없습니다: " + tagId)); + + entity.incrementClickCount(); + TagEntity saved = tagJpaRepository.save(entity); + + return toDomain(saved); + } + + @Override + public Tag saveTag(Tag tag) { + log.info("태그 저장: tagName={}", tag.getTagName()); + + TagEntity entity = toEntity(tag); + TagEntity saved = tagJpaRepository.save(entity); + + return toDomain(saved); + } + + /** + * 엔티티를 도메인으로 변환 + */ + private Tag toDomain(TagEntity entity) { + return Tag.builder() + .id(entity.getId()) + .tagName(entity.getTagName()) + .tagCategory(entity.getTagCategory()) + .tagColor(entity.getTagColor()) + .sortOrder(entity.getSortOrder()) + .isActive(entity.getIsActive()) + .clickCount(entity.getClickCount()) + .build(); + } + + /** + * 도메인을 엔티티로 변환 + */ + private TagEntity toEntity(Tag domain) { + return TagEntity.builder() + .id(domain.getId()) + .tagName(domain.getTagName()) + .tagCategory(domain.getTagCategory()) + .tagColor(domain.getTagColor()) + .sortOrder(domain.getSortOrder()) + .isActive(domain.getIsActive()) + .clickCount(domain.getClickCount()) + .build(); + } +} diff --git a/store/src/main/java/com/ktds/hi/store/infra/gateway/entity/TagEntity.java b/store/src/main/java/com/ktds/hi/store/infra/gateway/entity/TagEntity.java new file mode 100644 index 0000000..3e48973 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/infra/gateway/entity/TagEntity.java @@ -0,0 +1,52 @@ +package com.ktds.hi.store.infra.gateway.entity; + +import jakarta.persistence.*; +import lombok.*; +import com.ktds.hi.store.domain.TagCategory; +/** + * 태그 엔티티 클래스 + * 매장 태그 정보를 저장 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Entity +@Table(name = "tags") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TagEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tag_name", nullable = false, length = 50) + private String tagName; // 매운맛, 깨끗한, 유제품 등 + + @Enumerated(EnumType.STRING) + @Column(name = "tag_category", nullable = false) + private TagCategory tagCategory; // TASTE, ATMOSPHERE, ALLERGY 등 + + @Column(name = "tag_color", length = 7) + private String tagColor; // #FF5722 + + @Column(name = "sort_order") + private Integer sortOrder; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @Column(name = "click_count") + @Builder.Default + private Long clickCount = 0L; + + /** + * 클릭 수 증가 + */ + public void incrementClickCount() { + this.clickCount = this.clickCount != null ? this.clickCount + 1 : 1L; + } +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/gateway/repository/TagJpaRepository.java b/store/src/main/java/com/ktds/hi/store/infra/gateway/repository/TagJpaRepository.java new file mode 100644 index 0000000..3bed601 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/infra/gateway/repository/TagJpaRepository.java @@ -0,0 +1,49 @@ +package com.ktds.hi.store.infra.gateway.repository; + +import com.ktds.hi.store.domain.TagCategory; +import com.ktds.hi.store.infra.gateway.entity.TagEntity; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 태그 JPA 리포지토리 인터페이스 + * 태그 데이터의 CRUD 작업을 담당 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Repository +public interface TagJpaRepository extends JpaRepository { + + /** + * 활성화된 태그 목록 조회 + */ + List findByIsActiveTrueOrderByTagName(); + + /** + * 태그명으로 조회 + */ + Optional findByTagNameAndIsActiveTrue(String tagName); + + /** + * 카테고리별 태그 조회 + */ + List findByTagCategoryAndIsActiveTrueOrderByTagName(TagCategory category); + + /** + * 클릭 수 기준 상위 태그 조회 + */ + @Query("SELECT t FROM TagEntity t WHERE t.isActive = true ORDER BY t.clickCount DESC") + List findTopClickedTags(PageRequest pageRequest); + + /** + * 클릭 수 기준 상위 5개 태그 조회 + */ + @Query("SELECT t FROM TagEntity t WHERE t.isActive = true ORDER BY t.clickCount DESC") + List findTop5ByOrderByClickCountDesc(PageRequest pageRequest); +}