diff --git a/src/main/java/life/mosu/mosuserver/application/profile/RecommenderService.java b/src/main/java/life/mosu/mosuserver/application/profile/RecommenderService.java new file mode 100644 index 00000000..1d3af1fc --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/profile/RecommenderService.java @@ -0,0 +1,39 @@ +package life.mosu.mosuserver.application.profile; + +import life.mosu.mosuserver.domain.profile.ProfileJpaEntity; +import life.mosu.mosuserver.domain.profile.ProfileJpaRepository; +import life.mosu.mosuserver.global.exception.CustomRuntimeException; +import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.presentation.profile.dto.RecommenderRegistrationRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RecommenderService { + + private final ProfileJpaRepository profileJpaRepository; + + @Transactional + public void registerRecommender(Long userId, RecommenderRegistrationRequest request) { + ProfileJpaEntity profile = profileJpaRepository.findByUserId(userId) + .orElseThrow(() -> new CustomRuntimeException(ErrorCode.PROFILE_NOT_FOUND)); + if (profile.getRecommenderPhoneNumber() != null) { + throw new CustomRuntimeException(ErrorCode.ALREADY_REGISTERED_RECOMMENDER); + } + + profile.registerRecommenderPhoneNumber(request.phoneNumber()); + } + + @Transactional(readOnly = true, propagation = Propagation.NOT_SUPPORTED) + public Boolean verifyRecommender(Long userId) { + ProfileJpaEntity profile = profileJpaRepository.findByUserId(userId) + .orElseThrow(() -> new CustomRuntimeException(ErrorCode.PROFILE_NOT_FOUND)); + return profile.getRecommenderPhoneNumber() != null; + } + +} diff --git a/src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaEntity.java index cdff38b7..93484f15 100644 --- a/src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaEntity.java @@ -29,28 +29,22 @@ public class ApplicationJpaEntity extends BaseTimeEntity { @Column(name = "guardian_phone_number") private String guardianPhoneNumber; - @Column(name = "recommender_phone_number") - private String recommenderPhoneNumber; - @Column(name = "agreed_to_notices") private Boolean agreedToNotices; @Column(name = "agreed_to_refund_policy") private Boolean agreedToRefundPolicy; - @Builder public ApplicationJpaEntity( final Long userId, final String guardianPhoneNumber, - final String recommenderPhoneNumber, final boolean agreedToNotices, final boolean agreedToRefundPolicy ) { this.userId = userId; this.guardianPhoneNumber = guardianPhoneNumber; - this.recommenderPhoneNumber = recommenderPhoneNumber; this.agreedToNotices = agreedToNotices; this.agreedToRefundPolicy = agreedToRefundPolicy; } diff --git a/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaEntity.java index f8c6ac94..3ebba57a 100644 --- a/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaEntity.java @@ -1,6 +1,16 @@ package life.mosu.mosuserver.domain.profile; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.time.LocalDate; import life.mosu.mosuserver.domain.base.BaseTimeEntity; import life.mosu.mosuserver.presentation.profile.dto.EditProfileRequest; import lombok.AccessLevel; @@ -8,12 +18,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDate; - @Entity @Getter @Table(name = "profile", - uniqueConstraints = @UniqueConstraint(columnNames = "user_id")) + uniqueConstraints = @UniqueConstraint(columnNames = "user_id")) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProfileJpaEntity extends BaseTimeEntity { @@ -47,20 +55,23 @@ public class ProfileJpaEntity extends BaseTimeEntity { @Embedded private SchoolInfoJpaVO schoolInfo; + @Column(name = "recommender_phone_number") + private String recommenderPhoneNumber; + @Enumerated(EnumType.STRING) private Grade grade; @Builder public ProfileJpaEntity( - final Long userId, - final String userName, - final Gender gender, - final LocalDate birth, - final String phoneNumber, - final String email, - final Education education, - final SchoolInfoJpaVO schoolInfo, - final Grade grade + final Long userId, + final String userName, + final Gender gender, + final LocalDate birth, + final String phoneNumber, + final String email, + final Education education, + final SchoolInfoJpaVO schoolInfo, + final Grade grade ) { this.userId = userId; this.userName = userName; @@ -84,4 +95,7 @@ public void edit(final EditProfileRequest request) { this.grade = request.grade(); } + public void registerRecommenderPhoneNumber(final String recommenderPhoneNumber) { + this.recommenderPhoneNumber = recommenderPhoneNumber; + } } diff --git a/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaRepository.java index 9b44caab..3a7754b7 100644 --- a/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/profile/ProfileJpaRepository.java @@ -8,4 +8,5 @@ public interface ProfileJpaRepository extends JpaRepository findByUserId(Long userId); + } diff --git a/src/main/java/life/mosu/mosuserver/global/config/SecurityConfig.java b/src/main/java/life/mosu/mosuserver/global/config/SecurityConfig.java index 6030ad40..f841e39c 100644 --- a/src/main/java/life/mosu/mosuserver/global/config/SecurityConfig.java +++ b/src/main/java/life/mosu/mosuserver/global/config/SecurityConfig.java @@ -12,7 +12,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -71,7 +70,8 @@ public WebSecurityCustomizer webSecurityCustomizer() { @Bean public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) - .cors(Customizer.withDefaults()) +// .cors(Customizer.withDefaults()) + .cors(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .headers(c -> c.frameOptions(FrameOptionsConfig::disable)) .sessionManagement( diff --git a/src/main/java/life/mosu/mosuserver/global/config/WebMvcConfig.java b/src/main/java/life/mosu/mosuserver/global/config/WebMvcConfig.java index 5d2254b3..de2ba092 100644 --- a/src/main/java/life/mosu/mosuserver/global/config/WebMvcConfig.java +++ b/src/main/java/life/mosu/mosuserver/global/config/WebMvcConfig.java @@ -1,20 +1,32 @@ package life.mosu.mosuserver.global.config; +import java.util.List; import life.mosu.mosuserver.global.resolver.UserIdArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.util.List; - @Configuration @RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { + private final UserIdArgumentResolver resolver; @Override public void addArgumentResolvers(final List resolvers) { resolvers.add(resolver); } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("*") + .allowedOrigins("http://localhost:3000", "http://localhost:8080", + "http://api.mosuedu.com", "https://api.mosuedu.com") + .allowCredentials(true) + .maxAge(3600); + } } diff --git a/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java b/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java index f067ccb9..c63d3f1b 100644 --- a/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java +++ b/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java @@ -50,6 +50,7 @@ public enum ErrorCode { PROFILE_CREATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "프로필 등록에 실패했습니다."), PROFILE_UPDATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "프로필 수정에 실패했습니다."), INVALID_GENDER(HttpStatus.BAD_REQUEST, "유효하지 않은 성별 값입니다."), + ALREADY_REGISTERED_RECOMMENDER(HttpStatus.CONFLICT, "이미 추천인을 등록하였습니다."), // 파일 관련 에러 FILE_NOT_FOUND(HttpStatus.NOT_FOUND, "파일을 찾을 수 없습니다."), diff --git a/src/main/java/life/mosu/mosuserver/global/initializer/UserAndProfileInitializer.java b/src/main/java/life/mosu/mosuserver/global/initializer/UserAndProfileInitializer.java new file mode 100644 index 00000000..1aef8164 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/initializer/UserAndProfileInitializer.java @@ -0,0 +1,94 @@ +package life.mosu.mosuserver.global.initializer; + +import static life.mosu.mosuserver.domain.user.UserRole.ROLE_ADMIN; +import static life.mosu.mosuserver.domain.user.UserRole.ROLE_USER; + +import jakarta.annotation.PostConstruct; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import life.mosu.mosuserver.domain.profile.Education; +import life.mosu.mosuserver.domain.profile.Gender; +import life.mosu.mosuserver.domain.profile.Grade; +import life.mosu.mosuserver.domain.profile.ProfileJpaEntity; +import life.mosu.mosuserver.domain.profile.ProfileJpaRepository; +import life.mosu.mosuserver.domain.profile.SchoolInfoJpaVO; +import life.mosu.mosuserver.domain.user.UserJpaEntity; +import life.mosu.mosuserver.domain.user.UserJpaRepository; +import life.mosu.mosuserver.domain.user.UserRole; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserAndProfileInitializer { + + private final UserJpaRepository userRepository; + private final ProfileJpaRepository profileRepository; + private final PasswordEncoder passwordEncoder; + + @PostConstruct + public void init() { + if (userRepository.count() > 0 || profileRepository.count() > 0) { + log.info("이미 더미 데이터가 존재하여 초기화를 건너뜝니다."); + return; + } + + List createdUsers = new ArrayList<>(); + Random random = new Random(); + + for (int i = 1; i <= 10; i++) { + String loginId = "user" + i; + String name = (i % 2 == 0) ? "김철수" + i : "이영희" + i; + Gender gender = (i % 2 == 0) ? Gender.MALE : Gender.FEMALE; + LocalDate birth = LocalDate.of(1990 + (i % 5), (i % 12) + 1, (i % 28) + 1); + String customerKey = "CK-" + i + "-" + System.currentTimeMillis(); + boolean agreedToMarketing = random.nextBoolean(); + UserRole userRole = (i == 1) ? ROLE_ADMIN : ROLE_USER; + + UserJpaEntity user = UserJpaEntity.builder() + .loginId(loginId) + .password(passwordEncoder.encode("password" + i + "!")) + .gender(gender) + .name(name) + .birth(birth) + .customerKey(customerKey) + .agreedToTermsOfService(true) + .agreedToPrivacyPolicy(true) + .agreedToMarketing(agreedToMarketing) + .userRole(userRole) + .build(); + + createdUsers.add(userRepository.save(user)); + + String phoneNumber = + "010-" + String.format("%04d", i) + "-" + String.format("%04d", i + 1000); + String email = "user" + i + "@example.com"; + Education education = Education.values()[random.nextInt(Education.values().length)]; + Grade grade = Grade.values()[random.nextInt(Grade.values().length)]; + SchoolInfoJpaVO schoolInfo = new SchoolInfoJpaVO(("모수대학교" + (i % 3 + 1)), "123-23", + "서울시 모수구 모수동"); + String recommenderPhoneNumber = (i % 3 == 0) ? "010-1234-5678" : null; + + ProfileJpaEntity profile = ProfileJpaEntity.builder() + .userId(user.getId()) + .userName(user.getName()) + .gender(user.getGender()) + .birth(user.getBirth()) + .phoneNumber(phoneNumber) + .email(email) + .education(education) + .schoolInfo(schoolInfo) + .grade(grade) + .build(); + + profile.registerRecommenderPhoneNumber(recommenderPhoneNumber); + + profileRepository.save(profile); + } + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationRequest.java b/src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationRequest.java index a6a1b3cb..877cecb3 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationRequest.java +++ b/src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationRequest.java @@ -13,14 +13,10 @@ public record ApplicationRequest( @Schema(description = "수험표 파일 정보", implementation = FileRequest.class) FileRequest admissionTicket, - @Schema(description = "보호자 전화번호", example = "010-1234-5678") + @Schema(description = "보호자 전화번호 (전화번호 형식은 010-XXXX-XXXX 이어야 합니다.)", example = "010-1234-5678") @PhoneNumberPattern String guardianPhoneNumber, - @Schema(description = "추천인 전화번호", example = "010-8765-4321") - @PhoneNumberPattern - String recommenderPhoneNumber, - @Schema(description = "신청 학교 목록", required = true) @NotNull Set schools, @@ -35,7 +31,6 @@ public ApplicationJpaEntity toEntity(Long userId) { return ApplicationJpaEntity.builder() .userId(userId) .guardianPhoneNumber(guardianPhoneNumber) - .recommenderPhoneNumber(recommenderPhoneNumber) .agreedToNotices(agreementRequest().agreedToNotices()) .agreedToRefundPolicy(agreementRequest().agreedToRefundPolicy()) .build(); diff --git a/src/main/java/life/mosu/mosuserver/presentation/profile/RecommenderController.java b/src/main/java/life/mosu/mosuserver/presentation/profile/RecommenderController.java new file mode 100644 index 00000000..0c327ed1 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/profile/RecommenderController.java @@ -0,0 +1,48 @@ +package life.mosu.mosuserver.presentation.profile; + +import jakarta.validation.Valid; +import life.mosu.mosuserver.application.profile.RecommenderService; +import life.mosu.mosuserver.global.util.ApiResponseWrapper; +import life.mosu.mosuserver.presentation.profile.dto.RecommenderRegistrationRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/recommender") +public class RecommenderController implements RecommenderControllerDocs { + + private final RecommenderService recommenderService; + + @Override + @PostMapping + public ResponseEntity> register( + @RequestParam Long userId, + @Valid @RequestBody RecommenderRegistrationRequest request) { + recommenderService.registerRecommender(userId, request); + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponseWrapper.success(HttpStatus.CREATED, "추천인 등록 성공")); + } + + @Override + @GetMapping("/verify") + public ResponseEntity> verify( + @RequestParam Long userId) { + Boolean isRegistered = recommenderService.verifyRecommender(userId); + + if (isRegistered) { + return ResponseEntity.ok( + ApiResponseWrapper.success(HttpStatus.OK, "이미 추천인이 등록되었습니다.", isRegistered)); + } + return ResponseEntity.ok( + ApiResponseWrapper.success(HttpStatus.OK, "추천인을 등록할 수 있습니다.", isRegistered)); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/profile/RecommenderControllerDocs.java b/src/main/java/life/mosu/mosuserver/presentation/profile/RecommenderControllerDocs.java new file mode 100644 index 00000000..48eeee96 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/profile/RecommenderControllerDocs.java @@ -0,0 +1,25 @@ +package life.mosu.mosuserver.presentation.profile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import life.mosu.mosuserver.global.util.ApiResponseWrapper; +import life.mosu.mosuserver.presentation.profile.dto.RecommenderRegistrationRequest; +import org.springframework.http.ResponseEntity; + +@Tag(name = "Recommender API", description = "추천인 관련 API") +public interface RecommenderControllerDocs { + + @Operation(description = "추천인 등록 API", summary = "추천인 등록은 전화번호로 등록가능합니다. 신청을 진행할때 호출하시면 됩니다. ") + public ResponseEntity> register( + @Schema(description = "API 연동을 위한 사용자 쿼리파라미터 userId 인증 인가 적용때 변경 예정", example = "1") + Long userId, + RecommenderRegistrationRequest request + ); + + @Operation(description = "추천인 등록한지 검증하는 API", summary = "추천인 등록은 딱 한번만 할 수 있어서 검증하는 API입니다.") + public ResponseEntity> verify( + @Schema(description = "API 연동을 위한 사용자 쿼리파라미터 userId 인증 인가 적용때 변경 예정", example = "1") + Long userId + ); +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/profile/dto/RecommenderRegistrationRequest.java b/src/main/java/life/mosu/mosuserver/presentation/profile/dto/RecommenderRegistrationRequest.java new file mode 100644 index 00000000..ff2ddebe --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/profile/dto/RecommenderRegistrationRequest.java @@ -0,0 +1,12 @@ +package life.mosu.mosuserver.presentation.profile.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import life.mosu.mosuserver.global.annotation.PhoneNumberPattern; + +public record RecommenderRegistrationRequest( + @Schema(description = "추천인 전화번호 (전화번호 형식은 010-XXXX-XXXX 이어야 합니다.)", example = "010-8765-4322") + @PhoneNumberPattern + String phoneNumber +) { + +} \ No newline at end of file