Skip to content
Original file line number Diff line number Diff line change
@@ -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);
}
Comment on lines +25 to +27

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider throwing a more specific exception or creating a custom exception to better reflect the business logic. This can improve error handling and provide more context to the caller.

Suggested change
if (profile.getRecommenderPhoneNumber() != null) {
throw new CustomRuntimeException(ErrorCode.ALREADY_REGISTERED_RECOMMENDER);
}
if (profile.getRecommenderPhoneNumber() != null) {
throw new AlreadyRegisteredRecommenderException("Recommender already registered for this profile.");
}


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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines 32 to 36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The removal of recommenderPhoneNumber from ApplicationJpaEntity seems correct given the new design. Ensure that all references to this field have been removed or updated in other parts of the application to avoid potential NullPointerException or unexpected behavior.



@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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
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;
import lombok.Builder;
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 {

Expand Down Expand Up @@ -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;
Expand All @@ -84,4 +95,7 @@ public void edit(final EditProfileRequest request) {
this.grade = request.grade();
}

public void registerRecommenderPhoneNumber(final String recommenderPhoneNumber) {
this.recommenderPhoneNumber = recommenderPhoneNumber;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public interface ProfileJpaRepository extends JpaRepository<ProfileJpaEntity, Lo
boolean existsByUserId(Long userId);

Optional<ProfileJpaEntity> findByUserId(Long userId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Disabling CORS completely (.cors(AbstractHttpConfigurer::disable)) is generally not recommended as it opens up the application to potential security vulnerabilities. It's better to configure CORS properly to allow only specific origins. If CORS is intended to be disabled temporarily, add a comment explaining the reason and a reminder to re-enable it with proper configuration.

// TODO: Re-enable CORS with proper configuration before deploying to production
// http.cors(Customizer.withDefaults());
http.cors(AbstractHttpConfigurer::disable);

.httpBasic(AbstractHttpConfigurer::disable)
.headers(c -> c.frameOptions(FrameOptionsConfig::disable))
.sessionManagement(
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/life/mosu/mosuserver/global/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -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<HandlerMethodArgumentResolver> 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)
Comment on lines +24 to +29

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider using environment variables or configuration properties to manage the allowed origins instead of hardcoding them. This allows for easier configuration changes without requiring code deployments. For example, you can define a property cors.allowed-origins in application.properties and inject it into the WebMvcConfig class.

String[] allowedOrigins = {"http://localhost:3000", "http://localhost:8080",
                        "http://api.mosuedu.com", "https://api.mosuedu.com"};
registry.addMapping("/**")
        .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
        .allowedHeaders("*")
        .allowedOrigins(allowedOrigins)
        .allowCredentials(true)
        .maxAge(3600);

.maxAge(3600);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, "파일을 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserJpaEntity> 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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The recommenderPhoneNumber is hardcoded with a default value. Consider making this configurable or removing it entirely from the initializer if it's not essential for dummy data.


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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApplicationSchoolRequest> schools,
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiResponseWrapper<Void>> 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<ApiResponseWrapper<Boolean>> 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));
}

}
Loading
Loading