Merge branch 'main' of http://114.203.209.83:3000/SamProject/sam-docs
This commit is contained in:
82
dev/changes/20260320_sales_to_mng_rd_migration.md
Normal file
82
dev/changes/20260320_sales_to_mng_rd_migration.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Sales → MNG 연구개발 메뉴 통합 이관
|
||||
|
||||
**날짜:** 2026-03-20
|
||||
**작업자:** Claude Code
|
||||
|
||||
## 변경 개요
|
||||
|
||||
`sales.sam.kr`에서 운영하던 연구개발/기획 관련 페이지 10개를 MNG 백오피스의 연구개발 메뉴 하위로 이관했다. standalone PHP → Laravel Blade 전환, MNG 톤앤매너 적용.
|
||||
|
||||
## 이관 대상 및 매핑
|
||||
|
||||
| # | 원본 (sales) | MNG 라우트 | 메뉴명 |
|
||||
|---|-------------|-----------|--------|
|
||||
| 1 | `debt/index.php` | `/rd/debt-collection` | 채권추심 프로세스 |
|
||||
| 2 | `company/index.php` | `/rd/company-analysis` | 기업 분석 라이브러리 |
|
||||
| 3 | `company/loudsourcing/index.php` | `/rd/company-analysis/loudsourcing` | (하위) 라우드소싱 |
|
||||
| 4 | `company/peoplelife/index.php` | `/rd/company-analysis/peoplelife` | (하위) 피플라이프 |
|
||||
| 5 | `company/looka/index.php` | `/rd/company-analysis/looka` | (하위) Looka vs Brandmark |
|
||||
| 6 | `coocon/index.php` | `/rd/coocon-credit` | 기업신용 쿠콘 |
|
||||
| 7 | `creditreport/index.php` | `/rd/coocon-credit/report` | (하위) 신용분석 리포트 |
|
||||
| 8 | `etc/myoctopus.php` | `/rd/myoctopus` | 문어이미지 선발 |
|
||||
| 9 | `Requestforcorrection/index.php` | `/rd/correction-request` | 경정청구 소개 |
|
||||
| 10 | `barobill/index.php` | `/rd/barobill-plan` | 바로빌 API 기획서 |
|
||||
| 11 | `plan/index.php` | `/rd/auto-quotation` | 견적서 자동기획 |
|
||||
|
||||
## 수정된 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `app/Http/Controllers/RdController.php` | 11개 메소드 추가 (HX-Redirect 패턴) |
|
||||
| `routes/web.php` | rd prefix 그룹에 11개 라우트 추가 |
|
||||
| `resources/views/rd/debt-collection.blade.php` | 채권추심 4-Phase 워크플로우 |
|
||||
| `resources/views/rd/company-analysis/index.blade.php` | 기업분석 목록 (카드 그리드) |
|
||||
| `resources/views/rd/company-analysis/loudsourcing.blade.php` | 라우드소싱 리포트 (탭 4개) |
|
||||
| `resources/views/rd/company-analysis/peoplelife.blade.php` | 피플라이프 리포트 (Chart.js + Plotly) |
|
||||
| `resources/views/rd/company-analysis/looka.blade.php` | Looka vs Brandmark 비교 (탭 4개) |
|
||||
| `resources/views/rd/coocon-credit/index.blade.php` | 쿠콘 기업분석 (Chart.js 3개) |
|
||||
| `resources/views/rd/coocon-credit/report.blade.php` | 신용분석 리포트 (Chart.js 3개 + 시뮬레이터) |
|
||||
| `resources/views/rd/myoctopus.blade.php` | My Octopus 홍보 영상 (Vimeo 6개) |
|
||||
| `resources/views/rd/correction-request.blade.php` | 경정청구 소개 (탭 8개, Chart.js) |
|
||||
| `resources/views/rd/barobill-plan.blade.php` | 바로빌 API 통합기획서 (탭 4개, 모달 3개) |
|
||||
| `resources/views/rd/auto-quotation.blade.php` | 견적서 자동기획 AI 워크플로우 |
|
||||
|
||||
## 스타일 변환 규칙
|
||||
|
||||
| 항목 | 원본 (sales) | MNG 이관 후 |
|
||||
|------|-------------|------------|
|
||||
| 레이아웃 | standalone HTML | `@extends('layouts.app')` |
|
||||
| 아이콘 | Lucide / emoji | Remix Icon (`ri-*`) |
|
||||
| 색상 | `slate-*` | `gray-*` |
|
||||
| 카드 | `rounded-2xl shadow-lg` | `rounded-lg shadow-sm` |
|
||||
| Nav/Footer | 자체 포함 | 레이아웃 제공 (제거) |
|
||||
| CDN | Tailwind/Font CDN | 레이아웃에서 제공 |
|
||||
| HTMX | 없음 | HX-Redirect 패턴 적용 |
|
||||
|
||||
## 가독성 이슈 해결
|
||||
|
||||
JS `innerHTML`로 동적 렌더링되는 요소에서 Tailwind 그라데이션 클래스(`bg-gradient-to-r`, `from-*`, `to-*`)가 빌드에 포함되지 않아 배경이 투명하게 렌더링되는 문제 발생. **inline style**로 배경색과 텍스트 색상을 강제 지정하여 해결.
|
||||
|
||||
```
|
||||
❌ class="bg-gradient-to-r from-teal-600 to-emerald-700 text-white" (JS innerHTML에서 빌드 누락)
|
||||
✅ style="background: linear-gradient(to right, #0d9488, #047857); color: #fff;"
|
||||
```
|
||||
|
||||
## DB 메뉴 등록
|
||||
|
||||
tinker로 `menus` 테이블에 직접 추가 (parent_id: 15556 = 연구개발):
|
||||
|
||||
| sort_order | 메뉴명 | URL |
|
||||
|:----------:|--------|-----|
|
||||
| 0 | 대시보드 | `/rd` |
|
||||
| 9 | 채권추심 프로세스 | `/rd/debt-collection` |
|
||||
| 10 | 기업 분석 라이브러리 | `/rd/company-analysis` |
|
||||
| 11 | 기업신용 쿠콘 | `/rd/coocon-credit` |
|
||||
| 12 | 문어이미지 선발 | `/rd/myoctopus` |
|
||||
| 13 | 경정청구 소개 | `/rd/correction-request` |
|
||||
| 14 | 바로빌 API 기획서 | `/rd/barobill-plan` |
|
||||
| 15 | 견적서 자동기획 | `/rd/auto-quotation` |
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- `system/migration-status.md` — MNG→서비스 이관 현황
|
||||
@@ -44,6 +44,7 @@
|
||||
| Redis | 7.0.15 | 6379 (localhost) | active |
|
||||
| PM2 | 6.0.14 | 3000 (cluster x2), 3100 (fork x1) | active |
|
||||
| Supervisor | - | - | active (queue worker x2) |
|
||||
| Laravel Scheduler | - | - | cron (API + MNG, www-data) |
|
||||
| node_exporter | 1.8.2 | 9100 | active |
|
||||
| Certbot | 2.9.0 | - | timer active |
|
||||
| fail2ban | - | - | active |
|
||||
@@ -133,10 +134,10 @@
|
||||
| IP | 110.10.147.46 |
|
||||
| SSH 별칭 | sam-cicd |
|
||||
| OS | Ubuntu 24.04.4 LTS |
|
||||
| Kernel | 6.8.0-41-generic |
|
||||
| CPU | 4 vCPU |
|
||||
| RAM | 8GB (Swap 4GB) |
|
||||
| Disk | 98GB (사용 15GB / 여유 79GB) |
|
||||
| Kernel | 6.8.0-106-generic |
|
||||
| CPU | 8 vCPU |
|
||||
| RAM | 16GB (Swap 4GB) |
|
||||
| Disk | 98GB (사용 32GB / 여유 61GB) |
|
||||
| 사용자 | hskwon(개발팀장), pro(개발실장/잠금), kkk(개발자/잠금) |
|
||||
|
||||
### 도메인 매핑
|
||||
@@ -166,14 +167,15 @@
|
||||
|
||||
| 서비스 | 할당 | 설정 |
|
||||
|--------|------|------|
|
||||
| Jenkins | ~2.0GB | -Xmx2048m |
|
||||
| MySQL | ~1.5GB | innodb_buffer_pool_size=1536M |
|
||||
| Gitea | ~0.5GB | Go 기반 |
|
||||
| Prometheus | ~0.5GB | retention 30d |
|
||||
| Jenkins | ~1.2GB | -Xmx2048m |
|
||||
| Jenkins Agent | ~0.3GB | local-agent (WebSocket) |
|
||||
| MySQL | ~0.7GB | innodb_buffer_pool_size=1536M |
|
||||
| Gitea | ~0.2GB | Go 기반 |
|
||||
| Prometheus | ~0.1GB | retention 30d |
|
||||
| Grafana | ~0.3GB | - |
|
||||
| Nginx | ~0.1GB | - |
|
||||
| node_exporter | ~10MB | - |
|
||||
| OS + 여유 | ~3.1GB | Swap 4GB |
|
||||
| OS + 여유 | ~13GB | Swap 4GB |
|
||||
|
||||
### 주요 설정 파일
|
||||
|
||||
@@ -194,8 +196,9 @@
|
||||
| MySQL 튜닝 | /etc/mysql/mysql.conf.d/sam-tuning.cnf |
|
||||
| fail2ban 설정 | /etc/fail2ban/ |
|
||||
| SSL 인증서 | /etc/letsencrypt/live/ |
|
||||
| 백업 스크립트 | /home/hskwon/scripts/backup-db.sh |
|
||||
| 백업 저장소 | /home/hskwon/backups/mysql/ |
|
||||
| 백업 스크립트 | /data/scripts/backup-db.sh |
|
||||
| 백업 인증 | /data/scripts/.sam_backup.cnf |
|
||||
| 백업 저장소 | /data/backups/mysql/ |
|
||||
|
||||
### 방화벽 (UFW) 규칙
|
||||
|
||||
@@ -301,7 +304,7 @@
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ CI/CD서버 (2 vCPU / 8GB) │
|
||||
│ CI/CD서버 (8 vCPU / 16GB) │
|
||||
│ Ubuntu 24.04 / IP: 110.10.147.46 │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │
|
||||
|
||||
@@ -226,6 +226,48 @@ sudo supervisorctl update
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
@@ -657,9 +657,12 @@ pipeline {
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
cd /home/webservice/mng/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} &&
|
||||
rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs &&
|
||||
ln -sfn /home/webservice/mng/shared/.env .env &&
|
||||
sudo chmod 640 /home/webservice/mng/shared/.env &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/app storage/app &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/logs storage/logs &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/credentials storage/credentials &&
|
||||
rm -rf storage/fonts && ln -sfn /home/webservice/mng/shared/storage/fonts storage/fonts &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
npm install --prefer-offline &&
|
||||
npm run build &&
|
||||
@@ -716,7 +719,11 @@ git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-manage.gi
|
||||
|
||||
ln -sfn /home/webservice/mng/shared/.env /home/webservice/mng/releases/$RELEASE_ID/.env
|
||||
ln -sfn /home/webservice/mng/shared/storage/app /home/webservice/mng/releases/$RELEASE_ID/storage/app
|
||||
rm -rf /home/webservice/mng/releases/$RELEASE_ID/storage/logs
|
||||
ln -sfn /home/webservice/mng/shared/storage/logs /home/webservice/mng/releases/$RELEASE_ID/storage/logs
|
||||
ln -sfn /home/webservice/mng/shared/storage/credentials /home/webservice/mng/releases/$RELEASE_ID/storage/credentials
|
||||
rm -rf /home/webservice/mng/releases/$RELEASE_ID/storage/fonts
|
||||
ln -sfn /home/webservice/mng/shared/storage/fonts /home/webservice/mng/releases/$RELEASE_ID/storage/fonts
|
||||
|
||||
cd /home/webservice/mng/releases/$RELEASE_ID
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions}
|
||||
|
||||
@@ -4,6 +4,38 @@
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 정책
|
||||
|
||||
### 원칙
|
||||
|
||||
| 대상 DB | 마이그레이션 위치 | 실행 주체 | 비고 |
|
||||
|---------|-----------------|----------|------|
|
||||
| **sam** DB | `api/database/migrations/` | API Jenkinsfile | sam DB 스키마/데이터 변경은 API에서만 |
|
||||
| **codebridge** DB | `mng/database/migrations/` | MNG Jenkinsfile | codebridge DB 변경은 MNG에서만 |
|
||||
|
||||
### 이력 관리
|
||||
|
||||
- API와 MNG 모두 `sam.migrations` 테이블에 이력 기록 (Laravel 기본 동작)
|
||||
- 양쪽에서 `php artisan migrate --force`를 실행해도 **파일명이 겹치지 않으면 충돌 없음**
|
||||
- Laravel은 자기 폴더의 파일만 스캔하고, `sam.migrations`에 이미 있으면 skip
|
||||
|
||||
```
|
||||
sam.migrations 테이블:
|
||||
1 (API 실행) ✓ ← API migrate 시: 이미 있으니 skip
|
||||
2 (API 실행) ✓
|
||||
3 (MNG 실행) ✓ ← API는 이 파일이 없으므로 모름 (무관)
|
||||
4 (API 실행) ✓
|
||||
5 (MNG 실행) ✓ ← MNG migrate 시: 이미 있으니 skip
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
|
||||
- `--force`는 production 환경에서 확인 프롬프트를 건너뛰는 옵션 (순서 충돌과 무관)
|
||||
- 파일명(타임스탬프)이 겹치지 않도록 주의
|
||||
- sam DB를 변경하는 마이그레이션은 **반드시 API에서** 작성 (MNG에서 sam DB 변경 금지)
|
||||
|
||||
---
|
||||
|
||||
## [운영] MySQL 접속
|
||||
|
||||
```bash
|
||||
@@ -42,32 +74,82 @@ mysqldump -u hskwon --single-transaction --routines --triggers --all-databases |
|
||||
mysqldump -u hskwon --single-transaction sam 테이블명 > /tmp/sam_테이블명_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
### [CI/CD] 자동 백업 (운영 DB)
|
||||
### [개발] 자동 백업
|
||||
|
||||
CI/CD 서버 crontab에서 매일 03:00에 원격 백업 수행. sam_backup 사용자로 운영 DB에 접속.
|
||||
개발서버 `/etc/crontab`에서 root로 매일 04:30 실행.
|
||||
|
||||
**스크립트:** /home/hskwon/scripts/backup-db.sh
|
||||
**저장소:** /home/hskwon/backups/mysql/
|
||||
**보존:** 14일
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 스크립트 | `/home/webservice/api/scripts/backup/sam-db-backup.sh` |
|
||||
| 설정 파일 | `/home/webservice/api/scripts/backup/backup.conf` (chmod 600) |
|
||||
| 저장소 | `/data/backup/mysql/daily/YYYY-MM-DD/` |
|
||||
| 대상 DB | sam, sam_stat, codebridge |
|
||||
| 보존 | daily 14일, weekly 28일 (일요일 자동 복사) |
|
||||
| 로그 | `/data/backup/mysql/logs/backup.log` |
|
||||
| 상태 파일 | `/data/backup/mysql/.backup_status` (JSON) |
|
||||
|
||||
```bash
|
||||
# 수동 원격 백업
|
||||
ssh sam-prod "mysqldump --single-transaction --routines sam" | gzip \
|
||||
> /home/hskwon/backups/mysql/sam_production_$(date +%Y%m%d).sql.gz
|
||||
# 수동 실행
|
||||
sudo /home/webservice/api/scripts/backup/sam-db-backup.sh
|
||||
|
||||
# 백업 확인
|
||||
ls -lh /data/backup/mysql/daily/$(date +%Y-%m-%d)/
|
||||
cat /data/backup/mysql/.backup_status
|
||||
|
||||
# 로그 확인
|
||||
tail -20 /data/backup/mysql/logs/backup.log
|
||||
```
|
||||
|
||||
### [CI/CD] Gitea DB 백업
|
||||
### [개발] 수동 백업
|
||||
|
||||
```bash
|
||||
mysqldump --single-transaction --routines --triggers gitea \
|
||||
| gzip > /home/hskwon/backups/mysql/gitea_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
# 개별 DB (codebridge 사용자)
|
||||
mysqldump -ucodebridge -p --single-transaction --routines --triggers --no-tablespaces sam | gzip > /tmp/sam_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
mysqldump -ucodebridge -p --single-transaction --routines --triggers --no-tablespaces codebridge | gzip > /tmp/codebridge_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
```
|
||||
|
||||
### [개발] DB 복구
|
||||
|
||||
```bash
|
||||
# 전체 DB 복구
|
||||
gunzip -c /data/backup/mysql/daily/2026-03-19/sam_20260319_0430.sql.gz | mysql -ucodebridge -p sam
|
||||
|
||||
# sam_stat 복구
|
||||
gunzip -c /data/backup/mysql/daily/2026-03-19/sam_stat_20260319_0430.sql.gz | mysql -ucodebridge -p sam_stat
|
||||
|
||||
# 주간 백업에서 복구 (7일 이전)
|
||||
ls /data/backup/mysql/weekly/
|
||||
gunzip -c /data/backup/mysql/weekly/sam_YYYYMMDD_HHMM_week.sql.gz | mysql -ucodebridge -p sam
|
||||
```
|
||||
|
||||
### [CI/CD] 자동 백업 (운영 DB + Gitea)
|
||||
|
||||
CI/CD 서버 `/etc/crontab`에서 root로 매일 03:00 실행. sam_backup 사용자로 운영 DB에 원격 접속.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 스크립트 | `/data/scripts/backup-db.sh` |
|
||||
| 인증 파일 | `/data/scripts/.sam_backup.cnf` (chmod 600) |
|
||||
| 저장소 | `/data/backups/mysql/` |
|
||||
| 실행 사용자 | root (`/etc/crontab`) |
|
||||
| 대상 DB | gitea (로컬, auth_socket), sam + sam_stat + codebridge (운영 원격) |
|
||||
| 보존 | 14일 |
|
||||
| 로그 | `/data/backups/mysql/backup.log` |
|
||||
|
||||
```bash
|
||||
# 수동 실행
|
||||
/data/scripts/backup-db.sh
|
||||
|
||||
# 백업 확인
|
||||
ls -lht /data/backups/mysql/ | head -10
|
||||
tail -10 /data/backups/mysql/backup.log
|
||||
```
|
||||
|
||||
### 백업 파일 외부 전송
|
||||
|
||||
```bash
|
||||
# 운영서버 -> CI/CD 서버
|
||||
scp /tmp/sam_*.sql.gz sam-cicd:/home/hskwon/backups/mysql/
|
||||
scp /tmp/sam_*.sql.gz sam-cicd:/data/backups/mysql/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -393,6 +393,54 @@ cat /home/webservice/mng/shared/storage/logs/laravel.log
|
||||
|
||||
---
|
||||
|
||||
### MNG 배포 후 storage/logs 권한 500 에러
|
||||
|
||||
**증상:** MNG 배포 직후 finance/barobill 관련 페이지에서 500 에러. Laravel 로그에 에러 기록 없음.
|
||||
|
||||
**원인:** 배포 시 `php artisan migrate --force`가 배포 사용자(hskwon)로 실행되면서 `storage/logs/laravel-YYYY-MM-DD.log` 파일이 hskwon 소유로 생성됨. 이후 웹 요청에서 www-data가 로그 파일에 쓰기 시도 → `Permission denied` → 500 에러.
|
||||
|
||||
**진단:**
|
||||
|
||||
```bash
|
||||
# 로그 파일 소유자 확인 (www-data:webservice여야 정상)
|
||||
ls -la /home/webservice/mng/current/storage/logs/
|
||||
|
||||
# storage/logs가 심링크인지 디렉토리인지 확인
|
||||
stat -c '%F' /home/webservice/mng/current/storage/logs
|
||||
|
||||
# tinker로 에러 재현
|
||||
cd /home/webservice/mng/current && sudo -u www-data php artisan tinker --execute="Log::info('test');"
|
||||
```
|
||||
|
||||
**긴급 조치:**
|
||||
|
||||
```bash
|
||||
sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/laravel-*.log
|
||||
```
|
||||
|
||||
**근본 해결 (2026-03-19 적용):**
|
||||
|
||||
Jenkinsfile에서 `storage/logs`를 디렉토리(`mkdir`)가 아닌 shared 심링크로 생성하도록 수정:
|
||||
|
||||
```bash
|
||||
# 변경 전 (문제 발생)
|
||||
mkdir -p storage/logs && sudo chown -R www-data:webservice storage/logs
|
||||
|
||||
# 변경 후 (심링크 — shared는 www-data 소유이므로 권한 문제 없음)
|
||||
rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs
|
||||
```
|
||||
|
||||
**실제 사례 (2026-03-19):**
|
||||
|
||||
1. `fix: [finance] 더존 3자리→KIS 5자리 계정코드 데이터 마이그레이션` 커밋 배포
|
||||
2. `migrate --force` 실행 시 로그 출력 → `laravel-2026-03-19.log`가 hskwon:hskwon으로 생성
|
||||
3. 이후 웹 요청에서 www-data가 Log::info() 호출 → Permission denied → 500
|
||||
4. Nginx 에러 로그에만 `recv() failed (104: Connection reset by peer)` 기록
|
||||
5. `chown www-data:webservice`로 긴급 조치 후 즉시 해소
|
||||
6. Jenkinsfile 수정으로 재발 방지
|
||||
|
||||
---
|
||||
|
||||
### MNG 전자계약(E-Sign) PDF 서명 합성 오류
|
||||
|
||||
**증상:** 전자계약 완료 후 다운로드한 PDF에 서명/도장/텍스트가 적용되지 않음. DB에서 `signed_file_path`가 null.
|
||||
|
||||
@@ -63,24 +63,85 @@ sudo mysql sam < /path/to/sam_테이블명_백업파일.sql
|
||||
|
||||
---
|
||||
|
||||
## [개발] DB 자동 백업
|
||||
|
||||
### 개요
|
||||
|
||||
개발서버(sam-dev)에서 `/etc/crontab`으로 매일 04:30 자동 백업.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 스케줄 | **매일 04:30** (`/etc/crontab`, root 실행) |
|
||||
| 스크립트 | `/home/webservice/api/scripts/backup/sam-db-backup.sh` |
|
||||
| 설정 파일 | `/home/webservice/api/scripts/backup/backup.conf` (chmod 600) |
|
||||
| 저장소 | `/data/backup/mysql/daily/YYYY-MM-DD/` |
|
||||
| 주간 백업 | `/data/backup/mysql/weekly/` (일요일 자동 복사) |
|
||||
| 대상 DB | sam, sam_stat, codebridge |
|
||||
| 보존 | daily **14일**, weekly **28일** |
|
||||
| 로그 | `/data/backup/mysql/logs/backup.log` |
|
||||
| 상태 파일 | `/data/backup/mysql/.backup_status` (JSON) |
|
||||
|
||||
### 백업 대상
|
||||
|
||||
| DB | 크기 (gzip) | 최소 크기 검증 | 비고 |
|
||||
|----|------------|---------------|------|
|
||||
| sam | ~19MB | 1MB | 메인 개발 DB (285 테이블) |
|
||||
| sam_stat | ~220KB | 100KB | 통계 DB (20 테이블) |
|
||||
| codebridge | ~5.4MB | 100KB | MNG 내부관리 DB (101 테이블) |
|
||||
|
||||
### 수동 실행 및 확인
|
||||
|
||||
```bash
|
||||
# 수동 실행
|
||||
sudo /home/webservice/api/scripts/backup/sam-db-backup.sh
|
||||
|
||||
# 백업 확인
|
||||
ls -lh /data/backup/mysql/daily/$(date +%Y-%m-%d)/
|
||||
|
||||
# 상태 확인 (JSON)
|
||||
cat /data/backup/mysql/.backup_status
|
||||
|
||||
# 로그 확인
|
||||
tail -20 /data/backup/mysql/logs/backup.log
|
||||
```
|
||||
|
||||
### DB 복구
|
||||
|
||||
```bash
|
||||
# sam DB 복구
|
||||
gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/sam_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p sam
|
||||
|
||||
# sam_stat 복구
|
||||
gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/sam_stat_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p sam_stat
|
||||
|
||||
# codebridge 복구
|
||||
gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/codebridge_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p codebridge
|
||||
|
||||
# 주간 백업에서 복구 (7일 이전)
|
||||
ls /data/backup/mysql/weekly/
|
||||
gunzip -c /data/backup/mysql/weekly/sam_YYYYMMDD_HHMM_week.sql.gz | mysql -ucodebridge -p sam
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [CI/CD] Gitea 백업/복구
|
||||
|
||||
### 백업
|
||||
|
||||
```bash
|
||||
# 전체 백업 (저장소 + DB + 설정)
|
||||
sudo mkdir -p /home/hskwon/backups/gitea
|
||||
sudo mkdir -p /data/backups/gitea
|
||||
sudo -u git /usr/local/bin/gitea dump \
|
||||
--config /etc/gitea/app.ini \
|
||||
--tempdir /tmp \
|
||||
--file /home/hskwon/backups/gitea/gitea-dump-$(date +%Y%m%d).zip
|
||||
--file /data/backups/gitea/gitea-dump-$(date +%Y%m%d).zip
|
||||
|
||||
# 저장소만
|
||||
sudo tar czf /home/hskwon/backups/gitea/repos-$(date +%Y%m%d).tar.gz \
|
||||
sudo tar czf /data/backups/gitea/repos-$(date +%Y%m%d).tar.gz \
|
||||
/var/lib/gitea/data/repositories/
|
||||
|
||||
# DB만
|
||||
mysqldump --single-transaction gitea | gzip > /home/hskwon/backups/gitea/gitea-db-$(date +%Y%m%d).sql.gz
|
||||
# DB만 (sudo mysql 사용 — auth_socket)
|
||||
sudo mysqldump --single-transaction --routines --triggers gitea | gzip > /data/backups/gitea/gitea-db-$(date +%Y%m%d).sql.gz
|
||||
```
|
||||
|
||||
### 복구
|
||||
@@ -171,30 +232,31 @@ CI/CD 서버(sam-cicd)에서 운영 서버(sam-prod)의 MySQL DB를 원격으로
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 스케줄 | **매일 03:00** (crontab) |
|
||||
| 스크립트 | `/home/hskwon/scripts/backup-db.sh` |
|
||||
| 인증 정보 | `/home/hskwon/.sam_backup.cnf` (chmod 600) |
|
||||
| 저장소 | `/home/hskwon/backups/mysql/` |
|
||||
| 스케줄 | **매일 03:00** (`/etc/crontab`, root 실행) |
|
||||
| 스크립트 | `/data/scripts/backup-db.sh` (owner: root) |
|
||||
| 인증 정보 | `/data/scripts/.sam_backup.cnf` (owner: root, chmod 600) |
|
||||
| 저장소 | `/data/backups/mysql/` |
|
||||
| 보존 기간 | **14일** (자동 삭제) |
|
||||
| 로그 | `/home/hskwon/backups/mysql/backup.log` |
|
||||
| 로그 | `/data/backups/mysql/backup.log` |
|
||||
|
||||
### 백업 대상
|
||||
|
||||
| DB | 서버 | 사용자 | 크기 (gzip) | 비고 |
|
||||
|----|------|--------|------------|------|
|
||||
| gitea | localhost | root (auth_socket) | ~50KB | Gitea DB |
|
||||
| sam | 211.117.60.189 (운영) | sam_backup | ~9.3MB | 운영 메인 DB (295 테이블) |
|
||||
| sam_stat | 211.117.60.189 (운영) | sam_backup | ~184KB | 통계 DB (20 테이블) |
|
||||
| gitea | localhost | root (auth_socket) | ~508KB | Gitea DB |
|
||||
| sam | 211.117.60.189 (운영) | sam_backup | ~3.5MB | 운영 메인 DB |
|
||||
| sam_stat | 211.117.60.189 (운영) | sam_backup | ~184KB | 통계 DB |
|
||||
| codebridge | 211.117.60.189 (운영) | sam_backup | ~117KB | MNG 내부관리 DB |
|
||||
|
||||
### 백업 스크립트
|
||||
|
||||
```bash
|
||||
# /home/hskwon/scripts/backup-db.sh
|
||||
# /data/scripts/backup-db.sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="/home/hskwon/backups/mysql"
|
||||
BACKUP_CNF="/home/hskwon/.sam_backup.cnf"
|
||||
BACKUP_DIR="/data/backups/mysql"
|
||||
BACKUP_CNF="/data/scripts/.sam_backup.cnf"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
RETENTION_DAYS=14
|
||||
|
||||
@@ -207,7 +269,8 @@ mysqldump --single-transaction --routines --triggers gitea | gzip > $BACKUP_DIR/
|
||||
if [ -f "$BACKUP_CNF" ]; then
|
||||
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam | gzip > $BACKUP_DIR/sam_production_$DATE.sql.gz
|
||||
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam_stat | gzip > $BACKUP_DIR/sam_stat_production_$DATE.sql.gz
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat)" >> $BACKUP_DIR/backup.log
|
||||
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces codebridge | gzip > $BACKUP_DIR/codebridge_production_$DATE.sql.gz
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat + codebridge)" >> $BACKUP_DIR/backup.log
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea only - $BACKUP_CNF not found)" >> $BACKUP_DIR/backup.log
|
||||
fi
|
||||
@@ -219,30 +282,33 @@ find $BACKUP_DIR -name '*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
### 인증 설정
|
||||
|
||||
```ini
|
||||
# /home/hskwon/.sam_backup.cnf (chmod 600)
|
||||
# /data/scripts/.sam_backup.cnf (chmod 600)
|
||||
[client]
|
||||
user=sam_backup
|
||||
password=<백업용_비밀번호>
|
||||
```
|
||||
|
||||
### 크론탭 (sam-cicd 서버, hskwon 유저)
|
||||
### 크론탭 (`/etc/crontab`, root 실행)
|
||||
|
||||
```crontab
|
||||
# SAM DB 백업 (매일 새벽 3시)
|
||||
0 3 * * * /home/hskwon/scripts/backup-db.sh >> /home/hskwon/backups/mysql/backup.log 2>&1
|
||||
# SAM DB 백업 (매일 새벽 3시) - root 실행 (계정 독립)
|
||||
0 3 * * * root /data/scripts/backup-db.sh >> /data/backups/mysql/backup.log 2>&1
|
||||
```
|
||||
|
||||
> root로 실행해야 Gitea DB의 auth_socket 인증이 동작한다.
|
||||
> 특정 사용자 계정에 의존하지 않아 계정 삭제 시에도 영향 없음.
|
||||
|
||||
### 수동 실행 및 확인
|
||||
|
||||
```bash
|
||||
# 수동 백업 실행
|
||||
/home/hskwon/scripts/backup-db.sh
|
||||
/data/scripts/backup-db.sh
|
||||
|
||||
# 백업 파일 확인
|
||||
ls -lht /home/hskwon/backups/mysql/
|
||||
ls -lht /data/backups/mysql/
|
||||
|
||||
# 백업 로그 확인
|
||||
tail -10 /home/hskwon/backups/mysql/backup.log
|
||||
tail -10 /data/backups/mysql/backup.log
|
||||
|
||||
# 크론 스케줄 확인
|
||||
crontab -l
|
||||
@@ -251,11 +317,13 @@ crontab -l
|
||||
### 백업 복원 (CI/CD → 운영)
|
||||
|
||||
```bash
|
||||
# sam DB 복원 (운영 서버에서 실행)
|
||||
gunzip -c /path/to/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam
|
||||
# CI/CD에서 운영서버로 백업 파일 전송
|
||||
scp /data/backups/mysql/sam_production_YYYYMMDD_HHMMSS.sql.gz sam-prod:/tmp/
|
||||
|
||||
# sam_stat DB 복원
|
||||
gunzip -c /path/to/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat
|
||||
# 운영 서버에서 복원
|
||||
gunzip -c /tmp/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam
|
||||
gunzip -c /tmp/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat
|
||||
gunzip -c /tmp/codebridge_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p codebridge
|
||||
```
|
||||
|
||||
### 운영 MySQL 백업 사용자 (운영 서버 설정)
|
||||
|
||||
461
dev/dev_plans/[TODO] deploy-account-migration-plan.md
Normal file
461
dev/dev_plans/[TODO] deploy-account-migration-plan.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# deploy 전용 계정 전환 계획
|
||||
|
||||
> **작성일**: 2026-03-19
|
||||
> **상태**: 계획 수립
|
||||
> **목적**: 개인 계정(hskwon, pro, kkk) 의존 제거 → 시스템 계정(deploy, root, www-data)으로 전환
|
||||
> **대상 서버**: sam-prod, sam-cicd, sam-dev (3대)
|
||||
|
||||
---
|
||||
|
||||
## 1. 현황 요약
|
||||
|
||||
### 개인 계정 의존 항목 (20개)
|
||||
|
||||
| 서버 | 항목 | 현재 계정 | 전환 대상 |
|
||||
|------|------|----------|----------|
|
||||
| **prod** | PM2 프로세스 + startup 서비스 | hskwon | root |
|
||||
| **prod** | Jenkinsfile DEPLOY_USER (api, mng, react, sales) | hskwon | deploy |
|
||||
| **prod** | releases/current 심링크 소유 | hskwon | deploy |
|
||||
| **prod** | /home/webservice/ 디렉토리 소유 | hskwon | root |
|
||||
| **prod** | ecosystem.config.js 소유 | hskwon | root |
|
||||
| **prod** | shared/.env 소유 (api, mng) | hskwon | deploy:webservice |
|
||||
| **prod** | sales/.env (chmod 600) | hskwon | deploy:webservice (640) |
|
||||
| **prod** | backups/ 디렉토리 | hskwon | root |
|
||||
| **cicd** | /data/ 디렉토리 | hskwon:kkk | root:root |
|
||||
| **cicd** | /data/backups/mysql/ 백업 파일 | hskwon:kkk | root:root |
|
||||
| **cicd** | hskwon 빈 crontab 잔존 | hskwon | 삭제 |
|
||||
| **dev** | Laravel scheduler (/etc/crontab) | hskwon | www-data |
|
||||
| **dev** | Gitea cache 정리 (hskwon crontab) | hskwon | root (/etc/crontab) |
|
||||
| **dev** | PM2 프로세스 + startup 서비스 | hskwon | root |
|
||||
| **dev** | /home/webservice/ 디렉토리 소유 | hskwon | root |
|
||||
| **dev** | sales_org/ 소유 | pro | root (또는 삭제) |
|
||||
| **dev** | sam_backup_20260317.sql 임시파일 | pro | 삭제 |
|
||||
| **모든 Jenkinsfile** | DEPLOY_USER = 'hskwon' | hskwon | deploy |
|
||||
| **mng Jenkinsfile** | storage/logs mkdir (심링크 아님) | — | 심링크로 변경 |
|
||||
| **Jenkins credential** | deploy-ssh-key → hskwon 키 | hskwon | deploy 키 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 전환 계획
|
||||
|
||||
### Phase 0: deploy 계정 생성 (서비스 영향 없음)
|
||||
|
||||
> 기존 hskwon 방식이 그대로 동작하는 상태에서 새 계정만 준비
|
||||
|
||||
#### 0-1. 3대 서버에 deploy 계정 생성
|
||||
|
||||
```bash
|
||||
# sam-prod
|
||||
sudo useradd -r -m -s /bin/bash -G webservice -c 'CI/CD Deploy Account' deploy
|
||||
sudo passwd -l deploy # 패스워드 로그인 차단 (SSH 키만 허용)
|
||||
|
||||
# sam-dev
|
||||
sudo useradd -r -m -s /bin/bash -G develop -c 'CI/CD Deploy Account' deploy
|
||||
sudo passwd -l deploy
|
||||
|
||||
# sam-cicd (직접 SSH 접속은 없지만 일관성)
|
||||
sudo useradd -r -m -s /bin/bash -c 'CI/CD Deploy Account' deploy
|
||||
sudo passwd -l deploy
|
||||
```
|
||||
|
||||
#### 0-2. SSH 키 생성 (sam-cicd에서)
|
||||
|
||||
```bash
|
||||
# Jenkins가 사용할 SSH 키 생성
|
||||
sudo -u jenkins ssh-keygen -t ed25519 -f /var/lib/jenkins/.ssh/id_ed25519_deploy -N '' -C 'jenkins-deploy@sam-cicd'
|
||||
```
|
||||
|
||||
#### 0-3. SSH 공개키 배포
|
||||
|
||||
```bash
|
||||
# sam-prod
|
||||
sudo mkdir -p /home/deploy/.ssh
|
||||
sudo cp /var/lib/jenkins/.ssh/id_ed25519_deploy.pub /tmp/deploy_key.pub
|
||||
# (scp로 전송 후)
|
||||
sudo sh -c 'cat /tmp/deploy_key.pub >> /home/deploy/.ssh/authorized_keys'
|
||||
sudo chown -R deploy:deploy /home/deploy/.ssh
|
||||
sudo chmod 700 /home/deploy/.ssh
|
||||
sudo chmod 600 /home/deploy/.ssh/authorized_keys
|
||||
|
||||
# sam-dev (동일)
|
||||
```
|
||||
|
||||
#### 0-4. sudoers 설정 (sam-prod, sam-dev)
|
||||
|
||||
```bash
|
||||
# /etc/sudoers.d/deploy
|
||||
sudo visudo -f /etc/sudoers.d/deploy
|
||||
```
|
||||
|
||||
```
|
||||
# sam-prod용
|
||||
deploy ALL=(ALL) NOPASSWD: /usr/sbin/service php8.4-fpm reload
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.4-fpm
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart php8.4-fpm
|
||||
deploy ALL=(ALL) NOPASSWD: /usr/bin/supervisorctl restart sam-queue-worker\:*
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:webservice *
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/chmod -R 775 *
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/chmod 640 *
|
||||
```
|
||||
|
||||
```
|
||||
# sam-dev용
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.4-fpm
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:webservice *
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:develop *
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/chmod -R 775 *
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/chmod 640 *
|
||||
```
|
||||
|
||||
#### 0-5. SSH 연결 테스트
|
||||
|
||||
```bash
|
||||
# sam-cicd에서 실행
|
||||
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519_deploy deploy@211.117.60.189 'whoami && hostname'
|
||||
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519_deploy deploy@114.203.209.83 'whoami && hostname'
|
||||
```
|
||||
|
||||
#### 0-6. deploy를 webservice 그룹에 추가 확인
|
||||
|
||||
```bash
|
||||
# sam-prod
|
||||
id deploy
|
||||
# → deploy groups: deploy webservice
|
||||
|
||||
# sam-dev
|
||||
id deploy
|
||||
# → deploy groups: deploy develop
|
||||
```
|
||||
|
||||
**Phase 0 완료 기준**: deploy 계정으로 SSH + sudo 테스트 통과. 기존 서비스 영향 **없음**.
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Jenkins 배포 전환 (배포 중단 5분)
|
||||
|
||||
> **작업 시간**: 야간 또는 사용자 없는 시간
|
||||
> **전제**: Phase 0 완료, deploy SSH 연결 확인됨
|
||||
|
||||
#### 1-1. Jenkins credential 추가
|
||||
|
||||
Jenkins 웹 UI → Credentials → Global:
|
||||
- ID: `deploy-ssh-key-v2` (기존 유지한 채 새로 추가)
|
||||
- 유형: SSH Username with private key
|
||||
- Username: `deploy`
|
||||
- Private Key: `/var/lib/jenkins/.ssh/id_ed25519_deploy`
|
||||
|
||||
#### 1-2. Jenkinsfile 수정 (4개 저장소)
|
||||
|
||||
**api/Jenkinsfile:**
|
||||
```groovy
|
||||
environment {
|
||||
DEPLOY_USER = 'deploy' // hskwon → deploy
|
||||
// ...
|
||||
}
|
||||
// sshagent: deploy-ssh-key → deploy-ssh-key-v2
|
||||
```
|
||||
|
||||
**mng/Jenkinsfile:**
|
||||
```groovy
|
||||
environment {
|
||||
DEPLOY_USER = 'deploy' // hskwon → deploy
|
||||
}
|
||||
// + storage/logs 심링크 수정 (500 에러 재발 방지)
|
||||
// 변경 전: mkdir -p ... storage/logs && sudo chown ...
|
||||
// 변경 후: rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs
|
||||
```
|
||||
|
||||
**react/Jenkinsfile, sales/Jenkinsfile:**
|
||||
```groovy
|
||||
environment {
|
||||
DEPLOY_USER = 'deploy' // hskwon → deploy
|
||||
}
|
||||
```
|
||||
|
||||
#### 1-3. Jenkinsfile credential ID 변경
|
||||
|
||||
모든 Jenkinsfile에서:
|
||||
```groovy
|
||||
// 변경 전
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
|
||||
// 변경 후
|
||||
sshagent(credentials: ['deploy-ssh-key-v2']) {
|
||||
```
|
||||
|
||||
#### 1-4. 테스트 배포
|
||||
|
||||
```
|
||||
1. mng develop push → 개발서버 배포 확인
|
||||
2. mng main push → 운영서버 배포 확인
|
||||
3. api main push → Stage → 승인 → Production 확인
|
||||
4. react develop push → 개발서버 확인
|
||||
```
|
||||
|
||||
#### 1-5. 파일 소유권 정리 (sam-prod)
|
||||
|
||||
```bash
|
||||
# /home/webservice 최상위
|
||||
sudo chown root:webservice /home/webservice/
|
||||
sudo chown root:webservice /home/webservice/ecosystem.config.js
|
||||
|
||||
# shared/.env 소유자 → deploy (Jenkins가 배포 시 접근 가능)
|
||||
sudo chown deploy:webservice /home/webservice/api/shared/.env
|
||||
sudo chown deploy:webservice /home/webservice/mng/shared/.env
|
||||
sudo chmod 640 /home/webservice/api/shared/.env
|
||||
sudo chmod 640 /home/webservice/mng/shared/.env
|
||||
|
||||
# sales .env (600 → 640)
|
||||
sudo chown deploy:webservice /home/webservice/sales/.env
|
||||
sudo chmod 640 /home/webservice/sales/.env
|
||||
|
||||
# api, mng, react 디렉토리 (releases/shared 상위)
|
||||
for d in api api-stage mng react react-stage sales landing; do
|
||||
sudo chown deploy:webservice /home/webservice/$d 2>/dev/null
|
||||
done
|
||||
|
||||
# backups
|
||||
sudo chown root:webservice /home/webservice/backups/
|
||||
```
|
||||
|
||||
**Phase 1 완료 기준**: Jenkins → deploy 계정으로 4개 프로젝트 배포 성공. hskwon SSH 키 미사용.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: PM2 전환 (Next.js 다운타임 1~2분)
|
||||
|
||||
> **작업 시간**: 야간 필수 (sam.it.kr 일시 중단)
|
||||
|
||||
#### 2-1. sam-prod PM2 전환
|
||||
|
||||
```bash
|
||||
# 1. 현재 PM2 정지
|
||||
pm2 stop all
|
||||
pm2 kill
|
||||
|
||||
# 2. 기존 PM2 서비스 비활성화
|
||||
sudo systemctl stop pm2-hskwon
|
||||
sudo systemctl disable pm2-hskwon
|
||||
|
||||
# 3. root PM2 서비스 생성
|
||||
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u root --hp /root
|
||||
# → /etc/systemd/system/pm2-root.service 생성
|
||||
|
||||
# 4. ecosystem.config.js로 시작
|
||||
cd /home/webservice && sudo pm2 start ecosystem.config.js
|
||||
sudo pm2 save
|
||||
|
||||
# 5. 서비스 확인
|
||||
sudo pm2 status
|
||||
curl -sI https://sam.it.kr | head -3
|
||||
curl -sI https://stage.sam.it.kr | head -3
|
||||
|
||||
# 6. 이전 서비스 파일 삭제
|
||||
sudo rm /etc/systemd/system/pm2-hskwon.service
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
#### 2-2. sam-dev PM2 전환
|
||||
|
||||
```bash
|
||||
# 동일 절차
|
||||
pm2 stop all && pm2 kill
|
||||
sudo systemctl stop pm2-hskwon && sudo systemctl disable pm2-hskwon
|
||||
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u root --hp /root
|
||||
cd /home/webservice && sudo pm2 start ecosystem.config.js
|
||||
sudo pm2 save
|
||||
sudo rm /etc/systemd/system/pm2-hskwon.service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# 확인
|
||||
sudo pm2 status
|
||||
```
|
||||
|
||||
**Phase 2 완료 기준**: PM2가 root로 실행. `pm2 status`에서 user=root 확인. 부팅 후 자동 복구 테스트 (선택).
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Cron/시스템 정리 (서비스 영향 없음)
|
||||
|
||||
#### 3-1. sam-dev: Laravel scheduler 전환
|
||||
|
||||
```bash
|
||||
# /etc/crontab 수정
|
||||
# 변경 전:
|
||||
# * * * * * hskwon cd /home/webservice/api && php artisan schedule:run >> /dev/null 2>&1
|
||||
# 변경 후:
|
||||
* * * * * www-data cd /home/webservice/api && php artisan schedule:run >> /dev/null 2>&1
|
||||
```
|
||||
|
||||
#### 3-2. sam-dev: Gitea cache 정리 → root crontab
|
||||
|
||||
```bash
|
||||
# hskwon crontab에서 삭제
|
||||
crontab -r # (이미 빈 crontab)
|
||||
|
||||
# /etc/crontab에 추가
|
||||
# 0 4 * * 0 root find /var/lib/gitea/data/repo-archive -type f -mtime +7 -delete 2>/dev/null
|
||||
# 0 4 * * 0 root find /var/lib/gitea/data/repo-archive -type d -empty -delete 2>/dev/null
|
||||
```
|
||||
|
||||
#### 3-3. sam-cicd: 빈 crontab 삭제
|
||||
|
||||
```bash
|
||||
crontab -r # hskwon 빈 crontab 삭제
|
||||
```
|
||||
|
||||
#### 3-4. sam-cicd: /data/ 소유권 변경
|
||||
|
||||
```bash
|
||||
sudo chown -R root:root /data/
|
||||
sudo chown root:root /data/scripts/backup-db.sh
|
||||
sudo chown root:root /data/scripts/.sam_backup.cnf
|
||||
sudo chmod 600 /data/scripts/.sam_backup.cnf
|
||||
```
|
||||
|
||||
#### 3-5. sam-dev: 디렉토리 소유권 정리
|
||||
|
||||
```bash
|
||||
# /home/webservice 최상위
|
||||
sudo chown root:develop /home/webservice/
|
||||
|
||||
# 임시 파일 삭제
|
||||
sudo rm -f /home/webservice/sam_backup_20260317.sql
|
||||
sudo rm -f /home/webservice/demo.tar.gz # 필요 여부 확인
|
||||
|
||||
# sales_org → 필요 여부 확인 후 삭제 또는 소유권 변경
|
||||
sudo chown -R root:develop /home/webservice/sales_org/
|
||||
```
|
||||
|
||||
**Phase 3 완료 기준**: 개인 계정 crontab 전부 비어 있음. /data/, /home/webservice/ root 소유.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 검증 및 문서 업데이트
|
||||
|
||||
#### 4-1. 전체 서버 개인 계정 의존 재점검
|
||||
|
||||
```bash
|
||||
# 3대 서버에서 실행
|
||||
for u in hskwon pro kkk; do
|
||||
echo "=== $u crontab ==="
|
||||
sudo crontab -u $u -l 2>/dev/null || echo 'none'
|
||||
done
|
||||
|
||||
# PM2 소유자 확인
|
||||
pm2 status # user 컬럼 = root
|
||||
|
||||
# 서비스 상태
|
||||
sudo systemctl status pm2-root
|
||||
sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor
|
||||
```
|
||||
|
||||
#### 4-2. ops-manual 문서 업데이트
|
||||
|
||||
- `01-server-overview.md`: 사용자 목록에 deploy 추가, 디렉토리 소유권
|
||||
- `05-deployment.md`: Jenkinsfile DEPLOY_USER=deploy 반영
|
||||
- `09-security.md`: deploy 계정 설명, sudoers 설정
|
||||
- `10-backup-recovery.md`: crontab 실행 주체 확인
|
||||
|
||||
#### 4-3. 이 계획 문서 → 완료 후 삭제
|
||||
|
||||
---
|
||||
|
||||
## 3. 롤백 계획
|
||||
|
||||
| Phase | 롤백 방법 | 소요 시간 |
|
||||
|-------|----------|----------|
|
||||
| Phase 0 | 계정 삭제 (`userdel deploy`) | 1분 |
|
||||
| Phase 1 | Jenkinsfile DEPLOY_USER → hskwon 복원, credential 원복 | 5분 |
|
||||
| Phase 2 | `sudo pm2 kill` → hskwon으로 PM2 재시작 | 2분 |
|
||||
| Phase 3 | crontab, 소유권 원복 | 5분 |
|
||||
|
||||
**핵심**: Phase 1까지는 기존 hskwon 방식이 병행 가능하므로 즉시 롤백 가능.
|
||||
|
||||
---
|
||||
|
||||
## 4. 작업 일정 (제안)
|
||||
|
||||
| Phase | 작업 | 시간대 | 서비스 영향 |
|
||||
|-------|------|--------|-----------|
|
||||
| **0** | deploy 계정 생성 + SSH 키 + sudoers | 업무 시간 | 없음 |
|
||||
| **1** | Jenkinsfile 수정 + 테스트 배포 | 업무 시간 (배포 조율) | 배포 불가 5분 |
|
||||
| **2** | PM2 전환 | **야간** | Next.js 다운 1~2분 |
|
||||
| **3** | Cron/소유권 정리 | 업무 시간 | 없음 |
|
||||
| **4** | 검증 + 문서 | 업무 시간 | 없음 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 함께 수정하는 항목 (Jenkinsfile 수정 시)
|
||||
|
||||
### MNG storage/logs 심링크 수정 (500 에러 재발 방지)
|
||||
|
||||
현재 Jenkinsfile:
|
||||
```bash
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
sudo chown -R www-data:webservice storage/logs &&
|
||||
```
|
||||
|
||||
수정 후:
|
||||
```bash
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} &&
|
||||
rm -rf storage/logs &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/logs storage/logs &&
|
||||
```
|
||||
|
||||
> `migrate --force` 실행 시 deploy 계정으로 로그 파일이 생성되어도, shared/storage/logs는
|
||||
> www-data:webservice 소유이므로 권한 문제 없음.
|
||||
|
||||
### API storage 권한도 동일 패턴 적용
|
||||
|
||||
현재 API Jenkinsfile에서 `sudo chown -R www-data:webservice storage bootstrap/cache`로 해결하고 있으나,
|
||||
storage/logs도 shared 심링크로 통일하는 것이 더 안전.
|
||||
|
||||
---
|
||||
|
||||
## 6. 체크리스트
|
||||
|
||||
### Phase 0
|
||||
- [ ] sam-prod에 deploy 계정 생성 + webservice 그룹
|
||||
- [ ] sam-dev에 deploy 계정 생성 + develop 그룹
|
||||
- [ ] sam-cicd에 deploy 계정 생성
|
||||
- [ ] SSH 키 생성 (sam-cicd jenkins 사용자)
|
||||
- [ ] SSH 공개키 → sam-prod, sam-dev authorized_keys
|
||||
- [ ] sudoers 설정 (sam-prod, sam-dev)
|
||||
- [ ] SSH 연결 테스트 통과
|
||||
|
||||
### Phase 1
|
||||
- [ ] Jenkins credential 추가 (deploy-ssh-key-v2)
|
||||
- [ ] api/Jenkinsfile DEPLOY_USER + credential 변경
|
||||
- [ ] mng/Jenkinsfile DEPLOY_USER + credential + storage/logs 심링크 수정
|
||||
- [ ] react/Jenkinsfile DEPLOY_USER + credential 변경
|
||||
- [ ] sales/Jenkinsfile DEPLOY_USER + credential 변경
|
||||
- [ ] mng 테스트 배포 (develop → 개발서버)
|
||||
- [ ] mng 테스트 배포 (main → 운영서버)
|
||||
- [ ] api 테스트 배포 (main → Stage → Production)
|
||||
- [ ] react 테스트 배포 (develop → 개발서버)
|
||||
- [ ] sam-prod 파일 소유권 정리
|
||||
- [ ] .env 권한 640 확인 (api, mng, sales)
|
||||
|
||||
### Phase 2
|
||||
- [ ] sam-prod PM2 → root 전환 (야간)
|
||||
- [ ] sam.it.kr 접속 확인
|
||||
- [ ] stage.sam.it.kr 접속 확인
|
||||
- [ ] sam-dev PM2 → root 전환
|
||||
- [ ] dev.codebridge-x.com 접속 확인
|
||||
|
||||
### Phase 3
|
||||
- [ ] sam-dev scheduler → www-data
|
||||
- [ ] sam-dev Gitea cache 정리 → root /etc/crontab
|
||||
- [ ] sam-cicd hskwon 빈 crontab 삭제
|
||||
- [ ] sam-cicd /data/ 소유권 → root
|
||||
- [ ] sam-dev 임시 파일 삭제 + 소유권 정리
|
||||
|
||||
### Phase 4
|
||||
- [ ] 전체 서버 개인 계정 의존 재점검 (0건 확인)
|
||||
- [ ] ops-manual 문서 업데이트
|
||||
- [ ] 이 계획 문서 삭제
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-19
|
||||
136
dev/dev_plans/bending-work-step-mapping-plan.md
Normal file
136
dev/dev_plans/bending-work-step-mapping-plan.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# 절곡품 작업단계 매핑 계획
|
||||
|
||||
> **작성일**: 2026-03-20
|
||||
> **상태**: 설계 확정
|
||||
> **담당**: R&D실
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목적
|
||||
|
||||
절곡 공정(P-003)의 작업단계가 모든 절곡 품목에 동일하게 표시되는 문제를 해결한다. BD 코드 접두사에 따라 해당 품목에 필요한 작업단계만 표시한다.
|
||||
|
||||
### 1.2 현재 문제
|
||||
|
||||
```
|
||||
현재: 모든 절곡 작업지시 → [가이드레일 제작, 케이스 제작, 하단마감재 제작, 검사]
|
||||
문제: BD-ST-24 (가이드레일 측면형)에 케이스/하단마감재 단계가 불필요하게 표시됨
|
||||
```
|
||||
|
||||
### 1.3 기대 결과
|
||||
|
||||
```
|
||||
BD-RS-30 (가이드레일 벽면) → [가이드레일 제작, 검사]
|
||||
BD-ST-24 (가이드레일 측면) → [가이드레일 제작, 검사]
|
||||
BD-CF-35 (케이스 전면부) → [케이스 제작, 검사]
|
||||
BD-CB-35 (케이스 린텔부) → [케이스 제작, 검사]
|
||||
BD-BL-35 (하단마감재 스크린) → [하단마감재 제작, 검사]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. BD 코드 분류 체계
|
||||
|
||||
### 2.1 코드 구조
|
||||
|
||||
```
|
||||
BD-{종류코드}{규격코드}-{길이코드}
|
||||
예: BD-RS-30 = BD + R(가이드레일) + S(측면형) + 30(길이)
|
||||
```
|
||||
|
||||
### 2.2 종류코드 → 작업단계 매핑
|
||||
|
||||
| 종류코드 접두사 | 제품 분류 | 필요 작업단계 |
|
||||
|:---:|---|---|
|
||||
| `R` | 가이드레일-벽면형 | `guide_rail`, `inspection` |
|
||||
| `S` | 가이드레일-측면형 | `guide_rail`, `inspection` |
|
||||
| `C` | 케이스 (린텔/전면/점검/후면) | `case`, `inspection` |
|
||||
| `B` | 하단마감재-스크린 | `bottom_finish`, `inspection` |
|
||||
| `T` | 하단마감재-철재 | `bottom_finish`, `inspection` |
|
||||
| `L` | L-Bar | `guide_rail`, `inspection` |
|
||||
| `G` | 연기차단재 | `guide_rail`, `inspection` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 방안: 프론트엔드 코드 매핑
|
||||
|
||||
### 3.1 선택 근거
|
||||
|
||||
| 방안 | 장점 | 단점 |
|
||||
|------|------|------|
|
||||
| ~~A. DB 규칙 기반~~ | 유연 | 마이그레이션, 관리 UI 필요 |
|
||||
| **B. 프론트엔드 코드 매핑** | 빠른 구현, 변경 용이 | BD 코드 체계 변경 시 수정 필요 |
|
||||
|
||||
BD 코드 체계가 안정적이고 종류가 7개로 한정되어 있어 프론트엔드 매핑이 적합하다.
|
||||
|
||||
### 3.2 구현 위치
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `react/src/components/production/WorkOrders/types.ts` | `BENDING_STEP_MAP` 상수 추가 |
|
||||
| `react/src/components/production/WorkOrders/WorkOrderDetail.tsx` | `ProcessStepPills`에 필터링 로직 적용 |
|
||||
|
||||
### 3.3 매핑 상수
|
||||
|
||||
```typescript
|
||||
// BD 코드 접두사 → 필요 작업단계 매핑
|
||||
const BENDING_STEP_MAP: Record<string, string[]> = {
|
||||
'R': ['guide_rail', 'inspection'], // 가이드레일-벽면
|
||||
'S': ['guide_rail', 'inspection'], // 가이드레일-측면
|
||||
'C': ['case', 'inspection'], // 케이스
|
||||
'B': ['bottom_finish', 'inspection'], // 하단마감재-스크린
|
||||
'T': ['bottom_finish', 'inspection'], // 하단마감재-철재
|
||||
'L': ['guide_rail', 'inspection'], // L-Bar
|
||||
'G': ['guide_rail', 'inspection'], // 연기차단재
|
||||
};
|
||||
```
|
||||
|
||||
### 3.4 필터링 로직
|
||||
|
||||
```
|
||||
1. 작업지시의 work_order_items에서 품목 코드(BD-XX-YY) 수집
|
||||
2. BD- 접두사 뒤의 첫 글자(종류코드)로 필요 단계 결정
|
||||
3. 여러 품목이 있으면 필요 단계의 합집합(union)으로 표시
|
||||
4. BD 코드가 아닌 품목이면 전체 단계 표시 (fallback)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 데이터 흐름
|
||||
|
||||
```
|
||||
API: WorkOrder → items[].item_code (BD-RS-30, BD-CF-35 등)
|
||||
↓
|
||||
React: transformApiToFrontend()
|
||||
↓ items에서 BD 코드 추출
|
||||
↓ BENDING_STEP_MAP으로 필요 단계 결정
|
||||
↓
|
||||
ProcessStepPills(filteredSteps)
|
||||
↓ 필요 단계만 렌더링
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 재고생산(STOCK)의 경우
|
||||
|
||||
재고생산은 단일 품목(BD-XX-YY)으로 생성되므로 매핑이 명확하다.
|
||||
|
||||
```
|
||||
STK202603180005 → BD-ST-24 (가이드레일 측면)
|
||||
→ 종류코드 'S' → ['guide_rail', 'inspection']
|
||||
→ 가이드레일 제작 + 검사 만 표시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [재고생산 개편](stock-production-lot-form-plan.md) — 절곡품 LOT 방식
|
||||
- [입고×수입검사 연동](receiving-inspection-integration-plan.md) — 입고-검사-재고 흐름
|
||||
- [재공품 생산 정책](../../rules/wip-production-policy.md) — 재공품 개념
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-20
|
||||
158
dev/dev_plans/module-separation-analysis.md
Normal file
158
dev/dev_plans/module-separation-analysis.md
Normal file
@@ -0,0 +1,158 @@
|
||||
1. 문서 전제 수정 필요
|
||||
|
||||
현재 (부정확):
|
||||
▎ "모든 테넌트에게 모든 메뉴가 보입니다" → 모듈 분리로 해결
|
||||
|
||||
실제:
|
||||
▎ 메뉴 표시/숨김은 권한 시스템이 이미 처리함. 모듈 분리는 권한으로 안 되는 영역을 보완하는 것
|
||||
|
||||
---
|
||||
2. 모듈 분리가 해야 할 것 (권한과 중복 제거)
|
||||
|
||||
┌───────────────────────────┬───────────────────────────────┬───────────────────────────┐
|
||||
│ 항목 │ 현재 모듈 분리 │ 제안 │
|
||||
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
|
||||
│ 사이드바 메뉴 숨김 │ isRouteAllowed로 이중 필터링 │ 제거 — 권한 시스템이 담당 │
|
||||
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
|
||||
│ 라우트 차단 (ModuleGuard) │ PermissionGate 위에 이중 차단 │ 제거 검토 — 권한으로 충분 │
|
||||
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
|
||||
│ 대시보드 섹션 필터링 │ 모듈 기반 섹션 ON/OFF │ 유지 — 권한으로 불가능 │
|
||||
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
|
||||
│ 대시보드 API 호출 스킵 │ 비활성 모듈 API 미호출 │ 유지 — 성능 최적화 │
|
||||
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
|
||||
│ 크로스 모듈 import 규칙 │ 코드 아키텍처 경계 │ 유지 — 코드 품질 │
|
||||
└───────────────────────────┴───────────────────────────────┴───────────────────────────┘
|
||||
|
||||
---
|
||||
3. 두 가지 방향 중 선택
|
||||
|
||||
A안: 모듈 분리를 "대시보드 전용"으로 축소
|
||||
|
||||
- ModuleGuard, 사이드바 필터링 제거
|
||||
- 대시보드 섹션/API 최적화 + 코드 아키텍처 경계만 유지
|
||||
- 메뉴/라우트 접근 제어는 100% 권한 시스템에 위임
|
||||
- 장점: 중복 제거, 단순화
|
||||
- 단점: 권한 미설정 시 불필요한 페이지 접근 가능
|
||||
|
||||
B안: 모듈 분리를 "권한 자동 설정의 상위 레이어"로 재정의
|
||||
|
||||
- industry 설정 시 → 백엔드에서 해당 업종의 메뉴 권한을 자동으로 일괄 설정
|
||||
- 프론트엔드 모듈 분리 코드 대부분 제거 (권한 시스템이 처리하므로)
|
||||
- 대시보드 최적화만 프론트에 유지
|
||||
- 장점: 권한 시스템 하나로 통합, 프론트 코드 단순화
|
||||
- 단점: 백엔드 작업 필요 (industry → 메뉴 권한 매핑 로직)
|
||||
|
||||
---
|
||||
4. 제 추천: B안
|
||||
|
||||
이유:
|
||||
- 권한 시스템이 이미 견고하게 구축되어 있음
|
||||
- 프론트에서 이중으로 제어할 이유가 없음
|
||||
- industry 값은 "새 테넌트 온보딩 시 메뉴 권한 기본값을 자동으로 세팅"하는 용도로 활용
|
||||
- 대시보드 섹션/API 최적화만 프론트에 남기면 깔끔
|
||||
|
||||
현재: industry → 프론트에서 메뉴 숨김 + 라우트 차단 + 대시보드 필터링
|
||||
개선: industry → 백엔드에서 메뉴 권한 자동 설정 + 프론트 대시보드 최적화만
|
||||
|
||||
|
||||
1. 테넌트 온보딩 자동화 (B안 확장)
|
||||
|
||||
현재 새 테넌트 추가 시 메뉴 권한을 수동으로 하나씩 설정해야 합니다.
|
||||
|
||||
현재 흐름:
|
||||
테넌트 생성 → 글로벌 메뉴 동기화 → 역할 생성 → 메뉴 권한 하나씩 설정 (수십 개)
|
||||
|
||||
개선 흐름:
|
||||
테넌트 생성 → industry 선택 → 업종별 메뉴 프리셋 자동 적용
|
||||
|
||||
백엔드에 메뉴 프리셋 테이블 또는 시더 로직 추가:
|
||||
// 예시: industry별 기본 활성 메뉴 세트
|
||||
$presets = [
|
||||
'shutter_mes' => ['대시보드', '판매관리', '생산관리', '품질관리', '차량관리', ...],
|
||||
'construction' => ['대시보드', '판매관리', '시공관리', '차량관리', ...],
|
||||
];
|
||||
|
||||
이러면 테넌트 생성 시 클릭 한 번으로 수십 개 메뉴 권한이 자동 세팅됩니다.
|
||||
|
||||
---
|
||||
2. 대시보드 섹션도 권한 시스템으로 통합 검토
|
||||
|
||||
현재 대시보드 섹션은 모듈 분리에서만 제어하는데, 이것도 메뉴 권한으로 통합 가능합니다.
|
||||
|
||||
현재: 대시보드는 하나의 메뉴 (/dashboard) → 내부 섹션은 모듈 분리가 제어
|
||||
대안: 대시보드 섹션을 가상 메뉴로 등록 → 권한으로 제어
|
||||
|
||||
예:
|
||||
menu: "대시보드 > 생산현황" url: /dashboard#production → view 권한
|
||||
menu: "대시보드 > 시공현황" url: /dashboard#construction → view 권한
|
||||
|
||||
다만 이건 과도할 수 있으니, 대시보드만큼은 모듈 분리(또는 사용자 설정)로 유지하는 게 현실적일 수
|
||||
있습니다. 현재 CEO 대시보드에 섹션 설정 팝업이 이미 있으니까요.
|
||||
|
||||
---
|
||||
3. 프론트엔드 코드 정리 범위
|
||||
|
||||
B안 채택 시 제거 가능한 코드:
|
||||
|
||||
┌───────────────────────────────┬────────────────────────────┬────────────────────────────────┐
|
||||
│ 파일 │ 현재 역할 │ B안 적용 후 │
|
||||
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
|
||||
│ ModuleGuard.tsx │ 라우트 차단 │ 제거 — PermissionGate가 처리 │
|
||||
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
|
||||
│ useModules.ts → │ 사이드바 메뉴 필터링 │ 제거 — 백엔드 메뉴 응답이 이미 │
|
||||
│ isRouteAllowed │ │ 필터링 │
|
||||
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
|
||||
│ useModules.ts → isEnabled │ 컴포넌트 내 분기 │ 대시보드 전용으로 축소 │
|
||||
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
|
||||
│ 각 페이지의 명시적 가드 │ /sales/production-orders │ 제거 — 권한 시스템이 처리 │
|
||||
│ │ 등 │ │
|
||||
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
|
||||
│ verify-module-separation.sh │ 크로스 모듈 import 검증 │ 유지 — 코드 품질 │
|
||||
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
|
||||
│ MODULE.md 경계 마커 │ 모듈 경계 문서 │ 유지 — 개발 가이드 │
|
||||
└───────────────────────────────┴────────────────────────────┴────────────────────────────────┘
|
||||
|
||||
---
|
||||
4. industry 값의 위치 재검토
|
||||
|
||||
현재 문서는 tenant.options.industry에 저장하는데, 이게 맞는지:
|
||||
|
||||
┌────────────────────────────────┬───────────────────────────┬────────────────────┐
|
||||
│ 방식 │ 장점 │ 단점 │
|
||||
├────────────────────────────────┼───────────────────────────┼────────────────────┤
|
||||
│ tenant.options.industry (현재) │ 마이그레이션 불필요, 유연 │ 검색/인덱싱 어려움 │
|
||||
├────────────────────────────────┼───────────────────────────┼────────────────────┤
|
||||
│ tenants.industry 컬럼 추가 │ 검색/인덱싱 가능, 명시적 │ 마이그레이션 필요 │
|
||||
└────────────────────────────────┴───────────────────────────┴────────────────────┘
|
||||
|
||||
B안에서 industry가 메뉴 프리셋 자동 적용의 트리거가 된다면, 별도 컬럼이 더 적절할 수 있습니다.
|
||||
다만 컬럼 추가 정책(FK/조인키만 컬럼)과 충돌하므로 options JSON 유지가 맞을 수도 있고요.
|
||||
|
||||
---
|
||||
5. 향후 SaaS 확장 시 모듈 과금
|
||||
|
||||
지금은 고려 안 해도 되지만, 향후 SAM을 SaaS로 확장할 때:
|
||||
|
||||
Free: 공통 ERP (회계, 인사, 영업)
|
||||
Standard: + 생산관리 OR 시공관리
|
||||
Premium: + 품질관리 + 차량관리 + AI 분석
|
||||
|
||||
이런 구조가 된다면, industry 대신 구독 플랜 기반 모듈 활성화가 필요합니다. 그때는 B안의
|
||||
"백엔드에서 모듈 → 메뉴 권한 자동 매핑" 구조가 그대로 확장 가능합니다.
|
||||
|
||||
---
|
||||
정리: 우선순위
|
||||
|
||||
┌──────┬──────────────────────────────────────────────────┬─────────────┬─────────────┐
|
||||
│ 순위 │ 항목 │ 난이도 │ 효과 │
|
||||
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
|
||||
│ 1 │ 문서 전제 수정 (부정확한 내용 교정) │ 낮음 │ 혼란 방지 │
|
||||
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
|
||||
│ 2 │ 프론트 중복 코드 정리 (ModuleGuard, 명시적 가드) │ 중간 │ 코드 단순화 │
|
||||
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
|
||||
│ 3 │ 테넌트 온보딩 자동화 (industry → 메뉴 프리셋) │ 중간 │ 운영 편의성 │
|
||||
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
|
||||
│ 4 │ 대시보드 최적화 유지 │ 없음 (현행) │ 성능 │
|
||||
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
|
||||
│ 5 │ SaaS 과금 구조 │ 높음 (향후) │ 사업 확장 │
|
||||
└──────┴──────────────────────────────────────────────────┴─────────────┴─────────────┘
|
||||
@@ -1,7 +1,7 @@
|
||||
# 입고등록 × 수입검사 연동 계획
|
||||
|
||||
> **작성일**: 2026-03-17
|
||||
> **상태**: 설계 중
|
||||
> **상태**: 구현 완료 (Phase 1 데이터 매핑 제외)
|
||||
> **담당**: R&D실 (API) + 프론트엔드 개발자 (React)
|
||||
|
||||
---
|
||||
@@ -19,12 +19,13 @@
|
||||
| `DocumentService::resolve()` | ✅ | 품목 ID → 수입검사 템플릿 자동 매칭 |
|
||||
| `ReceivingService::getItemsWithInspectionTemplate()` | ✅ | 입고 목록에서 `has_inspection_template` 플래그 반환 |
|
||||
| React `checkInspectionTemplate()` | ✅ | 입고 상세 로드 시 API 호출 → `hasInspectionTemplate` 상태 설정 |
|
||||
| React 수입검사 버튼 렌더링 | ✅ | `hasInspectionTemplate === true` → "수입검사하기" + "수입검사성적서 보기" 버튼 표시 |
|
||||
| React 수입검사 버튼 렌더링 | ✅ | 템플릿 존재 또는 검사결과 있으면 버튼 표시 |
|
||||
| `ImportInspectionInputModal` | ✅ | 수입검사 입력 모달 (검사항목 동적 로드) |
|
||||
| `InspectionModal` | ✅ | 수입검사 성적서 보기 모달 |
|
||||
| MNG 중복 검증 | ✅ | 동일 category 내 같은 품목 중복 연결 방지 |
|
||||
| 검사완료 → `inspection_completed` 상태 전이 | ✅ | 검사완료 시 자동 상태 변경 |
|
||||
| 검사완료 → 재고 자동 생성 | ✅ | `inspection_completed` 상태에서 Stock/StockLot 자동 생성 |
|
||||
| **품목 ↔ 템플릿 매핑 데이터** | ❌ | `linked_item_ids`에 품목 미연결 (27종 전부) |
|
||||
| 검사결과 → 입고 반영 API | ⚠️ | options에 저장은 되나 상태 전이 보강 필요 |
|
||||
|
||||
### 1.3 핵심 포인트
|
||||
|
||||
@@ -49,25 +50,61 @@ API DocumentService::resolve()
|
||||
└─ 출력: { template: {...}, is_new: true/false }
|
||||
```
|
||||
|
||||
### 2.2 입고 → 수입검사 흐름 (구현됨)
|
||||
### 2.2 입고 → 수입검사 → 재고 흐름 (구현 완료)
|
||||
|
||||
```
|
||||
입고 상세 로드 (ReceivingDetail.tsx)
|
||||
↓ loadData() → getReceivingById(id)
|
||||
↓ result.data.itemId 확인
|
||||
↓ checkInspectionTemplate(itemId) 호출
|
||||
↓ API: GET /api/v1/documents/resolve?category=incoming_inspection&item_id={itemId}
|
||||
입고 등록 (receiving_pending)
|
||||
↓
|
||||
├─ hasTemplate: true
|
||||
│ → hasInspectionTemplate = true
|
||||
│ → customHeaderActions 렌더링:
|
||||
│ [수입검사하기] → ImportInspectionInputModal 오픈
|
||||
│ [수입검사성적서 보기] → InspectionModal 오픈
|
||||
입고 상세 로드 (ReceivingDetail.tsx)
|
||||
↓ checkInspectionTemplate(itemId)
|
||||
↓
|
||||
├─ 템플릿 존재 또는 검사결과 있음
|
||||
│ → [수입검사하기] + [수입검사성적서 보기] 버튼 표시
|
||||
│
|
||||
└─ hasTemplate: false
|
||||
→ 버튼 미표시 (수입검사 불필요)
|
||||
└─ 템플릿 없고 검사결과 없음
|
||||
→ 버튼 미표시
|
||||
↓
|
||||
[수입검사하기] → ImportInspectionInputModal
|
||||
↓ 검사항목 입력 → 검사완료 버튼
|
||||
↓
|
||||
saveInspectionData()
|
||||
↓ Step 1: POST /v1/documents/upsert (검사 데이터 저장)
|
||||
↓ Step 2: PUT /v1/receivings/{id} (status → inspection_completed)
|
||||
↓
|
||||
ReceivingService::update()
|
||||
↓ inspection_completed 감지 → 재고 반영 대상
|
||||
↓ StockService::increaseFromReceiving()
|
||||
↓
|
||||
Stock + StockLot 자동 생성 → 재고현황에 표시
|
||||
```
|
||||
|
||||
### 2.3 상태 흐름 (확정)
|
||||
|
||||
```
|
||||
receiving_pending ──수입검사완료──→ inspection_completed ──(재고 자동 생성)
|
||||
(입고대기) (검사완료)
|
||||
|
||||
receiving_pending ──입고처리──→ completed ──(재고 자동 생성)
|
||||
(입고대기) (입고완료)
|
||||
```
|
||||
|
||||
> **핵심**: `inspection_completed`와 `completed` 두 상태 모두 재고 생성을 트리거한다.
|
||||
|
||||
### 2.4 재고 연동 조건 (`ReceivingService::update()`)
|
||||
|
||||
```php
|
||||
$stockStatuses = ['completed', 'inspection_completed'];
|
||||
$wasCompleted = in_array($oldStatus, $stockStatuses);
|
||||
$isCompletingReceiving = in_array($newStatus, $stockStatuses) && !$wasCompleted;
|
||||
```
|
||||
|
||||
| 상태 변경 | 재고 동작 |
|
||||
|----------|----------|
|
||||
| `receiving_pending` → `inspection_completed` | Stock/StockLot **생성** |
|
||||
| `receiving_pending` → `completed` | Stock/StockLot **생성** |
|
||||
| `inspection_completed` → `receiving_pending` | 재고 **차감** (전량) |
|
||||
| `inspection_completed` → `inspection_completed` (수량 변경) | 재고 **조정** (차이분) |
|
||||
|
||||
> **view/edit 모드에서만 버튼 표시.** new 모드에서는 아직 품목이 저장되지 않았으므로 미표시.
|
||||
|
||||
---
|
||||
@@ -134,55 +171,52 @@ MNG 문서양식관리에서 27종 수입검사 템플릿의 `linked_item_ids`
|
||||
| MNG에서 linked_item_ids 설정 | R&D실 | 각 템플릿 수정 |
|
||||
| 매핑 검증 (누락/중복 체크) | R&D실 | API로 검증 |
|
||||
|
||||
### Phase 2: API 보강 (백엔드, 필요 시)
|
||||
|
||||
> Phase 1 완료 후 실제 검사 플로우를 테스트하며 부족한 부분을 보강한다.
|
||||
### Phase 2: API 보강 (백엔드) — ✅ 완료
|
||||
|
||||
| 작업 | 상태 | 설명 |
|
||||
|------|:----:|------|
|
||||
| `DocumentService::resolve()` | ✅ 완료 | 품목 → 템플릿 매칭 |
|
||||
| `DocumentService::formatTemplateForReact()` | ✅ 완료 | 클로저 스코프 수정 (`$methodCodes`) |
|
||||
| `ReceivingService::getItemsWithInspectionTemplate()` | ✅ 완료 | 입고 목록 `has_inspection_template` 플래그 |
|
||||
| `checkInspectionTemplate()` React 호출 | ✅ 완료 | 입고 상세에서 버튼 표시 제어 |
|
||||
| 수입검사 결과 → Receiving options 반영 | ⚠️ 보강 필요 | 검사완료 시 inspectionDate/Result 자동 설정 |
|
||||
| 입고 상태 전이 (inspection_pending) | ⚠️ 보강 필요 | 검사 시작 → 검사대기, 검사완료 → 입고대기 |
|
||||
| `Receiving::STATUSES` | ✅ 완료 | `inspection_completed` 상태 추가 |
|
||||
| `ReceivingService::update()` | ✅ 완료 | `inspection_completed` 상태에서 재고 자동 생성 |
|
||||
| `StoreReceivingRequest` | ✅ 완료 | `order_qty` required 검증, `inspection_completed` 허용 |
|
||||
| 수입검사 결과 → Receiving options 반영 | ✅ 완료 | `saveInspectionData()`에서 자동 설정 |
|
||||
|
||||
### Phase 3: React UI 보강 (프론트엔드, 필요 시)
|
||||
|
||||
> 버튼 표시 및 모달 연동은 이미 구현됨. 검사 결과 저장 후 입고 데이터 자동 반영 부분만 보강.
|
||||
### Phase 3: React UI 보강 (프론트엔드) — ✅ 완료
|
||||
|
||||
| 작업 | 상태 | 설명 |
|
||||
|------|:----:|------|
|
||||
| 수입검사하기 버튼 | ✅ 완료 | `hasInspectionTemplate` 기반 조건부 렌더링 |
|
||||
| 수입검사하기 버튼 | ✅ 완료 | 템플릿 존재 또는 검사결과 있으면 표시 |
|
||||
| ImportInspectionInputModal | ✅ 완료 | 검사항목 동적 로드 + 입력 |
|
||||
| InspectionModal (성적서 보기) | ✅ 완료 | 저장된 검사 성적서 조회 |
|
||||
| 검사 결과 저장 후 입고 자동 갱신 | ⚠️ 보강 필요 | `handleImportInspectionSave()` → `loadData()` 이미 호출, API 보강 후 자동 반영 |
|
||||
| 검사완료 → `inspection_completed` 상태 전이 | ✅ 완료 | `saveInspectionData()` → status 자동 변경 |
|
||||
| 입고 목록 삭제 기능 | ✅ 완료 | 체크박스 선택 → 삭제 버튼 표시 |
|
||||
| 단위(unit) → API 전달 | ✅ 완료 | `transformFrontendToApi()`에서 `unit` 매핑 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 관련 API 엔드포인트
|
||||
|
||||
### 5.1 기존 (구현 완료)
|
||||
### 5.1 구현 완료
|
||||
|
||||
```
|
||||
GET /api/v1/documents/resolve
|
||||
params: { category: 'incoming_inspection', item_id: 101 }
|
||||
→ 해당 품목의 수입검사 템플릿 + 기존 문서 반환
|
||||
|
||||
POST /api/v1/documents
|
||||
→ 검사 결과 문서 저장
|
||||
POST /api/v1/documents/upsert
|
||||
→ 검사 데이터 저장 (sections/items/field_values)
|
||||
|
||||
PUT /api/v1/receivings/{id}
|
||||
body: { status: 'inspection_completed', inspection_status, inspection_date, inspection_result }
|
||||
→ 상태 변경 + 재고 자동 생성 (inspection_completed 시)
|
||||
|
||||
GET /api/v1/items/{id}
|
||||
→ 응답에 has_inspection_template 포함
|
||||
```
|
||||
|
||||
### 5.2 보강 필요
|
||||
|
||||
```
|
||||
PATCH /api/v1/receivings/{id}/inspection-result (신규)
|
||||
body: { inspection_date, inspection_result, document_id? }
|
||||
→ Receiving.options의 검사일/검사결과 업데이트
|
||||
→ 상태 전이 (inspection_pending → receiving_pending)
|
||||
```
|
||||
> 별도 `PATCH /inspection-result` 엔드포인트는 불필요. `saveInspectionData()`가 `documents/upsert` + `receivings/{id}` PUT 2단계로 처리한다.
|
||||
|
||||
---
|
||||
|
||||
@@ -205,11 +239,15 @@ PATCH /api/v1/receivings/{id}/inspection-result (신규)
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 버튼 렌더링 조건 (ReceivingDetail.tsx:882)
|
||||
### 버튼 렌더링 조건 (ReceivingDetail.tsx)
|
||||
|
||||
```typescript
|
||||
const showInspectionActions = hasInspectionTemplate
|
||||
|| !!detail?.inspectionResult
|
||||
|| !!detail?.inspectionDate;
|
||||
|
||||
const customHeaderActions =
|
||||
(isViewMode || isEditMode) && detail && hasInspectionTemplate ? (
|
||||
(isViewMode || isEditMode) && detail && showInspectionActions ? (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleInspection}>수입검사하기</Button>
|
||||
<Button onClick={handleViewInspectionReport}>수입검사성적서 보기</Button>
|
||||
@@ -220,10 +258,11 @@ const customHeaderActions =
|
||||
| 조건 | 결과 |
|
||||
|------|------|
|
||||
| new 모드 | 버튼 미표시 (품목 미저장) |
|
||||
| view/edit + `hasInspectionTemplate=false` | 버튼 미표시 |
|
||||
| view/edit + 템플릿 없고 검사결과 없음 | 버튼 미표시 |
|
||||
| view/edit + `hasInspectionTemplate=true` | **두 버튼 모두 표시** |
|
||||
| view/edit + 검사결과 또는 검사일 있음 | **두 버튼 모두 표시** (합격 후에도 유지) |
|
||||
|
||||
> **핵심**: MNG에서 해당 품목의 수입검사 템플릿에 `linked_item_ids`를 설정하면 버튼이 자동 표시된다.
|
||||
> **핵심**: 템플릿이 있거나, 이미 검사가 수행된 경우 버튼이 표시된다.
|
||||
|
||||
---
|
||||
|
||||
@@ -236,4 +275,4 @@ const customHeaderActions =
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-17
|
||||
**최종 업데이트**: 2026-03-20
|
||||
|
||||
@@ -1045,17 +1045,47 @@ export default function StorageQuotaBar({ used, limit }) {
|
||||
- 대시보드 추가
|
||||
- 차트 라이브러리 (Chart.js, Recharts)
|
||||
|
||||
### Phase 3 (1년 후)
|
||||
1. **Object Storage 전환**
|
||||
- AWS S3 / Naver Cloud Object Storage
|
||||
- Laravel Flysystem 드라이버 변경
|
||||
- 기존 파일 마이그레이션
|
||||
### Phase 3 — ✅ 완료 (2026-03-20)
|
||||
1. **Object Storage → Cloudflare R2** 전환 완료
|
||||
2. **이미지 서빙 → R2 Presigned URL** 방식 적용
|
||||
3. **CDN** — 미적용 (Cloudflare DNS 등록 필요)
|
||||
|
||||
2. **CDN 연동**
|
||||
- CloudFront / CloudFlare
|
||||
- 이미지 썸네일 자동 생성
|
||||
---
|
||||
|
||||
3. **고급 기능**
|
||||
- 파일 버전 관리
|
||||
- 협업 편집
|
||||
- 파일 잠금
|
||||
## ☁️ R2 파일 서빙 정책 (2026-03-20~)
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
- **파일 저장**: Cloudflare R2 (S3 호환)
|
||||
- **이미지 서빙**: API Resource에서 `image_url` (presigned URL) 반환 → 브라우저가 R2 직접 로드
|
||||
- **프록시 금지**: MNG/React에서 API를 경유한 바이너리 스트리밍 방식 사용하지 않음
|
||||
- **공개 라우트 금지**: 인증 없는 파일 접근 라우트 생성 금지
|
||||
|
||||
### 파일 접근 방식
|
||||
|
||||
```
|
||||
Browser → R2 presigned URL 직접 로드 (1홉)
|
||||
```
|
||||
|
||||
### API Resource 규칙
|
||||
|
||||
`image_file_id`를 반환하는 모든 API Resource는 `image_url`도 함께 반환한다.
|
||||
|
||||
- `File::presignedUrl()` 메서드 사용 (30분 유효)
|
||||
- `temporaryUrl()`은 로컬 서명 생성만 수행 (R2 네트워크 호출 없음)
|
||||
|
||||
### 소비자(MNG, React) 규칙
|
||||
|
||||
- API 응답의 `image_url` 필드를 `<img src>`에 직접 사용
|
||||
- `image_url`이 없으면 `route('files.view', $id)` fallback (MNG FileViewController redirect)
|
||||
|
||||
### 보안
|
||||
|
||||
- 서명 기반 접근: 30분 만료, 만료 후 403
|
||||
- 테넌트 격리: BelongsToTenant 스코프 적용 후 URL 발급
|
||||
- 인증 없는 파일 공개 라우트 금지
|
||||
|
||||
### 향후 개선
|
||||
|
||||
- **R2 Custom Domain**: 도메인을 Cloudflare DNS에 등록하면 CDN 캐시 적용 가능 (10~30ms)
|
||||
- **확대 적용**: 품목, 문서 등 `image_file_id`가 있는 모든 API Resource에 `image_url` 추가
|
||||
|
||||
215
dev/guides/r2-image-proxy-guide.md
Normal file
215
dev/guides/r2-image-proxy-guide.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# R2 이미지 프록시 가이드
|
||||
|
||||
> **작성일**: 2026-03-21
|
||||
> **상태**: 운영 중
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
SAM 프로젝트는 파일 저장소로 **Cloudflare R2** (S3 호환)를 사용한다. MNG에서 R2 이미지를 표시할 때 환경(Docker/서버)과 용도(일반 표시/Canvas 편집/미리보기)에 따라 다른 접근 방식이 필요하다.
|
||||
|
||||
### 핵심 문제
|
||||
|
||||
```
|
||||
브라우저 → R2 직접 접근: CORS 차단 (Canvas에서 사용 불가)
|
||||
Docker 내부 → api.sam.kr: DNS 해석 불가 (500 에러)
|
||||
브라우저 JS → https://nginx: Docker 내부 URL 접근 불가
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 이미지 접근 경로 3가지
|
||||
|
||||
### 2.1 일반 `<img>` 표시 — redirect 방식
|
||||
|
||||
```
|
||||
브라우저 → /files/{id}/view → MNG FileViewController → API presigned URL → 302 redirect → R2
|
||||
```
|
||||
|
||||
- **용도**: 목록/상세 화면의 이미지 표시
|
||||
- **라우트**: `GET /files/{id}/view` → `FileViewController@show`
|
||||
- **동작**: API에서 presigned URL을 받아 브라우저를 R2로 redirect
|
||||
- **장점**: 빠름 (서버에서 이미지 다운로드 안 함)
|
||||
- **제한**: Canvas에서 사용 불가 (redirect 후 cross-origin → tainted canvas)
|
||||
|
||||
### 2.2 Canvas 편집기 — streaming 프록시
|
||||
|
||||
```
|
||||
브라우저 → /files/{id}/proxy → MNG FileViewController → R2 다운로드 → 이미지 스트리밍
|
||||
```
|
||||
|
||||
- **용도**: 절곡품 전개도 Canvas 편집기 (`fabric.Image.fromURL`)
|
||||
- **라우트**: `GET /files/{id}/proxy` → `FileViewController@proxy`
|
||||
- **동작**: MNG 서버가 R2에서 이미지를 다운로드하여 같은 도메인으로 스트리밍
|
||||
- **장점**: CORS 문제 없음, `toDataURL()` 정상 동작
|
||||
- **제한**: `file_id`가 필요 (image_path만 있으면 사용 불가)
|
||||
|
||||
### 2.3 미리보기 모달 — MNG API 프록시
|
||||
|
||||
```
|
||||
브라우저 JS → /api/admin/document-templates/presigned-url-by-path → MNG API → API 서버 → R2 presigned URL 반환
|
||||
```
|
||||
|
||||
- **용도**: 문서양식 미리보기에서 섹션 이미지 (`image_path`만 있는 경우)
|
||||
- **라우트**: `POST /api/admin/document-templates/presigned-url-by-path`
|
||||
- **동작**: 브라우저 JS가 MNG API를 호출 → MNG가 API 서버에 presigned URL 요청 → URL 반환
|
||||
- **장점**: `file_id` 없이 `image_path`로 접근 가능
|
||||
- **주의**: 동기 XHR 사용 (미리보기 렌더링 시 순차 처리)
|
||||
|
||||
---
|
||||
|
||||
## 3. 환경별 설정
|
||||
|
||||
### 3.1 Docker (로컬)
|
||||
|
||||
```env
|
||||
# api/.env
|
||||
R2_ACCESS_KEY_ID=cecd4d4c...
|
||||
R2_SECRET_ACCESS_KEY=f20136ec...
|
||||
R2_BUCKET=sam
|
||||
R2_ENDPOINT=https://caf8dcb2c4ea443018ee5e7a7421db0e.r2.cloudflarestorage.com
|
||||
R2_REGION=auto
|
||||
```
|
||||
|
||||
```env
|
||||
# mng/.env (Docker 내부 통신)
|
||||
API_BASE_URL=https://api.sam.kr
|
||||
API_INTERNAL_URL=https://nginx
|
||||
```
|
||||
|
||||
### 3.2 서버 (개발/운영)
|
||||
|
||||
```env
|
||||
# api/.env — R2 설정 동일
|
||||
# mng/.env
|
||||
API_BASE_URL=https://api.dev.codebridge-x.com
|
||||
# API_INTERNAL_URL 미설정 (직접 접근)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. MNG → API 호출 시 필수 패턴
|
||||
|
||||
MNG에서 API를 호출할 때 `API_INTERNAL_URL` 분기 처리가 **필수**이다.
|
||||
|
||||
```php
|
||||
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
|
||||
$internalUrl = config('services.api.internal_url');
|
||||
|
||||
$headers = [
|
||||
'X-API-KEY' => config('services.api.key'),
|
||||
'X-TENANT-ID' => session('selected_tenant_id', 1),
|
||||
];
|
||||
|
||||
// Docker: nginx 컨테이너 경유, Host 헤더로 서버 블록 라우팅
|
||||
if ($internalUrl) {
|
||||
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
|
||||
$baseUrl = $internalUrl;
|
||||
}
|
||||
|
||||
$response = Http::baseUrl($baseUrl)
|
||||
->withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->timeout(10)
|
||||
->get('/api/v1/...');
|
||||
```
|
||||
|
||||
> 참조 구현: `FormulaApiService::resolveApiConnection()`
|
||||
|
||||
---
|
||||
|
||||
## 5. API 화이트리스트
|
||||
|
||||
MNG에서 Bearer 토큰 없이 호출하는 API는 `ApiKeyMiddleware`의 `allowWithoutAuth`에 등록 필요:
|
||||
|
||||
```
|
||||
api/v1/bending-items 절곡 기초관리
|
||||
api/v1/bending-items/* 절곡 기초관리 상세
|
||||
api/v1/guiderail-models 가이드레일 모델
|
||||
api/v1/guiderail-models/* 가이드레일 모델 상세
|
||||
api/v1/items/*/files 품목 파일
|
||||
api/v1/files/*/presigned-url 파일 presigned URL
|
||||
api/v1/files/presigned-url-by-path 경로 기반 presigned URL
|
||||
```
|
||||
|
||||
**파일 위치**: `api/app/Http/Middleware/ApiKeyMiddleware.php`
|
||||
|
||||
---
|
||||
|
||||
## 6. 트러블슈팅
|
||||
|
||||
### 이미지가 404로 나올 때
|
||||
|
||||
1. **R2 설정 확인**: API `.env`에 `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_ENDPOINT` 존재 여부
|
||||
2. **API 캐시 클리어**: `docker exec sam-api-1 php artisan config:clear`
|
||||
3. **R2 파일 존재 확인**: `Storage::disk('r2')->exists('경로')`
|
||||
|
||||
### 이미지가 401로 나올 때
|
||||
|
||||
1. **화이트리스트 확인**: `ApiKeyMiddleware`의 `allowWithoutAuth`에 해당 라우트 등록 여부
|
||||
2. **X-API-KEY 확인**: `config('services.api.key')` 값이 `api_keys` 테이블에 존재하는지
|
||||
3. **X-TENANT-ID 확인**: `session('selected_tenant_id')` 값
|
||||
|
||||
### Canvas에서 tainted canvas 에러
|
||||
|
||||
1. **프록시 사용 확인**: `/files/{id}/view`(redirect) 대신 `/files/{id}/proxy`(streaming) 사용
|
||||
2. **`data-proxy-url` 속성**: `<img>` 태그에 `data-proxy-url="{{ route('files.proxy', $fileId) }}"` 추가
|
||||
3. **JS에서 프록시 URL 우선**: `current.dataset.proxyUrl || current.src`
|
||||
|
||||
### Docker에서 api.sam.kr 연결 실패 (cURL error 7)
|
||||
|
||||
1. **`API_INTERNAL_URL` 설정**: MNG `.env`에 `API_INTERNAL_URL=https://nginx`
|
||||
2. **Host 헤더 추가**: `$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST)`
|
||||
3. **참조**: `BendingBaseController::api()`, `FileViewController`, `DocumentTemplateController`
|
||||
|
||||
### 미리보기에서 섹션 이미지 안 나올 때
|
||||
|
||||
1. **MNG API 프록시 확인**: `/api/admin/document-templates/presigned-url-by-path` 라우트 존재 여부
|
||||
2. **`image_url` 캐시**: `_previewImageUrl` 함수에서 한 번 조회 후 `section.image_url`에 캐시
|
||||
3. **브라우저 콘솔**: XHR 요청 상태 확인 (200이면 정상, 401이면 화이트리스트, 500이면 R2 설정)
|
||||
|
||||
---
|
||||
|
||||
## 7. 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `mng/app/Http/Controllers/FileViewController.php` | `show`(redirect), `proxy`(streaming) |
|
||||
| `mng/routes/web.php` | `/files/{id}/view`, `/files/{id}/proxy` |
|
||||
| `mng/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php` | `presignedUrlByPath` (미리보기용) |
|
||||
| `mng/resources/views/document-templates/partials/preview-modal.blade.php` | `_previewImageUrl` 함수 |
|
||||
| `mng/app/Http/Controllers/BendingBaseController.php` | `api()` 메서드 (internal_url 패턴) |
|
||||
| `mng/app/Http/Controllers/DocumentTemplateController.php` | `getPresignedUrlFromApi`, `getPresignedUrlByPath` |
|
||||
| `api/app/Http/Middleware/ApiKeyMiddleware.php` | `allowWithoutAuth` 화이트리스트 |
|
||||
| `api/config/filesystems.php` | R2 디스크 설정 (`disks.r2`) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 요약 다이어그램
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Cloudflare R2 │
|
||||
│ (S3 호환 파일 저장소) │
|
||||
└──────────┬───────────────────────┘
|
||||
│ presigned URL
|
||||
┌──────────┴───────────────────────┐
|
||||
│ API 서버 (Laravel) │
|
||||
│ /api/v1/files/{id}/presigned-url │
|
||||
│ /api/v1/files/presigned-url-by-path │
|
||||
└──────────┬───────────────────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌──────────┴──────────┐ ┌──────┴──────┐ ┌──────────┴──────────┐
|
||||
│ /files/{id}/view │ │ /files/{id} │ │ /api/admin/doc-tmpl │
|
||||
│ (redirect → R2) │ │ /proxy │ │ /presigned-url-by- │
|
||||
│ │ │ (streaming) │ │ path (MNG API) │
|
||||
│ 일반 <img> 표시 │ │ Canvas 편집 │ │ 미리보기 모달 │
|
||||
└─────────────────────┘ └─────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-21
|
||||
Reference in New Issue
Block a user