Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 유저가 선호하는 필터를 저장하는 기능을 구현한다 #453

Merged
merged 7 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ endif::[]
:sectlinks:

include::station.adoc[]
include::Filter.adoc[]
include::filter.adoc[]
35 changes: 35 additions & 0 deletions backend/src/docs/asciidoc/member.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
= ADMIN API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== 회원이 등록한 모든 필터를 조회한다.

=== Request

include::{snippets}/member-controller-test/find_all_filters/request-headers.adoc[]
include::{snippets}/member-controller-test/find_all_filters/path-parameters.adoc[]
include::{snippets}/member-controller-test/find_all_filters/request-body.adoc[]
include::{snippets}/member-controller-test/find_all_filters/http-request.adoc[]

=== Response

include::{snippets}/member-controller-test/find_all_filters/response-fields.adoc[]
include::{snippets}/member-controller-test/find_all_filters/http-response.adoc[]

== 회원의 선호 필터를 등록한다

=== Request

include::{snippets}/member-controller-test/add_member_filters/request-headers.adoc[]
include::{snippets}/member-controller-test/add_member_filters/path-parameters.adoc[]
include::{snippets}/member-controller-test/add_member_filters/request-fields.adoc[]
include::{snippets}/member-controller-test/add_member_filters/http-request.adoc[]

=== Response

include::{snippets}/member-controller-test/add_member_filters/response-fields.adoc[]
include::{snippets}/member-controller-test/add_member_filters/http-response.adoc[]

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.carffeine.carffeine.filter.controller.dto;

import com.carffeine.carffeine.filter.domain.Filter;
import com.carffeine.carffeine.member.domain.MemberFilter;

import java.util.List;

