[사용자의 파일을 받아 서버에 저장, DB에 저장, 게시판 View에 출력, 삭제]
테이블
create table macbook_banner(
bidx int unsigned auto_increment,
bname varchar(100) not null,
file_ori text null,
file_new text null,
file_url text null,
bdate timestamp not null default current_timestamp,
primary key(bidx)
);
banner_DTO.java
package spring_learning;
import org.springframework.stereotype.Repository;
import lombok.Data;
//@Getter
//@Setter
@Data //@Data = @Getter + @Setter 한방에 적용하는 어노테이션
@Repository("banner_DTO")
//DTO 생성 후 꼭 config.xml에 추가~!
public class banner_DTO {
//파일은 DTO에 쓰지않음!!! 절대안됨!!! I/O는 따로 핸들링함!!!
int bidx;
String bname, file_ori, file_new, file_url, bdate;
}
- @Data: Getter, Setter 자동 생성 어노테이션
- Getter, Setter, toString(), equals(), hashCode() 등 자주 사용하는 메서드 자동 생성
banner.html
입력하고 페이지 안만들어서 에러뜨는데 저장잘됨 -> 리스트로 가기
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>배너 등록 페이지</title>
</head>
<body>
<form id="frm" method="post" action="./bannerok" enctype="multipart/form-data">
이벤트 : <input type="text" name="bname"><br>
파일첨부 : <input type="file" name="bfile" ><br>
<input type="button" value="배너등록" onclick="gopage()">
</form>
</body>
<script>
var gopage = function(){
frm.submit();
}
</script>
</html>
- enctype="multipart/form-data": 파일 업로드 위한 필수 속성
mapper.xml 부분
<!-- 배너 파일 저장 테이블 -->
<insert id="banner_in">
insert into macbook_banner (bidx,bname,file_ori,file_new,file_url,bdate)
values ('0',#{bname},#{file_ori},#{file_new},#{file_url},now())
</insert>
<!-- 배너 전체 리스트 출력 쿼리문 + 페이징 추가 -->
<select id="banner_all" resultType="banner_DTO" parameterType="Map">
select * from macbook_banner order by bidx desc limit #{spage},#{epage}
</select>
<!--
* 2중 select 못하면 새로 써야됨
배너의 전체 데이터 개수 쿼리문
-->
<select id="banner_total" resultType="int">
select count(*) as total from macbook_banner
</select>
<!-- 배너명 검색 쿼리문 -->
<!--
[mybatis에서 DB에 따른 like 사용법] 잘알아두기 !
mysql & mariaDB : like concat('%',#{search},'%')
orecle : like '%'||#{search}||'%'
mssql : like '%'+#{search}+'%'
* like 외에도 다르게 써야하는 명령어 있음 : 트리거 등등 ...
-->
<select id="banner_search" resultType="banner_DTO" parameterType="String">
select * from macbook_banner where bname like concat('%',#{search},'%') order by bidx desc
</select>
<!-- 배너 고유값으로 삭제하는 쿼리문 -->
<delete id="banner_del">
delete from macbook_banner where bidx=#{no}
</delete>
- [검색] mybatis에서 DB에 따른 like 사용법 - 잘알아두기 !
- mysql & mariaDB : like concat('%',#{search},'%')
- orecle : like '%'||#{search}||'%'
- mssql : like '%'+#{search}+'%'
- like 외에도 다르게 써야하는 명령어 있음 : 트리거 등등 ...
banner_DAO.java
package spring_learning;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;
@Repository("banner_DAO")
public class banner_DAO {
@Resource(name = "template")
public SqlSessionTemplate st;
Integer page_ea = 5; //한페이지에 출력할 게시물 수
//신규 배너 등록 메소드
public int new_banner(banner_DTO dto) {
int result = this.st.insert("macbook_user.banner_in",dto);
return result;
}
//배너 전체 데이터 출력 + 페이징
public List<banner_DTO> all_banner(Integer pgno){ //Integer pgno : Controller에서 사용자가 클릭한 페이지 번호를 받는 역할
//limit을 사용하기 위해 Map 형태로 구성하여 Mapper로 전달
Map<String,Integer> data = new HashMap<String,Integer>();
data.put("spage", this.page_ea * (pgno - 1)); //limit 첫번째 번호 (페이지 번호에 맞는 시작 게시물 번호)
data.put("epage", this.page_ea); //limit 두번째 번호 (출력 개수)
List<banner_DTO> all = this.st.selectList("macbook_user.banner_all",data);
return all;
}
public int banner_total() {
int total = this.st.selectOne("macbook_user.banner_total");
return total;
}
//배너명으로 검색된 데이터를 가져오는 메소드 (DAO)
public List<banner_DTO> banner_search(String search) {
List<banner_DTO> sel = this.st.selectList("macbook_user.banner_search",search);
return sel;
}
//똑같은거 많으면 그냥 필드에 올리는게 더 좋음 List<banner_DTO> 이런거 필드에 올리기
//배너 삭제 메소드
public int banner_del(String no) {
int result = this.st.delete("macbook_user.banner_del", no);
return result;
}
}
banner_controller.java
package spring_learning;
import java.io.File;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
//실무처럼 쓴 컨트롤러!! 캬~
@Controller
public class banner_controller {
List<String> listdata = null;
Map<String, String> mapdata = null;
PrintWriter pw = null;
String result = null; //select 결과
int callback = 0; //update, delete, insert 결과
ModelAndView mv = null;
//Field(this. 사용)의 dto와 매개변수(그냥사용)의 dto는 다름 !!!
@Resource(name="banner_DTO")
banner_DTO dto;
@Resource(name="banner_DAO")
banner_DAO dao;
@Resource(name="file_rename")
m_file_rename fname; //파일명ㅇㄹ 개발자가 원하는 형태로 변경
//배너 등록 (.do안써도됨) do쓰는 이유는 그냥 암묵적인룰? 2~3년차되면 잘 안씀
@PostMapping("/banner/bannerok") //경로 잘 맞추기
public String bannerok(
@ModelAttribute(name="dto") banner_DTO dto,
MultipartFile bfile,
HttpServletRequest req
) throws Exception{
//@RequestParam(name="dto", required = false) 이거는 잘못된 코드 => int + String 섞여있어서 안됨
//=>
//@ModelAttribute : dto 전용 어노테이션 (근데 안써도됨 ㅋ) 부트에서 많이 쓰임 Spring에서는 딱히 ..
// 장점 : 1대1 매칭 => name과 DTO 자료형 변수가 같은것이 있으면 무조건 값을 setter 발동
String file_new = null;
//날짜
if(bfile.getSize() > 0) {
String url = req.getServletContext().getRealPath("/upload/");
// System.out.println(url);
file_new = this.fname.rename(bfile.getOriginalFilename());
FileCopyUtils.copy(bfile.getBytes(), new File(url + file_new));
dto.setFile_url("/upload/" + file_new); //웹디렉토리경로 및 파일명
dto.setFile_new(file_new); //개발자가 원하는 방식으로 변경한 파일명
dto.setFile_ori(bfile.getOriginalFilename()); //사용자가 적용한 파일명
}
this.callback = this.dao.new_banner(dto); //this.dto와 dto는 다름 주의!!!
System.out.println(this.callback);
return null;
}
//search 검색에 관련사항은 필수조건은 아니며, 또한 null일경우 공백처리
@GetMapping("/banner/bannerlist") //경로 잘 맞추기
public String bannerlist(Model m,
@RequestParam(name="search", defaultValue = "", required = false) String search,
@RequestParam(name="pageno", defaultValue = "1", required = false) Integer pageno) {
//페이징
//페이징한다고 쿼리문 따로 만들지않고 전체출력을 꾸며서 쓰는것이 좋음 !!!!!
//리스트 총개수확인
int total = this.dao.banner_total();
System.out.println(total);
//사용자가 클릭한 페이지 번호에 맞는 순차번호 계산값
int userpage = (pageno - 1) * 5;
m.addAttribute("userpage",userpage);
//검색
//search 의 값이 없을때 디폴트 "", 필수 X로 받겠다는 뜻 => 안쓰면 400!
List<banner_DTO> all = null;
if(search.equals("")) { //연산기호 X, equals
System.out.println("검색어없음");
all = this.dao.all_banner(pageno); //사용자가 클릭한 페이지 번호값 전달
}else {
all = this.dao.banner_search(search);
}
m.addAttribute("total",total);
m.addAttribute("search",search);
m.addAttribute("all",all);
return null;
}
//리스트 삭제
@PostMapping("/banner/bannerdel")
public String bannerdel(@RequestParam(name="ckdel", defaultValue = "", required = false) String ckdel, Model m) {
this.callback = 0;
String msg = "";
if(ckdel.equals("")) {
msg = "alert('올바른 접근이 아닙니다.'); location.href='./bannerlist';";
}else {
//front에서 넘어온 ckdel는 문자열형태 1,2,3
String no[] = ckdel.split(",");
int w = 0;
while(w < no.length) { //front-end에서 체크된 값만큼 반복
int result = this.dao.banner_del(no[w]);
if(result > 0) {
this.callback++;
}
w++;
}
//-1 사용하는 이유는 반복문에 조건이 없으므로 +1이 작동될 수 있음
if(no.length == this.callback) {
System.out.println(no.length);
System.out.println(this.callback);
msg = "alert('정상적으로 삭제되었습니다.'); location.href='./bannerlist';";
}else {
System.out.println(no.length);
System.out.println(this.callback);
msg = "alert('비정상적인 데이터가 확인되었습니다.'); location.href='./bannerlist';";
}
}
m.addAttribute("msg", msg);
return "load";
}
}
- FileCopyUtils.copy(): 파일 데이터를 지정한 경로에 복사
- req.getServletContext().getRealPath(): 서버의 실제 경로를 가져옴
m_file_rename.java
서버 저장용 이름 짓는 Model
package spring_learning;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.stereotype.Repository;
@Repository("file_rename")
public class m_file_rename {
//홍길동.jpg => 2025032755.jpg
public String rename(String filenm) {
//날짜
Date day = new Date();
SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd");
String today = sf.format(day); //년월일
//속성
int com = filenm.lastIndexOf(".");
String fnm = filenm.substring(com);
//랜덤값
int no = (int)Math.ceil(Math.random()*1000); //1~1000
String makefile = today + no + fnm;
return makefile;
}
}
bannerlist.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="cr" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>배너 리스트 페이지</title>
</head>
<body>
<!--
서브밋 Ajax 안씀! 위험 더블이벤트 발생 입력없는데 통신 이런거 발생 절대 X
버튼에만 Ajax 씀!
-->
<!-- 하나의 컨트롤러 메소드에 다해도됨 -->
<form id="sform" method="get" action="./bannerlist" onsubmit="return spage()">
<p>
배너명 검색 : <input type="text" name="search" value="${search}">
<input type="submit" value="검색">
<input type="button" value="전체목록" onclick="location.href='./bannerlist';">
</p>
</form>
<p>전체 등록된 배너 개수 : ${total} 개</p>
<table border="1" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th><input type="checkbox" id="allck" onclick="check_all(this.checked)"></th>
<th width="80">번호</th>
<th width="300">배너명</th>
<th width="100">이미지</th>
<th width="150">파일명</th>
<th width="150">등록일</th>
</tr>
</thead>
<%--
배열 값을 조건문으로 jstl에 처리시 functions 이용하여 length로 검토하여 처리함
<cr:if test ="${fn:length(all)==0}">
아래와 동일한 코드
--%>
<cr:if test ="${all.size()==0}">
<tbody>
<tr>
<td colspan="6" align="center">검색된 데이터가 없습니다.</td>
</tr>
</tbody>
</cr:if>
<tbody>
<cr:set var="ino" value="${total - userpage}"/> <!-- 게시물 일련번호 셋팅 -->
<!-- 반복문 안에는 절대 id로 같은 이름 사용 불가능 => class(ajax전송), name(form전송) 사용 -->
<!-- 고수는 data-value 이런 맘대로 쓴 속성으로 핸들링 -->
<cr:forEach var="bn" items="${all}" varStatus="idx">
<tr height="50">
<!-- 게시판번호 x DB에 저장된 auto_increment 값 -->
<td><input type="checkbox" name="ckbox" value="${bn.bidx}" onclick="checkdata()"></td>
<td align="center">
${ino - idx.index}
</td>
<td>${bn.bname }</td>
<td>
<cr:if test="${bn.file_url == null}">
NO IMAGE
</cr:if>
<cr:if test="${bn.file_url != null}">
<img width="100" src="..${bn.file_url }">
</cr:if>
</td>
<td align="center">
<a href="../upload/${bn.file_new}" target="_blank" title="${bn.file_new}">${bn.file_ori}</a>
</td>
<td align="center">${fn:substring(bn.bdate,0,10)}</td>
</tr>
</cr:forEach>
<!--
이미지 엑박뜨는 이유 : 경로 다름
http://localhost:8080/spring_learning/banner/bannerlist
여기가 현주소인데 여기서 img src="bn.file_url" 이래쓰면
http://localhost:8080/upload/20250327311.jpeg
여기로옴
근데 여기로 와야함
http://localhost:8080/spring_learning/upload/20250327311.jpeg
img src="..bn.file_url"이래 쓰기
-->
</tbody>
</table>
<br><br>
<!-- form 전송으로 선택된 값을 삭제하는 프로세서 -->
<!--
1. forEach => form => 동일한 name => post 전송 => 배열로 받음
2. form => 하나의 hidden을 이용 => post 전송 => 자료형 한개로 받음
주의 : 반복문 속 form, 반복문 속 id => 절대 X
-->
<form id="dform" method="post" action="./bannerdel">
<input type="hidden" name="ckdel" value="">
</form>
<input type="button" value="선택삭제" onclick="check_del()">
<br><br>
<!-- pageing -->
<table border="1" cellpadding="0" cellspacing="0">
<tbody>
<tr height="30">
<!--
Controller에서 데이터의 전체 갯수를 받음
해당 값을 한페이지당 5개씩 출력하는 구조
-->
<!--
total / 5 + (1 - ((total / 5) % 1)) % 1
- total / 5 → 몇 페이지가 필요한지 계산 (소수점 포함)
- (total / 5) % 1 → 남은 소수점이 있는지 확인
- 1 - ... → 소수점이 있으면 1을 더하기 위한 처리
- % 1 → 0 또는 1로 만들기 위한 트릭
//(1 - ((total / 5) % 1)) % 1 => 나머지가있으면 1페이지 추가
-->
<cr:set var="pageidx" value="${total / 5 + (1-((total / 5) % 1)) % 1}"/>
<cr:forEach var="no" begin="1" end="${pageidx}" step="1">
<td width="30" align="center" onclick="pg('${no}')">${no}</td>
</cr:forEach>
</tr>
</tbody>
</table>
</body>
<script>
var spage = function(){
if(sform.search.value==""){
alert("배너명을 입력하세요");
return false;
}else{
return;
}
}
function pg(no){
location.href='./bannerlist?pageno='+no;
}
//체크박스 전체선택 함수
//getElements : name // class getElement : id
function check_all(ck){ //ck : true, false
var ea = document.getElementsByName("ckbox");
var w= 0;
while(w < ea.length){
ea[w].checked = ck;
w++;
}
//위에거 길게쓰면 아래 코드
/*
if(ck == true){ //전체선택한 경우
var w= 0;
while(w < ea.length){
ea[w].checked = true;
w++;
}
}else{ //전체선택 해제한 경우
var w= 0;
while(w < ea.length){
ea[w].checked = false;
w++;
}
}
*/
}
//하나라도 체크 해제시 전체선택 체크 해제
function checkdata(){
}
//선택삭제 버튼 클릭시 리스트에서 체크된 값을 확인 후 배열화하여 hidden에 값을 적용하여 Back-end에 문자열로 전달
function check_del(){
var ar = new Array(); //script 배열
var ob = document.getElementsByName("ckbox");
var w = 0;
while(w < ob.length){
if(ob[w].checked == true){
ar.push(ob[w].value);
}
w++;
}
dform.ckdel.value = ar; //배열이 자동으로 문자열로 변해서 들어감 value="9,8,7,6,5"
if(confirm('해당 데이터를 삭제시 복구되지 않습니다.')){
dform.submit();
}
}
</script>
</html>
- <cr:forEach>: JSTL의 반복 태그로 리스트 데이터 출력
- fn:substring(): 문자열 자르기 함수