diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 95e19532..7225f58f 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -39,6 +39,21 @@ include::{snippets}/student-find-all/http-response.adoc[] .Response Body include::{snippets}/student-find-all/response-fields.adoc[] +=== `GET`: 교직원 목록 조회 + +.HTTP Request +include::{snippets}/faculty-find-all/http-request.adoc[] + +.Path Parameters +include::{snippets}/faculty-find-all/query-parameters.adoc[] + +.HTTP Response +include::{snippets}/faculty-find-all/http-response.adoc[] + +.Response Body +include::{snippets}/faculty-find-all/response-fields.adoc[] + + === `PATCH`: 비밀번호 수정 .HTTP Request @@ -334,4 +349,3 @@ include::{snippets}/admin-auth-delete/request-body.adoc[] .HTTP Response include::{snippets}/admin-auth-delete/http-response.adoc[] - diff --git a/backend/src/main/java/sw_css/admin/auth/api/AdminAuthController.java b/backend/src/main/java/sw_css/admin/auth/api/AdminAuthController.java index 682b4de5..a2e1fca7 100644 --- a/backend/src/main/java/sw_css/admin/auth/api/AdminAuthController.java +++ b/backend/src/main/java/sw_css/admin/auth/api/AdminAuthController.java @@ -47,7 +47,7 @@ public ResponseEntity registerFaculties( public ResponseEntity deleteFaculty( @SuperAdminInterface FacultyMember facultyMember, @RequestBody @Valid DeleteFacultyRequest request) { - adminAuthCommandService.deleteFaculty(request.member_id()); + adminAuthCommandService.deleteFaculty(request.faculty_id()); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java b/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java index 97a40999..ee5565d2 100644 --- a/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java +++ b/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java @@ -81,10 +81,12 @@ public void registerFaculties(MultipartFile file) { } @Transactional - public void deleteFaculty(Long memberId) { - FacultyMember facultyMember = facultyMemberRepository.findById(memberId) + public void deleteFaculty(Long facultyId) { + FacultyMember facultyMember = facultyMemberRepository.findById(facultyId) .orElseThrow(() -> new AdminAuthException(AdminAuthExceptionType.MEMBER_NOT_FOUND)); + if(facultyMember.getId() == 1) throw new AdminAuthException(AdminAuthExceptionType.MEMBER_NOT_FOUND); + Member member = facultyMember.getMember(); checkIsMemberDeleted(member); diff --git a/backend/src/main/java/sw_css/admin/auth/application/dto/request/DeleteFacultyRequest.java b/backend/src/main/java/sw_css/admin/auth/application/dto/request/DeleteFacultyRequest.java index 8927bace..8cffea8c 100644 --- a/backend/src/main/java/sw_css/admin/auth/application/dto/request/DeleteFacultyRequest.java +++ b/backend/src/main/java/sw_css/admin/auth/application/dto/request/DeleteFacultyRequest.java @@ -1,6 +1,6 @@ package sw_css.admin.auth.application.dto.request; public record DeleteFacultyRequest( - Long member_id + Long faculty_id ) { } diff --git a/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthExceptionType.java b/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthExceptionType.java index 2ae597c1..ddd0c7c6 100644 --- a/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthExceptionType.java +++ b/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthExceptionType.java @@ -10,6 +10,7 @@ public enum AdminAuthExceptionType implements BaseExceptionType { FAILED_REGISTER_FACULTY(HttpStatus.BAD_REQUEST, ""), MEMBER_EMAIL_DUPLICATE(HttpStatus.CONFLICT, "이메일이 중복됩니다."), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."), + CANNOT_DELETE_SUPER_ADMIN(HttpStatus.FORBIDDEN, "최고 관리자는 삭제할 수 없습니다.") ; private final HttpStatus httpStatus; diff --git a/backend/src/main/java/sw_css/admin/member/api/MemberAdminController.java b/backend/src/main/java/sw_css/admin/member/api/MemberAdminController.java index 9681e6a0..588940eb 100644 --- a/backend/src/main/java/sw_css/admin/member/api/MemberAdminController.java +++ b/backend/src/main/java/sw_css/admin/member/api/MemberAdminController.java @@ -2,23 +2,38 @@ import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import sw_css.admin.member.application.MemberAdminQueryService; +import sw_css.admin.member.application.dto.response.FacultyMemberResponse; import sw_css.member.application.dto.response.StudentMemberResponse; +import sw_css.member.domain.FacultyMember; +import sw_css.utils.annotation.AdminInterface; @Validated -@RequestMapping("/admin/members") +@RequestMapping("/admin/member") @RestController @RequiredArgsConstructor public class MemberAdminController { private final MemberAdminQueryService memberAdminQueryService; - @GetMapping - public ResponseEntity> findAllStudent() { + @GetMapping("/students") + public ResponseEntity> findAllStudent(@AdminInterface FacultyMember facultyMember) { return ResponseEntity.ok(memberAdminQueryService.findStudentMembers()); } + + @GetMapping("/faculties") + public ResponseEntity> findAllFaculty( + @AdminInterface FacultyMember facultyMember, + @RequestParam(value = "field", required = false) final Integer field, + @RequestParam(value = "keyword", required = false) final String keyword, + final Pageable pageable) { + return ResponseEntity.ok(memberAdminQueryService.findFacultyMembers(field, keyword, pageable)); + } } diff --git a/backend/src/main/java/sw_css/admin/member/application/MemberAdminQueryService.java b/backend/src/main/java/sw_css/admin/member/application/MemberAdminQueryService.java index ecfd8df3..dfa3b42c 100644 --- a/backend/src/main/java/sw_css/admin/member/application/MemberAdminQueryService.java +++ b/backend/src/main/java/sw_css/admin/member/application/MemberAdminQueryService.java @@ -2,10 +2,16 @@ import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import sw_css.admin.member.application.dto.response.FacultyMemberResponse; import sw_css.member.application.dto.response.StudentMemberResponse; +import sw_css.member.domain.FacultyMember; import sw_css.member.domain.StudentMember; +import sw_css.member.domain.repository.FacultyMemberCustomRepository; +import sw_css.member.domain.repository.FacultyMemberRepository; import sw_css.member.domain.repository.StudentMemberRepository; @Service @@ -13,9 +19,16 @@ @Transactional(readOnly = true) public class MemberAdminQueryService { private final StudentMemberRepository studentMemberRepository; + private final FacultyMemberCustomRepository facultyMemberCustomRepository; public List findStudentMembers() { final List students = studentMemberRepository.findAll(); return students.stream().map(StudentMemberResponse::from).toList(); } + + public Page findFacultyMembers(final Integer field, final String keyword, final Pageable pageable) { + final Page faculties = facultyMemberCustomRepository.findFacultyMembers(field, keyword, pageable); + + return FacultyMemberResponse.from(faculties, pageable); + } } diff --git a/backend/src/main/java/sw_css/admin/member/application/dto/response/FacultyMemberResponse.java b/backend/src/main/java/sw_css/admin/member/application/dto/response/FacultyMemberResponse.java new file mode 100644 index 00000000..f2e7bf38 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/member/application/dto/response/FacultyMemberResponse.java @@ -0,0 +1,29 @@ +package sw_css.admin.member.application.dto.response; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import sw_css.member.domain.FacultyMember; + +public record FacultyMemberResponse( + Long id, + Long facultyId, + String email, + String name, + String phoneNumber) { + public static FacultyMemberResponse from(final FacultyMember facultyMember) { + return new FacultyMemberResponse( + facultyMember.getMember().getId(), + facultyMember.getId(), + facultyMember.getMember().getEmail(), + facultyMember.getMember().getName(), + facultyMember.getMember().getPhoneNumber() + ); + } + + public static Page from(final Page faculties, final Pageable pageable) { + return new PageImpl<>(faculties.stream() + .map(FacultyMemberResponse::from) + .toList(), pageable, faculties.getTotalElements()); + } +} diff --git a/backend/src/main/java/sw_css/member/domain/FacultySearchField.java b/backend/src/main/java/sw_css/member/domain/FacultySearchField.java new file mode 100644 index 00000000..aa5cb556 --- /dev/null +++ b/backend/src/main/java/sw_css/member/domain/FacultySearchField.java @@ -0,0 +1,26 @@ +package sw_css.member.domain; + +import java.util.Arrays; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import sw_css.member.exception.MemberException; +import sw_css.member.exception.MemberExceptionType; + +@RequiredArgsConstructor +public enum FacultySearchField { + FACULTY_ID(1), + MEMBER_NAME(2), + MEMBER_EMAIL(3); + + final Integer fieldId; + + public static FacultySearchField fromValue(final Integer fieldId) { + return Arrays.stream(values()).filter(field -> Objects.equals(field.getFieldId(), fieldId)).findFirst() + .orElseThrow(() -> new MemberException( + MemberExceptionType.INVALID_SEARCH_FIELD_ID)); + } + + public Integer getFieldId() { + return fieldId; + } +} diff --git a/backend/src/main/java/sw_css/member/domain/repository/FacultyMemberCustomRepository.java b/backend/src/main/java/sw_css/member/domain/repository/FacultyMemberCustomRepository.java new file mode 100644 index 00000000..e242dcbb --- /dev/null +++ b/backend/src/main/java/sw_css/member/domain/repository/FacultyMemberCustomRepository.java @@ -0,0 +1,13 @@ +package sw_css.member.domain.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.lang.Nullable; +import sw_css.member.domain.FacultyMember; + +public interface FacultyMemberCustomRepository { + + Page findFacultyMembers(@Nullable final Integer field, + @Nullable final String keyword, + final Pageable pageable); +} diff --git a/backend/src/main/java/sw_css/member/domain/repository/FacultyMemberRepository.java b/backend/src/main/java/sw_css/member/domain/repository/FacultyMemberRepository.java index 8b0551b3..1a9b40f1 100644 --- a/backend/src/main/java/sw_css/member/domain/repository/FacultyMemberRepository.java +++ b/backend/src/main/java/sw_css/member/domain/repository/FacultyMemberRepository.java @@ -5,7 +5,5 @@ import sw_css.member.domain.FacultyMember; public interface FacultyMemberRepository extends JpaRepository { - boolean existsByMemberId(Long memberId); - Optional findByMemberId(Long memberId); } diff --git a/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java b/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java index 5c8b436a..8f25c5a9 100644 --- a/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java +++ b/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java @@ -6,7 +6,9 @@ public enum MemberExceptionType implements BaseExceptionType { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."), MEMBER_WRONG_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 잘못되었습니다."), - NOT_FOUND_STUDENT(HttpStatus.NOT_FOUND, "해당하는 학생이 존재하지 않습니다."); + NOT_FOUND_STUDENT(HttpStatus.NOT_FOUND, "해당하는 학생이 존재하지 않습니다."), + INVALID_SEARCH_FIELD_ID(HttpStatus.BAD_REQUEST, "검색 유형이 올바르지 않습니다."), + ; private final HttpStatus httpStatus; diff --git a/backend/src/main/java/sw_css/member/persistence/FacultyMemberCustomRepositoryImpl.java b/backend/src/main/java/sw_css/member/persistence/FacultyMemberCustomRepositoryImpl.java new file mode 100644 index 00000000..f9bc27c1 --- /dev/null +++ b/backend/src/main/java/sw_css/member/persistence/FacultyMemberCustomRepositoryImpl.java @@ -0,0 +1,73 @@ +package sw_css.member.persistence; + +import static sw_css.member.domain.QFacultyMember.facultyMember; +import static sw_css.member.domain.QMember.member; +import static sw_css.member.domain.QStudentMember.studentMember; +import static sw_css.milestone.domain.QMilestone.milestone; +import static sw_css.milestone.domain.QMilestoneHistory.milestoneHistory; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Repository; +import sw_css.member.domain.FacultyMember; +import sw_css.member.domain.FacultySearchField; +import sw_css.member.domain.repository.FacultyMemberCustomRepository; +import sw_css.milestone.domain.MilestoneHistorySearchField; +import sw_css.milestone.persistence.dto.MilestoneHistoryWithStudentInfo; + +@Repository +@AllArgsConstructor +public class FacultyMemberCustomRepositoryImpl implements FacultyMemberCustomRepository { + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Page findFacultyMembers(@Nullable final Integer field, + @Nullable final String keyword, + final Pageable pageable) { + final BooleanBuilder booleanBuilder = getBooleanBuilder(field, keyword); + + List facultyMembers = jpaQueryFactory + .selectFrom(facultyMember) + .join(facultyMember.member, member) + .where(booleanBuilder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + + final Long count = jpaQueryFactory + .select(facultyMember.count()) + .from(facultyMember) + .where(booleanBuilder) + .fetchOne(); + + return new PageImpl<>(facultyMembers, pageable, count); + } + + private static BooleanBuilder getBooleanBuilder(final Integer field, final String keyword) { + BooleanBuilder booleanBuilder = new BooleanBuilder(); + booleanBuilder.and(facultyMember.member.isDeleted.eq(false)); + if (field != null && keyword != null && !keyword.isEmpty()) { + switch (FacultySearchField.fromValue(field)) { + case FACULTY_ID: + booleanBuilder.and(facultyMember.id.stringValue().like("%" + keyword + "%")); + break; + case MEMBER_NAME: + booleanBuilder.and(facultyMember.member.name.containsIgnoreCase(keyword)); + break; + case MEMBER_EMAIL: + booleanBuilder.and(facultyMember.member.email.containsIgnoreCase(keyword)); + break; + } + } + return booleanBuilder; + } +} diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminAuthApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminAuthApiDocsTest.java index 72eccf52..7559b782 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminAuthApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminAuthApiDocsTest.java @@ -87,7 +87,7 @@ public void registerFaculties() throws Exception { public void deleteFaculty() throws Exception { // given final RequestFieldsSnippet requestFieldsSnippet = requestFields( - fieldWithPath("member_id").type(JsonFieldType.NUMBER).description("관리자의 id") + fieldWithPath("faculty_id").type(JsonFieldType.NUMBER).description("교직원 번호") ); final long faculty_id = 1L; @@ -96,7 +96,7 @@ public void deleteFaculty() throws Exception { final String token = "Bearer AccessToken"; // when - doNothing().when(adminAuthCommandService).deleteFaculty(request.member_id()); + doNothing().when(adminAuthCommandService).deleteFaculty(request.faculty_id()); // then mockMvc.perform(RestDocumentationRequestBuilders.delete("/admin/auth") diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/MemberAdminApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/MemberAdminApiDocsTest.java index d0d5651a..db219036 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/MemberAdminApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/MemberAdminApiDocsTest.java @@ -1,22 +1,49 @@ package sw_css.restdocs.docs.admin; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static sw_css.member.domain.CareerType.EMPLOYMENT_COMPANY; import static sw_css.member.domain.CareerType.GRADUATE_SCHOOL; +import static sw_css.milestone.domain.MilestoneGroup.ACTIVITY; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.PayloadDocumentation; import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.QueryParametersSnippet; import sw_css.admin.member.api.MemberAdminController; +import sw_css.admin.member.application.dto.response.FacultyMemberResponse; +import sw_css.admin.milestone.application.dto.response.MilestoneHistoryResponse; +import sw_css.major.domain.College; +import sw_css.major.domain.Major; +import sw_css.member.application.dto.response.StudentMemberReferenceResponse; import sw_css.member.application.dto.response.StudentMemberResponse; +import sw_css.member.domain.CareerType; +import sw_css.member.domain.FacultyMember; +import sw_css.member.domain.Member; +import sw_css.member.domain.StudentMember; +import sw_css.milestone.domain.Milestone; +import sw_css.milestone.domain.MilestoneCategory; +import sw_css.milestone.domain.MilestoneStatus; +import sw_css.milestone.persistence.dto.MilestoneHistoryWithStudentInfo; import sw_css.restdocs.RestDocsTest; @WebMvcTest(MemberAdminController.class) @@ -44,12 +71,73 @@ public void findStudents() throws Exception { new StudentMemberResponse(202055555L, "abcdefg@aaa.com", "아무개", "사회학과", "", "", "01012345678", EMPLOYMENT_COMPANY, "네카라쿠배")); + final String token = "Bearer AccessToken"; + // when when(memberAdminQueryService.findStudentMembers()).thenReturn(response); // then - mockMvc.perform(get("/admin/members")) + mockMvc.perform(RestDocumentationRequestBuilders.get("/admin/member/students") + .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isOk()) .andDo(document("student-find-all", responseFields)); } + + + @Test + @DisplayName("[성공] 모든 교직원들의 정보를 조회할 수 있다.") + public void findFacultyMembers() throws Exception { + // given + final QueryParametersSnippet queryParameters = queryParameters( + parameterWithName("field").description("검색 필드 번호(option)").optional(), + parameterWithName("keyword").description("검색할 키워드(option)").optional() + ); + + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), + fieldWithPath("totalElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("size").type(JsonFieldType.NUMBER).description("페이지 내 데이터 수"), + fieldWithPath("number").type(JsonFieldType.NUMBER).description("페이지 번호"), + fieldWithPath("sort.empty").type(JsonFieldType.BOOLEAN).description("정렬 속성의 존재 여부"), + fieldWithPath("sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬 여부"), + fieldWithPath("sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬여부"), + fieldWithPath("first").type(JsonFieldType.BOOLEAN).description("첫 페이지인지 여부"), + fieldWithPath("last").type(JsonFieldType.BOOLEAN).description("마지막 페이지인지 여부"), + fieldWithPath("pageable.pageNumber").type(JsonFieldType.NUMBER).description("요청한 페이지번호"), + fieldWithPath("pageable.pageSize").type(JsonFieldType.NUMBER).description("요청한 페이지크기"), + fieldWithPath("pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("요청한 데이터가 비었는지 여부"), + fieldWithPath("pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.offset").type(JsonFieldType.NUMBER).description("요청한 페이지오프셋"), + fieldWithPath("pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("pageable.unpaged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("numberOfElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("empty").type(JsonFieldType.BOOLEAN).description("데이터의 존재 여부"), + fieldWithPath("content[].id").type(JsonFieldType.NUMBER).description("교직원의 회원 id"), + fieldWithPath("content[].facultyId").type(JsonFieldType.NUMBER).description("교직원 번호"), + fieldWithPath("content[].email").type(JsonFieldType.STRING).description("교직원의 이메일"), + fieldWithPath("content[].name").type(JsonFieldType.STRING).description("교직원 이름 이름"), + fieldWithPath("content[].phoneNumber").type(JsonFieldType.STRING).description("교직원 전화번호") + ); + + final Pageable pageable = PageRequest.of(0, 10); + final Page faculties = new PageImpl<>(List.of( + new FacultyMember(123412L, new Member(1L, "asdf@pusan.ac.kr", "박길태", "", "01000000000", false)), + new FacultyMember(234523L, new Member(2L, "qwer@pusan.ac.kr", "오해영", "", "01000000000", false)), + new FacultyMember(345634L, new Member(3L, "zxcv@pusan.ac.kr", "김희수", "", "01000000000", false)) + )); + final Page response = FacultyMemberResponse.from(faculties, pageable); + final String token = "Bearer AccessToken"; + + //when + when(memberAdminQueryService.findFacultyMembers(any(), any(), any())).thenReturn(response); + + //then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/admin/member/faculties") + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isOk()) + .andDo(document("faculty-find-all", queryParameters, responseBodySnippet)); + + } } diff --git a/frontend/src/adminComponents/Pagination/index.tsx b/frontend/src/adminComponents/Pagination/index.tsx index 3cc16c00..8470dbd5 100644 --- a/frontend/src/adminComponents/Pagination/index.tsx +++ b/frontend/src/adminComponents/Pagination/index.tsx @@ -8,15 +8,15 @@ import Link from 'next/link'; export interface PaginationProps { currentPage: number; totalItems: number; + itemPerPage?: number; pathname: string; query?: string; } -const Pagination = ({ currentPage, totalItems, pathname, query }: PaginationProps) => { - const ItemPerPage = 10; +const Pagination = ({ currentPage, totalItems, itemPerPage = 10, pathname, query }: PaginationProps) => { const buttonPerPage = 10; - const totalPageCount = Math.ceil(totalItems / ItemPerPage); + const totalPageCount = Math.ceil(totalItems / itemPerPage); const totalPages = Array.from({ length: totalPageCount }, (v, i) => i + 1); const showIdx = Math.floor((currentPage - 1) / buttonPerPage); diff --git a/frontend/src/app/admin/faculty/list/components/MemberTable/index.tsx b/frontend/src/app/admin/faculty/list/components/MemberTable/index.tsx index f7326d91..b8603a09 100644 --- a/frontend/src/app/admin/faculty/list/components/MemberTable/index.tsx +++ b/frontend/src/app/admin/faculty/list/components/MemberTable/index.tsx @@ -2,37 +2,63 @@ 'use client'; -import { MemberDto } from '@/types/common.dto'; +import { useAppSelector } from '@/lib/hooks/redux'; +import { useDeleteFacultyMutation } from '@/lib/hooks/useAdminApi'; +import { FacultyMemberDto } from '@/types/common.dto'; +import { useRouter } from 'next/navigation'; +import { toast } from 'react-toastify'; -const MemberTable = ({ members }: { members: MemberDto[] }) => { - const handleDeleteButtonClick = (member: MemberDto) => { +const MemberTable = ({ members }: { members: FacultyMemberDto[] }) => { + const { mutate: deleteFaculty } = useDeleteFacultyMutation(); + const auth = useAppSelector((state) => state.auth).value; + const router = useRouter(); + + const handleDeleteButtonClick = (member: FacultyMemberDto) => { window.confirm(`${member.name}을(를) 삭제하시겠습니까?`); + deleteFaculty(member.facultyId, { + onSuccess(data, variables, context) { + toast.info('교직원 삭제에 성공했습니다.'); + router.refresh(); + }, + onError(error, variables, context) { + toast.error(error.message); + }, + }); }; + + console.log(auth); + return ( - - - - - + + + + + + {auth.id === 1 && } + {members.map((member) => ( - + - + {auth.id === 1 && ( + + )} ))} diff --git a/frontend/src/app/admin/faculty/list/page.tsx b/frontend/src/app/admin/faculty/list/page.tsx index 59bb374a..d9d52c7e 100644 --- a/frontend/src/app/admin/faculty/list/page.tsx +++ b/frontend/src/app/admin/faculty/list/page.tsx @@ -4,37 +4,50 @@ import { headers } from 'next/headers'; import Pagination from '@/adminComponents/Pagination'; import SearchBox from '@/components/SearchBox'; -import { fieldCategories, members } from '@/mocks/adminMember'; +import { facultyFieldCategories, members } from '@/mocks/adminMember'; import MemberTable from './components/MemberTable'; +import { getFacultyMembers } from '@/lib/api/server.api'; +import { AuthSliceState } from '@/store/auth.slice'; +import { getAuthFromCookie } from '@/lib/utils/auth'; -const Page = ({ searchParams }: { searchParams?: { [key: string]: string | undefined } }) => { +const Page = async ({ searchParams }: { searchParams?: { [key: string]: string | undefined } }) => { const headersList = headers(); const pathname = headersList.get('x-pathname') || ''; + const auth: AuthSliceState = getAuthFromCookie(); + const page = searchParams?.page ? parseInt(searchParams.page, 10) : 1; const field = searchParams?.field ? parseInt(searchParams.field, 10) : -1; const keyword = searchParams?.keyword ? searchParams.keyword : ''; - // TODO: 검색 영역 조회 api 호출 - // TODO: query에 따른 교직원 목록 조회 api 호출 + const facultyMembers = await getFacultyMembers(auth.token, field, keyword, page - 1); return (
-
개발 중인 기능입니다.
- {/*
+
{members.length}명의 회원이 있습니다. - +
- - */} + {facultyMembers.content.length === 0 ? ( +
조건에 부합하는 교직원이 없습니다.
+ ) : ( + <> + + + + )}
); }; diff --git a/frontend/src/app/admin/faculty/register/page.tsx b/frontend/src/app/admin/faculty/register/page.tsx index 459f56b7..e8bf2bda 100644 --- a/frontend/src/app/admin/faculty/register/page.tsx +++ b/frontend/src/app/admin/faculty/register/page.tsx @@ -16,6 +16,8 @@ import * as Yup from 'yup'; import 'react-toastify/dist/ReactToastify.css'; import EmailTextInput from '@/components/Formik/EmailTextInput'; import TextInput from '@/components/Formik/TextInput'; +import { useRegisterFacultiesByFileMutation, useRegisterFacultyMutation } from '@/lib/hooks/useAdminApi'; +import { useState } from 'react'; interface FormType { email: string; @@ -36,17 +38,33 @@ const validationSchema = Yup.object().shape({ }); const Page = () => { + const { mutate: registerFaculty } = useRegisterFacultyMutation(); + const { mutate: registerFacultiesByFile } = useRegisterFacultiesByFileMutation(); + const [errorMessage, setErrorMessage] = useState(); + const handleSubmitButtonClick = (values: FormType) => { - // TODO: 등록 api 호출; - console.log(values.email, values.name); + registerFaculty(values, { + onSuccess(data, variables, context) { + toast.info('교직원 등록에 성공했습니다.'); + }, + onError(error, variables, context) { + toast.error(error.message); + }, + }); }; const handleFilesChange = (e: React.ChangeEvent) => { const target = e.currentTarget; const file = (target.files as FileList)[0]; - // TODO: 일괄 등록 api 호출. - console.log(file); + registerFacultiesByFile(file, { + onSuccess(data, variables, context) { + toast.info('교직원 등록에 성공했습니다.'); + }, + onError(error, variables, context) { + setErrorMessage(error.message); + }, + }); }; const handleSampleButtonClick = () => { @@ -67,8 +85,6 @@ const Page = () => { } }; - return
개발 중인 기능입니다.
; - return ( <>
@@ -115,6 +131,7 @@ const Page = () => { className="m-0 flex-grow cursor-pointer rounded-sm border-[1px] border-admin-border bg-white p-2 text-base file:m-0 file:h-0 file:w-0 file:border-0 file:p-0" />
+

{errorMessage}

교직원 등록 방법

@@ -131,9 +148,12 @@ const Page = () => { { + onSubmit={(values, { setSubmitting, resetForm }) => { setSubmitting(false); handleSubmitButtonClick(values); + resetForm({ + values: initialValues, + }); }} className="w-full" > diff --git a/frontend/src/lib/api/server.api.ts b/frontend/src/lib/api/server.api.ts index cd95202b..37eaab1f 100644 --- a/frontend/src/lib/api/server.api.ts +++ b/frontend/src/lib/api/server.api.ts @@ -1,6 +1,7 @@ import { MilestoneHistoryStatus } from '@/data/milestone'; import { server } from '@/lib/api/server.axios'; import { + FacultyMemberPageableDto, HackathonInformationDto, HackathonPageableDto, HackathonPrizeDto, @@ -204,3 +205,24 @@ export async function getHackathonPrize(hackathonId: number) { // return response?.data; return mockHackathonPrize; } + +export async function getFacultyMembers( + token: string, + field?: number, + keyword?: string, + page: number = 0, + size: number = 10, +) { + return await server + .get('/admin/member/faculties', { + headers: { Authorization: token }, + params: removeEmptyField({ + field, + keyword, + page, + size, + }), + }) + .then((res) => res.data) + .catch((err) => Promise.reject(err)); +} diff --git a/frontend/src/lib/hooks/useAdminApi.ts b/frontend/src/lib/hooks/useAdminApi.ts index b99837f0..a3e4068c 100644 --- a/frontend/src/lib/hooks/useAdminApi.ts +++ b/frontend/src/lib/hooks/useAdminApi.ts @@ -5,8 +5,6 @@ import { useAxiosMutation, useAxiosQuery } from '@/lib/hooks/useAxios'; import { MilestoneScoreOfStudentPageableDto } from '@/types/common.dto'; import { BusinessError } from '@/types/error'; import { useAppSelector } from './redux'; -import { headerInfos } from '@/data/clientCategory'; -import { stat } from 'fs'; export function useMilestoneHistoryExcelFileQuery(field: number | null, keyword: string | null) { const auth = useAppSelector((state) => state.auth).value; @@ -132,3 +130,48 @@ export const useRegisterHistoryInBatchMutation = () => { }, }); }; + +export const useRegisterFacultyMutation = () => { + const auth = useAppSelector((state) => state.auth).value; + return useAxiosMutation({ + mutationFn: async ({ email, name }: { email: string; name: string }) => { + await client.post( + '/admin/auth', + { email: email + '@pusan.ac.kr', name }, + { + headers: { Authorization: auth.token }, + }, + ); + }, + }); +}; + +export const useRegisterFacultiesByFileMutation = () => { + const auth = useAppSelector((state) => state.auth).value; + return useAxiosMutation({ + mutationFn: async (file?: File) => { + const formData = new FormData(); + formData.append('file', file!); + await client.post('/admin/auth/files', formData, { + headers: { 'Content-Type': 'multipart/form-data', Authorization: auth.token }, + }); + }, + }); +}; + +export const useDeleteFacultyMutation = () => { + const auth = useAppSelector((state) => state.auth).value; + return useAxiosMutation({ + mutationFn: async (faculty_id: number) => { + return await client + .delete(`/admin/auth`, { + data: { + faculty_id, + }, + headers: { Authorization: auth.token }, + }) + .then((res) => res.data) + .catch((err) => Promise.reject(err)); + }, + }); +}; diff --git a/frontend/src/mocks/adminMember.ts b/frontend/src/mocks/adminMember.ts index b5e2bb7a..06ec5673 100644 --- a/frontend/src/mocks/adminMember.ts +++ b/frontend/src/mocks/adminMember.ts @@ -1,5 +1,11 @@ import { StudentMemberDto } from '@/types/common.dto'; +export const facultyFieldCategories = [ + { id: 1, name: '교직원번호' }, + { id: 2, name: '이름' }, + { id: 3, name: '이메일' }, +]; + export const fieldCategories = [ { id: 1, name: '아이디' }, { id: 2, name: '이름' }, diff --git a/frontend/src/types/common.dto.ts b/frontend/src/types/common.dto.ts index 90a70d28..52fdc064 100644 --- a/frontend/src/types/common.dto.ts +++ b/frontend/src/types/common.dto.ts @@ -160,6 +160,10 @@ export interface StudentMemberDto extends MemberDto { careerDetail: string; } +export interface FacultyMemberDto extends MemberDto { + facultyId: number; +} + interface StudentMemberReferenceDto { id: number; name: string; @@ -200,7 +204,7 @@ export interface HackathonTeamDto { } export interface TeamMember { - id: number|null; + id: number | null; role: TeamMemberRole; isLeader: boolean; } @@ -231,3 +235,15 @@ export interface HackathonPrizeDto { rank: number; teams: HackathonTeamReferenceDto[]; } + +export interface FacultyMemberDto { + id: number; + facultyId: number; + name: string; + email: string; + phoneNumber: string; +} + +export interface FacultyMemberPageableDto extends Pageable { + content: FacultyMemberDto[]; +}
이메일이름교직원 번호전화번호설정
이메일이름교직원 번호전화번호설정
{member.email} {member.name}{member.id}{member.facultyId} {member.phoneNumber} - - + {auth.id !== member.facultyId && ( + + )} +