@ mavenBoard 만들기 3 (페이징 재도전3)
- freeBoardMapper.xml
<select id="getDataCount" resultType="int" parameterType="hashMap">
select nvl(count(*),0) from freeboard
where ${searchKey} like '%' || #{searchValue} || '%'
</select>
<select id="getLists" resultType="freeBoardDto" parameterType="map">
select * from (
select rownum rnum, data.* from (
select num,title,name,TO_CHAR(REGDATE,'YYYY/MM/DD') regdate,content
from freeboard where ${searchKey} like '%' || #{searchValue} || '%'
order by num desc) data)
<![CDATA[
where rnum >= #{start} and rnum <= #{end}
]]>
</select>
- FreeBoardService
public List getLists(int start,int end,String searchKey,String searchValue) {//페이징할 번호의 시작과 끝
Map<String, Object> params = new HashMap<String, Object>();
params.put("start", start);
params.put("end", end);
params.put("searchKey", searchKey);
params.put("searchValue", searchValue);
List lists = sqlSessionTemplate.selectList("getLists", params);
return lists;
}
public int getDataCount(String searchKey,String searchValue) {
int totalDataCount = 0;
Map<String, Object> params = new HashMap<String, Object>();
params.put("searchKey", searchKey);
params.put("searchValue", searchValue);
totalDataCount = sqlSessionTemplate.selectOne("getDataCount", params);
return totalDataCount;
}
- FreeBoardController.java
@RequestMapping("/main.ino")
public ModelAndView main(HttpServletRequest request,Criteria cri) {
ModelAndView mav = new ModelAndView();
//http://localhost:8080/mavenBoard/
//http://localhost:8080/mavenBoard/main.ino
String cp = request.getContextPath();
String pageNum = request.getParameter("pageNum");
int currentPage = 1;
if(pageNum!=null) {
currentPage = Integer.parseInt(pageNum);
}
String searchKey = request.getParameter("searchKey");
String searchValue = request.getParameter("searchValue");
if(searchValue==null) {
searchKey = "title";
searchValue = "";
}else {
if(request.getMethod().equalsIgnoreCase("GET")) {
//searchValue = URLDecoder.decode(searchValue, "UTF-8");
}
}
int dataCount = freeBoardService.getDataCount(searchKey, searchValue);
int numPerPage = 5;
int totalPage = myUtil.getPageCount(numPerPage, dataCount);
if(currentPage>totalPage) {
currentPage = totalPage;
}
int start = (currentPage-1)*numPerPage+1;
int end = currentPage*numPerPage;
List lists = freeBoardService.getLists(start, end, searchKey, searchValue);
String listUrl = cp + "/main.ino";
//페이징
String pageIndexList = myUtil.pageIndexList(currentPage, totalPage, listUrl);
//int rowStart = cri.getRowStart();
//int rowEnd = cri.getRowEnd();
//int pageNum = cri.getPageNum();
//int amount = cri.getAmount();
//String keyword = cri.getKeyword();
//String type = cri.getType();
//List list = freeBoardService.freeBoardList();
//List list = freeBoardService.freeBoardList2(cri);
//PageDTO pageDTO = new PageDTO();
//pageDTO.setCri(cri);
//pageDTO.setTotalCount(freeBoardService.listCount());
//int total = freeBoardService.getTotal(cri);
mav.setViewName("boardMain"); //뷰의 경로 - tiles.xml에 되어있다.
//mav.addObject("freeBoardList",list); //변수이름,데이터값
//mav.addObject("pageMaker",new PageDTO(cri,total));
//mav.addObject("pageMaker",new PageDTO(cri, 123)); //전체 글 개수 설정
//mav.addObject("pageMaker",new PageDTO(total));
mav.addObject("freeBoardList", lists);
mav.addObject("pageIndexList", pageIndexList);
mav.addObject("dataCount", dataCount);
return mav;
}
- freeBoardMain.jsp
<div>
<h1>자유게시판</h1>
</div>
<div style="width:650px;" align="right">
<a href="./freeBoardInsert.ino">글쓰기</a>
</div>
<hr style="width: 600px">
<c:forEach items="${freeBoardList }" var="dto">
<div style="width: 50px; float: left;">${dto.num }</div>
<div style="width: 300px; float: left;"><a href="./freeBoardDetail.ino?num=${dto.num }">${dto.title }</a></div>
<%--
<div style="width: 300px; float: left;">
<a class="move" href="<c:out value="${dto.num }"/>">
<c:out value="${dto.title }"/>
</a>
</div>
--%>
<div style="width: 150px; float: left;">${dto.name }</div>
<div style="width: 150px; float: left;">${dto.regdate }</div>
<hr style="width: 600px">
</c:forEach>
<!-- 검색 -->
<div id="leftHeader">
<form action="" name="searchForm" method="post">
<select name="searchKey" class="selectField">
<option value="title">제목</option>
<option value="name">작성자</option>
<option value="content">내용</option>
</select>
<input type="text" name="searchValue" class="textField"/>
<input type="button" value=" 검 색 " class="btn2" onclick="sendIt()"/>
</form>
</div>
<!-- 페이징 -->
<div>
<c:if test="${dataCount!=0 }">
${pageIndexList }
</c:if>
<c:if test="${dataCount==0 }">
등록된 게시물이 없습니다.
</c:if>
</div>
- MyUtil.java
//페이징 처리 클래스
@Component("myUtil")
public class MyUtil {
//전체 페이지 수 구하기
//numPerPage : 한 화면에 표시할 데이터의 갯수 (페이지수)
//dataCount : 전체데이터 갯수
public int getPageCount(int numPerPage, int dataCount) {//(3,34)
int pageCount = 0; //페이지 수 (나눠진 전체 페이지 수 : 12페이지)
//11.xx = 34/3
pageCount = dataCount / numPerPage;
//나머지가 있으면 페이지를 만들어라 (나머지 없으면 페이지 추가할 필요 없다.)
if(dataCount % numPerPage != 0) {
pageCount++;
}
//12
return pageCount;
}
//페이징 처리 메소드
//currentPage : 현재 표시할 페이지
//totalPage : 전체 페이지 수
//listUrl : 보여줄 링크를 설정할 url
//매개변수(현재 보고자하는 페이지, 전체페이지, list.jsp)
public String pageIndexList(int currentPage,int totalPage,String listUrl) {
int numPerBlock = 5; //뿌려지는 페이지 목록 개수 (◀이전 6 7 8 9 10 ▶다음)
int currentPageSetup; //◀이전,▶다음 눌렀을 때 값 (표시할 첫 페이지 - 1 한 값)
int page; //포문 페이지 설정, 페이지로 뿌려질 숫자
StringBuffer sb = new StringBuffer(); //스트링 반환값 쓰려고 버퍼에 누적
if(currentPage==0||totalPage==0) {//검증 : 현재 페이지 또는 전체 페이지 없으면 멈춰
return "";
}
//list.jsp
//list.jsp?searchKey=subject&searchValue=1
//?가 없으면 검색X, ?있다면 검색O
if(listUrl.indexOf("?")!=-1) {//?가 있다면
listUrl += "&"; //&붙여라
}else {
listUrl += "?"; //?붙여라
}
//값을 넘길 준비가 됨
//이전페이지 번호 구하기
//예시:◀이전 6 7 8 9 10 ▶다음 여기서 이전 눌르면 5페이지 나오도록 하는 공식!!
//◀이전 눌렀을 때 = (현재 페이지 / 한 화면에 표시할 데이터의 갯수)*뿌려지는 페이지 목록 개수
//currentPageSetup = 0아니면 5,10 ... 즉 이전페이지 눌렀을 때 가야할 페이지
currentPageSetup = (currentPage/numPerBlock)*numPerBlock; //컴퓨터 연산으론 3/5가 0이니 0*5는 0이다.
//5로 나누어 떨어질뗀 - 5해서 0만드는 수식!!
if(currentPage % numPerBlock == 0) { //currentPage가 10페이지 자리 수 일 때[(현재 페이지/뿌려지는 페이지 목록 개수) = 0] 나눈 나머지 0일때
currentPageSetup = currentPageSetup - numPerBlock; //5=5-5
}
//◀이전 나타내기 위한 if문
//1 2 3 4 5 페이지일 땐 이전 안 만들기 이것 빼고는 다 만든다.
//12 > 5 && 0 > 0 - 이전 글자 안나옴 currentPageSetup=0 (이전페이지 눌렀을 때 가야할 페이지)
//12 > 5 && 5 > 0 - 이전 글자 나옴 currentPageSetup=5
if(totalPage > numPerBlock && currentPageSetup > 0) {//전체페이지 > 뿌려지는 페이지 목록 개수 &그리고& 이전 페이지 번호 > 0 (1 2 3 4 5는 이전 없기 때문)
//\" 들어가는 값 \" 는 <a href=> 안의 ""를 문자로 표시한 것 //listUrl에 ?붙어있다.
sb.append("<a href=\"" + listUrl + "pageNum=" + currentPageSetup + "\">◀이전</a> ");
//<a href="list.jsp?pageNum=5">◀이전</a> //6에서 이전했을 때
}
//바로가기 페이지
//페이지 = 이전 페이지 + 1;
//이전값은 정했으니 첫값 정하기 (1,6,...)
page = currentPageSetup + 1; //★ 이전 바로 다음에 오는 숫자
//여기서 무조건 currentPageSetup은 5의 배수
//◀이전 6 7 8 9 10 ▶
//5에서 6눌렀다고 가정
//6 <= 12(전체 페이지) && 6(누른 페이지) <= (5(이전페이지 눌렀을 때 가야할 페이지)+5(페이지블럭))) 즉 페이지 6부터 시작해서 10까지 찍어라
//페이지가 전체 데이터보다 작거나 같을때까지
//찍은페이지 <= 전체페이지(12) && 찍은페이지 <= 이전페이지 눌렀을 때 가야할 페이지(5의 배수) + 페이지블럭(5)
//최종페이지가 전체페이지만큼만 출력되도록 잡아줌.
while(page <= totalPage && page <= (currentPageSetup + numPerBlock)) {
//(지금찍은 페이지6 <= 12 (여기까진 7개 출력되야 하는데)&& 6 <= (5+5)=10까지만 보게 설정
//현재 내가본 페이지 색깔 바꾸기
if(page==currentPage) {
//누적
sb.append("<font color=\"Fuchsia\">" + page + "</font> "); //내가보는 페이지는 색깔만 입히고 링크 안걸기
//<font color="Fuchsia">2</font>
}else {
sb.append("<a href=\"" + listUrl + "pageNum=" + page + "\">" + page + "</a> "); //그 외는 링크 걸기 -> get방식으로 다이렉트로 전달하여 페이지 이동
//<a href="list.jsp?pageNum=9">9</a>
}
//계속 증가
page++;
}//빠져나오면 숫자는 1~5,6~10,.. 찍음
//▶다음 찍어줄지 말지, 최종페이지인지 체크
//예시:◀이전 6 7 8 9 10 ▶다음
//◀이전 11 12 는 더 만들어지면 안되니
//(전체 페이지 - 이전페이지 눌렀을 때 가야할 페이지 > 페이지블럭)
//(12-5) > 5 - 다음O
//(12-10) >5 - 다음X
if(totalPage - currentPageSetup > numPerBlock) {
sb.append("<a href=\"" + listUrl + "pageNum=" + page + "\">▶다음</a> ");
//<a href="list.jsp?pageNum=11">▶다음</a>
}
//5,10,... 이상이면 빠져나와서 ▶다음 링크 건다
//즉 11,12 이후 ▶다음 나올지 유무를 적용하는 곳
return sb.toString(); //스트링 버퍼를 스트링으로 변환!!
}
}
페이징 처리 클래스(MyUtil.java)를 만들어 페이징을 구현하였다.
@ mavenBoard 만들기 4 (파일 게시판 만들기)
https://melonpeach.tistory.com/51 - 참고
1. DB 설계
CREATE TABLE MP_BOARD(
BNO NUMBER(5),
TITLE VARCHAR2(100),
CONTENT VARCHAR2(1000),
WRITER VARCHAR2(20),
REGDATE DATE,
CONSTRAINT PK_MP_BOARD PRIMARY KEY (BNO)
);
CREATE TABLE MP_FILE(
FILE_NO NUMBER,
BNO NUMBER NOT NULL,
ORG_FILE_NAME VARCHAR2(260) NOT NULL,
STORED_FILE_NAME VARCHAR2(36) NOT NULL,
FILE_SIZE NUMBER,
REGDATE DATE,
DEL_GB VARCHAR2(1) DEFAULT 'N' NOT NULL,
CONSTRAINT PK_MP_FILE PRIMARY KEY (FILE_NO)
);
-- (파일 번호, 게시판 번호, 원본 파일 이름, 변경된 파일 이름, 파일 크기, 파일 등록일, 삭제 구분)
CREATE SEQUENCE MP_BOARD_SEQ
START WITH 1
INCREMENT BY 1
NOMAXVALUE NOCACHE;
CREATE SEQUENCE MP_FILE_SEQ
START WITH 1
INCREMENT BY 1
NOMAXVALUE NOCACHE;
2. Maven Dependency 추가
- pom.xml
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
3. MultipartResolver 설정 코딩 추가
- lime-dispatcher.xml (web.xml의 contextConfigLocation에 등록된 경로)
<!-- MultipartResolver 설정 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="100000000" />
<property name="maxInMemorySize" value="100000000" />
</bean>
4. 첨부파일의 정보이용하여 여러가지 조작 할 클래스 생성, DTO 생성
- FileUtil.java
@Component("fileUtil")
public class FileUtil {
private static final String filePath = "C:\\mp\\file\\"; //파일이 저장될 위치
public Map<String, Object> parseInsertFileInfo(BoardDTO boardDTO,
MultipartHttpServletRequest mpRequest) throws Exception{
/*
Iterator은 데이터들의 집합체? 에서 컬렉션으로부터 정보를 얻어올 수 있는 인터페이스입니다.
List나 배열은 순차적으로 데이터의 접근이 가능하지만, Map등의 클래스들은 순차적으로 접근할 수가 없습니다.
Iterator을 이용하여 Map에 있는 데이터들을 while문을 이용하여 순차적으로 접근합니다.
*/
Iterator<String> iterator = mpRequest.getFileNames();
MultipartFile multipartFile = null;
String originalFileName = null;
String originalFileExtension = null;
String storedFileName = null;
List<Map<String, Object>> list = new ArrayList<Map<String,Object>>();
Map<String, Object> listMap = null;
int bno = boardDTO.getBno();
File file = new File(filePath);
if(file.exists() == false) {
file.mkdirs();
}
while(iterator.hasNext()) {
multipartFile = mpRequest.getFile(iterator.next());
if(multipartFile.isEmpty() == false) {
originalFileName = multipartFile.getOriginalFilename();
originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf(".")); //확장자
storedFileName = getRandomString() + originalFileExtension;
file = new File(filePath + storedFileName);
multipartFile.transferTo(file);
listMap = new HashMap<String, Object>();
listMap.put("BNO", bno);
listMap.put("ORG_FILE_NAME", originalFileName);
listMap.put("STORED_FILE_NAME", storedFileName);
listMap.put("FILE_SIZE", multipartFile.getSize());
//list.add(listMap);
}
}
//return list;
return listMap;
}
public static String getRandomString() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
- BoardDTO.java
@Alias("boardDTO")
public class BoardDTO {
private int bno;
private String title;
private String content;
private String writer;
private String regdate;
public int getBno() {
return bno;
}
public void setBno(int bno) {
this.bno = bno;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public String getRegdate() {
return regdate;
}
public void setRegdate(String regdate) {
this.regdate = regdate;
}
}
5. 컨트롤러 생성
- BoardController.java
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
@Autowired
@Qualifier("myUtil")
MyUtil myUtil;
@Autowired
@Qualifier("fileUtil")
FileUtil fileUtil;
@RequestMapping("/insert.ino")
public ModelAndView insert() {
ModelAndView mav = new ModelAndView();
mav.setViewName("insert");
return mav;
}
@RequestMapping("/insertPro.ino")
public ModelAndView insertPro(BoardDTO boardDTO, MultipartHttpServletRequest mpRequest) throws Exception {
boardService.insertPro(boardDTO);
//List<Map<String,Object>> list = fileUtil.parseInsertFileInfo(boardDTO, mpRequest); //위 selectKey에서 저절로 dto로 넣어진 bno 들어있다.
Map<String, Object> map = fileUtil.parseInsertFileInfo(boardDTO, mpRequest);
System.out.println(map);
boardService.insertFile(map);
/*
int size = list.size();
for(int i=0;i<size;i++) {//list의 size만큼 넣어주는 이유는 나중에 여러개의 첨부파일을 등록하기 위함
boardService.insertFile(list.get(i));
}
*/
ModelAndView mav = new ModelAndView();
mav.setViewName("redirect:/detail.ino?bno="+boardDTO.getBno());
return mav;
}
@RequestMapping("/list.ino")
public ModelAndView list(HttpServletRequest request) {
//http://localhost:8080/mavenBoard/list.ino
String cp = request.getContextPath();
String pageNum = request.getParameter("pageNum");
int currentPage = 1;
if(pageNum!=null) {
currentPage = Integer.parseInt(pageNum);
}
String searchKey = request.getParameter("searchKey");
String searchValue = request.getParameter("searchValue");
if(searchValue==null) {
searchKey = "writer";
searchValue = "";
}else {
if(request.getMethod().equalsIgnoreCase("GET")) {
//searchValue = URLDecoder.decode(searchValue, "UTF-8");
}
}
int dataCount = boardService.getDataCount(searchKey, searchValue);
int numPerPage = 5;
int totalPage = myUtil.getPageCount(numPerPage, dataCount);
if(currentPage>totalPage) {
currentPage = totalPage;
}
int start = (currentPage-1)*numPerPage+1;
int end = currentPage*numPerPage;
List lists = boardService.getLists(start, end, searchKey, searchValue);
String listUrl = cp + "/list.ino";
//페이징
String pageIndexList = myUtil.pageIndexList(currentPage, totalPage, listUrl);
ModelAndView mav = new ModelAndView();
mav.setViewName("list");
mav.addObject("lists", lists);
mav.addObject("pageIndexList", pageIndexList);
mav.addObject("dataCount", dataCount);
return mav;
}
@RequestMapping("/detail.ino")
public ModelAndView detail(HttpServletRequest request) {
int bno = Integer.parseInt(request.getParameter("bno"));
BoardDTO dto = boardService.getDetailByNum(bno);
ModelAndView mav = new ModelAndView();
mav.setViewName("detail");
mav.addObject("dto",dto);
return mav;
}
}
6. 서비스 생성
- BoardService.java
@Service
public class BoardService {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
public void insertPro(BoardDTO boardDTO) {
sqlSessionTemplate.insert("boardMapper.insert", boardDTO);
}
public void insertFile(Map<String,Object> map) {
sqlSessionTemplate.insert("boardMapper.insertFile",map);
}
public List getLists(int start,int end,String searchKey,String searchValue) {
Map<String, Object> params = new HashMap<String, Object>();
params.put("start", start);
params.put("end", end);
params.put("searchKey", searchKey);
params.put("searchValue", searchValue);
List lists = sqlSessionTemplate.selectList("boardMapper.getLists", params);
return lists;
}
public int getDataCount(String searchKey,String searchValue) {
int totalDataCount = 0;
Map<String, Object> params = new HashMap<String, Object>();
params.put("searchKey", searchKey);
params.put("searchValue", searchValue);
totalDataCount = sqlSessionTemplate.selectOne("boardMapper.getDataCount", params);
return totalDataCount;
}
public BoardDTO getDetailByNum(int bno) {
return sqlSessionTemplate.selectOne("getDetail",bno);
}
}
7. View 생성
- insert.jsp
<div>
<h1>자료실</h1>
</div>
<div style="width:650px;" align="right">
<a href="./list.ino">리스트로</a>
</div>
<hr style="width: 600px">
<form action="./insertPro.ino" method="post" name="myForm" enctype="multipart/form-data">
<div style="width: 150px; float: left;">이름 :</div>
<div style="width: 500px; float: left;" align="left"><input type="text" name="writer"/></div>
<div style="width: 150px; float: left;">제목 :</div>
<div style="width: 500px; float: left;" align="left"><input type="text" name="title"/></div>
<div style="width: 150px; float: left;">내용 :</div>
<div style="width: 500px; float: left;" align="left"><textarea name="content" rows="25" cols="65" ></textarea></div>
<div>
<input type="file" name="file">
</div>
<div align="right">
<input type="submit" value="글쓰기">
<input type="reset" value="다시쓰기" onclick="document.myForm.name.focus();">
<input type="button" value="취소" onclick="cancel();">
</div>
</form>
function cancel() {
f = document.myForm;
if(confirm("글쓰기를 취소하고 리스트로 돌아가시겠습니까?") == true) {
location.replace('./list.ino');
}else {
return false;
}
}
- list.jsp
<div>
<h1>자료실</h1>
</div>
<div style="width:650px;" align="right">
<a href="./insert.ino">글쓰기</a> <!-- 여기부터 다시 작성 -->
</div>
<hr style="width: 600px">
<c:forEach items="${lists }" var="dto">
<div style="width: 50px; float: left;">${dto.bno }</div>
<div style="width: 300px; float: left;"><a href="./detail.ino?bno=${dto.bno }">${dto.title }</a></div>
<%--
<div style="width: 300px; float: left;">
<a class="move" href="<c:out value="${dto.num }"/>">
<c:out value="${dto.title }"/>
</a>
</div>
--%>
<div style="width: 150px; float: left;">${dto.writer }</div>
<div style="width: 150px; float: left;">${dto.regdate }</div>
<hr style="width: 600px">
</c:forEach>
<!-- 검색 -->
<div id="leftHeader">
<form action="" name="searchForm" method="post">
<select name="searchKey" class="selectField">
<option value="title">제목</option>
<option value="writer">작성자</option>
</select>
<input type="text" name="searchValue" class="textField"/>
<input type="button" value=" 검 색 " class="btn2" onclick="sendIt()"/>
</form>
</div>
<!-- 페이징 -->
<div>
<c:if test="${dataCount!=0 }">
${pageIndexList }
</c:if>
<c:if test="${dataCount==0 }">
등록된 게시물이 없습니다.
</c:if>
</div>
function sendIt() {
var f = document.searchForm;
f.action = "./list.ino";
f.submit();
}
- detail.jsp
<div>
<h1>자료실</h1>
</div>
<div style="width:650px;" align="right">
<a href="./list.ino">리스트로</a>
</div>
<hr style="width: 600px">
<form action="">
<div style="width: 150px; float: left;">이름 :</div>
<div style="width: 500px; float: left;" align="left"><input type="text" name="writer" value="${dto.writer }" readonly/></div>
<div style="width: 150px; float: left;">제목 :</div>
<div style="width: 500px; float: left;" align="left"><input type="text" name="title" value="${dto.title }" readonly/></div>
<div style="width: 150px; float: left;">작성날자</div>
<div style="width: 500px; float: left;" align="left"><input type="text" name="regdate" value="${dto.regdate }" readonly/></div>
<div style="width: 150px; float: left;">내용 :</div>
<div style="width: 500px; float: left;" align="left"><textarea name="content" rows="25" cols="65" readonly>${dto.content }</textarea></div>
<div align="right">
<input type="button" value="수정" onclick="location.href='./update.ino?bno=${dto.bno }'">
<input type="button" value="취소" onclick="location.href='./list.ino'">
</div>
</form>
'교육 정리 > 입사교육' 카테고리의 다른 글
mavenBoard 만들기 6 (오류 수정), boardAjax 만들기(환경 설정,회원가입 구현) (0) | 2021.08.09 |
---|---|
mavenBoard 만들기 5 (파일 게시판 만들기2) (0) | 2021.08.08 |
mavenBoard 만들기 2 (수정, 삭제 구현, 페이징 재도전2) (0) | 2021.08.04 |
mavenBoard 만들기 (0) | 2021.08.03 |
교육환경 만들기, mavenBoard 오류 고치기 (0) | 2021.08.02 |
댓글