
📃 요약
옥션, 롯데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 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.
감사합니다.



