데이터베이스 팀프로젝트 4주차 작업로그

박서영·2026년 6월 9일

데이터베이스 수업

목록 보기
13/13

4주차 작업

1-3주차에 작업한 데이터베이스 쿼리 및 API 명세서를 기반으로 스프링의 CRUD 기능을 사용해 필요한 백엔드 부분을 구현한 뒤에 AI를 사용해 구현한 백엔드의 DTO 및 API를 활용해 프론트엔드를 구현. 이후 Vercel과 AWS를 활용해 배포를 진행하였습니다.


Spring CRUD 작성

상품

1. 폴더 분리

  • 폴더는 controller, domain(entity), dto(response/request), service, repository로 분리하여 진행하였습니다.

  • SQL 쿼리 공유

2. Controller

  • 상품 생성 (POST)
  • 상품 조회 (GET)
    • 상품 목록 조회 (일반)
    • 상품 카테고리별 조회
    • 상품 가격순 조회
    • 상품 개별 조회
  • 상품 가격 수정 (PATCH)
  • 상품 삭제 (DELETE)

코드

@Controller
@RequiredArgsConstructor
@RequestMapping("/products")
public class ProductController {
    private final ProductService productService;

    //TODO: 상품 생성
    @PostMapping
    public ResponseEntity<ProductResponse> createProduct(@RequestBody CreateProductRequest request) {
        ProductResponse response = productService.createProduct(request);

        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    //TODO: 상품 목록 조회 (가격순 정렬)
    @GetMapping
    public ResponseEntity<ProductListResponse> getProductList(@RequestParam(defaultValue = "productName,desc") String sortBy) {
        ProductListResponse response = productService.getProductList(sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 카테고리별 상품 목록 조회 (가격순 정렬)
    @GetMapping("/category/{categoryId}")
    public ResponseEntity<ProductListResponse> getProductListByCategory(@PathVariable("categoryId") Long categoryId,
                                                                        @RequestParam(defaultValue = "productName,desc") String sortBy) {
        ProductListResponse response = productService.getProductListByCategory(categoryId, sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 개별 조회
    @GetMapping("/{productId}")
    public ResponseEntity<ProductResponse> getProductDetail(@PathVariable("productId") Long productId) {
        ProductResponse response = productService.getProductDetail(productId);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 검색
    @GetMapping("/search")
    public ResponseEntity<ProductListResponse> searchProduct(@RequestBody SearchProduct request,
                                                         @RequestParam(defaultValue = "productName,desc") String sortBy) {
        ProductListResponse response = productService.searchProduct(request, sortBy);

        return ResponseEntity.ok(response);

    }

    //TODO: 상품 가격 업데이트
    @PatchMapping("/{productId}")
    public ResponseEntity<Void> updateProductPrice(@PathVariable("productId") Long productId,
                                                   @RequestBody UpdateProductPrice request) {
        productService.updateProductPrice(productId, request);

        return ResponseEntity.ok().build();
    }

    //TODO: 상품 삭제
    @DeleteMapping("/{productId}")
    public ResponseEntity<Void> deleteProduct(@PathVariable("productId") Long productId) {
        productService.deleteProduct(productId);

        return ResponseEntity.ok().build();
    }
}

3. Domain(Entity)

Category

@Entity
@Getter
@NoArgsConstructor
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "category_id")
    private Long id;

    @Column(name = "category_name")
    private String category;

    @Builder
    public Category(String category) {
        this.category = category;
    }
}

Manufacturer

@Entity
@Getter
@NoArgsConstructor
public class Manufacturer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "manufacturer_id")
    private Long id;

    @Column (name = "company_name")
    private String company;

    @Column (name = "owner")
    private String owner;

    @Builder
    public Manufacturer(String company, String owner) {
        this.company = company;
        this.owner = owner;
    }
}

Product

@Entity
@Getter
@NoArgsConstructor
public class Product {
    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    @Column(name = "product_id")
    private Long id;

    @ManyToOne (fetch = FetchType.LAZY)
    @JoinColumn(name = "manufacturer_id")
    private Manufacturer manufacturer;

    @Column(name = "product_name")
    private String productName;

    @Column(name = "price")
    private int price;

    @OneToOne
    @JoinColumn(name = "category_id")
    private Category category;

    @Column (name = "image_url")
    private String imageUrl;

    @Builder
    public Product(Manufacturer manufacturer, String name, int price, Category category, String imageUrl) {
        this.manufacturer = manufacturer;
        this.productName = name;
        this.category = category;
        this.price = price;
        this.imageUrl = imageUrl;
    }

    public void updatePrice(int price) {
        this.price = price;
    }

}

4. Dto

CreateProductRequest

@Getter
@AllArgsConstructor
public class CreateProductRequest {
    private Long manufacturerId;
    private String productName;
    private int price;
    private Long categoryId;
    private String imageUrl;
}

SearchProduct

@Getter
@AllArgsConstructor
public class SearchProduct {
    private String keyword;
}

UpdateProductPrice

@Getter
@AllArgsConstructor
public class UpdateProductPrice {
    private int price;
}

ProductListResponse

@Getter
@Builder
@AllArgsConstructor
public class ProductListResponse {
    private List<ProductResponse> productResponseList;
    private Long productCount;

    public static ProductListResponse of (List<Product> products) {
        return ProductListResponse.builder()
                .productResponseList(products.stream().map(ProductResponse::of).toList())
                .productCount((long) products.size())
                .build();
    }
}

ProductResponse

@Getter
@Builder
@AllArgsConstructor
public class ProductResponse {
    private Long productId;
    private String productName;
    private int price;
    private String manufacturer;
    private String category;
    private String imageUrl;

    public static ProductResponse of (Product product) {
        return ProductResponse.builder()
                .productId(product.getId())
                .productName(product.getProductName())
                .price(product.getPrice())
                .category(product.getCategory().getCategory())
                .manufacturer(product.getManufacturer().getCompany())
                .imageUrl(product.getImageUrl())
                .build();
    }
}

5. Repository

  • 검색 관련 쿼리만 JPQL을 사용하여 작성하였습니다.
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findAllByCategory(Category category, Sort sort);
    @Query("select p from Product p where p.productName LIKE %:keyword%" )
    List<Product> searchByName(@Param("keyword")String keyword, Sort sort);
}

6. Service

코드

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ManufacturerRepository manufacturerRepository;
    private final CategoryRepository categoryRepository;
    private final ProductRepository productRepository;

