Skip to content

Commit 4789466

Browse files
authored
[Feat/#244] 오늘의 코디 조회 api 구현 (#248)
* refactor: feed 관련 수정 * feat: 오늘의 코디 조회 api 구현
1 parent 7910145 commit 4789466

11 files changed

Lines changed: 278 additions & 9 deletions

File tree

clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ public BaseResponse<SliceResponse<DailyCoordinateListResponse>> getDailyCoordina
101101
return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response);
102102
}
103103

104+
@GetMapping("/daily/today")
105+
@Operation(
106+
operationId = "Coordinate_getTodayDailyCoordinateClothes",
107+
summary = "오늘의 코디 옷 정보 조회",
108+
description = "오늘의 코디에 포함된 옷 정보를 조회하는 API입니다.")
109+
public BaseResponse<List<DailyCoordinateClothResponse>> getTodayDailyCoordinateClothes() {
110+
List<DailyCoordinateClothResponse> response =
111+
coordinateService.getTodayDailyCoordinateClothes();
112+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response);
113+
}
114+
104115
@GetMapping("/{coordinateId}/preview")
105116
@Operation(
106117
operationId = "Coordinate_getCoordinatePreview",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.clokey.domain.coordinate.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
@Schema(name = "DailyCoordinateClothResponse", description = "오늘의 코디 옷 정보")
6+
public record DailyCoordinateClothResponse(
7+
@Schema(description = "옷 이미지 URL", example = "https://example.jpg") String imageUrl,
8+
@Schema(description = "브랜드", example = "나이키") String brand,
9+
@Schema(description = "옷 이름", example = "맨투맨") String name,
10+
@Schema(description = "하위 카테고리", example = "맨투맨") String category,
11+
@Schema(description = "상위 카테고리", example = "상의") String parentCategory) {
12+
public static DailyCoordinateClothResponse from(CoordinateDetailsListResponse details) {
13+
return new DailyCoordinateClothResponse(
14+
details.imageUrl(),
15+
details.brand(),
16+
details.name(),
17+
details.category(),
18+
details.parentCategory());
19+
}
20+
}

clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public interface CoordinateService {
2424
SliceResponse<DailyCoordinateListResponse> getDailyCoordinates(
2525
Long lastCoordinateId, int size, SortDirection direction);
2626

27+
List<DailyCoordinateClothResponse> getTodayDailyCoordinateClothes();
28+
2729
CoordinatePreviewResponse getCoordinatePreview(Long coordinateId);
2830

2931
List<CoordinateDetailsListResponse> getCoordinateDetails(Long coordinateId);

clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,28 @@ public SliceResponse<DailyCoordinateListResponse> getDailyCoordinates(
310310
return SliceResponse.from(result);
311311
}
312312

313+
@Override
314+
public List<DailyCoordinateClothResponse> getTodayDailyCoordinateClothes() {
315+
final Member currentMember = memberUtil.getCurrentMember();
316+
Optional<Coordinate> coordinate =
317+
coordinateRepository.findDailyCoordinateByDateAndMemberId(
318+
LocalDate.now(), currentMember.getId());
319+
320+
if (coordinate.isEmpty()) {
321+
return List.of();
322+
}
323+
324+
List<CoordinateDetailsListResponse> details =
325+
coordinateRepository.findAllCoordinateDetailsByCoordinateId(
326+
coordinate.get().getId());
327+
328+
if (details.isEmpty()) {
329+
return List.of();
330+
}
331+
332+
return details.stream().map(DailyCoordinateClothResponse::from).toList();
333+
}
334+
313335
@Override
314336
public CoordinatePreviewResponse getCoordinatePreview(Long coordinateId) {
315337
final Member currentMember = memberUtil.getCurrentMember();

clokey-api/src/main/java/org/clokey/domain/feed/controller/FeedController.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.clokey.domain.feed.controller;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.tags.Tag;
35
import java.util.List;
46
import lombok.RequiredArgsConstructor;
57
import org.clokey.code.GlobalBaseSuccessCode;
@@ -15,14 +17,19 @@
1517
import org.springframework.web.bind.annotation.RestController;
1618

1719
@RestController
20+
@RequestMapping("/feeds")
1821
@RequiredArgsConstructor
22+
@Tag(name = "06. 피드 API", description = "피드 관련 API입니다.")
1923
@Validated
20-
@RequestMapping("/feeds")
2124
public class FeedController {
2225

2326
private final FeedService feedService;
2427

2528
@GetMapping
29+
@Operation(
30+
operationId = "Feed_getFeeds",
31+
summary = "피드 조회(전체/팔로잉)",
32+
description = "필터링을 통해 피드를 조회합니다.")
2633
public BaseResponse<FeedListResponse> getFeeds(
2734
@RequestParam(defaultValue = "ALL") FollowScope followScope,
2835
@RequestParam(required = false) String styleIds,

clokey-api/src/main/java/org/clokey/domain/feed/repository/FeedQueryRepository.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static org.clokey.member.entity.QMember.member;
77

88
import com.querydsl.core.types.dsl.BooleanExpression;
9+
import com.querydsl.core.types.dsl.Expressions;
910
import com.querydsl.jpa.JPAExpressions;
1011
import com.querydsl.jpa.impl.JPAQueryFactory;
1112
import java.time.LocalDateTime;
@@ -14,6 +15,7 @@
1415
import org.clokey.domain.feed.query.FeedCursor;
1516
import org.clokey.domain.feed.query.FollowScope;
1617
import org.clokey.history.entity.History;
18+
import org.clokey.member.entity.QBlock;
1719
import org.springframework.stereotype.Repository;
1820

1921
@Repository
@@ -41,6 +43,7 @@ public List<History> findFeeds(
4143
.where(
4244
history.banned.isFalse(),
4345
followScopeCondition(currentMemberId, followScope),
46+
notBlockedCondition(currentMemberId),
4447
styleFilterCondition(styleIds),
4548
situationFilterCondition(situationIds),
4649
cursorCondition(cursor))
@@ -49,15 +52,18 @@ public List<History> findFeeds(
4952
.fetch();
5053
}
5154

52-
public List<History> findFeedsByIds(List<Long> historyIds) {
55+
public List<History> findFeedsByIds(Long currentMemberId, List<Long> historyIds) {
5356
if (historyIds == null || historyIds.isEmpty()) {
5457
return List.of();
5558
}
5659
return queryFactory
5760
.selectFrom(history)
5861
.join(history.member, member)
5962
.fetchJoin()
60-
.where(history.banned.isFalse(), history.id.in(historyIds))
63+
.where(
64+
history.banned.isFalse(),
65+
history.id.in(historyIds),
66+
notBlockedCondition(currentMemberId))
6167
.fetch();
6268
}
6369

@@ -105,4 +111,28 @@ private BooleanExpression cursorCondition(FeedCursor cursor) {
105111
.lt(createdAt)
106112
.or(history.createdAt.eq(createdAt).and(history.id.lt(feedId)));
107113
}
114+
115+
private BooleanExpression notBlockedCondition(Long currentMemberId) {
116+
if (currentMemberId == null) {
117+
return null;
118+
}
119+
120+
QBlock blockCheck = new QBlock("blockCheck");
121+
BooleanExpression blockCondition =
122+
blockCheck
123+
.blocker
124+
.id
125+
.eq(currentMemberId)
126+
.and(blockCheck.blocked.id.eq(history.member.id))
127+
.or(
128+
blockCheck
129+
.blocker
130+
.id
131+
.eq(history.member.id)
132+
.and(blockCheck.blocked.id.eq(currentMemberId)));
133+
134+
return Expressions.asBoolean(
135+
JPAExpressions.selectOne().from(blockCheck).where(blockCondition).exists())
136+
.not();
137+
}
108138
}

clokey-api/src/main/java/org/clokey/domain/feed/service/FeedServiceImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public FeedListResponse getFeeds(
7373
int takeCount = Math.min(pageSize, basePendingIds.size());
7474
List<Long> takeIds = basePendingIds.subList(0, takeCount);
7575
remainingPendingIds = basePendingIds.subList(takeCount, basePendingIds.size());
76-
List<History> pendingHistories = feedQueryRepository.findFeedsByIds(takeIds);
76+
List<History> pendingHistories =
77+
feedQueryRepository.findFeedsByIds(currentMember.getId(), takeIds);
7778
Map<Long, History> pendingMap =
7879
pendingHistories.stream()
7980
.collect(Collectors.toMap(History::getId, history -> history));

clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,4 +1657,49 @@ class 최애_코디_조회_요청_시 {
16571657
.andExpect(jsonPath("$.result[1].imageUrl").value("testImageUrl2"));
16581658
}
16591659
}
1660+
1661+
@Nested
1662+
class 오늘의_코디_조회_요청_시 {
1663+
1664+
@Test
1665+
void 유효한_요청이면_오늘의_코디_정보를_반환한다() throws Exception {
1666+
// given
1667+
List<DailyCoordinateClothResponse> response =
1668+
List.of(
1669+
new DailyCoordinateClothResponse(
1670+
"https://image.example/cloth1.jpg",
1671+
"brand1",
1672+
"name1",
1673+
"category1",
1674+
"parent1"),
1675+
new DailyCoordinateClothResponse(
1676+
"https://image.example/cloth2.jpg",
1677+
"brand2",
1678+
"name2",
1679+
"category2",
1680+
"parent2"));
1681+
1682+
given(coordinateService.getTodayDailyCoordinateClothes()).willReturn(response);
1683+
1684+
ResultActions perform = mockMvc.perform(get("/coordinate/daily/today"));
1685+
// when & then
1686+
perform.andExpect(status().isOk())
1687+
.andExpect(jsonPath("$.isSuccess").value(true))
1688+
.andExpect(jsonPath("$.code").value("COMMON200"))
1689+
.andExpect(
1690+
jsonPath("$.result[0].imageUrl")
1691+
.value("https://image.example/cloth1.jpg"))
1692+
.andExpect(jsonPath("$.result[0].brand").value("brand1"))
1693+
.andExpect(jsonPath("$.result[0].name").value("name1"))
1694+
.andExpect(jsonPath("$.result[0].category").value("category1"))
1695+
.andExpect(jsonPath("$.result[0].parentCategory").value("parent1"))
1696+
.andExpect(
1697+
jsonPath("$.result[1].imageUrl")
1698+
.value("https://image.example/cloth2.jpg"))
1699+
.andExpect(jsonPath("$.result[1].brand").value("brand2"))
1700+
.andExpect(jsonPath("$.result[1].name").value("name2"))
1701+
.andExpect(jsonPath("$.result[1].category").value("category2"))
1702+
.andExpect(jsonPath("$.result[1].parentCategory").value("parent2"));
1703+
}
1704+
}
16601705
}

clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.clokey.domain.coordinate.dto.request.DailyCoordinateCreateRequest;
2525
import org.clokey.domain.coordinate.dto.response.CoordinateDetailsListResponse;
2626
import org.clokey.domain.coordinate.dto.response.CoordinatePreviewResponse;
27+
import org.clokey.domain.coordinate.dto.response.DailyCoordinateClothResponse;
2728
import org.clokey.domain.coordinate.dto.response.DailyCoordinateListResponse;
2829
import org.clokey.domain.coordinate.dto.response.FavoriteCoordinateResponse;
2930
import org.clokey.domain.coordinate.exception.CoordinateErrorCode;
@@ -1935,4 +1936,92 @@ void setUp() {
19351936
assertThat(responses).isEmpty();
19361937
}
19371938
}
1939+
1940+
@Nested
1941+
class 오늘의_코디를_조회할_때 {
1942+
1943+
@BeforeEach
1944+
void setUp() {
1945+
Member member1 =
1946+
Member.createMember(
1947+
"todayEmail1",
1948+
"todayClokeyId1",
1949+
"todayNickName1",
1950+
OauthInfo.createOauthInfo("todayOauthId1", OauthProvider.KAKAO));
1951+
Member member2 =
1952+
Member.createMember(
1953+
"todayEmail2",
1954+
"todayClokeyId2",
1955+
"todayNickName2",
1956+
OauthInfo.createOauthInfo("todayOauthId2", OauthProvider.KAKAO));
1957+
memberRepository.saveAll(List.of(member1, member2));
1958+
given(memberUtil.getCurrentMember()).willReturn(member1);
1959+
1960+
Category parentCategory = Category.createCategory("parentCategory", null);
1961+
Category category = Category.createCategory("category", parentCategory);
1962+
categoryRepository.saveAll(List.of(parentCategory, category));
1963+
1964+
Cloth cloth1 =
1965+
Cloth.createCloth(
1966+
"imageUrl1",
1967+
null,
1968+
"name1",
1969+
"brand1",
1970+
List.of(Season.SPRING),
1971+
category,
1972+
member1);
1973+
Cloth cloth2 =
1974+
Cloth.createCloth(
1975+
"imageUrl2",
1976+
null,
1977+
"name2",
1978+
"brand2",
1979+
List.of(Season.SPRING),
1980+
category,
1981+
member1);
1982+
clothRepository.saveAll(List.of(cloth1, cloth2));
1983+
1984+
Coordinate coordinate = Coordinate.createDailyCoordinate("coordImage", member1);
1985+
coordinateRepository.save(coordinate);
1986+
1987+
CoordinateCloth coordinateCloth1 =
1988+
CoordinateCloth.createCoordinateCloth(
1989+
10.0, 20.0, 1.0, 30.0, 1, coordinate, cloth1);
1990+
CoordinateCloth coordinateCloth2 =
1991+
CoordinateCloth.createCoordinateCloth(
1992+
11.0, 21.0, 1.0, 31.0, 2, coordinate, cloth2);
1993+
coordinateClothRepository.saveAll(List.of(coordinateCloth1, coordinateCloth2));
1994+
}
1995+
1996+
@Test
1997+
void 유효한_요청이면_오늘의_코디를_반환한다() {
1998+
// when
1999+
List<DailyCoordinateClothResponse> responses =
2000+
coordinateService.getTodayDailyCoordinateClothes();
2001+
2002+
// then
2003+
assertThat(responses)
2004+
.extracting(
2005+
DailyCoordinateClothResponse::imageUrl,
2006+
DailyCoordinateClothResponse::brand,
2007+
DailyCoordinateClothResponse::name,
2008+
DailyCoordinateClothResponse::category,
2009+
DailyCoordinateClothResponse::parentCategory)
2010+
.containsExactly(
2011+
tuple("imageUrl1", "brand1", "name1", "category", "parentCategory"),
2012+
tuple("imageUrl2", "brand2", "name2", "category", "parentCategory"));
2013+
}
2014+
2015+
@Test
2016+
void 오늘의_코디가_없으면_빈_리스트를_반환한다() {
2017+
// given
2018+
Member member = memberRepository.findByClokeyId("todayClokeyId2").orElseThrow();
2019+
given(memberUtil.getCurrentMember()).willReturn(member);
2020+
// when
2021+
List<DailyCoordinateClothResponse> responses =
2022+
coordinateService.getTodayDailyCoordinateClothes();
2023+
// then
2024+
assertThat(responses).isEmpty();
2025+
}
2026+
}
19382027
}

clokey-api/src/test/java/org/clokey/domain/feed/controller/FeedControllerTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class 피드_조회_요청_시 {
3333

3434
@Test
3535
void 유효한_요청이면_피드_리스트를_반환한다() throws Exception {
36+
// given
3637
FeedListResponse response =
3738
FeedListResponse.of(
3839
List.of(
@@ -57,7 +58,7 @@ class 피드_조회_요청_시 {
5758
eq(10),
5859
eq("cursorValue")))
5960
.willReturn(response);
60-
61+
// when
6162
ResultActions perform =
6263
mockMvc.perform(
6364
get("/feeds")
@@ -66,7 +67,7 @@ class 피드_조회_요청_시 {
6667
.param("situationIds", "3")
6768
.param("size", "10")
6869
.param("cursor", "cursorValue"));
69-
70+
// then
7071
perform.andExpect(status().isOk())
7172
.andExpect(jsonPath("$.isSuccess").value(true))
7273
.andExpect(jsonPath("$.code").value("COMMON200"))

0 commit comments

Comments
 (0)