Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
11 commits
Select commit Hold shift + click to select a range
f968e47
fix: κ΄€λ¦¬μžμš© μ§€μ›μ„œ λͺ©λ‘ μ΅œμ‹ μˆœ μ •λ ¬ μΆ”κ°€ #159
EunjinWoo Dec 11, 2025
b308ae4
fix: κ΄€λ¦¬μžμš© μ§€μ›μ„œ λͺ©λ‘ 쑰직 파트, μ§€μ›μ„œ μƒνƒœ ν•„ν„° μΆ”κ°€ #159
EunjinWoo Dec 11, 2025
744a7e8
fix: κ΄€λ¦¬μžμš© μ§€μ›μ„œ λͺ©λ‘ 이름 검색 μΆ”κ°€ #159
EunjinWoo Dec 11, 2025
dc8bdfd
feat: 메일/문자 ν…œν”Œλ¦Ώ μˆ˜μ • κΈ°λŠ₯ κ΅¬ν˜„ #159
EunjinWoo Dec 11, 2025
3157af1
feat: μ§€μ›μ„œ λͺ©λ‘ μ—‘μ…€ μΆ”μΆœ κΈ°λŠ₯ κ΅¬ν˜„ #159
EunjinWoo Dec 11, 2025
39ebf1d
fix: .toList()둜 λΆˆλ³€ 리슀트 생성 ν›„ μ •λ ¬ μ‹œλ„ν•˜λ˜ 것 μˆ˜μ • #159
EunjinWoo Dec 13, 2025
3f7dda6
fix: ν…œν”Œλ¦Ώ μˆ˜μ • μ‹œ medium 검증 μƒμ„±μžμ™€ λ™μΌν•˜κ²Œ μˆ˜μ • #159
EunjinWoo Dec 13, 2025
8ac22dd
fix: 문자인 경우 subject 빈 κ°’μœΌλ‘œ 보낼 수 있게 μˆ˜μ • #159
EunjinWoo Dec 13, 2025
dfe294c
fix: Workbook λ¦¬μ†ŒμŠ€ λˆ„μˆ˜ κ°€λŠ₯μ„± 제거 #159
EunjinWoo Dec 13, 2025
cfaf223
chore: λˆ„λ½λœ μœ νš¨μ„± 검사 TODO μΆ”κ°€ #159
EunjinWoo Dec 13, 2025
eb009a7
fix: ν‚€μ›Œλ“œ 검색 trim μΆ”κ°€ 및 빈 IN-query 문제 μˆ˜μ • #159
EunjinWoo Dec 13, 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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ dependencies {

// H2 (ν…ŒμŠ€νŠΈ ν™˜κ²½)
testImplementation 'com.h2database:h2'

// Excel
implementation 'org.apache.poi:poi-ooxml:5.2.3'
}

