빈티지 카메라 웹 사이트 만들기

빈티지 카메라를 살 수 있는 웹 사이트이며 프로젝트 만드는 과정을 정리하고 기록해보자 

 

 

  • 기능
    • 회원 
      • 로그인 / 회원가입
      • 회원 조회
    • 상품
      • 상품 등록
      • 상품 수정
      • 상품 조회
      • 상품 삭제
    • 주문
      • 상품 주문
      • 장바구니
      • 주문 취소
      • 주문 조회
    • 기타
      • 상품 재고 관리를 할 수 있다. 

 

 

  • 패키지 구조

도메인 : 핵심 비즈니스 업무 영역 (컨트롤러나 폼은 핵심 업무 도메인이 아님)

 

  • domain
    • login
    • item
    • member
    • order
    • orderItem
    • delivery
    • address
  • web
    • login
    • item
    • member
    • order
    • orderItem
    • delivery
    • address
  • exception
  • repository
  • service

 

"도메인"은 웹 기술에 의존하지 않고 "웹"은 다른 웹 기술로 바꿀 수 있다.

web 은 domain 을 의존(참조)해서 다른 웹기술로 변경되거나 삭제를 하더라도 domain은 영향을 받지 않는다.

그렇기에 웹과 도메인은 서로 다른 영역이며 domain은 web을 알 필요가 없다.

이렇게 의존관계가 단방향이 되어야 유지보수에 좋은 설계!

 

예를들어 domain의 itemRepository에 web 영역의 ItemSaveForm 이 사용되면 의존하게 돼버려서 쓰면 안되고 

web 영역의 ItemSaveForm 에 domain영역의 Item 을 불러와야한다. (new Item)

 

 

 

도메인 모델링

엔티티 매핑

  • 많이 헷갈리는 다대다 관계 정리
    • Order 주문과 Item 상품은 다대다 관계이다
      • 회원은 여러 주문을 할 수 있다
      • 그리고 주문에서 여러개의 상품을 담을 수 있다.
    • 주문(Order)과 상품(Item)은 다대다 관계이기 때문에 중간에 OrderItem 엔티티를 추가해준다
    • OrderItem 엔티티를 추가해서 주문 관계와는 일대다, 상품 관계와는 다대일이 된다. 
    • 왜냐하면 다대다 관계는 거의 사용하지 않기 때문!
  • 왜 item엔티티는 orderItem 을 참조안하는걸까?
    • 연관관계는 꼭 필요한 경우에만 설정하는 것이 좋다.
    • orderItem 입장에서 item 알아야하지만 반대로 item 은 orderItem 을 찾아갈 일이 많이 없기때문!

 

  • 왜 order 엔티티만 member엔티티를 참조하는걸까?
    • 보통 일대다 관계에서 다(N) 쪽이 외래키(FK) 를 가진다.
    • 외래키(FK) 를 갖고있는 쪽이 연관관계 주인이다.
    • order 엔티티가 연관관계 주인이라서 member 엔티티를 참조하는 것
  • 일대일 관계에서는 주로 양쪽 엔티티에서 서로를 참조한다 
    • Order 엔티티와 Delivery 엔티티처럼

 

 

 

테이블 매핑

외래 키가 있는 곳이 연관관계의 주인

 

 

 

 

프로젝트 생성

 

  • Project: Gradle Project
  • Language: Java
  • Spring Boot: 3.2.3  (뒤에 아무것도 안 붙인걸로 선택한다.) 

Project Metadata

  • Group: hello
  • Artifact: vintage-camera-shop
  • Name: vintage-camera-shop
  • Package name: hello.vintage-camera-shop
  • Packaging: Jar (주의) 
  • ava: 17

Dependencies

  • Spring Web
  • Lombok
  • Thymeleaf (JSP를 안 쓰고 타임리프를 사용)

 

 

 

 

프로젝트 생성 후 정상 실행이 되는지 확인

  • 메인메서드 run 해보기 
  • 콘솔에 8080 확인하고 http://localhost:8080/ 들어가서 Whitelabel Error Page 확인하면 성공

 

 

 

 

기본 세팅 

  • 롬북세팅 

settings > plugin > lombok 검색 후 실행 (재시작)

 

 

settings > Annotation Processors > Enable annotation processiong 체크하기!

 

 

 

  • 인텔리제이로 변경하기 

