Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
974d055
:sparkles: Feat: 테마 엔티티 type 구현
hosung-222 Jan 1, 2025
14ab965
:sparkles: Feat: 테마 엔티티 구현 Theme.class
hosung-222 Jan 1, 2025
4dd6408
:sparkles: Feat: 테마 구매 내역 연결 엔티티 구현
hosung-222 Jan 1, 2025
b46dfaa
:sparkles: Feat: 테마 상세 사진 여러장 저장을 위한 엔티티 구현
hosung-222 Jan 1, 2025
33fe500
:sparkles: Feat: 테마 샵 관련 JPA 레파지토리 구현
hosung-222 Jan 1, 2025
759bd31
:sparkles: Feat: 테마 샵 관련 도메인 서비스 구조 생성
hosung-222 Jan 1, 2025
760bf23
:sparkles: Feat: 테마 조회 API 명세
hosung-222 Jan 2, 2025
e2cacfd
:sparkles: Feat: 테마 판매 상태 Enum 추가
hosung-222 Jan 2, 2025
a4c23b8
:sparkles: Feat: 테마 판매 상태와 종류 별 조회 도메인 기능 구현
hosung-222 Jan 2, 2025
b5cedc8
:sparkles: Feat: 조회된 테마를 dto로 변환하는 Converter 구현
hosung-222 Jan 2, 2025
7ae826e
:sparkles: Feat: 타입별 판매중인 테마 조회 API 구현
hosung-222 Jan 2, 2025
7e05b1b
:sparkles: Feat: 테마 상세 조회 API 스펙 명세
hosung-222 Jan 4, 2025
6c6f9f5
:recycle: Recycle: 테마 상세 조회에 보유 여부 추가
hosung-222 Jan 4, 2025
b1ff6a5
:recycle: Recycle: 테마 판매 상태 세분화
hosung-222 Jan 4, 2025
cbf9f54
:sparkles: Feat: 테마 상태, id로 조회하는 도메인 로직 구현
hosung-222 Jan 4, 2025
d13b4c9
:sparkles: Feat: 테마 구매 정보를 반환하는 도메인 로직 구현
hosung-222 Jan 4, 2025
cb2fbce
:sparkles: Feat: 테마 정보 없음에 대한 error code
hosung-222 Jan 4, 2025
a770a82
:sparkles: Feat: DB에 저장되지 않는 테마 소유 여부 필드 추가
hosung-222 Jan 4, 2025
fa86a1c
:sparkles: Feat: 테마 정보와 해당 테마의 소유 여부를 반환하는 비즈니스 로직 구현
hosung-222 Jan 4, 2025
66bca3f
:sparkles: Feat: 테마 정보 -> DTO 컨버터 구현
hosung-222 Jan 4, 2025
13f4cfe
:sparkles: Feat: 테마 상세 정보 (보유여부 포함) 를 조회하는 API 구현
hosung-222 Jan 4, 2025
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,57 @@
package com.namo.spring.application.external.api.shop.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.namo.spring.application.external.api.shop.dto.ThemeResponse;
import com.namo.spring.application.external.api.shop.usecase.ShoppingUseCase;
import com.namo.spring.application.external.global.common.security.authentication.SecurityUserDetails;
import com.namo.spring.core.common.response.ResponseDto;
import com.namo.spring.db.mysql.domains.shop.enums.ThemeType;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;

import lombok.RequiredArgsConstructor;

