# 3. 운영서버 서비스 관리

[목차로 돌아가기](./README.md) | 서버: sam-prod (211.117.60.189)

---

## Nginx

**명령어:**

```bash
sudo systemctl status nginx
sudo nginx -t                     # 설정 테스트 (반드시 reload/restart 전에 실행)
sudo systemctl reload nginx       # 설정 리로드 (무중단)
sudo systemctl restart nginx      # 재시작 (연결 끊김 발생)
sudo systemctl stop nginx
sudo systemctl start nginx
```

**설정 파일:**

| 파일 | 용도 |
|------|------|
| /etc/nginx/nginx.conf | 메인 설정 (worker_connections 1024, client_max_body_size 50M) |
| /etc/nginx/sites-available/ | 사이트별 설정 |
| /etc/nginx/sites-enabled/ | 활성화된 사이트 (심링크) |
| /etc/nginx/snippets/security.conf | 보안 규칙 (.env, .git 차단) |

**로그 파일:**

| 파일 | 내용 |
|------|------|
| /var/log/nginx/api.sam.it.kr.access.log | API 접근 로그 |
| /var/log/nginx/api.sam.it.kr.error.log | API 에러 로그 |
| /var/log/nginx/sam.it.kr.access.log | 프론트엔드 접근 로그 |
| /var/log/nginx/sam.it.kr.error.log | 프론트엔드 에러 로그 |
| /var/log/nginx/mng.codebridge-x.com.access.log | Admin 접근 로그 |
| /var/log/nginx/mng.codebridge-x.com.error.log | Admin 에러 로그 |
| /var/log/nginx/sales.codebridge-x.com.access.log | Sales 접근 로그 |
| /var/log/nginx/sales.codebridge-x.com.error.log | Sales 에러 로그 |

**주요 설정 값:**

- worker_processes: auto
- worker_connections: 1024
- client_max_body_size: 50M
- keepalive_timeout: 65
- gzip: on (text/plain, application/json, application/javascript, text/css)

---

## PHP-FPM

**명령어:**

```bash
sudo systemctl status php8.4-fpm
sudo systemctl reload php8.4-fpm    # 무중단, 설정 변경 시
sudo systemctl restart php8.4-fpm
sudo systemctl stop php8.4-fpm
sudo systemctl start php8.4-fpm
```

**Pool 설정:**

| Pool | 설정 파일 | 소켓 | max_children | memory_limit |
|------|----------|------|-------------|-------------|
| api | /etc/php/8.4/fpm/pool.d/api.conf | /run/php/php8.4-fpm-api.sock | 10 | 128M |
| admin | /etc/php/8.4/fpm/pool.d/admin.conf | /run/php/php8.4-fpm-admin.sock | 5 | 128M |
| sales | /etc/php/8.4/fpm/pool.d/sales.conf | /run/php/php8.4-fpm-sales.sock | 3 | 128M |
| api-stage | /etc/php/8.4/fpm/pool.d/api-stage.conf | /run/php/php8.4-fpm-api-stage.sock | 3 | 128M |

모든 Pool 공통 설정: upload_max_filesize=50M, post_max_size=50M, display_errors=Off

**로그:** /var/log/php8.4-fpm.log

---

## MySQL

**명령어:**

```bash
sudo systemctl status mysql
sudo systemctl restart mysql       # 주의: 연결 끊김
sudo systemctl stop mysql
sudo systemctl start mysql

# 접속
sudo mysql                          # root (auth_socket)
mysql -u hskwon                     # hskwon (auth_socket, sudo 불필요)
mysql -u codebridge -p sam          # 앱 사용자
```

**설정 파일:**

| 파일 | 용도 |
|------|------|
| /etc/mysql/mysql.conf.d/sam-tuning.cnf | 성능 튜닝 |
| /etc/mysql/mysql.conf.d/mysqld.cnf | 기본 설정 |

**주요 튜닝 값:**

- innodb_buffer_pool_size: 2048M
- innodb_log_file_size: 512M
- innodb_flush_log_at_trx_commit: 2
- max_connections: 100
- slow_query_log: ON (long_query_time: 2s)

**로그:**

| 파일 | 내용 |
|------|------|
| /var/log/mysql/slow.log | 느린 쿼리 (2초 이상) |
| /var/log/mysql/error.log | 에러 로그 |

**데이터베이스:**

| DB 이름 | 용도 |
|---------|------|
| sam | 메인 운영 DB |
| sam_stage | Stage 환경 DB |
| sam_stat | 통계 DB |
| codebridge | Sales 레거시 DB |

---

## Redis

**명령어:**

```bash
sudo systemctl status redis-server
sudo systemctl restart redis-server
sudo systemctl stop redis-server
sudo systemctl start redis-server

redis-cli              # CLI 접속
redis-cli ping         # 연결 테스트 → PONG
```

**설정 파일:** /etc/redis/redis.conf

**주요 설정:**

- bind: 127.0.0.1 ::1 (로컬 전용)
- maxmemory: 512mb
- maxmemory-policy: allkeys-lru
- supervised: systemd

