Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6e365b9
refactor: banner valid 추가
chominju02 Aug 7, 2025
510da44
refactor: AdminDashboard response 수정
chominju02 Aug 7, 2025
84a88f2
refactor: AdminDashboard count 함수 구현
chominju02 Aug 7, 2025
cc3e589
refactor: 신청 중복 검증 로직 이동
chominju02 Aug 7, 2025
36f5a58
refactor: 신청 약관 동의 체크 validator 구현
chominju02 Aug 7, 2025
c9f0463
refactor: 신청 응시과목 notNull 추가
chominju02 Aug 7, 2025
77ec41d
refactor: event null 체크 및 duration startDate 변경
chominju02 Aug 7, 2025
59a4aee
refactor: recommendation 변수 변경 및 notBlank 추가
chominju02 Aug 7, 2025
da821bd
refactor: profile null 체크 추가
chominju02 Aug 7, 2025
eb090a8
refactor: 신청 multi-insert 시 deleted 옵션 추가, 과목 삭제 시 p.deleted 옵션 검사 추가
chominju02 Aug 7, 2025
f883a22
refactor: 에러 코드 추가
chominju02 Aug 7, 2025
37fc049
refactor: request valid 검증 및 lunch 등록 여부 검증 로직 추가
chominju02 Aug 7, 2025
1562bbc
remove: 주석 제거
chominju02 Aug 7, 2025
af910d3
refactor: examTicketImgUrl 생성 함수 추가
chominju02 Aug 7, 2025
bc960b8
Merge branch 'develop' of https://github.com/mosu-dev/mosu-server int…
chominju02 Aug 8, 2025
ebfa48d
refactor: 관리자 DashBoardResponse 수정
chominju02 Aug 8, 2025
f279815
refactor: whiteList 수정
chominju02 Aug 8, 2025
c8c936c
feat: enhance admin dashboard to count aborted refunds and update ban…
polyglot-k Aug 11, 2025
3a9d195
feat: add agreement validation method to AgreementRequest
polyglot-k Aug 11, 2025
5951dc1
feat: remove existsByUserIdAndExamIds query from ApplicationJpaReposi…
polyglot-k Aug 11, 2025
f85c830
feat: update subjects field to use List instead of Set in Application…
polyglot-k Aug 11, 2025
a46f2ac
feat: enhance application validation by adding agreement check and im…
polyglot-k Aug 11, 2025
cd6da2b
feat: remove commented code from BannerRequest to improve clarity
polyglot-k Aug 11, 2025
c825149
feat: rename refundCounts to refundAbortedCounts in DashBoardResponse…
polyglot-k Aug 11, 2025
26e233c
feat: update DurationRequest to use current date for startDate in toD…
polyglot-k Aug 11, 2025
201b2b8
feat: simplify education description in EditProfileRequest schema
polyglot-k Aug 11, 2025
ec85327
feat: add error codes for terms agreement and exam application requir…
polyglot-k Aug 11, 2025
c72ab75
feat: improve null handling for attachment and duration in EventReque…
polyglot-k Aug 11, 2025
e9372d1
feat: add deleted flag handling in ExamApplication repository queries
polyglot-k Aug 11, 2025
29053d7
feat: refactor exam ticket image URL retrieval and enhance request va…
polyglot-k Aug 11, 2025
a1d61d4
feat: enhance lunch availability check to include lunch price validation
polyglot-k Aug 11, 2025
d7ecbe2
feat: add validation annotations for required fields in ExamRequest
polyglot-k Aug 11, 2025
d3d563a
feat: add exam date validation in register method of ExamService
polyglot-k Aug 11, 2025
64800d9
feat: add filter for deleted payments in deleteExamSubjectsWithDonePa…
polyglot-k Aug 11, 2025
d2a4f2d
feat: remove logging from GetApplicationsStepProcessor
polyglot-k Aug 11, 2025
716093d
feat: rename recommendee fields to recommender in projection and DTO
polyglot-k Aug 11, 2025
14dcdf9
feat: add method to count users by role exclusion in UserJpaRepository
polyglot-k Aug 11, 2025
4084362
feat: simplify user existence check in SignUpAccountStepProcessor
polyglot-k Aug 11, 2025
d8331c6
Merge pull request #250 from mosu-dev/refactor/mosu-241
polyglot-k Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package life.mosu.mosuserver.application.admin;