    //TODO: 상품 생성
    @Transactional
    public ProductResponse createProduct(CreateProductRequest request) {
        Manufacturer manufacturer = getManufacturer(request.getManufacturerId());
        Category category = getCategory(request.getCategoryId());

        Product newProduct = Product.builder()
                .manufacturer(manufacturer)
                .name(request.getProductName())
                .price(request.getPrice())
                .category(category)
                .imageUrl(request.getImageUrl())
                .build();

        productRepository.save(newProduct);

        return ProductResponse.of(newProduct);
    }

    //TODO: 상품 목록조회 (일반)
    @Transactional(readOnly = true)
    public ProductListResponse getProductList(String sortBy) {
        Sort sort = createSort(sortBy);
        List<Product> productList = productRepository.findAll(sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 목록조회 (카테고리)
    @Transactional(readOnly = true)
    public ProductListResponse getProductListByCategory(Long categoryId, String sortBy) {
        Category category = getCategory(categoryId);
        Sort sort = createSort(sortBy);
        List<Product> productList = productRepository.findAllByCategory(category, sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 개별조회
    @Transactional(readOnly = true)
    public ProductResponse getProductDetail(Long productId) {
        Product product = getProduct(productId);

        return ProductResponse.of(product);
    }

    //TODO: 상품 가격 수정
    @Transactional
    public void updateProductPrice(Long productId, UpdateProductPrice request) {
        Product product = getProduct(productId);
        product.updatePrice(request.getPrice());
    }

    //TODO: 상품 삭제
    @Transactional
    public void deleteProduct(Long productId) {
        Product product = getProduct(productId);
        productRepository.delete(product);
    }

    //TODO: 상품 검색
    @Transactional(readOnly = true)
    public ProductListResponse searchProduct(SearchProduct request, String sortBy) {
        Sort sort = createSort(sortBy);
        List<Product> productList = productRepository.searchByName(request.getKeyword(),sort);

        return ProductListResponse.of(productList);
    }

    private Product getProduct(Long productId) {
        return productRepository.findById(productId)
                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));
    }
    private Manufacturer getManufacturer(Long manufacturerId) {
        return manufacturerRepository.findById(manufacturerId)
                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));
    }

