feat: TBM 시스템 구축 및 페이지 권한 관리 기능 추가
## 주요 변경사항 ### 1. TBM (Tool Box Meeting) 시스템 구축 - **데이터베이스 스키마** (5개 테이블 생성) - tbm_sessions: TBM 세션 관리 - tbm_team_assignments: 팀 구성 관리 - tbm_safety_checks: 안전 체크리스트 마스터 (17개 항목) - tbm_safety_records: 안전 체크 기록 - team_handovers: 작업 인계 관리 - **API 엔드포인트** (17개) - TBM 세션 CRUD - 팀 구성 관리 - 안전 체크리스트 - 작업 인계 - 통계 및 리포트 - **프론트엔드** - TBM 관리 페이지 (/pages/work/tbm.html) - 모달 기반 UI (세션 생성, 팀 구성, 안전 체크) ### 2. 페이지 권한 관리 시스템 - 페이지별 접근 권한 설정 기능 - 관리자 페이지 (/pages/admin/page-access.html) - 사용자별 페이지 권한 부여/회수 - TBM 페이지 등록 및 권한 연동 ### 3. 네비게이션 role 표시 버그 수정 - load-navbar.js: case-insensitive role 매칭 적용 - JWT의 "Admin" role이 "관리자"로 정상 표시 - admin-only 메뉴 항목 정상 표시 ### 4. 대시보드 개선 - 작업 현황 테이블 가독성 향상 - 고대비 색상 및 명확한 구분선 적용 - 이모지 제거 및 SVG 아이콘 적용 ### 5. 문서화 - TBM 배포 가이드 작성 (docs/TBM_DEPLOYMENT_GUIDE.md) - 데이터베이스 스키마 상세 기록 - 배포 절차 및 체크리스트 제공 ## 기술 스택 - Backend: Node.js, Express, MySQL - Frontend: Vanilla JavaScript, HTML5, CSS3 - Database: MySQL (InnoDB) ## 파일 변경사항 ### 신규 파일 - api.hyungi.net/db/migrations/20260120000000_create_tbm_system.js - api.hyungi.net/db/migrations/20260120000001_add_tbm_page.js - api.hyungi.net/models/tbmModel.js - api.hyungi.net/models/pageAccessModel.js - api.hyungi.net/controllers/tbmController.js - api.hyungi.net/controllers/pageAccessController.js - api.hyungi.net/routes/tbmRoutes.js - web-ui/pages/work/tbm.html - web-ui/pages/admin/page-access.html - web-ui/js/page-access-management.js - docs/TBM_DEPLOYMENT_GUIDE.md ### 수정 파일 - api.hyungi.net/config/routes.js (TBM 라우트 추가) - web-ui/js/load-navbar.js (role 매칭 버그 수정) - web-ui/pages/admin/workers.html (HTML 구조 수정) - web-ui/pages/dashboard.html (이모지 제거) - web-ui/css/design-system.css (색상 팔레트 추가) - web-ui/css/modern-dashboard.css (가독성 개선) - web-ui/js/modern-dashboard.js (SVG 아이콘 적용) ## 배포 시 주의사항 ⚠️ 본 서버 배포 시 반드시 마이그레이션 실행 필요: ```bash npm run db:migrate ``` 상세한 배포 절차는 docs/TBM_DEPLOYMENT_GUIDE.md 참조 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
393
docs/TBM_DEPLOYMENT_GUIDE.md
Normal file
393
docs/TBM_DEPLOYMENT_GUIDE.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# TBM 시스템 배포 가이드
|
||||
|
||||
## 📋 개요
|
||||
|
||||
TBM (Tool Box Meeting) 시스템은 아침 안전 회의 및 팀 구성 관리를 위한 기능입니다.
|
||||
|
||||
**배포일**: 2026-01-20
|
||||
**버전**: 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 데이터베이스 마이그레이션
|
||||
|
||||
### 필수 마이그레이션 파일
|
||||
|
||||
본 서버에 배포 시 반드시 실행해야 할 마이그레이션:
|
||||
|
||||
1. **`20260120000000_create_tbm_system.js`** - TBM 시스템 테이블 생성
|
||||
2. **`20260120000001_add_tbm_page.js`** - TBM 페이지 등록
|
||||
|
||||
### 생성되는 테이블
|
||||
|
||||
#### 1. `tbm_sessions` - TBM 세션 (아침 미팅)
|
||||
```sql
|
||||
CREATE TABLE `tbm_sessions` (
|
||||
`session_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_date` DATE NOT NULL COMMENT 'TBM 날짜',
|
||||
`leader_id` INT NOT NULL COMMENT '팀장 worker_id',
|
||||
`project_id` INT NULL COMMENT '프로젝트 ID',
|
||||
`work_location` VARCHAR(200) NULL COMMENT '작업 장소',
|
||||
`work_description` TEXT NULL COMMENT '작업 내용',
|
||||
`safety_notes` TEXT NULL COMMENT '안전 관련 특이사항',
|
||||
`status` ENUM('draft', 'completed', 'cancelled') DEFAULT 'draft' COMMENT '상태',
|
||||
`start_time` TIME NULL COMMENT 'TBM 시작 시간',
|
||||
`end_time` TIME NULL COMMENT 'TBM 종료 시간',
|
||||
`created_by` INT NOT NULL COMMENT '생성자 user_id',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_session_date_leader` (`session_date`, `leader_id`),
|
||||
FOREIGN KEY (`leader_id`) REFERENCES `workers` (`worker_id`),
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`) ON DELETE SET NULL,
|
||||
FOREIGN KEY (`created_by`) REFERENCES `users` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
#### 2. `tbm_team_assignments` - TBM 팀 구성
|
||||
```sql
|
||||
CREATE TABLE `tbm_team_assignments` (
|
||||
`assignment_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` INT UNSIGNED NOT NULL COMMENT 'TBM 세션 ID',
|
||||
`worker_id` INT NOT NULL COMMENT '팀원 worker_id',
|
||||
`assigned_role` VARCHAR(100) NULL COMMENT '역할/담당',
|
||||
`work_detail` TEXT NULL COMMENT '세부 작업 내용',
|
||||
`is_present` BOOLEAN DEFAULT TRUE COMMENT '출석 여부',
|
||||
`absence_reason` TEXT NULL COMMENT '결석 사유',
|
||||
`assigned_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uk_session_worker` (`session_id`, `worker_id`),
|
||||
FOREIGN KEY (`session_id`) REFERENCES `tbm_sessions` (`session_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`worker_id`) REFERENCES `workers` (`worker_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
#### 3. `tbm_safety_checks` - 안전 체크리스트 마스터
|
||||
```sql
|
||||
CREATE TABLE `tbm_safety_checks` (
|
||||
`check_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`check_category` VARCHAR(50) NOT NULL COMMENT '카테고리 (PPE, EQUIPMENT, ENVIRONMENT, EMERGENCY)',
|
||||
`check_item` VARCHAR(200) NOT NULL COMMENT '체크 항목',
|
||||
`description` TEXT NULL COMMENT '설명',
|
||||
`display_order` INT DEFAULT 0 COMMENT '표시 순서',
|
||||
`is_required` BOOLEAN DEFAULT TRUE COMMENT '필수 체크 여부',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '활성 여부',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_check_category` (`check_category`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
**초기 데이터 (17개 항목)**:
|
||||
- PPE (개인 보호 장비): 5개
|
||||
- EQUIPMENT (장비 점검): 4개
|
||||
- ENVIRONMENT (작업 환경): 4개
|
||||
- EMERGENCY (비상 대응): 3개
|
||||
|
||||
#### 4. `tbm_safety_records` - 안전 체크 기록
|
||||
```sql
|
||||
CREATE TABLE `tbm_safety_records` (
|
||||
`record_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` INT UNSIGNED NOT NULL COMMENT 'TBM 세션 ID',
|
||||
`check_id` INT UNSIGNED NOT NULL COMMENT '체크 항목 ID',
|
||||
`is_checked` BOOLEAN DEFAULT FALSE COMMENT '체크 여부',
|
||||
`notes` TEXT NULL COMMENT '비고/특이사항',
|
||||
`checked_by` INT NULL COMMENT '체크한 user_id',
|
||||
`checked_at` TIMESTAMP NULL COMMENT '체크 시간',
|
||||
UNIQUE KEY `uk_session_check` (`session_id`, `check_id`),
|
||||
FOREIGN KEY (`session_id`) REFERENCES `tbm_sessions` (`session_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`check_id`) REFERENCES `tbm_safety_checks` (`check_id`),
|
||||
FOREIGN KEY (`checked_by`) REFERENCES `users` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
#### 5. `team_handovers` - 작업 인계
|
||||
```sql
|
||||
CREATE TABLE `team_handovers` (
|
||||
`handover_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` INT UNSIGNED NOT NULL COMMENT 'TBM 세션 ID',
|
||||
`from_leader_id` INT NOT NULL COMMENT '인계자 worker_id',
|
||||
`to_leader_id` INT NOT NULL COMMENT '인수자 worker_id',
|
||||
`handover_date` DATE NOT NULL COMMENT '인계 날짜',
|
||||
`handover_time` TIME NULL COMMENT '인계 시간',
|
||||
`reason` ENUM('half_day', 'early_leave', 'emergency', 'other') NOT NULL COMMENT '인계 사유',
|
||||
`handover_notes` TEXT NULL COMMENT '인계 내용',
|
||||
`worker_ids` TEXT NULL COMMENT '인계하는 작업자 IDs (JSON array)',
|
||||
`is_confirmed` BOOLEAN DEFAULT FALSE COMMENT '인수 확인 여부',
|
||||
`confirmed_at` TIMESTAMP NULL COMMENT '인수 확인 시간',
|
||||
`confirmed_by` INT NULL COMMENT '인수 확인자 user_id',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_session_handover_date` (`session_id`, `handover_date`),
|
||||
FOREIGN KEY (`session_id`) REFERENCES `tbm_sessions` (`session_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`from_leader_id`) REFERENCES `workers` (`worker_id`),
|
||||
FOREIGN KEY (`to_leader_id`) REFERENCES `workers` (`worker_id`),
|
||||
FOREIGN KEY (`confirmed_by`) REFERENCES `users` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 배포 절차
|
||||
|
||||
### 1. 데이터베이스 마이그레이션 실행
|
||||
|
||||
```bash
|
||||
# 본 서버에서 실행
|
||||
cd /path/to/api.hyungi.net
|
||||
|
||||
# 환경 변수 설정 (필요 시)
|
||||
export DB_HOST=your_db_host
|
||||
export DB_PORT=your_db_port
|
||||
export DB_USER=your_db_user
|
||||
export DB_PASSWORD=your_db_password
|
||||
export DB_NAME=hyungi
|
||||
|
||||
# 마이그레이션 실행
|
||||
npm run db:migrate
|
||||
|
||||
# 또는 직접 실행
|
||||
npx knex migrate:latest --knexfile knexfile.js
|
||||
```
|
||||
|
||||
### 2. 마이그레이션 확인
|
||||
|
||||
```bash
|
||||
# 마이그레이션 상태 확인
|
||||
npx knex migrate:status --knexfile knexfile.js
|
||||
|
||||
# 테이블 생성 확인
|
||||
mysql -u root -p -e "SHOW TABLES LIKE 'tbm%'" hyungi
|
||||
mysql -u root -p -e "SHOW TABLES LIKE 'team_handovers'" hyungi
|
||||
|
||||
# 안전 체크리스트 초기 데이터 확인
|
||||
mysql -u root -p -e "SELECT check_category, COUNT(*) as count FROM tbm_safety_checks GROUP BY check_category" hyungi
|
||||
```
|
||||
|
||||
예상 결과:
|
||||
```
|
||||
+----------------+-------+
|
||||
| check_category | count |
|
||||
+----------------+-------+
|
||||
| PPE | 5 |
|
||||
| EQUIPMENT | 4 |
|
||||
| ENVIRONMENT | 4 |
|
||||
| EMERGENCY | 3 |
|
||||
+----------------+-------+
|
||||
```
|
||||
|
||||
### 3. API 서버 재시작
|
||||
|
||||
```bash
|
||||
# PM2 사용 시
|
||||
pm2 restart api-hyungi
|
||||
|
||||
# 또는 직접 재시작
|
||||
pm2 stop api-hyungi
|
||||
pm2 start ecosystem.config.js --env production
|
||||
```
|
||||
|
||||
### 4. 페이지 권한 확인
|
||||
|
||||
```bash
|
||||
# TBM 페이지가 pages 테이블에 등록되었는지 확인
|
||||
mysql -u root -p -e "SELECT * FROM pages WHERE page_key='tbm'\G" hyungi
|
||||
```
|
||||
|
||||
예상 결과:
|
||||
```
|
||||
page_id: [auto_increment]
|
||||
page_key: tbm
|
||||
page_name: TBM 관리
|
||||
page_path: /pages/work/tbm.html
|
||||
category: work
|
||||
description: Tool Box Meeting - 아침 안전 회의 및 팀 구성 관리
|
||||
is_admin_only: 0
|
||||
display_order: 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 API 엔드포인트
|
||||
|
||||
### TBM 세션 관리
|
||||
- `POST /api/tbm/sessions` - TBM 세션 생성
|
||||
- `GET /api/tbm/sessions/date/:date` - 특정 날짜의 TBM 세션 목록
|
||||
- `GET /api/tbm/sessions/:sessionId` - TBM 세션 상세 조회
|
||||
- `PUT /api/tbm/sessions/:sessionId` - TBM 세션 수정
|
||||
- `POST /api/tbm/sessions/:sessionId/complete` - TBM 세션 완료
|
||||
|
||||
### 팀 구성 관리
|
||||
- `POST /api/tbm/sessions/:sessionId/team` - 팀원 추가 (단일)
|
||||
- `POST /api/tbm/sessions/:sessionId/team/batch` - 팀원 일괄 추가
|
||||
- `GET /api/tbm/sessions/:sessionId/team` - 팀 구성 조회
|
||||
- `DELETE /api/tbm/sessions/:sessionId/team/:workerId` - 팀원 제거
|
||||
|
||||
### 안전 체크리스트
|
||||
- `GET /api/tbm/safety-checks` - 모든 안전 체크 항목 조회
|
||||
- `GET /api/tbm/sessions/:sessionId/safety` - 안전 체크 기록 조회
|
||||
- `POST /api/tbm/sessions/:sessionId/safety` - 안전 체크 일괄 저장
|
||||
|
||||
### 작업 인계
|
||||
- `POST /api/tbm/handovers` - 작업 인계 생성
|
||||
- `POST /api/tbm/handovers/:handoverId/confirm` - 작업 인계 확인
|
||||
- `GET /api/tbm/handovers/date/:date` - 특정 날짜의 인계 목록
|
||||
- `GET /api/tbm/handovers/pending` - 나에게 온 미확인 인계 건
|
||||
|
||||
### 통계 및 리포트
|
||||
- `GET /api/tbm/statistics/tbm?startDate=&endDate=` - TBM 통계
|
||||
- `GET /api/tbm/statistics/leaders?startDate=&endDate=` - 리더별 통계
|
||||
|
||||
---
|
||||
|
||||
## 🔐 권한 설정
|
||||
|
||||
### 1. 관리자가 페이지 권한 설정
|
||||
1. 관리자 계정으로 로그인
|
||||
2. `/pages/admin/page-access.html` 접속
|
||||
3. 권한을 부여할 사용자 선택
|
||||
4. "TBM 관리" 페이지 체크
|
||||
5. 저장
|
||||
|
||||
### 2. 기본 권한 (권장)
|
||||
- **그룹장 (Leader)**: TBM 페이지 접근 권한 부여 필요
|
||||
- **관리자 (Admin)**: 자동으로 모든 페이지 접근 가능
|
||||
- **일반 작업자 (User)**: 필요에 따라 부여
|
||||
|
||||
---
|
||||
|
||||
## 📁 파일 구조
|
||||
|
||||
### 백엔드 (API)
|
||||
```
|
||||
api.hyungi.net/
|
||||
├── db/migrations/
|
||||
│ ├── 20260120000000_create_tbm_system.js # TBM 테이블 생성
|
||||
│ └── 20260120000001_add_tbm_page.js # TBM 페이지 등록
|
||||
├── models/
|
||||
│ └── tbmModel.js # TBM 데이터 모델
|
||||
├── controllers/
|
||||
│ └── tbmController.js # TBM 컨트롤러
|
||||
├── routes/
|
||||
│ └── tbmRoutes.js # TBM 라우트
|
||||
└── config/
|
||||
└── routes.js # 라우트 등록 (수정됨)
|
||||
```
|
||||
|
||||
### 프론트엔드 (Web UI)
|
||||
```
|
||||
web-ui/
|
||||
├── pages/work/
|
||||
│ └── tbm.html # TBM 페이지
|
||||
└── js/
|
||||
└── tbm.js # TBM JavaScript (예정)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
### 1. 외래 키 제약 조건
|
||||
- `workers` 테이블의 `worker_id`는 **signed INT(11)**
|
||||
- `users` 테이블의 `user_id`는 **signed INT(11)**
|
||||
- `projects` 테이블의 PK는 `project_id` (NOT `id`)
|
||||
- 외래 키 컬럼은 반드시 **signed INT**로 선언 (unsigned 사용 금지)
|
||||
|
||||
### 2. 데이터 정합성
|
||||
- TBM 세션 삭제 시 관련 팀 구성, 안전 체크 기록 자동 삭제 (CASCADE)
|
||||
- 작업자 삭제 시 관련 TBM 세션도 삭제됨 (workers 테이블의 CASCADE 설정)
|
||||
|
||||
### 3. 백업
|
||||
마이그레이션 실행 전 **반드시 데이터베이스 백업**:
|
||||
```bash
|
||||
mysqldump -u root -p hyungi > backup_before_tbm_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
### 4. 롤백
|
||||
문제 발생 시 롤백:
|
||||
```bash
|
||||
# 한 단계 롤백
|
||||
npx knex migrate:rollback --knexfile knexfile.js
|
||||
|
||||
# 또는 백업 복구
|
||||
mysql -u root -p hyungi < backup_before_tbm_YYYYMMDD_HHMMSS.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 배포 체크리스트
|
||||
|
||||
배포 전 확인:
|
||||
- [ ] 데이터베이스 백업 완료
|
||||
- [ ] 마이그레이션 파일 본 서버에 복사 완료
|
||||
- [ ] 환경 변수 설정 확인 (DB 접속 정보)
|
||||
- [ ] 마이그레이션 실행 완료
|
||||
- [ ] 5개 테이블 생성 확인
|
||||
- [ ] tbm_safety_checks 초기 데이터 (17개) 확인
|
||||
- [ ] pages 테이블에 TBM 페이지 등록 확인
|
||||
- [ ] API 서버 재시작 완료
|
||||
- [ ] API 엔드포인트 테스트 (최소 1개)
|
||||
- [ ] TBM 페이지 접속 테스트
|
||||
- [ ] 권한 설정 테스트 (그룹장 계정)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 테스트 방법
|
||||
|
||||
### 1. API 테스트
|
||||
```bash
|
||||
# 안전 체크 항목 조회
|
||||
curl -X GET http://localhost:20005/api/tbm/safety-checks \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# 오늘 날짜의 TBM 세션 조회
|
||||
curl -X GET http://localhost:20005/api/tbm/sessions/date/2026-01-20 \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
### 2. 웹 페이지 테스트
|
||||
1. 그룹장 계정으로 로그인
|
||||
2. `/pages/work/tbm.html` 접속
|
||||
3. "새 TBM 시작" 버튼 클릭
|
||||
4. TBM 세션 생성 및 팀 구성
|
||||
|
||||
---
|
||||
|
||||
## 📞 문제 해결
|
||||
|
||||
### 문제 1: 마이그레이션 실패 (errno: 150)
|
||||
**원인**: 외래 키 제약 조건 오류 (데이터 타입 불일치)
|
||||
**해결**: 마이그레이션 파일에서 외래 키 컬럼을 signed INT로 수정
|
||||
|
||||
### 문제 2: API 엔드포인트 404
|
||||
**원인**: 라우트 등록 누락 또는 서버 미재시작
|
||||
**해결**:
|
||||
```bash
|
||||
pm2 logs api-hyungi --lines 50 # 로그 확인
|
||||
pm2 restart api-hyungi # 서버 재시작
|
||||
```
|
||||
|
||||
### 문제 3: TBM 페이지 접근 불가
|
||||
**원인**: 페이지 권한 미설정
|
||||
**해결**: 관리자가 `/pages/admin/page-access.html`에서 권한 부여
|
||||
|
||||
---
|
||||
|
||||
## 📝 변경 이력
|
||||
|
||||
### v1.0.0 (2026-01-20)
|
||||
- TBM 시스템 초기 배포
|
||||
- 5개 테이블 생성
|
||||
- 17개 안전 체크리스트 항목
|
||||
- API 엔드포인트 17개
|
||||
- 페이지 권한 시스템 연동
|
||||
|
||||
---
|
||||
|
||||
## 📚 관련 문서
|
||||
|
||||
- [작업자-계정 연동 가이드](./worker-account-integration.md)
|
||||
- [페이지 권한 관리 가이드](./page-access-management.md)
|
||||
- [데이터베이스 스키마](./database-schema.md)
|
||||
|
||||
---
|
||||
|
||||
**작성자**: Claude
|
||||
**최종 수정일**: 2026-01-20
|
||||
Reference in New Issue
Block a user