feat: 게시글 인라인 이미지 서버 업로드(DB 저장) — base64 대체

- board_image 테이블 + BoardImageController/Service/Mapper
  · POST /api/board/images(로그인) → {url:/api/images/{id}}
  · GET  /api/images/{id}(공개, img src 로 로드) — 1년 캐시
- 본문엔 짧은 URL 만 들어가 글이 비대해지지 않음

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-07 19:17:25 +09:00
co-authored by Claude Opus 4.8
parent 2da43b2f77
commit f9abd0c989
6 changed files with 159 additions and 0 deletions
@@ -0,0 +1,57 @@
package com.sb.web.board.controller;
import com.sb.web.auth.dto.SessionUser;
import com.sb.web.auth.web.AuthInterceptor;
import com.sb.web.board.domain.BoardImage;
import com.sb.web.board.service.BoardImageService;
import com.sb.web.common.exception.ApiException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 게시글 인라인 이미지.
* POST /api/board/images 업로드(로그인 필요) → {url: "/api/images/{id}"}
* GET /api/images/{id} 이미지 바이트(공개 — img src 로 인증 없이 로드)
*/
@RestController
@RequiredArgsConstructor
public class BoardImageController {
private final BoardImageService service;
@PostMapping("/api/board/images")
public Map<String, String> upload(
@RequestParam("image") MultipartFile file,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) throws IOException {
if (file.isEmpty()) {
throw new ApiException(HttpStatus.BAD_REQUEST, "이미지가 비어 있습니다.");
}
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new ApiException(HttpStatus.BAD_REQUEST, "이미지 파일만 업로드할 수 있습니다.");
}
Long id = service.save(current.getId(), contentType, file.getBytes());
return Map.of("url", "/api/images/" + id);
}
@GetMapping("/api/images/{id}")
public ResponseEntity<byte[]> get(@PathVariable Long id) {
BoardImage img = service.get(id);
if (img == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(img.getContentType()))
.cacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
.body(img.getData());
}
}