Skip to content

Commit a564c48

Browse files
authored
Merge pull request #26 from Searchweb-Dev/feat/SW-40
Feat/sw 40 folder기본 기능 생성
2 parents 96c0073 + 3181301 commit a564c48

26 files changed

+470
-498
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
implementation 'org.springframework.boot:spring-boot-starter-validation'
3535
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
3636
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
37+
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
3738

3839
compileOnly 'org.projectlombok:lombok'
3940
runtimeOnly 'org.postgresql:postgresql'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.web.SearchWeb.config;
2+
3+
import lombok.AccessLevel;
4+
import lombok.Getter;
5+
import lombok.RequiredArgsConstructor;
6+
7+
@Getter
8+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
9+
public class ApiResponse<T> {
10+
private final boolean success;
11+
private final T data;
12+
private final ErrorResponse error; // 여기서 ErrorResponse는 JSON 포장지 역할
13+
14+
public static <T> ApiResponse<T> success(T data) {
15+
return new ApiResponse<>(true, data, null);
16+
}
17+
18+
// Enum을 인자로 받음으로써 타입 안정성 확보
19+
public static ApiResponse<Void> fail(ErrorCode errorCode) {
20+
return new ApiResponse<>(
21+
false,
22+
null,
23+
new ErrorResponse(errorCode) // Enum을 생성자로 넘겨서 변환
24+
);
25+
}
26+
27+
@Getter
28+
public static class ErrorResponse {
29+
private final String code;
30+
private final String message;
31+
32+
// Enum에서 필요한 정보만 추출하여 세팅
33+
private ErrorResponse(ErrorCode errorCode) {
34+
this.code = errorCode.getCode();
35+
this.message = errorCode.getMessage();
36+
}
37+
}
38+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.web.SearchWeb.config;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class BusinessException extends RuntimeException {
7+
private final ErrorCode errorCode;
8+
9+
public BusinessException(ErrorCode errorCode) {
10+
super(errorCode.getMessage());
11+
this.errorCode = errorCode;
12+
}
13+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.web.SearchWeb.config;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.http.HttpStatus;
6+
7+
@Getter
8+
@RequiredArgsConstructor
9+
public enum CommonErrorCode implements ErrorCode{
10+
// 500 Internal Server Error: 서버 내부 오류
11+
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C006", "서버에 오류가 발생했습니다.");
12+
13+
private final HttpStatus status;
14+
private final String code;
15+
private final String message;
16+
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.web.SearchWeb.config;
2+
3+
import org.springframework.http.HttpStatus;
4+
5+
public interface ErrorCode {
6+
HttpStatus getStatus();
7+
String getCode();
8+
String getMessage();
9+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.web.SearchWeb.config;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.http.HttpStatus;
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.ExceptionHandler;
7+
import org.springframework.web.bind.annotation.RestControllerAdvice;
8+
9+
@RestControllerAdvice
10+
@Slf4j
11+
public class GlobalExceptionHandler {
12+
13+
// FolderException, MemberException 등 모든 비즈니스 예외가 이 메서드 하나로 들어옵니다.
14+
@ExceptionHandler(BusinessException.class)
15+
protected ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e) {
16+
ErrorCode errorCode = e.getErrorCode();
17+
18+
// 로그에는 어떤 도메인에서 예외가 났는지 클래스명과 메시지를 찍어줍니다.
19+
log.warn("BusinessException [{}]: {}", e.getClass().getSimpleName(), errorCode.getMessage());
20+
21+
return ResponseEntity
22+
.status(errorCode.getStatus())
23+
.body(ApiResponse.fail(errorCode));
24+
}
25+
26+
// 그 외 예상치 못한 서버 에러(500) 처리
27+
@ExceptionHandler(Exception.class)
28+
protected ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
29+
log.error("Internal Server Error", e);
30+
return ResponseEntity
31+
.status(HttpStatus.INTERNAL_SERVER_ERROR)
32+
.body(ApiResponse.fail(CommonErrorCode.INTERNAL_SERVER_ERROR));
33+
}
34+
}

src/main/java/com/web/SearchWeb/folder/controller/FolderController.java

Lines changed: 0 additions & 117 deletions
This file was deleted.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.web.SearchWeb.folder.controller;
2+
3+
import com.web.SearchWeb.config.ApiResponse;
4+
import com.web.SearchWeb.folder.controller.dto.MemberFolderRequests;
5+
import com.web.SearchWeb.folder.controller.dto.MemberFolderResponses;
6+
import com.web.SearchWeb.folder.domain.MemberFolder;
7+
import com.web.SearchWeb.folder.service.MemberFolderService;
8+
import java.util.List;
9+
import java.util.stream.Collectors;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.DeleteMapping;
14+
import org.springframework.web.bind.annotation.GetMapping;
15+
import org.springframework.web.bind.annotation.PathVariable;
16+
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.PutMapping;
18+
import org.springframework.web.bind.annotation.RequestBody;
19+
import org.springframework.web.bind.annotation.RequestMapping;
20+
import org.springframework.web.bind.annotation.RestController;
21+
22+
@RestController
23+
@RequiredArgsConstructor
24+
@RequestMapping("/api/folders")
25+
public class MemberFolderController {
26+
27+
private final MemberFolderService memberFolderService;
28+
29+
// 생성 (201 Created 응답)
30+
@PostMapping
31+
public ResponseEntity<ApiResponse<Long>> create(@RequestBody MemberFolderRequests.Create req) {
32+
Long folderId = memberFolderService.create(
33+
req.ownerMemberId,
34+
req.parentFolderId,
35+
req.folderName,
36+
req.description
37+
);
38+
return ResponseEntity
39+
.status(HttpStatus.CREATED)
40+
.body(ApiResponse.success(folderId));
41+
}
42+
43+
// 단건 조회
44+
@GetMapping("/{folderId}")
45+
public ResponseEntity<ApiResponse<MemberFolderResponses>> get(@PathVariable Long folderId) {
46+
MemberFolder folder = memberFolderService.get(folderId);
47+
return ResponseEntity.ok(ApiResponse.success(MemberFolderResponses.from(folder)));
48+
}
49+
50+
// 루트 폴더 조회
51+
@GetMapping("/owners/{ownerMemberId}/root")
52+
public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listRoot(@PathVariable Long ownerMemberId) {
53+
List<MemberFolderResponses> responses = memberFolderService.listRootFolders(ownerMemberId)
54+
.stream()
55+
.map(MemberFolderResponses::from)
56+
.collect(Collectors.toList());
57+
58+
return ResponseEntity.ok(ApiResponse.success(responses));
59+
}
60+
61+
// 하위 폴더 조회
62+
@GetMapping("/owners/{ownerMemberId}/children/{parentFolderId}")
63+
public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listChildren(
64+
@PathVariable Long ownerMemberId,
65+
@PathVariable Long parentFolderId
66+
) {
67+
List<MemberFolderResponses> responses = memberFolderService.listChildren(ownerMemberId, parentFolderId)
68+
.stream()
69+
.map(MemberFolderResponses::from)
70+
.collect(Collectors.toList());
71+
72+
return ResponseEntity.ok(ApiResponse.success(responses));
73+
}
74+
75+
// 수정 (200 OK)
76+
@PutMapping("/{folderId}")
77+
public ResponseEntity<ApiResponse<Void>> update(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Update req) {
78+
memberFolderService.update(folderId, req.folderName, req.description);
79+
return ResponseEntity.ok(ApiResponse.success(null));
80+
}
81+
82+
// 이동(부모 변경)
83+
@PutMapping("/{folderId}/move")
84+
public ResponseEntity<ApiResponse<Void>> move(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Move req) {
85+
memberFolderService.move(folderId, req.newParentFolderId);
86+
return ResponseEntity.ok(ApiResponse.success(null));
87+
}
88+
89+
// 삭제
90+
@DeleteMapping("/{folderId}")
91+
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long folderId) {
92+
memberFolderService.delete(folderId);
93+
return ResponseEntity.ok(ApiResponse.success(null));
94+
}
95+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.web.SearchWeb.folder.controller.dto;
2+
3+
public class MemberFolderRequests {
4+
5+
/**
6+
* 폴더 생성 요청
7+
*/
8+
public static class Create {
9+
public Long ownerMemberId;
10+
public Long parentFolderId; // null이면 루트
11+
public String folderName;
12+
public String description;
13+
}
14+
15+
/**
16+
* 폴더 수정 요청
17+
*/
18+
public static class Update {
19+
public String folderName;
20+
public String description;
21+
}
22+
23+
/**
24+
* 폴더 이동 요청
25+
*/
26+
public static class Move {
27+
public Long newParentFolderId; // null이면 루트로 이동
28+
}
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.web.SearchWeb.folder.controller.dto;
2+
3+
import com.web.SearchWeb.folder.domain.MemberFolder;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Builder
9+
public class MemberFolderResponses {
10+
private final Long memberFolderId;
11+
private final Long ownerMemberId;
12+
private final Long parentFolderId;
13+
private final String folderName;
14+
private final String description;
15+
16+
// Entity -> DTO 변환을 위한 정적 팩토리 메서드
17+
public static MemberFolderResponses from(MemberFolder folder) {
18+
return MemberFolderResponses.builder()
19+
.memberFolderId(folder.getMemberFolderId())
20+
.ownerMemberId(folder.getOwnerMemberId())
21+
.parentFolderId(folder.getParentFolderId())
22+
.folderName(folder.getFolderName())
23+
.description(folder.getDescription())
24+
.build();
25+
}
26+
}

0 commit comments

Comments
 (0)