내용 입력할때 문서편집 기능을 넣기

 

CK5 다운로드 

https://ckeditor.com/

 

WYSIWYG HTML Editor with Collaborative Rich Text Editing

Rock-solid, Free WYSIWYG Editor with Collaborative Editing, 200+ features, Full Documentation and Support. Trusted by 20k+ companies.

ckeditor.com

 

footer에 추가하기

<script src="https://cdn.ckeditor.com/ckeditor5/32.0.0/classic/ckeditor.js"></script>

 

 

- CK 에디터는 textarea 태그를 변화시켜준다.

 

1. 페이지에 add.html  - th:field="*{content}"

2. 상품에 add.html  th:field="*{description}"

 

 

 

 

ck editor cdn 검색하면 나오는 script 안에 내용을 textarea있는 id를 복붙하기

 <script>
    ClassicEditor
        .create( document.querySelector( '#classic' ))
        .catch( error => {
            console.error( error );
        } );
</script>

 

app.js에 textarea태그의 id 로 넣기

페이지 내용에 ck에디터
//CK에디터 추가
if($('#content').length){ //length는 있어야 나옴 *안쓰면 #content없어도 나오기때문
    ClassicEditor
        .create( document.querySelector( '#content' ))
        .catch( error => {
            console.error( error );
        } );
}

* .length 는 '#content 가 있을 때' 만 나온다. 없을때는 안나오도록 (아래도 동일)

 

상품 내용에 ck에디터
if($('#description').length){ //length는 있어야 나옴 *안쓰면 #content없어도 나오기때문
    ClassicEditor
        .create( document.querySelector( '#description' ))
        .catch( error => {
            console.error( error );
        } );
}

 

 

문서편집 기능 적용됨.

 

 

 

- ck에디터 높이크게 css 적용하기

.ck.ck-content {
    height: 15em; 
    /* 글자 15개 높이 */
}

 

반응형
LIST

 

전체리스트 get매핑에 추가하기

@RequestParam(value = "page",defaultValue = "0") int page 

= 현재페이지가 0

 

int perPage = 4; //한페이지에 4개
Pageable pageable = PageRequest.of(page, perPage); //표시할페이지, 한페이지당 몇개(4개)

 

- 리스트 상품을 페이지네이션하기

List<Product> products = productRepo.findAll(); 을 

Page<Product> products = productRepo.findAll(pageable); 로 바꾼다

 

 

한페이지당 4개 상품만 나온다

총 상품 갯수를 알아야 총 몇페이지 나오는지 나온다.

 

// 페이지를 보여주기 위한 계산
long count = productRepo.count(); //전체 상품갯수(long타입 리턴)
double pageCount = Math.ceil((double)count / (double)perPage); // 13/6개 = 2.1(3페이지) double(소수점나오도록)

model.addAttribute("pageCount", (int)pageCount); //총페이지
model.addAttribute("perPage", perPage); 		 //한 페이지당 상품갯수
model.addAttribute("count", count);				 //전체 상품개수
model.addAttribute("page", page); 				 //현재 페이지

 

 

 

[ index.html ]

 

 

th:if="${count > perPage}" 
 
th:each="number : ${#numbers.sequence(0, pageCount-1)}" : 0부터 ~ 총페이지-1 까지 숫자로. 반복 
  • 1을 하는 이유? 1페이지는 인덱스[0] 부터 시작하니까 (1,2페이지가 있으면 인덱스가 0,1이라서 -1을 해준다.)
    근데 화면에는 0,1,2페이지로 보이면 안되니까 th:text="${number + 1}"  1을 더해준다.
<li class="page-item" th:each="number : ${#numbers.sequence(0,pageCount-1)}">
	<a class="page-link" href="" th:text="${number+1}"></a>
</li>

 

 

 

페이지에 마우스 올려보면 http://localhost:8080/admin/products 파라미터가 뜨는데

 

a태그에 th:href="@{/admin/products/} + '?page=__${number}__'"  추가하고 마우스 올리면

http://localhost:8080/admin/products/?page=0

페이지 누르면 이동이 된다.

 

 

 

css ) 페이지 누를때 버튼 진하게 표시하는 법 (active) 

주의!  띄어쓰기 조심

th:classappend="${page==number} ? 'active' : ''"

 

 

 

- 이전, 다음 버튼도 만들기

<ul class="pagination">
    <li class="page-item" th:if="${page > 0}">
        <a th:href="@{/admin/products/} + '?page=__${page-1}__'" class="page-link">이전</a>
    </li>
    <li class="page-item" th:classappend="${page==number} ? 'active' : ''"
        th:each="number : ${#numbers.sequence(0,pageCount-1)}">
        <a th:href="@{/admin/products/} + '?page=__${number}__'" class="page-link" th:text="${number+1}"></a>
    </li>
    <li class="page-item" th:if="${page < pageCount-1}">
        <a th:href="@{/admin/products/} + '?page=__${page+1}__'" class="page-link">다음</a>
    </li>
