Project

블로그 만들기(3) 어려웠던 점

코딩이 어려운 사람 2024. 7. 16. 15:31

이번에 블로그 만들기를 진행하면서 가장 어려웠던 점은 역시 블로그의 핵심기능인 포스트 작성이었다.

일단 포스트 작성에 이미지도 들어가야 하고 그렇게 들어간 이미지를 썸네일로 사용하기도 하고 포스트를 작성하기 위해 에디터도 필요하고 핵심 기능이다 보니 좋아요, 댓글 유저 등 여러 엔티티와 얽혀있어서 굉장히 까다로웠다. 지난 포스트에서 말했듯이 여기서 레스트 컨트롤러와 컨트롤러 두 개를 혼용해 버려서 2배로 헷갈리기는 기분이었다. 

우선 코드를 보자

@PostMapping("/create")
    public String createPost(@PathVariable("blogId") Long blogId,
                             @ModelAttribute PostRequestDto postRequestDto,
                             @AuthenticationPrincipal UserDetails userDetails,
                             HttpSession session,
                             RedirectAttributes redirectAttributes) {

        try {
            if (userDetails == null) {
                redirectAttributes.addFlashAttribute("error", "로그인 후 이용 가능합니다.");
                return "redirect:/api/login";
            }
            User user = userRepository.findByLoginId(userDetails.getUsername())
                    .orElseThrow(() -> new UsernameNotFoundException("해당 유저를 찾을 수 없습니다."));
            postRequestDto.setBlogId(blogId);
            String content = commonUtil.markdownToHtml(postRequestDto.getContent());
            postRequestDto.setContent(content);
            Post post = postService.createPost(postRequestDto, user);

            // 세션에서 이미지 URL을 가져와서 PostImage로 저장
            List<String> imageUrls = (List<String>) session.getAttribute("imageUrls");
            if (imageUrls != null) {
                for (String imageUrl : imageUrls) {
                    PostImage postImage = new PostImage();
                    postImage.setPost(post);
                    postImage.setUrl(imageUrl);
                    postImageService.save(postImage);
                }
                session.removeAttribute("imageUrls"); // 세션에서 이미지 URL 제거
            }

            return "redirect:/api/blogs/" + blogId;
        } catch (Exception e) {
            log.error("게시글 작성 중 오류 발생", e);
            redirectAttributes.addFlashAttribute("error", "게시글 작성 중 오류가 발생했습니다.");
            return "redirect:/api/blogs/" + blogId + "/create";
        }
    }

여기서 까다로웠던 점은 일단 게시글 작성 자체가 아니라 이미지 업로드와 같이 진행되야 하기에 어려웠던 것이었다. 에디터를 오픈소스인 ToastUi에디터를 이용했기 때문에 게시글이 마크다운형식으로 작성되는데 업로드된 이미지는 db에서 관리될 때 포스트에 속해 있어야 하기에 포스트가 완성된 후에 이미지가 저장되어야 했다 하지만 이미지가 자꾸 먼저 저장되는 바람에 db에서 오류가 떴는데 이걸 해결하기 위해서 이미지 업로드 시 이미지의 url을 우선 세션에 잠시 맡겨놓고 포스트 작성 후 세션에 맡겨놓은 이미지 url을 이용해 db에 저장하는 방식을 사용했다. 분명 이것보다 더 좋은 방법이 있을 거 같은데 아직까지도 그 방법은 생각이 안 나긴 한다.. 아래 코드에서 세션에 url을 맡기는 걸 확인할 수 있을 것이다. 

@ResponseBody
    @PostMapping("/uploadImage")
    public ResponseEntity<Map<String, String>> uploadImage(@RequestParam("file") MultipartFile file,
                                                           @AuthenticationPrincipal UserDetails userDetails, HttpSession session) {
        Map<String, String> response = new HashMap<>();
        try {
            User user = userRepository.findByLoginId(userDetails.getUsername())
                    .orElseThrow(() -> new UsernameNotFoundException("해당 유저를 찾을 수 없습니다."));

            String imageUrl = postImageService.uploadImage(file, userDetails.getUsername());

            // 세션에 이미지 URL 저장
            List<String> imageUrls = (List<String>) session.getAttribute("imageUrls");
            if (imageUrls == null) {
                imageUrls = new ArrayList<>();
            }
            imageUrls.add(imageUrl);
            session.setAttribute("imageUrls", imageUrls);

            response.put("url", imageUrl);
            return ResponseEntity.ok(response);
        } catch (IOException e) {
            log.error("이미지 업로드 중 오류 발생", e);
            response.put("error", "이미지 업로드 중 오류 발생: " + e.getMessage());
            return ResponseEntity.status(500).body(response);
        } catch (Exception e) {
            log.error("기타 오류 발생", e);
            response.put("error", "기타 오류 발생: " + e.getMessage());
            return ResponseEntity.status(500).body(response);
        }
    }

 

