From 4eee16d9ed8ac4b7db8776b7a11c96aa844ffe81 Mon Sep 17 00:00:00 2001 From: MinJunKKang Date: Thu, 14 Aug 2025 14:09:44 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=88=EA=B5=AC=EB=A7=88=EA=B5=AC=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../canfly/domain/field/FieldBookmark.java | 3 ++ .../canfly/domain/major/MajorBookmark.java | 3 ++ .../canfly/domain/user/Address.java | 29 ++++++++++++++ .../likelion13th/canfly/domain/user/User.java | 11 ++++-- .../canfly/global/api/SuccessCode.java | 1 + .../login/auth/mapper/CustomUserDetails.java | 3 +- .../auth/utils/OAuth2SuccessHandler.java | 11 ++++-- .../login/controller/UserController.java | 19 +++++++++- .../canfly/login/dto/ProfileRequestDto.java | 38 +++++++++++++++++++ .../canfly/login/service/UserService.java | 38 +++++++++++++++++++ 10 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 src/main/java/hackerthon/likelion13th/canfly/domain/user/Address.java create mode 100644 src/main/java/hackerthon/likelion13th/canfly/login/dto/ProfileRequestDto.java diff --git a/src/main/java/hackerthon/likelion13th/canfly/domain/field/FieldBookmark.java b/src/main/java/hackerthon/likelion13th/canfly/domain/field/FieldBookmark.java index da94d3f..b285c5c 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/domain/field/FieldBookmark.java +++ b/src/main/java/hackerthon/likelion13th/canfly/domain/field/FieldBookmark.java @@ -20,6 +20,9 @@ public class FieldBookmark extends BaseEntity { @Column(name = "fbId", nullable = false) private Integer id; + @Column(name = "fieldRecommend") + private Boolean isRecommend = false; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fId", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) diff --git a/src/main/java/hackerthon/likelion13th/canfly/domain/major/MajorBookmark.java b/src/main/java/hackerthon/likelion13th/canfly/domain/major/MajorBookmark.java index f2b2525..fd05ad5 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/domain/major/MajorBookmark.java +++ b/src/main/java/hackerthon/likelion13th/canfly/domain/major/MajorBookmark.java @@ -21,6 +21,9 @@ public class MajorBookmark extends BaseEntity { @Column(name = "mbId", nullable = false) private Integer id; + @Column(name = "majorRecommend") + private Boolean isRecommend = false; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "mId", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) diff --git a/src/main/java/hackerthon/likelion13th/canfly/domain/user/Address.java b/src/main/java/hackerthon/likelion13th/canfly/domain/user/Address.java new file mode 100644 index 0000000..ca7f17b --- /dev/null +++ b/src/main/java/hackerthon/likelion13th/canfly/domain/user/Address.java @@ -0,0 +1,29 @@ +package hackerthon.likelion13th.canfly.domain.user; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Address { + + @Column(name = "zipcode", length = 20, nullable = true) + private String zipcode; + + @Column(name = "address", nullable = true) + private String address; + + @Column(name = "address_detail", nullable = true) + private String addressDetail; + + public boolean isEmpty() { + return (zipcode == null || zipcode.isBlank()) + && (address == null || address.isBlank()) + && (addressDetail == null || addressDetail.isBlank()); + } +} \ No newline at end of file diff --git a/src/main/java/hackerthon/likelion13th/canfly/domain/user/User.java b/src/main/java/hackerthon/likelion13th/canfly/domain/user/User.java index 4ffeddc..699d451 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/domain/user/User.java +++ b/src/main/java/hackerthon/likelion13th/canfly/domain/user/User.java @@ -1,10 +1,8 @@ package hackerthon.likelion13th.canfly.domain.user; -import hackerthon.likelion13th.canfly.domain.cst.Cst; import hackerthon.likelion13th.canfly.domain.entity.BaseEntity; import hackerthon.likelion13th.canfly.domain.entity.Sex; import hackerthon.likelion13th.canfly.domain.field.FieldBookmark; -import hackerthon.likelion13th.canfly.domain.hmt.Hmt; import hackerthon.likelion13th.canfly.domain.major.MajorBookmark; import hackerthon.likelion13th.canfly.domain.mock.Mock; import hackerthon.likelion13th.canfly.domain.report.Report; @@ -50,6 +48,10 @@ public class User extends BaseEntity { @Column(name = "gradeNum") private Byte gradeNum; + @Setter + @Embedded + private Address address; + @Column(nullable = false) @Builder.Default private int token = 30; @@ -73,7 +75,6 @@ public void addMock(Mock mock) { mock.setUser(this); } - @Builder.Default @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List reportList = new ArrayList<>(); @@ -82,5 +83,7 @@ public void addReport(Report report) { report.setUser(this); } - + public void updateAddress(Address address) { + this.address = address; + } } diff --git a/src/main/java/hackerthon/likelion13th/canfly/global/api/SuccessCode.java b/src/main/java/hackerthon/likelion13th/canfly/global/api/SuccessCode.java index 72a2c73..1b3bb5f 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/global/api/SuccessCode.java +++ b/src/main/java/hackerthon/likelion13th/canfly/global/api/SuccessCode.java @@ -14,6 +14,7 @@ public enum SuccessCode implements BaseCode { // 성공 USER_LOGOUT_SUCCESS(HttpStatus.OK, "USER_2001", "로그아웃 되었습니다."), USER_REISSUE_SUCCESS(HttpStatus.OK, "USER_2002", "토큰 재발급이 완료되었습니다."), USER_DELETE_SUCCESS(HttpStatus.OK, "USER_2003", "회원탈퇴가 완료되었습니다."), + USER_PROFILE_UPDATE_SUCCESS(HttpStatus.OK, "USER_2006", "프로필 저장이 완료되었습니다."), MOCK_CREATE_SUCCESS(HttpStatus.CREATED, "MOCK_2011", "모의고사 등록이 완료되었습니다."), MOCKSCORE_CREATE_SUCCESS(HttpStatus.CREATED, "MOCKSCORE_2011", "모의고사 성적 등록이 완료되었습니다."), diff --git a/src/main/java/hackerthon/likelion13th/canfly/login/auth/mapper/CustomUserDetails.java b/src/main/java/hackerthon/likelion13th/canfly/login/auth/mapper/CustomUserDetails.java index 17b646a..5b1d172 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/login/auth/mapper/CustomUserDetails.java +++ b/src/main/java/hackerthon/likelion13th/canfly/login/auth/mapper/CustomUserDetails.java @@ -1,12 +1,12 @@ package hackerthon.likelion13th.canfly.login.auth.mapper; +import hackerthon.likelion13th.canfly.domain.user.Address; import hackerthon.likelion13th.canfly.domain.user.OAuth; import hackerthon.likelion13th.canfly.domain.user.User; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -25,6 +25,7 @@ public class CustomUserDetails implements UserDetails { private String providerId; private String username; private String nickname; + private Address address; private String email; private String provider; private String accessToken; diff --git a/src/main/java/hackerthon/likelion13th/canfly/login/auth/utils/OAuth2SuccessHandler.java b/src/main/java/hackerthon/likelion13th/canfly/login/auth/utils/OAuth2SuccessHandler.java index dcc6765..59253a9 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/login/auth/utils/OAuth2SuccessHandler.java +++ b/src/main/java/hackerthon/likelion13th/canfly/login/auth/utils/OAuth2SuccessHandler.java @@ -33,7 +33,8 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private List getAuthorizedUris() { return List.of( defaultRedirect, - "http://localhost:3000" + "http://localhost:3000", + "http://localhost:3000/" ); } @@ -74,15 +75,19 @@ public void onAuthenticationSuccess( JwtDto jwt = userService.jwtMakeSave(providerId); log.info("🔑 JWT 발급 완료 | providerId={}", providerId); - /* 5. redirect_uri 검증 */ + /* 5. 프로필 미완료 여부(needsProfile) 계산 */ + boolean needsProfile = userService.needsProfile(providerId); + + /* 6. redirect_uri 검증 */ String frontRedirect = request.getParameter("redirect_uri"); if (frontRedirect == null || !getAuthorizedUris().contains(frontRedirect)) { frontRedirect = defaultRedirect; } - /* 6. accessToken 쿼리 파라미터로 리다이렉트 */ + /* 7. accessToken + needsProfile로 리다이렉트 */ String redirectUrl = UriComponentsBuilder.fromUriString(frontRedirect) .queryParam("accessToken", jwt.getAccessToken()) + .queryParam("needsProfile", needsProfile) .build() .toUriString(); diff --git a/src/main/java/hackerthon/likelion13th/canfly/login/controller/UserController.java b/src/main/java/hackerthon/likelion13th/canfly/login/controller/UserController.java index d55e7fd..c0b1584 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/login/controller/UserController.java +++ b/src/main/java/hackerthon/likelion13th/canfly/login/controller/UserController.java @@ -7,6 +7,7 @@ import hackerthon.likelion13th.canfly.login.auth.mapper.CustomUserDetails; import hackerthon.likelion13th.canfly.login.dto.CoinRequestDto; import hackerthon.likelion13th.canfly.login.dto.CoinResponseDto; +import hackerthon.likelion13th.canfly.login.dto.ProfileRequestDto; import hackerthon.likelion13th.canfly.login.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -17,8 +18,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.Map; - @Tag(name = "회원", description = "회원 관련 api 입니다.") @RestController @RequiredArgsConstructor @@ -73,6 +72,22 @@ public ApiResponse updateUserCoins( // 3. 최종적으로 변환된 DTO를 클라이언트에게 전달합니다. return ApiResponse.onSuccess(SuccessCode.TOKEN_PROCESS_SUCCESS, responseDTO); } + + @Operation(summary = "프로필 수정", description = "팝업에서 입력한 전화번호/성별/학교/학년을 저장합니다 -> 성별 (MAN/WOMAN), 학년 (1, 2, 3)") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "USER_2006", description = "프로필 저장이 완료되었습니다.") + }) + @PutMapping("/me/profile") + public ApiResponse completeProfile( + @AuthenticationPrincipal CustomUserDetails principal, + @RequestBody ProfileRequestDto req + ) { + String providerId = principal.getUsername(); // providerId (CustomUserDetails.getUsername()가 providerId를 반환하도록 한 현재 설계) + boolean updated = userService.completeProfile(providerId, req); + return ApiResponse.onSuccess(SuccessCode.USER_PROFILE_UPDATE_SUCCESS, updated); + } + /* @Operation(summary = "프로필 사진 첨부", description = "프로필 사진을 첨부하는 메서드입니다.") @ApiResponses(value = { diff --git a/src/main/java/hackerthon/likelion13th/canfly/login/dto/ProfileRequestDto.java b/src/main/java/hackerthon/likelion13th/canfly/login/dto/ProfileRequestDto.java new file mode 100644 index 0000000..a64b137 --- /dev/null +++ b/src/main/java/hackerthon/likelion13th/canfly/login/dto/ProfileRequestDto.java @@ -0,0 +1,38 @@ +package hackerthon.likelion13th.canfly.login.dto; + +import hackerthon.likelion13th.canfly.domain.entity.Sex; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +@Schema(description = "ProfileReqDto") +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ProfileRequestDto { + @Schema(description = "전화번호") + @Pattern( + regexp = "^[0-9\\-]{9,15}$", + message = "전화번호 형식이 올바르지 않습니다." + ) + private String phoneNumber; + + @Schema(description = "성별") + private Sex sex; + + @Schema(description = "고등학교") + private String highschool; + + @Schema(description = "학년") + private Byte gradeNum; + + @Schema(description = "우편번호") + private String zipcode; + + @Schema(description = "주소") + private String address; + + @Schema(description = "상세주소") + private String addressDetail; +} diff --git a/src/main/java/hackerthon/likelion13th/canfly/login/service/UserService.java b/src/main/java/hackerthon/likelion13th/canfly/login/service/UserService.java index 9c8185b..590d064 100644 --- a/src/main/java/hackerthon/likelion13th/canfly/login/service/UserService.java +++ b/src/main/java/hackerthon/likelion13th/canfly/login/service/UserService.java @@ -1,5 +1,6 @@ package hackerthon.likelion13th.canfly.login.service; +import hackerthon.likelion13th.canfly.domain.user.Address; import hackerthon.likelion13th.canfly.domain.user.User; import hackerthon.likelion13th.canfly.global.api.ErrorCode; import hackerthon.likelion13th.canfly.global.exception.GeneralException; @@ -8,6 +9,7 @@ import hackerthon.likelion13th.canfly.login.auth.jwt.JwtTokenUtils; import hackerthon.likelion13th.canfly.login.auth.service.JpaUserDetailsManager; import hackerthon.likelion13th.canfly.login.converter.UserConverter; +import hackerthon.likelion13th.canfly.login.dto.ProfileRequestDto; import hackerthon.likelion13th.canfly.login.dto.UserRequestDto; import hackerthon.likelion13th.canfly.login.repository.OAuthRepository; import hackerthon.likelion13th.canfly.login.repository.UserRepository; @@ -126,6 +128,42 @@ public void deleteUser(String providerId) { userRepository.delete(user); } + public boolean needsProfile(String providerId) { + User user = findUserByProviderId(providerId); + + boolean missingSex = (user.getSex() == null); + boolean missingPhone = (user.getPhoneNumber() == null || user.getPhoneNumber().isBlank()); + boolean missingHighschool = (user.getHighschool() == null || user.getHighschool().isBlank()); + + Address addr = user.getAddress(); + boolean missingAddress = (addr == null) || addr.isEmpty(); + + return missingSex || missingPhone || missingHighschool || missingAddress; + } + + @Transactional + public boolean completeProfile(String providerId, ProfileRequestDto req) { + User user = findUserByProviderId(providerId); + + user.setPhoneNumber(req.getPhoneNumber()); + user.setSex(req.getSex()); + user.setHighschool(req.getHighschool()); + user.setGradeNum(req.getGradeNum()); + + String zipcode = req.getZipcode(); + String address = req.getAddress(); + String detail = req.getAddressDetail(); + + // 새로운 주소 설정 + Address newAddress = new Address(zipcode, address, detail); + user.updateAddress(newAddress); // User 엔티티에 주소 업데이트 + userRepository.save(user); + + boolean stillNeeds = this.needsProfile(providerId); + + return !stillNeeds; + } + public String checkMemberByName(String username) { User user = findUserByUserName(username); if (user.getPhoneNumber() != null) return "wasUser";