번호 자동 부여 대응 및 API 공통 처리 보강
This commit is contained in:
@@ -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. 후속 조치
|
||||
- 백엔드 담당자가 개발 일정·우선순위를 산출해 프런트 팀과 공유.
|
||||
- 구현 완료 후 샌드박스 환경에서 계약 검증 → 프런트엔드 실연동 검증 착수.
|
||||
|
||||
Reference in New Issue
Block a user