241106 에이콘 아카데미 수업을 기반하여 작성되었음을 알립니다.
RESTful
겟/포스트 방식만 사용하는 것보다 싱글페이지어플리케이션을 구현하기 위해 레스트풀을 사용하면 쉽게 구현 가능하다 html/자바스크립트/css만으로도 구현 가능하다!
일반 html 요청과 레스트풀 요청으로 나누어서 컨트롤러 작성
자바스크립트, 함수를 잘 알면 새로운 기술이 나와도 금방 적응 가능하다~
RESTful 실습 (CRUD)
dependencies
Spring Web / Spring Boot DevTools / Spring Data JPA / Lombok / MariaDB Driver / Thymeleaf
application.properties
spring.application.name=sprweb35restful_mem
server.port=80
spring.thymeleaf.cache=false
spring.jpa.open-in-view=false
#mariadb server connect
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/memberdb
spring.datasource.username=root
spring.datasource.password=1111
# jpa
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
[2m2024-11-06T11:00:40.710+09:00[0;39m [33m WARN[0;39m [35m16496[0;39m [2m---[0;39m [2m[sprweb35restful_mem] [ restartedMain][0;39m [2m[0;39m[36mJpaBaseConfiguration$JpaWebConfiguration[0;39m [2m:[0;39m spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
Spring Boot App 실행시 경고가 뜨는데 경고는 무시해도 무방하지만 안뜨게 해줄 수 있음.
해당 LAZY 로딩과 관련있음, 지연로딩을 막아줌, 필요할 때만 로딩되게 설정하는면 되는데 방법은 아래와 같다.
application.properties에 spring.jpa.open-in-view=false 추가 해주면 된다.
Bean
package pack.controller;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MemBean {
private int num;
private String name;
private String addr;
}
Entity
package pack.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Mem {
@Id
private int num;
private String name;
private String addr;
}
Repository
package pack.model;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface MemCrudRepository extends JpaRepository<Mem, Integer> {
// num 자동 증가 처리를 위한 코드 작성
@Query(value = "select max(m.num) from Mem as m")
int findByMaxNum();
@Query(value = "select m from Mem as m where m.num=?1")
Mem findByNum(String num);
}
DAO
package pack.model;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import pack.controller.MemBean;
@Repository
public class DataProcess {
@Autowired
private MemCrudRepository repository; // hikari pool 자동 지원
// 전체 자료 읽기
public List<Mem> getDataAll() {
List<Mem> list = repository.findAll(); // 기본 메소드
return list;
}
// 추가
public String insert(MemBean bean) {
// 가장 큰 번호를 구해 +1 하고 추가 시 사용
//int max = repository.findByMaxNum();
// 입력한 번호 중복 확인(실습)
try {
Mem mem = repository.findById(bean.getNum()).get();
return "이미 등록된 번호입니다.";
} catch (Exception e) {
// findById의 결과가 에러인 경우(등록 가능한 번호)
try {
Mem mem = new Mem(bean.getNum(), bean.getName(), bean.getAddr());
repository.save(mem);
return "success";
} catch (Exception e2) {
return "입력 자료 오류 : " + e2.getMessage();
}
}
}
// 수정/삭제를 위한 레코드 읽기
public Mem getData(String num) {
//Mem mem = repository.findById(bean.getNum()).get(); // 기본 제공 메소드
Mem mem = repository.findByNum(num);
return mem;
}
// 수정
public String update(MemBean bean) {
try {
Mem mem = new Mem(bean.getNum(), bean.getName(), bean.getAddr());
repository.save(mem);
return "success";
} catch (Exception e) {
return "수정 작업 오류 : " + e.getMessage();
}
}
// 삭제
public String delete(int num) {
try {
repository.deleteById(num);
return "success";
} catch (Exception e) {
return "삭제 작업 오류 : " + e.getMessage();
}
}
}
ViewController
package pack.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import pack.model.DataProcess;
import pack.model.Mem;
@Controller
@RequestMapping("/members")
public class MemViewController {
@Autowired
private DataProcess dataProcess;
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/list")
public String list() {
return "list";
}
@GetMapping("/new")
public String insert() {
return "insert";
}
@GetMapping("/update/{num}") // 수정할 대상 파일을 읽어 준 후 수정 페이지로 호출
public String updateProcess(@PathVariable("num")String num, Model model) {
Mem mem = dataProcess.getData(num);
model.addAttribute("data", mem);
return "update";
}
}
Controller
package pack.controller;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pack.model.DataProcess;
import pack.model.Mem;
@RestController // JSON을 반환하겠다는 의미
@RequestMapping("/api") // 아래 모든 메소드의 앤드포인트 경로에 /api를 기본 경로(prefix)로 설정, MemViewController와 구분
public class MemController {
@Autowired
private DataProcess dataProcess;
@GetMapping("/members") // SELECT
public List<Mem> listProcess() { // 전체 자료 읽기
return dataProcess.getDataAll();
}
@PostMapping("/members") // INSERT
public Map<String, Object> listProcess(@RequestBody MemBean bean) { // 자료 추가
dataProcess.insert(bean);
return Map.of("isSuccess", true);
}
@PutMapping("/members") // UPDATE
public Map<String, Object> updateProcess(@RequestBody MemBean bean) { // 자료 수정
dataProcess.update(bean);
return Map.of("isSuccess", true);
}
@DeleteMapping("/members/{num}") // DELETE
public Map<String, Object> deleteProcess(@PathVariable("num")int num) { // 자료 삭제
dataProcess.delete(num);
return Map.of("isSuccess", true);
}
}
결과
Swagger로 GET 요청
Swagger로 POST 요청
Swagger로 PUT 요청
Swagger로 DELETE 요청
문제
싱글페이지로 CRUD 구현해보기!
1. CSS 적용해주기
2. 자바스크립트 파일 따로 만들기
에러
🚨 TypeError: products.forEach is not a function
해당 에러는 함수에 전달된 products 가 배열이 아닌 경우에 발생한다고 한다.
{ "products": [ { "dan": "999", "sang": "장갑", "su": "999", "code": "1" }, { "dan": "4444", "sang": "벙어리", "su": "4444", "code": "2" }, { "dan": "555", "sang": "지갑", "su": "555", "code": "3" }, { "dan": "99999", "sang": "커피", "su": "9999", "code": "4" }, { "dan": "5555", "sang": "안경", "su": "5555", "code": "5" } ] }
반환되는 데이터가 " products" 키에 배열이 포함된 객체로 넘어오고 있는 상태였다.
해당 코드를 아래처럼 변경하여 products의 배열로 접근하게 하여 에러 해결
.then(products => { const products = products; displayProduct(products);
▼
.then(list => { const products = list.products; displayProduct(products);
🚨 The method getCode() in the type SangpumBean is not applicable for the arguments (int)
Bean으로 받은 데이터를 인자로 받을 수 있는 생성자가 엔티티에 없는 것을 확인!
그래서 엔티티에 롬복 어노테이션인 @AllArgsConstructor을 추가해주어 인자를 받을 수 있게해주었다.
🚨 스웨거로 확인 시 수정할 데이터를 잘 가져오지만폼 데이터를 읽어오지 못하는 것을 확인
객체들을 각각 만들어주어 데이터들을 담을 수 있게 해주었다.
const form = document.querySelector("#updateFrm");
const formData = new FormData(form);
▼
const form = document.querySelector("#updateFrm");
const formData = new FormData(form);
const code = formData.get("code");
const sang = formData.get("sang");
const su = formData.get("su");
const dan = formData.get("dan");
결과
피드백
CSS 좀 더 적용해보기!
상품 추가, 수정 완료 시 폼이 유지되는 점 고쳐보기!
'Study > Acorn' 카테고리의 다른 글
241108/241111 파일 업로드/다운로드 (0) | 2024.11.08 |
---|---|
241107 RESTful / AOP (3) | 2024.11.07 |
241105 RESTful (PUT/DELETE/문제) (0) | 2024.11.05 |
241105 RESTful (GET/POST) (2) | 2024.11.05 |
241104 AJAX (DB연동/문제) (0) | 2024.11.04 |