diff --git a/Gibeom/umc10th/build.gradle b/Gibeom/umc10th/build.gradle index 2314bead..183ef1c7 100644 --- a/Gibeom/umc10th/build.gradle +++ b/Gibeom/umc10th/build.gradle @@ -31,6 +31,10 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testAnnotationProcessor 'org.projectlombok:lombok' + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' diff --git a/Gibeom/umc10th/keyword_summary/ch07.md b/Gibeom/umc10th/keyword_summary/ch07.md deleted file mode 100644 index b28a1fba..00000000 --- a/Gibeom/umc10th/keyword_summary/ch07.md +++ /dev/null @@ -1,80 +0,0 @@ -- JPA란? - - JPA(Java Persistence API)란, **자바 진영에서 ORM(Object-Relational Mapping) 기술의 표준스펙.** - - 실제 구현체는 Hibernatem EclipseLink 등이 있고, 스프링 부트에선 보통 Hibernate를 기본 구현체로 사용. - -- N+1 문제란? - - 1번의 쿼리로 N개의 데이터를 가져왔는데, 연관된 데이터를 채우기 위해 추가로 N번의 쿼리가 실행되는 현상을 말함 - - 발생과정 - - 1. 최초쿼리 (1) : `SELECT * FROM Member` 를 실행해 10명의 회원을 가져옴 - 2. 연관 데이터 조회(N) : 즉시 로딩 설정이 되어 있다면, JPA는 각 회원(10명)이 가진 팀(Team) 정보를 채우기 위해 회원별로 팀 조회 쿼리를 각각 날림 - 3. 결과 : 쿼리 1번으로 끝날일이 총 11번(1 + 10) 호출되어 DB 성능이 저하됨 - -- 지연로딩과 즉시로딩의 차이는? - - 지연 로딩 - - - 필요할 때 까지 미룬다 - - 엔티티 조회할 때 연관된 객체는 가져오지 않고, 실제 그 데이터를 사용하는 시점에 DB를 조회함 - - 동작 방식 : 조회 시점에 연관된 객체 자리에 프록시라는 가짜 객체를 넣어둠. 실제로 연관객체의 내부 데이터에 접근할 때 SQL이 실행. - - 어노테이션 : `ManyToOne(fetch=FetchType.LAZY)` - - 단점 : 연관된 데이터가 필요 없는 경우 불필요한 조인이나 쿼리를 줄여 성능 최적화 가능 - - 즉시 로딩 - - - 한번에 다 가져온다 - - 동작 방식 : JPA 구현체는 대개 SQL의 JOIN을 사용하여 한번의 쿼리로 모든 객체를 즉시 함께 조회 - - 어노테이션 : `@ManyToOne(fetch = FetchType.EAGER)` - - 단점 : 연관된 테이블이 많아질수록 쿼리가 복잡해지고, N + 1 문제를 발생시켜 성능 저하의 주범이 됨 - -- JPQL란? - - 엔티티 객체를 대상으로 쿼리하는 객체지향 쿼리 언어(단순히 JPA SQL) - - - 객체지향 쿼리 : 테이블 컬럼명이 아니라 엔티티의 필드(변수)이름을 사용해 쿼리를 작성함 - - 왜 사용할까? - - - JPA의 기본 메서드 (find())는 식별자로 조회할 때 유용하지만 복잡한 검색조건을 처리하기엔 한계가 있음. - - 코드는 자바객체인데 쿼리는 테이블 기준이면 싱크가 깨짐. JPQL은 자바 클래스와 필드 이름을 그대로 쓰기 때문에 훨씬 직관적 - - 이를 해결하기 위해 SQL의 장점을 가져오면서 객체 지향의 이점을 유지하는 JPQL을 사용. - -- Fetch Join란? - - 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 방법 - - 1. 일반 Join VS Fetch Join - - **일반 Join** - - 동작 : SQL 상에서는 조인이 일어나지만, JPQL은 SELECT절에 명시된 엔티티만 조회 - - 결과 : 연관된 엔티티는 프록시가 상태로 남아있어, 나중에 접근할 때 추가쿼리가 발생 - - **Fetch Join** - - 동작 : SQL 조인을 실행하면서 연관된 엔티티의 데이터까지 한번에 SELECT해서 가져옴. - - 결과 : 연관된 엔티티가 프록시가 아닌 실제 엔티티로 채워진 상태로 조회됨. 따라서 추가쿼리 없이 데이터를 바로 사용할 수 있음. - -- @EntityGraph란? - - Fetch 조인을 보완하기 위해 나온 기능. 쿼리 수행 시점에 연관된 엔티티들을 함께 조회하도록 설정하는 어노테이션 - - - 사용 이유 - - JPQL의 Fetch Join은 쿼리문 안에 조인코드를 직접 작성해야 한다는 번거로움이 있음. - - 코드의 간결함 : 스프링 부트 JPA를 사용하면 메서드 위에 어노테이션 하나만 붙여서 N + 1 문제를 해결할 수 있음. - - 쿼리분리 : 똑같은 findAll()이라도 어떤 메서드에서는 연관데이터를 가져오고, 어떤 메서드에서는 안 가져오게끔 유연히 관리 가능 - -- commit과 flush 차이점은? - - Flush (플러시) : 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업 - - 하는일 : 쓰기 지연 SQL 저장소에 쌓여있던 INSERT, UPDATE, DELETE 쿼리를 DB로 보냄 - - 영향 : 쿼리가 DB로 전송되지만, DB 입장에서는 아직 임시 쿼리 상태임. 다른 트랜잭션에선 이 데이터를 못 봄 - - 영속성 컨텍스트 유지 : 플러시가 일어나도 영속성 컨텍스트 내의 엔티티들은 지워지지 않고 그대로 유지. - - 발생시점 - 1. em.flush() 직접 호출 - 2. 트랜잭션 커밋 시 - 3. JPQL 쿼리 실행 시 - - Commit (커밋) : 데이터베이스의 트랜잭션을 종료하고 변경사항을 영구적으로 반영하는 작업 - - 하는일 : 내부적으로 em.flush()를 먼저 호출해 쿼리를 보낸 후 DB에 커밋 명령을 내림 - - 영향 : 데이터가 실제로 DB에 저장되며, 다른 사용자나 시스템에서도 변경된 데이터를 조회할 수 있게됨 - - 트랜잭션 종료 : 커밋이 완료되면 해당 트랜잭션이 완전히 종료. - diff --git a/Gibeom/umc10th/keyword_summary/ch08.md b/Gibeom/umc10th/keyword_summary/ch08.md new file mode 100644 index 00000000..737accb1 --- /dev/null +++ b/Gibeom/umc10th/keyword_summary/ch08.md @@ -0,0 +1,42 @@ +- Spring Security가 무엇인가? + + 스프링 기반 애플리케이션의 보안을 담당하는 강력하고 포괄적인 하위 프레임워크 + + 복잡한 보안 로직을 직접 구현할 필요 없이 표준화된 필터 기반의 설정을 통해 시스템을 안전하게 보호한다. + +- 인증(Authentication)vs 인가(Authorization) + + 비슷해보이지만 서로 다른 개념이다. + + 인증 (Authentication) + + - 본인확인 절차 + - 사용자가 자신이 주장하는 사람이 맞는지 확인하는 과정 + + 인가 (Authorization) + + - 권한확인 절차 + - 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정 + +- Stateful vs Stateless + + 논점 : 서버가 클라이언트의 세션 정보를 기억하는가? + + Stateful(상태유지) : 세션 정보를 기억함 + + Stateless(토큰 기반) : 서버가 상태를 유지하지 않으므로 요청에 포함된 토큰(JWT)로 검증 + + | 구분 | Stateful | Stateless | + | --- | --- | --- | + | 특징 | 서버가 세션 저장소에 로그인 상태 유지 | 서버가 상태를 유지하지 않음, 요청에 포함된 토큰으로 검증 | + | 인증방식 | JSESSION쿠키를 통해 서버 메모리/DB의 세션 조회 | 매 요청시 HTTP헤더에 토큰을 담아서 전송 (Authorization:Bearer) | + | 서버 확장 | 세션 불일치 문제 발생 가능 | 각 요청이 독립적이므로 서버 증설에 유리 | + | 메모리 및 비용 | 동시접속자가 많을수록 서버 세션 메모리 소비 증가 | 토큰 검증 연산이 필요하며, 서버 메모리 사용량은 적음 | + | 주요 활용처 | 전톤적인 웹 애플리케이션 | REST API, 모바일 앱, MSA | + + 서버 확장 방법 + + - Scale-up : 단일 서버 성능 향상 + - Scale-out : 서버의 개수를 늘리기 + - 로드밸런서 : 서버 부하를 분산시키는 H/W, S/W + - 클라이언트와 서버Pool 사이에 위치해 서버의 부하를 분산시키는 하드웨어나 S/W \ No newline at end of file diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 2343770c..941d296e 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.controller; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.enums.MissionStatus; import com.example.umc10th.domain.member.service.MemberService; @@ -7,7 +8,10 @@ import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.entity.AuthMember; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -15,32 +19,37 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/members") +@RequestMapping("/api") public class MemberController { private final MemberService memberService; //마이페이지 - @GetMapping("/me") - public ApiResponse getInfo( + @GetMapping("/v1/members/me") + public ResponseEntity> getInfo( @AuthenticationPrincipal Long memberId ){ BaseSuccessCode code = MemberSuccessCode.OK; - return ApiResponse.onSuccess(code, memberService.getInfo(memberId)); + MemberResDTO.GetInfo result = memberService.getInfo(memberId); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } // 홈화면 - @GetMapping("/home") - public ApiResponse getHome( - @AuthenticationPrincipal Long memberId, + @GetMapping("/v1/members/home") + public ResponseEntity> getHome( + @AuthenticationPrincipal AuthMember authMember, @RequestParam(defaultValue = "0") int page ){ - BaseSuccessCode code = MemberSuccessCode.OK; - return ApiResponse.onSuccess(code, memberService.getHome(memberId, page)); + MemberResDTO.HomeResultDto result = memberService.getHome(authMember.getMember().getId(), page); + return ResponseEntity + .status(MemberSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result)); } // 진행중/완료 미션 목록 조회 - @GetMapping("/missions") - public ApiResponse> getMissionsByStatus( + @GetMapping("/v1/members/missions") + public ResponseEntity>> getMissionsByStatus( @AuthenticationPrincipal Long memberId, @RequestParam MissionStatus status, @RequestParam Integer pageSize, @@ -48,7 +57,21 @@ public ApiResponse> getMissionsByStatus( @RequestParam (required = false) String sort ){ BaseSuccessCode code = MemberSuccessCode.OK; - return ApiResponse.onSuccess(code, memberService.getMissionsByStatus(memberId, status, pageSize, pageNum, sort)); + List result = memberService.getMissionsByStatus(memberId, status, pageSize, pageNum, sort); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } + + //회원가입 + @PostMapping("/auth/sign-up") + public ResponseEntity> signUp( + @RequestBody @Valid MemberReqDTO.SignUp req + ){ + memberService.signUp(req); + return ResponseEntity + .status(MemberSuccessCode.CREATED.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.CREATED, null)); + } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index a7412b8f..e1297d29 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -6,7 +6,11 @@ import com.example.umc10th.domain.member.entity.mapping.MemberMission; import org.springframework.data.domain.Page; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -44,4 +48,16 @@ public static MemberResDTO.HomeMissionDto toHomeMissionDto(MemberMission memberM .status(memberMission.getStatus().name()) .build(); } + + public static Member toMember(MemberReqDTO.SignUp req, String emcodedPasssword){ + return Member.builder() + .name(req.name()) + .password(emcodedPasssword) + .phoneNumber(req.phoneNumber()) + .email(req.email()) + .gender(req.gender()) + .userPoint(0) + .nickname(req.nickname()) + .build(); + } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index 943a141f..047e254a 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -1,5 +1,24 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.enums.Gender; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + public class MemberReqDTO { + public record SignUp ( + @NotBlank + String name, + @NotBlank + String nickname, + @Email @NotBlank + String email, + @NotBlank + String password, + @NotBlank + String phoneNumber, + Gender gender + ){} + } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 1f7fca6b..51df7d92 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -46,6 +46,9 @@ public class Member extends BaseEntity{ @Column(name = "email") private String email; + @Column(name = "password", nullable = false) + private String password; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "region_id") private Region region; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 98c24940..3ffc9cef 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -12,7 +12,15 @@ public enum MemberErrorCode implements BaseErrorCode { "COMMON404_1", "해당 사용자를 찾을 수 없습니다." ), + EMAIL_DUPLICATED(HttpStatus.NOT_FOUND, + "COMMON404_2", + "해당 사용자를 찾을 수 없습니다." + ), + NICKNAME_DUPLICATED(HttpStatus.CONFLICT, + "COMMON409_2", + "이미 존재하는 닉네임입니다." + ) ; private final HttpStatus status; private final String code; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index 5a5c3362..2d76db0b 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -11,7 +11,7 @@ public enum MemberSuccessCode implements BaseSuccessCode { "MEMBER200_1", "성공적으로 유저를 조회했습니다."), - SIGNUP_SUCCESS(HttpStatus.CREATED, + CREATED(HttpStatus.CREATED, "MEMBER_201_1", "회원가입이 성공적으로 완료되었습니다.") ; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java index 87a8a246..8415eda3 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java @@ -13,6 +13,7 @@ public interface MemberMissionRepository extends JpaRepository { List findAllByMember_IdAndStatus(Long memberId, MissionStatus missionStatus); + Page findAllByMember_IdAndStatus(Long memberId, MissionStatus missionStatus, Pageable pageable); Optional findByMember_IdAndMission_Id(Long memberId, Long missionId); @Query("SELECT mm FROM MemberMission mm WHERE mm.member.id = :memberId AND mm.status = :status") diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 054ad34f..9813a157 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -3,5 +3,10 @@ import com.example.umc10th.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { + Boolean existsByEmail(String email); + Optional findByEmail(String email); + Boolean existsByNickname(String nickname); } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 90561356..0d9b76f1 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -17,7 +17,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.security.core.parameters.P; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @@ -28,6 +28,7 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberMissionRepository memberMissionRepository; + private final PasswordEncoder passwordEncoder; public MemberResDTO.GetInfo getInfo(Long memberId) { Member member = memberRepository.findById(memberId) @@ -57,15 +58,26 @@ public List getMissionsByStatus( } else { sortInfo = Sort.by("id").descending(); } - // PageRequest 클래스를 사용해 Pageable 객체를 인스턴스화 - PageRequest pageRequest - = PageRequest.of(pageNum, pageSize, sortInfo); + PageRequest pageRequest = PageRequest.of(pageNum, pageSize, sortInfo); - List memberMissions = memberMissionRepository - .findAllByMember_IdAndStatus(memberId, status); + Page memberMissions = memberMissionRepository + .findAllByMember_IdAndStatus(memberId, status, pageRequest); List missions = memberMissions.stream() .map(MemberMission::getMission) .collect(Collectors.toList()); return MissionConverter.toMissionDtoList(missions); } + + public void signUp(MemberReqDTO.SignUp req) { + //닉네임 혹은 이메일이 이미 존재할 때 + if(memberRepository.existsByEmail(req.email())){ + throw new MemberException(MemberErrorCode.EMAIL_DUPLICATED); + } else if (memberRepository.existsByNickname(req.nickname())){ + throw new MemberException(MemberErrorCode.NICKNAME_DUPLICATED); + } + + String encodedPassword = passwordEncoder.encode(req.password()); + Member member = MemberConverter.toMember(req, encodedPassword); + memberRepository.save(member); + } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 36e7c085..240ba037 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -11,6 +11,9 @@ import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -23,47 +26,59 @@ public class MissionController { private final MissionService missionService; - //리뷰 작성 (완료된 미션에 한해서) @PostMapping("v1/missions/{missionId}/reviews") - public ApiResponse writeReview( + public ResponseEntity> writeReview( @PathVariable Long missionId, @AuthenticationPrincipal Long memberId, @RequestBody ReviewReqDTO.WriteReviewDto dto) { BaseSuccessCode code = ReviewSuccessCode.WRITE_SUCCESS; - return ApiResponse.onSuccess(code, missionService.writeReview(memberId, missionId, dto)); + ReviewResDTO.WriteReviewResultDto result = missionService.writeReview(memberId, missionId, dto); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } + //가게 미션 생성 @PostMapping("v1/stores/{storeId}/missions") - public ApiResponse createMission( + public ResponseEntity> createMission( @PathVariable Long storeId, @RequestBody @Valid MissionReqDTO.CreateMission dto ){ MissionSuccessCode code = MissionSuccessCode.CREATED; - return ApiResponse.onSuccess(code, missionService.createMission(storeId, dto)); + MissionResDTO.CreateMissionResult result = missionService.createMission(storeId, dto); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } - //가게 미션들 조회 - @GetMapping("v1/store/{storeId}/missions") - public ApiResponse> getMissions( + //가게 미션들 조회 (오프셋 기반) + @GetMapping("v1/stores/{storeId}/missions") + public ResponseEntity>> getMissions( @PathVariable Long storeId, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam(required = false) String sort ){ BaseSuccessCode code = MissionSuccessCode.OK; - return ApiResponse.onSuccess(code, missionService.getMissions(storeId, pageSize, pageNumber, sort)); + Page result = missionService.getMissions(storeId, pageSize, pageNumber, sort); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } //내가 진행중인 미션 조회하기 @GetMapping("v1/members/{memberId}/missions") - public ApiResponse> getMemberMissions( + public ResponseEntity>> getMemberMissions( @PathVariable Long memberId, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam(required = false) String sort ){ BaseSuccessCode code = MissionSuccessCode.OK; - return ApiResponse.onSuccess(code, missionService.getMemberMissions(memberId, pageSize, pageNumber, sort)); + MissionResDTO.Pagination result = missionService.getMemberMissions(memberId, pageSize, pageNumber, sort); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.onSuccess(code, result)); } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 938a879a..ec65420a 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -59,4 +59,15 @@ public static MissionResDTO.Pagination toPagination( .pageSize(pageSize) .build(); } + + public static MissionResDTO.CreateMissionResult toCreateMissionResult( + Mission mission + ){ + return MissionResDTO.CreateMissionResult.builder() + .conditional(mission.getConditional()) + .point(mission.getPoint()) + .missionId(mission.getId()) + .deadline(mission.getDeadline()) + .build(); + } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index a9319844..73156036 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.mission.dto; import com.example.umc10th.domain.member.enums.MissionStatus; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; @@ -12,7 +13,7 @@ public record CreateMission( LocalDate deadLine, @NotNull(message = "미션 성공 포인트는 필수입니다.") Integer point, - @NotNull(message = "조건은 빈칸일 수 없습니다.") + @NotBlank(message = "조건은 빈칸일 수 없습니다.") String conditional ){} } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index 0c075bf1..e25087af 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.member.enums.MissionStatus; import lombok.Builder; +import java.time.LocalDate; import java.util.List; public class MissionResDTO { @@ -29,4 +30,13 @@ public record Pagination( Integer pageNumber, Integer pageSize ){} + + //생성된 미션 정보 반환 DTO + @Builder + public record CreateMissionResult( + Long missionId, + String conditional, + Integer point, + LocalDate deadline + ){} } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index 124d440c..d4692a8b 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -69,7 +69,7 @@ public ReviewResDTO.WriteReviewResultDto writeReview(Long memberId, Long mission //가게 미션 생성 @Transactional - public Void createMission( + public MissionResDTO.CreateMissionResult createMission( Long storeId, MissionReqDTO.CreateMission dto ){ @@ -81,11 +81,11 @@ public Void createMission( //미션 DB에 미션 저장 missionRepository.save(mission); - return null; + return MissionConverter.toCreateMissionResult(mission); } //가게 미션 조회 - public MissionResDTO.Pagination getMissions( + public Page getMissions( Long storeId, Integer pageSize, Integer pageNumber, @@ -105,11 +105,7 @@ public MissionResDTO.Pagination getMissions( //가게 내 미션들 조회 Page missionList = missionRepository.findAllByStore_Id(storeId, pageRequest); - return MissionConverter.toPagination( - missionList.map(MissionConverter::toGetMission).toList(), - missionList.getNumber(), - missionList.getSize() - ); + return missionList.map(MissionConverter::toGetMission); } //유저가 진행중인 미션 조회하기 diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 351d0744..e78133cb 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.apache.coyote.Request; import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,14 +23,17 @@ public class ReviewController { //유저의 모든 리뷰 가져오기 (Cursor) @GetMapping("/v1/members/{memberId}/reviews") - public ApiResponse> getMemberReviews( + public ResponseEntity>> getMemberReviews( @PathVariable Long memberId, @RequestParam Integer pageSize, @RequestParam String cursor, @RequestParam String query ) { BaseSuccessCode code = ReviewSuccessCode.OK; - return ApiResponse.onSuccess(code, reviewService.getMemberReviewsOrderById(memberId,pageSize, cursor, query)); + ReviewResDTO.Pagination result = reviewService.getMemberReviewsOrderByScore(memberId, pageSize, cursor, query); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index d9f57f18..782cc7b9 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -14,7 +14,7 @@ public interface ReviewRepository extends JpaRepository { Page findByMember_Id(Long memberId, Pageable pageable); - List member(Member member); + List findByMember(Member member); // id 순 페이징 Slice findReviewsByMember_IdAndIdLessThanOrderByIdDesc(Long memberId, Long idCursor, Pageable pageable); @@ -28,7 +28,8 @@ public interface ReviewRepository extends JpaRepository { "ORDER BY r.score DESC, r.id DESC") Slice findReviewsByScoreCursor( @Param("memberId") Long memberId, - @Param("scoreCursor") int scoreCursor, + @Param("scoreCursor") long scoreCursor, + @Param("idCursor") long idCursor, Pageable pageable ); } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 1e82b29d..f6c18235 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -19,7 +19,7 @@ @RequiredArgsConstructor public class ReviewService { private final ReviewRepository reviewRepository; - + //id 페이지네이션 public ReviewResDTO.Pagination getMemberReviewsOrderById( Long memberId, Integer pageSize, @@ -57,8 +57,11 @@ public ReviewResDTO.Pagination getMemberReviewsOrderById reviewList = reviewRepository.findReviewsByMember_IdOrderByIdDesc(memberId, pageRequest); } - //다음 커서 계산 (마지막 요소의 id) - nextCursor = reviewList.getContent().getLast().getId() + ":" + reviewList.getContent().getLast().getId(); + List content = reviewList.getContent(); + nextCursor = (reviewList.hasNext() && !content.isEmpty()) + ? content.getLast().getId() + ":" + content.getLast().getId() + : null; + // 응답 DTO로 포장하기 return ReviewConverter.toPagination( reviewList.map(ReviewConverter::toGetReview).toList(), @@ -84,12 +87,14 @@ public ReviewResDTO.Pagination getMemberReviewsOrderBySc switch (query.toLowerCase()) { case "score": // 커서 타입 변환 - int scoreCursor = Integer.parseInt(cursorSplit[0]); + long scoreCursor = Long.parseLong(cursorSplit[0]); + long idCursor = Long.parseLong(cursorSplit[1]); //리뷰들 조회 & where절에 커서 값 기입 reviewList = reviewRepository.findReviewsByScoreCursor( memberId, scoreCursor, + idCursor, pageRequest); break; default: @@ -99,8 +104,11 @@ public ReviewResDTO.Pagination getMemberReviewsOrderBySc //커서 없이 조회 reviewList = reviewRepository.findReviewsByMember_IdOrderByScoreDescIdDesc(memberId, pageRequest); } - //다음 커서 계산 - nextCursor = reviewList.getContent().getLast().getId() + ":" + reviewList.getContent().getLast().getId(); + List content = reviewList.getContent(); + nextCursor = (reviewList.hasNext() && !content.isEmpty()) + ? content.getLast().getScore() + ":" + content.getLast().getId() + : null; + //응답 DTO로 포장하기 return ReviewConverter.toPagination( reviewList.map(ReviewConverter::toGetReview).toList(), @@ -109,4 +117,4 @@ public ReviewResDTO.Pagination getMemberReviewsOrderBySc reviewList.getSize() ); } -} +} \ No newline at end of file diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java index 386d4589..f1c27841 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java @@ -6,6 +6,7 @@ import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -20,11 +21,14 @@ public class StoreController { private final StoreService storeService; - @GetMapping("/v1/stores") // [수정] 경로 앞에 / 추가 - public ApiResponse> getStoreList( - @RequestParam Long regionId // [수정] Region 엔티티 → Long + @GetMapping("/v1/stores") + public ResponseEntity>> getStoreList( + @RequestParam Long regionId ){ BaseSuccessCode code = StoreSuccessCode.OK; - return ApiResponse.onSuccess(code, storeService.getStoreList(regionId)); + List result = storeService.getStoreList(regionId); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java index b05e24f2..87c93a9c 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java @@ -10,9 +10,6 @@ public enum GeneralErrorCode implements BaseErrorCode{ BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400_1", "잘못된 요청입니다."), - UNAUTHORTIZED(HttpStatus.UNAUTHORIZED, - "COMMON401_1", - "인증되지 않았습니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403_1", "접근이 금지되었습니다."), @@ -29,6 +26,10 @@ public enum GeneralErrorCode implements BaseErrorCode{ "COMMON500", "서버 내부 오류입니다." ), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "COMMON401_2", + "승인되지 않았습니다." + ) ; private final HttpStatus status; private final String code; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 6b8c1df0..d28d0edf 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,23 +1,70 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.CustomAccessDenied; +import com.example.umc10th.global.security.CustomEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { + private final String[] allowUris = { + //swagger 허용 + // 자유롭게 이용할 수 있는 주소 (비로그인) + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + //로그인 + "/api/auth/**", + }; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() - ); + .authorizeHttpRequests(requests ->requests + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ) + //예외 상황 핸들러 + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDenied()) + .authenticationEntryPoint(customEntryPoint()) + ) + ; return http.build(); } + + //해시 알고리즘을 이용해 함호화된 Bcrypt 알고리즘 객체 반환 + //알고리즘은 실행때마다 매번 다른 결과물 생성 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CustomAccessDenied customAccessDenied() { + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint() { + return new CustomEntryPoint(); + } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java new file mode 100644 index 00000000..0a9e8f40 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java @@ -0,0 +1,29 @@ +package com.example.umc10th.global.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class AuthMember implements UserDetails { + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(); + } + @Override + public String getPassword() { + return member.getPassword(); + } + @Override + public String getUsername() { + return member.getEmail(); + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java new file mode 100644 index 00000000..fc4ad57b --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java @@ -0,0 +1,21 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.FORBIDDEN); + } +} \ No newline at end of file diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java new file mode 100644 index 00000000..60e7713b --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java @@ -0,0 +1,21 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class CustomEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java new file mode 100644 index 00000000..251ca496 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java @@ -0,0 +1,19 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class SecurityResponseUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void writeErrorResponse(HttpServletResponse response, BaseErrorCode code) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + objectMapper.writeValue(response.getOutputStream(), ApiResponse.onFailure(code, null)); + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java new file mode 100644 index 00000000..173be810 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.service; + +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.exception.MemberException; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.global.entity.AuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername( + String username + ) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + return new AuthMember(member); + } +} diff --git a/Gibeom/umc10th/src/main/resources/application.yml b/Gibeom/umc10th/src/main/resources/application.yml index 874078ee..f7681ea6 100644 --- a/Gibeom/umc10th/src/main/resources/application.yml +++ b/Gibeom/umc10th/src/main/resources/application.yml @@ -13,7 +13,7 @@ spring: database-platform: org.hibernate.dialect.MySQLDialect # Hibernate?? ??? MySQL ??(dialect) ?? show-sql: true # ??? SQL ??? ??? ???? ?? ?? hibernate: - ddl-auto: create # ?????? ?? ? ?????? ???? ??? ?? + ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? properties: hibernate: format_sql: true # ???? SQL ??? ?? ?? ??? \ No newline at end of file