</ul>

 

이전버튼

현재페이지가 0(첫페이지)보다 넘을때 이전페이지 나타남  th:if="${page > 0}"

이전버튼은 현재페이지 - 1 

<li class="page-item" th:if="${page > 0}">
     <a th:href="@{/admin/products/} + '?page=__${page-1}__'" class="page-link">이전</a>
</li>

 

다음버튼

현재페이지가 총페이지보다 밑일때 다음버튼 나타남 page < pageCount-1 

다음버튼은 현재페이지 + 1

<li class="page-item" th:if="${page < pageCount-1}">
    <a th:href="@{/admin/products/} + '?page=__${page+1}__'" class="page-link">다음</a>
</li>

 

 

반응형
LIST

수정할때 필요한 것들

1. product 객체

2. 카테고리(과일,채소) 리스트

 

 

 

상품은 DB에서 id로 가져온다

Product product = productRepo.getById(id);

 

카테고리는 모두 가져온다

List categories = categoryRepo.findAll();

 

 

 

edit.html는 add.html 복붙하고 

th:action="@{/admin/products/edit}"
까먹지 말기!!

 

 

수정페이지로 갈때 이미지가 안불러짐 → img태그에 경로 넣기

이미지 불러오는 이유? 수정 전 현재 이미지와 새로 수정한 이미지를 비교하기 위해 

<label for="">현재 이미지</label>
<img th:src="@{'/media/'+${__product.image__}}" style="width: 200px;"/>

언더바 두개 써도안써도 상관은 없음.

상품 페이지에 카테고리 이름 불러올때는 id는 넣길래 그냥 넣어봤다.

 

<br>
<table >
<tr>
  <th> 
    <label for="">현재 이미지</label>
  </th>
  <th> 
    <label for="">수정 이미지</label>
  </th>
</tr>
<tr>
  <td>
    <img th:src="@{'/media/'+${__product.image__}}" style="width: 200px;"/>
  </td>
  <td>
    <img src="#" id="imgPreview"/>
  </td>
</tr>
</table>


post 매핑

 

add매핑과 비슷하니 복붙한다.

 

 

수정 전 상품을 불러와서 (상품 id로 불러온다 = product.getId() )

이미지는 수정할게 있으면 수정 전 상품을 삭제하고,  수정할 게 없으면 원래 이미지 그대로 수정가능하도록

//수정 전 상품 불러오기(id로 검색) -> 수정할게 있다면 삭제하도록
Product currentProduct = productRepo.getById(product.getId());

 

IF절에 else에서

최종적으로 슬러그, 이미지 저장할 때 

슬러그는 저장하고 이미지만 수정할게 있을 경우의 if절 추가한다.

//수정 할 이미지 파일이 있다
if(!file.isEmpty()) { 
    //수정 전 이미지 주소 삭제
    Path currentPath = Paths.get("src/main/resources/static/media/"+ currentProduct.getImage());
    Files.delete(currentPath);

    //다시 올라간 수정한 이미지로 저장
    product.setImage(fileName);
    Files.write(path, bytes);
}else { //수정할 이미지가 없다
    product.setImage(currentProduct.getImage());
}

수정전 이미지를 삭제하고 수정 한 이미지를 저장하기 위한 코드

 

 

 

- 수정을 안했을때 이미지 파일 확장자 오류가 뜬다 (이미지를 jpg 또는 png를 사용하세요)

if(fileName.endsWith("jpg")||fileName.endsWith("png")) {
    fileOk = true;
}

이 부분을 아래처럼 수정 

 

- (새 이미지를  jpg 또는 png를 사용하세요. 새 이미지가 없으면 기존 이미지를 사용.)

새 이미지 파일이 있을때 
if(!file.isEmpty()) {
    if(fileName.endsWith("jpg") || fileName.endsWith("png")) {
        fileOk = true; // 확장자가 .jpg .png 만 ok
    }
}else { // 수정 안하면
    fileOk  = true; //기존이미지 사용
}

사실 기존에 있는 이미지도 불러와서 fileName이 있을텐데 왜 오류가 뜨지?

 

 

 

- 이미지 수정을 안했는데도 이미 등록한 상품이 있다는 오류가 뜬다 

 

Product productExists = productRepo.findByName(product.getName());

이 부분을 아래처럼 고쳐준다.

수정 안했을때 똑같은 상품명이어도 id로도 같이 검색해서 id는 not으로

