Skip to content

Commit 25b90b5

Browse files
authored
Merge pull request #87 from Moongeul/feat/#86
[FEAT] ์ฝ์€์ฑ… ๋ณ„์  ๊ฐ„๋žต/์ƒ์„ธ ์กฐํšŒ ๊ตฌํ˜„
2 parents 4e81b92 + 62437b3 commit 25b90b5

8 files changed

Lines changed: 256 additions & 1 deletion

File tree

โ€Žsrc/main/java/com/moongeul/backend/api/bookshelf/controller/DoneReadBookshelfController.javaโ€Ž

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.moongeul.backend.api.bookshelf.controller;
22

33
import com.moongeul.backend.api.bookshelf.dto.DoneReadCalendarResponseDTO;
4+
import com.moongeul.backend.api.bookshelf.dto.DoneReadRatingSummaryResponseDTO;
45
import com.moongeul.backend.api.bookshelf.dto.DoneReadBookshelfResponseDTO;
56
import com.moongeul.backend.api.bookshelf.service.DoneReadBookshelfService;
7+
import com.moongeul.backend.api.post.dto.CategoryPostListResponseDTO;
68
import com.moongeul.backend.common.response.ApiResponse;
79
import com.moongeul.backend.common.response.SuccessStatus;
810
import io.swagger.v3.oas.annotations.Operation;
@@ -65,4 +67,49 @@ public ResponseEntity<ApiResponse<DoneReadCalendarResponseDTO>> getDoneReadCalen
6567
userDetails.getUsername(), year, month);
6668
return ApiResponse.success(SuccessStatus.GET_DONE_READ_CALENDAR_SUCCESS, doneReadCalendar);
6769
}
70+
71+
@Operation(
72+
summary = "์ฝ์€ ์ฑ… ๋ณ„์  ์š”์•ฝ ์กฐํšŒ API",
73+
description = "์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋กํ•œ ์ด ์ฑ… ์ˆ˜์™€ ๋ณ„์  ๊ตฌ๊ฐ„(1.0~1.4, 1.5~1.9, ... , 4.5~5.0)๋ณ„ ๊ธฐ๋ก ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค."
74+
)
75+
@ApiResponses({
76+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "์ฝ์€ ์ฑ… ๋ณ„์  ์š”์•ฝ ์กฐํšŒ ์„ฑ๊ณต"),
77+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
78+
})
79+
@GetMapping("/rating-summary")
80+
public ResponseEntity<ApiResponse<DoneReadRatingSummaryResponseDTO>> getDoneReadRatingSummary(
81+
@AuthenticationPrincipal UserDetails userDetails) {
82+
83+
DoneReadRatingSummaryResponseDTO doneReadRatingSummaryResponseDTO = doneReadBookshelfService.getDoneReadRatingSummary(userDetails.getUsername());
84+
return ApiResponse.success(SuccessStatus.GET_DONE_READ_RATING_SUMMARY_SUCCESS, doneReadRatingSummaryResponseDTO);
85+
}
86+
87+
@Operation(
88+
summary = "์ฝ์€ ์ฑ… ๋ณ„์  ๊ตฌ๊ฐ„ ์ƒ์„ธ ์กฐํšŒ API",
89+
description = "๋ณ„์  ๊ตฌ๊ฐ„(range)์— ํ•ด๋‹นํ•˜๋Š” ๊ธฐ๋ก ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. " +
90+
"์‘๋‹ต ํ˜•์‹์€ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ธฐ๋ก ๋ฆฌ์ŠคํŠธ ์กฐํšŒ์™€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค." +
91+
"<br><br>์˜ˆ์‹œ range: 1.0~1.4, 1.5~1.9, ... , 4.5~5.0" +
92+
"<br><br>[enum] ์ •๋ ฌ ์˜ต์…˜ (sortBy):" +
93+
"<br>- LATEST: ์ตœ์‹ ์ˆœ (๊ธฐ๋ณธ๊ฐ’)" +
94+
"<br>- OLDEST: ์˜ค๋ž˜๋œ์ˆœ" +
95+
"<br>- RATING_HIGH: ํ‰์  ๋†’์€์ˆœ" +
96+
"<br>- RATING_LOW: ํ‰์  ๋‚ฎ์€์ˆœ"
97+
)
98+
@ApiResponses({
99+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "์ฝ์€ ์ฑ… ๋ณ„์  ๊ตฌ๊ฐ„ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต"),
100+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ„์  ๊ตฌ๊ฐ„์ž…๋‹ˆ๋‹ค."),
101+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
102+
})
103+
@GetMapping("/rating-summary/details")
104+
public ResponseEntity<ApiResponse<CategoryPostListResponseDTO>> getDoneReadRatingDetail(
105+
@AuthenticationPrincipal UserDetails userDetails,
106+
@RequestParam String range,
107+
@RequestParam(defaultValue = "LATEST") String sortBy,
108+
@RequestParam(defaultValue = "1") @Min(value = 1, message = "ํŽ˜์ด์ง€๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") Integer page,
109+
@RequestParam(defaultValue = "10") @Min(value = 1, message = "ํ•œ ํŽ˜์ด์ง€๋‹น ๊ฐœ์ˆ˜๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") Integer size) {
110+
111+
CategoryPostListResponseDTO categoryPostListResponseDTO =
112+
doneReadBookshelfService.getDoneReadRatingDetail(userDetails.getUsername(), range, sortBy, page, size);
113+
return ApiResponse.success(SuccessStatus.GET_DONE_READ_RATING_DETAIL_SUCCESS, categoryPostListResponseDTO);
114+
}
68115
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.moongeul.backend.api.bookshelf.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@Builder
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
public class DoneReadRatingRangeCountDTO {
13+
private String range; // ๋ณ„์  ๊ตฌ๊ฐ„ (์˜ˆ: 1.0~1.4)
14+
private Integer count; // ๊ตฌ๊ฐ„๋ณ„ ๊ธฐ๋ก ์ˆ˜
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.moongeul.backend.api.bookshelf.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.List;
9+
10+
@Getter
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class DoneReadRatingSummaryResponseDTO {
15+
private Long totalBooks; // ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋กํ•œ ์ด ์ฑ… ์ˆ˜
16+
private List<DoneReadRatingRangeCountDTO> data; // ๋ณ„์  ๊ตฌ๊ฐ„๋ณ„ ๊ธฐ๋ก ์ˆ˜
17+
}

โ€Žsrc/main/java/com/moongeul/backend/api/bookshelf/repository/DoneReadBookshelfRepository.javaโ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ public interface DoneReadBookshelfRepository extends JpaRepository<DoneReadBooks
1717

1818
@Query("SELECT d FROM DoneReadBookshelf d WHERE d.member = :member AND d.article.book = :book")
1919
Optional<DoneReadBookshelf> findByMemberAndBook(@Param("member") Member member, @Param("book") Book book);
20-
}
2120