이런 식으로 진행해서 아래와 같은 게시글 작성폼이 나타난다.

그리고 위에 게시글 작성 코드에서

String content = commonUtil.markdownToHtml(postRequestDto.getContent()); 
이 부분은 작성한 markdown 게시글을 html태그로 변환해서 db에 저장하는 것이다 그렇지 않으면 markdown 문법 그대로 예시라는 단어를 적었다 치고 볼드에 이탤릭체를 적용했다 해보자 그러면
***예시*** 이런 식으로 그대로 나와 버린다. 이렇게 때문에 build.gradle 파일에 CommonMark에 대한 의존성을 추가해줬다 물론 작성된 게시글을 수정할 때 폼에 html태그로 저장했기에 그냥 불러오면 html태그로 변환한 형태가 그대로 넘어온다 그래서 다시 markdown 문법에 맞게 변환해 줘야 한다 이때는 flexMark의존성을 추가해 변환해 주었다.

    //markdown to html
    implementation 'org.commonmark:commonmark:0.22.0'
    implementation 'org.commonmark:commonmark-ext-gfm-tables:0.22.0'
    implementation 'org.commonmark:commonmark-ext-gfm-strikethrough:0.22.0'
    implementation 'org.commonmark:commonmark-ext-task-list-items:0.22.0'
    implementation 'org.commonmark:commonmark-ext-autolink:0.22.0'
    implementation 'org.commonmark:commonmark-ext-heading-anchor:0.22.0'

    //html to markdown
    implementation 'com.vladsch.flexmark:flexmark-all:0.64.8'
@Component
public class CommonUtil {
    private final Parser parser;
    private final HtmlRenderer htmlRenderer;

    public CommonUtil() {
        List extensions = Arrays.asList(
                TablesExtension.create(),
                StrikethroughExtension.create(),
                TaskListItemsExtension.create()
        );
        parser = Parser.builder().extensions(extensions).build();
        htmlRenderer = HtmlRenderer.builder().extensions(extensions).build();
    }

    public String markdownToHtml(String markdown) {
        Node document = parser.parse(markdown);
        return htmlRenderer.render(document);
    }

    public String htmlToMarkdown(String html) {
        return FlexmarkHtmlConverter.builder().build().convert(html);
    }
}

이렇게 변환 메서드들을 만들고 등록해 생성자 주입을 통해 사용했다.

아래는 CommonMark, FlexMark의 Github주소들이다

 

CommonMark

A strongly specified, highly compatible implementation of Markdown - CommonMark

github.com

 

GitHub - vsch/flexmark-java: CommonMark/Markdown Java parser with source level AST. CommonMark 0.28, emulation of: pegdown, kram

CommonMark/Markdown Java parser with source level AST. CommonMark 0.28, emulation of: pegdown, kramdown, markdown.pl, MultiMarkdown. With HTML to MD, MD to PDF, MD to DOCX conversion modules. - vs...

github.com

위의 과정들을 거치고 나면 이제 이렇게 게시글이 작성된 걸 볼 수 있다.

이렇게 이미지 존재 시 첫 번째 이미지가 썸네일로 사용되고 없으면 저 velog라 보이는 기본이미지가 썸네일로 설정되게 했다.

 

완성은 했지만 아쉬운 점도 많고 수정해야 할 부분도 많아 보이지만 프로젝트에만 시간을 쏟을 수 없으니 일단 이 정도로 마무리하고 다음에 더 프로젝트를 완성도 있고 이번에 부족했던 점을 보완하기 위해 공부를 좀 더 하고 진행해야 할거 같다.