import life.mosu.mosuserver.domain.examapplication.repository.ExamApplicationJpaRepository;
import life.mosu.mosuserver.domain.refund.repository.RefundJpaRepository;
import life.mosu.mosuserver.domain.refund.repository.RefundFailureLogJpaRepository;
import life.mosu.mosuserver.domain.user.entity.UserRole;
import life.mosu.mosuserver.domain.user.repository.UserJpaRepository;
import life.mosu.mosuserver.presentation.admin.dto.DashBoardResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -13,15 +14,19 @@ public class AdminDashboardService {

private final ExamApplicationJpaRepository examApplicationJpaRepository;
private final UserJpaRepository userJpaRepository;
private final RefundJpaRepository refundJpaRepository;
private final RefundFailureLogJpaRepository refundFailureLogJpaRepository;

// 대시보드 정보 조회
public DashBoardResponse getAll() {
Long applicationCounts = examApplicationJpaRepository.count();
Long refundCounts = refundJpaRepository.count();
Long userCounts = userJpaRepository.count();
return new DashBoardResponse(applicationCounts, refundCounts, userCounts);
}

Long applicationCounts = examApplicationJpaRepository.countAll();
Long refundAbortedCounts = refundFailureLogJpaRepository.count();
Long userCounts = userJpaRepository.countByUserRoleNot(UserRole.ROLE_ADMIN);

return DashBoardResponse.of(
applicationCounts,
refundAbortedCounts,
userCounts
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public CreateApplicationResponse apply(Long userId, ApplicationRequest request)
List<Long> examIds = request.examApplication().stream()
.map(ExamApplicationRequest::examId)
.toList();

validator.agreedToTerms(request);
validator.requestNoDuplicateExams(examIds);
return handleApplication(
userId,
examIds,
Expand Down Expand Up @@ -78,7 +81,6 @@ private CreateApplicationResponse handleApplication(
List<ExamApplicationRequest> examApplications,
FileRequest admissionTicket
) {
validator.requestNoDuplicateExams(examIds);
List<ExamJpaEntity> exams = examJpaRepository.findAllById(examIds);
validator.examDateNotPassed(exams);
validator.examNotFull(exams);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@
import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse;
import life.mosu.mosuserver.presentation.examapplication.dto.ExamApplicationWithStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class GetApplicationsStepProcessor implements
Expand All @@ -33,7 +31,6 @@ public class GetApplicationsStepProcessor implements
public List<ApplicationResponse> process(Long userId) {

List<ApplicationJpaEntity> applications = applicationJpaRepository.findAllByUserId(userId);
log.info("applications info: {}", applications.size());
if (applications.isEmpty()) {
return List.of();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,41 @@
import java.util.Set;
import java.util.stream.Collectors;
import life.mosu.mosuserver.application.exam.cache.ExamQuotaCacheManager;
import life.mosu.mosuserver.domain.application.repository.ApplicationJpaRepository;
import life.mosu.mosuserver.domain.exam.entity.ExamJpaEntity;
import life.mosu.mosuserver.domain.exam.entity.ExamJpaRepository;
import life.mosu.mosuserver.domain.exam.entity.ExamStatus;
import life.mosu.mosuserver.domain.examapplication.repository.ExamApplicationJpaRepository;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.presentation.application.dto.ApplicationRequest;
import life.mosu.mosuserver.presentation.application.dto.ExamApplicationRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class ApplicationValidator {

private final ExamJpaRepository examJpaRepository;
private final ApplicationJpaRepository applicationJpaRepository;
private final ExamApplicationJpaRepository examApplicationJpaRepository;
private final ExamQuotaCacheManager examQuotaCacheManager;

public void agreedToTerms(ApplicationRequest request) {
if (!request.agreement().validateAgreement()) {
throw new CustomRuntimeException(ErrorCode.NOT_AGREED_TO_TERMS);
}
}

public void requestNoDuplicateExams(List<Long> examIds) {
Set<Long> examIdSet = new HashSet<>(examIds);
if (examIds.size() != examIdSet.size()) {
throw new CustomRuntimeException(ErrorCode.EXAM_DUPLICATED);
}
if (examIdSet.isEmpty()) {
throw new CustomRuntimeException(ErrorCode.EXAM_NOT_APPLIED);
}
}

public void examIdsAndLunchSelection(List<ExamApplicationRequest> requests) {
Expand All @@ -39,12 +51,9 @@ public void examIdsAndLunchSelection(List<ExamApplicationRequest> requests) {
List<Long> requestedExamIds = requests.stream()
.map(ExamApplicationRequest::examId)
.toList();
Set<Long> examIdSet = new HashSet<>(requestedExamIds);

List<ExamJpaEntity> existingExams = examJpaRepository.findAllById(requestedExamIds);

if (existingExams.size() != requestedExamIds.size()) {
throw new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND);
}
List<ExamJpaEntity> existingExams = examJpaRepository.findAllById(examIdSet);

lunchSelection(requests, existingExams);
Comment on lines +56 to 58

Choose a reason for hiding this comment

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

high

The validation to check if all requested exams exist has been removed. findAllById will silently ignore non-existent IDs, which could lead to unexpected behavior. It's important to verify that all provided examIds correspond to actual exams in the database by comparing the size of the examIdSet with the number of existingExams found.

Suggested change
List<ExamJpaEntity> existingExams = examJpaRepository.findAllById(examIdSet);
lunchSelection(requests, existingExams);
List<ExamJpaEntity> existingExams = examJpaRepository.findAllById(examIdSet);
if (existingExams.size() != examIdSet.size()) {
throw new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND);
}
lunchSelection(requests, existingExams);

}
Expand All @@ -56,16 +65,18 @@ private void lunchSelection(List<ExamApplicationRequest> requests,
.map(ExamJpaEntity::getId)
.collect(Collectors.toSet());

boolean hasInvalidLunchRequest = requests.stream()
.anyMatch(req -> examsWithoutLunch.contains(req.examId()) && req.isLunchChecked());
requests.stream()
.filter(req -> req.isLunchChecked() && examsWithoutLunch.contains(req.examId()))
.findFirst()
.ifPresent(req -> {
throw new CustomRuntimeException(ErrorCode.LUNCH_SELECTION_INVALID);
});

if (hasInvalidLunchRequest) {
throw new CustomRuntimeException(ErrorCode.LUNCH_SELECTION_INVALID);
}
}

public void noDuplicateApplication(Long userId, List<Long> examIds) {
boolean alreadyApplied = applicationJpaRepository.existsByUserIdAndExamIds(userId, examIds);
boolean alreadyApplied = examApplicationJpaRepository.existsByUserIdAndExamIds(userId,
examIds);
if (alreadyApplied) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_DUPLICATED);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ public class SignUpAccountStepProcessor implements StepProcessor<UserJpaEntity,
@Transactional
@Override
public UserJpaEntity process(UserJpaEntity user) {
if (userRepository.existsByPhoneNumber(user.getPhoneNumber())
|| userRepository.existsByLoginId(user.getLoginId())) {
if (userRepository.existsByPhoneNumber(user.getPhoneNumber())) {
throw new CustomRuntimeException(ErrorCode.USER_ALREADY_EXISTS);
} else if (userRepository.existsByLoginId(user.getLoginId())) {
throw new CustomRuntimeException(ErrorCode.USER_ALREADY_EXISTS);
}
return userRepository.save(user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public class ExamService {

@Transactional
public void register(ExamRequest request) {
validateExamDate(request);
ExamJpaEntity exam = request.toEntity();

examJpaRepository.save(exam);
}

Expand Down Expand Up @@ -71,4 +73,10 @@ public void close(Long examId) {
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND));
exam.close();
}

private void validateExamDate(ExamRequest request) {
if (!request.deadlineTime().isBefore(request.examDate().atStartOfDay())) {
throw new CustomRuntimeException(ErrorCode.EXAM_DATE_AFTER_DEADLINE);
}
Comment on lines +78 to +80

Choose a reason for hiding this comment

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

critical

The deadlineTime in ExamRequest can be null, which will cause a NullPointerException when request.deadlineTime().isBefore() is called. You should add a null check for deadlineTime before using it.

Suggested change
if (!request.deadlineTime().isBefore(request.examDate().atStartOfDay())) {
throw new CustomRuntimeException(ErrorCode.EXAM_DATE_AFTER_DEADLINE);
}
if (request.deadlineTime() != null && !request.deadlineTime().isBefore(request.examDate().atStartOfDay())) {
throw new CustomRuntimeException(ErrorCode.EXAM_DATE_AFTER_DEADLINE);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public class ExamApplicationService {
private final S3Service s3Service;
private final FixedQuantityDiscountCalculator calculator;


@Transactional
public List<ExamApplicationJpaEntity> register(RegisterExamApplicationEvent event) {
List<ExamApplicationJpaEntity> examApplicationEntities = event.toEntity();
Expand All @@ -57,7 +56,6 @@ public void updateSubjects(Long userId, Long examApplicationId,
examSubjectJpaRepository.deleteExamSubjectsWithDonePayment(examApplicationId);
List<ExamSubjectJpaEntity> examSubjects = request.toEntityList(examApplicationId);
examSubjectJpaRepository.saveAll(examSubjects);

}

@Transactional(propagation = Propagation.REQUIRES_NEW)
Expand Down Expand Up @@ -97,27 +95,17 @@ public ExamTicketResponse getExamTicket(Long userId, Long examApplicationId) {
.map(Subject::getSubjectName)
.toList();

String s3Key = examTicketInfo.s3Key();
String examTicketImgUrl = null;

if (s3Key != null) {
examTicketImgUrl = s3Service.getPreSignedUrl(s3Key);
}
String examTicketImgUrl = getExamTicketImgUrl(examTicketInfo);

return ExamTicketResponse.of(examTicketImgUrl, examTicketInfo.userName(),
examTicketInfo.birth(),
examTicketInfo.examNumber(), subjects, examTicketInfo.schoolName());

}


public ExamApplicationInfoResponse getApplication(Long userId, Long examApplicationId,
Long applicationId) {
validateUser(userId, examApplicationId);

//상세 조회는 done 만 가능
// Integer examApplicationCount = paymentJpaRepository.countByExamApplicationId(
// examApplicationId);
List<ExamApplicationJpaEntity> examApplicationEntities = examApplicationJpaRepository.findByApplicationId(
applicationId);
int lunchCount = (int) examApplicationEntities.stream()
Expand All @@ -135,6 +123,7 @@ public ExamApplicationInfoResponse getApplication(Long userId, Long examApplicat
Set<String> subjects = examSubjects.stream()
.map(ExamSubjectJpaEntity::getSubjectName)
.collect(Collectors.toSet());

//totalAmount 는 Lunch 가격이 포함되었을 수도 있음
//totalAmount - Lunch 가격으로 getAppliedDiscountAmount() 메소드에 넣어야함.

Expand Down Expand Up @@ -187,4 +176,15 @@ private void validateExamTicketOpenDate(LocalDate examDate, String examNumber) {
throw new CustomRuntimeException(ErrorCode.EXAM_TICKET_NOT_OPEN);
}
}

private String getExamTicketImgUrl(ExamTicketInfoProjection examTicketInfo) {
String s3Key = examTicketInfo.s3Key();
String examTicketImgUrl = null;

if (s3Key != null) {
examTicketImgUrl = s3Service.getPreSignedUrl(s3Key);
}
return examTicketImgUrl;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public record RecommendationDetailsProjection(
LocalDate birth,
String recommendeeName,
String recommendeePhoneNumber,
String recommendeeBank,
String recommendeeAccountNumber
String recommenderBank,
String recommenderAccountNumber
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,6 @@ AND a.status IN ('PENDING', 'ABORT')
""")
List<ApplicationJpaEntity> findAllByUserId(@Param("userId") Long userId);

@Query(
"""
SELECT CASE WHEN COUNT(a) > 0 THEN true ELSE false END
FROM ApplicationJpaEntity a
JOIN ExamApplicationJpaEntity ea ON a.id = ea.applicationId
JOIN ExamJpaEntity e ON ea.examId = e.id
JOIN PaymentJpaEntity p ON ea.id = p.examApplicationId
WHERE a.userId = :userId
AND p.paymentStatus = 'DONE'
AND e.id IN :examIds
"""
)
boolean existsByUserIdAndExamIds(@Param("userId") Long userId,
@Param("examIds") List<Long> examIds);

@Modifying
@Query(value = """
UPDATE application a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public ExamJpaEntity(
}

public boolean hasNotLunch() {
return lunchName == null;
return this.lunchName == null || this.lunchPrice == null;
}

public void close() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ public interface ExamApplicationJpaRepository extends
JOIN PaymentJpaEntity p on p.examApplicationId = ea.id
WHERE ea.id = :examApplicationId
AND p.paymentStatus = 'DONE'
AND ea.userId = :userId
AND p.deleted = false
""")
Optional<ExamApplicationInfoProjection> findExamApplicationInfoById(Long userId,
Long examApplicationId);
Optional<ExamApplicationInfoProjection> findExamApplicationInfoById(
@Param("userId") Long userId,
@Param("examApplicationId") Long examApplicationId);


@Query("""
Expand All @@ -67,6 +70,7 @@ Optional<ExamApplicationInfoProjection> findExamApplicationInfoById(Long userId,
WHERE ea.id = :examApplicationId
AND u.id = :userId
AND p.paymentStatus = 'DONE'
AND p.deleted = false
""")
Optional<ExamTicketInfoProjection> findExamTicketInfoProjectionById(
@Param("userId") Long userId,
Expand Down Expand Up @@ -105,6 +109,7 @@ Optional<ExamTicketInfoProjection> findExamTicketInfoProjectionById(
JOIN PaymentJpaEntity p on p.examApplicationId = ea.id
WHERE ea.id = :targetId
AND p.paymentStatus = 'DONE'
AND p.deleted = false
""")
Optional<ExamApplicationNotifyProjection> findExamAndPaymentByExamApplicationId(
@Param("targetId") Long targetId);
Expand All @@ -121,6 +126,7 @@ Optional<ExamApplicationNotifyProjection> findExamAndPaymentByExamApplicationId(
JOIN PaymentJpaEntity p ON p.examApplicationId = ea.id
WHERE ea.id = :examApplicationId
AND p.paymentStatus = 'DONE'
AND p.deleted = false
""")
Optional<ExamInfoProjection> findExamInfo(@Param("examApplicationId") Long examApplicationId);

Expand All @@ -137,6 +143,7 @@ Optional<ExamApplicationNotifyProjection> findExamAndPaymentByExamApplicationId(
JOIN PaymentJpaEntity p ON p.examApplicationId = ea.id
WHERE ea.id = :examApplicationId
AND p.paymentStatus = 'DONE'
AND p.deleted = false
""")
Optional<ExamInfoWithExamNumberProjection> findExamInfoWithExamNumber(
@Param("examApplicationId") Long examApplicationId);
Expand All @@ -148,6 +155,7 @@ SELECT case when COUNT(ea) > 0 then true else false end
WHERE ea.id = :examApplicationId
AND ea.userId = :userId
AND p.paymentStatus = 'DONE'
AND p.deleted = false
""")
boolean existByUserIdAndExamApplicationId(@Param("userId") Long userId,
@Param("examApplicationId") Long examApplicationId);
Expand All @@ -161,4 +169,29 @@ boolean existByUserIdAndExamApplicationId(@Param("userId") Long userId,
AND e.deleted = false
""")
Optional<ExamApplicationJpaEntity> findByOrderId(String orderId);

@Query(
"""
SELECT CASE WHEN COUNT(ea) > 0 THEN true ELSE false END
FROM ExamApplicationJpaEntity ea
JOIN PaymentJpaEntity p ON ea.id = p.examApplicationId
WHERE ea.userId = :userId
AND p.paymentStatus = 'DONE'
AND p.deleted = false
AND ea.examId IN :examIds
"""
)
boolean existsByUserIdAndExamIds(
@Param("userId") Long userId,
@Param("examIds") List<Long> examIds);


@Query("""
SELECT COUNT(ea)
FROM ExamApplicationJpaEntity ea
JOIN PaymentJpaEntity p ON ea.id = p.examApplicationId
WHERE p.paymentStatus = 'DONE'
AND p.deleted = false
""")
long countAll();
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public interface ExamSubjectJpaRepository extends JpaRepository<ExamSubjectJpaEn
FROM exam_subject es
JOIN payment p ON es.exam_application_id = p.exam_application_id
WHERE p.status = 'DONE'
AND p.deleted = false
AND p.exam_application_id = :examApplicationId
""", nativeQuery = true)
void deleteExamSubjectsWithDonePayment(@Param("examApplicationId") Long examApplicationId);
Expand Down
Loading