PostService
// 게시글 등록 서비스
public void insertPost(String userId, PostRegisterForm form) {
Post post = new Post();
post.setUserId(userId);
BeanUtils.copyProperties(form, post);
postMapper.insertPost(post);
// SPRING_POST_ATTACHED_FILES 테이블에 게시글 첨부파일 정보 저장
if(form.getFilename() !=null ) {
AttachedFile attachedFile = new AttachedFile();
attachedFile.setPostNo(post.getNo());
attachedFile.setFilename(form.getFilename());
postMapper.insertAttachedFile(attachedFile);
}
}
첨부파일, 태그가 없는 경우
등록폼의 정보 PostRegisterForm [title=첨부파일, 태그가 없는 경우, content=내용입니다., upfile=MultipartFile[field="upfile", filename=, contentType=application/octet-stream, size=0], tags=null, filename=null]
- 첨부파일이 없어도 multipartfile은 null이 아니다.
- tag는 없으면 null이다.
첨부파일은 있고 태그는 없는 경우
등록폼의 정보 PostRegisterForm [title=첨부파일은 있고 태그는 없는 경우, content=내용, upfile=MultipartFile[field="upfile", filename=1.hwp, contentType=application/haansofthwp, size=13312], tags=null, filename=null]
첨부파일도 있고 태그도 있는 경우
등록폼의 정보 PostRegisterForm [title=첨부파일도 있고 태그도 있는 경우, content=내용, upfile=MultipartFile[field="upfile", filename=1.hwp, contentType=application/haansofthwp, size=13312], tags=[태그1, 태그2, 태그3], filename=null]
- title과 content, upfile은 입력을 하지 않아도 hidden필드가 아니므로 null이 아니다.
- tag는 hidden이고 동적으로 추가되기 때문에 아무것도 없으면 null이 나올 수 있다.
테이블 생성
CREATE TABLE SPRING_POST_TAGS ( POST_NO NUMBER(5) NOT NULL CONSTRAINT SPRING_TAG_POST_NO_FK REFERENCES SPRING_POSTS (POST_NO), TAG_CONTENT VARCHAR2(100) NOT NULL );
- 테이블의 생성 순서는 부모 -> 자식 -> 손자 순으로
- 테이블의 삭제 순서는 손자 -> 자식 -> 부모 순으로 한다.
Tag.java
package com.sample.vo;
import org.apache.ibatis.type.Alias;
@Alias("Tag")
public class Tag {
private int postNo;
private String content;
public Tag() {}
public Tag(int postNo, String content) {
this.postNo = postNo;
this.content = content;
}
public int getPostNo() {
return postNo;
}
public void setPostNo(int postNo) {
this.postNo = postNo;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
PostMapper.java
void insertAttachedFile(AttachedFile attachedFile);
// 태그 저장
void insertTag(Tag tag);
// 첨부파일 가지고 오는 것(우리는 하나이기 때문에 List를 붙이지 않지만, 원래는 첨부파일을 많이 넣기 때문에 List를 붙여야 한다.)
AttachedFile getAttachedFilesByPostNo(int postNo);
// 태그 가지고 오는 것
List<Tag> getTagsByPostNo(int postNo);
posts.xml
<!--
void insertTag(Tag tag);
-->
<insert id="insertTag" parameterType="Tag">
insert into spring_post_tags
(post_no, tag_content)
values
(#{postNo}, #{content})
</insert>
<!--
List<AttachedFile> getAttachedFilesByPostNo(int postNo);
-->
<select id="getAttachedFilesByPostNo" parameterType="int" resultType="AttachedFile">
select
post_no as postNo,
file_name as filename
from
spring_post_attached_files
where
post_no = #{value}
</select>
<!--
List<Tag> getTagsByPostNo(int postNo)
-->
<select id="getTagsByPostNo" parameterType="int" resultType="Tag">
select
post_no as postNo,
tag_content as content
from
spring_post_tags
where
post_no = #{value}
</select>
PostService.java
// SPRING_POST_TAGS 테이블에 게시글 태그정보 저장
if (form.getTags() != null) {
List<String> tags = form.getTags();
for (String tagContent : tags) {
Tag tag = new Tag(post.getNo(), tagContent);
postMapper.insertTag(tag);
}
}
실행결과
PostService의 두가지 역할
- 입력과 반대로 각각의 테이블에서 정보를 가지고 와서 Dto에 담는다.
- Dto는 여러 테이블의 정보를 하나로 담을 수 있게 해준다.
- 출력작업 : Dto에 정보를 모두 담아서 저장하는 작업을 한다.
- 입력작업 : Form객체를 정의하고, 각 테이블에 저장할 값을 VO 객체로 복사해서 테이블에 저장시키는 작업을 한다.
PostDetailDto.java
// 게시글 정보
private int no;
private String title;
private String userId;
private String userName;
private int readCount;
private int commentCount;
private Date createdDate;
private Date updatedDate;
private String content;
// 댓글정보
private List<PostCommentListDto> comments;
// 첨부파일 정보
private List<AttachedFile> attahcedFiles;
// 태그 정보
private List<Tag> tags;
PostService.java
// 댓글 정보 조회
List<PostCommentListDto> postCommentListDtos = postCommentMapper.getPostCommentsByPostNo(postNo);
postDetailDto.setComments(postCommentListDtos);
// 첨부파일 정보 조회
List<AttachedFile> attachedFiles = postMapper.getAttachedFileByPostNo(postNo);
postDetailDto.setAttachedFiles(attachedFiles);
// 태그 정보 조회
List<Tag> tags = postMapper.getTagsByPostNo(postNo);
postDetailDto.setTags(tags);
return postDetailDto;
detail.jsp
<tr>
<th>첨부파일</th>
<td colspan="3">
<c:choose>
<c:when test="${empty post.attachedFiles }">
등록된 첨부파일이 없습니다.
</c:when>
<c:otherwise>
<!-- list라 반복처리 해야 한다. -->
<c:forEach var="file" items="${post.attachedFiles }">
<a href="" class="btn btn-outline-dark btn-sm">${file.filename }<i class="bi bi-download ms-2"></i></a>
</c:forEach>
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<th>태그</th>
<td colspan="3">
<c:choose>
<c:when test="${empty post.tags }">
등록된 태그가 없습니다.
</c:when>
<c:otherwise>
<c:forEach var="tag" items="${post.tags }">
<span class="badge text-bg-secondary">#${tag.content }</span>
</c:forEach>
</c:otherwise>
</c:choose>
</td>
</tr>
실행결과
PostController
@GetMapping("/download")
public ModelAndView fileDownload(@RequestParam("filename") String filename) {
// 지정된 파일정보를 표현하는 File객체 생성, 파일이 존재하지 않더라도 File객체는 생성할 수 있다.(파일이 존재하지 않더라도 null은 아니다.)
File file = new File(directory, filename);
// 파일이 존재하지 않으면 예외를 던진다.
if(!file.exists()) {
throw new ApplicationException("["+filename+"] 파일이 존재하지 않습니다.");
}
ModelAndView mav = new ModelAndView();
// ModelAndView의 Model에 값 저장
mav.addObject("file", file);
// ModelAndView의 View에 DownloadView객체 저장
mav.setView(fileDownloadView);
return mav;
}
detail.jsp
<tr>
<th>첨부파일</th>
<td colspan="3">
<c:choose>
<c:when test="${empty post.attachedFiles }">
등록된 첨부파일이 없습니다.
</c:when>
<c:otherwise>
<!-- list라 반복처리 해야 한다. -->
<c:forEach var="file" items="${post.attachedFiles }">
<a href="download?filename=${file.filename }" class="btn btn-outline-dark btn-sm">${file.filename }<i class="bi bi-download ms-2"></i></a>
</c:forEach>
</c:otherwise>
</c:choose>
</td>
</tr>
FileDownloadView
package com.sample.web.view;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;
import com.sample.exception.ApplicationException;
@Component
// 보통 view를 직접 구현하지 않고 AbstractView를 통해 구현한다.
public class FileDownloadView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
File file = (File) model.get("file");
// application/octet-stream - 일반적인 바이너리 데이터에 대한 컨텐츠 타입이다.(메모장으로 읽을 수 없는 타입)
setContentType("application/octet-stream");
// 응답메세지의 헤더부에 다운로드되는 첨부파일을 이름으로 설정한다.
// attachment;는 브라우저에서 파일을 열지 않고, 항상 다운로드되게 한다.
// URLEncoder.encode(text, encoding)은 텍스트를 지정된 인코딩 방식으로 변환시킨다.
// 텍스트에 한글이 포함되어 있는 경우 utf-8방식으로 인코딩(변환)하지 않으면 한글이 전부 깨진다.
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(file.getName(), "utf-8"));
// 파일을 읽어오는 스트림 객체를 생성한다.
InputStream in = new FileInputStream(file);
// 브라우저와 연결된 출력스트림을 획득한다.
OutputStream out = response.getOutputStream();
// 입력스트림으로 읽은 데이터를 출력스트림으로 복사해서 출력시킨다.
IOUtils.copy(in, out);
}
}
실행결과