diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleApi.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleApi.java index e49fccd64..e0bd951fb 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleApi.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleApi.java @@ -1,32 +1,20 @@ package in.koreatech.koin.domain.community.article.controller; -import static in.koreatech.koin.domain.user.model.UserType.*; -import static in.koreatech.koin.global.code.ApiResponseCode.*; import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; import java.util.List; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import in.koreatech.koin.domain.community.article.dto.ArticleHotKeywordResponse; import in.koreatech.koin.domain.community.article.dto.ArticleResponse; import in.koreatech.koin.domain.community.article.dto.ArticlesResponse; -import in.koreatech.koin.domain.community.article.dto.FoundLostItemArticleCountResponse; import in.koreatech.koin.domain.community.article.dto.HotArticleItemResponse; -import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; -import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; -import in.koreatech.koin.domain.community.article.dto.LostItemArticlesResponse; -import in.koreatech.koin.domain.community.article.model.LostItemFoundStatus; -import in.koreatech.koin.global.auth.Auth; import in.koreatech.koin.global.auth.UserId; -import in.koreatech.koin.global.code.ApiResponseCodes; import in.koreatech.koin.global.ipaddress.IpAddress; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -35,7 +23,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; @Tag(name = "(Normal) Articles: 게시글", description = "게시글 정보를 관리한다") @RequestMapping("/articles") @@ -97,21 +84,6 @@ ResponseEntity searchArticles( @UserId Integer userId ); - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - } - ) - @Operation(summary = "분실물 게시글 검색") - @GetMapping("/lost-item/search") - ResponseEntity searchArticles( - @RequestParam String query, - @RequestParam(required = false) Integer page, - @RequestParam(required = false) Integer limit, - @IpAddress String ipAddress, - @UserId Integer userId - ); - @ApiResponses( value = { @ApiResponse(responseCode = "200") @@ -122,105 +94,4 @@ ResponseEntity searchArticles( ResponseEntity getArticlesHotKeyword( @RequestParam Integer count ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "분실물 게시글 목록 조회") - @GetMapping("/lost-item") - ResponseEntity getLostItemArticles( - @RequestParam(required = false) String type, - @RequestParam(required = false) Integer page, - @RequestParam(required = false) Integer limit, - @UserId Integer userId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "분실물 게시글 목록 조회 V2", description = """ - ### 분실물 게시글 목록 조회 V2 변경점 - - Request Param 추가: foundStatus (ALL, FOUND, NOT_FOUND) - - ALL : 모든 분실물 게시글 조회 (Default) - - FOUND : '주인 찾음' 상태인 게시글 조회 - - NOT_FOUND : '찾는 중' 상태인 게시글 조회 - """) - @GetMapping("/lost-item/v2") - ResponseEntity getLostItemArticlesV2( - @RequestParam(required = false) String type, - @RequestParam(required = false) Integer page, - @RequestParam(required = false) Integer limit, - @RequestParam(required = false, defaultValue = "ALL") LostItemFoundStatus foundStatus, - @UserId Integer userId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "분실물 게시글 단건 조회") - @GetMapping("/lost-item/{id}") - ResponseEntity getLostItemArticle( - @Parameter(in = PATH) @PathVariable("id") Integer articleId, - @UserId Integer userId - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "201"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "분실물 게시글 등록") - @PostMapping("/lost-item") - ResponseEntity createLostItemArticle( - @Auth(permit = {STUDENT, COUNCIL}) Integer userId, - @RequestBody @Valid LostItemArticlesRequest lostItemArticlesRequest - ); - - @ApiResponses( - value = { - @ApiResponse(responseCode = "204"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(hidden = true))), - } - ) - @Operation(summary = "분실물 게시글 삭제") - @DeleteMapping("/lost-item/{id}") - ResponseEntity deleteLostItemArticle( - @PathVariable("id") Integer articleId, - @Auth(permit = {STUDENT, COUNCIL}) Integer councilId - ); - - @ApiResponseCodes({ - NO_CONTENT, - FORBIDDEN_AUTHOR, - DUPLICATE_FOUND_STATUS - }) - @Operation(summary = "분실물 게시글 찾음 처리") - @PostMapping("/lost-item/{id}/found") - ResponseEntity markLostItemArticleAsFound( - @PathVariable("id") Integer articleId, - @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId - ); - - @ApiResponseCodes({ - OK - }) - @Operation(summary = "주인 찾음 상태인 분실물 게시글 총 개수 조회") - @GetMapping("/lost-item/found/count") - ResponseEntity getFoundLostItemArticlesCount(); } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleController.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleController.java index 00aee8e99..9f43694dc 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleController.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleController.java @@ -1,16 +1,10 @@ package in.koreatech.koin.domain.community.article.controller; -import static in.koreatech.koin.domain.user.model.UserType.*; - import java.util.List; -import org.springframework.http.HttpStatus; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -18,18 +12,10 @@ import in.koreatech.koin.domain.community.article.dto.ArticleHotKeywordResponse; import in.koreatech.koin.domain.community.article.dto.ArticleResponse; import in.koreatech.koin.domain.community.article.dto.ArticlesResponse; -import in.koreatech.koin.domain.community.article.dto.FoundLostItemArticleCountResponse; import in.koreatech.koin.domain.community.article.dto.HotArticleItemResponse; -import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; -import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; -import in.koreatech.koin.domain.community.article.dto.LostItemArticlesResponse; -import in.koreatech.koin.domain.community.article.model.LostItemFoundStatus; import in.koreatech.koin.domain.community.article.service.ArticleService; -import in.koreatech.koin.domain.community.article.service.LostItemFoundService; -import in.koreatech.koin.global.auth.Auth; import in.koreatech.koin.global.auth.UserId; import in.koreatech.koin.global.ipaddress.IpAddress; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RestController @@ -38,7 +24,6 @@ public class ArticleController implements ArticleApi { private final ArticleService articleService; - private final LostItemFoundService lostItemFoundService; @GetMapping("/{id}") public ResponseEntity getArticle( @@ -80,19 +65,6 @@ public ResponseEntity searchArticles( return ResponseEntity.ok().body(foundArticles); } - @GetMapping("/lost-item/search") - public ResponseEntity searchArticles( - @RequestParam String query, - @RequestParam(required = false) Integer page, - @RequestParam(required = false) Integer limit, - @IpAddress String ipAddress, - @UserId Integer userId - ) { - LostItemArticlesResponse foundArticles = articleService.searchLostItemArticles(query, page, limit, ipAddress, - userId); - return ResponseEntity.ok().body(foundArticles); - } - @GetMapping("/hot/keyword") public ResponseEntity getArticlesHotKeyword( @RequestParam Integer count @@ -100,68 +72,4 @@ public ResponseEntity getArticlesHotKeyword( ArticleHotKeywordResponse response = articleService.getArticlesHotKeyword(count); return ResponseEntity.ok().body(response); } - - @GetMapping("/lost-item") - public ResponseEntity getLostItemArticles( - @RequestParam(required = false) String type, - @RequestParam(required = false) Integer page, - @RequestParam(required = false) Integer limit, - @UserId Integer userId - ) { - LostItemArticlesResponse response = articleService.getLostItemArticles(type, page, limit, userId); - return ResponseEntity.ok().body(response); - } - - @GetMapping("/lost-item/v2") - public ResponseEntity getLostItemArticlesV2( - @RequestParam(required = false) String type, - @RequestParam(required = false) Integer page, - @RequestParam(required = false) Integer limit, - @RequestParam(required = false, defaultValue = "ALL") LostItemFoundStatus foundStatus, - @UserId Integer userId - ) { - LostItemArticlesResponse response = articleService.getLostItemArticlesV2(type, page, limit, userId, foundStatus); - return ResponseEntity.ok().body(response); - } - - @GetMapping("/lost-item/{id}") - public ResponseEntity getLostItemArticle( - @PathVariable("id") Integer articleId, - @UserId Integer userId - ) { - return ResponseEntity.ok().body(articleService.getLostItemArticle(articleId, userId)); - } - - @PostMapping("/lost-item") - public ResponseEntity createLostItemArticle( - @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer studentId, - @RequestBody @Valid LostItemArticlesRequest lostItemArticlesRequest - ) { - LostItemArticleResponse response = articleService.createLostItemArticle(studentId, lostItemArticlesRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @DeleteMapping("/lost-item/{id}") - public ResponseEntity deleteLostItemArticle( - @PathVariable("id") Integer articleId, - @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId - ) { - articleService.deleteLostItemArticle(articleId, userId); - return ResponseEntity.noContent().build(); - } - - @PostMapping("/lost-item/{id}/found") - public ResponseEntity markLostItemArticleAsFound( - @PathVariable("id") Integer articleId, - @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId - ) { - lostItemFoundService.markAsFound(userId, articleId); - return ResponseEntity.noContent().build(); - } - - @GetMapping("/lost-item/found/count") - public ResponseEntity getFoundLostItemArticlesCount() { - FoundLostItemArticleCountResponse response = lostItemFoundService.countFoundArticles(); - return ResponseEntity.ok().body(response); - } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleApi.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleApi.java new file mode 100644 index 000000000..ef569c838 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleApi.java @@ -0,0 +1,165 @@ +package in.koreatech.koin.domain.community.article.controller; + +import static in.koreatech.koin.domain.user.model.UserType.*; +import static in.koreatech.koin.global.code.ApiResponseCode.*; +import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; + +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticleStatisticsResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; +import in.koreatech.koin.domain.community.article.dto.LostItemArticlesResponse; +import in.koreatech.koin.domain.community.article.model.filter.LostItemAuthorFilter; +import in.koreatech.koin.domain.community.article.model.filter.LostItemCategoryFilter; +import in.koreatech.koin.domain.community.article.model.filter.LostItemFoundStatus; +import in.koreatech.koin.domain.community.article.model.filter.LostItemSortType; +import in.koreatech.koin.global.auth.Auth; +import in.koreatech.koin.global.auth.UserId; +import in.koreatech.koin.global.code.ApiResponseCodes; +import in.koreatech.koin.global.ipaddress.IpAddress; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "(Normal) LostItem Articles: 분실물 게시글", description = "분실물 게시글 정보를 관리한다") +@RequestMapping("/articles") +public interface LostItemArticleApi { + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + } + ) + @Operation(summary = "분실물 게시글 검색") + @GetMapping("/lost-item/search") + ResponseEntity searchArticles( + @RequestParam String query, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit, + @IpAddress String ipAddress, + @UserId Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "분실물 게시글 목록 조회") + @GetMapping("/lost-item") + ResponseEntity getLostItemArticles( + @RequestParam(required = false) String type, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit, + @UserId Integer userId + ); + + @ApiResponseCodes({ + OK, + UNAUTHORIZED_USER, + }) + @Operation(summary = "분실물 게시글 목록 조회 V2", description = """ + ### 분실물 게시글 목록 조회 V2 변경점 + - Request Param 추가: foundStatus, category, sort, authorType + - 내 게시물 필터를 설정할 경우, 토큰을 포함하여 요청하지 않으면 401 응답이 반환됩니다 + """) + @GetMapping("/lost-item/v2") + ResponseEntity getLostItemArticlesV2( + @Parameter(description = "분실물 타입 (LOST: 분실물, FOUND: 습득물)") + @RequestParam(required = false) String type, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit, + @RequestParam(required = false, name = "category", defaultValue = "ALL") LostItemCategoryFilter itemCategory, + @Parameter(description = "물품 상태 (ALL: 전체, FOUND: 찾음, NOT_FOUND: 찾는 중)") + @RequestParam(required = false, defaultValue = "ALL") LostItemFoundStatus foundStatus, + @Parameter(description = "정렬 순서 (LATEST: 최신순(default), OLDEST: 오래된순)") + @RequestParam(required = false, defaultValue = "LATEST") LostItemSortType sort, + @Parameter(description = "내 게시물 (ALL: 전체, MY: 내 게시물)") + @RequestParam(required = false, name = "author", defaultValue = "ALL") LostItemAuthorFilter authorType, + @Parameter(description = "게시글 제목") + @RequestParam(required = false) String title, + @UserId Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "분실물 게시글 단건 조회") + @GetMapping("/lost-item/{id}") + ResponseEntity getLostItemArticle( + @Parameter(in = PATH) @PathVariable("id") Integer articleId, + @UserId Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "201"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "분실물 게시글 등록") + @PostMapping("/lost-item") + ResponseEntity createLostItemArticle( + @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId, + @RequestBody @Valid LostItemArticlesRequest lostItemArticlesRequest + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "분실물 게시글 삭제") + @DeleteMapping("/lost-item/{id}") + ResponseEntity deleteLostItemArticle( + @PathVariable("id") Integer articleId, + @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer councilId + ); + + @ApiResponseCodes({ + NO_CONTENT, + FORBIDDEN_AUTHOR, + DUPLICATE_FOUND_STATUS + }) + @Operation(summary = "분실물 게시글 찾음 처리") + @PostMapping("/lost-item/{id}/found") + ResponseEntity markLostItemArticleAsFound( + @PathVariable("id") Integer articleId, + @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId + ); + + @ApiResponseCodes({ + OK + }) + @Operation(summary = "분실물 게시글 통계 조회", description = """ + ### 분실물 게시글 통계 조회 + - found_count : 주인 찾음 상태의 게시글 개수 + - not_found_count : 주인 찾는 중 상태의 게시글 개수 + """) + @GetMapping("/lost-item/stats") + ResponseEntity getLostItemArticlesStats(); +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleController.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleController.java new file mode 100644 index 000000000..e044cc1de --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleController.java @@ -0,0 +1,121 @@ +package in.koreatech.koin.domain.community.article.controller; + +import static in.koreatech.koin.domain.user.model.UserType.*; + +import org.springframework.http.HttpStatus; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticleStatisticsResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; +import in.koreatech.koin.domain.community.article.dto.LostItemArticlesResponse; +import in.koreatech.koin.domain.community.article.model.filter.LostItemAuthorFilter; +import in.koreatech.koin.domain.community.article.model.filter.LostItemCategoryFilter; +import in.koreatech.koin.domain.community.article.model.filter.LostItemFoundStatus; +import in.koreatech.koin.domain.community.article.model.filter.LostItemSortType; +import in.koreatech.koin.domain.community.article.service.LostItemArticleService; +import in.koreatech.koin.global.auth.Auth; +import in.koreatech.koin.global.auth.UserId; +import in.koreatech.koin.global.ipaddress.IpAddress; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/articles") +public class LostItemArticleController implements LostItemArticleApi { + + private final LostItemArticleService lostItemArticleService; + + @GetMapping("/lost-item/search") + public ResponseEntity searchArticles( + @RequestParam String query, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit, + @IpAddress String ipAddress, + @UserId Integer userId + ) { + LostItemArticlesResponse foundArticles = lostItemArticleService.searchLostItemArticles(query, page, limit, ipAddress, + userId); + return ResponseEntity.ok().body(foundArticles); + } + + @GetMapping("/lost-item") + public ResponseEntity getLostItemArticles( + @RequestParam(required = false) String type, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit, + @UserId Integer userId + ) { + LostItemArticlesResponse response = lostItemArticleService.getLostItemArticles(type, page, limit, userId); + return ResponseEntity.ok().body(response); + } + + @GetMapping("/lost-item/v2") + public ResponseEntity getLostItemArticlesV2( + @Parameter(description = "분실물 타입 (LOST: 분실물, FOUND: 습득물)") @RequestParam(required = false) String type, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit, + @RequestParam(required = false, name = "category", defaultValue = "ALL") LostItemCategoryFilter itemCategory, + @RequestParam(required = false, defaultValue = "ALL") LostItemFoundStatus foundStatus, + @RequestParam(required = false, name = "sort", defaultValue = "LATEST") LostItemSortType sort, + @RequestParam(required = false, name = "author", defaultValue = "ALL") LostItemAuthorFilter authorType, + @RequestParam(required = false, name = "title") String title, + @UserId Integer userId + ) { + LostItemArticlesResponse response = lostItemArticleService.getLostItemArticlesV2(type, page, limit, userId, + foundStatus, itemCategory, sort, authorType, title); + return ResponseEntity.ok().body(response); + } + + @GetMapping("/lost-item/{id}") + public ResponseEntity getLostItemArticle( + @PathVariable("id") Integer articleId, + @UserId Integer userId + ) { + return ResponseEntity.ok().body(lostItemArticleService.getLostItemArticle(articleId, userId)); + } + + @PostMapping("/lost-item") + public ResponseEntity createLostItemArticle( + @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer studentId, + @RequestBody @Valid LostItemArticlesRequest lostItemArticlesRequest + ) { + LostItemArticleResponse response = lostItemArticleService.createLostItemArticle(studentId, + lostItemArticlesRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @DeleteMapping("/lost-item/{id}") + public ResponseEntity deleteLostItemArticle( + @PathVariable("id") Integer articleId, + @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId + ) { + lostItemArticleService.deleteLostItemArticle(articleId, userId); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/lost-item/{id}/found") + public ResponseEntity markLostItemArticleAsFound( + @PathVariable("id") Integer articleId, + @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId + ) { + lostItemArticleService.markLostItemArticleAsFound(userId, articleId); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/lost-item/stats") + public ResponseEntity getLostItemArticlesStats() { + LostItemArticleStatisticsResponse response = lostItemArticleService.getLostItemArticlesStats(); + return ResponseEntity.ok().body(response); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemReportApi.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemReportApi.java index 9afb70e1b..44db8448e 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemReportApi.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemReportApi.java @@ -15,7 +15,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -@Tag(name = "(Normal) Articles: 게시글", description = "게시글 정보를 관리한다") +@Tag(name = "(Normal) LostItem Articles: 분실물 게시글", description = "분실물 게시글 정보를 관리한다") public interface LostItemReportApi { @Operation(summary = "분실물 게시글 신고하기") diff --git a/src/main/java/in/koreatech/koin/domain/community/article/dto/FoundLostItemArticleCountResponse.java b/src/main/java/in/koreatech/koin/domain/community/article/dto/LostItemArticleStatisticsResponse.java similarity index 69% rename from src/main/java/in/koreatech/koin/domain/community/article/dto/FoundLostItemArticleCountResponse.java rename to src/main/java/in/koreatech/koin/domain/community/article/dto/LostItemArticleStatisticsResponse.java index 45af929d2..90cbdcf8f 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/dto/FoundLostItemArticleCountResponse.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/dto/LostItemArticleStatisticsResponse.java @@ -8,10 +8,12 @@ import io.swagger.v3.oas.annotations.media.Schema; @JsonNaming(SnakeCaseStrategy.class) -public record FoundLostItemArticleCountResponse( +public record LostItemArticleStatisticsResponse( @Schema(description = "찾음 상태의 분실물 게시글 개수", example = "13", requiredMode = REQUIRED) - Integer foundCount -) { + Integer foundCount, + @Schema(description = "찾는 중 상태의 분실물 게시글 개수", example = "36", requiredMode = REQUIRED) + Integer notFoundCount +) { } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemAuthorFilter.java b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemAuthorFilter.java new file mode 100644 index 000000000..172cb673b --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemAuthorFilter.java @@ -0,0 +1,29 @@ +package in.koreatech.koin.domain.community.article.model.filter; + +import in.koreatech.koin.global.code.ApiResponseCode; +import in.koreatech.koin.global.exception.CustomException; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum LostItemAuthorFilter { + + ALL { + @Override + public Integer getRequiredAuthorId(Integer userId) { + return null; + } + }, + MY { + @Override + public Integer getRequiredAuthorId(Integer userId) { + if (userId == null) { + throw CustomException.of(ApiResponseCode.UNAUTHORIZED_USER); + } + return userId; + } + }; + + public abstract Integer getRequiredAuthorId(Integer userId); +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemCategoryFilter.java b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemCategoryFilter.java new file mode 100644 index 000000000..2757fdaee --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemCategoryFilter.java @@ -0,0 +1,18 @@ +package in.koreatech.koin.domain.community.article.model.filter; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum LostItemCategoryFilter { + + ALL(null), + CARD("카드"), + ID("신분증"), + WALLET("지갑"), + ELECTRONICS("전자제품"), + ETC("기타"); + + private String status; +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/LostItemFoundStatus.java b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemFoundStatus.java similarity index 79% rename from src/main/java/in/koreatech/koin/domain/community/article/model/LostItemFoundStatus.java rename to src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemFoundStatus.java index 5488eaa06..85cb876aa 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/model/LostItemFoundStatus.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemFoundStatus.java @@ -1,4 +1,4 @@ -package in.koreatech.koin.domain.community.article.model; +package in.koreatech.koin.domain.community.article.model.filter; public enum LostItemFoundStatus { diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemSortType.java b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemSortType.java new file mode 100644 index 000000000..e22eeefcd --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/filter/LostItemSortType.java @@ -0,0 +1,13 @@ +package in.koreatech.koin.domain.community.article.model.filter; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum LostItemSortType { + LATEST("LATEST"), + OLDEST("OLDEST"); + + private final String value; +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepository.java index 05973b594..e7360b4f1 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepository.java @@ -6,12 +6,15 @@ import in.koreatech.koin.domain.community.article.dto.LostItemArticleSummary; import in.koreatech.koin.domain.community.article.model.Article; +import in.koreatech.koin.domain.community.article.model.filter.LostItemSortType; public interface LostItemArticleCustomRepository { LostItemArticleSummary getArticleSummary(Integer articleId); - Long countLostItemArticlesWithFilters(String type, Boolean isFound, Integer lostItemArticleBoardId); + Long countLostItemArticlesWithFilters(String type, Boolean isFound, String itemCategory, + Integer lostItemArticleBoardId, Integer authorId, String titleQuery); - List
findLostItemArticlesWithFilters(Integer boardId, String type, Boolean isFound, PageRequest pageRequest); + List
findLostItemArticlesWithFilters(Integer boardId, String type, Boolean isFound, + String itemCategoryFilter, LostItemSortType sort, PageRequest pageRequest, Integer authorId, String titleQuery); } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepositoryImpl.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepositoryImpl.java index a93a08965..19d8b0a16 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepositoryImpl.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleCustomRepositoryImpl.java @@ -6,17 +6,17 @@ import java.util.List; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Repository; +import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import in.koreatech.koin.domain.community.article.dto.LostItemArticleSummary; import in.koreatech.koin.domain.community.article.model.Article; +import in.koreatech.koin.domain.community.article.model.filter.LostItemSortType; import lombok.RequiredArgsConstructor; @Repository @@ -38,8 +38,9 @@ public LostItemArticleSummary getArticleSummary(Integer articleId) { .fetchFirst(); } - public Long countLostItemArticlesWithFilters(String type, Boolean isFound, Integer lostItemArticleBoardId) { - BooleanExpression filter = getFilter(lostItemArticleBoardId, type, isFound); + public Long countLostItemArticlesWithFilters(String type, Boolean isFound, String itemCategory, + Integer lostItemArticleBoardId, Integer authorId, String titleQuery) { + BooleanExpression filter = getFilter(lostItemArticleBoardId, type, itemCategory, isFound, authorId, titleQuery); return queryFactory .select(article.count()) @@ -49,23 +50,25 @@ public Long countLostItemArticlesWithFilters(String type, Boolean isFound, Integ .fetchOne(); } - public List
findLostItemArticlesWithFilters( - Integer boardId, String type, Boolean isFound, PageRequest pageRequest) { + public List
findLostItemArticlesWithFilters(Integer boardId, String type, Boolean isFound, + String itemCategory, LostItemSortType sort, PageRequest pageRequest, Integer authorId, String titleQuery) { - BooleanExpression predicate = getFilter(boardId, type, isFound); + BooleanExpression predicate = getFilter(boardId, type, itemCategory, isFound, authorId, titleQuery); + OrderSpecifier[] orderSpecifiers = getOrderSpecifiers(sort); return queryFactory .selectFrom(article) .leftJoin(article.lostItemArticle, lostItemArticle).fetchJoin() .leftJoin(lostItemArticle.author).fetchJoin() .where(predicate) - .orderBy(article.createdAt.desc(), article.id.desc()) + .orderBy(orderSpecifiers) .offset(pageRequest.getOffset()) .limit(pageRequest.getPageSize()) .fetch(); } - private BooleanExpression getFilter(Integer boardId, String type, Boolean isFound) { + private BooleanExpression getFilter(Integer boardId, String type, String itemCategory, Boolean isFound, + Integer authorId, String titleQuery) { BooleanExpression filter = article.board.id.eq(boardId) .and(article.isDeleted.isFalse()) .and(article.lostItemArticle.isNotNull()); @@ -78,6 +81,32 @@ private BooleanExpression getFilter(Integer boardId, String type, Boolean isFoun filter = filter.and(lostItemArticle.isFound.eq(isFound)); } + if (itemCategory != null && !itemCategory.isBlank()) { + filter = filter.and(lostItemArticle.category.eq(itemCategory)); + } + + if (authorId != null) { + filter = filter.and(lostItemArticle.author.id.eq(authorId)); + } + + if (titleQuery != null && !titleQuery.isBlank()) { + filter = filter.and(article.title.containsIgnoreCase(titleQuery)); + } + return filter; } + + private OrderSpecifier[] getOrderSpecifiers(LostItemSortType sort) { + if (sort == LostItemSortType.OLDEST) { + return new OrderSpecifier[] { + article.createdAt.asc(), + article.id.asc() + }; + } + + return new OrderSpecifier[] { + article.createdAt.desc(), + article.id.desc() + }; + } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleRepository.java index 2b42158c8..5bf6bbb5b 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/LostItemArticleRepository.java @@ -23,8 +23,14 @@ default LostItemArticle getByArticleId(Integer articleId) { } @Query( - value = "SELECT count(*) FROM lost_item_articles WHERE is_found = 1 AND is_deleted = 0", + value = "SELECT count(*) FROM lost_item_articles WHERE is_found = 1 AND is_deleted = 0 AND author_id is not null", nativeQuery = true ) Integer getFoundLostItemArticleCount(); + + @Query( + value = "SELECT count(*) FROM lost_item_articles WHERE is_found = 0 AND is_deleted = 0 AND author_id is not null", + nativeQuery = true + ) + Integer getNotFoundLostItemArticleCount(); } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java index df235c6be..71b1116c1 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java @@ -6,12 +6,9 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; @@ -21,26 +18,16 @@ import in.koreatech.koin.domain.community.article.dto.ArticleResponse; import in.koreatech.koin.domain.community.article.dto.ArticlesResponse; import in.koreatech.koin.domain.community.article.dto.HotArticleItemResponse; -import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; -import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; -import in.koreatech.koin.domain.community.article.dto.LostItemArticlesResponse; import in.koreatech.koin.domain.community.article.exception.ArticleBoardMisMatchException; import in.koreatech.koin.domain.community.article.model.Article; import in.koreatech.koin.domain.community.article.model.Board; -import in.koreatech.koin.domain.community.article.model.LostItemFoundStatus; import in.koreatech.koin.domain.community.article.model.redis.ArticleHitUser; import in.koreatech.koin.domain.community.article.model.redis.PopularKeywordTracker; import in.koreatech.koin.domain.community.article.model.KeywordRankingManager; import in.koreatech.koin.domain.community.article.repository.ArticleRepository; import in.koreatech.koin.domain.community.article.repository.BoardRepository; -import in.koreatech.koin.domain.community.article.repository.LostItemArticleRepository; import in.koreatech.koin.domain.community.article.repository.redis.ArticleHitUserRepository; import in.koreatech.koin.domain.community.article.repository.redis.HotArticleRepository; -import in.koreatech.koin.common.event.ArticleKeywordEvent; -import in.koreatech.koin.domain.community.util.KeywordExtractor; -import in.koreatech.koin.domain.user.model.User; -import in.koreatech.koin.domain.user.repository.UserRepository; -import in.koreatech.koin.global.auth.exception.AuthorizationException; import in.koreatech.koin.global.exception.custom.KoinIllegalArgumentException; import in.koreatech.koin.common.model.Criteria; import in.koreatech.koin.infrastructure.s3.client.S3Client; @@ -51,7 +38,6 @@ @Transactional(readOnly = true) public class ArticleService { - public static final int LOST_ITEM_BOARD_ID = 14; public static final int NOTICE_BOARD_ID = 4; private static final int HOT_ARTICLE_BEFORE_DAYS = 30; private static final int HOT_ARTICLE_LIMIT = 10; @@ -63,16 +49,13 @@ public class ArticleService { Sort.Order.desc("id") ); - private final ApplicationEventPublisher eventPublisher; + private final ArticleRepository articleRepository; - private final LostItemArticleRepository lostItemArticleRepository; private final BoardRepository boardRepository; private final HotArticleRepository hotArticleRepository; private final ArticleHitUserRepository articleHitUserRepository; - private final UserRepository userRepository; private final Clock clock; private final S3Client s3Client; - private final KeywordExtractor keywordExtractor; private final PopularKeywordTracker popularKeywordTracker; private final KeywordRankingManager keywordRankingManager; @@ -161,103 +144,12 @@ public ArticleHotKeywordResponse getArticlesHotKeyword(int count) { return ArticleHotKeywordResponse.from(topKeywords); } - @Transactional - public LostItemArticlesResponse searchLostItemArticles(String query, Integer page, Integer limit, - String ipAddress, Integer userId) { - verifyQueryLength(query); - Criteria criteria = Criteria.of(page, limit); - PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), NATIVE_ARTICLES_SORT); - Page
articles = articleRepository.findAllByBoardIdAndTitleContaining(LOST_ITEM_BOARD_ID, query, - pageRequest); - - String[] keywords = query.split("\\s+"); - - for (String keyword : keywords) { - popularKeywordTracker.updateKeywordWeight(ipAddress, keyword); - } - - return LostItemArticlesResponse.of(articles, criteria, userId); - } - private void verifyQueryLength(String query) { if (query.length() >= MAXIMUM_SEARCH_LENGTH) { throw new KoinIllegalArgumentException("검색어의 최대 길이를 초과했습니다."); } } - public LostItemArticlesResponse getLostItemArticles(String type, Integer page, Integer limit, Integer userId) { - Long total = articleRepository.countBy(); - Criteria criteria = Criteria.of(page, limit, total.intValue()); - PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), ARTICLES_SORT); - Page
articles; - - if (type == null) { - articles = articleRepository.findAllByBoardId(LOST_ITEM_BOARD_ID, pageRequest); - } else { - articles = articleRepository.findAllByLostItemArticleType(type, pageRequest); - } - - return LostItemArticlesResponse.of(articles, criteria, userId); - } - - public LostItemArticlesResponse getLostItemArticlesV2(String type, Integer page, Integer limit, Integer userId, - LostItemFoundStatus foundStatus) { - Boolean foundStatusFilter = Optional.ofNullable(foundStatus) - .map(LostItemFoundStatus::getQueryStatus) - .orElse(null); - - Long total = lostItemArticleRepository.countLostItemArticlesWithFilters(type, foundStatusFilter, - LOST_ITEM_BOARD_ID); - - Criteria criteria = Criteria.of(page, limit, total.intValue()); - PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), ARTICLES_SORT); - - List
articles = lostItemArticleRepository.findLostItemArticlesWithFilters(LOST_ITEM_BOARD_ID, type, - foundStatusFilter, pageRequest); - Page
articlePage = new PageImpl<>(articles, pageRequest, total); - - return LostItemArticlesResponse.of(articlePage, criteria, userId); - } - - public LostItemArticleResponse getLostItemArticle(Integer articleId, Integer userId) { - Article article = articleRepository.getById(articleId); - setPrevNextArticle(LOST_ITEM_BOARD_ID, article); - - boolean isMine = false; - User author = article.getLostItemArticle().getAuthor(); - if (author != null && Objects.equals(author.getId(), userId)) { - isMine = true; - } - - return LostItemArticleResponse.of(article, isMine); - } - - @Transactional - public LostItemArticleResponse createLostItemArticle(Integer userId, LostItemArticlesRequest requests) { - Board lostItemBoard = boardRepository.getById(LOST_ITEM_BOARD_ID); - User user = userRepository.getById(userId); - List
newArticles = new ArrayList<>(); - - for (var article : requests.articles()) { - Article lostItemArticle = Article.createLostItemArticle(article, lostItemBoard, user); - articleRepository.save(lostItemArticle); - newArticles.add(lostItemArticle); - } - - sendKeywordNotification(newArticles, userId); - return LostItemArticleResponse.of(newArticles.get(0), true); - } - - @Transactional - public void deleteLostItemArticle(Integer articleId, Integer userId) { - Article foundArticle = articleRepository.getById(articleId); - User author = foundArticle.getLostItemArticle().getAuthor(); - if (!Objects.equals(author.getId(), userId)) { - throw AuthorizationException.withDetail("userId: " + userId); - } - foundArticle.delete(); - } - private void setPrevNextArticle(Integer boardId, Article article) { Article prevArticle; Article nextArticle; @@ -282,13 +174,4 @@ private Board getBoard(Integer boardId, Article article) { } return boardRepository.getById(boardId); } - - private void sendKeywordNotification(List
articles, Integer authorId) { - List keywordEvents = keywordExtractor.matchKeyword(articles, authorId); - if (!keywordEvents.isEmpty()) { - for (ArticleKeywordEvent event : keywordEvents) { - eventPublisher.publishEvent(event); - } - } - } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java new file mode 100644 index 000000000..1b3135641 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java @@ -0,0 +1,215 @@ +package in.koreatech.koin.domain.community.article.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.common.event.ArticleKeywordEvent; +import in.koreatech.koin.common.model.Criteria; +import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticleStatisticsResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; +import in.koreatech.koin.domain.community.article.dto.LostItemArticlesResponse; +import in.koreatech.koin.domain.community.article.exception.ArticleBoardMisMatchException; +import in.koreatech.koin.domain.community.article.model.Article; +import in.koreatech.koin.domain.community.article.model.Board; +import in.koreatech.koin.domain.community.article.model.LostItemArticle; +import in.koreatech.koin.domain.community.article.model.filter.LostItemAuthorFilter; +import in.koreatech.koin.domain.community.article.model.filter.LostItemFoundStatus; +import in.koreatech.koin.domain.community.article.model.filter.LostItemCategoryFilter; +import in.koreatech.koin.domain.community.article.model.filter.LostItemSortType; +import in.koreatech.koin.domain.community.article.model.redis.PopularKeywordTracker; +import in.koreatech.koin.domain.community.article.repository.ArticleRepository; +import in.koreatech.koin.domain.community.article.repository.BoardRepository; +import in.koreatech.koin.domain.community.article.repository.LostItemArticleRepository; +import in.koreatech.koin.domain.community.util.KeywordExtractor; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.koin.global.auth.exception.AuthorizationException; +import in.koreatech.koin.global.exception.custom.KoinIllegalArgumentException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class LostItemArticleService { + + public static final int LOST_ITEM_BOARD_ID = 14; + private static final int MAXIMUM_SEARCH_LENGTH = 100; + public static final int NOTICE_BOARD_ID = 4; + private static final Sort NATIVE_ARTICLES_SORT = Sort.by( + Sort.Order.desc("id") + ); + private static final Sort ARTICLES_SORT = Sort.by( + Sort.Order.desc("id") + ); + + private final ArticleRepository articleRepository; + private final LostItemArticleRepository lostItemArticleRepository; + private final BoardRepository boardRepository; + private final UserRepository userRepository; + private final PopularKeywordTracker popularKeywordTracker; + private final ApplicationEventPublisher eventPublisher; + private final KeywordExtractor keywordExtractor; + + @Transactional + public LostItemArticlesResponse searchLostItemArticles(String query, Integer page, Integer limit, + String ipAddress, Integer userId) { + if (query.length() >= MAXIMUM_SEARCH_LENGTH) { + throw new KoinIllegalArgumentException("검색어의 최대 길이를 초과했습니다."); + } + Criteria criteria = Criteria.of(page, limit); + PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), NATIVE_ARTICLES_SORT); + Page
articles = articleRepository.findAllByBoardIdAndTitleContaining(LOST_ITEM_BOARD_ID, query, + pageRequest); + + String[] keywords = query.split("\\s+"); + + for (String keyword : keywords) { + popularKeywordTracker.updateKeywordWeight(ipAddress, keyword); + } + + return LostItemArticlesResponse.of(articles, criteria, userId); + } + + public LostItemArticlesResponse getLostItemArticles(String type, Integer page, Integer limit, Integer userId) { + Long total = articleRepository.countBy(); + Criteria criteria = Criteria.of(page, limit, total.intValue()); + PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), ARTICLES_SORT); + Page
articles; + + if (type == null) { + articles = articleRepository.findAllByBoardId(LOST_ITEM_BOARD_ID, pageRequest); + } else { + articles = articleRepository.findAllByLostItemArticleType(type, pageRequest); + } + + return LostItemArticlesResponse.of(articles, criteria, userId); + } + + public LostItemArticlesResponse getLostItemArticlesV2(String type, Integer page, Integer limit, Integer userId, + LostItemFoundStatus foundStatus, LostItemCategoryFilter itemCategory, LostItemSortType sort, + LostItemAuthorFilter authorType, String titleQuery) { + Integer authorIdFilter = authorType.getRequiredAuthorId(userId); + + String refinedTitleQuery = Optional.ofNullable(titleQuery) + .map(String::trim) + .orElse(null); + + Boolean foundStatusFilter = Optional.ofNullable(foundStatus) + .map(LostItemFoundStatus::getQueryStatus) + .orElse(null); + + String itemCategoryFilter = Optional.ofNullable(itemCategory) + .filter(category -> category != LostItemCategoryFilter.ALL) + .map(LostItemCategoryFilter::getStatus) + .orElse(null); + + Long total = lostItemArticleRepository.countLostItemArticlesWithFilters(type, foundStatusFilter, + itemCategoryFilter, LOST_ITEM_BOARD_ID, authorIdFilter, refinedTitleQuery); + + Criteria criteria = Criteria.of(page, limit, total.intValue()); + PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit()); + + List
articles = lostItemArticleRepository.findLostItemArticlesWithFilters(LOST_ITEM_BOARD_ID, type, + foundStatusFilter, itemCategoryFilter, sort, pageRequest, authorIdFilter, refinedTitleQuery); + Page
articlePage = new PageImpl<>(articles, pageRequest, total); + + return LostItemArticlesResponse.of(articlePage, criteria, userId); + } + + public LostItemArticleResponse getLostItemArticle(Integer articleId, Integer userId) { + Article article = articleRepository.getById(articleId); + setPrevNextArticle(LOST_ITEM_BOARD_ID, article); + + boolean isMine = false; + User author = article.getLostItemArticle().getAuthor(); + if (author != null && Objects.equals(author.getId(), userId)) { + isMine = true; + } + + return LostItemArticleResponse.of(article, isMine); + } + + @Transactional + public LostItemArticleResponse createLostItemArticle(Integer userId, LostItemArticlesRequest requests) { + Board lostItemBoard = boardRepository.getById(LOST_ITEM_BOARD_ID); + User user = userRepository.getById(userId); + List
newArticles = new ArrayList<>(); + + for (var article : requests.articles()) { + Article lostItemArticle = Article.createLostItemArticle(article, lostItemBoard, user); + articleRepository.save(lostItemArticle); + newArticles.add(lostItemArticle); + } + + sendKeywordNotification(newArticles, userId); + return LostItemArticleResponse.of(newArticles.get(0), true); + } + + @Transactional + public void deleteLostItemArticle(Integer articleId, Integer userId) { + Article foundArticle = articleRepository.getById(articleId); + User author = foundArticle.getLostItemArticle().getAuthor(); + if (!Objects.equals(author.getId(), userId)) { + throw AuthorizationException.withDetail("userId: " + userId); + } + foundArticle.delete(); + } + + public LostItemArticleStatisticsResponse getLostItemArticlesStats() { + Integer foundCount = lostItemArticleRepository.getFoundLostItemArticleCount(); + Integer notFoundCount = lostItemArticleRepository.getNotFoundLostItemArticleCount(); + return new LostItemArticleStatisticsResponse(foundCount, notFoundCount); + } + + @Transactional + public void markLostItemArticleAsFound(Integer userId, Integer articleId) { + LostItemArticle lostItemArticle = articleRepository.getById(articleId).getLostItemArticle(); + lostItemArticle.checkOwnership(userId); + lostItemArticle.markAsFound(); + } + + private void setPrevNextArticle(Integer boardId, Article article) { + Article prevArticle; + Article nextArticle; + if (boardId != null) { + Board board = getBoard(boardId, article); + prevArticle = articleRepository.getPreviousArticle(board, article); + nextArticle = articleRepository.getNextArticle(board, article); + } else { + prevArticle = articleRepository.getPreviousAllArticle(article); + nextArticle = articleRepository.getNextAllArticle(article); + } + article.setPrevNextArticles(prevArticle, nextArticle); + } + + private Board getBoard(Integer boardId, Article article) { + if (boardId == null) { + boardId = article.getBoard().getId(); + } + if (!Objects.equals(boardId, article.getBoard().getId()) + && (!article.getBoard().isNotice() || boardId != NOTICE_BOARD_ID)) { + throw ArticleBoardMisMatchException.withDetail("boardId: " + boardId + ", articleId: " + article.getId()); + } + return boardRepository.getById(boardId); + } + + private void sendKeywordNotification(List
articles, Integer authorId) { + List keywordEvents = keywordExtractor.matchKeyword(articles, authorId); + if (!keywordEvents.isEmpty()) { + for (ArticleKeywordEvent event : keywordEvents) { + eventPublisher.publishEvent(event); + } + } + } +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemFoundService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemFoundService.java deleted file mode 100644 index d2fc8cc6b..000000000 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemFoundService.java +++ /dev/null @@ -1,31 +0,0 @@ -package in.koreatech.koin.domain.community.article.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.domain.community.article.dto.FoundLostItemArticleCountResponse; -import in.koreatech.koin.domain.community.article.model.LostItemArticle; -import in.koreatech.koin.domain.community.article.repository.ArticleRepository; -import in.koreatech.koin.domain.community.article.repository.LostItemArticleRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class LostItemFoundService { - - private final ArticleRepository articleRepository; - private final LostItemArticleRepository lostItemArticleRepository; - - @Transactional - public void markAsFound(Integer userId, Integer articleId) { - LostItemArticle lostItemArticle = articleRepository.getById(articleId).getLostItemArticle(); - lostItemArticle.checkOwnership(userId); - lostItemArticle.markAsFound(); - } - - @Transactional(readOnly = true) - public FoundLostItemArticleCountResponse countFoundArticles() { - Integer foundCount = lostItemArticleRepository.getFoundLostItemArticleCount(); - return new FoundLostItemArticleCountResponse(foundCount); - } -} diff --git a/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java b/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java index 46fb02300..207dc2476 100644 --- a/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java +++ b/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java @@ -88,6 +88,7 @@ public enum ApiResponseCode { * 401 Unauthorized (인증 필요) */ WITHDRAWN_USER(HttpStatus.UNAUTHORIZED, "탈퇴한 계정입니다."), + UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), /** * 403 Forbidden (인가 필요)