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
46 changes: 46 additions & 0 deletions src/main/java/com/fowoco/server/worker/api/WorkerController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import com.fowoco.server.auth.application.port.ActorContextProvider;
import com.fowoco.server.worker.application.WorkerCreateCommand;
import com.fowoco.server.worker.application.WorkerPageResult;
import com.fowoco.server.worker.application.WorkerPatchCommand;
import com.fowoco.server.worker.application.WorkerSearchQuery;
import com.fowoco.server.worker.application.WorkerService;
import com.fowoco.server.worker.domain.Worker;
import com.fowoco.server.worker.domain.WorkerStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -14,23 +17,30 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
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;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Worker", description = "근로자 기본정보 등록·조회·수정")
@RestController
@RequestMapping("/api/v1/workers")
@SecurityRequirement(name = "bearerAuth")
@Validated
public class WorkerController {

private final WorkerService workerService;
Expand All @@ -44,6 +54,42 @@ public WorkerController(
this.actorContextProvider = actorContextProvider;
}

@Operation(
operationId = "listWorkers",
summary = "근로자 목록 조회",
description = "JWT 사업장에 소속된 근로자 요약 목록을 페이지로 조회합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = WorkerPageResponse.class)
)
),
@ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"),
@ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"),
@ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden")
})
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyRole('ADMIN', 'HR', 'VIEWER')")
public WorkerPageResponse list(
@Parameter(description = "근무 상태 필터") @RequestParam(required = false) WorkerStatus status,
@Parameter(description = "선호 언어 필터") @RequestParam(required = false) String language,
@Parameter(description = "체류 만료일 이전 필터") @RequestParam(required = false) LocalDate expiryBefore,
@Parameter(description = "페이지 번호 (0부터 시작)")
@RequestParam(defaultValue = "0") @Min(0) int page,
@Parameter(description = "페이지당 항목 수 (1~100)")
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
) {
UUID companyId = actorContextProvider.requireCurrentActor().companyId();
WorkerSearchQuery query = new WorkerSearchQuery(status, language, expiryBefore, page, size);
WorkerPageResult result = workerService.findPage(companyId, query);
List<WorkerResponse> items = result.items().stream().map(WorkerResponse::from).toList();
return new WorkerPageResponse(items, result.page(), result.size(), result.totalElements());
}

