Study
Spring Boot, Vue.js 캘린더 일정 조회 및 세부사항 조회
Stranger_s
2020. 1. 30. 01:18
추가된 이벤트 조회 및 상세보기
지난 게시글에서 구현한 이벤트 추가하기에 이어 이벤트 조회 및 상세보기를 구현해 볼 것이다.
- Event 조회는 Event의 시작일 혹은 종료일이 해당 캘린더의 Month와 같은 것들만 조회할 것이다.
- 캘린더에 보이는 이벤트를 클릭했을 때 이벤트의 세부 정보를 볼 수 있는 Dialog를 띄울 것이다.
이벤트 조회
Spring Boot
- Event의 시작일의 연도, 개월과 종료일의 연도, 개월을 각각 비교해야 하고 이벤트를 조회하는 회원도 고려해야 한다.
- 쿼리가 조금씩 난잡해질수록 QueryDSL를 사용하는 게 매우 효율적이다.
- 그러므로 Event를 조회하는 쿼리는 QueryDSL로 작성해 볼 것이다.
QueryDSL 의존성 추가
plugins {
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
dependencies {
implementation 'com.querydsl:querydsl-jpa'
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
- QueryDSL의존성을 추가하고 queryDSL의 Q타입 객체들은 빌드 폴더에 넣었다.
./gradlew compileQuerydsl
- 의존성 추가 후 compileQuerydsl을 통해 Q타입 객체들을 생성한다.
@Configuration
public class QueryDslConfig {
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
- 마지막으로 JpaQueryFactory를 빈으로 등록하면 QueryDSL을 사용할 준비가 끝났다.
QueryDSL로 이벤트 조회하기
- 우선 Spring Data JPA의 Repository와 QueryDSL을 함께 쓰기 위해서는 몇 가지 절차가 필요하다.
public interface EventRepositoryCustom { // (1)
}
// (2)
public interface EventRepository extends JpaRepository<Event, Long>, EventRepositoryCustom {
}
@RequiredArgsConstructor // (3)
public class EventRepositoryImpl implements EventRepositoryCustom {
}
- 처음으로 1번과 같이 interface 타입으로 EventRepositoryCustom을 생성한다.
- 그 후 Spring Data JPA를 사용하는 EventRepository에서 1번을 상속받는다.
- 마지막으로 EventRepositoryImpl를 생성하여 EventRepositoryCusom(1번)을 구현한다.
- Custom을 구현하는 Impl에서 QueryDSl를 작성하면 된다.
- Repository뒤 접미사인 Impl, Custom의 이름을 변경하면 Spring Data JPA가 인식하지 못하는 것으로 알고 있으므로 그대로 사용하는 게 좋을 거 같다.
public interface EventRepositoryCustom {
List<Event> findByMonthAndMemberId(LocalDate date, Long memberId);
}
@RequiredArgsConstructor
public class EventRepositoryImpl implements EventRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<Event> findByMonthAndMemberId(LocalDate date, Long memberId) {
return queryFactory
.selectFrom(event)
.join(event.member, member)
.where(
member.id.eq(memberId),
startDateEq(date).or(endDateEq(date))
)
.fetch();
}
private BooleanExpression startDateEq(LocalDate date) {
return event.startDate.year().eq(date.getYear())
.and(event.startDate.month().eq(date.getMonthValue()));
}
private BooleanExpression endDateEq(LocalDate date) {
return event.endDate.year().eq(date.getYear())
.and(event.endDate.month().eq(date.getMonthValue()));
}
}
- EventRepositoryCusom에 기능을 정의하고 EventRepositoryImpl에서 기능을 구현한다.
- 이전에 등록한 jpaQueryFactory를 사용하여 쿼리를 작성한다.
- 작성된 쿼리를 보면 queryDSL을 사용하면 자바 코드로 매우 직관적으로 짤 수 있는 것을 알 수 있다.
- event를 membet와 join 하여 조회한다. 조회할 때 where 조건문을 보면 쉼표로 구분되어있다.
- 쉼표는 and와 똑같은 의미로 memberId가 같으면서 startDate, endDate 조건이 맞는 이벤트를 조회하는 것이다.
- startDateEq, endDateEq를 통해 각 시작일, 종료일의 연도와 개월이 파라미터의 연도와 개월과 일치하는지 검증한다.
TEST
@Test
void findEventsByDate() throws Exception {
//given
Member member = Member.builder()
.email("asd@asd.com")
.password("password")
.name("name")
.role(MemberRole.USER)
.build();
em.persist(member);
// 연도는 모두 20년이다.
// content이후 첫번째 인자가 시작 Month, 두번째 인자가 종료 Month이다.
Event event1 = getEvent(member, "title1", "content", 1, 1);
Event event2 = getEvent(member, "title2", "content", 12, 1);
Event event3 = getEvent(member, "title3", "content", 1, 2);
Event event4 = getEvent(member, "title4", "content", 2, 2);
Event event5 = getEvent(member, "title5", "content", 12, 12);
em.persist(event1);
em.persist(event2);
em.persist(event3);
em.persist(event4);
em.persist(event5);
em.flush();
em.clear();
//when
List<Event> findEvent = eventRepository.findByMonthAndMemberId(of(2020, 1, 20), member.getId());
//then
assertThat(findEvent).extracting("title").containsExactly("title1", "title2", "title3");
}
- 테스트로 기능이 제대로 동작하는지 확인해본다.
- 아규먼트가 20년 1월이므로 event1, 2, 3이 조회되어야 한다.
- 아래와 같이 테스트가 통과되는 것을 알 수 있다.
Service, Controller
// Service
public List<EventResponseDto> findByMonthAndMember(LocalDate date, String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("회원이 존재하지 않습니다."));
List<Event> findEvents = eventRepository.findByMonthAndMemberId(date, member.getId());
return findEvents.stream().map(EventResponseDto::new).collect(Collectors.toList());
}
// Controller
@GetMapping
public ResponseEntity queryEvents(@DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam LocalDate date,
@TokenMemberEmail String email){
List<EventResponseDto> findDtos = eventService.findByMonthAndMember(date, email);
return ResponseEntity.ok(findDtos);
}
Service
- 서비스에서는 파라미터로 받은 email을 통해 회원을 조회하고 그 회원의 아이디와 날짜에 맞게 이벤트들을 조회한 후 Dto로 변환하여 반환해준다.
Controller
- 컨트롤러에서는 Client가 Param으로 보내는 Date String을 @DateTimeFormat을 통해 LocalDate로 변환해준다.
- @TokenMemberEmail은 이벤트 조회 시 해당 유저의 JWT 토큰을 통해 email 정보를 받아온다.
- 두 개의 파라미터를 통해 service에게 조회를 요청하고 조회된 값을 반환해준다.
Vue.js
- vue.js는 지난 게시글들에서 디자인은 다 구현하였으므로 로직만 작성하면 된다.
// actions
async REQEUST_QUERY_EVENTS_BY_DATE(context, date) {
try {
const response = await requestQueryEvents(date);
context.commit('ADD_EVENTS', response.data);
} catch (e) {
store.commit('SET_SNACKBAR', setSnackBarInfo('이벤트 전체 조회를 실패하였습니다.', 'error', 'top'))
}
},
// api
function requestQueryEvents(date) {
return axios.get(`${process.env.VUE_APP_BASEURL}/api/events`, {
params: {date: date}
});
}
// mutations
ADD_EVENTS(state, events) {
state.events = [];
events.forEach(e => {
state.events.push(makeEvent(e));
})
},
// Calendar.vue
computed: {
events() {
return this.$store.state.calendar.events;
}
},
- Vuex를 통해 이벤트를 조회하는 방법으로 구현하였다.
- actions는 파라미터의 date를 api통신을 담당하는 requestQueryEvents에게 전달 인자로 넘겨준다.
- requestQueryEvents는 get요청을 통해 date를 파라미터로 넘겨주고 이벤트를 조회한 후 그 결과를 리턴한다.
- 리턴 값을 받은 actions는 mutations인 ADD_EVENTS에게 그 결과를 전달한다.
- ADD_EVENTS는 전달받은 이벤트를 state.events에 채워 넣는다.
- 그 값은 Calendar.vue에서 computed로 받아 캘린더에 담아준다.
watch:{
start(newDate, oldDate) {
let newDateMonth = this.$moment(newDate).format('MM');
let oldDateMonth = this.$moment(oldDate).format('MM');
if(newDateMonth !== oldDateMonth){
this.$store.dispatch('REQEUST_QUERY_EVENTS_BY_DATE', newDate);
}
}
}
- Calendar.vue에서는 캘린더의 날짜인 start를 watch를 통해 관찰한다.
- 값이 바뀔 때 이전 값과 바뀐 값의 개월 수가 다르면 actions에게 해당 날짜에 맞는 이벤트 조회를 요청한다.
결과 확인
@RequiredArgsConstructor
@Component
@Profile("local")
@Transactional
public class AppRunner implements ApplicationRunner {
private final MemberService memberService;
private final MemberRepository memberRepository;
private final EventRepository eventRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
Long mem = memberService.save(MemberJoinRequestDto.builder()
.email("123@123.com")
.password("123")
.name("John")
.role(MemberRole.ADMIN)
.build());
Member member = memberRepository.findById(mem).get();
saveEvents("블로그 작성", of(2020, 1, 28), of(2020, 1, 30), member);
saveEvents("회의", of(2019, 12, 20), of(2019, 12, 25), member);
saveEvents("2월달 면접준비", of(2020, 2, 2), of(2020, 2, 11), member);
saveEvents("면접 공부", of(2020, 1, 20), of(2020, 1, 30), member);
saveEvents("책 읽기", of(2020, 12, 30), of(2020, 1, 3), member);
}
private void saveEvents(String title, LocalDate start, LocalDate end, Member member){
Event event = Event.builder()
.startDate(start)
.endDate(end)
.title(title)
.content("content")
.build();
event.setMember(member);
eventRepository.save(event);
}
}
- 우선 서버단에서 초기 데이터를 미리 넣어준다.
- Vue 프로젝트를 구동시켜 캘린더를 확인해보면 제대로 값들이 들어오는 것을 볼 수 있다.
- 일정이 1 ~ 2월일 경우에도 1월 2월 모두 조회하여 정상적으로 보이는 것을 알 수 있다.
이벤트 상세보기
- 이벤트를 클릭하면 이벤트에 대한 세부사항을 보여줄 모달을 구현해본다.
- 서버단은 매우 단순한 조회이므로 따로 생략한다.
EventDetail.vue
<template>
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-width="400">
<v-card>
<v-card-title class="headline">{{event.title}}</v-card-title>
<v-card-text style="font-size: 1rem" class="font-weight-bold">{{event.content}}</v-card-text>
<div class="ml-5 font-weight-light">
<div>시작일: {{getEventStart()}}</div>
<div>종료일: {{getEventEnd()}}</div>
</div>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="green darken-1" text @click="close()">닫기</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</template>
<script>
export default {
name: "EventDetail",
computed: {
dialog() {
return this.$store.state.calendar.eventDetailDialog;
},
event() {
return this.$store.state.calendar.event;
}
},
methods: {
getEventStart() {
return this.event.startDate + getTime(this.event.startTime);
},
getEventEnd() {
return this.event.endDate + getTime(this.event.endTime);
},
close(){
return this.$store.commit('CLOSE_EVENT_DETAIL');
}
}
}
const getTime = (time) => {
return time === null ? '' : ` ${time}`;
};
</script>
- 이벤트 세부사항을 보여주는 모달이다.
- dialog, event는 vuex state를 통해 받아온다.
- 시작일과 종료일은 날짜와 시간을 합쳐서 보여줄 것이다.
구현 로직
// Calendar.vue
showEvent({event}) {
this.$store.dispatch('REQUEST_DETAIL_EVENT', event.id);
},
// actions
async REQUEST_DETAIL_EVENT(context, eventId) {
try {
const respone = await requestEventDetail(eventId);
context.commit('SHOW_EVENT_DETAIL', respone.data);
} catch (e) {
store.commit('SET_SNACKBAR', setSnackBarInfo('이벤트 상세 조회를 실패하였습니다.', 'error', 'top'))
}
},
// api
function requestEventDetail(eventId) {
return axios.get(`${process.env.VUE_APP_BASEURL}/api/events/${eventId}`);
}
// mutations
SHOW_EVENT_DETAIL(state, event) {
state.event = event;
state.eventDetailDialog = true;
},
- showEvent는 캘린더에서 이벤트를 클릭했을 때 동작하는 메서드이다.
- 해당 이벤트의 아이디를 아규먼트로 넘겨 actions를 요청한다.
- api 요청 결과를 mutations을 통해 event에 넣고 eventDetailDialog를 오픈하게 된다.
- 우선 이벤트를 추가해본다. 이벤트가 추가되면 캘린더에 반영되고 캘린더에서 더 이상 보여줄 칸이 없다면 more로 표시된다.
- more를 클릭하면 해당 일자의 일정을 아래와 같이 확인할 수 있다.
- 해당 이벤트를 클릭하면 이벤트 세부사항을 보여주는 모달이 나타난다.
현재까지 일정을 추가하고 조회하는 것까지 구현하였다. 다음에는 일정을 수정하고 해당 일정에 체크리스트 기능을 추가해볼 생각이다.