diff --git a/build.gradle b/build.gradle index 2ced02b..d5b32e5 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,13 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13' - + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' +// Jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-configuration-processor' implementation 'org.springframework.boot:spring-boot-starter-validation' // QueryDSL : OpenFeign implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" diff --git a/src/main/java/com/example/umc_9th/domain/converter/MissionConverter.java b/src/main/java/com/example/umc_9th/domain/converter/MissionConverter.java deleted file mode 100644 index b9b913d..0000000 --- a/src/main/java/com/example/umc_9th/domain/converter/MissionConverter.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.umc_9th.domain.converter; - -import com.example.umc_9th.domain.mission.dto.res.MissionResponseDTO; -import com.example.umc_9th.domain.mission.entity.Mission; -import com.example.umc_9th.domain.mission.mapping.UserMission; - -public class MissionConverter { - - public static MissionResponseDTO.ChallengeMissionResultDTO toChallengeMissionResultDTO( - UserMission userMission - ) { - Mission mission = userMission.getMission(); - - return MissionResponseDTO.ChallengeMissionResultDTO.builder() - .userMissionId(userMission.getId()) - .missionId(mission.getId()) - .missionTitle(mission.getTitle()) - .missionDescription(mission.getDescription()) - .storeName(mission.getStore().getName()) - .points(mission.getPoints()) - .status(userMission.getStatus()) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/food/entity/FoodCategory.java b/src/main/java/com/example/umc_9th/domain/food/entity/FoodCategory.java index 34d23b2..f2ef092 100644 --- a/src/main/java/com/example/umc_9th/domain/food/entity/FoodCategory.java +++ b/src/main/java/com/example/umc_9th/domain/food/entity/FoodCategory.java @@ -22,14 +22,7 @@ public class FoodCategory extends BaseEntity { private String category; //음식 카테고리 - @OneToOne(fetch = FetchType.LAZY) //Store 테이블과 1:1 관계 매핑 - @JoinColumn(name = "store_id") - private Store store; - - //양방향 고려 -// @OneToMany(fetch = FetchType.LAZY)//멤버별 푸드 카테고리 테이블과 1:N관계매핑 -// @JoinColumn(name="memberFoodCategory_id") -// private ListmemberFoodCategory; + diff --git a/src/main/java/com/example/umc_9th/domain/member/controller/MemberController.java b/src/main/java/com/example/umc_9th/domain/member/controller/MemberController.java index 0b7a3d6..41099ad 100644 --- a/src/main/java/com/example/umc_9th/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/umc_9th/domain/member/controller/MemberController.java @@ -5,15 +5,17 @@ import com.example.umc_9th.domain.member.exception.code.MemberSuccessCode; import com.example.umc_9th.domain.member.service.MemberService; import com.example.umc_9th.grobal.apiPayload.ApiResponse; +import com.example.umc_9th.grobal.auth.enums.Role; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor +@RequestMapping("/api/members") public class MemberController { private final MemberService memberService; @@ -22,7 +24,7 @@ public class MemberController { @PostMapping("/sign-up") public ApiResponse signUp( @RequestBody MemberReqDTO.JoinDTO dto - ) { + ) { return ApiResponse.success(MemberSuccessCode.FOUND, memberService.signup(dto)); } @@ -34,4 +36,8 @@ public ApiResponse login( return ApiResponse.success(MemberSuccessCode.FOUND, memberService.login(dto)); } + + + + } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/member/controller/MemberLogoutController.java b/src/main/java/com/example/umc_9th/domain/member/controller/MemberLogoutController.java new file mode 100644 index 0000000..2591ccf --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/member/controller/MemberLogoutController.java @@ -0,0 +1,42 @@ +package com.example.umc_9th.domain.member.controller; + + +import com.example.umc_9th.domain.member.dto.req.MemberReqDTO; +import com.example.umc_9th.domain.member.dto.res.MemberResDTO; +import com.example.umc_9th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc_9th.domain.member.service.MemberService; +import com.example.umc_9th.grobal.apiPayload.ApiResponse; +import com.example.umc_9th.grobal.auth.enums.Role; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class MemberLogoutController { + + private final MemberService memberService; + + + + @PostMapping("/logout") + @Operation(summary = "로그아웃") + public ApiResponse logout(HttpServletRequest request) { + memberService.logout(); + + // 세션 무효화 + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + + return ApiResponse.onSuccess("로그아웃되었습니다."); + } + + + + +} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/member/converter/MemberConverter.java b/src/main/java/com/example/umc_9th/domain/member/converter/MemberConverter.java index 937ce85..f05d167 100644 --- a/src/main/java/com/example/umc_9th/domain/member/converter/MemberConverter.java +++ b/src/main/java/com/example/umc_9th/domain/member/converter/MemberConverter.java @@ -3,15 +3,20 @@ import com.example.umc_9th.domain.member.dto.req.MemberReqDTO; import com.example.umc_9th.domain.member.dto.res.MemberResDTO; import com.example.umc_9th.domain.member.entity.Member; +import com.example.umc_9th.grobal.auth.enums.Role; +import lombok.Builder; + +import java.time.LocalDateTime; public class MemberConverter { - public static Member toMember(MemberReqDTO.JoinDTO dto) { + public static Member toMember(MemberReqDTO.JoinDTO dto,String password,Role role) { return Member.builder() .email(dto.email()) - .password(dto.password()) + .password(password) .phoneNumber(dto.phoneNumber()) .name(dto.name()) + .role(role) .gender(dto.gender()) .birthDate(dto.birthDate()) .address(dto.address()) @@ -20,12 +25,40 @@ public static Member toMember(MemberReqDTO.JoinDTO dto) { .build(); } +// 실습1 +public static MemberResDTO.LoginDTO toLoginDTO(Member member) { + return MemberResDTO.LoginDTO.builder() + .memberId(member.getMemberId()) + .email(member.getEmail()) + .name(member.getName()) + .role(member.getRole().name()) // ROLE_USER, ROLE_ADMIN + .build(); +} + //실습 2 +// public static MemberResDTO.LoginDTO toLoginDTO(Member member, String accessToken) { +// return MemberResDTO.LoginDTO.builder() +// .memberId(member.getMemberId()) +// .accessToken(accessToken) // JWT 토큰 추가 +// .build(); +// } + +// public static MemberResDTO.LoginDTO toLoginDTO(Member member) { +// return MemberResDTO.LoginDTO.builder() +// .memberId(member.getMemberId()) +// .email(member.getEmail()) +// .name(member.getName()) +// .build(); +// } + public static MemberResDTO.JoinDTO toJoinDTO(Member member) { return MemberResDTO.JoinDTO.builder() .memberId(member.getMemberId()) .createAt(member.getCreatedAt()) .build(); } + + + } diff --git a/src/main/java/com/example/umc_9th/domain/member/dto/req/MemberReqDTO.java b/src/main/java/com/example/umc_9th/domain/member/dto/req/MemberReqDTO.java index 5887eec..36f1db1 100644 --- a/src/main/java/com/example/umc_9th/domain/member/dto/req/MemberReqDTO.java +++ b/src/main/java/com/example/umc_9th/domain/member/dto/req/MemberReqDTO.java @@ -1,6 +1,8 @@ package com.example.umc_9th.domain.member.dto.req; import com.example.umc_9th.domain.member.Gender; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import java.time.LocalDate; import java.util.List; @@ -8,7 +10,9 @@ public class MemberReqDTO { public record JoinDTO( + @Email String email, // 추가 + @NotBlank String password, // 추가 String phoneNumber, String name, @@ -21,7 +25,9 @@ public record JoinDTO( ){} public record LoginDTO( + @NotBlank String email, + @NotBlank String password ){} diff --git a/src/main/java/com/example/umc_9th/domain/member/dto/res/MemberResDTO.java b/src/main/java/com/example/umc_9th/domain/member/dto/res/MemberResDTO.java index d3fc328..b7439a2 100644 --- a/src/main/java/com/example/umc_9th/domain/member/dto/res/MemberResDTO.java +++ b/src/main/java/com/example/umc_9th/domain/member/dto/res/MemberResDTO.java @@ -12,12 +12,23 @@ public record JoinDTO( LocalDateTime createAt ){} - @Builder + //실습1 + @Builder public record LoginDTO( Long memberId, - String accessToken, - String refreshToken, - LocalDateTime loginAt + String name, + String email, + String role ){} + + //실습2 +// @Builder +// public record LoginDTO( +// Long memberId, +// String accessToken +// ){} + + + } diff --git a/src/main/java/com/example/umc_9th/domain/member/entity/Member.java b/src/main/java/com/example/umc_9th/domain/member/entity/Member.java index 0ee2c2a..68e70c8 100644 --- a/src/main/java/com/example/umc_9th/domain/member/entity/Member.java +++ b/src/main/java/com/example/umc_9th/domain/member/entity/Member.java @@ -3,6 +3,7 @@ import com.example.umc_9th.domain.food.entity.MemberFood; import com.example.umc_9th.domain.member.Gender; import com.example.umc_9th.grobal.BaseEntity; +import com.example.umc_9th.grobal.auth.enums.Role; import jakarta.persistence.*; import lombok.*; @@ -55,6 +56,12 @@ public class Member extends BaseEntity { @Column(length = 255) String refreshToken; + @Enumerated(EnumType.STRING) + private Role role; + + + + // 회원의 선호 카테고리 목록 (1:N 관계) @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List memberPreferList = new ArrayList<>(); diff --git a/src/main/java/com/example/umc_9th/domain/member/service/MemberService.java b/src/main/java/com/example/umc_9th/domain/member/service/MemberService.java index aff28ff..b5c4dac 100644 --- a/src/main/java/com/example/umc_9th/domain/member/service/MemberService.java +++ b/src/main/java/com/example/umc_9th/domain/member/service/MemberService.java @@ -12,8 +12,18 @@ import com.example.umc_9th.domain.member.exception.MemberException; import com.example.umc_9th.domain.member.exception.code.MemberErrorCode; import com.example.umc_9th.domain.member.repository.MemberRepository; +import com.example.umc_9th.grobal.auth.CustomUserDetails; +import com.example.umc_9th.grobal.auth.JwtUtil; +import com.example.umc_9th.grobal.auth.enums.Role; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; @@ -27,13 +37,18 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberFoodRepository memberFoodRepository; private final FoodRepository foodRepository; - + private final PasswordEncoder passwordEncoder; + // 인증을 관리하는 중심 컴포넌트 + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; // 회원가입 - public MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto){ + public MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto) { // 멤버 객체 생성 - Member member = MemberConverter.toMember(dto); + String salt = passwordEncoder.encode(dto.password()); + Member member = MemberConverter.toMember(dto, salt, Role.ROLE_USER); + // DB 적용 memberRepository.save(member); // 선호 음식 존재 여부 확인 @@ -52,24 +67,75 @@ public MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto){ return MemberConverter.toJoinDTO(member); } - //로그인 - public MemberResDTO.LoginDTO login(MemberReqDTO.LoginDTO dto){ - Member member =memberRepository.findByEmail(dto.email()) - .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); +// 1. 실습1 - // 임시 토큰 부여 - String acceessToken="임시 엑세스 토큰 "; - String refreshToken="임시 리프레쉬 토큰"; + @Transactional + public MemberResDTO.LoginDTO login(MemberReqDTO.LoginDTO dto) { - // 응답 DTO 생성 - return MemberResDTO.LoginDTO.builder() - .memberId(member.getMemberId()) - .accessToken(acceessToken) - .refreshToken(refreshToken) - .loginAt(LocalDateTime.now()) - .build(); + // 1. Spring Security 인증 객체 생성 + // UsernamePasswordAuthenticationToken 클래스는 implements를 통해 Authentication를 구현한 객체 + // 인증 전 사용자 정보 담은 객체 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken( + dto.email(), + dto.password() + ); + + // 2. 인증 수행 + //AuthenticationManager가 맞는 AuthenticationProvider 찾음 + // 대부적으로 UserDetailsService.loadUserByUsername(email) 호출 + // 커스텀한 CustomUserDetailsService의 loadUserByUsername메서드 실행 DB에서 사용자 정보 조회 + + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 3. SecurityContext에 저장 → 세션에 자동 저장됨 + // 이 시점에 HttpSession에 인증 정보가 저장되고 + // 클라이언트에게 JSESSIONID 쿠키가 발급됨 + //인증된 authentication객체를 SecurityContext에 저장함 -> HttpSession에도 자동 저장 + // 클라이언트에게 JSESSIONID 쿠키가 발급됨 + SecurityContextHolder.getContext().setAuthentication(authentication); + + + // 인증된 authentication 객체에서 사용자 상세정보 뽑아옴 + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + Member member = userDetails.getMember(); + + // 응답 DTO로 변환 + return MemberConverter.toLoginDTO(member); + } + + + public void logout() { + // SecurityContext 초기화 + SecurityContextHolder.clearContext(); } + //실습2 +// public MemberResDTO.LoginDTO login( +// MemberReqDTO.@Valid LoginDTO dto +// ) { +// +// // Member 조회 +// Member member = memberRepository.findByEmail(dto.email()) +// .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); +// +// // 비밀번호 검증 +// if (!passwordEncoder.matches(dto.password(), member.getPassword())) { +// throw new MemberException(MemberErrorCode.NOT_FOUND); +// } +// +// // JWT 토큰 발급용 UserDetails +// CustomUserDetails userDetails = new CustomUserDetails(member); +// // 엑세스 토큰 발급 +// String accessToken = jwtUtil.createAccessToken(userDetails); +// +// // DTO +// return MemberConverter.toLoginDTO(member, accessToken); +// } + + + + } diff --git a/src/main/java/com/example/umc_9th/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc_9th/domain/mission/controller/MissionController.java index 7c310b7..8aaf640 100644 --- a/src/main/java/com/example/umc_9th/domain/mission/controller/MissionController.java +++ b/src/main/java/com/example/umc_9th/domain/mission/controller/MissionController.java @@ -1,22 +1,26 @@ package com.example.umc_9th.domain.mission.controller; -import com.example.umc_9th.domain.converter.MissionConverter; +import com.example.umc_9th.domain.mission.converter.MissionConverter; import com.example.umc_9th.domain.mission.dto.res.MissionResponseDTO; import com.example.umc_9th.domain.mission.mapping.UserMission; +import com.example.umc_9th.domain.mission.service.MissionQueryServiceImpl; import com.example.umc_9th.domain.mission.service.MissionService; -import com.example.umc_9th.grobal.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; +import com.example.umc_9th.domain.store.exception.code.StoreSuccessCode; +import com.example.umc_9th.grobal.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import static com.example.umc_9th.domain.store.exception.code.StoreSuccessCode.STORE_SUCCESS_CODE; + @RestController @RequiredArgsConstructor @RequestMapping("/api/stores") @Tag(name = "Mission", description = "미션 관련 API") -public class MissionController { +public class MissionController implements MissionControllerDocs{ private final MissionService missionService; + private final MissionQueryServiceImpl missionQueryServiceImpl; @PostMapping("/{storeId}/missions/{missionId}/challenge") public ApiResponse challengeMission( @@ -25,8 +29,35 @@ public ApiResponse challengeMissio @RequestParam Long memberId // 추후 JWT에서 추출 ) { UserMission userMission = missionService.challengeMission(storeId, missionId, memberId); - return ApiResponse.onSuccess( + return ApiResponse.success(StoreSuccessCode.STORE_SUCCESS_CODE, MissionConverter.toChallengeMissionResultDTO(userMission) ); } + + + @Override + @GetMapping("/stores/{storeId}/missions") + public ApiResponse getMissions( + @PathVariable Long storeId, + @RequestParam Integer page + ) { + // page - 1 : 클라이언트는 1페이지부터 보내지만, JPA는 0페이지부터 시작하므로 보정 + MissionResponseDTO.MissionListDTO result = missionQueryServiceImpl.getMissions(storeId, page - 1); + + return ApiResponse.success(StoreSuccessCode.STORE_SUCCESS_CODE, result); } + + @Override // 인터페이스 구현 + @GetMapping("/members/{memberId}/missions/challenging") + public ApiResponse getMyMissions( + @PathVariable Long memberId, + @RequestParam Integer page // 검증 로직은 구현체에 붙이는 게 명확합니다. + ) { + // 서비스 호출 (페이지 번호 -1 보정) + MissionResponseDTO.MissionListDTO result = missionQueryServiceImpl.getMyMissions(memberId, page - 1); + + return ApiResponse.success(STORE_SUCCESS_CODE, result); + } + + + } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/mission/controller/MissionControllerDocs.java b/src/main/java/com/example/umc_9th/domain/mission/controller/MissionControllerDocs.java new file mode 100644 index 0000000..cd49411 --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/mission/controller/MissionControllerDocs.java @@ -0,0 +1,26 @@ +package com.example.umc_9th.domain.mission.controller; + +import com.example.umc_9th.domain.mission.dto.res.MissionResponseDTO; +import io.swagger.v3.oas.annotations.Operation; +import com.example.umc_9th.grobal.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +public interface MissionControllerDocs { + @Operation(summary = "미션2. 특정 가게의 미션 목록 조회 ", description = "특정 가게의 미션들을 조회합니다. 페이징을 포함합니다.") + + ApiResponse getMissions( + @Parameter(description = "가게 ID", required = true) @PathVariable Long storeId, + @Parameter(description = "페이지 번호 (1부터 시작)") @RequestParam Integer page + ); + + @Operation(summary = "미션3. 내가 진행중인 미션 조회 ", description = "진행중인 미션을 조회합니다. 페이징을 포함합니다. ") + + ApiResponse getMyMissions( + @Parameter(description = "회원 ID", required = true) @PathVariable Long memberId, + @Parameter(description = "페이지 번호 (1부터 시작)") @RequestParam Integer page + ); + + +} diff --git a/src/main/java/com/example/umc_9th/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/umc_9th/domain/mission/converter/MissionConverter.java new file mode 100644 index 0000000..e79da88 --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/mission/converter/MissionConverter.java @@ -0,0 +1,92 @@ +package com.example.umc_9th.domain.mission.converter; + +import com.example.umc_9th.domain.mission.dto.res.MissionResponseDTO; +import com.example.umc_9th.domain.mission.entity.Mission; +import com.example.umc_9th.domain.mission.mapping.UserMission; +import lombok.Builder; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +@Builder + +public class MissionConverter { + + // Entity -> DTO 변환 + public static MissionResponseDTO.MissionDTO toMissionDTO(Mission mission) { + return MissionResponseDTO.MissionDTO.builder() + .missionId(mission.getId()) + .title(mission.getTitle()) // 제목 매핑 + .description(mission.getDescription()) // 설명 매핑 + .points(mission.getPoints()) // 포인트 매핑 + .verificationCode(mission.getVerificationCode()) // 인증코드 매핑 + .createdAt(mission.getCreatedAt().toLocalDate()) + .build(); + } + + // Page -> ListDTO 변환 + public static MissionResponseDTO.MissionListDTO toMissionListDTO(Page missionPage) { + List missionDTOList = missionPage.stream() + .map(MissionConverter::toMissionDTO) + .collect(Collectors.toList()); + + return MissionResponseDTO.MissionListDTO.builder() + .missionList(missionDTOList) + .listSize(missionDTOList.size()) + .totalPage(missionPage.getTotalPages()) + .totalElements(missionPage.getTotalElements()) + .isFirst(missionPage.isFirst()) + .isLast(missionPage.isLast()) + .build(); + } + + // UserMission(매핑 엔티티) -> ChallengeMissionResultDTO(응답 DTO) 변환 + public static MissionResponseDTO.ChallengeMissionResultDTO toChallengeMissionResultDTO(UserMission userMission) { + + return MissionResponseDTO.ChallengeMissionResultDTO.builder() + .userMissionId(userMission.getId()) + .missionId(userMission.getMission().getId()) + .missionTitle(userMission.getMission().getTitle()) + .missionDescription(userMission.getMission().getDescription()) + .storeName(userMission.getMission().getStore().getName()) + .points(userMission.getMission().getPoints()) + .status(true) + .createdAt(userMission.getCompletedAt()) + .build(); + } + + + + public static MissionResponseDTO.MyMissionDTO toMyMissionDTO(UserMission userMission) { + return MissionResponseDTO.MyMissionDTO.builder() + .userMissionId(userMission.getId()) + .storeName(userMission.getMission().getStore().getName()) + .missionTitle(userMission.getMission().getTitle()) + .point(userMission.getMission().getPoints()) + .createdAt(userMission.getCompletedAt()) + .build(); + } + + + public static MissionResponseDTO.MissionListDTO toMyMissionListDTO(Page page) { + List myMissionDTOList = page.stream() + .map(MissionConverter::toMyMissionDTO) + .collect(Collectors.toList()); + + return MissionResponseDTO.MissionListDTO.builder() + // 여기서는 제네릭을 쓰거나 MyMissionDTO 리스트 필드를 DTO에 추가해야 하지만, + // 편의상 기존 구조에 맞춘다면 아래와 같이 변환하거나 새로 DTO를 파는게 좋습니다. + // (지금은 MissionListDTO가 MissionDTO 리스트만 받게 되어 있어, MyMissionListDTO를 따로 만드는 것을 추천합니다.) + // 일단 로직 흐름만 보여드립니다: + .listSize(myMissionDTOList.size()) + .totalPage(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .isFirst(page.isFirst()) + .isLast(page.isLast()) + .build(); + } + + + +} diff --git a/src/main/java/com/example/umc_9th/domain/mission/dto/res/MissionResponseDTO.java b/src/main/java/com/example/umc_9th/domain/mission/dto/res/MissionResponseDTO.java index cc57d5a..57a3a80 100644 --- a/src/main/java/com/example/umc_9th/domain/mission/dto/res/MissionResponseDTO.java +++ b/src/main/java/com/example/umc_9th/domain/mission/dto/res/MissionResponseDTO.java @@ -1,10 +1,16 @@ package com.example.umc_9th.domain.mission.dto.res; +import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; public class MissionResponseDTO { + @Builder public record ChallengeMissionResultDTO( Long userMissionId, @@ -13,7 +19,50 @@ public record ChallengeMissionResultDTO( String missionDescription, String storeName, Integer points, - Boolean status, // true=진행중, false=완료 + Boolean status, LocalDateTime createdAt ) {} -} + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MissionDTO { + private Long missionId; + private String title; + private String description; + private Integer points; + private String verificationCode; + private LocalDate createdAt; + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MissionListDTO { + private List missionList; + private Integer listSize; + private Integer totalPage; + private Long totalElements; + private Boolean isFirst; + private Boolean isLast; + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MyMissionDTO { + private Long userMissionId; + private String storeName; + private String missionTitle; + private Integer point; + private LocalDateTime createdAt; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/mission/repository/MissionRepository.java b/src/main/java/com/example/umc_9th/domain/mission/repository/MissionRepository.java index 594016e..8bfbcaa 100644 --- a/src/main/java/com/example/umc_9th/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/example/umc_9th/domain/mission/repository/MissionRepository.java @@ -1,8 +1,11 @@ package com.example.umc_9th.domain.mission.repository; import com.example.umc_9th.domain.mission.entity.Mission; +import com.example.umc_9th.domain.store.entity.Store; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface MissionRepository extends JpaRepository { - + Page findAllByStore(Store store, Pageable pageable); } diff --git a/src/main/java/com/example/umc_9th/domain/mission/repository/UserMissionRepository.java b/src/main/java/com/example/umc_9th/domain/mission/repository/UserMissionRepository.java index 182665b..bc26f44 100644 --- a/src/main/java/com/example/umc_9th/domain/mission/repository/UserMissionRepository.java +++ b/src/main/java/com/example/umc_9th/domain/mission/repository/UserMissionRepository.java @@ -5,6 +5,7 @@ import com.example.umc_9th.domain.mission.mapping.UserMission; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -31,4 +32,7 @@ public interface UserMissionRepository extends JpaRepository // 회원의 진행 중인 미션 목록 List findByMemberAndStatus(Member member, Boolean status); + @EntityGraph(attributePaths = {"mission", "mission.store"}) + Page findAllByMemberAndStatus(Member member, Boolean status, Pageable pageable); + } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/mission/service/MissionQueryServiceImpl.java b/src/main/java/com/example/umc_9th/domain/mission/service/MissionQueryServiceImpl.java new file mode 100644 index 0000000..580f507 --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/mission/service/MissionQueryServiceImpl.java @@ -0,0 +1,79 @@ +package com.example.umc_9th.domain.mission.service; + +import com.example.umc_9th.domain.member.entity.Member; +import com.example.umc_9th.domain.member.exception.MemberException; +import com.example.umc_9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc_9th.domain.member.repository.MemberRepository; +import com.example.umc_9th.domain.mission.converter.MissionConverter; +import com.example.umc_9th.domain.mission.dto.res.MissionResponseDTO; +import com.example.umc_9th.domain.mission.entity.Mission; +import com.example.umc_9th.domain.mission.mapping.UserMission; +import com.example.umc_9th.domain.mission.repository.MissionRepository; +import com.example.umc_9th.domain.mission.repository.UserMissionRepository; +import com.example.umc_9th.domain.store.entity.Store; +import com.example.umc_9th.domain.store.exception.StoreException; +import com.example.umc_9th.domain.store.exception.code.StoreErrorCode; +import com.example.umc_9th.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MissionQueryServiceImpl { + private final StoreRepository storeRepository; + private final MissionRepository missionRepository; + private final MemberRepository memberRepository; + private final UserMissionRepository userMissionRepository; + + + public MissionResponseDTO.MissionListDTO getMissions(Long storeId, Integer page) { + + // 0. 페이지 파라미터 검증 + if (page == null || page < 0) { + page = 0; + } + + // 1. 가게 존재 확인 + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + // 2. 미션 목록 조회 + PageRequest pageRequest = PageRequest.of(page, 10); + Page missionPage = missionRepository.findAllByStore(store, pageRequest); + + // 3. DTO 변환 + return MissionConverter.toMissionListDTO(missionPage); + } + + + + public MissionResponseDTO.MissionListDTO getMyMissions(Long memberId, Integer page) { + + // 0. 페이지 파라미터 검증 + if (page == null || page < 0) { + page = 0; + } + + // 1. 멤버 확인 + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + // 2. 진행 중인(true) 미션 목록 조회 + PageRequest pageRequest = PageRequest.of(page, 10); + + Page userMissionPage = userMissionRepository.findAllByMemberAndStatus( + member, + true, + pageRequest + ); + + // 3. 변환 및 반환 + return MissionConverter.toMyMissionListDTO(userMissionPage); + } + + +} diff --git a/src/main/java/com/example/umc_9th/domain/mission/service/MissionService.java b/src/main/java/com/example/umc_9th/domain/mission/service/MissionService.java index d1c4304..2b4e07d 100644 --- a/src/main/java/com/example/umc_9th/domain/mission/service/MissionService.java +++ b/src/main/java/com/example/umc_9th/domain/mission/service/MissionService.java @@ -55,4 +55,8 @@ public UserMission challengeMission(Long storeId, Long missionId, Long memberId) return userMissionRepository.save(userMission); } + + + + } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/review/controller/Review7thController.java b/src/main/java/com/example/umc_9th/domain/review/controller/Review7thController.java index 4b08d13..14e473b 100644 --- a/src/main/java/com/example/umc_9th/domain/review/controller/Review7thController.java +++ b/src/main/java/com/example/umc_9th/domain/review/controller/Review7thController.java @@ -3,6 +3,7 @@ import com.example.umc_9th.domain.review.converter.ReviewConverter; import com.example.umc_9th.domain.review.dto.req.ReviewRequestDTO.CreateReviewDTO; import com.example.umc_9th.domain.review.dto.res.ReviewResponseDTO.CreateReviewResultDTO; +import com.example.umc_9th.domain.review.entity.Review; import com.example.umc_9th.domain.review.service.ReviewService; import com.example.umc_9th.grobal.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -26,7 +27,7 @@ public ApiResponse createReview( @RequestParam Long memberId, @Valid @RequestBody CreateReviewDTO request ) { - var review = reviewService.createReview(storeId, memberId, request); + Review review = reviewService.createReview(storeId, memberId, request); return ApiResponse.onSuccess( ReviewConverter.toCreateReviewResultDTO(review, request.getImageUrls()) ); diff --git a/src/main/java/com/example/umc_9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc_9th/domain/review/controller/ReviewController.java index b96a7c2..bbeacc1 100644 --- a/src/main/java/com/example/umc_9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc_9th/domain/review/controller/ReviewController.java @@ -1,7 +1,9 @@ package com.example.umc_9th.domain.review.controller; import com.example.umc_9th.domain.review.dto.res.ReviewResponseDTO; -import com.example.umc_9th.domain.review.service.ReviewQueryService; +import com.example.umc_9th.domain.review.entity.Review; +import com.example.umc_9th.domain.review.exception.code.ReviewSuccessCode; +import com.example.umc_9th.domain.review.service.query.ReviewQueryService; import com.example.umc_9th.grobal.apiPayload.ApiResponse; import com.example.umc_9th.grobal.apiPayload.code.GeneralSuccessCode; import io.swagger.v3.oas.annotations.Operation; @@ -18,27 +20,48 @@ @RestController @RequiredArgsConstructor @Tag(name = "Review", description = "리뷰 관련 API") -public class ReviewController { +public class ReviewController implements ReviewControllerDocs{ private final ReviewQueryService reviewQueryService; + @GetMapping("/reviews") + public ApiResponse getReviews( + @RequestParam String storeName, + @RequestParam Integer page + ){ + ReviewSuccessCode code=ReviewSuccessCode.FOUND; + + return ApiResponse.success(code,null); + } + +// @GetMapping("/reviews/search") +// public ListsearchReview(@RequestParam String filter, @RequestParam String type)throws Exception{ +// // 서비스 요청 +// //Listresult=reviewQueryService.searchReview(filter,type); +// //return result; +// +// } + +// 1. 내가 작성한 리뷰 목록 + @Override @GetMapping("/members/{memberId}/reviews/search") - @Operation( - summary = "내 리뷰 조회", - description = "회원이 작성한 리뷰를 조회합니다. storeId와 rating으로 필터링할 수 있습니다." - ) public ApiResponse> searchMyReview( - @Parameter(description = "회원 ID", required = true) + @Parameter( required = true) @PathVariable Long memberId, - @Parameter(description = "가게 ID (선택)") + @Parameter() @RequestParam(required = false) Long storeId, - @Parameter(description = "별점 (1-5, 선택)") - @RequestParam(required = false) Integer rating + @Parameter() + @RequestParam(required = false) Integer rating, + + @Parameter() + @RequestParam(required = false)Integer page + ) { - List reviewList = reviewQueryService.searchMyReviews(memberId, storeId, rating); + List reviewList = reviewQueryService.searchMyReviews(memberId, storeId, rating,page); return ApiResponse.success(GeneralSuccessCode.REVIEWS_FOUND, reviewList); } + } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/review/controller/ReviewControllerDocs.java b/src/main/java/com/example/umc_9th/domain/review/controller/ReviewControllerDocs.java new file mode 100644 index 0000000..b5ecbf6 --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/review/controller/ReviewControllerDocs.java @@ -0,0 +1,26 @@ +package com.example.umc_9th.domain.review.controller; + +import com.example.umc_9th.domain.review.dto.res.ReviewResponseDTO; +import io.swagger.v3.oas.annotations.Operation; +import com.example.umc_9th.grobal.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +public interface ReviewControllerDocs { + + + + @Operation( + summary = "미션1 내 리뷰 조회", + description = "회원이 작성한 리뷰를 조회합니다. storeId와 rating으로 필터링할 수 있습니다." + ) + ApiResponse> searchMyReview( + @Parameter(description = "회원 ID", required = true) Long memberId, + @Parameter(description = "가게 ID (선택)") Long storeId, + @Parameter(description = "별점 (1-5, 선택)") Integer rating, + @RequestParam(required = false)Integer page + ); + +} diff --git a/src/main/java/com/example/umc_9th/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc_9th/domain/review/converter/ReviewConverter.java index b8a84d9..35ff290 100644 --- a/src/main/java/com/example/umc_9th/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/umc_9th/domain/review/converter/ReviewConverter.java @@ -6,7 +6,10 @@ import com.example.umc_9th.domain.review.entity.Review; import com.example.umc_9th.domain.review.entity.ReviewImage; import com.example.umc_9th.domain.store.entity.Store; +import org.springframework.data.domain.Page; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -68,4 +71,36 @@ public static ReviewResponseDTO.CreateReviewResultDTO toCreateReviewResultDTO(Re .createdAt(review.getCreatedAt()) .build(); } + // 페이지 사용 컨버터 + // + public static ReviewResponseDTO.ReviewPreViewListDTO toReviewPreviewListDTO( + Page result + ){ + return ReviewResponseDTO.ReviewPreViewListDTO.builder() + .reviewList(result.getContent().stream() + .map(ReviewConverter::toReviewPreviewDTO) + .toList() + ) + .listSize(result.getSize()) + .totalPage(result.getTotalPages()) + .totalElements(result.getTotalElements()) + .isFirst(result.isFirst()) + .isLast(result.isLast()) + .build(); + } + + //getMember 불러올 수 있는 이유? + //JPA가 테이블의 연관관계 파악하고 연관성 있음 불러올 수 있는 기능 제공 + public static ReviewResponseDTO.ReviewPreViewDTO toReviewPreviewDTO( + Review review + ){ + return ReviewResponseDTO.ReviewPreViewDTO.builder() + .ownerNickname(review.getMember().getName()) + .score(review.getRating()) + .body(review.getContent()) + .createdAt(LocalDateTime.from(review.getCreatedAt())) + .build(); + } + + } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/review/dto/res/ReviewResponseDTO.java b/src/main/java/com/example/umc_9th/domain/review/dto/res/ReviewResponseDTO.java index a6559e7..289f7a4 100644 --- a/src/main/java/com/example/umc_9th/domain/review/dto/res/ReviewResponseDTO.java +++ b/src/main/java/com/example/umc_9th/domain/review/dto/res/ReviewResponseDTO.java @@ -44,8 +44,6 @@ public static class CreateReviewResultDTO { private LocalDateTime createdAt; } - - @Getter @Builder @NoArgsConstructor @@ -71,5 +69,28 @@ public static class ReviewDTO { @Schema(description = "작성일시") private LocalDateTime createdAt; } + // 워크북 Chapter9 + // recordㄴ나 static class 로 두어서 DTO 정리 + @Builder + public record ReviewPreViewListDTO( + List reviewList, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ){} + + @Builder + public record ReviewPreViewDTO( + // 주인이름 + String ownerNickname, + //평점 + int score, + //리뷰내용 + String body, + //생성 일자 + LocalDateTime createdAt + ){} } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/review/exception/code/ReviewErrorCode.java b/src/main/java/com/example/umc_9th/domain/review/exception/code/ReviewErrorCode.java index 4f36085..68beef3 100644 --- a/src/main/java/com/example/umc_9th/domain/review/exception/code/ReviewErrorCode.java +++ b/src/main/java/com/example/umc_9th/domain/review/exception/code/ReviewErrorCode.java @@ -11,9 +11,10 @@ public enum ReviewErrorCode implements BaseErrorCode { // 리뷰 검색 관련 에러 // 회원 에러 - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 ID의 회원을 찾을 수 없습니다."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 리뷰를 찾을 수 없습니다."), // 평점 유효성 - INVALID_RATING_VALUE(HttpStatus.BAD_REQUEST, "REVIEW400_1", "평점 값은 1에서 5 사이여야 합니다."); + INVALID_RATING_VALUE(HttpStatus.BAD_REQUEST, "REVIEW400_1", "평점 값은 1에서 5 사이여야 합니다."), + INVALID_PAGE_VALUE(HttpStatus.BAD_REQUEST, "PAGE400_1", "페이지 번호는 1 이상이어야 합니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/umc_9th/domain/review/exception/code/ReviewSuccessCode.java b/src/main/java/com/example/umc_9th/domain/review/exception/code/ReviewSuccessCode.java new file mode 100644 index 0000000..6bc77a1 --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/review/exception/code/ReviewSuccessCode.java @@ -0,0 +1,37 @@ +package com.example.umc_9th.domain.review.exception.code; +import com.example.umc_9th.grobal.apiPayload.code.BaseSuccessCode; +import com.example.umc_9th.grobal.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReviewSuccessCode implements BaseSuccessCode { + + REVIEW_REGISTER_SUCCESS(HttpStatus.OK, "REVIEW200", "리뷰 등록에 성공했습니다."), + REVIEW_UPDATE_SUCCESS(HttpStatus.OK, "REVIEW200", "리뷰 수정에 성공했습니다."), + REVIEW_DELETE_SUCCESS(HttpStatus.OK, "REVIEW200", "리뷰 삭제에 성공했습니다."), + FOUND(HttpStatus.OK, "REVIEW200", "리뷰 조회에 성공했습니다."); + + private final HttpStatus status; + private final String code; + private final String message; + + + @Override + public HttpStatus getStatus() { + return this.status; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getMessage() { + return this.message; + } + +} diff --git a/src/main/java/com/example/umc_9th/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/umc_9th/domain/review/repository/ReviewRepository.java index fd30ad1..5fed278 100644 --- a/src/main/java/com/example/umc_9th/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/umc_9th/domain/review/repository/ReviewRepository.java @@ -1,8 +1,15 @@ package com.example.umc_9th.domain.review.repository; import com.example.umc_9th.domain.member.entity.Member; +import com.example.umc_9th.domain.review.converter.ReviewConverter; +import com.example.umc_9th.domain.review.dto.res.ReviewResponseDTO; import com.example.umc_9th.domain.review.entity.Review; import com.example.umc_9th.domain.store.entity.Store; +import com.example.umc_9th.domain.store.exception.StoreException; +import com.example.umc_9th.domain.store.exception.code.StoreErrorCode; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,6 +18,7 @@ public interface ReviewRepository extends JpaRepository,ReviewQueryDsl { + //2. 리뷰 작성하는 쿼리, //* 사진의 경우는 일단 배제 //(메서드 생성 방식 권장) @@ -21,7 +29,8 @@ public interface ReviewRepository extends JpaRepository,ReviewQuer // Member와 Store를 기준으로 Review 조회 boolean existsByMemberAndStore(Member member, Store store); - + //가게별 조회 + Page findAllByStore(Store store, Pageable pageable); diff --git a/src/main/java/com/example/umc_9th/domain/review/service/ReviewService.java b/src/main/java/com/example/umc_9th/domain/review/service/ReviewService.java index d8aaaf3..f53324d 100644 --- a/src/main/java/com/example/umc_9th/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/umc_9th/domain/review/service/ReviewService.java @@ -1,8 +1,13 @@ package com.example.umc_9th.domain.review.service; import com.example.umc_9th.domain.review.dto.req.ReviewRequestDTO; +import com.example.umc_9th.domain.review.dto.res.ReviewResponseDTO; import com.example.umc_9th.domain.review.entity.Review; public interface ReviewService { Review createReview(Long storeId, Long memberId, ReviewRequestDTO.CreateReviewDTO request); + public ReviewResponseDTO.ReviewPreViewListDTO findReview( + String storeName, + Integer page + ); } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/example/umc_9th/domain/review/service/ReviewServiceImpl.java index 6573fe1..37af48e 100644 --- a/src/main/java/com/example/umc_9th/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/example/umc_9th/domain/review/service/ReviewServiceImpl.java @@ -4,13 +4,18 @@ import com.example.umc_9th.domain.member.repository.MemberRepository; import com.example.umc_9th.domain.review.converter.ReviewConverter; import com.example.umc_9th.domain.review.dto.req.ReviewRequestDTO; +import com.example.umc_9th.domain.review.dto.res.ReviewResponseDTO; import com.example.umc_9th.domain.review.entity.Review; import com.example.umc_9th.domain.review.entity.ReviewImage; import com.example.umc_9th.domain.review.repository.ReviewImageRepository; import com.example.umc_9th.domain.review.repository.ReviewRepository; import com.example.umc_9th.domain.store.entity.Store; +import com.example.umc_9th.domain.store.exception.StoreException; +import com.example.umc_9th.domain.store.exception.code.StoreErrorCode; import com.example.umc_9th.domain.store.repository.StoreRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -49,4 +54,23 @@ public Review createReview(Long storeId, Long memberId, ReviewRequestDTO.CreateR return savedReview; } + + @Override + public ReviewResponseDTO.ReviewPreViewListDTO findReview( + String storeName, + Integer page + ){ + // - 가게를 가져온다 (가게 존재 여부 검증) + Store store = storeRepository.findByName(storeName) + // - 없으면 예외 터뜨린다 + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + //- 가게에 맞는 리뷰를 가져온다 (Offset 페이징) + PageRequest pageRequest = PageRequest.of(page, 5); + Page result = reviewRepository.findAllByStore(store, pageRequest); + + //- 결과를 응답 DTO로 변환한다 (컨버터 이용) + return ReviewConverter.toReviewPreviewListDTO(result); + } + } diff --git a/src/main/java/com/example/umc_9th/domain/review/service/ReviewQueryService.java b/src/main/java/com/example/umc_9th/domain/review/service/query/ReviewQueryService.java similarity index 69% rename from src/main/java/com/example/umc_9th/domain/review/service/ReviewQueryService.java rename to src/main/java/com/example/umc_9th/domain/review/service/query/ReviewQueryService.java index 108ebf7..71d67d5 100644 --- a/src/main/java/com/example/umc_9th/domain/review/service/ReviewQueryService.java +++ b/src/main/java/com/example/umc_9th/domain/review/service/query/ReviewQueryService.java @@ -1,4 +1,4 @@ -package com.example.umc_9th.domain.review.service; +package com.example.umc_9th.domain.review.service.query; import com.example.umc_9th.domain.member.entity.Member; import com.example.umc_9th.domain.member.repository.MemberRepository; @@ -8,7 +8,11 @@ import com.example.umc_9th.domain.review.exception.ReviewException; import com.example.umc_9th.domain.review.exception.code.ReviewErrorCode; import com.example.umc_9th.domain.review.repository.ReviewRepository; +import com.example.umc_9th.domain.store.entity.Store; +import com.example.umc_9th.domain.store.repository.StoreRepository; + import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,8 +25,14 @@ public class ReviewQueryService { private final ReviewRepository reviewRepository; private final MemberRepository memberRepository; + private final StoreRepository storeRepository; + + public List searchMyReviews(Long memberId, Long storeId, Integer rating,Integer page) { - public List searchMyReviews(Long memberId, Long storeId, Integer rating) { + // 페이지 파라미터 검증 + if (page == null || page < 0) { + page = 0; + } // 회원 유효성 검사 및 조회 Member member = memberRepository.findById(memberId) @@ -33,6 +43,9 @@ public List searchMyReviews(Long memberId, Long sto throw new ReviewException(ReviewErrorCode.INVALID_RATING_VALUE); } + // TODO: 페이징 처리 개선 필요 - Repository에 적절한 메서드 추가 후 PageRequest 사용 + // PageRequest pageRequest = PageRequest.of(page, 5); + // 모든 리뷰 조회 후 필터링 (기존 Repository 메서드 활용) List reviewList = reviewRepository.findAll(); @@ -43,4 +56,11 @@ public List searchMyReviews(Long memberId, Long sto return ReviewConverter.toReviewDTOList(reviewList); } + +// //리뷰 조회 로직 +// ListsearchReview(String filter, String type)throws ReviewException { +// +// } +// + } \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/store/entity/Store.java b/src/main/java/com/example/umc_9th/domain/store/entity/Store.java index c538f29..a947029 100644 --- a/src/main/java/com/example/umc_9th/domain/store/entity/Store.java +++ b/src/main/java/com/example/umc_9th/domain/store/entity/Store.java @@ -21,14 +21,16 @@ public class Store extends BaseEntity { @Column(nullable = false, length = 50) private String name; //가게 이름 + @Column(nullable = false) + private Integer score; // 가게 평점 //양방향 고려 // @OneToMany(fetch = FetchType.LAZY)//미션 테이블과 1:N관계매핑 // @JoinColumn(name="mission_id") // private List missions; - @OneToOne(fetch = FetchType.LAZY) //FoodCategory 테이블과 1:1 관계 매핑 - @JoinColumn(name = "foodCategory_id") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "food_category_id") private FoodCategory foodCategory; @ManyToOne(fetch = FetchType.LAZY)//지역 테이블과 N:1관계매핑 diff --git a/src/main/java/com/example/umc_9th/domain/store/exception/StoreException.java b/src/main/java/com/example/umc_9th/domain/store/exception/StoreException.java new file mode 100644 index 0000000..8c81f9d --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/store/exception/StoreException.java @@ -0,0 +1,10 @@ +package com.example.umc_9th.domain.store.exception; + +import com.example.umc_9th.grobal.apiPayload.code.BaseErrorCode; +import com.example.umc_9th.grobal.apiPayload.exception.GeneralException; + +public class StoreException extends GeneralException { + public StoreException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc_9th/domain/store/exception/code/StoreErrorCode.java b/src/main/java/com/example/umc_9th/domain/store/exception/code/StoreErrorCode.java new file mode 100644 index 0000000..b6479b4 --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/store/exception/code/StoreErrorCode.java @@ -0,0 +1,36 @@ +package com.example.umc_9th.domain.store.exception.code; + +import com.example.umc_9th.grobal.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum StoreErrorCode implements BaseErrorCode { + + // 가게 검색 관련 에러 + // 회원 에러 + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404_1", "해당 가게를 찾을 수 없습니다."); + + private final HttpStatus status; + private final String code; + private final String message; + + + @Override + public HttpStatus getStatus() { + return this.status; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getMessage() { + return this.message; + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/domain/store/exception/code/StoreSuccessCode.java b/src/main/java/com/example/umc_9th/domain/store/exception/code/StoreSuccessCode.java new file mode 100644 index 0000000..7900830 --- /dev/null +++ b/src/main/java/com/example/umc_9th/domain/store/exception/code/StoreSuccessCode.java @@ -0,0 +1,33 @@ +package com.example.umc_9th.domain.store.exception.code; +import com.example.umc_9th.grobal.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum StoreSuccessCode implements BaseSuccessCode { + + STORE_SUCCESS_CODE(HttpStatus.OK, "STORE200", "가게 조회에 성공했습니다."); + + private final HttpStatus status; + private final String code; + private final String message; + + + @Override + public HttpStatus getStatus() { + return this.status; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getMessage() { + return this.message; + } + +} diff --git a/src/main/java/com/example/umc_9th/domain/store/repository/StoreRepository.java b/src/main/java/com/example/umc_9th/domain/store/repository/StoreRepository.java index f1fc875..b29ad96 100644 --- a/src/main/java/com/example/umc_9th/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/example/umc_9th/domain/store/repository/StoreRepository.java @@ -4,6 +4,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface StoreRepository extends JpaRepository { + Optional findByName(String name); } diff --git a/src/main/java/com/example/umc_9th/grobal/ApiResponse.java b/src/main/java/com/example/umc_9th/grobal/ApiResponse.java index 0db0b4e..d2c521b 100644 --- a/src/main/java/com/example/umc_9th/grobal/ApiResponse.java +++ b/src/main/java/com/example/umc_9th/grobal/ApiResponse.java @@ -27,7 +27,7 @@ public class ApiResponse { // 성공한 경우 응답 생성 public static ApiResponse onSuccess(T result) { - return new ApiResponse<>(true, "2000", "성공하였습니다.", result); + return new ApiResponse<>(true, "200", "성공하였습니다.", result); } // 실패한 경우 응답 생성 diff --git a/src/main/java/com/example/umc_9th/grobal/apiPayload/ApiResponse.java b/src/main/java/com/example/umc_9th/grobal/apiPayload/ApiResponse.java index c6360f2..50a84e8 100644 --- a/src/main/java/com/example/umc_9th/grobal/apiPayload/ApiResponse.java +++ b/src/main/java/com/example/umc_9th/grobal/apiPayload/ApiResponse.java @@ -35,4 +35,8 @@ public static ApiResponse success(BaseSuccessCode code, T result) { public static ApiResponse onFailure(BaseErrorCode code, T result) { return new ApiResponse<>(false, code.getCode(), code.getMessage(), result); } + + public static ApiResponse onSuccess(T result) { + return new ApiResponse<>(true, "COMMON200", "성공입니다.", result); + } } diff --git a/src/main/java/com/example/umc_9th/grobal/auth/AuthenticationEntryPointImpl.java b/src/main/java/com/example/umc_9th/grobal/auth/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..de6b55e --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/auth/AuthenticationEntryPointImpl.java @@ -0,0 +1,33 @@ +package com.example.umc_9th.grobal.auth; + +import com.example.umc_9th.grobal.apiPayload.ApiResponse; +import com.example.umc_9th.grobal.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +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 AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper = new ObjectMapper(); + + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/grobal/auth/CustomUserDetails.java b/src/main/java/com/example/umc_9th/grobal/auth/CustomUserDetails.java new file mode 100644 index 0000000..95da827 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/auth/CustomUserDetails.java @@ -0,0 +1,32 @@ +package com.example.umc_9th.grobal.auth; + +import com.example.umc_9th.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; + +@RequiredArgsConstructor +@Getter +public class CustomUserDetails implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(() -> member.getRole().toString()); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} diff --git a/src/main/java/com/example/umc_9th/grobal/auth/CustomUserDetailsService.java b/src/main/java/com/example/umc_9th/grobal/auth/CustomUserDetailsService.java new file mode 100644 index 0000000..4711749 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/auth/CustomUserDetailsService.java @@ -0,0 +1,33 @@ +package com.example.umc_9th.grobal.auth; + +import com.example.umc_9th.domain.member.entity.Member; +import com.example.umc_9th.domain.member.exception.MemberException; +import com.example.umc_9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc_9th.domain.member.repository.MemberRepository; +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; + + //생성한 UserDetails 검증 + // Authentication authentication = authenticationManager.authenticate(authenticationToken); + // 이 한 줄 호출 딜 때 CustomUserDetailsService에서 loadUserByUsername 호출해서 사용자 검색 + // 인증 성공 시 CustomUserDetails 반환 인자값으로 Member 객체 담아서 + @Override + public UserDetails loadUserByUsername( + String username // 이메일 + ) throws UsernameNotFoundException { + // 검증할 Member 조회 + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + // CustomUserDetails 반환 + return new CustomUserDetails(member); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/grobal/auth/JwtAuthFilter.java b/src/main/java/com/example/umc_9th/grobal/auth/JwtAuthFilter.java new file mode 100644 index 0000000..08e6002 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/auth/JwtAuthFilter.java @@ -0,0 +1,79 @@ +package com.example.umc_9th.grobal.auth; + +import com.example.umc_9th.grobal.apiPayload.ApiResponse; +import com.example.umc_9th.grobal.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + // doFilterInternal 메서드를 실행 + // 원래 요청을 request, 응답할 객체를 response에 넣는다 + // 이 필터를 호출한 FilterChain을 filterChain에 넣는다 + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + try { + // 토큰 가져오기 + // 이 헤더를 읽어서 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출, // Bearer 접두사를 제거하고 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + // 타당한 토큰인지 확인하고 + //jwtUtil.isValid 메서드는 인자값으로 토큰을 정제한 토큰을 담아서 + // 타당한 토큰인지 확인하고 사용자 정보를 추출 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String email = jwtUtil.getEmail(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} diff --git a/src/main/java/com/example/umc_9th/grobal/auth/JwtUtil.java b/src/main/java/com/example/umc_9th/grobal/auth/JwtUtil.java new file mode 100644 index 0000000..0b609bc --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/auth/JwtUtil.java @@ -0,0 +1,86 @@ +package com.example.umc_9th.grobal.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; // 필수 import +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, // yml 경로 일치 + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + // Base64로 인코딩된 키를 디코딩하여 사용 (특수문자/공백 문제 해결) + byte[] keyBytes = Decoders.BASE64.decode(secret); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(CustomUserDetails user) { + return createToken(user, accessExpiration); + } + + /** 토큰에서 이메일 가져오기 */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 내부 메서드 + private String createToken(CustomUserDetails user, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 (Role) + String authorities = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(user.getUsername()) + .claim("role", authorities) + .claim("email", user.getUsername()) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plus(expiration))) + .signWith(secretKey) + .compact(); + } + + // Claims 추출 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/grobal/auth/enums/Role.java b/src/main/java/com/example/umc_9th/grobal/auth/enums/Role.java new file mode 100644 index 0000000..4ffd504 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/auth/enums/Role.java @@ -0,0 +1,6 @@ +package com.example.umc_9th.grobal.auth.enums; + +public enum Role { + ROLE_ADMIN, ROLE_USER + +} diff --git a/src/main/java/com/example/umc_9th/grobal/auth/enums/SocialType.java b/src/main/java/com/example/umc_9th/grobal/auth/enums/SocialType.java new file mode 100644 index 0000000..18e69ef --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/auth/enums/SocialType.java @@ -0,0 +1,4 @@ +package com.example.umc_9th.grobal.auth.enums; + +public enum SocialType { +} diff --git a/src/main/java/com/example/umc_9th/grobal/config/SecurityConfig.java b/src/main/java/com/example/umc_9th/grobal/config/SecurityConfig.java new file mode 100644 index 0000000..229fb9e --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/config/SecurityConfig.java @@ -0,0 +1,110 @@ +package com.example.umc_9th.grobal.config; + +import com.example.umc_9th.grobal.auth.CustomUserDetailsService; +import com.example.umc_9th.grobal.auth.JwtAuthFilter; +import com.example.umc_9th.grobal.auth.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +// 어노테이션은 Spring Security 설정을 활성화시키는 역할 +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + + // 비밀번호 솔트를 위한 PasswordEncoder 설정 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + + + // 허용할 URL를 따로 뺴서 관리 + private final String[] allowUris = { + "/api/members/login", // API 경로로 수정 + "/api/members/sign-up", + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/logout" + }; + + @Bean + // SecurityFilterChain를 정의하는 메서드 + // HttpSecurity 객체를 통해 다양한 보안 설정구성 + + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // HTTP 요청에 대한 접근 제어를 설정 + .authorizeHttpRequests(requests -> requests + //requestMatchers 를 사용하여 특정 URL에 대한 권한 접근 설정 + // allowUris배열로 빼서 정의 + .requestMatchers(allowUris).permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + // ADMIN 역할을 가진 사용자만 접근 가능 + // 그외 모든 요청 인증 요구 authenticated + .anyRequest().authenticated() + ) + // 폼 기반 로그인에 대한 설정 로그인 성공 시 /swagger-ui/index.html로 디라이렉트 + // alwaysUse를 true 로 설정하면 로그인 성공 시 항상 Swagger로 리다이렉트 + // 실습 1 + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) + //permitAll은 인증 없이 접근 가능한 경로지정 + .permitAll()) + + +// // 실습 2 +// // 폼로그인 비활성화 +// .formLogin(AbstractHttpConfigurer::disable) +// // JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가 +// // // JwtAuthFilter를 UsernamePasswordAuthenticationFilter +// .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) +// //세션 사용 안 함 +// .sessionManagement(session -> +// session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) +// ) + + .csrf(AbstractHttpConfigurer::disable) + + .logout(logout -> logout + // /logout 경로로 로그아웃 처리 + .logoutUrl("/logout") + + // 성공 시 리다이렉트 + .logoutSuccessUrl("/swagger-ui/index.html") + .permitAll() + ); + + return http.build(); + } + // 실습 1. 세션 방식 AuthenticationManager 빈등록 + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + // 실습 2. jwt 방식 JwtAuthFilter 빈 등록 + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/grobal/jwt/MemberController.java b/src/main/java/com/example/umc_9th/grobal/jwt/MemberController.java new file mode 100644 index 0000000..340f4ce --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/jwt/MemberController.java @@ -0,0 +1,30 @@ +//package com.example.umc_9th.grobal.jwt; +// +//import lombok.RequiredArgsConstructor; +//import org.springframework.web.bind.annotation.PostMapping; +//import org.springframework.web.bind.annotation.RequestBody; +//import org.springframework.web.bind.annotation.RestController; +// +//@RestController +//@RequiredArgsConstructor +//public class MemberController { +// +// private final MemberCommandService memberCommandService; +// private final MemberQueryService memberQueryService; +// +// // 회원가입 +// @PostMapping("/sign-up") +// public ApiResponse signUp( +// @RequestBody @Valid MemberReqDTO.JoinDTO dto +// ){ +// return ApiResponse.onSuccess(MemberSuccessCode.FOUND, memberCommandService.signup(dto)); +// } +// +// // 로그인 +// @PostMapping("/login") +// public ApiResponse login( +// @RequestBody @Valid MemberReqDTO.LoginDTO dto +// ){ +// return ApiResponse.onSuccess(MemberSuccessCode.FOUND, memberQueryService.login(dto)); +// } +//} diff --git a/src/main/java/com/example/umc_9th/grobal/jwt/MemberQueryService.java b/src/main/java/com/example/umc_9th/grobal/jwt/MemberQueryService.java new file mode 100644 index 0000000..e8cac86 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/jwt/MemberQueryService.java @@ -0,0 +1,12 @@ +//package com.example.umc_9th.grobal.jwt; +// +// +//import com.example.umc_9th.domain.member.dto.req.MemberReqDTO; +//import com.example.umc_9th.domain.member.dto.res.MemberResDTO; +//import jakarta.validation.Valid; +// +//public interface MemberQueryService { +// +// +// MemberResDTO.LoginDTO login(@Valid MemberReqDTO.LoginDTO dto); +//} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/grobal/jwt/MemberQueryServiceImpl.java b/src/main/java/com/example/umc_9th/grobal/jwt/MemberQueryServiceImpl.java new file mode 100644 index 0000000..dba9c35 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/jwt/MemberQueryServiceImpl.java @@ -0,0 +1,48 @@ +//package com.example.umc_9th.grobal.jwt; +// +//import com.example.umc_9th.domain.member.converter.MemberConverter; +//import com.example.umc_9th.domain.member.dto.req.MemberReqDTO; +//import com.example.umc_9th.domain.member.dto.res.MemberResDTO; +//import com.example.umc_9th.domain.member.entity.Member; +//import com.example.umc_9th.domain.member.exception.MemberException; +//import com.example.umc_9th.domain.member.exception.code.MemberErrorCode; +//import com.example.umc_9th.domain.member.repository.MemberRepository; +//import com.example.umc_9th.grobal.auth.CustomUserDetails; +//import com.example.umc_9th.grobal.auth.JwtUtil; +//import jakarta.validation.Valid; +//import lombok.RequiredArgsConstructor; +//import org.springframework.security.crypto.password.PasswordEncoder; +//import org.springframework.stereotype.Service; +// +//@Service +//@RequiredArgsConstructor +//public class MemberQueryServiceImpl implements MemberQueryService{ +// +// private final MemberRepository memberRepository; +// private final JwtUtil jwtUtil; +// private final PasswordEncoder encoder; +// +// @Override +// public MemberResDTO.LoginDTO login( +// MemberReqDTO.@Valid LoginDTO dto +// ) { +// +// // Member 조회 +// Member member = memberRepository.findByEmail(dto.email()) +// .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); +// +// // 비밀번호 검증 +// if (!encoder.matches(dto.password(), member.getPassword())){ +// throw new MemberException(MemberErrorCode.NOT_FOUND); +// } +// +// // JWT 토큰 발급용 UserDetails +// CustomUserDetails userDetails = new CustomUserDetails(member); +// +// // 엑세스 토큰 발급 +// String accessToken = jwtUtil.createAccessToken(userDetails); +// +// // DTO 조립 +// return MemberConverter.toLoginDTO(member, accessToken); +// } +//} \ No newline at end of file diff --git a/src/main/java/com/example/umc_9th/grobal/jwt/MemberReqDTO.java b/src/main/java/com/example/umc_9th/grobal/jwt/MemberReqDTO.java new file mode 100644 index 0000000..fcc7864 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/jwt/MemberReqDTO.java @@ -0,0 +1,16 @@ +//package com.example.umc_9th.grobal.jwt; +// +//import jakarta.validation.constraints.NotBlank; +// +//public class MemberReqDTO { +// +// public record LoginDTO( +// @NotBlank +// String email, +// @NotBlank +// String password +// ){} +// +// +// +//} diff --git a/src/main/java/com/example/umc_9th/grobal/jwt/MemberResDTO.java b/src/main/java/com/example/umc_9th/grobal/jwt/MemberResDTO.java new file mode 100644 index 0000000..bee30b6 --- /dev/null +++ b/src/main/java/com/example/umc_9th/grobal/jwt/MemberResDTO.java @@ -0,0 +1,12 @@ +//package com.example.umc_9th.grobal.jwt; +// +//import lombok.Builder; +// +//public class MemberResDTO { +// // 로그인 +// @Builder +// public record LoginDTO( +// Long memberId, +// String accessToken +// ){} +//} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6368497..0be5c86 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,8 +12,15 @@ spring: database: mysql show-sql: true hibernate: - # (경고) 테이블 생성 후 반드시 'validate'로 변경하세요. - ddl-auto: create-drop + # (참고) 개발 중에는 'update', 배포 시에는 'validate' 또는 'none' 권장 + ddl-auto: update properties: hibernate: - format_sql: true \ No newline at end of file + format_sql: true + +jwt: + token: + # ⚠️ 중요: 특수문자와 공백이 포함된 키 값은 반드시 큰따옴표("")로 감싸야 합니다. + secretKey: "ZGh3YWlkc2F2ZXdhZXZ3b2EgMTM5ZXUgMDMxdWMyIHEyMiBAIDAgKTJFVio=" + expiration: + access: 14400000 \ No newline at end of file diff --git a/src/main/resources/test.sql b/src/main/resources/test.sql new file mode 100644 index 0000000..ca98a5f --- /dev/null +++ b/src/main/resources/test.sql @@ -0,0 +1,38 @@ +-- Food Category (3개) +INSERT INTO food_category (category, created_at, updated_at) VALUES +('한식', NOW(), NOW()), +('일식', NOW(), NOW()), +('양식', NOW(), NOW()); + +-- Member (3명) +INSERT INTO member (name, email, phone_number, gender, birth_date, address, status, points, password, role, created_at, updated_at) VALUES +('홍길동', 'user1@test.com', '010-1111-1111', 'MALE', '1990-01-01', '서울시 강남구', true, 0, '1234', 'USER', NOW(), NOW()), +('김철수', 'user2@test.com', '010-2222-2222', 'MALE', '1992-02-02', '서울시 서초구', true, 0, '1234', 'USER', NOW(), NOW()), +('이영희', 'user3@test.com', '010-3333-3333', 'FEMALE', '1994-03-03', '서울시 송파구', true, 0, '1234', 'USER', NOW(), NOW()); + +-- Region (2개) +INSERT INTO region (porvince_name, city_name, member_id, created_at, updated_at) VALUES +('서울시', '강남구', 1, NOW(), NOW()), +('서울시', '송파구', 3, NOW(), NOW()); + +-- Store (3개) - score는 Integer 타입이므로 정수로 입력 +INSERT INTO store (name, region_id, food_category_id, score, created_at, updated_at) VALUES +('강남 맛집', 1, 1, 4, NOW(), NOW()), +('강남 스시', 1, 2, 5, NOW(), NOW()), +('송파 카페', 2, 3, 4, NOW(), NOW()); + +-- Review (각 가게당 2-3개씩) +INSERT INTO review (rating, content, member_id, shop_id, created_at, updated_at) VALUES +-- 강남 맛집 리뷰 (3개) +(5, '인생 맛집입니다.', 1, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), NOW()), +(4, '맛있는데 비싸요.', 2, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), NOW()), +(5, '강추!', 3, 1, DATE_SUB(NOW(), INTERVAL 3 DAY), NOW()), + +-- 강남 스시 리뷰 (2개) +(5, '신선해요.', 1, 2, DATE_SUB(NOW(), INTERVAL 1 HOUR), NOW()), +(4, '깔끔해요.', 2, 2, DATE_SUB(NOW(), INTERVAL 2 HOUR), NOW()), + +-- 송파 카페 리뷰 (3개) +(5, '커피 맛집.', 1, 3, DATE_SUB(NOW(), INTERVAL 3 HOUR), NOW()), +(4, '디저트 맛집.', 2, 3, DATE_SUB(NOW(), INTERVAL 4 HOUR), NOW()), +(5, '조용함.', 3, 3, DATE_SUB(NOW(), INTERVAL 5 HOUR), NOW());