diff --git a/src/main/java/com/fowoco/server/demo/infrastructure/seed/DemoWorkerDocumentSeeder.java b/src/main/java/com/fowoco/server/demo/infrastructure/seed/DemoWorkerDocumentSeeder.java index ad09664..6c8e035 100644 --- a/src/main/java/com/fowoco/server/demo/infrastructure/seed/DemoWorkerDocumentSeeder.java +++ b/src/main/java/com/fowoco/server/demo/infrastructure/seed/DemoWorkerDocumentSeeder.java @@ -36,6 +36,7 @@ void seed(DocumentSeed seed, DemoOperationalSeedContext context) { seed.documentId(), seed.workerId(), context.companyId(), + null, seed.documentType(), seed.submissionStatus(), expiryDate, diff --git a/src/main/java/com/fowoco/server/document/api/DocumentController.java b/src/main/java/com/fowoco/server/document/api/DocumentController.java index 0bd8be4..56bffc4 100644 --- a/src/main/java/com/fowoco/server/document/api/DocumentController.java +++ b/src/main/java/com/fowoco/server/document/api/DocumentController.java @@ -68,6 +68,7 @@ public DocumentController( @PreAuthorize("hasAnyRole('ADMIN', 'HR', 'VIEWER')") public DocumentPageResponse list( @Parameter(description = "근로자 ID 필터") @RequestParam(required = false) UUID workerId, + @Parameter(description = "업무 ID 필터") @RequestParam(required = false) UUID taskId, @Parameter(description = "서류 유형 필터") @RequestParam(required = false) DocumentType documentType, @Parameter(description = "제출 상태 필터") @RequestParam(required = false) SubmissionStatus status, @Parameter(description = "이 날짜 이전 만료 필터") @RequestParam(required = false) LocalDate expiryBefore, @@ -79,6 +80,7 @@ public DocumentPageResponse list( ActorContext actor = actorContextProvider.requireCurrentActor(); WorkerDocumentSearchQuery query = new WorkerDocumentSearchQuery( workerId, + taskId, documentType, status, expiryBefore, diff --git a/src/main/java/com/fowoco/server/document/application/DocumentReadinessService.java b/src/main/java/com/fowoco/server/document/application/DocumentReadinessService.java index 88bcb4d..6879b68 100644 --- a/src/main/java/com/fowoco/server/document/application/DocumentReadinessService.java +++ b/src/main/java/com/fowoco/server/document/application/DocumentReadinessService.java @@ -56,7 +56,7 @@ public DocumentReadinessResult calculate(UUID taskId, ActorContext actor) { LocalDate today = LocalDate.now(clock); WorkerDocumentSearchQuery allDocumentsQuery = new WorkerDocumentSearchQuery( - task.workerId(), null, null, null, 0, 100 + task.workerId(), null, null, null, null, 0, 100 ); List workerDocuments = workerDocumentRepository.findPage(companyId, allDocumentsQuery); diff --git a/src/main/java/com/fowoco/server/worker/api/WorkerDocumentController.java b/src/main/java/com/fowoco/server/worker/api/WorkerDocumentController.java index 88d793e..4aecdd4 100644 --- a/src/main/java/com/fowoco/server/worker/api/WorkerDocumentController.java +++ b/src/main/java/com/fowoco/server/worker/api/WorkerDocumentController.java @@ -74,6 +74,7 @@ public ResponseEntity register( ActorContext actor = actorContextProvider.requireCurrentActor(); WorkerDocumentCreateCommand command = new WorkerDocumentCreateCommand( workerId, + request.getTaskId(), request.getDocumentType(), request.getSubmissionStatus(), request.getExpiryDate(), diff --git a/src/main/java/com/fowoco/server/worker/api/WorkerDocumentCreateRequest.java b/src/main/java/com/fowoco/server/worker/api/WorkerDocumentCreateRequest.java index 169dd63..35f7dbf 100644 --- a/src/main/java/com/fowoco/server/worker/api/WorkerDocumentCreateRequest.java +++ b/src/main/java/com/fowoco/server/worker/api/WorkerDocumentCreateRequest.java @@ -8,6 +8,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.time.LocalDate; +import java.util.UUID; @Schema( name = "WorkerDocumentCreateRequest", @@ -44,19 +45,24 @@ public final class WorkerDocumentCreateRequest { @Size(max = 500, message = "note는 500자 이하여야 합니다.") private final String note; + @Schema(name = "task_id", description = "연결할 업무카드 ID (선택)", format = "uuid") + private final UUID taskId; + @JsonCreator public WorkerDocumentCreateRequest( @JsonProperty("document_type") DocumentType documentType, @JsonProperty("submission_status") SubmissionStatus submissionStatus, @JsonProperty("expiry_date") LocalDate expiryDate, @JsonProperty("destination") String destination, - @JsonProperty("note") String note + @JsonProperty("note") String note, + @JsonProperty("task_id") UUID taskId ) { this.documentType = documentType; this.submissionStatus = submissionStatus; this.expiryDate = expiryDate; this.destination = destination; this.note = note; + this.taskId = taskId; } public DocumentType getDocumentType() { @@ -78,4 +84,8 @@ public String getDestination() { public String getNote() { return note; } + + public UUID getTaskId() { + return taskId; + } } diff --git a/src/main/java/com/fowoco/server/worker/application/WorkerDocumentCreateCommand.java b/src/main/java/com/fowoco/server/worker/application/WorkerDocumentCreateCommand.java index 45d98f4..b1b5110 100644 --- a/src/main/java/com/fowoco/server/worker/application/WorkerDocumentCreateCommand.java +++ b/src/main/java/com/fowoco/server/worker/application/WorkerDocumentCreateCommand.java @@ -8,6 +8,7 @@ public final class WorkerDocumentCreateCommand { private final UUID workerId; + private final UUID taskId; private final DocumentType documentType; private final SubmissionStatus submissionStatus; private final LocalDate expiryDate; @@ -16,6 +17,7 @@ public final class WorkerDocumentCreateCommand { public WorkerDocumentCreateCommand( UUID workerId, + UUID taskId, DocumentType documentType, SubmissionStatus submissionStatus, LocalDate expiryDate, @@ -23,6 +25,7 @@ public WorkerDocumentCreateCommand( String note ) { this.workerId = workerId; + this.taskId = taskId; this.documentType = documentType; this.submissionStatus = submissionStatus; this.expiryDate = expiryDate; @@ -34,6 +37,10 @@ public UUID workerId() { return workerId; } + public UUID taskId() { + return taskId; + } + public DocumentType documentType() { return documentType; } diff --git a/src/main/java/com/fowoco/server/worker/application/WorkerDocumentSearchQuery.java b/src/main/java/com/fowoco/server/worker/application/WorkerDocumentSearchQuery.java index 887fb4e..56185b5 100644 --- a/src/main/java/com/fowoco/server/worker/application/WorkerDocumentSearchQuery.java +++ b/src/main/java/com/fowoco/server/worker/application/WorkerDocumentSearchQuery.java @@ -11,6 +11,7 @@ public final class WorkerDocumentSearchQuery { private static final int MAX_SIZE = 100; private final UUID workerId; + private final UUID taskId; private final DocumentType documentType; private final SubmissionStatus status; private final LocalDate expiryBefore; @@ -19,6 +20,7 @@ public final class WorkerDocumentSearchQuery { public WorkerDocumentSearchQuery( UUID workerId, + UUID taskId, DocumentType documentType, SubmissionStatus status, LocalDate expiryBefore, @@ -26,6 +28,7 @@ public WorkerDocumentSearchQuery( Integer size ) { this.workerId = workerId; + this.taskId = taskId; this.documentType = documentType; this.status = status; this.expiryBefore = expiryBefore; @@ -43,6 +46,10 @@ public UUID workerId() { return workerId; } + public UUID taskId() { + return taskId; + } + public DocumentType documentType() { return documentType; } diff --git a/src/main/java/com/fowoco/server/worker/application/WorkerDocumentService.java b/src/main/java/com/fowoco/server/worker/application/WorkerDocumentService.java index f6f516b..0593bbc 100644 --- a/src/main/java/com/fowoco/server/worker/application/WorkerDocumentService.java +++ b/src/main/java/com/fowoco/server/worker/application/WorkerDocumentService.java @@ -65,6 +65,7 @@ public WorkerDocument register(WorkerDocumentCreateCommand command, ActorContext uuidGenerator.generate(), command.workerId(), actor.companyId(), + command.taskId(), command.documentType(), command.submissionStatus(), command.expiryDate(), @@ -115,6 +116,7 @@ public WorkerDocument patch( existing.workerDocumentId(), existing.workerId(), existing.companyId(), + existing.taskId(), orElseKeep(command.documentType(), existing.documentType()), orElseKeep(command.submissionStatus(), existing.submissionStatus()), orElseKeep(command.expiryDate(), existing.expiryDate()), diff --git a/src/main/java/com/fowoco/server/worker/domain/WorkerDocument.java b/src/main/java/com/fowoco/server/worker/domain/WorkerDocument.java index e7b81a2..f0bbe41 100644 --- a/src/main/java/com/fowoco/server/worker/domain/WorkerDocument.java +++ b/src/main/java/com/fowoco/server/worker/domain/WorkerDocument.java @@ -13,6 +13,7 @@ public final class WorkerDocument { private final UUID workerDocumentId; private final UUID workerId; private final UUID companyId; + private final UUID taskId; private final DocumentType documentType; private final SubmissionStatus submissionStatus; private final LocalDate expiryDate; @@ -27,6 +28,7 @@ public WorkerDocument( UUID workerDocumentId, UUID workerId, UUID companyId, + UUID taskId, DocumentType documentType, SubmissionStatus submissionStatus, LocalDate expiryDate, @@ -40,6 +42,7 @@ public WorkerDocument( this.workerDocumentId = Objects.requireNonNull(workerDocumentId, "workerDocumentId must not be null"); this.workerId = Objects.requireNonNull(workerId, "workerId must not be null"); this.companyId = Objects.requireNonNull(companyId, "companyId must not be null"); + this.taskId = taskId; this.documentType = Objects.requireNonNull(documentType, "documentType must not be null"); this.submissionStatus = Objects.requireNonNull(submissionStatus, "submissionStatus must not be null"); this.expiryDate = expiryDate; @@ -61,6 +64,7 @@ public static WorkerDocument create( UUID workerDocumentId, UUID workerId, UUID companyId, + UUID taskId, DocumentType documentType, SubmissionStatus submissionStatus, LocalDate expiryDate, @@ -73,6 +77,7 @@ public static WorkerDocument create( workerDocumentId, workerId, companyId, + taskId, documentType, submissionStatus, expiryDate, @@ -97,6 +102,10 @@ public UUID companyId() { return companyId; } + public UUID taskId() { + return taskId; + } + public DocumentType documentType() { return documentType; } diff --git a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerDocumentRepository.java b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerDocumentRepository.java index 6f80fc6..56be632 100644 --- a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerDocumentRepository.java +++ b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerDocumentRepository.java @@ -103,6 +103,9 @@ private String buildWhereClause(WorkerDocumentSearchQuery query) { if (query.workerId() != null) { where.append(" and document.workerId = :workerId"); } + if (query.taskId() != null) { + where.append(" and document.taskId = :taskId"); + } if (query.documentType() != null) { where.append(" and document.documentType = :documentType"); } @@ -120,6 +123,9 @@ private void bindParameters(Query jpaQuery, UUID companyId, WorkerDocumentSearch if (query.workerId() != null) { jpaQuery.setParameter("workerId", query.workerId()); } + if (query.taskId() != null) { + jpaQuery.setParameter("taskId", query.taskId()); + } if (query.documentType() != null) { jpaQuery.setParameter("documentType", query.documentType()); } diff --git a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/WorkerDocumentJpaEntity.java b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/WorkerDocumentJpaEntity.java index 9cf2385..dfd67c4 100644 --- a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/WorkerDocumentJpaEntity.java +++ b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/WorkerDocumentJpaEntity.java @@ -29,6 +29,9 @@ public class WorkerDocumentJpaEntity { @Column(name = "company_id", nullable = false, updatable = false) private UUID companyId; + @Column(name = "task_id") + private UUID taskId; + @Enumerated(EnumType.STRING) @Column(name = "document_type", nullable = false, length = 40) private DocumentType documentType; @@ -66,6 +69,7 @@ private WorkerDocumentJpaEntity( UUID workerDocumentId, UUID workerId, UUID companyId, + UUID taskId, DocumentType documentType, SubmissionStatus submissionStatus, LocalDate expiryDate, @@ -79,6 +83,7 @@ private WorkerDocumentJpaEntity( this.workerDocumentId = workerDocumentId; this.workerId = workerId; this.companyId = companyId; + this.taskId = taskId; this.documentType = documentType; this.submissionStatus = submissionStatus; this.expiryDate = expiryDate; @@ -96,6 +101,7 @@ public static WorkerDocumentJpaEntity fromDomain(WorkerDocument document) { document.workerDocumentId(), document.workerId(), document.companyId(), + document.taskId(), document.documentType(), document.submissionStatus(), document.expiryDate(), @@ -113,6 +119,7 @@ public WorkerDocument toDomain() { workerDocumentId, workerId, companyId, + taskId, documentType, submissionStatus, expiryDate, diff --git a/src/main/resources/db/migration/V15__add_worker_document_task_id.sql b/src/main/resources/db/migration/V15__add_worker_document_task_id.sql new file mode 100644 index 0000000..d3f86cf --- /dev/null +++ b/src/main/resources/db/migration/V15__add_worker_document_task_id.sql @@ -0,0 +1,9 @@ +ALTER TABLE worker_document + ADD COLUMN task_id UUID; + +ALTER TABLE worker_document + ADD CONSTRAINT fk_worker_document_task_company + FOREIGN KEY (task_id, company_id) + REFERENCES task (task_id, company_id) ON DELETE RESTRICT; + +CREATE INDEX idx_worker_document_task ON worker_document (task_id, company_id); diff --git a/src/test/java/com/fowoco/server/worker/WorkerDocumentSecurityIntegrationTest.java b/src/test/java/com/fowoco/server/worker/WorkerDocumentSecurityIntegrationTest.java index 299e7c2..48c87c3 100644 --- a/src/test/java/com/fowoco/server/worker/WorkerDocumentSecurityIntegrationTest.java +++ b/src/test/java/com/fowoco/server/worker/WorkerDocumentSecurityIntegrationTest.java @@ -73,6 +73,12 @@ void seedCompaniesAndUsers() { @BeforeEach void resetWorkerDocumentState() throws Exception { jdbcTemplate.update("DELETE FROM worker_document"); + jdbcTemplate.update("DELETE FROM task_checklist_item"); + jdbcTemplate.update("DELETE FROM task_transition_history"); + jdbcTemplate.update("DELETE FROM approval_request"); + jdbcTemplate.update("DELETE FROM external_submission"); + jdbcTemplate.update("DELETE FROM task_evidence"); + jdbcTemplate.update("DELETE FROM task"); jdbcTemplate.update("DELETE FROM worker"); workerIdInCompanyA = registerWorker(accessToken(login(HR_A_EMAIL)), "서류테스트근로자"); } @@ -151,6 +157,24 @@ void patchWithStaleExpectedVersionReturnsConflict() throws Exception { .isEqualTo("WORKER_DOCUMENT_VERSION_CONFLICT"); } + @Test + void listDocumentsFiltersByTaskId() throws Exception { + String accessToken = accessToken(login(HR_A_EMAIL)); + String taskId = createTask(accessToken, workerIdInCompanyA); + String documentIdWithTask = registerDocument(accessToken, workerIdInCompanyA, taskId); + String documentIdWithoutTask = registerDocument(accessToken, workerIdInCompanyA, null); + + HttpResponse response = getJson( + "/api/v1/documents?taskId=" + taskId, + accessToken + ); + + assertThat(response.statusCode()).isEqualTo(200); + java.util.List ids = JsonPath.read(response.body(), "$.items[*].worker_document_id"); + assertThat(ids).contains(documentIdWithTask); + assertThat(ids).doesNotContain(documentIdWithoutTask); + } + @Test void documentFromAnotherCompanyIsReturnedAsNotFoundOnPatch() throws Exception { String companyAToken = accessToken(login(HR_A_EMAIL)); @@ -210,6 +234,36 @@ private String registerDocument(String accessToken, String workerId) throws Exce return JsonPath.read(response.body(), "$.worker_document_id"); } + private String registerDocument(String accessToken, String workerId, String taskId) throws Exception { + String body = """ + {"document_type": "PASSPORT_COPY", "submission_status": "MISSING", "task_id": %s} + """.formatted(taskId == null ? "null" : "\"" + taskId + "\""); + HttpResponse response = postJson( + "/api/v1/workers/" + workerId + "/documents", + body, + accessToken + ); + assertThat(response.statusCode()).isEqualTo(201); + return JsonPath.read(response.body(), "$.worker_document_id"); + } + + private String createTask(String accessToken, String workerId) throws Exception { + String body = """ + { + "worker_id":"%s", + "task_type":"RECONTRACT", + "workflow_id":"WF-CON-001", + "title":"필터 테스트 업무", + "description":"taskId 필터 검증용", + "due_date":"2026-12-31", + "business_data":{} + } + """.formatted(workerId); + HttpResponse response = postJson("/api/v1/tasks", body, accessToken); + assertThat(response.statusCode()).isEqualTo(201); + return JsonPath.read(response.body(), "$.task_id"); + } + private void insertCompany(UUID companyId, String name) { jdbcTemplate.update( """ @@ -257,6 +311,14 @@ private HttpResponse patchJson(String path, String body, String accessTo return sendJson(path, body, accessToken, "PATCH"); } + private HttpResponse getJson(String path, String accessToken) throws Exception { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri(path)).GET(); + if (accessToken != null) { + requestBuilder.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + } + return httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + } + private HttpResponse sendJson(String path, String body, String accessToken, String method) throws Exception { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri(path))