test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
public interface ApplicantAvailabilityRepository extends JpaRepository<ApplicantAvailability, Long> {
List<ApplicantAvailability> findByApplicationIn(List<Application> applications);
List<ApplicantAvailability> findByApplicationId(Long id);
List<ApplicantAvailability> findAllByApplicationIdIn(List<Long> appIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@
import KUSITMS.WITHUS.domain.application.application.service.ApplicationSmsService;
import KUSITMS.WITHUS.domain.application.applicationEvaluator.dto.ApplicationEvaluatorRequestDTO;
import KUSITMS.WITHUS.domain.application.distributionRequest.dto.DistributionRequestResponseDTO;
import KUSITMS.WITHUS.domain.application.distributionRequest.entity.DistributionRequest;
import KUSITMS.WITHUS.domain.application.enumerate.ApplicationStatus;
import KUSITMS.WITHUS.domain.user.user.entity.User;
import KUSITMS.WITHUS.global.common.annotation.CurrentUser;
import KUSITMS.WITHUS.global.response.PagedResponse;
import KUSITMS.WITHUS.global.response.SuccessResponse;
import KUSITMS.WITHUS.global.util.excel.ExcelExporter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.mail.MessagingException;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.List;

@RestController
Expand All @@ -41,15 +47,53 @@ public class AdminApplicationController {
public SuccessResponse<PagedResponse<ApplicationResponseDTO.SummaryForAdmin>> getList(
@PathVariable Long recruitmentId,
@RequestParam(defaultValue = "DOCUMENT") AdminStageFilter stage,
@RequestParam(defaultValue = "NAME") AdminApplicationSortField sortBy,
@RequestParam(defaultValue = "ASC") Sort.Direction direction,
@RequestParam(defaultValue = "LATEST") AdminApplicationSortField sortBy,
@RequestParam(defaultValue = "DESC") Sort.Direction direction,
@RequestParam(required = false) List<Long> organizationRoleIds,
@RequestParam(required = false) List<ApplicationStatus> statuses,
@RequestParam(required = false) String keyword,
@PageableDefault(size = 7) Pageable pageable
) {
ApplicationResponseDTO.AdminPageWithStageCounts result = applicationService.getByRecruitmentIdForAdmin(recruitmentId, stage, pageable, sortBy, direction);
ApplicationResponseDTO.AdminPageWithStageCounts result = applicationService.getByRecruitmentIdForAdmin(recruitmentId, stage, pageable, sortBy, direction, organizationRoleIds, statuses, keyword);
PagedResponse<ApplicationResponseDTO.SummaryForAdmin> paged = PagedResponse.from(result.page(), result.counts());
return SuccessResponse.ok(paged);
}

@GetMapping("/recruitment/{recruitmentId}/excel")
public void downloadExcel(
@PathVariable Long recruitmentId,
@RequestParam(defaultValue = "DOCUMENT") AdminStageFilter stage,
@RequestParam(defaultValue = "LATEST") AdminApplicationSortField sortBy,
@RequestParam(defaultValue = "DESC") Sort.Direction direction,
@RequestParam(required = false) List<Long> organizationRoleIds,
@RequestParam(required = false) List<ApplicationStatus> statuses,
@RequestParam(required = false) String keyword,
@CurrentUser User user,
HttpServletResponse response
) throws IOException {

List<ApplicationResponseDTO.Detail> list =
applicationService.getAllDetailForExcel(
recruitmentId,
stage,
sortBy,
direction,
organizationRoleIds,
statuses,
keyword,
user.getId()
);

String fileName = "applications-detail.xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);

try (Workbook workbook = ExcelExporter.createExcel(list)) {
workbook.write(response.getOutputStream());
}
}
Comment thread
EunjinWoo marked this conversation as resolved.


@GetMapping("/recruitments/{recruitmentId}/timeslots/{timeslotId}/candidates")
@Operation(summary = "νƒ€μž„ν…Œμ΄λΈ” ν›„λ³΄μž 쑰회")
public SuccessResponse<List<ApplicationResponseDTO.CandidateDTO>> getTimetableCandidates(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package KUSITMS.WITHUS.domain.application.application.enumerate;

public enum AdminApplicationSortField {
LATEST,
NAME,
POSITION_NAME,
DOCUMENT_EVALUATION_STATUS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import KUSITMS.WITHUS.domain.application.application.enumerate.EvaluationStatus;
import KUSITMS.WITHUS.domain.application.applicationEvaluator.dto.ApplicationEvaluatorRequestDTO;
import KUSITMS.WITHUS.domain.application.distributionRequest.dto.DistributionRequestResponseDTO;
import KUSITMS.WITHUS.domain.application.enumerate.ApplicationStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
Expand All @@ -19,11 +20,12 @@ public interface ApplicationService {
void delete(Long id);
ApplicationResponseDTO.Detail getById(Long id, Long currentUserId);
Page<ApplicationResponseDTO.SummaryForUser> getByRecruitmentId(Long recruitmentId, Long currentUserId, EvaluationStatus evaluationStatus, String keyword, Pageable pageable);
ApplicationResponseDTO.AdminPageWithStageCounts getByRecruitmentIdForAdmin(Long recruitmentId, AdminStageFilter stage, Pageable pageable, AdminApplicationSortField sortBy, Sort.Direction direction);
ApplicationResponseDTO.AdminPageWithStageCounts getByRecruitmentIdForAdmin(Long recruitmentId, AdminStageFilter stage, Pageable pageable, AdminApplicationSortField sortBy, Sort.Direction direction, List<Long> organizationRoleIds, List<ApplicationStatus> statuses, String keyword);
List<ApplicationResponseDTO.Summary> updateStatus(ApplicationRequestDTO.UpdateStatus request);
void distributeEvaluators(ApplicationEvaluatorRequestDTO.Distribute request);
DistributionRequestResponseDTO.Detail distributeEvaluatorsLatestRequest(Long recruitmentId);
void updateEvaluators(ApplicationEvaluatorRequestDTO.Update request);
boolean toggleAcquaintance(Long applicationId, Long userId);
List<ApplicationResponseDTO.CandidateDTO> findTimeslotCandidates(Long recruitmentId, Long timeslotId, String query, boolean excludeCurrent);
List<ApplicationResponseDTO.Detail> getAllDetailForExcel(Long recruitmentId, AdminStageFilter stage, AdminApplicationSortField sortBy, Sort.Direction direction, List<Long> organizationRoleIds, List<ApplicationStatus> statuses, String keyword, Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import KUSITMS.WITHUS.global.infra.upload.dto.FileResponseDTO;
import KUSITMS.WITHUS.global.infra.upload.service.FileUploadService;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

org.jetbrains.annotations.NotNull λ„μž…μ€ ν”„λ‘œμ νŠΈ ν‘œμ€€κ³Ό μΌμΉ˜ν•˜λŠ”μ§€ 확인 ν•„μš”
μ„œλ²„ μ½”λ“œ μ „λ°˜μ—μ„œ @NotNull을 μ–΄λ–€ μ–΄λ…Έν…Œμ΄μ…˜(예: jakarta.validation.constraints.NotNull, org.springframework.lang.NonNull)둜 ν†΅μΌν•˜λŠ”μ§€μ— 따라 일관성이 깨질 수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ€– Prompt for AI Agents
In
src/main/java/KUSITMS/WITHUS/domain/application/application/service/ApplicationServiceImpl.java
around line 49, the file imports org.jetbrains.annotations.NotNull which may not
match the project's standard annotation; replace this import with the project's
agreed-upon nullability/validation annotation (e.g.,
jakarta.validation.constraints.NotNull or org.springframework.lang.NonNull) or
remove the import if unused, and update any @NotNull usages in this class to the
chosen annotation to keep consistency across the codebase.

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -254,11 +255,75 @@ public ApplicationResponseDTO.AdminPageWithStageCounts getByRecruitmentIdForAdmi
AdminStageFilter stage,
Pageable pageable,
AdminApplicationSortField sortBy,
Sort.Direction direction
Sort.Direction direction,
List<Long> organizationRoleIds,
List<ApplicationStatus> statuses,
String keyword
) {
List<Application> allApps = applicationRepository
.findByRecruitmentIdAndStatusIn(recruitmentId, stage.toStatusList());

// POSITION_NAME ν•„ν„°
if (organizationRoleIds != null && !organizationRoleIds.isEmpty()) {
allApps = allApps.stream()
.filter(app ->
app.getOrganizationRole() != null &&
organizationRoleIds.contains(app.getOrganizationRole().getId())
)
.collect(Collectors.toList());
}

// STATUS ν•„ν„°
if (statuses != null && !statuses.isEmpty()) {
allApps = allApps.stream()
.filter(app -> statuses.contains(app.getStatus()))
.collect(Collectors.toList());
}

// NAME KEYWORD ν•„ν„°
if (keyword != null && !keyword.trim().isEmpty()) {
String lower = keyword.trim().toLowerCase(Locale.ROOT);
allApps = allApps.stream()
.filter(app ->
app.getName() != null &&
app.getName().toLowerCase().contains(lower)
)
.collect(Collectors.toList());
}

List<Application> sortedApps = getSortedApps(sortBy, direction, allApps);

List<ApplicationResponseDTO.SummaryForAdmin> allDtos = IntStream.range(0, sortedApps.size())
.mapToObj(i -> ApplicationResponseDTO.SummaryForAdmin.from(sortedApps.get(i), i + 1L))
.toList();

int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), allDtos.size());
List<ApplicationResponseDTO.SummaryForAdmin> content = start > end
? List.of()
: allDtos.subList(start, end);

Page<ApplicationResponseDTO.SummaryForAdmin> page = new PageImpl<>(content, pageable, allDtos.size());

long documentCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.DOCUMENT.toStatusList());
long interviewCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.INTERVIEW.toStatusList());
long finalPassCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.FINAL_PASS.toStatusList());
long failCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.FAIL.toStatusList());

ApplicationResponseDTO.StageCount counts = ApplicationResponseDTO.StageCount.from(
documentCnt, interviewCnt, finalPassCnt, failCnt
);

return ApplicationResponseDTO.AdminPageWithStageCounts.from(page, counts);
}

