[미니프로젝트] Jelog 만들기 #9

박제현·2023년 8월 25일
0

jelog 만들기

목록 보기
9/15

📝 게시글 관리에 필요한 API 명세하기

지난 "Jelog 만들기 #8" 에 이어서, 이번에는 게시글 관리에 필요한 API 를 명세해보자.

우선, 게시글 작성, 삭제, 조회 등의 기능이 필요할 것 같다.

게시글을 추가하는 publish API
POST, /api/publish, { 'publisher' : String, 'title' : String, 'content' : String } 으로 명세한다.

게시자의 아이디로 모든 게시글을 불러오는 findByPublisher API
GET, /api/articles/{publisher}, publisher@PathVariable 로 받아온다.

게시자의 아이디와, 게시글의 아이디를 통해 특정 하나의 게시글을 불러오는 findById API
GET, /api/articles/{publisher}/{id}, publisherid@PathVariable 이다.

👨‍🏫 스프링 부트로 코드 작성하기.

이번 API 역시 역순으로 코드를 작성하자.

// Article.java
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "publisher", nullable = false)
    private String publisher;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "content", nullable = false, length = 500000)
    private String content;

    @CreatedDate
    @Column(name = "created_at", nullable = false)
    private LocalDateTime createAt;

    @LastModifiedDate
    @Column(name = "update_at", nullable = false)
    private LocalDateTime updatedAt;

    @Builder
    public Article(String publisher, String title, String content){
        this.publisher = publisher;
        this.title = title;
        this.content = content;
    }
}

DB 에 저장될 Article, Entity 이다.
PKid 로 설정하고, 고유번호가 자도 생성 되도록 어노테이션을 붙여준다.

// ArticleRepository.java
public interface ArticleRepository extends JpaRepository<Article, Long> {

    Optional<List<Article>> findByPublisher(String publisher);
    Optional<Article> findByPublisherAndId(String publisher, Long id);
}

Article 저장소를 repositoryinterface 로 생성하고, JpaRepository 를 상속받아 Spring Data JPA 를 사용한다.
내가 필요로하는 findByPublisher 와, findByPublisherAndId 메소드를 작성한다.

// AddArticleRequestDto.java
@Getter
@Setter
public class AddArticleRequestDto {

    private String publisher;
    private String title;
    private String content;
}

새로운 게시글 작성시 들어올 요청에 대한 Dto 이다.

// ArticleService.java
@RequiredArgsConstructor
@Service
public class ArticleService {

    private final ArticleRepository articleRepository;

    public Long save(AddArticleRequestDto requestDto){
        return articleRepository.save(
                Article.builder()
                        .publisher(requestDto.getPublisher())
                        .title(requestDto.getTitle())
                        .content(requestDto.getContent())
                        .build()
        ).getId();
    }

    public List<Article> findByPublisher(String publisher){
        return articleRepository.findByPublisher(publisher).get();
    }

    public Article findByPublisherAndId(String publisher, Long id) {
        return articleRepository.findByPublisherAndId(publisher, id).orElse(null);
    }


    public List<Article> findAll(){
        return articleRepository.findAll();
    }
}

ArticleRepository 와 직접 동작할 서비스인 ArticleService 를 생성한다.
필요한 어노테이션을 달아주어 Bean 으로 정상 등록 될 수 있도록 한다.

save 메소드를 통해서, requestDto 로 입력받은 게시자, 제목, 내용 정보를 Article 빌더를 통해서 새로운 게시글을 저장한다.

findByPublisher 메소드를 통해서, publisher 를 입력받아 해당 게시자가 작성한 모든 글을 리스트 형태로 반환한다.

findByPublisherAndId 메소드는 publisherid 를 입력받아 특정 게시글 하나를 반환한다.

