diff --git a/.gitignore b/.gitignore index 561da450..a4ecbc2f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ out/ /sample_data/** !/sample_data/.gitkeep + +.DS_Store diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bb641cb..479df083 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # 베이스 이미지 -FROM openjdk:17-jdk-alpine +FROM openjdk:17-jdk-slim # 작업 디렉토리 설정 WORKDIR /app diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3f4415c5..28983a71 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -6,7 +6,7 @@ services: dockerfile: docker/Dockerfile container_name: mywork-application ports: - - "8080:8080" + - "8081:8080" environment: - SPRING_PROFILES_ACTIVE=local deploy: @@ -66,7 +66,7 @@ services: image: grafana/grafana:11.5.6 container_name: grafana ports: - - "3000:3000" + - "3001:3000" volumes: - ../grafana-data:/var/lib/grafana environment: diff --git a/k6/member/MemberLoadTest.js b/k6/member/MemberLoadTest.js index fad7ee1a..77f3c48b 100644 --- a/k6/member/MemberLoadTest.js +++ b/k6/member/MemberLoadTest.js @@ -19,7 +19,7 @@ export default function() { let response = loginMember({ email: user.email, - password: user.password + password: '1234' }); check(response, { diff --git a/src/main/java/kr/mywork/domain/company/service/CompanyImageService.java b/src/main/java/kr/mywork/domain/company/service/CompanyImageService.java index 3a8f6174..55d276f8 100644 --- a/src/main/java/kr/mywork/domain/company/service/CompanyImageService.java +++ b/src/main/java/kr/mywork/domain/company/service/CompanyImageService.java @@ -29,7 +29,7 @@ public class CompanyImageService { private final CompanyRepository companyRepository; private final CompanyImageFileHandler companyImageFileHandler; - @Transactional(readOnly = true) + @Transactional public CompanyImageUploadUrlIssueResponse issueCompanyImageUploadUrl(final UUID companyId, final String fileName) { final Optional companyOptional = companyRepository.findById(companyId); diff --git a/src/main/java/kr/mywork/domain/member/repository/MemberRepository.java b/src/main/java/kr/mywork/domain/member/repository/MemberRepository.java index 8252a878..76c7996f 100644 --- a/src/main/java/kr/mywork/domain/member/repository/MemberRepository.java +++ b/src/main/java/kr/mywork/domain/member/repository/MemberRepository.java @@ -1,19 +1,19 @@ package kr.mywork.domain.member.repository; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - import kr.mywork.domain.company.service.dto.response.MemberDetailResponse; import kr.mywork.domain.member.model.Member; import kr.mywork.domain.member.service.dto.response.MemberSelectResponse; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + public interface MemberRepository { Optional findByEmailAndDeletedFalse(String email); - List findMemberByCompanyId(UUID companyId, int page, int memberPageSize); + List findMemberByCompanyId(UUID companyId, int page, int memberPageSize, String memberName); - long countByCompanyIdAndDeletedFalse(UUID companyId); + long countByCompanyIdAndDeletedFalse(UUID companyId,String memberName); Member save(Member member); diff --git a/src/main/java/kr/mywork/domain/member/service/MemberService.java b/src/main/java/kr/mywork/domain/member/service/MemberService.java index 48063db7..99e2acb8 100644 --- a/src/main/java/kr/mywork/domain/member/service/MemberService.java +++ b/src/main/java/kr/mywork/domain/member/service/MemberService.java @@ -1,16 +1,5 @@ package kr.mywork.domain.member.service; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import kr.mywork.common.auth.components.dto.LoginMemberDetail; import kr.mywork.domain.activityLog.listener.eventObject.ActivityLogCreateEvent; import kr.mywork.domain.activityLog.listener.eventObject.ActivityLogDeleteEvent; @@ -25,6 +14,16 @@ import kr.mywork.domain.member.service.dto.response.MemberSelectResponse; import kr.mywork.interfaces.member.controller.dto.request.ResetPasswordWebRequest; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -38,9 +37,9 @@ public class MemberService { private final ApplicationEventPublisher eventPublisher; @Transactional - public List findMemberByCompanyId(UUID companyId, int page) { + public List findMemberByCompanyId(UUID companyId, int page, String memberName) { - return memberRepository.findMemberByCompanyId(companyId, page, memberPageSize) + return memberRepository.findMemberByCompanyId(companyId, page, memberPageSize, memberName) .stream() .map(CompanyMemberResponse::fromEntity) .collect(Collectors.toList()); @@ -48,8 +47,8 @@ public List findMemberByCompanyId(UUID companyId, int pag } @Transactional - public long countMembersByCompanyId(UUID companyId) { - return memberRepository.countByCompanyIdAndDeletedFalse(companyId); + public long countMembersByCompanyId(UUID companyId, String memberName) { + return memberRepository.countByCompanyIdAndDeletedFalse(companyId,memberName); } @Transactional diff --git a/src/main/java/kr/mywork/domain/member/service/dto/response/CompanyMemberResponse.java b/src/main/java/kr/mywork/domain/member/service/dto/response/CompanyMemberResponse.java index b07d9f4b..40ee301f 100644 --- a/src/main/java/kr/mywork/domain/member/service/dto/response/CompanyMemberResponse.java +++ b/src/main/java/kr/mywork/domain/member/service/dto/response/CompanyMemberResponse.java @@ -9,7 +9,9 @@ public record CompanyMemberResponse( String name, String phoneNumber, String position, - String department + String department, + String role, + String email ){ public static CompanyMemberResponse fromEntity(Member member) { @@ -18,6 +20,8 @@ public static CompanyMemberResponse fromEntity(Member member) { member.getName(), member.getPhoneNumber(), member.getPosition(), - member.getDepartment()); + member.getDepartment(), + member.getRole().name(), + member.getEmail()); } } diff --git a/src/main/java/kr/mywork/domain/post/listener/PostApprovalNotificationTxListener.java b/src/main/java/kr/mywork/domain/post/listener/PostApprovalNotificationTxListener.java index b71fbcc2..cc9975df 100644 --- a/src/main/java/kr/mywork/domain/post/listener/PostApprovalNotificationTxListener.java +++ b/src/main/java/kr/mywork/domain/post/listener/PostApprovalNotificationTxListener.java @@ -30,7 +30,7 @@ public void handlePostApprovalAlarmEvent(final PostApprovalNotificationEvent eve private void saveNotification(final PostApprovalNotificationEvent event) { notificationService.save( - event.authorId(), event.authorName(), event.postTitle(), event.memberName(), event.memberId(), + event.authorId(), event.authorName(), event.postTitle(), event.actorName(), event.actorId(), event.targetType(), event.postId(), event.notificationActionType(), event.modifiedAt(), event.projectId(), event.projectStepId()); } diff --git a/src/main/java/kr/mywork/domain/post/listener/event/PostApprovalNotificationEvent.java b/src/main/java/kr/mywork/domain/post/listener/event/PostApprovalNotificationEvent.java index 39481a75..f970df68 100644 --- a/src/main/java/kr/mywork/domain/post/listener/event/PostApprovalNotificationEvent.java +++ b/src/main/java/kr/mywork/domain/post/listener/event/PostApprovalNotificationEvent.java @@ -6,8 +6,8 @@ import kr.mywork.domain.notification.model.NotificationActionType; import kr.mywork.domain.notification.model.TargetType; -public record PostApprovalNotificationEvent(UUID authorId, String authorName, String postTitle, UUID memberId, - String memberName, TargetType targetType, UUID postId, +public record PostApprovalNotificationEvent(UUID authorId, String authorName, String postTitle, UUID actorId, + String actorName, TargetType targetType, UUID postId, NotificationActionType notificationActionType, LocalDateTime modifiedAt, UUID projectId, UUID projectStepId) { } diff --git a/src/main/java/kr/mywork/domain/project_checklist/listener/CheckListNotificationTxListener.java b/src/main/java/kr/mywork/domain/project_checklist/listener/CheckListNotificationTxListener.java index 7872521b..d2bbb14f 100644 --- a/src/main/java/kr/mywork/domain/project_checklist/listener/CheckListNotificationTxListener.java +++ b/src/main/java/kr/mywork/domain/project_checklist/listener/CheckListNotificationTxListener.java @@ -33,8 +33,8 @@ private void saveNotification(final CheckListApprovalNotificationEvent event) { event.authorId(), event.authorName(), event.checkListTitle(), - event.authorName(), - event.memberId(), + event.actorName(), + event.actorId(), event.targetType(), event.checkListId(), event.notificationActionType(), diff --git a/src/main/java/kr/mywork/domain/project_checklist/listener/event/CheckListApprovalNotificationEvent.java b/src/main/java/kr/mywork/domain/project_checklist/listener/event/CheckListApprovalNotificationEvent.java index c452430b..bf38f49b 100644 --- a/src/main/java/kr/mywork/domain/project_checklist/listener/event/CheckListApprovalNotificationEvent.java +++ b/src/main/java/kr/mywork/domain/project_checklist/listener/event/CheckListApprovalNotificationEvent.java @@ -7,7 +7,7 @@ import kr.mywork.domain.notification.model.TargetType; public record CheckListApprovalNotificationEvent(UUID authorId, String authorName, String checkListTitle, - String memberName, UUID memberId, TargetType targetType, + String actorName, UUID actorId, TargetType targetType, UUID checkListId, NotificationActionType notificationActionType, LocalDateTime modifiedAt, UUID projectId, UUID projectStepId) { diff --git a/src/main/java/kr/mywork/infrastructure/member/rdb/QueryDslMemberRepository.java b/src/main/java/kr/mywork/infrastructure/member/rdb/QueryDslMemberRepository.java index cf0100c8..c33b0243 100644 --- a/src/main/java/kr/mywork/infrastructure/member/rdb/QueryDslMemberRepository.java +++ b/src/main/java/kr/mywork/infrastructure/member/rdb/QueryDslMemberRepository.java @@ -2,6 +2,7 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import kr.mywork.domain.company.service.dto.response.MemberDetailResponse; import kr.mywork.domain.member.model.Member; @@ -31,23 +32,41 @@ public Optional findByEmailAndDeletedFalse(String email) { } @Override - public List findMemberByCompanyId(UUID companyId, int page, int memberPageSize) { + public List findMemberByCompanyId(UUID companyId, int page, int memberPageSize,String memberName) { final int offset = (page - 1) * memberPageSize; return queryFactory - .selectFrom(member) - .where( - member.companyId.eq(companyId), - member.deleted.eq(false)) - .orderBy(member.name.asc()) - .offset(offset) - .limit(memberPageSize) - .fetch(); + .selectFrom(member) + .where( + member.companyId.eq(companyId), + member.deleted.eq(false), + memberNameCondition(memberName) + ) + .orderBy(member.name.asc()) + .offset(offset) + .limit(memberPageSize) + .fetch(); + } + + private BooleanExpression memberNameCondition(String memberName) { + if (memberName == null || memberName.trim().isEmpty()) { + return null; + } + return member.name.containsIgnoreCase(memberName); } @Override - public long countByCompanyIdAndDeletedFalse(UUID companyId) { - return memberRepository.countByCompanyIdAndDeletedFalse(companyId); + public long countByCompanyIdAndDeletedFalse(UUID companyId,String memberName) { + Long result = queryFactory + .select(member.count()) + .from(member) + .where( + member.companyId.eq(companyId), + member.deleted.eq(false), + memberNameCondition(memberName) // 부분 검색 조건 + ) + .fetchOne(); + return result != null ? result : 0L; } @Override diff --git a/src/main/java/kr/mywork/interfaces/company/controller/CompanyController.java b/src/main/java/kr/mywork/interfaces/company/controller/CompanyController.java index 3da32255..e57f85f3 100644 --- a/src/main/java/kr/mywork/interfaces/company/controller/CompanyController.java +++ b/src/main/java/kr/mywork/interfaces/company/controller/CompanyController.java @@ -1,19 +1,5 @@ package kr.mywork.interfaces.company.controller; -import java.util.List; -import java.util.UUID; - -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Pattern; import kr.mywork.common.api.support.response.ApiResponse; @@ -29,18 +15,15 @@ import kr.mywork.domain.member.service.dto.response.CompanyMemberResponse; import kr.mywork.interfaces.company.controller.dto.request.CompanyCreateWebRequest; import kr.mywork.interfaces.company.controller.dto.request.CompanyUpdateWebRequest; -import kr.mywork.interfaces.company.controller.dto.response.CompanyCreateWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanyDeleteWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanyDetailWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanyIdCreateWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanyListWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanyNameWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanyNamesWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanySelectWebResponse; -import kr.mywork.interfaces.company.controller.dto.response.CompanyUpdateWebResponse; +import kr.mywork.interfaces.company.controller.dto.response.*; import kr.mywork.interfaces.member.controller.dto.response.CompanyMemberListWebResponse; import kr.mywork.interfaces.member.controller.dto.response.CompanyMemberWebResponse; import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; @RestController @RequiredArgsConstructor @@ -141,11 +124,14 @@ public ApiResponse companyListOnlyIdNameWebResponseApiR } @GetMapping("/{companyId}/members") - public ApiResponse getCompanyMember(@PathVariable(name = "companyId") UUID companyId, - @RequestParam(defaultValue = "1") @Min(value = 1, message = "{invalid.page-size}") int page) { - List companyMemberResponses = memberService.findMemberByCompanyId(companyId, page); + public ApiResponse getCompanyMember( + @PathVariable(name = "companyId") UUID companyId, + @RequestParam(defaultValue = "1") @Min(value = 1, message = "{invalid.page-size}") int page, + @RequestParam(name = "memberName", required = false) final String memberName + ) { + List companyMemberResponses = memberService.findMemberByCompanyId(companyId, page,memberName); - long total = memberService.countMembersByCompanyId(companyId); + long total = memberService.countMembersByCompanyId(companyId,memberName); List companyMemberWebResponses = companyMemberResponses.stream() .map(CompanyMemberListWebResponse::fromService) diff --git a/src/main/java/kr/mywork/interfaces/member/controller/dto/response/CompanyMemberListWebResponse.java b/src/main/java/kr/mywork/interfaces/member/controller/dto/response/CompanyMemberListWebResponse.java index 48ca566d..c2033bfa 100644 --- a/src/main/java/kr/mywork/interfaces/member/controller/dto/response/CompanyMemberListWebResponse.java +++ b/src/main/java/kr/mywork/interfaces/member/controller/dto/response/CompanyMemberListWebResponse.java @@ -2,12 +2,12 @@ import kr.mywork.domain.member.service.dto.response.CompanyMemberResponse; -public record CompanyMemberListWebResponse(String id, String name, String phoneNumber, String position, String department) { +public record CompanyMemberListWebResponse(String id, String name, String phoneNumber, String position, String department, String role, String email) { /** * 서비스-레벨 DTO(CompanyMemberResponse)를 웹-레벨 DTO로 변환 */ public static CompanyMemberListWebResponse fromService(CompanyMemberResponse response) { return new CompanyMemberListWebResponse(response.id().toString(), response.name(), response.phoneNumber(), - response.position(), response.department()); + response.position(), response.department(), response.role(), response.email()); } } diff --git a/src/test/java/kr/mywork/docs/CompanyDocumentationTest.java b/src/test/java/kr/mywork/docs/CompanyDocumentationTest.java index a1f1377f..502bbb5f 100644 --- a/src/test/java/kr/mywork/docs/CompanyDocumentationTest.java +++ b/src/test/java/kr/mywork/docs/CompanyDocumentationTest.java @@ -1,19 +1,11 @@ package kr.mywork.docs; -import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; -import static com.epages.restdocs.apispec.ResourceDocumentation.headerWithName; -import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; -import static com.epages.restdocs.apispec.ResourceDocumentation.resource; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.UUID; - +import com.epages.restdocs.apispec.ResourceSnippet; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.fasterxml.uuid.Generators; +import kr.mywork.common.api.support.response.ResultType; +import kr.mywork.interfaces.company.controller.dto.request.CompanyCreateWebRequest; +import kr.mywork.interfaces.company.controller.dto.request.CompanyUpdateWebRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -23,13 +15,14 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.ResultActions; -import com.epages.restdocs.apispec.ResourceSnippet; -import com.epages.restdocs.apispec.ResourceSnippetParameters; -import com.fasterxml.uuid.Generators; +import java.util.UUID; -import kr.mywork.common.api.support.response.ResultType; -import kr.mywork.interfaces.company.controller.dto.request.CompanyCreateWebRequest; -import kr.mywork.interfaces.company.controller.dto.request.CompanyUpdateWebRequest; +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class CompanyDocumentationTest extends RestDocsDocumentation { @@ -464,6 +457,7 @@ private ResourceSnippet findAllCompanyListSuccessResource() { final ResultActions result = mockMvc.perform( get("/api/companies/{companyId}/members", id) .param("page", "1") + .param("memberName","김민수") .contentType(MediaType.APPLICATION_JSON) .header(HttpHeaders.AUTHORIZATION, toBearerAuthorizationHeader(accessToken))); @@ -483,7 +477,8 @@ private ResourceSnippet CompanyMemberGetSuccess() { .summary("회사의 직원 목록 조회 API") .description("회사의 직원 목록을 조회한다.") .queryParameters( - parameterWithName("page").description("페이지 번호")) + parameterWithName("page").description("페이지 번호"), + parameterWithName("memberName").optional().description("이름 검색어 (부분 일치 검색)")) .requestHeaders( headerWithName(HttpHeaders.CONTENT_TYPE).description("컨텐츠 타입"), headerWithName(HttpHeaders.AUTHORIZATION).description("엑세스 토큰")) @@ -495,6 +490,8 @@ private ResourceSnippet CompanyMemberGetSuccess() { fieldWithPath("data.members[].phoneNumber").type(JsonFieldType.STRING).description("멤버 전화번호"), fieldWithPath("data.members[].position").type(JsonFieldType.STRING).description("멤버 직급"), fieldWithPath("data.members[].department").type(JsonFieldType.STRING).description("멤버 부서"), + fieldWithPath("data.members[].email").type(JsonFieldType.STRING).description("멤버 이메일"), + fieldWithPath("data.members[].role").type(JsonFieldType.STRING).description("멤버 권한"), fieldWithPath("error").type(JsonFieldType.NULL).description("에러 정보")) .build() );