src/maim/java - com.example.demo.dto - Page
생성
package com.example.demo.dto;
import java.util.*;
import lombok.*;
// 페이지 정보를 담고 있는 DTO : 이전 페이지, 시작 페이지, 끝 페이지, 다음 페이지, 글들
@Data
@AllArgsConstructor
public class Page {
private Integer prev;
private Integer start;
private Integer end;
private Integer next;
private Integer pageno;
private List<ProductDto.ReadAll> products;
}
ProductDto
에 코드 추가public class ProductDto {
// 제품 목록 출력용 DTO
@Data
public static class ReadAll {
private Integer pno;
private String vendor;
private String name;
private Double star;
private String imagename;
}
src/main/java - com.example.demo.service - ProductService
수정
package com.example.demo.service;
import java.io.*;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.dao.ProductDao;
import com.example.demo.dto.*;
import com.example.demo.dto.ProductDto.Add;
import com.example.demo.entity.Product;
import lombok.*;
@RequiredArgsConstructor
@Service
public class ProductService {
@Value("${ck.image.folder}")
private String CKImageFolder;
@Value("${ck.image.path}")
private String ckImagepath;
@Value("${product.image.folder}")
private String imageFolder;
@Value("${product.image.path}")
private String imagePath;
@Value("${default.image.name}")
private String defaultImage;
private final ProductDao productDao;
public CKResponse ckImageUpload(MultipartFile image){
if (image != null && image.isEmpty() == false) {
String imageName = UUID.randomUUID() + "-" + image.getOriginalFilename();
File file = new File(CKImageFolder, imageName);
try {
image.transferTo(file);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
return new CKResponse(1, imageName, ckImagepath + imageName);
}
return null;
}
public void add(Add dto) {
Product product = dto.toEntity();
MultipartFile image = dto.getImagename();
product.setImagename(defaultImage);
if (image != null && image.isEmpty() != false && image.getContentType().toLowerCase().startsWith("image/")) {
String imagename = UUID.randomUUID() + "-" + image.getOriginalFilename();
File file = new File(imageFolder, imagename);
try {
image.transferTo(file);
product.setImagename(imagename);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
}
productDao.save(product);
}
// ★ 페이징
public Page readAll(int pageno) {
// 한 페이지당 상품의 개수는 10개, 블록당 페이지의 개수는 5개
int PRODUCT_PER_PAGE = 10;
int PAGE_PER_BLOCK = 5;
int count = productDao.count(null);
/*
글이 123개라고 치면
pageno startRownum endRownum
1 1 10
2 11 20
3 21 30
13 121 123
*/
int startRownum = ((pageno-1)*PRODUCT_PER_PAGE) + 1;
int endRownum = (startRownum + PRODUCT_PER_PAGE) - 1;
if (count < endRownum)
endRownum = count;
List<ProductDto.ReadAll> products = productDao.findAll(startRownum, endRownum);
// 페이지의 개수 -> 글이 119개면 12 페이지, 120개면 12 페이지, 121개면 13 페이지
int countOfPage = (count/PRODUCT_PER_PAGE) + 1;
// 글이 120개면 countOfPage는 13이 나온다. PRODUCT_PER_PAGE의 배수인 경우 countOfPage를 1 감소시켜야 한다.
if (count % PRODUCT_PER_PAGE == 0)
countOfPage--;
/*
pageno blockNo prev start end next
1~5 0 0 1 5 6
6~10 1 5 6 10 11
11~13 2 10 11 13 0
*/
int blockNo = pageno/PAGE_PER_BLOCK;
// pageno가 5면(PAGE_PER_BLOCK의 배수) 1이 나오게 되므로 따로 처리
if (pageno % PAGE_PER_BLOCK == 0)
blockNo--;
int start = blockNo * PAGE_PER_BLOCK + 1;
int prev = start - 1;
int end = start + PAGE_PER_BLOCK - 1;
int next = end + 1;
// blockNo가 2인 경우 end는 15, next는 16이므로 따로 처리
if (end >= countOfPage) {
end = countOfPage;
next = 0;
}
return new Page(prev, start, end, next, pageno, products);
}
}
src/main/java - com.example.demo.dao - ProductDao
package com.example.demo.dao;
import java.util.List;
import org.apache.ibatis.annotations.*;
import com.example.demo.dto.ProductDto;
import com.example.demo.entity.*;
@Mapper
public interface ProductDao {
public void save(Product product);
public int count(String categoryCode);
public List<ProductDto.ReadAll> findAll(int startRownum, int endRownum);
}
src/main/resources - mapper - productMapper.xml
<?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="com.example.demo.dao.ProductDao">
<insert id="save" useGeneratedKeys="true" keyProperty="pno">
<selectKey keyProperty="pno" order="BEFORE" resultType="int">
select product_seq.nextval from dual
</selectKey>
insert into product(pno, vendor, name, info, imagename, price, salesVolume, countOfStar, sumOfStar, countOfReview, stock, categoryCode)
values(#{pno}, #{vendor}, #{name}, #{info}, #{imagename}, #{price}, #{salesVolume}, #{countOfStar}, #{sumOfStar}, #{countOfReview}, #{stock}, #{categoryCode})
</insert>
<select id="count" resultType="int">
select /*+ index_ffs(product product_pk_pno) */ count(pno) from product
<where>
<if test="categoryCode!=null">categoryCode=#{categoryCode}</if>
</where>
</select>
<select id="findAll" resultType="com.example.demo.dto.ProductDto$ReadAll">
<![CDATA[
select * from
(select rownum as rnum, newdesc.* from
(select /*+ index_desc(product product_pk_pno)*/ pno, vendor, name, nvl(sumOfStar/nullif(countOfStar, 0), 0.0) as star, imagename from product) newdesc
where rownum<=#{endRownum})
where rnum>=#{startRownum}
]]>
</select>
</mapper>
📝
/*+ INDEX_FFS(테이블명 인덱스명) */
논리적인 트리 구조를 무시하고, 인덱스를 병렬 방식으로 읽는다. 따라서 Full Scan보다 속도가 빠르다. 쿼리에 사용한 컬럼이 모두 인덱스에 포함되어 있을 때만 사용 가능하다.
📝nvl(sumOfStar/nullif(countOfStar, 0), 0.0)
countOfStar가 0이면 null이 되고, null이 들어간 연산의 결과는 null이므로 nvl 함수를 이용해 0.0으로 바꾼다.