Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.example.solidconnection.admin.university.controller;

import com.example.solidconnection.admin.university.dto.AdminUnivApplyInfoCreateRequest;
import com.example.solidconnection.admin.university.dto.AdminUnivApplyInfoResponse;
import com.example.solidconnection.admin.university.dto.AdminUnivApplyInfoUpdateRequest;
import com.example.solidconnection.admin.university.dto.UnivApplyInfoFieldResponse;
import com.example.solidconnection.admin.university.dto.UnivApplyInfoImportRequest;
import com.example.solidconnection.admin.university.dto.UnivApplyInfoImportResponse;
import com.example.solidconnection.admin.university.service.AdminUnivApplyInfoService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -25,10 +31,33 @@ public ResponseEntity<UnivApplyInfoFieldResponse> getFields() {
return ResponseEntity.ok(adminUnivApplyInfoService.getFields());
}

@PostMapping
@PostMapping("/import")
public ResponseEntity<UnivApplyInfoImportResponse> importUnivApplyInfos(
@Valid @RequestBody UnivApplyInfoImportRequest request
) {
return ResponseEntity.ok(adminUnivApplyInfoService.importUnivApplyInfos(request));
}

@PostMapping
public ResponseEntity<AdminUnivApplyInfoResponse> createUnivApplyInfo(
@Valid @RequestBody AdminUnivApplyInfoCreateRequest request
) {
return ResponseEntity.ok(adminUnivApplyInfoService.createUnivApplyInfo(request));
}