21+
long countByMember(Member member);
22+
}

โ€Žsrc/main/java/com/moongeul/backend/api/bookshelf/service/DoneReadBookshelfService.javaโ€Ž

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,38 @@
55
import com.moongeul.backend.api.bookshelf.dto.DoneReadCalendarResponseDTO;
66
import com.moongeul.backend.api.bookshelf.dto.DoneReadBookshelfItemDTO;
77
import com.moongeul.backend.api.bookshelf.dto.DoneReadBookshelfResponseDTO;
8+
import com.moongeul.backend.api.bookshelf.dto.DoneReadRatingRangeCountDTO;
9+
import com.moongeul.backend.api.bookshelf.dto.DoneReadRatingSummaryResponseDTO;
810
import com.moongeul.backend.api.bookshelf.entity.DoneReadBookshelf;
911
import com.moongeul.backend.api.bookshelf.repository.DoneReadBookshelfRepository;
1012
import com.moongeul.backend.api.member.entity.Member;
1113
import com.moongeul.backend.api.member.repository.MemberRepository;
14+
import com.moongeul.backend.api.post.dto.CategoryPostListResponseDTO;
15+
import com.moongeul.backend.api.post.dto.PostDTO;
1216
import com.moongeul.backend.api.post.entity.Post;
17+
import com.moongeul.backend.api.post.entity.Quote;
1318
import com.moongeul.backend.api.post.repository.PostRepository;
19+
import com.moongeul.backend.api.post.repository.QuoteRepository;
20+
import com.moongeul.backend.common.exception.BadRequestException;
1421
import com.moongeul.backend.common.exception.NotFoundException;
1522
import com.moongeul.backend.common.response.ErrorStatus;
1623
import lombok.RequiredArgsConstructor;
1724
import lombok.extern.slf4j.Slf4j;
1825
import org.springframework.data.domain.Page;
1926
import org.springframework.data.domain.PageRequest;
2027
import org.springframework.data.domain.Pageable;
28+
import org.springframework.data.domain.Sort;
2129
import org.springframework.stereotype.Service;
2230
import org.springframework.transaction.annotation.Transactional;
2331

