春コース10日目

134483 ワード

韓日

  • category sorting
  • 商品ページ作成
  • 追加商品
  • 商品修正/削除
  • category sorting


    - AdminCategoryController -
    @GetMapping
    private String index(Model model) {
    	List<Category> categories = categoryRepo.findAllByOrderBySortingAsc(); 
    	model.addAttribute("categories", categories);
    	return "admin/categories/index";
    }
    	...
    @PostMapping("/reorder")
    public @ResponseBody String reorder(@RequestParam("id[]") int[] id) {
        
        int count = 1;
        Category category;
    
        for (int categoryId : id) {
        	category = categoryRepo.getById(categoryId);	// DB에서 id로 category객체 검색
        	category.setSorting(count);				// setSorting에 count값을 넣어줌
        	categoryRepo.save(category); 	//sorting 값을 순서대로 저장
            count++;
        }
        return "ok";	// view페이지가 아니라 문자열 ok로 리턴
    }
    - CategoryRepository.inferface -
    List<Category> findAllByOrderBySortingAsc();
    =>メソッドの名前は明確ですので、メソッド名の変更部分と変更すべきでない部分に注意してください.
    最初に書くList<Category> findAllOrderBySortingAsc();エラーが発生しました.


    順序変更時にソートに保存し、そのまま出力します.

    商品ページの作成


    製品テーブルの作成
    CREATE TABLE IF NOT EXISTS products (
    	id int not null aUTO_INCREMENT,
        name VARCHAR(45) not null,
        slug VARCHAR(45) not null,
        description text not null,
        image VARCHAR(45) not null,
        price INT not null,
        category_id int not null,
        create_at timestamp not null,
        update_at timestamp not null,
    	PRIMARY KEY (id)
    );
    text:大型文字列データ
    decimal(x,y):小数点を表す数値形式.整数xビット、小数yビット
    タイムスタンプ:日付と時刻を表すデータ型
    MySQL日付タイプを参照
    Java日付タイプを参照

    新しいクラスの作成
    - Product -
    @Entity
    @Table(name = "products")
    @Data  //겟,셋 생성자, toString 생성됨
    public class Product {
    	@Id
    	@GeneratedValue(strategy = GenerationType.IDENTITY)
    	private int id;
    
    	@NotBlank(message = "품명을 입력해 주세요.")
    	@Size(min = 2, message = "품명은 2자 이상")
    	private String name;
    	
    	private String slug;	
    	private String description;	// 상품설명
    	private String image;		// 상품이미지 주소
    	private String price;		// 문자열로 지정 후 변환해서 사용
    	
    	@Column(name = "category_id")	// DB의 테이블명과 다를경우 @column을 이용해 매핑
    	private String categoryId;		// 상품의 카테고리ID
    	
    	@Column(name = "create_at")
    	@CreationTimestamp				// insert시 자동으로 시각이 입력됨
    	private LocalDateTime createAt;	// 상품 등록 시간
    	
    	@Column(name = "update_at")		// update시 자동으로 시각이 입력됨
    	private LocalDateTime updateAt;	// 상품 업데이트 시간
    }

    新しいインタフェースの作成
    - ProductRepository -
    public interface ProductRepository extends JpaRepository<Product, Integer>{	
    }

    新しいクラスの作成
    - AdminProductController -
    @Controller
    @RequestMapping("/admin/products")
    public class AdminProductController {
    	@Autowired
    	private ProductRepository productRepo;
    	
    	@GetMapping
    	public String index(Model model) {
    		List<Product> products = productRepo.findAll();
    		model.addAttribute("products", products);
    		
    		return "admin/products/index";
    	}
    }
    索引ページの作成


    - index.html -
    <main role="main" class="container">
      <div class="display-2">Products</div>
      <a th:href="@{/admin/products/add}" class="btn btn-primary my-3">추가하기</a>
      <div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>
    
      <div th:if="${!products.empty}">
        <table class="table" id="products">
          <tr>
            <th>상품명</th>
            <th>이미지</th>
            <th>카테고리</th>
            <th>가 격</th>
            <th>수 정</th>
            <th>삭 제</th>
          </tr>
          <tr th:each="product : ${products}">
            <td th:text="${product.name}"></td>
            <td th:text="${product.image}"></td>
            <td th:text="${product.categoryId}"></td>
            <td th:text="${product.price}"></td>
            <td><a th:href="@{'/admin/products/edit/' + ${product.id}}">수정</a></td>
            <td><a class="deleteConfirm" th:href="@{'/admin/products/delete/' + ${product.id}}">삭제</a></td>
          </tr>
        </table>
      </div>
      <div th:if="${products.empty}">
        <div class="display-4">등록된 상품이 없습니다...</div>
      </div>
    </main>
    http://localhost:8080/admin/products

    商品ページが正常に出力されていることを確認します.

    商品を追加する


    商品ページの追加


    以前に作成したadmin categorycontrollerをインポートして変更します.
    - AdminProductController -
    @Autowired
    private CategoryRepository categoryRepo;
    	...
    @GetMapping("/add")
    public String add(@ModelAttribute Product product, Model model) {
    	List<Category> categories = categoryRepo.findAll();
    	model.addAttribute("categories", categories);
    	// 상품을 추가하는 add 페이지에 상품객체와 상품의 카테고리를 선택할수있도록 카테고리 리스트도 전달
    	return "admin/products/add";
    }
    =>商品を追加するにはカテゴリ情報が必要なので、持ってきましたCategoryRepository- add.html -
    <div class="container">
      <div class="display-2">카테고리 추가</div>
      <a th:href="@{/admin/products}" class="btn btn-primary my-3">돌아가기</a>
    
      <form method="post" enctype="multipart/form-data" th:object="${product}" th:action="@{/admin/products/add}">
        <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
        <div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>
    
        <div class="form-group">
          <label for="">상품명</label>
          <input type="text" class="form-control" th:field="*{name}" placeholder="상품명" />
          <span class="error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
        </div>
        <div class="form-group">
          <label for="">상품설명</label>
          <input type="text" class="form-control" th:field="*{description}" placeholder="상품설명" />
          <span class="error" th:if="${#fields.hasErrors('description')}" th:errors="*{description}"></span>
        </div>
        <div class="form-group">
          <label for="file">이미지</label>
          <input type="file" class="form-control" th:id="file" placeholder="상품 이미지" />
          <!-- 미리 올릴 이미지를 표시 -->
          <img src="#" id="imgPreview" />
        </div>
        <div class="form-group">
          <label for="">가 격</label>
          <input type="text" class="form-control" th:field="*{price}" placeholder="가격" />
          <span class="error" th:if="${#fields.hasErrors('price')}" th:errors="*{price}"></span>
        </div>
    
        <div class="form-group">
          <label for="">카테고리</label>
          <select th:field="*{categoryId}" class="form-control">
            <option value="0">카테고리 선택</option>
            <option th:each="category : ${categories}" th:value="${category.id}" th:th:text="${category.name}"></option>
          </select>
          <span class="error" th:if="${#fields.hasErrors('categoryId')}" th:error="*{categoryId}"></span>
        </div>
        <button type="submit" class="btn btn-danger">추 가</button>
      </form>
    </div>
    <form enctype="multipart/form-data">:サーバ転送時のデータ符号化・変換を指定するために使用します.ファイル、イメージサーバの転送時にmultipart/form-dataを使用します.
    http://localhost:8080/admin/products/add

    検証#ケンショウ#


    - Product -
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    
    @NotBlank(message = "품명을 입력해 주세요.")
    @Size(min = 2, message = "품명은 2자 이상")
    private String name;
    
    private String slug;	
    
    @NotBlank(message = "상품설명을 입력해 주세요.")
    @Size(min = 2, message = "설명은 2자 이상")
    private String description;	// 상품설명
    private String image;		// 상품이미지 주소
    
    @Pattern(regexp = "^[1-9][0-9]*")	// 1 ~ 999999999 까지 사용가능
    private String price;		// 문자열로 지정 후 변환해서 사용
    
    @Pattern(regexp = "^[1-9][0-9]*", message = "카테고리를 선택해주세요")
    @Column(name = "category_id")	// DB의 테이블명과 다를경우 @column을 이용해 매핑
    private String categoryId;		// 상품의 카테고리ID
    
    @Column(name = "create_at", updatable = false)
    @CreationTimestamp				// insert시 자동으로 시각이 입력됨
    private LocalDateTime createAt;	// 상품 등록 시간
    
    @Column(name = "update_at")		
    @UpdateTimestamp				// update시 자동으로 시각이 입력됨 
    private LocalDateTime updateAt;	// 상품 업데이트 시간
    @Pattern(regexp = "^[1-9][0-9]*"):1文字目は1~9文字のうち1文字のみ使用できます.2番目の文字から0~9文字のいずれかを使用できます.*複数回繰り返すことが許される.
    その他はエラー処理です.
    [java正規表現://coding-factory.tistory.com/529)@Column(name = "create_at", updatable = false):登録時に最初の視点のみ入力します.すなわち,時間はxを更新する.
    - add.html -
    スクリプトラベルの追加
    <script>
      $(function () {
        $('#imgPreview').hide(); // 처음에 숨김
        $('#file').change(function () {
          // 파일 변경시(새로올림, 교체) 이벤트 발생
          readURL(this); // readURL 함수 실행
        });
      });
    
      function readURL(input) {
        // 파일(이미지)이 있을 경우 실행
        if (input.files && input.files[0]) {
          let reader = new FileReader(); // 파일리더 객체생성
    
          reader.readAsDataURL(input.files[0]); // 파일리더로 첫번째 파일경로 읽기
    
          // 파일리더가 주소를 다 읽으면 onload 이벤트가 발생하고 이때 화면에 img를 출력
          reader.onload = function (e) {
            $('imgPreview').attr('src', e.target.result).width(200).show();
          };
        }
      }
    </script>

    商品を追加する


    - AdminProductController -
    @PostMapping("/add") 
    public String add(@Valid Product product, BindingResult bindingResult, MultipartFile file,
    										RedirectAttributes attr, Model model) throws IOException {
    	if (bindingResult.hasErrors()) {
    		List<Category> categories = categoryRepo.findAll();
    		model.addAttribute("categories", categories);
    		return "admin/products/add";	// 유효성검사 에러발생시 되돌아감
    	}
    	
    	boolean fileOk = false;
    	byte[] bytes = file.getBytes();	// 업로드된 img파일의 데이터
    	String fileName = file.getOriginalFilename();	// 파일의 이름
    	Path path = Paths.get("src/main/resources/static/media/" + fileName); // 파일을 저장할 위치와 이름까지
    	
    	if (fileName.endsWith("jpg") || fileName.endsWith("png")) {
    		fileOk = true;	// 확장자가 jpg, png인 파일만 true
    	}
    	// 성공적으로 추가됨
    	attr.addFlashAttribute("message", "상품이 성공적으로 추가됨!");
    	attr.addFlashAttribute("alertClass", "alert-success");
    	// 슬러그 만들기
    	String slug = product.getName().toLowerCase().replace(" ", "-");
    	// 동일한 상품명이 있는지 검사
    	Product productExists = productRepo.findByName(product.getName());
    	
    	if(!fileOk) {	// 파일 업로드가 안됐거나 확장자가 jpg, png가 아닌경우
    		attr.addFlashAttribute("message", "이미지는 jpg나 png파일을 사용해주세요!");
    		attr.addFlashAttribute("alertClass", "alert-danger");
    		attr.addFlashAttribute("product", product);
    		
    	} else if (productExists != null) {	// 동일한 상품명이 DB에 존재
    		attr.addFlashAttribute("message", "이미 존재하는 상품명입니다.");
    		attr.addFlashAttribute("alertClass", "alert-danger");
    		attr.addFlashAttribute("product", product);
    		
    	} else {	// 상품과 이미지 파일을 저장함
    		product.setSlug(slug);
    		product.setImage(fileName);	// img는 파일의 이름만 입력(주소는 /media/폴더 이므로 동일)
    		productRepo.save(product);
    		
    		Files.write(path, bytes);	// (저장주소, 데이터)
    	}
    	return "redirect:/admin/products/add";
    }
    パラメータMultipartFile:ファイルを渡す必要があるString fileName = file.getOriginalFilename();:パラメータとして受信した複数のファイルの対象ファイルの名前をfilenameに保存するfileName.endsWith("jpg")||fileName.endsWith("png"):ファイル拡張子は.jpg, .pngという名前のファイルのみを通過
    - ProductRepository -
    Product findByName(String name);
    ここでは、商品の画像を英語に準備してこそ、以下の画像をロードする操作でエラーを回避することができる.

    保存した画像を読み込むパスを設定


    - WebConfig -
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    	// 저장된 이미지파일을 불러올 경로 지정
    	registry.addResourceHandler("/media/**")
    			.addResourceLocations("file:///C:/java502/SpringWorkspace/shoppingmall/src/main/resources/static/media/");
    }
    - application.properties -
    サーバ負荷を回避するために、最大ファイルサイズ(複数のイメージをアップグレードする場合)と合計ファイルサイズを指定します.
    # File upload setting
    spring.servlet.multipart.max-file-size = 10MB
    spring.servlet.multipart.max-request-size = 30MB
    - index.html -
    <table class="table" id="products">
      <tr>
        <th>상품명</th>
        <th>이미지</th>
        <th>카테고리</th>
        <th>가 격</th>
        <th>수 정</th>
        <th>삭 제</th>
      </tr>
      <tr th:each="product : ${products}">
        <td th:text="${product.name}"></td>
        <td>
          <img th:src="@{'/media/' + ${product.image}}" style="height: 2em" />
        </td>
        <td th:text="${product.categoryId}"></td>
        <td th:text="${product.price}"></td>
        <td><a th:href="@{'/admin/products/edit/' + ${product.id}}">수정</a></td>
        <td><a class="deleteConfirm" th:href="@{'/admin/products/delete/' + ${product.id}}">삭제</a></td>
      </tr>
    </table>
    imgセクションを変更して、商品テーブル全体に商品の画像をプレビューします.

    商品リストのカテゴリ名の変更


    DBに外部キーが設定されていないため、製品のidとcategoryをマッピングとしてマッチング
    - AdminProductController -
    indexメソッドの変更
    @GetMapping
    public String index(Model model) {
    	List<Product> products = productRepo.findAll();
    	List<Category> categories = categoryRepo.findAll();
    	
    	HashMap<Integer, String> cateIdAndName = new HashMap<>();
    	for (Category category : categories) {
    		cateIdAndName.put(category.getId(), category.getName());
    	}
    	
    	model.addAttribute("products", products);
    	model.addAttribute("cateIdAndName", cateIdAndName);
    	
    	return "admin/products/index";
    }
    - index.html -
    <table class="table" id="products">
      <tr>
        <th>상품명</th>
        <th>이미지</th>
        <th>카테고리</th>
        <th>가 격</th>
        <th>수 정</th>
        <th>삭 제</th>
      </tr>
      <tr th:each="product : ${products}">
        <td th:text="${product.name}"></td>
        <td>
          <img th:src="@{'/media/'+${product.image}}" style="height: 2em" />
        </td>
        <td th:text="${cateIdAndName[__${product.categoryId}__]}"></td>
        <td th:text="${product.price}+' 원'"></td>
        <td><a th:href="@{'/admin/products/edit/' + ${product.id}}">수정</a></td>
        <td><a class="deleteConfirm" th:href="@{'/admin/products/delete/' + ${product.id}}">삭제</a></td>
      </tr>
    </table>
    ${}${}繰り返しが必要な場合は、内部${}に前後__を加え、${__${}__}の形で使用する.
    =>category idは、そのidに対応するカテゴリ名を出力します.
    @ModelAttribute概念整理が必要

    商品の修正/削除


    商品修正


    - AdminProductController -
    @GetMapping("/edit/{id}")
    public String edit(@PathVariable int id, Model model) {
    	
    	Product product = productRepo.getById(id);
    	List<Category> categories = categoryRepo.findAll(); 
    	
    	model.addAttribute("categories", categories);
    	model.addAttribute("product", product);
    	return "admin/products/edit";
    }
    
    @PostMapping("/edit")
    public String edit(@Valid Product product, BindingResult bindingResult, MultipartFile file,
    		RedirectAttributes redirectAttributes, Model model) throws IOException {
        //우선 수정하기전의 상품의 객체를 DB에서 읽어오기 ( id 로 검색 )
    	Product currentProduct = productRepo.getById(product.getId());
    
    	if (bindingResult.hasErrors()) {
    		List<Category> categories = categoryRepo.findAll();
    		model.addAttribute("categories", categories);
            if(product.getImage() == null)	product.setImage(currentProduct.getImage());	// 저장된 이미지 불러오기
    		return "/admin/products/edit";
    	}
    
    	boolean fileOk = false;
    	byte[] bytes = file.getBytes(); // 업로드한 파일의 데이터
    	String fileName = file.getOriginalFilename(); // 업로드한 파일의 이름
    	Path path = Paths.get("src/main/resources/static/media/" + fileName); // 파일을 저장할 컨텍스트 안의 경로
    
    	if (!file.isEmpty()) { // 새 이미지 파일이 있을경우
    		if (fileName.endsWith("jpg") || fileName.endsWith("png")) { // 파일의 확장자 jpg , png
    			fileOk = true;
    		}
    	} else { // 파일이 없을경우 ( 수정 이므로 이미지 파일이 없어도 OK )
    		fileOk = true;
    	}
    
    	// 성공적으로 product 수정 되는 경우
    	redirectAttributes.addFlashAttribute("message", "상품이 수정됨");
    	redirectAttributes.addFlashAttribute("alertClass", "alert-success");
    
    	String slug = product.getName().toLowerCase().replace(" ", "-");
                             //제품이름을 수정했을 경우에 slug가 다름 제품과 같지 않는지 검사
    	Product productExists = productRepo.findBySlugAndIdNot(slug, product.getId());
    
    	if (!fileOk) { // file 업로드 안되거나 확장자가 틀림
    
    		redirectAttributes.addFlashAttribute("message", "이미지는 jpg나 png를 사용해 주세요");
    		redirectAttributes.addFlashAttribute("alertClass", "alert-danger");
    		redirectAttributes.addFlashAttribute("product", product);
    
    	} else if (productExists != null) { // 이미 등록된 상품 있음
    
    		redirectAttributes.addFlashAttribute("message", "상품이 이미 있습니다. 다른것을 고르세요");
    		redirectAttributes.addFlashAttribute("alertClass", "alert-danger");
    		redirectAttributes.addFlashAttribute("product", product);
    
    	} else { // 상품과 이미지 파일을 저장한다.
    
    		product.setSlug(slug); // 슬러그 저장
    		
    		if (!file.isEmpty()) { // 수정할 이미지 파일이 있을 경우에만 저장(이때 이전 파일 삭제)
    			Path currentPath = Paths.get("src/main/resources/static/media/" + currentProduct.getImage());
    			Files.delete(currentPath);
    			product.setImage(fileName);				
    			Files.write(path, bytes);	//Files 클래스를 사용해 파일을 저장		
    		} else {
    			product.setImage(currentProduct.getImage());	
    		}
    		
    		productRepo.save(product);
    		
    	}
    	return "redirect:/admin/products/edit/" + product.getId();
    }
    - ProductRepository -
    Product findBySlugAndIdNot(String slug, int id);
    - edit.html -
    残りはaddhtmlと同じ
    <div class="form-group">
      <label for="">이미지</label>
      <input type="file" class="form-control" th:id="file" th:name="file" />
      <img src="#" id="imgPreview" />
      <br /><br />
      <label for="">현재 이미지</label>
      <img th:src="@{'/media/'+${product.image}}" style="width: 200px" />
    </div>
    <div class="form-group">
      <label for="">가 격</label>
      <input type="text" class="form-control" th:field="*{price}" placeholder="가격(원)" />
      <span class="error" th:if="${#fields.hasErrors('price')}" th:errors="*{price}"></span>
    </div>
    <div class="form-group">
      <label for="">카테고리</label>
      <select th:field="*{categoryId}" class="form-control">
        <option value="0">카테고리 선택</option>
        <option th:each="category : ${categories}" th:value="${category.id}" th:text="${category.name}"></option>
      </select>
      <span class="error" th:if="${#fields.hasErrors('categoryId')}" th:errors="*{categoryId}"></span>
    </div>
    <input type="hidden" th:field="*{id}" th:value="${product.id}" />
    <button class="btn btn-danger">수정 하기</button>

    商品の削除


    - AdminProductController -
    @GetMapping("/delete/{id}")
    public String delete(@PathVariable int id, RedirectAttributes redirectAttributes) throws IOException {
    	// id로 상품을 삭제하기 전 먼저 id로 상품객체를 불러와 이미지파일을 삭제한 후 삭제진행
    	Product currentProduct = productRepo.getById(id) ;
    	Path currentPath = Paths.get("src/main/resources/static/media/" + currentProduct.getImage());
    	
    	Files.delete(currentPath);		// 파일먼저 삭제
    	productRepo.deleteById(id);		// 상품 삭제
    	
    	redirectAttributes.addFlashAttribute("message", "성공적으로 삭제 되었습니다.");
    	redirectAttributes.addFlashAttribute("alertClass", "alert-success");
    	
    	return "redirect:/admin/products";
    }