Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8a3f710
feat(worker): Worker
chaeliki Jul 23, 2026
d56178d
feat(worker): Worker 응답 DTO
chaeliki Jul 23, 2026
f47e23a
feat(worker): Worker 등록수정 DTO
chaeliki Jul 23, 2026
6213323
feat(worker): Doc 유형 제출 상태 Enum
chaeliki Jul 23, 2026
a88b3c2
feat(worker): WorkerDoc
chaeliki Jul 23, 2026
0a59328
feat(worker): Worker patch request 수정
chaeliki Jul 23, 2026
0566410
feat(worker): Worker 테이블 마이그레이션과 JPA 매핑
chaeliki Jul 23, 2026
059ed24
feat(worker): Worker 상태 반영 메서드 추가
chaeliki Jul 23, 2026
e5363fe
feat(worker): WorkerDocument JPA 매핑과 상태 반영 메서드 추가
chaeliki Jul 23, 2026
45c60c2
feat(worker): Worker·WorkerDocument Repository port 추가
chaeliki Jul 23, 2026
6800f68
feat(worker): Worker,WorkerDocument JPA Repository 구현
chaeliki Jul 23, 2026
3257bcb
feat(worker): Worker 등록 Application Service 추가
chaeliki Jul 23, 2026
92ff74a
feat(worker): Worker 조회,수정 Application Service 추가
chaeliki Jul 23, 2026
505e01e
feat(worker): WorkerDocument 등록,수정 Application Service 추가
chaeliki Jul 23, 2026
3e8da6f
feat(worker): WorkerController 등록,조회,수정 API 추가
chaeliki Jul 23, 2026
dcd7c67
feat(worker): WorkerDocument 등록,수정,응답 DTO
chaeliki Jul 23, 2026
d8d5f19
feat(worker): WorkerDocumentController 등록,수정 API 추가
chaeliki Jul 23, 2026
0ed4d54
fix(worker): update()가 flush 후 최신 버전을 반환하도록 수정
chaeliki Jul 23, 2026
8f45cf8
test(worker): Worker 등록,조회,수정 통합 테스트 추가
chaeliki Jul 23, 2026
4feb4ef
test(worker): WorkerDocument 등록,수정 통합 테스트 추가
chaeliki Jul 23, 2026
be90ff1
refactor(worker): visa_expiry_date를 stay_expiry_date로 변경
chaeliki Jul 24, 2026
94d9e03
merge: main의 ADR 문서 갱신 반영
chaeliki Jul 24, 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
167 changes: 167 additions & 0 deletions src/main/java/com/fowoco/server/worker/api/WorkerController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.fowoco.server.worker.api;

import com.fowoco.server.auth.application.port.ActorContextProvider;
import com.fowoco.server.worker.application.WorkerCreateCommand;
import com.fowoco.server.worker.application.WorkerPatchCommand;
import com.fowoco.server.worker.application.WorkerService;
import com.fowoco.server.worker.domain.Worker;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
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.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.RestController;

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

private final WorkerService workerService;
private final ActorContextProvider actorContextProvider;

public WorkerController(
WorkerService workerService,
ActorContextProvider actorContextProvider
) {
this.workerService = workerService;
this.actorContextProvider = actorContextProvider;
}

@Operation(
operationId = "registerWorker",
summary = "근로자 등록",
description = "업무에 필요한 최소 개인정보로 근로자를 등록합니다. "
+ "여권번호·외국인등록번호·전화번호·계좌번호는 이 API로 수집하지 않습니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "201",
description = "등록 성공",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = WorkerResponse.class)
)
),
@ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"),
@ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"),
@ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden")
})
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyRole('ADMIN', 'HR')")
public ResponseEntity<WorkerResponse> register(@Valid @RequestBody WorkerCreateRequest request) {
UUID companyId = actorContextProvider.requireCurrentActor().companyId();
WorkerCreateCommand command = new WorkerCreateCommand(
companyId,
request.getDisplayName(),
request.getNationalityCode(),
request.getPreferredLanguage(),
request.getStayExpiryDate(),
request.getContractStartDate(),
request.getContractEndDate()
);
Worker worker = workerService.register(command);
return ResponseEntity.status(HttpStatus.CREATED).body(WorkerResponse.from(worker));
}