@NotNull
private static List<Application> getSortedApps(AdminApplicationSortField sortBy, Sort.Direction direction, List<Application> allApps) {

allApps.sort((a, b) -> {
var sa = ApplicationResponseDTO.SummaryForAdmin.from(a, 0L);
var sb = ApplicationResponseDTO.SummaryForAdmin.from(b, 0L);
Expand Down Expand Up @@ -300,41 +365,17 @@ public ApplicationResponseDTO.AdminPageWithStageCounts getByRecruitmentIdForAdmi
boolean smsB = Boolean.TRUE.equals(sb.isSmsSent());
cmp = Boolean.compare(smsA, smsB);
break;
case LATEST:
cmp = a.getCreatedAt().compareTo(b.getCreatedAt());
break;
case NAME:
default:
cmp = sa.name().compareToIgnoreCase(sb.name());
break;
}
return direction.isDescending() ? -cmp : cmp;
});


List<ApplicationResponseDTO.SummaryForAdmin> allDtos = IntStream.range(0, allApps.size())
.mapToObj(i -> ApplicationResponseDTO.SummaryForAdmin.from(allApps.get(i), i + 1L))
.toList();

int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), allDtos.size());
List<ApplicationResponseDTO.SummaryForAdmin> content = start > end
? List.of()
: allDtos.subList(start, end);

