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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ AI_RUNTIME_MAX_CONCURRENT_CALLS=8
AI_RUNTIME_CIRCUIT_BREAKER_FAILURE_THRESHOLD=5
AI_RUNTIME_CIRCUIT_BREAKER_OPEN_DURATION=30s

WORKER_IMPORT_SOURCE_RETENTION=7d

# Client용 AiRun 상태 SSE입니다. 실행 기준은 SSE가 아니라 DB와 GET /api/v1/ai-runs/{id}입니다.
AI_RUN_SSE_TIMEOUT=5m
AI_RUN_SSE_HEARTBEAT_INTERVAL=15s
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ FOWOCO는 단순 번역 서비스가 아닙니다. 해야 할 일을 업무카
| 패키지·모듈 경계 | [프로젝트 구조](docs/project-structure.md) | 코드를 어느 패키지에 구현해야 하는지 설명 |
| 중요한 설계 결정 | [ADR 목록](docs/adr/README.md) | 저장소 경계, API·보안, Task·AiRun, RLS 결정 원본 |
| Server ↔ AI 계약 | [AI Runtime 계약](docs/ai-runtime-contract.md) | Server가 AI에 보내고 받을 수 있는 값과 검증 기준 |
| 근로자 명단 가져오기 | [Worker Import 가이드](docs/worker-import.md) | CSV/XLSX 업로드부터 검증·수정·등록까지의 API 순서 |
| Agent DB 정보 보충 | [Slot 조회·재호출](docs/ai-slot-resolution.md) | canonical key allow-list, tenant 조회와 ANALYZE 재호출 기준 |
| 이벤트 유실·재처리 | [Outbox 운영 가이드](docs/reliability/transactional-outbox.md) | 이벤트 발행, lease, 재시도와 장애 복구 기준 |
| 구현 계획·업무 상태 | [Server Roadmap](https://github.com/orgs/fowoco/projects/3) · [Issues](https://github.com/fowoco/server/issues) | 실제 담당자, 우선순위와 진행 상태 |
Expand Down Expand Up @@ -72,6 +73,7 @@ Provider SDK, Prompt와 모델 라우팅은 Server에 구현하지 않습니다.
| --- | --- |
| Auth·Company | 회원가입, 로그인, JWT Access Token, Refresh Token 회전·로그아웃 |
| Worker·Document | 근로자 기본정보와 서류 메타데이터 등록·조회·수정 |
| Worker Import | CSV/XLSX 열 연결, 행 검증·제외·수정과 선택 등록 |
| Task·Workflow | Knowledge projection 조회, 업무카드·체크리스트·상태 전이 |
| Approval·Audit | 승인·반려·외부 제출·증빙·완료와 감사 이벤트 |
| AI Integration | Provider-neutral 계약, 개인정보 차단과 응답·version 검증 |
Expand Down Expand Up @@ -159,7 +161,7 @@ Server는 하나의 Spring Boot 애플리케이션과 PostgreSQL로 배포하는
src/main/java/com/fowoco/server/
├── common
├── auth / company
├── worker
├── worker / workerimport
├── workflow / task
├── approval / audit
├── workerlink / file
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
compileOnly 'org.projectlombok:lombok'
implementation 'org.apache.poi:poi:5.4.0'
implementation 'org.apache.poi:poi-ooxml:5.4.0'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.flywaydb:flyway-database-postgresql'
runtimeOnly 'org.postgresql:postgresql'
Expand Down
1 change: 1 addition & 0 deletions docs/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ server/
│ │ ├── auth/
│ │ ├── company/
│ │ ├── worker/
│ │ ├── workerimport/ # CSV/XLSX 명단 검증·검토·등록
│ │ ├── document/
│ │ ├── file/
│ │ ├── workflow/
Expand Down
142 changes: 142 additions & 0 deletions docs/worker-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Worker Import 구현·연동 가이드

## 한 줄 설명

HR이 CSV/XLSX 근로자 명단을 올리면 서버가 즉시 등록하지 않고, 열 연결과 행 검증을 거친 뒤 HR이 선택한 정상 행만 `worker`로 등록합니다.

```text
UPLOADED → MAPPED → REVIEW_REQUIRED 또는 READY → COMMITTED
```

오류 행을 수정하거나 제외하면 `MAPPED`로 돌아가며, `retry`로 다시 검증합니다. 일부 정상 행만 먼저 등록한 경우 남은 오류·정상 행을 계속 처리할 수 있습니다.

## 화면 단계와 API

| 화면 단계 | API | 서버 동작 |
| --- | --- | --- |
| 파일 업로드 | `POST /api/v1/imports` | CSV/XLSX 구조·크기·수식·차단 열을 검사하고 작업 생성 |
| 진행·검토 조회 | `GET /api/v1/imports/{importId}` | 현재 상태, 건수와 행별 오류 조회 |
| 열 연결 | `PUT /api/v1/imports/{importId}/mappings` | 업로드 열을 Worker 필드에 연결 |
| 자료 검증 | `POST /api/v1/imports/{importId}/validate` | 필수값·날짜·국적 코드·중복 후보 검사 |
| 오류 수정·제외 | `PATCH /api/v1/imports/{importId}/rows` | 원본과 별도로 수정값 저장 또는 행 제외 |
| 등록 확정 | `POST /api/v1/imports/{importId}/commit` | 선택한 `VALID` 행만 근로자로 등록 |
| 실패 행 재검증 | `POST /api/v1/imports/{importId}/retry` | 수정 후 아직 등록하지 않은 행 재검증 |

모든 API는 `ADMIN` 또는 `HR`만 사용할 수 있습니다. `company_id`는 요청에서 받지 않고 JWT의 `ActorContext`로 결정하며, 다른 사업장 작업은 `404`로 숨깁니다.

## 지원 필드

열 연결의 값은 아래 canonical key만 허용합니다.

- `display_name` — 필수
- `nationality_code` — ISO alpha-2, 예: `VN`
- `preferred_language`
- `visa_type`
- `stay_expiry_date`
- `contract_start_date`
- `contract_end_date`
- `employment_permit_end_date`
- `employment_activity_end_date`

날짜는 `YYYY-MM-DD` 형식입니다. 새 근로자의 `work_status`는 기존 Worker 등록 규칙과 동일하게 `ACTIVE`로 시작합니다.

## 요청 예시

### 1. 업로드

```http
POST /api/v1/imports
Authorization: Bearer <access-token>
Idempotency-Key: import-20260807-001
Content-Type: multipart/form-data

file=@workers.csv
```

### 2. 열 연결

```json
{
"expected_version": 0,
"mappings": {
"이름": "display_name",
"국적": "nationality_code",
"언어": "preferred_language",
"체류만료일": "stay_expiry_date"
}
}
```

### 3. 오류 행 수정

```json
{
"expected_version": 2,
"rows": [
{
"row_number": 3,
"excluded": false,
"values": {
"stay_expiry_date": "2027-02-01"
}
}
]
}
```

### 4. 정상 행 등록

```http
POST /api/v1/imports/{importId}/commit
Idempotency-Key: commit-20260807-001
```

```json
{
"expected_version": 4,
"selected_row_numbers": [2, 3]
}
```

`selected_row_numbers`를 생략하면 현재 `VALID`인 모든 행을 등록합니다. 등록에 사용한 모든 `Idempotency-Key`와 요청·응답 snapshot을 별도 기록하므로 부분 등록을 여러 번 수행한 뒤 이전 요청을 다시 보내도 당시 성공 응답을 반환하며, 같은 키로 다른 행을 요청하면 `409`로 거부합니다.

## 파일·개인정보 규칙

- 최대 5MB, 1,000개 데이터 행, 50개 열까지만 허용합니다.
- UTF-8 CSV와 매크로가 없는 XLSX만 지원합니다.
- XLSX 날짜 셀은 화면 표시 형식과 관계없이 `YYYY-MM-DD`로 정규화합니다.
- CSV 수식 시작 문자(`=`, `+`, `-`, `@`)와 XLSX Formula Cell을 거부합니다.
- 여권번호·외국인등록번호·계좌번호와 MVP에서 수집하지 않는 연락처·이메일·주소·생년월일·법정실명 열은 업로드 단계에서 거부합니다.
- 원본 행 전체를 일반 로그나 감사로그에 남기지 않습니다.
- 원본 파일 만료 예정 시각은 `source_file_expires_at`으로 계산하며 기본값은 7일입니다.

`WORKER_IMPORT_SOURCE_RETENTION`은 보존기간 계산값입니다. 실제 Object Storage 삭제 배치는 File Storage 운영 정책과 함께 연결해야 하며, 만료 시각이 지나도 DB 이력을 임의 삭제하지 않습니다.

## 오류를 읽는 방법

행 오류는 `field`, `code`, `message`로 반환됩니다.

```json
{
"field": "stay_expiry_date",
"code": "INVALID_DATE",
"message": "날짜는 YYYY-MM-DD 형식이어야 합니다."
}
```

대표 코드는 `REQUIRED`, `INVALID_FORMAT`, `INVALID_DATE`, `DATE_ORDER`, `DUPLICATE_CANDIDATE`입니다. 서버는 중복 후보를 자동 병합하지 않으며 HR이 값을 수정하거나 해당 행을 제외해야 합니다.

## 개발자가 확인할 테스트

```bash
./gradlew test --tests '*DefaultWorkerImportFileParserTest' \
--tests '*WorkerImportApiIntegrationTest'
```

PostgreSQL 환경변수가 준비된 경우 Migration과 RLS도 확인합니다.

```bash
POSTGRES_TEST_ENABLED=true ./gradlew test \
--tests '*PostgreSqlMigrationTests' \
--tests '*PostgreSqlWorkerImportRlsTest'
```
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,11 @@ public enum AuditAction {
WORKER_LINK_ACCESSED,
USER_AGREEMENTS_RECORDED,
PASSWORD_RESET_REQUESTED,
PASSWORD_RESET_COMPLETED
PASSWORD_RESET_COMPLETED,
WORKER_IMPORT_CREATED,
WORKER_IMPORT_MAPPING_UPDATED,
WORKER_IMPORT_VALIDATED,
WORKER_IMPORT_ROWS_UPDATED,
WORKER_IMPORT_COMMITTED,
WORKER_IMPORT_RETRIED
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ public enum AuditTargetType {
DOCUMENT_REQUEST_DRAFT,
AI_RUN,
OUTBOX_EVENT,
USER_ACCOUNT
USER_ACCOUNT,
WORKER_IMPORT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.fowoco.server.workerimport.api;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.util.Set;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record WorkerImportCommitRequest(
@NotNull Long expectedVersion,
Set<@Min(2) Integer> selectedRowNumbers
) {
public WorkerImportCommitRequest {
selectedRowNumbers = selectedRowNumbers == null ? Set.of() : Set.copyOf(selectedRowNumbers);
}
}
Loading
Loading