    private Category getCategory (Long categoryId) {
        return categoryRepository.findById(categoryId)
                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));
    }

    //AI 도움.
    private Sort createSort (String sortBy) {
        try {
            String[] sortParams = sortBy.split(",");
            String property = sortParams[0];
            String direction = sortParams[1];

            if (direction.equalsIgnoreCase("asc")) {
                return Sort.by(Sort.Direction.ASC, property);
            } else {
                return Sort.by(Sort.Direction.DESC, property);
            }
        } catch (Exception e) {
            return Sort.by(Sort.Direction.DESC, "productName");
        }
    }

}

7. 테스트 화면

(1) 상품 목록 조회: 일반 (GET)

URL: /products


(2) 상품 목록 조회: 카테고리별 (GET)

URL: /product/category/{categoryId}


(3) 상품 목록 조회 (가격순: 내림차순) (GET)

URL: /products?sortBy=price,desc


(4) 상품 목록 조회 (가격순: 오름차순) (GET)

URL: /products?sortBy=price,asc


(5) 상품 개별 조회 (GET)

URL: /products/{productId}


(6) 상품 생성 (POST)

URL: /products


(7) 상품 가격 수정

URL: /products/{productId}


(8) 상품 삭제

URL: /products/{productId}


(9) 상품 키워드 검색

URL: /products/search


상품상세(SKU)

상품상세 생성

상품상세 조회

상품마다 어떤 옵션이 존재하고 있는지를 반환받도록 구성

보면 ProductId는 모두 동일하고 SKU(판매단위)가 되는 ProductDetailId 및 관련 내용들만 상이함.

상품옵션 조회

상품상세 하나를 조회하는 것 (즉, 사이즈, 색상까지 모두 선택한 상품상세 SKU를 하나 조회)

상품옵션 수정

  • 남은 재고 수량, 판매량, 추가금, 이미지 링크 등을 수정할 수 있으면 변화가 없을 때는 기존의 값이 유지됨.

리뷰

리뷰 생성

(상품별)리뷰 조회

리뷰 수정

리뷰 삭제


통계

(1) 연/월/일별 매출 조회

연매출 조회

월별 매출 조회

일별 매출 조회

(2) 상품별 판매량 조회

(3) 카테고리별 조회


소감

뭔가 처음에는 빨리해서 끝내려고 했는데 생각보다 이렇게 쌓아보니까 팀원 각자가 한 일이 너무 많았다. 그리고 뭔가 부피도 엄청 컸다. 스프링으로 구현하니까 더 컸다.

왠지 ERD 작성하고 개념적 설계, 논리적 설계 이런 부분을 진행할 때는 목표에 엔티티도 많고 해서 팀원 각자가 설계해볼 수 있어서 괜찮은 것 같았는데 이걸 또 다 구현하고 정규화도 하고, 쿼리도 작성하다보니까 정말 힘들었다.

약간 규모를 좀 더 잘 잡을 수 있지 않았을까 싶기도 하다.

힘들었다는 내용은 일단 제쳐두고 소감을 생각해보면, 이게 상품, 장바구니 류를 주로 맡아서 계속 진행을 했는데 상품 부분 데이터베이스를 짜는게 생각보다 까다로웠다. 옵션을 만들까말까하다가 사실 처음에 빼자고 했는데 ERD 그리면서 만들어버려서 그냥 끝까지 만들게되었다.

상품마다 옵션이 여러 개가 있고? 또 종류도 여러개가 있으니까 이게 구현하기가 진짜 어려웠다. 다대다 관계를 테이블로 빼는건 둘째치고, 이걸 계속 조인해서 쓰고 이런게 진짜 좋은건지는? 일단 그런식으로 구현을 하긴했는데 잘 모르겠다. 원래는 EXPLAIN 이런걸 써서 인덱스 설정 효과를 보거나 이것저것 더 테스트를 해보려고 했는데 여력이 안돼서 포기해야해서 아쉬웠다.

profile
이불 밖은 위험해.

0개의 댓글