Project : File Image APIs With JPA/MySQL + API Design

#Foreword

While implementing File Image APIs, I’ve run into a snag for almost 4 days. Eventually I could develop APIs successfully. This document will not cover the troubleshooting process but File Image Upload implementation code and API design will be included in this. (I will add reference Url for File Image API's trouble shooting)

TroubleShooting Reference Url

MysqlDataTruncation(MySQL) / FileSizeExceededException(tomcat)

: https://joo.hashnode.dev/project-trouble-shooting-file-image-upload-file-size-problems-with-mysql-tomcat

NonUniqueResultException(Spring Data JPA)

: https://joo.hashnode.dev/project-trouble-shooting-nonuniqueresultexception-by-jpa-using-imagehash-to-prevent-uploading-duplicated-images

File Image APIs Summary

: File Image APIs are used across four domains using MySQL and JPA (office, notice, place, member)

For the POST method, I utilized @RequestParam for images (MultipartFile), along with each domain's entityType and entityId. This implementation allows for automatic classification and storage of the images upon upload. Additionally, for image retrieval, I used @PathVariable with entityId to enable fetching specific image URLs or retrieving all image URLs.

( +When uploading Images, how to classify these images where they should be included.)

Implementation Tips for Unfamiliar Tasks

: I've realized these tips through developing these new APIs.

  1. Understand the Overall Flow:

    • It's crucial to understand how the mechanism you want to implement works. The implemented code and API mechanism might differ, so draw a layout of what you want to implement first based on your comprehension.
  2. Use References and Read the code.

    • Reading the code is vital. If you can't read or don't read the code, you will never understand how it works or be able to refactor it as needed. Once you understand the flow, you can customize it as you wish.

    • Once you have a grasp of the overall flow, refer to other sources such as references, Google, ChatGPT, etc.

  3. Implement and Test:

    • Implement your code first and then test it to ensure it runs correctly.

    • Focus solely on implementation without considering the project structure or other details initially, you can refine these aspects after confirming the code runs well.
      (nothing's gonna be late even if you check and refine it afterward.)

  4. Refactor as Needed:

    • Refactor the code according to the structure of your project or your specific requirements.

#I Felt This,

When you don't know where to start, Try to make it simple, Once you start, you can progress one step at a time. This is why I recommended focussing solely on implementation without considering other detailed requirements initially. On Successful Implementation, you can refine it much easier than before...................................................................................................................................!


#File Image APIs Code Implementation

Global(ImageUtils, ConfigClass) + application

application.yml

spring:
#spring:
  servlet:
    multipart:
      max-file-size: 16MB  # Set desired maximum file size
      max-request-size: 16MB  # Set desired maximum request size

server:
  tomcat:
    max-swallow-size: 16MB  # This sets the maximum size, Tomcat will swallow

application.yml snapshot

Config - FileUploadConfig

package com.FC.SharedOfficePlatform.global.config;

import jakarta.servlet.MultipartConfigElement;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;

@Configuration
public class FileUploadConfig {

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        factory.setMaxFileSize(DataSize.ofMegabytes(16));
        factory.setMaxRequestSize(DataSize.ofMegabytes(16));
        return factory.createMultipartConfig();
    }
}

Util - TomcatCustomizer

package com.FC.SharedOfficePlatform.global.util;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class TomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addConnectorCustomizers(connector -> {
            connector.setMaxPostSize(16 * 1024 * 1024); // 16MB
        });
    }
}

Util - ImageUtils

package com.FC.SharedOfficePlatform.global.util;

import java.io.ByteArrayOutputStream;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

public class ImageUtils {