Settings > Gradle > IntelliJ 로 변경하기 

gradle로 하면 gradle을 통해 실행이 돼서 속도가 느려지기 때문

 

 

  • 데브툴 설정하기

html 수정할때마다 서버 재실행 안 하고 build > Recompile 만 클릭하면 수정이된다. 

 

 

build.gradle 에 spring-boot-devtools 의존성 추가하기 

runtimeOnly('org.springframework.boot:spring-boot-devtools')

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    runtimeOnly('org.springframework.boot:spring-boot-devtools')
}

 

 

settings 에 가서 Compiler > Build project automatically 체크하기

 

 

 

 

H2 DB 사용

설치 : https://www.h2database.com/

 

주의! 스프링 부트 3.x 이상은 2.1.214 버전 이상 사용해야 한다.

 

 

h2 를 설치가 끝나면

1. cmd창 열어서 h2 가 설치된 경로로 이동

 

2. h2.bat 을 입력하면 h2 창이 열린다. 

 

 

데이터베이스 파일 생성 순서

  • jdbc:h2:~/vintage-camera-shop (처음에 한번만!)
  • url 에 숫자부분을 localhost 로 수정해준다 
    •  
  • 사용자 폴더에 vintage-camera-shop.mv.db 파일 생성 확인
    • cmd창에서 확인 하는법
    • cd %HOMEPATH%
    • dir
  • vintage-camera-shop.mv.db가 생성되는 것을 확인
  • 이후부터는 jdbc:h2:tcp://localhost/~/vintage-camera-shop  접속해도 된다.

 

 

** H2 버전확인하기 ( 스프링 부트 3.x 이상이라1.4.200 버전이면 안됨!)

SELECT H2VERSION() FROM DUAL; 

삭제할때 혹시나 안되면 작업관리자에서 작업 끝내기 후에 삭제하면된다.

 

 

 

 

DB와 연결이 되는지 동작확인 테스트

main/resources/application.yml

 

properties 또는 yml 둘 중 하나를 써도 되는데 복잡하면 yml을 쓰는게 더 좋다고 한다.

기존에 있던 application.properties 을 삭제하고 application.yml 을 생성해서 붙여넣으면 된다.

 

** 띄어쓰기 오타 주의!!!

spring:
    datasource:
    url: jdbc:h2:tcp://localhost/~/vintage-camera-shop
    username: sa
    password:
    driver-class-name: org.h2.Driver
jpa:
    hibernate:
    	ddl-auto: create
    properties:
    	hibernate:
            # show_sql: true
            format_sql: true
    
logging.level:
	org.hibernate.SQL: debug
    # org.hibernate.orm.jdbc.bind: trace #스프링 부트 3.x, hibernate6

 

create : 애플리케이션 실행 시점에 테이블을 모두 지우고 다시 생성 

 

 

 

[Member]

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String username;

}

 

 

 

[MemberRepository]

 

회원 저장, 조회 메서드 생성하기

@Repository
public class MemberRepository {

	@PersistenceContext
	EntityManager em;

	public Long save(Member member) {
		em.persist(member);
		return member.getId();
	}

	public Member find(Long id) {
		return em.find(Member.class, id);
	}
}
  • @PersistenceContext : JPA에서 사용되는 어노테이션이며 엔티티 매니저를 주입받기 위해 사용된다.
  • EntityManager : 엔티티 매니저는 영속성 컨텍스트를 관리하고 DB와의 상호작용을 해준다.
    • em.persist(member) : Member 엔티티를 영속성 컨텍스트에 넣어 관리할 수 있고 조회, 수정 작업을 할 수 있다. 

 

 

 

테스트 클래스 [MemberRepositoryTest] 

 

참고) MemberRepository 에 shift + ctrl + T 단축키하면 테스트 클래스 생성 된다 (JUnit5 로 생성!)

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Transactional
    @Rollback(false)
    public void testMember() throws Exception {
        //given
        Member member = new Member();
        member.setUsername("memberA");

        //when
        Long savedId = memberRepository.save(member);
        Member findMember = memberRepository.find(savedId);

        //then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
        Assertions.assertThat(findMember).isEqualTo(member); //JPA 엔티티 동일성 보장
    }
}

 

