-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/sw 40 - folder기본 기능 생성 #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6eab28f
a8f52b5
fd85eee
a998167
1bc4545
6bd8b8d
3181301
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.web.SearchWeb.config; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class ApiResponse<T> { | ||
| private final boolean success; | ||
| private final T data; | ||
| private final ErrorResponse error; // 여기서 ErrorResponse는 JSON 포장지 역할 | ||
|
|
||
| public static <T> ApiResponse<T> success(T data) { | ||
| return new ApiResponse<>(true, data, null); | ||
| } | ||
|
|
||
| // Enum을 인자로 받음으로써 타입 안정성 확보 | ||
| public static ApiResponse<Void> fail(ErrorCode errorCode) { | ||
| return new ApiResponse<>( | ||
| false, | ||
| null, | ||
| new ErrorResponse(errorCode) // Enum을 생성자로 넘겨서 변환 | ||
| ); | ||
| } | ||
|
|
||
| @Getter | ||
| public static class ErrorResponse { | ||
| private final String code; | ||
| private final String message; | ||
|
|
||
| // Enum에서 필요한 정보만 추출하여 세팅 | ||
| private ErrorResponse(ErrorCode errorCode) { | ||
| this.code = errorCode.getCode(); | ||
| this.message = errorCode.getMessage(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.web.SearchWeb.config; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class BusinessException extends RuntimeException { | ||
| private final ErrorCode errorCode; | ||
|
|
||
| public BusinessException(ErrorCode errorCode) { | ||
| super(errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.web.SearchWeb.config; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum CommonErrorCode implements ErrorCode{ | ||
| // 500 Internal Server Error: 서버 내부 오류 | ||
| INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C006", "서버에 오류가 발생했습니다."); | ||
|
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 에러 코드 "C006" 번호 체계 확인 현재 유일한 상수인 🤖 Prompt for AI Agents |
||
|
|
||
| private final HttpStatus status; | ||
| private final String code; | ||
| private final String message; | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.web.SearchWeb.config; | ||
|
|
||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public interface ErrorCode { | ||
| HttpStatus getStatus(); | ||
| String getCode(); | ||
| String getMessage(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package com.web.SearchWeb.config; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
|
||
| @RestControllerAdvice | ||
| @Slf4j | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| // FolderException, MemberException 등 모든 비즈니스 예외가 이 메서드 하나로 들어옵니다. | ||
| @ExceptionHandler(BusinessException.class) | ||
| protected ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e) { | ||
| ErrorCode errorCode = e.getErrorCode(); | ||
|
|
||
| // 로그에는 어떤 도메인에서 예외가 났는지 클래스명과 메시지를 찍어줍니다. | ||
| log.warn("BusinessException [{}]: {}", e.getClass().getSimpleName(), errorCode.getMessage()); | ||
|
|
||
| return ResponseEntity | ||
| .status(errorCode.getStatus()) | ||
| .body(ApiResponse.fail(errorCode)); | ||
| } | ||
|
|
||
| // 그 외 예상치 못한 서버 에러(500) 처리 | ||
| @ExceptionHandler(Exception.class) | ||
| protected ResponseEntity<ApiResponse<Void>> handleException(Exception e) { | ||
| log.error("Internal Server Error", e); | ||
| return ResponseEntity | ||
| .status(HttpStatus.INTERNAL_SERVER_ERROR) | ||
| .body(ApiResponse.fail(CommonErrorCode.INTERNAL_SERVER_ERROR)); | ||
| } | ||
|
Comment on lines
+26
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐛 IllegalArgumentException 핸들러 추가 제안+ `@ExceptionHandler`(IllegalArgumentException.class)
+ protected ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException e) {
+ log.warn("IllegalArgumentException: {}", e.getMessage());
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST)
+ .body(ApiResponse.fail(CommonErrorCode.INVALID_INPUT));
+ }
+
// 그 외 예상치 못한 서버 에러(500) 처리
`@ExceptionHandler`(Exception.class)또는 🤖 Prompt for AI Agents |
||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||||||||||||||||||||||
| package com.web.SearchWeb.folder.controller; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import com.web.SearchWeb.config.ApiResponse; | ||||||||||||||||||||||||||
| import com.web.SearchWeb.folder.controller.dto.MemberFolderRequests; | ||||||||||||||||||||||||||
| import com.web.SearchWeb.folder.controller.dto.MemberFolderResponses; | ||||||||||||||||||||||||||
| import com.web.SearchWeb.folder.domain.MemberFolder; | ||||||||||||||||||||||||||
| import com.web.SearchWeb.folder.service.MemberFolderService; | ||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||
| import java.util.stream.Collectors; | ||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||
| import org.springframework.http.HttpStatus; | ||||||||||||||||||||||||||
| import org.springframework.http.ResponseEntity; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.PathVariable; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.PutMapping; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.RestController; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @RestController | ||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||
| @RequestMapping("/api/folders") | ||||||||||||||||||||||||||
| public class MemberFolderController { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private final MemberFolderService memberFolderService; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 생성 (201 Created 응답) | ||||||||||||||||||||||||||
| @PostMapping | ||||||||||||||||||||||||||
| public ResponseEntity<ApiResponse<Long>> create(@RequestBody MemberFolderRequests.Create req) { | ||||||||||||||||||||||||||
| Long folderId = memberFolderService.create( | ||||||||||||||||||||||||||
| req.ownerMemberId, | ||||||||||||||||||||||||||
| req.parentFolderId, | ||||||||||||||||||||||||||
| req.folderName, | ||||||||||||||||||||||||||
| req.description | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| return ResponseEntity | ||||||||||||||||||||||||||
| .status(HttpStatus.CREATED) | ||||||||||||||||||||||||||
| .body(ApiResponse.success(folderId)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+31
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요청 DTO에 유효성 검증이 없습니다.
컨트롤러 파라미터에 🛡️ 컨트롤러 수정 예시- public ResponseEntity<ApiResponse<Long>> create(`@RequestBody` MemberFolderRequests.Create req) {
+ public ResponseEntity<ApiResponse<Long>> create(`@Valid` `@RequestBody` MemberFolderRequests.Create req) {DTO 수정 예시 ( public static class Create {
`@NotNull`
public Long ownerMemberId;
public Long parentFolderId; // null이면 루트
`@NotBlank`
public String folderName;
public String description;
}
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 단건 조회 | ||||||||||||||||||||||||||
| @GetMapping("/{folderId}") | ||||||||||||||||||||||||||
| public ResponseEntity<ApiResponse<MemberFolderResponses>> get(@PathVariable Long folderId) { | ||||||||||||||||||||||||||
| MemberFolder folder = memberFolderService.get(folderId); | ||||||||||||||||||||||||||
| return ResponseEntity.ok(ApiResponse.success(MemberFolderResponses.from(folder))); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 루트 폴더 조회 | ||||||||||||||||||||||||||
| @GetMapping("/owners/{ownerMemberId}/root") | ||||||||||||||||||||||||||
| public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listRoot(@PathVariable Long ownerMemberId) { | ||||||||||||||||||||||||||
| List<MemberFolderResponses> responses = memberFolderService.listRootFolders(ownerMemberId) | ||||||||||||||||||||||||||
| .stream() | ||||||||||||||||||||||||||
| .map(MemberFolderResponses::from) | ||||||||||||||||||||||||||
| .collect(Collectors.toList()); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ResponseEntity.ok(ApiResponse.success(responses)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 하위 폴더 조회 | ||||||||||||||||||||||||||
| @GetMapping("/owners/{ownerMemberId}/children/{parentFolderId}") | ||||||||||||||||||||||||||
| public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listChildren( | ||||||||||||||||||||||||||
| @PathVariable Long ownerMemberId, | ||||||||||||||||||||||||||
| @PathVariable Long parentFolderId | ||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||
| List<MemberFolderResponses> responses = memberFolderService.listChildren(ownerMemberId, parentFolderId) | ||||||||||||||||||||||||||
| .stream() | ||||||||||||||||||||||||||
| .map(MemberFolderResponses::from) | ||||||||||||||||||||||||||
| .collect(Collectors.toList()); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ResponseEntity.ok(ApiResponse.success(responses)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 수정 (200 OK) | ||||||||||||||||||||||||||
| @PutMapping("/{folderId}") | ||||||||||||||||||||||||||
| public ResponseEntity<ApiResponse<Void>> update(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Update req) { | ||||||||||||||||||||||||||
| memberFolderService.update(folderId, req.folderName, req.description); | ||||||||||||||||||||||||||
| return ResponseEntity.ok(ApiResponse.success(null)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 이동(부모 변경) | ||||||||||||||||||||||||||
| @PutMapping("/{folderId}/move") | ||||||||||||||||||||||||||
| public ResponseEntity<ApiResponse<Void>> move(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Move req) { | ||||||||||||||||||||||||||
| memberFolderService.move(folderId, req.newParentFolderId); | ||||||||||||||||||||||||||
| return ResponseEntity.ok(ApiResponse.success(null)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 삭제 | ||||||||||||||||||||||||||
| @DeleteMapping("/{folderId}") | ||||||||||||||||||||||||||
| public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long folderId) { | ||||||||||||||||||||||||||
| memberFolderService.delete(folderId); | ||||||||||||||||||||||||||
| return ResponseEntity.ok(ApiResponse.success(null)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+89
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 삭제 응답 코드로 RESTful 관례상 삭제 성공 시 ♻️ 수정 제안 `@DeleteMapping`("/{folderId}")
- public ResponseEntity<ApiResponse<Void>> delete(`@PathVariable` Long folderId) {
+ public ResponseEntity<Void> delete(`@PathVariable` Long folderId) {
memberFolderService.delete(folderId);
- return ResponseEntity.ok(ApiResponse.success(null));
+ return ResponseEntity.noContent().build();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||||||||||||||||||||
| package com.web.SearchWeb.folder.controller.dto; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| public class MemberFolderRequests { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * 폴더 생성 요청 | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| public static class Create { | ||||||||||||||||||||||||
| public Long ownerMemberId; | ||||||||||||||||||||||||
| public Long parentFolderId; // null이면 루트 | ||||||||||||||||||||||||
| public String folderName; | ||||||||||||||||||||||||
| public String description; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+8
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 보안 문제:
🔒 Create DTO에서 ownerMemberId 제거 제안 public static class Create {
- public Long ownerMemberId;
public Long parentFolderId; // null이면 루트
public String folderName;
public String description;
}컨트롤러에서 인증 정보로부터 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * 폴더 수정 요청 | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| public static class Update { | ||||||||||||||||||||||||
| public String folderName; | ||||||||||||||||||||||||
| public String description; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * 폴더 이동 요청 | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| public static class Move { | ||||||||||||||||||||||||
| public Long newParentFolderId; // null이면 루트로 이동 | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+3
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요청 DTO에 Bean Validation 어노테이션 누락 모든 inner class 필드가 ♻️ Validation 적용 및 캡슐화 개선 제안+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
public class MemberFolderRequests {
public static class Create {
- public Long parentFolderId;
- public String folderName;
- public String description;
+ `@Getter` `@Setter`
+ private Long parentFolderId;
+
+ `@NotBlank`
+ `@Size`(max = 100)
+ `@Getter` `@Setter`
+ private String folderName;
+
+ `@Size`(max = 500)
+ `@Getter` `@Setter`
+ private String description;
}컨트롤러의 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.web.SearchWeb.folder.controller.dto; | ||
|
|
||
| import com.web.SearchWeb.folder.domain.MemberFolder; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public class MemberFolderResponses { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 클래스명이 복수형으로 혼동 가능
🤖 Prompt for AI Agents |
||
| private final Long memberFolderId; | ||
| private final Long ownerMemberId; | ||
| private final Long parentFolderId; | ||
| private final String folderName; | ||
| private final String description; | ||
|
|
||
| // Entity -> DTO 변환을 위한 정적 팩토리 메서드 | ||
| public static MemberFolderResponses from(MemberFolder folder) { | ||
| return MemberFolderResponses.builder() | ||
| .memberFolderId(folder.getMemberFolderId()) | ||
| .ownerMemberId(folder.getOwnerMemberId()) | ||
| .parentFolderId(folder.getParentFolderId()) | ||
| .folderName(folder.getFolderName()) | ||
| .description(folder.getDescription()) | ||
| .build(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spring-boot-starter-data-jpa의존성이 중복 선언되어 있습니다.31번 라인에 이미 동일한 의존성이 선언되어 있으므로, 37번 라인의 중복 항목을 제거해 주세요. 또한 31번 라인의 들여쓰기가 탭이 아닌 스페이스로 되어 있어 파일 내 다른 라인들과 일관성이 맞지 않습니다.
🔧 수정 제안
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'me.paulschwarz:spring-dotenv:4.0.0' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa'31번 라인의 들여쓰기도 스페이스에서 탭으로 수정하세요:
🤖 Prompt for AI Agents