Skip to content

Commit

Permalink
Merge pull request #20 from kakao-tech-campus-2nd-step3/feature/19-me…
Browse files Browse the repository at this point in the history
…mber

feat: 회원 관련 API 구현 및 CI 관련 워크플로우 추가
  • Loading branch information
peeerr authored Sep 27, 2024
2 parents 7c69c7e + 0aa036d commit b994bce
Show file tree
Hide file tree
Showing 29 changed files with 973 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: master 브랜치 자동 배포
name: master 브랜치 merge 시 CI/CD 파이프라인

on:
push:
Expand All @@ -19,6 +19,15 @@ jobs:
distribution: 'corretto'
java-version: '21'

- name: AWS S3 관련 정보를 설정 파일에 주입
uses: microsoft/variable-substitution@v1
with:
files: ./src/main/resources/application-prod.yml
env:
aws.s3.bucket: ${{ secrets.AWS_S3_BUCKET }}
aws.s3.accessKey: ${{ secrets.AWS_S3_ACCESS_KEY }}
aws.s3.secretKey: ${{ secrets.AWS_S3_SECRET_KEY }}

- name: 빌드로 테스트 수행 및 Jar 파일 생성
run: |
chmod +x ./gradlew
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/pr_weekly_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: weekly 브랜치 PR에 대한 CI 테스트

on:
pull_request:
branches:
- 'weekly/**'

jobs:
ci:
runs-on: ubuntu-latest

permissions:
checks: write
pull-requests: write

steps:
- name: 프로젝트 코드를 CI 서버로 옮겨오기
uses: actions/checkout@v4

- name: JDK 21 설치
uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: '21'

- name: AWS S3 관련 정보를 설정 파일에 주입
uses: microsoft/variable-substitution@v1
with:
files: ./src/main/resources/application-prod.yml
env:
aws.s3.bucket: ${{ secrets.AWS_S3_BUCKET }}
aws.s3.accessKey: ${{ secrets.AWS_S3_ACCESS_KEY }}
aws.s3.secretKey: ${{ secrets.AWS_S3_SECRET_KEY }}

- name: 빌드 테스트 수행
run: |
chmod +x ./gradlew
./gradlew clean build
- name: 테스트 수행 결과 보고
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: '**/build/test-results/test/TEST-*.xml'