Product productExists = productRepo.findBySlugAndIdNot(slug, product.getId());

 

 


오류 ( unable to find ~ id 0 )

 

해결

edit.html 에 수정을 위한 id를 입력해야함

<input type="hidden" th:field="*{id}">

 


 

수정 직후 이미지 파일이 안보임..

새로고침하면 나옴

 

이미지 파일의 경로가 문제가 있는지 WebConfig 에 가본다

 

WebConfig에서 백슬래시를 슬래시로 바꿔주니 새로고침 없이 바로 화면에 이미지가 뜬다

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 저장된 파일(이미지)의 경로를 지정한다. (이미지를 사용하기 위함)
    registry
    .addResourceHandler(".media/**")
    .addResourceLocations("C://SPRINGBOOT//Spring-workspace//uglyMarket//src//main//resources//static//media");
}

 

파일경로는 윈도우의 경우 역슬래시가 파일 Path 기본 표현이라고 했는데 

검색해보니 파일경로는

파일 처리를위한 Java 라이브러리를 사용하면 / 모든 플랫폼에서 안전하게 백 슬래시가 아닌 슬래시를 사용할 수 있습니다 

라고 나왔다..

백슬래시보다 슬래시로 적는걸로!!

반응형
LIST

post매핑

public String add(@Valid Product product, BindingResult bindingResult,
MultipartFile file, RedirectAttributes attr, Model model)

MultipartFile : 실제파일은 따로 보관장소를 만들어 저장

 

실제파일의 데이터와 이름을 media에 저장하려고?
1. bytes 데이터 
2. fileName 이름
3. Path path = Paths.get("mdia의 위치" + 파일이름)
세가지가 필요함



 

1. 파일의 데이터 ( jpg,png 둘다 가능하도록 리스트[] )

byte[] bytes = file.getBytes();

2. 파일 이름  /??getName()은?

String fileName = file.getOriginalFilename();

3. 파일 저장할 경로 + 파일이름

Path path = Paths.get("src/main/resources/static/media/"+fileName);

 

 

 boolean fileOk = false;

// 파일의 확장자 jpg , png
if(fileName.endsWith("jpg") || fileName.endsWith("png")) {
    fileOk = true;
}

endsWith() : 특정문자열로 끝나는지 확인하기 위한 메소드

 

 

- 상품추가 성공적일때

attr.addFlashAttribute("message", "상품이 추가되었습니다.");
attr.addFlashAttribute("alertClass", "alert-success");
위와 관련된 메세지가 뜨도록 html에 추가하기 
<div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>

 

- 슬러그 설정

String slug = product.getName().toLowerCase().replace(" ", "-");

 

- 이미 등록한 DB에 존재하는 (상품이름으로 메서드 만들기)

Product productExists = productRepo.findbyName(product.getName());

 

 


오류

 

No property 'findbyName' found for type 'Product'! 

 

findByName 으로 메서드 이름 고쳐보자!

 

 

 

- 위에 정해놓은 jpg,png 확장자 파일이 아닐때 false

if(!fileOk) { 
    attr.addFlashAttribute("message", "이미지를 jpg 또는 png를 사용하세요");
    attr.addFlashAttribute("alertClass", "alert-success");

}else if (productExists != null) {//이미 등록한 상품이름 있다
    attr.addFlashAttribute("message", "등록한 상품이 있습니다. 다른 상품을 적으세요");
    attr.addFlashAttribute("alertClass", "alert-success");

}else { //슬러그, 이미지 저장
    product.setSlug(slug);
    product.setImage(fileName);
    productRepo.save(product);

    Files.write(path, bytes);
}

 

상품,이미지 저장할때
Files.write(path, bytes);  

 

 

BindingResult.hasErrors() 관련 오류

이미지 파일 올릴때 input태그에서 문제발견

th:field="*{image}"를 
th:id="file"  th:name="file"로 바꿔주니 해결됨

왜왜왜왜왜왜

 

th:field 는 id, name, value 속성을 자동으로 생성해주지 th:id, th:name 를 생성하는게 아니기 때문!!

 

 


 

 

이번엔 이미지 부분에 사진이 보이도록 해보자!!

 

 

- WebConfig 에 media(이미지 저장폴더)를 설정해줌

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 저장된 파일(이미지)의 경로를 지정한다. (이미지를 사용하기 위함)
    registry
        .addResourceHandler(".media/**")
        .addResourceLocations("C:\\SPRINGBOOT\\Spring-workspace\\uglyMarket\\src\\main\\resources\\static\\media");
}

 

 

- index.html 

이미지 사진 변수넣고, 사진크기 조절해줌

 <td >
    <img th:src="@{'/media/'+${product.image}}" style="height: 2em">
</td>

 

