diff --git a/build.gradle b/build.gradle index fddc4be4..86ab2874 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,12 @@ dependencies { // Security 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' } tasks.named('test') { diff --git "a/src/main/java/mission/6\354\243\274\354\260\250 \352\263\274\354\240\2341.png" "b/src/main/java/mission/6\354\243\274\354\260\250/6\354\243\274\354\260\250 \352\263\274\354\240\2341.png" similarity index 100% rename from "src/main/java/mission/6\354\243\274\354\260\250 \352\263\274\354\240\2341.png" rename to "src/main/java/mission/6\354\243\274\354\260\250/6\354\243\274\354\260\250 \352\263\274\354\240\2341.png" diff --git "a/src/main/java/mission/6\354\243\274\354\260\250 \352\263\274\354\240\2342.png" "b/src/main/java/mission/6\354\243\274\354\260\250/6\354\243\274\354\260\250 \352\263\274\354\240\2342.png" similarity index 100% rename from "src/main/java/mission/6\354\243\274\354\260\250 \352\263\274\354\240\2342.png" rename to "src/main/java/mission/6\354\243\274\354\260\250/6\354\243\274\354\260\250 \352\263\274\354\240\2342.png" diff --git "a/src/main/java/mission/9\354\243\274\354\260\250/9\354\243\274\354\260\250 \353\257\270\354\205\2301-1.png" "b/src/main/java/mission/9\354\243\274\354\260\250/9\354\243\274\354\260\250 \353\257\270\354\205\2301-1.png" new file mode 100644 index 00000000..0de48c78 Binary files /dev/null and "b/src/main/java/mission/9\354\243\274\354\260\250/9\354\243\274\354\260\250 \353\257\270\354\205\2301-1.png" differ diff --git "a/src/main/java/mission/9\354\243\274\354\260\250/9\354\243\274\354\260\250 \353\257\270\354\205\2301-2.png" "b/src/main/java/mission/9\354\243\274\354\260\250/9\354\243\274\354\260\250 \353\257\270\354\205\2301-2.png" new file mode 100644 index 00000000..c43fb573 Binary files /dev/null and "b/src/main/java/mission/9\354\243\274\354\260\250/9\354\243\274\354\260\250 \353\257\270\354\205\2301-2.png" differ diff --git a/src/main/java/umc/domain/member/controller/MemberController.java b/src/main/java/umc/domain/member/controller/MemberController.java index 82dd2dc7..fea18406 100644 --- a/src/main/java/umc/domain/member/controller/MemberController.java +++ b/src/main/java/umc/domain/member/controller/MemberController.java @@ -1,64 +1,80 @@ package umc.domain.member.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import umc.domain.member.dto.MemberReqDTO; import umc.domain.member.dto.MemberResDTO; import umc.domain.member.exception.code.MemberSuccessCode; import umc.domain.member.service.MemberService; +import umc.domain.mission.dto.MissionResDTO; import umc.global.apiPayload.ApiResponse; import umc.global.apiPayload.code.BaseSuccessCode; +import umc.global.security.entity.AuthMember; import java.util.List; @RestController @RequiredArgsConstructor -@RequestMapping("/api") +@RequestMapping("/api/members") public class MemberController { private final MemberService memberService; - // 멤버 생성 - @PostMapping("/v1/member/me") - public ApiResponse createMember( - @RequestBody MemberReqDTO.CreateMember dto // ✅ @PathVariable → @RequestBody + // 멤버 조회 - 마이페이지 + @PostMapping("/me") + public ApiResponse getMember( + @RequestBody MemberReqDTO.GetMemberDTO dto ) { - BaseSuccessCode code = MemberSuccessCode.CREATED; - return ApiResponse.onSuccess(code, memberService.createMember(dto)); + return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_OK, + memberService.getMember(dto.id())); } - // 멤버 조회 - @GetMapping("/v1/member/me") // ✅ @PostMapping → @GetMapping - public ApiResponse> getMembers( - @RequestParam Integer pageSize, // ✅ @PathVariable → @RequestParam - @RequestParam Integer pageNumber, - @RequestParam(required = false) String sort - ){ - BaseSuccessCode code = MemberSuccessCode.OK; // ✅ MissionSuccessCode → MemberSuccessCode - return ApiResponse.onSuccess(code, memberService.getMembers(pageSize, pageNumber, sort)); + // 마이페이지 + @GetMapping("/me/v2") + public ApiResponse getMember( + @AuthenticationPrincipal AuthMember member + ) { + BaseSuccessCode code = MemberSuccessCode.MEMBER_OK; + return ApiResponse.onSuccess(code, memberService.getMember(member)); + } + + // 회원가입 + @PostMapping("/signup") + public ApiResponse signUp( + @RequestBody @Valid MemberReqDTO.SignUpDTO requestDto + ) { + return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_CREATED, memberService.signUp(requestDto)); + } + + // 로그인 + @PostMapping("/login") + public ApiResponse login( + @RequestBody MemberReqDTO.LoginRequest request + ) { + return ApiResponse.onSuccess(MemberSuccessCode.LOGIN_SUCCESS, memberService.login(request)); } - // 멤버 미션 생성 - @PostMapping("/v1/member/missions") - public ApiResponse createMemberMissions( - @RequestParam Long memberId, // ✅ 추가 - @RequestParam Long missionId, - @RequestBody MemberReqDTO.CreateMemberMission dto // ✅ @PathVariable → @RequestBody + // 내 미션 생성 + @PostMapping("/me/missions") + public ApiResponse createMyMission( + @RequestParam Long memberId, + @RequestParam Long missionId ) { - BaseSuccessCode code = MemberSuccessCode.MISSIONCREATED; - return ApiResponse.onSuccess(code, memberService.createMemberMission(memberId, missionId, dto)); + return ApiResponse.onSuccess(MemberSuccessCode.MISSION_CREATED, + memberService.createMyMission(memberId, missionId)); // ← 메서드명 수정 } - // 멤버 미션 조회 - @GetMapping("/v1/member/missions") // ✅ @PostMapping → @GetMapping - public ApiResponse> getMemberMissions( - @RequestParam Long memberId, // ✅ 추가 - @RequestParam Long missionId, - @RequestParam Integer pageSize, // ✅ @PathVariable → @RequestParam + // 내 미션 조회 + @GetMapping("/me/missions") + public ApiResponse> getMyMissions( + @RequestParam Long memberId, + @RequestParam Integer pageSize, // ← missionId 제거 @RequestParam Integer pageNumber, @RequestParam(required = false) String sort ){ - BaseSuccessCode code = MemberSuccessCode.MISSIONOK; // ✅ MissionSuccessCode → MemberSuccessCode - return ApiResponse.onSuccess(code, memberService.getMemberMissions(memberId, missionId, pageSize, pageNumber, sort)); + return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_MISSION_OK, + memberService.getMyMissions(memberId, pageSize, pageNumber, sort)); // ← 메서드명 수정 } } \ No newline at end of file diff --git a/src/main/java/umc/domain/member/converter/MemberConverter.java b/src/main/java/umc/domain/member/converter/MemberConverter.java index 4e4260d5..021e6911 100644 --- a/src/main/java/umc/domain/member/converter/MemberConverter.java +++ b/src/main/java/umc/domain/member/converter/MemberConverter.java @@ -4,17 +4,32 @@ import umc.domain.member.dto.MemberReqDTO; import umc.domain.member.entity.Member; import umc.domain.member.entity.MemberMission; +import umc.domain.member.enums.Social_Type; +import umc.domain.member.enums.Status; +import umc.domain.mission.dto.MissionResDTO; import umc.domain.mission.entity.Mission; public class MemberConverter { - // 멤버 생성 — 암호화된 비밀번호를 인자로 받음 - public static Member toMember( - MemberReqDTO.CreateMember dto, - String encodedPassword - ) { + // 멤버 조회 - 마이페이지 + public static MemberResDTO.GetMemberDTO toGetMember(Member member) { + return MemberResDTO.GetMemberDTO.builder() + .member_id(member.getId()) + .email(member.getEmail()) + .name(member.getName()) + .gender(member.getGender()) + .birth(member.getBirth()) + .phone(member.getPhone()) + .point(member.getPoint()) + .status(member.getStatus()) + .build(); + } + + // 회원가입 생성 — 암호화된 비밀번호를 인자로 받음 + public static Member toPutMember( + MemberReqDTO.SignUpDTO dto, String encodedPassword + ) { return Member.builder() - .log_id(dto.log_id()) .email(dto.email()) .password(encodedPassword) // ★ 암호화된 비밀번호 사용 .name(dto.name()) @@ -24,61 +39,51 @@ public static Member toMember( .add1(dto.add1()) .add2(dto.add2()) .phone(dto.phone()) - .point(dto.point()) - .status(dto.status()) - .org_cd(dto.org_cd()) + .point(0) + .status(Status.ACTIVE) + .social_provider(Social_Type.NONE) .build(); } - // 멤버 조회 - public static MemberResDTO.GetMember toGetMember( - Member member - ) { - return MemberResDTO.GetMember.builder() - .member_id(member.getId()) - .log_id(member.getLog_id()) - .email(member.getEmail()) - .password(member.getPassword()) - .name(member.getName()) - .gender(member.getGender()) - .birth(member.getBirth()) - .post(member.getPost()) - .add1(member.getAdd1()) - .add2(member.getAdd2()) - .phone(member.getPhone()) - .point(member.getPoint()) - .status(member.getStatus()) - .org_cd(member.getOrg_cd()) - .build(); + // 회원가입 조회 + public static MemberResDTO.GetSignUpDTO toGetSignUp(Member member) { + return new MemberResDTO.GetSignUpDTO( + member.getId(), + member.getCreatedAt() + ); + } + // 로그인 인증 + public static MemberResDTO.LoginResponse toLoginResponse(String accessToken) { + return new MemberResDTO.LoginResponse(accessToken); } // 멤버 미션 생성 - public static MemberMission toMemberMission( - Mission mission, - Member member, - MemberReqDTO.CreateMemberMission dto + public static MemberMission toPutMemberMission( + Mission mission, Member member ) { return MemberMission.builder() .mission(mission) .member(member) - .succ_yn(dto.succ_yn()) - .user_start_dt(dto.user_start_dt()) + .succ_yn("N") // 기본값 미완료 + .user_start_dt(java.time.LocalDate.now()) .build(); } // 멤버 미션 조회 - public static MemberResDTO.GetMemberMission toGetMemberMission( + public static MemberResDTO.GetMemberMissionDTO toGetMemberMission( MemberMission memberMission - ){ - return MemberResDTO.GetMemberMission.builder() + ) { + return MemberResDTO.GetMemberMissionDTO.builder() .member_id(memberMission.getMember().getId()) .mission_id(memberMission.getMission().getId()) .succ_yn(memberMission.getSucc_yn()) .user_start_dt(memberMission.getUser_start_dt()) .build(); - } + // 홈 조회 추가 + + } \ No newline at end of file diff --git a/src/main/java/umc/domain/member/dto/MemberReqDTO.java b/src/main/java/umc/domain/member/dto/MemberReqDTO.java index 96017374..528ddcd5 100644 --- a/src/main/java/umc/domain/member/dto/MemberReqDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberReqDTO.java @@ -1,30 +1,64 @@ package umc.domain.member.dto; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import umc.domain.member.enums.Gender; -import umc.domain.member.enums.Org_cd; +import umc.domain.member.enums.Social_Type; import umc.domain.member.enums.Status; import java.time.LocalDate; +import java.util.List; public class MemberReqDTO { - public record CreateMember( - String log_id, + + // Member 조회 + public record GetMemberDTO( + Long id + ){} + + // 회원가입 + public record SignUpDTO( + @NotBlank @Email String email, + @NotBlank String password, + @NotBlank String name, + @NotNull Gender gender, + @NotBlank String birth, + @NotBlank String post, - String add1, - String add2, + @NotBlank + String add1, // 시, 구 + @NotBlank + String add2, // 상세주소 동, 호 + @NotBlank String phone, - Integer point, - Status status, - Org_cd org_cd - ){} - public record CreateMemberMission( - String succ_yn, - LocalDate user_start_dt + List foodId, + @NotNull + @NotEmpty + @Valid + List terms ){} + + // 약관 + public record TermDTO( + @NotNull + Long termId, + @NotNull + Boolean isAgreed + ) {} + + // 로그인 + public record LoginRequest( + String email, + String password + ) {} + } diff --git a/src/main/java/umc/domain/member/dto/MemberResDTO.java b/src/main/java/umc/domain/member/dto/MemberResDTO.java index 3fc4e63c..802da359 100644 --- a/src/main/java/umc/domain/member/dto/MemberResDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberResDTO.java @@ -2,37 +2,48 @@ import lombok.Builder; import umc.domain.member.enums.Gender; -import umc.domain.member.enums.Org_cd; +import umc.domain.member.enums.Social_Type; import umc.domain.member.enums.Status; import java.time.LocalDate; +import java.time.LocalDateTime; public class MemberResDTO { + // Member 조회 @Builder - public record GetMember( + public record GetMemberDTO( Long member_id, - String log_id, String email, - String password, String name, Gender gender, String birth, - String post, - String add1, - String add2, String phone, Integer point, - Status status, - Org_cd org_cd - + Status status ){} + // 회원가입 @Builder - public record GetMemberMission( + public record GetSignUpDTO( + Long member_id, + LocalDateTime createdAt + ) { + } + + // 로그인 + public record LoginResponse( + String accessToken + ) {} + + // 내 미션 조회 + @Builder + public record GetMemberMissionDTO( Long member_id, Long mission_id, String succ_yn, LocalDate user_start_dt ){} -} + + +} \ No newline at end of file diff --git a/src/main/java/umc/domain/member/entity/Member.java b/src/main/java/umc/domain/member/entity/Member.java index b5a1ba53..a9987808 100644 --- a/src/main/java/umc/domain/member/entity/Member.java +++ b/src/main/java/umc/domain/member/entity/Member.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import umc.domain.common.BaseEntity; import umc.domain.member.enums.Gender; -import umc.domain.member.enums.Org_cd; +import umc.domain.member.enums.Social_Type; import umc.domain.member.enums.Status; @@ -24,7 +24,7 @@ public class Member extends BaseEntity { @Column(name = "member_id", nullable = false) private Long id; - @Column(name = "log_id", nullable = false, length = 50) + @Column(name = "log_id", nullable = true, length = 50) private String log_id; @Column(name = "email", nullable = false, length = 50) @@ -62,8 +62,10 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private Status status; - @Column(name = "org_cd", nullable = false, length = 10) + @Column(name = "social_provider", nullable = false, length = 10) @Enumerated(EnumType.STRING) - private Org_cd org_cd; + private Social_Type social_provider; + @Column(name = "social_uid", length = 20) + private String social_uid; } \ No newline at end of file diff --git a/src/main/java/umc/domain/member/entity/MemberFood.java b/src/main/java/umc/domain/member/entity/MemberFood.java index 87d8f537..6724706d 100644 --- a/src/main/java/umc/domain/member/entity/MemberFood.java +++ b/src/main/java/umc/domain/member/entity/MemberFood.java @@ -1,4 +1,33 @@ package umc.domain.member.entity; -public class MemberFood { +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name="memberFood") +public class MemberFood extends BaseEntity{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "prefer_id", nullable = false) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + /* + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "food_id", nullable = false) + private Food food; + */ + } diff --git a/src/main/java/umc/domain/member/entity/MemberMission.java b/src/main/java/umc/domain/member/entity/MemberMission.java index fef57474..cca3ba26 100644 --- a/src/main/java/umc/domain/member/entity/MemberMission.java +++ b/src/main/java/umc/domain/member/entity/MemberMission.java @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; import umc.domain.common.BaseEntity; import umc.domain.mission.entity.Mission; -import umc.domain.store.entity.Store; import java.time.LocalDate; @@ -21,7 +20,7 @@ public class MemberMission extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "mebmer_mission_id", nullable = false) + @Column(name = "member_mission_id", nullable = false) private Long id; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/umc/domain/member/entity/MemberTerm.java b/src/main/java/umc/domain/member/entity/MemberTerm.java index 2363b421..ea98f013 100644 --- a/src/main/java/umc/domain/member/entity/MemberTerm.java +++ b/src/main/java/umc/domain/member/entity/MemberTerm.java @@ -1,4 +1,41 @@ package umc.domain.member.entity; -public class MemberTerm { -} +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name="term") +public class MemberTerm extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "term_id", nullable = false) + private Long id; + + @Column(name = "term1_yn", nullable = false, length = 1) + private Boolean term1_yn; + + @Column(name = "term2_yn", nullable = false, length = 1) + private Boolean term2_yn; + + @Column(name = "term3_yn", nullable = false, length = 1) + private Boolean term3_yn; + + @Column(name = "term4_yn", nullable = false, length = 1) + private Boolean term4_yn; + + @Column(name = "term5_yn", nullable = false, length = 1) + private Boolean term5_yn; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; +} \ No newline at end of file diff --git a/src/main/java/umc/domain/member/enums/Org_cd.java b/src/main/java/umc/domain/member/enums/Social_Type.java similarity index 62% rename from src/main/java/umc/domain/member/enums/Org_cd.java rename to src/main/java/umc/domain/member/enums/Social_Type.java index 29bdaf3c..d4602e0c 100644 --- a/src/main/java/umc/domain/member/enums/Org_cd.java +++ b/src/main/java/umc/domain/member/enums/Social_Type.java @@ -1,8 +1,8 @@ package umc.domain.member.enums; -public enum Org_cd { +public enum Social_Type { NAVER, GOOGLE, KAKAO, - APPLE + NONE, APPLE } diff --git a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java index bbc2058a..0b731615 100644 --- a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java @@ -9,7 +9,14 @@ @RequiredArgsConstructor public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 사용자를 찾을 수 없습니다."), - MEMBER_MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBERMISSION404_1", "해당 미션을 찾을 수 없습니다."); + + MEMBER_MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBERMISSION404_1", "해당 미션을 찾을 수 없습니다."), + + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "MEMBER409_1", "이미 사용 중인 이메일입니다."), + + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "비밀번호가 일치하지 않습니다."), + + ; private final HttpStatus status; private final String code; diff --git a/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java b/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java index 16d973c8..6c7d29e7 100644 --- a/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java +++ b/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java @@ -9,11 +9,13 @@ @RequiredArgsConstructor public enum MemberSuccessCode implements BaseSuccessCode { - CREATED(HttpStatus.CREATED, "MEMBER200_1", "성공적으로 유저를 생성했습니다."), - OK(HttpStatus.OK, "MEMBER200_2", "성공적으로 유저를 조회했습니다."), + MEMBER_CREATED(HttpStatus.CREATED, "MEMBER200_1", "성공적으로 유저를 생성했습니다."), + MEMBER_OK(HttpStatus.OK, "MEMBER200_2", "성공적으로 유저를 조회했습니다."), - MISSIONCREATED(HttpStatus.CREATED, "MEMBERMISSION200_1", "성공적으로 멤버미션을 생성했습니다."), - MISSIONOK(HttpStatus.OK, "MEMBERMISSION200_2", "성공적으로 멤버미션을 조회했습니다."); + LOGIN_SUCCESS(HttpStatus.OK, "MEMBER200_3", "성공적으로 로그인했습니다."), // 추가! + + MISSION_CREATED(HttpStatus.CREATED, "MEMBERMISSION200_1", "성공적으로 멤버 미션을 생성했습니다."), + MEMBER_MISSION_OK(HttpStatus.OK, "MEMBERMISSION200_2", "성공적으로 멤버미션을 조회했습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/umc/domain/member/repository/MemberFoodRepository.java b/src/main/java/umc/domain/member/repository/MemberFoodRepository.java new file mode 100644 index 00000000..596f9d9b --- /dev/null +++ b/src/main/java/umc/domain/member/repository/MemberFoodRepository.java @@ -0,0 +1,4 @@ +package umc.domain.member.repository; + +public interface MemberFoodRepository { +} diff --git a/src/main/java/umc/domain/member/repository/MemberMissionRepository.java b/src/main/java/umc/domain/member/repository/MemberMissionRepository.java index f5a58049..f3e989fa 100644 --- a/src/main/java/umc/domain/member/repository/MemberMissionRepository.java +++ b/src/main/java/umc/domain/member/repository/MemberMissionRepository.java @@ -8,5 +8,6 @@ import umc.domain.mission.entity.Mission; public interface MemberMissionRepository extends JpaRepository { - Page findAllByMember_IdAndMission_Id(Long memberId, Long missionId, Pageable pageable); + // 내 미션 전체 조회 + Page findAllByMember_Id(Long memberId, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/umc/domain/member/repository/MemberRepository.java b/src/main/java/umc/domain/member/repository/MemberRepository.java index 0ee33da4..5505194a 100644 --- a/src/main/java/umc/domain/member/repository/MemberRepository.java +++ b/src/main/java/umc/domain/member/repository/MemberRepository.java @@ -13,4 +13,5 @@ public interface MemberRepository extends JpaRepository { // findById는 JPA가 기본 제공! Optional findByEmail(String email); + boolean existsByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/umc/domain/member/repository/MemberTermRepository.java b/src/main/java/umc/domain/member/repository/MemberTermRepository.java new file mode 100644 index 00000000..6d8c2dc7 --- /dev/null +++ b/src/main/java/umc/domain/member/repository/MemberTermRepository.java @@ -0,0 +1,4 @@ +package umc.domain.member.repository; + +public interface MemberTermRepository { +} diff --git a/src/main/java/umc/domain/member/service/MemberService.java b/src/main/java/umc/domain/member/service/MemberService.java index 30050a2d..e5048d8b 100644 --- a/src/main/java/umc/domain/member/service/MemberService.java +++ b/src/main/java/umc/domain/member/service/MemberService.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -16,9 +17,11 @@ import umc.domain.member.exception.code.MemberErrorCode; import umc.domain.member.repository.MemberMissionRepository; import umc.domain.member.repository.MemberRepository; +import umc.domain.mission.dto.MissionResDTO; import umc.domain.mission.entity.Mission; import umc.domain.mission.repository.MissionRepository; - +import umc.global.security.entity.AuthMember; +import umc.global.security.util.JwtUtil; import java.util.List; @@ -31,88 +34,87 @@ public class MemberService { private final MemberMissionRepository memberMissionRepository; private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; - // 멤버 생성 + // 멤버 조회 - 마이페이지 + public MemberResDTO.GetMemberDTO getMember(Long id) { + Member member = memberRepository.findById(id) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return MemberConverter.toGetMember(member); + } + + // 마이페이지 + public MemberResDTO.GetMemberDTO getMember(AuthMember member) { + // 컨버터를 이용해서 응답 DTO 생성 & return + return MemberConverter.toGetMember(member.getMember()); + } + + // 회원가입 @Transactional - public Void createMember( - MemberReqDTO.CreateMember dto - ){ + public MemberResDTO.GetSignUpDTO signUp(MemberReqDTO.SignUpDTO dto) { + validateEmailNotDuplicate(dto.email()); + Member member = createMember(dto); + // 선호 음식 생성 savePreferFoods(member, dto.foodIds()); + // 약관 생성 saveTermAgreements(member, dto.terms()); + return MemberConverter.toGetSignUp(member); + } + + private void validateEmailNotDuplicate(String email) { + if (memberRepository.existsByEmail(email)) { + throw new MemberException(MemberErrorCode.DUPLICATE_EMAIL); + } + } + + // 멤버 생성 + private Member createMember(MemberReqDTO.SignUpDTO dto) { // ★ 비밀번호 암호화 String encodedPassword = passwordEncoder.encode(dto.password()); - // 멤버 생성 - Member member = MemberConverter.toMember(dto, encodedPassword); - + Member member = MemberConverter.toPutMember(dto, encodedPassword); // 멤버 DB 저장 - memberRepository.save(member); - - return null; + return memberRepository.save(member); } - // 멤버 호출 - public List getMembers( - Integer pageSize, - Integer pageNumber, - String sort - ){ - // 정렬 정보 생성 - Sort sortInfo; - if(sort != null){ - if(sort.equalsIgnoreCase("asc")){ - sortInfo = Sort.by("id").ascending(); - } else if(sort.equalsIgnoreCase("desc")){ - sortInfo = Sort.by("id").descending(); - } else { - sortInfo = Sort.by(sort); // 컬럼명으로 정렬 - } - } else { - sortInfo = Sort.by("id").descending(); - } + // 선호음식 + 약관 추가 - // 페이지 정보들을 PageRequest로 만들기 - PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); + // 로그인 + public MemberResDTO.LoginResponse login(MemberReqDTO.LoginRequest request) { + // 1. 이메일로 회원 조회 + Member member = memberRepository.findByEmail(request.email()) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - // 가게 내 아이디 조회 - Page memberList = memberRepository.findAll(pageRequest); + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(request.password(), member.getPassword())) { + throw new MemberException(MemberErrorCode.INVALID_PASSWORD); + } + // 3. JWT 토큰 발급 + AuthMember authMember = new AuthMember(member); + String accessToken = jwtUtil.createAccessToken(authMember); - // 미션들 응답 DTO로 포장하기 - return memberList.map(MemberConverter::toGetMember).getContent(); + // 4. 응답 반환 + return MemberConverter.toLoginResponse(accessToken); } - // 멤버 미션 생성 + // 내 미션 생성 @Transactional - public Void createMemberMission( - Long memberId, - Long missionId, - MemberReqDTO.CreateMemberMission dto - ){ - // 멤버 찾기 + public Void createMyMission(Long memberId, Long missionId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - - // 미션 찾기 Mission mission = missionRepository.findById(missionId) - .orElseThrow(() ->new MemberException(MemberErrorCode.MEMBER_MISSION_NOT_FOUND)); - - // 미션 생성 - MemberMission memberMission = MemberConverter.toMemberMission(mission, member, dto); - - // 미션 DB 저장 + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_MISSION_NOT_FOUND)); + MemberMission memberMission = MemberConverter.toPutMemberMission(mission, member); memberMissionRepository.save(memberMission); - return null; } - // 멤버 미션 조회 - public List getMemberMissions( + // 내 미션 조회 + public List getMyMissions( Long memberId, - Long missionId, Integer pageSize, Integer pageNumber, String sort ){ - // 정렬 정보 생성 Sort sortInfo; if(sort != null){ if(sort.equalsIgnoreCase("asc")){ @@ -120,20 +122,13 @@ public List getMemberMissions( } else if(sort.equalsIgnoreCase("desc")){ sortInfo = Sort.by("id").descending(); } else { - sortInfo = Sort.by(sort); // 컬럼명으로 정렬 + sortInfo = Sort.by(sort); } } else { sortInfo = Sort.by("id").descending(); } - - // 페이지 정보들을 PageRequest로 만들기 PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); - - // 멤버 미션 아이디 조회 - Page memberMissionList = memberMissionRepository.findAllByMember_IdAndMission_Id(memberId, missionId, pageRequest); - - - // 미션들 응답 DTO로 포장하기 + Page memberMissionList = memberMissionRepository.findAllByMember_Id(memberId, pageRequest); return memberMissionList.map(MemberConverter::toGetMemberMission).getContent(); } diff --git a/src/main/java/umc/domain/mission/controller/MissionController.java b/src/main/java/umc/domain/mission/controller/MissionController.java index 78b03d1a..72588957 100644 --- a/src/main/java/umc/domain/mission/controller/MissionController.java +++ b/src/main/java/umc/domain/mission/controller/MissionController.java @@ -1,5 +1,6 @@ package umc.domain.mission.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import umc.domain.mission.dto.MissionReqDTO; @@ -7,36 +8,35 @@ import umc.domain.mission.exception.code.MissionSuccessCode; import umc.domain.mission.service.MissionService; import umc.global.apiPayload.ApiResponse; -import umc.global.apiPayload.code.BaseSuccessCode; import java.util.List; @RestController @RequiredArgsConstructor -@RequestMapping("/api") +@RequestMapping("/api/stores") public class MissionController { private final MissionService missionService; // 가게 미션 생성 - @PostMapping("/v1/stores/{storeId}/missions") - public ApiResponse createMission( + @PostMapping("/{storeId}/missions") + public ApiResponse createMission( @PathVariable Long storeId, - @RequestBody MissionReqDTO.CreateMission dto + @RequestBody @Valid MissionReqDTO.CreateMissionDTO dto ){ - BaseSuccessCode code = MissionSuccessCode.CREATED; - return ApiResponse.onSuccess(code, missionService.createMission(storeId, dto)); + return ApiResponse.onSuccess(MissionSuccessCode.CREATED, + missionService.createMission(storeId, dto)); } - // 가게 내 미션들 조회 - @GetMapping("/v1/stores/{storeId}/missions") - public ApiResponse> getMissions( + // 가게 미션 조회 + @GetMapping("/{storeId}/missions") + public ApiResponse> 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)); + return ApiResponse.onSuccess(MissionSuccessCode.OK, + missionService.getMissions(storeId, pageSize, pageNumber, sort)); } -} +} \ No newline at end of file diff --git a/src/main/java/umc/domain/mission/converter/MissionConverter.java b/src/main/java/umc/domain/mission/converter/MissionConverter.java index 140c501b..bb5f2c06 100644 --- a/src/main/java/umc/domain/mission/converter/MissionConverter.java +++ b/src/main/java/umc/domain/mission/converter/MissionConverter.java @@ -6,9 +6,11 @@ import umc.domain.store.entity.Store; public class MissionConverter { - public static Mission toMission( + + // 미션 생성 (ReqDTO → Entity) + public static Mission toPutMission( Store store, - MissionReqDTO.CreateMission dto + MissionReqDTO.CreateMissionDTO dto ){ return Mission.builder() .store(store) @@ -19,18 +21,22 @@ public static Mission toMission( .build(); } - // 가게 내 미션 조회 - public static MissionResDTO.GetMission toGetMission( - Mission mission - ){ - return MissionResDTO.GetMission.builder() + // 미션 생성 조회 (Entity → ResDTO) + public static MissionResDTO.GetCreateMissionDTO toGetCreateMission(Mission mission) { + return new MissionResDTO.GetCreateMissionDTO( + mission.getId(), + mission.getCreatedAt() + ); + } + + // 미션 조회 (Entity → ResDTO) + public static MissionResDTO.GetMissionDTO toGetMission(Mission mission) { + return MissionResDTO.GetMissionDTO.builder() .missionId(mission.getId()) .conditional(mission.getConditional()) .reward_point(mission.getReward_point()) .start_dt(mission.getStart_dt()) .end_dt(mission.getEnd_dt()) .build(); - } - -} +} \ No newline at end of file diff --git a/src/main/java/umc/domain/mission/dto/MissionReqDTO.java b/src/main/java/umc/domain/mission/dto/MissionReqDTO.java index f365d71c..1e1cc24a 100644 --- a/src/main/java/umc/domain/mission/dto/MissionReqDTO.java +++ b/src/main/java/umc/domain/mission/dto/MissionReqDTO.java @@ -1,14 +1,21 @@ package umc.domain.mission.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + import java.time.LocalDate; public class MissionReqDTO { // 가게 미션 생성 - public record CreateMission( + public record CreateMissionDTO( + @NotNull Integer reward_point, + @NotBlank String conditional, + @NotNull LocalDate start_dt, + @NotNull LocalDate end_dt ){} diff --git a/src/main/java/umc/domain/mission/dto/MissionResDTO.java b/src/main/java/umc/domain/mission/dto/MissionResDTO.java index b720f61d..fc71d0b7 100644 --- a/src/main/java/umc/domain/mission/dto/MissionResDTO.java +++ b/src/main/java/umc/domain/mission/dto/MissionResDTO.java @@ -3,16 +3,24 @@ import lombok.Builder; import java.time.LocalDate; +import java.time.LocalDateTime; public class MissionResDTO { - // 가게 내 미션 조회 + // 미션 생성 응답 @Builder - public record GetMission( + public record GetCreateMissionDTO( + Long mission_id, + LocalDateTime createdAt + ){} + + // 미션 조회 응답 + @Builder + public record GetMissionDTO( Long missionId, Integer reward_point, String conditional, LocalDate start_dt, LocalDate end_dt ){} -} +} \ No newline at end of file diff --git a/src/main/java/umc/domain/mission/service/MissionService.java b/src/main/java/umc/domain/mission/service/MissionService.java index 83ce7c1e..56021918 100644 --- a/src/main/java/umc/domain/mission/service/MissionService.java +++ b/src/main/java/umc/domain/mission/service/MissionService.java @@ -27,30 +27,23 @@ public class MissionService { // 가게 미션 생성 @Transactional - public Void createMission( + public MissionResDTO.GetCreateMissionDTO createMission( Long storeId, - MissionReqDTO.CreateMission dto + MissionReqDTO.CreateMissionDTO dto ){ - // 가게 찾기 Store store = storeRepository.findById(storeId) .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); - - // 미션 생성 - Mission mission = MissionConverter.toMission(store, dto); - - // 미션 DB 저장 - missionRepository.save(mission); - - return null; + Mission mission = MissionConverter.toPutMission(store, dto); + return MissionConverter.toGetCreateMission(missionRepository.save(mission)); } - public List getMissions( + // 가게 미션 조회 + public List getMissions( Long storeId, Integer pageSize, Integer pageNumber, String sort ){ - // 정렬 정보 생성 Sort sortInfo; if(sort != null){ if(sort.equalsIgnoreCase("asc")){ @@ -58,20 +51,14 @@ public List getMissions( } else if(sort.equalsIgnoreCase("desc")){ sortInfo = Sort.by("id").descending(); } else { - sortInfo = Sort.by(sort); // 컬럼명으로 정렬 + sortInfo = Sort.by(sort); } } else { sortInfo = Sort.by("id").descending(); } - // 페이지 정보들을 PageRequest로 만들기 PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); - - // 가게 내 미션들 조회 Page missionList = missionRepository.findAllByStoreId(storeId, pageRequest); - - - // 미션들 응답 DTO로 포장하기 return missionList.map(MissionConverter::toGetMission).getContent(); } -} +} \ No newline at end of file diff --git a/src/main/java/umc/domain/store/controller/StoreController.java b/src/main/java/umc/domain/store/controller/StoreController.java index 3e3b81cd..f3b70f82 100644 --- a/src/main/java/umc/domain/store/controller/StoreController.java +++ b/src/main/java/umc/domain/store/controller/StoreController.java @@ -1,49 +1,57 @@ package umc.domain.store.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import umc.domain.store.enums.ReviewSuccessCode; -import umc.domain.store.enums.StoreSuccessCode; import umc.domain.store.dto.StoreReqDTO; import umc.domain.store.dto.StoreResDTO; +import umc.domain.store.exception.code.StoreSuccessCode; import umc.domain.store.service.StoreService; import umc.global.apiPayload.ApiResponse; -import umc.global.apiPayload.code.BaseSuccessCode; @RestController @RequiredArgsConstructor -@RequestMapping("/api") +@RequestMapping("/api/stores") public class StoreController { private final StoreService storeService; - @PostMapping("/v1/stores/{storeId}") - public ApiResponse getStoreInfo( - @RequestBody StoreReqDTO.GetStoreInfo dto + // 가게 조회 + @GetMapping("/{storeId}") + public ApiResponse getStoreInfo( + @PathVariable Long storeId ){ - BaseSuccessCode code = StoreSuccessCode.OK; - return ApiResponse.onSuccess(code, storeService.getStoreInfo(dto)); + return ApiResponse.onSuccess(StoreSuccessCode.STORE_SUCCESS_CODE, + storeService.getStoreInfo(storeId)); } - @PostMapping("/v1/stores/{storeId}/reviews") - public ApiResponse getReviewInfo( - @RequestBody StoreReqDTO.GetReviewInfo dto + // 리뷰 조회 + @GetMapping("/{storeId}/reviews") + public ApiResponse getReviewInfo( + @PathVariable Long storeId, + @RequestParam Long memberId ){ - BaseSuccessCode code = ReviewSuccessCode.OK; - return ApiResponse.onSuccess(code, storeService.getReviewInfo(dto)); + return ApiResponse.onSuccess(StoreSuccessCode.REVIEW_SUCCESS_CODE, + storeService.getReviewInfo(memberId, storeId)); } - @PostMapping("/v1/stores/create") - public ApiResponse createStore() { - storeService.createStore(); - return ApiResponse.onSuccess(StoreSuccessCode.OK, "저장 완료"); + // 가게 생성 + @PostMapping + public ApiResponse createStore( + @RequestBody @Valid StoreReqDTO.CreateStoreDTO dto + ){ + return ApiResponse.onSuccess(StoreSuccessCode.STORE_CREATED, + storeService.createStore(dto)); } - @PostMapping("/v1/stores/reviews/create") - public ApiResponse createReview() { - storeService.createReview(); - return ApiResponse.onSuccess(ReviewSuccessCode.OK, "저장 완료"); + // 리뷰 생성 + @PostMapping("/{storeId}/reviews") + public ApiResponse createReview( + @PathVariable Long storeId, + @RequestParam Long memberId, + @RequestBody @Valid StoreReqDTO.CreateReviewDTO dto + ){ + return ApiResponse.onSuccess(StoreSuccessCode.REVIEW_CREATED, + storeService.createReview(memberId, storeId, dto)); } - - } \ No newline at end of file diff --git a/src/main/java/umc/domain/store/converter/StoreConverter.java b/src/main/java/umc/domain/store/converter/StoreConverter.java index 546f4326..325f57e6 100644 --- a/src/main/java/umc/domain/store/converter/StoreConverter.java +++ b/src/main/java/umc/domain/store/converter/StoreConverter.java @@ -1,15 +1,55 @@ package umc.domain.store.converter; import umc.domain.member.entity.Member; +import umc.domain.store.dto.StoreReqDTO; import umc.domain.store.dto.StoreResDTO; import umc.domain.store.entity.Review; import umc.domain.store.entity.Store; public class StoreConverter { - public static StoreResDTO.GetStoreInfo toGetStoreInfo( - Store store + + // 가게 생성 (ReqDTO → Entity) + public static Store toPutStore(StoreReqDTO.CreateStoreDTO dto) { + return Store.builder() + .store_nm(dto.store_nm()) + .region_nm(dto.region_nm()) + .open_dt(dto.open_dt()) + .close_dt(dto.close_dt()) + .build(); + } + + // 가게 생성 조회 (Entity → ResDTO) + public static StoreResDTO.GetCreateStoreDTO toGetStore(Store store) { + return new StoreResDTO.GetCreateStoreDTO( + store.getId(), + store.getCreatedAt() + ); + } + + // 리뷰 생성 (ReqDTO → Entity) + public static Review toPutReview( + StoreReqDTO.CreateReviewDTO dto, Member member, Store store ) { - return StoreResDTO.GetStoreInfo.builder() + return Review.builder() + .review_text(dto.review_text()) + .star_point(dto.star_point()) + .img_id(dto.img_id()) + .member(member) + .store(store) + .build(); + } + + // 리뷰 생성 조회 (Entity → ResDTO) + public static StoreResDTO.GetCreateReviewDTO toGetReview(Review review) { + return new StoreResDTO.GetCreateReviewDTO( + review.getId(), + review.getCreatedAt() + ); + } + + // 가게 조회 + public static StoreResDTO.GetStoreInfoDTO toGetStoreInfo(Store store) { + return StoreResDTO.GetStoreInfoDTO.builder() .store_nm(store.getStore_nm()) .region_nm(store.getRegion_nm()) .open_dt(store.getOpen_dt()) @@ -17,13 +57,12 @@ public static StoreResDTO.GetStoreInfo toGetStoreInfo( .build(); } - public static StoreResDTO.GetReviewInfo toGetReviewInfo( - Review review - ) { - return StoreResDTO.GetReviewInfo.builder() + // 리뷰 조회 + public static StoreResDTO.GetReviewInfoDTO toGetReviewInfo(Review review) { + return StoreResDTO.GetReviewInfoDTO.builder() .review_text(review.getReview_text()) .star_point(review.getStar_point()) .img_id(review.getImg_id()) .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/umc/domain/store/dto/StoreReqDTO.java b/src/main/java/umc/domain/store/dto/StoreReqDTO.java index 5b7747b0..171daa7c 100644 --- a/src/main/java/umc/domain/store/dto/StoreReqDTO.java +++ b/src/main/java/umc/domain/store/dto/StoreReqDTO.java @@ -1,14 +1,30 @@ package umc.domain.store.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + public class StoreReqDTO { - public record GetStoreInfo( - Long store_id + // 가게 생성 + public record CreateStoreDTO( + @NotBlank + String store_nm, + @NotBlank + String region_nm, + @NotBlank + String open_dt, + @NotBlank + String close_dt ){} - // 멤버 번호, 가게 번호로 찾기 - public record GetReviewInfo( - Long member_id, - Long store_id + // 리뷰 생성 + public record CreateReviewDTO( + @NotBlank + String review_text, + @NotBlank + String star_point, + @NotBlank + String img_id ){} -} + +} \ No newline at end of file diff --git a/src/main/java/umc/domain/store/dto/StoreResDTO.java b/src/main/java/umc/domain/store/dto/StoreResDTO.java index 7ee88007..f973b50d 100644 --- a/src/main/java/umc/domain/store/dto/StoreResDTO.java +++ b/src/main/java/umc/domain/store/dto/StoreResDTO.java @@ -2,23 +2,38 @@ import lombok.Builder; +import java.time.LocalDateTime; + public class StoreResDTO { + + // 가게 생성 응답 + @Builder + public record GetCreateStoreDTO( + Long store_id, + LocalDateTime createdAt + ){} + + // 리뷰 생성 응답 @Builder - public record GetStoreInfo( + public record GetCreateReviewDTO( + Long review_id, + LocalDateTime createdAt + ){} + + // 가게 조회 응답 + @Builder + public record GetStoreInfoDTO( String store_nm, String region_nm, String open_dt, String close_dt ){} + // 리뷰 조회 응답 @Builder - public record GetReviewInfo( + public record GetReviewInfoDTO( String review_text, String star_point, - String img_id, - String member_id, - String store_id + String img_id ){} - - } diff --git a/src/main/java/umc/domain/store/enums/ReviewSuccessCode.java b/src/main/java/umc/domain/store/enums/ReviewSuccessCode.java deleted file mode 100644 index 12018744..00000000 --- a/src/main/java/umc/domain/store/enums/ReviewSuccessCode.java +++ /dev/null @@ -1,17 +0,0 @@ -package umc.domain.store.enums; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import umc.global.apiPayload.code.BaseSuccessCode; - -@Getter -@RequiredArgsConstructor -public enum ReviewSuccessCode implements BaseSuccessCode { - - OK(HttpStatus.OK, "REVIEW200_1", "성공적으로 리뷰를 조회했습니다."); - - private final HttpStatus status; - private final String code; - private final String message; -} \ No newline at end of file diff --git a/src/main/java/umc/domain/store/enums/StoreSuccessCode.java b/src/main/java/umc/domain/store/enums/StoreSuccessCode.java deleted file mode 100644 index 93296657..00000000 --- a/src/main/java/umc/domain/store/enums/StoreSuccessCode.java +++ /dev/null @@ -1,17 +0,0 @@ -package umc.domain.store.enums; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import umc.global.apiPayload.code.BaseSuccessCode; - -@Getter -@RequiredArgsConstructor -public enum StoreSuccessCode implements BaseSuccessCode { - - OK(HttpStatus.OK, "STORE200_1", "성공적으로 가게를 조회했습니다."); - - private final HttpStatus status; - private final String code; - private final String message; -} \ No newline at end of file diff --git a/src/main/java/umc/domain/store/exception/code/StoreSuccessCode.java b/src/main/java/umc/domain/store/exception/code/StoreSuccessCode.java index 87b5d496..ea3d6804 100644 --- a/src/main/java/umc/domain/store/exception/code/StoreSuccessCode.java +++ b/src/main/java/umc/domain/store/exception/code/StoreSuccessCode.java @@ -7,9 +7,15 @@ @Getter @RequiredArgsConstructor - public enum StoreSuccessCode implements BaseSuccessCode { - OK(HttpStatus.OK, "COMMON200_1", "성공적으로 요청을 처리했습니다."); // ← 세미콜론! + + STORE_CREATED(HttpStatus.CREATED, "STORE200_1", "성공적으로 가게를 생성했습니다."), + STORE_SUCCESS_CODE(HttpStatus.OK, "STORE200_2", "성공적으로 가게를 조회했습니다."), + + REVIEW_CREATED(HttpStatus.CREATED, "REVIEW200_1", "성공적으로 리뷰를 생성했습니다."), + REVIEW_SUCCESS_CODE(HttpStatus.OK, "REVIEW200_2", "성공적으로 리뷰를 조회했습니다."), + + ; private final HttpStatus status; private final String code; diff --git a/src/main/java/umc/domain/store/service/StoreService.java b/src/main/java/umc/domain/store/service/StoreService.java index 7843f5e8..68410ab8 100644 --- a/src/main/java/umc/domain/store/service/StoreService.java +++ b/src/main/java/umc/domain/store/service/StoreService.java @@ -1,9 +1,11 @@ package umc.domain.store.service; -import lombok.Builder; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import umc.domain.member.entity.Member; +import umc.domain.member.exception.MemberException; +import umc.domain.member.exception.code.MemberErrorCode; import umc.domain.store.converter.StoreConverter; import umc.domain.store.dto.StoreReqDTO; import umc.domain.store.dto.StoreResDTO; @@ -24,49 +26,41 @@ public class StoreService { private final ReviewRepository reviewRepository; private final MemberRepository memberRepository; - public StoreResDTO.GetStoreInfo getStoreInfo(StoreReqDTO.GetStoreInfo dto) { - Long storeId = dto.store_id(); + // 가게 조회 + public StoreResDTO.GetStoreInfoDTO getStoreInfo(Long storeId) { Store store = storeRepository.findById(storeId) .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); return StoreConverter.toGetStoreInfo(store); } - public StoreResDTO.GetReviewInfo getReviewInfo(StoreReqDTO.GetReviewInfo dto) { + // 리뷰 조회 + public StoreResDTO.GetReviewInfoDTO getReviewInfo(Long memberId, Long storeId) { List reviews = reviewRepository - .findByMemberIdAndStoreId(dto.member_id(), dto.store_id()); - + .findByMemberIdAndStoreId(memberId, storeId); if (reviews.isEmpty()) { throw new StoreException(StoreErrorCode.REVIEW_NOT_FOUND); } - return StoreConverter.toGetReviewInfo(reviews.get(0)); } - public void createStore() { - Store store = Store.builder() - .store_nm("중국집") - .region_nm("서울시 구로구") - .open_dt("060001") - .close_dt("225959") - .build(); - - storeRepository.save(store); // ← 여기서 INSERT 실행 + // 가게 생성 + @Transactional + public StoreResDTO.GetCreateStoreDTO createStore(StoreReqDTO.CreateStoreDTO dto) { + Store store = StoreConverter.toPutStore(dto); + return StoreConverter.toGetStore(storeRepository.save(store)); } - public void createReview() { - Member member = memberRepository.findById(2L) - .orElseThrow(() -> new RuntimeException("멤버 없음")); - Store store = storeRepository.findById(2L) - .orElseThrow(() -> new RuntimeException("가게 없음")); - - Review review = Review.builder() - .review_text("음~ 너무 맛있어요!") - .star_point("5") - .img_id("이미지 아이디") - .member(member) // ← Member 객체 - .store(store) - .build(); + // 리뷰 생성 + @Transactional + public StoreResDTO.GetCreateReviewDTO createReview( + Long memberId, Long storeId, StoreReqDTO.CreateReviewDTO dto + ) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); - reviewRepository.save(review); // ← 여기서 INSERT 실행 + Review review = StoreConverter.toPutReview(dto, member, store); + return StoreConverter.toGetReview(reviewRepository.save(review)); } } \ No newline at end of file diff --git a/src/main/java/umc/global/config/SecurityConfig.java b/src/main/java/umc/global/config/SecurityConfig.java index fe4f6ed9..0472716a 100644 --- a/src/main/java/umc/global/config/SecurityConfig.java +++ b/src/main/java/umc/global/config/SecurityConfig.java @@ -1,5 +1,6 @@ package umc.global.config; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -8,44 +9,75 @@ 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; +import umc.global.security.filter.JwtAuthFilter; +import umc.global.security.handler.CustomAccessDenied; +import umc.global.security.handler.CustomEntryPoint; +import umc.global.security.service.CustomUserDetailsService; +import umc.global.security.util.JwtUtil; @EnableWebSecurity @Configuration +@RequiredArgsConstructor // 추가 public class SecurityConfig { + // 추가 + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + private final String[] allowUris = { - // Swagger 허용 "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", - // 회원가입은 로그인 없이 가능해야 함 - "/api/v1/member/me" // 회원가입 경로 + // 로그인, 회원가입은 인증 없이 가능 + "/api/members/signup", + "/api/members/login" }; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth + .authorizeHttpRequests(requests -> requests .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) + .formLogin(AbstractHttpConfigurer::disable) // 변경 + .sessionManagement(AbstractHttpConfigurer::disable) // 추가 + // JWT 필터 추가 + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) .logout(logout -> logout .logoutUrl("/logout") - .logoutSuccessUrl("/swagger-ui/index.html") + .logoutSuccessUrl("/login?logout") .permitAll() + ) + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDenied()) + .authenticationEntryPoint(customEntryPoint()) ); return http.build(); } + // 추가 + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public CustomAccessDenied customAccessDenied() { + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint() { + return new CustomEntryPoint(); + } } \ No newline at end of file diff --git a/src/main/java/umc/global/security/filter/JwtAuthFilter.java b/src/main/java/umc/global/security/filter/JwtAuthFilter.java new file mode 100644 index 00000000..6e5bbb57 --- /dev/null +++ b/src/main/java/umc/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,74 @@ +package umc.global.security.filter; + +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 umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.BaseErrorCode; +import umc.global.apiPayload.code.GeneralErrorCode; +import umc.global.security.service.CustomUserDetailsService; +import umc.global.security.util.JwtUtil; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @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이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + 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) { + ObjectMapper mapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code,null); + + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/security/handler/CustomAccessDenied.java b/src/main/java/umc/global/security/handler/CustomAccessDenied.java new file mode 100644 index 00000000..1cdee86d --- /dev/null +++ b/src/main/java/umc/global/security/handler/CustomAccessDenied.java @@ -0,0 +1,37 @@ +package umc.global.security.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; +import umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.BaseErrorCode; +import umc.global.apiPayload.code.GeneralErrorCode; + +import java.io.IOException; + +@Component +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.FORBIDDEN; + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/security/handler/CustomEntryPoint.java b/src/main/java/umc/global/security/handler/CustomEntryPoint.java new file mode 100644 index 00000000..268eebf4 --- /dev/null +++ b/src/main/java/umc/global/security/handler/CustomEntryPoint.java @@ -0,0 +1,37 @@ +package umc.global.security.handler; + +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 org.springframework.stereotype.Component; +import umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.BaseErrorCode; +import umc.global.apiPayload.code.GeneralErrorCode; + +import java.io.IOException; + +@Component +public class CustomEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; // ← 차이점 1: 401 + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/security/util/JwtUtil.java b/src/main/java/umc/global/security/util/JwtUtil.java new file mode 100644 index 00000000..6e002030 --- /dev/null +++ b/src/main/java/umc/global/security/util/JwtUtil.java @@ -0,0 +1,89 @@ +package umc.global.security.util; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; +import umc.global.security.entity.AuthMember; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +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, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(AuthMember member) { + return createToken(member, accessExpiration); + } + + /** 토큰에서 이메일 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(AuthMember member, Duration expiration) { + Instant now = Instant.now(); + + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUsername()) + .claim("role", authorities) + .claim("email", member.getUsername()) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plus(expiration))) + .signWith(secretKey) + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} \ No newline at end of file