    public static byte[] compressImage(byte[] data) {
        Deflater deflater = new Deflater();
        deflater.setLevel(Deflater.BEST_COMPRESSION);
        deflater.setInput(data);
        deflater.finish();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
        byte[] tmp = new byte[4 * 1024];
        try {
            while (!deflater.finished()) {
                int size = deflater.deflate(tmp);
                outputStream.write(tmp, 0, size);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return outputStream.toByteArray();
    }

    public static byte[] decompressImage(byte[] data) {
        Inflater inflater = new Inflater();
        inflater.setInput(data);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
        byte[] tmp = new byte[4 * 1024];
        try {
            while (!inflater.finished()) {
                int count = inflater.inflate(tmp);
                outputStream.write(tmp, 0, count);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return outputStream.toByteArray();
    }
}

Image

Controller - ImageDataController

package com.FC.SharedOfficePlatform.domain.image.controller;

import com.FC.SharedOfficePlatform.domain.image.dto.ImageDataResponse;
import com.FC.SharedOfficePlatform.domain.image.entity.ImageData;
import com.FC.SharedOfficePlatform.domain.image.service.ImageDataService;
import com.FC.SharedOfficePlatform.global.util.ResponseDTO;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/images")
@RequiredArgsConstructor
public class ImageDataController {

    private final ImageDataService imageDataService;

    @PostMapping
    public ResponseEntity<ResponseDTO<ImageDataResponse>> uploadImage(
        @RequestParam("images") MultipartFile file,
        @RequestParam("entityType") String entityType,
        @RequestParam("entityId") Long entityId,
        HttpServletRequest request
    ) throws IOException, NoSuchAlgorithmException {
        ImageDataResponse response = imageDataService.uploadImage(file, entityType, entityId, request);
        return ResponseEntity.ok(ResponseDTO.okWithData(response));
    }

    // ->return image itself in the browser, with this, we can download images in the browser through Url.
    @GetMapping("/{fileName}")
    public ResponseEntity<?> downloadImage(@PathVariable String fileName) {
        byte[] imageData = imageDataService.downloadImage(fileName);

        // Determine the MIME type based on the file extension (Multipurpose Internet Mail Extensions)
        String contentType;
        if (fileName.endsWith(".png")) {
            contentType = "image/png";
        } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
            contentType = "image/jpeg";
        } else if (fileName.endsWith(".gif")) {
            contentType = "image/gif";
        } else if (fileName.endsWith(".bmp")) {
            contentType = "image/bmp";
        } else if (fileName.endsWith(".tiff")) {
            contentType = "image/tiff";
        } else {
            contentType = "application/octet-stream"; // Default to binary stream if unknown type
        }
        return ResponseEntity
            .status(HttpStatus.OK)
            .contentType(MediaType.valueOf(contentType))
            .body(imageData);
    }

    @GetMapping("/url/{imageId}")
    public ResponseEntity<ResponseDTO<ImageDataResponse>> getImageUrl(
        @PathVariable Long imageId
    ) {
        ImageDataResponse imageUrl = imageDataService.getImageUrl(imageId);
        return ResponseEntity.ok(ResponseDTO.okWithData(imageUrl));
    }

    @GetMapping("/urls")
    public ResponseEntity<ResponseDTO<List<ImageDataResponse>>> getAllImageUrls() {
        List<ImageDataResponse> allImageUrls = imageDataService.getAllImageUrls();
        return ResponseEntity.ok(ResponseDTO.okWithData(allImageUrls));
    }

    @DeleteMapping("/{imageId}")
    public ResponseEntity<ResponseDTO<Void>> deleteImage(@PathVariable Long imageId) {
        imageDataService.deleteImage(imageId);
        return ResponseEntity.ok(ResponseDTO.ok());
    }

    @GetMapping("/member/{memberId}")
    public ResponseEntity<ResponseDTO<List<ImageDataResponse>>> getImagesByMember(@PathVariable Long memberId) {
        List<ImageData> imagesByMember = imageDataService.getImagesByMember(memberId);
        List<ImageDataResponse> responses = imagesByMember.stream()
            .map(ImageDataResponse::from)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ResponseDTO.okWithData(responses));
    }

    @GetMapping("/freeBoard/{boardId}")
    public ResponseEntity<ResponseDTO<List<ImageDataResponse>>> getImagesByFreeBoard(@PathVariable Long boardId) {
        List<ImageData> imagesByFreeBoard = imageDataService.getImagesByFreeBoard(boardId);
        List<ImageDataResponse> responses = imagesByFreeBoard.stream()
            .map(ImageDataResponse::from)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ResponseDTO.okWithData(responses));
    }

    @GetMapping("/office/{officeId}")
    public ResponseEntity<ResponseDTO<List<ImageDataResponse>>> getImagesByOffice(@PathVariable Long officeId) {
        List<ImageData> imagesByOffice = imageDataService.getImagesByOffice(officeId);
        List<ImageDataResponse> responses = imagesByOffice.stream()
            .map(ImageDataResponse::from)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ResponseDTO.okWithData(responses));
    }