@PatchMapping("/{id}")
public ResponseEntity<AdminUnivApplyInfoResponse> updateUnivApplyInfo(
@PathVariable long id,
@Valid @RequestBody AdminUnivApplyInfoUpdateRequest request
) {
return ResponseEntity.ok(adminUnivApplyInfoService.updateUnivApplyInfo(id, request));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUnivApplyInfo(
@PathVariable long id
) {
adminUnivApplyInfoService.deleteUnivApplyInfo(id);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.solidconnection.admin.university.dto;

import com.example.solidconnection.university.domain.SemesterAvailableForDispatch;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;

public record AdminUnivApplyInfoCreateRequest(
@NotNull Long termId,
@NotNull Long homeUniversityId,
@NotNull Long hostUniversityId,
Integer studentCapacity,
SemesterAvailableForDispatch semesterAvailableForDispatch,
Comment on lines +13 to +14

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 Require fields used by public responses

When an admin omits studentCapacity or semesterAvailableForDispatch (the new request accepts both as null, and the added tests exercise that path), the service stores nulls that public DTOs cannot handle: UnivApplyInfoPreviewResponse unboxes getStudentCapacity() to int, and UnivApplyInfoDetailResponse dereferences getSemesterAvailableForDispatch().getKoreanName(). A newly created current-term record with either field missing can therefore make search/detail endpoints return 500s until the data is repaired, so these fields should be validated as required or the public DTOs made null-safe.

Useful? React with 👍 / 👎.

String semesterRequirement,
String detailsForLanguage,
String gpaRequirement,
String gpaRequirementCriteria,
String detailsForAccommodation,
Map<String, String> extraInfo,
@Valid List<@NotNull AdminUnivApplyInfoLanguageRequirementRequest> languageRequirements
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.admin.university.dto;

import com.example.solidconnection.university.domain.LanguageTestType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record AdminUnivApplyInfoLanguageRequirementRequest(
@NotNull LanguageTestType languageTestType,
@NotBlank String minScore
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.solidconnection.admin.university.dto;

import com.example.solidconnection.university.domain.SemesterAvailableForDispatch;
import com.example.solidconnection.university.domain.UnivApplyInfo;
import com.example.solidconnection.university.dto.LanguageRequirementResponse;
import java.util.List;
import java.util.Map;

public record AdminUnivApplyInfoResponse(
long id,
long termId,
Long homeUniversityId,
long hostUniversityId,
String koreanName,
Integer studentCapacity,
SemesterAvailableForDispatch semesterAvailableForDispatch,
String semesterRequirement,
String detailsForLanguage,
String gpaRequirement,
String gpaRequirementCriteria,
String detailsForAccommodation,
Map<String, String> extraInfo,
List<LanguageRequirementResponse> languageRequirements
) {

public static AdminUnivApplyInfoResponse from(UnivApplyInfo univApplyInfo) {
return new AdminUnivApplyInfoResponse(
univApplyInfo.getId(),
univApplyInfo.getTermId(),
univApplyInfo.getHomeUniversity() != null ? univApplyInfo.getHomeUniversity().getId() : null,
univApplyInfo.getUniversity().getId(),
univApplyInfo.getKoreanName(),
univApplyInfo.getStudentCapacity(),
univApplyInfo.getSemesterAvailableForDispatch(),
univApplyInfo.getSemesterRequirement(),
univApplyInfo.getDetailsForLanguage(),
univApplyInfo.getGpaRequirement(),
univApplyInfo.getGpaRequirementCriteria(),
univApplyInfo.getDetailsForAccommodation(),
univApplyInfo.getExtraInfo(),
univApplyInfo.getLanguageRequirements().stream()
.map(LanguageRequirementResponse::from)
.sorted()
.toList()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.solidconnection.admin.university.dto;

import com.example.solidconnection.university.domain.SemesterAvailableForDispatch;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;

public record AdminUnivApplyInfoUpdateRequest(
Integer studentCapacity,
SemesterAvailableForDispatch semesterAvailableForDispatch,
String semesterRequirement,
String detailsForLanguage,
String gpaRequirement,
String gpaRequirementCriteria,
String detailsForAccommodation,
Map<String, String> extraInfo,
@Valid List<@NotNull AdminUnivApplyInfoLanguageRequirementRequest> languageRequirements
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@
import static com.example.solidconnection.common.exception.ErrorCode.HOME_UNIVERSITY_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_INPUT;
import static com.example.solidconnection.common.exception.ErrorCode.TERM_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.UNIV_APPLY_INFO_HAS_REFERENCES;
import static com.example.solidconnection.common.exception.ErrorCode.UNIV_APPLY_INFO_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND;

import com.example.solidconnection.admin.university.dto.AdminUnivApplyInfoCreateRequest;
import com.example.solidconnection.admin.university.dto.AdminUnivApplyInfoResponse;
import com.example.solidconnection.admin.university.dto.AdminUnivApplyInfoUpdateRequest;
import com.example.solidconnection.admin.university.dto.UnivApplyInfoFieldResponse;
import com.example.solidconnection.admin.university.dto.UnivApplyInfoImportRequest;
import com.example.solidconnection.admin.university.dto.UnivApplyInfoImportResponse;
import com.example.solidconnection.application.repository.ApplicationRepository;
import com.example.solidconnection.cache.annotation.DefaultCacheOut;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.common.util.MarkdownTableParser;
import com.example.solidconnection.term.repository.TermRepository;
import com.example.solidconnection.university.domain.HomeUniversity;
import com.example.solidconnection.university.domain.HostUniversity;
import com.example.solidconnection.university.domain.LanguageRequirement;
import com.example.solidconnection.university.domain.UnivApplyInfo;
import com.example.solidconnection.university.repository.HomeUniversityRepository;
import com.example.solidconnection.university.repository.HostUniversityRepository;
import com.example.solidconnection.university.repository.LikedUnivApplyInfoRepository;
import com.example.solidconnection.university.repository.UnivApplyInfoRepository;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
Expand All @@ -28,6 +42,10 @@ public class AdminUnivApplyInfoService {
private final HomeUniversityRepository homeUniversityRepository;
private final MarkdownTableParser markdownTableParser;
private final AdminUnivApplyInfoRowSaver rowSaver;
private final UnivApplyInfoRepository univApplyInfoRepository;
private final HostUniversityRepository hostUniversityRepository;
private final LikedUnivApplyInfoRepository likedUnivApplyInfoRepository;
private final ApplicationRepository applicationRepository;

public UnivApplyInfoFieldResponse getFields() {
return UnivApplyInfoFieldResponse.of();
Expand Down Expand Up @@ -75,4 +93,106 @@ private HomeUniversity findHomeUniversity(Long homeUniversityId) {
return homeUniversityRepository.findById(homeUniversityId)
.orElseThrow(() -> new CustomException(HOME_UNIVERSITY_NOT_FOUND));
}

@Transactional
@DefaultCacheOut(
key = {"univApplyInfoTextSearch", "university:recommend:general"},
cacheManager = "customCacheManager",
prefix = true
)
public AdminUnivApplyInfoResponse createUnivApplyInfo(AdminUnivApplyInfoCreateRequest request) {
validateTermExists(request.termId());
HomeUniversity homeUniversity = findHomeUniversity(request.homeUniversityId());
HostUniversity hostUniversity = findHostUniversity(request.hostUniversityId());

UnivApplyInfo univApplyInfo = new UnivApplyInfo(
null,
request.termId(),
homeUniversity,
hostUniversity.getKoreanName(),
request.studentCapacity(),
request.semesterAvailableForDispatch(),
request.semesterRequirement(),
request.detailsForLanguage(),
request.gpaRequirement(),
request.gpaRequirementCriteria(),
request.detailsForAccommodation(),
request.extraInfo(),
new HashSet<>(),
hostUniversity
);

UnivApplyInfo saved = univApplyInfoRepository.save(univApplyInfo);

if (request.languageRequirements() != null) {
request.languageRequirements().forEach(lr -> {
LanguageRequirement languageRequirement = new LanguageRequirement(
null, lr.languageTestType(), lr.minScore(), saved
);
saved.addLanguageRequirements(languageRequirement);
});
}

return AdminUnivApplyInfoResponse.from(saved);
}

private HostUniversity findHostUniversity(Long hostUniversityId) {
return hostUniversityRepository.findById(hostUniversityId)
.orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND));
}

@Transactional
@DefaultCacheOut(
key = {"univApplyInfoTextSearch", "university:recommend:general"},
cacheManager = "customCacheManager",
Comment on lines +145 to +147

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 Evict the detail cache on updates

Updating an existing apply info only clears search/recommendation prefixes, but getUnivApplyInfoDetail is cached separately under univApplyInfo:{id} for 24 hours. If a user has opened the detail page before an admin changes capacity, requirements, or language scores, subsequent detail requests keep serving the stale cached response even though the update succeeded; evict the affected univApplyInfo:<id> key as the host-university update path already does for affected apply infos.

Useful? React with 👍 / 👎.

prefix = true
)
public AdminUnivApplyInfoResponse updateUnivApplyInfo(long id, AdminUnivApplyInfoUpdateRequest request) {
UnivApplyInfo univApplyInfo = univApplyInfoRepository.findById(id)
.orElseThrow(() -> new CustomException(UNIV_APPLY_INFO_NOT_FOUND));

univApplyInfo.update(
request.studentCapacity(),
request.semesterAvailableForDispatch(),
request.semesterRequirement(),
request.detailsForLanguage(),
request.gpaRequirement(),
request.gpaRequirementCriteria(),
request.detailsForAccommodation(),
request.extraInfo()
);

if (request.languageRequirements() != null) {
univApplyInfo.clearLanguageRequirements();
request.languageRequirements().forEach(lr -> {
LanguageRequirement languageRequirement = new LanguageRequirement(
null, lr.languageTestType(), lr.minScore(), univApplyInfo
);
univApplyInfo.addLanguageRequirements(languageRequirement);
});
}

return AdminUnivApplyInfoResponse.from(univApplyInfo);
}

@Transactional
@DefaultCacheOut(
key = {"univApplyInfoTextSearch", "university:recommend:general"},
cacheManager = "customCacheManager",
prefix = true
)
public void deleteUnivApplyInfo(long id) {
UnivApplyInfo univApplyInfo = univApplyInfoRepository.findById(id)
.orElseThrow(() -> new CustomException(UNIV_APPLY_INFO_NOT_FOUND));
validateNoReferences(id);
univApplyInfoRepository.delete(univApplyInfo);
}

private void validateNoReferences(long id) {
if (likedUnivApplyInfoRepository.existsByUnivApplyInfoId(id)
|| applicationRepository.existsByChoicesUnivApplyInfoId(id)) {
throw new CustomException(UNIV_APPLY_INFO_HAS_REFERENCES);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,13 @@ default Application getApplicationBySiteUserIdAndTermId(long siteUserId, long te
.orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND));
}

@Query("""
SELECT CASE WHEN COUNT(a) > 0 THEN true ELSE false END
FROM Application a
JOIN a.choices c
WHERE c.univApplyInfoId = :univApplyInfoId
""")
boolean existsByChoicesUnivApplyInfoId(@Param("univApplyInfoId") long univApplyInfoId);

void deleteAllBySiteUserId(long siteUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public enum ErrorCode {
// data not found
UNIV_APPLY_INFO_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 대학교 지원 정보입니다."),
UNIV_APPLY_INFO_NOT_FOUND_FOR_TERM(HttpStatus.NOT_FOUND.value(), "해당하는 대학교가 이번 모집 기간에 열리지 않았습니다."),
UNIV_APPLY_INFO_HAS_REFERENCES(HttpStatus.CONFLICT.value(), "해당 대학 지원 정보를 참조하는 데이터가 존재합니다."),
APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "사용자의 대학 지원 정보를 찾을 수 없습니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원을 찾을 수 없습니다."),
UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "대학교를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ public ResponseEntity<UnivApplyInfoDetailResponse> getUnivApplyInfoDetails(

@GetMapping("/search/text")
public ResponseEntity<UnivApplyInfoPreviewResponses> searchUnivApplyInfoByText(
@RequestParam(required = false) String value
@RequestParam(required = false) String value,
@RequestParam(required = false) Long homeUniversityId,
@RequestParam(required = false) Long termId
) {
UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(value);
UnivApplyInfoPreviewResponses response =
univApplyInfoQueryService.searchUnivApplyInfoByText(value, homeUniversityId, termId);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ public void addLanguageRequirements(LanguageRequirement languageRequirements) {
this.languageRequirements.add(languageRequirements);
}

public void update(
Integer studentCapacity,
SemesterAvailableForDispatch semesterAvailableForDispatch,
String semesterRequirement,
String detailsForLanguage,
String gpaRequirement,
String gpaRequirementCriteria,
String detailsForAccommodation,
Map<String, String> extraInfo
) {
if (studentCapacity != null) this.studentCapacity = studentCapacity;
if (semesterAvailableForDispatch != null) this.semesterAvailableForDispatch = semesterAvailableForDispatch;
if (semesterRequirement != null) this.semesterRequirement = semesterRequirement;
if (detailsForLanguage != null) this.detailsForLanguage = detailsForLanguage;
if (gpaRequirement != null) this.gpaRequirement = gpaRequirement;
if (gpaRequirementCriteria != null) this.gpaRequirementCriteria = gpaRequirementCriteria;
if (detailsForAccommodation != null) this.detailsForAccommodation = detailsForAccommodation;
if (extraInfo != null) this.extraInfo = extraInfo;
}

public void clearLanguageRequirements() {
this.languageRequirements.clear();
}

public void updateExtraInfo(Map<String, String> extraInfo) {
this.extraInfo = extraInfo;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ public interface LikedUnivApplyInfoRepository extends JpaRepository<LikedUnivApp

boolean existsBySiteUserIdAndUnivApplyInfoId(long siteUserId, long univApplyInfoId);

boolean existsByUnivApplyInfoId(long univApplyInfoId);

void deleteAllBySiteUserId(long siteUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ public interface UnivApplyInfoFilterRepository {

List<UnivApplyInfo> findAllByRegionCodeAndKeywordsAndTermId(String regionCode, List<String> keywords, Long term);

List<UnivApplyInfo> findAllByText(String text, Long termId);
List<UnivApplyInfo> findAllByText(String text, Long termId, Long homeUniversityId);
}
Loading
Loading