Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
35c52a6
#136 test: recruitment admin CRUD 테스트 추가 (REC-ADMIN-002~009)
seoyeon83 Jun 18, 2026
cf06600
#136 test: 검수 피드백 반영 — 누락 경계/분기 케이스 보강
seoyeon83 Jun 18, 2026
fd9a0ea
#136 test: 명세서 TC ID 정합 — 신규 케이스 ID 충돌 해소
seoyeon83 Jun 18, 2026
ca82026
#170 feat: 지원서 평가 어드민 기능 구현
seoyeon83 Jun 22, 2026
a542ae3
#170 fix: 개인 평가 upsert에 flushAutomatically 추가
seoyeon83 Jun 22, 2026
166d9e3
#170 test: 지원서 평가 어드민 4계층 테스트 추가
seoyeon83 Jun 22, 2026
4fa5b50
#170 feat: 관리자 본인 정보 조회 API 추가
seoyeon83 Jun 23, 2026
07998bc
#170 chore: cors 추가
seoyeon83 Jun 23, 2026
b1088b6
#170 feat: 지원서 답변 조회 API 추가
seoyeon83 Jun 23, 2026
fe6f019
#170 chore: 조회 권한 범위 수정
seoyeon83 Jun 23, 2026
21642b8
#170 feat: 개인 평가 면접 질문 추가 및 지원서별 면접 질문 조회 API
seoyeon83 Jun 23, 2026
fb0c55d
#170 feat: 차기대표진 신설 — 전 부문 조회/평가는 차기대표진, 최종평가는 현재+차기대표진
seoyeon83 Jun 23, 2026
a0affd7
#170 feat: 평가 대시보드에 로그인 본인 개인 평가(decision) 반환
seoyeon83 Jun 23, 2026
035ed90
#170 fix: 평가자 풀에 차기대표진 포함 (지원서별 평가/면접질문 조회)
seoyeon83 Jun 23, 2026
42edce1
#170 chore: data.sql 평가 시드 데이터 반영 — 차기대표진/final_decision/applicant_eval
seoyeon83 Jun 25, 2026
5c21850
Merge pull request #171 from BOAZ-website/feat/#170-applicant-evaluation
seoyeon83 Jun 25, 2026
05abfd1
Merge remote-tracking branch 'origin/dev' into test/#136-recruitment-…
seoyeon83 Jun 25, 2026
c97e0d3
#136 test: createQuestions DB order_num 중복 케이스 보강 (TC-006b)
seoyeon83 Jun 25, 2026
e6cda58
Merge pull request #173 from BOAZ-website/test/#136-recruitment-admin…
seoyeon83 Jun 25, 2026
911c540
#172 feat: 지원서 CSV에 최종 평가(final_decision) 컬럼 추가
seoyeon83 Jun 25, 2026
4e2da2e
#172 feat: 지원서 CSV 합격/불합격/전체 추출 필터(decision) 추가
seoyeon83 Jun 25, 2026
092f1c1
#172 test: 최종 평가 컬럼·decision 필터 테스트 추가/수정
seoyeon83 Jun 25, 2026
4acf18a
#172 chore: CSV 추출 필터 테스트용 지원자 시드 추가
seoyeon83 Jun 25, 2026
cff81af
Merge pull request #175 from BOAZ-website/feat/#172-csv-decision-filter
seoyeon83 Jun 26, 2026
33abfbf
#174 test: Admin 도메인 테스트 코드 추가 (Service/Controller/Repository/Integra…
seoyeon83 Jun 26, 2026
cbf2273
Merge pull request #176 from BOAZ-website/test/#174-admin
seoyeon83 Jun 26, 2026
fd71c48
#170 docs: 평가 API 권한 설명을 실제 track 접근 규칙(차기대표진 전 부문)에 맞게 정정
seoyeon83 Jun 26, 2026
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
Expand Up @@ -5,6 +5,7 @@
import com.boaz.backend.domain.admin.dto.request.AdminUpdateRequest;
import com.boaz.backend.domain.admin.dto.response.AdminAccountResponse;
import com.boaz.backend.domain.admin.dto.response.AdminIdResponse;
import com.boaz.backend.domain.admin.dto.response.AdminMeResponse;
import com.boaz.backend.domain.admin.service.AdminService;
import com.boaz.backend.global.common.ApiResponse;
import com.boaz.backend.global.security.AdminUserDetails;
Expand Down Expand Up @@ -56,6 +57,15 @@ public ResponseEntity<ApiResponse<List<AdminAccountResponse>>> getAccounts(
return ResponseEntity.ok(ApiResponse.ok(response));
}

