feat(user): 사용자 자기정보 편집과 관리자 재설정 플로우를 연동
- lib/widgets/app_shell.dart에서 내 정보 다이얼로그를 추가하고 UserRepository.updateMe·비밀번호 변경 로직을 연결 - lib/features/masters/user/* 모듈에 phone·forcePasswordChange·passwordUpdatedAt 필드를 반영하고 reset-password/update-me API를 사용 - lib/core/validation/password_rules.dart을 신설해 비밀번호 정책 검증을 공통화하고 신규 위젯·테스트에서 재사용 - doc/stock_approval_system_api_v4.md 등 문서를 users 스펙 개편 내용으로 갱신하고 user_management_plan.md를 추가 - test/widgets/app_shell_test.dart 등에서 자기정보 수정·비밀번호 재설정 시나리오를 검증하고 기존 테스트를 보강
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
---
|
||||
|
||||
## 0. 구현 현황 요약 (2025-09-18 기준)
|
||||
- 마스터 데이터: `/vendors`, `/uoms`, `/transaction-types`, `/transaction-statuses`, `/approval-statuses`, `/approval-actions`, `/warehouses`, `/customers`, `/products`, `/employees`, `/groups`, `/menus`, `/group-menu-permissions`, `/zipcodes`
|
||||
- 마스터 데이터: `/vendors`, `/uoms`, `/transaction-types`, `/transaction-statuses`, `/approval-statuses`, `/approval-actions`, `/warehouses`, `/customers`, `/products`, `/users`, `/groups`, `/menus`, `/group-menu-permissions`, `/zipcodes`
|
||||
- 각 자원은 `/api/v1/<resource>` 패턴을 따르며, 목록 필터·페이지네이션·`include` 확장을 지원한다.
|
||||
- 그룹 권한은 `/api/v1/group-menu-permissions`와 `/api/v1/groups/{id}/permissions` 일괄 갱신 엔드포인트로 관리한다. `group-menu-permissions` 응답의 `menu` 객체에는 `route_path`와 동일 값을 가진 `path`가 포함되며 각 항목은 `is_deleted`를 노출한다. `include=group,menu` 확장과 `include_deleted=true` 파라미터로 삭제 권한을 함께 조회할 수 있다.
|
||||
- 우편번호 검색 `/api/v1/zipcodes`는 부분 일치 검색(`q`, `zipcode`, `road_name`)과 단건 조회를 제공한다.
|
||||
@@ -26,7 +26,7 @@
|
||||
- **증분 조회:** `updated_since=ISO8601`.
|
||||
- **정렬:** `sort`(기본 `updated_at`), `order=asc|desc`(기본 desc).
|
||||
- **검색:** `q` 파라미터로 코드/명칭 부분 일치. 필요한 경우 컬럼별 필터 지원.
|
||||
- **Include 확장:** `include` 쿼리로 추가 데이터(`lines`, `customers`, `approval`, `steps`, `histories`, `permissions`, `employees` 등) 선택 가능. 포함 대상은 FK 요약 정보를 이미 반환하므로 `include`는 상세 컬렉션을 불러올 때 사용.
|
||||
- **Include 확장:** `include` 쿼리로 추가 데이터(`lines`, `customers`, `approval`, `steps`, `histories`, `permissions`, `users` 등) 선택 가능. 포함 대상은 FK 요약 정보를 이미 반환하므로 `include`는 상세 컬렉션을 불러올 때 사용.
|
||||
- **배열 입력:** 트랜잭션 라인, 트랜잭션 고객, 결재 단계, 그룹 메뉴 권한 등 다건 작업은 항상 배열(`[]`) 기반으로 요청한다.
|
||||
- **Primary Key 규칙:** Create 요청 바디에는 PK를 포함하지 않는다. Create 응답 및 나머지 모든 요청·응답에는 PK가 포함돼야 한다(경로에 이미 포함된 경우라도 바디 내 `id`를 명시).
|
||||
- **에러 규격:**
|
||||
@@ -130,7 +130,7 @@
|
||||
---
|
||||
|
||||
## 3. 마스터 데이터 API
|
||||
리소스: `/vendors`, `/warehouses`, `/customers`, `/employees`, `/products`, `/menus`, `/groups`, `/zipcodes`
|
||||
리소스: `/vendors`, `/warehouses`, `/customers`, `/users`, `/products`, `/menus`, `/groups`, `/zipcodes`
|
||||
|
||||
> 기본 정렬: 별도 `sort` 파라미터가 없으면 항상 `id` 오름차순으로 응답을 정렬한다. (`order` 기본값도 `asc`)
|
||||
|
||||
@@ -245,20 +245,22 @@
|
||||
|
||||
> `contact_name`은 고객사 담당자 실명. 선택 입력이며 미입력 시 `null`.
|
||||
|
||||
`GET /employees?page=1`
|
||||
`GET /users?page=1`
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_name": "김승인",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인",
|
||||
"email": "approver@example.com",
|
||||
"mobile_no": "010-2222-1111",
|
||||
"phone": "+82-10-2222-1111",
|
||||
"group": {
|
||||
"id": 2,
|
||||
"group_name": "창고 관리자"
|
||||
},
|
||||
"force_password_change": false,
|
||||
"password_updated_at": "2025-01-10T09:00:00Z",
|
||||
"is_active": true,
|
||||
"created_at": "2025-01-02T09:00:00Z",
|
||||
"updated_at": "2025-01-10T11:00:00Z"
|
||||
@@ -270,7 +272,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
`GET /groups?include=permissions,employees`
|
||||
`GET /groups?include=permissions,users`
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
@@ -295,11 +297,11 @@
|
||||
"can_delete": false
|
||||
}
|
||||
],
|
||||
"employees": [
|
||||
"users": [
|
||||
{
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_name": "김승인"
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
}
|
||||
],
|
||||
"created_at": "2025-01-01T00:00:00Z",
|
||||
@@ -405,6 +407,107 @@
|
||||
|
||||
> `zipcodes`는 대량 데이터 특성상 `GET /zipcodes?zipcode=06000&road_name=세종대로` 형태로 조회하며, 응답 항목에는 `zipcode`, `sido`, `sigungu`, `road_name`, `building_main_no` 등 주소 구성 요소가 포함된다.
|
||||
|
||||
### 3.7 사용자 계정 전용 엔드포인트
|
||||
|
||||
#### 3.7.1 관리자 사용자 생성
|
||||
`POST /users`
|
||||
```json
|
||||
{
|
||||
"employee_id": "E2025012",
|
||||
"name": "홍관리",
|
||||
"email": "admin@example.com",
|
||||
"phone": "+82-10-3333-4444",
|
||||
"group_id": 1,
|
||||
"password": "Admin!234"
|
||||
}
|
||||
```
|
||||
응답:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 15,
|
||||
"employee_id": "E2025012",
|
||||
"name": "홍관리",
|
||||
"email": "admin@example.com",
|
||||
"phone": "+82-10-3333-4444",
|
||||
"group": {
|
||||
"id": 1,
|
||||
"group_name": "전사 관리자"
|
||||
},
|
||||
"force_password_change": true,
|
||||
"password_updated_at": "2025-03-10T00:00:00Z",
|
||||
"is_active": true,
|
||||
"created_at": "2025-03-10T00:00:00Z",
|
||||
"updated_at": "2025-03-10T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `employee_id`는 영문/숫자 4~32자(`^[A-Za-z0-9]{4,32}$`)이고 대소문자 구분 없이 유니크해야 한다. 저장 시 대문자로 정규화한다.
|
||||
- `password`는 길이 8~24자, 대/소문자·숫자·특수문자 각 1자 이상을 포함해야 하며 평문은 응답에 포함되지 않는다.
|
||||
- 생성 즉시 `force_password_change=true`로 설정하고 임시 비밀번호 안내 메일을 발송 큐에 적재한다.
|
||||
- 최고 관리자 계정 `terabits`는 삭제하거나 비활성화하지 않으며, 비밀번호 재설정만 허용한다.
|
||||
|
||||
#### 3.7.2 자기 정보 수정
|
||||
`PATCH /users/me`
|
||||
```json
|
||||
{
|
||||
"email": "approver@example.com",
|
||||
"phone": "+82-10-2222-1111",
|
||||
"password": "NewPass!234",
|
||||
"current_password": "TempPass1"
|
||||
}
|
||||
```
|
||||
응답:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 7,
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인",
|
||||
"email": "approver@example.com",
|
||||
"phone": "+82-10-2222-1111",
|
||||
"force_password_change": false,
|
||||
"password_updated_at": "2025-03-11T02:00:00Z",
|
||||
"is_active": true,
|
||||
"created_at": "2025-01-02T09:00:00Z",
|
||||
"updated_at": "2025-03-11T02:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 변경 가능한 필드는 `email`, `phone`, `password` 3개다. `password`를 변경할 때는 `current_password` 필드가 필수다.
|
||||
- 비밀번호 변경이 성공하면 서버는 기존 액세스/리프레시 토큰을 모두 폐기하고, 클라이언트는 로그아웃 팝업을 띄운 뒤 재로그인을 요구해야 한다.
|
||||
|
||||
#### 3.7.3 관리자 사용자 수정
|
||||
`PATCH /users/{id}`
|
||||
```json
|
||||
{
|
||||
"id": 7,
|
||||
"email": "approver@example.com",
|
||||
"phone": "+82-10-2222-1111",
|
||||
"group_id": 2,
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
응답은 갱신된 사용자 단건을 반환한다. 관리자는 `group_id`, `email`, `phone`, `is_active`, `force_password_change`를 변경할 수 있다(비밀번호 직접 수정 불가).
|
||||
|
||||
#### 3.7.4 관리자 비밀번호 재설정
|
||||
`POST /users/{id}/reset-password`
|
||||
|
||||
요청 바디는 비워둔다. 서버는 8자 영문 대소문자+숫자 조합으로 임시 비밀번호를 생성해 이메일 발송 큐에 넣고, 응답으로 상태를 전달한다.
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 7,
|
||||
"employee_id": "E2025001",
|
||||
"email": "approver@example.com",
|
||||
"force_password_change": true,
|
||||
"password_updated_at": "2025-03-11T02:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 임시 비밀번호 값은 응답에 포함하지 않는다. 메일 템플릿에서 안내하며, 관리 콘솔에는 마스킹된 값만 노출한다.
|
||||
- 재설정 시 즉시 기존 세션을 무효화하고, 사용자 최초 로그인 시 비밀번호 변경 화면으로 리다이렉션한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 트랜잭션 API
|
||||
@@ -476,8 +579,8 @@
|
||||
"transaction_date": "2025-09-18",
|
||||
"created_by": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_name": "김승인"
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"note": "창고 입고",
|
||||
"is_active": true,
|
||||
@@ -537,7 +640,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"assigned_at": "2025-09-18T06:05:00Z",
|
||||
@@ -546,7 +649,7 @@
|
||||
},
|
||||
"requester": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"requested_at": "2025-09-18T06:00:00Z",
|
||||
@@ -593,8 +696,8 @@
|
||||
"transaction_date": "2025-09-18",
|
||||
"created_by": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_name": "김승인"
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"note": "창고 입고",
|
||||
"is_active": true,
|
||||
@@ -649,7 +752,7 @@
|
||||
"step_order": 1,
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"status": {
|
||||
@@ -664,7 +767,7 @@
|
||||
},
|
||||
"requester": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"requested_at": "2025-09-18T06:00:00Z",
|
||||
@@ -677,7 +780,7 @@
|
||||
"step_order": 1,
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"status": {
|
||||
@@ -707,7 +810,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"action_at": "2025-09-18T06:00:00Z",
|
||||
@@ -882,7 +985,7 @@
|
||||
"current_step": null,
|
||||
"requester": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"requested_at": "2025-09-18T06:00:00Z",
|
||||
@@ -929,7 +1032,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"assigned_at": "2025-09-18T06:05:00Z",
|
||||
@@ -938,7 +1041,7 @@
|
||||
},
|
||||
"requester": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"requested_at": "2025-09-18T06:00:00Z",
|
||||
@@ -960,7 +1063,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"assigned_at": "2025-09-18T06:05:00Z",
|
||||
@@ -984,7 +1087,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"action_at": "2025-09-18T06:00:00Z",
|
||||
@@ -1028,7 +1131,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"assigned_at": "2025-09-18T06:05:00Z",
|
||||
@@ -1037,7 +1140,7 @@
|
||||
},
|
||||
"requester": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"requested_at": "2025-09-18T06:00:00Z",
|
||||
@@ -1055,7 +1158,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"assigned_at": "2025-09-18T06:05:00Z",
|
||||
@@ -1079,7 +1182,7 @@
|
||||
},
|
||||
"approver": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"action_at": "2025-09-18T06:00:00Z",
|
||||
@@ -1285,7 +1388,7 @@
|
||||
"step_order": 2,
|
||||
"approver": {
|
||||
"id": 34,
|
||||
"employee_no": "E2025003",
|
||||
"employee_id": "E2025003",
|
||||
"name": "최검토"
|
||||
},
|
||||
"status": {
|
||||
@@ -1345,7 +1448,7 @@
|
||||
"step_order": 2,
|
||||
"approver": {
|
||||
"id": 34,
|
||||
"employee_no": "E2025003",
|
||||
"employee_id": "E2025003",
|
||||
"name": "최검토"
|
||||
},
|
||||
"status": {
|
||||
@@ -1413,7 +1516,7 @@
|
||||
},
|
||||
"requester": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"requested_at": "2025-09-18T06:00:00Z",
|
||||
@@ -1445,8 +1548,8 @@
|
||||
"note": "보류 코멘트",
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_name": "박검토"
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"from_status": {
|
||||
"id": 1,
|
||||
@@ -1476,7 +1579,7 @@
|
||||
"step_order": 1,
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"status": {
|
||||
@@ -1524,8 +1627,8 @@
|
||||
"note": "보류 코멘트",
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_name": "박검토"
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"from_status": null,
|
||||
"to_status": {
|
||||
@@ -1550,8 +1653,8 @@
|
||||
"step_order": 1,
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_name": "박검토"
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"status": {
|
||||
"id": 2,
|
||||
@@ -1581,8 +1684,8 @@
|
||||
"description": "입고 결재 2단계",
|
||||
"created_by": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_name": "김승인"
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"is_active": true,
|
||||
"created_at": "2025-01-20T00:00:00Z",
|
||||
@@ -1594,6 +1697,7 @@
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
- `created_by`는 작성자의 `id`, `employee_id`, `name`을 포함하며 `include=` 파라미터 없이도 기본 반환된다.
|
||||
|
||||
### 6.2 단건 조회
|
||||
`GET /approval-templates/3001?include=steps`
|
||||
@@ -1606,8 +1710,8 @@
|
||||
"description": "입고 결재 2단계",
|
||||
"created_by": {
|
||||
"id": 7,
|
||||
"employee_no": "E2025001",
|
||||
"employee_name": "김승인"
|
||||
"employee_id": "E2025001",
|
||||
"name": "김승인"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
@@ -1615,8 +1719,8 @@
|
||||
"step_order": 1,
|
||||
"approver": {
|
||||
"id": 21,
|
||||
"employee_no": "E2025002",
|
||||
"employee_name": "박검토"
|
||||
"employee_id": "E2025002",
|
||||
"name": "박검토"
|
||||
},
|
||||
"note": null
|
||||
}
|
||||
@@ -1733,7 +1837,7 @@
|
||||
"user": {
|
||||
"id": 7,
|
||||
"name": "김승인",
|
||||
"employee_no": "E2025001",
|
||||
"employee_id": "E2025001",
|
||||
"email": "approver@example.com",
|
||||
"primary_group": {
|
||||
"id": 3,
|
||||
@@ -1765,6 +1869,7 @@
|
||||
- 잘못된 자격 증명: `invalid credentials`
|
||||
- 비활성 계정 접근: `account is inactive`
|
||||
- 만료된 토큰: `token expired`
|
||||
- 비밀번호 변경 등으로 무효화된 토큰: `token revoked`
|
||||
- 재사용·서명 오류: `invalid token`
|
||||
|
||||
### 8.1 대시보드 요약
|
||||
@@ -1798,7 +1903,7 @@
|
||||
],
|
||||
"pending_approvals": [
|
||||
{
|
||||
"approval_id": 5005,
|
||||
"approval_id": 5001,
|
||||
"approval_no": "APP-202511100005",
|
||||
"title": "출고 결재",
|
||||
"step_summary": "2단계/3단계 진행중",
|
||||
@@ -1809,8 +1914,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
- `pending_approvals[].approval_id`는 결재 상세 조회(`GET /approvals/{id}`)에 사용되는 `approvals.id` 값을 그대로 노출한다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 구현 참고
|
||||
|
||||
Reference in New Issue
Block a user