Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Commit

Permalink
[BE] feat: 노션 이미지 기능 구현 (#438)
Browse files Browse the repository at this point in the history
* refactor: Parser -> Block 표현 변및 NotionDivider 추가

* feat: Notion Image로부터 url 파싱 로직 구현

* feat: Image url로부터 이미지 upload 요청을 처리하는 NotionImageUploader 구현

* feat: S3 Client 빈 설정

* feat: NotionImageUploadListener 구현

* feat: S3 업로드를 위한 `S3Uploader` 구현

* feat: Notion에서 다운받은 이미지를 업로드 요청하는 `NotionImageUploader` 구현

* feat: ImageUploader를 이용해 이미지 파일 업로드 구현

* refactor: 사용하지 않는 설정들 제거

* refactor: 의존성 분리를 위한 패키지 분리 - `NotionParser` -> `NotionParseService`
  • Loading branch information
echo724 committed Sep 22, 2023
1 parent c619cf3 commit cfe15e7
Show file tree
Hide file tree
Showing 29 changed files with 336 additions and 122 deletions.
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation platform('software.amazon.awssdk:bom:2.20.56')
implementation 'software.amazon.awssdk:s3'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java:8.0.28'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.donggle.backend.application.client;

import java.util.Optional;

public interface FileHandlerClient {
Optional<String> syncUpload(String url);

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
package org.donggle.backend.domain.parser.notion;
package org.donggle.backend.application.service.parse;

import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;
import org.donggle.backend.application.client.FileHandlerClient;
import org.donggle.backend.domain.parser.notion.FileType;
import org.donggle.backend.domain.parser.notion.NotionBlockType;
import org.donggle.backend.domain.parser.notion.NotionBookmark;
import org.donggle.backend.domain.parser.notion.NotionCallout;
import org.donggle.backend.domain.parser.notion.NotionCodeBlock;
import org.donggle.backend.domain.parser.notion.NotionDefaultBlock;
import org.donggle.backend.domain.parser.notion.NotionDivider;
import org.donggle.backend.domain.parser.notion.NotionHeading;
import org.donggle.backend.domain.parser.notion.NotionImage;
import org.donggle.backend.domain.parser.notion.NotionNormalBlock;
import org.donggle.backend.domain.parser.notion.NotionTodo;
import org.donggle.backend.domain.writing.BlockType;
import org.donggle.backend.domain.writing.block.Block;
import org.donggle.backend.domain.writing.block.CodeBlock;
Expand All @@ -12,52 +23,59 @@
import org.donggle.backend.domain.writing.block.Language;
import org.donggle.backend.domain.writing.block.NormalBlock;
import org.donggle.backend.domain.writing.block.RawText;
import org.springframework.stereotype.Component;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;
import org.springframework.stereotype.Service;

import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

@Component
public class NotionParser {
@Service
public class NotionParseService {
private final FileHandlerClient fileHandlerClient;
private final Map<NotionBlockType, Function<NotionBlockNodeResponse, Optional<Block>>> NOTION_BLOCK_TYPE_MAP = new EnumMap<>(NotionBlockType.class);

public NotionParser() {
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.BOOKMARK, notionBlockNode -> createNormalBlock(notionBlockNode, BookmarkParser.from(notionBlockNode), BlockType.PARAGRAPH));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.CALLOUT, notionBlockNode -> createNormalBlock(notionBlockNode, CalloutParser.from(notionBlockNode), BlockType.BLOCKQUOTE));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.CODE, notionBlockNode -> createCodeBlock(notionBlockNode, CodeBlockParser.from(notionBlockNode)));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.HEADING_1, notionBlockNode -> createNormalBlock(notionBlockNode, HeadingParser.from(notionBlockNode), BlockType.HEADING1));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.HEADING_2, notionBlockNode -> createNormalBlock(notionBlockNode, HeadingParser.from(notionBlockNode), BlockType.HEADING2));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.HEADING_3, notionBlockNode -> createNormalBlock(notionBlockNode, HeadingParser.from(notionBlockNode), BlockType.HEADING3));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.BULLETED_LIST_ITEM, notionBlockNode -> createNormalBlock(notionBlockNode, DefaultBlockParser.from(notionBlockNode), BlockType.UNORDERED_LIST));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.NUMBERED_LIST_ITEM, notionBlockNode -> createNormalBlock(notionBlockNode, DefaultBlockParser.from(notionBlockNode), BlockType.ORDERED_LIST));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.PARAGRAPH, notionBlockNode -> createNormalBlock(notionBlockNode, DefaultBlockParser.from(notionBlockNode), BlockType.PARAGRAPH));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.QUOTE, notionBlockNode -> createNormalBlock(notionBlockNode, DefaultBlockParser.from(notionBlockNode), BlockType.BLOCKQUOTE));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.TO_DO, notionBlockNode -> createTaskListBLock(notionBlockNode, TodoParser.from(notionBlockNode)));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.TOGGLE, notionBlockNode -> createNormalBlock(notionBlockNode, DefaultBlockParser.from(notionBlockNode), BlockType.TOGGLE));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.IMAGE, notionBlockNode -> createImageBlock(notionBlockNode, ImageParser.from(notionBlockNode)));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.DIVIDER, notionBlockNode -> createHorizontalBlock(notionBlockNode));
public NotionParseService(final FileHandlerClient fileHandlerClient) {
this.fileHandlerClient = fileHandlerClient;
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.BOOKMARK, notionBlockNode -> createNormalBlock(notionBlockNode, NotionBookmark.from(notionBlockNode), BlockType.PARAGRAPH));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.CALLOUT, notionBlockNode -> createNormalBlock(notionBlockNode, NotionCallout.from(notionBlockNode), BlockType.BLOCKQUOTE));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.CODE, notionBlockNode -> createCodeBlock(notionBlockNode, NotionCodeBlock.from(notionBlockNode)));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.HEADING_1, notionBlockNode -> createNormalBlock(notionBlockNode, NotionHeading.from(notionBlockNode), BlockType.HEADING1));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.HEADING_2, notionBlockNode -> createNormalBlock(notionBlockNode, NotionHeading.from(notionBlockNode), BlockType.HEADING2));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.HEADING_3, notionBlockNode -> createNormalBlock(notionBlockNode, NotionHeading.from(notionBlockNode), BlockType.HEADING3));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.BULLETED_LIST_ITEM, notionBlockNode -> createNormalBlock(notionBlockNode, NotionDefaultBlock.from(notionBlockNode), BlockType.UNORDERED_LIST));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.NUMBERED_LIST_ITEM, notionBlockNode -> createNormalBlock(notionBlockNode, NotionDefaultBlock.from(notionBlockNode), BlockType.ORDERED_LIST));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.PARAGRAPH, notionBlockNode -> createNormalBlock(notionBlockNode, NotionDefaultBlock.from(notionBlockNode), BlockType.PARAGRAPH));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.QUOTE, notionBlockNode -> createNormalBlock(notionBlockNode, NotionDefaultBlock.from(notionBlockNode), BlockType.BLOCKQUOTE));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.TO_DO, notionBlockNode -> createTaskListBLock(notionBlockNode, NotionTodo.from(notionBlockNode)));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.TOGGLE, notionBlockNode -> createNormalBlock(notionBlockNode, NotionDefaultBlock.from(notionBlockNode), BlockType.TOGGLE));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.IMAGE, notionBlockNode -> createImageBlock(notionBlockNode, NotionImage.from(notionBlockNode)));
NOTION_BLOCK_TYPE_MAP.put(NotionBlockType.DIVIDER, notionBlockNode -> createHorizontalBlock(notionBlockNode, NotionDivider.from(notionBlockNode)));
}