@Tag(name = "12. 테마샵", description = "테마샵 쇼핑 관련 API ")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v2/shop/themes")
public class ShoppingController {

private final ShoppingUseCase shoppingUseCase;

@GetMapping("")
@Operation(summary = "판매 중인 테마 조회", description = "판매 중인 테마 목록을 조회합니다.")
public ResponseDto<ThemeResponse.ThemeDtoList> getThemes(
@AuthenticationPrincipal SecurityUserDetails memberInfo,
@Parameter(description = "조회할 테마 타입입니다.", example = "background // profile")
@RequestParam("type") String type,
@Parameter(description = "1 부터 시작하는 페이지 번호입니다 (기본값 1)", example = "1")
@RequestParam(value = "page", defaultValue = "1") int page,
@Parameter(description = "한번에 조회할 테마 갯수 입니다 (기본값 6)", example = "6")
@RequestParam(value = "size", defaultValue = "6") int size) {

ThemeType themeType = ThemeType.valueOf(type.toUpperCase());

return ResponseDto.onSuccess(shoppingUseCase
.getThemesByType(themeType, page, size));
}

@GetMapping("{/themeId}")
@Operation(summary = "테마 상세 조회", description = "테마 상세 정보를 조회합니다.")
public ResponseDto<ThemeResponse.ThemeInfoDto> getThemeDetail(
@AuthenticationPrincipal SecurityUserDetails memberInfo,
@Parameter(description = "조회할 테마 ID입니다.", example = "1")
@PathVariable("themeId") Long themeId) {

return ResponseDto.onSuccess(shoppingUseCase
.getThemeDetail(memberInfo.getUserId(), themeId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.namo.spring.application.external.api.shop.converter;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;

import com.namo.spring.application.external.api.shop.dto.ThemeResponse;
import com.namo.spring.db.mysql.domains.shop.entity.Theme;
import com.namo.spring.db.mysql.domains.shop.entity.ThemeDetailImage;

public class ThemeConverter {

public static ThemeResponse.ThemeDtoList toThemeDtoList(Page<Theme> themes) {
List<ThemeResponse.ThemePreviewDto> themeDtoList = themes.stream()
.map(ThemeConverter::toThemeResponseDto)
.collect(Collectors.toList());

return ThemeResponse.ThemeDtoList.builder()
.themes(themeDtoList)
.totalPages(themes.getTotalPages())
.currentPage(themes.getNumber() + 1)
.pageSize(themes.getSize())
.totalItems(themes.getTotalElements())
.build();
}

public static ThemeResponse.ThemePreviewDto toThemeResponseDto(Theme theme) {
return ThemeResponse.ThemePreviewDto.builder()
.id(theme.getId())
.name(theme.getName())
.description(theme.getDescription())
.price(theme.getPrice())
.previewImageUrl(theme.getPreviewImageUrl())
.build();
}

public static ThemeResponse.ThemeInfoDto toThemeInfoDto(Theme theme) {
List<String> detailImages = theme.getDetailImages().stream()
.map(ThemeDetailImage::getImageUrl)
.toList();

return ThemeResponse.ThemeInfoDto.builder()
.id(theme.getId())
.name(theme.getName())
.description(theme.getDescription())
.price(theme.getPrice())
.detailImages(detailImages)
.isOwned(theme.isOwned())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.namo.spring.application.external.api.shop.dto;

import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class ThemeResponse {

@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class ThemeDtoList {
@Schema(description = "총 페이지 수", example = "5")
private int totalPages;
@Schema(description = "현재 페이지 번호", example = "1")
private int currentPage;
@Schema(description = "한 페이지당 항목 수", example = "20")
private int pageSize;
@Schema(description = "전체 항목 수", example = "100")
private long totalItems;
private List<ThemePreviewDto> themes;

}

@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class ThemePreviewDto {
private Long id;
private String name;
private String description;
private Integer price;
private String previewImageUrl;
}

@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class ThemeInfoDto {
private Long id;
private String name;
private String description;
private Integer price;
private String type;
private List<String> detailImages;
private Boolean isOwned; // 테마 보유 여부
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.namo.spring.application.external.api.shop.service;


import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.namo.spring.core.common.code.status.ErrorStatus;
import com.namo.spring.db.mysql.domains.shop.entity.Theme;
import com.namo.spring.db.mysql.domains.shop.enums.ThemeStatus;
import com.namo.spring.db.mysql.domains.shop.enums.ThemeType;
import com.namo.spring.db.mysql.domains.shop.exceptions.ThemeException;
import com.namo.spring.db.mysql.domains.shop.service.MemberThemeService;
import com.namo.spring.db.mysql.domains.shop.service.ThemeService;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ThemeManageService {

private final ThemeService themeService;
private final MemberThemeService memberThemeService;

/**
* 테마 타입에 따른 테마 목록을 반환 합니다.
* @param themeType (프로필, 배경)
* @param page
* @param size
* @return
*/
public Page<Theme> getSellingThemesByType(ThemeType themeType, int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
return themeService.findByTypeAndStatus(themeType, pageable, ThemeStatus.SELLING);
}

/**
* 테마 정보와 소유 여부를 반환 합니다.
* @param memberId
* @param themeId
* @return
*/
public Theme getThemeByIdWithOwnership(Long memberId, Long themeId) {
Theme theme = themeService.findByIdAndStatus(themeId, ThemeStatus.SELLING)
.orElseThrow(() -> new ThemeException(ErrorStatus.NOT_FOUND_THEME));
boolean isOwned = memberThemeService.purchaseInfo(memberId, themeId);
theme.setOwned(isOwned);
return theme;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.namo.spring.application.external.api.shop.usecase;

import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.namo.spring.application.external.api.shop.converter.ThemeConverter;
import com.namo.spring.application.external.api.shop.dto.ThemeResponse;
import com.namo.spring.application.external.api.shop.service.ThemeManageService;
import com.namo.spring.db.mysql.domains.shop.entity.Theme;
import com.namo.spring.db.mysql.domains.shop.enums.ThemeType;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ShoppingUseCase {

private final ThemeManageService themeManageService;

@Transactional(readOnly = true)
public ThemeResponse.ThemeDtoList getThemesByType(ThemeType themeType, int page, int size) {
Page<Theme> themes = themeManageService.getSellingThemesByType(themeType, page, size);
return ThemeConverter.toThemeDtoList(themes);
}

@Transactional(readOnly = true)
public ThemeResponse.ThemeInfoDto getThemeDetail(Long memberId, Long themeId) {
Theme theme = themeManageService.getThemeByIdWithOwnership(memberId, themeId);
return ThemeConverter.toThemeInfoDto(theme);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ public enum ErrorStatus implements BaseErrorCode {

NOT_FOUND_FRIENDSHIP_REQUEST(NOT_FOUND, "친구 요청을 찾을 수 없습니다."),
NOT_FOUND_POINT_REQUEST(NOT_FOUND, "대기중인 포인트 입금 요청을 찾을 수 없습니다."),
NOT_FOUND_THEME(NOT_FOUND, "테마를 찾을 수 없습니다."),

/**
* 404 : 예외 상황 에러
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.namo.spring.db.mysql.domains.shop.entity;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

import org.hibernate.annotations.DynamicInsert;

import com.namo.spring.db.mysql.common.model.BaseTimeEntity;
import com.namo.spring.db.mysql.domains.user.entity.Member;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicInsert
public class MemberTheme extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Member member; // 사용자

@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Theme theme; // 테마

@Column(nullable = false)
private Boolean isActive; // 활성화 여부

@Column(nullable = false)
private LocalDateTime purchasedAt; // 구매 시점

@Builder
public MemberTheme(Member member, Theme theme, Boolean isActive, LocalDateTime purchasedAt) {
this.member = member;
this.theme = theme;
this.isActive = isActive;
this.purchasedAt = LocalDateTime.now();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.namo.spring.db.mysql.domains.shop.entity;

import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Transient;

import org.hibernate.annotations.DynamicInsert;

import com.namo.spring.db.mysql.common.model.BaseTimeEntity;
import com.namo.spring.db.mysql.domains.shop.enums.ThemeStatus;
import com.namo.spring.db.mysql.domains.shop.enums.ThemeType;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicInsert
public class Theme extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name; // 테마 이름

private String description; // 테마 설명

@Column(nullable = false)
private Integer price; // 테마 가격

private String previewImageUrl; // 미리보기 이미지 URL

@OneToMany(mappedBy = "theme", cascade = CascadeType.ALL)
private List<ThemeDetailImage> detailImages;

@Enumerated(EnumType.STRING)
private ThemeType type; // 테마 유형 (배경 테마, 프로필 테마 등)

@Enumerated(EnumType.STRING)
private ThemeStatus status; // 테마 판매 상태

@Setter
@Transient // DB에 저장되지 않는 필드
private boolean isOwned;

@Builder
public Theme(String name, String description, Integer price, String previewImageUrl, ThemeType type) {
this.name = name;
this.description = description;
this.price = price;
this.previewImageUrl = previewImageUrl;
this.type = type;
}


}
Loading
Loading