Skip to content

Commit fc09163

Browse files
authored
Feature/156 봉사자와 기관의 자신에게 온 쪽지 목록 (#184)
1 parent dbb2b03 commit fc09163

File tree

11 files changed

+582
-2
lines changed

11 files changed

+582
-2
lines changed

src/main/java/com/somemore/global/common/entity/BaseEntity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class BaseEntity {
2424
@Column(name = "updated_at")
2525
private LocalDateTime updatedAt;
2626

27-
@Column(name = "deleted")
27+
@Column(name = "deleted", nullable = false)
2828
private Boolean deleted;
2929

3030
@PrePersist
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.somemore.note.controller;
2+
3+
import com.somemore.auth.annotation.CurrentUser;
4+
import com.somemore.global.common.response.ApiResponse;
5+
import com.somemore.note.repository.mapper.NoteReceiverViewForCenter;
6+
import com.somemore.note.repository.mapper.NoteReceiverViewForVolunteer;
7+
import com.somemore.note.usecase.NoteQueryUseCase;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.data.domain.Page;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.security.access.annotation.Secured;
14+
import org.springframework.web.bind.annotation.*;
15+
16+
import java.util.UUID;
17+
18+
@Tag(name = "Note Query API", description = "쪽지 조회 API")
19+
@RequiredArgsConstructor
20+
@RequestMapping("/api/note")
21+
@RestController
22+
public class NoteQueryApiController {
23+
24+
private final NoteQueryUseCase noteQueryUseCase;
25+
26+
@Secured("ROLE_CENTER")
27+
@Operation(summary = "기관의 자신에게 온 쪽지 조회")
28+
@GetMapping("/center")
29+
public ApiResponse<Page<NoteReceiverViewForCenter>> getNotesByCenterId(@CurrentUser UUID centerId, Pageable pageable) {
30+
31+
Page<NoteReceiverViewForCenter> response = noteQueryUseCase.getNotesForCenter(centerId, pageable);
32+
33+
return ApiResponse.ok(200, response, "내 쪽지 조회 성공");
34+
}
35+
36+
@Secured("ROLE_VOLUNTEER")
37+
@Operation(summary = "봉사자의 자신에게 온 쪽지 조회")
38+
@GetMapping("/volunteer")
39+
public ApiResponse<Page<NoteReceiverViewForVolunteer>> getNotesByVolunteerId(@CurrentUser UUID volunteerId, Pageable pageable) {
40+
41+
Page<NoteReceiverViewForVolunteer> response = noteQueryUseCase.getNotesForVolunteer(volunteerId, pageable);
42+
43+
return ApiResponse.ok(200, response, "내 쪽지 조회 성공");
44+
}
45+
46+
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package com.somemore.note.repository;
22

33
import com.somemore.note.domain.Note;
4+
import com.somemore.note.repository.mapper.NoteReceiverViewForCenter;
5+
import com.somemore.note.repository.mapper.NoteReceiverViewForVolunteer;
6+
import org.springframework.data.domain.Page;
7+
import org.springframework.data.domain.Pageable;
48

5-
public interface NoteRepository {
9+
import java.util.UUID;
610

11+
public interface NoteRepository {
712
Note save(Note note);
13+
Page<NoteReceiverViewForCenter> findNotesByReceiverIsCenter(UUID centerId, Pageable pageable);
14+
Page<NoteReceiverViewForVolunteer> findNotesByReceiverIsVolunteer(UUID volunteerId, Pageable pageable);
815
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,101 @@
11
package com.somemore.note.repository;
22

3+
import com.querydsl.core.types.Projections;
4+
import com.querydsl.core.types.dsl.BooleanExpression;
5+
import com.querydsl.jpa.impl.JPAQuery;
6+
import com.querydsl.jpa.impl.JPAQueryFactory;
7+
import com.somemore.center.domain.QCenter;
38
import com.somemore.note.domain.Note;
9+
import com.somemore.note.domain.QNote;
10+
import com.somemore.note.repository.mapper.NoteReceiverViewForCenter;
11+
import com.somemore.note.repository.mapper.NoteReceiverViewForVolunteer;
12+
import com.somemore.volunteer.domain.QVolunteer;
413
import lombok.RequiredArgsConstructor;
14+
import org.springframework.data.domain.Page;
15+
import org.springframework.data.domain.Pageable;
16+
import org.springframework.data.support.PageableExecutionUtils;
517
import org.springframework.stereotype.Repository;
618

19+
import java.util.List;
20+
import java.util.UUID;
21+
722
@RequiredArgsConstructor
823
@Repository
924
public class NoteRepositoryImpl implements NoteRepository {
1025

26+
private final JPAQueryFactory queryFactory;
1127
private final NoteJpaRepository noteJpaRepository;
1228

29+
private static final QNote note = QNote.note;
30+
private static final QVolunteer volunteer = QVolunteer.volunteer;
31+
private static final QCenter center = QCenter.center;
32+
1333
@Override
1434
public Note save(Note note) {
1535
return noteJpaRepository.save(note);
1636
}
1737

38+
@Override
39+
public Page<NoteReceiverViewForCenter> findNotesByReceiverIsCenter(UUID centerId, Pageable pageable) {
40+
41+
BooleanExpression activeVolunteer = volunteer.deleted.eq(false);
42+
BooleanExpression condition = note.receiverId.eq(centerId)
43+
.and(note.deleted.eq(false));
44+
45+
List<NoteReceiverViewForCenter> results = queryFactory
46+
.select(Projections.constructor(
47+
NoteReceiverViewForCenter.class,
48+
note.id,
49+
note.title,
50+
volunteer.id.as("senderId"),
51+
volunteer.nickname.as("senderName"),
52+
note.isRead
53+
))
54+
.from(note)
55+
.join(volunteer).on(note.senderId.eq(volunteer.id).and(activeVolunteer))
56+
.where(condition)
57+
.offset(pageable.getOffset())
58+
.limit(pageable.getPageSize())
59+
.orderBy(note.createdAt.desc())
60+
.fetch();
61+
62+
JPAQuery<Long> count = queryFactory
63+
.select(note.count())
64+
.from(note)
65+
.where(condition);
66+
67+
return PageableExecutionUtils.getPage(results, pageable, count::fetchOne);
68+
}
69+
70+
@Override
71+
public Page<NoteReceiverViewForVolunteer> findNotesByReceiverIsVolunteer(UUID volunteerId, Pageable pageable) {
72+
BooleanExpression activeCenter = center.deleted.eq(false);
73+
BooleanExpression condition = note.receiverId.eq(volunteerId)
74+
.and(note.deleted.eq(false));
75+
76+
List<NoteReceiverViewForVolunteer> results = queryFactory
77+
.select(Projections.constructor(
78+
NoteReceiverViewForVolunteer.class,
79+
note.id,
80+
note.title,
81+
center.id.as("senderId"),
82+
center.name.as("senderName"),
83+
note.isRead
84+
))
85+
.from(note)
86+
.join(center).on(note.senderId.eq(center.id).and(activeCenter))
87+
.where(condition)
88+
.offset(pageable.getOffset())
89+
.limit(pageable.getPageSize())
90+
.orderBy(note.createdAt.desc())
91+
.fetch();
92+
93+
JPAQuery<Long> count = queryFactory
94+
.select(note.count())
95+
.from(note)
96+
.where(condition);
97+
98+
return PageableExecutionUtils.getPage(results, pageable, count::fetchOne);
99+
}
100+
18101
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.somemore.note.repository.mapper;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import lombok.Builder;
7+
8+
import java.util.UUID;
9+
10+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
11+
@Builder
12+
public record NoteReceiverViewForCenter(
13+
@Schema(description = "쪽지 ID", example = "1111")
14+
Long id,
15+
@Schema(description = "쪽지 제목", example = "문의 드립니다.")
16+
String title,
17+
@Schema(description = "송신자 id", example = "1342134-32423-35345")
18+
UUID senderId,
19+
@Schema(description = "송신 봉사자 닉네임", example = "봉사왕")
20+
String senderName,
21+
@Schema(description = "읽음 여부", example = "true = 읽음, false = 안읽음")
22+
boolean isRead
23+
) {
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.somemore.note.repository.mapper;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import lombok.Builder;
7+
8+
import java.util.UUID;
9+
10+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
11+
@Builder
12+
public record NoteReceiverViewForVolunteer(
13+
@Schema(description = "쪽지 ID", example = "1111")
14+
Long id,
15+
@Schema(description = "쪽지 제목", example = "답변 드립니다..")
16+
String title,
17+
@Schema(description = "송신한 기관 id", example = "1342134-32423-35345")
18+
UUID senderId,
19+
@Schema(description = "송신 기관 닉네임", example = "서울 도서관")
20+
String senderName,
21+
@Schema(description = "읽음 여부", example = "true = 읽음, false = 안읽음")
22+
boolean isRead
23+
) {
24+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.somemore.note.service;
2+
3+
import com.somemore.note.repository.NoteRepository;
4+
import com.somemore.note.repository.mapper.NoteReceiverViewForCenter;
5+
import com.somemore.note.repository.mapper.NoteReceiverViewForVolunteer;
6+
import com.somemore.note.usecase.NoteQueryUseCase;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.data.domain.Page;
9+
import org.springframework.data.domain.Pageable;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
13+
import java.util.UUID;
14+
15+
@RequiredArgsConstructor
16+
@Service
17+
@Transactional(readOnly = true)
18+
public class NoteQueryService implements NoteQueryUseCase {
19+
20+
private final NoteRepository noteRepository;
21+
22+
@Override
23+
public Page<NoteReceiverViewForCenter> getNotesForCenter(UUID centerId, Pageable pageable) {
24+
return noteRepository.findNotesByReceiverIsCenter(centerId, pageable);
25+
}
26+
27+
@Override
28+
public Page<NoteReceiverViewForVolunteer> getNotesForVolunteer(UUID volunteerId, Pageable pageable) {
29+
return noteRepository.findNotesByReceiverIsVolunteer(volunteerId, pageable);
30+
}
31+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.somemore.note.usecase;
2+
3+
import com.somemore.note.repository.mapper.NoteReceiverViewForCenter;
4+
import com.somemore.note.repository.mapper.NoteReceiverViewForVolunteer;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
8+
import java.util.UUID;
9+
10+
public interface NoteQueryUseCase {
11+
Page<NoteReceiverViewForCenter> getNotesForCenter(UUID centerId, Pageable pageable);
12+
Page<NoteReceiverViewForVolunteer> getNotesForVolunteer(UUID volunteerId, Pageable pageable);
13+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.somemore.note.controller;
2+
3+
import com.somemore.ControllerTestSupport;
4+
import com.somemore.WithMockCustomUser;
5+
import com.somemore.note.repository.mapper.NoteReceiverViewForCenter;
6+
import com.somemore.note.repository.mapper.NoteReceiverViewForVolunteer;
7+
import com.somemore.note.usecase.NoteQueryUseCase;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.boot.test.mock.mockito.MockBean;
11+
import org.springframework.data.domain.Page;
12+
import org.springframework.data.domain.PageImpl;
13+
import org.springframework.data.domain.PageRequest;
14+
import org.springframework.data.domain.Pageable;
15+
import org.springframework.http.MediaType;
16+
17+
import java.util.List;
18+
import java.util.UUID;
19+
20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.ArgumentMatchers.eq;
22+
import static org.mockito.Mockito.when;
23+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
24+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
25+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
26+
27+
class NoteQueryApiControllerTest extends ControllerTestSupport {
28+
29+
@MockBean
30+
private NoteQueryUseCase noteQueryUseCase;
31+
32+
@DisplayName("기관은 자신에게 온 쪽지를 페이지 형태로 확인할 수 있다. (Controller)")
33+
@Test
34+
@WithMockCustomUser(role = "CENTER")
35+
void getNotesByCenterId() throws Exception {
36+
// given
37+
Pageable pageable = PageRequest.of(0, 6);
38+
List<NoteReceiverViewForCenter> notes = List.of(
39+
NoteReceiverViewForCenter.builder()
40+
.id(1L)
41+
.title("문의 드립니다.")
42+
.senderName("봉사왕")
43+
.isRead(false)
44+
.build(),
45+
NoteReceiverViewForCenter.builder()
46+
.id(2L)
47+
.title("긴급 문의")
48+
.senderName("지원자")
49+
.isRead(true)
50+
.build()
51+
);
52+
Page<NoteReceiverViewForCenter> mockResponse = new PageImpl<>(notes, pageable, notes.size());
53+
54+
when(noteQueryUseCase.getNotesForCenter(any(UUID.class), eq(pageable)))
55+
.thenReturn(mockResponse);
56+
57+
// When & Then
58+
mockMvc.perform(get("/api/note/center")
59+
.param("page", "0")
60+
.param("size", "6")
61+
.accept(MediaType.APPLICATION_JSON))
62+
.andExpect(status().isOk())
63+
.andExpect(jsonPath("$.code").value(200))
64+
.andExpect(jsonPath("$.message").value("내 쪽지 조회 성공"))
65+
.andExpect(jsonPath("$.data.content").isArray())
66+
.andExpect(jsonPath("$.data.content[0].id").value(1))
67+
.andExpect(jsonPath("$.data.content[0].title").value("문의 드립니다."))
68+
.andExpect(jsonPath("$.data.content[0].sender_name").value("봉사왕"))
69+
.andExpect(jsonPath("$.data.content[0].is_read").value(false))
70+
.andExpect(jsonPath("$.data.content[1].id").value(2))
71+
.andExpect(jsonPath("$.data.content[1].title").value("긴급 문의"))
72+
.andExpect(jsonPath("$.data.content[1].sender_name").value("지원자"))
73+
.andExpect(jsonPath("$.data.content[1].is_read").value(true))
74+
.andExpect(jsonPath("$.data.pageable.pageNumber").value(0))
75+
.andExpect(jsonPath("$.data.pageable.pageSize").value(6))
76+
.andExpect(jsonPath("$.data.totalElements").value(2));
77+
}
78+
79+
@DisplayName("봉사자는 자신에게 온 쪽지를 페이지 형태로 확인할 수 있다. (Controller)")
80+
@Test
81+
@WithMockCustomUser
82+
void getNotesByVolunteerId() throws Exception {
83+
// given
84+
Pageable pageable = PageRequest.of(0, 6);
85+
List<NoteReceiverViewForVolunteer> notes = List.of(
86+
NoteReceiverViewForVolunteer.builder()
87+
.id(1L)
88+
.title("답변 드립니다.")
89+
.senderName("서울 도서관")
90+
.isRead(false)
91+
.build(),
92+
NoteReceiverViewForVolunteer.builder()
93+
.id(2L)
94+
.title("요양원 입니다.")
95+
.senderName("서울 요양원")
96+
.isRead(true)
97+
.build()
98+
);
99+
Page<NoteReceiverViewForVolunteer> mockResponse = new PageImpl<>(notes, pageable, notes.size());
100+
101+
when(noteQueryUseCase.getNotesForVolunteer(any(UUID.class), eq(pageable)))
102+
.thenReturn(mockResponse);
103+
104+
// When & Then
105+
mockMvc.perform(get("/api/note/volunteer")
106+
.param("page", "0")
107+
.param("size", "6")
108+
.accept(MediaType.APPLICATION_JSON))
109+
.andExpect(status().isOk())
110+
.andExpect(jsonPath("$.code").value(200))
111+
.andExpect(jsonPath("$.message").value("내 쪽지 조회 성공"))
112+
.andExpect(jsonPath("$.data.content").isArray())
113+
.andExpect(jsonPath("$.data.content[0].id").value(1))
114+
.andExpect(jsonPath("$.data.content[0].title").value("답변 드립니다."))
115+
.andExpect(jsonPath("$.data.content[0].sender_name").value("서울 도서관"))
116+
.andExpect(jsonPath("$.data.content[0].is_read").value(false))
117+
.andExpect(jsonPath("$.data.content[1].id").value(2))
118+
.andExpect(jsonPath("$.data.content[1].title").value("요양원 입니다."))
119+
.andExpect(jsonPath("$.data.content[1].sender_name").value("서울 요양원"))
120+
.andExpect(jsonPath("$.data.content[1].is_read").value(true))
121+
.andExpect(jsonPath("$.data.pageable.pageNumber").value(0))
122+
.andExpect(jsonPath("$.data.pageable.pageSize").value(6))
123+
.andExpect(jsonPath("$.data.totalElements").value(2));
124+
}
125+
}

0 commit comments

Comments
 (0)