private Optional<Block> createNormalBlock(final NotionBlockNodeResponse notionBlockNodeResponse, final NotionNormalBlockParser blockParser, final BlockType blockType) {
private Optional<Block> createNormalBlock(final NotionBlockNodeResponse notionBlockNodeResponse, final NotionNormalBlock blockParser, final BlockType blockType) {
return Optional.of(new NormalBlock(Depth.from(notionBlockNodeResponse.depth()), blockType, RawText.from(blockParser.parseRawText()), blockParser.parseStyles()));
}

private Optional<Block> createCodeBlock(final NotionBlockNodeResponse notionBlockNodeResponse, final CodeBlockParser blockParser) {
private Optional<Block> createCodeBlock(final NotionBlockNodeResponse notionBlockNodeResponse, final NotionCodeBlock blockParser) {
return Optional.of(new CodeBlock(Depth.from(notionBlockNodeResponse.depth()), BlockType.CODE_BLOCK, RawText.from(blockParser.parseRawText()), Language.from(blockParser.language())));
}

private Optional<Block> createImageBlock(final NotionBlockNodeResponse notionBlockNodeResponse, final ImageParser blockParser) {
private Optional<Block> createImageBlock(final NotionBlockNodeResponse notionBlockNodeResponse, final NotionImage blockParser) {
if (blockParser.fileType() == FileType.FILE) {
return fileHandlerClient.syncUpload(blockParser.url())
.map(uploadedUrl -> new ImageBlock(Depth.from(notionBlockNodeResponse.depth()), BlockType.IMAGE, new ImageUrl(uploadedUrl), new ImageCaption(blockParser.parseCaption())));
}
return Optional.of(new ImageBlock(Depth.from(notionBlockNodeResponse.depth()), BlockType.IMAGE, new ImageUrl(blockParser.url()), new ImageCaption(blockParser.parseCaption())));
}

private Optional<Block> createHorizontalBlock(final NotionBlockNodeResponse notionBlockNodeResponse) {
return Optional.of(new HorizontalRulesBlock(Depth.from(notionBlockNodeResponse.depth()), BlockType.HORIZONTAL_RULES, RawText.from("---")));
private Optional<Block> createHorizontalBlock(final NotionBlockNodeResponse notionBlockNodeResponse, final NotionDivider blockParser) {
return Optional.of(new HorizontalRulesBlock(Depth.from(notionBlockNodeResponse.depth()), BlockType.HORIZONTAL_RULES, RawText.from(blockParser.parseRawText())));
}

private Optional<Block> createTaskListBLock(final NotionBlockNodeResponse notionBlockNodeResponse, final TodoParser blockParser) {
private Optional<Block> createTaskListBLock(final NotionBlockNodeResponse notionBlockNodeResponse, final NotionTodo blockParser) {
if (blockParser.checked()) {
return Optional.of(new NormalBlock(Depth.from(notionBlockNodeResponse.depth()), BlockType.CHECKED_TASK_LIST, RawText.from(blockParser.parseRawText()), blockParser.parseStyles()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import lombok.RequiredArgsConstructor;
import org.donggle.backend.application.service.concurrent.NoConcurrentExecution;
import org.donggle.backend.application.service.parse.NotionParseService;
import org.donggle.backend.application.service.request.MarkdownUploadRequest;
import org.donggle.backend.application.service.request.NotionUploadRequest;
import org.donggle.backend.application.service.request.WritingModifyRequest;
import org.donggle.backend.domain.category.Category;
import org.donggle.backend.domain.member.Member;
import org.donggle.backend.domain.parser.markdown.MarkDownParser;
import org.donggle.backend.domain.parser.notion.NotionParser;
import org.donggle.backend.domain.renderer.html.HtmlRenderer;
import org.donggle.backend.domain.writing.Title;
import org.donggle.backend.domain.writing.Writing;
Expand All @@ -32,8 +32,8 @@ public class WritingFacadeService {
private static final String MD_FORMAT = ".md";

private final WritingService writingService;
private final NotionParseService notionParser;
private final MarkDownParser markDownParser;
private final NotionParser notionParser;
private final HtmlRenderer htmlRenderer;

@NoConcurrentExecution
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.donggle.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class S3ClientConfig {
@Bean
public S3Client s3Client() {
return S3Client.builder()
.credentialsProvider(InstanceProfileCredentialsProvider.create())
.region(Region.AP_NORTHEAST_2)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.donggle.backend.domain.parser.notion;

public enum FileType {
FILE, EXTERNAL
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package org.donggle.backend.domain.parser.notion;

import com.fasterxml.jackson.databind.JsonNode;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;
import org.donggle.backend.domain.writing.Style;
import org.donggle.backend.domain.writing.StyleRange;
import org.donggle.backend.domain.writing.StyleType;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;

import java.util.List;

public record BookmarkParser(List<RichText> caption, String url) implements NotionNormalBlockParser {
public static BookmarkParser from(final NotionBlockNodeResponse blockNode) {
public record NotionBookmark(List<RichText> caption, String url) implements NotionNormalBlock {
public static NotionBookmark from(final NotionBlockNodeResponse blockNode) {
final JsonNode blockProperties = blockNode.getBlockProperties();
final List<RichText> caption = RichText.parseRichTexts(blockProperties, "caption");
final String url = blockProperties.get("url").asText();
return new BookmarkParser(caption, url);
return new NotionBookmark(caption, url);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package org.donggle.backend.domain.parser.notion;

import com.fasterxml.jackson.databind.JsonNode;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;
import org.donggle.backend.domain.writing.Style;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;

import java.util.List;

public record CalloutParser(List<RichText> richTexts, String icon) implements NotionNormalBlockParser {
public static NotionNormalBlockParser from(final NotionBlockNodeResponse blockNode) {
public record NotionCallout(List<RichText> richTexts, String icon) implements NotionNormalBlock {
public static NotionNormalBlock from(final NotionBlockNodeResponse blockNode) {
final JsonNode blockProperties = blockNode.getBlockProperties();
final List<RichText> richTexts = RichText.parseRichTexts(blockProperties, "rich_text");
String icon = "";
if (blockProperties.get("icon").has("emoji")) {
icon = blockProperties.get("icon").get("emoji").asText();
}

return new CalloutParser(richTexts, icon);
return new NotionCallout(richTexts, icon);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import java.util.List;

public record CodeBlockParser(List<RichText> richTexts, String language) {
public static CodeBlockParser from(final NotionBlockNodeResponse blockNode) {
public record NotionCodeBlock(List<RichText> richTexts, String language) {
public static NotionCodeBlock from(final NotionBlockNodeResponse blockNode) {
final JsonNode blockProperties = blockNode.getBlockProperties();
final List<RichText> richTexts = RichText.parseRichTexts(blockProperties, "rich_text");
final String language = blockProperties.get("language").asText();
return new CodeBlockParser(richTexts, language);
return new NotionCodeBlock(richTexts, language);
}

public String parseRawText() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.donggle.backend.domain.parser.notion;

import com.fasterxml.jackson.databind.JsonNode;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;
import org.donggle.backend.domain.writing.Style;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;

import java.util.List;

public record DefaultBlockParser(List<RichText> richTexts) implements NotionNormalBlockParser {
public static NotionNormalBlockParser from(final NotionBlockNodeResponse blockNode) {
public record NotionDefaultBlock(List<RichText> richTexts) implements NotionNormalBlock {
public static NotionNormalBlock from(final NotionBlockNodeResponse blockNode) {
final JsonNode blockProperties = blockNode.getBlockProperties();
final List<RichText> richTexts = RichText.parseRichTexts(blockProperties, "rich_text");
return new DefaultBlockParser(richTexts);
return new NotionDefaultBlock(richTexts);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.donggle.backend.domain.parser.notion;

import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;

public record NotionDivider() {
public static NotionDivider from(final NotionBlockNodeResponse blockNode) {
return new NotionDivider();
}

public String parseRawText() {
return "---";
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package org.donggle.backend.domain.parser.notion;

import com.fasterxml.jackson.databind.JsonNode;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;
import org.donggle.backend.domain.writing.Style;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;

import java.util.List;

public record HeadingParser(List<RichText> richTexts, boolean isToggleable) implements NotionNormalBlockParser {
public static NotionNormalBlockParser from(final NotionBlockNodeResponse blockNode) {
public record NotionHeading(List<RichText> richTexts, boolean isToggleable) implements NotionNormalBlock {
public static NotionNormalBlock from(final NotionBlockNodeResponse blockNode) {
final JsonNode blockProperties = blockNode.getBlockProperties();
final List<RichText> richTexts = RichText.parseRichTexts(blockProperties, "rich_text");
final boolean isToggleable = blockProperties.get("is_toggleable").asBoolean();
return new HeadingParser(richTexts, isToggleable);
return new NotionHeading(richTexts, isToggleable);
}

@Override
Expand Down
Loading

0 comments on commit cfe15e7

Please sign in to comment.