Skip to content

Commit

Permalink
Feature/33 : 스크랩 기능 (#36)
Browse files Browse the repository at this point in the history
* ✨ Feat: Scrap 엔티티 생성 & Member 테이블 Auditing

Scrap 테이블

* ✨ Feat: Scrap 코드 베이스 작성

컨트롤러, 서비스, 리포지토리, 컨버터, DTO

* ✨ Feat: 비즈니스 로직 작성

Service create 메소드

* ✨ Feat: 스크랩 취소, 내 스크랩 조회 API

기능 완료
  • Loading branch information
swa07016 authored Sep 26, 2023
1 parent 56bd521 commit 69263e7
Show file tree
Hide file tree
Showing 15 changed files with 549 additions and 2,323 deletions.
426 changes: 0 additions & 426 deletions logs/info.2023-09-16.log

This file was deleted.

2,145 changes: 255 additions & 1,890 deletions logs/info.log

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions src/main/java/briefing/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,29 @@ public enum ErrorCode {
HIJACK_JWT_TOKEN_EXCEPTION(UNAUTHORIZED,"AUTH007","탈취된(로그아웃 된) 토큰입니다 다시 로그인 해주세요."),
INVALID_REFRESH_TOKEN(BAD_REQUEST,"AUTH009","리프레쉬 토큰이 유효하지 않습니다. 다시 로그인 해주세요"),



// oauth 에러
APPLE_BAD_REQUEST(BAD_REQUEST, "OAUTH001", "애플 토큰이 잘못되었습니다."),
APPLE_SERVER_ERROR(FORBIDDEN, "OAUTH002", "애플 서버와 통신에 실패 하였습니다."),
FAIL_TO_MAKE_APPLE_PUBLIC_KEY(BAD_REQUEST, "OAUTH003", "새로운 애플 공개키 생성에 실패하였습니다."),
// feign client 에러

// feign client 에러
FEIGN_BAD_REQUEST(BAD_REQUEST, "FEIGN_400_1", "Feign server bad request"),
FEIGN_UNAUTHORIZED(BAD_REQUEST, "FEIGN_400_2", "Feign server unauthorized"),
FEIGN_FORBIDDEN(BAD_REQUEST, "FEIGN_400_3", "Feign server forbidden"),
FEIGN_EXPIRED_TOKEN(BAD_REQUEST, "FEIGN_400_4", "Feign server expired token"),
FEIGN_NOT_FOUND(BAD_REQUEST, "FEIGN_400_5", "Feign server not found error"),
FEIGN_INTERNAL_SERVER_ERROR(BAD_REQUEST, "FEIGN_400_6", "Feign server internal server error"),
FEIGN_METHOD_NOT_ALLOWED(BAD_REQUEST,"FEIGN_400_7" , "Feign server method not allowed"),
FEIGN_SERVER_ERROR(BAD_REQUEST,"FEIGN_500" , "Feign server error");
FEIGN_SERVER_ERROR(BAD_REQUEST,"FEIGN_500" , "Feign server error"),

// member 에러
MEMBER_NOT_FOUND(NOT_FOUND, "MEMBER_001", "존재하지 않는 사용자입니다."),


// scrap 에러
SCRAP_ALREADY_EXISTS(CONFLICT, "SCRAP_001", "이미 스크랩했습니다."),
SCRAP_NOT_FOUND(NOT_FOUND, "SCRAP_002", "존재하지 않는 스크랩입니다.");


private final HttpStatus httpStatus;
private final String code;
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/briefing/member/domain/Member.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package briefing.member.domain;

import briefing.base.BaseDateTimeEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter @Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member {
public class Member extends BaseDateTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/briefing/member/exception/MemberException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package briefing.member.exception;

import briefing.exception.ErrorCode;
import briefing.exception.GeneralException;

public class MemberException extends GeneralException {
public MemberException(ErrorCode errorCode){
super(errorCode);
}
}
44 changes: 44 additions & 0 deletions src/main/java/briefing/scrap/api/ScrapApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package briefing.scrap.api;

import briefing.common.response.CommonResponse;
import briefing.scrap.application.ScrapCommandService;
import briefing.scrap.application.ScrapQueryService;
import briefing.scrap.application.dto.ScrapRequest;
import briefing.scrap.application.dto.ScrapResponse;
import briefing.scrap.domain.Scrap;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "05-Scrap 📁", description = "스크랩 관련 API")
@RestController
@RequestMapping("/scraps")
@RequiredArgsConstructor
public class ScrapApi {
private final ScrapQueryService scrapQueryService;
private final ScrapCommandService scrapCommandService;

@Operation(summary = "05-01 Scrap📁 스크랩하기 #FRAME", description = "브리핑을 스크랩하는 API입니다.")
@PostMapping("/briefings")
public CommonResponse<ScrapResponse.CreateDTO> create(@RequestBody ScrapRequest.CreateDTO request) {
Scrap createdScrap = scrapCommandService.create(request);
return CommonResponse.onSuccess(ScrapConverter.toCreateDTO(createdScrap));
}

@Operation(summary = "05-02 Scrap📁 스크랩 취소 #FRAME", description = "스크랩을 취소하는 API입니다.")
@DeleteMapping("/{scrapId}")
public CommonResponse<ScrapResponse.DeleteDTO> delete(@PathVariable Long scrapId) {
Scrap deletedScrap = scrapCommandService.delete(scrapId);
return CommonResponse.onSuccess(ScrapConverter.toDeleteDTO(deletedScrap));
}

@Operation(summary = "05-03 Scrap📁 내 스크랩 조회 #FRAME", description = "내 스크랩을 조회하는 API입니다.")
@GetMapping("/briefings/members/{memberId}")
public CommonResponse<List<ScrapResponse.ReadDTO>> getScrapsByMember(@PathVariable Long memberId) {
List<Scrap> scraps = scrapQueryService.getScrapsByMemberId(memberId);
return CommonResponse.onSuccess(scraps.stream().map(ScrapConverter::toReadDTO).toList());
}
}
43 changes: 43 additions & 0 deletions src/main/java/briefing/scrap/api/ScrapConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package briefing.scrap.api;

import briefing.briefing.domain.Briefing;
import briefing.member.domain.Member;
import briefing.scrap.application.dto.ScrapResponse;
import briefing.scrap.domain.Scrap;

import java.time.LocalDateTime;

public class ScrapConverter {
public static ScrapResponse.CreateDTO toCreateDTO(Scrap createdScrap) {
return ScrapResponse.CreateDTO.builder()
.scrapId(createdScrap.getId())
.memberId(createdScrap.getMember().getId())
.briefingId(createdScrap.getBriefing().getId())
.createdAt(createdScrap.getCreatedAt())
.build();
}

public static Scrap toScrap(Member member, Briefing briefing) {
return Scrap.builder()
.member(member)
.briefing(briefing)
.build();
}

public static ScrapResponse.DeleteDTO toDeleteDTO(Scrap deletedScrap) {
return ScrapResponse.DeleteDTO.builder()
.scrapId(deletedScrap.getId())
.deletedAt(LocalDateTime.now())
.build();
}

public static ScrapResponse.ReadDTO toReadDTO(Scrap scrap) {
return ScrapResponse.ReadDTO.builder()
.briefingId(scrap.getBriefing().getId())
.ranks(scrap.getBriefing().getRanks())
.title(scrap.getBriefing().getTitle())
.subtitle(scrap.getBriefing().getSubtitle())
.date(scrap.getBriefing().getCreatedAt().toLocalDate())
.build();
}
}
53 changes: 53 additions & 0 deletions src/main/java/briefing/scrap/application/ScrapCommandService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package briefing.scrap.application;

import briefing.briefing.domain.Briefing;
import briefing.briefing.domain.repository.BriefingRepository;
import briefing.briefing.exception.BriefingException;
import briefing.briefing.exception.BriefingExceptionType;
import briefing.exception.ErrorCode;
import briefing.member.domain.Member;
import briefing.member.domain.repository.MemberRepository;
import briefing.member.exception.MemberException;
import briefing.scrap.api.ScrapConverter;
import briefing.scrap.application.dto.ScrapRequest;
import briefing.scrap.domain.Scrap;
import briefing.scrap.domain.repository.ScrapRepository;
import briefing.scrap.exception.ScrapException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class ScrapCommandService {

private final ScrapRepository scrapRepository;
private final MemberRepository memberRepository;
private final BriefingRepository briefingRepository;


public Scrap create(ScrapRequest.CreateDTO request) {
// 이미 스크랩한경우
if(scrapRepository.existsByMember_IdAndBriefing_Id(request.getMemberId(), request.getBriefingId()))
throw new ScrapException(ErrorCode.SCRAP_ALREADY_EXISTS);

Member member = memberRepository.findById(request.getMemberId())
.orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND));

Briefing briefing = briefingRepository.findById(request.getBriefingId())
.orElseThrow(() -> new BriefingException(BriefingExceptionType.NOT_FOUND_BRIEFING));

Scrap scrap = ScrapConverter.toScrap(member, briefing);

// Scrap 엔티티 저장 및 반환
return scrapRepository.save(scrap);
}

public Scrap delete(Long scrapId) {
Scrap scrap = scrapRepository.findById(scrapId)
.orElseThrow(() -> new ScrapException(ErrorCode.SCRAP_NOT_FOUND));
scrapRepository.delete(scrap);
return scrap;
}
}
21 changes: 21 additions & 0 deletions src/main/java/briefing/scrap/application/ScrapQueryService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package briefing.scrap.application;

