Skip to content

Commit

Permalink
Merge branch 'dev' into feat/#440
Browse files Browse the repository at this point in the history
  • Loading branch information
brgndyy committed Sep 20, 2024
2 parents 1aabcec + 1bfe652 commit 3a5ee50
Show file tree
Hide file tree
Showing 49 changed files with 1,074 additions and 127 deletions.
2 changes: 1 addition & 1 deletion backend/secrets
8 changes: 8 additions & 0 deletions backend/src/main/java/develup/api/DiscussionApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,12 @@ public ResponseEntity<ApiResponse<DiscussionResponse>> createDiscussion(

return ResponseEntity.ok(new ApiResponse<>(response));
}

@GetMapping("/discussions/mine")
@Operation(summary = "나의 디스커션 목록 조회 API", description = "내가 작성한 디스커션 목록을 조회합니다.")
public ResponseEntity<ApiResponse<List<SummarizedDiscussionResponse>>> getMyDiscussions(@Auth Accessor accessor) {
List<SummarizedDiscussionResponse> response = discussionService.getDiscussionsByMemberId(accessor.id());

return ResponseEntity.ok(new ApiResponse<>(response));
}
}
154 changes: 154 additions & 0 deletions backend/src/main/java/develup/api/logging/LoggingFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package develup.api.logging;

import java.io.IOException;
import java.util.Collections;
import java.util.UUID;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

@Component
@Order(1)
public class LoggingFilter extends OncePerRequestFilter {

private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);

private final ObjectMapper objectMapper;

public LoggingFilter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
saveTraceId(request);

ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper cachedResponse = new ContentCachingResponseWrapper(response);

saveRequestBody(cachedRequest);

filterChain.doFilter(cachedRequest, cachedResponse);

saveRequestMethod(cachedRequest);
saveRequestUri(cachedRequest);
saveQueryString(cachedRequest);
saveRequestHeader(cachedRequest);
printRequestLog();

addTraceIdToResponse(cachedResponse);
saveResponseBody(cachedResponse);
cachedResponse.copyBodyToResponse();
saveResponseHeader(cachedResponse);
printResponseLog();
MDC.clear();
}

private void saveTraceId(HttpServletRequest request) {
String header = request.getHeader("X-Request-ID");
if (header == null || header.isBlank()) {
header = UUID.randomUUID().toString();
}
MDC.put("traceId", header);
}

private void saveRequestBody(ContentCachingRequestWrapper cachedRequest) {
String content = cachedRequest.getContentAsString();
String prettyJsonString = toPrettyJsonString(content);
MDC.put("requestBody", prettyJsonString);
}

private String toPrettyJsonString(String jsonString) {
try {
Object json = objectMapper.readValue(jsonString, Object.class);
ObjectWriter writer = objectMapper.writerWithDefaultPrettyPrinter();
return writer.writeValueAsString(json);
} catch (JsonProcessingException e) {
return jsonString;
}
}

private void saveRequestMethod(ContentCachingRequestWrapper cachedRequest) {
MDC.put("method", cachedRequest.getMethod());
}

private void saveRequestUri(ContentCachingRequestWrapper cachedRequest) {
MDC.put("requestUri", cachedRequest.getRequestURI());
}

private void saveQueryString(ContentCachingRequestWrapper cachedRequest) {
String queryString = "?" + cachedRequest.getQueryString();
if (queryString.equals("?null")) {
queryString = "";
}
MDC.put("queryString", queryString);
}

private void saveRequestHeader(HttpServletRequest request) {
String headers = Collections.list(request.getHeaderNames())
.stream()
.map(headerName -> headerName + " : " + request.getHeader(headerName))
.collect(Collectors.joining("\n"));
MDC.put("requestHeaders", headers);
}

private void addTraceIdToResponse(HttpServletResponse response) {
response.addHeader("Trace-Id", MDC.get("traceId"));
}

private void saveResponseBody(ContentCachingResponseWrapper cachedResponse) {
String content = new String(cachedResponse.getContentAsByteArray());
MDC.put("responseBody", toPrettyJsonString(content));
}

private void saveResponseHeader(ContentCachingResponseWrapper cachedResponse) {
String responseHeader = cachedResponse.getHeaderNames()
.stream()
.map(headerName -> headerName + " : " + cachedResponse.getHeader(headerName))
.collect(Collectors.joining("\n"));
MDC.put("responseHeader", responseHeader);
}