    @GetMapping("/place/{placeId}")
    public ResponseEntity<ResponseDTO<List<ImageDataResponse>>> getImagesByPlace(@PathVariable Long placeId) {
        List<ImageData> imagesByPlace = imageDataService.getImagesByPlace(placeId);
        List<ImageDataResponse> responses = imagesByPlace.stream()
            .map(ImageDataResponse::from)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ResponseDTO.okWithData(responses));
    }

}

dto - ImageDataResponse

package com.FC.SharedOfficePlatform.domain.image.dto;
import com.FC.SharedOfficePlatform.domain.image.entity.ImageData;

public record ImageDataResponse(
    Long imageId,
    String fileName,
    String url,
    Long memberId,
    Long boardId,
    Long officeId,
    Long placeId
) {
    public static ImageDataResponse from(ImageData imageData) {
        Long memberId = (imageData.getMember() != null) ? imageData.getMember().getId() : null;
        Long boardId = (imageData.getFreeBoard() != null) ? imageData.getFreeBoard().getBoardId() : null;
        Long officeId = (imageData.getOffice() != null) ? imageData.getOffice().getOfficeId() : null;
        Long placeId = (imageData.getPlace() != null) ? imageData.getPlace().getPlaceId() : null;
        return new ImageDataResponse(
            imageData.getId(),
            imageData.getName(),
            imageData.getUrl(),
            memberId,
            boardId,
            officeId,
            placeId
        );
    }
}

entity - ImageData

package com.FC.SharedOfficePlatform.domain.image.entity;

import com.FC.SharedOfficePlatform.domain.freeBoard.entity.FreeBoard;
import com.FC.SharedOfficePlatform.domain.member.entity.Member;
import com.FC.SharedOfficePlatform.domain.office.entity.Office;
import com.FC.SharedOfficePlatform.domain.place.entity.Place;
import com.FC.SharedOfficePlatform.global.common.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "ImageData")
@NoArgsConstructor
@Getter
public class ImageData extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String type;

    // to prevent uploading duplicated image files.
    private String hash;

    @Lob
    @Column(name = "image_data", columnDefinition = "MEDIUMBLOB")
    private byte[] imageData;

    // New field for storing the URL
    private String url;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "board_id")
    private FreeBoard freeBoard;

    @ManyToOne
    @JoinColumn(name = "office_id")
    private Office office;

    @ManyToOne
    @JoinColumn(name = "place_id")
    private Place place;

    @Builder
    public ImageData(String name, String type, String hash, byte[] imageData, String url, Member member,
                    FreeBoard freeBoard, Office office, Place place) {
        this.name = name;
        this.type = type;
        this.hash = hash;
        this.imageData = imageData;
        this.url = url;
        this.member = member;
        this.freeBoard = freeBoard;
        this.office = office;
        this.place = place;
    }
}

+ 4 Domain Jpa Relationship Snapshot (Specific Section)

Member Entity

FreeBoard Entity

Office Entity

Place Entity

exception - ImageFileNotFoundException (1 reference)

package com.FC.SharedOfficePlatform.domain.image.exception;

import com.FC.SharedOfficePlatform.global.exception.ApplicationException;
import com.FC.SharedOfficePlatform.global.exception.ErrorCode;

public class ImageFileNotFoundException extends ApplicationException {

    private static final ErrorCode ERROR_CODE = ErrorCode.IMAGE_FILE_NOT_FOUND;
    public ImageFileNotFoundException() {
        super(ErrorCode.IMAGE_FILE_NOT_FOUND);
    }
}

repsoitory - ImageDataRepository

package com.FC.SharedOfficePlatform.domain.image.repository;

