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


@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();
}
}
@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;
}
}
@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;
}
}
@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;
}
}
@Getter
@AllArgsConstructor
public class CreateProductRequest {
private Long manufacturerId;
private String productName;
private int price;
private Long categoryId;
private String imageUrl;
}
@Getter
@AllArgsConstructor
public class SearchProduct {
private String keyword;
}
@Getter
@AllArgsConstructor
public class UpdateProductPrice {
private int price;
}
@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();
}
}
@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();
}
}
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);
}
@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");
}
}
}










상품마다 어떤 옵션이 존재하고 있는지를 반환받도록 구성
보면 ProductId는 모두 동일하고 SKU(판매단위)가 되는 ProductDetailId 및 관련 내용들만 상이함.

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











소감
뭔가 처음에는 빨리해서 끝내려고 했는데 생각보다 이렇게 쌓아보니까 팀원 각자가 한 일이 너무 많았다. 그리고 뭔가 부피도 엄청 컸다. 스프링으로 구현하니까 더 컸다.
왠지 ERD 작성하고 개념적 설계, 논리적 설계 이런 부분을 진행할 때는 목표에 엔티티도 많고 해서 팀원 각자가 설계해볼 수 있어서 괜찮은 것 같았는데 이걸 또 다 구현하고 정규화도 하고, 쿼리도 작성하다보니까 정말 힘들었다.
약간 규모를 좀 더 잘 잡을 수 있지 않았을까 싶기도 하다.
힘들었다는 내용은 일단 제쳐두고 소감을 생각해보면, 이게 상품, 장바구니 류를 주로 맡아서 계속 진행을 했는데 상품 부분 데이터베이스를 짜는게 생각보다 까다로웠다. 옵션을 만들까말까하다가 사실 처음에 빼자고 했는데 ERD 그리면서 만들어버려서 그냥 끝까지 만들게되었다.
상품마다 옵션이 여러 개가 있고? 또 종류도 여러개가 있으니까 이게 구현하기가 진짜 어려웠다. 다대다 관계를 테이블로 빼는건 둘째치고, 이걸 계속 조인해서 쓰고 이런게 진짜 좋은건지는? 일단 그런식으로 구현을 하긴했는데 잘 모르겠다. 원래는 EXPLAIN 이런걸 써서 인덱스 설정 효과를 보거나 이것저것 더 테스트를 해보려고 했는데 여력이 안돼서 포기해야해서 아쉬웠다.