Skip to content

Commit 13bc306

Browse files
authored
Merge pull request #125 from Pinback-Team/dev
Dev -> Main : 메타데이터 저장 기능
2 parents b45e0b8 + ec008d2 commit 13bc306

17 files changed

Lines changed: 406 additions & 11 deletions

File tree

api/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ dependencies {
1616

1717
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
1818

19+
implementation 'org.jsoup:jsoup:1.17.2'
20+
1921
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
2022
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
2123
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'
2224

2325
implementation 'org.springframework.boot:spring-boot-starter-webflux'
24-
26+
2527
runtimeOnly 'com.mysql:mysql-connector-j'
2628
runtimeOnly 'com.h2database:h2'
2729

api/src/main/java/com/pinback/api/PinbackApiApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.springframework.boot.context.properties.EnableConfigurationProperties;
77
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
88
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
9+
import org.springframework.scheduling.annotation.EnableAsync;
910

1011
import com.pinback.application.config.ProfileImageConfig;
1112

@@ -18,6 +19,7 @@
1819
@EntityScan("com.pinback.domain")
1920
@EnableJpaRepositories("com.pinback.infrastructure")
2021
@EnableJpaAuditing
22+
@EnableAsync
2123
@EnableConfigurationProperties(ProfileImageConfig.class)
2224
public class PinbackApiApplication {
2325
public static void main(String[] args) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.pinback.api.article.controller;
2+
3+
import org.springframework.web.bind.annotation.PostMapping;
4+
import org.springframework.web.bind.annotation.RequestBody;
5+
import org.springframework.web.bind.annotation.RequestMapping;
6+
import org.springframework.web.bind.annotation.RestController;
7+
8+
import com.pinback.api.article.dto.request.ArticleCreateRequest;
9+
import com.pinback.application.article.port.in.CreateArticlePort;
10+
import com.pinback.domain.user.entity.User;
11+
import com.pinback.infrastructure.article.service.ArticleUpdateService;
12+
import com.pinback.shared.annotation.CurrentUser;
13+
import com.pinback.shared.dto.ResponseDto;
14+
15+
import io.swagger.v3.oas.annotations.Operation;
16+
import io.swagger.v3.oas.annotations.Parameter;
17+
import io.swagger.v3.oas.annotations.tags.Tag;
18+
import jakarta.validation.Valid;
19+
import lombok.RequiredArgsConstructor;
20+
import lombok.extern.slf4j.Slf4j;
21+
22+
@Slf4j
23+
@RestController
24+
@RequestMapping("/api/v3/articles")
25+
@RequiredArgsConstructor
26+
@Tag(name = "ArticleV3", description = "아티클 관리 API V3")
27+
public class ArticleControllerV3 {
28+
private final CreateArticlePort createArticlePort;
29+
private final ArticleUpdateService articleMetadataUpdateService;
30+
31+
@Operation(summary = "아티클 생성v3", description = "url에서 썸네일과 제목을 추출하여 새로운 아티클을 생성합니다")
32+
@PostMapping
33+
public ResponseDto<Void> createArticle(
34+
@Parameter(hidden = true) @CurrentUser User user,
35+
@Valid @RequestBody ArticleCreateRequest request
36+
) {
37+
createArticlePort.createArticleV3(user, request.toCommand());
38+
return ResponseDto.ok();
39+
}
40+
41+
@PostMapping("/metadata")
42+
public ResponseDto<Void> migrateMetadata() {
43+
articleMetadataUpdateService.migrateMissingMetadata();
44+
return ResponseDto.ok();
45+
}
46+
}

api/src/main/resources/application.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,6 @@ profile-images:
5151
images:
5252
IMAGE1: ${PROFILE_IMAGE1:}
5353
IMAGE2: ${PROFILE_IMAGE2:}
54-
IMAGE3: ${PROFILE_IMAGE3:}
54+
IMAGE3: ${PROFILE_IMAGE3:}
55+
56+
default-thumbnail: ${DEFAULT_THUMBNAIL:}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.pinback.application.article.dto.response;
2+
3+
public record ArticleMetadataResponse(
4+
String title,
5+
String thumbnailUrl
6+
) {
7+
public static ArticleMetadataResponse of(String title, String thumbnailUrl) {
8+
return new ArticleMetadataResponse(title, thumbnailUrl);
9+
}
10+
}

application/src/main/java/com/pinback/application/article/port/in/CreateArticlePort.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55

66
public interface CreateArticlePort {
77
void createArticle(User user, ArticleCreateCommand command);
8+
9+
void createArticleV3(User user, ArticleCreateCommand command);
810
}

application/src/main/java/com/pinback/application/article/usecase/command/CreateArticleUsecase.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.springframework.transaction.annotation.Transactional;
77

88
import com.pinback.application.article.dto.command.ArticleCreateCommand;
9+
import com.pinback.application.article.dto.response.ArticleMetadataResponse;
910
import com.pinback.application.article.port.in.CreateArticlePort;
1011
import com.pinback.application.article.port.out.ArticleGetServicePort;
1112
import com.pinback.application.article.port.out.ArticleSaveServicePort;
@@ -14,14 +15,17 @@
1415
import com.pinback.application.common.exception.MemoLengthLimitException;
1516
import com.pinback.application.notification.port.in.GetPushSubscriptionPort;
1617
import com.pinback.application.notification.port.in.ScheduleArticleReminderPort;
18+
import com.pinback.application.test.port.out.ArticleMetadataPort;
1719
import com.pinback.domain.article.entity.Article;
1820
import com.pinback.domain.category.entity.Category;
1921
import com.pinback.domain.notification.entity.PushSubscription;
2022
import com.pinback.domain.user.entity.User;
2123
import com.pinback.shared.util.TextUtil;
2224

2325
import lombok.RequiredArgsConstructor;
26+
import lombok.extern.slf4j.Slf4j;
2427

28+
@Slf4j
2529
@Service
2630
@RequiredArgsConstructor
2731
@Transactional
@@ -37,6 +41,8 @@ public class CreateArticleUsecase implements CreateArticlePort {
3741
private final GetPushSubscriptionPort getPushSubscription;
3842
private final ScheduleArticleReminderPort scheduleArticleReminder;
3943

44+
private final ArticleMetadataPort articleMetadataPort;
45+
4046
@Override
4147
public void createArticle(User user, ArticleCreateCommand command) {
4248
validateArticleCreation(user, command);
@@ -48,6 +54,26 @@ public void createArticle(User user, ArticleCreateCommand command) {
4854
scheduleReminderIfNeeded(savedArticle, user, command.remindTime());
4955
}
5056

57+
@Override
58+
public void createArticleV3(User user, ArticleCreateCommand command) {
59+
// 1. url 중복 검증
60+
validateArticleCreation(user, command);
61+
62+
// 2. 메타데이터 가져오기
63+
ArticleMetadataResponse metadata = articleMetadataPort.extractMetadata(command.url());
64+
65+
// 3.0 로그로 찍어서 확인해보기
66+
log.info("title: {}, thumbnail: {}", metadata.title(), metadata.thumbnailUrl());
67+
68+
// 3. 아티클 저장
69+
Category category = getCategoryPort.getCategoryAndUser(command.categoryId(), user);
70+
// s3에 url 있는지 확인 후 있으면 가져오고, 없으면 저장해서 가져오기
71+
Article article = Article.createWithMetaData(command.url(), command.memo(), user, category,
72+
command.remindTime(), metadata.title(), metadata.thumbnailUrl());
73+
Article savedArticle = articleSaveService.save(article);
74+
scheduleReminderIfNeeded(savedArticle, user, command.remindTime());
75+
}
76+
5177
private void validateArticleCreation(User user, ArticleCreateCommand command) {
5278
if (articleGetService.checkExistsByUserAndUrl(user, command.url())) {
5379
throw new ArticleAlreadyExistException();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.pinback.application.test.port.out;
2+
3+
import com.pinback.application.article.dto.response.ArticleMetadataResponse;
4+
5+
public interface ArticleMetadataPort {
6+
ArticleMetadataResponse extractMetadata(String url);
7+
}

domain/src/main/java/com/pinback/domain/article/entity/Article.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ public class Article extends BaseEntity {
6262
@ColumnDefault("false")
6363
private Boolean isReadAfterRemind;
6464

65+
@Column(name = "title")
66+
private String title;
67+
68+
@Column(name = "thumbnail")
69+
private String thumbnail;
70+
6571
public static Article create(String url, String memo, User user, Category category, LocalDateTime remindAt) {
6672
validateMemo(memo);
6773

@@ -76,6 +82,23 @@ public static Article create(String url, String memo, User user, Category catego
7682
.build();
7783
}
7884

85+
public static Article createWithMetaData(String url, String memo, User user, Category category,
86+
LocalDateTime remindAt, String title, String thumbnail) {
87+
validateMemo(memo);
88+
89+
return Article.builder()
90+
.url(url)
91+
.memo(memo)
92+
.user(user)
93+
.category(category)
94+
.isRead(false)
95+
.remindAt(remindAt)
96+
.isReadAfterRemind(false)
97+
.title(title)
98+
.thumbnail(thumbnail)
99+
.build();
100+
}
101+
79102
// 아티클 자체의 비즈니스 로직을 보호하기 위한 유효성 검사
80103
private static void validateMemo(String memo) {
81104
if (memo != null && TextUtil.countGraphemeClusters(memo) > 500) {
@@ -117,4 +140,15 @@ public boolean hasReminder() {
117140
public boolean isReminderDue(LocalDateTime now) {
118141
return hasReminder() && this.remindAt.isBefore(now);
119142
}
143+
144+
public void updateMetadata(String title, String thumbnail) {
145+
if (title != null && !title.isBlank()) {
146+
this.title = title;
147+
}
148+
149+
// 썸네일이 유효할 때만 업데이트 (null 방어 로직)
150+
if (thumbnail != null && !thumbnail.isBlank()) {
151+
this.thumbnail = thumbnail;
152+
}
153+
}
120154
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.pinback.domain.article.exception;
2+
3+
import com.pinback.shared.constant.ExceptionCode;
4+
import com.pinback.shared.exception.ApplicationException;
5+
6+
public class ArticleTitleNotFoundException extends ApplicationException {
7+
public ArticleTitleNotFoundException() {
8+
super(ExceptionCode.ARTICLE_TILE_NOT_FOUND);
9+
}
10+
}

0 commit comments

Comments
 (0)