**Redis CLI 유용한 명령어:**

```bash
redis-cli info memory           # 메모리 사용량
redis-cli dbsize                # 키 개수
redis-cli keys '*' | head -20   # 키 확인 (운영 주의)
redis-cli ttl "키이름"           # TTL 확인
redis-cli flushall              # 전체 삭제 (주의: 세션도 삭제됨)
```

**용도:**

| 기능 | 드라이버 | .env 설정 |
|------|---------|----------|
| 캐시 | Redis | CACHE_STORE=redis |
| 세션 | Database | SESSION_DRIVER=database |
| 큐 | Redis | Supervisor에서 `queue:work redis` 명시 |

---

## PM2 (Next.js)

**명령어:**

```bash
pm2 status                              # 전체 상태
pm2 reload sam-front                    # 운영 무중단 재시작 (cluster 모드)
pm2 restart sam-front-stage             # Stage 재시작
pm2 logs sam-front --lines 100          # 로그 확인
pm2 logs sam-front-stage --lines 100
pm2 monit                               # 실시간 CPU/메모리
pm2 describe sam-front                  # 상세 정보
pm2 stop all                            # 전체 정지
pm2 start all                           # 전체 시작
cd /home/webservice && pm2 start ecosystem.config.js  # 설정 파일로 시작
pm2 save                                # 현재 상태 저장 (부팅 시 자동 복구용)
```

**설정 파일:** /home/webservice/ecosystem.config.js

**프로세스 목록:**

| 프로세스명 | 모드 | 인스턴스 | 포트 | 메모리 제한 | 용도 |
|-----------|------|---------|------|-----------|------|
| sam-front | cluster | 2 | 3000 | 300M (max-old-space-size=256) | 운영 프론트엔드 |
| sam-front-stage | fork | 1 | 3100 | 200M (max-old-space-size=128) | Stage 프론트엔드 |

**로그 파일:** ~/.pm2/logs/ (sam-front-out.log, sam-front-error.log 등)

---

## Supervisor (Queue Worker)

**명령어:**

```bash
sudo supervisorctl status                       # 전체 상태
sudo supervisorctl restart sam-queue-worker:*    # 재시작
sudo supervisorctl stop sam-queue-worker:*       # 정지
sudo supervisorctl start sam-queue-worker:*      # 시작
sudo supervisorctl reread                        # 설정 리로드
sudo supervisorctl update
```

**설정 파일:** /etc/supervisor/conf.d/sam-queue.conf

**프로세스 구성:**

- 프로그램명: sam-queue-worker
- 프로세스 수: 2 (numprocs=2)
- 실행 명령: `php /home/webservice/api/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600`
- 실행 사용자: www-data
- 자동 재시작: true

**로그:** /home/webservice/api/shared/storage/logs/queue-worker.log

---

## Laravel Scheduler (cron)

API와 MNG의 스케줄 작업을 `/etc/crontab`에서 www-data로 실행.

**등록 위치:** `/etc/crontab`

```crontab
# Laravel Scheduler - API
* * * * * www-data	cd /home/webservice/api/current && php artisan schedule:run >> /dev/null 2>&1

# Laravel Scheduler - MNG
* * * * * www-data	cd /home/webservice/mng/current && php artisan schedule:run >> /dev/null 2>&1
```

**등록된 스케줄 확인:**

```bash
# API 스케줄 목록
cd /home/webservice/api/current && php artisan schedule:list

# MNG 스케줄 목록
cd /home/webservice/mng/current && php artisan schedule:list
```

**주요 스케줄:**

| 프로젝트 | 명령 | 주기 | 설명 |
|---------|------|------|------|
| API | `api-log:prune` | 매일 03:00 | API 로그 정리 |
| API | `audit:prune` | 매일 03:10 | 감사 로그 정리 |
| API | `stat:aggregate-daily` | 매일 02:00 | 일별 통계 집계 |
| API | `sanctum:prune-expired` | 매일 03:20 | 만료 토큰 정리 |
| API | `storage:cleanup-*` | 매일 03:30~50 | 스토리지 정리 |
| API | `stat:check-kpi-alerts` | 매일 09:00 | KPI 알림 체크 |
| MNG | `attendance:mark-absent` | 매일 23:50 | 미출근 자동 처리 |
| MNG | `barobill:sync-cards` | 2시간마다 | 바로빌 카드거래 동기화 |

> **주의:** 실행 계정은 반드시 `www-data`여야 합니다. 개인 계정(hskwon 등)으로 실행하면
> storage/logs 파일 권한 문제로 500 에러가 발생합니다.

---

## node_exporter

```bash
sudo systemctl status node_exporter
sudo systemctl restart node_exporter
curl -s localhost:9100/metrics | head -20    # 메트릭 확인
```

**포트:** 9100 (UFW에서 CI/CD 서버 IP만 허용)

**역할:** CPU, RAM, 디스크, 네트워크 메트릭을 CI/CD 서버의 Prometheus에 제공.

---

## Certbot (SSL)

