번호 자동 부여 대응 및 API 공통 처리 보강

This commit is contained in:
JiWoong Sul
2025-10-23 14:02:31 +09:00
parent 09c31b2503
commit 7e933a2dda
55 changed files with 948 additions and 586 deletions

56
doc/DTO_TASKS.md Normal file
View File

@@ -0,0 +1,56 @@
# DTO 작업 현황
## 1. 백엔드 협업 로그인·리프레시·대시보드 정비
### 진행된 작업
- 로그인·대시보드·보고서·결재 스키마 요구사항을 `doc/backup/backend_change_requests.md:1-79`에 재정리해 백엔드와 공유 가능한 단일 요청서로 정리했다.
### 남은 작업
- 백엔드 담당자와 개발 일정·샌드박스 검증 순서를 확정하고 문서에 일정표/담당자를 추가해야 한다.
- 로그인·대시보드 외 보고서/권한/단계 API의 문서화 내용이 구현으로 연결되는지 주간 점검 미팅을 마련한다.
## 2. API 계약 정비 (route_path, include_deleted, 표준 응답/에러 구조)
### 진행된 작업
- 401 응답 메시지를 UI 안내 문구로 변환하도록 `lib/core/network/api_error.dart:90-139`을 보강했고, 대응 단위 테스트를 `test/core/network/api_error_test.dart:84-138`에 추가했다.
- 그룹 권한 목록에 `include_deleted` 파라미터와 `include=group,menu`를 반영하고(`lib/features/masters/group_permission/data/repositories/group_permission_repository_remote.dart:21-43`), UI에서 `menu.path`를 노출하도록 테이블 컬럼을 확장했다(`lib/features/masters/group_permission/presentation/pages/group_permission_page.dart:855-888`).
- 결재 단계 원격 저장소가 `include=approval,approver,status`를 강제하도록 수정해 v4 스키마와 맞췄다(`lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart:30-44`).
- KPI delta/증감 아이콘을 대시보드 카드에 노출해 백엔드의 delta 필드를 활용할 준비를 마쳤다(`lib/features/dashboard/presentation/pages/dashboard_page.dart:258-326`).
### 남은 작업
- ApiClient 계층에서 `{ "data": ... }` 언랩 처리를 공통화하거나, 미적용 저장소에서 동일 패턴을 반복하는 부분을 리팩터링해야 한다.
- `include_deleted`·`route_path`가 필요한 다른 리포지토리(예: 메뉴/사용자/보고서)가 기존 파라미터를 사용 중인지 재점검 후 일괄 적용이 필요하다.
- 백엔드에서 통일된 에러 코드/세부 구조를 확정하면 문서와 코드 주석을 업데이트한다.
## 3. 번호 자동 부여 대응
### 진행된 작업
- 재고 트랜잭션/결재 생성 DTO에서 `transaction_no`·`approval_no`를 제거하고 요청 payload를 정리했다(`lib/features/inventory/transactions/domain/entities/stock_transaction_input.dart:1-229`, `lib/features/approvals/domain/entities/approval.dart:200-224`).
- 입고/출고/대여/결재 생성 UI에서 번호 입력 필드를 텍스트 안내로 교체하고, 생성/수정 토스트에 서버가 할당한 번호를 표기하도록 수정했다(`lib/features/inventory/inbound/presentation/pages/inbound_page.dart:1711-1725`, `lib/features/inventory/outbound/presentation/pages/outbound_page.dart:1911-1917`, `lib/features/inventory/rental/presentation/pages/rental_page.dart:1909-1917`, `lib/features/approvals/presentation/pages/approval_page.dart:485-684`).
- 저장소/위젯/통합 테스트를 새 규칙에 맞춰 정비했다(`test/features/inventory/transactions/data/stock_transaction_repository_remote_test.dart:160-199`, `test/features/inventory/inbound_page_test.dart:110-207`, `test/features/inventory/outbound_page_test.dart:1-100`, `test/features/inventory/rental_page_test.dart:1-100`, `integration_test/stock_transaction_state_flow_test.dart:184-238`).
- 번호 정책과 작업 절차를 `doc/frontend_auto_numbering_update.md``doc/stock_approval_system_api_v4.md`에 반영했다.
### 남은 작업
- 생성 직후 알림/딥링크/내부 공유 링크에 새 번호를 주입하는 흐름(예: Slack·메일 템플릿, 상세 페이지 자동 이동)을 확인하고 필요한 리팩터링을 진행한다.
- QA 체크리스트(`doc/frontend_auto_numbering_update.md:23-28`)의 미완료 항목을 실제 시나리오 테스트로 채우고 결과를 문서화한다.
## 4. 인증·대시보드 연동
### 진행된 작업
- 앱 시작 시 저장된 리프레시 토큰으로 세션을 갱신하고(`lib/main.dart:15-20`), 라우터에서 비로그인 사용자를 로그인 화면으로 리다이렉트하도록 가드 로직을 추가했다(`lib/core/routing/app_router.dart:37-49`).
- 백엔드 401 메시지를 UI 알림과 동기화하기 위해 에러 매퍼와 테스트를 보강했다(동일 경로 참조).
- 대시보드 카드가 delta 데이터와 아이콘을 표시할 수 있도록 뷰를 업데이트하고(`lib/features/dashboard/presentation/pages/dashboard_page.dart:258-326`), 저장소가 `/dashboard/summary` 응답의 `data` 래퍼를 처리하게 했다(`lib/features/dashboard/data/repositories/dashboard_repository_remote.dart:17-31`).
### 남은 작업
- `AuthService.refreshSession()` 실패 시 초기 부트가 중단되지 않도록 예외 처리/로그아웃 플로우를 보완해야 한다.
- 대시보드 요약·로그인 요청에 대한 실패 로그/토스트 정책을 정의하고 컨트롤러에 적용한다.
- 백엔드에서 권한 목록/템플릿 포함 응답을 완료하면 PermissionSynchronizer와의 연동 테스트를 추가한다.
## 5. 결재·재고 화면 보강
### 진행된 작업
- 창고 코드/우편번호/주소 등 중첩 객체를 상세 뷰에서 표시하도록 입고·출고·대여 레코드와 위젯을 확장했다(`lib/features/inventory/inbound/presentation/models/inbound_record.dart:31-60`, `lib/features/inventory/outbound/presentation/models/outbound_record.dart:31-60`, `lib/features/inventory/rental/presentation/models/rental_record.dart:33-65` 및 각 상세 위젯).
- 결재 생성 다이얼로그와 재고 모달이 서버 응답을 기반으로 성공 메시지를 출력하고 최신 번호를 다시 표시하도록 수정했다(`lib/features/approvals/presentation/pages/approval_page.dart:485-684`, `lib/features/inventory/inbound/presentation/pages/inbound_page.dart:1520-1566`, `lib/features/inventory/outbound/presentation/pages/outbound_page.dart:1706-1762`, `lib/features/inventory/rental/presentation/pages/rental_page.dart:1684-1742`).
### 남은 작업
- 결재 단계/상태 전환 후 `ApprovalController`와 재고 컨트롤러가 API에서 받은 최신 객체를 즉시 바인딩하는지 확인하고, 필요한 경우 fetch 로직을 후속 호출로 보강한다.
- 재고 상세 다이얼로그(`lib/features/inventory/transactions/presentation/widgets/transaction_detail_dialog.dart`)에서 새 번호·창고 상세를 활용하는지 검증하고 누락된 필드를 추가한다.
- 그룹 권한/재고 화면 필터에 `include_deleted`·`route_path` 확장 옵션을 노출할 UI 개선이 남아 있다.
## 6. 테스트·문서 검증
### 진행된 작업
- 번호 자동 부여와 에러 매핑 관련 단위/위젯/통합 테스트를 업데이트했고, 신규 검증 시나리오를 문서로 가이드했다(상기 테스트 파일 및 `doc/frontend_auto_numbering_update.md`).
- `doc/stock_approval_system_spec_v4.md:389-507`에 자동 번호 규칙을 명시해 백엔드와의 참조 문서를 최신화했다.
### 남은 작업
- 보고서 Export, 대시보드 요약, 인증 토큰 시나리오에 대한 단위/위젯 테스트를 추가해 회귀 범위를 확대한다.
- QA 시나리오(번호 증가·Export 성공·대시보드 데이터 확인)를 `doc/IMPLEMENTATION_TASKS.md`나 별도 체크리스트에 반영하고, 실행 결과를 주기적으로 기록해야 한다.