2432
import java.time.LocalDate;
2533
import java.time.LocalDateTime;
2634
import java.time.YearMonth;
35+
import java.util.ArrayList;
2736
import java.util.LinkedHashMap;
2837
import java.util.List;
2938
import java.util.Map;
39+
import java.util.stream.Collectors;
3040

3141
@Slf4j
3242
@Service
@@ -36,6 +46,13 @@ public class DoneReadBookshelfService {
3646
private final DoneReadBookshelfRepository doneReadBookshelfRepository;
3747
private final MemberRepository memberRepository;
3848
private final PostRepository postRepository;
49+
private final QuoteRepository quoteRepository;
50+
private static final String[] RATING_RANGES = {
51+
"1.0~1.4", "1.5~1.9", "2.0~2.4", "2.5~2.9",
52+
"3.0~3.4", "3.5~3.9", "4.0~4.4", "4.5~5.0"
53+
};
54+
private static final double[] RANGE_STARTS = {1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5};
55+
private static final double[] RANGE_ENDS = {1.4, 1.9, 2.4, 2.9, 3.4, 3.9, 4.4, 5.0};
3956

4057
@Transactional(readOnly = true)
4158
public DoneReadBookshelfResponseDTO getDoneReadBooks(String email, Integer page, Integer size) {
@@ -137,6 +154,75 @@ public DoneReadCalendarResponseDTO getDoneReadCalendar(String email, Integer yea
137154
.build();
138155
}
139156

157+
// ์ฝ์€ ์ฑ… ๋ณ„์  ์š”์•ฝ ์กฐํšŒ
158+
@Transactional(readOnly = true)
159+
public DoneReadRatingSummaryResponseDTO getDoneReadRatingSummary(String email) {
160+
161+
Member member = memberRepository.findByEmail(email)
162+
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));
163+
164+
long totalBooks = doneReadBookshelfRepository.countByMember(member);
165+
List<Double> ratings = postRepository.findRatingsByMember(member);
166+
167+
int[] counts = new int[RATING_RANGES.length];
168+
for (Double rating : ratings) {
169+
if (rating == null) {
170+
continue;
171+
}
172+
173+
for (int i = 0; i < RATING_RANGES.length; i++) {
174+
if (rating >= RANGE_STARTS[i] && rating <= RANGE_ENDS[i] + 1e-9) {
175+
counts[i]++;
176+
break;
177+
}
178+
}
179+
}
180+
181+
List<DoneReadRatingRangeCountDTO> data = new ArrayList<>();
182+
for (int i = 0; i < RATING_RANGES.length; i++) {
183+
data.add(DoneReadRatingRangeCountDTO.builder()
184+
.range(RATING_RANGES[i])
185+
.count(counts[i])
186+
.build());
187+
}
188+
189+
return DoneReadRatingSummaryResponseDTO.builder()
190+
.totalBooks(totalBooks)
191+
.data(data)
192+
.build();
193+
}
194+
195+
// ์ฝ์€ ์ฑ… ๋ณ„์  ๊ตฌ๊ฐ„ ์ƒ์„ธ ์กฐํšŒ
196+
@Transactional(readOnly = true)
197+
public CategoryPostListResponseDTO getDoneReadRatingDetail(String email, String range, String sortBy, Integer page, Integer size) {
198+
199+
Member member = memberRepository.findByEmail(email)
200+
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));
201+
202+
int rangeIndex = findRangeIndex(range);
203+
Pageable pageable = PageRequest.of(page - 1, size, resolveSort(sortBy));
204+
205+
Page<Post> postPage = postRepository.findByMemberAndRatingBetween(
206+
member,
207+
RANGE_STARTS[rangeIndex],
208+
RANGE_ENDS[rangeIndex],
209+
pageable
210+
);
211+
212+
List<PostDTO> postList = postPage.getContent().stream()
213+
.map(this::convertToPostDTO)
214+
.collect(Collectors.toList());
215+
216+
return CategoryPostListResponseDTO.builder()
217+
.total(postPage.getTotalElements())
218+
.page(page)
219+
.size(size)
220+
.totalPages(postPage.getTotalPages())
221+
.isLast(postPage.isLast())
222+
.data(postList)
223+
.build();
224+
}
225+
140226
private DoneReadBookshelfItemDTO convertToItemDTO(DoneReadBookshelf doneReadBookshelf) {
141227
Book book = doneReadBookshelf.getArticle().getBook();
142228

@@ -152,6 +238,86 @@ private DoneReadBookshelfItemDTO convertToItemDTO(DoneReadBookshelf doneReadBook
152238
.build();
153239
}
154240

241+
private PostDTO convertToPostDTO(Post post) {
242+
243+
Book book = post.getBook();
244+
245+
PostDTO.MemberInfo memberInfo = PostDTO.MemberInfo.builder()
246+
.memberId(post.getMember().getId())
247+
.nickname(post.getMember().getNickname())
248+
.profileImage(post.getMember().getProfileImage())
249+
.readingTasteType(post.getMember().getReadingTasteType())
250+
.build();
251+
252+
PostDTO.BookInfo bookInfo = PostDTO.BookInfo.builder()
253+
.isbn(book.getIsbn())
254+
.bookImage(book.getBookImage())
255+
.title(book.getTitle())
256+
.author(book.getAuthor())
257+
.publisher(book.getPublisher())
258+
.pubdate(book.getPubdate())
259+
.ratingAverage(book.getRatingAverage())
260+
.build();
261+
262+
List<Quote> quotes = quoteRepository.findByPostId(post.getId());
263+
List<PostDTO.QuoteDTO> quoteDTOList = quotes.stream()
264+
.map(quote -> PostDTO.QuoteDTO.builder()
265+
.quoteContent(quote.getQuoteContent())
266+
.pageNumber(quote.getPageNumber())
267+
.build())
268+
.collect(Collectors.toList());
269+
270+
PostDTO.LikesCnt likesCnt = PostDTO.LikesCnt.builder()
271+
.relatableCount(post.getRelatableCount())
272+
.sameTasteCount(post.getSameTasteCount())
273+
.impressiveExpressionCount(post.getImpressiveExpressionCount())
274+
.wantToReadCount(post.getWantToReadCount())
275+
.helpfulCount(post.getHelpfulCount())
276+
.build();
277+
278+
return PostDTO.builder()
279+
.postId(post.getId())
280+
.memberInfo(memberInfo)
281+
.created(post.getCreatedAt())
282+
.bookInfo(bookInfo)
283+
.rating(post.getRating())
284+
.content(post.getContent())
285+
.readDate(post.getReadDate())
286+
.quotesCnt(quoteDTOList.size())
287+
.quotes(quoteDTOList)
288+
.likesCnt(likesCnt)
289+
.build();
290+
}
291+
292+
private Sort resolveSort(String sortBy) {
293+
if (sortBy == null) {
294+
return Sort.by(Sort.Order.desc("createdAt"));
295+
}
296+
297+
return switch (sortBy.toUpperCase()) {
298+
case "OLDEST" -> Sort.by(Sort.Order.asc("createdAt"));
299+
case "RATING_HIGH" -> Sort.by(Sort.Order.desc("rating"), Sort.Order.desc("createdAt"));
300+
case "RATING_LOW" -> Sort.by(Sort.Order.asc("rating"), Sort.Order.desc("createdAt"));
301+
case "LATEST" -> Sort.by(Sort.Order.desc("createdAt"));
302+
default -> Sort.by(Sort.Order.desc("createdAt"));
303+
};
304+
}
305+
306+
private int findRangeIndex(String range) {
307+
if (range == null) {
308+
throw new BadRequestException(ErrorStatus.INVALID_RATING_RANGE_EXCEPTION.getMessage());
309+
}
310+
311+
String normalizedRange = range.replace(" ", "");
312+
for (int i = 0; i < RATING_RANGES.length; i++) {
313+
if (RATING_RANGES[i].equals(normalizedRange)) {
314+
return i;
315+
}
316+
}
317+
318+
throw new BadRequestException(ErrorStatus.INVALID_RATING_RANGE_EXCEPTION.getMessage());
319+
}
320+
155321
private static class CalendarDayAggregate {
156322
private Long postId;
157323
private String isbn;

โ€Žsrc/main/java/com/moongeul/backend/api/post/repository/PostRepository.javaโ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,10 @@ List<Post> findCalendarPostsByMemberAndReadDateBetweenOrderByReadDateAscCreatedA
7979
@Param("member") Member member,
8080
@Param("startDate") LocalDate startDate,
8181
@Param("endDate") LocalDate endDate);
82+
83+
// ์‚ฌ์šฉ์ž ๋ณ„์  ๋ชฉ๋ก ์กฐํšŒ (null ์ œ์™ธ)
84+
@Query("SELECT p.rating FROM Post p WHERE p.member = :member AND p.rating IS NOT NULL")
85+
List<Double> findRatingsByMember(@Param("member") Member member);
86+
87+
Page<Post> findByMemberAndRatingBetween(Member member, Double startRating, Double endRating, Pageable pageable);
8288
}