Expand All @@ -11,6 +12,10 @@ public record FiltersResponse(
) {

public static FiltersResponse from(List<Filter> filters) {
return getFiltersResponse(filters);
}

private static FiltersResponse getFiltersResponse(List<Filter> filters) {
List<String> companies = filters.stream()
.filter(it -> it.getFilterType().isCompanyType())
.map(Filter::getName)
Expand All @@ -32,4 +37,12 @@ public static FiltersResponse from(List<Filter> filters) {
connectorTypes
);
}

public static FiltersResponse fromMemberFilters(List<MemberFilter> memberFilters) {
List<Filter> filters = memberFilters.stream()
.map(MemberFilter::getFilter)
.toList();

return getFiltersResponse(filters);
Comment on lines +41 to +46
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filter response를 두 api에서 사용하는 것 같아보이는데요 이렇게 하신 특별한 이유가 있으실까요? 만약 하나를 위해 변경하면 둘 다 변경되는 일이 생길 것 같아서요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter 등록 후 반환과, 멤버가 선호 Filter를 등록하고 반환하는 형식은 같습니다
멤버가 선호하는 MemberFilter 역시도 Filter 테이블 안에 속하기 때문입니다.

그래서 따로 반환될 일은 없다고 생각했습니다

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class Filter extends BaseEntity {
@Enumerated(EnumType.STRING)
private FilterType filterType;

public Filter(final String name, final FilterType filterType) {
public Filter(String name, FilterType filterType) {
this.name = name;
this.filterType = filterType;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.carffeine.carffeine.member.controller;

import com.carffeine.carffeine.auth.controller.AuthMember;
import com.carffeine.carffeine.filter.controller.dto.FiltersResponse;
import com.carffeine.carffeine.filter.domain.Filter;
import com.carffeine.carffeine.filter.service.dto.FiltersRequest;
import com.carffeine.carffeine.member.domain.MemberFilter;
import com.carffeine.carffeine.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequiredArgsConstructor
@RequestMapping("/members")
@RestController
public class MemberController {

private final MemberService memberService;

@GetMapping("/{memberId}/filters")
public ResponseEntity<FiltersResponse> findMemberFilters(@PathVariable Long memberId,
@AuthMember Long loginMember) {
List<Filter> memberFilters = memberService.findMemberFilters(memberId, loginMember);
return ResponseEntity.ok(FiltersResponse.from(memberFilters));
}

@PostMapping("/{memberId}/filters")
public ResponseEntity<FiltersResponse> addMemberFilters(@PathVariable Long memberId,
@AuthMember Long loginMember,
@RequestBody FiltersRequest filtersRequest) {
List<MemberFilter> memberFilters = memberService.addMemberFilters(memberId, loginMember, filtersRequest);
return ResponseEntity.ok(FiltersResponse.fromMemberFilters(memberFilters));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -18,11 +19,12 @@
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(of = {"id"}, callSuper = false)
@Entity
public class Member extends BaseEntity {

private static final int EMAIL_MASKING_LENGTH = 2;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -41,6 +43,10 @@ public boolean isAdmin() {
return memberRole == MemberRole.ADMIN;
}

public boolean isSame(Long id) {
return this.id.equals(id);
}

public String maskEmail() {
return this.email.charAt(0) + "*".repeat(EMAIL_MASKING_LENGTH) + this.email.substring(EMAIL_MASKING_LENGTH + 1);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.carffeine.carffeine.member.domain;

import com.carffeine.carffeine.common.domain.BaseEntity;
import com.carffeine.carffeine.filter.domain.Filter;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "member_filter")
@EqualsAndHashCode(of = {"id"}, callSuper = false)
@Entity
public class MemberFilter extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@JoinColumn(name = "member_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on delete를 사용하신 이유가 무엇인가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mebmer_filter 테이블은 (id, member_id, filter_id) 컬럼을 가지고, 멤버가 선호하는 필터를 나타내는 테이블입니다.

만약 member_filter테이블에서 member가 제거되거나, filter가 제거된다면 의미가 없는 column이 됩니다.
그래서 참조하는 객체가 삭제되면 해당 객체도 삭제되는 것을 의도했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 질문이 너무 짧았네요 cascade와 orphanremoval 속성을 사용하지 않고 on delete 옵션을 사용하신 이유가 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 지금 테이블 관계가 Member <--(1:n)-- MemberFilter --(n:1)--> Filter 이렇게 되어 있습니다.
단방향 관계로 MemberFilter에서만 Member, Filter를 참조하는 구조입니다.

그래서 orphanremoval은 적용할 수 없었고, cacade = CASCADE.REMOVE 혹은 OnDelete를 적용할 수 있었습니다.

제가 알기로는 cascade로 삭제하는 것은 jpa 레벨에서 진행되는 것이고, 참조하는 레코드 수 만큼 쿼리가 나가고, OnDelete로 삭제하는 것은 db단에서 처리하기 때문에 삭제쿼리가 나가지 않고 삭제가 되는 것으로 알고 있습니다!

혹시 틀린 부분이 있다면 말씀해주세요~

private Member member;

@ManyToOne(cascade = CascadeType.REMOVE)
@JoinColumn(name = "filter_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Filter filter;

public MemberFilter(Member member, Filter filter) {
this.member = member;
this.filter = filter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.carffeine.carffeine.member.domain;

import org.springframework.data.repository.Repository;

import java.util.List;

public interface MemberFilterRepository extends Repository<MemberFilter, Long> {

List<MemberFilter> findAllByMember(Member member);

void deleteAllByMember(Member member);

<S extends MemberFilter> List<S> saveAll(Iterable<S> memberFilters);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
public enum MemberExceptionType implements ExceptionType {

NOT_FOUND(Status.NOT_FOUND, 3001, "회원이 없습니다"),
NOT_FOUND_ROLE(Status.NOT_FOUND, 3002, "일치하는 권한이 없습니다");
NOT_FOUND_ROLE(Status.NOT_FOUND, 3002, "일치하는 권한이 없습니다"),
INVALID_ACCESS(Status.INVALID, 3003, "본인의 계정이 아닙니다");

private final Status status;
private final int exceptionCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.carffeine.carffeine.member.service;

import com.carffeine.carffeine.filter.domain.Filter;
import com.carffeine.carffeine.filter.domain.FilterRepository;
import com.carffeine.carffeine.filter.exception.FilterException;
import com.carffeine.carffeine.filter.exception.FilterExceptionType;
import com.carffeine.carffeine.filter.service.dto.FiltersRequest;
import com.carffeine.carffeine.member.domain.Member;
import com.carffeine.carffeine.member.domain.MemberFilter;
import com.carffeine.carffeine.member.domain.MemberFilterRepository;
import com.carffeine.carffeine.member.domain.MemberRepository;
import com.carffeine.carffeine.member.exception.MemberException;
import com.carffeine.carffeine.member.exception.MemberExceptionType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
public class MemberService {

private final MemberRepository memberRepository;
private final MemberFilterRepository memberFilterRepository;
private final FilterRepository filterRepository;

@Transactional(readOnly = true)
public List<Filter> findMemberFilters(Long memberId, Long loginMember) {
Member member = findMember(memberId, loginMember);

return memberFilterRepository.findAllByMember(member).stream()
.map(MemberFilter::getFilter)
.collect(Collectors.toList());
}

private Member findMember(Long memberId, Long loginMember) {
Member member = memberRepository.findById(loginMember)
.orElseThrow(() -> new MemberException(MemberExceptionType.NOT_FOUND));

validateMember(memberId, member);
return member;
}

private static void validateMember(Long memberId, Member member) {
if (!member.isSame(memberId)) {
throw new MemberException(MemberExceptionType.INVALID_ACCESS);
}
}

@Transactional
public List<MemberFilter> addMemberFilters(Long memberId, Long loginMember, FiltersRequest filtersRequest) {
Member member = findMember(memberId, loginMember);
memberFilterRepository.deleteAllByMember(member);
return memberFilterRepository.saveAll(makeMemberFilters(filtersRequest, member));
}

private List<MemberFilter> makeMemberFilters(FiltersRequest filtersRequest, Member member) {
List<Filter> filters = filtersRequest.filters()
.stream()
.map(it -> filterRepository.findByName(it.name())
.orElseThrow(() -> new FilterException(FilterExceptionType.FILTER_NOT_FOUND)))
.toList();

return filters.stream()
.map(it -> new MemberFilter(member, it))
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public BigDecimal getCapacity() {
return capacity;
}

public boolean isUpdated(final Charger charger) {
public boolean isUpdated(Charger charger) {
if (!this.type.equals(charger.type)) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Optional<Filter> findByName(String name) {
}

@Override
public void deleteById(final Long id) {
public void deleteById(Long id) {
map.remove(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.carffeine.carffeine.auth.service.OAuthRequester;
import com.carffeine.carffeine.filter.service.FilterService;
import com.carffeine.carffeine.member.domain.MemberRepository;
import com.carffeine.carffeine.member.service.MemberService;
import com.carffeine.carffeine.station.service.congestion.CongestionService;
import com.carffeine.carffeine.station.service.report.ReportService;
import com.carffeine.carffeine.station.service.station.StationService;
Expand Down Expand Up @@ -42,4 +43,6 @@ public class MockBeanInjection {
protected AdminMemberService adminMemberService;
@MockBean
protected FilterService filterService;
@MockBean
protected MemberService memberService;
}