Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fowoco.server.auth.application.ActorContext;
import com.fowoco.server.auth.application.port.ActorContextProvider;
import com.fowoco.server.document.application.DocumentDetailResult;
import com.fowoco.server.document.application.DocumentPageResult;
import com.fowoco.server.document.application.DocumentService;
import com.fowoco.server.worker.application.WorkerDocumentSearchQuery;
Expand All @@ -23,6 +24,7 @@
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down Expand Up @@ -96,4 +98,30 @@ public DocumentPageResponse list(
.toList();
return new DocumentPageResponse(items, result.page(), result.size(), result.totalElements());
}

@Operation(
operationId = "getDocument",
summary = "서류 단건 상세 조회",
description = "연결된 파일 정보와 expected_version 기준값을 함께 반환합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = DocumentDetailResponse.class)
)
),
@ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"),
@ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"),
@ApiResponse(responseCode = "404", ref = "#/components/responses/NotFound")
})
@GetMapping(path = "/{documentId}", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyRole('ADMIN', 'HR', 'VIEWER')")
public DocumentDetailResponse findById(@Parameter(description = "서류 ID") @PathVariable UUID documentId) {
ActorContext actor = actorContextProvider.requireCurrentActor();
DocumentDetailResult result = documentService.findById(documentId, actor);
return DocumentDetailResponse.from(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package com.fowoco.server.document.api;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fowoco.server.document.application.DocumentDetailResult;
import com.fowoco.server.file.domain.ScanStatus;
import com.fowoco.server.worker.domain.DocumentType;
import com.fowoco.server.worker.domain.SubmissionStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.util.UUID;

@Schema(name = "DocumentDetailResponse", description = "서류 단건 상세 응답 (연결된 파일 정보 포함)")
public final class DocumentDetailResponse {

@JsonProperty("worker_document_id")
@Schema(name = "worker_document_id", format = "uuid", requiredMode = Schema.RequiredMode.REQUIRED)
private final UUID workerDocumentId;

@JsonProperty("worker_id")
@Schema(name = "worker_id", format = "uuid", requiredMode = Schema.RequiredMode.REQUIRED)
private final UUID workerId;

@JsonProperty("task_id")
@Schema(name = "task_id", format = "uuid", description = "연결된 업무카드 ID")
private final UUID taskId;

@JsonProperty("display_name")
@Schema(name = "display_name", description = "근로자 화면 표시 이름")
private final String displayName;

@JsonProperty("document_type")
@Schema(name = "document_type", requiredMode = Schema.RequiredMode.REQUIRED)
private final DocumentType documentType;

@JsonProperty("submission_status")
@Schema(name = "submission_status", requiredMode = Schema.RequiredMode.REQUIRED)
private final SubmissionStatus submissionStatus;

@JsonProperty("expiry_date")
@Schema(name = "expiry_date", format = "date")
private final LocalDate expiryDate;

@JsonProperty("version")
@Schema(name = "version", description = "PATCH 요청의 expected_version 기준값", requiredMode = Schema.RequiredMode.REQUIRED)
private final long version;

@JsonProperty("file_id")
@Schema(name = "file_id", format = "uuid")
private final UUID fileId;

@JsonProperty("file_name")
@Schema(name = "file_name", description = "연결된 파일의 표시 파일명 (파일 없으면 null)")
private final String fileName;

@JsonProperty("file_mime_type")
@Schema(name = "file_mime_type", description = "연결된 파일의 MIME 타입 (파일 없으면 null)")
private final String fileMimeType;

@JsonProperty("file_size")
@Schema(name = "file_size", description = "연결된 파일의 크기(byte) (파일 없으면 null)")
private final Long fileSize;

@JsonProperty("file_scan_status")
@Schema(name = "file_scan_status", description = "연결된 파일의 검사 상태 (파일 없으면 null)")
private final ScanStatus fileScanStatus;

private DocumentDetailResponse(
UUID workerDocumentId,
UUID workerId,
UUID taskId,
String displayName,
DocumentType documentType,
SubmissionStatus submissionStatus,
LocalDate expiryDate,
long version,
UUID fileId,
String fileName,
String fileMimeType,
Long fileSize,
ScanStatus fileScanStatus
) {
this.workerDocumentId = workerDocumentId;
this.workerId = workerId;
this.taskId = taskId;
this.displayName = displayName;
this.documentType = documentType;
this.submissionStatus = submissionStatus;
this.expiryDate = expiryDate;
this.version = version;
this.fileId = fileId;
this.fileName = fileName;
this.fileMimeType = fileMimeType;
this.fileSize = fileSize;
this.fileScanStatus = fileScanStatus;
}

public static DocumentDetailResponse from(DocumentDetailResult result) {
var document = result.document();
var storedFile = result.storedFile();
return new DocumentDetailResponse(
document.workerDocumentId(),
document.workerId(),
document.taskId(),
result.workerDisplayName(),
document.documentType(),
document.submissionStatus(),
document.expiryDate(),
document.version(),
document.fileId(),
storedFile == null ? null : storedFile.name(),
storedFile == null ? null : storedFile.mimeType(),
storedFile == null ? null : storedFile.size(),
storedFile == null ? null : storedFile.scanStatus()
);
}

public UUID getWorkerDocumentId() {
return workerDocumentId;
}

public UUID getWorkerId() {
return workerId;
}

public UUID getTaskId() {
return taskId;
}

public String getWorkerDisplayName() {
return displayName;
}

public DocumentType getDocumentType() {
return documentType;
}

public SubmissionStatus getSubmissionStatus() {
return submissionStatus;
}

public LocalDate getExpiryDate() {
return expiryDate;
}

public long getVersion() {
return version;
}

public UUID getFileId() {
return fileId;
}

public String getFileName() {
return fileName;
}

public String getFileMimeType() {
return fileMimeType;
}

public Long getFileSize() {
return fileSize;
}

public ScanStatus getFileScanStatus() {
return fileScanStatus;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public final class DocumentItemResponse {
@Schema(name = "worker_id", format = "uuid", requiredMode = Schema.RequiredMode.REQUIRED)
private final UUID workerId;

@JsonProperty("task_id")
@Schema(name = "task_id", format = "uuid", description = "연결된 업무카드 ID")
private final UUID taskId;

@JsonProperty("display_name")
@Schema(
name = "display_name",
Expand All @@ -43,33 +47,43 @@ public final class DocumentItemResponse {
@Schema(name = "file_id", format = "uuid")
private final UUID fileId;

@JsonProperty("version")
@Schema(name = "version", description = "PATCH 요청의 expected_version 기준값", requiredMode = Schema.RequiredMode.REQUIRED)
private final long version;

private DocumentItemResponse(
UUID workerDocumentId,
UUID workerId,
UUID taskId,
String displayName,
DocumentType documentType,
SubmissionStatus submissionStatus,
LocalDate expiryDate,
UUID fileId
UUID fileId,
long version
) {
this.workerDocumentId = workerDocumentId;
this.workerId = workerId;
this.taskId = taskId;
this.displayName = displayName;
this.documentType = documentType;
this.submissionStatus = submissionStatus;
this.expiryDate = expiryDate;
this.fileId = fileId;
this.version = version;
}

public static DocumentItemResponse from(WorkerDocument document, String displayName) {
return new DocumentItemResponse(
document.workerDocumentId(),
document.workerId(),
document.taskId(),
displayName,
document.documentType(),
document.submissionStatus(),
document.expiryDate(),
document.fileId()
document.fileId(),
document.version()
);
}

Expand All @@ -81,6 +95,10 @@ public UUID getWorkerId() {
return workerId;
}

public UUID getTaskId() {
return taskId;
}

public String getWorkerDisplayName() {
return displayName;
}
Expand All @@ -100,4 +118,8 @@ public LocalDate getExpiryDate() {
public UUID getFileId() {
return fileId;
}

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

import com.fowoco.server.file.domain.StoredFile;
import com.fowoco.server.worker.domain.WorkerDocument;

public record DocumentDetailResult(WorkerDocument document, String workerDisplayName, StoredFile storedFile) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.fowoco.server.document.application;

import com.fowoco.server.auth.application.ActorContext;
import com.fowoco.server.common.error.ApiException;
import com.fowoco.server.common.security.TenantDatabaseContext;
import com.fowoco.server.document.application.error.DocumentErrorCode;
import com.fowoco.server.file.application.port.StoredFileRepository;
import com.fowoco.server.file.domain.StoredFile;
import com.fowoco.server.worker.application.WorkerDocumentSearchQuery;
import com.fowoco.server.worker.application.port.WorkerDocumentRepository;
import com.fowoco.server.worker.application.port.WorkerRepository;
Expand All @@ -21,18 +25,38 @@ public class DocumentService {

private final WorkerDocumentRepository workerDocumentRepository;
private final WorkerRepository workerRepository;
private final StoredFileRepository storedFileRepository;
private final TenantDatabaseContext tenantDatabaseContext;

public DocumentService(
WorkerDocumentRepository workerDocumentRepository,
WorkerRepository workerRepository,
StoredFileRepository storedFileRepository,
TenantDatabaseContext tenantDatabaseContext
) {
this.workerDocumentRepository = workerDocumentRepository;
this.workerRepository = workerRepository;
this.storedFileRepository = storedFileRepository;
this.tenantDatabaseContext = tenantDatabaseContext;
}

@Transactional(readOnly = true)
public DocumentDetailResult findById(UUID workerDocumentId, ActorContext actor) {
tenantDatabaseContext.setCompanyIdForCurrentTransaction(actor.companyId());
UUID companyId = actor.companyId();
WorkerDocument document = workerDocumentRepository
.findByIdAndCompanyId(workerDocumentId, companyId)
.orElseThrow(() -> new ApiException(DocumentErrorCode.DOCUMENT_NOT_FOUND));
String displayName = workerRepository
.findByWorkerIdAndCompanyId(document.workerId(), companyId)
.map(Worker::displayName)
.orElse(null);
StoredFile storedFile = document.fileId() == null
? null
: storedFileRepository.findByIdAndCompanyId(document.fileId(), companyId).orElse(null);
return new DocumentDetailResult(document, displayName, storedFile);
}

@Transactional(readOnly = true)
public DocumentPageResult findPage(ActorContext actor, WorkerDocumentSearchQuery query) {
tenantDatabaseContext.setCompanyIdForCurrentTransaction(actor.companyId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ public enum DocumentErrorCode implements ApiErrorCode {
DOCUMENT_REQUEST_DRAFT_VERSION_CONFLICT(
HttpStatus.CONFLICT,
"다른 사용자가 먼저 수정했습니다. 새로고침 후 다시 시도해 주세요."
),
DOCUMENT_NOT_FOUND(
HttpStatus.NOT_FOUND,
"문서를 찾을 수 없습니다."
);

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public class FileService {
"image/jpeg",
"image/png",
"image/webp",
"application/pdf"
"application/pdf",
"application/hwp+zip"
);

private final StoredFileRepository storedFileRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Optional<WorkerDocument> findByIdAndWorkerIdAndCompanyId(
UUID companyId
);

Optional<WorkerDocument> findByIdAndCompanyId(UUID workerDocumentId, UUID companyId);

WorkerDocument update(WorkerDocument document);

List<WorkerDocument> findPage(UUID companyId, WorkerDocumentSearchQuery query);
Expand Down
Loading
Loading