โ€Žsrc/main/java/com/moongeul/backend/common/response/ErrorStatus.javaโ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public enum ErrorStatus {
6262
NO_FOLLOW_RELATIONSHIP(HttpStatus.BAD_REQUEST, "ํŒ”๋กœ์šฐ ๊ด€๊ณ„๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
6363
EXISTS_FOLLOW_ACCEPTED(HttpStatus.BAD_REQUEST, "์ด๋ฏธ ํŒ”๋กœ์šฐํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค."),
6464
EXISTS_FOLLOW_PENDING(HttpStatus.BAD_REQUEST, "์ด๋ฏธ ํŒ”๋กœ์šฐ ์š”์ฒญ์„ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค. ์Šน์ธ์„ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”."),
65+
INVALID_RATING_RANGE_EXCEPTION(HttpStatus.BAD_REQUEST, "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ„์  ๊ตฌ๊ฐ„์ž…๋‹ˆ๋‹ค."),
6566
BAD_FOLLOW_PROCESS_REQUEST(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ํŒ”๋กœ์šฐ ์ฒ˜๋ฆฌ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."),
6667
PRIVACY_FORBIDDEN_EXCEPTION(HttpStatus.FORBIDDEN, "ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋Š” ๊ณต๊ฐœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
6768

โ€Žsrc/main/java/com/moongeul/backend/common/response/SuccessStatus.javaโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public enum SuccessStatus {
4646
GET_WISH_READ_BOOKS_SUCCESS(HttpStatus.OK, "์ฝ๊ณ  ์‹ถ์€ ์ฑ…์žฅ ์กฐํšŒ ์„ฑ๊ณต"),
4747
GET_DONE_READ_BOOKS_SUCCESS(HttpStatus.OK, "์ฝ์€ ์ฑ…์žฅ ์กฐํšŒ ์„ฑ๊ณต"),
4848
GET_DONE_READ_CALENDAR_SUCCESS(HttpStatus.OK, "์ฝ์€ ์ฑ… ์บ˜๋ฆฐ๋” ์กฐํšŒ ์„ฑ๊ณต"),
49+
GET_DONE_READ_RATING_SUMMARY_SUCCESS(HttpStatus.OK, "์ฝ์€ ์ฑ… ๋ณ„์  ์š”์•ฝ ์กฐํšŒ ์„ฑ๊ณต"),
50+
GET_DONE_READ_RATING_DETAIL_SUCCESS(HttpStatus.OK, "์ฝ์€ ์ฑ… ๋ณ„์  ๊ตฌ๊ฐ„ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต"),
4951

5052
/* POST */
5153
GET_ALL_POST_SUCCESS(HttpStatus.OK, "๊ธฐ๋ก(๊ฒŒ์‹œ๊ธ€) ์ „์ฒด ์กฐํšŒ ์„ฑ๊ณต"),

0 commit comments

Comments
ย (0)