Page<ApplicationResponseDTO.SummaryForAdmin> page = new PageImpl<>(content, pageable, allDtos.size());

long documentCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.DOCUMENT.toStatusList());
long interviewCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.INTERVIEW.toStatusList());
long finalPassCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.FINAL_PASS.toStatusList());
long failCnt = applicationRepository.countByRecruitmentIdAndStatusIn(
recruitmentId, AdminStageFilter.FAIL.toStatusList());

ApplicationResponseDTO.StageCount counts = ApplicationResponseDTO.StageCount.from(
documentCnt, interviewCnt, finalPassCnt, failCnt
);

return ApplicationResponseDTO.AdminPageWithStageCounts.from(page, counts);
return allApps;
}


Expand Down Expand Up @@ -425,6 +466,99 @@ public List<ApplicationResponseDTO.CandidateDTO> findTimeslotCandidates(
return applicationRepository.findEligibleCandidates(recruitmentId, timeslotId, q, excludeCurrent);
}

@Override
public List<ApplicationResponseDTO.Detail> getAllDetailForExcel(
Long recruitmentId,
AdminStageFilter stage,
AdminApplicationSortField sortBy,
Sort.Direction direction,
List<Long> organizationRoleIds,
List<ApplicationStatus> statuses,
String keyword,
Long currentUserId
) {

List<Application> apps = applicationRepository.findByRecruitmentIdAndStatusIn(
recruitmentId, stage.toStatusList()
);

// POSITION(OrganizationRole) ν•„ν„°
if (organizationRoleIds != null && !organizationRoleIds.isEmpty()) {
apps = apps.stream()
.filter(a -> a.getOrganizationRole() != null &&
organizationRoleIds.contains(a.getOrganizationRole().getId()))
.collect(Collectors.toList());
}

// STATUS ν•„ν„°
if (statuses != null && !statuses.isEmpty()) {
apps = apps.stream()
.filter(a -> statuses.contains(a.getStatus()))
.collect(Collectors.toList());
}

// KEYWORD (name 검색)
if (keyword != null && !keyword.isBlank()) {
String kw = keyword.trim().toLowerCase(Locale.ROOT);
apps = apps.stream()
.filter(a -> a.getName() != null && a.getName().toLowerCase().contains(kw))
.collect(Collectors.toList());
}

// SORT
apps = getSortedApps(sortBy, direction, apps);
Comment thread
EunjinWoo marked this conversation as resolved.

if (apps.isEmpty()) {
return List.of();
}

// μ„±λŠ₯ μ΅œμ ν™” - bulk 쑰회
List<Long> appIds = apps.stream()
.map(Application::getId)
.toList();

// λ©΄μ ‘ κ°€λŠ₯ μ‹œκ°„ 전체 쑰회
List<ApplicantAvailability> allAvail =
applicantAvailabilityRepository.findAllByApplicationIdIn(appIds);

// 평가 전체 쑰회
List<Evaluation> allEvaluations =
evaluationRepository.findAllByApplicationIdIn(appIds);

// 평가 κΈ°μ€€ (κΈ°μ‘΄ 단건 상세 방식과 동일)
// λ‹¨κ±΄μ—μ„œλŠ” DOCUMENT κΈ°μ€€λ§Œ κ°€μ Έμ˜€μ§€λ§Œ, Detail.from() λ‚΄λΆ€μ—μ„œ 인터뷰/μ„œλ₯˜ λͺ¨λ‘ μ‚¬μš©ν•˜λ―€λ‘œ recruitment.getEvaluationCriteriaList() κ·ΈλŒ€λ‘œ 써도 됨.
Recruitment recruitment = apps.isEmpty() ? null : apps.get(0).getRecruitment();
List<EvaluationCriteria> criteriaList =
recruitment != null ? recruitment.getEvaluationCriteriaList() : List.of();


// previous, next β†’ μ—‘μ…€μ—μ„œλŠ” λΆˆν•„μš”ν•˜λ―€λ‘œ null
Long previous = null;
Long next = null;


// Detail DTO λ³€ν™˜
Map<Long, List<ApplicantAvailability>> availMap =
allAvail.stream().collect(Collectors.groupingBy(a -> a.getApplication().getId()));

Map<Long, List<Evaluation>> evalMap =
allEvaluations.stream().collect(Collectors.groupingBy(e -> e.getApplication().getId()));


return apps.stream()
.map(app -> assembler.toDetail(
app,
availMap.getOrDefault(app.getId(), List.of()),
evalMap.getOrDefault(app.getId(), List.of()),
criteriaList,
currentUserId,
previous,
next
))
.toList();
}
Comment thread
EunjinWoo marked this conversation as resolved.