@Operation(summary = "내 계정 정보 조회", description = "인증된 본인의 id, 소속, 이름을 반환.")
@GetMapping("/accounts/me")
public ResponseEntity<ApiResponse<AdminMeResponse>> getMe(
@AuthenticationPrincipal AdminUserDetails userDetails
) {
AdminMeResponse response = adminService.getMe(userDetails.getAdmin());
return ResponseEntity.ok(ApiResponse.ok(response));
}

@Operation(summary = "id별 계정 조회 (SUPER: 전체 / TEAM: 본인만)")
@GetMapping("/accounts/{id}")
public ResponseEntity<ApiResponse<AdminAccountResponse>> getAccount(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ public class AdminCreateRequest {

@NotNull
@Schema(description = "직책/소속", example = "서비스운영팀",
allowableValues = {"대표진", "디자인팀", "자료연구팀", "운영지원팀", "기획팀", "대외협력팀", "서비스운영팀"})
allowableValues = {"대표진", "차기대표진", "디자인팀", "자료연구팀", "운영지원팀", "기획팀", "대외협력팀", "서비스운영팀"})
private Admin.TeamName teamName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.boaz.backend.domain.admin.dto.response;

import com.boaz.backend.domain.admin.entity.Admin;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class AdminMeResponse {

@Schema(description = "계정 ID", example = "1")
private Long id;

@Schema(description = "직책/소속", example = "대표진",
allowableValues = {"대표진", "디자인팀", "자료연구팀", "운영지원팀", "기획팀", "대외협력팀", "서비스운영팀"})
private String teamName;

@Schema(description = "이름", example = "문혁준")
private String name;

public static AdminMeResponse from(Admin admin) {
return AdminMeResponse.builder()
.id(admin.getId())
.teamName(admin.getTeamName().name())
.name(admin.getName())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum Role {

public enum TeamName {
@Schema(description = "대표진") 대표진,
@Schema(description = "차기대표진") 차기대표진,
@Schema(description = "디자인팀") 디자인팀,
@Schema(description = "자료연구팀") 자료연구팀,
@Schema(description = "운영지원팀") 운영지원팀,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.boaz.backend.domain.admin.repository;

import com.boaz.backend.domain.admin.entity.Admin;
import com.boaz.backend.global.common.enums.Track;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;
Expand All @@ -17,4 +20,15 @@ public interface AdminRepository extends JpaRepository<Admin, Long> {
Optional<Admin> findByIdAndDeletedAtIsNull(Long id);

long countByRoleAndDeletedAtIsNull(Admin.Role role);

// 지원서별 평가 조회 — 해당 부문 평가자 풀 (미평가자 null 포함용)
List<Admin> findByTrackAndDeletedAtIsNullOrderByNameAsc(Track track);

// 평가자 풀 = 해당 부문 + 전 부문 평가 권한자(차기 대표진). 차기 대표진은 본인 track과 무관하게 모든 지원자 풀에 포함.
@Query("SELECT DISTINCT a FROM Admin a WHERE a.deletedAt IS NULL " +
"AND (a.track = :track OR (a.role = :allTrackRole AND a.teamName = :allTrackTeam)) " +
"ORDER BY a.name ASC")
List<Admin> findEvaluatorPool(@Param("track") Track track,
@Param("allTrackRole") Admin.Role allTrackRole,
@Param("allTrackTeam") Admin.TeamName allTrackTeam);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.boaz.backend.domain.admin.dto.request.AdminUpdateRequest;
import com.boaz.backend.domain.admin.dto.response.AdminAccountResponse;
import com.boaz.backend.domain.admin.dto.response.AdminIdResponse;
import com.boaz.backend.domain.admin.dto.response.AdminMeResponse;
import com.boaz.backend.domain.admin.entity.Admin;
import com.boaz.backend.domain.admin.repository.AdminRepository;
import com.boaz.backend.domain.auth.repository.RefreshTokenRepository;
Expand Down Expand Up @@ -66,6 +67,10 @@ public AdminIdResponse createAccount(AdminCreateRequest request, Admin currentAd
return new AdminIdResponse(admin.getId());
}

public AdminMeResponse getMe(Admin currentAdmin) {
return AdminMeResponse.from(currentAdmin);
}

public AdminAccountResponse getAccount(Long id, Admin currentAdmin) {
boolean isSelf = currentAdmin.getId().equals(id);
boolean isTeam = currentAdmin.getRole() == Admin.Role.TEAM;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.boaz.backend.domain.recruitment.controller;

import com.boaz.backend.domain.recruitment.dto.request.EvaluationSaveRequest;
import com.boaz.backend.domain.recruitment.dto.request.FinalDecisionUpdateRequest;
import com.boaz.backend.domain.recruitment.dto.response.*;
import com.boaz.backend.domain.recruitment.service.RecruitmentService;
import com.boaz.backend.global.common.ApiResponse;
import com.boaz.backend.global.security.AdminUserDetails;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "[Admin] Applicant Evaluation", description = "Admin 전용 지원서 평가 API")
@SecurityRequirement(name = "bearerAuth")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/admin/recruitment")
public class ApplicantEvaluationAdminController {

private final RecruitmentService recruitmentService;

@Operation(summary = "전체 지원서 조회 (지원자 대시보드)",
description = "공고 내 전체 지원서를 반환합니다. DRAFT 포함, 정렬·필터·검색은 프론트에서 처리. 차기대표진은 전 부문, 그 외(현재 대표진 포함)는 본인 부문만 조회.")
@GetMapping("/{recruitmentId}/applicants")
public ResponseEntity<ApiResponse<List<ApplicantSummaryResponse>>> getApplicants(
@PathVariable Long recruitmentId,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.getApplicants(recruitmentId, userDetails.getAdmin())));
}

@Operation(summary = "전체 지원서 및 평가 조회 (평가 대시보드)",
description = "공고 내 SUBMITTED 지원서 + 평가 집계(합격/보류/불합 개수·총점) + 최종 평가를 반환합니다. 차기대표진은 전 부문, 그 외(현재 대표진 포함)는 본인 부문만 조회.")
@GetMapping("/{recruitmentId}/applicants/evaluations")
public ResponseEntity<ApiResponse<List<ApplicantEvaluationResponse>>> getApplicantEvaluations(
@PathVariable Long recruitmentId,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.getApplicantEvaluations(recruitmentId, userDetails.getAdmin())));
}

@Operation(summary = "최종 평가 수정 (대표진 전용)",
description = "현재 대표진 또는 차기대표진(role=SUPER)만 지원자의 최종 평가(final_decision)를 수정합니다. 현재 대표진은 본인 부문만, 차기대표진은 전 부문.")
@PatchMapping("/applicants/{applicantId}/final-decision")
public ResponseEntity<ApiResponse<FinalDecisionResponse>> updateFinalDecision(
@PathVariable Long applicantId,
@RequestBody @Valid FinalDecisionUpdateRequest request,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.updateFinalDecision(applicantId, request, userDetails.getAdmin())));
}

@Operation(summary = "지원서별 평가 조회",
description = "한 지원자에 대한 해당 부문 평가자 전체의 평가를 반환합니다. 미평가 평가자는 null로 포함. 차기대표진 외(현재 대표진 포함)에는 본인 부문 지원자만 조회.")
@GetMapping("/applicants/{applicantId}/evaluations")
public ResponseEntity<ApiResponse<ApplicantEvaluatorsResponse>> getApplicantEvaluators(
@PathVariable Long applicantId,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.getApplicantEvaluators(applicantId, userDetails.getAdmin())));
}