결과

 

 

이번엔 가격 부분에  25000.00원이 나오는 부분 수정을 해보자!

가격에 소수점 붙이게 했던 DB에  DECIMAL(8,2) --> INT 로 수정.

근데 일단 dao에는 수정 안해주고 그대로 String

 

 

 

 

카테고리 이름 화면에 보이게 해보자!

 

카테고리name과 id를  map 에 담아서 화면에 보내주면 
카테고리 id로 카테고리 name을 찾을수있도록 한다.

 

@GetMapping
public String index(Model model) {

    List<Product> products = productRepo.findAll();
    List<Category> categories = categoryRepo.findAll();

    model.addAttribute("products", products);

    //map으로 카테고리 id, name을 같이 불러와서 카테고리id으로 name이 화면에 나오도록 
    HashMap<Integer, String> categoryIdAndName = new HashMap<>();
    for(Category category : categories) {
        categoryIdAndName.put(category.getId(), category.getName());
    }
    model.addAttribute("categoryIdAndName", categoryIdAndName);

    return "/admin/products/index";
}

model.addAttribute("categoryIdAndName", categoryIdAndName); 가 나오도록 하려면 index에도 수정을!

 

<td th:text="${categoryIdAndName[__${product.categoryId}__]}"></td>

 

카테고리에 
 <td th:text="${cateIdAndName[id]}"> id를 넣으면 value가된다
-> ${cateIdAndName[__${product.categoryId}__]} 

☆ 언더바 두번!!
= 카테고리 이름이 나온다

반응형
LIST

from submit을 할때 th:object="${project}" →  project객체를 받는다.

 

 

전체에러

<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
 
 
 
유효성검사
<span class="error" th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></span>

 

 

<form enctype="multipart>

:모든 문자를 인코딩하지 않음을 명시함.
이 방식은 <form> 요소가 파일이나 이미지를 서버로 전송할 때 주로 사용함.

martipart : 파일이나 이미지를 서버로 전송할 때 주로 사용함.

 

 

유효성 검사

- 가격에는 어노테이션을

@Pattern(regexp = "^[1-9][0-9]*")

^시작할때 [1-9] 1에서9까지 쓸수있다 [0-9]뒷부분은 0에서 9까지 = 1~9999999까지
 
 
 
 

updatable = false 

처음 생성할때는 업데이트가 안되도록
@Column(name = "created_at", updatable = false) 
@CreationTimestamp   //생성될 때마다 자동생성(insert)
private LocalDateTime createAt;
 
 
 

 

카테고리 반복문

th:each="category : ${categories}"

<div class="form-group">
  <label for="">카테고리</label>
  <select class="form-control" th:field="*{categoId}" >
    <option th:each="category : ${categories}" th:text="${category.name}"></option>
  </select>
  <span class="error" th:if="${#fields.hasErrors('categoryId')}" th:errors="*{categoryId}"></span>
</div>

 

get매핑

카테고리 리스트가 나오도록 

//카테고리 선택하도록
List<Category> categories = categoryRepo.findAll();
model.addAttribute("categories", categories);

 


오류발생

 

카테고리 옵션에 value 값을 줘야한다?

 

th:value="${category.id}" 

<option th:each="category : ${categories}" th:value="${category.id}" th:text="${category.name}"></option>

 

결과 

value값에 id를 넣어서 카테고리를 찾아넣어준다.

카테고리 선택시 전체는 빼준다.
<option th:if="${category.name} != '전체'" th:each="category : ${categories}" th:value="${category.id}" th:text="${category.name}"></option>
 
 
 
 
 
 
#
select에 th:field="*{categoryId}" 는 왜 필요할까??

 

반응형
LIST

products 테이블 생성

- DECIMAL(8,2) : 8자리 소수점 2자리 

- TIMESTAMP : 날짜 + 시간 (숫자형),자동으로 현날짜 입력 ↔  DATETIME 문자형, 직접입력

 

@Entity
@Table(name = "products")
@Data
public class Product {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	
	private String name;
	private String slug;
	private String description;
	private String image;
	private String price;
	
	@Column(name = "category_id")
	private String categoryId;
	
	@Column(name = "created_at")
   	@CreationTimestamp
	private LocalDateTime createAt;
	
	@Column(name = "updated_at")
    	@UpdateTimestamp
	private LocalDateTime updateAt;
	
}

- LocalDate : 날짜만 2019-11-12
- LocalTime 시간만 18:32:17
- LocalDateTime 날짜 + 시간 2019-11-12T16:23:53

 

 

- 날짜를 자동생성하는 어노테이션

insert할때
@CreationTimestamp

update할때(수정)
@UpdateTimestamp

 

반응형
LIST

+ Recent posts