package kr.itn.itnhub.review; import kr.itn.itnhub.org.OrgNotFoundException; import kr.itn.itnhub.org.Organization; import kr.itn.itnhub.org.OrganizationMapper; import kr.itn.itnhub.seed.SeedParseException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; /** 기관관리 상세의 권리확인 탭이 부르는 엔드포인트. */ @RestController public class ReviewController { private final ReviewService reviewService; private final ReviewImportService importService; private final OrganizationMapper orgMapper; public ReviewController(ReviewService reviewService, ReviewImportService importService, OrganizationMapper orgMapper) { this.reviewService = reviewService; this.importService = importService; this.orgMapper = orgMapper; } @PostMapping("/api/orgs/{id}/review/import") public ReviewImportReport upload(@PathVariable("id") Long orgId, @RequestParam("file") MultipartFile file) { if (file.isEmpty()) { throw new SeedParseException("업로드된 파일이 비어 있습니다."); } String filename = file.getOriginalFilename(); if (filename == null || !hasAllowedExtension(filename)) { throw new SeedParseException("엑셀 파일(.xlsx 또는 .xlsm)만 업로드할 수 있습니다."); } try (InputStream in = file.getInputStream()) { return importService.importFile(orgId, in); } catch (IOException e) { throw new SeedParseException("엑셀 파일을 읽지 못했습니다.", e); } } /** * keyword는 사이트명/게시판명/제목 통합검색, site/board/title은 열별 조건(서로 AND)이다. */ @GetMapping("/api/orgs/{id}/review") public ReviewPage list(@PathVariable("id") Long orgId, @RequestParam(required = false) String keyword, @RequestParam(required = false) String site, @RequestParam(required = false) String board, @RequestParam(required = false) String title, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "30") int size) { return reviewService.list(orgId, keyword, site, board, title, page, size); } /** 목록에서 체크한 건들에 같은 값을 한 번에 반영한다. 반영된 건수를 돌려준다. */ @PutMapping("/api/orgs/{id}/review/bulk") public java.util.Map updateBulk(@PathVariable("id") Long orgId, @RequestBody BulkJudgmentRequest request) { return java.util.Map.of("updated", reviewService.updateBulk(orgId, request)); } /** 목록에서 체크한 건들을 지운다. */ @PostMapping("/api/orgs/{id}/review/bulk-delete") public java.util.Map deleteBulk(@PathVariable("id") Long orgId, @RequestBody java.util.List ids) { return java.util.Map.of("deleted", reviewService.deleteBulk(orgId, ids)); } @GetMapping("/api/orgs/{id}/review/{itemId}") public ReviewItem get(@PathVariable("id") Long orgId, @PathVariable Long itemId) { return reviewService.get(orgId, itemId); } @PutMapping("/api/orgs/{id}/review/{itemId}") public ReviewItem update(@PathVariable("id") Long orgId, @PathVariable Long itemId, @RequestBody JudgmentRequest request) { return reviewService.updateJudgment(orgId, itemId, request); } @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping("/api/orgs/{id}/review/{itemId}") public void delete(@PathVariable("id") Long orgId, @PathVariable Long itemId) { reviewService.delete(orgId, itemId); } /** 한글 파일명이라 ChannelFeedController.download와 동일하게 RFC 5987 인코딩을 쓴다. */ @GetMapping("/api/orgs/{id}/review/download") public ResponseEntity download(@PathVariable("id") Long orgId) { Organization org = orgMapper.findById(orgId); if (org == null) { throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId); } byte[] content = reviewService.downloadWorkbook(orgId); String filename = "권리확인_" + org.getOrgName() + ".xlsx"; String encodedName = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20"); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedName) .contentType(MediaType.parseMediaType( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) .body(content); } private static boolean hasAllowedExtension(String filename) { String lower = filename.toLowerCase(); return lower.endsWith(".xlsx") || lower.endsWith(".xlsm"); } }