@Operation(
operationId = "getWorkerDetail",
summary = "근로자 상세 조회",
description = "근로자 기본정보와 체류·계약기간, 서류 상태 요약을 조회합니다. "
+ "타 사업장 소속 근로자 ID는 404로 응답합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = WorkerResponse.class)
)
),
@ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"),
@ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"),
@ApiResponse(responseCode = "404", ref = "#/components/responses/NotFound")
})
@GetMapping(path = "/{workerId}", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyRole('ADMIN', 'HR', 'VIEWER')")
public WorkerResponse getDetail(
@Parameter(description = "근로자 ID") @PathVariable UUID workerId
) {
UUID companyId = actorContextProvider.requireCurrentActor().companyId();
Worker worker = workerService.findDetail(workerId, companyId);
return WorkerResponse.from(worker);
}

@Operation(
operationId = "patchWorker",
summary = "근로자 수정",
description = "근무상태·언어·체류일·계약기간을 부분 수정합니다. "
+ "expected_version이 현재 값과 다르면 409로 응답합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "수정 성공",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = WorkerResponse.class)
)
),
@ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"),
@ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"),
@ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"),
@ApiResponse(responseCode = "404", ref = "#/components/responses/NotFound"),
@ApiResponse(
responseCode = "409",
description = "expected_version이 현재 값과 달라 다른 사용자의 수정과 충돌함. "
+ "OpenApiConfig에 공통 Conflict 응답이 아직 없어 인라인으로 정의함",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = com.fowoco.server.common.error.ApiErrorResponse.class)
)
)
})
@PatchMapping(
path = "/{workerId}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
@PreAuthorize("hasAnyRole('ADMIN', 'HR')")
public WorkerResponse patch(
@Parameter(description = "근로자 ID") @PathVariable UUID workerId,
@Valid @RequestBody WorkerPatchRequest request
) {
UUID companyId = actorContextProvider.requireCurrentActor().companyId();
WorkerPatchCommand command = new WorkerPatchCommand(
workerId,
companyId,
request.getDisplayName(),
request.getNationalityCode(),
request.getPreferredLanguage(),
request.getWorkStatus(),
request.getStayExpiryDate(),
request.getContractStartDate(),
request.getContractEndDate(),
request.getExpectedVersion()
);
Worker worker = workerService.patch(command);
return WorkerResponse.from(worker);
}
}
113 changes: 113 additions & 0 deletions src/main/java/com/fowoco/server/worker/api/WorkerCreateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.fowoco.server.worker.api;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;

@Schema(
name = "WorkerCreateRequest",
description = "근로자 등록 요청. 여권번호·외국인등록번호·전화번호·계좌번호는 이 API로 수집하지 않습니다."
)
public final class WorkerCreateRequest {

@Schema(
description = "화면 표시용 근로자 이름",
example = "응웬반A",
maxLength = 120,
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotBlank(message = "표시 이름을 입력해 주세요.")
@Size(max = 120, message = "표시 이름은 120자 이하여야 합니다.")
private final String displayName;

@Schema(
description = "국적 코드",
example = "VN",
maxLength = 10
)
@Size(max = 10, message = "국적 코드는 10자 이하여야 합니다.")
private final String nationalityCode;

@Schema(
description = "선호 언어",
example = "vi",
maxLength = 20
)
@Size(max = 20, message = "선호 언어는 20자 이하여야 합니다.")
private final String preferredLanguage;

@Schema(
description = "체류 만료일",
example = "2027-03-01",
format = "date"
)
private final LocalDate stayExpiryDate;

@Schema(
description = "계약 시작일",
example = "2026-01-01",
format = "date"
)
private final LocalDate contractStartDate;

@Schema(
description = "계약 종료일. contract_start_date보다 빠를 수 없습니다.",
example = "2027-12-31",
format = "date"
)
private final LocalDate contractEndDate;

@JsonCreator
public WorkerCreateRequest(
@JsonProperty("display_name") String displayName,
@JsonProperty("nationality_code") String nationalityCode,
@JsonProperty("preferred_language") String preferredLanguage,
@JsonProperty("stay_expiry_date") LocalDate stayExpiryDate,
@JsonProperty("contract_start_date") LocalDate contractStartDate,
@JsonProperty("contract_end_date") LocalDate contractEndDate
) {
this.displayName = displayName;
this.nationalityCode = nationalityCode;
this.preferredLanguage = preferredLanguage;
this.stayExpiryDate = stayExpiryDate;
this.contractStartDate = contractStartDate;
this.contractEndDate = contractEndDate;
}

@AssertTrue(message = "contract_end_date는 contract_start_date보다 빠를 수 없습니다.")
@Schema(hidden = true)
public boolean isContractPeriodValid() {
if (contractStartDate == null || contractEndDate == null) {
return true;
}
return !contractEndDate.isBefore(contractStartDate);
}

public String getDisplayName() {
return displayName;
}

public String getNationalityCode() {
return nationalityCode;
}

public String getPreferredLanguage() {
return preferredLanguage;
}

public LocalDate getStayExpiryDate() {
return stayExpiryDate;
}

public LocalDate getContractStartDate() {
return contractStartDate;
}

public LocalDate getContractEndDate() {
return contractEndDate;
}
}
Loading