Skip to content

Commit

Permalink
Feat: 폴더 관련 기능 구현 (#39)
Browse files Browse the repository at this point in the history
* Feat: 요약 및 문제 생성 API 구현 (#4)

* Rename: AI Task 도메인 이름 변경

- llm 으로 변경

* Refactor: LLM 도메인 애플리케이션 계층과 프레젠테이션 계층 응답 분리

* Feat: LLM 작업 진행 상태 확인 기능 구현

* Feat: 요약 및 문제 결과 조회 기능 구현

* Feat: 요약 및 문제 생성 기능 구현

- AI 서버와 통신하는 부분 제외하고 기능 구현
- 임시 UUID 를 통해 task 저장

* Feat: LLM 서버 콜백 기능 구현

- LLM 서버가 API 콜을 통해 페이지별 요약 및 문제 내용 전달
- task id 를 통해 조회하여 요약 내용과 문제 내용 업데이트

* Refactor: 변수 이름 변경

- 필드명 카멜케이스로 변경

* Refactor: 통일성 없는 부분 수정

- 필드명 변경
- 변수 추출

* Refactor: 예외 종류, 메서드 네이밍 변경

- LLMQueryService 예외 타입 변경
- SummaryAndProblemUpdateResponse 메서드 네이밍 변경

* Refactor: LLMQueryService 응답과 LLMController 응답 분리

* Feat: 폴더 관련 기능 구현 (#6)

* Init: 프로젝트 기본설정 세팅

- 프로젝트 생성
- .gitignore설정
- 프로젝트 의존성 추가
- application.yml 설정파일 구성

* Init: 프로젝트 기본 구조 및 공통 컴포넌트 설정

- 공통 설정 클래스 추가 (JPA, QueryDSL, Swagger, Web)
- 공통 도메인 엔티티 (RootEntity) 정의
- 예외 처리 관련 클래스 및 타입 구현
- JSON 변환을 위한 AttributeConverter 추가
- 유틸리티 클래스 (Math) 추가

* Chore: Folder 도메인 폴더 구조 셋업

폴더 구조 셋업 작업

* Feat: Folder 도메인의 엔티티 생성

엔티티 생성자, 부모-자식간 연결로직 생성

* refactor: 자기 참조 관계 설정 수정

기존, 다대일 양방향 관계에서 다대일 단방향 관계로 설정하고, 삭제 등의 이슈 발생시 Service 계층에서 함수의 재귀사용을 통해 삭제할 예정

* Chore: Document 도메인 폴더 구조 셋업

폴더 구조 셋업 및 엔티티 생성

* Feat: Lombok 라이브러리 활용하여 기본 생성자 생성

기본 생성자 생성 lombok 라이브러리 활용하여 대체

* chore: name 필드의 length 50으로 설정

name 필드 (Document, Folder) 의 length = 50 으로 설정

* chore: Domain 계층의 Repository가 QueryRepository 상속받도록 함

상속 작업 수행

* chore: Member 도메인 매핑 작업 수행

Member 도메인 매핑 작업 수행

* chore: Member 도메인과 Folder 도메인 연결 작업 수행

Member 도메인과 Folder 도메인 연결 작업 수행

* feat: 루트 폴더 생성하는 기능 구현

루트 폴더 생성하는 기능 구현

* feat: 서브폴더 생성하는 기능 구현

서브 폴더 생성하는 기능 구현

* feat: 폴더를 루트로 이동시키는 기능 구현

폴더를 루트로 이동시키는 기능 구현

* feat: 새로운 폴더 내부로 이동시키는 기능 구현

새로운 폴더 내부로 이동시키는 기능 구현

* feat: 계층형 구조의 폴더 탐색 기능 구현

계층형 구조의 폴더 탐색 기능 구현

* test: 재귀적으로 폴더를 조회하는 테스트 코드 작성

재귀적으로 폴더 조회하는 테스트코드 작성

* remove: 사용하지 않는 QueryDSL 관련 파일 삭제

사용하지 않는 QueryDSL 관련 파일 삭제

* refactor: formatting 적용

formatting 적용

* feat: 폴더 재귀적으로 삭제하는 기능 구현

폴더 재귀적으로 삭제하는 기능 구현

* feat: @onDelete 어노테이션을 사용하여 삭제 기능 구현

삭제 기능 구현

* feat: 폴더 구조의 조회를 간편하게 개선

폴더 구조의 조회 간편하게 개선

* feat: 루트에 폴더를 생성하는 API 구현

루트에 폴더를 생성하는 API 구현

* feat: 서브 폴더를 생성하는 API 구현

서브 폴더 생성하는 API 구현

* feat: 폴더 이동하는 API 구현

폴더 이동하는 API 구현

* refactor: 중복된 함수 기능 병합 작업 수행

중복된 함수 기능 병합 작업 수행

* feat: 폴더 조회 API 구현

폴더 조회 API 구현

* feat: 폴더 삭제 API 구현

폴더 삭제 API 구현

* rename: 함수명 변경

함수 명 변경

* refactor: 메서드 분리 작업 수행

메서드 분리 작업 수행

* refactor: Delete API 204 로 반환

204로 반환

* feat: 요청마다 DTO를 다르게 설정

요청마다 DTO 다르게 설정

* refactor: 타입추론방식에서 타입명시방식으로 변경

타입명시방식으로 코드 스타일 변경

* refactor: 도메인 값에 대한 검증은 도메인계층으로 옮김

도메인 계층으로 값에 대한 검증 이동

* refactor: Owner가 아닌 폴더에 접근하려고 하는 경우 NotFoundException 예외 발생

예외 발생

---------

Co-authored-by: rladbrua0207 <[email protected]>

---------

Co-authored-by: 윤정훈 <[email protected]>
Co-authored-by: rladbrua0207 <[email protected]>
  • Loading branch information
3 people authored Sep 29, 2024
1 parent 849ac01 commit ee9c068
Show file tree
Hide file tree
Showing 26 changed files with 378 additions and 76 deletions.
5 changes: 5 additions & 0 deletions .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/main/java/notai/auth/TokenPair.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package notai.auth;

public record TokenPair(String accessToken, String refreshToken) {
public record TokenPair(
String accessToken,
String refreshToken
) {
}
28 changes: 11 additions & 17 deletions src/main/java/notai/auth/TokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,17 @@ public TokenService(TokenProperty tokenProperty, MemberRepository memberReposito
}

public String createAccessToken(Long memberId) {
return Jwts.builder()
.claim(MEMBER_ID_CLAIM, memberId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + accessTokenExpirationMillis))
.signWith(secretKey, Jwts.SIG.HS512)
.compact();
return Jwts.builder().claim(MEMBER_ID_CLAIM,
memberId
).issuedAt(new Date()).expiration(new Date(System.currentTimeMillis() + accessTokenExpirationMillis)).signWith(secretKey,
Jwts.SIG.HS512
).compact();
}

private String createRefreshToken() {
return Jwts.builder()
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + refreshTokenExpirationMillis))
.signWith(secretKey, Jwts.SIG.HS512)
.compact();
return Jwts.builder().issuedAt(new Date()).expiration(new Date(System.currentTimeMillis() + refreshTokenExpirationMillis)).signWith(secretKey,
Jwts.SIG.HS512
).compact();
}

public TokenPair createTokenPair(Long memberId) {
Expand Down Expand Up @@ -71,12 +68,9 @@ public TokenPair refreshTokenPair(String refreshToken) {

public Long extractMemberId(String token) {
try {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get(MEMBER_ID_CLAIM, Long.class);
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(MEMBER_ID_CLAIM,
Long.class
);
} catch (Exception e) {
throw new UnAuthorizedException("유효하지 않은 토큰입니다.");
}
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/notai/client/oauth/kakao/KakaoClient.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package notai.client.oauth.kakao;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.service.annotation.GetExchange;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;

public interface KakaoClient {

@GetExchange(url = "https://kapi.kakao.com/v2/user/me")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package notai.client.oauth.kakao;

import lombok.extern.slf4j.Slf4j;
import static notai.client.HttpInterfaceUtil.createHttpInterface;
import notai.common.exception.type.ExternalApiException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.client.RestClient;

import static notai.client.HttpInterfaceUtil.createHttpInterface;

@Slf4j
@Configuration
public class KakaoClientConfig {
Expand Down
48 changes: 26 additions & 22 deletions src/main/java/notai/client/oauth/kakao/KakaoMemberResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,33 @@

@JsonNaming(value = SnakeCaseStrategy.class)
public record KakaoMemberResponse(
Long id,
boolean hasSignedUp,
LocalDateTime connectedAt,
KakaoAccount kakaoAccount) {
Long id,
boolean hasSignedUp,
LocalDateTime connectedAt,
KakaoAccount kakaoAccount
) {

public Member toDomain() {
return new Member(
new OauthId(String.valueOf(id), OauthProvider.KAKAO),
kakaoAccount.email,
kakaoAccount.profile.nickname);
}
public Member toDomain() {
return new Member(
new OauthId(String.valueOf(id), OauthProvider.KAKAO),
kakaoAccount.email,
kakaoAccount.profile.nickname
);
}

@JsonNaming(value = SnakeCaseStrategy.class)
public record KakaoAccount(
Profile profile,
boolean emailNeedsAgreement,
boolean isEmailValid,
boolean isEmailVerified,
String email) {
}
@JsonNaming(value = SnakeCaseStrategy.class)
public record KakaoAccount(
Profile profile,
boolean emailNeedsAgreement,
boolean isEmailValid,
boolean isEmailVerified,
String email
) {
}

@JsonNaming(value = SnakeCaseStrategy.class)
public record Profile(
String nickname) {
}
@JsonNaming(value = SnakeCaseStrategy.class)
public record Profile(
String nickname
) {
}
}
3 changes: 1 addition & 2 deletions src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import notai.client.oauth.OauthClient;
import notai.member.domain.Member;
import notai.member.domain.OauthProvider;
import org.springframework.stereotype.Component;

import static notai.member.domain.OauthProvider.KAKAO;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/notai/folder/application/FolderQueryService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
package notai.folder.application;

import lombok.RequiredArgsConstructor;
import notai.folder.application.result.FolderFindResult;
import notai.folder.domain.Folder;
import notai.folder.domain.FolderRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class FolderQueryService {

private final FolderRepository folderRepository;

public List<FolderFindResult> getFolders(Long memberId, Long parentFolderId) {
List<Folder> folders = getFoldersWithMemberAndParent(memberId, parentFolderId);
// document read
return folders.stream().map(this::getFolderResult).toList();
}

private List<Folder> getFoldersWithMemberAndParent(Long memberId, Long parentFolderId) {
if (parentFolderId == null) {
return folderRepository.findAllByMemberIdAndParentFolderIsNull(memberId);
}
return folderRepository.findAllByMemberIdAndParentFolderId(memberId, parentFolderId);
}

private FolderFindResult getFolderResult(Folder folder) {
Long parentFolderId = folder.getParentFolder() != null ? folder.getParentFolder().getId() : null;
return FolderFindResult.of(folder.getId(), parentFolderId, folder.getName());
}
}
60 changes: 60 additions & 0 deletions src/main/java/notai/folder/application/FolderService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,69 @@
package notai.folder.application;

import lombok.RequiredArgsConstructor;
import notai.common.exception.type.BadRequestException;
import notai.folder.application.result.FolderMoveResult;
import notai.folder.application.result.FolderSaveResult;
import notai.folder.domain.Folder;
import notai.folder.domain.FolderRepository;
import notai.folder.presentation.request.FolderMoveRequest;
import notai.folder.presentation.request.FolderSaveRequest;
import notai.member.domain.Member;
import notai.member.domain.MemberRepository;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class FolderService {

private final FolderRepository folderRepository;
private final MemberRepository memberRepository;

public FolderSaveResult saveRootFolder(Long memberId, FolderSaveRequest folderSaveRequest) {
Member member = memberRepository.getById(memberId);
Folder folder = new Folder(member, folderSaveRequest.name());
Folder savedFolder = folderRepository.save(folder);
return getFolderSaveResult(savedFolder);
}

public FolderSaveResult saveSubFolder(Long memberId, FolderSaveRequest folderSaveRequest) {
Member member = memberRepository.getById(memberId);
Folder parentFolder = folderRepository.getById(folderSaveRequest.parentFolderId());
Folder folder = new Folder(member, folderSaveRequest.name(), parentFolder);
Folder savedFolder = folderRepository.save(folder);
return getFolderSaveResult(savedFolder);
}

public FolderMoveResult moveRootFolder(Long memberId, Long id) {
Folder folder = folderRepository.getById(id);
folder.validateOwner(memberId);
folder.moveRootFolder();
folderRepository.save(folder);
return getFolderMoveResult(folder);
}

public FolderMoveResult moveNewParentFolder(Long memberId, Long id, FolderMoveRequest folderMoveRequest) {
Folder folder = folderRepository.getById(id);
Folder parentFolder = folderRepository.getById(folderMoveRequest.targetFolderId());
folder.validateOwner(memberId);
folder.moveNewParentFolder(parentFolder);
folderRepository.save(folder);
return getFolderMoveResult(folder);
}

public void deleteFolder(Long memberId, Long id) {
if (!folderRepository.existsByMemberIdAndId(memberId, id)) {
throw new BadRequestException("올바르지 않은 요청입니다.");
}
folderRepository.deleteById(id);
}

private FolderSaveResult getFolderSaveResult(Folder folder) {
Long parentFolderId = folder.getParentFolder() != null ? folder.getParentFolder().getId() : null;
return FolderSaveResult.of(folder.getId(), parentFolderId, folder.getName());
}

private FolderMoveResult getFolderMoveResult(Folder folder) {
return FolderMoveResult.of(folder.getId(), folder.getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package notai.folder.application.result;

public record FolderFindResult(
Long id,
Long parentId,
String name
) {
public static FolderFindResult of(Long id, Long parentId, String name) {
return new FolderFindResult(id, parentId, name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package notai.folder.application.result;

public record FolderMoveResult(
Long id,
String name
) {
public static FolderMoveResult of(Long id, String name) {
return new FolderMoveResult(id, name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package notai.folder.application.result;

public record FolderSaveResult(
Long id,
Long parentId,
String name
) {
public static FolderSaveResult of(Long id, Long parentId, String name) {
return new FolderSaveResult(id, parentId, name);
}
}
12 changes: 11 additions & 1 deletion src/main/java/notai/folder/domain/Folder.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import notai.common.domain.RootEntity;
import notai.common.exception.type.NotFoundException;
import notai.member.domain.Member;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

@Entity
@Table(name = "folder")
Expand All @@ -20,7 +23,7 @@ public class Folder extends RootEntity<Long> {
private Long id;

@NotNull
@ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

Expand All @@ -30,6 +33,7 @@ public class Folder extends RootEntity<Long> {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_folder_id", referencedColumnName = "id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Folder parentFolder;

public Folder(Member member, String name) {
Expand All @@ -50,4 +54,10 @@ public void moveRootFolder() {
public void moveNewParentFolder(Folder parentFolder) {
this.parentFolder = parentFolder;
}

public void validateOwner(Long memberId) {
if (!this.member.getId().equals(memberId)) {
throw new NotFoundException("해당 이용자가 보유한 폴더 중 이 폴더가 존재하지 않습니다.");
}
}
}
15 changes: 13 additions & 2 deletions src/main/java/notai/folder/domain/FolderRepository.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
package notai.folder.domain;

import notai.folder.query.FolderQueryRepository;
import notai.common.exception.type.NotFoundException;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FolderRepository extends JpaRepository<Folder, Long>, FolderQueryRepository {
import java.util.List;

public interface FolderRepository extends JpaRepository<Folder, Long> {
default Folder getById(Long id) {
return findById(id).orElseThrow(() -> new NotFoundException("폴더 정보를 찾을 수 없습니다."));
}

List<Folder> findAllByMemberIdAndParentFolderIsNull(Long memberId);

List<Folder> findAllByMemberIdAndParentFolderId(Long memberId, Long parentFolderId);

boolean existsByMemberIdAndId(Long memberId, Long id);
}
Loading

0 comments on commit ee9c068

Please sign in to comment.