import briefing.scrap.domain.Scrap;
import briefing.scrap.domain.repository.ScrapRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ScrapQueryService {

private final ScrapRepository scrapRepository;

public List<Scrap> getScrapsByMemberId(Long memberId) {
return scrapRepository.findByMember_Id(memberId);
}
}
12 changes: 12 additions & 0 deletions src/main/java/briefing/scrap/application/dto/ScrapRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package briefing.scrap.application.dto;

import lombok.Getter;

public class ScrapRequest {

@Getter
public static class CreateDTO {
private Long memberId;
private Long briefingId;
}
}
46 changes: 46 additions & 0 deletions src/main/java/briefing/scrap/application/dto/ScrapResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package briefing.scrap.application.dto;

import jakarta.persistence.Column;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;
import java.time.LocalDateTime;

public class ScrapResponse {

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class CreateDTO {
private Long scrapId;
private Long memberId;
private Long briefingId;
private LocalDateTime createdAt;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class DeleteDTO {
private Long scrapId;
private LocalDateTime deletedAt;
}


@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class ReadDTO {
private Long briefingId;
private Integer ranks;
private String title;
private String subtitle;
private LocalDate date;
}
}
26 changes: 26 additions & 0 deletions src/main/java/briefing/scrap/domain/Scrap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package briefing.scrap.domain;

import briefing.base.BaseDateTimeEntity;
import briefing.briefing.domain.Briefing;
import briefing.member.domain.Member;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter @Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Scrap extends BaseDateTimeEntity {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
private Member member;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
private Briefing briefing;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package briefing.scrap.domain.repository;

import briefing.scrap.domain.Scrap;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ScrapRepository extends JpaRepository<Scrap, Long> {

boolean existsByMember_IdAndBriefing_Id(Long memberId, Long briefingId);

List<Scrap> findByMember_Id(Long memberId);
}
10 changes: 10 additions & 0 deletions src/main/java/briefing/scrap/exception/ScrapException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package briefing.scrap.exception;

import briefing.exception.ErrorCode;
import briefing.exception.GeneralException;

public class ScrapException extends GeneralException {
public ScrapException(ErrorCode errorCode){
super(errorCode);
}
}
3 changes: 2 additions & 1 deletion src/main/java/briefing/security/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public WebSecurityCustomizer webSecurityCustomizer() {
"/swagger-ui/index.html",
"/swagger-ui/**",
"/docs/**",
"/members/auth/**");
"/members/auth/**",
"/scraps/**"); // NOTE - 토큰 발급 MERGE 전 테스트를 위해 허용
}

@Bean
Expand Down

0 comments on commit 69263e7

Please sign in to comment.