From 2d733243bf8ab6c0cf90d5c1b9da44e542cdb0cc Mon Sep 17 00:00:00 2001 From: parksomii Date: Wed, 23 Jul 2025 01:36:35 +0900 Subject: [PATCH 01/22] feat: Add block and connection info to Project entity Introduced blockInfo and connectionInfo fields to store architecture data as JSON in the Project entity. Added updateArchitecture method for updating block information and improved documentation for update methods. --- .../blockcloud/domain/project/Project.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/blockcloud/domain/project/Project.java b/src/main/java/com/blockcloud/domain/project/Project.java index c9b5509..f9fec11 100644 --- a/src/main/java/com/blockcloud/domain/project/Project.java +++ b/src/main/java/com/blockcloud/domain/project/Project.java @@ -35,6 +35,11 @@ public class Project { @JsonIgnore private List members = new ArrayList<>(); + @Column(name = "block_info", columnDefinition = "LONGTEXT") + private String blockInfo; // JSON 형태로 블록 데이터 저장 + + @Column(name = "connection_info", columnDefinition = "LONGTEXT") + private String connectionInfo; // JSON 형태로 연결 데이터 저장 @PrePersist public void onCreate() { @@ -42,14 +47,29 @@ public void onCreate() { this.updateAt = LocalDateTime.now(); } + /** + * 프로젝트 수정 시 업데이트 시간 갱신 + */ @PreUpdate public void onUpdate() { this.updateAt = LocalDateTime.now(); } + /** + * 프로젝트 기본 정보(이름, 설명) 업데이트 + */ public void updateInfo(String name, String description) { this.name = name; this.description = description; this.updateAt = LocalDateTime.now(); } + + /** + * 블록 및 연결 정보 업데이트 + */ + public void updateArchitecture(String blockInfoJson) { + this.blockInfo = blockInfoJson; + this.connectionInfo = null; + this.updateAt = LocalDateTime.now(); + } } \ No newline at end of file From 7d6f29f3c193ebfe1f2fa939332495d9721ae8c5 Mon Sep 17 00:00:00 2001 From: parksomii Date: Wed, 23 Jul 2025 01:37:08 +0900 Subject: [PATCH 02/22] feat: Add BlockSaveRequestDto for block save requests Introduces BlockSaveRequestDto with createdAt, updatedAt, and blocks fields to support block save operations. Includes Lombok annotations for boilerplate code reduction. --- .../dto/RequestDto/BlockSaveRequestDto.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java diff --git a/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java b/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java new file mode 100644 index 0000000..d968223 --- /dev/null +++ b/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java @@ -0,0 +1,18 @@ +package com.blockcloud.dto.RequestDto; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BlockSaveRequestDto { + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Object blocks; +} \ No newline at end of file From c2f08d30ce53a2b5dbc8ac40493fe5746174c1c9 Mon Sep 17 00:00:00 2001 From: parksomii Date: Wed, 23 Jul 2025 01:37:23 +0900 Subject: [PATCH 03/22] feat: Add BlockSaveResponseDto for block save responses Introduces BlockSaveResponseDto with success, message, and data fields, including a nested BlockInfo class to encapsulate projectId, architectureName, and updatedAt. This DTO will be used to standardize responses for block save operations. --- .../dto/ResponseDto/BlockSaveResponseDto.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java diff --git a/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java b/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java new file mode 100644 index 0000000..b395ce2 --- /dev/null +++ b/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java @@ -0,0 +1,23 @@ +package com.blockcloud.dto.ResponseDto; + +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BlockSaveResponseDto { + + private boolean success; + private String message; + private BlockInfo data; + + @Getter + @Builder + public static class BlockInfo { + + private Long projectId; + private String architectureName; + private LocalDateTime updatedAt; + } +} \ No newline at end of file From be0d6653dff84d44b676991b0f19c0451616c53b Mon Sep 17 00:00:00 2001 From: parksomii Date: Wed, 23 Jul 2025 01:38:15 +0900 Subject: [PATCH 04/22] feat: Add BlockService for block management Introduces BlockService with methods to save and retrieve block architecture data for projects. Integrates with ProjectRepository and handles JSON serialization of block information. --- .../com/blockcloud/service/BlockService.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/main/java/com/blockcloud/service/BlockService.java diff --git a/src/main/java/com/blockcloud/service/BlockService.java b/src/main/java/com/blockcloud/service/BlockService.java new file mode 100644 index 0000000..7ec2e21 --- /dev/null +++ b/src/main/java/com/blockcloud/service/BlockService.java @@ -0,0 +1,54 @@ +package com.blockcloud.service; + +import com.blockcloud.domain.project.Project; +import com.blockcloud.domain.project.ProjectRepository; +import com.blockcloud.dto.RequestDto.BlockSaveRequestDto; +import com.blockcloud.dto.ResponseDto.BlockSaveResponseDto; +import com.nimbusds.jose.shaded.gson.Gson; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class BlockService { + + private final ProjectRepository projectRepository; + + @Transactional + public BlockSaveResponseDto saveBlocks(Long projectId, BlockSaveRequestDto dto) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new IllegalArgumentException("해당 프로젝트를 찾을 수 없습니다.")); + + String blockInfoJson = new Gson().toJson(dto.getBlocks()); + project.updateArchitecture(blockInfoJson); + + projectRepository.save(project); + + return BlockSaveResponseDto.builder() + .success(true) + .message("아키텍처가 성공적으로 저장되었습니다.") + .data(BlockSaveResponseDto.BlockInfo.builder() + .projectId(project.getId()) + .architectureName(project.getName() + "-" + LocalDateTime.now()) + .updatedAt(project.getUpdateAt()) + .build()) + .build(); + } + + @Transactional(readOnly = true) + public Map getBlocks(Long projectId) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new IllegalArgumentException("해당 프로젝트를 찾을 수 없습니다.")); + + Map response = new HashMap<>(); + response.put("createdAt", project.getCreateAt()); + response.put("updatedAt", project.getUpdateAt()); + response.put("blocks", + new Gson().fromJson(project.getBlockInfo(), Object.class)); + return response; + } +} \ No newline at end of file From 9d6991b71724d3c3204af026b224b524dac4e73d Mon Sep 17 00:00:00 2001 From: parksomii Date: Wed, 23 Jul 2025 01:38:57 +0900 Subject: [PATCH 05/22] feat: Add BlockController for block architecture API Introduces BlockController with endpoints to save and retrieve block architecture data for specific projects. Includes Swagger documentation for API operations and response examples. --- .../controller/BlockController.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/main/java/com/blockcloud/controller/BlockController.java diff --git a/src/main/java/com/blockcloud/controller/BlockController.java b/src/main/java/com/blockcloud/controller/BlockController.java new file mode 100644 index 0000000..15d1bdc --- /dev/null +++ b/src/main/java/com/blockcloud/controller/BlockController.java @@ -0,0 +1,135 @@ +package com.blockcloud.controller; + +import com.blockcloud.dto.RequestDto.BlockSaveRequestDto; +import com.blockcloud.dto.ResponseDto.BlockSaveResponseDto; +import com.blockcloud.service.BlockService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Block API", description = "블록 아키텍처 저장 및 조회 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/block") +public class BlockController { + + private final BlockService blockService; + + /** + * 블록 아키텍처 저장 특정 프로젝트(projectId)에 대한 블록 인프라 데이터를 저장합니다. + */ + @Operation( + summary = "블록 아키텍처 저장", + description = "특정 프로젝트(`projectId`)에 대한 블록 인프라 데이터를 저장합니다. " + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "저장 성공", + content = @Content(schema = @Schema(implementation = BlockSaveResponseDto.class), + examples = @ExampleObject(value = """ + { + "success": true, + "message": "아키텍처가 성공적으로 저장되었습니다.", + "data": { + "projectId": 1, + "architectureName": "aws-architecture-2025-07-22", + "updatedAt": "2025-07-22T14:30:00" + } + } + """))), + @ApiResponse(responseCode = "400", description = "INVALID_BLOCKS (블록 데이터가 유효하지 않음)"), + @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (JWT 인증 필요)"), + @ApiResponse(responseCode = "404", description = "PROJECT_NOT_FOUND (해당 프로젝트 없음)") + }) + @PostMapping("/{projectId}") + public ResponseEntity saveBlocks( + @Parameter(description = "블록을 저장할 프로젝트 ID", required = true) + @PathVariable Long projectId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "저장할 블록 아키텍처 데이터", + required = true, + content = @Content(examples = @ExampleObject(value = """ + { + "createdAt": "2025-07-22T14:20:00", + "updatedAt": "2025-07-22T14:25:00", + "blocks": [ + { + "id": "web-server", + "type": "aws_instance", + "position": { + "x": 100, + "y": 200 + }, + "properties": { + "ami": "ami-12345678", + "instance_type": "t2.micro", + "tags": { + "Name": "MyServer" + } + }, + "connections": [ + "subnet-1" + ] + } + ] + } + """)) + ) + @RequestBody BlockSaveRequestDto requestDto) { + return ResponseEntity.ok(blockService.saveBlocks(projectId, requestDto)); + } + + /** + * 블록 아키텍처 불러오기 특정 프로젝트(projectId)의 블록 및 연결 정보를 불러옵니다. + */ + @Operation( + summary = "블록 아키텍처 불러오기", + description = "특정 프로젝트(`projectId`)의 블록 및 연결 정보를 불러옵니다. " + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(examples = @ExampleObject(value = """ + { + "createdAt": "2025-07-22T14:20:00", + "updatedAt": "2025-07-22T14:25:00", + "blocks": [ + { + "id": "web-server", + "type": "aws_instance", + "position": { "x": 100, "y": 200 }, + "properties": { ... }, + "connections": ["subnet-1"] + }, + { + "id": "subnet-1", + "type": "aws_subnet", + "position": { "x": 200, "y": 300 }, + "properties": { ... }, + "connections": [] + } + ] + } + """))), + @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (JWT 인증 필요)"), + @ApiResponse(responseCode = "403", description = "FORBIDDEN (프로젝트 접근 권한 없음)"), + @ApiResponse(responseCode = "404", description = "PROJECT_NOT_FOUND (프로젝트 없음)") + }) + + @GetMapping("/{projectId}") + public ResponseEntity> getBlocks(@PathVariable Long projectId) { + return ResponseEntity.ok(blockService.getBlocks(projectId)); + } +} \ No newline at end of file From bdabfbcc20e634b72ca4ecf0da112abc94d5eb87 Mon Sep 17 00:00:00 2001 From: parksomii Date: Thu, 24 Jul 2025 21:02:36 +0900 Subject: [PATCH 06/22] feat: Return JSON response for 401 Unauthorized errors Added custom authenticationEntryPoint to respond with a JSON message when authentication fails, improving client-side error handling for unauthorized requests. --- .../com/blockcloud/config/SecurityConfig.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/blockcloud/config/SecurityConfig.java b/src/main/java/com/blockcloud/config/SecurityConfig.java index 8a6cb53..670d88d 100644 --- a/src/main/java/com/blockcloud/config/SecurityConfig.java +++ b/src/main/java/com/blockcloud/config/SecurityConfig.java @@ -6,6 +6,7 @@ import com.blockcloud.jwt.JWTUtil; import com.blockcloud.service.CookieService; import com.blockcloud.service.CustomOAuth2UserService; +import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,6 +55,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .successHandler(new OAuth2SuccessHandler(jwtUtil, cookieService)) ) + // 401 Unauthorized를 JSON으로 응답 + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(""" + { + "success": false, + "errorCode": "UNAUTHORIZED", + "message": "인증이 필요합니다" + } + """); + }) + ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) From 608409ca498c23b348d65207478916cf52a86499 Mon Sep 17 00:00:00 2001 From: parksomii Date: Thu, 24 Jul 2025 21:03:29 +0900 Subject: [PATCH 07/22] feat: Add global exception handler for API errors Introduces GlobalExceptionHandler to handle common API errors: invalid JSON (400), access denied (403), and project not found (404), returning structured error responses. --- .../config/GlobalExceptionHandler.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/com/blockcloud/config/GlobalExceptionHandler.java diff --git a/src/main/java/com/blockcloud/config/GlobalExceptionHandler.java b/src/main/java/com/blockcloud/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..ab6ba66 --- /dev/null +++ b/src/main/java/com/blockcloud/config/GlobalExceptionHandler.java @@ -0,0 +1,50 @@ +package com.blockcloud.config; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + // 400 - JSON 파싱 오류, 잘못된 블록 데이터 + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleInvalidJson() { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body( + Map.of( + "success", false, + "errorCode", "INVALID_BLOCKS", + "message", "블록 데이터가 유효하지 않습니다" + ) + ); + } + + // 403 - 권한 부족 + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity> handleForbidden() { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body( + Map.of( + "success", false, + "errorCode", "FORBIDDEN", + "message", "프로젝트에 접근할 수 없습니다" + ) + ); + } + + // 404 - 프로젝트 없음 + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleProjectNotFound() { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body( + Map.of( + "success", false, + "errorCode", "PROJECT_NOT_FOUND", + "message", "해당 프로젝트를 찾을 수 없습니다" + ) + ); + } +} \ No newline at end of file From 37a3197cf91d144adb418925faef0142b69e77d9 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 17:51:25 +0900 Subject: [PATCH 08/22] feat: Add NOT_FOUND_PROJECT error code --- src/main/java/com/blockcloud/exception/error/ErrorCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/blockcloud/exception/error/ErrorCode.java b/src/main/java/com/blockcloud/exception/error/ErrorCode.java index c866650..76780bd 100644 --- a/src/main/java/com/blockcloud/exception/error/ErrorCode.java +++ b/src/main/java/com/blockcloud/exception/error/ErrorCode.java @@ -49,8 +49,9 @@ public enum ErrorCode { // External Server Error EXTERNAL_SERVER_ERROR(50200, HttpStatus.BAD_GATEWAY, "서버 외부 에러입니다."), - ; + // Project Errors + NOT_FOUND_PROJECT(40401, HttpStatus.NOT_FOUND, "존재하지 않는 프로젝트입니다."); private final Integer code; private final HttpStatus httpStatus; From fcb70633e2102e58648114164923bc0343227a63 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 18:37:21 +0900 Subject: [PATCH 09/22] refactor: Remove GlobalExceptionHandler --- .../config/GlobalExceptionHandler.java | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 src/main/java/com/blockcloud/config/GlobalExceptionHandler.java diff --git a/src/main/java/com/blockcloud/config/GlobalExceptionHandler.java b/src/main/java/com/blockcloud/config/GlobalExceptionHandler.java deleted file mode 100644 index ab6ba66..0000000 --- a/src/main/java/com/blockcloud/config/GlobalExceptionHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.blockcloud.config; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -import java.util.Map; - -@ControllerAdvice -public class GlobalExceptionHandler { - - // 400 - JSON 파싱 오류, 잘못된 블록 데이터 - @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handleInvalidJson() { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body( - Map.of( - "success", false, - "errorCode", "INVALID_BLOCKS", - "message", "블록 데이터가 유효하지 않습니다" - ) - ); - } - - // 403 - 권한 부족 - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity> handleForbidden() { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body( - Map.of( - "success", false, - "errorCode", "FORBIDDEN", - "message", "프로젝트에 접근할 수 없습니다" - ) - ); - } - - // 404 - 프로젝트 없음 - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleProjectNotFound() { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body( - Map.of( - "success", false, - "errorCode", "PROJECT_NOT_FOUND", - "message", "해당 프로젝트를 찾을 수 없습니다" - ) - ); - } -} \ No newline at end of file From 7e037aca8da011fe8fc13d60b655d788e6648104 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 18:39:32 +0900 Subject: [PATCH 10/22] feat: Add validation to BlockSaveRequestDto --- .../blockcloud/dto/RequestDto/BlockSaveRequestDto.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java b/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java index d968223..59c3688 100644 --- a/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java +++ b/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java @@ -1,18 +1,21 @@ package com.blockcloud.dto.RequestDto; -import java.time.LocalDateTime; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +/** + * 블록 저장 요청 DTO + * 블록 정보를 포함하여 저장 요청을 처리하기 위한 DTO 클래스입니다. + */ @Getter @NoArgsConstructor @AllArgsConstructor @Builder public class BlockSaveRequestDto { - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + @NotNull(message = "블록 정보는 필수입니다.") private Object blocks; } \ No newline at end of file From dd64be1d678b35e1bcb4be891e26c9585e89cfa2 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 18:40:25 +0900 Subject: [PATCH 11/22] feat: Add BlockGetResponseDto for block retrieval responses --- .../dto/ResponseDto/BlockGetResponseDto.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/com/blockcloud/dto/ResponseDto/BlockGetResponseDto.java diff --git a/src/main/java/com/blockcloud/dto/ResponseDto/BlockGetResponseDto.java b/src/main/java/com/blockcloud/dto/ResponseDto/BlockGetResponseDto.java new file mode 100644 index 0000000..326c377 --- /dev/null +++ b/src/main/java/com/blockcloud/dto/ResponseDto/BlockGetResponseDto.java @@ -0,0 +1,17 @@ +package com.blockcloud.dto.ResponseDto; + +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +/** + * 블록 조회 성공 시 'data' 필드에 담길 응답 DTO + */ +@Getter +@Builder +public class BlockGetResponseDto { + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Object blocks; +} \ No newline at end of file From b8f86812db3d61be82475afca3f33b45aa366047 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 18:41:43 +0900 Subject: [PATCH 12/22] refactor: Flatten BlockSaveResponseDto structure Flattened BlockSaveResponseDto by removing the nested BlockInfo class and moving its fields to the top level. Removed unused fields 'success', 'message', and 'data' for a simpler response structure. --- .../dto/ResponseDto/BlockSaveResponseDto.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java b/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java index b395ce2..fdad649 100644 --- a/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java +++ b/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java @@ -4,20 +4,14 @@ import lombok.Builder; import lombok.Getter; +/** + * 블록 저장 응답 DTO + */ @Getter @Builder public class BlockSaveResponseDto { - private boolean success; - private String message; - private BlockInfo data; - - @Getter - @Builder - public static class BlockInfo { - - private Long projectId; - private String architectureName; - private LocalDateTime updatedAt; - } -} \ No newline at end of file + private Long projectId; + private String architectureName; + private LocalDateTime updatedAt; +} From 7383c15ac887dce1897cede252562f336d6e02e4 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 18:46:31 +0900 Subject: [PATCH 13/22] refactor: Enhance BlockService with member validation and DTOs Refactored BlockService to add project member validation for block save and get operations, ensuring only authorized users can access or modify project architecture. Replaced generic response maps with dedicated DTOs for block retrieval, improved exception handling, and updated method signatures to include user email for authorization. --- .../com/blockcloud/service/BlockService.java | 75 +++++++++++++------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/blockcloud/service/BlockService.java b/src/main/java/com/blockcloud/service/BlockService.java index 7ec2e21..9aec0a2 100644 --- a/src/main/java/com/blockcloud/service/BlockService.java +++ b/src/main/java/com/blockcloud/service/BlockService.java @@ -3,52 +3,85 @@ import com.blockcloud.domain.project.Project; import com.blockcloud.domain.project.ProjectRepository; import com.blockcloud.dto.RequestDto.BlockSaveRequestDto; +import com.blockcloud.dto.ResponseDto.BlockGetResponseDto; import com.blockcloud.dto.ResponseDto.BlockSaveResponseDto; +import com.blockcloud.exception.CommonException; +import com.blockcloud.exception.error.ErrorCode; import com.nimbusds.jose.shaded.gson.Gson; import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +/** + * 블록 아키텍처 관리를 위한 서비스 클래스 + */ @Service @RequiredArgsConstructor public class BlockService { private final ProjectRepository projectRepository; + /** + * 프로젝트의 블록 아키텍처 정보를 저장합니다. + * + * @param projectId 저장할 프로젝트의 ID + * @param dto 블록 정보가 담긴 요청 DTO + * @return 저장된 블록 정보를 담은 DTO (데이터 부분) + * @throws CommonException 해당 프로젝트를 찾을 수 없는 경우 + */ @Transactional - public BlockSaveResponseDto saveBlocks(Long projectId, BlockSaveRequestDto dto) { + public BlockSaveResponseDto saveBlocks(Long projectId, BlockSaveRequestDto dto, String email) { Project project = projectRepository.findById(projectId) - .orElseThrow(() -> new IllegalArgumentException("해당 프로젝트를 찾을 수 없습니다.")); + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_PROJECT)); + + validateProjectMember(project, email); String blockInfoJson = new Gson().toJson(dto.getBlocks()); project.updateArchitecture(blockInfoJson); - projectRepository.save(project); return BlockSaveResponseDto.builder() - .success(true) - .message("아키텍처가 성공적으로 저장되었습니다.") - .data(BlockSaveResponseDto.BlockInfo.builder() - .projectId(project.getId()) - .architectureName(project.getName() + "-" + LocalDateTime.now()) - .updatedAt(project.getUpdateAt()) - .build()) + .projectId(project.getId()) + .architectureName(project.getName() + "-" + LocalDateTime.now()) + .updatedAt(project.getUpdatedAt()) .build(); } + /** + * 프로젝트의 블록 아키텍처 정보를 조회합니다. + * + * @param projectId 조회할 프로젝트의 ID + * @param email 요청한 사용자의 이메일 + * @return 블록 아키텍처 정보가 담긴 응답 DTO (데이터 부분) + * @throws CommonException 해당 프로젝트를 찾을 수 없는 경우 또는 접근 권한이 없는 경우 + */ @Transactional(readOnly = true) - public Map getBlocks(Long projectId) { + public BlockGetResponseDto getBlocks(Long projectId, String email) { Project project = projectRepository.findById(projectId) - .orElseThrow(() -> new IllegalArgumentException("해당 프로젝트를 찾을 수 없습니다.")); - - Map response = new HashMap<>(); - response.put("createdAt", project.getCreateAt()); - response.put("updatedAt", project.getUpdateAt()); - response.put("blocks", - new Gson().fromJson(project.getBlockInfo(), Object.class)); - return response; + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_PROJECT)); + + validateProjectMember(project, email); + + return BlockGetResponseDto.builder() + .createdAt(project.getCreatedAt()) + .updatedAt(project.getUpdatedAt()) + .blocks(new Gson().fromJson(project.getBlockInfo(), Object.class)) + .build(); + } + + /** + * 사용자가 해당 프로젝트의 멤버인지 검증하는 메서드 + * + * @param project 검증할 프로젝트 + * @param email 요청한 사용자의 이메일 + * @throws CommonException 접근 권한이 없는 경우 + */ + private void validateProjectMember(Project project, String email) { + boolean isMember = project.getMembers().stream() + .anyMatch(member -> member.getUser().getEmail().equals(email)); + if (!isMember) { + throw new CommonException(ErrorCode.ACCESS_DENIED); + } } } \ No newline at end of file From 466bb62438c702b04ec550e25cd50ef0529bc994 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 18:51:38 +0900 Subject: [PATCH 14/22] refactor: Apply ResponseDto and authentication to BlockController Updated BlockController endpoints to return ResponseDto wrappers instead of ResponseEntity, and to require Authentication for both saving and retrieving blocks. --- .../controller/BlockController.java | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/blockcloud/controller/BlockController.java b/src/main/java/com/blockcloud/controller/BlockController.java index 15d1bdc..745fe2a 100644 --- a/src/main/java/com/blockcloud/controller/BlockController.java +++ b/src/main/java/com/blockcloud/controller/BlockController.java @@ -1,7 +1,10 @@ package com.blockcloud.controller; import com.blockcloud.dto.RequestDto.BlockSaveRequestDto; +import com.blockcloud.dto.ResponseDto.BlockGetResponseDto; import com.blockcloud.dto.ResponseDto.BlockSaveResponseDto; +import com.blockcloud.dto.common.ResponseDto; +import com.blockcloud.dto.oauth.CustomUserDetails; import com.blockcloud.service.BlockService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -11,15 +14,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.Map; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -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.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; @Tag(name = "Block API", description = "블록 아키텍처 저장 및 조회 API") @RestController @@ -38,7 +36,7 @@ public class BlockController { ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "저장 성공", - content = @Content(schema = @Schema(implementation = BlockSaveResponseDto.class), + content = @Content(schema = @Schema(implementation = ResponseDto.class), examples = @ExampleObject(value = """ { "success": true, @@ -50,12 +48,13 @@ public class BlockController { } } """))), - @ApiResponse(responseCode = "400", description = "INVALID_BLOCKS (블록 데이터가 유효하지 않음)"), - @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (JWT 인증 필요)"), - @ApiResponse(responseCode = "404", description = "PROJECT_NOT_FOUND (해당 프로젝트 없음)") + @ApiResponse(responseCode = "400", description = "INVALID_ARGUMENT (요청 데이터 유효성 검증 실패)"), + @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (인증 실패)"), + @ApiResponse(responseCode = "403", description = "ACCESS_DENIED (접근 권한 없음)"), + @ApiResponse(responseCode = "404", description = "NOT_FOUND_PROJECT (프로젝트를 찾을 수 없음)") }) @PostMapping("/{projectId}") - public ResponseEntity saveBlocks( + public ResponseDto saveBlocks( @Parameter(description = "블록을 저장할 프로젝트 ID", required = true) @PathVariable Long projectId, @io.swagger.v3.oas.annotations.parameters.RequestBody( @@ -88,8 +87,12 @@ public ResponseEntity saveBlocks( } """)) ) - @RequestBody BlockSaveRequestDto requestDto) { - return ResponseEntity.ok(blockService.saveBlocks(projectId, requestDto)); + @Valid @RequestBody BlockSaveRequestDto requestDto, + Authentication authentication) { + + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + return ResponseDto.ok( + blockService.saveBlocks(projectId, requestDto, userDetails.getUsername())); } /** @@ -123,13 +126,16 @@ public ResponseEntity saveBlocks( ] } """))), - @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (JWT 인증 필요)"), - @ApiResponse(responseCode = "403", description = "FORBIDDEN (프로젝트 접근 권한 없음)"), - @ApiResponse(responseCode = "404", description = "PROJECT_NOT_FOUND (프로젝트 없음)") + @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (인증 실패)"), + @ApiResponse(responseCode = "403", description = "ACCESS_DENIED (접근 권한 없음)"), + @ApiResponse(responseCode = "404", description = "NOT_FOUND_PROJECT (프로젝트를 찾을 수 없음)") }) - @GetMapping("/{projectId}") - public ResponseEntity> getBlocks(@PathVariable Long projectId) { - return ResponseEntity.ok(blockService.getBlocks(projectId)); + public ResponseDto getBlocks( + @Parameter(description = "블록을 조회할 프로젝트 ID", required = true) @PathVariable Long projectId, + Authentication authentication) { + + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + return ResponseDto.ok(blockService.getBlocks(projectId, userDetails.getUsername())); } } \ No newline at end of file From f0b528610e81f730bd38ac5710c0e52be82ea105 Mon Sep 17 00:00:00 2001 From: parksomii Date: Fri, 8 Aug 2025 19:16:31 +0900 Subject: [PATCH 15/22] refactor: Standardize authentication error response Updated the SecurityConfig to return 401 Unauthorized errors using a standardized response DTO and error code (AUTHENTICATION_REQUIRED). --- .../com/blockcloud/config/SecurityConfig.java | 34 +++++++++++++------ .../blockcloud/exception/error/ErrorCode.java | 1 + 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/blockcloud/config/SecurityConfig.java b/src/main/java/com/blockcloud/config/SecurityConfig.java index 81dcbe9..ebc2acf 100644 --- a/src/main/java/com/blockcloud/config/SecurityConfig.java +++ b/src/main/java/com/blockcloud/config/SecurityConfig.java @@ -1,15 +1,21 @@ package com.blockcloud.config; +import com.blockcloud.dto.common.ExceptionDto; +import com.blockcloud.dto.common.ResponseDto; +import com.blockcloud.exception.error.ErrorCode; import com.blockcloud.exception.handler.CustomLogoutSuccessHandler; import com.blockcloud.exception.handler.OAuth2SuccessHandler; import com.blockcloud.jwt.JWTFilter; import com.blockcloud.jwt.JWTUtil; import com.blockcloud.service.CookieService; import com.blockcloud.service.CustomOAuth2UserService; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -23,12 +29,14 @@ @Configuration @EnableWebSecurity -@AllArgsConstructor +@RequiredArgsConstructor + public class SecurityConfig { private final JWTUtil jwtUtil; private final CookieService cookieService; private final CustomOAuth2UserService customOAuth2UserService; + private final ObjectMapper objectMapper; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -55,18 +63,24 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .successHandler(new OAuth2SuccessHandler(jwtUtil, cookieService)) ) - // 401 Unauthorized를 JSON으로 응답 + // 401 Unauthorized 에러를 공통 응답 포맷으로 변경 .exceptionHandling(ex -> ex .authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(""" - { - "success": false, - "errorCode": "UNAUTHORIZED", - "message": "인증이 필요합니다" - } - """); + + ExceptionDto errorDto = ExceptionDto.of(ErrorCode.AUTHENTICATION_REQUIRED); + + // 공통 응답 DTO로 감싸기 + ResponseDto errorResponse = ResponseDto.builder() + .httpStatus(HttpStatus.UNAUTHORIZED) + .success(false) + .data(null) + .error(errorDto) + .build(); + + // ObjectMapper를 사용하여 JSON으로 변환 후 응답 + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); }) ) .sessionManagement(session -> session @@ -85,7 +99,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of("*")); - configuration.setAllowedMethods(List.of("GET", "POST", "OPTIONS")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "cookie")); configuration.setExposedHeaders(List.of("Authorization", "verify")); configuration.setAllowCredentials(true); diff --git a/src/main/java/com/blockcloud/exception/error/ErrorCode.java b/src/main/java/com/blockcloud/exception/error/ErrorCode.java index 76780bd..365dae5 100644 --- a/src/main/java/com/blockcloud/exception/error/ErrorCode.java +++ b/src/main/java/com/blockcloud/exception/error/ErrorCode.java @@ -42,6 +42,7 @@ public enum ErrorCode { TOKEN_UNSUPPORTED_ERROR(40105, HttpStatus.UNAUTHORIZED, "지원하지 않는 토큰입니다."), TOKEN_GENERATION_ERROR(40106, HttpStatus.UNAUTHORIZED, "토큰 생성에 실패하였습니다."), TOKEN_UNKNOWN_ERROR(40107, HttpStatus.UNAUTHORIZED, "알 수 없는 토큰입니다."), + AUTHENTICATION_REQUIRED(40108, HttpStatus.UNAUTHORIZED, "토큰 인증이 필요합니다."), // Internal Server Error INTERNAL_SERVER_ERROR(50000, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 에러입니다."), From 4112e104c2a18e6cd2880297c7661247b369b233 Mon Sep 17 00:00:00 2001 From: parksomii Date: Sat, 9 Aug 2025 00:34:43 +0900 Subject: [PATCH 16/22] feat: Add blockInfo field and updateArchitecture method to Project Introduces a new 'blockInfo' field to store additional project information as LONGTEXT and provides an 'updateArchitecture' method to update this field. --- src/main/java/com/blockcloud/domain/project/Project.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/blockcloud/domain/project/Project.java b/src/main/java/com/blockcloud/domain/project/Project.java index 4fdbe1a..b868aa2 100644 --- a/src/main/java/com/blockcloud/domain/project/Project.java +++ b/src/main/java/com/blockcloud/domain/project/Project.java @@ -25,6 +25,9 @@ public class Project extends BaseTimeEntity { @Column(columnDefinition = "TEXT") private String description; + @Column(name = "block_info", columnDefinition = "LONGTEXT") + private String blockInfo; + @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private List members = new ArrayList<>(); @@ -33,4 +36,8 @@ public void updateInfo(String name, String description) { this.name = name; this.description = description; } + + public void updateArchitecture(String blockInfo) { + this.blockInfo = blockInfo; + } } From 4258910fb7b16912cee40feec239684c8fd20d3d Mon Sep 17 00:00:00 2001 From: parksomii Date: Sat, 9 Aug 2025 00:36:54 +0900 Subject: [PATCH 17/22] refactor: Change projects field type in ProjectListResponseDto Changed the type of the 'projects' field from List to List for consistency. --- .../dto/ResponseDto/ProjectListResponseDto.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/blockcloud/dto/ResponseDto/ProjectListResponseDto.java b/src/main/java/com/blockcloud/dto/ResponseDto/ProjectListResponseDto.java index 94cef4c..bab43c9 100644 --- a/src/main/java/com/blockcloud/dto/ResponseDto/ProjectListResponseDto.java +++ b/src/main/java/com/blockcloud/dto/ResponseDto/ProjectListResponseDto.java @@ -4,11 +4,13 @@ import lombok.Builder; import lombok.Getter; +/** + * 프로젝트 목록 조회 시 'data' 필드에 담길 응답 DTO. + */ @Getter @Builder public class ProjectListResponseDto { - private boolean success; - private List projects; + private List projects; private boolean hasNext; -} \ No newline at end of file +} From 09f4cd7d2cb7fb4a63b3a8fa5a837e381d69d1cd Mon Sep 17 00:00:00 2001 From: parksomii Date: Sat, 9 Aug 2025 00:39:54 +0900 Subject: [PATCH 18/22] refactor: Flatten ProjectResponseDto structure Removed the nested ProjectInfo class and moved its fields directly into ProjectResponseDto. Also removed the 'success' field. This simplifies the DTO structure for project responses. --- .../dto/ResponseDto/ProjectResponseDto.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/blockcloud/dto/ResponseDto/ProjectResponseDto.java b/src/main/java/com/blockcloud/dto/ResponseDto/ProjectResponseDto.java index 9fae9d4..43855d3 100644 --- a/src/main/java/com/blockcloud/dto/ResponseDto/ProjectResponseDto.java +++ b/src/main/java/com/blockcloud/dto/ResponseDto/ProjectResponseDto.java @@ -4,21 +4,16 @@ import lombok.Builder; import lombok.Getter; +/** + * 프로젝트 응답 DTO 프로젝트의 ID, 이름, 설명, 생성 및 수정 시간을 포함합니다. + */ @Getter @Builder public class ProjectResponseDto { - private boolean success; - private ProjectInfo project; - - @Getter - @Builder - public static class ProjectInfo { - - private Long id; - private String name; - private String description; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - } + private Long id; + private String name; + private String description; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; } \ No newline at end of file From f1a762cbd736ba4be7999338c9b3be0296cd5a99 Mon Sep 17 00:00:00 2001 From: parksomii Date: Sat, 9 Aug 2025 00:42:14 +0900 Subject: [PATCH 19/22] refactor: Improve exception handling and DTO mapping in ProjectService Replaced generic and security exceptions with custom CommonException and ErrorCode for better error handling. Extracted project member validation and project lookup into private methods. Simplified ProjectResponseDto creation with a dedicated mapping method and updated methods to use it. Updated list and update methods to use new DTO mapping and error handling. --- .../blockcloud/service/ProjectService.java | 123 +++++++++--------- 1 file changed, 59 insertions(+), 64 deletions(-) diff --git a/src/main/java/com/blockcloud/service/ProjectService.java b/src/main/java/com/blockcloud/service/ProjectService.java index 433e242..073fc36 100644 --- a/src/main/java/com/blockcloud/service/ProjectService.java +++ b/src/main/java/com/blockcloud/service/ProjectService.java @@ -9,12 +9,13 @@ import com.blockcloud.dto.RequestDto.ProjectRequestDto; import com.blockcloud.dto.ResponseDto.ProjectListResponseDto; import com.blockcloud.dto.ResponseDto.ProjectResponseDto; +import com.blockcloud.exception.CommonException; +import com.blockcloud.exception.error.ErrorCode; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,7 +42,7 @@ public class ProjectService { public ProjectResponseDto create(ProjectRequestDto dto, String email) { // 이메일로 사용자 조회 User user = userRepository.findByEmail(email) - .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_ACCOUNT)); // 프로젝트 엔티티 생성 및 저장 Project project = Project.builder() @@ -57,17 +58,7 @@ public ProjectResponseDto create(ProjectRequestDto dto, String email) { .build(); projectUserRepository.save(link); - // 응답 DTO 생성 및 반환 - return ProjectResponseDto.builder() - .success(true) - .project(ProjectResponseDto.ProjectInfo.builder() - .id(project.getId()) - .name(project.getName()) - .description(project.getDescription()) - .createdAt(project.getCreatedAt()) - .updatedAt(project.getUpdatedAt()) - .build()) - .build(); + return toProjectResponseDto(project); } /** @@ -84,86 +75,90 @@ public ProjectListResponseDto findNext(Long lastId, int size) { // 프로젝트 목록 조회 List projects = projectRepository.findNextProjects(lastId, pageable); - // 다음 페이지 존재 여부 확인 - boolean hasNext = projects.size() == size; - - // DTO 변환 - List projectInfos = projects.stream() - .map(project -> ProjectResponseDto.ProjectInfo.builder() - .id(project.getId()) - .name(project.getName()) - .description(project.getDescription()) - .createdAt(project.getCreatedAt()) - .updatedAt(project.getUpdatedAt()) - .build()) + List projectDtos = projects.stream() + .map(this::toProjectResponseDto) .collect(Collectors.toList()); // 응답 DTO 생성 및 반환 return ProjectListResponseDto.builder() - .success(true) - .projects(projectInfos) - .hasNext(hasNext) + .projects(projectDtos) + .hasNext(projects.size() == size) .build(); } + /** * 기존 프로젝트의 정보를 수정 * * @param projectId 수정할 프로젝트 ID * @param dto 수정할 프로젝트 정보 (이름, 설명 등) + * @param email 요청한 사용자의 이메일 * @return 수정된 프로젝트 정보가 담긴 응답 DTO - * @throws IllegalArgumentException 해당 ID의 프로젝트를 찾을 수 없는 경우 발생 - * @throws AccessDeniedException 사용자가 프로젝트의 멤버가 아닌 경우 발생 + * @throws CommonException 해당 프로젝트를 찾을 수 없는 경우 또는 접근 권한이 없는 경우 발생 */ @Transactional public ProjectResponseDto update(Long projectId, ProjectRequestDto dto, String email) { - Project project = projectRepository.findById(projectId) - .orElseThrow(() -> new IllegalArgumentException("프로젝트를 찾을 수 없습니다.")); - - boolean hasAccess = project.getMembers().stream() - .anyMatch(member -> member.getUser().getEmail().equals(email)); - - if (!hasAccess) { - throw new AccessDeniedException("프로젝트에 접근할 수 없습니다."); - } + Project project = findProjectById(projectId); + validateProjectMember(project, email); project.updateInfo(dto.getName(), dto.getDescription()); - - return ProjectResponseDto.builder() - .success(true) - .project(ProjectResponseDto.ProjectInfo.builder() - .id(project.getId()) - .name(project.getName()) - .description(project.getDescription()) - .createdAt(project.getCreatedAt()) - .updatedAt(project.getUpdatedAt()) - .build()) - .build(); + return toProjectResponseDto(project); } /** * 프로젝트를 삭제 * * @param projectId 삭제할 프로젝트 ID - * @param email 삭제를 요청한 사용자의 이메일 - * @throws IllegalArgumentException 해당 ID의 프로젝트를 찾을 수 없는 경우 발생 - * @throws AccessDeniedException 사용자가 프로젝트의 멤버가 아닌 경우 발생 + * @param email 요청한 사용자의 이메일 + * @throws CommonException 해당 프로젝트를 찾을 수 없는 경우 또는 접근 권한이 없는 경우 발생 */ @Transactional public void delete(Long projectId, String email) { - // 프로젝트 ID로 프로젝트 조회 - Project project = projectRepository.findById(projectId) - .orElseThrow(() -> new IllegalArgumentException("프로젝트를 찾을 수 없습니다.")); + Project project = findProjectById(projectId); + validateProjectMember(project, email); + projectRepository.delete(project); + } - // 사용자가 프로젝트의 멤버인지 확인 - boolean hasAccess = project.getMembers().stream() - .anyMatch(member -> member.getUser().getEmail().equals(email)); + /** + * 프로젝트 ID로 프로젝트를 조회하고 존재하지 않으면 예외 발생 + * + * @param projectId 조회할 프로젝트 ID + * @return 해당 프로젝트 엔티티 + * @throws CommonException 해당 프로젝트를 찾을 수 없는 경우 발생 + */ + private Project findProjectById(Long projectId) { + return projectRepository.findById(projectId) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_PROJECT)); + } - // 접근 권한이 없으면 예외 발생 - if (!hasAccess) { - throw new AccessDeniedException("프로젝트에 접근할 수 없습니다."); + /** + * 프로젝트의 멤버인지 검증 + * + * @param project 조회할 프로젝트 + * @param email 요청한 사용자의 이메일 + * @throws CommonException 접근 권한이 없는 경우 발생 + */ + private void validateProjectMember(Project project, String email) { + boolean isMember = project.getMembers().stream() + .anyMatch(member -> member.getUser().getEmail().equals(email)); + if (!isMember) { + throw new CommonException(ErrorCode.ACCESS_DENIED); } - // 프로젝트 삭제 - projectRepository.delete(project); + } + + /** + * Project 엔티티를 ProjectResponseDto로 변환 + * + * @param project 변환할 프로젝트 엔티티 + * @return 변환된 응답 DTO + */ + private ProjectResponseDto toProjectResponseDto(Project project) { + return ProjectResponseDto.builder() + .id(project.getId()) + .name(project.getName()) + .description(project.getDescription()) + .createdAt(project.getCreatedAt()) + .updatedAt(project.getUpdatedAt()) + .build(); } } \ No newline at end of file From 6f8cdae3d38921bae6e4b08ae410e3a970c45195 Mon Sep 17 00:00:00 2001 From: parksomii Date: Sat, 9 Aug 2025 00:44:10 +0900 Subject: [PATCH 20/22] refactor: Standardize API responses and documentation in ProjectController Replaces ResponseEntity and CommonResponse with a generic ResponseDto for all endpoints in ProjectController. Updates API response documentation, simplifies constructor with Lombok's @RequiredArgsConstructor, and standardizes response codes and messages for consistency. --- .../controller/ProjectController.java | 83 +++++++------------ 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/blockcloud/controller/ProjectController.java b/src/main/java/com/blockcloud/controller/ProjectController.java index 91587dc..6d33aa8 100644 --- a/src/main/java/com/blockcloud/controller/ProjectController.java +++ b/src/main/java/com/blockcloud/controller/ProjectController.java @@ -1,9 +1,9 @@ package com.blockcloud.controller; -import com.blockcloud.dto.common.CommonResponse; import com.blockcloud.dto.RequestDto.ProjectRequestDto; import com.blockcloud.dto.ResponseDto.ProjectListResponseDto; import com.blockcloud.dto.ResponseDto.ProjectResponseDto; +import com.blockcloud.dto.common.ResponseDto; import com.blockcloud.dto.oauth.CustomUserDetails; import com.blockcloud.service.ProjectService; import io.swagger.v3.oas.annotations.Operation; @@ -14,8 +14,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @@ -25,14 +24,11 @@ @Tag(name = "Project API", description = "프로젝트 생성, 조회, 수정, 삭제 관련 API") @RestController @RequestMapping("/api/projects") +@RequiredArgsConstructor public class ProjectController { private final ProjectService projectService; - public ProjectController(ProjectService projectService) { - this.projectService = projectService; - } - /** * 새로운 프로젝트를 생성합니다. * @@ -45,25 +41,17 @@ public ProjectController(ProjectService projectService) { description = "새로운 프로젝트를 생성합니다. 요청 바디에 `name`, `description`을 포함해야 하며, JWT 토큰이 필요합니다." ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "프로젝트 생성 성공", - content = @Content(schema = @Schema(implementation = ProjectResponseDto.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @ApiResponse(responseCode = "401", description = "인증 실패 (JWT 필요)") + @ApiResponse(responseCode = "201", description = "프로젝트 생성 성공", + content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "400", description = "INVALID_ARGUMENT (요청 데이터 유효성 검증 실패)"), + @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (인증 실패)") }) @PostMapping - public ResponseEntity create( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "생성할 프로젝트 정보 (name: 프로젝트 이름, description: 설명)", - required = true - ) - @RequestBody @Valid ProjectRequestDto dto, + public ResponseDto create( + @Valid @RequestBody ProjectRequestDto dto, Authentication authentication) { - CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); - String email = userDetails.getUsername(); - - ProjectResponseDto response = projectService.create(dto, email); - return ResponseEntity.status(HttpStatus.CREATED).body(response); + return ResponseDto.created(projectService.create(dto, userDetails.getUsername())); } /** @@ -79,17 +67,15 @@ public ResponseEntity create( ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProjectListResponseDto.class))), - @ApiResponse(responseCode = "401", description = "인증 실패 (JWT 필요)") + content = @Content(schema = @Schema(implementation = ResponseDto.class))) }) @GetMapping - public ResponseEntity getProjects( + public ResponseDto getProjects( @Parameter(description = "마지막으로 조회한 프로젝트 ID (첫 호출 시 생략 가능)") @RequestParam(required = false) Long lastId, @Parameter(description = "가져올 데이터 개수 (기본값 8)") @RequestParam(defaultValue = "8") int size) { - - return ResponseEntity.ok(projectService.findNext(lastId, size)); + return ResponseDto.ok(projectService.findNext(lastId, size)); } /** @@ -105,26 +91,19 @@ public ResponseEntity getProjects( ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "수정 성공", - content = @Content(schema = @Schema(implementation = ProjectResponseDto.class))), - @ApiResponse(responseCode = "401", description = "인증 실패 (JWT 필요)"), - @ApiResponse(responseCode = "403", description = "접근 권한 없음"), - @ApiResponse(responseCode = "404", description = "프로젝트를 찾을 수 없음") + content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "400", description = "INVALID_ARGUMENT (요청 데이터 유효성 검증 실패)"), + @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (인증 실패)"), + @ApiResponse(responseCode = "403", description = "FORBIDDEN (접근 권한 없음)"), + @ApiResponse(responseCode = "404", description = "NOT_FOUND (프로젝트를 찾을 수 없음)") }) @PutMapping("/{projectId}") - public ResponseEntity update( - @Parameter(description = "수정할 프로젝트 ID", required = true) - @PathVariable Long projectId, - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "수정할 프로젝트 정보 (name, description 포함)", - required = true - ) - @RequestBody @Valid ProjectRequestDto dto, + public ResponseDto update( + @Parameter(description = "수정할 프로젝트 ID", required = true) @PathVariable Long projectId, + @Valid @RequestBody ProjectRequestDto dto, Authentication authentication) { - CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); - String email = userDetails.getUsername(); - - return ResponseEntity.ok(projectService.update(projectId, dto, email)); + return ResponseDto.ok(projectService.update(projectId, dto, userDetails.getUsername())); } /** @@ -138,22 +117,18 @@ public ResponseEntity update( description = "프로젝트를 삭제합니다. JWT 인증 필요." ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "삭제 성공", - content = @Content(schema = @Schema(implementation = CommonResponse.class))), - @ApiResponse(responseCode = "401", description = "인증 실패 (JWT 필요)"), - @ApiResponse(responseCode = "403", description = "접근 권한 없음"), - @ApiResponse(responseCode = "404", description = "프로젝트를 찾을 수 없음") + @ApiResponse(responseCode = "204", description = "삭제 성공"), + @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (인증 실패)"), + @ApiResponse(responseCode = "403", description = "FORBIDDEN (접근 권한 없음)"), + @ApiResponse(responseCode = "404", description = "NOT_FOUND (프로젝트를 찾을 수 없음)") }) @DeleteMapping("/{projectId}") - public ResponseEntity delete( + public ResponseDto delete( @Parameter(description = "삭제할 프로젝트 ID", required = true) @PathVariable Long projectId, Authentication authentication) { - CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); - String email = userDetails.getUsername(); - - projectService.delete(projectId, email); - return ResponseEntity.ok(new CommonResponse(true, "프로젝트를 성공적으로 삭제했습니다.")); + projectService.delete(projectId, userDetails.getUsername()); + return ResponseDto.noContent(); } } \ No newline at end of file From cf045395e976aa84af215d4ce09f6bcfb786a46a Mon Sep 17 00:00:00 2001 From: parksomii Date: Tue, 12 Aug 2025 00:20:32 +0900 Subject: [PATCH 21/22] fix: test.yml --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5bb2c08..c065fe1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,11 +27,11 @@ jobs: run: | mkdir -p src/main/resources mkdir -p src/test/resources - echo "${{ secrets.APPLICATION_YML }}" > ./src/main/resources/application.yml - echo "${{ secrets.APPLICATION_YML_DEV }}" > ./src/main/resources/application-dev.yml - echo "${{ secrets.APPLICATION_YML_PROD }}" > ./src/main/resources/application-prod.yml - echo "${{ secrets.APPLICATION_YML_SECRET }}" > ./src/main/resources/application-secret.yml - echo "${{ secrets.APPLICATION_YML_TEST }}" > ./src/test/resources/application.yml + echo '${{ secrets.APPLICATION_YML }}' > ./src/main/resources/application.yml + echo '${{ secrets.APPLICATION_YML_DEV }}' > ./src/main/resources/application-dev.yml + echo '${{ secrets.APPLICATION_YML_PROD }}' > ./src/main/resources/application-prod.yml + echo '${{ secrets.APPLICATION_YML_SECRET }}' > ./src/main/resources/application-secret.yml + echo '${{ secrets.APPLICATION_YML_TEST }}' > ./src/test/resources/application.yml - name: Debug created files run: | From ddaad23998cc4aa85b4503c227d9da29c839e4a2 Mon Sep 17 00:00:00 2001 From: parksomii Date: Tue, 12 Aug 2025 00:36:29 +0900 Subject: [PATCH 22/22] refactor: Remove detailed response annotations --- .../controller/BlockController.java | 86 +------------------ 1 file changed, 1 insertion(+), 85 deletions(-) diff --git a/src/main/java/com/blockcloud/controller/BlockController.java b/src/main/java/com/blockcloud/controller/BlockController.java index 745fe2a..7e71c23 100644 --- a/src/main/java/com/blockcloud/controller/BlockController.java +++ b/src/main/java/com/blockcloud/controller/BlockController.java @@ -8,11 +8,6 @@ import com.blockcloud.service.BlockService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -27,65 +22,17 @@ public class BlockController { private final BlockService blockService; - /** - * 블록 아키텍처 저장 특정 프로젝트(projectId)에 대한 블록 인프라 데이터를 저장합니다. - */ @Operation( summary = "블록 아키텍처 저장", description = "특정 프로젝트(`projectId`)에 대한 블록 인프라 데이터를 저장합니다. " ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "저장 성공", - content = @Content(schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = """ - { - "success": true, - "message": "아키텍처가 성공적으로 저장되었습니다.", - "data": { - "projectId": 1, - "architectureName": "aws-architecture-2025-07-22", - "updatedAt": "2025-07-22T14:30:00" - } - } - """))), - @ApiResponse(responseCode = "400", description = "INVALID_ARGUMENT (요청 데이터 유효성 검증 실패)"), - @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (인증 실패)"), - @ApiResponse(responseCode = "403", description = "ACCESS_DENIED (접근 권한 없음)"), - @ApiResponse(responseCode = "404", description = "NOT_FOUND_PROJECT (프로젝트를 찾을 수 없음)") - }) @PostMapping("/{projectId}") public ResponseDto saveBlocks( @Parameter(description = "블록을 저장할 프로젝트 ID", required = true) @PathVariable Long projectId, @io.swagger.v3.oas.annotations.parameters.RequestBody( description = "저장할 블록 아키텍처 데이터", - required = true, - content = @Content(examples = @ExampleObject(value = """ - { - "createdAt": "2025-07-22T14:20:00", - "updatedAt": "2025-07-22T14:25:00", - "blocks": [ - { - "id": "web-server", - "type": "aws_instance", - "position": { - "x": 100, - "y": 200 - }, - "properties": { - "ami": "ami-12345678", - "instance_type": "t2.micro", - "tags": { - "Name": "MyServer" - } - }, - "connections": [ - "subnet-1" - ] - } - ] - } - """)) + required = true ) @Valid @RequestBody BlockSaveRequestDto requestDto, Authentication authentication) { @@ -95,41 +42,10 @@ public ResponseDto saveBlocks( blockService.saveBlocks(projectId, requestDto, userDetails.getUsername())); } - /** - * 블록 아키텍처 불러오기 특정 프로젝트(projectId)의 블록 및 연결 정보를 불러옵니다. - */ @Operation( summary = "블록 아키텍처 불러오기", description = "특정 프로젝트(`projectId`)의 블록 및 연결 정보를 불러옵니다. " ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(examples = @ExampleObject(value = """ - { - "createdAt": "2025-07-22T14:20:00", - "updatedAt": "2025-07-22T14:25:00", - "blocks": [ - { - "id": "web-server", - "type": "aws_instance", - "position": { "x": 100, "y": 200 }, - "properties": { ... }, - "connections": ["subnet-1"] - }, - { - "id": "subnet-1", - "type": "aws_subnet", - "position": { "x": 200, "y": 300 }, - "properties": { ... }, - "connections": [] - } - ] - } - """))), - @ApiResponse(responseCode = "401", description = "UNAUTHORIZED (인증 실패)"), - @ApiResponse(responseCode = "403", description = "ACCESS_DENIED (접근 권한 없음)"), - @ApiResponse(responseCode = "404", description = "NOT_FOUND_PROJECT (프로젝트를 찾을 수 없음)") - }) @GetMapping("/{projectId}") public ResponseDto getBlocks( @Parameter(description = "블록을 조회할 프로젝트 ID", required = true) @PathVariable Long projectId,