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: | diff --git a/src/main/java/com/blockcloud/config/SecurityConfig.java b/src/main/java/com/blockcloud/config/SecurityConfig.java index e2f6fe6..ebc2acf 100644 --- a/src/main/java/com/blockcloud/config/SecurityConfig.java +++ b/src/main/java/com/blockcloud/config/SecurityConfig.java @@ -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; @@ -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 { @@ -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 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) ) @@ -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); 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..7e71c23 --- /dev/null +++ b/src/main/java/com/blockcloud/controller/BlockController.java @@ -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 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 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 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 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; + } } 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..59c3688 --- /dev/null +++ b/src/main/java/com/blockcloud/dto/RequestDto/BlockSaveRequestDto.java @@ -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; +} \ No newline at end of file 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 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..fdad649 --- /dev/null +++ b/src/main/java/com/blockcloud/dto/ResponseDto/BlockSaveResponseDto.java @@ -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; +} 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 +} 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 diff --git a/src/main/java/com/blockcloud/exception/error/ErrorCode.java b/src/main/java/com/blockcloud/exception/error/ErrorCode.java index c866650..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, "서버 내부 에러입니다."), @@ -49,8 +50,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; 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..9aec0a2 --- /dev/null +++ b/src/main/java/com/blockcloud/service/BlockService.java @@ -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); + } + } +} \ No newline at end of file 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