diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 9190e01..007e757 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -1,13 +1,15 @@ -name: PR Reminder + name: PR Reminder -on: - schedule: - - cron: "0 0,5,8 * * *" # 아침 9시, 오후 2시, 오후 5시에 실행 (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 }} + 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 diff --git a/.github/workflows/trigger_infra.yml b/.github/workflows/trigger_infra.yml new file mode 100644 index 0000000..37bbb92 --- /dev/null +++ b/.github/workflows/trigger_infra.yml @@ -0,0 +1,20 @@ +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-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 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/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..c0b7dad --- /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", unique = true) + 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..d63540a --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/bom/service/BomService.java @@ -0,0 +1,132 @@ +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(); + + 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/api/factory/controller/FactoryController.java b/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java new file mode 100644 index 0000000..401ca13 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/controller/FactoryController.java @@ -0,0 +1,29 @@ +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 jakarta.validation.Valid; +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(@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 new file mode 100644 index 0000000..836203f --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/dto/FactoryCreateRequestDto.java @@ -0,0 +1,26 @@ +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; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class FactoryCreateRequestDto { + + @NotBlank(message = "공장 이름은 필수입니다") + private String name; + + @NotBlank(message = "공장 위치는 필수입니다") + 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/entity/Factory.java b/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java new file mode 100644 index 0000000..761bc04 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/factory/entity/Factory.java @@ -0,0 +1,25 @@ +package com.sampoom.factory.api.factory.entity; + + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "factory") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@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/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/factory/service/FactoryService.java b/src/main/java/com/sampoom/factory/api/factory/service/FactoryService.java new file mode 100644 index 0000000..65a5a0f --- /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) + .material(material) + .quantity(0L) + .build(); + + factoryMaterialRepository.save(factoryMaterial); + } + + return FactoryResponseDto.from(factory); + } +} 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 new file mode 100644 index 0000000..41b9bf7 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/controller/FactoryMaterialController.java @@ -0,0 +1,126 @@ +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() +@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 = "특정 공장의 자재를 페이징 조회합니다. 카테고리(categoryId)로 필터링하고, keyword(자재명/자재코드)로 검색합니다." + ) + @GetMapping("/{factoryId}/material") + 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.searchMaterials(factoryId, categoryId, keyword, 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)); + } + + @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); + } + + @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/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/entity/FactoryMaterial.java b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java new file mode 100644 index 0000000..b895045 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/FactoryMaterial.java @@ -0,0 +1,34 @@ +package com.sampoom.factory.api.material.entity; + +import com.sampoom.factory.api.factory.entity.Factory; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "factory_material") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@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 void increaseQuantity(Long amount) { + this.quantity += amount; + } +} 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..8cfb995 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/Material.java @@ -0,0 +1,28 @@ +package com.sampoom.factory.api.material.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Immutable; + +@Entity +@Table(name = "material") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Immutable +public class Material { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "material_id") + private Long id; + + @Column(name = "material_name", nullable = false) + private String name; + + @Column(name = "material_code", nullable = false) + 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..b74c689 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialCategory.java @@ -0,0 +1,24 @@ +package com.sampoom.factory.api.material.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Immutable; + +@Entity +@Table(name = "material_category") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Immutable +public class MaterialCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "material_category_id") + private Long id; + + @Column(name = "material_category_name", nullable = false) + private String name; + + @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/MaterialOrder.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java new file mode 100644 index 0000000..dc2d945 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrder.java @@ -0,0 +1,57 @@ +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; + +@Entity +@Getter +@Table(name = "material_order") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@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") + private Long id; + + private String code; + private LocalDateTime orderAt; + private LocalDateTime receivedAt; + private LocalDateTime canceledAt; + + @Enumerated(EnumType.STRING) + private OrderStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "factory_id") + private Factory factory; + + public void receive() { + if (this.status != OrderStatus.ORDERED) { + throw new BadRequestException(ErrorStatus.ORDER_ALREADY_PROCESSED); + } + 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/MaterialOrderItem.java b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java new file mode 100644 index 0000000..bdb26a8 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/MaterialOrderItem.java @@ -0,0 +1,31 @@ +package com.sampoom.factory.api.material.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Table(name = "material_order_item") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@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..2b587f9 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/entity/OrderStatus.java @@ -0,0 +1,10 @@ +package com.sampoom.factory.api.material.entity; + +import lombok.Getter; + + +public enum OrderStatus { + ORDERED, // 주문됨 + 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 new file mode 100644 index 0000000..813e51e --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/repository/FactoryMaterialRepository.java @@ -0,0 +1,61 @@ +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 org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +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); + + @EntityGraph(attributePaths = {"material", "material.materialCategory"}) + @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 + ); + + @EntityGraph(attributePaths = {"material", "material.materialCategory"}) + @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/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..a720ff5 --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/repository/MaterialOrderRepository.java @@ -0,0 +1,14 @@ +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; + +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/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..6ec53fd --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/service/FactoryMaterialService.java @@ -0,0 +1,134 @@ +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.data.domain.Sort; +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(); + } + + @Transactional(readOnly = true) + public PageResponseDto searchMaterials( + Long factoryId, + Long categoryId, + String keyword, + 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; + 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 new file mode 100644 index 0000000..f73767d --- /dev/null +++ b/src/main/java/com/sampoom/factory/api/material/service/MaterialOrderService.java @@ -0,0 +1,171 @@ +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); + } + + @Transactional + 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); + } + + @Transactional + 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); + + } + + @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(); + } +} \ No newline at end of file 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/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/config/swagger/SwaggerConfig.java b/src/main/java/com/sampoom/factory/common/config/swagger/SwaggerConfig.java index 17d3549..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 @@ -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/") + .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( prodServer,localServer)); } // @Bean 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/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..4a5d0a6 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,36 @@ 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), + PART_NOT_FOUND(HttpStatus.NOT_FOUND, "부품을 찾을 수 없습니다.", 40406), + BOM_NOT_FOUND(HttpStatus.NOT_FOUND, "BOM을 찾을 수 없습니다.", 40407), // 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,프라이머 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 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..a08a4a4 --- /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::getMaterial).map(Material::getId).toList()) + .containsExactlyInAnyOrder(1L, 2L); + } +} \ No newline at end of file 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 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