From 6fbdaef22e64cdd57113f8fb569d8d426f5d112d Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Sun, 12 Oct 2025 18:30:43 +0900 Subject: [PATCH 01/29] =?UTF-8?q?[FEAT]=20=EC=9E=90=EC=9E=AC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../factory/api/factory/entity/Factory.java | 28 +++++ .../api/material/entity/FactoryMaterial.java | 37 ++++++ .../factory/api/material/entity/Material.java | 31 +++++ .../api/material/entity/MaterialCategory.java | 27 +++++ .../api/material/entity/MaterialOrder.java | 45 ++++++++ .../material/entity/MaterialOrderItem.java | 30 +++++ .../api/material/entity/OrderStatus.java | 9 ++ .../common/config/DataInitializer.java | 108 ++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 4 +- .../factory/common/response/ErrorStatus.java | 24 ++-- .../common/response/PageResponseDto.java | 16 +++ .../data/materials_master_cleaned.csv | 41 +++++++ 13 files changed, 390 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/sampoom/factory/api/factory/entity/Factory.java create mode 100644 src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java create mode 100644 src/main/java/com/sampoom/factory/api/material/entity/Material.java create mode 100644 src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java create mode 100644 src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java create mode 100644 src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java create mode 100644 src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java create mode 100644 src/main/java/com/sampoom/factory/common/config/DataInitializer.java create mode 100644 src/main/java/com/sampoom/factory/common/response/PageResponseDto.java create mode 100644 src/main/resources/data/materials_master_cleaned.csv diff --git a/build.gradle b/build.gradle index 7d1d36a..62b43ca 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,8 @@ dependencies { //Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + + implementation("com.opencsv:opencsv:5.9") // CSV 파싱용 } tasks.named('test') { diff --git a/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java b/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java new file mode 100644 index 0000000..d301b70 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java @@ -0,0 +1,28 @@ +package com.sampoom.factory.api.factory.entity; + + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "factory") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Factory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "factory_id") + private Long id; + + @Column(name = "factory_name") + private String name; + + private String location; + +} diff --git a/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java new file mode 100644 index 0000000..d0d2863 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java @@ -0,0 +1,37 @@ +package com.sampoom.factory.api.material.entity; + +import com.sampoom.factory.api.factory.entity.Factory; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "factory_material") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FactoryMaterial { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "factory_material_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "factory_id") + private Factory factory; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "material_id") + private Material material; + + private Long quantity; + + public FactoryMaterial increaseQuantity(Long amount) { + this.quantity += amount; + return this; + } +} diff --git a/src/main/java/com/sampoom/factory/api/material/entity/Material.java b/src/main/java/com/sampoom/factory/api/material/entity/Material.java new file mode 100644 index 0000000..51592f5 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/Material.java @@ -0,0 +1,31 @@ +package com.sampoom.factory.api.material.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "material") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Material { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "material_id") + private Long id; + + @Column(name = "material_name") + private String name; + + @Column(name = "material_code") + private String code; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "material_category_id") + private MaterialCategory materialCategory; +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java new file mode 100644 index 0000000..5d8cb28 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java @@ -0,0 +1,27 @@ +package com.sampoom.factory.api.material.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "material_category") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MaterialCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "material_category_id") + private Long id; + + @Column(name = "material_category_name") + private String name; + + @Column(name = "material_category_code") + private String code; +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java new file mode 100644 index 0000000..ab87458 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java @@ -0,0 +1,45 @@ +package com.sampoom.factory.api.material.entity; + +import com.sampoom.factory.api.factory.entity.Factory; +import com.sampoom.factory.common.exception.BadRequestException; +import com.sampoom.factory.common.response.ErrorStatus; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Table(name = "material_order") +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MaterialOrder { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "material_order_id") + private Long id; + + private String code; + private LocalDateTime orderAt; + private LocalDateTime receivedAt; + + @Enumerated(EnumType.STRING) + private OrderStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "factory_id") + private Factory factory; + + public MaterialOrder receive() { + if (this.status != OrderStatus.ORDERED) { + throw new BadRequestException(ErrorStatus.ORDER_ALREADY_PROCESSED); + } + this.status = OrderStatus.RECEIVED; + this.receivedAt = LocalDateTime.now(); + return this; + } +} diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java new file mode 100644 index 0000000..baad238 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java @@ -0,0 +1,30 @@ +package com.sampoom.factory.api.material.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "material_order_item") +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MaterialOrderItem { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "material_order_item_id") + private Long id; + + private Long quantity; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "material_order_id") + private MaterialOrder materialOrder; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "material_id") + private Material material; +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java b/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java new file mode 100644 index 0000000..2f82ce9 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java @@ -0,0 +1,9 @@ +package com.sampoom.factory.api.material.entity; + +import lombok.Getter; + + +public enum OrderStatus { + ORDERED, // 주문됨 + RECEIVED // 입고됨 +} diff --git a/src/main/java/com/sampoom/factory/common/config/DataInitializer.java b/src/main/java/com/sampoom/factory/common/config/DataInitializer.java new file mode 100644 index 0000000..ad0fcf6 --- /dev/null +++ b/src/main/java/com/sampoom/factory/common/config/DataInitializer.java @@ -0,0 +1,108 @@ +package com.sampoom.factory.common.config; + +import com.opencsv.CSVReader; +import com.sampoom.factory.api.material.entity.Material; +import com.sampoom.factory.api.material.entity.MaterialCategory; +import com.sampoom.factory.api.material.repository.MaterialCategoryRepository; +import com.sampoom.factory.api.material.repository.MaterialRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Slf4j +@Component +@RequiredArgsConstructor +@Transactional +public class DataInitializer implements CommandLineRunner { + + private final MaterialRepository materialRepository; + private final MaterialCategoryRepository categoryRepository; + + @Override + public void run(String... args) throws Exception { + if (materialRepository.count() > 0) { + log.info("Material data already exists, skipping import."); + return; + } + + log.info("Importing CSV data into PostgreSQL..."); + + try (Reader reader = new InputStreamReader( + Objects.requireNonNull(getClass().getResourceAsStream("/data/materials_master_cleaned.csv")), + "UTF-8"); + CSVReader csvReader = new CSVReader(reader)) { + + List rows = csvReader.readAll(); + // 첫 줄 헤더 제거 + rows.remove(0); + + Map categoryCache = new HashMap<>(); + + for (String[] row : rows) { + Long id = Long.parseLong(row[0]); + Long categoryId = Long.parseLong(row[1]); + String categoryName = row[2]; + String code = row[3]; + String name = row[4]; + + // 카테고리 처리 로직 수정 + MaterialCategory category = categoryCache.get(categoryId); + if (category == null) { + // DB에서 먼저 찾고, 없으면 새로 생성 + category = categoryRepository.findById(categoryId).orElse(null); + if (category == null) { + // 카테고리 ID에 따라 적절한 접두사 선택 + String prefix; + switch(categoryId.intValue()) { + case 1: + prefix = "MTL"; + break; + case 2: + prefix = "PLS"; + break; + case 3: + prefix = "ELC"; + break; + case 4: + prefix = "CHM"; + break; + default: + prefix = "CAT"; + break; + } + + category = MaterialCategory.builder() + .name(categoryName) + .code(prefix) + .build(); + // ID는 명시적으로 설정하지 않음 (자동 생성) + category = categoryRepository.save(category); + } + categoryCache.put(categoryId, category); + } + + // 자재 저장 (기존과 동일) + Material material = Material.builder() + .name(name) + .code(code) + .materialCategory(category) + .build(); + + materialRepository.save(material); + } + + log.info("CSV import completed. Inserted materials: " + materialRepository.count()); + + + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/common/exception/GlobalExceptionHandler.java b/src/main/java/com/sampoom/factory/common/exception/GlobalExceptionHandler.java index 4439ed2..ea30741 100644 --- a/src/main/java/com/sampoom/factory/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/sampoom/factory/common/exception/GlobalExceptionHandler.java @@ -23,7 +23,7 @@ public ResponseEntity> handleRuntimeException(RuntimeException log.error(e.getMessage(), e); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.errorWithCode(20500, "런타임 오류가 발생했습니다.")); + .body(ApiResponse.errorWithCode(40500, "런타임 오류가 발생했습니다.")); } @ExceptionHandler(MethodArgumentNotValidException.class) @@ -31,7 +31,7 @@ public ResponseEntity> handleValidationException(MethodArgumen String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); return ResponseEntity .status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.errorWithCode(20000, errorMessage)); + .body(ApiResponse.errorWithCode(40000, errorMessage)); } } diff --git a/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java b/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java index 8f19cf6..47e87a3 100644 --- a/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java +++ b/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java @@ -10,29 +10,33 @@ public enum ErrorStatus { // 400 BAD_REQUEST - BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다.",20001), - MISSING_EMAIL_VERIFICATION_EXCEPTION(HttpStatus.BAD_REQUEST, "이메일 인증을 진행해주세요.",20002), - ALREADY_REGISTER_EMAIL_EXCEPETION(HttpStatus.BAD_REQUEST, "이미 가입된 이메일 입니다.",20003), + BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다.",40001), + MISSING_EMAIL_VERIFICATION_EXCEPTION(HttpStatus.BAD_REQUEST, "이메일 인증을 진행해주세요.",40002), + ALREADY_REGISTER_EMAIL_EXCEPETION(HttpStatus.BAD_REQUEST, "이미 가입된 이메일 입니다.",40003), + FACTORY_ORDER_MISMATCH(HttpStatus.BAD_REQUEST, "해당 공장의 주문이 아닙니다.",40004), + ORDER_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "이미 처리된 주문입니다.",40005), // 401 UNAUTHORIZED - UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다.", 20101), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다.", 40101), // 403 FORBIDDEN - FORBIDDEN(HttpStatus.FORBIDDEN, "접근 권한이 없습니다.",20301), + FORBIDDEN(HttpStatus.FORBIDDEN, "접근 권한이 없습니다.",40301), // 404 NOT_FOUND - NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다.",20401), - MATERIAL_NOT_FOUND(HttpStatus.NOT_FOUND, "자재를 찾을 수 없습니다.", 20402), - CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다.", 20403), + NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다.",40401), + MATERIAL_NOT_FOUND(HttpStatus.NOT_FOUND, "자재를 찾을 수 없습니다.", 40402), + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다.", 40403), + FACTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "공장을 찾을 수 없습니다.", 40404), + ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다.", 40405), // 409 CONFLICT - CONFLICT(HttpStatus.CONFLICT, "충돌이 발생했습니다.",20901), + CONFLICT(HttpStatus.CONFLICT, "충돌이 발생했습니다.",40901), // 500 INTERNAL_SERVER_ERROR - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다.",20501); + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다.",40501); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/sampoom/factory/common/response/PageResponseDto.java b/src/main/java/com/sampoom/factory/common/response/PageResponseDto.java new file mode 100644 index 0000000..1576e56 --- /dev/null +++ b/src/main/java/com/sampoom/factory/common/response/PageResponseDto.java @@ -0,0 +1,16 @@ +package com.sampoom.factory.common.response; + +import lombok.*; + +import java.util.List; + + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PageResponseDto { + private List content; // 실제 데이터 + private long totalElements; // 총 요소 수 + private int totalPages; // 총 페이지 수 +} \ No newline at end of file diff --git a/src/main/resources/data/materials_master_cleaned.csv b/src/main/resources/data/materials_master_cleaned.csv new file mode 100644 index 0000000..eb1d32f --- /dev/null +++ b/src/main/resources/data/materials_master_cleaned.csv @@ -0,0 +1,41 @@ +id,category_id,category,code,name +1,1,금속,MTL-0001,냉간압연강판 +2,1,금속,MTL-0002,열간압연강판 +3,1,금속,MTL-0003,스테인리스강 +4,1,금속,MTL-0004,알루미늄 합금봉 +5,1,금속,MTL-0005,구리판 +6,1,금속,MTL-0006,마그네슘 합금 +7,1,금속,MTL-0007,아연 도금강판 +8,1,금속,MTL-0008,철선(강선) +9,1,금속,MTL-0009,베어링강 +10,1,금속,MTL-0010,주철 +11,2,플라스틱/고무,PLS-0001,ABS 수지 +12,2,플라스틱/고무,PLS-0002,폴리프로필렌 +13,2,플라스틱/고무,PLS-0003,폴리우레탄 +14,2,플라스틱/고무,PLS-0004,고무 원자재 +15,2,플라스틱/고무,PLS-0005,실리콘 +16,2,플라스틱/고무,PLS-0006,나일론 +17,2,플라스틱/고무,PLS-0007,폴리에틸렌 +18,2,플라스틱/고무,PLS-0008,폴리카보네이트 +19,2,플라스틱/고무,PLS-0009,열가소성 엘라스토머 +20,2,플라스틱/고무,PLS-0010,EPDM 고무 +21,3,전기전자,ELC-0001,구리 전선 +22,3,전기전자,ELC-0002,알루미늄 전선 +23,3,전기전자,ELC-0003,커넥터 핀 +24,3,전기전자,ELC-0004,PCB 원판 +25,3,전기전자,ELC-0005,반도체 칩 +26,3,전기전자,ELC-0006,저항기 +27,3,전기전자,ELC-0007,콘덴서 +28,3,전기전자,ELC-0008,다이오드 +29,3,전기전자,ELC-0009,릴레이 +30,3,전기전자,ELC-0010,퓨즈 +31,4,화학/소모품,CHM-0001,도료(블랙) +32,4,화학/소모품,CHM-0002,도료(화이트) +33,4,화학/소모품,CHM-0003,접착제 +34,4,화학/소모품,CHM-0004,실리콘실란트 +35,4,화학/소모품,CHM-0005,윤활유 +36,4,화학/소모품,CHM-0006,그리스 +37,4,화학/소모품,CHM-0007,냉각수 +38,4,화학/소모품,CHM-0008,브레이크액 +39,4,화학/소모품,CHM-0009,세정제 +40,4,화학/소모품,CHM-0010,프라이머 From 387f9000def4a8252cb891968679bfa420938103 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Sun, 12 Oct 2025 18:31:19 +0900 Subject: [PATCH 02/29] =?UTF-8?q?[FEAT]=20=EA=B3=B5=EC=9E=A5=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=20=EA=B4=80=EB=A0=A8=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../factory/repository/FactoryRepository.java | 7 + .../controller/FactoryMaterialController.java | 83 +++++++++++ .../dto/MaterialCategoryResponseDto.java | 25 ++++ .../material/dto/MaterialOrderItemDto.java | 25 ++++ .../dto/MaterialOrderItemRequestDto.java | 23 +++ .../material/dto/MaterialOrderRequestDto.java | 17 +++ .../dto/MaterialOrderResponseDto.java | 43 ++++++ .../api/material/dto/MaterialResponseDto.java | 35 +++++ .../repository/FactoryMaterialRepository.java | 24 +++ .../MaterialCategoryRepository.java | 7 + .../MaterialOrderItemRepository.java | 10 ++ .../repository/MaterialOrderRepository.java | 10 ++ .../repository/MaterialRepository.java | 7 + .../service/FactoryMaterialService.java | 89 +++++++++++ .../service/MaterialOrderService.java | 139 ++++++++++++++++++ 15 files changed, 544 insertions(+) create mode 100644 src/main/java/com/sampoom/factory/api/factory/repository/FactoryRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java create mode 100644 src/main/java/com/sampoom/factory/api/material/dto/MaterialCategoryResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemDto.java create mode 100644 src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemRequestDto.java create mode 100644 src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderRequestDto.java create mode 100644 src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/material/dto/MaterialResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/material/repository/MaterialCategoryRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderItemRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/material/repository/MaterialRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java create mode 100644 src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java diff --git a/src/main/java/com/sampoom/factory/api/factory/repository/FactoryRepository.java b/src/main/java/com/sampoom/factory/api/factory/repository/FactoryRepository.java new file mode 100644 index 0000000..cee17c9 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/repository/FactoryRepository.java @@ -0,0 +1,7 @@ +package com.sampoom.factory.api.factory.repository; + +import com.sampoom.factory.api.factory.entity.Factory; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FactoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java new file mode 100644 index 0000000..a8f3c29 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java @@ -0,0 +1,83 @@ +package com.sampoom.factory.api.material.controller; + +import com.sampoom.factory.api.material.dto.MaterialCategoryResponseDto; +import com.sampoom.factory.api.material.dto.MaterialResponseDto; +import com.sampoom.factory.common.response.PageResponseDto; +import com.sampoom.factory.api.material.service.FactoryMaterialService; +import com.sampoom.factory.api.material.dto.MaterialOrderRequestDto; +import com.sampoom.factory.api.material.dto.MaterialOrderResponseDto; +import com.sampoom.factory.api.material.service.MaterialOrderService; +import com.sampoom.factory.common.response.ApiResponse; +import com.sampoom.factory.common.response.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "FactoryMaterial", description = "FactoryMaterial 관련 API 입니다.") +@RestController +@RequestMapping("/api/factory") +@RequiredArgsConstructor +public class FactoryMaterialController { + + private final FactoryMaterialService factoryMaterialService; + private final MaterialOrderService materialOrderService; + + @Operation(summary = "자재 카테고리 조회", description = "모든 자재 카테고리를 조회합니다.") + @GetMapping("/material/categories") + public ResponseEntity>> getMaterialCategories() { + return ApiResponse.success(SuccessStatus.OK, factoryMaterialService.getAllMaterialCategories()); + } + + @Operation(summary = "공장별 자재 카테고리별 자재 조회", description = "특정 공장의 특정 카테고리에 속한 자재를 조회합니다.") + @GetMapping("/{factoryId}/material/category/{categoryId}") + public ResponseEntity>> getMaterialsByFactoryAndCategory( + @PathVariable Long factoryId, + @PathVariable Long categoryId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return ApiResponse.success(SuccessStatus.OK, + factoryMaterialService.getMaterialsByFactoryAndCategory(factoryId, categoryId, page, size)); + } + + @Operation(summary = "공장별 자재 목록 조회", description = "특정 공장에 있는 모든 자재를 조회합니다.") + @GetMapping("/{factoryId}/material") + public ResponseEntity>> getMaterialsByFactory( + @PathVariable Long factoryId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return ApiResponse.success(SuccessStatus.OK, + factoryMaterialService.getMaterialsByFactoryId(factoryId, page, size)); + } + + @Operation(summary = "자재 주문 생성", description = "공장에 필요한 자재 주문을 생성합니다.") + @PostMapping("/{factoryId}/material/order") + public ResponseEntity> createMaterialOrder( + @PathVariable Long factoryId, + @RequestBody MaterialOrderRequestDto requestDto) { + return ApiResponse.success(SuccessStatus.CREATED, + materialOrderService.createMaterialOrder(factoryId, requestDto)); + } + + @Operation(summary = "자재 주문 목록 조회", description = "공장의 자재 주문 목록을 조회합니다.") + @GetMapping("/{factoryId}/material/order") + public ResponseEntity>> getMaterialOrders( + @PathVariable Long factoryId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return ApiResponse.success(SuccessStatus.OK, + materialOrderService.getMaterialOrdersByFactory(factoryId, page, size)); + } + + @Operation(summary = "자재 주문 입고 처리", description = "자재 주문을 입고 처리합니다.") + @PutMapping("/{factoryId}/material/order/{orderId}/receive") + public ResponseEntity> receiveMaterialOrder( + @PathVariable Long factoryId, + @PathVariable Long orderId) { + return ApiResponse.success(SuccessStatus.OK, + materialOrderService.receiveMaterialOrder(factoryId, orderId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/dto/MaterialCategoryResponseDto.java b/src/main/java/com/sampoom/factory/api/material/dto/MaterialCategoryResponseDto.java new file mode 100644 index 0000000..2635795 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/dto/MaterialCategoryResponseDto.java @@ -0,0 +1,25 @@ +package com.sampoom.factory.api.material.dto; + +import com.sampoom.factory.api.material.entity.MaterialCategory; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MaterialCategoryResponseDto { + private Long id; + private String name; + private String code; + + public static MaterialCategoryResponseDto from(MaterialCategory category) { + return MaterialCategoryResponseDto.builder() + .id(category.getId()) + .name(category.getName()) + .code(category.getCode()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemDto.java b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemDto.java new file mode 100644 index 0000000..166af66 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemDto.java @@ -0,0 +1,25 @@ +package com.sampoom.factory.api.material.dto; + +import com.sampoom.factory.api.material.entity.MaterialOrderItem; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MaterialOrderItemDto { + private Long materialId; + private String materialName; + private Long quantity; + + public static MaterialOrderItemDto from(MaterialOrderItem item) { + return MaterialOrderItemDto.builder() + .materialId(item.getMaterial().getId()) + .materialName(item.getMaterial().getName()) + .quantity(item.getQuantity()) + .build(); + } +} diff --git a/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemRequestDto.java b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemRequestDto.java new file mode 100644 index 0000000..014474c --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderItemRequestDto.java @@ -0,0 +1,23 @@ +package com.sampoom.factory.api.material.dto; + +import com.sampoom.factory.api.material.entity.MaterialOrderItem; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MaterialOrderItemRequestDto { + private Long materialId; + private Long quantity; + + public static MaterialOrderItemRequestDto from(MaterialOrderItem item) { + return MaterialOrderItemRequestDto.builder() + .materialId(item.getMaterial().getId()) + .quantity(item.getQuantity()) + .build(); + } +} diff --git a/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderRequestDto.java b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderRequestDto.java new file mode 100644 index 0000000..ce65d2c --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderRequestDto.java @@ -0,0 +1,17 @@ +package com.sampoom.factory.api.material.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MaterialOrderRequestDto { + private List items; + +} diff --git a/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderResponseDto.java b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderResponseDto.java new file mode 100644 index 0000000..a503afb --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/dto/MaterialOrderResponseDto.java @@ -0,0 +1,43 @@ +package com.sampoom.factory.api.material.dto; + +import com.sampoom.factory.api.material.entity.OrderStatus; +import com.sampoom.factory.api.material.entity.MaterialOrder; +import com.sampoom.factory.api.material.entity.MaterialOrderItem; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MaterialOrderResponseDto { + private Long id; + private String code; + private Long factoryId; + private String factoryName; + private OrderStatus status; + private LocalDateTime orderAt; + private LocalDateTime receivedAt; + private List items; + + public static MaterialOrderResponseDto from(MaterialOrder order, List orderItems) { + return MaterialOrderResponseDto.builder() + .id(order.getId()) + .code(order.getCode()) + .factoryId(order.getFactory().getId()) + .factoryName(order.getFactory().getName()) + .status(order.getStatus()) + .orderAt(order.getOrderAt()) + .receivedAt(order.getReceivedAt()) + .items(orderItems.stream() + .map(MaterialOrderItemDto::from) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/com/sampoom/factory/api/material/dto/MaterialResponseDto.java b/src/main/java/com/sampoom/factory/api/material/dto/MaterialResponseDto.java new file mode 100644 index 0000000..ec7962a --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/dto/MaterialResponseDto.java @@ -0,0 +1,35 @@ +package com.sampoom.factory.api.material.dto; + +import com.sampoom.factory.api.material.entity.Material; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MaterialResponseDto { + private Long id; + private String name; + private String materialCode; + private Long materialCategoryId; + private String materialCategoryName; + private Long quantity; + + public static MaterialResponseDto from(Material material) { + return MaterialResponseDto.builder() + .id(material.getId()) + .name(material.getName()) + .materialCode(material.getCode()) + .materialCategoryId(material.getMaterialCategory().getId()) + .materialCategoryName(material.getMaterialCategory().getName()) + .build(); + } + + public MaterialResponseDto withQuantity(Long quantity) { + this.quantity = quantity; + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java new file mode 100644 index 0000000..38d6468 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java @@ -0,0 +1,24 @@ +package com.sampoom.factory.api.material.repository; + + +import com.sampoom.factory.api.material.entity.FactoryMaterial; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FactoryMaterialRepository extends JpaRepository { + + + + @EntityGraph(attributePaths = {"material", "material.materialCategory"}) + Page findByFactory_IdAndMaterial_MaterialCategory_Id( + Long factoryId, Long categoryId, Pageable pageable); + + @EntityGraph(attributePaths = {"material"}) + Page findByFactory_Id(Long factoryId, Pageable pageable); + + Optional findByFactoryIdAndMaterialId(Long factoryId, Long materialId); +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/repository/MaterialCategoryRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/MaterialCategoryRepository.java new file mode 100644 index 0000000..70e9be0 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/repository/MaterialCategoryRepository.java @@ -0,0 +1,7 @@ +package com.sampoom.factory.api.material.repository; + +import com.sampoom.factory.api.material.entity.MaterialCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MaterialCategoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderItemRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderItemRepository.java new file mode 100644 index 0000000..d8b9793 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderItemRepository.java @@ -0,0 +1,10 @@ +package com.sampoom.factory.api.material.repository; + +import com.sampoom.factory.api.material.entity.MaterialOrderItem; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MaterialOrderItemRepository extends JpaRepository { + List findByMaterialOrderId(Long materialOrderId); +} diff --git a/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java new file mode 100644 index 0000000..a91b153 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java @@ -0,0 +1,10 @@ +package com.sampoom.factory.api.material.repository; + +import com.sampoom.factory.api.material.entity.MaterialOrder; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MaterialOrderRepository extends JpaRepository { + Page findByFactoryId(Long factoryId, Pageable pageable); +} diff --git a/src/main/java/com/sampoom/factory/api/material/repository/MaterialRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/MaterialRepository.java new file mode 100644 index 0000000..15dcaa7 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/repository/MaterialRepository.java @@ -0,0 +1,7 @@ +package com.sampoom.factory.api.material.repository; + +import com.sampoom.factory.api.material.entity.Material; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MaterialRepository extends JpaRepository { +} diff --git a/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java b/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java new file mode 100644 index 0000000..5baf7b1 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java @@ -0,0 +1,89 @@ +package com.sampoom.factory.api.material.service; + +import com.sampoom.factory.api.material.dto.MaterialCategoryResponseDto; +import com.sampoom.factory.api.material.dto.MaterialResponseDto; +import com.sampoom.factory.common.response.PageResponseDto; +import com.sampoom.factory.api.factory.entity.Factory; +import com.sampoom.factory.api.material.entity.FactoryMaterial; +import com.sampoom.factory.api.material.repository.FactoryMaterialRepository; +import com.sampoom.factory.api.factory.repository.FactoryRepository; +import com.sampoom.factory.api.material.entity.Material; +import com.sampoom.factory.api.material.entity.MaterialCategory; +import com.sampoom.factory.api.material.repository.MaterialCategoryRepository; +import com.sampoom.factory.common.exception.NotFoundException; +import com.sampoom.factory.common.response.ErrorStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FactoryMaterialService { + + private final FactoryRepository factoryRepository; + private final FactoryMaterialRepository factoryMaterialRepository; + private final MaterialCategoryRepository materialCategoryRepository; + + public List getAllMaterialCategories() { + List categories = materialCategoryRepository.findAll(); + return categories.stream() + .map(MaterialCategoryResponseDto::from) + .collect(Collectors.toList()); + } + + public PageResponseDto getMaterialsByFactoryAndCategory( + Long factoryId, Long categoryId, int page, int size) { + Factory factory = factoryRepository.findById(factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.FACTORY_NOT_FOUND)); + + MaterialCategory category = materialCategoryRepository.findById(categoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.CATEGORY_NOT_FOUND)); + + Pageable pageable = PageRequest.of(page, size); + Page materialsPage = factoryMaterialRepository + .findByFactory_IdAndMaterial_MaterialCategory_Id(factoryId, categoryId, pageable); + + List content = materialsPage.getContent().stream() + .map(factoryMaterial -> { + Material material = factoryMaterial.getMaterial(); + return MaterialResponseDto.from(material) + .withQuantity(factoryMaterial.getQuantity()); + }) + .collect(Collectors.toList()); + + return PageResponseDto.builder() + .content(content) + .totalElements(materialsPage.getTotalElements()) + .totalPages(materialsPage.getTotalPages()) + .build(); + } + + public PageResponseDto getMaterialsByFactoryId(Long factoryId, int page, int size) { + Factory factory = factoryRepository.findById(factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.FACTORY_NOT_FOUND)); + + Pageable pageable = PageRequest.of(page, size); + Page factoryMaterialsPage = factoryMaterialRepository.findByFactory_Id(factoryId, pageable); + + List content = factoryMaterialsPage.getContent().stream() + .map(factoryMaterial -> { + Material material = factoryMaterial.getMaterial(); + return MaterialResponseDto.from(material) + .withQuantity(factoryMaterial.getQuantity()); + }) + .collect(Collectors.toList()); + + return PageResponseDto.builder() + .content(content) + .totalElements(factoryMaterialsPage.getTotalElements()) + .totalPages(factoryMaterialsPage.getTotalPages()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java new file mode 100644 index 0000000..c4b22d6 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java @@ -0,0 +1,139 @@ +package com.sampoom.factory.api.material.service; + +import com.sampoom.factory.common.response.PageResponseDto; +import com.sampoom.factory.api.factory.entity.Factory; +import com.sampoom.factory.api.material.entity.FactoryMaterial; +import com.sampoom.factory.api.material.repository.FactoryMaterialRepository; +import com.sampoom.factory.api.factory.repository.FactoryRepository; +import com.sampoom.factory.api.material.entity.Material; +import com.sampoom.factory.api.material.entity.OrderStatus; +import com.sampoom.factory.api.material.repository.MaterialRepository; +import com.sampoom.factory.api.material.dto.MaterialOrderRequestDto; +import com.sampoom.factory.api.material.dto.MaterialOrderResponseDto; +import com.sampoom.factory.api.material.entity.MaterialOrder; +import com.sampoom.factory.api.material.entity.MaterialOrderItem; +import com.sampoom.factory.api.material.repository.MaterialOrderItemRepository; +import com.sampoom.factory.api.material.repository.MaterialOrderRepository; +import com.sampoom.factory.common.exception.BadRequestException; +import com.sampoom.factory.common.exception.NotFoundException; +import com.sampoom.factory.common.response.ErrorStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class MaterialOrderService { + + private final MaterialOrderRepository orderRepository; + private final MaterialOrderItemRepository orderItemRepository; + private final FactoryRepository factoryRepository; + private final MaterialRepository materialRepository; + private final FactoryMaterialRepository factoryMaterialRepository; + + @Transactional + public MaterialOrderResponseDto createMaterialOrder(Long factoryId, MaterialOrderRequestDto requestDto) { + Factory factory = factoryRepository.findById(factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.FACTORY_NOT_FOUND)); + + MaterialOrder order = MaterialOrder.builder() + .code(generateOrderCode()) + .factory(factory) + .status(OrderStatus.ORDERED) + .orderAt(LocalDateTime.now()) + .build(); + + orderRepository.save(order); + + List orderItems = requestDto.getItems().stream() + .map(item -> { + Material material = materialRepository.findById(item.getMaterialId()) + .orElseThrow(() -> new NotFoundException(ErrorStatus.MATERIAL_NOT_FOUND)); + + return MaterialOrderItem.builder() + .materialOrder(order) + .material(material) + .quantity(item.getQuantity()) + .build(); + }) + .collect(Collectors.toList()); + + orderItemRepository.saveAll(orderItems); + + return MaterialOrderResponseDto.from(order, orderItems); + } + + @Transactional(readOnly = true) + public PageResponseDto getMaterialOrdersByFactory(Long factoryId, int page, int size) { + factoryRepository.findById(factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.FACTORY_NOT_FOUND)); + + PageRequest pageRequest = PageRequest.of(page, size); + Page ordersPage = orderRepository.findByFactoryId(factoryId, pageRequest); + + List content = ordersPage.getContent().stream() + .map(order -> { + List items = orderItemRepository.findByMaterialOrderId(order.getId()); + return MaterialOrderResponseDto.from(order, items); + }) + .collect(Collectors.toList()); + + return PageResponseDto.builder() + .content(content) + .totalElements(ordersPage.getTotalElements()) + .totalPages(ordersPage.getTotalPages()) + .build(); + } + + @Transactional + public MaterialOrderResponseDto receiveMaterialOrder(Long factoryId, Long orderId) { + factoryRepository.findById(factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.FACTORY_NOT_FOUND)); + + MaterialOrder order = orderRepository.findById(orderId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.ORDER_NOT_FOUND)); + + if (!order.getFactory().getId().equals(factoryId)) { + throw new BadRequestException(ErrorStatus.FACTORY_ORDER_MISMATCH); + } + + order.receive(); + orderRepository.save(order); + + // 주문 아이템 조회 + List items = orderItemRepository.findByMaterialOrderId(orderId); + + // 각 주문 아이템에 대해 공장 자재 수량 증가 + for (MaterialOrderItem item : items) { + Material material = item.getMaterial(); + Long quantity = item.getQuantity(); + + // 해당 공장의 자재 찾기 + FactoryMaterial factoryMaterial = factoryMaterialRepository.findByFactoryIdAndMaterialId( + factoryId, material.getId()) + .orElseGet(() -> { + // 없으면 새로 생성 + FactoryMaterial newMaterial = FactoryMaterial.builder() + .factory(order.getFactory()) + .material(material) + .quantity(0L) + .build(); + return factoryMaterialRepository.save(newMaterial); + }); + + // 수량 증가 + factoryMaterial.increaseQuantity(quantity); + } + return MaterialOrderResponseDto.from(order, items); + } + + private String generateOrderCode() { + return "ORD-" + System.currentTimeMillis(); + } +} \ No newline at end of file From 4147fa354aacd8e64efdc5b2e035ed65673f73df Mon Sep 17 00:00:00 2001 From: Choosla Date: Tue, 14 Oct 2025 10:48:20 +0900 Subject: [PATCH 03/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 9190e01..928e790 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -2,7 +2,7 @@ name: PR Reminder on: schedule: - - cron: "0 0,5,8 * * *" # 아침 9시, 오후 2시, 오후 5시에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) + - cron: "47 23,4,7,8, 10 * * *" # 아침 8시 47분, 오후 2시 47분, 오후 4시 47분, 오후 5시 47분, 오후 7시 47분 에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) workflow_dispatch: jobs: From 1d6e0b280daa9c24e5018f15a1976b9498ab0072 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Tue, 14 Oct 2025 14:26:51 +0900 Subject: [PATCH 04/29] =?UTF-8?q?[FIX]=20=EA=B3=B5=EC=9E=A5=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20api=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sampoom/factory/api/health/HealthCheckController.java | 2 +- .../api/material/controller/FactoryMaterialController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sampoom/factory/api/health/HealthCheckController.java b/src/main/java/com/sampoom/factory/api/health/HealthCheckController.java index 40b60f6..1410abc 100644 --- a/src/main/java/com/sampoom/factory/api/health/HealthCheckController.java +++ b/src/main/java/com/sampoom/factory/api/health/HealthCheckController.java @@ -18,7 +18,7 @@ @Tag(name = "HealthCheck", description = "HealthCheck 관련 API 입니다.") @RestController -@RequestMapping("/api/v1") +@RequestMapping() public class HealthCheckController { diff --git a/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java index a8f3c29..3867b75 100644 --- a/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java +++ b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java @@ -19,7 +19,7 @@ @Tag(name = "FactoryMaterial", description = "FactoryMaterial 관련 API 입니다.") @RestController -@RequestMapping("/api/factory") +@RequestMapping() @RequiredArgsConstructor public class FactoryMaterialController { From d78c46bdabb1ddcec3bba771558d83dfbc25aebb Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Tue, 14 Oct 2025 17:28:38 +0900 Subject: [PATCH 05/29] =?UTF-8?q?[FIX]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/swagger/SwaggerConfig.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java b/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java index 17d3549..ba60caa 100644 --- a/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java +++ b/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java @@ -3,8 +3,11 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.List; + @Configuration public class SwaggerConfig { @@ -14,16 +17,22 @@ public class SwaggerConfig { // @Value("${jwt.refresh.header}") // private String refreshTokenHeader; + @Bean public OpenAPI openAPI() { - Server server = new Server(); - server.setUrl("http://localhost:8080"); + Server localServer = new Server() + .url("http://localhost:8080/api/factory") + .description("로컬 서버"); + + Server prodServer = new Server() + .url("https://sampoom.store/api/factory") + .description("배포 서버"); return new OpenAPI() .info(new Info() - .title("삼삼오토") - .description("삼삼오토 REST API Document") + .title("삼삼오토 Factory Service API") + .description("Factory 서비스 REST API 문서") .version("1.0.0")) - .addServersItem(server); + .servers(List.of(localServer, prodServer)); } // @Bean From ee38ef3f256f945ba3a69fbaa9cea56295598162 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Wed, 15 Oct 2025 10:36:24 +0900 Subject: [PATCH 06/29] =?UTF-8?q?[FIX]=20=EC=9E=90=EC=9E=AC,=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=9D=BD=EA=B8=B0=20=EC=A0=84=EC=9A=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/material/entity/FactoryMaterial.java | 10 +- .../factory/api/material/entity/Material.java | 15 +-- .../api/material/entity/MaterialCategory.java | 15 +-- .../material/entity/MaterialOrderItem.java | 9 +- .../service/MaterialOrderService.java | 8 +- .../common/config/DataInitializer.java | 108 ------------------ .../common/config/swagger/SwaggerConfig.java | 4 +- 7 files changed, 31 insertions(+), 138 deletions(-) delete mode 100644 src/main/java/com/sampoom/factory/common/config/DataInitializer.java diff --git a/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java index d0d2863..4dfe6c2 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java @@ -24,14 +24,16 @@ public class FactoryMaterial { @JoinColumn(name = "factory_id") private Factory factory; + @Column(name = "material_id", nullable = false) + private Long materialId; // 실제 DB에 저장되는 FK 값 + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "material_id") - private Material material; + @JoinColumn(name = "material_id", insertable = false, updatable = false) + private Material material; // 읽기 전용 뷰 private Long quantity; - public FactoryMaterial increaseQuantity(Long amount) { + public void increaseQuantity(Long amount) { this.quantity += amount; - return this; } } diff --git a/src/main/java/com/sampoom/factory/api/material/entity/Material.java b/src/main/java/com/sampoom/factory/api/material/entity/Material.java index 51592f5..8cfb995 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/Material.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/Material.java @@ -1,17 +1,14 @@ package com.sampoom.factory.api.material.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; +import org.hibernate.annotations.Immutable; @Entity @Table(name = "material") @Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Immutable public class Material { @Id @@ -19,10 +16,10 @@ public class Material { @Column(name = "material_id") private Long id; - @Column(name = "material_name") + @Column(name = "material_name", nullable = false) private String name; - @Column(name = "material_code") + @Column(name = "material_code", nullable = false) private String code; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java index 5d8cb28..b74c689 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java @@ -1,17 +1,14 @@ package com.sampoom.factory.api.material.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; +import org.hibernate.annotations.Immutable; @Entity @Table(name = "material_category") @Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Immutable public class MaterialCategory { @Id @@ -19,9 +16,9 @@ public class MaterialCategory { @Column(name = "material_category_id") private Long id; - @Column(name = "material_category_name") + @Column(name = "material_category_name", nullable = false) private String name; - @Column(name = "material_category_code") + @Column(name = "material_category_code", nullable = false) private String code; } \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java index baad238..aad2e60 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java @@ -24,7 +24,12 @@ public class MaterialOrderItem { @JoinColumn(name = "material_order_id") private MaterialOrder materialOrder; + @Column(name = "material_id", nullable = false) + private Long materialId; // 실제 DB에 저장되는 FK 값 + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "material_id") - private Material material; + @JoinColumn(name = "material_id", insertable = false, updatable = false) + private Material material; // 읽기 전용 뷰 + + } \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java index c4b22d6..7c1b27a 100644 --- a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java +++ b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java @@ -58,7 +58,7 @@ public MaterialOrderResponseDto createMaterialOrder(Long factoryId, MaterialOrde return MaterialOrderItem.builder() .materialOrder(order) - .material(material) + .materialId(item.getMaterialId()) .quantity(item.getQuantity()) .build(); }) @@ -111,17 +111,17 @@ public MaterialOrderResponseDto receiveMaterialOrder(Long factoryId, Long orderI // 각 주문 아이템에 대해 공장 자재 수량 증가 for (MaterialOrderItem item : items) { - Material material = item.getMaterial(); + Long materialId = item.getMaterialId(); Long quantity = item.getQuantity(); // 해당 공장의 자재 찾기 FactoryMaterial factoryMaterial = factoryMaterialRepository.findByFactoryIdAndMaterialId( - factoryId, material.getId()) + factoryId, materialId) .orElseGet(() -> { // 없으면 새로 생성 FactoryMaterial newMaterial = FactoryMaterial.builder() .factory(order.getFactory()) - .material(material) + .materialId(materialId) .quantity(0L) .build(); return factoryMaterialRepository.save(newMaterial); diff --git a/src/main/java/com/sampoom/factory/common/config/DataInitializer.java b/src/main/java/com/sampoom/factory/common/config/DataInitializer.java deleted file mode 100644 index ad0fcf6..0000000 --- a/src/main/java/com/sampoom/factory/common/config/DataInitializer.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.sampoom.factory.common.config; - -import com.opencsv.CSVReader; -import com.sampoom.factory.api.material.entity.Material; -import com.sampoom.factory.api.material.entity.MaterialCategory; -import com.sampoom.factory.api.material.repository.MaterialCategoryRepository; -import com.sampoom.factory.api.material.repository.MaterialRepository; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -@Slf4j -@Component -@RequiredArgsConstructor -@Transactional -public class DataInitializer implements CommandLineRunner { - - private final MaterialRepository materialRepository; - private final MaterialCategoryRepository categoryRepository; - - @Override - public void run(String... args) throws Exception { - if (materialRepository.count() > 0) { - log.info("Material data already exists, skipping import."); - return; - } - - log.info("Importing CSV data into PostgreSQL..."); - - try (Reader reader = new InputStreamReader( - Objects.requireNonNull(getClass().getResourceAsStream("/data/materials_master_cleaned.csv")), - "UTF-8"); - CSVReader csvReader = new CSVReader(reader)) { - - List rows = csvReader.readAll(); - // 첫 줄 헤더 제거 - rows.remove(0); - - Map categoryCache = new HashMap<>(); - - for (String[] row : rows) { - Long id = Long.parseLong(row[0]); - Long categoryId = Long.parseLong(row[1]); - String categoryName = row[2]; - String code = row[3]; - String name = row[4]; - - // 카테고리 처리 로직 수정 - MaterialCategory category = categoryCache.get(categoryId); - if (category == null) { - // DB에서 먼저 찾고, 없으면 새로 생성 - category = categoryRepository.findById(categoryId).orElse(null); - if (category == null) { - // 카테고리 ID에 따라 적절한 접두사 선택 - String prefix; - switch(categoryId.intValue()) { - case 1: - prefix = "MTL"; - break; - case 2: - prefix = "PLS"; - break; - case 3: - prefix = "ELC"; - break; - case 4: - prefix = "CHM"; - break; - default: - prefix = "CAT"; - break; - } - - category = MaterialCategory.builder() - .name(categoryName) - .code(prefix) - .build(); - // ID는 명시적으로 설정하지 않음 (자동 생성) - category = categoryRepository.save(category); - } - categoryCache.put(categoryId, category); - } - - // 자재 저장 (기존과 동일) - Material material = Material.builder() - .name(name) - .code(code) - .materialCategory(category) - .build(); - - materialRepository.save(material); - } - - log.info("CSV import completed. Inserted materials: " + materialRepository.count()); - - - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java b/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java index ba60caa..7776086 100644 --- a/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java +++ b/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java @@ -20,7 +20,7 @@ public class SwaggerConfig { @Bean public OpenAPI openAPI() { Server localServer = new Server() - .url("http://localhost:8080/api/factory") + .url("http://localhost:8080/") .description("로컬 서버"); Server prodServer = new Server() @@ -32,7 +32,7 @@ public OpenAPI openAPI() { .title("삼삼오토 Factory Service API") .description("Factory 서비스 REST API 문서") .version("1.0.0")) - .servers(List.of(localServer, prodServer)); + .servers(List.of( prodServer,localServer)); } // @Bean From d53fc1a3ffa2827e89b57b15c92b95e9505979cc Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Wed, 15 Oct 2025 11:17:05 +0900 Subject: [PATCH 07/29] =?UTF-8?q?[FEAT]=20=EA=B3=B5=EC=9E=A5=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../factory/controller/FactoryController.java | 28 +++++++ .../factory/dto/FactoryCreateRequestDto.java | 21 ++++++ .../api/factory/dto/FactoryResponseDto.java | 21 ++++++ .../api/factory/service/FactoryService.java | 47 ++++++++++++ .../factory/service/FactoryServiceTest.java | 74 +++++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java create mode 100644 src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java create mode 100644 src/main/java/com/sampoom/factory/api/factory/dto/FactoryResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java create mode 100644 src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java diff --git a/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java b/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java new file mode 100644 index 0000000..a923fa1 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java @@ -0,0 +1,28 @@ +package com.sampoom.factory.api.factory.controller; + +import com.sampoom.factory.api.factory.dto.FactoryCreateRequestDto; +import com.sampoom.factory.api.factory.dto.FactoryResponseDto; +import com.sampoom.factory.api.factory.service.FactoryService; +import com.sampoom.factory.common.response.ApiResponse; +import com.sampoom.factory.common.response.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Factory", description = "Factory 관련 API 입니다.") +@RestController +@RequiredArgsConstructor +public class FactoryController { + + private final FactoryService factoryService; + + @Operation(summary = "공장 생성", description = "공장을 생성합니다.") + @PostMapping + public ResponseEntity> createFactory(@RequestBody FactoryCreateRequestDto requestDto) { + FactoryResponseDto responseDto = factoryService.createFactory(requestDto); + return ApiResponse.success(SuccessStatus.CREATED,responseDto); + } +} diff --git a/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java b/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java new file mode 100644 index 0000000..af4074b --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java @@ -0,0 +1,21 @@ +package com.sampoom.factory.api.factory.dto; + +import com.sampoom.factory.api.factory.entity.Factory; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class FactoryCreateRequestDto { + private String name; + private String location; + + public Factory toEntity() { + return Factory.builder() + .name(this.name) + .location(this.location) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/factory/dto/FactoryResponseDto.java b/src/main/java/com/sampoom/factory/api/factory/dto/FactoryResponseDto.java new file mode 100644 index 0000000..90c998f --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/dto/FactoryResponseDto.java @@ -0,0 +1,21 @@ +package com.sampoom.factory.api.factory.dto; + +import com.sampoom.factory.api.factory.entity.Factory; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FactoryResponseDto { + private Long id; + private String name; + private String location; + + public static FactoryResponseDto from(Factory factory) { + return FactoryResponseDto.builder() + .id(factory.getId()) + .name(factory.getName()) + .location(factory.getLocation()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java b/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java new file mode 100644 index 0000000..95afe70 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java @@ -0,0 +1,47 @@ +package com.sampoom.factory.api.factory.service; + +import com.sampoom.factory.api.factory.dto.FactoryCreateRequestDto; +import com.sampoom.factory.api.factory.dto.FactoryResponseDto; +import com.sampoom.factory.api.factory.entity.Factory; +import com.sampoom.factory.api.factory.repository.FactoryRepository; +import com.sampoom.factory.api.material.entity.FactoryMaterial; +import com.sampoom.factory.api.material.entity.Material; +import com.sampoom.factory.api.material.repository.FactoryMaterialRepository; +import com.sampoom.factory.api.material.repository.MaterialRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FactoryService { + + private final FactoryRepository factoryRepository; + private final MaterialRepository materialRepository; + private final FactoryMaterialRepository factoryMaterialRepository; + + @Transactional + public FactoryResponseDto createFactory(FactoryCreateRequestDto requestDto) { + // 공장 생성 + Factory factory = requestDto.toEntity(); + factory = factoryRepository.save(factory); + + // 모든 재료 조회 + List allMaterials = materialRepository.findAll(); + + // 각 재료에 대해 공장 자재 정보 생성 (수량 0으로 설정) + for (Material material : allMaterials) { + FactoryMaterial factoryMaterial = FactoryMaterial.builder() + .factory(factory) + .materialId(material.getId()) + .quantity(0L) + .build(); + + factoryMaterialRepository.save(factoryMaterial); + } + + return FactoryResponseDto.from(factory); + } +} diff --git a/src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java b/src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java new file mode 100644 index 0000000..ef6ab76 --- /dev/null +++ b/src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java @@ -0,0 +1,74 @@ +package com.sampoom.factory.api.factory.service; + +import com.sampoom.factory.api.factory.dto.FactoryCreateRequestDto; +import com.sampoom.factory.api.factory.dto.FactoryResponseDto; +import com.sampoom.factory.api.factory.entity.Factory; +import com.sampoom.factory.api.factory.repository.FactoryRepository; +import com.sampoom.factory.api.material.entity.FactoryMaterial; +import com.sampoom.factory.api.material.entity.Material; +import com.sampoom.factory.api.material.repository.FactoryMaterialRepository; +import com.sampoom.factory.api.material.repository.MaterialRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FactoryServiceTest { + + @Mock private FactoryRepository factoryRepository; + @Mock private MaterialRepository materialRepository; // read-only면 MaterialReadRepository로 바꿔도 됨 + @Mock private FactoryMaterialRepository factoryMaterialRepository; + + @InjectMocks private FactoryService factoryService; + + @Test + @DisplayName("공장 생성 시 모든 자재가 수량 0으로 연결된다") + void createFactory_ShouldCreateFactoryWithAllMaterialsQuantityZero() { + // Given + var requestDto = new FactoryCreateRequestDto("테스트 공장", "테스트 위치"); + var factory = Factory.builder().id(1L).name("테스트 공장").location("테스트 위치").build(); + + // Material은 읽기 전용 → mock으로 id만 스텁 + Material material1 = mock(Material.class); + Material material2 = mock(Material.class); + when(material1.getId()).thenReturn(1L); + when(material2.getId()).thenReturn(2L); + + when(factoryRepository.save(any(Factory.class))).thenReturn(factory); + when(materialRepository.findAll()).thenReturn(List.of(material1, material2)); + + // When + FactoryResponseDto responseDto = factoryService.createFactory(requestDto); + + // Then + assertThat(responseDto).isNotNull(); + assertThat(responseDto.getId()).isEqualTo(1L); + + // 저장된 FactoryMaterial들의 실제 값 검증 + var captor = ArgumentCaptor.forClass(FactoryMaterial.class); + verify(factoryMaterialRepository, times(2)).save(captor.capture()); + var saved = captor.getAllValues(); + + assertThat(saved).hasSize(2); + assertThat(saved).allSatisfy(fm -> { + assertThat(fm.getFactory().getId()).isEqualTo(1L); + assertThat(fm.getQuantity()).isZero(); + }); + assertThat(saved.stream().map(FactoryMaterial::getMaterialId)) + .containsExactlyInAnyOrder(1L, 2L); + } +} \ No newline at end of file From 15edb2ea0f3ca0761ace2994b50d07fb79a550f1 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Wed, 15 Oct 2025 11:27:25 +0900 Subject: [PATCH 08/29] =?UTF-8?q?[FEAT]=20=EA=B3=B5=EC=9E=A5=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EC=9E=85=EB=A0=A5=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../factory/api/factory/controller/FactoryController.java | 3 ++- .../factory/api/factory/dto/FactoryCreateRequestDto.java | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java b/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java index a923fa1..401ca13 100644 --- a/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java +++ b/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java @@ -7,6 +7,7 @@ import com.sampoom.factory.common.response.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -21,7 +22,7 @@ public class FactoryController { @Operation(summary = "공장 생성", description = "공장을 생성합니다.") @PostMapping - public ResponseEntity> createFactory(@RequestBody FactoryCreateRequestDto requestDto) { + public ResponseEntity> createFactory(@Valid @RequestBody FactoryCreateRequestDto requestDto) { FactoryResponseDto responseDto = factoryService.createFactory(requestDto); return ApiResponse.success(SuccessStatus.CREATED,responseDto); } diff --git a/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java b/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java index af4074b..836203f 100644 --- a/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java +++ b/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java @@ -1,6 +1,7 @@ package com.sampoom.factory.api.factory.dto; import com.sampoom.factory.api.factory.entity.Factory; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,7 +10,11 @@ @NoArgsConstructor @AllArgsConstructor public class FactoryCreateRequestDto { + + @NotBlank(message = "공장 이름은 필수입니다") private String name; + + @NotBlank(message = "공장 위치는 필수입니다") private String location; public Factory toEntity() { From 3961874c415250bfc3cb2cbbbb5fd81ff635e505 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Wed, 15 Oct 2025 14:49:53 +0900 Subject: [PATCH 09/29] =?UTF-8?q?[FIX]=20=EA=B3=B5=EC=9E=A5=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=20=EA=B4=80=EB=A0=A8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../factory/api/factory/service/FactoryService.java | 2 +- .../factory/api/material/entity/FactoryMaterial.java | 6 ++---- .../factory/api/material/entity/MaterialOrder.java | 3 +-- .../factory/api/material/entity/MaterialOrderItem.java | 7 +++---- .../api/material/service/MaterialOrderService.java | 8 ++++---- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java b/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java index 95afe70..65a5a0f 100644 --- a/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java +++ b/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java @@ -35,7 +35,7 @@ public FactoryResponseDto createFactory(FactoryCreateRequestDto requestDto) { for (Material material : allMaterials) { FactoryMaterial factoryMaterial = FactoryMaterial.builder() .factory(factory) - .materialId(material.getId()) + .material(material) .quantity(0L) .build(); diff --git a/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java index 4dfe6c2..d30b455 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java @@ -24,12 +24,10 @@ public class FactoryMaterial { @JoinColumn(name = "factory_id") private Factory factory; - @Column(name = "material_id", nullable = false) - private Long materialId; // 실제 DB에 저장되는 FK 값 @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "material_id", insertable = false, updatable = false) - private Material material; // 읽기 전용 뷰 + @JoinColumn(name = "material_id") + private Material material; private Long quantity; diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java index ab87458..02bee1e 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java @@ -34,12 +34,11 @@ public class MaterialOrder { @JoinColumn(name = "factory_id") private Factory factory; - public MaterialOrder receive() { + public void receive() { if (this.status != OrderStatus.ORDERED) { throw new BadRequestException(ErrorStatus.ORDER_ALREADY_PROCESSED); } this.status = OrderStatus.RECEIVED; this.receivedAt = LocalDateTime.now(); - return this; } } diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java index aad2e60..04efe1b 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java @@ -24,12 +24,11 @@ public class MaterialOrderItem { @JoinColumn(name = "material_order_id") private MaterialOrder materialOrder; - @Column(name = "material_id", nullable = false) - private Long materialId; // 실제 DB에 저장되는 FK 값 + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "material_id", insertable = false, updatable = false) - private Material material; // 읽기 전용 뷰 + @JoinColumn(name = "material_id") + private Material material; } \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java index 7c1b27a..c4b22d6 100644 --- a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java +++ b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java @@ -58,7 +58,7 @@ public MaterialOrderResponseDto createMaterialOrder(Long factoryId, MaterialOrde return MaterialOrderItem.builder() .materialOrder(order) - .materialId(item.getMaterialId()) + .material(material) .quantity(item.getQuantity()) .build(); }) @@ -111,17 +111,17 @@ public MaterialOrderResponseDto receiveMaterialOrder(Long factoryId, Long orderI // 각 주문 아이템에 대해 공장 자재 수량 증가 for (MaterialOrderItem item : items) { - Long materialId = item.getMaterialId(); + Material material = item.getMaterial(); Long quantity = item.getQuantity(); // 해당 공장의 자재 찾기 FactoryMaterial factoryMaterial = factoryMaterialRepository.findByFactoryIdAndMaterialId( - factoryId, materialId) + factoryId, material.getId()) .orElseGet(() -> { // 없으면 새로 생성 FactoryMaterial newMaterial = FactoryMaterial.builder() .factory(order.getFactory()) - .materialId(materialId) + .material(material) .quantity(0L) .build(); return factoryMaterialRepository.save(newMaterial); From 8643ae86f78a89fdc3d2eb82ef2e445bfcbd501e Mon Sep 17 00:00:00 2001 From: Choosla Date: Wed, 15 Oct 2025 15:48:47 +0900 Subject: [PATCH 10/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 928e790..d2410b3 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -2,7 +2,7 @@ name: PR Reminder on: schedule: - - cron: "47 23,4,7,8, 10 * * *" # 아침 8시 47분, 오후 2시 47분, 오후 4시 47분, 오후 5시 47분, 오후 7시 47분 에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) + - cron: "47 23,4,7,8,10 * * *" # 아침 8시 47분, 오후 2시 47분, 오후 4시 47분, 오후 5시 47분, 오후 7시 47분 에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) workflow_dispatch: jobs: From d5dcfdbd0c828ee5f2ca03c423ed62af371683d7 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Wed, 15 Oct 2025 22:35:15 +0900 Subject: [PATCH 11/29] =?UTF-8?q?[FEAT]=20=EB=B6=80=ED=92=88=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../part/controller/CategoryController.java | 39 +++++++ .../api/part/dto/CategoryResponseDto.java | 25 +++++ .../api/part/dto/PartGroupResponseDto.java | 29 +++++ .../factory/api/part/entity/Category.java | 25 +++++ .../sampoom/factory/api/part/entity/Part.java | 28 +++++ .../factory/api/part/entity/PartGroup.java | 26 +++++ .../part/repository/CategoryRepository.java | 7 ++ .../part/repository/PartGroupRepository.java | 11 ++ .../api/part/repository/PartRepository.java | 7 ++ .../api/part/service/CategoryService.java | 49 +++++++++ .../controller/CategoryControllerTest.java | 101 ++++++++++++++++++ 11 files changed, 347 insertions(+) create mode 100644 src/main/java/com/sampoom/factory/api/part/controller/CategoryController.java create mode 100644 src/main/java/com/sampoom/factory/api/part/dto/CategoryResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/part/dto/PartGroupResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/part/entity/Category.java create mode 100644 src/main/java/com/sampoom/factory/api/part/entity/Part.java create mode 100644 src/main/java/com/sampoom/factory/api/part/entity/PartGroup.java create mode 100644 src/main/java/com/sampoom/factory/api/part/repository/CategoryRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/part/repository/PartGroupRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/part/repository/PartRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/part/service/CategoryService.java create mode 100644 src/test/java/com/sampoom/factory/api/part/controller/CategoryControllerTest.java diff --git a/src/main/java/com/sampoom/factory/api/part/controller/CategoryController.java b/src/main/java/com/sampoom/factory/api/part/controller/CategoryController.java new file mode 100644 index 0000000..4fe9388 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/controller/CategoryController.java @@ -0,0 +1,39 @@ +package com.sampoom.factory.api.part.controller; + +import com.sampoom.factory.api.part.dto.CategoryResponseDto; +import com.sampoom.factory.api.part.dto.PartGroupResponseDto; +import com.sampoom.factory.api.part.service.CategoryService; +import com.sampoom.factory.common.response.ApiResponse; +import com.sampoom.factory.common.response.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "Category", description = "카테고리 관련 API 입니다.") +@RestController +@RequestMapping("/categories") +@RequiredArgsConstructor +public class CategoryController { + + private final CategoryService categoryService; + + @Operation(summary = "카테고리 목록 조회", description = "전체 카테고리 목록을 조회합니다.") + @GetMapping + public ResponseEntity>> getAllCategories() { + return ApiResponse.success(SuccessStatus.OK, categoryService.getAllCategories()); + } + + @Operation(summary = "카테고리별 그룹 조회", description = "특정 카테고리에 속한 그룹 목록을 조회합니다.") + @GetMapping("/{categoryId}/groups") + public ResponseEntity>> getGroupsByCategory( + @PathVariable Long categoryId) { + return ApiResponse.success(SuccessStatus.OK, categoryService.getGroupsByCategory(categoryId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/part/dto/CategoryResponseDto.java b/src/main/java/com/sampoom/factory/api/part/dto/CategoryResponseDto.java new file mode 100644 index 0000000..fa2ee0b --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/dto/CategoryResponseDto.java @@ -0,0 +1,25 @@ +package com.sampoom.factory.api.part.dto; + +import com.sampoom.factory.api.part.entity.Category; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CategoryResponseDto { + private Long id; + private String code; + private String name; + + public static CategoryResponseDto from(Category category) { + return CategoryResponseDto.builder() + .id(category.getId()) + .code(category.getCode()) + .name(category.getName()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/part/dto/PartGroupResponseDto.java b/src/main/java/com/sampoom/factory/api/part/dto/PartGroupResponseDto.java new file mode 100644 index 0000000..363e719 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/dto/PartGroupResponseDto.java @@ -0,0 +1,29 @@ +package com.sampoom.factory.api.part.dto; + +import com.sampoom.factory.api.part.entity.PartGroup; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PartGroupResponseDto { + private Long id; + private String code; + private String name; + private Long categoryId; + private String categoryName; + + public static PartGroupResponseDto from(PartGroup partGroup) { + return PartGroupResponseDto.builder() + .id(partGroup.getId()) + .code(partGroup.getCode()) + .name(partGroup.getName()) + .categoryId(partGroup.getCategory().getId()) + .categoryName(partGroup.getCategory().getName()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/part/entity/Category.java b/src/main/java/com/sampoom/factory/api/part/entity/Category.java new file mode 100644 index 0000000..398672b --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/entity/Category.java @@ -0,0 +1,25 @@ +package com.sampoom.factory.api.part.entity; + +import com.sampoom.factory.common.entitiy.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Immutable; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "category") +@Getter +@NoArgsConstructor +public class Category extends BaseTimeEntity{ + + @Id + private Long id; + + private String code; + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/part/entity/Part.java b/src/main/java/com/sampoom/factory/api/part/entity/Part.java new file mode 100644 index 0000000..b8b348c --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/entity/Part.java @@ -0,0 +1,28 @@ +package com.sampoom.factory.api.part.entity; + +import com.sampoom.factory.common.entitiy.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Immutable; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "part") +@Getter +@NoArgsConstructor +@Immutable +public class Part extends BaseTimeEntity{ + + @Id + private Long id; + + private String code; + private String name; + private String status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_id") + private PartGroup group; +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/part/entity/PartGroup.java b/src/main/java/com/sampoom/factory/api/part/entity/PartGroup.java new file mode 100644 index 0000000..5791aff --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/entity/PartGroup.java @@ -0,0 +1,26 @@ +package com.sampoom.factory.api.part.entity; + +import com.sampoom.factory.common.entitiy.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Immutable; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "part_group") +@Getter +@NoArgsConstructor +public class PartGroup extends BaseTimeEntity { + + @Id + private Long id; + + private String code; + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + private Category category; +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/part/repository/CategoryRepository.java b/src/main/java/com/sampoom/factory/api/part/repository/CategoryRepository.java new file mode 100644 index 0000000..ff1ed98 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/repository/CategoryRepository.java @@ -0,0 +1,7 @@ +package com.sampoom.factory.api.part.repository; + +import com.sampoom.factory.api.part.entity.Category; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CategoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/sampoom/factory/api/part/repository/PartGroupRepository.java b/src/main/java/com/sampoom/factory/api/part/repository/PartGroupRepository.java new file mode 100644 index 0000000..853e0f2 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/repository/PartGroupRepository.java @@ -0,0 +1,11 @@ +package com.sampoom.factory.api.part.repository; + +import com.sampoom.factory.api.part.entity.Category; +import com.sampoom.factory.api.part.entity.PartGroup; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PartGroupRepository extends JpaRepository { + List findByCategory(Category category); +} diff --git a/src/main/java/com/sampoom/factory/api/part/repository/PartRepository.java b/src/main/java/com/sampoom/factory/api/part/repository/PartRepository.java new file mode 100644 index 0000000..b152e07 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/repository/PartRepository.java @@ -0,0 +1,7 @@ +package com.sampoom.factory.api.part.repository; + +import com.sampoom.factory.api.part.entity.Part; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PartRepository extends JpaRepository { +} diff --git a/src/main/java/com/sampoom/factory/api/part/service/CategoryService.java b/src/main/java/com/sampoom/factory/api/part/service/CategoryService.java new file mode 100644 index 0000000..137c70f --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/part/service/CategoryService.java @@ -0,0 +1,49 @@ +package com.sampoom.factory.api.part.service; + +import com.sampoom.factory.api.part.dto.CategoryResponseDto; +import com.sampoom.factory.api.part.dto.PartGroupResponseDto; +import com.sampoom.factory.api.part.entity.Category; +import com.sampoom.factory.api.part.repository.CategoryRepository; +import com.sampoom.factory.api.part.repository.PartGroupRepository; +import com.sampoom.factory.common.exception.NotFoundException; +import com.sampoom.factory.common.response.ApiResponse; +import com.sampoom.factory.common.response.ErrorStatus; +import com.sampoom.factory.common.response.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CategoryService { + + private final CategoryRepository categoryRepository; + private final PartGroupRepository partGroupRepository; + + @Transactional(readOnly = true) + public List getAllCategories() { + return categoryRepository.findAll().stream() + .map(CategoryResponseDto::from) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public List getGroupsByCategory(Long categoryId) { + Category category = categoryRepository.findById(categoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.CATEGORY_NOT_FOUND)); + + return partGroupRepository.findByCategory(category).stream() + .map(PartGroupResponseDto::from) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/com/sampoom/factory/api/part/controller/CategoryControllerTest.java b/src/test/java/com/sampoom/factory/api/part/controller/CategoryControllerTest.java new file mode 100644 index 0000000..7bab4bf --- /dev/null +++ b/src/test/java/com/sampoom/factory/api/part/controller/CategoryControllerTest.java @@ -0,0 +1,101 @@ +package com.sampoom.factory.api.part.controller; + +import com.sampoom.factory.api.part.dto.CategoryResponseDto; +import com.sampoom.factory.api.part.dto.PartGroupResponseDto; +import com.sampoom.factory.api.part.service.CategoryService; +import com.sampoom.factory.common.response.SuccessStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; + +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(CategoryController.class) +public class CategoryControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private CategoryService categoryService; + + @Test + @DisplayName("모든 카테고리 조회 테스트") + void getAllCategoriesTest() throws Exception { + // given + List categories = Arrays.asList( + CategoryResponseDto.builder().id(1L).code("CAT001").name("전자부품").build(), + CategoryResponseDto.builder().id(2L).code("CAT002").name("기계부품").build() + ); + + when(categoryService.getAllCategories()).thenReturn(categories); + + // when & then + mockMvc.perform(get("/categories") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(SuccessStatus.OK.getMessage())) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data[0].id").value(1)) + .andExpect(jsonPath("$.data[0].code").value("CAT001")) + .andExpect(jsonPath("$.data[0].name").value("전자부품")) + .andExpect(jsonPath("$.data[1].id").value(2)) + .andExpect(jsonPath("$.data[1].code").value("CAT002")) + .andExpect(jsonPath("$.data[1].name").value("기계부품")); + } + + @Test + @DisplayName("카테고리별 그룹 조회 테스트") + void getGroupsByCategoryTest() throws Exception { + // given + Long categoryId = 1L; + List groups = Arrays.asList( + PartGroupResponseDto.builder() + .id(1L) + .code("GRP001") + .name("반도체") + .categoryId(categoryId) + .categoryName("전자부품") + .build(), + PartGroupResponseDto.builder() + .id(2L) + .code("GRP002") + .name("저항") + .categoryId(categoryId) + .categoryName("전자부품") + .build() + ); + + when(categoryService.getGroupsByCategory(categoryId)).thenReturn(groups); + + // when & then + mockMvc.perform(get("/categories/{categoryId}/groups", categoryId) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(SuccessStatus.OK.getMessage())) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data[0].id").value(1)) + .andExpect(jsonPath("$.data[0].code").value("GRP001")) + .andExpect(jsonPath("$.data[0].name").value("반도체")) + .andExpect(jsonPath("$.data[0].categoryId").value(categoryId)) + .andExpect(jsonPath("$.data[0].categoryName").value("전자부품")) + .andExpect(jsonPath("$.data[1].id").value(2)) + .andExpect(jsonPath("$.data[1].code").value("GRP002")) + .andExpect(jsonPath("$.data[1].name").value("저항")); + } +} \ No newline at end of file From 3aacd6fca5463ce50f6832b2f063cf552ae9b51c Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Wed, 15 Oct 2025 22:36:34 +0900 Subject: [PATCH 12/29] =?UTF-8?q?[FIX]=20=EA=B3=B5=EC=9E=A5=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=90=EC=9E=AC=20=EA=B4=80=EB=A0=A8=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sampoom/factory/api/factory/entity/Factory.java | 7 ++----- .../factory/api/material/entity/FactoryMaterial.java | 7 ++----- .../sampoom/factory/api/material/entity/MaterialOrder.java | 7 ++----- .../factory/api/material/entity/MaterialOrderItem.java | 7 ++----- .../factory/api/factory/service/FactoryServiceTest.java | 2 +- 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java b/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java index d301b70..761bc04 100644 --- a/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java +++ b/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java @@ -2,15 +2,12 @@ import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Table(name = "factory") @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class Factory { diff --git a/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java index d30b455..b895045 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java @@ -2,15 +2,12 @@ import com.sampoom.factory.api.factory.entity.Factory; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Table(name = "factory_material") @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class FactoryMaterial { diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java index 02bee1e..a44fd1d 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java @@ -4,17 +4,14 @@ import com.sampoom.factory.common.exception.BadRequestException; import com.sampoom.factory.common.response.ErrorStatus; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.time.LocalDateTime; @Entity @Getter @Table(name = "material_order") -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class MaterialOrder { diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java index 04efe1b..bdb26a8 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java @@ -1,15 +1,12 @@ package com.sampoom.factory.api.material.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Getter @Table(name = "material_order_item") -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class MaterialOrderItem { diff --git a/src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java b/src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java index ef6ab76..a08a4a4 100644 --- a/src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java +++ b/src/test/java/com/sampoom/factory/api/factory/service/FactoryServiceTest.java @@ -68,7 +68,7 @@ void createFactory_ShouldCreateFactoryWithAllMaterialsQuantityZero() { assertThat(fm.getFactory().getId()).isEqualTo(1L); assertThat(fm.getQuantity()).isZero(); }); - assertThat(saved.stream().map(FactoryMaterial::getMaterialId)) + assertThat(saved.stream().map(FactoryMaterial::getMaterial).map(Material::getId).toList()) .containsExactlyInAnyOrder(1L, 2L); } } \ No newline at end of file From f8443cdfc4fd0048901d448da0a89fe18d5a9613 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Wed, 15 Oct 2025 22:37:01 +0900 Subject: [PATCH 13/29] =?UTF-8?q?[FEAT]=20BOM=20=EA=B4=80=EB=A0=A8=20api?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/bom/controller/BomController.java | 72 ++++++++ .../api/bom/dto/BomDetailResponseDto.java | 59 +++++++ .../factory/api/bom/dto/BomMaterialDto.java | 29 ++++ .../factory/api/bom/dto/BomRequestDto.java | 26 +++ .../factory/api/bom/dto/BomResponseDto.java | 40 +++++ .../sampoom/factory/api/bom/entity/Bom.java | 47 ++++++ .../factory/api/bom/entity/BomMaterial.java | 38 +++++ .../bom/repository/BomMaterialRepository.java | 7 + .../api/bom/repository/BomRepository.java | 32 ++++ .../factory/api/bom/service/BomService.java | 133 +++++++++++++++ .../common/config/JpaAuditingConfig.java | 10 ++ .../factory/common/response/ErrorStatus.java | 2 + .../api/bom/controller/BomControllerTest.java | 156 ++++++++++++++++++ 13 files changed, 651 insertions(+) create mode 100644 src/main/java/com/sampoom/factory/api/bom/controller/BomController.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/dto/BomDetailResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/dto/BomMaterialDto.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/dto/BomRequestDto.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/dto/BomResponseDto.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/entity/Bom.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/entity/BomMaterial.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/repository/BomMaterialRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/repository/BomRepository.java create mode 100644 src/main/java/com/sampoom/factory/api/bom/service/BomService.java create mode 100644 src/main/java/com/sampoom/factory/common/config/JpaAuditingConfig.java create mode 100644 src/test/java/com/sampoom/factory/api/bom/controller/BomControllerTest.java diff --git a/src/main/java/com/sampoom/factory/api/bom/controller/BomController.java b/src/main/java/com/sampoom/factory/api/bom/controller/BomController.java new file mode 100644 index 0000000..1b4d20f --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/controller/BomController.java @@ -0,0 +1,72 @@ +package com.sampoom.factory.api.bom.controller; + +import com.sampoom.factory.api.bom.dto.BomDetailResponseDto; +import com.sampoom.factory.api.bom.dto.BomRequestDto; +import com.sampoom.factory.api.bom.dto.BomResponseDto; +import com.sampoom.factory.api.bom.service.BomService; +import com.sampoom.factory.common.response.ApiResponse; +import com.sampoom.factory.common.response.PageResponseDto; +import com.sampoom.factory.common.response.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "BOM", description = "BOM(Bill of Materials) 관련 API 입니다.") +@RestController +@RequestMapping("/bom") +@RequiredArgsConstructor +public class BomController { + + private final BomService bomService; + + @Operation(summary = "BOM 추가", description = "새로운 BOM을 등록합니다.") + @PostMapping + public ResponseEntity> createBom(@RequestBody BomRequestDto requestDto) { + return ApiResponse.success(SuccessStatus.CREATED, bomService.createBom(requestDto)); + } + + @Operation(summary = "BOM 목록 조회", description = "페이징 처리된 BOM 목록을 조회하고, 카테고리나 그룹으로 필터링합니다.") + @GetMapping + public ResponseEntity>> getBoms( + @RequestParam(required = false) Long categoryId, + @RequestParam(required = false) Long groupId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return ApiResponse.success(SuccessStatus.OK, bomService.searchBoms(null, categoryId, groupId, page, size)); + } + + @Operation(summary = "BOM 상세 조회", description = "특정 BOM의 상세 정보를 조회합니다.") + @GetMapping("/{bomId}") + public ResponseEntity> getBomDetail(@PathVariable Long bomId) { + return ApiResponse.success(SuccessStatus.OK, bomService.getBomDetail(bomId)); + } + + @Operation(summary = "BOM 수정", description = "특정 BOM 정보를 수정합니다.") + @PutMapping("/{bomId}") + public ResponseEntity> updateBom( + @PathVariable Long bomId, + @RequestBody BomRequestDto requestDto) { + return ApiResponse.success(SuccessStatus.OK, bomService.updateBom(bomId, requestDto)); + } + + @Operation(summary = "BOM 삭제", description = "특정 BOM을 삭제합니다.") + @DeleteMapping("/{bomId}") + public ResponseEntity> deleteBom(@PathVariable Long bomId) { + bomService.deleteBom(bomId); + return ApiResponse.success_only(SuccessStatus.OK); + } + + @Operation(summary = "BOM 검색", description = "부품 이름 또는 부품 코드로 BOM을 검색하고, 카테고리나 그룹으로 필터링합니다.") + @GetMapping("/search") + public ResponseEntity>> searchBoms( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long categoryId, + @RequestParam(required = false) Long groupId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return ApiResponse.success(SuccessStatus.OK, + bomService.searchBoms(keyword, categoryId, groupId, page, size)); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/bom/dto/BomDetailResponseDto.java b/src/main/java/com/sampoom/factory/api/bom/dto/BomDetailResponseDto.java new file mode 100644 index 0000000..ae02bb3 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/dto/BomDetailResponseDto.java @@ -0,0 +1,59 @@ +package com.sampoom.factory.api.bom.dto; + +import com.sampoom.factory.api.bom.entity.Bom; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BomDetailResponseDto { + private Long id; + private String partName; + private String partCode; + private Long partId; + private List materials; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class BomMaterialDto { + private Long id; + private Long materialId; + private String materialName; + private String materialCode; + private Long quantity; + } + + public static BomDetailResponseDto from(Bom bom) { + List materialDtos = bom.getMaterials().stream() + .map(material -> BomMaterialDto.builder() + .id(material.getId()) + .materialId(material.getMaterial().getId()) + .materialName(material.getMaterial().getName()) + .materialCode(material.getMaterial().getCode()) + .quantity(material.getQuantity()) + .build()) + .collect(Collectors.toList()); + + return BomDetailResponseDto.builder() + .id(bom.getId()) + .partId(bom.getPart().getId()) + .partName(bom.getPart().getName()) + .partCode(bom.getPart().getCode()) + .materials(materialDtos) + .createdAt(bom.getCreatedAt()) + .updatedAt(bom.getUpdatedAt()) + .build(); + } +} diff --git a/src/main/java/com/sampoom/factory/api/bom/dto/BomMaterialDto.java b/src/main/java/com/sampoom/factory/api/bom/dto/BomMaterialDto.java new file mode 100644 index 0000000..f542c4e --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/dto/BomMaterialDto.java @@ -0,0 +1,29 @@ +package com.sampoom.factory.api.bom.dto; + +import com.sampoom.factory.api.bom.entity.BomMaterial; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BomMaterialDto { + private Long id; // BomMaterial 엔티티의 ID + private Long materialId; // Material 엔티티의 ID + private String materialName; // 자재명 + private String materialCode; // 자재 코드 + private Long quantity; // 수량 + + public static BomMaterialDto from(BomMaterial material) { + return BomMaterialDto.builder() + .id(material.getId()) + .materialId(material.getMaterial().getId()) + .materialName(material.getMaterial().getName()) + .materialCode(material.getMaterial().getCode()) + .quantity(material.getQuantity()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/bom/dto/BomRequestDto.java b/src/main/java/com/sampoom/factory/api/bom/dto/BomRequestDto.java new file mode 100644 index 0000000..a7ab3e5 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/dto/BomRequestDto.java @@ -0,0 +1,26 @@ +package com.sampoom.factory.api.bom.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BomRequestDto { + private Long partId; + private List materials; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class BomMaterialDto { + private Long materialId; + private Long quantity; + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/bom/dto/BomResponseDto.java b/src/main/java/com/sampoom/factory/api/bom/dto/BomResponseDto.java new file mode 100644 index 0000000..76e8b3d --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/dto/BomResponseDto.java @@ -0,0 +1,40 @@ +package com.sampoom.factory.api.bom.dto; + + +import com.sampoom.factory.api.bom.entity.Bom; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BomResponseDto { + private Long id; + private String partName; + private String partCode; + private Long partId; + private List materials; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static BomResponseDto from(Bom bom) { + return BomResponseDto.builder() + .id(bom.getId()) + .partId(bom.getPart().getId()) + .partName(bom.getPart().getName()) + .partCode(bom.getPart().getCode()) + .materials(bom.getMaterials().stream() + .map(BomMaterialDto::from) + .collect(Collectors.toList())) + .createdAt(bom.getCreatedAt()) + .updatedAt(bom.getUpdatedAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/bom/entity/Bom.java b/src/main/java/com/sampoom/factory/api/bom/entity/Bom.java new file mode 100644 index 0000000..a65ab92 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/entity/Bom.java @@ -0,0 +1,47 @@ +package com.sampoom.factory.api.bom.entity; + +import com.sampoom.factory.api.part.entity.Part; +import com.sampoom.factory.common.entitiy.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "bom") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Bom extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "bom_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "part_id") + private Part part; + + @OneToMany(mappedBy = "bom", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List materials = new ArrayList<>(); + + public void addMaterial(BomMaterial bomMaterial) { + this.materials.add(bomMaterial); + + if (bomMaterial.getBom() != this) { + bomMaterial.updateBom(this); + } + } + + public void touchNow() { this.updatedAt = LocalDateTime.now(); } + + + +} + + diff --git a/src/main/java/com/sampoom/factory/api/bom/entity/BomMaterial.java b/src/main/java/com/sampoom/factory/api/bom/entity/BomMaterial.java new file mode 100644 index 0000000..999fa34 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/entity/BomMaterial.java @@ -0,0 +1,38 @@ +package com.sampoom.factory.api.bom.entity; + +import com.sampoom.factory.api.material.entity.Material; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "bom_material") // 실제 테이블명이 'BOM-자재'가 아니라면 명확히 지정 +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class BomMaterial { + + @Id + @Column(name = "bom_material_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "bom_id", nullable = false) + private Bom bom; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "material_id", nullable = false) + private Material material; + + private Long quantity; + + public void updateBom(Bom bom) { + this.bom = bom; + } + + + +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/bom/repository/BomMaterialRepository.java b/src/main/java/com/sampoom/factory/api/bom/repository/BomMaterialRepository.java new file mode 100644 index 0000000..1300af1 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/repository/BomMaterialRepository.java @@ -0,0 +1,7 @@ +package com.sampoom.factory.api.bom.repository; + +import com.sampoom.factory.api.bom.entity.BomMaterial; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BomMaterialRepository extends JpaRepository { +} diff --git a/src/main/java/com/sampoom/factory/api/bom/repository/BomRepository.java b/src/main/java/com/sampoom/factory/api/bom/repository/BomRepository.java new file mode 100644 index 0000000..68461d8 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/repository/BomRepository.java @@ -0,0 +1,32 @@ +package com.sampoom.factory.api.bom.repository; + +import com.sampoom.factory.api.bom.entity.Bom; +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; + +public interface BomRepository extends JpaRepository { + @Query(""" +SELECT b FROM Bom b +JOIN b.part p +JOIN p.group g +JOIN g.category c +WHERE ( + COALESCE(:keyword, '') = '' + OR p.name ILIKE CONCAT('%', :keyword, '%') + OR p.code ILIKE CONCAT('%', :keyword, '%') +) +AND (:categoryId IS NULL OR c.id = :categoryId) +AND (:groupId IS NULL OR g.id = :groupId) +ORDER BY b.createdAt DESC +""") + Page findByFilters( + @Param("keyword") String keyword, + @Param("categoryId") Long categoryId, + @Param("groupId") Long groupId, + Pageable pageable); + + +} diff --git a/src/main/java/com/sampoom/factory/api/bom/service/BomService.java b/src/main/java/com/sampoom/factory/api/bom/service/BomService.java new file mode 100644 index 0000000..345a824 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/service/BomService.java @@ -0,0 +1,133 @@ +package com.sampoom.factory.api.bom.service; + +import com.sampoom.factory.api.bom.dto.BomDetailResponseDto; +import com.sampoom.factory.api.bom.dto.BomRequestDto; +import com.sampoom.factory.api.bom.dto.BomResponseDto; +import com.sampoom.factory.api.bom.entity.Bom; +import com.sampoom.factory.api.bom.entity.BomMaterial; +import com.sampoom.factory.api.bom.repository.BomRepository; +import com.sampoom.factory.api.material.entity.Material; +import com.sampoom.factory.api.material.repository.MaterialRepository; +import com.sampoom.factory.api.part.entity.Part; +import com.sampoom.factory.api.part.repository.PartRepository; +import com.sampoom.factory.common.exception.NotFoundException; +import com.sampoom.factory.common.response.ErrorStatus; +import com.sampoom.factory.common.response.PageResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BomService { + private final BomRepository bomRepository; + private final PartRepository partRepository; + private final MaterialRepository materialRepository; + + + @Transactional + public BomResponseDto createBom(BomRequestDto requestDto) { + Part part = partRepository.findById(requestDto.getPartId()) + .orElseThrow(() -> new NotFoundException(ErrorStatus.PART_NOT_FOUND)); + + Bom bom = Bom.builder() + .part(part) + .materials(new ArrayList<>()) + .build(); + + for (BomRequestDto.BomMaterialDto materialDto : requestDto.getMaterials()) { + Material material = materialRepository.findById(materialDto.getMaterialId()) + .orElseThrow(() -> new NotFoundException(ErrorStatus.MATERIAL_NOT_FOUND)); + + BomMaterial bomMaterial = BomMaterial.builder() + .bom(bom) + .material(material) + .quantity(materialDto.getQuantity()) + .build(); + + bom.addMaterial(bomMaterial); + } + + return BomResponseDto.from(bomRepository.save(bom)); + } + + + public PageResponseDto getBoms(int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page bomPage = bomRepository.findAll(pageable); + + return PageResponseDto.builder() + .content(bomPage.getContent().stream() + .map(BomResponseDto::from) + .collect(Collectors.toList())) + .totalPages(bomPage.getTotalPages()) + .totalElements(bomPage.getTotalElements()) + .build(); + } + + + public BomDetailResponseDto getBomDetail(Long bomId) { + Bom bom = bomRepository.findById(bomId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.BOM_NOT_FOUND)); + + return BomDetailResponseDto.from(bom); + } + + + @Transactional + public BomResponseDto updateBom(Long bomId, BomRequestDto requestDto) { + Bom bom = bomRepository.findById(bomId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.BOM_NOT_FOUND)); + + // 기존 자재 삭제 + bom.getMaterials().clear(); + + // 새 자재 추가 + for (BomRequestDto.BomMaterialDto materialDto : requestDto.getMaterials()) { + Material material = materialRepository.findById(materialDto.getMaterialId()) + .orElseThrow(() -> new NotFoundException(ErrorStatus.MATERIAL_NOT_FOUND)); + + BomMaterial bomMaterial = BomMaterial.builder() + .bom(bom) + .material(material) + .quantity(materialDto.getQuantity()) + .build(); + + bom.addMaterial(bomMaterial); + } + bom.touchNow(); + bomRepository.save(bom); + + return BomResponseDto.from(bom); + } + + + @Transactional + public void deleteBom(Long bomId) { + Bom bom = bomRepository.findById(bomId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.BOM_NOT_FOUND)); + + bomRepository.delete(bom); + } + + + public PageResponseDto searchBoms(String keyword, Long categoryId, Long groupId, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page bomPage = bomRepository.findByFilters(keyword, categoryId, groupId, pageable); + + return PageResponseDto.builder() + .content(bomPage.getContent().stream() + .map(BomResponseDto::from) + .collect(Collectors.toList())) + .totalPages(bomPage.getTotalPages()) + .totalElements(bomPage.getTotalElements()) + .build(); + } +} diff --git a/src/main/java/com/sampoom/factory/common/config/JpaAuditingConfig.java b/src/main/java/com/sampoom/factory/common/config/JpaAuditingConfig.java new file mode 100644 index 0000000..a8f5816 --- /dev/null +++ b/src/main/java/com/sampoom/factory/common/config/JpaAuditingConfig.java @@ -0,0 +1,10 @@ +package com.sampoom.factory.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { + +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java b/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java index 47e87a3..61a4d9f 100644 --- a/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java +++ b/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java @@ -29,6 +29,8 @@ public enum ErrorStatus { CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다.", 40403), FACTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "공장을 찾을 수 없습니다.", 40404), ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다.", 40405), + PART_NOT_FOUND(HttpStatus.NOT_FOUND, "부품을 찾을 수 없습니다.", 40406), + BOM_NOT_FOUND(HttpStatus.NOT_FOUND, "BOM을 찾을 수 없습니다.", 40407), diff --git a/src/test/java/com/sampoom/factory/api/bom/controller/BomControllerTest.java b/src/test/java/com/sampoom/factory/api/bom/controller/BomControllerTest.java new file mode 100644 index 0000000..d616ada --- /dev/null +++ b/src/test/java/com/sampoom/factory/api/bom/controller/BomControllerTest.java @@ -0,0 +1,156 @@ +package com.sampoom.factory.api.bom.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sampoom.factory.api.bom.dto.BomDetailResponseDto; +import com.sampoom.factory.api.bom.dto.BomRequestDto; +import com.sampoom.factory.api.bom.dto.BomResponseDto; +import com.sampoom.factory.api.bom.service.BomService; +import com.sampoom.factory.common.response.PageResponseDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; + +import org.springframework.http.MediaType; + +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(BomController.class) +class BomControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private BomService bomService; + + @Test + @DisplayName("BOM 추가 테스트") + void createBom() throws Exception { + BomRequestDto requestDto = BomRequestDto.builder() + .partId(1L) + .materials(Collections.emptyList()) + .build(); + + BomResponseDto responseDto = BomResponseDto.builder() + .id(1L) + .partId(1L) + .partName("Part A") + + .build(); + + Mockito.when(bomService.createBom(any(BomRequestDto.class))).thenReturn(responseDto); + + mockMvc.perform(post("/bom") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.id").value(1L)) + .andExpect(jsonPath("$.data.partId").value(1L)) + .andExpect(jsonPath("$.data.partName").value("Part A")); + } + + @Test + @DisplayName("BOM 목록 조회 테스트") + void getBoms() throws Exception { + PageResponseDto pageResponse = PageResponseDto.builder() + .content(Collections.emptyList()) + .totalPages(1) + .totalElements(0) + .build(); + + Mockito.when(bomService.getBoms(anyInt(), anyInt())).thenReturn(pageResponse); + + mockMvc.perform(get("/bom") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()); + + } + + + @Test + @DisplayName("BOM 상세 조회 테스트") + void getBomDetail() throws Exception { + BomDetailResponseDto responseDto = BomDetailResponseDto.builder() + .id(1L) + .partId(1L) + .partName("Part A") + .materials(Collections.emptyList()) + .build(); + + Mockito.when(bomService.getBomDetail(anyLong())).thenReturn(responseDto); + + mockMvc.perform(get("/bom/{bomId}", 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(1L)) + .andExpect(jsonPath("$.data.partName").value("Part A")); + } + @Test + @DisplayName("BOM 수정 테스트") + void updateBom() throws Exception { + BomRequestDto requestDto = BomRequestDto.builder() + .partId(1L) + .materials(Collections.emptyList()) + .build(); + + BomResponseDto responseDto = BomResponseDto.builder() + .id(1L) + .partId(1L) + .partName("Updated Part") + + .build(); + + Mockito.when(bomService.updateBom(anyLong(), any(BomRequestDto.class))).thenReturn(responseDto); + + mockMvc.perform(put("/bom/{bomId}", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.partName").value("Updated Part")); + } + + @Test + @DisplayName("BOM 삭제 테스트") + void deleteBom() throws Exception { + mockMvc.perform(delete("/bom/{bomId}", 1L)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("BOM 검색 테스트") + void searchBoms() throws Exception { + PageResponseDto pageResponse = PageResponseDto.builder() + .content(Collections.emptyList()) + .totalPages(1) + .totalElements(0) + .build(); + + Mockito.when(bomService.searchBoms(any(), anyLong(), anyLong(), anyInt(), anyInt())).thenReturn(pageResponse); + + mockMvc.perform(get("/bom/search") + .param("keyword", "Part") + .param("partId", "1") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()); + + } +} \ No newline at end of file From 9a6d58464e56704682320a91714ad9d1cba02488 Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 09:04:48 +0900 Subject: [PATCH 14/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index d2410b3..c58e365 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -11,3 +11,5 @@ jobs: secrets: # 해당 시크릿은 조직의 시크릿에 저장되어 있음 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + # 조직 변수 전달 + SLACK_USER_MAP: ${{ vars.SLACK_USER_MAP }} From 481233b6dcafea6fa388b12494f1650dfbbe1ce4 Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 14:25:32 +0900 Subject: [PATCH 15/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index c58e365..e4b028b 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -2,14 +2,15 @@ name: PR Reminder on: schedule: - - cron: "47 23,4,7,8,10 * * *" # 아침 8시 47분, 오후 2시 47분, 오후 4시 47분, 오후 5시 47분, 오후 7시 47분 에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) + - cron: "47 23,4,7,8,10 * * *" workflow_dispatch: jobs: call-reusable-reminder: + secrets: inherit uses: 33-Auto/.github/.github/workflows/reusable-pr-reminder.yml@main - secrets: - # 해당 시크릿은 조직의 시크릿에 저장되어 있음 + with: + # 시크릿 전달 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - # 조직 변수 전달 - SLACK_USER_MAP: ${{ vars.SLACK_USER_MAP }} + # 변수 전달 + SLACK_USER_MAP: ${{ vars.SLACK_USER_MAP }} \ No newline at end of file From fe1462d0b24f25f5090a8e2989890636a2f2483d Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 14:33:08 +0900 Subject: [PATCH 16/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index e4b028b..8958a9c 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -7,10 +7,10 @@ on: jobs: call-reusable-reminder: - secrets: inherit uses: 33-Auto/.github/.github/workflows/reusable-pr-reminder.yml@main - with: - # 시크릿 전달 + + secrets: + # 조직 시크릿을 secrets 입력으로 전달 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - # 변수 전달 + # 조직 변수를 secrets 입력으로 전달 SLACK_USER_MAP: ${{ vars.SLACK_USER_MAP }} \ No newline at end of file From fa3cb6961af7270e08cc0193d0a1764f21798664 Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 14:36:25 +0900 Subject: [PATCH 17/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 8958a9c..25ba9eb 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -13,4 +13,4 @@ jobs: # 조직 시크릿을 secrets 입력으로 전달 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # 조직 변수를 secrets 입력으로 전달 - SLACK_USER_MAP: ${{ vars.SLACK_USER_MAP }} \ No newline at end of file + SLACK_USER_MAP: ${{ secrets.SLACK_USER_MAP }} \ No newline at end of file From 6ed3f84af8349105771799d67ee54174a2c41b5e Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 15:14:00 +0900 Subject: [PATCH 18/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 25ba9eb..311c71b 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -2,15 +2,12 @@ name: PR Reminder on: schedule: - - cron: "47 23,4,7,8,10 * * *" + - cron: "47 23,4,7,8,10 * * *" # 아침 8시 47분, 오후 2시 47분, 오후 4시 47분, 오후 5시 47분, 오후 7시 47분 에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) workflow_dispatch: jobs: call-reusable-reminder: uses: 33-Auto/.github/.github/workflows/reusable-pr-reminder.yml@main - secrets: - # 조직 시크릿을 secrets 입력으로 전달 - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - # 조직 변수를 secrets 입력으로 전달 - SLACK_USER_MAP: ${{ secrets.SLACK_USER_MAP }} \ No newline at end of file + # 해당 시크릿은 조직의 시크릿에 저장되어 있음 + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file From 5eab7dac3595e551e5b0af97b74c25d25a554e43 Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 15:22:30 +0900 Subject: [PATCH 19/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 311c71b..87dc299 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -10,4 +10,5 @@ jobs: uses: 33-Auto/.github/.github/workflows/reusable-pr-reminder.yml@main secrets: # 해당 시크릿은 조직의 시크릿에 저장되어 있음 - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_USER_MAP : ${{ vars.SLACK_USER_MAP }} \ No newline at end of file From ea12978f2efa40eca8c0e25b5c2fe88f2241e71e Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 15:25:43 +0900 Subject: [PATCH 20/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 87dc299..c666fcd 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -11,4 +11,4 @@ jobs: secrets: # 해당 시크릿은 조직의 시크릿에 저장되어 있음 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_USER_MAP : ${{ vars.SLACK_USER_MAP }} \ No newline at end of file + SLACK_USER_MAP : ${{ secrets.SLACK_USER_MAP }} \ No newline at end of file From c17461dc4b2deadf477d372cd71c1ee81c8fec25 Mon Sep 17 00:00:00 2001 From: Choosla Date: Thu, 16 Oct 2025 15:36:52 +0900 Subject: [PATCH 21/29] chore: Apply batch updates from central configuration --- .github/workflows/pr-reminder.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index c666fcd..007e757 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -1,14 +1,15 @@ -name: PR Reminder + name: PR Reminder -on: - schedule: - - cron: "47 23,4,7,8,10 * * *" # 아침 8시 47분, 오후 2시 47분, 오후 4시 47분, 오후 5시 47분, 오후 7시 47분 에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) - workflow_dispatch: + on: + schedule: + - cron: "47 23,4,7,8,10 * * *" # 아침 8시 47분, 오후 2시 47분, 오후 4시 47분, 오후 5시 47분, 오후 7시 47분 에 실행 (UTC 기준으로 설정해서 한국 시간에 맞춤) + workflow_dispatch: -jobs: - call-reusable-reminder: - uses: 33-Auto/.github/.github/workflows/reusable-pr-reminder.yml@main - secrets: - # 해당 시크릿은 조직의 시크릿에 저장되어 있음 - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_USER_MAP : ${{ secrets.SLACK_USER_MAP }} \ No newline at end of file + jobs: + call-reusable-reminder: + uses: 33-Auto/.github/.github/workflows/reusable-pr-reminder.yml@main + secrets: + # 해당 시크릿은 조직의 시크릿에 저장되어 있음 + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + with: + SLACK_USER_MAP: ${{ vars.SLACK_USER_MAP }} \ No newline at end of file From 90d916b6be8887efe03591772ff291774d108aaf Mon Sep 17 00:00:00 2001 From: taemin Kim Date: Thu, 16 Oct 2025 21:18:13 +0900 Subject: [PATCH 22/29] =?UTF-8?q?[FIX]=20bom=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/sampoom/factory/api/bom/entity/Bom.java | 2 +- .../java/com/sampoom/factory/api/bom/service/BomService.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/sampoom/factory/api/bom/entity/Bom.java b/src/main/java/com/sampoom/factory/api/bom/entity/Bom.java index a65ab92..c0b7dad 100644 --- a/src/main/java/com/sampoom/factory/api/bom/entity/Bom.java +++ b/src/main/java/com/sampoom/factory/api/bom/entity/Bom.java @@ -23,7 +23,7 @@ public class Bom extends BaseTimeEntity { private Long id; @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "part_id") + @JoinColumn(name = "part_id", unique = true) private Part part; @OneToMany(mappedBy = "bom", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/main/java/com/sampoom/factory/api/bom/service/BomService.java b/src/main/java/com/sampoom/factory/api/bom/service/BomService.java index 345a824..d63540a 100644 --- a/src/main/java/com/sampoom/factory/api/bom/service/BomService.java +++ b/src/main/java/com/sampoom/factory/api/bom/service/BomService.java @@ -103,7 +103,6 @@ public BomResponseDto updateBom(Long bomId, BomRequestDto requestDto) { bom.addMaterial(bomMaterial); } bom.touchNow(); - bomRepository.save(bom); return BomResponseDto.from(bom); } From b37ec3b6825670dcdee5568b2ee32dbd0e3f2f76 Mon Sep 17 00:00:00 2001 From: taemin Kim Date: Thu, 16 Oct 2025 23:25:36 +0900 Subject: [PATCH 23/29] =?UTF-8?q?[FEAT]=20=EC=9E=90=EC=9E=AC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D/=EA=B2=80=EC=83=89=20api=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=90=EC=9E=AC=20=EC=A3=BC=EB=AC=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C,=20=EC=B7=A8=EC=86=8C=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FactoryMaterialController.java | 40 +++++++++++++++++-- .../api/material/entity/MaterialOrder.java | 18 ++++++++- .../api/material/entity/OrderStatus.java | 3 +- .../repository/FactoryMaterialRepository.java | 35 ++++++++++++++++ .../repository/MaterialOrderRepository.java | 4 ++ .../service/FactoryMaterialService.java | 36 +++++++++++++++++ .../service/MaterialOrderService.java | 22 ++++++++++ .../common/entitiy/BaseTimeEntity.java | 2 + .../common/entitiy/SoftDeleteEntity.java | 22 ++++++++++ .../factory/common/response/ErrorStatus.java | 1 + 10 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/sampoom/factory/common/entitiy/SoftDeleteEntity.java diff --git a/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java index 3867b75..3eb21ac 100644 --- a/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java +++ b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java @@ -43,14 +43,23 @@ public ResponseEntity>> getMate factoryMaterialService.getMaterialsByFactoryAndCategory(factoryId, categoryId, page, size)); } - @Operation(summary = "공장별 자재 목록 조회", description = "특정 공장에 있는 모든 자재를 조회합니다.") + + @Operation( + summary = "공장별 자재 검색/목록 조회", + description = "특정 공장의 자재를 페이징 조회합니다. 카테고리(categoryId)로 필터링하고, keyword(자재명/자재코드)로 검색합니다." + ) @GetMapping("/{factoryId}/material") - public ResponseEntity>> getMaterialsByFactory( + public ResponseEntity>> getMaterials( @PathVariable Long factoryId, + @RequestParam(required = false) Long categoryId, + @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { - return ApiResponse.success(SuccessStatus.OK, - factoryMaterialService.getMaterialsByFactoryId(factoryId, page, size)); + + return ApiResponse.success( + SuccessStatus.OK, + factoryMaterialService.searchMaterials(factoryId, categoryId, keyword, page, size) + ); } @Operation(summary = "자재 주문 생성", description = "공장에 필요한 자재 주문을 생성합니다.") @@ -80,4 +89,27 @@ public ResponseEntity> receiveMaterialOrde return ApiResponse.success(SuccessStatus.OK, materialOrderService.receiveMaterialOrder(factoryId, orderId)); } + + @Operation( + summary = "자재 주문 취소", + description = "특정 공장의 자재 주문을 취소합니다. (받은(입고) 주문은 취소 불가)" + ) + @PutMapping("/{factoryId}/material/order/{orderId}/cancel") + public ResponseEntity> cancelMaterialOrder( + @PathVariable Long factoryId, + @PathVariable Long orderId) { + + return ApiResponse.success( + SuccessStatus.OK, + materialOrderService.cancelMaterialOrder(factoryId, orderId) + ); + } + + @Operation(summary = "자재 주문 삭제(소프트)", description = "주문 레코드를 실제로는 삭제하지 않고 숨깁니다.") + @DeleteMapping("/{factoryId}/material/order/{orderId}") + public ResponseEntity> deleteMaterialOrder( + @PathVariable Long factoryId, @PathVariable Long orderId) { + materialOrderService.softDeleteMaterialOrder(factoryId, orderId); + return ApiResponse.success_only(SuccessStatus.OK); + } } \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java index a44fd1d..dc2d945 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java @@ -1,10 +1,13 @@ package com.sampoom.factory.api.material.entity; import com.sampoom.factory.api.factory.entity.Factory; +import com.sampoom.factory.common.entitiy.SoftDeleteEntity; import com.sampoom.factory.common.exception.BadRequestException; import com.sampoom.factory.common.response.ErrorStatus; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; import java.time.LocalDateTime; @@ -14,7 +17,9 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder -public class MaterialOrder { +@SQLDelete(sql = "UPDATE material_order SET deleted = true, deleted_at = now() WHERE material_order_id = ?") +@Where(clause = "deleted = false") +public class MaterialOrder extends SoftDeleteEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "material_order_id") @@ -23,6 +28,7 @@ public class MaterialOrder { private String code; private LocalDateTime orderAt; private LocalDateTime receivedAt; + private LocalDateTime canceledAt; @Enumerated(EnumType.STRING) private OrderStatus status; @@ -38,4 +44,14 @@ public void receive() { this.status = OrderStatus.RECEIVED; this.receivedAt = LocalDateTime.now(); } + + public void cancel() { + + if (this.status != OrderStatus.ORDERED) { + throw new BadRequestException(ErrorStatus.ORDER_ALREADY_PROCESSED); + } + this.status = OrderStatus.CANCELED; + this.canceledAt = LocalDateTime.now(); + } + } diff --git a/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java b/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java index 2f82ce9..2b587f9 100644 --- a/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java +++ b/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java @@ -5,5 +5,6 @@ public enum OrderStatus { ORDERED, // 주문됨 - RECEIVED // 입고됨 + RECEIVED, // 입고됨 + CANCELED // 발주 취소됨 } diff --git a/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java index 38d6468..2d3425f 100644 --- a/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java +++ b/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; @@ -21,4 +23,37 @@ Page findByFactory_IdAndMaterial_MaterialCategory_Id( Page findByFactory_Id(Long factoryId, Pageable pageable); Optional findByFactoryIdAndMaterialId(Long factoryId, Long materialId); + + @Query(""" + select fm + from FactoryMaterial fm + join fm.factory factory + join fm.material material + left join material.materialCategory category + where factory.id = :factoryId + and (:categoryId is null or category.id = :categoryId) + """) + Page findByFactoryAndCategory( + @Param("factoryId") Long factoryId, + @Param("categoryId") Long categoryId, + Pageable pageable + ); + + @Query(""" + select fm + from FactoryMaterial fm + join fm.factory factory + join fm.material material + left join material.materialCategory category + where factory.id = :factoryId + and (:categoryId is null or category.id = :categoryId) + and (lower(material.name) like lower(concat('%', :keyword, '%')) + or lower(material.code) like lower(concat('%', :keyword, '%'))) + """) + Page findByFactoryCategoryAndKeyword( + @Param("factoryId") Long factoryId, + @Param("categoryId") Long categoryId, + @Param("keyword") String keyword, + Pageable pageable + ); } \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java index a91b153..a720ff5 100644 --- a/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java +++ b/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java @@ -5,6 +5,10 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MaterialOrderRepository extends JpaRepository { Page findByFactoryId(Long factoryId, Pageable pageable); + + Optional findByIdAndFactory_Id(Long orderId, Long factoryId); } diff --git a/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java b/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java index 5baf7b1..bf7e1c4 100644 --- a/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java +++ b/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java @@ -16,6 +16,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -86,4 +87,39 @@ public PageResponseDto getMaterialsByFactoryId(Long factory .totalPages(factoryMaterialsPage.getTotalPages()) .build(); } + + @Transactional(readOnly = true) + public PageResponseDto searchMaterials( + Long factoryId, + Long categoryId, + String keyword, + int page, + int size + ) { + Pageable pageable = PageRequest.of(page, size); + + Page fmPage; + if (keyword == null || keyword.isBlank()) { + fmPage = factoryMaterialRepository.findByFactoryAndCategory( + factoryId, categoryId, pageable); + } else { + fmPage = factoryMaterialRepository.findByFactoryCategoryAndKeyword( + factoryId, categoryId, keyword, pageable); + } + List content = fmPage.getContent().stream() + .map(factoryMaterial -> { + Material material = factoryMaterial.getMaterial(); + return MaterialResponseDto.from(material) + .withQuantity(factoryMaterial.getQuantity()); + }) + .collect(Collectors.toList()); + + return PageResponseDto.builder() + .content(content) + .totalElements(fmPage.getTotalElements()) + .totalPages(fmPage.getTotalPages()) + .build(); + } + + } \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java index c4b22d6..8207d8c 100644 --- a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java +++ b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java @@ -133,6 +133,28 @@ public MaterialOrderResponseDto receiveMaterialOrder(Long factoryId, Long orderI return MaterialOrderResponseDto.from(order, items); } + public MaterialOrderResponseDto cancelMaterialOrder(Long factoryId, Long orderId) { + MaterialOrder order = orderRepository + .findByIdAndFactory_Id(orderId, factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.ORDER_NOT_FOUND)); + order.cancel(); + List items = orderItemRepository.findByMaterialOrderId(orderId); + + return MaterialOrderResponseDto.from(order,items); + } + + public void softDeleteMaterialOrder(Long factoryId, Long orderId) { + MaterialOrder order = orderRepository + .findByIdAndFactory_Id(orderId, factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.ORDER_NOT_FOUND)); + + + + // JPA delete() 호출 → @SQLDelete가 UPDATE로 변환 + orderRepository.delete(order); + + } + private String generateOrderCode() { return "ORD-" + System.currentTimeMillis(); } diff --git a/src/main/java/com/sampoom/factory/common/entitiy/BaseTimeEntity.java b/src/main/java/com/sampoom/factory/common/entitiy/BaseTimeEntity.java index b927185..f6e711f 100644 --- a/src/main/java/com/sampoom/factory/common/entitiy/BaseTimeEntity.java +++ b/src/main/java/com/sampoom/factory/common/entitiy/BaseTimeEntity.java @@ -25,3 +25,5 @@ public abstract class BaseTimeEntity { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") protected LocalDateTime updatedAt; } + + diff --git a/src/main/java/com/sampoom/factory/common/entitiy/SoftDeleteEntity.java b/src/main/java/com/sampoom/factory/common/entitiy/SoftDeleteEntity.java new file mode 100644 index 0000000..6722cb8 --- /dev/null +++ b/src/main/java/com/sampoom/factory/common/entitiy/SoftDeleteEntity.java @@ -0,0 +1,22 @@ +package com.sampoom.factory.common.entitiy; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +import java.time.LocalDateTime; + +@MappedSuperclass +@Getter +public abstract class SoftDeleteEntity extends BaseTimeEntity { + @Column(nullable = false) + protected boolean deleted = false; + + protected LocalDateTime deletedAt; + + public void softDelete() { + if (this.deleted) return; + this.deleted = true; + this.deletedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java b/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java index 61a4d9f..4a5d0a6 100644 --- a/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java +++ b/src/main/java/com/sampoom/factory/common/response/ErrorStatus.java @@ -17,6 +17,7 @@ public enum ErrorStatus { ORDER_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "이미 처리된 주문입니다.",40005), + // 401 UNAUTHORIZED UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다.", 40101), From 2265e43152093a07e01b78cd793a60835f2f6a6f Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Fri, 17 Oct 2025 11:00:41 +0900 Subject: [PATCH 24/29] =?UTF-8?q?[FEAT]=20=EC=9E=90=EC=9E=AC=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FactoryMaterialController.java | 11 +++++++++++ .../api/material/service/MaterialOrderService.java | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java index 3eb21ac..41b9bf7 100644 --- a/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java +++ b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java @@ -112,4 +112,15 @@ public ResponseEntity> deleteMaterialOrder( materialOrderService.softDeleteMaterialOrder(factoryId, orderId); return ApiResponse.success_only(SuccessStatus.OK); } + + @Operation(summary = "자재 주문 상세 조회", description = "특정 자재 주문의 상세 정보를 조회합니다.") + @GetMapping("/{factoryId}/material/order/{orderId}") + public ResponseEntity> getMaterialOrderDetail( + @PathVariable Long factoryId, + @PathVariable Long orderId) { + return ApiResponse.success( + SuccessStatus.OK, + materialOrderService.getMaterialOrderDetail(factoryId, orderId) + ); + } } \ No newline at end of file diff --git a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java index 8207d8c..f4c8d2c 100644 --- a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java +++ b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java @@ -155,6 +155,14 @@ public void softDeleteMaterialOrder(Long factoryId, Long orderId) { } + @Transactional(readOnly = true) + public MaterialOrderResponseDto getMaterialOrderDetail(Long factoryId, Long orderId) { + MaterialOrder order = orderRepository.findByIdAndFactory_Id(orderId, factoryId ) + .orElseThrow(() -> new NotFoundException(ErrorStatus.ORDER_NOT_FOUND)); + List items = orderItemRepository.findByMaterialOrderId(orderId); + return MaterialOrderResponseDto.from(order,items); + } + private String generateOrderCode() { return "ORD-" + System.currentTimeMillis(); } From 1f5862bed1ba19cdb6f0d87cf9f5ecda77892f48 Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Fri, 17 Oct 2025 11:00:55 +0900 Subject: [PATCH 25/29] =?UTF-8?q?[FEAT]=20=EC=9E=90=EC=9E=AC=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FactoryMaterialControllerTest.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/test/java/com/sampoom/factory/api/material/controller/FactoryMaterialControllerTest.java diff --git a/src/test/java/com/sampoom/factory/api/material/controller/FactoryMaterialControllerTest.java b/src/test/java/com/sampoom/factory/api/material/controller/FactoryMaterialControllerTest.java new file mode 100644 index 0000000..6a951a6 --- /dev/null +++ b/src/test/java/com/sampoom/factory/api/material/controller/FactoryMaterialControllerTest.java @@ -0,0 +1,93 @@ +package com.sampoom.factory.api.material.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sampoom.factory.api.material.dto.MaterialOrderResponseDto; +import com.sampoom.factory.api.material.dto.MaterialResponseDto; +import com.sampoom.factory.api.material.service.FactoryMaterialService; +import com.sampoom.factory.api.material.service.MaterialOrderService; +import com.sampoom.factory.common.response.PageResponseDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; + +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(FactoryMaterialController.class) +class FactoryMaterialControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private FactoryMaterialService factoryMaterialService; + + @MockitoBean + private MaterialOrderService materialOrderService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("자재 검색/목록 조회") + void getMaterials() throws Exception { + PageResponseDto page = PageResponseDto.builder() + .content(Collections.emptyList()) + .totalElements(0) + .totalPages(0) + .build(); + + Mockito.when(factoryMaterialService.searchMaterials(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.anyInt(), Mockito.anyInt())) + .thenReturn(page); + + mockMvc.perform(get("/1/material") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("자재 주문 삭제") + void deleteMaterialOrder() throws Exception { + Mockito.doNothing().when(materialOrderService).softDeleteMaterialOrder(Mockito.anyLong(), Mockito.anyLong()); + + mockMvc.perform(delete("/1/material/order/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("자재 주문 취소") + void cancelMaterialOrder() throws Exception { + MaterialOrderResponseDto responseDto = Mockito.mock(MaterialOrderResponseDto.class); + Mockito.when(materialOrderService.cancelMaterialOrder(Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(responseDto); + + mockMvc.perform(put("/1/material/order/1/cancel")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("자재 주문 상세조회") + void getMaterialOrderDetail() throws Exception { + MaterialOrderResponseDto responseDto = Mockito.mock(MaterialOrderResponseDto.class); + Mockito.when(materialOrderService.getMaterialOrderDetail(Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(responseDto); + + mockMvc.perform(get("/1/material/order/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} \ No newline at end of file From 74d12b06e67f42ddb3e9e706273a6250621e6e9d Mon Sep 17 00:00:00 2001 From: Kim Taemin Date: Fri, 17 Oct 2025 11:44:16 +0900 Subject: [PATCH 26/29] =?UTF-8?q?[FIX]=20EntityGraph=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../material/repository/FactoryMaterialRepository.java | 2 ++ .../api/material/service/FactoryMaterialService.java | 9 +++++++++ .../api/material/service/MaterialOrderService.java | 2 ++ 3 files changed, 13 insertions(+) diff --git a/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java b/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java index 2d3425f..813e51e 100644 --- a/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java +++ b/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java @@ -24,6 +24,7 @@ Page findByFactory_IdAndMaterial_MaterialCategory_Id( Optional findByFactoryIdAndMaterialId(Long factoryId, Long materialId); + @EntityGraph(attributePaths = {"material", "material.materialCategory"}) @Query(""" select fm from FactoryMaterial fm @@ -39,6 +40,7 @@ Page findByFactoryAndCategory( Pageable pageable ); + @EntityGraph(attributePaths = {"material", "material.materialCategory"}) @Query(""" select fm from FactoryMaterial fm diff --git a/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java b/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java index bf7e1c4..6ec53fd 100644 --- a/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java +++ b/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java @@ -96,6 +96,15 @@ public PageResponseDto searchMaterials( int page, int size ) { + + factoryRepository.findById(factoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.FACTORY_NOT_FOUND)); + + if (categoryId != null) { + materialCategoryRepository.findById(categoryId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.CATEGORY_NOT_FOUND)); + } + Pageable pageable = PageRequest.of(page, size); Page fmPage; diff --git a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java index f4c8d2c..f73767d 100644 --- a/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java +++ b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java @@ -133,6 +133,7 @@ public MaterialOrderResponseDto receiveMaterialOrder(Long factoryId, Long orderI return MaterialOrderResponseDto.from(order, items); } + @Transactional public MaterialOrderResponseDto cancelMaterialOrder(Long factoryId, Long orderId) { MaterialOrder order = orderRepository .findByIdAndFactory_Id(orderId, factoryId) @@ -143,6 +144,7 @@ public MaterialOrderResponseDto cancelMaterialOrder(Long factoryId, Long orderI return MaterialOrderResponseDto.from(order,items); } + @Transactional public void softDeleteMaterialOrder(Long factoryId, Long orderId) { MaterialOrder order = orderRepository .findByIdAndFactory_Id(orderId, factoryId) From f0f9023501018701eab843d68cbce33125e2cf21 Mon Sep 17 00:00:00 2001 From: Choosla Date: Fri, 17 Oct 2025 15:58:24 +0900 Subject: [PATCH 27/29] chore: Apply batch updates from central configuration --- .github/workflows/trigger_infra.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/trigger_infra.yml diff --git a/.github/workflows/trigger_infra.yml b/.github/workflows/trigger_infra.yml new file mode 100644 index 0000000..bca571a --- /dev/null +++ b/.github/workflows/trigger_infra.yml @@ -0,0 +1,22 @@ +# changes/.github/workflows/trigger_infra.yml.template 파일 내용 + +name: Trigger Infra CD + +on: +  push: +    branches: +      - main + +jobs: +  trigger-infra: +    runs-on: ubuntu-latest +    steps: +      - name: Trigger infra repo deploy workflow +        uses: peter-evans/repository-dispatch@v3 +        with: +          token: ${{ secrets.ORGANIZATION_TOKEN }} +          # [중요] 아래 repository 값은 모든 앱이 공유하는 '중앙 인프라 리포지토리' 주소로 미리 수정해야 한다. +          repository: 33-Auto/Sampoom-Mangement-Infra +          event-type: deploy +          # 'Sampoom-Management-Backend-Factory'은 스크립트가 동적으로 치환할 자리표시자(placeholder)이다. +          client-payload: '{"service":"Sampoom-Management-Backend-Factory","branch":"main"}' From 473cc7558d24672e18317404bd9e2fb158703640 Mon Sep 17 00:00:00 2001 From: Choosla Date: Fri, 17 Oct 2025 16:24:32 +0900 Subject: [PATCH 28/29] chore: Apply batch updates from central configuration --- .github/workflows/trigger_infra.yml | 32 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/trigger_infra.yml b/.github/workflows/trigger_infra.yml index bca571a..3ebb9d5 100644 --- a/.github/workflows/trigger_infra.yml +++ b/.github/workflows/trigger_infra.yml @@ -1,22 +1,20 @@ -# changes/.github/workflows/trigger_infra.yml.template 파일 내용 - name: Trigger Infra CD on: -  push: -    branches: -      - main + push: + branches: + - main jobs: -  trigger-infra: -    runs-on: ubuntu-latest -    steps: -      - name: Trigger infra repo deploy workflow -        uses: peter-evans/repository-dispatch@v3 -        with: -          token: ${{ secrets.ORGANIZATION_TOKEN }} -          # [중요] 아래 repository 값은 모든 앱이 공유하는 '중앙 인프라 리포지토리' 주소로 미리 수정해야 한다. -          repository: 33-Auto/Sampoom-Mangement-Infra -          event-type: deploy -          # 'Sampoom-Management-Backend-Factory'은 스크립트가 동적으로 치환할 자리표시자(placeholder)이다. -          client-payload: '{"service":"Sampoom-Management-Backend-Factory","branch":"main"}' + trigger-infra: + runs-on: ubuntu-latest + steps: + - name: Trigger infra repo deploy workflow + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.ORGANIZATION_TOKEN }} + # [중요] 아래 repository 값은 모든 앱이 공유하는 '중앙 인프라 리포지토리' 주소이다. + repository: 33-Auto/Sampoom-Management-Backend-Infra + event-type: deploy + # 'Sampoom-Management-Backend-Part'은 스크립트가 동적으로 치환할 자리표시자(placeholder)이다. + client-payload: '{"service":"Sampoom-Management-Backend-Part","branch":"main"}' \ No newline at end of file From b43f3c4aa68ef44c033fe24b87c95cfca0cae683 Mon Sep 17 00:00:00 2001 From: Choosla Date: Fri, 17 Oct 2025 16:28:42 +0900 Subject: [PATCH 29/29] chore: Apply batch updates from central configuration --- .github/workflows/trigger_infra.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trigger_infra.yml b/.github/workflows/trigger_infra.yml index 3ebb9d5..37bbb92 100644 --- a/.github/workflows/trigger_infra.yml +++ b/.github/workflows/trigger_infra.yml @@ -14,7 +14,7 @@ jobs: with: token: ${{ secrets.ORGANIZATION_TOKEN }} # [중요] 아래 repository 값은 모든 앱이 공유하는 '중앙 인프라 리포지토리' 주소이다. - repository: 33-Auto/Sampoom-Management-Backend-Infra + repository: 33-Auto/Sampoom-Management-Infra event-type: deploy # 'Sampoom-Management-Backend-Part'은 스크립트가 동적으로 치환할 자리표시자(placeholder)이다. client-payload: '{"service":"Sampoom-Management-Backend-Part","branch":"main"}' \ No newline at end of file