```bash
sudo certbot certificates                    # 인증서 목록 및 만료일
sudo systemctl status certbot.timer          # 자동 갱신 타이머
sudo certbot renew --dry-run                 # 갱신 시뮬레이션
sudo certbot renew                           # 수동 갱신
sudo certbot --nginx -d 도메인명 --email develop@codebridge-x.com   # 새 도메인 발급
```

자동 갱신은 systemd 타이머(certbot.timer)가 처리한다. 별도 crontab 불필요.

---

## fail2ban

```bash
sudo systemctl status fail2ban
sudo fail2ban-client status                           # jail 목록
sudo fail2ban-client status sshd                      # SSH jail 상태 (차단 IP 목록)
sudo fail2ban-client set sshd unbanip 차단된_IP주소    # IP 차단 해제
sudo systemctl restart fail2ban
```

**설정 파일:** /etc/fail2ban/jail.local (또는 jail.d/)

---

## UFW (방화벽)

```bash
sudo ufw status verbose         # 상태 확인 (규칙 목록)
sudo ufw status numbered        # 번호로 규칙 목록
sudo ufw allow from IP주소 to any port 포트번호   # 규칙 추가
sudo ufw delete 번호            # 규칙 삭제 (번호 기반)
sudo ufw disable                # 비활성화 (비상시만)
sudo ufw enable                 # 활성화
```

---

## LibreOffice (문서 변환)

API 서버에서 문서 변환(Excel→PDF 등)에 사용. 헤드리스 모드로 동작.

**버전:** 24.2.7.2 (개발/운영 동일)

**명령어:**

```bash
libreoffice --version                    # 버전 확인
libreoffice --headless --convert-to pdf input.xlsx   # CLI 변환 테스트
```

**설치 패키지:**

```bash
sudo apt-get install -y libreoffice-core libreoffice-writer libreoffice-calc libreoffice-impress
```

---

## 폰트

LibreOffice 문서 변환 시 폰트가 없으면 글자가 깨지므로 개발/운영 서버 동일하게 설치 필수.

**설치된 한글 폰트:**

| 폰트 | 설치 방식 | 경로 |
|------|----------|------|
| **Pretendard** (9 웨이트) | 수동 설치 (OTF) | `/usr/local/share/fonts/Pretendard-*.otf` |
| **Nanum** (고딕/명조/스퀘어/손글씨 등) | apt (`fonts-nanum`, `fonts-nanum-extra`) | `/usr/share/fonts/truetype/nanum/` |
| **Noto CJK** (Sans/Serif) | apt (`fonts-noto-cjk`) | `/usr/share/fonts/opentype/noto/` |

**폰트 관리 명령어:**

```bash
fc-list :lang=ko family | sort -u       # 설치된 한글 폰트 목록
fc-list | grep -i pretendard             # Pretendard 설치 확인
sudo fc-cache -fv                        # 폰트 캐시 갱신 (새 폰트 추가 후 필수)
```

**새 폰트 추가 시:**

```bash
# 1. OTF/TTF 파일을 /usr/local/share/fonts/ 에 복사
sudo cp *.otf /usr/local/share/fonts/

# 2. 폰트 캐시 갱신
sudo fc-cache -fv

# 3. 확인
fc-list | grep -i "폰트이름"
```

> **주의:** 개발서버에 폰트를 추가하면 운영서버에도 동일하게 설치해야 변환 결과가 일치한다.

---

## SMTP (메일 발송)

Gmail SMTP를 통해 메일 발송. Google 앱 비밀번호 사용 (2단계 인증 필요).

**프로젝트별 SMTP 설정:**

| 항목 | api | mng |
|------|-----|-----|
| MAIL_HOST | smtp.gmail.com | smtp.gmail.com |
| MAIL_PORT | 587 | 587 |
| MAIL_USERNAME | shine1324@gmail.com | admin@codebridge-x.com |
| MAIL_FROM_ADDRESS | shine1324@gmail.com | develop@codebridge-x.com |
| MAIL_FROM_NAME | ${APP_NAME} | (주)코드브릿지엑스 |
| MAIL_ENCRYPTION | tls | tls |

> **주의:** 개발/운영 서버의 MAIL_PASSWORD(앱 비밀번호)는 반드시 동일하게 유지.
> Google 앱 비밀번호를 재발급하면 모든 서버에 동일하게 반영해야 한다.

**설정 파일 위치:**

| 프로젝트 | 운영 | 개발 |
|---------|------|------|
| api | `/home/webservice/api/shared/.env` | `/home/webservice/api/.env` |
| mng | `/home/webservice/mng/shared/.env` | `/home/webservice/mng/.env` |

**변경 후 반영:**

```bash
# api
cd /home/webservice/api/current && php artisan config:cache

# mng
cd /home/webservice/mng/current && php artisan config:cache
```

**트러블슈팅:**

- `535 Username and Password not accepted` → 앱 비밀번호 만료 또는 불일치. 개발서버 값과 비교 후 동기화
- `Connection refused` → 방화벽에서 587 포트 아웃바운드 차단 여부 확인
- Google 앱 비밀번호 발급: Google 계정 → 보안 → 2단계 인증 → 앱 비밀번호