오늘이라도
[Spring] 12. 웹사이트 만들기 ⑪ : 공지글 수정, 목록에서 첨부파일 다운로드, 페이징 밑작업 본문
취업성공패키지 SW 개발자 교육/Spring
[Spring] 12. 웹사이트 만들기 ⑪ : 공지글 수정, 목록에서 첨부파일 다운로드, 페이징 밑작업
upcake_ 2020. 7. 15. 09:26반응형
https://github.com/upcake/Class_Examples
교육 중에 작성한 예제들은 깃허브에 올려두고 있습니다.
gif 파일은 클릭해서 보는 것이 정확합니다.
- 웹사이트 만들기 ⑪ : 공지글 수정, 목록에서 첨부파일 다운로드, 페이징 밑작업 -
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>modify JSP</title>
</head>
<body>
<h3>공지글 수정</h3>
<!--
- 파일 첨부 시 form 반드시 갖고 있어야 할 속성
1. 반드시 method는 post이어야만 한다.
2. enctype을 지정한다. ▶ enctype='multipart/form-data'
-->
<form action="update.no" method="post" enctype="multipart/form-data">
<input type="hidden" name="id" value="${vo.id }"/>
<input type="hidden" name="attach" />
<table>
<tr>
<th class="w-px160">제목</th>
<td><input class="need" type="text" name="title" value="${vo.title }"/></td>
</tr>
<tr>
<th>내용</th>
<td><textarea class="need" name="content">${vo.content }</textarea></td>
</tr>
<tr>
<th>첨부 파일</th>
<td class="left">
<label>
<input id="attach-file" type="file" name="file" />
<img src="img/select.png" class="file-img" />
</label>
<span id="file-name">${vo.filename }</span>
<span id="delete-file" style='display:${empty vo.filename ? "none" : "inline"}; color:red; margin-left:20px;'><i class="fas fa-times font-img"></i></span>
</td>
</tr>
</table>
</form>
<div class="btnSet">
<a class="btn-fill" onclick="if( necessary() ) { $('[name=attach]').val($('#file-name').text()); $('form').submit(); }">저장</a>
<a class="btn-empty" href="detail.no?id=${vo.id }">취소</a>
<!-- <a class="btn-empty" href="javascript:history.go(-1)">취소</a> -->
</div>
<!-- 실시간 갱신을 위해 getTime을 붙여준다 -->
<script type="text/javascript" src="js/need_check.js?v=<%=new java.util.Date().getTime() %>"></script>
<script type="text/javascript" src="js/file_attach.js"></script>
</body>
</html>
▲modify.jsp
/**
* 입력 항목에 입력되어 있는지 여부를 반환하는 함수
*/
function necessary(){
var need = true;
$('.need').each(function(){
if( $(this).val()=='' ){
alert( '입력하세요!' );
$(this).focus();
need = false;
return need;
}
});
return need;
}
//엔터를 누를 경우
$('[name=title]').on('keypress', function(e) {
if(e.keyCode == 13) {
if(necessary()) {$('form').submit(); }
else {return false;}
}
});
▲need_check.js
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>new JSP</title>
</head>
<body>
<h3>신규 공지 글</h3>
<!--
- 파일 첨부 시 form 반드시 갖고 있어야 할 속성
1. 반드시 method는 post이어야만 한다.
2. enctype을 지정한다. ▶ enctype='multipart/form-data'
-->
<form action="insert.no" method="post" enctype="multipart/form-data">
<table>
<tr>
<th class="w-px160">제목</th>
<td><input type="text" name="title" class="need"/></td>
</tr>
<tr>
<th>작성자</th>
<td>${login_info.name }</td>
</tr>
<tr>
<th>내용</th>
<td><textarea name="content" class="need"></textarea></td>
</tr>
<tr>
<th>파일 첨부</th>
<td class="left">
<label>
<input type="file" name="file" id="attach-file"/>
<img src='img/select.png' class="file-img" />
</label>
<span id="file-name"></span>
<span id="delete-file" style="color: red; margin-left: 20px;" ><i class="fas fa-times font-img"></i></span>
</td>
</tr>
</table>
</form>
<div class="btnSet">
<a class="btn-fill" onclick="if(necessary()) $('form').submit()">저장</a>
<a class="btn-empty" href="list.no">취소</a>
</div>
<!-- 실시간 갱신을 위해 getTime을 붙여준다 -->
<script type="text/javascript" src="js/need_check.js?v=<%=new java.util.Date().getTime() %>"></script>
<script type="text/javascript" src="js/file_attach.js"></script>
</body>
</html>
▲new.jsp
package com.hanul.iot;
import java.io.File;
import java.util.HashMap;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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 common.CommonService;
import member.MemberServiceImpl;
import member.MemberVO;
import notice.NoticePage;
import notice.NoticeServiceImpl;
import notice.NoticeVO;
@Controller
public class NoticeController {
@Autowired private NoticeServiceImpl service;
@Autowired private MemberServiceImpl member;
@Autowired private CommonService common;
@Autowired private NoticePage page;
//공지사항 목록화면 요청//////////////////////////////////////////////////////
@RequestMapping("/list.no")
public String list(Model model, HttpSession session, @RequestParam(defaultValue = "1") int curPage) {
//공지사항 클릭 하면 admin으로 자동 로그인
HashMap<String, String> map = new HashMap<String, String>();
//HashMap : 데이터를 담을 자료 구조
map.put("id", "admin");
map.put("pw", "1234");
session.setAttribute("login_info", member.member_login(map));
session.setAttribute("category", "no");
//DB에서 공지 글 목록을 조회해와 목록 화면에 출력
page.setCurPage(curPage);
model.addAttribute("page", service.notice_list(page));
return "notice/list";
}
//신규 공지 글 작성 화면 요청//////////////////////////////////////////////////////
@RequestMapping("/new.no")
public String notice() {
return "notice/new";
}
//신규 공지 글 저장 처리 요청//////////////////////////////////////////////////////
@RequestMapping("/insert.no")
public String insert(MultipartFile file, NoticeVO vo, HttpSession session) {
//첨부한 파일을 서버 시스템에 업로드하는 처리
if( !file.isEmpty() ) {
vo.setFilepath(common.upload("notice", file, session));
vo.setFilename(file.getOriginalFilename());
}
vo.setWriter( ((MemberVO) session.getAttribute("login_info")).getId() );
//화면에서 입력한 정보를 DB에 저장한 후
service.notice_insert(vo);
//목록 화면으로 연결
return "redirect:list.no";
}
//공지글 상세 화면 요청//////////////////////////////////////////////////////
@RequestMapping("/detail.no")
public String detail(int id, Model model) {
//선택한 공지글에 대한 조회수 증가 처리
service.notice_read(id);
//선택한 공지글 정보를 DB에서 조회해와 상세 화면에 출력
model.addAttribute("vo", service.notice_detail(id));
model.addAttribute("crlf", "\r\n");
return "notice/detail";
} //detail()
//첨부파일 다운로드 요청//////////////////////////////////////////////////////
@ResponseBody @RequestMapping("/download.no")
public void download(int id, HttpSession session, HttpServletResponse response) {
NoticeVO vo = service.notice_detail(id);
common.download(vo.getFilename(), vo.getFilepath(), session, response);
} //download()
//공지글 삭제 처리 요청//////////////////////////////////////////////////////
@RequestMapping("/delete.no")
public String delete(int id, HttpSession session) {
//선택한 공지글에 첨부된 파일이 있다면 서버의 물리적 영역에서 해당 파일도 삭제한다
NoticeVO vo = service.notice_detail(id);
if(vo.getFilepath() != null) {
File file = new File(session.getServletContext().getRealPath("resources") + vo.getFilepath());
if( file.exists() ) { file.delete(); }
}
//선택한 공지글을 DB에서 삭제한 후 목록 화면으로 연결
service.notice_delete(id);
return "redirect:list.no";
} //delete()
//공지글 수정 화면 요청//////////////////////////////////////////////////////
@RequestMapping("/modify.no")
public String modify(int id, Model model) {
//선택한 공지글 정보를 DB에서 조회해와 수정화면에 출력
model.addAttribute("vo", service.notice_detail(id));
return "notice/modify";
} //modify()
//공지글 수정 처리 요청//////////////////////////////////////////////////////
@RequestMapping("/update.no")
public String update(NoticeVO vo, MultipartFile file, HttpSession session, String attach) {
//원래 공지글의 첨부 파일 관련 정보를 조회
NoticeVO notice = service.notice_detail(vo.getId());
String uuid = session.getServletContext().getRealPath("resources") + notice.getFilepath();
//파일을 첨부한 경우 - 없었는데 첨부 / 있던 파일을 바꿔서 첨부
if(!file.isEmpty()) {
vo.setFilename(file.getOriginalFilename());
vo.setFilepath(common.upload("notice", file, session));
//원래 있던 첨부 파일은 서버에서 삭제
if( notice.getFilename() != null ) {
File f = new File(uuid);
if ( f.exists() ) { f.delete(); }
}
} else {
//원래 있던 첨부 파일을 삭제됐거나 원래부터 첨부 파일이 없었던 경우
if(attach.isEmpty()) {
//원래 있던 첨부 파일은 서버에서 삭제
if( notice.getFilename() != null ) {
File f = new File(uuid);
if ( f.exists() ) { f.delete(); }
}
//원래 있던 첨부 파일을 그대로 사용하는 경우
} else {
vo.setFilename(notice.getFilename());
vo.setFilepath(notice.getFilepath());
}
}
//화면에서 변경한 정보를 DB에 저장한 후 상세 화면으로 연결
service.notice_update(vo);
return "redirect:detail.no?id=" + vo.getId();
} //update()
} //class
▲NoticeController.java
package notice;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class NoticeServiceImpl implements NoticeService {
@Autowired private NoticeDAO dao;
@Override
public void notice_insert(NoticeVO vo) {
dao.notice_insert(vo);
}
@Override
public List<NoticeVO> notice_list() {
return dao.notice_list();
}
@Override
public NoticeVO notice_detail(int id) {
return dao.notice_detail(id);
}
@Override
public void notice_update(NoticeVO vo) {
dao.notice_update(vo);
}
@Override
public void notice_delete(int id) {
dao.notice_delete(id);
}
@Override
public void notice_read(int id) {
dao.notice_read(id);
}
@Override
public NoticePage notice_list(NoticePage page) {
return dao.notice_list(page);
}
}
▲NoticeServiceImpl.java
package notice;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class NoticeDAO implements NoticeService {
@Autowired private SqlSession sql;
@Override
public void notice_insert(NoticeVO vo) {
sql.insert("notice.mapper.insert", vo);
}
@Override
public List<NoticeVO> notice_list() {
return sql.selectList("notice.mapper.list");
}
@Override
public NoticeVO notice_detail(int id) {
return sql.selectOne("notice.mapper.detail",id);
}
@Override
public void notice_update(NoticeVO vo) {
sql.update("notice.mapper.update", vo);
}
@Override
public void notice_delete(int id) {
sql.delete("notice.mapper.delete", id);
}
@Override
public void notice_read(int id) {
sql.update("notice.mapper.read", id);
}
@Override
public NoticePage notice_list(NoticePage page) {
page.setTotalList((Integer) sql.selectOne("notice.mapper.totalList"));
page.setList(sql.selectList("notice.mapper.list", page));
return page;
}
}
▲NoticeDAO.java
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="notice.mapper" >
<select id="list" resultType="notice.NoticeVO">
SELECT *
FROM (SELECT ROWNUM no, n.*
FROM (SELECT * FROM notice ORDER BY root, step DESC) n
ORDER BY no DESC) n
WHERE no BETWEEN #{beginList} and #{endList}
</select>
<select id="totalList" resultType="integer">
SELECT COUNT(*) FROM notice
</select>
<insert id="insert">
INSERT INTO notice (id, root, title, content, writer, filename, filepath)
VALUES (seq_notice.nextval, seq_notice.currval, #{title }, #{content }, #{writer }, #{filename, jdbcType=VARCHAR }, #{filepath, jdbcType=VARCHAR })
</insert>
<select id="detail" resultType="notice.NoticeVO">
SELECT n.*, (SELECT name FROM member m WHERE m.id = n.writer) name FROM notice n WHERE id=#{id }
</select>
<!-- jdbcType=VARCHAR 속성을 넣으면 null값이 허용된다. -->
<update id="read">
UPDATE notice SET readcnt = readcnt + 1 WHERE id=#{id}
</update>
<delete id="delete">
DELETE FROM notice WHERE id=#{id }
</delete>
<update id="update">
UPDATE notice set title = #{title }, content = #{content }, filename = #{filename, jdbcType=VARCHAR }, filepath = #{filepath, jdbcType=VARCHAR }
WHERE id=#{id }
</update>
<insert id="reply_insert">
<!-- 원글의 step보다 더 큰 step을 가진 글이 있다면 그 글들의 step을 먼저 +1 한다. -->
<![CDATA[
{CALL DECLARE BEGIN
UPDATE notice SET step = step + 1
WHERE root = #{root } AND step > #{step };
INSERT INTO notice (id, root, title, content , writer, step, indent, filename, filepath)
VALUES (seq_notice.NEXTVAL, #{root }, #{title }, #{content }, #{writer }, #{step } + 1, #{indent } + 1, #{filename, jdbcType=VARCHAR }, #{filepath, jdbcType=VARCHAR });
END}
]]>
</insert>
</mapper>
▲notice-mapper.xml
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>list JSP</title>
</head>
<body>
<h3>공지사항</h3>
<div id="list-top">
<div>
<ul>
<core:if test="${login_info.admin eq 'Y' }">
<li><a class="btn-fill" href="new.no">글쓰기</a></li>
</core:if>
</ul>
</div>
</div>
<table>
<tr>
<th class="w-px60">번호</th>
<th>제목</th>
<th class="w-px100">작성자</th>
<th class="w-px120">작성일자</th>
<th class="w-px60">첨부파일</th>
</tr>
<core:forEach items="${page.list }" var="vo">
<tr>
<td>${vo.no }</td>
<td class="left"><a href="detail.no?id=${vo.id }" >${vo.title }</a></td>
<td>${vo.name }</td>
<td>${vo.writedate }</td>
<td>
<core:if test="${!empty vo.filename }">
<a href="download.no?id=${vo.id }">
<img title="${vo.filename }" class="file-img" src="img/attach.png" />
</a>
</core:if>
</td>
</tr>
</core:forEach>
</table>
</body>
</html>
▲list.jsp
--테이블 생성
CREATE TABLE customer (
id NUMBER CONSTRAINT customer_id_pk PRIMARY KEY,
name VARCHAR2(50) NOT NULL,
gender VARCHAR2(3) NOT NULL,
email VARCHAR2(50),
phone VARCHAR2(13)
);
--시퀀스 생성
CREATE SEQUENCE seq_customer START WITH 1 INCREMENT BY 1;
--레코드 삽입
INSERT INTO customer(id, name, gender)
VALUES (seq_customer.NEXTVAL, '홍길동', '남');
INSERT INTO customer(name, gender)
VALUES ('심청', '여');
--트리거(trigger) 설정
CREATE OR REPLACE TRIGGER trg_customer
BEFORE INSERT ON customer --커스터머 테이블에 인서트가 되기전에
FOR EACH ROW --모든 행에 대하여
BEGIN --시작한다
SELECT seq_customer.NEXTVAL INTO :new.id FROM dual; --시퀀스의 데이터를 담고있는 테이블은 없으므로 더미 테이블(dual)에서 조회한다.
END;
/ --끝 슬래쉬까지 써줘야한다
drop trigger trg_customer;
CREATE OR REPLACE TRIGGER trg_customer
BEFORE INSERT ON customer
FOR EACH ROW
BEGIN
SELECT seq_customer.NEXTVAL INTO :new.id FROM dual;
END;
/
--조회
SELECT * FROM customer;
--커밋
COMMIT;
--20/07/02==================================================================
--회원 관리 테이블
CREATE TABLE member(
irum VARCHAR2(20) NOT NULL,
id VARCHAR2(20) CONSTRAINT member_id_pk PRIMARY KEY,
pw VARCHAR2(20) NOT NULL,
age NUMBER,
gender VARCHAR2(3) NOT NULL,
birth DATE,
post VARCHAR2(7),
addr VARCHAR2(50),
email VARCHAR2(50) NOT NULL, --유니크가 들어간다 생각하고 NOT NULL만 지정
tel VARCHAR2(20),
admin VARCHAR2(1) DEFAULT 'N'
);
--기존에 있던 member 테이블 수정
ALTER TABLE member
ADD(
gender VARCHAR2(3) DEFAULT '남' NOT NULL,
birth DATE,
post VARCHAR2(7),
email VARCHAR2(50),
admin VARCHAR2(1) default 'N'
);
UPDATE MEMBER SET email = id || '@naver.com';
ALTER TABLE member
MODIFY (irum NOT NULL, pw NOT NULL, email NOT NULL);
ALTER TABLE member RENAME COLUMN irum TO name;
ALTER TABLE member ADD CONSTRAINT member_id_pk PRIMARY KEY(id);
--관리자 회원 정보 저장
INSERT INTO member(name, id, pw, age, gender, email, admin)
VALUES ('관리자', 'admin', '1234', 25, '남', 'admin@admin.com', 'Y');
SELECT * FROM member;
--20/07/10======================================================================
CREATE TABLE notice(
id NUMBER CONSTRAINT notice_id_pk PRIMARY KEY,
title VARCHAR2(300) NOT NULL,
content VARCHAR2(4000) NOT NULL,
writer VARCHAR2(20) NOT NULL,
writedate DATE DEFAULT SYSDATE,
readcnt NUMBER DEFAULT 0,
filename VARCHAR2(300),
filepath VARCHAR2(300)
);
CREATE SEQUENCE seq_notice
START WITH 1 INCREMENT BY 1;
CREATE OR REPLACE TRIGGER trg_notice
BEFORE INSERT ON notice
FOR EACH ROW
BEGIN
SELECT seq_notice.NEXTVAL INTO:NEW.id FROM dual;
END;
/
SELECT * FROM notice;
INSERT INTO notice(title, content, writer)
VALUES ('공지 글 테스트', '테스트 공지 글 입니다.', 'admin');
COMMIT;
--20/07/15================================================
--테이블에 공지글 항목 추가
INSERT INTO notice(title, content ,writer, writedate, filepath, filename)
SELECT title, content, writer, writedate, filepath, filename FROM notice;
COMMIT;
▲table.sql
package common;
public class PageVO {
private int pageList = 10; //페이지당 목록수
private int blockPage = 10; //블럭당 페이지수
private int totalList; //총목록수
private int totalPage; //총페이지수
//157 페이지 = 총목록수/페이지당 목록수 + 나머지가 있으면+1
private int totalBlock; //총블럭수
//16 블럭 = 총페이지수/블럭당 페이지수 + 나머지가 있으면+1
private int curPage; //현재페이지번호
private int beginList, endList; //현재페이지의 시작/끝 목록번호
private int beginPage, endPage; //현재블럭의 시작/끝 페이지번호
public int getPageList() {
return pageList;
}
public void setPageList(int pageList) {
this.pageList = pageList;
}
public int getBlockPage() {
return blockPage;
}
public void setBlockPage(int blockPage) {
this.blockPage = blockPage;
}
public int getTotalList() {
return totalList;
}
public void setTotalList(int totalList) {
this.totalList = totalList;
//총페이지수=총목록수/페이지당보여질목록수
//576/10 --> 57 ..6 -> 58페이지
totalPage = totalList / pageList;
if( totalList % pageList >0 ) ++totalPage;
//총블럭수=총페이지수/블럭당보여질페이지수
//58/10 --> 5..8 -> 6블럭
totalBlock = totalPage / blockPage;
//시작/끝 목록번호
//끝목록번호: 576, 566, 556,
endList = totalList - (curPage-1) * pageList;
//시작목록번호: 567, 557, 547,
//= 끝목록번호 - (페이지당보여질목록수-1)
beginList = endList - (pageList-1);
//현재 블럭번호
curBlock = curPage / blockPage;
if( curPage % blockPage > 0 ) ++curBlock;
//시작/끝 페이지번호
//끝페이지번호: 10, 20, 30, ...
endPage = curBlock * blockPage;
//시작페이지번호 : 1, 11, 21, ...
beginPage = endList - (blockPage-1);
//2048건 ▶ 1페이지 : 2048 ~ 2039, 1 ~ 10
// 205페이지 : 8 ~ 1, 51 ~ 58
//끝 페이지 번호가 총 페이지 번호보다 크면 총 페이지 번호가 끝 페이지 번호이다.
if(endPage > totalPage) {endPage = totalPage; }
}
private int curBlock; //현재블럭번호
public int getCurBlock() {
return curBlock;
}
public void setCurBlock(int curBlock) {
this.curBlock = curBlock;
}
public int getTotalPage() {
return totalPage;
}
public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}
public int getTotalBlock() {
return totalBlock;
}
public void setTotalBlock(int totalBlock) {
this.totalBlock = totalBlock;
}
public int getCurPage() {
return curPage;
}
public void setCurPage(int curPage) {
this.curPage = curPage;
}
public int getBeginList() {
return beginList;
}
public void setBeginList(int beginList) {
this.beginList = beginList;
}
public int getEndList() {
return endList;
}
public void setEndList(int endList) {
this.endList = endList;
}
public int getBeginPage() {
return beginPage;
}
public void setBeginPage(int beginPage) {
this.beginPage = beginPage;
}
public int getEndPage() {
return endPage;
}
public void setEndPage(int endPage) {
this.endPage = endPage;
}
}
▲PageVO.java
package notice;
import java.util.List;
import org.springframework.stereotype.Component;
import common.PageVO;
@Component
public class NoticePage extends PageVO {
private List<NoticeVO> list;
public List<NoticeVO> getList() {
return list;
}
public void setList(List<NoticeVO> list) {
this.list = list;
}
}
▲NoticePage.java
package notice;
import java.util.List;
public interface NoticeService {
//CRUD : Create, Read, Update, Delete
void notice_insert(NoticeVO vo); //공지글 저장
List<NoticeVO> notice_list(); //공지글 목록 조회
NoticePage notice_list(NoticePage page); //페이지 처리 된 공지글 목록 조회
NoticeVO notice_detail(int id); //공지글 상세 조회
void notice_update(NoticeVO vo); //공지글 변경 저장
void notice_delete(int id); //공지글 삭제
void notice_read(int id); //조회수 증가 처리
}
▲NoticeService.java
반응형
'취업성공패키지 SW 개발자 교육 > Spring' 카테고리의 다른 글
[Spring] 14. 웹사이트 만들기 ⑬ : 답글 처리, 검색 기능 (0) | 2020.07.17 |
---|---|
[Spring] 13. 웹사이트 만들기 ⑫ : 페이징, 답글 작성 (0) | 2020.07.16 |
[Spring] 11. 웹사이트 만들기 ⑩ : 파일 첨부, 업로드, 다운로드, 글 삭제 (0) | 2020.07.14 |
[Spring] 10. 웹사이트 만들기 ⑨ : 공지사항에 첨부 파일 기능 추가 (2) | 2020.07.13 |
[Spring] 9. 웹사이트 만들기 ⑧ : 첨부 파일 메일, HTML 메일 발송, 공지사항 목록 조회 (1) | 2020.07.10 |