-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 콘텐츠 관리 - 사전 클래스 작성 및 콘텐츠 등록 #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
e7d170d
feat: Content 엔티티 클래스 작성
plzslp 3337fac
feat: Tag, ContentTag 엔티티 클래스 작성
plzslp 42ecbb1
feat: ContentStats 엔티티 클래스 작성
plzslp d7cb0ec
refactor: watcher_count DDL 수정 및 Java 필드 타입 변경
plzslp bcf824c
feat: 콘텐츠 요청/응답 DTO 작성
plzslp 2cd12d4
feat: 콘텐츠 예외처리 추가
plzslp bc1c868
feat: ContentType Json 변환 로직 추가
plzslp 096b91d
refactor: ContentSource ADMIN 소스 추가
plzslp 186e607
refactor: create 정적 팩토리 메서드 추가 및 Content 연관관계 편의 메서드 추가
plzslp 255845b
feat: ContentMapper 구현
plzslp b93ed4c
feat: Content 관련 예외 클래스 추가
plzslp f906c34
build: SwaggerUI 버전 변경
plzslp e80de6d
refactor: contents 스키마 파일 변경
plzslp 3a1524e
feat: 콘텐츠 컨트롤러 추가
plzslp 889f0de
feat: 콘텐츠, 태그, 콘텐츠 스탯 Repository 추가
plzslp 07533d4
refactor: Tag 디렉토리 구조 변경
plzslp d94a467
refactor: Content, ContentStats 생성 로직 수정
plzslp 4b6722b
feat: ContentService 콘텐츠 생성 로직 작성
plzslp 273a319
test: ContentService 테스트 코드 작성
plzslp 5c82758
test: ContentController 테스트 코드 작성
plzslp f3d36a5
refactor: 불필요한 import문 제거
plzslp df9cc57
refactor: 오버라이드 어노테이션 추가
plzslp 29a1b03
refactor: tag 중복 저장 요청 방지 distinct 추가
plzslp 764f4ce
test: 중복 태그 요청 서비스 테스트 코드 작성
plzslp 3c2074c
feat: Tag Exception 클래스 작성
plzslp 79827cf
refactor: 코드래빗 피드백 반영
plzslp 2b87123
test: WatchingSession 테스트 Content 클래스 변경사항 반영
plzslp bc7731e
refactor: 코드래빗 리뷰 2차 반영
plzslp 20daeb0
refactor: 코드래빗 리뷰 3차 반영
plzslp 6556afb
refactor: 코드 리뷰 반영
plzslp bcb55e5
refactor: 새로운 태그가 없을 시 saveAll 호출하지 않도록 수정
plzslp ac21a78
test: saveAll 호출로 인한 테스트 코드 수정
plzslp 786ea0c
build: Swagger UI 버전 변경
plzslp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.codeit.team5.mopl.content.controller; | ||
|
|
||
| import com.codeit.team5.mopl.content.controller.api.ContentApi; | ||
| import com.codeit.team5.mopl.content.dto.request.ContentCreateRequest; | ||
| import com.codeit.team5.mopl.content.dto.response.ContentResponse; | ||
| import com.codeit.team5.mopl.content.service.ContentService; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestPart; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/contents") | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class ContentController implements ContentApi { | ||
|
|
||
| private final ContentService contentService; | ||
|
|
||
| @Override | ||
| @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | ||
| public ResponseEntity<ContentResponse> postContent( | ||
| @Valid @RequestPart("request") ContentCreateRequest request, | ||
| @RequestPart(value = "thumbnail", required = false) MultipartFile thumbnail | ||
| ) { | ||
| log.info("Content Create request: POST /api/contents"); | ||
| ContentResponse response = contentService.create(request, thumbnail); | ||
| return ResponseEntity.status(HttpStatus.CREATED).body(response); | ||
| } | ||
|
plzslp marked this conversation as resolved.
|
||
| } | ||
|
plzslp marked this conversation as resolved.
|
||
53 changes: 53 additions & 0 deletions
53
src/main/java/com/codeit/team5/mopl/content/controller/api/ContentApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.codeit.team5.mopl.content.controller.api; | ||
|
|
||
| import com.codeit.team5.mopl.content.dto.request.ContentCreateRequest; | ||
| import com.codeit.team5.mopl.content.dto.response.ContentResponse; | ||
| import com.codeit.team5.mopl.global.dto.ErrorResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.media.Content; | ||
| import io.swagger.v3.oas.annotations.media.Encoding; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import io.swagger.v3.oas.annotations.parameters.RequestBody; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| @Tag(name = "콘텐츠 관리", description = "콘텐츠 관련 API") | ||
| public interface ContentApi { | ||
|
|
||
| @Schema(name = "ContentCreateMultipartRequest") | ||
| class MultipartRequest { | ||
| @Schema(implementation = ContentCreateRequest.class) | ||
| public ContentCreateRequest request; | ||
|
|
||
| @Schema(type = "string", format = "binary", description = "썸네일 이미지") | ||
| public MultipartFile thumbnail; | ||
| } | ||
|
|
||
| @Operation(summary = "[어드민] 콘텐츠 생성", description = "새로운 콘텐츠를 등록합니다.") | ||
| @ApiResponses({ | ||
| @ApiResponse(responseCode = "201", description = "성공", | ||
| content = @Content(schema = @Schema(implementation = ContentResponse.class))), | ||
| @ApiResponse(responseCode = "400", description = "잘못된 요청", | ||
| content = @Content(schema = @Schema(implementation = ErrorResponse.class))), | ||
| @ApiResponse(responseCode = "401", description = "인증 오류", | ||
| content = @Content(schema = @Schema(implementation = ErrorResponse.class))), | ||
| @ApiResponse(responseCode = "403", description = "권한 오류", | ||
| content = @Content(schema = @Schema(implementation = ErrorResponse.class))), | ||
| @ApiResponse(responseCode = "500", description = "서버 오류", | ||
| content = @Content(schema = @Schema(implementation = ErrorResponse.class))) | ||
| }) | ||
| @RequestBody(content = @Content( | ||
| mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, | ||
| schema = @Schema(implementation = MultipartRequest.class), | ||
| encoding = @Encoding(name = "request", contentType = MediaType.APPLICATION_JSON_VALUE) | ||
| )) | ||
| ResponseEntity<ContentResponse> postContent( | ||
| @Parameter(hidden = true) ContentCreateRequest request, | ||
| @Parameter(hidden = true) MultipartFile thumbnail | ||
| ); | ||
| } |
24 changes: 24 additions & 0 deletions
24
src/main/java/com/codeit/team5/mopl/content/dto/request/ContentCreateRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.codeit.team5.mopl.content.dto.request; | ||
|
|
||
| import com.codeit.team5.mopl.content.entity.ContentType; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotEmpty; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
| import java.util.List; | ||
|
|
||
| public record ContentCreateRequest( | ||
| @NotNull(message = "콘텐츠 타입은 필수입니다.") | ||
| ContentType type, | ||
|
|
||
| @NotBlank(message = "제목은 필수입니다.") | ||
| @Size(max = 500, message = "제목은 500자를 초과할 수 없습니다.") | ||
| String title, | ||
|
|
||
| String description, | ||
|
|
||
| @NotNull(message = "콘텐츠 태그는 필수 입니다.") | ||
| @NotEmpty(message = "콘텐츠 태그 목록은 비어있을 수 없습니다.") | ||
| List<@NotBlank(message = "태그는 공백일 수 없습니다.") String> tags | ||
| ) { | ||
| } |
18 changes: 18 additions & 0 deletions
18
src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.codeit.team5.mopl.content.dto.response; | ||
|
|
||
| import com.codeit.team5.mopl.content.entity.ContentType; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
|
|
||
| public record ContentResponse( | ||
| UUID id, | ||
| ContentType type, | ||
| String title, | ||
| String description, | ||
| String thumbnailUrl, | ||
| List<String> tags, | ||
| double averageRating, | ||
| int reviewCount, | ||
| long watcherCount | ||
| ) { | ||
| } |
109 changes: 83 additions & 26 deletions
109
src/main/java/com/codeit/team5/mopl/content/entity/Content.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,59 +1,116 @@ | ||
| package com.codeit.team5.mopl.content.entity; | ||
|
|
||
| import com.codeit.team5.mopl.content.exception.InvalidContentSourceException; | ||
| import com.codeit.team5.mopl.global.entity.BaseUpdatableEntity; | ||
| import com.codeit.team5.mopl.tag.entity.Tag; | ||
| import jakarta.persistence.CascadeType; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.OneToMany; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import java.time.Instant; | ||
| import java.util.Map; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import lombok.NoArgsConstructor; | ||
| import org.hibernate.annotations.JdbcTypeCode; | ||
| import org.hibernate.type.SqlTypes; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @Entity | ||
| @Table(name = "contents") | ||
| @Table( | ||
| name = "contents", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint( | ||
| name = "uk_contents_source_external_id", | ||
| columnNames = {"source", "external_id"} | ||
| ) | ||
| } | ||
| ) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class Content extends BaseUpdatableEntity { | ||
|
|
||
| @Size(max = 20) | ||
| @NotNull | ||
| @Column(name = "type", nullable = false, length = 20) | ||
| private String type; | ||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, length = 20) | ||
| private ContentType type; | ||
|
|
||
| @Size(max = 500) | ||
| @NotNull | ||
| @Column(name = "title", nullable = false, length = 500) | ||
| @Column(nullable = false, length = 500) | ||
| private String title; | ||
|
|
||
| @Column(name = "description", length = Integer.MAX_VALUE) | ||
| @Column(columnDefinition = "TEXT") | ||
| private String description; | ||
|
|
||
| @Size(max = 512) | ||
| @Column(name = "thumbnail_url", length = 512) | ||
| private String thumbnailUrl; | ||
|
|
||
| @Column(name = "released_at") | ||
| private Instant releasedAt; | ||
|
|
||
| @JdbcTypeCode(SqlTypes.JSON) | ||
| @Column(name = "metadata") | ||
| private Map<String, Object> metadata; | ||
| @Column(columnDefinition = "JSONB") | ||
| private String metadata; | ||
|
|
||
| @Size(max = 20) | ||
| @NotNull | ||
| @Column(name = "source", nullable = false, length = 20) | ||
| private String source; | ||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, length = 20) | ||
| private ContentSource source; | ||
|
|
||
| @Size(max = 100) | ||
| @NotNull | ||
| @Column(name = "external_id", nullable = false, length = 100) | ||
| private String externalId; | ||
|
|
||
| @NotNull | ||
| @Column(name = "updated_at", nullable = false) | ||
| private Instant updatedAt; | ||
| @OneToMany(mappedBy = "content", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) | ||
| private List<ContentTag> contentTags = new ArrayList<>(); | ||
|
|
||
| public static Content createByAdmin(ContentType type, String title, String description) { | ||
| Content content = new Content(); | ||
| content.type = type; | ||
| content.title = title; | ||
| content.description = description; | ||
| content.source = ContentSource.ADMIN; | ||
| content.externalId = UUID.randomUUID().toString(); | ||
| return content; | ||
| } | ||
|
|
||
| public static Content createByExternalSource(ContentType type, String title, String description, | ||
| ContentSource source, String externalId, Instant releasedAt, String metadata) { | ||
| if (source == null || source == ContentSource.ADMIN) { | ||
| throw new InvalidContentSourceException("외부 소스 생성에 ADMIN 소스는 사용할 수 없습니다."); | ||
| } | ||
| if (externalId == null || externalId.isBlank()) { | ||
| throw new InvalidContentSourceException("외부 소스 콘텐츠는 externalId가 필수입니다."); | ||
| } | ||
| Content content = new Content(); | ||
| content.type = type; | ||
| content.title = title; | ||
| content.description = description; | ||
| content.source = source; | ||
| content.externalId = externalId; | ||
| content.releasedAt = releasedAt; | ||
| content.metadata = metadata; | ||
| return content; | ||
|
plzslp marked this conversation as resolved.
|
||
| } | ||
|
|
||
| public void addTag(Tag tag) { | ||
| if (tag == null) { | ||
| return; | ||
| } | ||
|
|
||
| boolean alreadyExists = this.contentTags.stream() | ||
| .anyMatch(ct -> { | ||
| Tag existing = ct.getTag(); | ||
| if (existing.getId() != null && tag.getId() != null) { | ||
| return existing.getId().equals(tag.getId()); | ||
| } | ||
| return existing.getName().equals(tag.getName()); | ||
| }); | ||
|
|
||
| if (!alreadyExists) { | ||
| contentTags.add(ContentTag.create(this, tag)); | ||
| } | ||
| } | ||
|
plzslp marked this conversation as resolved.
|
||
| } | ||
7 changes: 7 additions & 0 deletions
7
src/main/java/com/codeit/team5/mopl/content/entity/ContentSource.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.codeit.team5.mopl.content.entity; | ||
|
|
||
| public enum ContentSource { | ||
| TMDB, | ||
| SPORTS_DB, | ||
| ADMIN | ||
| } |
58 changes: 58 additions & 0 deletions
58
src/main/java/com/codeit/team5/mopl/content/entity/ContentStats.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package com.codeit.team5.mopl.content.entity; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.MapsId; | ||
| import jakarta.persistence.OneToOne; | ||
| import jakarta.persistence.PreUpdate; | ||
| import jakarta.persistence.Table; | ||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table(name = "content_stats") | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class ContentStats { | ||
|
|
||
| @Id | ||
| private UUID contentId; | ||
|
|
||
| @MapsId | ||
|
plzslp marked this conversation as resolved.
|
||
| @OneToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "content_id") | ||
| private Content content; | ||
|
|
||
| @Column(nullable = false) | ||
| private int reviewCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private double ratingSum; | ||
|
plzslp marked this conversation as resolved.
|
||
|
|
||
| @Column(nullable = false) | ||
| private long watcherCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private Instant updatedAt; | ||
|
plzslp marked this conversation as resolved.
|
||
|
|
||
| @PreUpdate | ||
| protected void onUpdate() { | ||
| this.updatedAt = Instant.now(); | ||
| } | ||
|
|
||
| public static ContentStats create(Content content) { | ||
| ContentStats stats = new ContentStats(); | ||
| stats.content = content; | ||
| stats.reviewCount = 0; | ||
| stats.ratingSum = 0.0; | ||
| stats.watcherCount = 0; | ||
| stats.updatedAt = Instant.now(); | ||
| return stats; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.