📃 요약
옥션, 롯데On 등의 쇼핑몰 기본 기능을 제작하는 예제입니다.
기본적인 DB CRUD 를 사용해 제작합니다.
DB 프레임워크는 JPA 를 사용해 자동화기능을 강화합니다.
MSA 를 고려하여 카프카를 사용해서 분산 트랜잭션에서 사용하는 결과적 일관성을 적용합니다.
뷰(Vue) & 스프링부트 연동 기초 예제
- axios CRUD 함수들과 스프링부트의 컨트롤러 함수들과 네트웍으로 연결됨
이때 컨트롤러는 @RestController 어노테이션을 사용해야함 - Vue : axios 라이브러리의 get(), post(), put(), delete() 함수 사용
- 스프링부트 – @RestController 어노테이션 사용해서 컨트롤러 클래스 생성
CRUD : @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
나머지 모델, 레포지토리, 서비스는 JSP 와 동일함
요소 기술 및 테이블 설계는 아래와 같습니다.
요소 기술 :
– 프론트엔드 : Vue
– 벡엔드 : 스프링부트 & JPA & Oracle 18xe(Oracle Cloud 19c)
– 기타 : 카프카(Kafka)
결과 화면 :
- 상품 전체 조회
- 장바구니 상세 페이지
- 장바구니 전체 페이지
- 주문 페이지
- 주문 상태 보기 및 결재 페이지(참고 : 관리자 페이지)
프로젝트 탐색기 : Vue
프로젝트 탐색기 : String Boot
Rest API :
- 상품 페이지
메소드 | URL | 설명 |
---|---|---|
GET | /simple-product | 상품 조회 |
GET | /simple-product/{spno} | 상품 상세조회 |
POST | /dept | 저장 |
PUT | /dept/{dno} | 수정 |
DELETE | /dept/deletion/{dno} | 삭제 |
- 장바구니 페이지
메소드 | URL | 설명 |
---|---|---|
GET | /simple-cart | 장바구니 & 상품(조인) 전체조회 |
GET | /simple-cart/{scno} | 장바구니 상세조회 |
POST | /simple-cart | 장바구니 저장 |
DELETE | /simple-cart/deletion/{scno} | 삭제 |
- 주문 페이지
메소드 | URL | 설명 |
---|---|---|
GET | /simple-order | 주문 전체조회 |
GET | /simple-order/{sono} | 장바구니 상세조회 |
POST | /simple-order | 주문 저장 |
PUT | /simple-order/{sono} | 주문 수정 |
DELETE | /simple-order/deletion/{sono} | 주문 삭제 |
- 결재 페이지
메소드 | URL | 설명 |
---|---|---|
GET | /simple-approval | 주문 전체조회 |
GET | /simple-approval/{sano} | 장바구니 상세조회 |
POST | /simple-approval | 주문 저장 |
DELETE | /simple-approval/deletion/{sano} | 주문 삭제 |
📃 기술 구현
스펙 :
- jdk 17 - spring boot 3.x - gradle
테이블 설계
DROP SEQUENCE SQ_SIMPLE_PRODUCT; CREATE SEQUENCE SQ_SIMPLE_PRODUCT START WITH 1 INCREMENT BY 1; DROP SEQUENCE SQ_SIMPLE_ORDER; CREATE SEQUENCE SQ_SIMPLE_ORDER START WITH 1 INCREMENT BY 1; DROP SEQUENCE SQ_SIMPLE_APPROVAL; CREATE SEQUENCE SQ_SIMPLE_APPROVAL START WITH 1 INCREMENT BY 1; DROP SEQUENCE SQ_SIMPLE_CART; CREATE SEQUENCE SQ_SIMPLE_CART START WITH 1 INCREMENT BY 1; -- 공통코드 테이블은 시퀀스는 사용하지 않음 -- 공통코드 테이블의 등록된 코드는 향후에 않쓰이더라도 삭제/수정하지 않음 : 데이터가 많지않아 오버헤드가 없음 DROP TABLE TB_CODE_CATEGORY CASCADE CONSTRAINT; DROP TABLE TB_CODE CASCADE CONSTRAINT; DROP TABLE TB_SIMPLE_PRODUCT CASCADE CONSTRAINT; DROP TABLE TB_SIMPLE_CART CASCADE CONSTRAINT; DROP TABLE TB_SIMPLE_ORDER CASCADE CONSTRAINT; DROP TABLE TB_SIMPLE_ORDER_DETAIL CASCADE CONSTRAINT; DROP TABLE TB_SIMPLE_APPROVAL CASCADE CONSTRAINT; -- 코드성 테이블 : 공통 코드 유형(대분류) 테이블 CREATE TABLE TB_CODE_CATEGORY ( CATEGORY_ID NUMBER NOT NULL PRIMARY KEY, CATEGORY_NAME VARCHAR2(255) ); -- 코드성 테이블 : 공통 코드 테이블 CREATE TABLE TB_CODE ( CODE_ID NUMBER NOT NULL PRIMARY KEY, CODE_NAME VARCHAR2(255), CATEGORY_ID NUMBER NOT NULL, USE_YN VARCHAR(1) DEFAULT 'Y', FOREIGN KEY (CATEGORY_ID) REFERENCES TB_CODE_CATEGORY (CATEGORY_ID) ); -- 마스터성 테이블 : 점이력 관리 않함 -- 1) 추가/수정만 가능 -- 2) 삭제 않하고 사용여부만 관리 -- 사용않하는 레코드는 향후에 배치잡으로 일괄 삭제 -- 심픔 상품 테이블 CREATE TABLE TB_SIMPLE_PRODUCT ( SPNO NUMBER NOT NULL PRIMARY KEY, -- 상품번호 CODE_ID NUMBER, -- 상품종류코드 TITLE VARCHAR2(255), -- 상품명 IMG_PATH VARCHAR2(255), -- 이미지 경로 UNIT_PRICE NUMBER, -- 단가 INVENTORY_COUNT NUMBER, -- 재고수량 USE_YN VARCHAR2(1) DEFAULT 'Y' -- 사용여부 ); -- 심플 장바구니 테이블 CREATE TABLE TB_SIMPLE_CART ( SCNO NUMBER NOT NULL PRIMARY KEY, -- 장바구니번호 SPNO NUMBER NOT NULL, -- 상품번호 CART_COUNT NUMBER DEFAULT 0, -- 장바구니 상품개수 DELETE_YN VARCHAR2(1) DEFAULT 'N', INSERT_TIME VARCHAR2(255), UPDATE_TIME VARCHAR2(255), DELETE_TIME VARCHAR2(255), FOREIGN KEY(SPNO) REFERENCES TB_SIMPLE_PRODUCT (SPNO) ); -- 주문 테이블 CREATE TABLE TB_SIMPLE_ORDER ( SONO NUMBER NOT NULL PRIMARY KEY, -- 주문번호 ORDER_DATE VARCHAR2(1000) NOT NULL, -- 주문일자 : YYYY-MM-DD HH24:MI:SS ORDER_STATUS NUMBER NOT NULL, -- 주문상태(50001: 주문완료, 50002: 결재완료, 50003: 상품준비중, 50004: 배송준비중, 50007:배송중, 50006:배송완료, 50007:주문확정 50011: 결재취소, 50012: 재고부족취소, 50013:고객취소) PRODUCT_AMOUNT NUMBER NOT NULL, -- 총 상품금액 DELIVERY_AMOUNT NUMBER, -- 배송비 ORDER_AMOUNT NUMBER, -- 주문금액 = 총 상품금액 + 배송비 DELIVERY_ADDR VARCHAR2(1000), -- 배송지 주소 DELIVERY_MSG VARCHAR2(400), -- 배송지 메모 DELETE_YN VARCHAR2(1) DEFAULT 'N', INSERT_TIME VARCHAR2(255), UPDATE_TIME VARCHAR2(255), DELETE_TIME VARCHAR2(255) ); -- 주문 상세 테이블 : 중간 테이블(상품(M) <-> 주문(M)) CREATE TABLE TB_SIMPLE_ORDER_DETAIL ( SONO NUMBER NOT NULL , -- 주문번호(PK), FK SPNO NUMBER NOT NULL , -- 상품번호(PK2), FK PRODUCT_COUNT NUMBER DEFAULT 0, -- 상품수량 PRIMARY KEY (SONO, SPNO), FOREIGN KEY(SONO) REFERENCES TB_SIMPLE_ORDER (SONO) ON DELETE CASCADE, FOREIGN KEY(SPNO) REFERENCES TB_SIMPLE_PRODUCT (SPNO) ); -- 결재 정보 테이블 CREATE TABLE TB_SIMPLE_APPROVAL ( SANO NUMBER NOT NULL PRIMARY KEY, -- 결재번호(PK), 시퀀스 SONO NUMBER NOT NULL , -- 주문번호(FK) APPROVAL_DATE VARCHAR2(1000) , -- 결재일자 : YYYY-MM-DD HH24:MI:SS APPROVAL_AMOUNT NUMBER NOT NULL, FOREIGN KEY(SONO) REFERENCES TB_SIMPLE_ORDER (SONO) );
쇼핑몰 핵심기능 구현을 위한 테이블 설계입니다.
데이터베이스를 오라클을 사용하여 구현해 보겠습니다.
환경설정 :
카프카 프로그램 설치 : 도커 컴포즈(docker-compose) 활용 ( 도커를 위한 오케스트레이션 툴)
- 주키퍼 / 카프카 설치
- 주키퍼 : 카프카 관리 프로그램
- 카프카 : 메세징 서비스 프로그램
kafka-compose.yml
version: "3" services: zookeeper: image: wurstmeister/zookeeper container_name: zookeeper expose: - "2181" kafka: image: wurstmeister/kafka ports: - "9092:9092" expose: - "9092" environment: KAFKA_LISTENERS: PLAINTEXT://:9092 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
- 위의 도커 컴포즈 파일이 있는 위치에서 명령프롬프트 (터미널) 를 열고 아래 명령어 실행하면 도커 데스크탑에 설치됨
docker-compose up
카프카 메세징 서버 명령어 참고
# 카프카 컨테이너를 접속 docker exec -it kafka /bin/bash # Topic 생성 - partitions, replication-factor ./bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 --topic test # Topic 확인 ./bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic test # Topic 목록 ./bin/kafka-topics.sh --list --bootstrap-server localhost:9092 # Topic 삭제 ./bin/kafka-topics.sh --delete --bootstrap-server localhost:9092 --topic test # 생산자 ./bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test # 소비자 ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test # 소비자 그룹 확인 ./bin/kafka-consumer-groups.sh --list --bootstrap-server localhost:9092
카프카 : application.properties
... # todo: kafka 설정 # kafka setting # consumer bootstrap servers가 따로 존재하면 설정 spring.kafka.bootstrap-servers=localhost:9092 # 식별 가능한 Consumer Group Id spring.kafka.consumer.group-id=academy # Kafka 서버에 초기 offset이 없거나, 서버에 현재 offset이 더 이상 존재하지 않을 경우 수행할 작업을 설정 # latest: 가장 최근에 생산된 메시지로 offset reset # earliest: 가장 오래된 메시지로 offset reset # none: offset 정보가 없으면 Exception 발생 spring.kafka.consumer.auto-offset-reset=earliest # 데이터를 받아올 때, key/value 를 역직렬화 # JSON 데이터를 받아올 것이라면 StringDeserializer spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer # producer bootstrap servers 가 따로 존재하면 설정 # 데이터를 보낼 때, key/value를 직렬화 # JSON 데이터를 보낼 것이라면 StringSerializer spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer ...
모델 : 엔티티
공통 모델
BaseTimeEntity.java
package com.example.simpledms.model.common; import lombok.Getter; import lombok.Setter; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; import javax.persistence.PrePersist; import javax.persistence.PreUpdate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * packageName : com.example.jpaexam.model * fileName : BaseTimeEntity * author : kangtaegyung * date : 2022/10/16 * description : JPA 에서 자동으로 생성일자/수정일자 만들어주는 클래스 * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2022/10/16 kangtaegyung 최초 생성 */ @Getter @Setter // @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 // 필드들(createdDate, modifiedDate)도 칼럼으로 인식하도록 한다. @MappedSuperclass // @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에 // Auditing 기능을(자동 생성일, 수정일) 포함시킨다. @EntityListeners(AuditingEntityListener.class) public abstract class BaseTimeEntity { private String insertTime; private String updateTime; private String deleteYn; // 소프트 삭제 private String deleteTime; // 소프트 삭제 @PrePersist //해당 엔티티 저장하기 전 void onPrePersist(){ this.insertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); this.deleteYn = "N"; } @PreUpdate //해당 엔티티 수정 하기 전 void onPreUpdate(){ this.updateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); this.insertTime = this.updateTime; this.deleteYn = "N"; } }
복합키(기본키 2개) 모델
SonoSpnoPk.java
package com.example.simpledms.model.common; import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.ToString; import java.io.Serializable; /** * packageName : com.example.simpledms.model.entity.common * fileName : SonoSpnoId * author : kangtaegyung * date : 11/20/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/20/23 kangtaegyung 최초 생성 */ @Data public class SonoSpnoPk implements Serializable { private Integer sono; private Integer spno; }
- 기본키 2개를 연결한 복합키입니다.
- 자바에서 복합키는 복합키 클래스를 만들고 엔티티에서 복합키 클래스의 각각의 속성(필드)에 @Id 를 붙이고 사용함
- @IdClass(SonoSpnoPk.class) : 엔티티 클래스 위에 붙임 : 아래 SimpleOrderDetail.java 클래스 참고
- 각각의 속성(필드)에 @Id 를 붙임 : 2개 사용
상품 : 엔티티
SimpleProduct.java
package com.example.simpledms.model.entity.shop; import com.fasterxml.jackson.annotation.JsonManagedReference; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import javax.persistence.*; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * packageName : com.example.simpledms.model.entity.shop * fileName : SimpleProduct * author : GGG * date : 2023-11-08 * description : 상품 정보 클래스 : 마스터성 데이터 * 요약 : * 1) 삭제 않함 : 삭제 기능 없음 * => useYn : 사용여부로 대체 * 2) 공통컬럼 없음 : (생성일자, 수정일자, 삭제일자) 사용 않함 * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-08 GGG 최초 생성 */ @Entity @Table(name = "TB_SIMPLE_PRODUCT") @SequenceGenerator( name = "SQ_SIMPLE_PRODUCT_GENERATOR" , sequenceName = "SQ_SIMPLE_PRODUCT" , initialValue = 1 , allocationSize = 1 ) @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @DynamicInsert @DynamicUpdate public class SimpleProduct { // 속성 @Id @GeneratedValue(strategy = GenerationType.SEQUENCE , generator = "SQ_SIMPLE_PRODUCT_GENERATOR" ) private Integer spno; // 기본키, 시퀀스 private Integer codeId; private String title; private String imgPath; private Integer unitPrice; private Integer inventoryCount; // 재고 수량 private String useYn; }
장바구니 : 엔티티
SimpleCart.java
package com.example.simpledms.model.entity.shop; import com.example.simpledms.model.common.BaseTimeEntity; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import javax.persistence.*; /** * packageName : com.example.simpledms.model.entity.shop * fileName : SimpleCart * author : GGG * date : 2023-11-09 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-09 GGG 최초 생성 */ @Entity @Table(name="TB_SIMPLE_CART") @SequenceGenerator( name = "SQ_SIMPLE_CART_GENERATOR" , sequenceName = "SQ_SIMPLE_CART" , initialValue = 1 , allocationSize = 1 ) @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @DynamicInsert @DynamicUpdate // soft delete @Where(clause = "DELETE_YN = 'N'") @SQLDelete(sql = "UPDATE TB_SIMPLE_CART SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE SCNO = ?") public class SimpleCart extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE , generator = "SQ_SIMPLE_CART_GENERATOR" ) private Integer scno; // 기본키, 시퀀스, 장바구니 번호 private Integer spno; // 상품번호(참조키) private Integer cartCount = 0; // 장바구니 개수 }
주문 : 엔티티
SimpleOrder.java
package com.example.simpledms.model.entity.shop; import com.example.simpledms.model.common.BaseTimeEntity; import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonManagedReference; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * packageName : com.example.simpledms.model.entity.shop * fileName : SimpleOrder * author : kangtaegyung * date : 11/20/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/20/23 kangtaegyung 최초 생성 */ @Entity @Table(name = "TB_SIMPLE_ORDER") @SequenceGenerator( name = "SQ_SIMPLE_ORDER_GENERATOR" , sequenceName = "SQ_SIMPLE_ORDER" , initialValue = 1 , allocationSize = 1 ) @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @DynamicInsert @DynamicUpdate public class SimpleOrder extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE , generator = "SQ_SIMPLE_ORDER_GENERATOR" ) private Integer sono; // 기본키, 시퀀스 private String orderDate; // -- 주문일자 : yyyy-mm-dd hh24:mi:ss private Integer orderStatus; // 주문상태(50001: 입금전, 50002: 결재완료, 50003: 상품준비중, 50004: 배송준비중, 50007:배송중, 50006:배송완료) private Integer productAmount; // 총 상품금액 private Integer deliveryAmount; // 배송비 private Integer orderAmount; // 주문금액 = 총 상품금액 + 배송비 private String deliveryAddr; // 배송지 주소 private String deliveryMsg; // 배송지 메모 }
주문상세 : 엔티티
SimpleOrderDetail.java
package com.example.simpledms.model.entity.shop; import com.example.simpledms.model.common.SonoSpnoPk; import com.fasterxml.jackson.annotation.JsonBackReference; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; /** * packageName : com.example.simpledms.model.entity.shop * fileName : SimpleOrderDetail * author : kangtaegyung * date : 11/20/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/20/23 kangtaegyung 최초 생성 */ @Entity @Table(name = "TB_SIMPLE_ORDER_DETAIL") @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @DynamicInsert @DynamicUpdate @IdClass(SonoSpnoPk.class) public class SimpleOrderDetail { @Id private Integer sono; // 복합키 # 1, FK @Id private Integer spno; // 복합키 # 2, FK private Integer productCount; // 수량 }
결재 : 엔티티
SimpleApproval.java
package com.example.simpledms.model.entity.shop; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; /** * packageName : com.example.simpledms.model.entity.shop * fileName : SimpleApproval * author : kangtaegyung * date : 11/22/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/22/23 kangtaegyung 최초 생성 */ @Entity @Table(name = "TB_SIMPLE_APPROVAL") @SequenceGenerator( name = "SQ_SIMPLE_APPROVAL_GENERATOR" , sequenceName = "SQ_SIMPLE_APPROVAL" , initialValue = 1 , allocationSize = 1 ) @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @DynamicInsert @DynamicUpdate public class SimpleApproval { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE , generator = "SQ_SIMPLE_APPROVAL_GENERATOR" ) private Integer sano; // 기본키, 시퀀스 private Integer sono; private String approvalDate; private Integer approvalAmount; }
상품 레포지토리
package com.example.simpledms.repository.shop; import com.example.simpledms.model.entity.shop.SimpleProduct; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * packageName : com.example.simpledms.repository.shop * fileName : SimpleProductRepository * author : GGG * date : 2023-11-08 * description : JPA CRUD 인터페이스 * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-08 GGG 최초 생성 */ @Repository public interface SimpleProductRepository extends JpaRepository<SimpleProduct, Integer> { // title like 검색 : 쿼리메소드(JPQL) Page<SimpleProduct> findAllByTitleContaining(String title, Pageable pageable); }
select : @Query 이용한 오라클 기반 쿼리(nativeQuery = true) 임
장바구니 레포지토리
package com.example.simpledms.repository.shop; import com.example.simpledms.model.dto.shop.ISimpleCartDto; import com.example.simpledms.model.entity.shop.SimpleCart; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; /** * packageName : com.example.simpledms.repository.shop * fileName : SimpleCartRepository * author : GGG * date : 2023-11-09 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-09 GGG 최초 생성 */ @Repository public interface SimpleCartRepository extends JpaRepository<SimpleCart, Integer> { // like 검색 : 상품테이블(TB_SIMPLE_PRODUCT) 조인 @Query(value = "SELECT SC.SCNO AS scno " + " , SP.SPNO AS spno" + " , SP.CODE_ID AS codeId " + " , SP.TITLE as title " + " , SP.IMG_PATH AS imgPath " + " , SP.UNIT_PRICE AS unitPrice " + " , SC.CART_COUNT AS cartCount " + "FROM TB_SIMPLE_CART SC " + " ,TB_SIMPLE_PRODUCT SP " + "WHERE SC.SPNO = SP.SPNO " + "AND SP.TITLE LIKE '%' || :title || '%' " + "AND SC.DELETE_YN = 'N'" , countQuery = "SELECT count(*) " + "FROM TB_SIMPLE_CART SC " + " ,TB_SIMPLE_PRODUCT SP " + "WHERE SC.SPNO = SP.SPNO " + "AND SP.TITLE LIKE '%' || :title || '%' " + "AND SC.DELETE_YN = 'N'" , nativeQuery = true) Page<ISimpleCartDto> selectByTitleContaining( @Param("title") String title, Pageable pageable ); // @Query(value = "select new com.example.simpledms.model.dto.shop.SimpleCartDto(sc.scno, sp.spno, sp.codeId, sp.title, sp.imgPath, sp.unitPrice, sc.cartCount) " + // "from SimpleCart sc join SimpleProduct sp on (sc.spno = sp.spno) " + // "where sp.title like '%' || :title || '%' ") // Page<SimpleCartDto> selectByTitleContaining( // @Param("title") String title, // Pageable pageable // ); // 상품 + 장바구니(조인) 상세조회 : 페이징없음(객체) @Query(value = "SELECT SC.SCNO AS scno " + " , SP.SPNO AS spno" + " , SP.CODE_ID AS codeId " + " , SP.TITLE as title " + " , SP.IMG_PATH AS imgPath " + " , SP.UNIT_PRICE AS unitPrice " + " , SC.CART_COUNT AS cartCount " + "FROM TB_SIMPLE_CART SC " + " ,TB_SIMPLE_PRODUCT SP " + "WHERE SC.SPNO = SP.SPNO " + "AND SC.SCNO = :scno " + "AND SC.DELETE_YN = 'N'", nativeQuery = true) Optional<ISimpleCartDto> selectById(@Param("scno") int scno); // @Query(value = "select new com.example.simpledms.model.dto.shop.SimpleCartDto(sc.scno, sp.spno, sp.codeId, sp.title, sp.imgPath, sp.unitPrice, sc.cartCount) " + // "from SimpleCart sc join SimpleProduct sp on (sc.spno = sp.spno) " + // "where sc.scno = :scno ") // Optional<SimpleCartDto> selectById(@Param("scno") int scno); }
주문 레포지토리
package com.example.simpledms.repository.shop; import com.example.simpledms.model.dto.shop.ISimpleOrderDto; import com.example.simpledms.model.dto.shop.SimpleOrderDto; import com.example.simpledms.model.entity.shop.SimpleOrder; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; /** * packageName : com.example.simpledms.repository.shop * fileName : SimpleOrderRepository * author : kangtaegyung * date : 11/20/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/20/23 kangtaegyung 최초 생성 */ @Repository public interface SimpleOrderRepository extends JpaRepository<SimpleOrder, Integer> { // 상품 기본 주문 상세 @Query(value = "SELECT SO.SONO AS sono " + " ,SO.ORDER_DATE AS orderDate " + " ,SO.ORDER_STATUS AS orderStatus " + " ,SP.TITLE AS title " + " ,SP.IMG_PATH AS imgPath " + " ,SP.UNIT_PRICE AS unitPrice " + " ,SD.PRODUCT_COUNT AS productCount " + " ,SO.PRODUCT_AMOUNT AS productAmount " + " ,SO.DELIVERY_AMOUNT AS deliveryAmount " + " ,SO.ORDER_AMOUNT AS orderAmount " + " ,SO.DELIVERY_ADDR AS deliveryAddr " + " ,SO.DELIVERY_MSG AS deliveryMsg " + "FROM TB_SIMPLE_PRODUCT SP " + " ,TB_SIMPLE_ORDER SO " + " ,TB_SIMPLE_ORDER_DETAIL SD " + "WHERE SP.SPNO = SD.SPNO " + "AND SD.SONO = SO.SONO" , countQuery = "SELECT COUNT(*) " + "FROM TB_SIMPLE_PRODUCT SP " + " ,TB_SIMPLE_ORDER SO " + " ,TB_SIMPLE_ORDER_DETAIL SD " + "WHERE SP.SPNO = SD.SPNO " + "AND SD.SONO = SO.SONO" , nativeQuery = true) Page<ISimpleOrderDto> selectByOrderDateContaining( @Param("orderDate") String orderDate, Pageable pageable ); }
주문상세 레포지토리
package com.example.simpledms.model.entity.shop; import com.example.simpledms.model.common.SonoSpnoPk; import com.fasterxml.jackson.annotation.JsonBackReference; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; /** * packageName : com.example.simpledms.model.entity.shop * fileName : SimpleOrderDetail * author : kangtaegyung * date : 11/20/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/20/23 kangtaegyung 최초 생성 */ @Entity @Table(name = "TB_SIMPLE_ORDER_DETAIL") @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @DynamicInsert @DynamicUpdate @IdClass(SonoSpnoPk.class) public class SimpleOrderDetail { @Id private Integer sono; // 복합키 # 1, FK @Id private Integer spno; // 복합키 # 2, FK private Integer productCount; // 수량 }
결재 레포지토리
package com.example.simpledms.model.entity.shop; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; /** * packageName : com.example.simpledms.model.entity.shop * fileName : SimpleApproval * author : kangtaegyung * date : 11/22/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/22/23 kangtaegyung 최초 생성 */ @Entity @Table(name = "TB_SIMPLE_APPROVAL") @SequenceGenerator( name = "SQ_SIMPLE_APPROVAL_GENERATOR" , sequenceName = "SQ_SIMPLE_APPROVAL" , initialValue = 1 , allocationSize = 1 ) @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @DynamicInsert @DynamicUpdate public class SimpleApproval { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE , generator = "SQ_SIMPLE_APPROVAL_GENERATOR" ) private Integer sano; // 기본키, 시퀀스 private Integer sono; private String approvalDate; private Integer approvalAmount; }
상품 게시판 서비스
package com.example.simpledms.service.shop; import com.example.simpledms.model.entity.shop.SimpleProduct; import com.example.simpledms.repository.shop.SimpleProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Optional; /** * packageName : com.example.simpledms.service.shop * fileName : SimpleProductService * author : GGG * date : 2023-11-08 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-08 GGG 최초 생성 */ @Service public class SimpleProductService { @Autowired SimpleProductRepository simpleProductRepository; // DI // title like 검색 public Page<SimpleProduct> findAllByTitleContaining(String title, Pageable pageable) { Page<SimpleProduct> page = simpleProductRepository.findAllByTitleContaining(title, pageable); return page; } // 상세조회(1건조회) public Optional<SimpleProduct> findById(int spno) { Optional<SimpleProduct> optionalSimpleProduct = simpleProductRepository.findById(spno); return optionalSimpleProduct; } }
장바구니 게시판 서비스
package com.example.simpledms.service.shop; import com.example.simpledms.model.dto.shop.ISimpleCartDto; import com.example.simpledms.model.entity.shop.SimpleCart; import com.example.simpledms.repository.shop.SimpleCartRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Optional; /** * packageName : com.example.simpledms.service.shop * fileName : SimpleCartService * author : GGG * date : 2023-11-09 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-09 GGG 최초 생성 */ @Service public class SimpleCartService { @Autowired SimpleCartRepository simpleCartRepository; // DI // 전체 조회(like) + 페이징(조인) + DTO public Page<ISimpleCartDto> selectByTitleContaining( String title, Pageable pageable) { Page<ISimpleCartDto> page = simpleCartRepository .selectByTitleContaining(title, pageable); return page; } // 저장함수(수정함수) public SimpleCart save(SimpleCart simpleCart) { SimpleCart simpleCart2 = simpleCartRepository.save(simpleCart); return simpleCart2; } // 조인 상세조회(1건조회) public Optional<ISimpleCartDto> selectById(int scno) { Optional<ISimpleCartDto> optionalSimpleCart = simpleCartRepository.selectById(scno); return optionalSimpleCart; } // 삭제함수 public boolean removeById(int scno) { if(simpleCartRepository.existsById(scno)) { // scno 있는지 확인 simpleCartRepository.deleteById(scno); // 삭제 진행 return true; } return false; } }
주문 게시판 서비스
package com.example.simpledms.service.shop; import com.example.simpledms.model.common.KafkaMsg; import com.example.simpledms.model.common.SonoSpnoPk; import com.example.simpledms.model.dto.shop.ISimpleOrderDto; import com.example.simpledms.model.dto.shop.SimpleOrderDto; import com.example.simpledms.model.entity.shop.SimpleOrder; import com.example.simpledms.model.entity.shop.SimpleOrderDetail; import com.example.simpledms.repository.shop.SimpleOrderDetailRepository; import com.example.simpledms.repository.shop.SimpleOrderRepository; import com.example.simpledms.service.shop.kafka.KafkaOrderProducer; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.util.List; import java.util.Optional; /** * packageName : com.example.simpledms.service.shop * fileName : SimpleOrderService * author : kangtaegyung * date : 11/20/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/20/23 kangtaegyung 최초 생성 */ @Service public class SimpleOrderService { @Autowired SimpleOrderRepository simpleOrderRepository; // DI @Autowired SimpleOrderDetailRepository simpleOrderDetailRepository; // DI ModelMapper modelMapper = new ModelMapper(); @Autowired private KafkaOrderProducer producer; // kafka service DI // 전체 조회(like) + 페이징(조인) + DTO public Page<ISimpleOrderDto> selectByOrderDateContaining( String orderDate, Pageable pageable) { Page<ISimpleOrderDto> page = simpleOrderRepository.selectByOrderDateContaining(orderDate, pageable); return page; } // 저장함수가 여러개일 경우 트랜잭션 처리를 위해 @Transactional 붙일것 @Transactional // 저장함수 public SimpleOrder insert(SimpleOrderDto simpleOrderDto) { // 1) DTO -> Model 변환 SimpleOrder simpleOrder = modelMapper.map(simpleOrderDto, SimpleOrder.class); SimpleOrder simpleOrder2 = simpleOrderRepository.save(simpleOrder); // 부모 테이블 저장 // 자식 테이블 저장 for (int i = 0; i < simpleOrderDto.getSimpleOrderDetailList().size(); i++) { simpleOrderDto.getSimpleOrderDetailList().get(i).setSono(simpleOrder2.getSono()); // 기본키는 부모쪽 값을 넣기 simpleOrderDetailRepository.save(simpleOrderDto.getSimpleOrderDetailList().get(i)); } return simpleOrder2; } // 수정함수 public void update(SimpleOrderDto simpleOrderDto) { // SimpleOrderDto -> SimpleOrder ( 속성 있는것만 변환 ) SimpleOrder simpleOrder = modelMapper.map(simpleOrderDto, SimpleOrder.class); // 1) 수정 SimpleOrder simpleOrder2 = simpleOrderRepository.save(simpleOrder); //// 2) 카프카에 메세지 전송 : KafkaMsg.ORDER_COMPLETE.getValue() - 주문완료(50001) // for (int i = 0; i < simpleOrderDto.getSimpleOrderDetailList().size(); i++) { //// 주문상품 품목별 카프카 메세지 발행 // producer.sendMessage(simpleOrder2.getSono() + ":" // + simpleOrderDto.getSimpleOrderDetailList().get(i).getSpno() + ":" // + simpleOrderDto.getSimpleOrderDetailList().get(i).getProductCount() + ":" // + "50001"); // } // producer.sendMessage( simpleOrder2.getSono() + ":" + "50001"); // 주문상태 반영 메세지 } // 수정함수 2 public void update(SimpleOrder simpleOrder) { // 1) 수정 simpleOrderRepository.save(simpleOrder); } // 삭제함수 public boolean removeById(int sono) { if (simpleOrderRepository.existsById(sono)) { // sono 있는지 확인 simpleOrderRepository.deleteById(sono); // 삭제 진행 return true; } return false; } // sono 로 DTO 조회 public SimpleOrderDto findBySimpleOrderDto(int sono) { Optional<SimpleOrder> optionalSimpleOrder = simpleOrderRepository.findById(sono); List<SimpleOrderDetail> simpleOrderDetailList = simpleOrderDetailRepository.findAllBySono(sono); SimpleOrderDto simpleOrderDto = modelMapper.map(optionalSimpleOrder.get(), SimpleOrderDto.class); simpleOrderDto.setSimpleOrderDetailList(simpleOrderDetailList); return simpleOrderDto; } // 상세조회 public Optional<SimpleOrder> findById(int sono) { Optional<SimpleOrder> optionalSimpleOrder = simpleOrderRepository.findById(sono); return optionalSimpleOrder; } }
결재 게시판 서비스
package com.example.simpledms.service.shop; import com.example.simpledms.model.common.KafkaMsg; import com.example.simpledms.model.dto.shop.SimpleApprovalDto; import com.example.simpledms.model.entity.shop.SimpleApproval; import com.example.simpledms.model.entity.shop.SimpleOrder; import com.example.simpledms.repository.shop.SimpleApprovalRepository; import com.example.simpledms.service.shop.kafka.KafkaApprovalProducer; import com.example.simpledms.service.shop.kafka.KafkaOrderProducer; import org.apache.kafka.clients.producer.KafkaProducer; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Optional; /** * packageName : com.example.simpledms.service.shop * fileName : SimpleApprovalService * author : kangtaegyung * date : 11/22/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/22/23 kangtaegyung 최초 생성 */ @Service public class SimpleApprovalService { @Autowired private KafkaApprovalProducer producer; // kafka service DI ModelMapper modelMapper = new ModelMapper(); @Autowired SimpleApprovalRepository simpleApprovalRepository; // DI // 전체 조회(like) + 페이징(조인) + DTO public Page<SimpleApproval> findAllByApprovalDateContaining( String approvalDate, Pageable pageable) { Page<SimpleApproval> page = simpleApprovalRepository.findAllByApprovalDateContaining(approvalDate, pageable); return page; } // 저장함수 public SimpleApproval save(SimpleApprovalDto simpleApprovalDto) { SimpleApproval simpleApproval = modelMapper.map(simpleApprovalDto, SimpleApproval.class); // 1) 저장 SimpleApproval simpleApproval2 = simpleApprovalRepository.save(simpleApproval); //// 2) 카프카에 메세지 전송 : KafkaMsg.APPROVAL_COMPLETE.getValue() - 결재완료(50002) // for (int i = 0; i < simpleApprovalDto.getSimpleOrderDetailList().size(); i++) { //// 주문상품 품목별 카프카 메세지 발행 // producer.sendMessage(simpleApproval2.getSono() + ":" // + simpleApprovalDto.getSimpleOrderDetailList().get(i).getSpno() + ":" // + simpleApprovalDto.getSimpleOrderDetailList().get(i).getProductCount() + ":" // + "50002"); // } producer.sendMessage( simpleApproval2.getSono() + ":" + "50002"); // 주문상태 반영 메세지 return simpleApproval2; } // 삭제함수 public boolean removeById(int sano) { if (simpleApprovalRepository.existsById(sano)) { // sono 있는지 확인 simpleApprovalRepository.deleteById(sano); // 삭제 진행 return true; } return false; } // 상세조회 public Optional<SimpleApproval> findById(int sano) { Optional<SimpleApproval> optionalSimpleApproval = simpleApprovalRepository.findById(sano); return optionalSimpleApproval; } // 재고부족 취소 메세지 송신 public void cancelApproval(SimpleOrder simpleOrder) { // 재고부족취소(50012) 수정 } }
카프카 소비자 서비스
package com.example.simpledms.service.shop.kafka; import com.example.simpledms.model.entity.shop.SimpleOrder; import com.example.simpledms.service.shop.SimpleApprovalService; import com.example.simpledms.service.shop.SimpleOrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.Optional; /** * packageName : com.example.backedu.service * fileName : KafkaConsumer * author : kangtaegyung * date : 2023/10/01 * description : 메세지 받는 객체 - 데몬 역할 * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2023/10/01 kangtaegyung 최초 생성 */ @Service @Slf4j public class KafkaConsumer { @Autowired SimpleApprovalService simpleApprovalService; // DI @Autowired SimpleOrderService simpleOrderService; // DI // 주문 서비스는 모든 상태값을 전달 받아 저장함 : 모든 토픽 구독 @KafkaListener(topics = {"approval", "order"}, groupId = "academy") public void consume(String message) throws IOException { log.debug("KafkaOrderConsumer message : {}", message); // 수신메세지 로그 if (message.contains(":")) { String[] token = message.split(":"); int sono = Integer.parseInt(token[0]); int orderStatus = Integer.parseInt(token[1]); log.debug("{} {}", sono, orderStatus); if (orderStatus == 50011) { // TODO: 결재 취소 -> 보상 트랜잭션 발생 : } else if(orderStatus == 50001 || orderStatus == 50002){ // 99999 이면 주문 진행 상태 DB 수정해 달라는 메세지 // => orderStatus : 주문상태코드 있음 Optional<SimpleOrder> optionalSimpleOrder = simpleOrderService.findById(sono); SimpleOrder simpleOrder = optionalSimpleOrder.get(); simpleOrder.setOrderStatus(orderStatus); // 주문 상태 저장 simpleOrderService.update(simpleOrder); // 수신 주문상태 DB 저장 } } } }
- 카프카 리스너 :
- 카프카는 소비자(구독) – 생산자(주제 생성) 사이에서 이루어지는 메세징 프로그램임
- 토픽 – 카프카 메세지를 사용할때 주제 지정
- 소비자그룹 ID – 소비자들의 그룹 ID 지정
- 지정된 토픽과 소비자그룹에 대해 카프카 메세징 서비스가 새로운 소식이 들어올때 까지 대기함
- 사용법 : @KafkaListener(topics = {“토픽1”, “토픽2”}, groupId = “소비자그룹ID명”) :
카프카 주문 생산자 서비스
package com.example.simpledms.service.shop.kafka; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; /** * packageName : com.example.backedu.service * fileName : KafkaProducer * author : kangtaegyung * date : 2023/10/01 * description : 메세지 주는 객체 * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2023/10/01 kangtaegyung 최초 생성 */ @Service @Slf4j public class KafkaOrderProducer { private static final String TOPIC = "order"; @Autowired private KafkaTemplate<String, String> kafkaTemplate; public void sendMessage(String message) { log.debug("KafkaOrderProducer message : {}", message); this.kafkaTemplate.send(TOPIC, message); } }
- 생산자에서 카프카 sendMessage() 함수를 통해 새로운 소식을 생산해서 카프카 소비자에게 전송함
카프카 결재 생산자 서비스
package com.example.simpledms.service.shop.kafka; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; /** * packageName : com.example.backedu.service * fileName : KafkaProducer * author : kangtaegyung * date : 2023/10/01 * description : 메세지 주는 객체 * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2023/10/01 kangtaegyung 최초 생성 */ @Service @Slf4j public class KafkaApprovalProducer { private static final String TOPIC = "approval"; @Autowired private KafkaTemplate<String, String> kafkaTemplate; public void sendMessage(String message) { log.debug("KafkaApprovalProducer message : {}", message); this.kafkaTemplate.send(TOPIC, message); } }
- 생산자에서 카프카 sendMessage() 함수를 통해 새로운 소식을 생산해서 카프카 소비자에게 전송함
상품 컨트롤러 : Rest Controller 사용 ( Vue, React, Angular.js 등 )
package com.example.simpledms.controller.shop; import com.example.simpledms.model.entity.shop.SimpleProduct; import com.example.simpledms.service.shop.SimpleProductService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * packageName : com.example.simpledms.controller.shop * fileName : SimpleProductController * author : GGG * date : 2023-11-08 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-08 GGG 최초 생성 */ @Slf4j @RestController @RequestMapping("/api/shop") public class SimpleProductController { @Autowired SimpleProductService simpleProductService; // DI // like 검색 @GetMapping("/simple-product") public ResponseEntity<Object> findAllByTitleContaining( @RequestParam(defaultValue = "") String title, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "3") int size ) { try { Pageable pageable = PageRequest.of(page, size); Page<SimpleProduct> simpleProductPage = simpleProductService .findAllByTitleContaining(title, pageable); Map<String, Object> response = new HashMap<>(); response.put("simpleProduct", simpleProductPage.getContent()); // simpleProduct 배열 response.put("currentPage", simpleProductPage.getNumber()); // 현재페이지번호 response.put("totalItems", simpleProductPage.getTotalElements()); // 총건수(개수) response.put("totalPages", simpleProductPage.getTotalPages()); // 총페이지수 if (simpleProductPage.isEmpty() == false) { // 성공 return new ResponseEntity<>(response, HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } catch (Exception e) { log.debug(e.getMessage()); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 상세조회 @GetMapping("/simple-product/{spno}") public ResponseEntity<Object> findById(@PathVariable int spno) { try { // 상세조회 실행 Optional<SimpleProduct> optionalSimpleProduct = simpleProductService.findById(spno); if (optionalSimpleProduct.isPresent()) { // 성공 return new ResponseEntity<>(optionalSimpleProduct.get(), HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } catch (Exception e) { // 서버 에러 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } }
장바구니 컨트롤러 : Rest Controller 사용 ( Vue, React, Angular.js 등 )
package com.example.simpledms.controller.shop; import com.example.simpledms.model.dto.shop.ISimpleCartDto; import com.example.simpledms.model.entity.shop.SimpleCart; import com.example.simpledms.service.shop.SimpleCartService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * packageName : com.example.simpledms.controller.shop * fileName : SimpleCartController * author : GGG * date : 2023-11-09 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ————————————————————————————— * 2023-11-09 GGG 최초 생성 */ @Slf4j @RestController @RequestMapping("/api/shop") public class SimpleCartController { @Autowired SimpleCartService simpleCartService; // DI // 상세조회 @GetMapping("/simple-cart/{scno}") public ResponseEntity<Object> selectById(@PathVariable int scno) { try { // 상세조회 실행 Optional<ISimpleCartDto> optionalSimpleCartDto = simpleCartService.selectById(scno); if (optionalSimpleCartDto.isPresent()) { // 성공 return new ResponseEntity<>(optionalSimpleCartDto.get(), HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } catch (Exception e) { // 서버 에러 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 저장 함수 @PostMapping("/simple-cart") public ResponseEntity<Object> create(@RequestBody SimpleCart simpleCart) { try { SimpleCart simpleCart2 = simpleCartService.save(simpleCart); // db 저장 return new ResponseEntity<>(simpleCart2, HttpStatus.OK); } catch (Exception e) { // DB 에러가 났을경우 : INTERNAL_SERVER_ERROR 프론트엔드로 전송 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 전체 조회 + like 검색 (조인) @GetMapping("/simple-cart") public ResponseEntity<Object> selectByTitleContaining( @RequestParam(defaultValue = "") String title, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "3") int size ){ try { Pageable pageable = PageRequest.of(page, size); Page<ISimpleCartDto> simpleCartDtoPage = simpleCartService .selectByTitleContaining(title, pageable); Map<String , Object> response = new HashMap<>(); response.put("simpleCart", simpleCartDtoPage.getContent()); // simpleCart 배열 response.put("currentPage", simpleCartDtoPage.getNumber()); // 현재페이지번호 response.put("totalItems", simpleCartDtoPage.getTotalElements()); // 총건수(개수) response.put("totalPages", simpleCartDtoPage.getTotalPages()); // 총페이지수 if (simpleCartDtoPage.isEmpty() == false) { // 성공 return new ResponseEntity<>(response, HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } }catch (Exception e) { log.debug(e.getMessage()); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 삭제함수 @DeleteMapping("/simple-cart/deletion/{scno}") public ResponseEntity<Object> delete(@PathVariable int scno) { try { boolean bSuccess = simpleCartService.removeById(scno); if (bSuccess == true) { // delete 문이 성공했을 경우 return new ResponseEntity<>(HttpStatus.OK); } // delete 실패했을 경우( 0건 삭제가 될경우 ) return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { // DB 에러가 날경우 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } }
주문 컨트롤러 : Rest Controller 사용 ( Vue, React, Angular.js 등 )
package com.example.simpledms.controller.shop; import com.example.simpledms.model.dto.shop.ISimpleOrderDto; import com.example.simpledms.model.dto.shop.SimpleOrderDto; import com.example.simpledms.model.entity.shop.SimpleOrder; import com.example.simpledms.service.shop.SimpleOrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * packageName : com.example.simpledms.controller.shop * fileName : SimpleOrderController * author : kangtaegyung * date : 11/20/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/20/23 kangtaegyung 최초 생성 */ @Slf4j @RestController @RequestMapping("/api/shop") public class SimpleOrderController { @Autowired SimpleOrderService simpleOrderService; // DI // 저장 함수 @PostMapping("/simple-order") public ResponseEntity<Object> create(@RequestBody SimpleOrderDto simpleOrderDto) { try { SimpleOrder simpleOrder2 = simpleOrderService.insert(simpleOrderDto); // db 저장 return new ResponseEntity<>(simpleOrder2, HttpStatus.OK); } catch (Exception e) { // DB 에러가 났을경우 : INTERNAL_SERVER_ERROR 프론트엔드로 전송 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 수정 함수 @PutMapping("/simple-order/{sono}") public ResponseEntity<Object> update( @PathVariable int sono, @RequestBody SimpleOrderDto simpleOrderDto) { try { simpleOrderService.update(simpleOrderDto); // // 1) 수정 2) 카프카 주문완료 메세지 전송 : return new ResponseEntity<>(HttpStatus.OK); } catch (Exception e) { // DB 에러가 났을경우 : INTERNAL_SERVER_ERROR 프론트엔드로 전송 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 전체 조회 + like 검색 (조인) @GetMapping("/simple-order") public ResponseEntity<Object> selectByOrderDateContaining( @RequestParam(defaultValue = "") String orderDate, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "3") int size ) { try { Pageable pageable = PageRequest.of(page, size); Page<ISimpleOrderDto> simpleOrderDtoPage = simpleOrderService .selectByOrderDateContaining(orderDate, pageable); Map<String, Object> response = new HashMap<>(); response.put("simpleOrderDto", simpleOrderDtoPage.getContent()); // simpleOrderDto 배열 response.put("currentPage", simpleOrderDtoPage.getNumber()); // 현재페이지번호 response.put("totalItems", simpleOrderDtoPage.getTotalElements()); // 총건수(개수) response.put("totalPages", simpleOrderDtoPage.getTotalPages()); // 총페이지수 if (simpleOrderDtoPage.isEmpty() == false) { // 성공 return new ResponseEntity<>(response, HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } catch (Exception e) { log.debug(e.getMessage()); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 삭제함수 @DeleteMapping("/simple-order/deletion/{sono}") public ResponseEntity<Object> delete(@PathVariable int sono) { try { boolean bSuccess = simpleOrderService.removeById(sono); if (bSuccess == true) { // delete 문이 성공했을 경우 return new ResponseEntity<>(HttpStatus.OK); } // delete 실패했을 경우( 0건 삭제가 될경우 ) return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { // DB 에러가 날경우 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 상세조회 : DTO 전송(부모 + 자식) @GetMapping("/simple-order/{sono}") public ResponseEntity<Object> findById(@PathVariable int sono) { try { // 상세조회 실행 SimpleOrderDto simpleOrderDto = simpleOrderService.findBySimpleOrderDto(sono); if (simpleOrderDto != null) { // 성공 return new ResponseEntity<>(simpleOrderDto, HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } catch (Exception e) { // 서버 에러 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } }
결재 컨트롤러 : Rest Controller 사용 ( Vue, React, Angular.js 등 )
package com.example.simpledms.controller.shop; import com.example.simpledms.model.dto.shop.SimpleApprovalDto; import com.example.simpledms.model.entity.shop.SimpleApproval; import com.example.simpledms.service.shop.SimpleApprovalService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * packageName : com.example.simpledms.controller.shop * fileName : SimpleApprovalController * author : kangtaegyung * date : 11/22/23 * description : * 요약 : * <p> * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 11/22/23 kangtaegyung 최초 생성 */ @Slf4j @RestController @RequestMapping("/api/shop") public class SimpleApprovalController { @Autowired SimpleApprovalService simpleApprovalService; // 저장 함수 @PostMapping("/simple-approval") public ResponseEntity<Object> create(@RequestBody SimpleApprovalDto simpleApprovalDto) { try { SimpleApproval simpleApproval = simpleApprovalService.save(simpleApprovalDto); // db 저장 return new ResponseEntity<>(simpleApproval, HttpStatus.OK); } catch (Exception e) { // DB 에러가 났을경우 : INTERNAL_SERVER_ERROR 프론트엔드로 전송 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 전체 조회 + like 검색 (조인) @GetMapping("/simple-approval") public ResponseEntity<Object> findAllByApprovalDateContaining( @RequestParam(defaultValue = "") String approvalDate, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "3") int size ) { try { Pageable pageable = PageRequest.of(page, size); Page<SimpleApproval> simpleApprovalDtoPage = simpleApprovalService.findAllByApprovalDateContaining(approvalDate, pageable); Map<String, Object> response = new HashMap<>(); response.put("simpleApproval", simpleApprovalDtoPage.getContent()); // simpleApproval 배열 response.put("currentPage", simpleApprovalDtoPage.getNumber()); // 현재페이지번호 response.put("totalItems", simpleApprovalDtoPage.getTotalElements()); // 총건수(개수) response.put("totalPages", simpleApprovalDtoPage.getTotalPages()); // 총페이지수 if (simpleApprovalDtoPage.isEmpty() == false) { // 성공 return new ResponseEntity<>(response, HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } catch (Exception e) { log.debug(e.getMessage()); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 삭제함수 @DeleteMapping("/simple-approval/deletion/{sano}") public ResponseEntity<Object> delete(@PathVariable int sano) { try { boolean bSuccess = simpleApprovalService.removeById(sano); if (bSuccess == true) { // delete 문이 성공했을 경우 return new ResponseEntity<>(HttpStatus.OK); } // delete 실패했을 경우( 0건 삭제가 될경우 ) return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { // DB 에러가 날경우 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } // 상세조회 @GetMapping("/simple-approval/{sano}") public ResponseEntity<Object> findById(@PathVariable int sano) { try { // 상세조회 실행 Optional<SimpleApproval> optionalSimpleApproval = simpleApprovalService.findById(sano); if (optionalSimpleApproval.isPresent()) { // 성공 return new ResponseEntity<>(optionalSimpleApproval.get(), HttpStatus.OK); } else { // 데이터 없음 return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } catch (Exception e) { // 서버 에러 return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } }
Vue 페이지
1) 공통 js
– axios import 및 기본설정파일
- utils/http-common.js
// 1)
import axios from "axios";
export default axios.create({
baseURL: “http://localhost:8000/api",
headers: {
“Content-Type”: “application/json”
}
});
#### - axios 공통 함수 : get/post/put/delete 방식 함수들 - SimpleProductService.js ```js import http from '@/utils/http-common'; // springboot 로 연결할 axios 함수들 // 전송 경로 : springboot url을 코딩 class SimpleProductService { getAll(title, page, size) { return http.get(`/shop/simple-product?title${title}&page${page}&size${size}`); } get(spno) { return http.get(`/shop/simple-product/${spno}`); } create(data) { return http.post("/shop/simple-product", data); } update(spno, data) { return http.put(`/shop/simple-product/${spno}`, data); } remove(spno) { return http.delete(`/shop/simple-product/deletion/${spno}`); } } export default new SimpleProductService();
- SimpleCartService.js
import http from '@/utils/http-common'; // springboot 로 연결할 axios 함수들 // 전송 경로 : springboot url을 코딩 class SimpleCartService { getAll(title, page, size) { return http.get(`/shop/simple-cart?title${title}&page${page}&size${size}`); } get(scno) { return http.get(`/shop/simple-cart/${scno}`); } create(data) { return http.post("/shop/simple-cart", data); } update(scno, data) { return http.put(`/shop/simple-cart/${scno}`, data); } remove(scno) { return http.delete(`/shop/simple-cart/deletion/${scno}`); } } export default new SimpleCartService();
- SimpleOrderService.js
import http from '@/utils/http-common'; // springboot 로 연결할 axios 함수들 // 전송 경로 : springboot url을 코딩 class SimpleOrderService { getAll(orderDate, page, size) { return http.get(`/shop/simple-order?orderDate${orderDate}&page${page}&size${size}`); } get(sono) { return http.get(`/shop/simple-order/${sono}`); } create(data) { console.log("create", data); return http.post("/shop/simple-order", data); } update(sono, data) { return http.put(`/shop/simple-order/${sono}`, data); } remove(sono) { return http.delete(`/shop/simple-order/deletion/${sono}`); } } export default new SimpleOrderService();
- SimpleApprovalService.js
import http from '@/utils/http-common'; // springboot 로 연결할 axios 함수들 // 전송 경로 : springboot url을 코딩 class SimpleApprovalService { getAll(approvalDate, page, size) { return http.get(`/shop/simple-approval?approvalDate${approvalDate}&page${page}&size${size}`); } get(sano) { return http.get(`/shop/simple-approval/${sano}`); } create(data) { return http.post("/shop/simple-approval", data); } update(sano, data) { return http.put(`/shop/simple-approval/${sano}`, data); } remove(sano) { return http.delete(`/shop/simple-approval/deletion/${sano}`); } } export default new SimpleApprovalService();
2) Vue 페이지
상품 페이지
결과 화면 :
- 상품 전체 조회
DeptList.vue
<template> <div> <!-- {/* dname start */} --> <div class="row mb-5 justify-content-center"> <!-- {/* w-50 : 크기 조정, mx-auto : 중앙정렬(margin: 0 auto), justify-content-center */} --> <div class="col-12 w-50 input-group mb-3"> <input type="text" class="form-control" placeholder="Search by Title" v-model="searchTitle" /> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="button" :click="retrieveSimpleProduct" > Search </button> </div> </div> </div> <!-- {/* dname end */} --> <!-- {/* paging 시작 */} --> <div class="mb-3"> Items per Page: <select v-model="pageSize" @change="handlePageSizeChange($event)"> <option v-for="size in pageSizes" :key="size" :value="size"> {{ size }} </option> </select> </div> <b-pagination v-model="page" :total-rows="count" :per-page="pageSize" prev-text="Prev" next-text="Next" @change="handlePageChange" ></b-pagination> <!-- {/* paging 끝 */} --> <div class="row"> <div v-for="(data, index) in simpleProduct" class="ms-5 col-lg-3 col-md-3 mt-5" :key="index" > <div class="card"> <img :src="data.imgPath" class="card-img-top" alt="..." /> <div class="card-body"> <h5 class="card-title">{{ data.title }}</h5> <h5 class="card-title">₩ {{ data.unitPrice }}</h5> <a :href="'/simple-cart/' + data.spno" class="btn btn-primary"> SimpleProduct Cart </a> </div> </div> </div> </div> </div> </template> <script> import SimpleProductService from '@/services/shop/SimpleProductService'; export default { data() { return { simpleProduct: [], searchTitle: "", page: 1, count: 0, pageSize: 3, pageSizes: [3, 6, 9], }; }, methods: { retrieveSimpleProduct() { SimpleProductService.getAll( this.searchTitle, this.page - 1, this.pageSize ) .then((response) => { const { simpleProduct, totalItems } = response.data; this.simpleProduct = simpleProduct; this.count = totalItems; console.log("response", response.data); }) .catch((e) => { console.log(e); }); }, // 페이지 번호 변경시 실행되는 함수 // 부트스트랩-페이지 양식에 페이지번호만 매개변수로 전달하면 됨 // 페이지번호를 변경한 숫자가 매개변수(value)로 전달됨 handlePageChange(value) { this.page = value; this.retrieveCodeCategory(); }, // 셀렉트 박스 값 변경시 (페이지 크기 변경) 실행되는 함수 // event.target.value : 셀렉트 박스에서 선택된 값 handlePageSizeChange(event) { this.pageSize = event.target.value; this.page = 1; this.retrieveCodeCategory(); }, }, mounted() { this.retrieveSimpleProduct(); }, }; </script> <style lang=""></style>
Vue 장바구니 상세 페이지
결과 화면 :
- 장바구니 상세 페이지
SimpleCartDetail.vue
<template> <div> <div v-if="simpleProduct" class="card mb-3"> <div class="row g-0"> <div class="col-md-4"> <img :src="simpleProduct.imgPath" class="img-fluid rounded-start" alt="..." /> </div> <div class="col-md-8"> <div class="card-body"> <h5 class="card-title">{{ simpleProduct.title }}</h5> <h5 class="card-title">₩ {{ simpleProduct.unitPrice }}</h5> <p class="card-text"> 영원한 아이콘인 {{ simpleProduct.title }} 으로 스타일링 해보세요. <br /> 또한, 4계절을 함께 할 {{ simpleProduct.title }} 으로 여러분의 OOTD 를 표현해 보세요. </p> <div class="btn-group col" role="group" aria-label="Basic outlined example" > <button type="button" class="btn btn-outline-secondary opacity-50" @click="decreaseCount" > - </button> <button type="button" class="btn btn-outline-dark" disabled> {{ cartCount }} </button> <button type="button" class="btn btn-outline-secondary opacity-50" @click="increaseCount" > + </button> </div> <div class="mt-3"> <button type="submit" @click="saveSimpleCart" class="btn btn-primary w-25" > Add to Simple Cart </button> <button type="submit" @click="goSimpleCart" class="btn btn-success w-25 ms-2" > Go to Simple Cart </button> </div> <div class="mt-3"> <button type="button" @click="goOrder" class="btn btn-warning w-25" > Simple Order </button> </div> <p v-if="message" class="alert alert-success mt-3 text-center"> {{ message }} </p> </div> </div> </div> </div> </div> </template> <script> import SimpleCartService from '@/services/shop/SimpleCartService'; import SimpleProductService from '@/services/shop/SimpleProductService'; export default { data() { return { simpleProduct: null, message: "", cartCount: 0, }; }, methods: { getSimpleProduct(spno) { SimpleProductService.get(spno) // spring 요청 // 성공/실패 모르겠지만 // 성공하면 then안에 있는것이 실행 .then((response) => { this.simpleProduct = response.data; console.log(response.data); }) // 실패하면 catch안에 있는것이 실행 .catch((e) => { console.log(e); }); }, saveSimpleCart() { let data = { spno: this.simpleProduct.spno, codeId: this.simpleProduct.codeId, title: this.simpleProduct.title, imgPath: this.simpleProduct.imgPath, unitPrice: this.simpleProduct.unitPrice, cartCount: this.cartCount, }; SimpleCartService.create(data) .then((response) => { console.log(response.data); this.message = "The simpleProduct was created successfully!"; }) .catch((e) => { console.log(e); }); }, goSimpleCart(){ this.$router.push("/simple-cart"); }, increaseCount() { this.cartCount += 1; }, decreaseCount() { if (this.cartCount > 0) { this.cartCount -= 1; } }, // 주문하기 페이지로 이동 함수 goOrder() { if (this.cartCount == 0) { this.message = "The simpleProduct cartCount is greater than 0"; return; } alert(`주문했습니다. ${this.simpleProduct.title}, ${this.cartCount}`); } }, mounted() { this.message = ""; this.getSimpleProduct(this.$route.params.spno); }, }; </script> <style lang=""></style>
Vue 장바구니 전체 페이지
결과 화면 :
- 장바구니 전체 페이지
SimpleCartList.vue
<template> <div> <!-- {/* dname start */} --> <div class="row mb-5 justify-content-center"> <!-- {/* w-50 : 크기 조정, mx-auto : 중앙정렬(margin: 0 auto), justify-content-center */} --> <div class="col-12 w-50 input-group mb-3"> <input type="text" class="form-control" placeholder="Search by Title" v-model="searchTitle" /> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="button" @click="retrieveSimpleCart" > Search </button> </div> </div> </div> <!-- {/* dname end */} --> <!-- {/* paging 시작 */} --> <div class="mb-3"> Items per Page: <select v-model="pageSize" @change="handlePageSizeChange($event)"> <option v-for="size in pageSizes" :key="size" :value="size"> {{ size }} </option> </select> </div> <b-pagination v-model="page" :total-rows="count" :per-page="pageSize" prev-text="Prev" next-text="Next" @change="handlePageChange" ></b-pagination> <!-- {/* paging 끝 */} --> <div class="row"> <div v-for="(data, index) in simpleCart" class="card mb-3" :key="index"> <div class="row g-0 p-3"> <div class="col-md-4 p-3 border"> <img :src="data.imgPath" class="img-fluid rounded-start" alt="..." style="{ height: 15 + 'vh', width: 5 + 'vw' }" /> </div> <div class="col-md-8"> <div class="card-body"> <h5 class="card-title">물품 : {{ data.title }}</h5> <h5 class="card-title"> 가격 : {{ data.unitPrice * data.cartCount }} </h5> <h5 class="card-title">장바구니 : {{ data.cartCount }}</h5> <div class="mt-3"> <!-- {/* 삭제 버튼 시작 */} --> <button type="button" @click="deleteSimpleCart(data.scno)" class="btn btn-danger w-25" > delete to Cart </button> <!-- {/* 삭제 버튼 끝 */} --> </div> </div> </div> </div> </div> </div> <div v-if="simpleCart" class="row d-flex justify-content-end"> <!-- {/* 삭제 버튼 시작 */} --> <button type="button" @click="goOrder" class="btn btn-warning w-25"> Simple Order </button> <!-- {/* 삭제 버튼 끝 */} --> </div> </div> </template> <script> import SimpleCartService from "@/services/shop/SimpleCartService" import SimpleOrderService from "@/services/shop/SimpleOrderService" export default { data() { return { simpleCart: [], searchTitle: "", page: 1, count: 0, pageSize: 3, pageSizes: [3, 6, 9], }; }, methods: { retrieveSimpleCart() { SimpleCartService.getAll(this.searchTitle, this.page - 1, this.pageSize) .then((response) => { const { simpleCart, totalItems } = response.data; this.simpleCart = simpleCart; this.count = totalItems; console.log("response", response.data); }) .catch((e) => { console.log(e); }); }, deleteSimpleCart(scno) { SimpleCartService.remove(scno) .then((response) => { console.log("response", response.data); let message = "정상적으로 삭제되었습니다. "; alert(message); // 삭제 후 재조회 this.retrieveSimpleCart(); }) .catch((e) => { console.log(e); }); }, goOrder() { if (this.simpleCart.length <= 0) { alert("주문할 물품이 없습니다."); return; } // 1) 오늘 날짜 let now = new Date(); // yyyy-mm-dd hh:mi:ss let formatNow = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; // 1) 단가 * 개수 = 장바구니 물품별금액 // 2) 모든 장바구니 물품별 금액 더하기 == 총금액 let totalPrice = this.simpleCart // 1) 물품별금액 배열 만들기 .map((data) => data.unitPrice * data.cartCount) // 2) 누적합 : 물품별금액 더하기 .reduce((acc, cur) => acc + cur); let simpleOrderDetails = { sono: null, spno: 0, productCount: 0, }; let simpleOrderDetailList = []; // simpleOrderDetails 배열 만들기 this.simpleCart.forEach((element) => { simpleOrderDetails.spno = element.spno; simpleOrderDetails.productCount = element.cartCount; simpleOrderDetailList.push(simpleOrderDetails); }); // 임시 객체 let data = { simpleOrderDetailList: simpleOrderDetailList, // 주문상세 객체 저장 orderDate: formatNow, // 주문상태(50001: 입금전, 50002: 결재완료, 50003: 상품준비중, 50004: 배송준비중, 50007:배송중, 50006:배송완료) orderStatus: 50001, productAmount: totalPrice, deliveryAmount: 0, orderAmount: totalPrice, deliveryAddr: "", deliveryMsg: "", }; console.log(data); SimpleOrderService.create(data) .then((response) => { console.log("response", response); this.$router.push("/simple-order/" + response.data.sono); }) .catch((e) => { console.log(e); }); }, // 페이지 번호 변경시 실행되는 함수 // 부트스트랩-페이지 양식에 페이지번호만 매개변수로 전달하면 됨 // 페이지번호를 변경한 숫자가 매개변수(value)로 전달됨 handlePageChange(value) { this.page = value; this.retrieveCodeCategory(); }, // 셀렉트 박스 값 변경시 (페이지 크기 변경) 실행되는 함수 // event.target.value : 셀렉트 박스에서 선택된 값 handlePageSizeChange(event) { this.pageSize = event.target.value; this.page = 1; this.retrieveCodeCategory(); }, }, mounted() { this.retrieveSimpleCart(); }, }; </script> <style lang=""></style>
Vue 주문 페이지
결과 화면 :
- 주문 페이지
SimpleOrderList.vue
<template> <div> <!-- {/* 제목 start */} --> <h1>SimpleOrder List</h1> <!-- {/* 제목 end */} --> <!-- {/* paging 시작 */} --> <div class="mb-3"> Items per Page: <select v-model="pageSize" @change="handlePageSizeChange($event)"> <option v-for="size in pageSizes" :key="size" :value="size"> {{ size }} </option> </select> </div> <b-pagination v-model="page" :total-rows="count" :per-page="pageSize" prev-text="Prev" next-text="Next" @change="handlePageChange" ></b-pagination> <!-- {/* paging 끝 */} --> <!-- {/* 주문내역 시작 */} --> <div class="row"> <div class="card mb-3"> <div class="row g-0 p-3"> <div class="col-md-4 p-3 border"> 주문번호 : {{ simpleOrder?.sono }} </div> <div class="col-md-8"> <div class="card-body"> <h5 class="card-title"> 주문날짜 : {{ simpleOrder?.orderDate }} </h5> <h5 class="card-title">개수 : {{ simpleOrder?.orderAmount }}</h5> </div> </div> </div> </div> </div> <!-- {/* 주문내역 끝 */} --> <!-- {/* 제목 start */} --> <h1>SimpleOrder Delivery</h1> <!-- {/* 제목 end */} --> <!-- {/* 배송지 시작 */} --> <div class="col-12 mx-auto"> <div class="row g-3 align-items-center mb-3"> <div class="col-3"> <label htmlFor="deliveryAddr" class="col-form-label"> Delivery Address </label> </div> <div class="col-9"> <input type="text" id="deliveryAddr" required class="form-control" v-model="simpleDelivery.deliveryAddr" placeholder="deliveryAddr" name="deliveryAddr" /> </div> </div> <div class="row g-3 align-items-center mb-3"> <div class="col-3"> <label htmlFor="deliveryMsg" class="col-form-label"> Melivery Message </label> </div> <div class="col-9"> <input type="text" id="deliveryMsg" required class="form-control" v-model="simpleDelivery.deliveryMsg" placeholder="deliveryMsg" name="deliveryMsg" /> </div> </div> </div> <!-- {/* 배송지 끝 */} {/* 버튼 시작 */} --> <div class="row d-flex justify-content-end"> <!-- {/* 취소 버튼 시작 */} --> <button type="button" @click="cancelOrder" class="btn btn-danger w-25 me-3" > Cancel Order </button> <!-- {/* 취소 버튼 끝 */} {/* 결재 버튼 시작 */} --> <button type="button" @click="goApproval" class="btn btn-warning w-25"> Go Approval </button> <!-- {/* 결재 버튼 끝 */} --> </div> <!-- {/* 버튼 끝 */} --> </div> </template> <script> import SimpleOrderService from "@/services/shop/SimpleOrderService"; export default { data() { return { simpleOrder: null, simpleDelivery: { deliveryAmount: 3000, // 기본 : 3000 deliveryAddr: "", deliveryMsg: "", }, page: 1, count: 0, pageSize: 3, pageSizes: [3, 6, 9], }; }, methods: { retrieveSimpleOrder(sono) { SimpleOrderService.get(sono) .then((response) => { this.simpleOrder = response.data; console.log("response", response.data); }) .catch((e) => { console.log(e); }); }, // 주문 취소 시 : 주문 테이블 삭제 -> 자식 테이블도 자동삭제됨 cancelOrder() { SimpleOrderService.remove(this.simpleOrder?.sono) .then((response) => { console.log(response); this.$router.push("/simple-cart"); // 장바구니로 이동 }) .catch((e) => { console.log(e); }); }, // 결재 테이블에 저장 goApproval() { // 임시 객체 : simpleOrderDto[0] 의 부모 컬럼만 수정 : 배열이지만 값이 모두 같음 let data = { sono: this.simpleOrder?.sono || 0, simpleOrderDetailList: this.simpleOrder?.simpleOrderDetailList || null, orderDate: this.simpleOrder?.orderDate || "", orderStatus: 50001, // 주문완료 : 50001 productAmount: this.simpleOrder?.productAmount || 0, deliveryAmount: this.simpleDelivery.deliveryAmount, // 기본 : 3000 orderAmount: (this.simpleOrder?.orderAmount || 0) + this.simpleDelivery.deliveryAmount, deliveryAddr: this.simpleDelivery.deliveryAddr, deliveryMsg: this.simpleDelivery.deliveryMsg, }; console.log("goApproval", data); // 1) 주문 테이블에 데이터 저장 // 2) 카프카 메세지 전송 : order 토픽 , 50001 : 주문완료 SimpleOrderService.update(this.simpleOrder?.sono, data) .then((response) => { console.log(response); // 관리자 : 결재서비스로 이동 this.$router.push(`/simple-approval/${this.simpleOrder?.sono}`); }) .catch((e) => { console.log(e); }); }, // 페이지 번호 변경시 실행되는 함수 // 부트스트랩-페이지 양식에 페이지번호만 매개변수로 전달하면 됨 // 페이지번호를 변경한 숫자가 매개변수(value)로 전달됨 handlePageChange(value) { this.page = value; this.retrieveCodeCategory(); }, // 셀렉트 박스 값 변경시 (페이지 크기 변경) 실행되는 함수 // event.target.value : 셀렉트 박스에서 선택된 값 handlePageSizeChange(event) { this.pageSize = event.target.value; this.page = 1; this.retrieveCodeCategory(); }, }, mounted() { this.retrieveSimpleOrder(this.$route.params.sono); }, }; </script> <style></style>
Vue 결재 전체 페이지
결과 화면 :
- 주문 상태 보기 및 결재 페이지(참고 : 관리자 페이지)
SimpleApprovalList.vue
<template> <div> <h1>SimpleApproval</h1> <div class="col-md-12"> <!-- {/* table start */} --> <table class="table"> <thead> <tr> <th scope="col">sono<br /> </th> <th scope="col">order<br />Date</th> <th scope="col">order<br />Status</th> <th scope="col">product<br />Amount</th> <th scope="col">delivery<br />Amount</th> <th scope="col">order<br />Amount</th> <th scope="col">delivery<br />Addr</th> <th scope="col">delivery<br />Msg</th> </tr> </thead> <tbody> <tr> <td>{{ simpleOrder?.sono }}</td> <td>{{ simpleOrder?.orderDate }}</td> <td>{{ simpleOrder?.orderStatus }}</td> <td>{{ simpleOrder?.productAmount }}</td> <td>{{ simpleOrder?.deliveryAmount }}</td> <td>{{ simpleOrder?.orderAmount }}</td> <td>{{ simpleOrder?.deliveryAddr }}</td> <td>{{ simpleOrder?.deliveryMsg }}</td> </tr> </tbody> </table> <!-- {/* table end */} --> </div> <h1>SimpleApproval Admin Panel</h1> <div v-if="simpleOrder" class="row d-flex justify-content-end"> <button type="button" @click="approvalCancel" class="btn btn-danger w-25 me-3" > Approval Cancel </button> <button type="button" @click="confirmApproval" class="btn btn-warning w-25"> Approval </button> </div> </div> </template> <script> import SimpleApprovalService from "@/services/shop/SimpleApprovalService"; import SimpleOrderService from '@/services/shop/SimpleOrderService'; export default { data() { return { simpleOrder: null, simpleApproval: null, sono: this.$route.params.sono, page: 1, count: 0, pageSize: 3, pageSizes: [3, 6, 9], }; }, methods: { retrieveSimpleOrder(sono) { SimpleOrderService.get(sono) .then((response) => { this.simpleOrder = response.data; console.log("SimpleApprovalService", response.data); }) .catch((e) => { console.log(e); }); }, confirmApproval() { let now = new Date(); // yyyy-mm-dd hh:mi:ss let formatNow = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; // 임시 객체 let data = { sano: null, simpleOrderDetailList: this.simpleOrder?.simpleOrderDetailList || null, sono: this.simpleOrder?.sono || "", approvalDate: formatNow, approvalAmount: this.simpleOrder?.orderAmount || 0, // 상품금액 + 배송비 = 주문금액(결재금액) }; console.log("goApproval", data); // 1) 주문 테이블에 데이터 수정 // 2) 카프카 메세지 전송 : order 토픽 , 50001 : 주문완료 SimpleApprovalService.create(data) .then((response) => { this.simpleApproval = response.data; console.log(response); alert("결재가 완료되었습니다.") // 관리자 : 배송서비스로 이동 // this.$router.push(`/simple-delivery/${response.data.sano}`); }) .catch((e) => { console.log(e); }); }, // 취소 시 : 카프카로 결재취소(50011) 메세지 전송 : 50011: 결재취소, 50012: 재고부족취소, 50013:고객취소) // 보상 트랜잭션 approvalCancel() { this.$router.push(`/simple-order/${this.sono}`); }, }, mounted() { this.retrieveSimpleOrder(this.$route.params.sono); }, }; </script> <style lang=""></style>
📃 결론
쇼핑몰 샘플 예제를 Vue & Spring boot 연동 예제를 살펴보았습니다.
기본적인 CRUD 기능을 구현했으며 게시판의 페이징 처리도 해봤습니다.
Spring Boot 는 @RestController 어노테이션을 이용해 구현했으며, 결과는 JSON 데이터로 리턴했으며,
Vue 에 axios 라이브러리를 이용해 전달했습니댜.
카프카를 이용해서 메세징서비스로 트랜잭션을 전달했으며 결과적 일관성을 구현했습니다.
DB 프레임워크는 JPA 를 이용해서 sql 문을 직접 제작하지 않고 자동화기능을 이용해 구현했습니다.
Mybatis 의 직접 sql 문 제작 기능에 대응해 자주 반복되고 쉬운 기능은 JPA 의 sql 자동생성 기능을 이용하고,
복잡한 sql 문은 @Query 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.
감사합니다.