* JUnit5 라면 @ RunWith(SpringRunner.class) 대신 @ExtendWith(SpringExtension.class)을 사용

 

  •    @Transactional : 엔티티 매니저를 통한 모든 데이터 변경은 트랜잭션 안에서 이루어져야한다. 그래서 어노테이션 꼭 붙여야함!

 

  • Assertions 생략하기

import org.assertj.core.api.Assertions; 로 임포트하기

 

 

Assertions 에 커서를 두고 alt + enter 해서 Add on-demand~~ 클릭하면 앞으로 Assertions 생략이 된다.

 

 

생략된 Assertions 코드가 간결해졌다

// then
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
assertThat(findMember).isEqualTo(member);

 

 

 

 

[실행결과]

 

 

 

쿼리 파라미터가 ?? 로 보이니까 SQL을 로그에 보이게 하고싶다면?? 

 

SQL 쿼리를 로그에 찍히게 하기 

 

1. application.yml 에 org.hibernate.orm.jdbc.bind: trace 추가하면 된다. (스프링 부트 3.x 이상일 경우)

logging.level:
 org.hibernate.SQL: debug
 org.hibernate.orm.jdbc.bind: trace #스프링 부트 3.x, hibernate6

 

 

* 스프링 부트 2.x  이상은 "org.hibernate.type: trace"  추가

 

 

[실행결과]

로그가 찍히는걸 볼 수 있다! 

 

 

 

 

좀 더 자세히 보고 싶다면? 

 

2. build.gradle 에 외부 라이브러리 추가하기 (스프링 부트 3.x 이상일 경우)

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'

 

 

[실행결과]

 

 

 

 

 

 

 

마지막으로 타임리프 적용되는지 테스트 해보기 

 

 

[HomeController]

@Controller
@Slf4j
public class HomeController {

    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!!");
        return "home";
    }
}

 

 

[home.html]

<html xmlns:th="http://www.thymeleaf.org">
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>

 

 

 

 

반응형
LIST

 

h2 연동 에러들

 

에러문구 

Caused by: java.lang.RuntimeException: Driver com.mysql.cj.jdbc.Driver claims to not accept jdbcUrl,

 

 

해결

application.yml 또는 .properties 에 분명 오타 또는 띄어쓰기 잘못된거 있으니까 잘 보면 된다..

 

 

정말 삽질을 오래했는데 알고봤더니 jdbc 에서 j가 빠져있었다.. 

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/vintage-camera-shop

 

 

 

 

에러문구 : h2 DB가 제대로 연결이 안됐다?

java.lang.ClassNotFoundException: org.h2.Driver

 

 

해결

 build.gradle 에 runtimeOnly 'com.h2database:h2'  추가하면 된다!

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    compileOnly 'org.projectlombok:lombok'

    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    runtimeOnly('org.springframework.boot:spring-boot-devtools')
    
     runtimeOnly 'com.h2database:h2'
}
반응형
LIST

 

 

상품등록 후 새로고침하면 중복등록이 되는 문제가 발생

 

 

[실제 상품을 등록한 뷰 화면]

 

위처럼 상품 등록한 상태에서 새로고침을 누른다면 상품ID는 계속 올라가고 상품이 계속 등록되는 이슈가 생긴다.

 

 

 

[상품등록 POST - BasicItemController ]

@PostMapping("/add")
public String addItemV4(Item item){
    itemRepository.save(item);
    return "basic/item";
}

 

  • 전체 흐름
    • 실제 상품등록
    • 상품 상세 뷰로 뷰템플릿(basic/item)을 호출 - 끝
    • 상품 등록 (POST / add) URL 이 유지되어있는 상태
    • 새로고침하면 계속 POST / add 행위가 지속된다.
    • 새로고침 시 마지막 행위가 다시 요청되기 때문에 상품이 중복저장이 되는 이슈가 발생한다. 
  •  

 

 

 

 

 

문제 해결 방법은 리다이렉트! 

 

 

POST ▶ Redirect ▶ GET

실제 상품을 등록하고 뷰 템플릿으로 호출하는게 아니라 상품 상세화면으로 리다이렉트를 하면된다.

