Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
59 changes: 59 additions & 0 deletions .github/workflows/full-test-repeat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Full Test Suite Repeat

on:
workflow_dispatch:
inputs:
repeat_count:
description: '전체 테스트를 반복할 횟수'
required: true
default: '10'

jobs:
repeat-test:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write

steps:
- name: Checkout the code
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Make Gradle wrapper executable
run: chmod +x ./gradlew

- name: Run full test suite N times
run: |
set +e
REPEAT_COUNT="${{ github.event.inputs.repeat_count }}"
FAIL_COUNT=0
for i in $(seq 1 "$REPEAT_COUNT"); do

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-positive or non-integer repeat counts

When a dispatcher supplies 0, a negative value, a fractional value, or malformed text, this loop may execute fewer tests than requested—or none at all—while the preceding set +e ignores seq errors and leaves FAIL_COUNT at zero, producing a successful workflow without the intended test coverage. GNU seq --help confirms that arguments are interpreted as floating-point values and that a default positive sequence below its first value emits nothing; validate REPEAT_COUNT as a positive integer before entering the loop.

Useful? React with 👍 / 👎.

echo "=== Run $i/$REPEAT_COUNT ==="
./gradlew test --rerun-tasks
STATUS=$?
mkdir -p "build/repeat-test-results/run-$i"
cp -r build/test-results/test/. "build/repeat-test-results/run-$i/" 2>/dev/null || true
if [ $STATUS -ne 0 ]; then
FAIL_COUNT=$((FAIL_COUNT + 1))
echo "Run $i FAILED"
fi
done
echo "Total failures: $FAIL_COUNT / $REPEAT_COUNT"
if [ "$FAIL_COUNT" -ne 0 ]; then
exit 1
fi

- name: Publish Test Report
uses: mikepenz/action-junit-report@v5
if: success() || failure()
with:
report_paths: 'build/repeat-test-results/**/TEST-*.xml'
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.solidconnection.application.controller;

import com.example.solidconnection.application.dto.ApplicationPreviewResponse;
import com.example.solidconnection.application.dto.ApplicationSubmissionResponse;
import com.example.solidconnection.application.dto.ApplicationsResponse;
import com.example.solidconnection.application.dto.ApplyRequest;
Expand Down Expand Up @@ -37,6 +38,14 @@ public ResponseEntity<ApplicationSubmissionResponse> apply(
.body(applicationSubmissionResponse);
}

@GetMapping("/preview")
public ResponseEntity<ApplicationPreviewResponse> getApplicationPreview(
@AuthorizedUser long siteUserId
) {
ApplicationPreviewResponse result = applicationQueryService.getApplicantUniversityPreviews(siteUserId);
return ResponseEntity.ok(result);
}