private void printRequestLog() {
String template = """
Request
{} {}{}
Headers :
{}
Content :
{}
""";
log.info(
template,
MDC.get("method"),
MDC.get("requestUri"),
MDC.get("queryString"),
MDC.get("requestHeaders"),
MDC.get("requestBody")
);
}

private void printResponseLog() {
String responseLogTemplate = """
Response
Headers :
{}
Content :
{}
""";
log.info(responseLogTemplate, MDC.get("responseHeader"), MDC.get("responseBody"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ public DiscussionResponse create(Long memberId, CreateDiscussionRequest request)
}

public List<SummarizedDiscussionResponse> getSummaries(String mission, String hashTagName) {
return discussionRepository.findByMissionAndHashTagName(mission, hashTagName).stream()
return discussionRepository.findAllByMissionAndHashTagName(mission, hashTagName).stream()
.map(SummarizedDiscussionResponse::from)
.toList();
}

public List<SummarizedDiscussionResponse> getDiscussionsByMemberId(Long memberId) {
List<Discussion> myDiscussions = discussionRepository.findAllByMember_Id(memberId);

return myDiscussions.stream()
.map(SummarizedDiscussionResponse::from)
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public interface DiscussionRepository extends JpaRepository<Discussion, Long> {
AND sht.name = :hashTag
))
""")
List<Discussion> findByMissionAndHashTagName(
List<Discussion> findAllByMissionAndHashTagName(
@Param("mission") String mission,
@Param("hashTag") String hashTagName
);
Expand All @@ -41,4 +41,15 @@ List<Discussion> findByMissionAndHashTagName(
WHERE d.id = :id
""")
Optional<Discussion> findFetchById(Long id);

@Query("""
SELECT d
FROM Discussion d
JOIN FETCH d.mission m
JOIN FETCH d.member me
JOIN FETCH d.discussionHashTags.hashTags dhts
JOIN FETCH dhts.hashTag ht
WHERE me.id = :memberId
""")
List<Discussion> findAllByMember_Id(Long memberId);
}
7 changes: 4 additions & 3 deletions backend/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<springProperty scope="context" name="appName" source="spring.application.name"/>

<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>UTF-8</charset>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} | %t | %highlight(%-5p) | %cyan(%logger{36}) | %m%n
%d{yyyy-MM-dd HH:mm:ss.SSS} | %t | traceId=%X{traceId} | %highlight(%-5p) | %cyan(%logger{36}) | %m%n
</pattern>
</encoder>
</appender>
Expand All @@ -18,7 +17,9 @@
</http>
<format>
<label>
<pattern>app=${appName},host=${HOSTNAME},traceID=%X{traceId:-NONE},level=%level</pattern>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} | %t | traceId=%X{traceId} | %highlight(%-5p) | %cyan(%logger{36}) | app=${appName} | host=${HOSTNAME} | %m%n
</pattern>
</label>
<message>
<pattern>${FILE_LOG_PATTERN}</pattern>
Expand Down
24 changes: 24 additions & 0 deletions backend/src/test/java/develup/api/DiscussionApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,30 @@ void getSolution() throws Exception {
.andExpect(jsonPath("$.data.mission.url", equalTo("https://github.com/develup-mission/java-smoking")));
}

