This commit is contained in:
2026-03-21 19:50:25 +09:00
42 changed files with 4478 additions and 162 deletions

View 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

View 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

View 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 과금 구조 │ 높음 (향후) │ 사업 확장 │
└──────┴──────────────────────────────────────────────────┴─────────────┴─────────────┘

View File

@@ -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