@Operation(
operationId = "registerWorker",
summary = "근로자 등록",
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/com/fowoco/server/worker/api/WorkerPageResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.fowoco.server.worker.api;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

@Schema(name = "WorkerPageResponse", description = "근로자 목록 페이지 응답")
public final class WorkerPageResponse {

@JsonProperty("items")
@Schema(description = "이번 페이지의 근로자 목록", requiredMode = Schema.RequiredMode.REQUIRED)
private final List<WorkerResponse> items;

@JsonProperty("page")
@Schema(description = "현재 페이지 번호 (0부터 시작)", example = "0", requiredMode = Schema.RequiredMode.REQUIRED)
private final int page;

@JsonProperty("size")
@Schema(description = "페이지당 항목 수", example = "20", requiredMode = Schema.RequiredMode.REQUIRED)
private final int size;

@JsonProperty("total_elements")
@Schema(name = "total_elements", description = "전체 근로자 수", example = "42", requiredMode = Schema.RequiredMode.REQUIRED)
private final long totalElements;

public WorkerPageResponse(List<WorkerResponse> items, int page, int size, long totalElements) {
this.items = items;
this.page = page;
this.size = size;
this.totalElements = totalElements;
}

public List<WorkerResponse> getItems() {
return items;
}

public int getPage() {
return page;
}

public int getSize() {
return size;
}

public long getTotalElements() {
return totalElements;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.fowoco.server.worker.application;

import com.fowoco.server.worker.domain.Worker;
import java.util.List;

public record WorkerPageResult(List<Worker> items, int page, int size, long totalElements) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.fowoco.server.worker.application;

import com.fowoco.server.worker.domain.WorkerStatus;
import java.time.LocalDate;

public final class WorkerSearchQuery {

private static final int MIN_SIZE = 1;
private static final int MAX_SIZE = 100;

private final WorkerStatus status;
private final String language;
private final LocalDate expiryBefore;
private final int page;
private final int size;

public WorkerSearchQuery(
WorkerStatus status,
String language,
LocalDate expiryBefore,
Integer page,
Integer size
) {
this.status = status;
this.language = language;
this.expiryBefore = expiryBefore;
this.page = page == null ? 0 : page;
this.size = size == null ? 20 : size;
if (this.page < 0) {
throw new IllegalArgumentException("page must not be negative");
}
if (this.size < MIN_SIZE || this.size > MAX_SIZE) {
throw new IllegalArgumentException("size must be between " + MIN_SIZE + " and " + MAX_SIZE);
}
}

public WorkerStatus status() {
return status;
}

public String language() {
return language;
}

public LocalDate expiryBefore() {
return expiryBefore;
}

public int page() {
return page;
}

public int size() {
return size;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fowoco.server.worker.application.port.WorkerRepository;
import com.fowoco.server.worker.domain.Worker;
import java.time.Clock;
import java.util.List;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -50,6 +51,13 @@ public Worker findDetail(UUID workerId, UUID companyId) {
.orElseThrow(() -> new ApiException(WorkerErrorCode.WORKER_NOT_FOUND));
}

@Transactional(readOnly = true)
public WorkerPageResult findPage(UUID companyId, WorkerSearchQuery query) {
List<Worker> items = workerRepository.findPage(companyId, query);
long totalElements = workerRepository.countPage(companyId, query);
return new WorkerPageResult(items, query.page(), query.size(), totalElements);
}

@Transactional
public Worker patch(WorkerPatchCommand command) {
Worker existing = findDetail(command.workerId(), command.companyId());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.fowoco.server.worker.application.port;

import com.fowoco.server.worker.application.WorkerSearchQuery;
import com.fowoco.server.worker.domain.Worker;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

Expand All @@ -10,5 +12,9 @@ public interface WorkerRepository {

Optional<Worker> findByWorkerIdAndCompanyId(UUID workerId, UUID companyId);

Worker update(Worker worker);
Worker update(Worker worker);

List<Worker> findPage(UUID companyId, WorkerSearchQuery query);

long countPage(UUID companyId, WorkerSearchQuery query);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.fowoco.server.worker.infrastructure.persistence;

import com.fowoco.server.worker.application.WorkerSearchQuery;
import com.fowoco.server.worker.application.port.WorkerRepository;
import com.fowoco.server.worker.domain.Worker;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
Expand Down Expand Up @@ -55,4 +58,65 @@ public Worker update(Worker worker) {
entityManager.flush();
return entity.toDomain();
}

@Override
public List<Worker> findPage(UUID companyId, WorkerSearchQuery query) {
Objects.requireNonNull(companyId, "companyId must not be null");
Objects.requireNonNull(query, "query must not be null");
TypedQuery<WorkerJpaEntity> jpaQuery = entityManager.createQuery(
buildWhereClause(query) + " order by worker.createdAt desc",
WorkerJpaEntity.class
);
bindParameters(jpaQuery, companyId, query);
return jpaQuery
.setFirstResult(query.page() * query.size())
.setMaxResults(query.size())
.getResultList()
.stream()
.map(WorkerJpaEntity::toDomain)
.toList();
}

@Override
public long countPage(UUID companyId, WorkerSearchQuery query) {
Objects.requireNonNull(companyId, "companyId must not be null");
Objects.requireNonNull(query, "query must not be null");
TypedQuery<Long> jpaQuery = entityManager.createQuery(
"select count(worker) " + buildWhereClause(query).replaceFirst("^select worker ", ""),
Long.class
);
bindParameters(jpaQuery, companyId, query);
return jpaQuery.getSingleResult();
}

private String buildWhereClause(WorkerSearchQuery query) {
StringBuilder jpql = new StringBuilder("select worker from WorkerJpaEntity worker where worker.companyId = :companyId");
if (query.status() != null) {
jpql.append(" and worker.workStatus = :status");
}
if (query.language() != null) {
jpql.append(" and worker.preferredLanguage = :language");
}
if (query.expiryBefore() != null) {
jpql.append(" and worker.stayExpiryDate < :expiryBefore");
}
return jpql.toString();
}

private void bindParameters(
jakarta.persistence.Query jpaQuery,
UUID companyId,
WorkerSearchQuery query
) {
jpaQuery.setParameter("companyId", companyId);
if (query.status() != null) {
jpaQuery.setParameter("status", query.status());
}
if (query.language() != null) {
jpaQuery.setParameter("language", query.language());
}
if (query.expiryBefore() != null) {
jpaQuery.setParameter("expiryBefore", query.expiryBefore());
}
}
}
Loading