/**
* PASS/FAIL/HOLD의 간단 μƒνƒœλ₯Ό 단계와 ν˜„μž¬ μƒνƒœμ— 맞좰 ApplicationStatus으둜 λ§€ν•‘
* @param stage λ³€κ²½ν•  단계 (DOCUMENT, INTERVIEW, FINAL_PASS, FAIL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public interface EvaluationJpaRepository extends JpaRepository<Evaluation, Long>
List<Evaluation> findByApplicationAndUserAndCriteriaIn(Application application, User user, List<EvaluationCriteria> criterias);
long countByApplication_IdAndUser_IdAndCriteria_IdIn(Long applicationId, Long userId, List<Long> criteriaIds);
long countByApplication_IdAndUser_IdAndCriteria_EvaluationType(Long id, Long userId, EvaluationType evaluationType);
List<Evaluation> findAllByApplicationIdIn(List<Long> appIds);
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ public interface EvaluationRepository {
long countByApplication_IdAndUser_IdAndCriteria_IdIn(Long applicationId, Long userId, List<Long> criteriaIds);
long countByApplication_IdAndUser_IdAndCriteria_EvaluationType(Long id, Long userId, EvaluationType evaluationType);
void deleteAll(List<Evaluation> existingEvaluations);
List<Evaluation> findAllByApplicationIdIn(List<Long> appIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@ public long countByApplication_IdAndUser_IdAndCriteria_EvaluationType(Long id, L
public void deleteAll(List<Evaluation> existingEvaluations) {
evaluationJpaRepository.deleteAll(existingEvaluations);
}

@Override
public List<Evaluation> findAllByApplicationIdIn(List<Long> appIds) {
return evaluationJpaRepository.findAllByApplicationIdIn(appIds);
}
}
Loading
Loading