diff --git a/settings.gradle b/settings.gradle index 12485962..fd337a17 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,5 +2,13 @@ pluginManagement { plugins { id 'org.jetbrains.kotlin.jvm' version '2.2.0' } + repositories { + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' } + rootProject.name = 'Assu' diff --git a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java index 35a2744b..a63b1f63 100644 --- a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java +++ b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java @@ -1,24 +1,24 @@ package com.assu.server.domain.auth.controller; -import com.assu.server.domain.auth.dto.login.CommonLoginRequest; -import com.assu.server.domain.auth.dto.login.LoginResponse; -import com.assu.server.domain.auth.dto.login.RefreshResponse; -import com.assu.server.domain.auth.dto.phone.PhoneAuthRequestDTO; -import com.assu.server.domain.auth.dto.signup.AdminSignUpRequest; -import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequest; -import com.assu.server.domain.auth.dto.signup.SignUpResponse; -import com.assu.server.domain.auth.dto.signup.StudentTokenSignUpRequest; -import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload; -import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest; -import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse; +import com.assu.server.domain.auth.dto.login.CommonLoginRequestDTO; +import com.assu.server.domain.auth.dto.login.LoginResponseDTO; +import com.assu.server.domain.auth.dto.login.RefreshResponseDTO; +import com.assu.server.domain.auth.dto.phone.PhoneAuthSendRequestDTO; +import com.assu.server.domain.auth.dto.phone.PhoneAuthVerifyRequestDTO; +import com.assu.server.domain.auth.dto.signup.AdminSignUpRequestDTO; +import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequestDTO; +import com.assu.server.domain.auth.dto.signup.SignUpResponseDTO; +import com.assu.server.domain.auth.dto.signup.StudentTokenSignUpRequestDTO; +import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayloadDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthRequestDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthResponseDTO; +import com.assu.server.domain.auth.dto.email.EmailVerificationCheckRequestDTO; import com.assu.server.domain.auth.service.*; import com.assu.server.domain.user.entity.enums.University; -import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -57,9 +57,19 @@ public class AuthController { ) @PostMapping("/phone-verification/check-and-send") public BaseResponse checkPhoneAvailabilityAndSendAuthNumber( - @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthSendRequest request + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "휴대폰 인증번호 발송 요청", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PhoneAuthSendRequestDTO.class) + ) + ) + @RequestBody + @Valid + PhoneAuthSendRequestDTO request ) { - phoneAuthService.checkAndSendAuthNumber(request.getPhoneNumber()); + phoneAuthService.checkAndSendAuthNumber(request.phoneNumber()); return BaseResponse.onSuccess(SuccessStatus.SEND_AUTH_NUMBER_SUCCESS, null); } @@ -76,16 +86,27 @@ public BaseResponse checkPhoneAvailabilityAndSendAuthNumber( ) @PostMapping("/phone-verification/verify") public BaseResponse checkAuthNumber( - @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthVerifyRequest request + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "휴대폰 인증번호 검증 요청", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PhoneAuthVerifyRequestDTO.class) + ) + ) + @RequestBody + @Valid + PhoneAuthVerifyRequestDTO request ) { phoneAuthService.verifyAuthNumber( - request.getPhoneNumber(), - request.getAuthNumber() + request.phoneNumber(), + request.authNumber() ); return BaseResponse.onSuccess(SuccessStatus.VERIFY_AUTH_NUMBER_SUCCESS, null); } - @Operation(summary = "이메일 형식 및 중복가입 확인 API", + @Operation( + summary = "이메일 형식 및 중복가입 확인 API", description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2551197c19ed802d8f6dd373dd045f3a?source=copy_link)\n" + "- 입력한 이메일이 이미 가입된 사용자가 있는지 확인합니다.\n" + "- 중복된 이메일이 있으면 에러를 반환합니다.\n" + @@ -93,10 +114,22 @@ public BaseResponse checkAuthNumber( " - `email` (String, required): 확인할 이메일 주소\n" + "\n**Response:**\n" + " - 성공 시 200(OK)과 사용 가능 메시지 반환\n" + - " - 중복 시 404(NOT_FOUND)와 에러 메시지 반환") + " - 중복 시 404(NOT_FOUND)와 에러 메시지 반환" + ) @PostMapping("/email-verification/check") public BaseResponse checkEmailAvailability( - @RequestBody @Valid VerificationRequestDTO.EmailVerificationCheckRequest request) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "이메일 중복 확인 요청", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = EmailVerificationCheckRequestDTO.class) + ) + ) + @RequestBody + @Valid + EmailVerificationCheckRequestDTO request + ) { emailAuthService.checkEmailAvailability(request); return BaseResponse.onSuccess(SuccessStatus._OK, null); } @@ -126,14 +159,24 @@ public BaseResponse checkEmailAvailability( " - `name` (String): 학생 이름\n" + " - `university` (String): 대학교 (한글명)\n" + " - `department` (String): 단과대 (한글명)\n" + - " - `major` (String): 전공/학과 (한글명)") - @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(schema = @Schema(implementation = StudentTokenSignUpRequest.class))) + " - `major` (String): 전공/학과 (한글명)" + ) @PostMapping(value = "/students/signup", consumes = MediaType.APPLICATION_JSON_VALUE) - public BaseResponse signupStudent( - @Valid @RequestBody StudentTokenSignUpRequest request + public BaseResponse signupStudent( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "JSON 형식의 학생 유저 가입 정보", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = StudentTokenSignUpRequestDTO.class) + ) + ) + @RequestBody + @Valid + StudentTokenSignUpRequestDTO request ) { - SignUpResponse response; - if(request.getStudentTokenAuth().getUniversity().equals(University.SSU)){ + SignUpResponseDTO response; + if(request.studentTokenAuth().university().equals(University.SSU)){ response = signUpService.signupSsuStudent(request); } else { response = null; @@ -141,7 +184,8 @@ public BaseResponse signupStudent( return BaseResponse.onSuccess(SuccessStatus._OK, response); } - @Operation(summary = "제휴업체 회원가입 API", + @Operation( + summary = "제휴업체 회원가입 API", description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed80d7a8f2c3a6fcd8b537)\n" + "- `multipart/form-data`로 호출합니다.\n" + "- 파트: `payload`(JSON, PartnerSignUpRequest) + `licenseImage`(파일, 사업자등록증).\n" + @@ -165,18 +209,37 @@ public BaseResponse signupStudent( " - `tokens` (Object): JWT 토큰 정보 (accessToken, refreshToken, expiresAt)\n" + " - `basicInfo` (UserBasicInfo): 사용자 기본 정보 (프론트 캐싱용)\n" + " - `name` (String): 업체명\n" + - " - `university`, `department`, `major`: null (Partner는 해당 없음)") + " - `university`, `department`, `major`: null (Partner는 해당 없음)" + ) @PostMapping(value = "/partners/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public BaseResponse signupPartner( - @Valid @RequestPart("request") @Parameter(description = "JSON 형식의 제휴업체 가입 정보", - // 'request' 파트의 content type을 명시적으로 지정 - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = PartnerSignUpRequest.class))) PartnerSignUpRequest request, - - @RequestPart("licenseImage") @Parameter(description = "사업자등록증 이미지 파일 (Multipart Part)", required = true, content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, schema = @Schema(type = "string", format = "binary"))) MultipartFile licenseImage) { + public BaseResponse signupPartner( + @RequestPart("request") + @Parameter( + description = "JSON 형식의 제휴업체 가입 정보", + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PartnerSignUpRequestDTO.class) + ) + ) + @Valid + PartnerSignUpRequestDTO request, + @RequestPart("licenseImage") + @Parameter( + description = "사업자등록증 이미지 파일", + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, + schema = @Schema(type = "string", format = "binary") + ) + ) + MultipartFile licenseImage + ) { return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupPartner(request, licenseImage)); } - @Operation(summary = "관리자 회원가입 API", + @Operation( + summary = "관리자 회원가입 API", description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48)\n" + "- `multipart/form-data`로 호출합니다.\n" + "- 파트: `payload`(JSON, AdminSignUpRequest) + `signImage`(파일, 신분증).\n" + @@ -204,30 +267,38 @@ public BaseResponse signupPartner( " - `name` (String): 단체명/관리자 이름\n" + " - `university` (String): 대학교 (한글명)\n" + " - `department` (String): 단과대 (한글명)\n" + - " - `major` (String): 전공/학과 (한글명)") + " - `major` (String): 전공/학과 (한글명)" + ) @PostMapping(value = "/admins/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public BaseResponse signupAdmin( - @Valid @RequestPart("request") + public BaseResponse signupAdmin( + @RequestPart("request") @Parameter( description = "JSON 형식의 관리자 가입 정보", - // 'request' 파트의 content type을 명시적으로 지정 - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = AdminSignUpRequest.class)) + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = AdminSignUpRequestDTO.class) + ) ) - AdminSignUpRequest request, + @Valid + AdminSignUpRequestDTO request, @RequestPart("signImage") @Parameter( description = "인감 이미지 파일 (Multipart Part)", required = true, - content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, - schema = @Schema(type = "string", format = "binary")) + content = @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, + schema = @Schema(type = "string", format = "binary") + ) ) - MultipartFile signImage) { + MultipartFile signImage + ) { return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupAdmin(request, signImage)); } - @Operation(summary = "공통 로그인 API" - , description = "# [v1.1 (2025-09-13)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50)\n" + + @Operation( + summary = "공통 로그인 API", + description = "# [v1.1 (2025-09-13)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50)\n" + "- `application/json`로 호출합니다.\n" + "- 바디: `LoginRequest(email, password)`.\n" + "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" + @@ -244,17 +315,28 @@ public BaseResponse signupAdmin( " - `tokens` (Object): JWT 토큰 정보 (accessToken, refreshToken, expiresAt)\n" + " - `basicInfo` (UserBasicInfo): 사용자 기본 정보 (프론트 캐싱용)\n" + " - `name` (String): 업체명/단체명/관리자 이름\n" + - " - `university`, `department`, `major`: Admin의 경우 한글명, Partner의 경우 null") - @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(schema = @Schema(implementation = CommonLoginRequest.class))) + " - `university`, `department`, `major`: Admin의 경우 한글명, Partner의 경우 null" + ) @PostMapping(value = "/commons/login", consumes = MediaType.APPLICATION_JSON_VALUE) - public BaseResponse loginCommon( - @RequestBody @Valid CommonLoginRequest request + public BaseResponse loginCommon( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "공통 로그인 요청 (파트너/관리자)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = CommonLoginRequestDTO.class) + ) + ) + @RequestBody + @Valid + CommonLoginRequestDTO request ) { return BaseResponse.onSuccess(SuccessStatus._OK, loginService.loginCommon(request)); } - @Operation(summary = "학생 로그인 API" - , description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8)\n" + + @Operation( + summary = "학생 로그인 API", + description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8)\n" + "- `application/json`로 호출합니다.\n" + "- 바디: `StudentTokenLoginRequest(sToken, sIdno, university)`.\n" + "- 처리: 유세인트 인증 → 기존 회원 확인 → JWT 토큰 발급.\n" + @@ -274,14 +356,24 @@ public BaseResponse loginCommon( " - `name` (String): 학생 이름\n" + " - `university` (String): 대학교 (한글명)\n" + " - `department` (String): 단과대 (한글명)\n" + - " - `major` (String): 전공/학과 (한글명)") - @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(schema = @Schema(implementation = StudentTokenAuthPayload.class))) + " - `major` (String): 전공/학과 (한글명)" + ) @PostMapping(value = "/students/login", consumes = MediaType.APPLICATION_JSON_VALUE) - public BaseResponse loginStudent( - @RequestBody @Valid StudentTokenAuthPayload request + public BaseResponse loginStudent( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "학생 토큰 로그인 요청", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = StudentTokenAuthPayloadDTO.class) + ) + ) + @RequestBody + @Valid + StudentTokenAuthPayloadDTO request ) { - LoginResponse response; - if(request.getUniversity().equals(University.SSU)){ + LoginResponseDTO response; + if(request.university().equals(University.SSU)){ response = loginService.loginSsuStudent(request); } else { response = null; @@ -301,18 +393,23 @@ public BaseResponse loginStudent( " - `RefreshToken` (String, required): 리프레시 토큰\n" + "\n**Response:**\n" + " - 성공 시 200(OK)과 `RefreshResponse` 객체 반환\n" + + " - 성공 시 200(OK)과 `RefreshResponse` 객체 반환\n" + " - `accessToken` (String): 새로운 액세스 토큰\n" + " - `refreshToken` (String): 새로운 리프레시 토큰\n" + - " - `expiresAt` (LocalDateTime): 새 토큰 만료 시각\n" + - " - 성공 시 200(OK)과 새 토큰/만료시각 반환." + " - `expiresAt` (LocalDateTime): 새 토큰 만료 시각" ) - @Parameters({ - @Parameter(name = "Authorization", description = "Access Token (만료 허용). 형식: `Bearer `", required = true, in = ParameterIn.HEADER, schema = @Schema(type = "string")), - @Parameter(name = "RefreshToken", description = "Refresh Token", required = true, in = ParameterIn.HEADER, schema = @Schema(type = "string")) - }) @PostMapping("/tokens/refresh") - public BaseResponse refreshToken( - @RequestHeader("RefreshToken") String refreshToken) { + public BaseResponse refreshToken( + @Parameter( + name = "RefreshToken", + description = "Refresh Token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(type = "string") + ) + @RequestHeader("RefreshToken") + String refreshToken + ) { return BaseResponse.onSuccess(SuccessStatus._OK, loginService.refresh(refreshToken)); } @@ -326,16 +423,20 @@ public BaseResponse refreshToken( ) @PostMapping("/logout") public BaseResponse logout( + @Parameter( + name = "Authorization", + description = "Access Token. 형식: `Bearer `", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(type = "string") + ) @RequestHeader("Authorization") - @Parameter(name = "Authorization", description = "Access Token. 형식: `Bearer `", required = true, - in = ParameterIn.HEADER, schema = @Schema(type = "string")) String authorization ) { logoutService.logout(authorization); return BaseResponse.onSuccess(SuccessStatus._OK, null); } - // 숭실대 인증 및 개인정보 조회 @Operation( summary = "숭실대 유세인트 인증 API", description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed808d9266e641e5c4ea14?source=copy_link)\n" + @@ -347,15 +448,21 @@ public BaseResponse logout( " 3) 유세인트 포털 페이지 접근 및 HTML 파싱\n" + " 4) 이름, 학번, 소속, 학적 상태, 학년/학기 정보 추출\n" + " 5) 소속 문자열을 전공 Enum(`Major`)으로 매핑\n" + - " 6) 인증 결과를 `USaintAuthResponse` DTO로 반환\n" - ) - @io.swagger.v3.oas.annotations.parameters.RequestBody( - required = true, - content = @Content(schema = @Schema(implementation = USaintAuthRequest.class)) + " 6) 인증 결과를 `USaintAuthResponse` DTO로 반환" ) @PostMapping(value = "/students/ssu-verify", consumes = MediaType.APPLICATION_JSON_VALUE) - public BaseResponse ssuAuth( - @RequestBody @Valid USaintAuthRequest request + public BaseResponse ssuAuth( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "유세인트 인증 요청", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = USaintAuthRequestDTO.class) + ) + ) + @RequestBody + @Valid + USaintAuthRequestDTO request ) { return BaseResponse.onSuccess(SuccessStatus._OK, ssuAuthService.uSaintAuth(request)); } @@ -374,13 +481,17 @@ public BaseResponse ssuAuth( ) @PatchMapping("/withdraw") public BaseResponse withdrawMember( + @Parameter( + name = "Authorization", + description = "Access Token. 형식: `Bearer `", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(type = "string") + ) @RequestHeader("Authorization") - @Parameter(name = "Authorization", description = "Access Token. 형식: `Bearer `", required = true, - in = ParameterIn.HEADER, schema = @Schema(type = "string")) String authorization ) { withdrawalService.withdrawCurrentUser(authorization); return BaseResponse.onSuccess(SuccessStatus._OK, null); } - } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/auth/converter/AuthConverter.java b/src/main/java/com/assu/server/domain/auth/converter/AuthConverter.java deleted file mode 100644 index 1ce68f08..00000000 --- a/src/main/java/com/assu/server/domain/auth/converter/AuthConverter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.auth.converter; - -public class AuthConverter { -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/common/TokensDTO.java b/src/main/java/com/assu/server/domain/auth/dto/common/TokensDTO.java new file mode 100644 index 00000000..a5cd0bc2 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/common/TokensDTO.java @@ -0,0 +1,16 @@ +package com.assu.server.domain.auth.dto.common; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "JWT 토큰 정보") +public record TokensDTO( + @Schema(description = "액세스 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + String accessToken, + + @Schema(description = "리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + String refreshToken +) { + public static TokensDTO of(String accessToken, String refreshToken) { + return new TokensDTO(accessToken, refreshToken); + } +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfo.java b/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfo.java deleted file mode 100644 index 6bbe804d..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.assu.server.domain.auth.dto.common; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "사용자 기본 정보") -public class UserBasicInfo { - - @Schema(description = "이름/업체명/단체명", example = "홍길동") - private String name; - - @Schema(description = "대학교", example = "숭실대학교") - private String university; - - @Schema(description = "단과대", example = "IT공과대학") - private String department; - - @Schema(description = "전공/학과", example = "소프트웨어학부") - private String major; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfoDTO.java b/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfoDTO.java new file mode 100644 index 00000000..f0d7823b --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfoDTO.java @@ -0,0 +1,58 @@ +package com.assu.server.domain.auth.dto.common; + +import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.user.entity.Student; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "사용자 기본 정보") +@JsonInclude(JsonInclude.Include.NON_NULL) +public record UserBasicInfoDTO( + @Schema(description = "이름/업체명/단체명", example = "홍길동") + String name, + + @Schema(description = "대학교", example = "숭실대학교") + String university, + + @Schema(description = "단과대", example = "IT공과대학") + String department, + + @Schema(description = "전공/학과", example = "소프트웨어학부") + String major +) { + public static UserBasicInfoDTO from(Member member) { + String name = null; + String university = null; + String department = null; + String major = null; + + switch (member.getRole()) { + case STUDENT -> { + Student student = member.getStudentProfile(); + if (student != null) { + name = student.getName(); + university = student.getUniversity().getDisplayName(); + department = student.getDepartment().getDisplayName(); + major = student.getMajor().getDisplayName(); + } + } + case ADMIN -> { + var admin = member.getAdminProfile(); + if (admin != null) { + name = admin.getName(); + university = admin.getUniversity() != null ? admin.getUniversity().getDisplayName() : null; + department = admin.getDepartment() != null ? admin.getDepartment().getDisplayName() : null; + major = admin.getMajor() != null ? admin.getMajor().getDisplayName() : null; + } + } + case PARTNER -> { + var partner = member.getPartnerProfile(); + if (partner != null) { + name = partner.getName(); + } + } + } + + return new UserBasicInfoDTO(name, university, department, major); + } +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/email/EmailVerificationCheckRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/email/EmailVerificationCheckRequestDTO.java new file mode 100644 index 00000000..8b57e259 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/email/EmailVerificationCheckRequestDTO.java @@ -0,0 +1,14 @@ +package com.assu.server.domain.auth.dto.email; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "이메일 중복 확인 요청") +public record EmailVerificationCheckRequestDTO( + @Schema(description = "확인할 이메일 주소", example = "user@example.com") + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "올바른 이메일 형식이 아닙니다.") + String email +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequest.java b/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequest.java deleted file mode 100644 index 20ef18b3..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.assu.server.domain.auth.dto.login; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; -import lombok.*; - -/** 파트너/관리자 공통 로그인 요청 */ -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Schema(description = "파트너/관리자 공통 로그인 요청") -public class CommonLoginRequest { - - @Schema(description = "로그인 이메일", example = "user@example.com") - @NotBlank(message = "이메일은 필수입니다.") - @Email(message = "올바른 이메일 형식이 아닙니다.") - @Size(max = 255, message = "이메일은 255자를 넘을 수 없습니다.") - private String email; - - @Schema(description = "로그인 비밀번호(평문)", example = "P@ssw0rd!") - @NotBlank(message = "비밀번호는 필수입니다.") - @Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.") - private String password; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequestDTO.java new file mode 100644 index 00000000..317d8e03 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequestDTO.java @@ -0,0 +1,21 @@ +package com.assu.server.domain.auth.dto.login; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +@Schema(description = "파트너/관리자 공통 로그인 요청") +public record CommonLoginRequestDTO( + @Schema(description = "로그인 이메일", example = "user@example.com") + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "올바른 이메일 형식이 아닙니다.") + @Size(max = 255, message = "이메일은 255자를 넘을 수 없습니다.") + String email, + + @Schema(description = "로그인 비밀번호(평문)", example = "P@ssw0rd!") + @NotBlank(message = "비밀번호는 필수입니다.") + @Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.") + String password +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java b/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java deleted file mode 100644 index 0f24d204..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.assu.server.domain.auth.dto.login; - -import com.assu.server.domain.auth.dto.common.UserBasicInfo; -import com.assu.server.domain.auth.dto.signup.Tokens; -import com.assu.server.domain.common.enums.ActivationStatus; -import com.assu.server.domain.common.enums.UserRole; -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "로그인 성공 응답") -public class LoginResponse { - - @Schema(description = "회원 ID", example = "123") - private Long memberId; - - @Schema(description = "회원 역할", example = "STUDENT") - private UserRole role; - - @Schema(description = "회원 상태", example = "SUSPEND") - private ActivationStatus status; - - @Schema(description = "액세스 토큰/리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") - private Tokens tokens; - - @Schema(description = "사용자 기본 정보 (캐싱용)") - private UserBasicInfo basicInfo; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponseDTO.java b/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponseDTO.java new file mode 100644 index 00000000..b3e101bf --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponseDTO.java @@ -0,0 +1,38 @@ +package com.assu.server.domain.auth.dto.login; + +import com.assu.server.domain.auth.dto.common.UserBasicInfoDTO; +import com.assu.server.domain.auth.dto.common.TokensDTO; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "로그인 성공 응답") +@JsonInclude(JsonInclude.Include.NON_NULL) +public record LoginResponseDTO( + @Schema(description = "회원 ID", example = "123") + Long memberId, + + @Schema(description = "회원 역할", example = "STUDENT") + UserRole role, + + @Schema(description = "회원 상태", example = "SUSPEND") + ActivationStatus status, + + @Schema(description = "액세스 토큰/리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + TokensDTO tokens, + + @Schema(description = "사용자 기본 정보 (캐싱용)") + UserBasicInfoDTO basicInfo +) { + public static LoginResponseDTO from(Member member, TokensDTO tokens) { + return new LoginResponseDTO( + member.getId(), + member.getRole(), + member.getIsActivated(), + tokens, + UserBasicInfoDTO.from(member) + ); + } +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponse.java b/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponse.java deleted file mode 100644 index 4a983b5b..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.auth.dto.login; - -public record RefreshResponse(Long memberId, String newAccess, String newRefresh) { -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponseDTO.java b/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponseDTO.java new file mode 100644 index 00000000..a4f11011 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponseDTO.java @@ -0,0 +1,22 @@ +package com.assu.server.domain.auth.dto.login; + +import com.assu.server.domain.auth.dto.common.TokensDTO; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "액세스 토큰 갱신 응답") +public record RefreshResponseDTO( + @Schema(description = "회원 ID", example = "123") + Long memberId, + @Schema(description = "새로운 액세스 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + String newAccess, + @Schema(description = "새로운 리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + String newRefresh +) { + public static RefreshResponseDTO from(Long memberId, TokensDTO tokens) { + return new RefreshResponseDTO( + memberId, + tokens.accessToken(), + tokens.refreshToken() + ); + } +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthRequestDTO.java deleted file mode 100644 index d4179f0e..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthRequestDTO.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.assu.server.domain.auth.dto.phone; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -public class PhoneAuthRequestDTO { - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class PhoneAuthVerifyRequest { - @NotBlank - @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.") - private String phoneNumber; - - @NotBlank - private String authNumber; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class PhoneAuthSendRequest { - @NotBlank - @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.") - private String phoneNumber; - } -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthResponseDTO.java b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthResponseDTO.java deleted file mode 100644 index 5c26bc8c..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthResponseDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.auth.dto.phone; - -public class PhoneAuthResponseDTO { -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthSendRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthSendRequestDTO.java new file mode 100644 index 00000000..1d7bcef0 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthSendRequestDTO.java @@ -0,0 +1,14 @@ +package com.assu.server.domain.auth.dto.phone; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +@Schema(description = "휴대폰 번호 중복가입 확인 및 인증번호 발송 요청") +public record PhoneAuthSendRequestDTO( + @Schema(description = "인증번호를 받을 휴대폰 번호", example = "01012345678") + @NotBlank(message = "전화번호는 필수입니다.") + @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.") + String phoneNumber +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthVerifyRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthVerifyRequestDTO.java new file mode 100644 index 00000000..0fa86e6f --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthVerifyRequestDTO.java @@ -0,0 +1,18 @@ +package com.assu.server.domain.auth.dto.phone; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +@Schema(description = "휴대폰 인증번호 검증 요청") +public record PhoneAuthVerifyRequestDTO( + @Schema(description = "인증받을 휴대폰 번호", example = "01012345678") + @NotBlank(message = "전화번호는 필수입니다.") + @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.") + String phoneNumber, + + @Schema(description = "발송받은 인증번호(OTP)", example = "123456") + @NotBlank(message = "인증번호는 필수입니다.") + String authNumber +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequest.java deleted file mode 100644 index 65458dba..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.assu.server.domain.auth.dto.signup; - -import com.assu.server.domain.auth.dto.signup.common.CommonAuthPayload; -import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload; -import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest; -import jakarta.validation.Valid; -import jakarta.validation.constraints.*; -import lombok.*; -import lombok.experimental.SuperBuilder; - -/** 관리자 가입: multipart payload(JSON) */ -@Getter -@NoArgsConstructor -@AllArgsConstructor -@SuperBuilder -public class AdminSignUpRequest extends CommonSignUpRequest { - - @Valid - @NotNull - private CommonAuthPayload commonAuth; - - @Valid - @NotNull - private CommonInfoPayload commonInfo; - // signImage는 @RequestPart MultipartFile 로 별도 수신 -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequestDTO.java new file mode 100644 index 00000000..7192a9a7 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequestDTO.java @@ -0,0 +1,34 @@ +package com.assu.server.domain.auth.dto.signup; + +import com.assu.server.domain.auth.dto.signup.common.CommonAuthPayloadDTO; +import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayloadDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +@Schema(description = "관리자 회원가입 요청") +public record AdminSignUpRequestDTO( + @Schema(description = "휴대폰 번호", example = "01012345678") + @Pattern(regexp = "^(01[016789])\\d{3,4}\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다.") + String phoneNumber, + + @Schema(description = "마케팅 수신 동의", example = "true") + @NotNull(message = "마케팅 수신 동의는 필수입니다.") + Boolean marketingAgree, + + @Schema(description = "위치 정보 수집 동의", example = "true") + @NotNull(message = "위치 정보 수집 동의는 필수입니다.") + Boolean locationAgree, + + @Schema(description = "관리자/제휴업체공통 인증 정보") + @Valid + @NotNull(message = "공통 인증 정보는 필수입니다.") + CommonAuthPayloadDTO commonAuth, + + @Schema(description = "관리자/제휴업체 공통 정보") + @Valid + @NotNull(message = "공통 정보는 필수입니다.") + CommonInfoPayloadDTO commonInfo +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequest.java deleted file mode 100644 index 0c30e7a0..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.assu.server.domain.auth.dto.signup; - -import com.assu.server.domain.auth.dto.signup.common.CommonAuthPayload; -import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload; -import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -/** 제휴업체 가입: multipart payload(JSON) */ -@Getter -@NoArgsConstructor -@AllArgsConstructor -@SuperBuilder -public class PartnerSignUpRequest extends CommonSignUpRequest { - - @Valid - @NotNull - private CommonAuthPayload commonAuth; - - @Valid - @NotNull - private CommonInfoPayload commonInfo; - // licenseImage는 @RequestPart MultipartFile 로 별도 수신 -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequestDTO.java new file mode 100644 index 00000000..1c914dbf --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequestDTO.java @@ -0,0 +1,34 @@ +package com.assu.server.domain.auth.dto.signup; + +import com.assu.server.domain.auth.dto.signup.common.CommonAuthPayloadDTO; +import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayloadDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +@Schema(description = "제휴업체 회원가입 요청") +public record PartnerSignUpRequestDTO( + @Schema(description = "휴대폰 번호", example = "01012345678") + @Pattern(regexp = "^(01[016789])\\d{3,4}\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다.") + String phoneNumber, + + @Schema(description = "마케팅 수신 동의", example = "true") + @NotNull(message = "마케팅 수신 동의는 필수입니다.") + Boolean marketingAgree, + + @Schema(description = "위치 정보 수집 동의", example = "true") + @NotNull(message = "위치 정보 수집 동의는 필수입니다.") + Boolean locationAgree, + + @Schema(description = "관리자/제휴업체공통 인증 정보") + @Valid + @NotNull(message = "공통 인증 정보는 필수입니다.") + CommonAuthPayloadDTO commonAuth, + + @Schema(description = "관리자/제휴업체 공통 정보") + @Valid + @NotNull(message = "공통 정보는 필수입니다.") + CommonInfoPayloadDTO commonInfo +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java b/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java deleted file mode 100644 index 02bdd2ab..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.assu.server.domain.auth.dto.signup; - -import com.assu.server.domain.auth.dto.common.UserBasicInfo; -import com.assu.server.domain.common.enums.ActivationStatus; -import com.assu.server.domain.common.enums.UserRole; -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "회원가입 성공 응답") -public class SignUpResponse { - - @Schema(description = "회원 ID", example = "123") - private Long memberId; - - @Schema(description = "회원 역할", example = "STUDENT") - private UserRole role; - - @Schema(description = "회원 상태", example = "ACTIVE") - private ActivationStatus status; - - @Schema(description = "액세스 토큰/리프레시 토큰") - private Tokens tokens; - - @Schema(description = "사용자 기본 정보 (캐싱용)") - private UserBasicInfo basicInfo; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponseDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponseDTO.java new file mode 100644 index 00000000..506ccbed --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponseDTO.java @@ -0,0 +1,38 @@ +package com.assu.server.domain.auth.dto.signup; + +import com.assu.server.domain.auth.dto.common.TokensDTO; +import com.assu.server.domain.auth.dto.common.UserBasicInfoDTO; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "회원가입 성공 응답") +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SignUpResponseDTO( + @Schema(description = "회원 ID", example = "123") + Long memberId, + + @Schema(description = "회원 역할", example = "STUDENT") + UserRole role, + + @Schema(description = "회원 상태", example = "ACTIVE") + ActivationStatus status, + + @Schema(description = "액세스 토큰/리프레시 토큰") + TokensDTO tokens, + + @Schema(description = "사용자 기본 정보 (캐싱용)") + UserBasicInfoDTO basicInfo +) { + public static SignUpResponseDTO from(Member member, TokensDTO tokens) { + return new SignUpResponseDTO( + member.getId(), + member.getRole(), + member.getIsActivated(), + tokens, + UserBasicInfoDTO.from(member) + ); + } +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequest.java deleted file mode 100644 index c5051e65..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.assu.server.domain.auth.dto.signup; - -import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest; -import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import lombok.*; -import lombok.experimental.SuperBuilder; - -/** 학생 가입: sToken, sIdno 기반 */ -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@SuperBuilder -public class StudentTokenSignUpRequest extends CommonSignUpRequest { - - @Valid - @NotNull - private StudentTokenAuthPayload studentTokenAuth; -} - diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequestDTO.java new file mode 100644 index 00000000..1e7a32db --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequestDTO.java @@ -0,0 +1,28 @@ +package com.assu.server.domain.auth.dto.signup; + +import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayloadDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +@Schema(description = "학생 토큰 회원가입 요청") +public record StudentTokenSignUpRequestDTO( + @Schema(description = "휴대폰 번호", example = "01012345678") + @Pattern(regexp = "^(01[016789])\\d{3,4}\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다.") + String phoneNumber, + + @Schema(description = "마케팅 수신 동의", example = "true") + @NotNull(message = "마케팅 수신 동의는 필수입니다.") + Boolean marketingAgree, + + @Schema(description = "위치 정보 수집 동의", example = "true") + @NotNull(message = "위치 정보 수집 동의는 필수입니다.") + Boolean locationAgree, + + @Schema(description = "학생 토큰 인증 정보") + @Valid + @NotNull(message = "학생 토큰 인증 정보는 필수입니다.") + StudentTokenAuthPayloadDTO studentTokenAuth +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/Tokens.java b/src/main/java/com/assu/server/domain/auth/dto/signup/Tokens.java deleted file mode 100644 index e414d77e..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/Tokens.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.assu.server.domain.auth.dto.signup; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Tokens { - private String accessToken; - private String refreshToken; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java deleted file mode 100644 index bb80d8b0..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.assu.server.domain.auth.dto.signup.common; - -import com.assu.server.domain.auth.exception.annotation.PasswordMatches; -import com.assu.server.domain.user.entity.enums.Department; -import com.assu.server.domain.user.entity.enums.Major; -import com.assu.server.domain.user.entity.enums.University; - -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Email; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class CommonAuthPayload { - @Email @NotBlank - private String email; - - @Size(min = 8, max = 72) @NotBlank - private String password; - - private Department department; - - private Major major; - - private University university; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayloadDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayloadDTO.java new file mode 100644 index 00000000..b9e49e73 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayloadDTO.java @@ -0,0 +1,32 @@ +package com.assu.server.domain.auth.dto.signup.common; + +import com.assu.server.domain.user.entity.enums.Department; +import com.assu.server.domain.user.entity.enums.Major; +import com.assu.server.domain.user.entity.enums.University; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +@Schema(description = "공통 인증 정보 페이로드") +public record CommonAuthPayloadDTO( + @Schema(description = "이메일 주소", example = "user@example.com") + @Email(message = "올바른 이메일 형식이 아닙니다.") + @NotBlank(message = "이메일은 필수입니다.") + String email, + + @Schema(description = "비밀번호(평문)", example = "P@ssw0rd!") + @Size(min = 8, max = 72, message = "비밀번호는 8~72자여야 합니다.") + @NotBlank(message = "비밀번호는 필수입니다.") + String password, + + @Schema(description = "단과대", example = "IT공과대학") + Department department, + + @Schema(description = "전공/학과", example = "소프트웨어학부") + Major major, + + @Schema(description = "대학교", example = "SSU") + University university +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java deleted file mode 100644 index 116d6c19..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.assu.server.domain.auth.dto.signup.common; - -import com.assu.server.domain.map.dto.SelectedPlacePayload; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class CommonInfoPayload { - @Size(min = 1, max = 50) @NotBlank - private String name; - - @Size(max = 255) - private String detailAddress; - - @NotNull - private SelectedPlacePayload selectedPlace; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayloadDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayloadDTO.java new file mode 100644 index 00000000..815d6b0e --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayloadDTO.java @@ -0,0 +1,24 @@ +package com.assu.server.domain.auth.dto.signup.common; + +import com.assu.server.domain.map.dto.SelectedPlacePayload; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Schema(description = "공통 정보 페이로드") +public record CommonInfoPayloadDTO( + @Schema(description = "이름/업체명/단체명", example = "홍길동") + @Size(min = 1, max = 50, message = "이름은 1~50자여야 합니다.") + @NotBlank(message = "이름은 필수입니다.") + String name, + + @Schema(description = "상세 주소", example = "101호") + @Size(max = 255, message = "상세 주소는 255자를 넘을 수 없습니다.") + String detailAddress, + + @Schema(description = "선택된 장소 정보") + @NotNull(message = "선택된 장소 정보는 필수입니다.") + SelectedPlacePayload selectedPlace +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonSignUpRequest.java deleted file mode 100644 index 80b7570f..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonSignUpRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.assu.server.domain.auth.dto.signup.common; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; - -/** 공통 필드 */ -@Getter -@NoArgsConstructor -@AllArgsConstructor -@SuperBuilder -public class CommonSignUpRequest { - - @Pattern(regexp = "^(01[016789])\\d{3,4}\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다.") - @NotBlank - private String phoneNumber; - - @NotNull - private Boolean marketingAgree; - - @NotNull - private Boolean locationAgree; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java deleted file mode 100644 index 42907454..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.assu.server.domain.auth.dto.signup.student; - -import com.assu.server.domain.user.entity.enums.University; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class StudentTokenAuthPayload { - @Schema(description = "유세인트 sToken", example = "Vy3zFySFx5FASz175Kx7AzKyuSFQEgQ...") - @NotNull(message = "sToken은 필수입니다.") - @JsonProperty(value = "sToken") - private String sToken; - - @Schema(description = "유세인트 sIdno", example = "20211438") - @NotNull(message = "sIdno는 필수입니다.") - @JsonProperty(value = "sIdno") - private String sIdno; - - private University university; -} - - diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayloadDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayloadDTO.java new file mode 100644 index 00000000..3c5d4769 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayloadDTO.java @@ -0,0 +1,23 @@ +package com.assu.server.domain.auth.dto.signup.student; + +import com.assu.server.domain.user.entity.enums.University; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "학생 토큰 인증 페이로드") +public record StudentTokenAuthPayloadDTO( + @Schema(description = "유세인트 sToken", example = "Vy3zFySFx5FASz175Kx7AzKyuSFQEgQ...") + @NotNull(message = "sToken은 필수입니다.") + @JsonProperty(value = "sToken") + String sToken, + + @Schema(description = "유세인트 sIdno", example = "20211438") + @NotNull(message = "sIdno는 필수입니다.") + @JsonProperty(value = "sIdno") + String sIdno, + + @Schema(description = "대학교", example = "SSU") + University university +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java deleted file mode 100644 index 7e3cac94..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.assu.server.domain.auth.dto.ssu; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; -import org.jetbrains.annotations.NotNull; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class USaintAuthRequest { - @NotNull - @JsonProperty(value = "sToken") - private String sToken; - @NotNull - @JsonProperty(value = "sIdno") - private String sIdno; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequestDTO.java new file mode 100644 index 00000000..223c59dc --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequestDTO.java @@ -0,0 +1,23 @@ +package com.assu.server.domain.auth.dto.ssu; + +import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayloadDTO; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "유세인트 인증 요청") +public record USaintAuthRequestDTO( + @Schema(description = "유세인트 sToken", example = "Vy3zFySFx5FASz175Kx7AzKyuSFQEgQ...") + @NotNull(message = "sToken은 필수입니다.") + @JsonProperty(value = "sToken") + String sToken, + + @Schema(description = "유세인트 sIdno", example = "20211438") + @NotNull(message = "sIdno는 필수입니다.") + @JsonProperty(value = "sIdno") + String sIdno +) { + public static USaintAuthRequestDTO from(StudentTokenAuthPayloadDTO payload) { + return new USaintAuthRequestDTO(payload.sToken(), payload.sIdno()); + } +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java deleted file mode 100644 index d4fd4458..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.assu.server.domain.auth.dto.ssu; - -import com.assu.server.domain.user.entity.enums.Major; -import lombok.*; - -@Setter -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class USaintAuthResponse { - private String studentNumber; - private String name; - private String enrollmentStatus; - private String yearSemester; - private Major major; -} diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponseDTO.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponseDTO.java new file mode 100644 index 00000000..00a6e4f9 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponseDTO.java @@ -0,0 +1,32 @@ +package com.assu.server.domain.auth.dto.ssu; + +import com.assu.server.domain.user.entity.enums.Major; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "유세인트 인증 응답") +public record USaintAuthResponseDTO( + @Schema(description = "학번", example = "20211438") + String studentNumber, + + @Schema(description = "이름", example = "홍길동") + String name, + + @Schema(description = "학적 상태", example = "재학") + String enrollmentStatus, + + @Schema(description = "학년/학기", example = "4학년 1학기") + String yearSemester, + + @Schema(description = "전공/학과") + Major major +) { + public static USaintAuthResponseDTO of( + String studentNumber, + String name, + String enrollmentStatus, + String yearSemester, + Major major + ) { + return new USaintAuthResponseDTO(studentNumber, name, enrollmentStatus, yearSemester, major); + } +} diff --git a/src/main/java/com/assu/server/domain/auth/dto/verification/VerificationRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/verification/VerificationRequestDTO.java deleted file mode 100644 index 5ef713ed..00000000 --- a/src/main/java/com/assu/server/domain/auth/dto/verification/VerificationRequestDTO.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.assu.server.domain.auth.dto.verification; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -public class VerificationRequestDTO { - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class PhoneVerificationCheckRequest { - @NotBlank(message = "전화번호는 필수입니다.") - @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.") - private String phoneNumber; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class EmailVerificationCheckRequest { - @NotBlank(message = "이메일은 필수입니다.") - @Email(message = "올바른 이메일 형식이 아닙니다.") - private String email; - } -} diff --git a/src/main/java/com/assu/server/domain/auth/entity/AuthRealm.java b/src/main/java/com/assu/server/domain/auth/entity/AuthRealm.java deleted file mode 100644 index eacd0e1c..00000000 --- a/src/main/java/com/assu/server/domain/auth/entity/AuthRealm.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.assu.server.domain.auth.entity; - -public enum AuthRealm { - COMMON, SSU -} diff --git a/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java b/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java index 561f7d2f..6574515d 100644 --- a/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java +++ b/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java @@ -3,6 +3,8 @@ import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.member.entity.Member; import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; import lombok.*; import java.time.LocalDateTime; @@ -23,21 +25,24 @@ public class CommonAuth extends BaseEntity { @Id @Column(name = "member_id") + @NotNull private Long id; @OneToOne @MapsId @JoinColumn(name = "member_id", referencedColumnName = "id") + @NotNull private Member member; @Column(name = "email", length = 255, nullable = false) + @NotNull + @Email private String email; @Column(name = "password", length = 255, nullable = false) - private String password; // 해시 저장 - - @Column(name = "is_email_verified", nullable = false) - private Boolean isEmailVerified = Boolean.FALSE; + @NotNull + private String hashedPassword; @Column(name = "last_login_at") + @NotNull private LocalDateTime lastLoginAt; } diff --git a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java index a60aea68..a577bce8 100644 --- a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java +++ b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java @@ -3,6 +3,7 @@ import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.member.entity.Member; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.*; import java.time.LocalDateTime; @@ -23,18 +24,23 @@ public class SSUAuth extends BaseEntity { @Id @Column(name = "member_id") + @NotNull private Long id; @OneToOne @MapsId @JoinColumn(name = "member_id", referencedColumnName = "id") + @NotNull private Member member; @Column(name = "student_number", length = 20, nullable = false) + @NotNull private String studentNumber; @Column(name = "is_authenticated", nullable = false) + @NotNull private Boolean isAuthenticated = Boolean.FALSE; @Column(name = "authenticated_at") + @NotNull private LocalDateTime authenticatedAt; } diff --git a/src/main/java/com/assu/server/domain/auth/entity/enums/AuthRealm.java b/src/main/java/com/assu/server/domain/auth/entity/enums/AuthRealm.java new file mode 100644 index 00000000..252539ef --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/entity/enums/AuthRealm.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.auth.entity.enums; + +public enum AuthRealm { + COMMON, SSU +} diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java index 266d7ec4..9734a557 100644 --- a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java +++ b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java @@ -1,22 +1,27 @@ package com.assu.server.domain.auth.security.adapter; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import com.assu.server.domain.auth.entity.CommonAuth; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.auth.repository.CommonAuthRepository; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; + @Component @RequiredArgsConstructor public class CommonAuthAdapter implements RealmAuthAdapter { + private final CommonAuthRepository commonAuthRepository; - private final PasswordEncoder passwordEncoder; // BCrypt + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; @Override public boolean supports(AuthRealm realm) { @@ -33,7 +38,7 @@ public UserDetails loadUserDetails(String email) { return org.springframework.security.core.userdetails.User .withUsername(ca.getEmail()) - .password(ca.getPassword()) // BCrypt 해시 + .password(ca.getHashedPassword()) // BCrypt 해시 .authorities(authority) .accountExpired(false).accountLocked(false).credentialsExpired(false) .disabled(!enabled) @@ -46,10 +51,13 @@ public Member loadMember(String email) { .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER)) .getMember(); - // 탈퇴된 회원이 다시 로그인하면 복구 + CommonAuth commonAuth = member.getCommonAuth(); + commonAuth.setLastLoginAt(LocalDateTime.now()); + commonAuthRepository.save(commonAuth); + if (member.getDeletedAt() != null) { member.setDeletedAt(null); - commonAuthRepository.save(member.getCommonAuth()); + memberRepository.save(member); } return member; @@ -65,9 +73,10 @@ public void registerCredentials(Member member, String email, String rawPassword) CommonAuth.builder() .member(member) .email(email) - .password(hash) - .isEmailVerified(false) - .build()); + .hashedPassword(hash) + .lastLoginAt(LocalDateTime.now()) + .build() + ); } @Override diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java index 1f6748d7..e4b72d6a 100644 --- a/src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java +++ b/src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java @@ -1,6 +1,6 @@ package com.assu.server.domain.auth.security.adapter; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import com.assu.server.domain.member.entity.Member; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java index 8dcc7b78..e1e8e98a 100644 --- a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java +++ b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java @@ -1,11 +1,12 @@ package com.assu.server.domain.auth.security.adapter; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import com.assu.server.domain.auth.entity.SSUAuth; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.auth.repository.SSUAuthRepository; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; @@ -17,7 +18,9 @@ @Component @RequiredArgsConstructor public class SSUAuthAdapter implements RealmAuthAdapter { + private final SSUAuthRepository ssuAuthRepository; + private final MemberRepository memberRepository; @Override public boolean supports(AuthRealm realm) { @@ -48,10 +51,13 @@ public Member loadMember(String studentNumber) { .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER)) .getMember(); - // 탈퇴된 회원이 다시 로그인하면 복구 + SSUAuth ssuAuth = member.getSsuAuth(); + ssuAuth.setAuthenticatedAt(LocalDateTime.now()); + ssuAuthRepository.save(member.getSsuAuth()); + if (member.getDeletedAt() != null) { member.setDeletedAt(null); - ssuAuthRepository.save(member.getSsuAuth()); + memberRepository.save(member); } return member; diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java index 34a5b373..51ea8fe7 100644 --- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java +++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java @@ -1,7 +1,7 @@ package com.assu.server.domain.auth.security.jwt; -import com.assu.server.domain.auth.dto.signup.Tokens; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.dto.common.TokensDTO; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.member.entity.Member; @@ -102,7 +102,7 @@ private String generateToken(Map claims, int validSeconds, Strin * @param authRealm COMMON / SSU * @return 발급된 토큰 세트 */ - public Tokens issueTokens(Long memberId, String username, UserRole role, String authRealm) { + public TokensDTO issueTokens(Long memberId, String username, UserRole role, String authRealm) { Map baseClaims = new HashMap<>(); baseClaims.put("userId", memberId); baseClaims.put("username", username); @@ -118,10 +118,7 @@ public Tokens issueTokens(Long memberId, String username, UserRole role, String String refreshKey = String.format("refresh:%d:%s", memberId, refreshJti); redisTemplate.opsForValue().set(refreshKey, refreshToken, refreshValidSeconds, TimeUnit.SECONDS); - return Tokens.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); + return TokensDTO.of(accessToken, refreshToken); } // ───────── 검증 ───────── @@ -232,7 +229,7 @@ public Authentication getAuthenticationFromExpiredAccessToken(String expiredAcce AuthRealm realm = AuthRealm.valueOf(authRealmString); if (realm == AuthRealm.COMMON) { username = member.getCommonAuth().getEmail(); - password = member.getCommonAuth().getPassword(); + password = member.getCommonAuth().getHashedPassword(); } else if (realm == AuthRealm.SSU){ username = member.getSsuAuth().getStudentNumber(); password = ""; // 더미 처리 @@ -299,7 +296,7 @@ public void removeAllRefreshTokens(Long memberId) { * - 저장된 RT와 일치 여부 확인 * - 기존 RT 삭제 후 새 토큰 세트 발급 */ - public Tokens rotateRefreshToken(String refreshToken) { + public TokensDTO rotateRefreshToken(String refreshToken) { // 1) Refresh 토큰 서명/만료 검증 validateRefreshToken(refreshToken); diff --git a/src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java b/src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java index 01d5dc91..d1719b44 100644 --- a/src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java +++ b/src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java @@ -1,6 +1,6 @@ package com.assu.server.domain.auth.security.provider; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter; import com.assu.server.domain.auth.security.token.LoginUsernamePasswordAuthenticationToken; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java b/src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java index b90043cc..0daa67e9 100644 --- a/src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java +++ b/src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java @@ -1,11 +1,10 @@ package com.assu.server.domain.auth.security.token; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; // 단일 인증 토큰 -public class LoginUsernamePasswordAuthenticationToken - extends UsernamePasswordAuthenticationToken { +public class LoginUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken { private final AuthRealm realm; diff --git a/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java b/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java index c3f9bb0e..c7cc0bae 100644 --- a/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java +++ b/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java @@ -1,8 +1,8 @@ package com.assu.server.domain.auth.service; -import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; -public interface EmailAuthService { +import com.assu.server.domain.auth.dto.email.EmailVerificationCheckRequestDTO; - void checkEmailAvailability(VerificationRequestDTO.EmailVerificationCheckRequest request); +public interface EmailAuthService { + void checkEmailAvailability(EmailVerificationCheckRequestDTO request); } diff --git a/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java index c852420b..f347d225 100644 --- a/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java @@ -1,22 +1,24 @@ package com.assu.server.domain.auth.service; -import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; +import com.assu.server.domain.auth.dto.email.EmailVerificationCheckRequestDTO; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.auth.repository.CommonAuthRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class EmailAuthServiceImpl implements EmailAuthService { private final CommonAuthRepository commonAuthRepository; @Override - public void checkEmailAvailability(VerificationRequestDTO.EmailVerificationCheckRequest request) { + public void checkEmailAvailability(EmailVerificationCheckRequestDTO request) { - boolean exists = commonAuthRepository.existsByEmail(request.getEmail()); + boolean exists = commonAuthRepository.existsByEmail(request.email()); if (exists) { throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL); diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginService.java b/src/main/java/com/assu/server/domain/auth/service/LoginService.java index ff731366..901871fd 100644 --- a/src/main/java/com/assu/server/domain/auth/service/LoginService.java +++ b/src/main/java/com/assu/server/domain/auth/service/LoginService.java @@ -1,12 +1,14 @@ package com.assu.server.domain.auth.service; -import com.assu.server.domain.auth.dto.login.CommonLoginRequest; -import com.assu.server.domain.auth.dto.login.LoginResponse; -import com.assu.server.domain.auth.dto.login.RefreshResponse; -import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload; +import com.assu.server.domain.auth.dto.login.CommonLoginRequestDTO; +import com.assu.server.domain.auth.dto.login.LoginResponseDTO; +import com.assu.server.domain.auth.dto.login.RefreshResponseDTO; +import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayloadDTO; public interface LoginService { - LoginResponse loginCommon(CommonLoginRequest request); - LoginResponse loginSsuStudent(StudentTokenAuthPayload request); - RefreshResponse refresh(String refreshToken); + LoginResponseDTO loginCommon(CommonLoginRequestDTO request); + + LoginResponseDTO loginSsuStudent(StudentTokenAuthPayloadDTO request); + + RefreshResponseDTO refresh(String refreshToken); } diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java index 1c85152b..c1787a56 100644 --- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java @@ -1,44 +1,43 @@ package com.assu.server.domain.auth.service; -import com.assu.server.domain.auth.dto.common.UserBasicInfo; -import com.assu.server.domain.auth.dto.login.CommonLoginRequest; -import com.assu.server.domain.auth.dto.login.LoginResponse; -import com.assu.server.domain.auth.dto.login.RefreshResponse; -import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload; -import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest; -import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse; -import com.assu.server.domain.auth.dto.signup.Tokens; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.dto.login.CommonLoginRequestDTO; +import com.assu.server.domain.auth.dto.login.LoginResponseDTO; +import com.assu.server.domain.auth.dto.login.RefreshResponseDTO; +import com.assu.server.domain.auth.dto.common.TokensDTO; +import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayloadDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthRequestDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthResponseDTO; +import com.assu.server.domain.auth.entity.enums.AuthRealm; +import com.assu.server.domain.auth.exception.CustomAuthException; +import com.assu.server.domain.auth.repository.CommonAuthRepository; import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter; +import com.assu.server.domain.auth.security.jwt.JwtUtil; import com.assu.server.domain.auth.security.token.LoginUsernamePasswordAuthenticationToken; import com.assu.server.domain.member.entity.Member; -import com.assu.server.domain.auth.exception.CustomAuthException; -import com.assu.server.domain.auth.security.jwt.JwtUtil; import com.assu.server.domain.user.entity.Student; -import com.assu.server.domain.user.entity.enums.Department; import com.assu.server.domain.user.entity.enums.EnrollmentStatus; -import com.assu.server.domain.user.entity.enums.Major; -import com.assu.server.domain.user.entity.enums.University; import com.assu.server.domain.user.repository.StudentRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @RequiredArgsConstructor +@Transactional public class LoginServiceImpl implements LoginService { private final AuthenticationManager authenticationManager; private final JwtUtil jwtUtil; private final SSUAuthService ssuAuthService; + private final CommonAuthRepository commonAuthRepository; private final StudentRepository studentRepository; - // 공통/학생/기타 학교까지 모두 여기로 주입 private final List realmAuthAdapters; private RealmAuthAdapter pickAdapter(AuthRealm realm) { @@ -50,95 +49,82 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) { /** * 공통(파트너/관리자) 로그인: 이메일/비밀번호 기반. - * 1) 인증 성공 시 CommonAuth 조회 + * 1) 인증 성공 시 CommonAuth 조회 및 탈퇴 회원 복구 * 2) JWT 발급: username=email, authRealm=COMMON */ @Override - public LoginResponse loginCommon(CommonLoginRequest request) { - // 공통(파트너/관리자) 로그인: 이메일/비번 + public LoginResponseDTO loginCommon(CommonLoginRequestDTO request) { Authentication authentication = authenticationManager.authenticate( new LoginUsernamePasswordAuthenticationToken( AuthRealm.COMMON, - request.getEmail(), - request.getPassword())); + request.email(), + request.password() + ) + ); RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON); - // identifier = email Member member = adapter.loadMember(authentication.getName()); // 토큰 발급 (Access 미저장, Refresh는 Redis 저장) - Tokens tokens = jwtUtil.issueTokens( + TokensDTO tokens = jwtUtil.issueTokens( member.getId(), - authentication.getName(), // email + authentication.getName(), member.getRole(), - adapter.authRealmValue() // "COMMON" + adapter.authRealmValue() ); - return LoginResponse.builder() - .memberId(member.getId()) - .role(member.getRole()) - .status(member.getIsActivated()) - .tokens(tokens) - .basicInfo(buildUserBasicInfo(member)) - .build(); + return LoginResponseDTO.from(member, tokens); } /** * 숭실대 학생 로그인: sToken, sIdno 기반. * 1) 유세인트 인증으로 학생 정보 확인 - * 2) 기존 회원 확인 + * 2) 기존 회원 확인 및 탈퇴 회원 복구 * 3) Student 정보 업데이트 (유세인트에서 크롤링한 최신 정보로) * 4) JWT 발급: username=studentNumber, authRealm=SSU */ @Override - @Transactional - public LoginResponse loginSsuStudent(StudentTokenAuthPayload request) { + public LoginResponseDTO loginSsuStudent(StudentTokenAuthPayloadDTO request) { // 1) 유세인트 인증 - USaintAuthRequest authRequest = USaintAuthRequest.builder() - .sToken(request.getSToken()) - .sIdno(request.getSIdno()) - .build(); + USaintAuthRequestDTO authRequest = new USaintAuthRequestDTO( + request.sToken(), + request.sIdno() + ); - USaintAuthResponse authResponse = ssuAuthService.uSaintAuth(authRequest); + USaintAuthResponseDTO authResponse = ssuAuthService.uSaintAuth(authRequest); // 2) 기존 회원 확인 - String realmStr = request.getUniversity().toString(); + String realmStr = request.university().toString(); AuthRealm authRealm = AuthRealm.valueOf(realmStr); RealmAuthAdapter adapter = pickAdapter(authRealm); - Member member = adapter.loadMember(authResponse.getStudentNumber().toString()); + Member member = adapter.loadMember(authResponse.studentNumber()); - // 3) Student 정보 업데이트 (유세인트에서 크롤링한 최신 정보로) + // 3) Student 정보 업데이트 Student student = member.getStudentProfile(); if (student == null) { throw new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER); } - // 유세인트에서 크롤링한 최신 정보로 업데이트 student.updateStudentInfo( - authResponse.getName(), - authResponse.getMajor(), - parseEnrollmentStatus(authResponse.getEnrollmentStatus()), - authResponse.getYearSemester()); + authResponse.name(), + authResponse.major(), + parseEnrollmentStatus(authResponse.enrollmentStatus()), + authResponse.yearSemester() + ); studentRepository.save(student); // 4) 토큰 발급 - Tokens tokens = jwtUtil.issueTokens( + TokensDTO tokens = jwtUtil.issueTokens( member.getId(), - authResponse.getStudentNumber().toString(), // studentNumber + authResponse.studentNumber(), member.getRole(), - adapter.authRealmValue() // 예: "SSU" + adapter.authRealmValue() ); - return LoginResponse.builder() - .memberId(member.getId()) - .role(member.getRole()) - .status(member.getIsActivated()) - .tokens(tokens) - .basicInfo(buildUserBasicInfo(member)) - .build(); + return LoginResponseDTO.from(member, tokens); } /** @@ -152,12 +138,13 @@ public LoginResponse loginSsuStudent(StudentTokenAuthPayload request) { * 4) 기존 RT 키 삭제(회전), 새 토큰 발급(issueTokens) */ @Override - public RefreshResponse refresh(String refreshToken) { - Tokens rotated = jwtUtil.rotateRefreshToken(refreshToken); - return new RefreshResponse( - ((Number) jwtUtil.validateTokenOnlySignature(rotated.getAccessToken()).get("userId")).longValue(), - rotated.getAccessToken(), - rotated.getRefreshToken()); + @Transactional(propagation = Propagation.NOT_SUPPORTED) + public RefreshResponseDTO refresh(String refreshToken) { + TokensDTO rotated = jwtUtil.rotateRefreshToken(refreshToken); + + Long memberId = ((Number) jwtUtil.validateTokenOnlySignature(rotated.accessToken()).get("userId")).longValue(); + + return RefreshResponseDTO.from(memberId, rotated); } private EnrollmentStatus parseEnrollmentStatus(String status) { @@ -175,42 +162,4 @@ private EnrollmentStatus parseEnrollmentStatus(String status) { return EnrollmentStatus.ENROLLED; } } - - /** - * 사용자 기본 정보를 빌드하는 헬퍼 메서드 - */ - private UserBasicInfo buildUserBasicInfo(Member member) { - UserBasicInfo.UserBasicInfoBuilder builder = UserBasicInfo.builder(); - - switch (member.getRole()) { - case STUDENT -> { - Student student = member.getStudentProfile(); - if (student != null) { - builder.name(student.getName()) - .university(student.getUniversity().getDisplayName()) - .department(student.getDepartment().getDisplayName()) - .major(student.getMajor().getDisplayName()); - } - } - case ADMIN -> { - // Admin 엔티티에서 정보 추출 - var admin = member.getAdminProfile(); - if (admin != null) { - builder.name(admin.getName()) - .university(admin.getUniversity() != null ? admin.getUniversity().getDisplayName() : null) - .department(admin.getDepartment() != null ? admin.getDepartment().getDisplayName() : null) - .major(admin.getMajor() != null ? admin.getMajor().getDisplayName() : null); - } - } - case PARTNER -> { - // Partner 엔티티에서 정보 추출 (Partner는 name만 필요) - var partner = member.getPartnerProfile(); - if (partner != null) { - builder.name(partner.getName()); - } - } - } - - return builder.build(); - } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java index af98c3e2..9446c09f 100644 --- a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java @@ -3,7 +3,6 @@ import com.assu.server.domain.auth.security.jwt.JwtUtil; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @Service @@ -11,7 +10,6 @@ public class LogoutServiceImpl implements LogoutService { private final JwtUtil jwtUtil; - private final RedisTemplate redisTemplate; @Override public void logout(String authorization) { diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java index 1b345c37..bea62226 100644 --- a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java +++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java @@ -2,5 +2,6 @@ public interface PhoneAuthService { void checkAndSendAuthNumber(String phoneNumber); + void verifyAuthNumber(String phoneNumber, String authNumber); } diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java index 239650c5..d1fdd767 100644 --- a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java @@ -10,6 +10,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.Duration; @@ -24,13 +25,14 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { private static final Duration AUTH_CODE_TTL = Duration.ofMinutes(5); // 인증번호 5분 유효 @Override + @Transactional(readOnly = true) public void checkAndSendAuthNumber(String phoneNumber) { boolean exists = memberRepository.existsByPhoneNum(phoneNumber); if (exists) { throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); } - + String authNumber = RandomNumberUtil.generateSixDigit(); redisTemplate.opsForValue().set(phoneNumber, authNumber, AUTH_CODE_TTL); @@ -40,6 +42,7 @@ public void checkAndSendAuthNumber(String phoneNumber) { // 실패 처리 if (!response.getResult_code().equals("1")) { + redisTemplate.delete(phoneNumber); throw new CustomAuthException(ErrorStatus.FAILED_TO_SEND_SMS); } } @@ -53,7 +56,6 @@ public void verifyAuthNumber(String phoneNumber, String authNumber) { throw new CustomAuthException(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER); } - // 인증 성공 시 Redis에서 삭제(Optional) redisTemplate.delete(phoneNumber); } } diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java index 99d1098d..6cfe12eb 100644 --- a/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java +++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java @@ -1,9 +1,8 @@ package com.assu.server.domain.auth.service; - -import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest; -import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse; +import com.assu.server.domain.auth.dto.ssu.USaintAuthRequestDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthResponseDTO; public interface SSUAuthService { - USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest); + USaintAuthResponseDTO uSaintAuth(USaintAuthRequestDTO uSaintAuthRequest); } diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java index 515d7cfd..f2e4bc1f 100644 --- a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java @@ -1,8 +1,7 @@ package com.assu.server.domain.auth.service; - -import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest; -import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse; +import com.assu.server.domain.auth.dto.ssu.USaintAuthRequestDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthResponseDTO; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.user.entity.enums.Major; import com.assu.server.global.apiPayload.code.status.ErrorStatus; @@ -30,10 +29,10 @@ public class SSUAuthServiceImpl implements SSUAuthService { private static final String USaintPortalUrl = "https://saint.ssu.ac.kr/webSSUMain/main_student.jsp"; @Override - public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) { + public USaintAuthResponseDTO uSaintAuth(USaintAuthRequestDTO uSaintAuthRequest) { - String sToken = uSaintAuthRequest.getSToken(); - String sIdno = uSaintAuthRequest.getSIdno(); + String sToken = uSaintAuthRequest.sToken(); + String sIdno = uSaintAuthRequest.sIdno(); // 1) SSO 로그인 요청 ResponseEntity uSaintSSOResponseEntity; @@ -55,7 +54,6 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) { throw new CustomAuthException(ErrorStatus.SSU_SAINT_SSO_FAILED); } - // 쿠키 추출 HttpHeaders headers = uSaintSSOResponseEntity.getHeaders(); List setCookieList = headers.get(HttpHeaders.SET_COOKIE); @@ -82,7 +80,12 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) { } String uSaintPortalResponseBody = portalResponse.getBody(); - USaintAuthResponse usaintAuthResponse = USaintAuthResponse.builder().build(); + + String studentNumber = null; + String name = null; + String enrollmentStatus = null; + String yearSemester = null; + com.assu.server.domain.user.entity.enums.Major major = null; // 3) HTML 파싱 Document doc; @@ -102,15 +105,13 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) { throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED); } - // 이름 추출 Element span = nameBox.getElementsByTag("span").first(); if (span == null || span.text().isEmpty()) { log.error("Student name span not found or empty."); throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED); } - usaintAuthResponse.setName(span.text().split("님")[0]); + name = span.text().split("님")[0]; - // 학번, 소속, 학적 상태, 학년학기 추출 Elements infoLis = infoBox.getElementsByTag("li"); for (Element li : infoLis) { Element dt = li.getElementsByTag("dt").first(); @@ -124,85 +125,83 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) { switch (dt.text()) { case "학번" -> { try { - usaintAuthResponse.setStudentNumber(strong.text()); + studentNumber = strong.text(); } catch (NumberFormatException e) { log.error("Invalid studentId format: {}", strong.text()); throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED); } } case "소속" -> { - // 원본 문자열 저장 String majorStr = strong.text(); - // 매핑된 Enum 값 저장 switch (majorStr) { // 인문대학 - case "기독교학과" -> usaintAuthResponse.setMajor(Major.CHRISTIAN_STUDIES); - case "국어국문학과" -> usaintAuthResponse.setMajor(Major.KOREAN_LITERATURE); - case "영어영문학과" -> usaintAuthResponse.setMajor(Major.ENGLISH_LITERATURE); - case "독어독문학과" -> usaintAuthResponse.setMajor(Major.GERMAN_LITERATURE); - case "불어불문학과" -> usaintAuthResponse.setMajor(Major.FRENCH_LITERATURE); - case "중어중문학과" -> usaintAuthResponse.setMajor(Major.CHINESE_LITERATURE); - case "일어일문학과" -> usaintAuthResponse.setMajor(Major.JAPANESE_LITERATURE); - case "철학과" -> usaintAuthResponse.setMajor(Major.PHILOSOPHY); - case "사학과" -> usaintAuthResponse.setMajor(Major.HISTORY); - case "예술창작학부" -> usaintAuthResponse.setMajor(Major.CREATIVE_ARTS); - case "스포츠학부" -> usaintAuthResponse.setMajor(Major.SPORTS); + case "기독교학과" -> major = Major.CHRISTIAN_STUDIES; + case "국어국문학과" -> major = Major.KOREAN_LITERATURE; + case "영어영문학과" -> major = Major.ENGLISH_LITERATURE; + case "독어독문학과" -> major = Major.GERMAN_LITERATURE; + case "불어불문학과" -> major = Major.FRENCH_LITERATURE; + case "중어중문학과" -> major = Major.CHINESE_LITERATURE; + case "일어일문학과" -> major = Major.JAPANESE_LITERATURE; + case "철학과" -> major = Major.PHILOSOPHY; + case "사학과" -> major = Major.HISTORY; + case "예술창작학부" -> major = Major.CREATIVE_ARTS; + case "스포츠학부" -> major = Major.SPORTS; // 자연과학대학 - case "수학과" -> usaintAuthResponse.setMajor(Major.MATHEMATICS); - case "화학과" -> usaintAuthResponse.setMajor(Major.CHEMISTRY); - case "의생명시스템학부" -> usaintAuthResponse.setMajor(Major.BIOMEDICAL_SYSTEMS); - case "물리학과" -> usaintAuthResponse.setMajor(Major.PHYSICS); - case "정보통계ㆍ보험수리학과" -> usaintAuthResponse.setMajor(Major.STATISTICS_ACTUARIAL); + case "수학과" -> major = Major.MATHEMATICS; + case "화학과" -> major = Major.CHEMISTRY; + case "의생명시스템학부" -> major = Major.BIOMEDICAL_SYSTEMS; + case "물리학과" -> major = Major.PHYSICS; + case "정보통계ㆍ보험수리학과" -> major = Major.STATISTICS_ACTUARIAL; // 법과대학 - case "법학과" -> usaintAuthResponse.setMajor(Major.LAW); - case "국제법무학과" -> usaintAuthResponse.setMajor(Major.INTERNATIONAL_LAW); + case "법학과" -> major = Major.LAW; + case "국제법무학과" -> major = Major.INTERNATIONAL_LAW; // 사회과학대학 - case "사회복지학부" -> usaintAuthResponse.setMajor(Major.SOCIAL_WELFARE); - case "정치외교학과" -> usaintAuthResponse.setMajor(Major.POLITICAL_SCIENCE); - case "언론홍보학과" -> usaintAuthResponse.setMajor(Major.MEDIA_COMMUNICATION); - case "행정학부" -> usaintAuthResponse.setMajor(Major.PUBLIC_ADMINISTRATION); - case "정보사회학과" -> usaintAuthResponse.setMajor(Major.INFORMATION_SOCIETY); - case "평생교육학과" -> usaintAuthResponse.setMajor(Major.LIFELONG_EDUCATION); + case "사회복지학부" -> major = Major.SOCIAL_WELFARE; + case "정치외교학과" -> major = Major.POLITICAL_SCIENCE; + case "언론홍보학과" -> major = Major.MEDIA_COMMUNICATION; + case "행정학부" -> major = Major.PUBLIC_ADMINISTRATION; + case "정보사회학과" -> major = Major.INFORMATION_SOCIETY; + case "평생교육학과" -> major = Major.LIFELONG_EDUCATION; // 경제통상대학 - case "경제학과" -> usaintAuthResponse.setMajor(Major.ECONOMICS); - case "금융경제학과" -> usaintAuthResponse.setMajor(Major.FINANCIAL_ECONOMICS); - case "글로벌통상학과" -> usaintAuthResponse.setMajor(Major.GLOBAL_TRADE); - case "국제무역학과" -> usaintAuthResponse.setMajor(Major.INTERNATIONAL_TRADE); + case "경제학과" -> major = Major.ECONOMICS; + case "금융경제학과" -> major = Major.FINANCIAL_ECONOMICS; + case "글로벌통상학과" -> major = Major.GLOBAL_TRADE; + case "국제무역학과" -> major = Major.INTERNATIONAL_TRADE; // 경영대학 - case "경영학부" -> usaintAuthResponse.setMajor(Major.BUSINESS_ADMINISTRATION); - case "회계학과" -> usaintAuthResponse.setMajor(Major.ACCOUNTING); - case "벤처경영학과" -> usaintAuthResponse.setMajor(Major.VENTURE_MANAGEMENT); - case "복지경영학과" -> usaintAuthResponse.setMajor(Major.WELFARE_MANAGEMENT); - case "벤처중소기업학과" -> usaintAuthResponse.setMajor(Major.VENTURE_SME); - case "금융학부" -> usaintAuthResponse.setMajor(Major.FINANCE); - case "혁신경영학과" -> usaintAuthResponse.setMajor(Major.INNOVATION_MANAGEMENT); - case "회계세무학과" -> usaintAuthResponse.setMajor(Major.ACCOUNTING_TAX); + case "경영학부" -> major = Major.BUSINESS_ADMINISTRATION; + case "회계학과" -> major = Major.ACCOUNTING; + case "벤처경영학과" -> major = Major.VENTURE_MANAGEMENT; + case "복지경영학과" -> major = Major.WELFARE_MANAGEMENT; + case "벤처중소기업학과" -> major = Major.VENTURE_SME; + case "금융학부" -> major = Major.FINANCE; + case "혁신경영학과" -> major = Major.INNOVATION_MANAGEMENT; + case "회계세무학과" -> major = Major.ACCOUNTING_TAX; // 공과대학 - case "화학공학과" -> usaintAuthResponse.setMajor(Major.CHEMICAL_ENGINEERING); - case "전기공학부" -> usaintAuthResponse.setMajor(Major.ELECTRICAL_ENGINEERING); - case "건축학부" -> usaintAuthResponse.setMajor(Major.ARCHITECTURE); - case "산업ㆍ정보시스템공학과" -> usaintAuthResponse.setMajor(Major.INDUSTRIAL_INFO_SYSTEMS); - case "기계공학부" -> usaintAuthResponse.setMajor(Major.MECHANICAL_ENGINEERING); - case "신소재공학과" -> usaintAuthResponse.setMajor(Major.MATERIALS_SCIENCE); + case "화학공학과" -> major = Major.CHEMICAL_ENGINEERING; + case "전기공학부" -> major = Major.ELECTRICAL_ENGINEERING; + case "건축학부" -> major = Major.ARCHITECTURE; + case "산업ㆍ정보시스템공학과" -> major = Major.INDUSTRIAL_INFO_SYSTEMS; + case "기계공학부" -> major = Major.MECHANICAL_ENGINEERING; + case "신소재공학과" -> major = Major.MATERIALS_SCIENCE; // IT대학 - case "컴퓨터학부" -> usaintAuthResponse.setMajor(Major.COM); - case "소프트웨어학부" -> usaintAuthResponse.setMajor(Major.SW); - case "글로벌미디어학부" -> usaintAuthResponse.setMajor(Major.GM); - case "미디어경영학과" -> usaintAuthResponse.setMajor(Major.MB); - case "AI융합학부" -> usaintAuthResponse.setMajor(Major.AI); - case "전자정보공학부" -> usaintAuthResponse.setMajor(Major.EE); - case "정보보호학과" -> usaintAuthResponse.setMajor(Major.IP); + case "컴퓨터학부" -> major = Major.COM; + case "소프트웨어학부" -> major = Major.SW; + case "글로벌미디어학부" -> major = Major.GM; + case "미디어경영학과" -> major = Major.MB; + case "AI융합학부" -> major = Major.AI; + case "전자정보공학부" -> major = Major.EE; + case "정보보호학과" -> major = Major.IP; // 자유전공학부 - case "자유전공학부" -> usaintAuthResponse.setMajor(Major.LIBERAL_ARTS); + case "자유전공학부" -> major = Major.LIBERAL_ARTS; default -> { log.debug("{} is not a supported major.", majorStr); @@ -210,12 +209,18 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) { } } } - case "과정/학기" -> usaintAuthResponse.setEnrollmentStatus(strong.text()); - case "학년/학기" -> usaintAuthResponse.setYearSemester(strong.text()); + case "과정/학기" -> enrollmentStatus = strong.text(); + case "학년/학기" -> yearSemester = strong.text(); } } - return usaintAuthResponse; + return USaintAuthResponseDTO.of( + studentNumber, + name, + enrollmentStatus, + yearSemester, + major + ); } private ResponseEntity requestUSaintSSO(String sToken, String sIdno) { @@ -226,13 +231,13 @@ private ResponseEntity requestUSaintSSO(String sToken, String sIdno) { .header("Cookie", "sToken=" + sToken + "; sIdno=" + sIdno) .retrieve() .toEntity(String.class) // ResponseEntity 전체 반환 (body + header 포함) - .block(); // 동기 방식 + .block(); } private ResponseEntity requestUSaintPortal(StringBuilder cookie) { return webClient.get() .uri(USaintPortalUrl) - .header(HttpHeaders.COOKIE, cookie.toString()) // StringBuilder → String 변환 + .header(HttpHeaders.COOKIE, cookie.toString()) .retrieve() .toEntity(String.class) .block(); diff --git a/src/main/java/com/assu/server/domain/auth/service/SignUpService.java b/src/main/java/com/assu/server/domain/auth/service/SignUpService.java index 8cd8cd0c..8995c942 100644 --- a/src/main/java/com/assu/server/domain/auth/service/SignUpService.java +++ b/src/main/java/com/assu/server/domain/auth/service/SignUpService.java @@ -1,13 +1,15 @@ package com.assu.server.domain.auth.service; -import com.assu.server.domain.auth.dto.signup.AdminSignUpRequest; -import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequest; -import com.assu.server.domain.auth.dto.signup.SignUpResponse; -import com.assu.server.domain.auth.dto.signup.StudentTokenSignUpRequest; +import com.assu.server.domain.auth.dto.signup.AdminSignUpRequestDTO; +import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequestDTO; +import com.assu.server.domain.auth.dto.signup.SignUpResponseDTO; +import com.assu.server.domain.auth.dto.signup.StudentTokenSignUpRequestDTO; import org.springframework.web.multipart.MultipartFile; public interface SignUpService { - SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req); - SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage); - SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImage); + SignUpResponseDTO signupSsuStudent(StudentTokenSignUpRequestDTO req); + + SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFile licenseImage); + + SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile signImage); } diff --git a/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java index 51cfa441..8ca40939 100644 --- a/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java @@ -2,12 +2,12 @@ import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.admin.repository.AdminRepository; -import com.assu.server.domain.auth.dto.common.UserBasicInfo; +import com.assu.server.domain.auth.dto.common.TokensDTO; import com.assu.server.domain.auth.dto.signup.*; -import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload; -import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest; -import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayloadDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthRequestDTO; +import com.assu.server.domain.auth.dto.ssu.USaintAuthResponseDTO; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.auth.repository.SSUAuthRepository; import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter; @@ -26,12 +26,12 @@ import com.assu.server.domain.user.repository.StudentRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.infra.s3.AmazonS3Manager; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -39,6 +39,7 @@ @Service @RequiredArgsConstructor +@Transactional public class SignUpServiceImpl implements SignUpService { private final MemberRepository memberRepository; @@ -46,7 +47,6 @@ public class SignUpServiceImpl implements SignUpService { private final PartnerRepository partnerRepository; private final AdminRepository adminRepository; - // Adapter 들을 주입받아서, signup 시에 사용 private final List realmAuthAdapters; private final AmazonS3Manager amazonS3Manager; @@ -64,109 +64,92 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) { .orElseThrow(() -> new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION)); } - /* 숭실대 학생: sToken, sIdno 기반 회원가입 */ @Override - @Transactional - public SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req) { - // 중복 체크 - if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) { + public SignUpResponseDTO signupSsuStudent(StudentTokenSignUpRequestDTO req) { + if (memberRepository.existsByPhoneNum(req.phoneNumber())) { throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); } // 1) 유세인트 인증 및 학생 정보 추출 - USaintAuthRequest authRequest = USaintAuthRequest.builder() - .sToken(req.getStudentTokenAuth().getSToken()) - .sIdno(req.getStudentTokenAuth().getSIdno()) - .build(); + USaintAuthRequestDTO authRequest = new USaintAuthRequestDTO( + req.studentTokenAuth().sToken(), + req.studentTokenAuth().sIdno() + ); - USaintAuthResponse authResponse = ssuAuthService.uSaintAuth(authRequest); + USaintAuthResponseDTO authResponse = ssuAuthService.uSaintAuth(authRequest); - // 학번 중복 체크 - if (ssuAuthRepository.existsByStudentNumber(authResponse.getStudentNumber().toString())) { + if (ssuAuthRepository.existsByStudentNumber(authResponse.studentNumber())) { throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT); } // 2) member 생성 Member member = memberRepository.save( Member.builder() - .phoneNum(req.getPhoneNumber()) + .phoneNum(req.phoneNumber()) .isPhoneVerified(true) + .isLocationTermAgreed(req.locationAgree()) + .isMarketingTermAgreed(req.marketingAgree()) .role(UserRole.STUDENT) .isActivated(ActivationStatus.ACTIVE) .build()); // 3) SSUAuth 생성 (학번만 저장) RealmAuthAdapter adapter = pickAdapter(AuthRealm.SSU); - adapter.registerCredentials(member, authResponse.getStudentNumber().toString(), ""); // 더미 패스워드 + adapter.registerCredentials(member, authResponse.studentNumber(), ""); // 더미 패스워드 // 4) Student 프로필 생성 (크롤링된 정보 사용) Student student = Student.builder() .member(member) - .name(authResponse.getName()) - .department(authResponse.getMajor().getDepartment()) - .major(authResponse.getMajor()) - .enrollmentStatus(parseEnrollmentStatus(authResponse.getEnrollmentStatus())) - .yearSemester(authResponse.getYearSemester()) - .university(University.SSU) // 고정값 + .name(authResponse.name()) + .department(authResponse.major().getDepartment()) + .major(authResponse.major()) + .enrollmentStatus(parseEnrollmentStatus(authResponse.enrollmentStatus())) + .yearSemester(authResponse.yearSemester()) + .university(University.SSU) // Todo: 추후 다른 대학도 추가할 시 로직 변경 필요 .stamp(0) .build(); studentRepository.save(student); // 5) JWT 토큰 발급 - Tokens tokens = jwtUtil.issueTokens( + TokensDTO tokens = jwtUtil.issueTokens( member.getId(), - authResponse.getStudentNumber().toString(), // studentNumber + authResponse.studentNumber(), UserRole.STUDENT, "SSU"); - // 6) Student 정보로 직접 UserBasicInfo 생성 - UserBasicInfo basicInfo = UserBasicInfo.builder() - .name(student.getName()) - .university(student.getUniversity().getDisplayName()) - .department(student.getDepartment().getDisplayName()) - .major(student.getMajor().getDisplayName()) - .build(); - - return SignUpResponse.builder() - .memberId(member.getId()) - .role(UserRole.STUDENT) - .status(member.getIsActivated()) - .tokens(tokens) - .basicInfo(basicInfo) - .build(); + // 6) SignUpResponseDTO 생성 + return SignUpResponseDTO.from(member, tokens); } - /* 제휴업체: MULTIPART(payload JSON + licenseImage) */ @Override - @Transactional - public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage) { - if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) { + public SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFile licenseImage) { + if (memberRepository.existsByPhoneNum(req.phoneNumber())) { throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); } // 1) member 생성 Member member = memberRepository.save( Member.builder() - .phoneNum(req.getPhoneNumber()) + .phoneNum(req.phoneNumber()) .isPhoneVerified(true) + .isLocationTermAgreed(req.locationAgree()) + .isMarketingTermAgreed(req.marketingAgree()) .role(UserRole.PARTNER) .isActivated(ActivationStatus.ACTIVE) // Todo 초기에 SUSPEND 로직 추가해야함, 허가 후 ACTIVE .build()); // 2) RealmAuthAdapter 로 Common 자격 저장 RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON); - adapter.registerCredentials(member, req.getCommonAuth().getEmail(), req.getCommonAuth().getPassword()); + adapter.registerCredentials(member, req.commonAuth().email(), req.commonAuth().password()); - // 파일 업로드 + 파트너 정보 String keyPath = "partners/" + member.getId() + "/" + licenseImage.getOriginalFilename(); String keyName = amazonS3Manager.generateKeyName(keyPath); String licenseUrl = amazonS3Manager.uploadFile(keyName, licenseImage); - CommonInfoPayload info = req.getCommonInfo(); - var sp = Optional.ofNullable(info.getSelectedPlace()) + CommonInfoPayloadDTO info = req.commonInfo(); + var sp = Optional.ofNullable(info.selectedPlace()) .orElseThrow(() -> new CustomAuthException(ErrorStatus._BAD_REQUEST)); // selectedPlace 필수 - // selectedPlace로부터 주소/좌표 생성 String address = pickDisplayAddress(sp.getRoadAddress(), sp.getAddress()); Double lat = sp.getLatitude(); Double lng = sp.getLongitude(); @@ -176,9 +159,9 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice Partner partner = partnerRepository.save( Partner.builder() .member(member) - .name(info.getName()) + .name(info.name()) .address(address) - .detailAddress(info.getDetailAddress()) + .detailAddress(info.detailAddress()) .licenseUrl(licenseUrl) .point(point) .latitude(lat) @@ -186,11 +169,11 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice .build()); // store 생성/연결 - Optional storeOpt = storeRepository.findBySameAddress(address, info.getDetailAddress()); + Optional storeOpt = storeRepository.findBySameAddress(address, info.detailAddress()); if (storeOpt.isPresent()) { Store store = storeOpt.get(); store.linkPartner(partner); - store.setName(info.getName()); + store.setName(info.name()); store.setGeo(lat, lng, point); storeRepository.save(store); } else { @@ -198,9 +181,9 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice .partner(partner) .rate(0) .isActivate(ActivationStatus.ACTIVE) - .name(info.getName()) + .name(info.name()) .address(address) - .detailAddress(info.getDetailAddress()) + .detailAddress(info.detailAddress()) .latitude(lat) .longitude(lng) .point(point) @@ -209,53 +192,43 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice } // 4) 토큰 발급 - Tokens tokens = jwtUtil.issueTokens( + TokensDTO tokens = jwtUtil.issueTokens( member.getId(), - req.getCommonAuth().getEmail(), + req.commonAuth().email(), UserRole.PARTNER, - adapter.authRealmValue()); + adapter.authRealmValue() + ); - // 5) Partner 정보로 직접 UserBasicInfo 생성 - UserBasicInfo basicInfo = UserBasicInfo.builder() - .name(partner.getName()) - .build(); - - return SignUpResponse.builder() - .memberId(member.getId()) - .role(UserRole.PARTNER) - .status(member.getIsActivated()) - .tokens(tokens) - .basicInfo(basicInfo) - .build(); + // 5) SignUpResponseDTO 생성 + return SignUpResponseDTO.from(member, tokens); } - /* 관리자: MULTIPART(payload JSON + signImage) */ @Override - @Transactional - public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImage) { - if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) { + public SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile signImage) { + if (memberRepository.existsByPhoneNum(req.phoneNumber())) { throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); } // 1) member 생성 Member member = memberRepository.save( Member.builder() - .phoneNum(req.getPhoneNumber()) + .phoneNum(req.phoneNumber()) .isPhoneVerified(true) + .isLocationTermAgreed(req.locationAgree()) + .isMarketingTermAgreed(req.marketingAgree()) .role(UserRole.ADMIN) .isActivated(ActivationStatus.ACTIVE) // Todo 초기에 SUSPEND 로직 추가해야함, 허가 후 ACTIVE .build()); // 2) RealmAuthAdapter 로 Common 자격 저장 RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON); - adapter.registerCredentials(member, req.getCommonAuth().getEmail(), req.getCommonAuth().getPassword()); + adapter.registerCredentials(member, req.commonAuth().email(), req.commonAuth().password()); - // 파일 업로드 + 관리자 정보 String keyPath = "admins/" + member.getId() + "/" + signImage.getOriginalFilename(); String keyName = amazonS3Manager.generateKeyName(keyPath); String signUrl = amazonS3Manager.uploadFile(keyName, signImage); - CommonInfoPayload info = req.getCommonInfo(); - var sp = Optional.ofNullable(info.getSelectedPlace()) + CommonInfoPayloadDTO info = req.commonInfo(); + var sp = Optional.ofNullable(info.selectedPlace()) .orElseThrow(() -> new CustomAuthException(ErrorStatus._BAD_REQUEST)); // selectedPlace 필수 // selectedPlace로부터 주소/좌표 생성 @@ -267,13 +240,13 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag // 3) Admin 프로필 생성 Admin admin = adminRepository.save( Admin.builder() - .major(req.getCommonAuth().getMajor()) - .department(req.getCommonAuth().getDepartment()) - .university(req.getCommonAuth().getUniversity()) + .major(req.commonAuth().major()) + .department(req.commonAuth().department()) + .university(req.commonAuth().university()) .member(member) - .name(info.getName()) + .name(info.name()) .officeAddress(address) - .detailAddress(info.getDetailAddress()) + .detailAddress(info.detailAddress()) .signUrl(signUrl) .point(point) .latitude(lat) @@ -281,29 +254,14 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag .build()); // 4) 토큰 발급 - Tokens tokens = jwtUtil.issueTokens( + TokensDTO tokens = jwtUtil.issueTokens( member.getId(), - req.getCommonAuth().getEmail(), + req.commonAuth().email(), UserRole.ADMIN, adapter.authRealmValue()); - // 5) Admin 정보로 직접 UserBasicInfo 생성 + null check - String department = admin.getDepartment() != null ? admin.getDepartment().getDisplayName() : null; - String major = admin.getMajor() != null ? admin.getMajor().getDisplayName() : null; - UserBasicInfo basicInfo = UserBasicInfo.builder() - .name(admin.getName()) - .university(admin.getUniversity().getDisplayName()) - .department(department) - .major(major) - .build(); - - return SignUpResponse.builder() - .memberId(member.getId()) - .role(UserRole.ADMIN) - .status(member.getIsActivated()) - .tokens(tokens) - .basicInfo(basicInfo) - .build(); + // 5) SignUpResponseDTO 생성 + return SignUpResponseDTO.from(member, tokens); } private EnrollmentStatus parseEnrollmentStatus(String status) { @@ -323,7 +281,7 @@ private EnrollmentStatus parseEnrollmentStatus(String status) { } } - public Point toPoint(Double lat, Double lng) { + private Point toPoint(Double lat, Double lng) { if (lat == null || lng == null) return null; Point p = geometryFactory.createPoint(new Coordinate(lng, lat)); // x=lng, y=lat diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java b/src/main/java/com/assu/server/domain/auth/service/VerificationService.java deleted file mode 100644 index aaf28237..00000000 --- a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.assu.server.domain.auth.service; - -import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; - -public interface VerificationService { - void checkPhoneNumberAvailability( - VerificationRequestDTO.PhoneVerificationCheckRequest request); - - void checkEmailAvailability( - VerificationRequestDTO.EmailVerificationCheckRequest request); -} diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java deleted file mode 100644 index 85cc43ef..00000000 --- a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.assu.server.domain.auth.service; - -import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; -import com.assu.server.domain.auth.exception.CustomAuthException; -import com.assu.server.domain.auth.repository.CommonAuthRepository; -import com.assu.server.domain.member.repository.MemberRepository; -import com.assu.server.global.apiPayload.code.status.ErrorStatus; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class VerificationServiceImpl implements VerificationService { - - private final MemberRepository memberRepository; - private final CommonAuthRepository commonAuthRepository; - - @Override - public void checkPhoneNumberAvailability( - VerificationRequestDTO.PhoneVerificationCheckRequest request) { - - boolean exists = memberRepository.existsByPhoneNum(request.getPhoneNumber()); - - if (exists) { - throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); - } - } - - @Override - public void checkEmailAvailability( - VerificationRequestDTO.EmailVerificationCheckRequest request) { - - boolean exists = commonAuthRepository.existsByEmail(request.getEmail()); - - if (exists) { - throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL); - } - } -} diff --git a/src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java b/src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java index f7843c4b..3ef07b47 100644 --- a/src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java +++ b/src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java @@ -1,6 +1,5 @@ package com.assu.server.domain.auth.service; public interface WithdrawalService { - void withdrawCurrentUser(String authorization); } diff --git a/src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java index 0532d9d2..d4395df0 100644 --- a/src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java @@ -6,7 +6,7 @@ import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import io.jsonwebtoken.Claims; -import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -14,50 +14,36 @@ @Slf4j @Service @RequiredArgsConstructor +@Transactional public class WithdrawalServiceImpl implements WithdrawalService { private final MemberRepository memberRepository; private final JwtUtil jwtUtil; @Override - @Transactional public void withdrawCurrentUser(String authorization) { String rawAccessToken = jwtUtil.getTokenFromHeader(authorization); - // Access 토큰에서 memberId 추출 Claims claims = jwtUtil.validateTokenOnlySignature(rawAccessToken); Long memberId = ((Number) claims.get("userId")).longValue(); - log.info("현재 사용자 탈퇴 시작: memberId={}", memberId); - - // 2) 회원 탈퇴 처리 withdrawMember(memberId); - // 3) 현재 Access 토큰을 블랙리스트에 등록 jwtUtil.blacklistAccess(rawAccessToken); - - log.info("현재 사용자 탈퇴 완료: memberId={}", memberId); } private void withdrawMember(Long memberId) { - log.info("회원 탈퇴 시작: memberId={}", memberId); - - // 1) 회원 존재 여부 확인 Member member = memberRepository.findById(memberId) .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER)); - // 2) 이미 탈퇴된 회원인지 확인 if (member.getDeletedAt() != null) { throw new CustomAuthException(ErrorStatus.MEMBER_ALREADY_WITHDRAWN); } - // 3) 소프트 삭제: deletedAt 필드에 현재 시간 설정 + // 소프트 삭제 처리 member.setDeletedAt(java.time.LocalDateTime.now()); memberRepository.save(member); - // 4) 해당 회원의 모든 토큰 무효화 jwtUtil.removeAllRefreshTokens(memberId); - - log.info("회원 탈퇴 완료: memberId={}", memberId); } } diff --git a/src/main/java/com/assu/server/domain/member/entity/Member.java b/src/main/java/com/assu/server/domain/member/entity/Member.java index 552e57d3..f077437d 100644 --- a/src/main/java/com/assu/server/domain/member/entity/Member.java +++ b/src/main/java/com/assu/server/domain/member/entity/Member.java @@ -9,6 +9,7 @@ import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.user.entity.Student; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.*; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; @@ -31,23 +32,31 @@ public class Member extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @NotNull private String phoneNum; + @NotNull private Boolean isPhoneVerified; - private LocalDateTime phoneVerifiedAt; + @NotNull + private Boolean isLocationTermAgreed; - private String profileUrl; + @NotNull + private Boolean isMarketingTermAgreed; @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) @JdbcTypeCode(SqlTypes.VARCHAR) + @NotNull private UserRole role; // STUDENT, ADMIN, PARTNER @Enumerated(EnumType.STRING) @Column(nullable = false) + @NotNull private ActivationStatus isActivated; // ACTIVE, INACTIVE, SUSPEND + private String profileUrl; + // 소프트 삭제를 위한 삭제 시점 private LocalDateTime deletedAt; diff --git a/src/main/java/com/assu/server/domain/term/controller/TermController.java b/src/main/java/com/assu/server/domain/term/controller/TermController.java deleted file mode 100644 index abd9dfb5..00000000 --- a/src/main/java/com/assu/server/domain/term/controller/TermController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.term.controller; - -public class TermController { -} diff --git a/src/main/java/com/assu/server/domain/term/converter/TermConverter.java b/src/main/java/com/assu/server/domain/term/converter/TermConverter.java deleted file mode 100644 index a5da9407..00000000 --- a/src/main/java/com/assu/server/domain/term/converter/TermConverter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.term.converter; - -public class TermConverter { -} diff --git a/src/main/java/com/assu/server/domain/term/dto/TermRequestDTO.java b/src/main/java/com/assu/server/domain/term/dto/TermRequestDTO.java deleted file mode 100644 index 65ec572c..00000000 --- a/src/main/java/com/assu/server/domain/term/dto/TermRequestDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.term.dto; - -public class TermRequestDTO { -} diff --git a/src/main/java/com/assu/server/domain/term/dto/TermResponseDTO.java b/src/main/java/com/assu/server/domain/term/dto/TermResponseDTO.java deleted file mode 100644 index 2d45f0ec..00000000 --- a/src/main/java/com/assu/server/domain/term/dto/TermResponseDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.term.dto; - -public class TermResponseDTO { -} diff --git a/src/main/java/com/assu/server/domain/term/entity/Term.java b/src/main/java/com/assu/server/domain/term/entity/Term.java deleted file mode 100644 index dcece7d0..00000000 --- a/src/main/java/com/assu/server/domain/term/entity/Term.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.assu.server.domain.term.entity; - -import java.time.LocalDate; - -import com.assu.server.domain.common.entity.BaseEntity; - -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -@Builder -@AllArgsConstructor -public class Term extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - - private String content; - - private Boolean isAgreed; - - private LocalDate agreedDate; - - private LocalDate disagreedDate; -} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java b/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java deleted file mode 100644 index 11259e96..00000000 --- a/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.assu.server.domain.term.entity.mapping; -import com.assu.server.domain.common.entity.BaseEntity; -import com.assu.server.domain.member.entity.Member; -import com.assu.server.domain.term.entity.Term; - -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - - -@Entity -@Getter -@NoArgsConstructor -@Builder -@AllArgsConstructor -public class TermAgreement extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "term_id") - private Term term; -} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/term/repository/TermRepository.java b/src/main/java/com/assu/server/domain/term/repository/TermRepository.java deleted file mode 100644 index 7ffd80eb..00000000 --- a/src/main/java/com/assu/server/domain/term/repository/TermRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.term.repository; - -public class TermRepository { -} diff --git a/src/main/java/com/assu/server/domain/term/service/TermService.java b/src/main/java/com/assu/server/domain/term/service/TermService.java deleted file mode 100644 index e90bbf68..00000000 --- a/src/main/java/com/assu/server/domain/term/service/TermService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.assu.server.domain.term.service; - -import org.springframework.stereotype.Service; - -@Service -public interface TermService { -} diff --git a/src/main/java/com/assu/server/domain/term/service/TermServiceImpl.java b/src/main/java/com/assu/server/domain/term/service/TermServiceImpl.java deleted file mode 100644 index b3d80b20..00000000 --- a/src/main/java/com/assu/server/domain/term/service/TermServiceImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.term.service; - -public class TermServiceImpl implements TermService { -} diff --git a/src/main/java/com/assu/server/global/util/PrincipalDetails.java b/src/main/java/com/assu/server/global/util/PrincipalDetails.java index dc7582be..0263c3f7 100644 --- a/src/main/java/com/assu/server/global/util/PrincipalDetails.java +++ b/src/main/java/com/assu/server/global/util/PrincipalDetails.java @@ -1,7 +1,7 @@ package com.assu.server.global.util; import com.assu.server.domain.member.entity.Member; -import com.assu.server.domain.auth.entity.AuthRealm; +import com.assu.server.domain.auth.entity.enums.AuthRealm; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.common.enums.UserRole; import com.assu.server.domain.member.entity.Member; @@ -44,7 +44,7 @@ public Collection getAuthorities() { @Override public String getPassword() { // 폼 로그인/DaoAuthenticationProvider를 쓴다면 반드시 반환 - return member.getCommonAuth().getPassword(); + return member.getCommonAuth().getHashedPassword(); } @Override