Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cc0b84b
delete: 불필요한 파일 삭제
chominju02 Jul 21, 2025
e527e28
refacor: exam 관련 dto 수정
chominju02 Jul 21, 2025
4b24c81
refactor: lunch 관련 dto 수정
chominju02 Jul 21, 2025
95698e3
refactor: 테이블 구조 변경
chominju02 Jul 21, 2025
9174212
refactor: 신청 관련 dto 수정
chominju02 Jul 21, 2025
e427bdf
feat: 신청 단건 조회 기능 구현
chominju02 Jul 21, 2025
bf9279a
feat: 시험장 전체 조회 기능 구현
chominju02 Jul 21, 2025
56eab5f
refactor: 신청 관련 기능 수정
chominju02 Jul 21, 2025
4983a02
refactor: payment examApplicationId 로 수정
chominju02 Jul 21, 2025
f7153f5
feat: lunch 관련 에러코드 추가
chominju02 Jul 21, 2025
b96d776
chore: 주석 처리
chominju02 Jul 21, 2025
163e4c4
chore: comment out developmentOnly dependency for spring-boot-devtools
chominju02 Jul 22, 2025
4c1202f
feat: examApplication 관련 기능 구현
chominju02 Jul 22, 2025
b4e6183
feat: application 관련 기능 구현
chominju02 Jul 22, 2025
a2726d8
feat: refactor exam application handling and add new projections
chominju02 Jul 22, 2025
2a97477
chore: gitigore 추가
polyglot-k Jul 22, 2025
f59e3cb
refactor: update S3Properties usage in S3Service to eliminate direct …
polyglot-k Jul 22, 2025
3aa6f2e
feat: implement transaction event handling with DefaultTxEventPublish…
polyglot-k Jul 22, 2025
6b23a5e
feat: implement refund processing service and related components
polyglot-k Jul 22, 2025
ddc4eaa
feat: implement payment transaction handling with event publishing an…
polyglot-k Jul 22, 2025
907b71d
feat: add NumberGenerator interface for generating strings
polyglot-k Jul 22, 2025
823d296
feat: implement FixedQuantityDiscountCalculator and add getAppliedDis…
polyglot-k Jul 22, 2025
b301068
feat: add error codes for exam application and payment processing
polyglot-k Jul 22, 2025
42c98e2
fix: correct key format for S3 pre-signed URL expiration in applicati…
polyglot-k Jul 22, 2025
34f5872
test: add unit test for getAppliedDiscountAmount method in FixedQuant…
polyglot-k Jul 22, 2025
edb2276
chore: application 불필요한 코드 제거
chominju02 Jul 22, 2025
925d36f
chore : resolve the conflict
chominju02 Jul 22, 2025
ca76588
Merge branch 'refactor/mosu-121' of https://github.com/mosu-dev/mosu-…
chominju02 Jul 22, 2025
df3457a
chore : resolve the conflict
chominju02 Jul 22, 2025
a162d72
Merge branch 'develop' of https://github.com/mosu-dev/mosu-server int…
chominju02 Jul 22, 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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies {
testImplementation 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
3,074 changes: 3,074 additions & 0 deletions logs/app.log

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions pinpoint-agent/profiles/.gitignore

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@RequiredArgsConstructor
public class MosuServerApplication implements CommandLineRunner {
public class MosuServerApplication {

public static void main(String[] args) {
SpringApplication.run(MosuServerApplication.class, args);
}

@Override
public void run(String... args) throws Exception {
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
package life.mosu.mosuserver.application.application;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import life.mosu.mosuserver.application.examapplication.ExamApplicationService;
import life.mosu.mosuserver.application.examapplication.dto.RegisterExamApplicationEvent;
import life.mosu.mosuserver.domain.application.ApplicationJpaEntity;
import life.mosu.mosuserver.domain.application.ApplicationJpaRepository;
import life.mosu.mosuserver.domain.application.ExamTicketImageJpaEntity;
import life.mosu.mosuserver.domain.application.ExamTicketImageJpaRepository;
import life.mosu.mosuserver.domain.application.Subject;
import life.mosu.mosuserver.domain.exam.ExamJpaEntity;
import life.mosu.mosuserver.domain.exam.ExamJpaRepository;
import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaEntity;
import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaRepository;
import life.mosu.mosuserver.domain.examapplication.ExamSubjectJpaEntity;
import life.mosu.mosuserver.domain.examapplication.ExamSubjectJpaRepository;
import life.mosu.mosuserver.domain.payment.PaymentJpaEntity;
import life.mosu.mosuserver.domain.payment.PaymentJpaRepository;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.global.util.FileRequest;
import life.mosu.mosuserver.infra.respository.ExamApplicationBulkRepository;
import life.mosu.mosuserver.presentation.application.dto.ApplicationRequest;
import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse;
import life.mosu.mosuserver.presentation.application.dto.CreateApplicationResponse;
import life.mosu.mosuserver.presentation.application.dto.ExamApplicationRequest;
import life.mosu.mosuserver.presentation.application.dto.ExamWithSubjects;
import life.mosu.mosuserver.presentation.application.dto.ExamApplicationResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -39,18 +47,34 @@ public class ApplicationService {
private final ExamSubjectJpaRepository examSubjectJpaRepository;
private final ExamApplicationJpaRepository examApplicationJpaRepository;
private final ExamJpaRepository examJpaRepository;
private final ExamApplicationBulkRepository examApplicationBulkRepository;
private final PaymentJpaRepository paymentJpaRepository;


@Transactional
public ApplicationResponse apply(Long userId, ApplicationRequest request) {
public CreateApplicationResponse apply(Long userId, ApplicationRequest request) {

// 중복 신청 검증
List<Long> examIds = request.examApplication().stream()
List<ExamApplicationRequest> examApplicationRequests = request.examApplication();
Set<Subject> subjects = request.validatedSubjects();

List<Long> examIds = examApplicationRequests.stream()
.map(ExamApplicationRequest::examId)
.toList();

// examId 가 동일 하냐?
//다른 exam 인데 시간이 같나?
//exam_id lunch_id 쌍으로 포함된 exam_application 이 존재하냐
// 중복 신청 검증
Set<Long> examIdSet = new HashSet<>(examIds);
if (examIds.size() != examIdSet.size()) {
throw new RuntimeException("같은 시험을 신청할 수 없습니다.");
}
Comment on lines +65 to +68
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use consistent exception handling.

Good fix for the duplicate exam ID validation issue. However, use CustomRuntimeException for consistency with the rest of the codebase.

-        if (examIds.size() != examIdSet.size()) {
-            throw new RuntimeException("같은 시험을 신청할 수 없습니다.");
-        }
+        if (examIds.size() != examIdSet.size()) {
+            throw new CustomRuntimeException(ErrorCode.DUPLICATE_EXAM_APPLICATION);
+        }

Note: You'll need to add the corresponding error code to the ErrorCode enum.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Set<Long> examIdSet = new HashSet<>(examIds);
if (examIds.size() != examIdSet.size()) {
throw new RuntimeException("같은 시험을 신청할 수 없습니다.");
}
Set<Long> examIdSet = new HashSet<>(examIds);
if (examIds.size() != examIdSet.size()) {
throw new CustomRuntimeException(ErrorCode.DUPLICATE_EXAM_APPLICATION);
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java
around lines 65 to 68, replace the generic RuntimeException thrown for duplicate
exam IDs with CustomRuntimeException to maintain consistency with the codebase.
Also, add a new error code representing this specific validation error to the
ErrorCode enum and use it when constructing the CustomRuntimeException.


// 신청을 1개 이상 신청했는지 검증
if (examIds.isEmpty()) {
throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND);
}

//해당 시험이 진짜 있는 일정인지, lunch 가 없는 시험인데 lunch 를 신청했는지
validateExamIdsAndLunchSelection(examApplicationRequests);

boolean isDuplicate = applicationJpaRepository.existsByUserIdAndExamIds(userId, examIds);
if (isDuplicate) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_DUPLICATED);
Expand All @@ -63,82 +87,163 @@ public ApplicationResponse apply(Long userId, ApplicationRequest request) {
List<ExamApplicationJpaEntity> examApplicationEntities = examApplicationService.register(
RegisterExamApplicationEvent.of(request.examApplication(), applicationId)
);
examApplicationJpaRepository.saveAll(examApplicationEntities);

List<ExamSubjectJpaEntity> allExamSubjects = examApplicationEntities.stream()
.flatMap(examApplication -> {
Long examApplicationId = examApplication.getId();
return request.validatedSubjects().stream()
.map(subject -> ExamSubjectJpaEntity.create(examApplicationId,
subject));
})
.toList();

examSubjectJpaRepository.saveAll(allExamSubjects);
// 시험 신청 목록과 과목 multi-insert
examApplicationBulkRepository.saveAllExamApplicationsWithSubjects(examApplicationEntities,
subjects);

// 수험표 저장
FileRequest fileReq = request.admissionTicket();
if (fileReq.fileName() != null && fileReq.s3Key() != null) {
ExamTicketImageJpaEntity examTicketImage = fileReq
.toExamTicketImageEntity(applicationId);

examTicketImageJpaRepository.save(examTicketImage);
}
return ApplicationResponse.of(applicationId);
return CreateApplicationResponse.of(applicationId);
}


// 전체 신청 내역 조회
// TODO: 테스트 필요
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<ApplicationResponse> getApplications(Long userId) {
List<ApplicationJpaEntity> applications = applicationJpaRepository.findAllByUserId(userId);

List<ApplicationJpaEntity> applications = getUserApplications(userId);

List<ExamApplicationJpaEntity> examApplications = getExamApplications(applications);

Map<Long, ExamJpaEntity> examMap = getExamMap(examApplications);

Map<Long, List<ExamSubjectJpaEntity>> subjectMap = getSubjectMap(examApplications);

Map<Long, List<ExamApplicationResponse>> examResponsesGroupedByApplicationId =
groupExamResponsesByApplication(examApplications, examMap, subjectMap);

return applications.stream()
.map(app -> ApplicationResponse.of(
app.getId(),
examResponsesGroupedByApplicationId.getOrDefault(app.getId(), List.of())
))
.toList();
}

private void validateExamIdsAndLunchSelection(List<ExamApplicationRequest> requests) {

List<Long> examIds = requests.stream()
.map(ExamApplicationRequest::examId).toList();
Comment on lines +132 to +133

Choose a reason for hiding this comment

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

medium

The current logic for validating exam existence might be flawed if the input requests list contains duplicate examIds. requests.stream().map(ExamApplicationRequest::examId).toList() will create a list with duplicates, so examIds.size() could be larger than the number of unique exams. examJpaRepository.findAllById(examIds) will return only unique entities. This will cause the check existingExams.size() != examIds.size() to fail incorrectly. To fix this, work with a Set of exam IDs to ensure uniqueness.

Suggested change
List<Long> examIds = requests.stream()
.map(ExamApplicationRequest::examId).toList();
Set<Long> examIds = requests.stream()
.map(ExamApplicationRequest::examId).collect(Collectors.toSet());


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

Set<Long> examsWithoutLunch = existingExams.stream()
.filter(exam -> exam.getLunchName() == null)
.map(ExamJpaEntity::getId)
.collect(Collectors.toSet());

boolean invalidLunchSelection = requests.stream()
.anyMatch(req -> examsWithoutLunch.contains(req.examId()) && req.isLunchChecked());

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

private List<ApplicationJpaEntity> getUserApplications(Long userId) {
List<ApplicationJpaEntity> applications = applicationJpaRepository.findAllByUserId(userId);
if (applications.isEmpty()) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_LIST_NOT_FOUND);
}
return applications;
}

return applications.stream()
.map(application -> {
// 해당 신청의 시험 신청들 조회
List<ExamApplicationJpaEntity> examApplications =
examApplicationJpaRepository.findByApplicationId(application.getId());

if (examApplications.isEmpty()) {
throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND);
}
// ExamWithSubjects 리스트 생성
List<ExamWithSubjects> exams = examApplications.stream()
.map(examApplication -> {
// 시험 정보 조회
ExamJpaEntity exam = examJpaRepository.findById(
examApplication.getExamId())
.orElseThrow(() -> new CustomRuntimeException(
ErrorCode.EXAM_NOT_FOUND));

// 과목 정보 조회
List<ExamSubjectJpaEntity> examSubjects =
examSubjectJpaRepository.findByExamApplicationId(
examApplication.getId());
Set<String> subjects = examSubjects.stream()
.map(examSubject -> examSubject.getSubject()
.getSubjectName())
.collect(Collectors.toSet());

// ExamWithSubjects 생성
return new ExamWithSubjects(
examApplication.getId(),
exam.getArea().getAreaName(),
exam.getExamDate(),
exam.getSchoolName(),
null,
examApplication.getExamNumber() != null
? examApplication.getExamNumber() : "",
subjects
);
})
.toList();

return ApplicationResponse.of(application.getId(), exams);
})
private List<ExamApplicationJpaEntity> getExamApplications(
List<ApplicationJpaEntity> applications) {
List<Long> applicationIds = applications.stream()
.map(ApplicationJpaEntity::getId)
.toList();

List<ExamApplicationJpaEntity> examApplications =
examApplicationJpaRepository.findByApplicationIdIn(applicationIds);

if (examApplications.isEmpty()) {
throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND);
}
return examApplications;
}

private Map<Long, ExamJpaEntity> getExamMap(List<ExamApplicationJpaEntity> examApplications) {
List<Long> examIds = examApplications.stream()
.map(ExamApplicationJpaEntity::getExamId)
.distinct()
.toList();

return examJpaRepository.findByIdIn(examIds).stream()
.collect(Collectors.toMap(ExamJpaEntity::getId, Function.identity()));
}

private Map<Long, PaymentJpaEntity> getPaymentMap(
List<ExamApplicationJpaEntity> examApplications) {
List<Long> examApplicationIds = examApplications.stream()
.map(ExamApplicationJpaEntity::getId)
.toList();

return paymentJpaRepository.findByExamApplicationIdIn(examApplicationIds).stream()
.collect(Collectors.toMap(PaymentJpaEntity::getExamApplicationId,
Function.identity()));
}

private Map<Long, List<ExamSubjectJpaEntity>> getSubjectMap(
List<ExamApplicationJpaEntity> examApplications) {
List<Long> examApplicationIds = examApplications.stream()
.map(ExamApplicationJpaEntity::getId)
.toList();

return examSubjectJpaRepository.findByExamApplicationIdIn(examApplicationIds).stream()
.collect(Collectors.groupingBy(ExamSubjectJpaEntity::getExamApplicationId));
}

private Map<Long, List<ExamApplicationResponse>> groupExamResponsesByApplication(
List<ExamApplicationJpaEntity> examApplications,
Map<Long, ExamJpaEntity> examMap,
Map<Long, List<ExamSubjectJpaEntity>> subjectMap
) {
Map<Long, PaymentJpaEntity> paymentMap = getPaymentMap(examApplications);

return examApplications.stream()
.map(examApplication -> {
Long examApplicationId = examApplication.getId();
ExamJpaEntity exam = examMap.get(examApplication.getExamId());

Set<String> subjects = subjectMap.getOrDefault(examApplicationId, List.of())
.stream()
.map(es -> es.getSubject().getSubjectName())
.collect(Collectors.toSet());

PaymentJpaEntity payment = paymentMap.get(examApplicationId);
String paymentStatus =
(payment != null) ? payment.getPaymentStatus().name() : null;
Integer totalAmount = (payment != null && payment.getPaymentAmount() != null)
? payment.getPaymentAmount().getTotalAmount()
: 0;

ExamApplicationResponse response = ExamApplicationResponse.of(
examApplicationId,
examApplication.getCreatedAt(),
paymentStatus,
totalAmount,
exam.getSchoolName(),
exam.getExamDate(),
subjects,
examApplication.getIsLunchChecked() ? exam.getLunchName() : "신청 안 함"
);

return Map.entry(examApplication.getApplicationId(), response);
})
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ public void addSchoolCurrentApplicationCount(String schoolName, Long currentCoun
String key = REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName;
redisTemplate.opsForValue().set(key, currentCount);
}



public Long getSchoolApplicationCounts(String schoolName) {
return redisTemplate.opsForValue()
.get(REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,15 @@ public List<ExamResponse> getByArea(String areaName) {
return ExamResponse.fromList(foundExams);
}

public List<Area> getDistinctAreas() {
return examJpaRepository.findDistinctAreas();
public List<String> getDistinctAreas() {
return examJpaRepository.findDistinctAreas().stream()
.map(Area::getAreaName)
.toList();
}

public List<ExamResponse> getExams() {
List<ExamJpaEntity> exams = examJpaRepository.findAll();
return ExamResponse.fromList(exams);
}

}
Loading