그렇게 하면 상품 등록 후 상품 상세 화면으로 다시 이동해서 (리다이렉트) 마지막에 호출한 GET /items/{id} 이 된다.

 

  • 전체 흐름 
    • 실제 상품등록 POST/ add
    • 상품 상세 화면으로 리다이렉트 Redirect items/{itemId} 호출 http://localhost:8080/basic/items/3
    • 웹 브라우저가 상품 상세를 새로 요청 GET /items/{id} 
    • 새로고침을 해도 GET /items/{id} 상품 상세 화면으로 이동
    • 문제 해결 

 

 

 

[상품등록 POST redirect - BasicItemController ]

@PostMapping("/add")
public String addItemV5(Item item) {
    itemRepository.save(item);
    return "redirect:/basic/items/" + item.getId();
}

 

※ 주의

+item.getId() 처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험

 

 

 

 

RedirectAttributes

RedirectAttributes 을 사용하여 URL 인코딩 문제 해결과 상품이 실제로 등록이 됐을때 "저장되었습니다." 메세지가 나오도록 해보자 

@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes){
    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/basic/items/{itemId}";
}

 

RedirectAttributes 는 리다이렉트할 때 모델에 데이터를 전달할 수 있고 파라미터에 itemIdstatus 를 붙여주는 역할이다. 

  • basic/items/1?status=true

 

 

 

이제 저장완료 메세지를 만들어보자

 

리다이렉트  redirect:/basic/items/{itemId}  ▶ 최종 basic/item 뷰 템플릿을 호출한다. 

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    private final ItemRepository itemRepository;

    //상품 상세
    @GetMapping("/{itemId}")
    public String item(@PathVariable Long itemId, Model model){
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "basic/item";
    }
    
    ....

 

 

 

 

뷰템플릿

resources/templates/basic/item.html 

상품 상세

<div class="container">
    <div class="py-5 text-center">
        <h2>상품 상세</h2>
    </div>

    <!-- -->
    <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
    
    ...
  • th:if 해당 조건이 참일 때
  • ${param.status} : 파라미터 값을 조회할 수 있는 기
  • 참이면 '저장완료' 가 나온다.

 

 

 

상품 실제 등록시 '저장 완료' 메세지를 확인할 수 있다. 

반응형
LIST

 

상품수정은 상품등록처럼 같은 url (/{itemId}/edit)이다 

GET - 상품 수정 폼 

POST - 상품 수정 처리

 

 

 

컨트롤러 - 상품 수정 [BasicItemController] 에 추가 

 

  • 상품 수정 페이지를 열때는 GET (실제 수정 X)
//상품 수정
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model){

	Item item = itemRepository.findById(itemId);
	model.addAttribute("item", item);
	
	return "basic/editForm";
}

 

 

 

 

뷰 템플릿 [edit.html]

/resources/templates/basic/editForm.html

 

  • 타임리프 설정
<html xmlns:th="http://www.thymeleaf.org">
<link th:href="@{/css/bootstrap.min.css}"
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
        max-width: 560px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>상품 수정 폼</h2>
    </div>
    <form action="item.html" th:action method="post">
        <div>
            <label for="id">상품 ID</label>
            <input type="text" id="id" name="id" class="form-control" value="1"
                   readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="form-control" value="상품A" >
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   value="10000" th:value="${item.price}">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="form-control" value="10">
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">저장</button>
            </div>
            <!--수정폼에서 취소 눌렀을때 상품상세 페이지로-->
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='item.html'"
                        th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
                        type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>

 

 

<form action="item.html" th:action method="post">

상품수정은 상품등록처럼 유사하게 GET,POST URL이 같기 때문에 th:action 에 생략해도 괜찮다

 

 

 

상품 수정화면

그러나 '상품 수정' 버튼을 눌러서 상품 수정 폼에 들어가면 상품명이 안 바뀌어있다  ( itemA ▶ 상품A )

 

 

폼 input에 각 상품명, 가격, 수량에 th:value 값을 넣으면 된다.

  • th:value="${item.id}"
  • th:value="${item.itemName}"
  • th:value="${item.quantity}"

 

변경 되는 상품수정 화면 ( itemA itemA )

 

 

 

 

취소 버튼 눌렀을때 다시 수정 폼으로 돌아가기 

th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})
<button class="w-100 btn btn-secondary btn-lg"
        onclick="location.href='item.html'"
        th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
        type="button">취소
</button>

 

 

 

 

 

