[Springfolio] 6편 - 이미지 교체 + UX 개선 + 리펙토링

Jlee7362·2025년 8월 10일
post-thumbnail

이번에 CRUD와 이미지 수정을 모두 작업한 뒤 리펙토링까지 했다.


1. 수정 폼 + 처리 (이미지 교체 포함)

1-1. Controller

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";
	}

}

1-2. Service

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에 저장
	}

}

1-3. Repository

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);
  
}

1-4. VO

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;

}

1-5. 매퍼(Interface/XML)

<?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>

2. JSP

2-1. list.jsp

<%@ 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>

2-1. detail.jsp

<%@ 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>

2-1. modify.jsp

<%@ 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>

2-1. write.jsp

<%@ 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에 들어갈 수 있다.

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

이 상태에서 글을 수정하면

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

목록으로 나와서 확인이 가능하다.


해야할 것들

UI개선

  • Header + Footer 추가: 중복되는 코드 줄이고, 페이지 이동시 메뉴바(header)를 항시 보이게 한다.

기능추가

  • Member에 관한 기능 추가: 회원가입, 로그인, 로그아웃 기능 추가로 추후 권한체크도 가능하게 한다.
  • 권한체크: 작성자만 수정/삭제 가능하게 (sessionloginedMemberId vs article.memberId 비교)
  • 검증: 제목/본문 공백 방지, 파일 확장자·용량 제한

0개의 댓글