SNS 제작 (알람)

개발연습생log·2023년 1월 9일
0

SNS 제작

목록 보기
15/15
post-thumbnail

✨개요

🏃 목표

📢 알람 기능을 구현하자.

📌 요구사항

  • 특정 포스트에 새 댓글이 달리고, 좋아요가 눌리면 알람이 등록된다.
  • 알람 목록 pagination으로 받아온다.
    • 한 페이지당 default 피드 개수는 20개
    • 총 페이지 갯수가 표시
    • 작성 날짜 기준으로 최신순으로 sort
  • GET /alarms
  • 최신 순으로 20개씩 표시 (Pageable 사용)
  • 알람 리스트 조회 시 응답 필드
    • 테이블

      Untitled

    • id : 알람 ID

    • alarmType :알람 타입 (NEW_COMMENT_ON_POST, NEW_LIKE_ON_POST)

    • fromUserId: fromUserId(알림을 발생시킨 user id)

    • targetId : targetId(알림이 발생된 post id)

    • text : alarmType 따라 string 필드에 담아 줄 수 있도록 필드를 선언합니다.

      • NEW_COMMENT_ON_POST 일 때는 alarmText new comment!
      • NEW_LIKE_ON_POST 일 때는 alarmText "new like!"
    • createdAt : 등록일시

  • 리턴 (JSON 형식)
{
	"resultCode":"SUCCESS",
  "result": {
	"content":
	[
		{
	      "id": 1,
	      "alarmType": "NEW_LIKE_ON_POST",
        "fromUserId": 1,
        "targetId": 1,
	      "text": "new like!",
	      "createdAt": "2022-12-25T14:53:28.209+00:00",
	  }
	]
	}
}

📜 접근방법

  • 서비스계층에서 좋아요를 누르거나 댓글을 작성할 때 알람을 추가하는 로직으로 구현

✅ TO-DO

  • 알람 컨트롤러 테스트 구현
  • 알람 컨트롤러 구현
  • 알람 서비스 구현
  • 알람 리포지토리 구현
  • 알람 시큐리티 체인 수정

🔧 구현

알람 컨트롤러 테스트

알람 목록 조회 성공

<@Test
    @DisplayName("알람 조회 성공")
    @WithMockUser
    void alarm_SUCCESS() throws Exception {
        mockMvc.perform(get("/api/v1/alarms")
                        .with(csrf())
                        .param("page", "0")
                        .param("size", "10")
                        .param("sort", "createdAt,desc"))
                .andExpect(status().isOk());

        ArgumentCaptor<Pageable> pageableArgumentCaptor = ArgumentCaptor.forClass(Pageable.class);

        verify(alarmService).getAlarms(pageableArgumentCaptor.capture(),any());
        PageRequest pageRequest = (PageRequest) pageableArgumentCaptor.getValue();

        assertEquals(0, pageRequest.getPageNumber());
        assertEquals(10,pageRequest.getPageSize());
        assertEquals(Sort.by(Sort.Direction.DESC,"createdAt"), pageRequest.getSort());
    }

알람 목록 조회 실패 : 유저가 없는 경우

@Test
    @DisplayName("알람 조회 실패 : 유저가 없는 경우")
    @WithMockUser
    void alarm_FAIL_user() throws Exception {
        when(alarmService.getAlarms(any(), any()))
                .thenThrow(new AppException(ErrorCode.USERNAME_NOT_FOUND, ErrorCode.USERNAME_NOT_FOUND.getMessage()));

        mockMvc.perform(get("/api/v1/alarms")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isNotFound());
    }

알람 컨트롤러 구현

AlarmController

@RestController
@RequestMapping("/api/v1/alarms")
@RequiredArgsConstructor
public class AlarmController {

    private final AlarmService alarmService;

