2022-03-10(목)
파일을 업로드하기 위한 DB 테이블을 생성한다.
성공적으로 조회시 테이블이 나온다.
파일 업로드를 하기 위한
write.jsp 게시판 (보여지는 view)
WebContent.fileTest 에서 만든다.
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
request.setCharacterEncoding("UTF-8");
String cp = request.getContextPath();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="<%=cp%>/fileTest.do" method="post"
enctype="multipart/form-data">
제목 : <input type="text" name="subject"/><br/>
파일 : <input type="file" name="upload"/><br/>
<input type="hidden" name="method" value="write_ok"/>
<input type="submit" value="파일 업로드"/><br/>
<input type="button" value="리스트"
onclick="javascript:location.href='<%=cp%>/fileTest.do?method=list';"/>
</form>
</body>
</html>
파일 업로드를 하기위해서는
폼 부분에는
<form action="<%=cp%>/fileTest.do" method="post" enctype="multipart/form-data">
multipart를 줘야 한다.
submit으로 파일 업로드 버튼을 누르게 된다.
이때 subject , upload , method를 넘겨주는데 form action에 적힌 주소로 이동
이때 hidden 값도 넘겨주는데 name이 method 인 값이 write_ok 도 같이 넘겨주게 된다.
FileTestForm.java(DTO) getter/setter
package com.fileTest;
import org.apache.struts.action.ActionForm;
import org.apache.struts.upload.FormFile;
public class FileTestForm extends ActionForm {
private static final long serialVersionUID = 1L;
// 스트럿츠에 파일업로드할때 필수(위에서부터 5개)이다.
private int num;
private String subject;
private String saveFileName;
private String originalFileName;
// 스트럿츠에 파일을 업로드하는애
// 액션폼에서 관리하듯이 똑같이 관리해줌
private FormFile upload;
// listnum : 기존에 있는 데이터가 몇개이든간에 rownum 일련번호를 만들어서
// num 대신 일렬번호를 재정렬 만들어서 넣어줌
// urlFile : 파일클릭했을때 다운로드 경로
private int listNum;
private String urlFile;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getSaveFileName() {
return saveFileName;
}
public void setSaveFileName(String saveFileName) {
this.saveFileName = saveFileName;
}
public String getOriginalFileName() {
return originalFileName;
}
public void setOriginalFileName(String originalFileName) {
this.originalFileName = originalFileName;
}
public FormFile getUpload() {
return upload;
}
public void setUpload(FormFile upload) {
this.upload = upload;
}
public int getListNum() {
return listNum;
}
public void setListNum(int listNum) {
this.listNum = listNum;
}
public String getUrlFile() {
return urlFile;
}
public void setUrlFile(String urlFile) {
this.urlFile = urlFile;
}
}
WEB-INF에서 struts-config_fileTest.xml 등록
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config
PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.3//EN"
"http://struts.apache.org/dtds/struts-config_1_3.dtd">
<!-- 각각의 기능에 대한 환경설정 -->
<!-- mvc 에서의 controller 역할이다. -->
<struts-config>
<form-beans>
<form-bean name="fileTestForm" type="com.fileTest.FileTestForm"/>
</form-beans>
<action-mappings>
<action path="/fileTest" type="com.fileTest.FileTestAction"
name="fileTestForm" scope="request" parameter="method">
<forward name="write" path="/fileTest/write.jsp"/>
<forward name="wrtie_ok" redirect="true"
path="/fileTest.do?method=list"/>
<forward name="list" path="/fileTest/list.jsp"/>
<forward name="delete_ok" redirect="true"
path="/fileTest.do?method=list"/>
</action>
</action-mappings>
</struts-config>
web.xml 에 struts-config_fileTest.xml를 등록한다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>struts</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>
/WEB-INF/struts-config.xml,
/WEB-INF/struts-config_test.xml,
/WEB-INF/struts-config_board.xml,
/WEB-INF/struts-config_boardTest.xml,
/WEB-INF/struts-config_fileTest.xml
</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- 인코딩 필터 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>com.filter.EncodingFilter</filter-class>
<init-param>
<param-name>charset</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
fileTest.sqlMap.xml
sql문을 생성하여 DB에 데이터를 넣는다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap
PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-2.dtd">
<!-- 호출하는 임의이 이름 -->
<!-- 파일의 정보를 추출해서 db에 넣는곳 -->
<sqlMap namespace="fileTest">
<select id="maxNum" resultClass="int">
select nvl(max(num),0) from fileTest
</select>
<insert id="insertData" parameterClass="com.fileTest.FileTestForm">
insert into fileTest (num,subject,saveFileName,originalFileName)
values (#num#,#subject#,#saveFileName#,#originalFileName#)
</insert>
<!-- 전체데이터의 개수 읽어온다. -->
<select id="dataCount" resultClass="int">
select nvl(count(num),0) from fileTest
</select>
<!-- 하나의 데이터를 읽어온다. -->
<select id="readData" resultClass="com.fileTest.FileTestForm"
parameterClass="int">
select num,subject,saveFileName,originalFileName
from fileTest where num=#num#
</select>
<select id="listData" resultClass="com.fileTest.FileTestForm"
parameterClass="map">
<![CDATA[
select * from (
select rownum rnum, data.* from (
select num,subject,saveFileName,originalFileName
from fileTest order by num desc) data)
where rnum>=#start# and rnum<=#end#
]]>
</select>
<delete id="deleteData" parameterClass="int">
delete filetest where num=#num#
</delete>
</sqlMap>
추가한 fileTest_sqlMap.xml를 xml에 등록한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<!-- cacheModelsEnabled : 캐쉬 사용하지말라 -->
<!-- useStatementNamespaces -> sqlMap.xml에서 namespace를 사용할수있다. -->
<!-- dataSource type : simple 단일테이터베이스 -->
<!-- property : 등록
sqlMap resource : boardTest_sqlMap.xml를 불러온다. -->
<sqlMapConfig>
<settings
cacheModelsEnabled="false"
useStatementNamespaces="true"/>
<transactionManager type="JDBC" commitRequired="false">
<dataSource type="SIMPLE">
<property name="JDBC.Driver"
value="oracle.jdbc.driver.OracleDriver"/>
<property name="JDBC.ConnectionURL"
value="jdbc:oracle:thin:@localhost:1521:xe"/>
<property name="JDBC.Username" value="suzi"/>
<property name="JDBC.Password" value="a123"/>
</dataSource>
</transactionManager>
<sqlMap resource="com/util/sqlMap/boardTest_sqlMap.xml"/>
<sqlMap resource="com/util/sqlMap/fileTest_sqlMap.xml"/>
</sqlMapConfig>
FileTestAction.java(DAO-model)
메서드를 사용하기 위해서 DispatchAction를 상속한다.
package com.fileTest;
import java.io.File;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.actions.DispatchAction;
import com.util.FileManager;
import com.util.MyUtil;
import com.util.dao.CommonDAO;
import com.util.dao.CommonDAOImpl;
public class FileTestAction extends DispatchAction {
public ActionForward write(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
return mapping.findForward("write");
}
public ActionForward write_ok(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
// DB연결
CommonDAO dao = CommonDAOImpl.getInstance();
// 오리지널 경로를 쓰려면 세션이 필요
HttpSession session = request.getSession();
// root = "/"
String root = session.getServletContext().getRealPath("/");
// 실제 경로(파일저장되는 경로) pds파일 내 saveFile에 경로를 둔다.
String savePath = root + "pds" + File.separator + "saveFile";
// 넘어오는 데이터는 FileTestForm에 있다
// 다운캐스팅
FileTestForm f = (FileTestForm)form;
// 파일에 업로드 하고 파일정보를 db에 넣기
// 파일업로드
String newFileName =
FileManager.doFileUpload(f.getUpload(), savePath);
if(newFileName!=null) {
int maxNum = dao.getIntValue("fileTest.maxNum");
f.setNum(maxNum + 1);
f.setSaveFileName(newFileName);
f.setOriginalFileName(f.getUpload().getFileName());
dao.insertData("fileTest.insertData",f);
}
return mapping.findForward("write_ok");
}
public ActionForward list(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
CommonDAO dao = CommonDAOImpl.getInstance();
MyUtil myUtil = new MyUtil();
String cp = request.getContextPath();
int numPerPage = 5;
int totalPage = 0;
int totalDataCount = 0;
String pageNum = request.getParameter("pageNum");
int currentPage = 1;
if(pageNum!=null && !pageNum.equals("")) {
currentPage = Integer.parseInt(pageNum);
}
totalDataCount = dao.getIntValue("fileTest.dataCount");
if(totalDataCount!=0) {
totalPage = myUtil.getPageCount(numPerPage, totalDataCount);
}
if(currentPage > totalPage) {
currentPage = totalPage;
}
Map<String, Object> hMap = new HashMap<String, Object>();
int start = (currentPage-1)*numPerPage+1;
int end = currentPage*numPerPage;
hMap.put("start", start);
hMap.put("end", end);
// 5개의 데이터를 꺼낸다.
// num을 가지고 일련번호를 만들것이다.
List<Object> lists =
(List<Object>)dao.getListData("fileTest.listData",hMap);
// 꺼낸다.
Iterator<Object> it = lists.iterator();
int listNum,n=0;
String str;
// 리스트의 내용을 하나씩 꺼낸다 데이터타입이 Object이므로 다운캐스팅
while(it.hasNext()) {
FileTestForm dto = (FileTestForm)it.next();
// dto 에는 num savefile originalfile 등이 있다
// 일련번호 만드는 공식
// 1페이지 5개가 있으면 start 1 부터 시작해서
// n은 0이들어가있어서
// 5 - ( 1 + 0 - 1);
// 5 - 0 = 5 제일첫번째 데이터는 5가 들어간다.
// 다음 n은 증감연산자로인해 1이 된다.
// 일련번호(listNum)은 5, 4 , 3 , 2 , 1
listNum = totalDataCount - (start + n - 1);
dto.setListNum(listNum);
n++;
// 파일 다운 경로
// DTO에 주소를 미리 넣어놓는다
str = cp + "/fileTest.do?method=download&num=" + dto.getNum();
// 각각의 dto에 url 의 자기자신의 num으로 들어가게된다.
// UrlFile 다운로드 경로
dto.setUrlFile(str);
}
// 다운로드 경로
String urlList = cp + "/fileTest.do?method=list";
// lists를 넘긴다.
request.setAttribute("lists", lists);
request.setAttribute("pageNum", pageNum);
request.setAttribute("totalDataCount", totalDataCount);
request.setAttribute("pageIndexList",
myUtil.pageIndexList(currentPage, totalPage, urlList));
return mapping.findForward("list");
}
// method=delete
public ActionForward delete(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
CommonDAO dao = CommonDAOImpl.getInstance();
HttpSession session = request.getSession();
String root = session.getServletContext().getRealPath("/");
// 실제 경로
String savePath = root + "pds" + File.separator + "saveFile";
int num = Integer.parseInt(request.getParameter("num"));
// num으로 하나의 데이터를 가져온다
// 세이브파일과 오리지널 파일이 필요하기 때문에
FileTestForm dto =
(FileTestForm)dao.getReadData("fileTest.readData",num);
// 저장된파일의 이름으로 삭제한다.
FileManager.doFileDelete(dto.getSaveFileName(), savePath);
// db에 있는 내용을 지운다.
dao.deleteData("fileTest.deleteData", num);
return mapping.findForward("delete_ok");
}
// 다운로드
public ActionForward download(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
CommonDAO dao = CommonDAOImpl.getInstance();
HttpSession session = request.getSession();
String root = session.getServletContext().getRealPath("/");
// 실제 경로
String savePath = root + "pds" + File.separator + "saveFile";
int num = Integer.parseInt(request.getParameter("num"));
// num으로 하나의 데이터를 가져온다.
FileTestForm dto =
(FileTestForm)dao.getReadData("fileTest.readData", num);
if(dto==null) {
return mapping.findForward("list");
}
// 다운로드 한다.
boolean flag = FileManager.doFileDownload(response, dto.getSaveFileName(),
dto.getOriginalFileName(), savePath);
if(!flag) {
// flag값이 있지않으면
response.setContentType("text/html;charset=utf-8");
// 출력객체 생성
PrintWriter out = response.getWriter();
out.print("<script type='text/javascript'>");
out.print("alert('다운로드 에러!!');");
out.print("history.back()");
out.print("</script>");
}
// null 이면 아무대도 안가고 멈춰있는다.
return mapping.findForward(null);
}
}
FileManager
doFileUpload 파일 업로드 메서드를 추가함
package com.util;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Calendar;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.upload.FormFile;
// 스트럿츠로 파일업로드
public class FileManager {
// 스트럿츠 여기다가 파일업로드 만들것
public static String doFileUpload(FormFile upload,String path) throws IOException { // 어떤파일을 업로드할지 , 어이다가 경로
// upload 에 a.txt가 오면
// a.txt 에서 txt를 빼내고
// 파일명과 상관없이 20220310115020xxxxx 에 .txt를 붙일것이다
// 새롭게 저장되는 saveFileName이다
// 반환값으로 받아내야만 오리지널 파일네임과 세이브파일네임을 db에 저장하게된다.
// 그래서 반환값이 String이다.
String newFileName = null;
if(upload==null) {
return null;
}
// 클라이언트가 업로드한 파일 이름
// 오리지널파일네임
String originalFileName = upload.getFileName();
if(originalFileName.equals("")) {
return null;
}
// 확장자 추출 뺴뇌야만 나중에 붙일수있으니
// abc.txt 뒤에서부터 .를 찾아서 끝까지 ( 인수의 개수가 하나면 끝까지)
String fileExt = originalFileName.substring(originalFileName.lastIndexOf(".")); // 맨뒤의 인덱스부터 . 까지 txt
if(fileExt==null || fileExt.equals("")) {
return null;
}
// 서버에 저장할 새로운 파일 이름 생성
// 월 분 일 시 분 초
// 1딸라 : 캘린더 겟 인스턴스 하나로 값을 싹다 받으려고
//서버에 저장할 새로운 파일이름 생성
newFileName = String.format("%1$tY%1$tm%1$td%1$tH%1$tM%1$tS",
Calendar.getInstance());
newFileName += System.nanoTime(); // 절대중복되지않음 10의-9승값
newFileName += fileExt; // 확장자를 붙이면 업로드할 새로운 파일이름이 만들어짐
// 파일업로드
File f = new File(path);
if(!f.exists()) {
f.mkdirs();
}
// 풀 파일까지 포함된 경로
String fullFilePath = path + File.separator + newFileName;
// -------------파일 업로드 소스코드--------------
// 업로드할 파일의 데이터를 읽어서 배열에 넣음
byte[] fileData = upload.getFileData();
// 파일을 내보내는애
FileOutputStream fos = new FileOutputStream(fullFilePath);
fos.write(fileData);
fos.close();
return newFileName;
}
// db에 있는거 삭제는 dao
// 파일다운로드
// 다운로드는 서버에서 클라이언트로 response담아서 준다.
// 다운로드는 response가 필수이다.
// response , 파일 저장이름 , 오리지널파일이름 , 경로 매개변수로 받는다.
public static boolean doFileDownload(HttpServletResponse response,
String saveFileName, String originalFileName,String path) {
// a.txt를 올렸는데 a.txt로 저장되는데 b라는사람이 a.txt 올리게되면 a1.txt로저장
// a1이라는 이름으로 다운받을때 a1를 a.txt로 바꿔준다(오리지널파일네임).
// 그래서 오리지널 파일이름이 필요
try {
// 오리지널 파일이 없는경우 저장이름으로 넣음
String filePath = path + File.separator + saveFileName;
if(originalFileName==null || originalFileName.equals("")) {
originalFileName = saveFileName; // cat.jpg
}
// 파일을 다운받아 클라이언트 컴에 저장할때
// 파일 한글 이름 깨짐 방지 ( 반드시 써야한다. 한글파일을 받음 )
// 한글2바이트 영문 1바이트 웹에서는 한글을 3바이트로 쓰려고 UTF-8로씀
// ISO 생략가능 8859_1 만 써도 된다.
originalFileName =
new String(originalFileName.getBytes("euc-kr"),"ISO-8859-1");
File f = new File(filePath); // 파일이 존재하면 파일을 다운받음 , 없으면 자바스크립트로 에러 출력
// 존재하지않으면 false
if(!f.exists()) {
return false;
}
// html 를 utf-8로 처리한다고 알려줬었음
// application : 모든 프로그램에서 돌아감
// abc.txt 앞에있는건 특수기호 아니면 모두다 가능 뒤에는 3자리
// .를 octet라고 부름 , 모든파일은 application서 만들고 octet라고 구분지음
// 형식
response.setContentType("application/octet-stream");
// 내려보냄
// 서버가 파일을 read 읽어서 내보낼때 out를 쓴다.
// setHeader(String name, String value) : name 헤더의 값을 value로 지정한다.
// 데이터형식/성향설정 (attachment: 첨부파일)
response.setHeader("Content-disposition",
"attachment;fileName=" + originalFileName);
// 사진1
// 버퍼로 객체를 생성할 때 데이터를 읽어올 파일을 지정
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));
// 버퍼의 출력스트림을 출력
OutputStream out = response.getOutputStream();
int data;
byte[] bytes = new byte[4096];// 자동으로 내보내줌
// 4096 바이트로 읽어와서 없을때까지 읽어옴
// 사진2
// bytes의 0번째부터 4096번째 까지 읽어와서
while((data=bis.read(bytes, 0, 4096))!=-1) {
out.write(bytes,0,data);// 0부터 data의 양만큼
}
out.flush();
out.close();
bis.close();
} catch (Exception e) {
System.out.println(e.toString());
// 에러가 났으면 false
return false;
}
return true;
}
// 파일을 삭제하는 메소드~~~~~~~~~~~~~
public static void doFileDelete(String fileName,String path) {
//---------------중요 파일삭제할때 파일이름,파일경로 필요 --------------------
try {
// path는 서블릿의 파일저장경로 path를 넘겨줄건데
// 파일의 이름은 db에 있다
// 그리고 업로드된 파일 삭제하고 db정보도 삭제해야한다.
String filePath = path + File.separator + fileName; // 풀네임
File f = new File(filePath); // 경로를 넘겨줌
// 파일이 있으면 지워라 - 물리적인 파일삭제
if(f.exists()) {
f.delete();
}
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
list.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
request.setCharacterEncoding("UTF-8");
String cp = request.getContextPath();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<br/><br/>
<table width="500" align="center">
<tr height="30">
<td align="right">
<input type="button" value="파일 올리기"
onclick="javascript:location.href='<%=cp%>/fileTest.do?method=write';">
</td>
</tr>
</table>
<table width="500" align="center" border="1" style="font-size: 10pt;">
<tr height="30" align="center">
<td width="50">번호</td>
<td width="200">제목</td>
<td width="200">파일</td>
<td width="50">삭제</td>
</tr>
<c:forEach var="dto" items="${lists }">
<tr onmouseover="this.style.backgroundColor='#e4e4e4'"
onmouseout="this.style.backgroundColor=''" bgcolor="#ffffff">
<td width="50" align="center">${dto.listNum}</td>
<td width="200" align="left">${dto.subject }</td>
<td width="200" align="left">
<a href="${dto.urlFile}">${dto.originalFileName }</a></td>
<td width="50" align="center">
<a href="<%=cp %>/fileTest.do?method=delete&num=${dto.num}">
삭제</a></td>
</tr>
</c:forEach>
<c:if test="${totalDataCount==0 }">
<tr bgcolor="#ffffff">
<td align="center" colspan="5">등록된 자료가 없습니다.</td>
</tr>
</c:if>
</table>
<c:if test="${totalDataCount!=0 }">
<table width="500" border="0" cellpadding="0" cellspacing="3" align="center">
<tr align="center">
<td align="center" height="30">
${pageIndexList }
</td>
</tr>
</table>
</c:if>
</body>
</html>
실행
'BACKEND > 아파치 스트럿츠 1 , 2' 카테고리의 다른 글
[struts2] 스트럿츠2 + iBatis를 이용한 답변형 게시판 중 (2) (답변형 게시판 , 수정 , 삭제 ) (0) | 2022.03.14 |
---|---|
[Struts2] 스트럿츠2 + iBatis 파일업로드/다운로드/보기 기능 (0) | 2022.03.14 |
[struts2] 스트럿츠2 + iBatis를 이용한 답변형 게시판 중 1 (글쓰기 , 수정 , 이전글 , 다음글 ) (0) | 2022.03.14 |
[struts1] 스트럿츠1 + iBatis 게시판 만들기 (0) | 2022.03.07 |