@Operation(summary = "지원서별 면접 질문 조회",
description = "한 지원자에 대한 해당 부문 평가자별 면접 질문을 반환합니다. 미작성 평가자는 null로 포함. 차기대표진 외(현재 대표진 포함)에는 본인 부문 지원자만 조회.")
@GetMapping("/applicants/{applicantId}/interview-questions")
public ResponseEntity<ApiResponse<ApplicantInterviewQuestionsResponse>> getApplicantInterviewQuestions(
@PathVariable Long applicantId,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.getApplicantInterviewQuestions(applicantId, userDetails.getAdmin())));
}

@Operation(summary = "지원서 답변 조회",
description = "한 지원자가 작성한 문항별 답변을 문항 정보(질문 내용·유형)와 함께 문항 순서대로 반환합니다. (평가 사이드바 지원서 본문) 차기대표진 외(현재 대표진 포함)에는 본인 부문 지원자만 조회.")
@GetMapping("/applicants/{applicantId}/answers")
Comment thread
seoyeon83 marked this conversation as resolved.
public ResponseEntity<ApiResponse<ApplicantAnswersResponse>> getApplicantAnswers(
@PathVariable Long applicantId,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.getApplicantAnswers(applicantId, userDetails.getAdmin())));
}

@Operation(summary = "개인 평가 조회",
description = "로그인한 평가자 본인이 이 지원자에 매긴 평가를 반환합니다. 미평가 시 data=null.")
@GetMapping("/applicants/{applicantId}/evaluations/me")
public ResponseEntity<ApiResponse<MyEvaluationResponse>> getMyEvaluation(
@PathVariable Long applicantId,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.getMyEvaluation(applicantId, userDetails.getAdmin())));
}

