+ 00 00 0000

Have any Questions?

04_Simple Coding – SI-MSA – 카프카 실전예제 3

04_Simple Coding – SI-MSA – 카프카 실전예제 3

Table of Contents

📃 요약

옥션, 롯데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 를 붙이고 사용함
    1. @IdClass(SonoSpnoPk.class) : 엔티티 클래스 위에 붙임 : 아래 SimpleOrderDetail.java 클래스 참고
    2. 각각의 속성(필드)에 @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 저장
            }
        }
    }
}
  • 카프카 리스너 :
    1. 카프카는 소비자(구독) – 생산자(주제 생성) 사이에서 이루어지는 메세징 프로그램임
    2. 토픽 – 카프카 메세지를 사용할때 주제 지정
    3. 소비자그룹 ID – 소비자들의 그룹 ID 지정
    4. 지정된 토픽과 소비자그룹에 대해 카프카 메세징 서비스가 새로운 소식이 들어올때 까지 대기함
  • 사용법 : @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&quot;,
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 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.

감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다