From ee9c0689a419a1ab1929604a6e53311e331fc6bd Mon Sep 17 00:00:00 2001
From: Hyun-Seo Jeong <90139789+hynseoj@users.noreply.github.com>
Date: Sun, 29 Sep 2024 16:09:08 +0900
Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=8F=B4=EB=8D=94=20=EA=B4=80=EB=A0=A8?=
=?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#39)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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 <48901587+rladbrua0207@users.noreply.github.com>
---------
Co-authored-by: 윤정훈 <76200940+yunjunghun0116@users.noreply.github.com>
Co-authored-by: rladbrua0207 <48901587+rladbrua0207@users.noreply.github.com>
---
.idea/codeStyles/codeStyleConfig.xml | 5 ++
src/main/java/notai/auth/TokenPair.java | 5 +-
src/main/java/notai/auth/TokenService.java | 28 ++++----
.../notai/client/oauth/kakao/KakaoClient.java | 3 +-
.../client/oauth/kakao/KakaoClientConfig.java | 3 +-
.../oauth/kakao/KakaoMemberResponse.java | 48 +++++++-------
.../client/oauth/kakao/KakaoOauthClient.java | 3 +-
.../application/FolderQueryService.java | 25 +++++++
.../folder/application/FolderService.java | 60 +++++++++++++++++
.../application/result/FolderFindResult.java | 11 ++++
.../application/result/FolderMoveResult.java | 10 +++
.../application/result/FolderSaveResult.java | 11 ++++
src/main/java/notai/folder/domain/Folder.java | 12 +++-
.../notai/folder/domain/FolderRepository.java | 15 ++++-
.../folder/presentation/FolderController.java | 65 ++++++++++++++++++-
.../request/FolderMoveRequest.java | 6 ++
.../request/FolderSaveRequest.java | 7 ++
.../response/FolderFindResponse.java | 13 ++++
.../response/FolderMoveResponse.java | 12 ++++
.../response/FolderSaveResponse.java | 13 ++++
.../folder/query/FolderQueryRepository.java | 4 --
.../query/FolderQueryRepositoryImpl.java | 10 ---
.../java/notai/llm/domain/TaskStatus.java | 4 +-
.../java/notai/BackendApplicationTests.java | 6 +-
.../oauth/kakao/KakaoOauthClientTest.java | 11 ++--
.../application/FolderQueryServiceTest.java | 64 ++++++++++++++++++
26 files changed, 378 insertions(+), 76 deletions(-)
create mode 100644 .idea/codeStyles/codeStyleConfig.xml
create mode 100644 src/main/java/notai/folder/application/result/FolderFindResult.java
create mode 100644 src/main/java/notai/folder/application/result/FolderMoveResult.java
create mode 100644 src/main/java/notai/folder/application/result/FolderSaveResult.java
create mode 100644 src/main/java/notai/folder/presentation/request/FolderMoveRequest.java
create mode 100644 src/main/java/notai/folder/presentation/request/FolderSaveRequest.java
create mode 100644 src/main/java/notai/folder/presentation/response/FolderFindResponse.java
create mode 100644 src/main/java/notai/folder/presentation/response/FolderMoveResponse.java
create mode 100644 src/main/java/notai/folder/presentation/response/FolderSaveResponse.java
delete mode 100644 src/main/java/notai/folder/query/FolderQueryRepository.java
delete mode 100644 src/main/java/notai/folder/query/FolderQueryRepositoryImpl.java
create mode 100644 src/test/java/notai/folder/application/FolderQueryServiceTest.java
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..7f9d6a5
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/notai/auth/TokenPair.java b/src/main/java/notai/auth/TokenPair.java
index 4e51456..0051082 100644
--- a/src/main/java/notai/auth/TokenPair.java
+++ b/src/main/java/notai/auth/TokenPair.java
@@ -1,4 +1,7 @@
package notai.auth;
-public record TokenPair(String accessToken, String refreshToken) {
+public record TokenPair(
+ String accessToken,
+ String refreshToken
+) {
}
diff --git a/src/main/java/notai/auth/TokenService.java b/src/main/java/notai/auth/TokenService.java
index b4b8510..9204e5a 100644
--- a/src/main/java/notai/auth/TokenService.java
+++ b/src/main/java/notai/auth/TokenService.java
@@ -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) {
@@ -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("유효하지 않은 토큰입니다.");
}
diff --git a/src/main/java/notai/client/oauth/kakao/KakaoClient.java b/src/main/java/notai/client/oauth/kakao/KakaoClient.java
index a465b0d..8d0b08d 100644
--- a/src/main/java/notai/client/oauth/kakao/KakaoClient.java
+++ b/src/main/java/notai/client/oauth/kakao/KakaoClient.java
@@ -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")
diff --git a/src/main/java/notai/client/oauth/kakao/KakaoClientConfig.java b/src/main/java/notai/client/oauth/kakao/KakaoClientConfig.java
index 3b79924..af7f7ed 100644
--- a/src/main/java/notai/client/oauth/kakao/KakaoClientConfig.java
+++ b/src/main/java/notai/client/oauth/kakao/KakaoClientConfig.java
@@ -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 {
diff --git a/src/main/java/notai/client/oauth/kakao/KakaoMemberResponse.java b/src/main/java/notai/client/oauth/kakao/KakaoMemberResponse.java
index a6a473e..6e81ed8 100644
--- a/src/main/java/notai/client/oauth/kakao/KakaoMemberResponse.java
+++ b/src/main/java/notai/client/oauth/kakao/KakaoMemberResponse.java
@@ -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
+ ) {
+ }
}
diff --git a/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java b/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java
index 4688f41..781a134 100644
--- a/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java
+++ b/src/main/java/notai/client/oauth/kakao/KakaoOauthClient.java
@@ -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
diff --git a/src/main/java/notai/folder/application/FolderQueryService.java b/src/main/java/notai/folder/application/FolderQueryService.java
index b51c863..1a3a830 100644
--- a/src/main/java/notai/folder/application/FolderQueryService.java
+++ b/src/main/java/notai/folder/application/FolderQueryService.java
@@ -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 getFolders(Long memberId, Long parentFolderId) {
+ List folders = getFoldersWithMemberAndParent(memberId, parentFolderId);
+ // document read
+ return folders.stream().map(this::getFolderResult).toList();
+ }
+
+ private List 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());
+ }
}
diff --git a/src/main/java/notai/folder/application/FolderService.java b/src/main/java/notai/folder/application/FolderService.java
index 74ca5c6..4e5e5ec 100644
--- a/src/main/java/notai/folder/application/FolderService.java
+++ b/src/main/java/notai/folder/application/FolderService.java
@@ -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());
+ }
}
diff --git a/src/main/java/notai/folder/application/result/FolderFindResult.java b/src/main/java/notai/folder/application/result/FolderFindResult.java
new file mode 100644
index 0000000..3014ac0
--- /dev/null
+++ b/src/main/java/notai/folder/application/result/FolderFindResult.java
@@ -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);
+ }
+}
diff --git a/src/main/java/notai/folder/application/result/FolderMoveResult.java b/src/main/java/notai/folder/application/result/FolderMoveResult.java
new file mode 100644
index 0000000..4004836
--- /dev/null
+++ b/src/main/java/notai/folder/application/result/FolderMoveResult.java
@@ -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);
+ }
+}
diff --git a/src/main/java/notai/folder/application/result/FolderSaveResult.java b/src/main/java/notai/folder/application/result/FolderSaveResult.java
new file mode 100644
index 0000000..bb01f50
--- /dev/null
+++ b/src/main/java/notai/folder/application/result/FolderSaveResult.java
@@ -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);
+ }
+}
diff --git a/src/main/java/notai/folder/domain/Folder.java b/src/main/java/notai/folder/domain/Folder.java
index f6367e8..1cbc797 100644
--- a/src/main/java/notai/folder/domain/Folder.java
+++ b/src/main/java/notai/folder/domain/Folder.java
@@ -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")
@@ -20,7 +23,7 @@ public class Folder extends RootEntity {
private Long id;
@NotNull
- @ManyToOne
+ @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;
@@ -30,6 +33,7 @@ public class Folder extends RootEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_folder_id", referencedColumnName = "id")
+ @OnDelete(action = OnDeleteAction.CASCADE)
private Folder parentFolder;
public Folder(Member member, String name) {
@@ -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("해당 이용자가 보유한 폴더 중 이 폴더가 존재하지 않습니다.");
+ }
+ }
}
diff --git a/src/main/java/notai/folder/domain/FolderRepository.java b/src/main/java/notai/folder/domain/FolderRepository.java
index 40a2231..d95fc33 100644
--- a/src/main/java/notai/folder/domain/FolderRepository.java
+++ b/src/main/java/notai/folder/domain/FolderRepository.java
@@ -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, FolderQueryRepository {
+import java.util.List;
+
+public interface FolderRepository extends JpaRepository {
+ default Folder getById(Long id) {
+ return findById(id).orElseThrow(() -> new NotFoundException("폴더 정보를 찾을 수 없습니다."));
+ }
+
+ List findAllByMemberIdAndParentFolderIsNull(Long memberId);
+
+ List findAllByMemberIdAndParentFolderId(Long memberId, Long parentFolderId);
+
+ boolean existsByMemberIdAndId(Long memberId, Long id);
}
diff --git a/src/main/java/notai/folder/presentation/FolderController.java b/src/main/java/notai/folder/presentation/FolderController.java
index 0c0383c..2f503dc 100644
--- a/src/main/java/notai/folder/presentation/FolderController.java
+++ b/src/main/java/notai/folder/presentation/FolderController.java
@@ -1,10 +1,24 @@
package notai.folder.presentation;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
+import notai.auth.Auth;
import notai.folder.application.FolderQueryService;
import notai.folder.application.FolderService;
+import notai.folder.application.result.FolderFindResult;
+import notai.folder.application.result.FolderMoveResult;
+import notai.folder.application.result.FolderSaveResult;
+import notai.folder.presentation.request.FolderMoveRequest;
+import notai.folder.presentation.request.FolderSaveRequest;
+import notai.folder.presentation.response.FolderFindResponse;
+import notai.folder.presentation.response.FolderMoveResponse;
+import notai.folder.presentation.response.FolderSaveResponse;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.*;
+
+import java.net.URI;
+import java.util.List;
@Controller
@RequestMapping("/api/folders")
@@ -13,4 +27,53 @@ public class FolderController {
private final FolderService folderService;
private final FolderQueryService folderQueryService;
+
+ @PostMapping
+ public ResponseEntity saveFolder(
+ @Auth Long memberId, @Valid @RequestBody FolderSaveRequest folderSaveRequest
+ ) {
+ FolderSaveResult folderResult = saveFolderResult(memberId, folderSaveRequest);
+ FolderSaveResponse response = FolderSaveResponse.from(folderResult);
+ return ResponseEntity.created(URI.create("/api/folders/" + response.id())).body(response);
+ }
+
+ @PostMapping("/{id}/move")
+ public ResponseEntity moveFolder(
+ @Auth Long memberId, @PathVariable Long id, @Valid @RequestBody FolderMoveRequest folderMoveRequest
+ ) {
+ FolderMoveResult folderResult = moveFolderWithRequest(memberId, id, folderMoveRequest);
+ FolderMoveResponse response = FolderMoveResponse.from(folderResult);
+ return ResponseEntity.ok(response);
+ }
+
+ @GetMapping
+ public ResponseEntity> getFolders(
+ @Auth Long memberId, @RequestParam(required = false) Long parentFolderId
+ ) {
+ List folderResults = folderQueryService.getFolders(memberId, parentFolderId);
+ List response = folderResults.stream().map(FolderFindResponse::from).toList();
+ return ResponseEntity.ok(response);
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteFolder(
+ @Auth Long memberId, @PathVariable Long id
+ ) {
+ folderService.deleteFolder(memberId, id);
+ return ResponseEntity.noContent().build();
+ }
+
+ private FolderSaveResult saveFolderResult(Long memberId, FolderSaveRequest folderSaveRequest) {
+ if (folderSaveRequest.parentFolderId() != null) {
+ return folderService.saveSubFolder(memberId, folderSaveRequest);
+ }
+ return folderService.saveRootFolder(memberId, folderSaveRequest);
+ }
+
+ private FolderMoveResult moveFolderWithRequest(Long memberId, Long id, FolderMoveRequest folderMoveRequest) {
+ if (folderMoveRequest.targetFolderId() != null) {
+ return folderService.moveNewParentFolder(memberId, id, folderMoveRequest);
+ }
+ return folderService.moveRootFolder(memberId, id);
+ }
}
diff --git a/src/main/java/notai/folder/presentation/request/FolderMoveRequest.java b/src/main/java/notai/folder/presentation/request/FolderMoveRequest.java
new file mode 100644
index 0000000..a1f6ff3
--- /dev/null
+++ b/src/main/java/notai/folder/presentation/request/FolderMoveRequest.java
@@ -0,0 +1,6 @@
+package notai.folder.presentation.request;
+
+public record FolderMoveRequest(
+ Long targetFolderId
+) {
+}
diff --git a/src/main/java/notai/folder/presentation/request/FolderSaveRequest.java b/src/main/java/notai/folder/presentation/request/FolderSaveRequest.java
new file mode 100644
index 0000000..b16f7f9
--- /dev/null
+++ b/src/main/java/notai/folder/presentation/request/FolderSaveRequest.java
@@ -0,0 +1,7 @@
+package notai.folder.presentation.request;
+
+public record FolderSaveRequest(
+ Long parentFolderId,
+ String name
+) {
+}
diff --git a/src/main/java/notai/folder/presentation/response/FolderFindResponse.java b/src/main/java/notai/folder/presentation/response/FolderFindResponse.java
new file mode 100644
index 0000000..8d0a687
--- /dev/null
+++ b/src/main/java/notai/folder/presentation/response/FolderFindResponse.java
@@ -0,0 +1,13 @@
+package notai.folder.presentation.response;
+
+import notai.folder.application.result.FolderFindResult;
+
+public record FolderFindResponse(
+ Long id,
+ Long parentId,
+ String name
+) {
+ public static FolderFindResponse from(FolderFindResult folderFindResult) {
+ return new FolderFindResponse(folderFindResult.id(), folderFindResult.parentId(), folderFindResult.name());
+ }
+}
diff --git a/src/main/java/notai/folder/presentation/response/FolderMoveResponse.java b/src/main/java/notai/folder/presentation/response/FolderMoveResponse.java
new file mode 100644
index 0000000..0801a69
--- /dev/null
+++ b/src/main/java/notai/folder/presentation/response/FolderMoveResponse.java
@@ -0,0 +1,12 @@
+package notai.folder.presentation.response;
+
+import notai.folder.application.result.FolderMoveResult;
+
+public record FolderMoveResponse(
+ Long id,
+ String name
+) {
+ public static FolderMoveResponse from(FolderMoveResult folderMoveResult) {
+ return new FolderMoveResponse(folderMoveResult.id(), folderMoveResult.name());
+ }
+}
diff --git a/src/main/java/notai/folder/presentation/response/FolderSaveResponse.java b/src/main/java/notai/folder/presentation/response/FolderSaveResponse.java
new file mode 100644
index 0000000..cfc552f
--- /dev/null
+++ b/src/main/java/notai/folder/presentation/response/FolderSaveResponse.java
@@ -0,0 +1,13 @@
+package notai.folder.presentation.response;
+
+import notai.folder.application.result.FolderSaveResult;
+
+public record FolderSaveResponse(
+ Long id,
+ Long parentId,
+ String name
+) {
+ public static FolderSaveResponse from(FolderSaveResult folderSaveResult) {
+ return new FolderSaveResponse(folderSaveResult.id(), folderSaveResult.parentId(), folderSaveResult.name());
+ }
+}
diff --git a/src/main/java/notai/folder/query/FolderQueryRepository.java b/src/main/java/notai/folder/query/FolderQueryRepository.java
deleted file mode 100644
index 93bdaee..0000000
--- a/src/main/java/notai/folder/query/FolderQueryRepository.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package notai.folder.query;
-
-public interface FolderQueryRepository {
-}
diff --git a/src/main/java/notai/folder/query/FolderQueryRepositoryImpl.java b/src/main/java/notai/folder/query/FolderQueryRepositoryImpl.java
deleted file mode 100644
index c7b7681..0000000
--- a/src/main/java/notai/folder/query/FolderQueryRepositoryImpl.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package notai.folder.query;
-
-import com.querydsl.jpa.impl.JPAQueryFactory;
-import lombok.RequiredArgsConstructor;
-
-@RequiredArgsConstructor
-public class FolderQueryRepositoryImpl implements FolderQueryRepository {
-
- private final JPAQueryFactory jpaQueryFactory;
-}
diff --git a/src/main/java/notai/llm/domain/TaskStatus.java b/src/main/java/notai/llm/domain/TaskStatus.java
index be44ed8..aa0b6dd 100644
--- a/src/main/java/notai/llm/domain/TaskStatus.java
+++ b/src/main/java/notai/llm/domain/TaskStatus.java
@@ -1,7 +1,5 @@
package notai.llm.domain;
public enum TaskStatus {
- PENDING,
- IN_PROGRESS,
- COMPLETED
+ PENDING, IN_PROGRESS, COMPLETED
}
diff --git a/src/test/java/notai/BackendApplicationTests.java b/src/test/java/notai/BackendApplicationTests.java
index b50683a..e34c1e0 100644
--- a/src/test/java/notai/BackendApplicationTests.java
+++ b/src/test/java/notai/BackendApplicationTests.java
@@ -6,8 +6,8 @@
@SpringBootTest
class BackendApplicationTests {
- @Test
- void contextLoads() {
- }
+ @Test
+ void contextLoads() {
+ }
}
diff --git a/src/test/java/notai/client/oauth/kakao/KakaoOauthClientTest.java b/src/test/java/notai/client/oauth/kakao/KakaoOauthClientTest.java
index 2dd7dcb..880eb1b 100644
--- a/src/test/java/notai/client/oauth/kakao/KakaoOauthClientTest.java
+++ b/src/test/java/notai/client/oauth/kakao/KakaoOauthClientTest.java
@@ -2,17 +2,16 @@
import notai.member.domain.Member;
import notai.member.domain.OauthProvider;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import static org.mockito.Mockito.when;
import org.mockito.MockitoAnnotations;
import java.time.LocalDateTime;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.when;
-
public class KakaoOauthClientTest {
@Mock
@@ -39,12 +38,12 @@ public void testFetchMember() {
String nickname = "nickname";
KakaoMemberResponse.Profile profile = new KakaoMemberResponse.Profile(nickname);
- KakaoMemberResponse.KakaoAccount kakaoAccount = new KakaoMemberResponse.KakaoAccount(
- profile,
+ KakaoMemberResponse.KakaoAccount kakaoAccount = new KakaoMemberResponse.KakaoAccount(profile,
emailNeedsAgreement,
isEmailValid,
isEmailVerified,
- email);
+ email
+ );
KakaoMemberResponse kakaoMemberResponse = new KakaoMemberResponse(id, hasSignedUp, connectedAt, kakaoAccount);
diff --git a/src/test/java/notai/folder/application/FolderQueryServiceTest.java b/src/test/java/notai/folder/application/FolderQueryServiceTest.java
new file mode 100644
index 0000000..a567e1d
--- /dev/null
+++ b/src/test/java/notai/folder/application/FolderQueryServiceTest.java
@@ -0,0 +1,64 @@
+package notai.folder.application;
+
+import notai.folder.application.result.FolderFindResult;
+import notai.folder.domain.Folder;
+import notai.folder.domain.FolderRepository;
+import notai.member.domain.Member;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import static org.mockito.Mockito.*;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+@ExtendWith(MockitoExtension.class)
+class FolderQueryServiceTest {
+
+ @Mock
+ private FolderRepository folderRepository;
+ @InjectMocks
+ private FolderQueryService folderQueryService;
+
+ @Test
+ @DisplayName("루트 폴더 조회")
+ void getFolders_success_parentFolderIdIsNull() {
+ //given
+ Folder folder = getFolder(1L, null, "루트폴더");
+ List expectedResults = List.of(folder);
+
+ when(folderRepository.findAllByMemberIdAndParentFolderIsNull(any(Long.class))).thenReturn(expectedResults);
+ //when
+ List folders = folderQueryService.getFolders(1L, null);
+
+ Assertions.assertThat(folders.size()).isEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("계층적 구조의 폴더 조회")
+ void getFolders_success_parentFolderId() {
+ //given
+ Folder folder1 = getFolder(1L, null, "루트폴더");
+ Folder folder2 = getFolder(2L, folder1, "서브폴더");
+ Folder folder3 = getFolder(3L, folder1, "서브폴더");
+ List expectedResults = List.of(folder2, folder3);
+
+ when(folderRepository.findAllByMemberIdAndParentFolderId(any(Long.class), any(Long.class))).thenReturn(
+ expectedResults);
+ //when
+ List folders = folderQueryService.getFolders(1L, 1L);
+
+ Assertions.assertThat(folders.size()).isEqualTo(2);
+ }
+
+ private Folder getFolder(Long id, Folder parentFolder, String name) {
+ Member member = mock(Member.class);
+ Folder folder = spy(new Folder(member, name, parentFolder));
+ lenient().when(folder.getId()).thenReturn(id);
+ return folder;
+ }
+}