@Test
@DisplayName("나의 디스커션 목록을 조회한다.")
void getMyDiscussions() throws Exception {
List<SummarizedDiscussionResponse> myDiscussions = List.of(
SummarizedDiscussionResponse.from(createDiscussion()),
SummarizedDiscussionResponse.from(createDiscussion())
);

BDDMockito.given(discussionService.getDiscussionsByMemberId(any()))
.willReturn(myDiscussions);

mockMvc.perform(get("/discussions/mine"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].id", equalTo(1)))
.andExpect(jsonPath("$.data[0].title", equalTo("루터회관 흡연단속 구현에 대한 고찰")))
.andExpect(jsonPath("$.data[0].mission", equalTo("루터회관 흡연단속")))
.andExpect(jsonPath("$.data[0].hashTags[0].id", equalTo(1)))
.andExpect(jsonPath("$.data[0].hashTags[0].name", equalTo("JAVA")))
.andExpect(jsonPath("$.data[0].member.id", equalTo(1)))
.andExpect(jsonPath("$.data[0].member.name", equalTo("tester")))
.andExpect(jsonPath("$.data[0].commentCount", equalTo(100)));
}

private Discussion createDiscussion() {
HashTag hashTag = HashTagTestData.defaultHashTag()
.withId(1L)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ void getById() {
.hasMessage("존재하지 않는 디스커션입니다.");
}

@Test
@DisplayName("나의 디스커션 리스트를 조회한다.")
@Transactional
void getDiscussionsByMemberId() {
Member member = memberRepository.save(MemberTestData.defaultMember().withId(1L).build());
Mission mission = missionRepository.save(MissionTestData.defaultMission().build());
HashTag hashTag = hashTagRepository.save(HashTagTestData.defaultHashTag().build());
Discussion discussion = DiscussionTestData.defaultDiscussion()
.withMember(member)
.withMission(mission)
.withHashTags(List.of(hashTag))
.build();

discussionRepository.save(discussion);

assertThat(discussionService.getDiscussionsByMemberId(member.getId())).hasSize(1);
}

private void createDiscussion(Mission mission, HashTag hashTag) {
Member member = memberRepository.save(MemberTestData.defaultMember().build());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package develup.domain.discussion;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

import java.util.List;
import java.util.function.Function;
Expand Down Expand Up @@ -44,7 +45,7 @@ void findAllDiscussion() {
createDiscussion(mission, hashTag);
createDiscussion(mission, hashTag);

List<Discussion> actual = discussionRepository.findByMissionAndHashTagName(
List<Discussion> actual = discussionRepository.findAllByMissionAndHashTagName(
"all",
"all"
);
Expand All @@ -62,7 +63,7 @@ void findAllDiscussionByHashTag() {
createDiscussion(mission, hashTag1);
createDiscussion(mission, hashTag2);

List<Discussion> discussions = discussionRepository.findByMissionAndHashTagName(
List<Discussion> discussions = discussionRepository.findAllByMissionAndHashTagName(
"all",
"JAVA"
);
Expand All @@ -84,7 +85,7 @@ void findAllDiscussionByMission() {
createDiscussion(mission1, hashTag);
createDiscussion(mission2, hashTag);

List<Discussion> discussions = discussionRepository.findByMissionAndHashTagName(
List<Discussion> discussions = discussionRepository.findAllByMissionAndHashTagName(
"루터회관 흡연단속",
"all"
);
Expand Down Expand Up @@ -113,6 +114,38 @@ void findFetchById() {
.hasValue(savedDiscussion.getId());
}

@Test
@DisplayName("멤버 식별자를 통해 디스커션을 조회한다.")
@Transactional
void findByMember_Id() {
Member member1 = memberRepository.save(MemberTestData.defaultMember().withId(1L).build());
Member member2 = memberRepository.save(MemberTestData.defaultMember().withId(2L).build());
Mission mission = missionRepository.save(MissionTestData.defaultMission().build());
HashTag hashTag = hashTagRepository.save(HashTagTestData.defaultHashTag().build());

Discussion discussionByMember1 = DiscussionTestData.defaultDiscussion()
.withMission(mission)
.withMember(member1)
.withHashTags(List.of(hashTag))
.build();
Discussion discussionByMember2 = DiscussionTestData.defaultDiscussion()
.withMission(mission)
.withMember(member2)
.withHashTags(List.of(hashTag))
.build();

discussionRepository.save(discussionByMember1);
discussionRepository.save(discussionByMember2);

List<Discussion> discussionsByMember1 = discussionRepository.findAllByMember_Id(member1.getId());
List<Discussion> discussionsByMember2 = discussionRepository.findAllByMember_Id(member2.getId());

assertAll(
() -> assertThat(discussionsByMember1).hasSize(1),
() -> assertThat(discussionsByMember2).hasSize(1)
);
}

private void createDiscussion(Mission mission, HashTag hashTag) {
Member member = memberRepository.save(MemberTestData.defaultMember().build());

Expand Down
26 changes: 26 additions & 0 deletions frontend/src/apis/discussionAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Discussion } from '@/types';
import { develupAPIClient } from './clients/develupClient';
import { PATH } from './paths';

export const getDiscussions = async (mission = 'all', hashTag = 'all'): Promise<Discussion[]> => {
const { data } = await develupAPIClient.get<{ data: Discussion[] }>(PATH.discussions, {
mission,
hashTag,
});

return data;
};

export const postDiscussionSubmit = async (payload: {
title: string;
content: string;
missionId?: number;
hashTagIds: number[];
}): Promise<Discussion> => {
const { data } = await develupAPIClient.post<{ data: Discussion }>(
PATH.submitDiscussion,
payload,
);

return data;
};
Loading

0 comments on commit 3a5ee50

Please sign in to comment.