View File

@@ -1,97 +1,79 @@
# 백엔드 수정 요청서 (2025-10-20 갱신)
# 백엔드 수정 요청서 (2025-10-16 갱신)
## 1. 배경
- 프런트엔드는 `.env.development`에서 `API_BASE_URL=http://43.201.34.104:8080`용해 실서버 로그인 API를 호출한다.
- 동일 API를 사용하는 운영 환경에서는 CORS 문제가 없지만, 로컬 웹 개발 시 브라우저가 `http://localhost:<port>` 오리진으로 사전 요청(preflight)을 보내면서 403 대신 CORS 차단이 발생한다.
- 프런트(`superport_v2`)와 백엔드(`superport_api_v2`) 양쪽 구현을 재검토한 결과, 로그인 계약은 일치하나 실서버에서 CORS 응답 헤더가 전혀 내려오지 않는 것으로 확인되었다.
- 로컬 개발 및 QA가 모두 실서버를 바라보고 있어, 백엔드에서 CORS 허용 정책을 명시적으로 정비해야 한다.
- Flutter 프런트엔드(`superport_v2`)와 최신 백엔드(`superport_api_v2`)이 계약을 점검한 결과, 다수의 엔드포인트가 미구현이거나 응답 스키마가 상이해 실사용 플로우를 마무리할 수 없다.
- 프런트는 Clean Architecture 구조 및 DTO를 백엔드 스펙(v4)에 맞춰 구현한 상태이며, 실연동 전까지 계약 정합성을 확보해야 한다.
- 본 문서는 백엔드 측 추가 개발/수정을 요청하기 위한 정리 문서이다.
## 2. 현상 및 재현 절차
- 브라우저 콘솔 오류:
```
Access to XMLHttpRequest at 'http://43.201.34.104:8080/api/v1/auth/login' from origin 'http://localhost:50408'
has been blocked by CORS policy: Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
- curl 재현(사전 요청):
## 2. 주요 이슈 요약
- 로그인 및 대시보드 핵심 엔드포인트(`/api/v1/auth/**`, `/api/v1/dashboard/summary`)가 존재하지 않아 애플리케이션 초기 진입이 불가능하다.
- 보고서 다운로드 화면이 호출하는 `/api/v1/reports/**` 엔드포인트가 미구현 상태다.
- 결재·재고 API 응답 키가 프런트 DTO와 불일치하여 승인 상태, 요청자, 제품/벤더 정보 등이 전부 기본값으로 표시되며, 단계/상태 전환 이후 최신 데이터를 확보할 수 없다.
- 결재 단계(`approval-steps`) API가 단계 CRUD/액션 수행 후 적절한 본문을 반환하지 않고, 목록 필터(승인자·상태·검색)도 지원하지 않는다.
- 그룹-메뉴 권한 API가 라우팅 정보를 제공하지 않고, 삭제 항목 조회 파라미터가 프런트와 불일치해 권한 동기화가 깨진다.
```bash
curl -i -X OPTIONS \
http://43.201.34.104:8080/api/v1/auth/login \
-H 'Origin: http://localhost:50408' \
-H 'Access-Control-Request-Method: POST'
```
## 3. 상세 요청
실제 응답: `HTTP/1.1 404 Not Found` + 헤더 없음 → CORS 미적용.
### 3.1 로그인/세션 및 대시보드 API 구현
- 엔드포인트
- `POST /api/v1/auth/login`: `identifier`, `password`, `remember_me`(bool) 입력을 받아 `{ "data": { "access_token", "refresh_token", "expires_at", "user", "permissions" } }` 구조를 반환해야 한다. `user` 객체는 `{ id, name, employee_no, email, primary_group { id, name } }` 필드를 포함하고, `permissions``resource``actions[]`(소문자 문자열)로 구성된다.
- `POST /api/v1/auth/refresh`: `refresh_token`으로 세션을 갱신하며 응답 스키마는 로그인과 동일하다.
- `GET /api/v1/dashboard/summary`: `{ "data": { "generated_at", "kpis": [], "recent_transactions": [], "pending_approvals": [] } }` 형태로 내려 KPI 카드, 최근 전표, 결재 대기 목록을 채울 수 있어야 한다.
- 요구 사항
- `kpis[]` 항목은 `{ key, label, value, trend_label, delta }` 필드를 제공해 프런트 차트 증감률을 계산할 수 있도록 한다.
- `recent_transactions[]``{ transaction_no, transaction_date, transaction_type, status_name, created_by }` 문자열 필드로 구성한다.
- `pending_approvals[]``{ approval_no, title, step_summary, requested_at }`을 포함하며 `requested_at`은 ISO8601 UTC 문자열로 반환한다.
- 로그인 실패 시 `invalid credentials`, 비활성 계정 접근 시 `account is inactive`, 갱신 토큰 만료는 `token expired`, 재사용·서명 오류는 `invalid token` 메시지를 반환해 프런트 알림 문구와 동일하게 맞춘다.
- 인증 실패(401), 세션 만료·권한 거부(403) 시 `{ "error": { "code": <http-status>, "message": "...", "details": [...] } }` 규격을 사용하고, 만료/재사용 토큰별 메시지를 문서화한다.
- 실제 요청도 동일하게 헤더가 비어 있음:
### 3.2 보고서 Export API 구현
- 엔드포인트
- `GET /api/v1/reports/transactions/export`
- `GET /api/v1/reports/approvals/export`
- 요구 사항
- 공통 쿼리: `from`, `to`, `format(xlsx|pdf)`, `transaction_status_id`, `approval_status_id`, `requested_by_id`.
- 트랜잭션 보고서 `from`·`to` 값은 `yyyy-MM-dd` 형식, 결재 보고서는 ISO8601 UTC 타임스탬프를 지원한다.
- 응답은 기본적으로 파일 스트림(`Content-Type`은 선택한 포맷의 MIME, `Content-Disposition: attachment; filename="<name>"`)이며, 스토리지 연계 시 `{ "data": { "download_url", "filename", "mime_type", "expires_at" } }` 메타데이터로 대체할 수 있다.
- `format=pdf` 요청도 정상 처리하고, 지원 불가 시 명확한 4xx 코드·메시지를 반환하도록 문서화한다.
- 모든 다운로드 요청에 대해 접근 권한·감사 로그 정책을 명시한다.
```bash
curl -i -X POST \
http://43.201.34.104:8080/api/v1/auth/login \
-H 'Origin: http://localhost:50408' \
-H 'Content-Type: application/json' \
-d '{"identifier":"test","password":"test"}'
```
### 3.3 결재/재고 응답 스키마 정합성
- 결재 목록·단건 응답은 프런트 도메인 모델과 동일한 키를 사용한다.
- 단건 응답은 `{ "data": { ... } }` 혹은 `{ "data": { "approval": { ... } } }` 구조를 유지하고, `approval_no`, `transaction_no`, `status { id, name, color }`, `requester { id, employee_no, name }`, `current_step { id, step_order, status { id, name, is_blocking_next, is_terminal }, approver { id, employee_no, name }, assigned_at, decided_at, note }`, `steps[]`, `histories[]`, `created_at`, `updated_at`을 포함한다.
- 모든 단계·이력 항목은 `status` 키로 정규화하고(`step_status` 금지), `histories[]`에는 `action { id, name }`, `from_status`, `to_status`, `approver`, `action_at`, `note`를 내려준다.
- `approval` 객체에는 필요 시 `transaction { id, transaction_no }`, `template_name` 등을 함께 포함해 단계 목록에서도 동일 데이터를 재사용할 수 있도록 한다.
- 재고 트랜잭션 응답은 중첩 객체 구조를 보장한다.
- 헤더: `transaction_type { id, name }`, `transaction_status { id, name }`, `warehouse { id, warehouse_code, warehouse_name, zipcode { ... } }`, `created_by { id, employee_no, employee_name }`, `expected_return_date`.
- 라인: `lines[].product { id, product_code, product_name, vendor { id, vendor_name }, uom { id, uom_name } }`, `quantity`, `unit_price`, `note`.
- 고객: `customers[].customer { id, customer_code, customer_name }`, `note`.
- 결재 요약: `approval { id, approval_no, status { id, name, is_blocking_next } }`.
- `quantity`, `unit_price`는 BigDecimal 직렬화 그대로 전달하되 `null`은 키를 생략하지 말 것(프런트 DTO가 숫자·문자열 모두를 파싱한다).
- `warehouse.zipcode`는 최소 `zipcode`, `road_name`을 포함하고 추가 주소 필드가 있으면 그대로 노출한다.
- 상태 전환(Submit/Approve/Reject/Cancel/Complete) 응답은 최신 `data.transaction` 전체 또는 최소한 `data.transaction_status`, `data.updated_at`, `data.approval`을 포함해 UI가 즉시 갱신되도록 한다.
응답: `401 Unauthorized` 본문은 내려오지만 `Access-Control-Allow-Origin` 헤더가 없음.
### 3.4 결재 단계/행위 API 정합성
- `GET /api/v1/approval-steps``approver_id`, `approval_id`, `status_id`(또는 `step_status_id`), `q`(결재번호·승인자 키워드)를 지원하고, 항상 `include=approval,approver,status` 형태의 확장을 처리한다. 응답 항목에는 `approval { id, approval_no, transaction_no, template_name }`이 포함되어야 한다.
- 단계 생성·수정·복구 응답은 `{ "data": { ... } }` 형태로 단계 요약을 반환하고, 단계 행위·일괄 배정 응답은 최신 결재 데이터를 `data.approval` 또는 `data.approval.steps`/`data.histories`에 담아 돌려준다.
- 모든 단계·행위 응답에서 단계 상태 키는 `status`로 통일하고, `step_status_id`는 요청/응답에서 보조 필드로만 유지한다.
## 3. 프런트/백엔드 로그인 계약 점검
- **프런트 요청 구조**
- 경로: `POST ${ApiRoutes.apiV1}/auth/login` → `/api/v1/auth/login`
- 페이로드: `{ identifier, password, remember_me }` (`lib/features/auth/data/repositories/auth_repository_remote.dart:18`)
- 응답 파싱: `{ data: { access_token, refresh_token, expires_at, user, permissions[] } }`
(`lib/features/auth/data/dtos/auth_session_dto.dart`)
- **백엔드 구현**
- 라우트: `#[post("/login")]` (`backend/src/api/v1/auth.rs:17`) → `web::scope("/api/v1")`
- 요청 모델: `LoginRequest { identifier: String, password: String, remember_me: bool }`
(`backend/src/domain/auth.rs:9`)
- 응답 모델: `AuthSessionResponse { data: AuthSessionData { ... } }`
- **계약 비교 결론**
- 필드 명/자료형 모두 일치, remember_me 기본값 및 데이터 매핑도 호환.
- 로그인 자체는 401/403 흐름이 정상이나, 브라우저 오리진이 차단되어 요청이 전달되지 못함.
### 3.5 그룹-메뉴 권한 응답 확장
- `GET /api/v1/group-menu-permissions` 및 단건 응답의 `menu` 객체에 `route_path`(가능하면 `path` 보조 필드 포함)를 항상 채운다.
- `deleted=true`(또는 `include_deleted=true`) 파라미터를 허용해 소프트 삭제 항목을 조회할 수 있게 하고, 응답 항목에 `is_deleted`를 노출한다.
- `include=group,menu` 확장을 공식화해 그룹/메뉴 요약을 한 번에 받을 수 있도록 한다.
## 4. 근본 원인 분석
- 백엔드 `App::new()` 정의는 `build_cors(&config.cors)` 미들웨어를 `wrap`하고
`default_service`에서 `OPTIONS` 가드를 204로 처리하도록 구현되어 있음 (`backend/src/app/mod.rs:36-132`).
- `config/default.toml`의 `[cors]` 설정은 `allowed_origins = []`로 전체 허용이 기본이지만,
실제 운영 환경에서는 `APP_ENV`에 대응하는 설정 또는 환경 변수로 제한된 오리진만 등록한 것으로 추정된다.
- 하지만 허용 목록에서 로컬 호스트가 빠진 경우라면 CORS 미들웨어가 `403`을 반환해야 하는데,
현재는 단순 404/401 응답으로 보아 **미들웨어가 동작하지 않거나, 리버스 프록시/로드밸런서 구간에서 CORS 헤더가 삭제**되고 있다.
- 결과적으로 브라우저는 `Access-Control-Allow-Origin`을 받지 못하고 사전 요청 단계에서 차단된다.
### 3.6 결재 생성/수정 응답 보강
- `POST /api/v1/approvals`/`PATCH /api/v1/approvals/{id}` 응답은 `{ "data": { "approval": { ... } } }` 형태로 최신 결재 요약과 `steps[]`, 필요 시 `histories[]`를 포함해야 한다.
- `approval_status_id`가 생략되면 자동으로 기본 대기 상태를 설정하는 규칙을 명시하고, `approval_no`는 서버가 자동 발급(포맷 `APP-YYYYMMDDNNNN`)함을 문서화한다.
## 5. 요청 사항
1. **백엔드 Actix CORS 설정 재점검**
- `build_cors`가 실제 배포 바이너리에도 적용되는지 확인하고, 필요한 경우 `Cors::default()` 대신
`Cors::permissive()` 또는 `allowed_origin_fn` 로깅을 추가해 런타임에서 허용 여부를 추적한다.
- `supports_credentials()`를 유지하면서도 최소 `http://localhost` 기반 개발 포트 전체를 허용하도록
`allowed_origin_fn`에서 와일드카드 검사를 추가하거나, 설정 파일에 와일드카드 표기를 허용하도록 개선한다.
```rust
.allowed_origin_fn(move |origin, _| {
if allow_all {
return true;
}
if origin.as_bytes().starts_with(b"http://localhost") {
return true;
}
allowed_list.iter().any(|allowed| allowed == origin)
})
```
- 운영 배포용 설정(`APP_ENV=production`)에도 `cors.allowed_origins`에
`https://{prod-domain}` + `http://localhost` (또는 사내 VPN 도메인)을 명시한다.
2. **리버스 프록시/로드밸런서 검증**
- Nginx/ALB 등 중간 계층이 `OPTIONS` 메서드를 백엔드로 전달하는지 확인하고, 차단 시 `proxy_set_header Access-Control-Allow-Origin` 등을 설정한다.
- 모든 사전 요청이 최소 `204` 혹은 `200`과 함께 `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`를 반환하도록 보장한다.
3. **로그인 핸들러 응답 헤더 확인**
- 인증 성공/실패 여부와 관계없이 `Access-Control-Allow-Origin`이 반드시 포함되도록 통합 테스트를 추가한다.
- 예시: `cargo test cors_allows_login_origin` 형태의 통합 테스트에서 `Origin: http://localhost:50408` 헤더를 넣고 응답 헤더를 검증.
### 3.7 응답/에러 문서화 및 테스트
- `stock_approval_system_api_v4.md`에 변경된 요청/응답 예시를 모두 반영하고, 인증/대시보드/결재 단계/보고서 섹션을 최신 상태로 유지한다.
- 회귀 테스트(`cargo test`, 통합 시나리오 스크립트)에 신규 계약을 검증하는 케이스를 추가한다.
## 6. 검증 및 수용 기준
- `curl -X OPTIONS` 및 `curl -X POST` 재현 시 `Access-Control-Allow-Origin: http://localhost:50408` 헤더가 내려오고 브라우저 CORS 에러가 사라질 것.
- `flutter run -d chrome --web-port 50408`에서 로그인 성공/실패 흐름이 정상 동작.
- 백엔드 `cargo fmt`, `cargo test` 모두 통과.
- `stock_approval_system_api_v4.md` 또는 운영 문서에 허용 오리진 정책 및 설정 방법을 명시.
## 4. 수용 기준
- 상기 엔드포인트 및 스키마 변경이 구현되고, 요청/응답이 문서와 일치해야 한다.
- 기존 204 응답은 JSON 응답으로 교체되고, 키(`data.approval`, `data.transaction` 등)가 프런트 기대와 동일해야 한다.
- `cargo fmt`, `cargo check`, `cargo test` 및 CI 파이프라인이 통과한다.
## 7. 후속 조치
- 백엔드 담당자가 실제 배포 서버의 환경 변수/리버스 프록시 설정을 확인 후 조치 내용을 공유.
- 수정 배포 이후 프런트 팀이 실서버 연결 테스트를 수행하고, 필요한 경우 추가 허용 오리진 목록을 요청.
## 5. 후속 조치
- 백엔드 담당자가 개발 일정·우선순위를 산출해 프런트 팀과 공유.
- 구현 완료 후 샌드박스 환경에서 계약 검증 → 프런트엔드 실연동 검증 착수.

View File

@@ -0,0 +1,34 @@
# 입출고·결재 번호 자동 부여 대응 가이드
프런트엔드 변경 시 유의해야 할 내용을 정리했습니다. 모든 일정은 백엔드 배포(문서 버전 v4) 이후 적용을 권장합니다.
## 주요 변경 요약
- 서버가 `transaction_no``approval_no`를 자동 생성합니다. 포맷은 `TRX-YYYYMMDDNNNN`, `APP-YYYYMMDDNNNN`이며 일자별 4자리 시퀀스를 사용합니다.
- 생성 요청 본문에서 두 필드를 제거해야 합니다. 백엔드가 값을 무시하므로 전송 시 불필요한 필드 오류가 날 수 있습니다.
- 생성 응답(`POST /stock-transactions`, `POST /approvals`)에 포함된 번호를 UI에 표기하거나 후속 액션에 사용해야 합니다.
## 작업 항목
1. **트랜잭션 생성 화면**
- 번호 입력 필드 제거 및 레이아웃 정리.
- 생성 직후 응답(`data.transaction_no`)을 받아 상세 화면/알림에 표기.
2. **결재 생성/상신 화면**
- `approval.approval_no` 필드 제거.
- 응답(`data.approval.approval_no`)을 활용해 결재 상세 링크/알림 업데이트.
3. **API 클라이언트 수정**
- 공유 DTO/타입스크립트 인터페이스에서 `transaction_no`, `approval_no`를 삭제.
- E2E/단위 테스트에서 하드코딩된 번호 값 삭제 및 응답 값 기반 검증으로 변경.
4. **리스트/검색 기능**
- 표시 포맷이 바뀌었는지 확인하고 필요 시 날짜·시퀀스 분리 표시 적용.
## 검증 체크리스트
- [ ] 트랜잭션 생성 요청 payload에 `transaction_no`가 포함되지 않는다.
- [ ] 결재 생성 요청 payload에 `approval_no`가 포함되지 않는다.
- [ ] 생성 이후 상세 페이지/알림에 새 번호가 반영된다.
- [ ] 기존 북마크/딥링크가 새 번호 포맷(`-` 포함 13자리)과 호환되는지 확인한다.
- [ ] QA 환경에서 동일 일자 다건 생성 시 번호가 0001, 0002… 순으로 증가하는지 확인한다.
## 참고 문서
- `stock_approval_system_api_v4.md` 4.1, 5.1 섹션(요청 본문 변경 사항)
- `stock_approval_system_spec_v4.md` 3.14, 3.19 테이블(번호 관리 규칙)
문의 사항은 #inventory-backend 채널로 공유 바랍니다.

View File

@@ -414,7 +414,6 @@
`POST /stock-transactions`
```json
{
"transaction_no": "TXN-2025-0001",
"transaction_type_id": 1,
"transaction_status_id": 1,
"warehouse_id": 1,
@@ -437,13 +436,14 @@
],
"customers": [],
"approval": {
"approval_no": "APP-2025-0001",
"requested_by_id": 7,
"note": "입고 결재"
}
}
```
응답은 생성된 트랜잭션 전체 정보를 반환하며, 라인·고객 식별자가 포함된다. `approval`
응답은 생성된 트랜잭션 전체 정보를 반환하며, 라인·고객 식별자가 포함된다. `transaction_no`
`approval.approval_no`는 요청 시 생략하며, 서버가 각각 `TRX-YYYYMMDDNNNN`,
`APP-YYYYMMDDNNNN` 패턴으로 생성한 값을 응답에서 확인한다. `approval`
블록은 결재 생성에 필요한 정보를 담으며 생략할 수 없다.
### 4.2 목록 조회
@@ -455,7 +455,7 @@
"items": [
{
"id": 9001,
"transaction_no": "TXN-2025-0001",
"transaction_no": "TRX-202511100001",
"transaction_type": {
"id": 1,
"name": "입고"
@@ -519,7 +519,7 @@
],
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"status": {
"id": 1,
"name": "대기",
@@ -571,7 +571,7 @@
{
"data": {
"id": 9001,
"transaction_no": "TXN-2025-0001",
"transaction_no": "TRX-202511100001",
"transaction_type": {
"id": 1,
"name": "입고"
@@ -636,7 +636,7 @@
],
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"status": {
"id": 1,
"name": "대기",
@@ -861,7 +861,6 @@
```json
{
"transaction_id": 9001,
"approval_no": "APP-2025-0001",
"approval_status_id": 1,
"requested_by_id": 7,
"note": "입고 결재"
@@ -873,7 +872,7 @@
"data": {
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"status": {
"id": 1,
"name": "대기",
@@ -896,7 +895,7 @@
}
}
```
- `approval_no`활성 결재 기준으로 중복 불가하며(409 Conflict), 길이는 1~30자다.
- `approval_no`서버가 자동 발급하는 읽기 전용 필드로 `APP-YYYYMMDDNNNN` 형식을 따른다. 클라이언트는 필드를 전송하지 않으며, 중복 방지는 서버에서 처리된다.
- 최초 생성 시 `approval_status_id`에는 `대기` 상태 ID를 전달하고, 서버는 동일 상태로 저장한다.
- 단계나 이력이 존재하면 `data.approval.steps`, `data.approval.histories`가 함께 반환된다.
@@ -907,10 +906,10 @@
"items": [
{
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"transaction": {
"id": 9001,
"transaction_no": "TXN-2025-0001"
"transaction_no": "TRX-202511100001"
},
"status": {
"id": 1,
@@ -1006,10 +1005,10 @@
{
"data": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"transaction": {
"id": 9001,
"transaction_no": "TXN-2025-0001"
"transaction_no": "TRX-202511100001"
},
"status": {
"id": 1,
@@ -1159,7 +1158,7 @@
],
"approval": {
"id": 5001,
"transaction_no": "TXN-2025-0001",
"transaction_no": "TRX-202511100001",
"status": {
"id": 1,
"name": "대기",
@@ -1241,7 +1240,7 @@
],
"approval": {
"id": 5001,
"transaction_no": "TXN-2025-0001",
"transaction_no": "TRX-202511100001",
"status": {
"id": 1,
"name": "대기",
@@ -1274,7 +1273,7 @@
"data": {
"approval": {
"id": 5001,
"transaction_no": "TXN-2025-0001",
"transaction_no": "TRX-202511100001",
"status": {
"id": 2,
"name": "진행중",
@@ -1401,7 +1400,7 @@
"data": {
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"status": {
"id": 2,
"name": "진행중",
@@ -1463,7 +1462,7 @@
},
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"status": {
"id": 2,
"name": "진행중",
@@ -1537,7 +1536,7 @@
},
"approval": {
"id": 5001,
"approval_no": "APP-2025-0001",
"approval_no": "APP-202511100001",
"status": {
"id": 2,
"name": "진행중",
@@ -1790,7 +1789,7 @@
],
"recent_transactions": [
{
"transaction_no": "TXN-2025-0001",
"transaction_no": "TRX-202511100001",
"transaction_date": "2025-09-18",
"transaction_type": "입고",
"status_name": "상신",
@@ -1799,7 +1798,7 @@
],
"pending_approvals": [
{
"approval_no": "APP-2025-0005",
"approval_no": "APP-202511100005",
"title": "출고 결재",
"step_summary": "2단계/3단계 진행중",
"requested_at": "2025-09-17T03:00:00Z"

View File

@@ -389,6 +389,7 @@ zipcodes ||--o{ customers : addressed
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 주의: **벤더ID 없음**. 벤더 정보는 라인의 `product_id`가 가리키는 `products.vendor_id`로 파생.
> 번호 발급: 서버가 `TRX-YYYYMMDDNNNN` 형식으로 `transaction_no`를 생성하며 클라이언트 입력을 허용하지 않는다.
> 목록 조회는 `customer_id` 쿼리 파라미터를 지원해 특정 고객이 연결된 트랜잭션만 필터링할 수 있다. (2024-10 갱신)
---
@@ -500,6 +501,8 @@ zipcodes ||--o{ customers : addressed
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
> 번호 발급: 서버가 `APP-YYYYMMDDNNNN` 형식으로 `approval_no`를 생성하며 클라이언트 입력을 허용하지 않는다.
---
### 3.20 `approval_steps` (결재_단계)