- 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:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sb.web.board.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 게시글 인라인 이미지. board_image 테이블과 매핑(DB 저장).
|
||||
* 본문에는 base64 대신 /api/images/{id} URL 만 들어간다.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BoardImage {
|
||||
|
||||
private Long id;
|
||||
private Long memberId;
|
||||
private String contentType;
|
||||
private byte[] data;
|
||||
private Integer byteSize;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sb.web.board.mapper;
|
||||
|
||||
import com.sb.web.board.domain.BoardImage;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface BoardImageMapper {
|
||||
|
||||
int insert(BoardImage image);
|
||||
|
||||
BoardImage findById(@Param("id") Long id);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.sb.web.board.service;
|
||||
|
||||
import com.sb.web.board.domain.BoardImage;
|
||||
import com.sb.web.board.mapper.BoardImageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 게시글 인라인 이미지 저장/조회 (DB 저장).
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BoardImageService {
|
||||
|
||||
private final BoardImageMapper mapper;
|
||||
|
||||
@Transactional
|
||||
public Long save(Long memberId, String contentType, byte[] data) {
|
||||
BoardImage img = BoardImage.builder()
|
||||
.memberId(memberId)
|
||||
.contentType(contentType)
|
||||
.data(data)
|
||||
.byteSize(data.length)
|
||||
.build();
|
||||
mapper.insert(img);
|
||||
return img.getId();
|
||||
}
|
||||
|
||||
public BoardImage get(Long id) {
|
||||
return mapper.findById(id);
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,18 @@ CREATE TABLE IF NOT EXISTS board_setting (
|
||||
|
||||
INSERT IGNORE INTO board_setting (id, tag_category_id) VALUES (1, NULL);
|
||||
|
||||
-- 게시글 인라인 이미지 (DB 저장). 본문에는 /api/images/{id} URL 만 포함(base64 미사용).
|
||||
CREATE TABLE IF NOT EXISTS board_image (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
member_id BIGINT NOT NULL,
|
||||
content_type VARCHAR(100) NOT NULL,
|
||||
data LONGBLOB NOT NULL,
|
||||
byte_size INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_board_image_member (member_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 댓글
|
||||
CREATE TABLE IF NOT EXISTS comment (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.sb.web.board.mapper.BoardImageMapper">
|
||||
|
||||
<insert id="insert" parameterType="com.sb.web.board.domain.BoardImage"
|
||||
useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO board_image (member_id, content_type, data, byte_size, created_at)
|
||||
VALUES (#{memberId}, #{contentType}, #{data}, #{byteSize}, NOW())
|
||||
</insert>
|
||||
|
||||
<select id="findById" resultType="com.sb.web.board.domain.BoardImage">
|
||||
SELECT id, member_id, content_type, data, byte_size, created_at
|
||||
FROM board_image
|
||||
WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user