import com.FC.SharedOfficePlatform.domain.freeBoard.entity.FreeBoard;
import com.FC.SharedOfficePlatform.domain.image.entity.ImageData;
import com.FC.SharedOfficePlatform.domain.member.entity.Member;
import com.FC.SharedOfficePlatform.domain.office.entity.Office;
import com.FC.SharedOfficePlatform.domain.place.entity.Place;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ImageDataRepository extends JpaRepository<ImageData ,Long> {

    Optional<ImageData> findByName(String fileName);

    Optional<ImageData> findByHash(String hash);

    List<ImageData> findByMember(Member member);

    List<ImageData> findByFreeBoard(FreeBoard freeBoard);

    List<ImageData> findByOffice(Office office);

    List<ImageData> findByPlace(Place place);
}

service

package com.FC.SharedOfficePlatform.domain.image.service;

import com.FC.SharedOfficePlatform.domain.auth.exception.MemberNotFoundException;
import com.FC.SharedOfficePlatform.domain.freeBoard.entity.FreeBoard;
import com.FC.SharedOfficePlatform.domain.freeBoard.repository.FreeBoardRepository;
import com.FC.SharedOfficePlatform.domain.image.dto.ImageDataResponse;
import com.FC.SharedOfficePlatform.domain.image.entity.ImageData;
import com.FC.SharedOfficePlatform.domain.image.exception.FreeBoardNotFoundException;
import com.FC.SharedOfficePlatform.domain.image.exception.ImageFileAlreadyRegisteredException;
import com.FC.SharedOfficePlatform.domain.image.exception.ImageFileNotFoundException;
import com.FC.SharedOfficePlatform.domain.image.exception.OfficeNotFoundException;
import com.FC.SharedOfficePlatform.domain.image.exception.PlaceNotFoundException;
import com.FC.SharedOfficePlatform.domain.image.repository.ImageDataRepository;
import com.FC.SharedOfficePlatform.domain.member.entity.Member;
import com.FC.SharedOfficePlatform.domain.member.repository.MemberRepository;
import com.FC.SharedOfficePlatform.domain.office.entity.Office;
import com.FC.SharedOfficePlatform.domain.office.repository.OfficeRepository;
import com.FC.SharedOfficePlatform.domain.place.entity.Place;
import com.FC.SharedOfficePlatform.domain.place.repository.PlaceRepository;
import com.FC.SharedOfficePlatform.global.util.ImageUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
public class ImageDataService {

    private final ImageDataRepository imageDataRepository;
    private final MemberRepository memberRepository;
    private final FreeBoardRepository freeBoardRepository;
    private final OfficeRepository officeRepository;
    private final PlaceRepository placeRepository;

    public ImageDataResponse uploadImage(MultipartFile multipartFile, String entityType, Long entityId, HttpServletRequest httpServletRequest)
        throws IOException, NoSuchAlgorithmException {
        String imageHash = generateImageHash(multipartFile.getBytes());
        Optional<ImageData> existingImage = imageDataRepository.findByHash(imageHash);
        if (existingImage.isPresent()) {
            throw new ImageFileAlreadyRegisteredException();
        }

        String baseUrl = String.format(
            "%s://%s:%d/images",
            httpServletRequest.getScheme(),
            httpServletRequest.getServerName(),
            httpServletRequest.getServerPort()
        );

        Member member = null;
        FreeBoard freeBoard = null;
        Office office = null;
        Place place = null;

        // Determine the entity type and retrieve the corresponding entity
        if ("member".equalsIgnoreCase(entityType)) {
            member = memberRepository.findById(entityId)
                .orElseThrow(() -> new MemberNotFoundException());
        } else if ("freeBoard".equalsIgnoreCase(entityType)) {
            freeBoard = freeBoardRepository.findById(entityId)
                .orElseThrow(() -> new FreeBoardNotFoundException());
        } else if ("office".equalsIgnoreCase(entityType)) {
            office = officeRepository.findById(entityId)
                .orElseThrow(() -> new OfficeNotFoundException());
        } else if ("place".equalsIgnoreCase(entityType)) {
            place = placeRepository.findById(entityId)
                .orElseThrow(() -> new PlaceNotFoundException());
        }

        // Save new image data to the database if it does not already exist
        ImageData savedNewImageData = imageDataRepository.save(
            ImageData.builder()
                .name(multipartFile.getOriginalFilename())
                .type(multipartFile.getContentType())
                .hash(imageHash)
                .imageData(ImageUtils.compressImage(multipartFile.getBytes()))
                .url(baseUrl + "/" + multipartFile.getOriginalFilename())  // Construct the URL
                .member(member)
                .freeBoard(freeBoard)
                .office(office)
                .place(place)
                .build()
        );

        if (member != null) {
            member.getImages().add(savedNewImageData);
        }
        if (freeBoard != null) {
            freeBoard.getImages().add(savedNewImageData);
        }
        if (office != null) {
            office.getImages().add(savedNewImageData);
        }
        if (place != null) {
            place.getImages().add(savedNewImageData);
        }

        return ImageDataResponse.from(savedNewImageData);
    }

