Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
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
57 changes: 57 additions & 0 deletions src/main/java/com/blockcloud/controller/BlockController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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.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;

@Operation(
summary = "블록 아키텍처 저장",
description = "특정 프로젝트(`projectId`)에 대한 블록 인프라 데이터를 저장합니다. "
)
@PostMapping("/{projectId}")
public ResponseDto<BlockSaveResponseDto> saveBlocks(
@Parameter(description = "블록을 저장할 프로젝트 ID", required = true)
@PathVariable Long projectId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "저장할 블록 아키텍처 데이터",
required = true
)
@Valid @RequestBody BlockSaveRequestDto requestDto,
Authentication authentication) {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseDto.ok(
blockService.saveBlocks(projectId, requestDto, userDetails.getUsername()));
}

@Operation(
summary = "블록 아키텍처 불러오기",
description = "특정 프로젝트(`projectId`)의 블록 및 연결 정보를 불러옵니다. "
)
@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()));
}
}
83 changes: 29 additions & 54 deletions src/main/java/com/blockcloud/controller/ProjectController.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.*;

Expand All @@ -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;
}

/**
* 새로운 프로젝트를 생성합니다.
*
Expand All @@ -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<ProjectResponseDto> create(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "생성할 프로젝트 정보 (name: 프로젝트 이름, description: 설명)",
required = true
)
@RequestBody @Valid ProjectRequestDto dto,
public ResponseDto<ProjectResponseDto> 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()));
}

/**
Expand All @@ -79,17 +67,15 @@ public ResponseEntity<ProjectResponseDto> 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<ProjectListResponseDto> getProjects(
public ResponseDto<ProjectListResponseDto> 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));
}

/**
Expand All @@ -105,26 +91,19 @@ public ResponseEntity<ProjectListResponseDto> 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<ProjectResponseDto> 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<ProjectResponseDto> 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()));
}

/**
Expand All @@ -138,22 +117,18 @@ public ResponseEntity<ProjectResponseDto> 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<CommonResponse> delete(
public ResponseDto<Object> 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();
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/blockcloud/domain/project/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectUser> members = new ArrayList<>();
Expand All @@ -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;
}
}
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;
}
Loading