컨트롤러 - 상품 수정 [BasicItemController] 에 추가 

  • 상품을 실제로 수정할 때는 POST
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,@ModelAttribute Item item){

    itemRepository.update(itemId, item);
    //수정저장 후 redirect
    return "redirect:/basic/items/{itemId}";
}

 

페이지 이동 : 상품을 수정하고 저장하면 상품 상세 페이지로 이동하도록 한다.

 

 

 

 

리다이렉트 redirect 

상품 수정 마지막에 뷰 템플릿을 호출하는게 아니라 상품 상세 화면으로 이동하는 리다이렉트를 호출한다. 

  • redirect:/basic/items/{itemId}basic/items/1

 

 

 

상품 수정했을때 개발자 모드를 보면 

  • Location : http://localhost:8080/basic/items/1 로 리다이렉트를 한 뒤 
  • itemId가 1인 항목을 불러온다. 즉, 상품 상세 컨트롤러를 다시 호출한다.

 

 

 

 

반응형
LIST

 

 

컨트롤러 - 상품 등록 [BasicItemController] 에 추가 

 

  • 상품 등록 페이지를 열때는 GET (실제 등록 X)
//상품 등록 
@GetMapping("/add")
public String addForm(){
    return "basic/addForm";
}

 

  • 상품을 실제로 등록할 때는 POST
@PostMapping("/add")
public String save(){
    return "basic/addForm";
}

 

 

같은 URL인데 HTTP 메서드로 기능을 구별해준다. 

 

 

 

 

뷰 템플릿 [addForm.html]

/resources/templates/basic/addForm.html

 

  • 타임리프 설정
<html xmlns:th="http://www.thymeleaf.org">
<link th:href="@{/css/bootstrap.min.css}"
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
        max-width: 560px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>상품 등록 폼</h2>
    </div>
    <h4 class="mb-3">상품 입력</h4>
    <form action="item.html" th:action method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="form-control" placeholder="수량을 입력하세요">
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">상품 등
                    록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/basic/items}'|"
                        type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>

 

 

 

 

타임리프 속성

th:action

 

상품 등록 버튼을 눌렀을때 동적으로 URL이 변경되도록 하는 액션속성

 

  • 사실상 GET, POST 같은 URL (basic/items/add) 이기 때문에 안 써도 된다.
    • th:action="/basic/items/add"  ▶ <form action="item.html" th:action method="post">
  • 등록 페이지에 갔을때 '페이지 소스보기' 를 하면 action="" 비어있는 것을 확인할 수 있다. 

 

 

 

 

 

취소 버튼 눌렀을때 url 수정하기 

취소했을때는 목록으로 돌아가기 

th:onclick="|location.href='@{/basic/items}'|"
<button class="w-100 btn btn-secondary btn-lg"
        onclick="location.href='items.html'"
        th:onclick="|location.href='@{/basic/items}'|"
        type="button">취소
</button>

 

 

 


 

이제 실제로 상품을 등록해보자 

 

 

상품 등록 POST - @ModelAttribute

 

우선 @ModelAttribute 와 @RequestParam 차이점을 알아보자

 

 

@RequestParam

@RequestParam - 상품 등록 처리

 

클라이언트의 HTTP POST 요청파라미터를 처리하기 위해 @RequestParam 을 사용한다 

 

[addItemV1 - BasicItemController에 추가]

@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
                        @RequestParam int price,
                        @RequestParam Integer quantity,
                        Model model) {
                        
	Item item = new Item();
	item.setItemName(itemName);
	item.setPrice(price);
	item.setQuantity(quantity);

	itemRepository.save(item);

	model.addAttribute("item", item);
    
	return "";
}

 

 

@RequestParam String itemName 은 addForm.html 의 name="" 값이다. 

  • <input type="text" id="itemName" name="itemName" ~ >

 

 

 

실제 상품을 등록한 뷰 화면

 

 

 

@ModelAttribute 

@ModelAttribute - 상품 등록 처리

 

[addItemV2 - BasicItemController에 추가]