    // to prevent uploading duplicated file.
    private String generateImageHash(byte[] imageData) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = digest.digest(imageData);
        StringBuilder sb = new StringBuilder();
        for (byte b : hashBytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    // With this method, We can download image on the browser.
    public byte[] downloadImage(String fileName) {
        ImageData dbImageData = imageDataRepository.findByName(fileName)
            .orElseThrow(() -> new ImageFileNotFoundException());
        byte[] images = ImageUtils.decompressImage(dbImageData.getImageData());
        return images;
    }

    public ImageDataResponse getImageUrl(Long imageId) {
        ImageData dbImageData = imageDataRepository.findById(imageId)
            .orElseThrow(() -> new ImageFileNotFoundException());
        return ImageDataResponse.from(dbImageData);
    }

    public List<ImageDataResponse> getAllImageUrls() {
        List<ImageData> allImages = imageDataRepository.findAll();
        return allImages.stream()
            .map(ImageDataResponse::from)
            .collect(Collectors.toList());
    }

    public void deleteImage(Long imageId) {
        ImageData existingImageData = imageDataRepository.findById(imageId)
            .orElseThrow(() -> new ImageFileNotFoundException());
        imageDataRepository.delete(existingImageData);
    }

    public List<ImageData> getImagesByMember(Long memberId) {
        Member member = memberRepository.findById(memberId)
            .orElseThrow(() -> new MemberNotFoundException());
        return imageDataRepository.findByMember(member);
    }

    public List<ImageData> getImagesByFreeBoard(Long boardId) {
        FreeBoard freeBoard = freeBoardRepository.findById(boardId)
            .orElseThrow(() -> new FreeBoardNotFoundException());
        return imageDataRepository.findByFreeBoard(freeBoard);
    }

    public List<ImageData> getImagesByOffice(Long officeId) {
        Office office = officeRepository.findById(officeId)
            .orElseThrow(() -> new OfficeNotFoundException());
        return imageDataRepository.findByOffice(office);
    }

    public List<ImageData> getImagesByPlace(Long placeId) {
        Place place = placeRepository.findById(placeId)
            .orElseThrow(() -> new PlaceNotFoundException());
        return imageDataRepository.findByPlace(place);
    }

}

#API Design (/images) - POST, GET, DELETE

: File images are used across four domains. Below is the API design that I documented during the project. It's written in Korean because it was intended for my team members.

When uploading file images, they are automatically, they are automatically classified into each entity (four each domain) using three '@PathVariable' : 'MultipartFile', 'entityType', and 'entityId'

For retrieving images, we use generated image URLs. We can retrieve all image URLs or specific image URLs using '@PathVariable' with 'entityId'.

POST : 이미지 파일 등록 /images - 정병주(완료)

(예시 Url ,설명, Request Body, Response Body, 예외)

예시 Url : localhost:8080/images

설명

이미지 등록시 연관하는 entity의 id가 함께 저장됨(Member, FreeBoard) -> 포스트맨 사진 참고
멤버의 사진이 등록될 경우, memberId -> 반환 (구현 완료)
자유 게시판의 사진이 등록될 경우, freeBoardId -> 반환 (예정)

#Member 이미지 등록 시

Request Body (따로 RequestBody 입력 X - RequestParam 입력 : 사진 참고)

**Key (@Requestparam)  ------------   Value**
images                              image file (MultipartFile)
entityType                          member
entityId                            1   (memberId)

Response Body

{
 "code": 200,
 "errorMessage": null,
 "data": {
  "imageId": 1,
  "fileName": "ERD_SharedOffice.pptx",
  "url": "<http://localhost:8080/images/ERD_SharedOffice.pptx>",
  "memberId": 1,
  "boardId": null,
  "officeId": null,
  "placeId": null
 }
}

// 하단에 DB사진 참조 (실제 저장했던 사진이지만, 업데이트 되면서 세부 데이터는 다를 수 있음)

예외

동일한 이미지를 저장하려고 하는 경우 
-> entityId가 다르면 동일한 이미지도 올릴 수 있도록 수정해보려고 하였으나, 우선 현재로선
동일한 이미지 등록 금지, 동일한 이미지가 등록될 경우 디비에서 이미지 조회 에러가 생김 
기능 완성되면 이후에 고려해볼 수 있을 것 같음

{
 "code": 400,
 "errorMessage": "이미 등록된 이미지 파일입니다.",
 "data": null
}

존재하지 않는 memberId를 입력한 경우 (멤버 존재X)
{
 "code": 404,
 "errorMessage": "존재하지 않는 회원입니다.",
 "data": null
}

#FreeBoard 이미지 등록 시

Request Body (따로 RequestBody 입력 X - RequestParam 입력 : 사진 참고)

**Key (@Requestparam)  ------------   Value**
images                              파일 이미지 선택
entityType                          freeBoard
entityId                            1   (boardId)

Response Body

{
 "code": 200,
 "errorMessage": null,
 "data": {
  "imageId": 2,
  "fileName": "GitHub__headpic_Image.png",
  "url": "<http://localhost:8080/images/GitHub__headpic_Image.png>",
  "memberId": null,
  "boardId": 1,
  "officeId": null,
  "placeId": null
 }
}

// 하단에 DB사진 참조 (실제 저장했던 사진이지만, 업데이트 되면서 세부 데이터는 다를 수 있음)

예외

동일한 이미지를 저장하려고 하는 경우 
-> entityId가 다르면 동일한 이미지도 올릴 수 있도록 수정해보려고 하였으나, 우선 현재로선
동일한 이미지 등록 금지, 동일한 이미지가 등록될 경우 디비에서 이미지 조회 에러가 생김 
기능 완성되면 이후에 고려해볼 수 있을 것 같음

{
 "code": 400,
 "errorMessage": "이미 등록된 이미지 파일입니다.",
 "data": null
}

존재하지 않는 boardId를 입력한 경우 (게시판 존재X)
{
 "code": 400,
 "errorMessage": "존재하지 않는 게시글 입니다.",
 "data": null
}

#Office 이미지 등록 시

Request Body (따로 RequestBody 입력 X - RequestParam 입력 : 사진 참고)

**Key (@Requestparam)  ------------   Value**
images                              파일 이미지 선택
entityType                          office
entityId                            1   (officeId)

Response Body

{
 "code": 200,
 "errorMessage": null,
 "data": {
  "imageId": 3,
  "fileName": "Github_Image_redlikecolor.png",
  "url": "<http://localhost:8080/images/Github_Image_redlikecolor.png>",
  "memberId": null,
  "boardId": null,
  "officeId": 1,
  "placeId": null
 }
}

// 하단에 DB사진 참조 (실제 저장했던 사진이지만, 업데이트 되면서 세부 데이터는 다를 수 있음)

예외

동일한 이미지를 저장하려고 하는 경우 
-> entityId가 다르면 동일한 이미지도 올릴 수 있도록 수정해보려고 하였으나, 우선 현재로선
동일한 이미지 등록 금지, 동일한 이미지가 등록될 경우 디비에서 이미지 조회 에러가 생김 
기능 완성되면 이후에 고려해볼 수 있을 것 같음

{
 "code": 400,
 "errorMessage": "이미 등록된 이미지 파일입니다.",
 "data": null
}

존재하지 않는 officeId를 입력한 경우 (오피스 지점 존재x)
{
 "code": 404,
 "errorMessage": "존재하지 않는 지점 입니다.",
 "data": null
}

#Place 이미지 등록 시

Request Body (따로 RequestBody 입력 X - RequestParam 입력 : 사진 참고)

**Key (@Requestparam)  ------------   Value**
images                              파일 이미지 선택
entityType                          place
entityId                            1   (placeId)

Response Body

{
 "code": 200,
 "errorMessage": null,
 "data": {
  "imageId": 3,
  "fileName": "Github_Image_redlikecolor.png",
  "url": "<http://localhost:8080/images/Github_Image_redlikecolor.png>",
  "memberId": null,
  "boardId": null,
  "officeId": 1,
  "placeId": null
 }
}

// 하단에 DB사진 참조 (실제 저장했던 사진이지만, 업데이트 되면서 세부 데이터는 다를 수 있음)

예외

동일한 이미지를 저장하려고 하는 경우 
-> entityId가 다르면 동일한 이미지도 올릴 수 있도록 수정해보려고 하였으나, 우선 현재로선
동일한 이미지 등록 금지, 동일한 이미지가 등록될 경우 디비에서 이미지 조회 에러가 생김 
기능 완성되면 이후에 고려해볼 수 있을 것 같음

{
 "code": 400,
 "errorMessage": "이미 등록된 이미지 파일입니다.",
 "data": null
}

존재하지 않는 officeId를 입력한 경우 (오피스 지점 존재x)
{
 "code": 404,
 "errorMessage": "존재하지 않는 지점 입니다.",
 "data": null
}

GET /member/{memberId} : 멤버 이미지 조회 - 정병주(완료)

예시 : localhost:8080/images/member/1

ResponseBody

{
 "code": 200,
 "errorMessage": null,
 "data": [
  {
   "imageId": 1,
   "fileName": "ERD_SharedOffice.pptx",
   "url": "<http://localhost:8080/images/ERD_SharedOffice.pptx>",
   "memberId": 1,
   "boardId": null,
   "officeId": null,
   "placeId": null
  }
 ]
}

예외

memberId에 해당하는 member가 존재하지 않는 경우
{
 "code": 404,
 "errorMessage": "존재하지 않는 회원입니다.",
 "data": null
}

GET /freeBoard/{boardId} : 게시판 이미지 조회 - 정병주(완료)

예시 : localhost:8080/images/freeBoard/1

ResponseBody

{
 "code": 200,
 "errorMessage": null,
 "data": [
  {
   "imageId": 2,
   "fileName": "GitHub__headpic_Image.png",
   "url": "<http://localhost:8080/images/GitHub__headpic_Image.png>",
   "memberId": null,
   "boardId": 1,
   "officeId": null,
   "placeId": null
  }
 ]
}

예외

freeBoard에 해당하는 freeBoard 조회 실패
{
 "code": 400,
 "errorMessage": "존재하지 않는 게시글 입니다.",
 "data": null
}

GET /office/{officeId} : 오피스 이미지 조회 - 정병주(완료)

예시 : localhost:8080/images/office/1

ResponseBody

{
 "code": 200,
 "errorMessage": null,
 "data": [
  {
   "imageId": 3,
   "fileName": "Github_Image_redlikecolor.png",
   "url": "<http://localhost:8080/images/Github_Image_redlikecolor.png>",
   "memberId": null,
   "boardId": null,
   "officeId": 1,
   "placeId": null
  }
 ]
}

예외

{
 "code": 404,
 "errorMessage": "존재하지 않는 지점 입니다.",
 "data": null
}

GET /place/{placeId} : 장소 이미지 조회 - 정병주(완료)

예시 : localhost:8080/images/office/1

ResponseBody

{
 "code": 200,
 "errorMessage": null,
 "data": [
  {
   "imageId": 4,
   "fileName": "SignUp_회원가입_DB.png",
   "url": "<http://localhost:8080/images/SignUp_회원가입_DB.png>",
   "memberId": null,
   "boardId": null,
   "officeId": null,
   "placeId": 1
  },
  {
   "imageId": 5,
   "fileName": "LogIn_Postman_InValidPassword.png",
   "url": "<http://localhost:8080/images/LogIn_Postman_InValidPassword.png>",
   "memberId": null,
   "boardId": null,
   "officeId": null,
   "placeId": 1
  }
 ]
}

예외

{
 "code": 404,
 "errorMessage": "존재하지 않는 지점 입니다.",
 "data": null
}

GET /{fileName}: 이미지 조회 (직접사용X, 설명 참조) - 정병주(완료)

예시 : http://localhost:8080/images/1.4MB_ImageFile.png

설명

상기 사진처럼 해당 Get Method를 사용하면 이미지 자체가 조회가 되며, 해당 메서드가 있어야
이미지 등록, 이미지 조회 등을 통해 받은 url을 통해 브라우저에 이미지를 다운로드 할 수 있음.
(프론트에서 직접 해당 API를 호출하기 위해 생성되었다기 보다 브라우저에 해당 이미지를 다운로드
하여 사용할 수 있도록 돕는 API)

예외

fileName에 해당하는 등록된 이미지가 없는 경우
{
 "code": 404,
 "errorMessage": "해당하는 이미지가 없습니다.",
 "data": null
}

GET /url/{imageId} : 상세 이미지 Url 조회 - 정병주(완료)

예시 : localhost:8080/images/url/1

ResponseBody

{
 "code": 200,
 "errorMessage": null,
 "data": {
  "imageId": 2,
  "fileName": "872KB_Image.png",
  "url": "<http://localhost:8080/images/872KB_Image.png>",
  "memberId": null,
  "boardId": 1,
  "officeId": null
 }
}

예외

imageId에 해당하는 등록된 이미지가 없는 경우

{
 "code": 404,
 "errorMessage": "해당하는 이미지가 없습니다.",
 "data": null
}

GET /urls : 이미지 Url 전체 조회 - 정병주(완료)

예시 : localhost:8080/images/urls

ResponseBody

{
 "code": 200,
 "errorMessage": null,
 "data": [
  {
   "imageId": 1,
   "fileName": "LogIn_Postman_InValidPassword.png",
   "url": "<http://localhost:8080/images/LogIn_Postman_InValidPassword.png>",
   "memberId": null,
   "boardId": null,
   "officeId": 1
  },
  {
   "imageId": 2,
   "fileName": "872KB_Image.png",
   "url": "<http://localhost:8080/images/872KB_Image.png>",
   "memberId": null,
   "boardId": 1,
   "officeId": null
  },
  {
   "imageId": 3,
   "fileName": "ERD_SharedOffice.pptx",
   "url": "<http://localhost:8080/images/ERD_SharedOffice.pptx>",
   "memberId": 1,
   "boardId": null,
   "officeId": null
  }
 ]
}

등록된 이미지가 없는 경우 → 그냥 빈 데이터 조회

{
 "code": 200,
 "errorMessage": null,
 "data": []
}

Delete /{imageId} : 이미지 삭제 - 정병주(완료)

예시 : localhost:8080/images/2

ResponseBody

{
 "code": 200,
 "errorMessage": null,
 "data": null
}

예외

imageId에 해당하는 이미지가 존재하지 않는 경우
{
 "code": 404,
 "errorMessage": "해당하는 이미지가 없습니다.",
 "data": null
}

#이미지 파일 API 설명 요약

1. POST /images
이미지 등록 시, 파일 + entityType + entityId를 함께 입력해주면 해당 entity의 Id와 함께
ImageData 테이블의 database로 이미지들이 저장.
각 이미지 등록 시 이에 해당하는 entityId를 반환하고, 이를 통해 해당 이미지 조회 가능
(등록 시 Url이 생성되는데, 해당 Url을 통해 이미지에 접근 가능) 

2. GET /images/{fileName} 
이미지 자체를 반환해주는 API이며, 프론트에서 API 자체를 호출할 필요는 없을 수 있음,
해당 API를 생성해두어 Url을 통해 브라우저에서 이미지를 다운로드 받을 수 있게 도와주는
API라고 이해하면 됨. 

3. GET /images/url/{imageId}
이미지 id에 해당하는 imageData를 반환 : id, name, url

4. GET /images/urls
DB에 등록된 모든 imageData를 반환 : 각 image Data의 id, name, url

5. GET /images/member/{memberId}
멤버에 해당하는 이미지 조회

6. GET /images/freeBoard/{freeBoardId}
게시판에 해당하는 이미지 조회

7. DELETE /images/{imageId}
imageId에 해당하는 DB에 등록된 imageData를 삭제