Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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;
Expand All @@ -17,12 +18,15 @@
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;
Expand Down Expand Up @@ -134,6 +138,28 @@ ResponseEntity<LostItemArticlesResponse> getLostItemArticles(
@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<LostItemArticlesResponse> 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"),
Expand Down Expand Up @@ -178,4 +204,23 @@ ResponseEntity<Void> 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<Void> markLostItemArticleAsFound(
@PathVariable("id") Integer articleId,
@Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer userId
);

@ApiResponseCodes({
OK
})
@Operation(summary = "주인 찾음 상태인 분실물 게시글 총 개수 조회")
@GetMapping("/lost-item/found/count")
ResponseEntity<FoundLostItemArticleCountResponse> getFoundLostItemArticlesCount();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
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;
Expand All @@ -35,6 +38,7 @@
public class ArticleController implements ArticleApi {

private final ArticleService articleService;
private final LostItemFoundService lostItemFoundService;

@GetMapping("/{id}")
public ResponseEntity<ArticleResponse> getArticle(
Expand Down Expand Up @@ -108,6 +112,18 @@ public ResponseEntity<LostItemArticlesResponse> getLostItemArticles(
return ResponseEntity.ok().body(response);
}

@GetMapping("/lost-item/v2")
public ResponseEntity<LostItemArticlesResponse> 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<LostItemArticleResponse> getLostItemArticle(
@PathVariable("id") Integer articleId,
Expand All @@ -133,4 +149,19 @@ public ResponseEntity<Void> deleteLostItemArticle(
articleService.deleteLostItemArticle(articleId, userId);
return ResponseEntity.noContent().build();
}

@PostMapping("/lost-item/{id}/found")
public ResponseEntity<Void> 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<FoundLostItemArticleCountResponse> getFoundLostItemArticlesCount() {
FoundLostItemArticleCountResponse response = lostItemFoundService.countFoundArticles();
return ResponseEntity.ok().body(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package in.koreatech.koin.domain.community.article.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(SnakeCaseStrategy.class)
public record FoundLostItemArticleCountResponse(

@Schema(description = "찾음 상태의 분실물 게시글 개수", example = "13", requiredMode = REQUIRED)
Integer foundCount
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public record LostItemArticleResponse(
@Schema(description = "내 게시글 여부", example = "1", requiredMode = NOT_REQUIRED)
Boolean isMine,

@Schema(description = "분실물 게시글 찾음 상태 여부", example = "false", requiredMode = REQUIRED)
Boolean isFound,

@Schema(description = "분실물 사진")
List<InnerLostItemImageResponse> images,

Expand Down Expand Up @@ -77,6 +80,7 @@ public static LostItemArticleResponse of(Article article, Boolean isMine) {
article.getAuthor(),
lostItemArticle.getIsCouncil(),
isMine,
lostItemArticle.getIsFound(),
lostItemArticle.getImages().stream()
.map(InnerLostItemImageResponse::from)
.toList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ private record InnerLostItemArticleResponse(
LocalDate registeredAt,

@Schema(description = "처리되지 않은 자신의 신고 존재 여부", example = "true", requiredMode = REQUIRED)
Boolean isReported
Boolean isReported,

@Schema(description = "분실물 게시글 찾음 상태 여부", example = "false", requiredMode = REQUIRED)
Boolean isFound
) {

public static InnerLostItemArticleResponse of(Article article, Integer userId) {
Expand All @@ -91,7 +94,8 @@ public static InnerLostItemArticleResponse of(Article article, Integer userId) {
article.getContent(),
article.getAuthor(),
article.getRegisteredAt(),
lostItemArticle.isReportedByUserId(userId)
lostItemArticle.isReportedByUserId(userId),
lostItemArticle.getIsFound()
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package in.koreatech.koin.domain.community.article.model;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -11,6 +12,8 @@
import in.koreatech.koin.domain.shop.model.review.ReportStatus;
import in.koreatech.koin.domain.user.model.User;
import in.koreatech.koin.common.model.BaseEntity;
import in.koreatech.koin.global.code.ApiResponseCode;
import in.koreatech.koin.global.exception.CustomException;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -68,6 +71,13 @@ public class LostItemArticle extends BaseEntity {
@OneToMany(mappedBy = "lostItemArticle", cascade = CascadeType.ALL, orphanRemoval = true)
private List<LostItemImage> images = new ArrayList<>();

@NotNull
@Column(name = "is_found", nullable = false)
private Boolean isFound = false;

@Column(name = "found_at")
private LocalDateTime foundAt; // "찾음" 처리된 날짜

@NotNull
@Column(name = "is_council", nullable = false)
private Boolean isCouncil = false;
Expand Down Expand Up @@ -152,4 +162,18 @@ public boolean isReportedByUserId(Integer userId) {
.filter(report -> Objects.equals(report.getStudent().getId(), userId))
.anyMatch(report -> report.getReportStatus() == ReportStatus.UNHANDLED);
}

public void checkOwnership(Integer userId) {
if(!Objects.equals(author.getId(), userId)) {
throw CustomException.of(ApiResponseCode.FORBIDDEN_AUTHOR);
}
}

public void markAsFound() {
if (this.isFound) {
throw CustomException.of(ApiResponseCode.DUPLICATE_FOUND_STATUS);
}
this.isFound = true;
this.foundAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package in.koreatech.koin.domain.community.article.model;

public enum LostItemFoundStatus {

ALL,
FOUND,
NOT_FOUND;

public Boolean getQueryStatus() {
return switch (this) {
case FOUND -> true;
case NOT_FOUND -> false;
case ALL -> null;
};
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package in.koreatech.koin.domain.community.article.repository;

import java.util.List;

import org.springframework.data.domain.PageRequest;

import in.koreatech.koin.domain.community.article.dto.LostItemArticleSummary;
import in.koreatech.koin.domain.community.article.model.Article;

public interface LostItemArticleCustomRepository {

LostItemArticleSummary getArticleSummary(Integer articleId);

Long countLostItemArticlesWithFilters(String type, Boolean isFound, Integer lostItemArticleBoardId);

List<Article> findLostItemArticlesWithFilters(Integer boardId, String type, Boolean isFound, PageRequest pageRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
import static in.koreatech.koin.domain.community.article.model.QLostItemArticle.lostItemArticle;
import static in.koreatech.koin.domain.community.article.model.QLostItemImage.lostItemImage;

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.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 lombok.RequiredArgsConstructor;

@Repository
Expand All @@ -30,4 +37,47 @@ public LostItemArticleSummary getArticleSummary(Integer articleId) {
.where(article.id.eq(articleId))
.fetchFirst();
}

public Long countLostItemArticlesWithFilters(String type, Boolean isFound, Integer lostItemArticleBoardId) {
BooleanExpression filter = getFilter(lostItemArticleBoardId, type, isFound);

return queryFactory
.select(article.count())
.from(article)
.leftJoin(article.lostItemArticle, lostItemArticle)
.where(filter)
.fetchOne();
}

public List<Article> findLostItemArticlesWithFilters(
Integer boardId, String type, Boolean isFound, PageRequest pageRequest) {

BooleanExpression predicate = getFilter(boardId, type, isFound);

return queryFactory
.selectFrom(article)
.leftJoin(article.lostItemArticle, lostItemArticle).fetchJoin()
.leftJoin(lostItemArticle.author).fetchJoin()
.where(predicate)
.orderBy(article.createdAt.desc(), article.id.desc())
.offset(pageRequest.getOffset())
.limit(pageRequest.getPageSize())
.fetch();
}

private BooleanExpression getFilter(Integer boardId, String type, Boolean isFound) {
BooleanExpression filter = article.board.id.eq(boardId)
.and(article.isDeleted.isFalse())
.and(article.lostItemArticle.isNotNull());

if (type != null && !type.isBlank()) {
filter = filter.and(lostItemArticle.type.eq(type));
}

if (isFound != null) {
filter = filter.and(lostItemArticle.isFound.eq(isFound));
}

return filter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ default LostItemArticle getByArticleId(Integer articleId) {
return findByArticleId(articleId).orElseThrow(
() -> ArticleNotFoundException.withDetail("articleId: " + articleId));
}

@Query(
value = "SELECT count(*) FROM lost_item_articles WHERE is_found = 1 AND is_deleted = 0",
nativeQuery = true
)
Integer getFoundLostItemArticleCount();
}
Loading
Loading