@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item ) {
	
	itemRepository.save(item);
	//model.addAttribute("item",item);
    
	return "basic/item";
}
  • @ModelAttribute - 요청 파라미터 처리를 해준다. 
    • Item 객체를 생성하고 요청 파라미터 값을 setXxxx 으로 Item 객체의 필드에 값을 설정해준다.
    •  Spring이 자동으로 처리해줌 개발자가 직접 setXxxx 메서드를 호출할 필요가 없어진다. 
    • Item item = new Item();
      item.setItemName(itemName);
      item.setPrice(price);
      item.setQuantity(quantity);
  • @ModelAttribute 은 자동으로 model에 뷰를 넣어준다.
    • @ModelAttribute("item") 이름이 ▶ model.addAttribute("item", item) 이다. 
    • model.addAttribute("item", item) 가 주석처리를 해도 잘 동작한다. 

 

 

 

ModelAttribute 이름 생략

 

[addItemV3 - BasicItemController에 추가]

@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
	
	itemRepository.save(item);
	//model.addAttribute("item",item);
    
	return "basic/item";
}
  • @ModelAttribute("item") Item item 이름을 생략하면 클래스의 첫글자만 소문자로 변경해서 등록한다. 
  • 클래스명 Item item

 

 

 

ModelAttribute 전체 생략

 

[addItemV4 - BasicItemController에 추가]

@PostMapping("/add")
public String addItemV4(Item item) {
	
	itemRepository.save(item);
    
	return "basic/item";
}
  •  addItemV1 처럼 단순타입인 String이라면 @RequestParam 이 자동적용되고 
  • 객체 타입인 경우 @ ModelAttribute가 자동적용이 된다. 
  • 이것 또한 클래스명 Item  item 소문자로 변경해서 모델에 담긴다. 

 

 

 

반응형
LIST

 

 

컨트롤러 - 상품 상세 [BasicItemController] 에 추가 

//상품 상세
@GepMapping(/{itemId})
public String item(@PathVariable Long itemId, Model model) {
	Item item = itemRepository.findById(itemId);
	model.addAttribute("item", item);
	return "basic/item";
}
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    private final ItemRepository itemRepository;

	//상품 목록
    @GetMapping
    public String items(Model model){
        List<Item> itmes = itemRepository.findAll(); //아이템 전체목록
        model.addAttribute("items", itmes);
        return "basic/items";
    }

	//상품 상세
    @GepMapping(/{itemId})
    public String item(@PathVariable Long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "basic/item";
    }
}

 

 

 

 

뷰 템플릿 [ item.html ]

  /resources/templates/basic/item.html

 

  • 타임리프 설정
<html xmlns:th="http://www.thymeleaf.org">
<link th:href="@{/css/bootstrap.min.css}"
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
        max-width: 560px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>상품 상세</h2>
    </div>

    <div>
        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control"
               value="1" th:value="${item.id}" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control"
               value="상품A" th:value="${item.itemName}" readonly>
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control"
               value="10000" th:value="${item.price}" readonly>
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control"
               value="10" th:value="${item.quantity}" readonly>
    </div>
    <hr class="my-4">
    <div class="row">
        <div class="col">
            <button class="w-100 btn btn-primary btn-lg"
                    onclick="location.href='editForm.html'"
                    th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
                    type="button">상품 수정</button>
        </div>
        <div class="col">
            <button class="w-100 btn btn-secondary btn-lg"
                    onclick="location.href='items.html'"
                    th:onclick="|location.href='@{/basic/items}'|"
                    type="button">목록으로</button>
        </div>
    </div>
</div> <!-- /container -->
</body>
</html>

 

 

  • 렌더링 하기 - 상품ID 
    <div>
        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control"
               value="1" th:value="${item.id}" readonly>
    </div>

 

  • 상품명
    • th:value="${item.itemName}"
  • 가격
    • th:value="${item.price}"
  • 수량
    • th:value="${item.quantity}"

 

 

[뷰화면]

 

 

 

 

버튼 링크 수정하기 

 

  • 상품수정 버튼 
  •  th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
  • basic/items/1/edit 같은 URL
<button class="w-100 btn btn-primary btn-lg"
        onclick="location.href='editForm.html'"
        th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
        type="button">상품 수정
</button>

 

 

  • 목록으로 버튼
  • th:onclick="|location.href='@{/basic/items}'|"
<button class="w-100 btn btn-secondary btn-lg"
        onclick="location.href='items.html'"
        th:onclick="|location.href='@{/basic/items}'|"
        type="button">목록으로
</button>
반응형
LIST

+ Recent posts