- name: 테스트 실패 시, 실패한 코드 라인에 코멘트 자동 등록
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
token: ${{ github.token }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/

application-dev.yml

### STS ###
.apt_generated
.classpath
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'com.amazonaws:aws-java-sdk-s3:1.12.657'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.potatocake.everymoment.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "aws.s3")
public record AwsS3Properties(
String accessKey,
String secretKey,
String region,
String bucket
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/members/login", "/h2-console/**", "/error").permitAll()
.requestMatchers("/api/members/login", "/h2-console/**", "/error", "/favicon.ico").permitAll()
.requestMatchers("/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated());

http
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.potatocake.everymoment.controller;

import com.potatocake.everymoment.dto.SuccessResponse;
import com.potatocake.everymoment.dto.response.MemberDetailResponse;
import com.potatocake.everymoment.dto.response.MemberResponse;
import com.potatocake.everymoment.dto.response.MemberSearchResponse;
import com.potatocake.everymoment.exception.ErrorCode;
import com.potatocake.everymoment.exception.GlobalException;
import com.potatocake.everymoment.security.MemberDetails;
import com.potatocake.everymoment.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.util.StringUtils;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RequiredArgsConstructor
@RequestMapping("/api/members")
@RestController
public class MemberController {

private final MemberService memberService;

@GetMapping
public ResponseEntity<SuccessResponse<MemberSearchResponse>> searchMembers(
@RequestParam(required = false) String nickname,
@RequestParam(required = false) String email,
@RequestParam(required = false) Long key,
@RequestParam(defaultValue = "10") int size) {

MemberSearchResponse response = memberService.searchMembers(nickname, email, key, size);

return ResponseEntity.ok()
.body(SuccessResponse.ok(response));
}

@GetMapping("/me")
public ResponseEntity<SuccessResponse<MemberDetailResponse>> myInfo(
@AuthenticationPrincipal MemberDetails memberDetails) {
MemberDetailResponse response = memberService.getMyInfo(memberDetails.getId());

return ResponseEntity.ok()
.body(SuccessResponse.ok(response));
}

@GetMapping("/{memberId}")
public ResponseEntity<SuccessResponse<MemberResponse>> memberInfo(@PathVariable Long memberId) {
MemberResponse response = memberService.getMemberInfo(memberId);

return ResponseEntity.ok()
.body(SuccessResponse.ok(response));
}

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<SuccessResponse<Void>> updateMemberInfo(@AuthenticationPrincipal MemberDetails memberDetails,
@RequestParam(required = false) MultipartFile profileImage,
@RequestParam(required = false) String nickname) {
validateProfileUpdate(profileImage, nickname);

memberService.updateMemberInfo(memberDetails.getId(), profileImage, nickname);

return ResponseEntity.ok()
.body(SuccessResponse.ok());
}

private void validateProfileUpdate(MultipartFile profileImage, String nickname) {
if (profileImage == null && !StringUtils.hasText(nickname)) {
throw new GlobalException(ErrorCode.INFO_REQUIRED);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,27 @@ public class SuccessResponse<T> {
private String message;
private T info;

public static <T> SuccessResponse of(T info) {
public static <T> SuccessResponse of(int code, T info) {
return SuccessResponse.builder()
.code(code)
.message("success")
.info(info)
.build();
}

public static <T> SuccessResponse ok(T info) {
return SuccessResponse.builder()
.code(200)
.message("success")
.info(info)
.build();
}

public static <T> SuccessResponse ok() {
return SuccessResponse.builder()
.code(200)
.message("success")
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.potatocake.everymoment.dto.response;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class MemberDetailResponse {

private Long id;
private String profileImageUrl;
private String nickname;
private String email;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.potatocake.everymoment.dto.response;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class MemberResponse {

private Long id;
private String profileImageUrl;
private String nickname;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.potatocake.everymoment.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List;
import lombok.Builder;
import lombok.Getter;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Builder
@Getter
public class MemberSearchResponse {

private List<MemberResponse> members;
private Long next;

}
5 changes: 5 additions & 0 deletions src/main/java/com/potatocake/everymoment/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ public class Member extends BaseTimeEntity {
@Lob
private String profileImageUrl;

public void update(String nickname, String profileImageUrl) {
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
}

}
14 changes: 12 additions & 2 deletions src/main/java/com/potatocake/everymoment/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.potatocake.everymoment.exception;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.CONFLICT;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE;

import lombok.Getter;
import org.springframework.http.HttpStatus;
Expand All @@ -29,15 +31,23 @@ public enum ErrorCode {

/* File */
FILE_NOT_FOUND("존재하지 않는 파일입니다.", NOT_FOUND),
FILE_SIZE_EXCEEDED("전체 파일 크기를 10MB 이하로 첨부해 주세요.", PAYLOAD_TOO_LARGE),
FILE_SIZE_EXCEEDED("각 파일은 1MB 이하로, 전체 파일 크기는 10MB 이하로 첨부해 주세요.", PAYLOAD_TOO_LARGE),

/* Comment */
COMMENT_NOT_FOUND("존재하지 않는 댓글입니다.", NOT_FOUND),

UNKNOWN_ERROR("알 수 없는 오류가 발생했습니다.", INTERNAL_SERVER_ERROR),

LOGIN_FAILED("로그인에 실패했습니다.", UNAUTHORIZED),
LOGIN_REQUIRED("유효한 인증 정보가 필요합니다.", UNAUTHORIZED);
LOGIN_REQUIRED("유효한 인증 정보가 필요합니다.", UNAUTHORIZED),

MEMBER_NOT_FOUND("존재하지 않는 회원입니다.", NOT_FOUND),

/* S3FileUploader */
INVALID_FILE_TYPE("이미지 파일 형식만 첨부가 가능합니다. (JPEG, PNG)", UNSUPPORTED_MEDIA_TYPE),
FILE_STORE_FAILED("파일 저장에 실패했습니다.", INTERNAL_SERVER_ERROR),

INFO_REQUIRED("정보를 입력해 주세요.", BAD_REQUEST);

private final String message;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.potatocake.everymoment.exception;

import static com.potatocake.everymoment.exception.ErrorCode.FILE_SIZE_EXCEEDED;
import static com.potatocake.everymoment.exception.ValidationErrorMessage.VALIDATION_ERROR;
import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
Expand All @@ -8,6 +12,8 @@
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;

@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
Expand All @@ -16,10 +22,7 @@ public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> invalid(MethodArgumentNotValidException e) {
ErrorResponse errorResponse = ErrorResponse.builder()
.code(400)
.message(ValidationErrorMessage.VALIDATION_ERROR)
.build();
ErrorResponse errorResponse = getErrorResponse(400, VALIDATION_ERROR);

e.getFieldErrors().forEach(filedError ->
errorResponse.addValidation(filedError.getField(), filedError.getDefaultMessage()));
Expand All @@ -28,6 +31,22 @@ public ResponseEntity<ErrorResponse> invalid(MethodArgumentNotValidException e)
.body(errorResponse);
}

@ExceptionHandler(MissingServletRequestPartException.class)
public ResponseEntity<ErrorResponse> missingRequestPart(MissingServletRequestPartException e) {
ErrorResponse errorResponse = getErrorResponse(400, e.getMessage());

return ResponseEntity.badRequest()
.body(errorResponse);
}

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ErrorResponse> fileMaxSize() {
ErrorResponse errorResponse = getErrorResponse(PAYLOAD_TOO_LARGE.value(), FILE_SIZE_EXCEEDED.getMessage());

return ResponseEntity.status(PAYLOAD_TOO_LARGE)
.body(errorResponse);
}

@ExceptionHandler(GlobalException.class)
public ResponseEntity<ErrorResponse> handlerCustomException(GlobalException e) {
HttpStatus status = e.getErrorCode().getStatus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.potatocake.everymoment.entity.Member;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
Expand All @@ -10,4 +13,7 @@ public interface MemberRepository extends JpaRepository<Member, Long> {

boolean existsByEmail(String email);

Window<Member> findByNicknameContainingAndEmailContaining(String nickname, String email, ScrollPosition position,
Pageable pageable);

}
Loading

0 comments on commit b994bce

Please sign in to comment.