store update

This commit is contained in:
youbeen 2025-06-16 17:00:32 +09:00
parent 43a423dbc4
commit 68919c1615
11 changed files with 514 additions and 2 deletions

View File

@ -1,5 +1,6 @@
package com.ktds.hi.member.domain; package com.ktds.hi.member.domain;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
@ -13,11 +14,12 @@ import lombok.NoArgsConstructor;
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Table(name = "taste_tag")
public class TasteTag { public class TasteTag {
private Long id; private Long id;
private String tagName; private String tagName;
private TagType tagType; private TagType tagType; //카테고리
private String description; private String description; //매운맛, 짠맛
private Boolean isActive; private Boolean isActive;
} }

View File

@ -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<TopClickedTagResponse> getTopClickedTags() {
log.info("가장 많이 클릭된 상위 5개 태그 조회 시작");
List<Tag> topTags = tagRepositoryPort.findTopClickedTags();
AtomicInteger rank = new AtomicInteger(1);
List<TopClickedTagResponse> 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());
}
}

View File

@ -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<TopClickedTagResponse> getTopClickedTags();
/**
* 태그 클릭 이벤트 처리
*/
void recordTagClick(Long tagId);
}

View File

@ -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<Tag> findAllActiveTags();
/**
* 태그 ID로 태그 조회
*/
Optional<Tag> findTagById(Long tagId);
/**
* 태그명으로 태그 조회
*/
Optional<Tag> findTagByName(String tagName);
/**
* 가장 많이 클릭된 상위 5개 태그 조회
*/
List<Tag> findTopClickedTags();
/**
* 태그 클릭 증가
*/
Tag incrementTagClickCount(Long tagId);
/**
* 태그 저장
*/
Tag saveTag(Tag tag);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<ApiResponse<List<TopClickedTagResponse>>> getTopClickedTags() {
List<TopClickedTagResponse> topTags = tagUseCase.getTopClickedTags();
return ResponseEntity.ok(ApiResponse.success(topTags));
}
/**
* 태그 클릭 이벤트 기록 API
*/
@PostMapping("/{tagId}/click")
@Operation(summary = "태그 클릭 기록", description = "태그 클릭 이벤트를 기록하고 클릭 수를 증가시킵니다.")
public ResponseEntity<ApiResponse<Void>> recordTagClick(@PathVariable Long tagId) {
tagUseCase.recordTagClick(tagId);
return ResponseEntity.ok(ApiResponse.success());
}
}

View File

@ -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;
}

View File

@ -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<Tag> findAllActiveTags() {
log.info("활성화된 모든 태그 조회");
List<TagEntity> entities = tagJpaRepository.findByIsActiveTrueOrderByTagName();
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
@Override
public Optional<Tag> 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<Tag> findTagByName(String tagName) {
log.info("태그명으로 태그 조회: tagName={}", tagName);
return tagJpaRepository.findByTagNameAndIsActiveTrue(tagName)
.map(this::toDomain);
}
@Override
public List<Tag> findTopClickedTags() {
log.info("가장 많이 클릭된 상위 5개 태그 조회");
List<TagEntity> 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();
}
}

View File

@ -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;
}
}

View File

@ -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<TagEntity, Long> {
/**
* 활성화된 태그 목록 조회
*/
List<TagEntity> findByIsActiveTrueOrderByTagName();
/**
* 태그명으로 조회
*/
Optional<TagEntity> findByTagNameAndIsActiveTrue(String tagName);
/**
* 카테고리별 태그 조회
*/
List<TagEntity> findByTagCategoryAndIsActiveTrueOrderByTagName(TagCategory category);
/**
* 클릭 기준 상위 태그 조회
*/
@Query("SELECT t FROM TagEntity t WHERE t.isActive = true ORDER BY t.clickCount DESC")
List<TagEntity> findTopClickedTags(PageRequest pageRequest);
/**
* 클릭 기준 상위 5개 태그 조회
*/
@Query("SELECT t FROM TagEntity t WHERE t.isActive = true ORDER BY t.clickCount DESC")
List<TagEntity> findTop5ByOrderByClickCountDesc(PageRequest pageRequest);
}