Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2d73324
feat: Add block and connection info to Project entity
parksomii Jul 22, 2025
7d6f29f
feat: Add BlockSaveRequestDto for block save requests
parksomii Jul 22, 2025
c2f08d3
feat: Add BlockSaveResponseDto for block save responses
parksomii Jul 22, 2025
be0d665
feat: Add BlockService for block management
parksomii Jul 22, 2025
9d6991b
feat: Add BlockController for block architecture API
parksomii Jul 22, 2025
bdabfbc
feat: Return JSON response for 401 Unauthorized errors
parksomii Jul 24, 2025
608409c
feat: Add global exception handler for API errors
parksomii Jul 24, 2025
264d3a5
Merge branch 'develop' of https://github.com/BlockCloud-dev/blockclou…
parksomii Aug 2, 2025
f455a45
Merge branch 'develop' of https://github.com/BlockCloud-dev/blockclou…
parksomii Aug 8, 2025
37a3197
feat: Add NOT_FOUND_PROJECT error code
parksomii Aug 8, 2025
fcb7063
refactor: Remove GlobalExceptionHandler
parksomii Aug 8, 2025
7e037ac
feat: Add validation to BlockSaveRequestDto
parksomii Aug 8, 2025
dd64be1
feat: Add BlockGetResponseDto for block retrieval responses
parksomii Aug 8, 2025
b8f8681
refactor: Flatten BlockSaveResponseDto structure
parksomii Aug 8, 2025
7383c15
refactor: Enhance BlockService with member validation and DTOs
parksomii Aug 8, 2025
466bb62
refactor: Apply ResponseDto and authentication to BlockController
parksomii Aug 8, 2025
f0b5286
refactor: Standardize authentication error response
parksomii Aug 8, 2025
4112e10
feat: Add blockInfo field and updateArchitecture method to Project
parksomii Aug 8, 2025
4258910
refactor: Change projects field type in ProjectListResponseDto
parksomii Aug 8, 2025
09f4cd7
refactor: Flatten ProjectResponseDto structure
parksomii Aug 8, 2025
f1a762c
refactor: Improve exception handling and DTO mapping in ProjectService
parksomii Aug 8, 2025
6f8cdae
refactor: Standardize API responses and documentation in ProjectContr…
parksomii Aug 8, 2025
cf04539
fix: test.yml
parksomii Aug 11, 2025
c29680b
Merge branch 'feature/project-api' of https://github.com/BlockCloud-d…
parksomii Aug 11, 2025
ddaad23
refactor: Remove detailed response annotations
parksomii Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/main/java/com/blockcloud/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,14 +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;
Expand All @@ -22,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 {
Expand All @@ -54,6 +63,26 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
)
.successHandler(new OAuth2SuccessHandler(jwtUtil, cookieService))
)
// 401 Unauthorized 에러를 공통 응답 포맷으로 변경
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");

ExceptionDto errorDto = ExceptionDto.of(ErrorCode.AUTHENTICATION_REQUIRED);

// 공통 응답 DTO로 감싸기
ResponseDto<Object> errorResponse = ResponseDto.builder()
.httpStatus(HttpStatus.UNAUTHORIZED)
.success(false)
.data(null)
.error(errorDto)
.build();

// ObjectMapper를 사용하여 JSON으로 변환 후 응답
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
})
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
Expand All @@ -70,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);
Expand Down
141 changes: 141 additions & 0 deletions src/main/java/com/blockcloud/controller/BlockController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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;
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;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

@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 = 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<BlockSaveResponseDto> 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"
]
}
]
}
"""))
)
@Valid @RequestBody BlockSaveRequestDto requestDto,
Authentication authentication) {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseDto.ok(
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<BlockGetResponseDto> getBlocks(
@Parameter(description = "블록을 조회할 프로젝트 ID", required = true) @PathVariable Long projectId,
Authentication authentication) {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseDto.ok(blockService.getBlocks(projectId, userDetails.getUsername()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.blockcloud.dto.RequestDto;

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 {

@NotNull(message = "블록 정보는 필수입니다.")
private Object blocks;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.blockcloud.dto.ResponseDto;

import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;

/**
* 블록 저장 응답 DTO
*/
@Getter
@Builder
public class BlockSaveResponseDto {

private Long projectId;
private String architectureName;
private LocalDateTime updatedAt;
}
4 changes: 3 additions & 1 deletion src/main/java/com/blockcloud/exception/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ 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, "서버 내부 에러입니다."),
INTERNAL_DATA_ERROR(50001, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 데이터 에러입니다."),

// 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;
Expand Down
87 changes: 87 additions & 0 deletions src/main/java/com/blockcloud/service/BlockService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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.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 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, String email) {
Project project = projectRepository.findById(projectId)
.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()
.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 BlockGetResponseDto getBlocks(Long projectId, String email) {
Project project = projectRepository.findById(projectId)
.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);
}
}
}
Loading