
이번에 CRUD와 이미지 수정을 모두 작업한 뒤 리펙토링까지 했다.
package com.example.demo.controller;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
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.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.repository.ArticleRepository;
import com.example.demo.service.ArticleService;
import com.example.demo.vo.Article;
import com.example.demo.vo.ResultData;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class ArticleController {
private final ArticleService articleService;
private final ArticleRepository articleRepository;
@RequestMapping("/usr/article/list")
public String list(Model model) {
List<Article> articles = articleService.getArticles();
model.addAttribute("articles", articles);
return "article/list";
}
@RequestMapping("/usr/article/detail")
public String detail(@RequestParam int id, Model model) {
Article article = articleService.getArticleById(id);
model.addAttribute("article", article);
return "article/detail";
}
@GetMapping("/usr/article/write")
public String writeForm() {
return "article/write";
}
@PostMapping("/usr/article/write")
public String doWrite(@RequestParam String title,
@RequestParam int memberId,
@RequestParam(required = false) MultipartFile file){
articleService.writeArticle(title, memberId, file);
return "redirect:/usr/article/list";
}
@GetMapping("/usr/article/modify")
public String modifyForm(@RequestParam int id, Model model) {
Article article = articleService.getArticleById(id);
model.addAttribute("article", article);
return "article/modify";
}
@PostMapping("/usr/article/modify")
public String doModify(@RequestParam int id,
@RequestParam String title,
@RequestParam(required=false) MultipartFile file) {
articleService.modifyArticle(id, title, file);
return "redirect:/usr/article/detail?id=" + id;
}
@RequestMapping("/usr/article/delete")
public String doDelete(@RequestParam int id) {
articleRepository.deleteArticle(id);
return "redirect:/usr/article/list";
}
}
package com.example.demo.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.repository.ArticleRepository;
import com.example.demo.vo.Article;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ArticleService {
private final ArticleRepository articleRepository;
public List<Article> getArticles() {
return articleRepository.getArticles();
}
public Article getArticleById(int id) {
return articleRepository.getArticleById(id);
}
public void writeArticle(String title, int memberId, MultipartFile file) {
String thumbImg = null;
if(file != null && !file.isEmpty()){
thumbImg = saveUpload(file);
}
articleRepository.writeArticle(title, memberId, thumbImg);
}
public int modifyArticle(int id, String title, MultipartFile file) {
String thumbImg = null;
if(file != null && !file.isEmpty()) {
thumbImg = saveUpload(file);
}
return articleRepository.updateArticle(id, title, thumbImg);
}
public int deleteArticle(int id) {
return articleRepository.deleteArticle(id);
}
//Util
public String saveUpload(MultipartFile file) {
// 확장자/용량 검증
long max = 20L * 1024 * 1024; // 20MB
if(file.getSize() > max) throw new RuntimeException("파일 용량 초과(최대 20MB)");
String origin = file.getOriginalFilename();
String ext = (origin != null && origin.contains("."))
? origin.substring(origin.lastIndexOf('.') + 1).toLowerCase()
: "";
if (!(ext.equals("png")) || (ext.equals("jpg")) || (ext.equals("jpeg"))|| (ext.equals("webp"))) {
throw new RuntimeException("허용되지 않는 확장자");
}
// 3) 저장 파일명 생성
String newFileName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
Path uploadPath = Paths.get("C:/upload/", newFileName);
try {
Files.createDirectories(uploadPath.getParent());
Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
} catch(IOException e){
throw new RuntimeException("파일 저장 실패");
}
return newFileName;// ← 이 값을 DB의 thumbImg에 저장
}
}
package com.example.demo.repository;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.example.demo.vo.Article;
@Mapper
public interface ArticleRepository {
List<Article> getArticles();
Article getArticleById(@Param("id") int id);
void writeArticle(@Param("title") String title,
@Param("memberId") int memberId,
@Param("thumbImg") String thumbImg);
int updateArticle(@Param("id")int id,
@Param("title") String title,
@Param("thumbImg") String thumbImg);
int deleteArticle(@Param("id") int id);
}
package com.example.demo.vo;
import lombok.Data;
@Data
public class Article {
private int id;
private String regDate;
private String title;
private int memberId;
private String thumbImg;
}
<?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.example.demo.repository.ArticleRepository">
<resultMap id="ArticleMap" type="com.example.demo.vo.Article">
<id property="id" column="id"/>
<result property="regDate" column="regDate"/>
<result property="title" column="title"/>
<result property="memberId" column="memberId"/>
<result property="thumbImg" column="thumbImg"/>
</resultMap>
<select id="getArticles" resultMap="ArticleMap">
SELECT id, regDate, title, memberId, thumbImg
FROM article
ORDER BY id DESC
</select>
<select id="getArticleById" resultMap="ArticleMap">
SELECT id, regDate, title, memberId, thumbImg
FROM article
WHERE id = ${id}
</select>
<insert id="writeArticle">
INSERT INTO article (title, memberId, thumbImg)
VALUES(#{title}, #{memberId}, #{thumbImg})
</insert>
<update id="updateArticle">
UPDATE article
SET title = #{title}
<if test="thumbImg != null and thumbImg !=''">
, thumbImg = #{thumbImg}
</if>
WHERE id = #{id}
</update>
<delete id="deleteArticle">
DELETE FROM article
WHERE id = #{id}
</delete>
</mapper>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<!DOCTYPE html>
<html>
<head>
<title>Article List</title>
</head>
<body>
<h1>Article List</h1>
<p><a href="/usr/article/write">새 글 작성</a></p>
<table border="1" cellpadding="6">
<tr>
<th>Thumb</th>
<th>ID</th>
<th>RegDate</th>
<th>Title</th>
<th>Member</th>
<th>Action</th>
</tr>
<c:forEach var="a" items="${articles}">
<tr>
<!-- 썸네일 -->
<td>
<c:if test="${not empty a.thumbImg}">
<img src="/upload/${a.thumbImg}" style="height:40px"/>
</c:if>
</td>
<td>${a.id}</td>
<td>${a.regDate}</td>
<td><a href="/usr/article/detail?id=${a.id }">${a.title}</a></td>
<td>${a.memberId}</td>
<td>
<a href="/usr/article/modify?id=${a.id }">수정</a>|
<a href="/usr/article/delete?id=${a.id }" onclick="return confirm('삭제할까요?');">삭제</a>
</td>
</tr>
</c:forEach>
</table>
</body>
</html>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<!DOCTYPE html>
<html>
<head>
<title>Article Detail</title>
</head>
<body>
<h1>Article #${article.id }번</h1>
<p>RegDate: ${article.regDate }</p>
<p>Title: ${article.title }</p>
<p>Member: ${article.memberId }</p>
<c:if test="${not empty article.thumbImg }">
<p><img src="/upload/${article.thumbImg }"style="max-width: 360px"/></p>
</c:if>
<p>
<a href="/usr/article/list">목록</a>
<a href="/usr/article/modify?id=${article.id }">수정</a>|
<a href="/usr/article/delete?id=${article.id }" onclick="return confirm('삭제할까요?');">삭제</a>
</p>
</body>
</html>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<!DOCTYPE html>
<html>
<head>
<title>Modify</title>
</head>
<body>
<h1>글 수정</h1>
<form action="/usr/article/modify" method="POST" enctype="multipart/form-data">
<input type="hidden" name="id" value="${article.id}" />
<p>
<label>제목</label>
<br />
<input type="text" name="title" value="${article.title }" required />
</p>
<p>
<label>썸네일(선택: 업로드 시 교체)</label>
<br />
<input type="file" name="file" accept=".png,.jpg,.jpeg,.webp" />
</p>
<c:if test="${not empty article.thumbImg }">
<p>현재 이미지:</p>
<img src="/upload/${article.thumbImg }" style="max-width: 80px" />
</c:if>
<p>
<button type="submit">수정하기</button>
</p>
</form>
<p><a href="/usr/article/detail?id=${article.id }">← 상세</a></p>
</body>
</html>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>글 작성</title>
</head>
<body>
<h1>글 작성</h1>
<form action="/usr/article/write" method="POST" enctype="multipart/form-data">
<p>
<label>제목</label><br/>
<input type="text" name="title" required/>
</p>
<p>
<label>작성자 ID</label><br/>
<input type="number" name="memberId" required>
</p>
<p>
<label>썸네일 이미지(선택)</label><br/>
<input type="file" name="file" accept=".png,.jpg,.jpeg,.webp"/>
</p>
<button type="submit">저장</button>
</form>
<p><a href="/usr/article/list">← 목록</a></p>
</body>
</html>
이미지 추가, 수정 기능을 꼭 넣고 싶었다.(20MB짜리로)
그리고 단순하게나마 CRUD를 구현하면서 리팩토링도 했으니 다행이다.

Title을 통해서 detail에 들어갈 수 있다.

수정으로 제목과 이미지 교체가 가능하다.

이 상태에서 글을 수정하면

제목과 이미지를 바꿀 수 있다.

목록으로 나와서 확인이 가능하다.
session의 loginedMemberId vs article.memberId 비교)