// @RequireRoleAccess(roles = {Role.ADMIN}) // todo : 추후 어드민 페이지에서 권한 변경 기능 추가 필요
@GetMapping
public ResponseEntity<ApplicationsResponse> getApplicants(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.solidconnection.application.dto;

import java.util.List;

public record ApplicationPreviewResponse(
long totalUniversityCount,
List<ApplicationUniversityPreviewResponse> universities) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.solidconnection.application.dto;

import com.example.solidconnection.university.domain.UnivApplyInfo;

public record ApplicationUniversityPreviewResponse(
long id,
String koreanName,
Integer studentCapacity,
String region,
String country,
String logoImageUrl,
String backgroundImageUrl) {

public static ApplicationUniversityPreviewResponse from(UnivApplyInfo univApplyInfo) {
return new ApplicationUniversityPreviewResponse(
univApplyInfo.getId(),
univApplyInfo.getKoreanName(),
univApplyInfo.getStudentCapacity(),
univApplyInfo.getUniversity().getRegion().getKoreanName(),
univApplyInfo.getUniversity().getCountry().getKoreanName(),
univApplyInfo.getUniversity().getLogoImageUrl(),
univApplyInfo.getUniversity().getBackgroundImageUrl()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.example.solidconnection.common.exception.ErrorCode.APPLICATION_NOT_FOUND;

import com.example.solidconnection.application.domain.Application;
import com.example.solidconnection.application.dto.ApplicationUniversityPreviewResponse;
import com.example.solidconnection.common.VerifyStatus;
import com.example.solidconnection.common.exception.CustomException;
import java.util.List;
Expand All @@ -29,6 +30,38 @@ List<Application> findAllByUnivApplyInfoIds(
@Param("status") VerifyStatus status,
@Param("termId") long termId);

@Query("""
SELECT new com.example.solidconnection.application.dto.ApplicationUniversityPreviewResponse(
uai.id,
uai.koreanName,
uai.studentCapacity,
region.koreanName,
country.koreanName,
university.logoImageUrl,
university.backgroundImageUrl
)
FROM UnivApplyInfo uai
JOIN uai.university university
JOIN university.region region
JOIN university.country country
WHERE uai.termId = :termId
AND uai.homeUniversity.id = :homeUniversityId
AND EXISTS (
SELECT application.id
FROM Application application
JOIN application.choices choice
WHERE choice.univApplyInfoId = uai.id
AND application.verifyStatus = :status
AND application.termId = :termId
AND application.isDelete = false
)
ORDER BY uai.koreanName
""")
List<ApplicationUniversityPreviewResponse> findApplicantUniversityPreviews(
@Param("status") VerifyStatus status,
@Param("termId") long termId,
@Param("homeUniversityId") long homeUniversityId);

Optional<Application> findTopBySiteUserIdAndTermIdAndIsDeleteFalseOrderByIdDesc(long siteUserId, long termId);

default Application getApplicationBySiteUserIdAndTermId(long siteUserId, long termId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import static com.example.solidconnection.common.exception.ErrorCode.APPLICATION_NOT_APPROVED;
import static com.example.solidconnection.common.exception.ErrorCode.CURRENT_TERM_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_NOT_VERIFIED;
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;

import com.example.solidconnection.application.domain.Application;
import com.example.solidconnection.application.domain.ApplicationChoice;
import com.example.solidconnection.application.dto.ApplicantsResponse;
import com.example.solidconnection.application.dto.ApplicationPreviewResponse;
import com.example.solidconnection.application.dto.ApplicationsResponse;
import com.example.solidconnection.application.repository.ApplicationRepository;
import com.example.solidconnection.common.VerifyStatus;
Expand Down Expand Up @@ -41,6 +43,27 @@ public class ApplicationQueryService {
private final TermRepository termRepository;
private final HomeUniversityRepository homeUniversityRepository;

@Transactional(readOnly = true)
public ApplicationPreviewResponse getApplicantUniversityPreviews(long siteUserId) {
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));

if (siteUser.getHomeUniversityId() == null) {
throw new CustomException(SCHOOL_EMAIL_NOT_VERIFIED);
}

Term term = termRepository.findByIsCurrentTrue()
.orElseThrow(() -> new CustomException(CURRENT_TERM_NOT_FOUND));

return new ApplicationPreviewResponse(
univApplyInfoRepository.countByTermIdAndHomeUniversityId(term.getId(), siteUser.getHomeUniversityId()),
applicationRepository.findApplicantUniversityPreviews(
VerifyStatus.APPROVED,
term.getId(),
siteUser.getHomeUniversityId())
);
}

@Transactional(readOnly = true)
public ApplicationsResponse getApplicants(long siteUserId, String regionCode, String keyword) {
SiteUser siteUser = siteUserRepository.findById(siteUserId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ default UnivApplyInfo getUnivApplyInfoById(Long id) {

boolean existsByHomeUniversityId(Long homeUniversityId);

long countByTermIdAndHomeUniversityId(long termId, long homeUniversityId);

@Query("""
SELECT uai.id
FROM UnivApplyInfo uai
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.example.solidconnection.application.service;

import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_NOT_VERIFIED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.example.solidconnection.application.domain.Application;
import com.example.solidconnection.application.dto.ApplicantResponse;
import com.example.solidconnection.application.dto.ApplicantsResponse;
import com.example.solidconnection.application.dto.ApplicationPreviewResponse;
import com.example.solidconnection.application.dto.ApplicationUniversityPreviewResponse;
import com.example.solidconnection.application.dto.ApplicationsResponse;
import com.example.solidconnection.application.fixture.ApplicationFixture;
import com.example.solidconnection.application.repository.ApplicationRepository;
import com.example.solidconnection.common.VerifyStatus;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.location.region.fixture.RegionFixture;
import com.example.solidconnection.score.domain.GpaScore;
import com.example.solidconnection.score.domain.LanguageTestScore;
Expand All @@ -20,7 +25,9 @@
import com.example.solidconnection.term.domain.Term;
import com.example.solidconnection.term.fixture.TermFixture;
import com.example.solidconnection.university.domain.UnivApplyInfo;
import com.example.solidconnection.university.fixture.HomeUniversityFixture;
import com.example.solidconnection.university.fixture.UnivApplyInfoFixture;
import com.example.solidconnection.university.fixture.UnivApplyInfoFixtureBuilder;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -47,6 +54,12 @@ class ApplicationQueryServiceTest {
@Autowired
private UnivApplyInfoFixture univApplyInfoFixture;

@Autowired
private UnivApplyInfoFixtureBuilder univApplyInfoFixtureBuilder;

@Autowired
private HomeUniversityFixture homeUniversityFixture;

@Autowired
private GpaScoreFixture gpaScoreFixture;

Expand Down Expand Up @@ -98,6 +111,114 @@ void setUp() {
서던덴마크대학교_지원_정보 = univApplyInfoFixture.서던덴마크대학교_지원_정보(term.getId());
}

@Nested
@DisplayName("지원 현황 미리보기 조회")
class 지원_현황_미리보기_조회_테스트 {

@Test
void 자신의_모교에_속한_지원_대학만_중복_없이_조회한다() {
// given
SiteUser inhaUniversityUser = siteUserFixture.국내_대학_정보_소지_사용자(
괌대학_A_지원_정보.getHomeUniversity().getId());
applicationFixture.지원서(
user1, "nickname1", term.getId(),
gpaScore1.getGpa(), languageTestScore1.getLanguageTest(),
List.of(괌대학_A_지원_정보.getId(), 버지니아공과대학_지원_정보.getId())
);
applicationFixture.지원서(
user2, "nickname2", term.getId(),
gpaScore2.getGpa(), languageTestScore2.getLanguageTest(),
List.of(괌대학_A_지원_정보.getId())
);
UnivApplyInfo 인천대학교_전용_지원_정보 = univApplyInfoFixtureBuilder.univApplyInfo()
.termId(term.getId())
.koreanName("인천대학교 전용 교환 대학")
.university(서던덴마크대학교_지원_정보.getUniversity())
.homeUniversity(homeUniversityFixture.인천대학교())
.create();
applicationFixture.지원서(
user3, "nickname3", term.getId(),
gpaScore3.getGpa(), languageTestScore3.getLanguageTest(),
List.of(인천대학교_전용_지원_정보.getId())
);
Application deletedApplication = applicationFixture.지원서(
user3, "nickname3", term.getId(),
gpaScore3.getGpa(), languageTestScore3.getLanguageTest(),
List.of(서던덴마크대학교_지원_정보.getId())
);
deletedApplication.setIsDeleteTrue();
applicationRepository.save(deletedApplication);
UnivApplyInfo 승인_대기_지원_정보 = univApplyInfoFixtureBuilder.univApplyInfo()
.termId(term.getId())
.koreanName("승인 대기 교환 대학")
.university(서던덴마크대학교_지원_정보.getUniversity())
.homeUniversity(괌대학_A_지원_정보.getHomeUniversity())
.create();
Application pendingApplication = applicationFixture.지원서(
user1, "pending-nickname", term.getId(),
gpaScore1.getGpa(), languageTestScore1.getLanguageTest(),
List.of(승인_대기_지원_정보.getId())
);
pendingApplication.setVerifyStatus(VerifyStatus.PENDING);
applicationRepository.save(pendingApplication);
UnivApplyInfo 승인_거절_지원_정보 = univApplyInfoFixtureBuilder.univApplyInfo()
.termId(term.getId())
.koreanName("승인 거절 교환 대학")
.university(서던덴마크대학교_지원_정보.getUniversity())
.homeUniversity(괌대학_A_지원_정보.getHomeUniversity())
.create();
Application rejectedApplication = applicationFixture.지원서(
user2, "rejected-nickname", term.getId(),
gpaScore2.getGpa(), languageTestScore2.getLanguageTest(),
List.of(승인_거절_지원_정보.getId())
);
rejectedApplication.setVerifyStatus(VerifyStatus.REJECTED);
applicationRepository.save(rejectedApplication);

// when
ApplicationPreviewResponse response = applicationQueryService.getApplicantUniversityPreviews(
inhaUniversityUser.getId());

// then
assertThat(response.totalUniversityCount()).isEqualTo(5);
assertThat(response.universities()).containsExactly(
ApplicationUniversityPreviewResponse.from(괌대학_A_지원_정보),
ApplicationUniversityPreviewResponse.from(버지니아공과대학_지원_정보)
);
}

@Test
void 성적과_지원서를_제출하지_않은_로그인_사용자도_미리보기를_조회할_수_있다() {
// given
SiteUser signedInUserWithoutScores = siteUserFixture.국내_대학_정보_소지_사용자(
괌대학_A_지원_정보.getHomeUniversity().getId());
applicationFixture.지원서(
user2, "nickname2", term.getId(),
gpaScore2.getGpa(), languageTestScore2.getLanguageTest(),
List.of(괌대학_A_지원_정보.getId())
);

// when
ApplicationPreviewResponse response = applicationQueryService.getApplicantUniversityPreviews(
signedInUserWithoutScores.getId());

// then
assertThat(response.totalUniversityCount()).isEqualTo(3);
assertThat(response.universities()).containsExactly(
ApplicationUniversityPreviewResponse.from(괌대학_A_지원_정보)
);
}

@Test
void 모교가_등록되지_않은_사용자는_미리보기를_조회할_수_없다() {
// when
// then
assertThatThrownBy(() -> applicationQueryService.getApplicantUniversityPreviews(user1.getId()))
.isInstanceOf(CustomException.class)
.hasMessage(SCHOOL_EMAIL_NOT_VERIFIED.getMessage());
}
}

@Nested
class 지원자_목록_조회_테스트 {

Expand Down
Loading