    @GetMapping
    public ResponseEntity<Response> getAlarms(@PageableDefault(size = 20) @SortDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable, Authentication authentication) {
        String userName = authentication.getName();
        Page<AlarmResponse> alarmResponses = alarmService.getAlarms(pageable, userName);
        return ResponseEntity.ok().body(Response.of("SUCCESS", alarmResponses));
    }
}

AlarmResponse

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class AlarmResponse {
    private Long id;
    private String alarmType;
    private Long fromUserId;
    private Long targetId;
    private String text;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd' 'HH:mm:ss", timezone = "Asia/Seoul")
    private LocalDateTime createdAt;

    public static Page<AlarmResponse> listOf(Page<Alarm> alarms) {
        Page<AlarmResponse> alarmResponses = alarms.map(m -> AlarmResponse.builder()
                .id(m.getId())
                .alarmType(m.getAlarmType())
                .fromUserId(m.getFromUser().getUserId())
                .targetId(m.getTargetPost().getId())
                .text(m.getText())
                .createdAt(m.getCreatedAt())
                .build());
        return alarmResponses;
    }
}

알람 서비스 구현

AlarmService

@Service
@RequiredArgsConstructor
public class AlarmService {
    private final UserRepository userRepository;
    private final AlarmRepository alarmRepository;

    public Page<AlarmResponse> getAlarms(Pageable pageable, String userName) {
        //유저체크
        User findUser = AppUtil.findUser(userRepository, userName);
        //알람 가져오기
        Page<Alarm> alarmPage = alarmRepository.findByUser(pageable, findUser);
        //알람 dto 변환 후 리턴
        Page<AlarmResponse> alarmResponses = AlarmResponse.listOf(alarmPage);
        return alarmResponses;
    }
}

AlarmUtil

public class AlarmUtil {
    public static Alarm saveAlarm(AlarmRepository alarmRepository, AlarmType alarmType, User fromUser, Post post) {
        Alarm alarm = Alarm.of(alarmType, fromUser, post);
        return alarmRepository.save(alarm);
    }
}

알람 리포지토리 구현

AlarmRepository

public interface AlarmRepository extends JpaRepository<Alarm, Long> {
    Page<Alarm> findByUser(Pageable pageable, User user);
}

Alarm

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@Entity
public class Alarm extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String alarmType;

    @ManyToOne
    @JoinColumn(name = "userId")
    private User user;
    @ManyToOne
    @JoinColumn(name = "fromUserID")
    private User fromUser;
    @ManyToOne
    @JoinColumn(name = "targetId")
    private Post targetPost;

    private String text;

    public static Alarm of(AlarmType alarmType, User user, Post post) {
        return Alarm.builder()
                .alarmType(alarmType.name())
                .user(post.getUser())
                .fromUser(user)
                .targetPost(post)
                .text(alarmType.getMessage())
                .build();
    }
}

알람 시큐리티 체인 수정

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
    private final AuthenticationManager authenticationManager;
    private final JwtFilter jwtFilter;
    private final String[] PERMIT_URL = {
            "/api/v1/hello",
            "/api/v1/users/join",
            "/api/v1/users/login"
    };

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers(PERMIT_URL).permitAll()
                .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated()
                .antMatchers(HttpMethod.PUT, "/api/v1/**").authenticated()
                .antMatchers(HttpMethod.DELETE, "/api/v1/**").authenticated()
                .antMatchers(HttpMethod.GET, "/api/v1/posts/my").authenticated()
                .antMatchers(HttpMethod.GET,"/api/v1/alarms").authenticated()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().authenticationEntryPoint(authenticationManager)
                .and()
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

🌉 회고

  • 알람을 끝으로 sns 제작 프로젝트의 요구사항 기능을 모두 구현했다.
  • 리팩토링할 부분은 리팩토링하면서 보완이 필요할 것 같다.
  • 프론트와 백엔드의 통신과정을 이해하기 위해 리액트를 공부하여 간단하게 프론트를 구현해 볼 예정이다.
profile
주니어 개발자를 향해서..

0개의 댓글