package com.srsystem.controller; import com.srsystem.dto.ApiResponse; import com.srsystem.dto.BatchResult; import com.srsystem.dto.CreateEquipmentRequest; import com.srsystem.dto.CreateMaintenanceRequest; import com.srsystem.dto.EquipmentSummary; import com.srsystem.dto.UpdateEquipmentRequest; import com.srsystem.entity.Equipment; import com.srsystem.entity.EquipmentAssignment; import com.srsystem.entity.EquipmentMaintenance; import com.srsystem.entity.enums.EquipmentStatus; import com.srsystem.entity.enums.EquipmentType; import com.srsystem.service.EquipmentService; import com.srsystem.util.ExcelParser; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.ArrayList; import java.util.List; import java.util.Map; @Tag(name = "Equipment", description = "IT 자산 관리 API") @RestController @RequestMapping("/api/v1/equipment") @RequiredArgsConstructor @SecurityRequirement(name = "bearerAuth") public class EquipmentController { private final EquipmentService equipmentService; @Operation(summary = "장비 목록 조회", description = "필터 조건으로 장비 목록을 조회합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @GetMapping public ResponseEntity>> getEquipment( @RequestParam(required = false) String type, @RequestParam(required = false) EquipmentStatus status, @RequestParam(required = false) String keyword, @PageableDefault(size = 20) Pageable pageable) { Page equipment = equipmentService.getEquipment(type, status, keyword, pageable); return ResponseEntity.ok(ApiResponse.ok(equipment)); } @Operation(summary = "장비 상태별 카운트", description = "장비 상태별 개수를 조회합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @GetMapping("/summary") public ResponseEntity> getSummary() { Map counts = equipmentService.getSummary(); EquipmentSummary summary = new EquipmentSummary(counts); return ResponseEntity.ok(ApiResponse.ok(summary)); } @Operation(summary = "장비 단건 조회", description = "장비 ID 로 단일 장비 정보를 조회합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @GetMapping("/{equipId}") public ResponseEntity> getEquipment(@PathVariable Long equipId) { Equipment equipment = equipmentService.getEquipment(equipId); return ResponseEntity.ok(ApiResponse.ok(equipment)); } @Operation(summary = "장비 생성", description = "새로운 장비를 생성합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @PostMapping public ResponseEntity> createEquipment(@Valid @RequestBody CreateEquipmentRequest request) { Equipment equipment = equipmentService.createEquipment(request); return ResponseEntity.ok(ApiResponse.ok("장비가 등록되었습니다", equipment)); } @Operation(summary = "장비 정보 수정", description = "장비 정보를 수정합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @PutMapping("/{equipId}") public ResponseEntity> updateEquipment( @PathVariable Long equipId, @RequestBody UpdateEquipmentRequest request) { Equipment equipment = equipmentService.updateEquipment(equipId, request); return ResponseEntity.ok(ApiResponse.ok("장비 정보가 수정되었습니다", equipment)); } @Operation(summary = "장비 삭제", description = "장비를 삭제합니다 (Soft Delete)") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @DeleteMapping("/{equipId}") public ResponseEntity> deleteEquipment(@PathVariable Long equipId) { equipmentService.deleteEquipment(equipId); return ResponseEntity.ok(ApiResponse.ok("장비가 삭제되었습니다", null)); } @Operation(summary = "장비 지급", description = "사용자에게 장비를 지급합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @PostMapping("/{equipId}/assign") public ResponseEntity> assignEquipment( @PathVariable Long equipId, @RequestBody Map body) { Long userId = ((Number) body.get("userId")).longValue(); String condition = (String) body.get("condition"); EquipmentAssignment assignment = equipmentService.assignEquipment(equipId, userId, condition); return ResponseEntity.ok(ApiResponse.ok("장비가 지급되었습니다", assignment)); } @Operation(summary = "장비 반납", description = "장비를 반납합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @PutMapping("/{equipId}/return") public ResponseEntity> returnEquipment( @PathVariable Long equipId, @RequestBody Map body) { EquipmentAssignment assignment = equipmentService.returnEquipment(equipId, body.get("condition")); return ResponseEntity.ok(ApiResponse.ok("장비가 반납되었습니다", assignment)); } @Operation(summary = "장비 지급 이력 조회", description = "장비의 지급/반납 이력을 조회합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @GetMapping("/{equipId}/assignments") public ResponseEntity>> getAssignments(@PathVariable Long equipId) { List assignments = equipmentService.getAssignments(equipId); return ResponseEntity.ok(ApiResponse.ok(assignments)); } @Operation(summary = "장비 유지보수 이력 조회", description = "장비의 유지보수 이력을 조회합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @GetMapping("/{equipId}/maintenances") public ResponseEntity>> getMaintenanceHistory(@PathVariable Long equipId) { List history = equipmentService.getMaintenanceHistory(equipId); return ResponseEntity.ok(ApiResponse.ok(history)); } @Operation(summary = "장비 유지보수 기록 추가", description = "장비의 유지보수 기록을 추가합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @PostMapping("/{equipId}/maintenance") public ResponseEntity> addMaintenance( @PathVariable Long equipId, @Valid @RequestBody CreateMaintenanceRequest request) { EquipmentMaintenance maintenance = equipmentService.addMaintenance(equipId, request); return ResponseEntity.ok(ApiResponse.ok("유지보수 기록이 등록되었습니다", maintenance)); } @Operation(summary = "장비 이미지 추가", description = "장비에 이미지를 추가합니다 (JSONB 배열)") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @PostMapping("/{equipmentId}/images") public ResponseEntity> addImage( @PathVariable Long equipmentId, @RequestParam("file") MultipartFile file) { Equipment equipment = equipmentService.addImage(equipmentId, file); return ResponseEntity.ok(ApiResponse.ok("이미지가 추가되었습니다", equipment)); } @Operation(summary = "장비 이미지 삭제", description = "장비에서 특정 이미지를 삭제합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @DeleteMapping("/{equipmentId}/images/{imageIndex}") public ResponseEntity> removeImage( @PathVariable Long equipmentId, @PathVariable int imageIndex) { Equipment equipment = equipmentService.removeImage(equipmentId, imageIndex); return ResponseEntity.ok(ApiResponse.ok("이미지가 삭제되었습니다", equipment)); } // ---- Batch ---- @Operation(summary = "장비 일괄 등록", description = "엑셀 파일로 장비를 일괄 등록합니다") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @Transactional @PostMapping("/batch") public ResponseEntity>> batchCreate(@RequestParam("file") MultipartFile file) { try { ExcelParser.ParseResult parsed = ExcelParser.parse( file, (row) -> new CreateEquipmentRequest( EquipmentType.valueOf(row.getOrDefault("장비유형", "LAPTOP").toUpperCase()), row.getOrDefault("제조사", ""), row.getOrDefault("모델명", ""), row.getOrDefault("시리얼번호", ""), row.getOrDefault("자산코드", ""), row.getOrDefault("제원정보", ""), EquipmentStatus.AVAILABLE, row.containsKey("구매일") ? java.time.LocalDate.parse(row.get("구매일")) : java.time.LocalDate.now(), null ), List.of() ); List failures = new ArrayList<>(); List success = new ArrayList<>(); var parseFailures = parsed.failures(); for (var pf : parseFailures) { failures.add(new BatchResult.Failure(pf.row(), pf.reason())); } for (int i = 0; i < parsed.data().size(); i++) { try { Equipment eq = equipmentService.createEquipment(parsed.data().get(i)); success.add(eq); } catch (Exception e) { failures.add(new BatchResult.Failure(i + 2, e.getMessage())); } } BatchResult result = new BatchResult<>( parsed.data().size() + parseFailures.size(), success.size(), failures.size(), failures, success ); return ResponseEntity.ok(ApiResponse.ok( String.format("%d건 중 %d건 성공, %d건 실패", result.totalCount(), result.successCount(), result.failCount()), result )); } catch (Exception e) { return ResponseEntity.badRequest().body(ApiResponse.error("엑셀 파싱 실패: " + e.getMessage())); } } @Operation(summary = "장비 일괄 등록 템플릿 다운로드") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @GetMapping("/batch/template") public ResponseEntity downloadTemplate() throws Exception { String[] headers = {"장비유형", "제조사", "모델명", "시리얼번호", "자산코드", "제원정보", "구매일"}; Map dropdowns = Map.of( 0, new String[]{"LAPTOP", "MONITOR", "TEST_DEVICE", "ACCESSORY"} ); byte[] data = ExcelParser.generateTemplate(headers, "장비 일괄등록", dropdowns); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=equipment_batch_template.xlsx") .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) .body(data); } }