@Operation(summary = "개인 평가 저장 (upsert)",
description = "로그인한 평가자 본인의 평가를 저장합니다. 본인 부문 지원자만 가능, SUBMITTED만 가능.")
@PutMapping("/applicants/{applicantId}/evaluations/me")
public ResponseEntity<ApiResponse<MyEvaluationResponse>> saveMyEvaluation(
@PathVariable Long applicantId,
@RequestBody @Valid EvaluationSaveRequest request,
@AuthenticationPrincipal AdminUserDetails userDetails) {
return ResponseEntity.ok(ApiResponse.ok(
recruitmentService.saveMyEvaluation(applicantId, request, userDetails.getAdmin())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.boaz.backend.domain.recruitment.dto.response.RecruitmentIdResponse;
import com.boaz.backend.domain.recruitment.dto.response.RecruitmentResponse;
import com.boaz.backend.domain.recruitment.dto.response.SubscriptionResponse;
import com.boaz.backend.domain.recruitment.entity.DecisionFilter;
import com.boaz.backend.domain.recruitment.service.RecruitmentService;
import com.boaz.backend.global.common.ApiResponse;

Expand Down Expand Up @@ -64,11 +65,13 @@ public ResponseEntity<ApiResponse<Void>> deleteRecruitment(
return ResponseEntity.ok(ApiResponse.ok(null));
}

@Operation(summary = "지원서 CSV 파일 생성")
@Operation(summary = "지원서 CSV 파일 생성",
description = "term 공고의 지원서를 부문별 CSV로 생성합니다. decision으로 합격(PASS)/불합격(FAIL)/전체(ALL) 추출 범위를 지정합니다.")
@PostMapping("/applications/download")
public ResponseEntity<ApiResponse<Void>> downloadApplications(
@RequestParam Integer term) {
recruitmentService.downloadApplications(term);
@RequestParam Integer term,
@RequestParam(required = false, defaultValue = "ALL") DecisionFilter decision) {
recruitmentService.downloadApplications(term, decision);
return ResponseEntity.ok(ApiResponse.ok(null));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.boaz.backend.domain.recruitment.dto.request;

import com.boaz.backend.domain.recruitment.entity.EvaluationDecision;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;


@Getter
@NoArgsConstructor
public class EvaluationSaveRequest {

@NotNull
private EvaluationDecision decision;

// 1~10 또는 null (미점수). null이면 검증 통과
@Min(1)
@Max(10)
private Integer score;

private String memo;

// 면접 질문 (null 허용)
private String interviewQuestion;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.boaz.backend.domain.recruitment.dto.request;

import com.boaz.backend.domain.recruitment.entity.EvaluationDecision;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class FinalDecisionUpdateRequest {

@NotNull
private EvaluationDecision finalDecision;
Comment thread
seoyeon83 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.boaz.backend.domain.recruitment.dto.response;

import com.boaz.backend.domain.recruitment.entity.ApplicationQuestion;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Getter
@Builder
public class ApplicantAnswersResponse {

private final Long applicantId;

private final List<AnswerDetailResponse> answers;

public static ApplicantAnswersResponse of(Long applicantId, List<AnswerDetailResponse> answers) {
return ApplicantAnswersResponse.builder()
.applicantId(applicantId)
.answers(answers)
.build();
}

@Getter
@Builder
public static class AnswerDetailResponse {

private final Long questionId;

private final String label;

private final ApplicationQuestion.Category category;

private final ApplicationQuestion.Type type;

private final String content;

private final Integer orderNum;

// TEXT → 문자열, TABLE → 객체 (사용자용 AnswerItemResponse와 동일 규약)
private final JsonNode answer;
}
}
Loading
Loading