// ArticleApiController.java
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class ArticleApiController {

    private final ArticleService articleService;

    @PostMapping("/publish")
    public ResponseEntity<String> publish(@RequestBody AddArticleRequestDto requestDto){
        articleService.save(requestDto);
        return ResponseEntity.ok(articleService.findByPublisher(requestDto.getPublisher()).toString());
    }

    @GetMapping("/articles/{publisher}")
    public ResponseEntity<List<Article>> findByPublisher(@PathVariable String publisher){
        System.out.println("article Result ::: "+articleService.findByPublisher(publisher));
        return ResponseEntity.ok(articleService.findByPublisher(publisher));
    }

    @GetMapping("/articles/{publisher}/{id}")
    public ResponseEntity<Article> findById(@PathVariable String publisher, @PathVariable Long id) {
        Article article = articleService.findByPublisherAndId(publisher, id);
        System.out.println("article = " + article.toString());
        if (article == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(article);
    }


    @GetMapping("/articles")
    public ResponseEntity<List<Article>> findAll(){
        return ResponseEntity.ok(articleService.findAll());
    }
}

프론트에서 호출할 API 를 명세한다.

정상적으로 게시글이 등록되는 모습.

testpublisher 인 모든 게시물을 리스트로 정상적으로 반환한다.

test 가 작성한 1번째 게시물을 Article 객체로 반환하는 모습.

😵‍💫 발생한 문제점

기존의 게시글을 저장하는 방법은, 내용 영역에 있는 <div> 태그의 outerHTML 을 저장하는 방식으로 구현했었다.
그 이유는, 게시글에는 텍스트 뿐만 아니라, 이미지도 당연히 포함될 수 있기 때문이었고, 그렇기에 <textArea> 를 사용하지 않고 <div> 태그를 사용한 것이다.

그러나, 이러한 방식에는 치명적인 문제가 존재했다.
저장될 <img> 태그의 소스로 로컬 이미지 경로로 설정되는 것은 물론이고, <div> 에 바로 표현해주기 위해서 이미지 파일을 base64 형태로 인코딩하여 저장했다.

이렇게 저장하게 되면, Dto 로 넘겨줄 content 의 텍스트길이가 지나치게 길어지게 된다.
메모리도 낭비되고, 애초에 인코딩 된 텍스트는 오버플로우로 저장되지도 않았다.

며칠동안, 검색과 GPT의 도움을 받아서 다행히 해결했다.

방법은 스프링 서버에 먼저 이미지를 업로드하고, 해당 이미지를 게시글을 보여줄때 다운로드 받는 방식으로 해결했다.

  const uploadImage = async () => {
    const fileInput = document.querySelector(".imageSelector");
    const file = fileInput.files[0];

    const formData = new FormData();
    formData.append("file", file);

    try {
      const response = await axios.post("/api/uploadImage", formData, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      });
      const imageURL = response.data;
      insertImageToEditor(imageURL);
      console.log(response);
      return imageURL;
    } catch (err) {
      console.error("Error uploadTets : ", err);
    }
  };

이미지를 선택하면, 해당 이미지를 FormData 형식으로 생성하여, uploadImage API로 POST 요청을 날린다.
정상적으로 업로드가 완료되면 해당 이미지에 접근할 수 있는 URL을 insertImageToEditor 로 넘겨준다.

  const insertImageToEditor = async (imageURL) => {
    const imgTag = document.createElement("img");
    imgTag.src = imageURL;
    contentTextareaRef.current.appendChild(imgTag);
  };

넘겨받은 URL 을 imgTag 의 소스로 설정해주면, 정상적으로 contentTextareaRef<div> 영역에 이미지를 삽입할 수 있다.

// FileUploadController.java

    @PostMapping("/api/uploadImage")
    public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile file) {
        try {
            File directory = new File(UPLOAD_DIR);
            if (!directory.exists()) {
                directory.mkdirs(); // creates directory including any necessary but nonexistent parent directories
            }

            System.out.println("UPLOAD DIRECTORY: " + directory.getAbsolutePath()); // 절대 경로 출력


            String fileName = file.getOriginalFilename();
            String filePath = UPLOAD_DIR + fileName;
            File dest = new File(filePath);
            file.transferTo(dest);

            return ResponseEntity.ok().body(ACCESS_PATH+fileName);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to upload file: " + e.getMessage());
        }
    }
}

이미지를 업로드할 때 POST 요청을 날릴 uploadImage API 이다.
MultipartFile 형태로 파일을 입력받고, UPLOAD_DIR 에 이미지를 저장한다.
저장된 이미지에 접근 가능한 URL을 반환한다.

@RestController
public class FileDownloadController {

    private final String UPLOAD_DIR = "uploads/";

    @GetMapping("api/images/{filename:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename){
        try {
            Path file = Paths.get(UPLOAD_DIR).resolve(filename).normalize();
            Resource resource = new UrlResource(file.toUri());

            String mimeType = "image/png"; // 기본값으로 png 설정
            if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
                mimeType = "image/jpeg";
            } else if (filename.endsWith(".gif")) {
                mimeType = "image/gif";
            }

            return ResponseEntity.ok().contentType(MediaType.parseMediaType(mimeType))
                    .body(resource);
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
}

브라우저에서 이미지 파일로 인식하기 위해서 MIME 타입을 설정해주고, 이미지를 소스로 설정하여, 해당 URL 을 사용하여 리액트에서 이미지를 표시할 수 있도록 설정한다.

...짠! 🎉
드디어 해결했다.
이제 서버에 이미지가 저장되고, 해당 이미지를 다시 다운받는 형태로 동작한다.

계속해서 프로젝트를 진행하고 있지만..
진짜 어려운 것 같다.
프론트를 개발하려면 백엔드를 알아야하니.. 모든 웹 개발자들은 풀스택 개발자가 될 수 밖에 없는 운명인가.. 🧐

profile
닷넷 새싹

0개의 댓글