From 47586a2922f620ba59585819620ffdcd77d3a742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 00:37:24 +0900 Subject: [PATCH 01/69] =?UTF-8?q?docs:CI/CD=20=EC=84=9C=EB=B2=84=204vCPU?= =?UTF-8?q?=20=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81,=20=ED=83=80=EC=9D=B4=ED=8B=80=20=EC=A0=91=EB=91=90?= =?UTF-8?q?=EC=82=AC=20=EC=84=A4=EC=A0=95=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI/CD 서버 CPU 2 vCPU → 4 vCPU 업데이트 - 환경별 타이틀 접두사 ([L]/[D]) 설정 방법 추가 Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/01-server-overview.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/deploys/ops-manual/01-server-overview.md b/deploys/ops-manual/01-server-overview.md index 752081a..4f062a7 100644 --- a/deploys/ops-manual/01-server-overview.md +++ b/deploys/ops-manual/01-server-overview.md @@ -132,7 +132,7 @@ | SSH 별칭 | sam-cicd | | OS | Ubuntu 24.04.4 LTS | | Kernel | 6.8.0-41-generic | -| CPU | 2 vCPU | +| CPU | 4 vCPU | | RAM | 8GB (Swap 4GB) | | Disk | 98GB (사용 15GB / 여유 79GB) | | 사용자 | hskwon (SSH 키 인증, sudo NOPASSWD) | @@ -322,4 +322,22 @@ | API | api.sam.it.kr | stage-api.sam.it.kr | api.codebridge-x.com | | Admin | mng.codebridge-x.com | - | admin.codebridge-x.com | | Sales | sales.codebridge-x.com | - | salesdev.codebridge-x.com | -| Landing | codebridge-x.com | - | - | \ No newline at end of file +| Landing | codebridge-x.com | - | - | + +### 타이틀 접두사 (환경 구분) + +브라우저 탭에서 환경을 즉시 구분할 수 있도록 타이틀에 접두사를 표시한다. + +| 환경 | 접두사 | 예시 | +|------|--------|------| +| 로컬 | `[L]` | `[L]SAM_MNG` | +| 개발 | `[D]` | `[D]SAM_SYSTEM` | +| 운영 | 없음 | `SAM_SYSTEM` | + +**설정 위치:** + +| 프로젝트 | 방식 | 설정 파일 | +|---------|------|----------| +| mng | `.env`의 `APP_NAME`에 접두사 포함 | 로컬: `mng/.env`, 개발: `/home/webservice/mng/.env` | +| api | `.env`의 `APP_NAME`에 접두사 포함 | 로컬: `api/.env`, 개발: `/home/webservice/api/.env` | +| react | 코드에서 `NEXT_PUBLIC_APP_ENV` 값으로 자동 판별 | CI/CD: `/var/lib/jenkins/env-files/react/.env.develop` | \ No newline at end of file From 93803da56f363f474b064bbec52c86874012daad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 00:51:29 +0900 Subject: [PATCH 02/69] =?UTF-8?q?docs:=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=EC=97=90=20=EA=B0=9C=EB=B0=9C=EC=84=9C?= =?UTF-8?q?=EB=B2=84(sam-dev)=20=EC=B6=94=EA=B0=80=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 아키텍처 다이어그램에 개발서버 라인 추가 - Prometheus 스크래핑 설정에 sam-dev job 반영 - 스크래핑 대상 추가 가이드에 node_exporter 설치, 방화벽 허용 절차 보강 Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/07-monitoring.md | 30 ++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/deploys/ops-manual/07-monitoring.md b/deploys/ops-manual/07-monitoring.md index 8a01bea..d68182c 100644 --- a/deploys/ops-manual/07-monitoring.md +++ b/deploys/ops-manual/07-monitoring.md @@ -8,12 +8,14 @@ ``` 운영서버 (node_exporter:9100) --스크래핑--> CI/CD (Prometheus:9090) --> Grafana:3100 +개발서버 (node_exporter:9100) --스크래핑--> CI/CD (Prometheus:9090) --> Grafana:3100 CI/CD (node_exporter:9100) --스크래핑--> CI/CD (Prometheus:9090) --> Grafana:3100 ``` - **Grafana 대시보드:** https://monitor.sam.it.kr - **Prometheus 쿼리:** CI/CD 서버에서 http://localhost:9090 - **운영서버 메트릭:** 운영서버에서 http://localhost:9100/metrics +- **개발서버 메트릭:** 개발서버에서 http://localhost:9100/metrics --- @@ -38,25 +40,39 @@ scrape_configs: - targets: ['localhost:9100'] labels: server: 'cicd' + + - job_name: 'sam-dev' + static_configs: + - targets: ['114.203.209.83:9100'] + labels: + server: 'development' ``` ### 스크래핑 대상 추가 ```bash -# 1. 설정 파일 편집 +# 1. 대상 서버에 node_exporter 설치 (미설치 시) +# 바이너리: https://github.com/prometheus/node_exporter/releases +# 서비스: /etc/systemd/system/node_exporter.service +# 포트: 9100 (기본) + +# 2. 대상 서버 방화벽에서 CI/CD IP 허용 +sudo ufw allow from 110.10.147.46 to any port 9100 comment 'Prometheus scraping from CI/CD' + +# 3. CI/CD 서버에서 설정 파일 편집 sudo vim /etc/prometheus/prometheus.yml -# 2. 새 대상 추가 예시 -# - job_name: 'sam-dev' +# 4. 새 대상 추가 예시 +# - job_name: 'sam-new' # static_configs: -# - targets: ['114.203.209.83:9100'] +# - targets: ['<서버IP>:9100'] # labels: -# server: 'development' +# server: '<환경명>' -# 3. 문법 검사 +# 5. 문법 검사 promtool check config /etc/prometheus/prometheus.yml -# 4. 서비스 리로드 +# 6. 서비스 리로드 sudo systemctl restart prometheus ``` From cba43034eff64eae5c9e992a3b75d0c9c17d7e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 00:55:58 +0900 Subject: [PATCH 03/69] =?UTF-8?q?docs:=EA=B0=9C=EB=B0=9C=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20MySQL=208.0=E2=86=928.4=20=EC=97=85=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/01-server-overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploys/ops-manual/01-server-overview.md b/deploys/ops-manual/01-server-overview.md index 4f062a7..324c64a 100644 --- a/deploys/ops-manual/01-server-overview.md +++ b/deploys/ops-manual/01-server-overview.md @@ -222,7 +222,7 @@ |--------|------|------| | Nginx | 80/443 | active | | Apache | 8080 | active (레거시) | -| MySQL 8.0 | 3306 (localhost) | active | +| MySQL 8.4 | 3306 (localhost) | active | | Gitea | 3000 | active | | Next.js (PM2) | 3001 | active | | fail2ban | - | active | From 7922745bea9409bb26a66916697ac1a611ecb8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 04:33:12 +0900 Subject: [PATCH 04/69] =?UTF-8?q?docs:ops-manual=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EB=B3=B4=EA=B0=95=20=E2=80=94=20DB=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94,=20PM2,=20MySQL=20=EC=97=85?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 10-backup-recovery: 개발→운영 DB 동기화 절차 추가 - 05-deployment: Jenkins env-files에 APP_ENV 컬럼 및 접두사 설명 추가 - 11-server-setup: 개발서버 PM2 설정, MySQL 8.0→8.4 업그레이드 절차 추가 - 11-server-setup: 개발서버 MySQL 버전 8.0.45→8.4.8 반영 Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/05-deployment.md | 12 ++-- deploys/ops-manual/10-backup-recovery.md | 42 +++++++++++++ deploys/ops-manual/11-server-setup.md | 77 +++++++++++++++++++++++- 3 files changed, 125 insertions(+), 6 deletions(-) diff --git a/deploys/ops-manual/05-deployment.md b/deploys/ops-manual/05-deployment.md index e9582dc..be171fa 100644 --- a/deploys/ops-manual/05-deployment.md +++ b/deploys/ops-manual/05-deployment.md @@ -183,11 +183,13 @@ CI/CD Gitea push -> Webhook -> Jenkins **환경변수 파일 (CI/CD 서버):** /var/lib/jenkins/env-files/react/ -| 파일 | API URL | Frontend URL | -|------|---------|-------------| -| .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com | -| .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr | -| .env.main | https://api.sam.it.kr | https://sam.it.kr | +| 파일 | API URL | Frontend URL | APP_ENV | +|------|---------|-------------|---------| +| .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com | development | +| .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr | staging | +| .env.main | https://api.sam.it.kr | https://sam.it.kr | production | + +> `NEXT_PUBLIC_APP_ENV` 값으로 타이틀 접두사 결정: `development` → `[D]`, `local` → `[L]`, 그 외 → 없음 **rsync 주의:** trailing slash 사용 금지: `.next` (O), `.next/` (X) diff --git a/deploys/ops-manual/10-backup-recovery.md b/deploys/ops-manual/10-backup-recovery.md index 0db6f33..59dfbf8 100644 --- a/deploys/ops-manual/10-backup-recovery.md +++ b/deploys/ops-manual/10-backup-recovery.md @@ -322,6 +322,48 @@ cd /home/webservice/api-stage/current && php artisan config:cache && php artisan --- +## [개발→운영] DB 동기화 + +개발서버(sam-dev)의 sam DB를 운영서버(sam-prod)의 sam DB로 복원하는 절차입니다. + +> ⚠️ **운영 데이터가 덮어쓰기됩니다.** 반드시 운영 백업 후 진행하세요. + +### 절차 + +```bash +# 1. 운영 DB 백업 (안전용, 운영 서버) +ssh sam-prod "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/shared/.env | head -1 | cut -d= -f2) && \ + mysqldump -ucodebridge -p\$DB_PASS --no-tablespaces --skip-triggers --skip-routines sam | gzip > /home/webservice/backups/sam_prod_before_sync.sql.gz" + +# 2. 개발 DB 덤프 (개발 서버) +ssh sam-dev "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/.env | head -1 | cut -d= -f2) && \ + mysqldump -ucodebridge -p\$DB_PASS --no-tablespaces --skip-triggers --skip-routines sam | gzip > /tmp/sam_dev.sql.gz" + +# 3. 로컬 경유 전송 (dev→local→prod) +scp sam-dev:/tmp/sam_dev.sql.gz /tmp/sam_dev.sql.gz +scp /tmp/sam_dev.sql.gz sam-prod:/tmp/sam_dev.sql.gz + +# 4. 운영 DB 복원 +ssh sam-prod "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/shared/.env | head -1 | cut -d= -f2) && \ + gunzip -c /tmp/sam_dev.sql.gz | mysql -ucodebridge -p\$DB_PASS sam" + +# 5. 검증 +ssh sam-prod "DB_PASS=\$(grep DB_PASSWORD /home/webservice/mng/shared/.env | head -1 | cut -d= -f2) && \ + mysql -ucodebridge -p\$DB_PASS -e \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='sam';\"" + +# 6. 임시 파일 정리 +ssh sam-dev "rm -f /tmp/sam_dev.sql.gz" +ssh sam-prod "rm -f /tmp/sam_dev.sql.gz" +rm -f /tmp/sam_dev.sql.gz +``` + +### 주의사항 +- 개발↔운영 서버 간 직접 SCP 불가 → 로컬 경유 전송 +- `codebridge` 유저 권한 제한으로 `--no-tablespaces --skip-triggers --skip-routines` 필수 +- 롤백: `/home/webservice/backups/sam_prod_before_sync.sql.gz` 사용 + +--- + ## 전체 서버 복구 절차 ### [운영] 복구 순서 diff --git a/deploys/ops-manual/11-server-setup.md b/deploys/ops-manual/11-server-setup.md index 4ad9a7f..cac90e2 100644 --- a/deploys/ops-manual/11-server-setup.md +++ b/deploys/ops-manual/11-server-setup.md @@ -1188,7 +1188,7 @@ done | OS | Ubuntu 24.04.2 | Ubuntu 24.04 (kernel 6.8.0-100) | | CPU/RAM | 2C / 3.8GB (스왑 없음) | 2C / 8GB + 스왑 4GB | | PHP | 8.4.15 (+ 5.6, 7.3) | 8.4.18 | -| MySQL | **8.0.45** | **8.4.8** | +| MySQL | **8.4.8** | **8.4.8** | | Node.js | 22.17.1 | 22.17.1 | | Nginx | 1.24.0 | 1.24.0 | | Redis | - | 7.0.15 (512mb) | @@ -1197,3 +1197,78 @@ done | Supervisor | - | queue worker ×2 | | UFW | **비활성** | 활성 | | fail2ban | - | ✅ | + +--- + +## [개발] PM2 설정 + +개발서버는 ecosystem.config.js 없이 PM2 CLI로 직접 관리합니다. + +```bash +# 실행 (포트 3001, Gitea가 3000 사용) +cd /home/webservice/react && pm2 start npm --name sam-react -- start -- -p 3001 + +# 재부팅 자동 시작 등록 +pm2 save +sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u hskwon --hp /home/hskwon +``` + +| 이름 | 모드 | 포트 | 비고 | +|------|------|------|------| +| sam-react | fork | 3001 | Gitea가 3000 사용, Jenkins 배포 시 자동 restart | + +--- + +## [개발] MySQL 8.0 → 8.4 업그레이드 절차 + +Ubuntu 24.04 APT 기본은 MySQL 8.0입니다. 8.4로 업그레이드하는 절차: + +### 사전 준비 + +```bash +# 1. DB 백업 +DB_PASS=$(grep DB_PASSWORD /home/webservice/mng/.env | head -1 | cut -d= -f2) +for db in sam chandj sam_stat; do + mysqldump -ucodebridge -p$DB_PASS --no-tablespaces --skip-triggers --skip-routines $db | gzip > /tmp/${db}_backup.sql.gz +done + +# 2. 인증 방식 변환 (mysql_native_password → caching_sha2_password) +# 8.4에서 mysql_native_password가 deprecated +mysql -u debian-sys-maint -p'' -e " + ALTER USER 'codebridge'@'localhost' IDENTIFIED WITH caching_sha2_password BY '<비밀번호>'; + ALTER USER 'chandj'@'localhost' IDENTIFIED WITH caching_sha2_password BY '<비밀번호>'; + FLUSH PRIVILEGES;" +``` + +> debian-sys-maint 비밀번호: `/etc/mysql/debian.cnf` 참조 + +### 업그레이드 실행 + +```bash +# 3. MySQL 중지 +sudo systemctl stop mysql + +# 4. MySQL APT 레포 추가 +wget -q https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb -O /tmp/mysql-apt-config.deb +sudo DEBIAN_FRONTEND=noninteractive dpkg -i /tmp/mysql-apt-config.deb + +# 5. 레포를 8.4-lts로 변경 +sudo sed -i 's/mysql-8.0/mysql-8.4-lts/g' /etc/apt/sources.list.d/mysql.list +sudo apt-get update + +# 6. 업그레이드 (기존 설정 유지) +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confold" mysql-server mysql-client + +# 7. 시작 및 확인 +sudo systemctl start mysql +mysql --version # → 8.4.x 확인 +``` + +### GPG 키 만료 시 + +MySQL APT 레포의 GPG 키가 만료된 경우: + +```bash +sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C +# 또는 allow-insecure 임시 허용 후 설치 +``` From a68a9ba098022055533d008619ba954de41b7aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 10:48:23 +0900 Subject: [PATCH 05/69] =?UTF-8?q?docs:MNG=20500=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=A7=84=EB=8B=A8=20=EA=B2=B0=EA=B3=BC=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=E2=80=94=20php8.4-soap=20=EC=84=A4=EC=B9=98,=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EA=B2=BD=EB=A1=9C/=EA=B6=8C=ED=95=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 11-server-setup: PHP 확장 목록에 php8.4-soap 추가 - 02-daily-operations: MNG 로그 경로 수정 (shared → current, 심링크 아님) - 03-service-prod: Redis 용도 정확하게 테이블로 정리 (캐시=redis, 세션=database) - 05-deployment: MNG 배포 시 storage/logs 권한 경고 추가, 배포 후 확인에 MNG 로그 추가 - 08-troubleshooting: MNG 500 에러 트러블슈팅 사례 추가 (2026-02-25 실제 사례) Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/02-daily-operations.md | 4 +-- deploys/ops-manual/03-service-prod.md | 8 ++++- deploys/ops-manual/05-deployment.md | 11 ++++++ deploys/ops-manual/08-troubleshooting.md | 44 +++++++++++++++++++++++ deploys/ops-manual/11-server-setup.md | 2 +- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/deploys/ops-manual/02-daily-operations.md b/deploys/ops-manual/02-daily-operations.md index b6712d6..ab664d1 100644 --- a/deploys/ops-manual/02-daily-operations.md +++ b/deploys/ops-manual/02-daily-operations.md @@ -95,8 +95,8 @@ sudo tail -f /var/log/php8.4-fpm.log # API 로그 sudo tail -f /home/webservice/api/shared/storage/logs/laravel.log -# Admin(MNG) 로그 -sudo tail -f /home/webservice/mng/shared/storage/logs/laravel.log +# Admin(MNG) 로그 — storage/logs가 shared 심링크가 아니므로 current 경로 사용 +sudo tail -f /home/webservice/mng/current/storage/logs/laravel.log # API Stage 로그 sudo tail -f /home/webservice/api-stage/shared/storage/logs/laravel.log diff --git a/deploys/ops-manual/03-service-prod.md b/deploys/ops-manual/03-service-prod.md index 4503ea2..c28d524 100644 --- a/deploys/ops-manual/03-service-prod.md +++ b/deploys/ops-manual/03-service-prod.md @@ -158,7 +158,13 @@ redis-cli ttl "키이름" # TTL 확인 redis-cli flushall # 전체 삭제 (주의: 세션도 삭제됨) ``` -**용도:** Laravel 캐시, 세션, 큐 (QUEUE_CONNECTION=redis) +**용도:** + +| 기능 | 드라이버 | .env 설정 | +|------|---------|----------| +| 캐시 | Redis | CACHE_STORE=redis | +| 세션 | Database | SESSION_DRIVER=database | +| 큐 | Redis | Supervisor에서 `queue:work redis` 명시 | --- diff --git a/deploys/ops-manual/05-deployment.md b/deploys/ops-manual/05-deployment.md index be171fa..486222c 100644 --- a/deploys/ops-manual/05-deployment.md +++ b/deploys/ops-manual/05-deployment.md @@ -573,6 +573,16 @@ ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Worker 재시작 불필요. +> **주의: storage/logs 권한 문제** +> MNG Jenkinsfile은 `storage/logs`를 shared로 심링크하지 않고 릴리즈 디렉토리에 `mkdir`로 생성한다. +> 이 디렉토리는 `hskwon:hskwon` 소유로 생성되므로, PHP-FPM(`www-data`)이 로그를 쓸 수 없다. +> 배포 후 500 에러가 발생하는데 로그가 비어있으면 다음을 실행: +> ```bash +> sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/ +> sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/laravel.log 2>/dev/null +> ``` +> 근본 해결: Jenkinsfile에 `chown` 명령 추가 또는 storage/logs를 shared 심링크로 변경. + ### Jenkinsfile (mng/Jenkinsfile) ```groovy @@ -868,6 +878,7 @@ sudo supervisorctl status # 에러 로그 sudo tail -20 /var/log/nginx/api.sam.it.kr.error.log sudo tail -20 /home/webservice/api/shared/storage/logs/laravel.log +sudo tail -20 /home/webservice/mng/current/storage/logs/laravel.log # HTTP 응답 확인 curl -sI https://api.sam.it.kr diff --git a/deploys/ops-manual/08-troubleshooting.md b/deploys/ops-manual/08-troubleshooting.md index 5c64847..3b2b27e 100644 --- a/deploys/ops-manual/08-troubleshooting.md +++ b/deploys/ops-manual/08-troubleshooting.md @@ -258,6 +258,50 @@ sudo chmod -R 775 /home/webservice/api/current/bootstrap/cache --- +### MNG 500 에러 (storage/logs 권한 + SOAP) + +**증상:** mng.codebridge-x.com 특정 페이지에서 500 에러. Laravel 로그에 기록 없음. + +**배경:** MNG Jenkinsfile 배포 시 `storage/logs`는 shared로 심링크되지 않고 릴리즈 디렉토리에 직접 생성됨. +따라서 `hskwon:hskwon` 소유로 생성되어 `www-data`(PHP-FPM)가 로그를 쓸 수 없음. + +**진단 순서:** + +```bash +# 1. 실제 로그 위치 확인 (shared가 아닌 current) +ls -la /home/webservice/mng/current/storage/logs/laravel.log + +# 2. 로그 파일 소유자 확인 — hskwon이면 www-data가 쓸 수 없음 +# 3. nginx 접근 로그에서 500 확인 +sudo tail -20 /var/log/nginx/mng.codebridge-x.com.access.log | grep " 500 " +``` + +**조치:** + +```bash +# 로그 권한 수정 (배포마다 필요) +sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/ +sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/laravel.log + +# 로그 확인 후 실제 에러 파악 +cat /home/webservice/mng/current/storage/logs/laravel.log +``` + +**실제 사례 (2026-02-25):** + +1. 최초 증상: `Table 'sam.cache' doesn't exist` → `CACHE_STORE=database`였으나 cache 테이블 미존재 +2. 해결: `.env`에서 `CACHE_STORE=redis`로 변경 + `php artisan config:cache` +3. 여전히 500 → 로그 파일 권한 문제로 에러 미기록 → 권한 수정 후 실제 에러 확인 +4. 실제 원인: `Class "SoapClient" not found` → `php8.4-soap` 미설치 +5. 최종 해결: `sudo apt install php8.4-soap && sudo systemctl restart php8.4-fpm` + +**교훈:** +- MNG 로그는 `current/storage/logs/`에 있음 (shared 아님) +- 500 에러인데 로그가 비어있으면 **파일 권한 문제**를 먼저 의심 +- PHP 확장 누락은 artisan tinker로 확인 가능: `php artisan tinker --execute="new SoapClient('test');"` + +--- + ## 공통 장애 ### 디스크 공간 부족 diff --git a/deploys/ops-manual/11-server-setup.md b/deploys/ops-manual/11-server-setup.md index cac90e2..f51b731 100644 --- a/deploys/ops-manual/11-server-setup.md +++ b/deploys/ops-manual/11-server-setup.md @@ -197,7 +197,7 @@ sudo apt update sudo apt install -y \ php8.4-fpm php8.4-mysql php8.4-mbstring php8.4-xml \ php8.4-curl php8.4-zip php8.4-gd php8.4-bcmath \ - php8.4-intl php8.4-redis php8.4-opcache + php8.4-intl php8.4-redis php8.4-opcache php8.4-soap curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer From 4d9a04424312321376f4074aec8c16de82c5ecc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 11:23:35 +0900 Subject: [PATCH 06/69] =?UTF-8?q?docs:MNG=20=EB=B0=B0=ED=8F=AC=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=E2=80=94=20?= =?UTF-8?q?Jenkinsfile=20=EA=B6=8C=ED=95=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/05-deployment.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/deploys/ops-manual/05-deployment.md b/deploys/ops-manual/05-deployment.md index 486222c..8a416b2 100644 --- a/deploys/ops-manual/05-deployment.md +++ b/deploys/ops-manual/05-deployment.md @@ -573,15 +573,9 @@ ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Worker 재시작 불필요. -> **주의: storage/logs 권한 문제** -> MNG Jenkinsfile은 `storage/logs`를 shared로 심링크하지 않고 릴리즈 디렉토리에 `mkdir`로 생성한다. -> 이 디렉토리는 `hskwon:hskwon` 소유로 생성되므로, PHP-FPM(`www-data`)이 로그를 쓸 수 없다. -> 배포 후 500 에러가 발생하는데 로그가 비어있으면 다음을 실행: -> ```bash -> sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/ -> sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/laravel.log 2>/dev/null -> ``` -> 근본 해결: Jenkinsfile에 `chown` 명령 추가 또는 storage/logs를 shared 심링크로 변경. +> **참고: storage/logs 권한** +> MNG는 `storage/logs`를 shared로 심링크하지 않고 릴리즈 디렉토리에 `mkdir`로 생성한다. +> Jenkinsfile에서 `sudo chown -R www-data:webservice storage/logs`로 권한을 설정한다. (2026-02-25 적용) ### Jenkinsfile (mng/Jenkinsfile) From 46c5e23972ce4c8ded84106b524bb99e0689b13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 11:37:03 +0900 Subject: [PATCH 07/69] =?UTF-8?q?docs:=EB=B0=B0=ED=8F=AC=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=ED=98=84=ED=96=89=ED=99=94=20=E2=80=94=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EB=B9=8C=EB=93=9C=EB=B0=A9=EC=A7=80,=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=EC=95=8C=EB=A6=BC,=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B3=80=EA=B2=BD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 Jenkinsfile에 disableConcurrentBuilds() 반영 - react/api Production Approval에 #product_deploy Slack 알림 추가 - react 환경파일 .env.local → .env.production 변경 반영 - Slack 알림 채널 테이블 추가 (#product_infra, #product_deploy) - 환경변수 파일 테이블 DEV_TOOLBAR 컬럼 추가 - 수동 배포 섹션 .env.production 반영 Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/05-deployment.md | 62 ++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/deploys/ops-manual/05-deployment.md b/deploys/ops-manual/05-deployment.md index 8a416b2..43f7bd5 100644 --- a/deploys/ops-manual/05-deployment.md +++ b/deploys/ops-manual/05-deployment.md @@ -22,6 +22,13 @@ | sam-manage | Laravel Admin 배포 | main | 운영 (직접) | | sam-sales | 레거시 PHP 배포 | main | 운영 (직접) | +### Slack 알림 채널 + +| 채널 | 용도 | 알림 내용 | +|------|------|----------| +| `#product_infra` | 빌드/배포 상태 | 빌드 시작, 배포 성공/실패 | +| `#product_deploy` | 운영 배포 승인 | Stage 배포 완료 후 승인 대기 알림 (Jenkins 승인 링크 포함) | + ### 2-Branch 전략 (develop + main) > **stage 브랜치 없음.** main 브랜치 push 시 Stage 자동 배포 → Jenkins 승인 → Production 배포. @@ -35,7 +42,12 @@ 1. 개발자가 develop → main 머지 후 push 2. post-receive hook → CI/CD Gitea 자동 push 3. Jenkins 빌드 → Stage 자동 배포 -4. Jenkins UI에서 **승인 클릭** → Production 배포 (24시간 타임아웃) +4. `#product_deploy` Slack 채널에 승인 대기 알림 전송 +5. Jenkins UI에서 **승인 클릭** → Production 배포 (24시간 타임아웃) + +> **동시 빌드 방지:** 모든 파이프라인에 `disableConcurrentBuilds()` 적용. +> 같은 프로젝트에서 빌드가 동시에 2개 이상 돌지 않음. +> 승인 대기 중 새 push 시 → 기존 빌드 Abort 후 새 빌드 자동 시작. **main 브랜치 배포 흐름 (mng/sales):** 1. 개발자가 main push → hook → CI/CD Gitea → Jenkins → Production 직접 배포 @@ -141,6 +153,8 @@ Repository Settings → Webhooks → Add Webhook (Gitea) │ │ │ │ ├─ Stage 자동 배포 (react: .env.stage 빌드) │ │ │ │ +│ ├─ 📢 #product_deploy Slack 알림 (승인 링크 포함) │ +│ │ │ │ ├─ ⏸️ 승인 대기 (24시간 타임아웃) │ │ │ https://ci.sam.it.kr 에서 "운영 배포 진행" 클릭 │ │ │ │ @@ -183,11 +197,11 @@ CI/CD Gitea push -> Webhook -> Jenkins **환경변수 파일 (CI/CD 서버):** /var/lib/jenkins/env-files/react/ -| 파일 | API URL | Frontend URL | APP_ENV | -|------|---------|-------------|---------| -| .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com | development | -| .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr | staging | -| .env.main | https://api.sam.it.kr | https://sam.it.kr | production | +| 파일 | API URL | Frontend URL | APP_ENV | DEV_TOOLBAR | +|------|---------|-------------|---------|-------------| +| .env.develop | https://api.codebridge-x.com | https://dev.codebridge-x.com | development | - | +| .env.stage | https://stage-api.sam.it.kr | https://stage.sam.it.kr | staging | - | +| .env.main | https://api.sam.it.kr | https://sam.it.kr | production | false | > `NEXT_PUBLIC_APP_ENV` 값으로 타이틀 접두사 결정: `development` → `[D]`, `local` → `[L]`, 그 외 → 없음 @@ -201,6 +215,10 @@ CI/CD Gitea push -> Webhook -> Jenkins pipeline { agent any + options { + disableConcurrentBuilds() + } + environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') @@ -220,10 +238,10 @@ pipeline { script { if (env.BRANCH_NAME == 'main') { // main: Stage 빌드 먼저 (승인 후 Production 재빌드) - sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.local" + sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.production" } else { def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" - sh "cp ${envFile} .env.local" + sh "cp ${envFile} .env.production" } } } @@ -247,7 +265,7 @@ pipeline { --exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ - scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local + scp .env.production ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.production ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react' """ } @@ -264,7 +282,7 @@ pipeline { rsync -az --delete \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ - scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.local + scp .env.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.production ssh ${DEPLOY_USER}@211.117.60.189 ' ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 && @@ -279,6 +297,8 @@ pipeline { stage('Production Approval') { when { branch 'main' } steps { + slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token', + message: "🔔 *react* 운영 배포 승인 대기 중\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" timeout(time: 24, unit: 'HOURS') { input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr', ok: '운영 배포 진행' @@ -290,7 +310,7 @@ pipeline { stage('Rebuild for Production') { when { branch 'main' } steps { - sh "cp /var/lib/jenkins/env-files/react/.env.main .env.local" + sh "cp /var/lib/jenkins/env-files/react/.env.main .env.production" sh 'npm run build' } } @@ -305,7 +325,7 @@ pipeline { rsync -az --delete \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ - scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local + scp .env.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.production ssh ${DEPLOY_USER}@211.117.60.189 ' ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && cd /home/webservice && pm2 reload sam-front && @@ -334,6 +354,10 @@ pipeline { > Stage(.env.stage)와 Production(.env.main)에서 별도 빌드가 필요하다. > main 빌드 시 Stage용으로 먼저 빌드 → 승인 후 Production용으로 재빌드. +> **환경파일:** Jenkins는 CI/CD 서버의 env-files를 `.env.production`으로 복사하여 빌드한다. +> Next.js 우선순위: `.env.local` > `.env.production` > `.env` +> 따라서 서버에 `.env.local`이 있으면 `.env.production`을 덮어쓰므로 `.env.local`은 사용하지 않는다. + ### PM2 수동 재시작 ```bash @@ -374,6 +398,10 @@ CI/CD Gitea push -> Webhook -> Jenkins pipeline { agent any + options { + disableConcurrentBuilds() + } + environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') @@ -423,6 +451,8 @@ pipeline { stage('Production Approval') { when { branch 'main' } steps { + slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token', + message: "🔔 *api* 운영 배포 승인 대기 중\nStage API: https://stage-api.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" timeout(time: 24, unit: 'HOURS') { input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr', ok: '운영 배포 진행' @@ -583,6 +613,10 @@ API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Work pipeline { agent any + options { + disableConcurrentBuilds() + } + environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') @@ -813,7 +847,7 @@ ssh sam-prod " cd /tmp git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-react-prod.git react-build cd react-build -cp /var/lib/jenkins/env-files/react/.env.main .env.local +cp /var/lib/jenkins/env-files/react/.env.main .env.production npm install --prefer-offline npm run build @@ -824,7 +858,7 @@ ssh sam-prod "mkdir -p /home/webservice/react/releases/${RELEASE_ID}" rsync -az --delete \ .next package.json next.config.ts public node_modules \ hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ -scp .env.local hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local +scp .env.production hskwon@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.production # 심링크 전환 및 PM2 재시작 ssh sam-prod " From 8a00a9ec7d15b5b532de9622e1fb09289228a422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 12:54:42 +0900 Subject: [PATCH 08/69] =?UTF-8?q?docs:=20=EB=B0=B0=ED=8F=AC=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20Jenkinsfile=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=EC=97=90=20=EC=BB=A4=EB=B0=8B=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=95=8C=EB=A6=BC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3개 Jenkinsfile(react, api, mng) 코드 블록 업데이트 - Checkout 단계: checkout scm → GIT_COMMIT_MSG 캡처 → slackSend 순서 - 모든 slackSend 메시지에 ${env.GIT_COMMIT_MSG} 추가 Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/05-deployment.md | 37 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/deploys/ops-manual/05-deployment.md b/deploys/ops-manual/05-deployment.md index 43f7bd5..0bd560f 100644 --- a/deploys/ops-manual/05-deployment.md +++ b/deploys/ops-manual/05-deployment.md @@ -227,9 +227,12 @@ pipeline { stages { stage('Checkout') { steps { - slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token', - message: "🚀 *react* 빌드 시작 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" checkout scm + script { + env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim() + } + slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token', + message: "🚀 *react* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } @@ -298,7 +301,7 @@ pipeline { when { branch 'main' } steps { slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token', - message: "🔔 *react* 운영 배포 승인 대기 중\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" + message: "🔔 *react* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" timeout(time: 24, unit: 'HOURS') { input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr', ok: '운영 배포 진행' @@ -340,11 +343,11 @@ pipeline { post { success { slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token', - message: "✅ *react* 배포 성공 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + message: "✅ *react* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } failure { slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token', - message: "❌ *react* 배포 실패 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + message: "❌ *react* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } } @@ -410,9 +413,12 @@ pipeline { stages { stage('Checkout') { steps { - slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token', - message: "🚀 *api* 빌드 시작 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" checkout scm + script { + env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim() + } + slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token', + message: "🚀 *api* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } @@ -452,7 +458,7 @@ pipeline { when { branch 'main' } steps { slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token', - message: "🔔 *api* 운영 배포 승인 대기 중\nStage API: https://stage-api.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" + message: "🔔 *api* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage API: https://stage-api.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" timeout(time: 24, unit: 'HOURS') { input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr', ok: '운영 배포 진행' @@ -498,11 +504,11 @@ pipeline { post { success { slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token', - message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } failure { slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token', - message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" script { if (env.BRANCH_NAME == 'main') { sshagent(credentials: ['deploy-ssh-key']) { @@ -625,9 +631,12 @@ pipeline { stages { stage('Checkout') { steps { - slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token', - message: "🚀 *mng* 빌드 시작 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" checkout scm + script { + env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim() + } + slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token', + message: "🚀 *mng* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } @@ -671,11 +680,11 @@ pipeline { post { success { slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token', - message: "✅ *mng* 배포 성공 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + message: "✅ *mng* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } failure { slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token', - message: "❌ *mng* 배포 실패 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + message: "❌ *mng* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" script { if (env.BRANCH_NAME == 'main') { sshagent(credentials: ['deploy-ssh-key']) { From 87482be2b1b560bfeb137ad8644a9d270e7332d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 13:37:01 +0900 Subject: [PATCH 09/69] =?UTF-8?q?docs:=20=EC=9A=B4=EC=98=81=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20LibreOffice=20=EB=B0=8F=20=ED=8F=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LibreOffice 24.2.7 헤드리스 문서 변환 설정 - Pretendard (9 웨이트), Nanum, Noto CJK 폰트 설치 정보 - 폰트 관리 명령어 및 추가 절차 Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/03-service-prod.md | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/deploys/ops-manual/03-service-prod.md b/deploys/ops-manual/03-service-prod.md index c28d524..c6a72c0 100644 --- a/deploys/ops-manual/03-service-prod.md +++ b/deploys/ops-manual/03-service-prod.md @@ -277,4 +277,63 @@ sudo ufw allow from IP주소 to any port 포트번호 # 규칙 추가 sudo ufw delete 번호 # 규칙 삭제 (번호 기반) sudo ufw disable # 비활성화 (비상시만) sudo ufw enable # 활성화 +``` + +--- + +## LibreOffice (문서 변환) + +API 서버에서 문서 변환(Excel→PDF 등)에 사용. 헤드리스 모드로 동작. + +**버전:** 24.2.7.2 (개발/운영 동일) + +**명령어:** + +```bash +libreoffice --version # 버전 확인 +libreoffice --headless --convert-to pdf input.xlsx # CLI 변환 테스트 +``` + +**설치 패키지:** + +```bash +sudo apt-get install -y libreoffice-core libreoffice-writer libreoffice-calc libreoffice-impress +``` + +--- + +## 폰트 + +LibreOffice 문서 변환 시 폰트가 없으면 글자가 깨지므로 개발/운영 서버 동일하게 설치 필수. + +**설치된 한글 폰트:** + +| 폰트 | 설치 방식 | 경로 | +|------|----------|------| +| **Pretendard** (9 웨이트) | 수동 설치 (OTF) | `/usr/local/share/fonts/Pretendard-*.otf` | +| **Nanum** (고딕/명조/스퀘어/손글씨 등) | apt (`fonts-nanum`, `fonts-nanum-extra`) | `/usr/share/fonts/truetype/nanum/` | +| **Noto CJK** (Sans/Serif) | apt (`fonts-noto-cjk`) | `/usr/share/fonts/opentype/noto/` | + +**폰트 관리 명령어:** + +```bash +fc-list :lang=ko family | sort -u # 설치된 한글 폰트 목록 +fc-list | grep -i pretendard # Pretendard 설치 확인 +sudo fc-cache -fv # 폰트 캐시 갱신 (새 폰트 추가 후 필수) +``` + +**새 폰트 추가 시:** + +```bash +# 1. OTF/TTF 파일을 /usr/local/share/fonts/ 에 복사 +sudo cp *.otf /usr/local/share/fonts/ + +# 2. 폰트 캐시 갱신 +sudo fc-cache -fv + +# 3. 확인 +fc-list | grep -i "폰트이름" +``` + +> **주의:** 개발서버에 폰트를 추가하면 운영서버에도 동일하게 설치해야 변환 결과가 일치한다. ``` \ No newline at end of file From dc8b3ae0c9b44395db5d3808055ac7e65b575514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 25 Feb 2026 13:41:18 +0900 Subject: [PATCH 10/69] =?UTF-8?q?docs:=20=EC=9A=B4=EC=98=81=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20SMTP=20=EB=A9=94=EC=9D=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/mng 프로젝트별 SMTP 설정 정보 - Google 앱 비밀번호 관리 주의사항 - 트러블슈팅 가이드 (535 인증 실패 등) Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/03-service-prod.md | 44 ++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/deploys/ops-manual/03-service-prod.md b/deploys/ops-manual/03-service-prod.md index c6a72c0..965cebc 100644 --- a/deploys/ops-manual/03-service-prod.md +++ b/deploys/ops-manual/03-service-prod.md @@ -336,4 +336,46 @@ fc-list | grep -i "폰트이름" ``` > **주의:** 개발서버에 폰트를 추가하면 운영서버에도 동일하게 설치해야 변환 결과가 일치한다. -``` \ No newline at end of file + +--- + +## SMTP (메일 발송) + +Gmail SMTP를 통해 메일 발송. Google 앱 비밀번호 사용 (2단계 인증 필요). + +**프로젝트별 SMTP 설정:** + +| 항목 | api | mng | +|------|-----|-----| +| MAIL_HOST | smtp.gmail.com | smtp.gmail.com | +| MAIL_PORT | 587 | 587 | +| MAIL_USERNAME | shine1324@gmail.com | admin@codebridge-x.com | +| MAIL_FROM_ADDRESS | shine1324@gmail.com | develop@codebridge-x.com | +| MAIL_FROM_NAME | ${APP_NAME} | (주)코드브릿지엑스 | +| MAIL_ENCRYPTION | tls | tls | + +> **주의:** 개발/운영 서버의 MAIL_PASSWORD(앱 비밀번호)는 반드시 동일하게 유지. +> Google 앱 비밀번호를 재발급하면 모든 서버에 동일하게 반영해야 한다. + +**설정 파일 위치:** + +| 프로젝트 | 운영 | 개발 | +|---------|------|------| +| api | `/home/webservice/api/shared/.env` | `/home/webservice/api/.env` | +| mng | `/home/webservice/mng/shared/.env` | `/home/webservice/mng/.env` | + +**변경 후 반영:** + +```bash +# api +cd /home/webservice/api/current && php artisan config:cache + +# mng +cd /home/webservice/mng/current && php artisan config:cache +``` + +**트러블슈팅:** + +- `535 Username and Password not accepted` → 앱 비밀번호 만료 또는 불일치. 개발서버 값과 비교 후 동기화 +- `Connection refused` → 방화벽에서 587 포트 아웃바운드 차단 여부 확인 +- Google 앱 비밀번호 발급: Google 계정 → 보안 → 2단계 인증 → 앱 비밀번호 \ No newline at end of file From dbcfe6569211ab3f5608e2cfe0c1c6835378f9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 26 Feb 2026 14:39:50 +0900 Subject: [PATCH 11/69] =?UTF-8?q?docs:MNG=20storage/logs=20=EC=8B=AC?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20E-Sign=20?= =?UTF-8?q?PDF=20=ED=8A=B8=EB=9F=AC=EB=B8=94=EC=8A=88=ED=8C=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 05-deployment: MNG Jenkinsfile/수동배포 storage/logs 심링크 방식 반영 - 08-troubleshooting: 전자계약 PDF 서명 합성 오류 진단/조치 가이드 추가 - 08-troubleshooting: MNG 500 에러 섹션 로그 경로 shared로 업데이트 Co-Authored-By: Claude Opus 4.6 --- deploys/ops-manual/05-deployment.md | 17 +-- deploys/ops-manual/08-troubleshooting.md | 136 +++++++++++++++++++++-- 2 files changed, 134 insertions(+), 19 deletions(-) diff --git a/deploys/ops-manual/05-deployment.md b/deploys/ops-manual/05-deployment.md index 0bd560f..eb9d2ab 100644 --- a/deploys/ops-manual/05-deployment.md +++ b/deploys/ops-manual/05-deployment.md @@ -609,9 +609,10 @@ ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true API와 동일한 releases/shared 구조. 차이점: npm build 추가, Queue Worker 재시작 불필요. -> **참고: storage/logs 권한** -> MNG는 `storage/logs`를 shared로 심링크하지 않고 릴리즈 디렉토리에 `mkdir`로 생성한다. -> Jenkinsfile에서 `sudo chown -R www-data:webservice storage/logs`로 권한을 설정한다. (2026-02-25 적용) +> **참고: storage/logs 심링크 (2026-02-26 변경)** +> MNG는 storage/logs를 shared로 심링크하여 배포 간 로그를 영속 보존한다. +> 이전에는 `mkdir`로 릴리즈 디렉토리에 생성하여 배포마다 로그가 유실되었음. +> 변경: `ln -sfn /home/webservice/mng/shared/storage/logs storage/logs` ### Jenkinsfile (mng/Jenkinsfile) @@ -655,9 +656,10 @@ pipeline { . ${DEPLOY_USER}@211.117.60.189:/home/webservice/mng/releases/${RELEASE_ID}/ ssh ${DEPLOY_USER}@211.117.60.189 ' cd /home/webservice/mng/releases/${RELEASE_ID} && - mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs && + mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} && ln -sfn /home/webservice/mng/shared/.env .env && ln -sfn /home/webservice/mng/shared/storage/app storage/app && + ln -sfn /home/webservice/mng/shared/storage/logs storage/logs && composer install --no-dev --optimize-autoloader --no-interaction && npm install --prefer-offline && npm run build && @@ -712,11 +714,12 @@ RELEASE_ID=$(date +%Y%m%d_%H%M%S) cd /home/webservice/mng/releases git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-manage.git $RELEASE_ID -ln -sfn /home/webservice/mng/shared/storage /home/webservice/mng/releases/$RELEASE_ID/storage 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 +ln -sfn /home/webservice/mng/shared/storage/logs /home/webservice/mng/releases/$RELEASE_ID/storage/logs cd /home/webservice/mng/releases/$RELEASE_ID -mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs +mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} composer install --no-dev --optimize-autoloader --no-interaction # Vite 빌드 (Blade + Tailwind) @@ -915,7 +918,7 @@ sudo supervisorctl status # 에러 로그 sudo tail -20 /var/log/nginx/api.sam.it.kr.error.log sudo tail -20 /home/webservice/api/shared/storage/logs/laravel.log -sudo tail -20 /home/webservice/mng/current/storage/logs/laravel.log +sudo tail -20 /home/webservice/mng/shared/storage/logs/laravel.log # HTTP 응답 확인 curl -sI https://api.sam.it.kr diff --git a/deploys/ops-manual/08-troubleshooting.md b/deploys/ops-manual/08-troubleshooting.md index 3b2b27e..7c92783 100644 --- a/deploys/ops-manual/08-troubleshooting.md +++ b/deploys/ops-manual/08-troubleshooting.md @@ -102,6 +102,39 @@ sudo mysql -e "SET GLOBAL max_connections = 150;" --- +### [개발] MySQL 8.4 인증 플러그인 오류 + +**증상:** `SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client` + +**원인:** MySQL 8.4에서 `mysql_native_password` 플러그인이 기본 비활성화됨. 레거시 PHP(5130 등)의 mysqlnd가 `caching_sha2_password`를 지원하지 못함. + +**조치:** + +```bash +# 1. mysqld.cnf에 플러그인 활성화 추가 +sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf +# [mysqld] 섹션에 추가: +# mysql_native_password=ON + +# 2. MySQL 재시작 +sudo systemctl restart mysql + +# 3. 레거시 PHP용 계정 인증 방식 변경 +mysql -u debian-sys-maint -p'비밀번호' -e " +ALTER USER '계정'@'localhost' IDENTIFIED WITH mysql_native_password BY '비밀번호'; +FLUSH PRIVILEGES;" +``` + +**실제 사례 (2026-02-25):** + +1. 5130 레거시 사이트 로그인 시 2054 에러 발생 +2. `/etc/mysql/mysql.conf.d/mysqld.cnf`에 `mysql_native_password=ON` 추가 후 MySQL 재시작 +3. `codebridge`, `pro`, `chandj` 계정을 `mysql_native_password`로 변경하여 해결 + +**참고:** debian-sys-maint 비밀번호는 `/etc/mysql/debian.cnf`에서 확인 가능. + +--- + ### Redis 메모리 부족 **증상:** "OOM command not allowed" 메시지. @@ -262,16 +295,18 @@ sudo chmod -R 775 /home/webservice/api/current/bootstrap/cache **증상:** mng.codebridge-x.com 특정 페이지에서 500 에러. Laravel 로그에 기록 없음. -**배경:** MNG Jenkinsfile 배포 시 `storage/logs`는 shared로 심링크되지 않고 릴리즈 디렉토리에 직접 생성됨. -따라서 `hskwon:hskwon` 소유로 생성되어 `www-data`(PHP-FPM)가 로그를 쓸 수 없음. +**배경:** 2026-02-26 이후 MNG `storage/logs`는 shared로 심링크됨. 이전에는 릴리즈 디렉토리에 직접 생성되어 배포마다 로그가 유실되었음. **진단 순서:** ```bash -# 1. 실제 로그 위치 확인 (shared가 아닌 current) -ls -la /home/webservice/mng/current/storage/logs/laravel.log +# 1. 로그 심링크 확인 +ls -la /home/webservice/mng/current/storage/logs +# → shared/storage/logs 심링크인지 확인 + +# 2. 로그 파일 소유자 확인 +ls -la /home/webservice/mng/shared/storage/logs/laravel.log -# 2. 로그 파일 소유자 확인 — hskwon이면 www-data가 쓸 수 없음 # 3. nginx 접근 로그에서 500 확인 sudo tail -20 /var/log/nginx/mng.codebridge-x.com.access.log | grep " 500 " ``` @@ -279,12 +314,16 @@ sudo tail -20 /var/log/nginx/mng.codebridge-x.com.access.log | grep " 500 " **조치:** ```bash -# 로그 권한 수정 (배포마다 필요) -sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/ -sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/laravel.log +# 로그 심링크가 아닌 경우 (이전 배포 방식) +rm -rf /home/webservice/mng/current/storage/logs +ln -sfn /home/webservice/mng/shared/storage/logs /home/webservice/mng/current/storage/logs -# 로그 확인 후 실제 에러 파악 -cat /home/webservice/mng/current/storage/logs/laravel.log +# shared 로그 권한 수정 +sudo chown www-data:webservice /home/webservice/mng/shared/storage/logs/ +sudo chown www-data:webservice /home/webservice/mng/shared/storage/logs/laravel.log + +# 로그 확인 +cat /home/webservice/mng/shared/storage/logs/laravel.log ``` **실제 사례 (2026-02-25):** @@ -296,12 +335,85 @@ cat /home/webservice/mng/current/storage/logs/laravel.log 5. 최종 해결: `sudo apt install php8.4-soap && sudo systemctl restart php8.4-fpm` **교훈:** -- MNG 로그는 `current/storage/logs/`에 있음 (shared 아님) -- 500 에러인데 로그가 비어있으면 **파일 권한 문제**를 먼저 의심 +- MNG 로그는 `shared/storage/logs/`에 있음 (2026-02-26~) +- 500 에러인데 로그가 비어있으면 **심링크 여부 → 파일 권한** 순서로 확인 - PHP 확장 누락은 artisan tinker로 확인 가능: `php artisan tinker --execute="new SoapClient('test');"` --- +### MNG 전자계약(E-Sign) PDF 서명 합성 오류 + +**증상:** 전자계약 완료 후 다운로드한 PDF에 서명/도장/텍스트가 적용되지 않음. DB에서 `signed_file_path`가 null. + +**진단:** + +```bash +# 1. 완료됐지만 signed_file_path 없는 계약 확인 +cd /home/webservice/mng/current && php artisan tinker --execute=" +\$contracts = App\Models\ESign\EsignContract::withoutGlobalScopes() + ->where('status', 'completed')->whereNull('signed_file_path') + ->get(['id','tenant_id','status','completed_at']); +echo \$contracts->toJson(JSON_PRETTY_PRINT); +" + +# 2. 서명 이미지 파일 존재 확인 +sudo ls -la /home/webservice/mng/shared/storage/app/private/esign/*/signatures/ + +# 3. signed 디렉토리 존재 및 권한 확인 +ls -la /home/webservice/mng/shared/storage/app/private/esign/*/signed/ + +# 4. 로그 확인 +grep -i "서명\|esign\|pdf" /home/webservice/mng/shared/storage/logs/laravel.log | tail -20 + +# 5. 한글 폰트 확인 +ls -la /usr/share/fonts/truetype/nanum/NanumGothic.ttf +``` + +**조치 (수동 PDF 재합성):** + +```bash +cd /home/webservice/mng/current && sudo -u www-data php artisan tinker --execute=" +try { + \$contract = App\Models\ESign\EsignContract::withoutGlobalScopes()->find(<계약ID>); + \$pdfService = new App\Services\ESign\PdfSignatureService; + \$result = \$pdfService->mergeSignatures(\$contract); + echo 'SUCCESS: ' . \$result; +} catch (\Throwable \$e) { + echo 'ERROR: ' . \$e->getMessage(); +} +" +``` + +**주의:** 반드시 `sudo -u www-data`로 실행해야 서명 이미지 파일 접근 가능. + +**주요 원인 및 해결:** + +| 원인 | 진단 방법 | 해결 | +|------|----------|------| +| `signed/` 디렉토리 미존재 | `ls esign/*/signed/` | `sudo -u www-data mkdir -p esign/{tenant_id}/signed` | +| `signatures/` 권한 부족 | `stat esign/*/signatures/` | `sudo chmod 2775 esign/*/signatures/` | +| 로그 유실로 에러 추적 불가 | `ls -la current/storage/logs` | `storage/logs` → shared 심링크 확인 | +| 한글 폰트 미설치 | `ls /usr/share/fonts/truetype/nanum/` | `sudo apt install fonts-nanum` | +| FPDI/TCPDF 미설치 | `composer show setasign/fpdi` | `composer install` | + +**esign 디렉토리 권한 기준:** + +```bash +# 모든 esign 하위 디렉토리: www-data:webservice 2775 +sudo chown -R www-data:webservice /home/webservice/mng/shared/storage/app/private/esign/ +sudo chmod -R 2775 /home/webservice/mng/shared/storage/app/private/esign/ +``` + +**실제 사례 (2026-02-26):** + +1. 계약 #17이 `completed`인데 `signed_file_path`가 null +2. 원인: `signatures/` 디렉토리 권한 `2700` (www-data만 접근 가능), `signed/` 디렉토리 미존재 +3. 추가 원인: `storage/logs`가 릴리즈 디렉토리에 있어 이전 배포 로그 유실 +4. 조치: 권한 `2775`로 수정 + `sudo -u www-data`로 수동 재합성 + storage/logs 심링크 적용 +5. 결과: 409KB signed PDF 생성 (원본 265KB + 서명 이미지 144KB) + +--- + ## 공통 장애 ### 디스크 공간 부족 From 94d65b82110f6b9f676bb08c673d183f10a40d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 26 Feb 2026 15:47:49 +0900 Subject: [PATCH 12/69] =?UTF-8?q?docs:E-Sign=20TCPDF=20=ED=8F=B0=ED=8A=B8?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=ED=8A=B8=EB=9F=AC=EB=B8=94=EC=8A=88=ED=8C=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TCPDF font definition file 오류 원인/진단/해결 문서화 - 개발 vs 운영 환경 차이 (vendor 권한) 비교표 추가 - registerKoreanFont() 코드 수정 배경 설명 - 긴급 임시 조치 명령어 포함 --- deploys/ops-manual/08-troubleshooting.md | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/deploys/ops-manual/08-troubleshooting.md b/deploys/ops-manual/08-troubleshooting.md index 7c92783..b45d9a1 100644 --- a/deploys/ops-manual/08-troubleshooting.md +++ b/deploys/ops-manual/08-troubleshooting.md @@ -395,6 +395,7 @@ try { | 로그 유실로 에러 추적 불가 | `ls -la current/storage/logs` | `storage/logs` → shared 심링크 확인 | | 한글 폰트 미설치 | `ls /usr/share/fonts/truetype/nanum/` | `sudo apt install fonts-nanum` | | FPDI/TCPDF 미설치 | `composer show setasign/fpdi` | `composer install` | +| TCPDF 폰트 정의 파일 오류 | 아래 "TCPDF 폰트 정의 파일 오류" 참고 | `registerKoreanFont()` 코드 수정 | **esign 디렉토리 권한 기준:** @@ -414,6 +415,62 @@ sudo chmod -R 2775 /home/webservice/mng/shared/storage/app/private/esign/ --- +### TCPDF 폰트 정의 파일 오류 (font definition file) + +**증상:** 전자계약 서명 페이지에서 `TCPDF ERROR: Could not include font definition file: pretendard` (또는 `nanumgothic`) 오류. + +**근본 원인:** + +운영 환경에서 `vendor/tecnickcom/tcpdf/fonts/` 디렉토리가 배포 사용자(`hskwon`) 소유이므로 PHP-FPM(`www-data`)이 쓰기 불가. +`TCPDF_FONTS::addTTFfont()`는 폰트 캐시 파일(.php, .z, .ctg.z)을 **생성만** 하고, +`$pdf->SetFont('폰트명')`은 `K_PATH_FONTS`(vendor 경로)에서 **찾기만** 해서 경로 불일치 발생. + +개발서버는 `vendor/` 권한이 `2775 pro:develop`이라 PHP가 직접 쓸 수 있어 문제없음. + +**진단:** + +```bash +# 폰트 캐시 존재 확인 (storage에 있으나 vendor에 없는 상태) +ls -la /home/webservice/mng/shared/storage/app/private/fonts/ +ls /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/pretendard* 2>/dev/null + +# vendor fonts 소유자 확인 +stat -c "%U:%G %a" /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/ + +# 에러 로그 확인 +grep -i "font definition\|Could not include" /home/webservice/mng/shared/storage/logs/laravel.log +``` + +**영구 해결 (코드 수정 - 2026-02-26 적용):** + +`PdfSignatureService.php`에서 `registerKoreanFont(Fpdi $pdf)` 메서드로 분리하여: +1. 폰트 캐시를 `storage/app/private/fonts/`에 생성 (vendor 의존 제거) +2. `$pdf->AddFont('pretendard', '', $fontDefFile)` — PDF 인스턴스에 **전체 경로로 등록** +3. 이후 `SetFont('pretendard')`가 이미 등록된 폰트를 사용하므로 K_PATH_FONTS 미참조 + +**긴급 임시 조치 (코드 수정 전):** + +```bash +# vendor 폰트 디렉토리 권한 변경 (배포 시마다 초기화됨) +sudo chown -R www-data:webservice /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/ +sudo chmod -R 775 /home/webservice/mng/current/vendor/tecnickcom/tcpdf/fonts/ + +# 기존 캐시 삭제 (코드 수정 후 새 경로로 재생성) +sudo rm -f /home/webservice/mng/shared/storage/app/private/fonts/pretendard.* +sudo rm -f /home/webservice/mng/shared/storage/app/private/fonts/nanumgothic.* +``` + +**개발 vs 운영 환경 차이:** + +| 항목 | 개발 서버 | 운영 서버 | +|------|----------|----------| +| vendor/ 소유자 | `pro:develop` (2775) | `hskwon:hskwon` (배포 사용자) | +| www-data vendor 쓰기 | ✅ 가능 | ❌ 불가 | +| 폰트 캐시 위치 | vendor 내부 (기본) | storage/app/private/fonts/ | +| `addTTFfont()` 결과 | vendor에 캐시 생성 → SetFont 성공 | storage에 캐시 생성 → SetFont 실패 (경로 불일치) | + +--- + ## 공통 장애 ### 디스크 공간 부족 From 3847483090f8bdf2c5d05ac50878bd8e3ffb2445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 21 Feb 2026 21:15:35 +0900 Subject: [PATCH 13/69] =?UTF-8?q?docs:=20CLAUDE.md=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PPT 제작 규칙 (회사명, BI 로고, 금지 사항) - Git 커밋/푸시 정책, 서버 접근 금지 등 전역 규칙 --- CLAUDE.md | 729 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 729 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ca30f35 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,729 @@ +# Claude Code 전역 설정 + +> 이 파일은 모든 프로젝트에 적용되는 전역 규칙입니다. + +## 메모리 + +### sam설명 +SAM 프로젝트의 기술적 개요 문서입니다. 이 문서를 참조하면 SAM 프로젝트가 무엇인지 이해할 수 있습니다. + +**파일 경로**: `/home/aweso/sam/docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` + +**핵심 요약**: +- **회사**: 주일/경동 (블라인드/스크린 제조업체) +- **프로젝트**: SAM (Smart Automation Management) - 차세대 ERP/MES 통합 시스템 +- **기술 스택**: Laravel 11 + HTMX + Tailwind CSS + MySQL 8.0 +- **아키텍처**: Multi-tenant (tenant_id 기반 데이터 격리) +- **레거시**: 5130.co.kr (PHP 기반) → SAM으로 마이그레이션 중 + +**사용자가 'sam설명'이라고 말하면**: +1. 위 경로의 `SAM_PROJECT_OVERVIEW_FOR_AI.md` 파일을 읽어서 전체 내용을 파악하세요 +2. SAM 프로젝트의 비즈니스 도메인, 기술 스택, 현재 작업 현황을 이해한 상태로 작업하세요 + +--- + +## Git 커밋 규칙 (최우선 필수 규칙) + +> **경고: 이 규칙은 절대 누락되어서는 안 됩니다!** +> **기준 문서**: `sam/docs/standards/git-conventions.md` + +### 필수 수행 절차 + +**모든 코드 작업 완료 후 반드시 다음을 수행:** + +1. 변경된 파일이 있는 Git 저장소로 이동 +2. `git status`로 변경사항 확인 +3. `git add <파일들>` 로 스테이징 +4. `git commit -m "type: [scope] 작업내용"` 로 커밋 + +### 커밋 메시지 형식 (필수) + +``` +type: [scope] 작업내용 + +- 세부항목 (생략가능) +- 세부항목 2 + +Issue: URL (생략가능) +``` + +**예시:** +```bash +feat: [calendar] 달력 기능 개선 + +- 클릭시 오류 기능 개선 +- 색상 변경 +``` + +```bash +fix: [auth] 로그인 시 세션 만료 오류 수정 +``` + +### Commit Types + +| Type | 설명 | 예시 | +|------|------|------| +| `feat` | 새로운 기능 추가 | `feat: [file] 파일 업로드 기능 추가` | +| `fix` | 버그 수정 | `fix: [auth] 세션 만료 오류 수정` | +| `chore` | 설정, 빌드 등 변경 | `chore: composer 패키지 업데이트` | +| `refactor` | 프로덕션 코드 리팩토링 | `refactor: [user] 서비스 메서드 분리` | +| `style` | 포맷/코딩 스타일 수정 | `style: Pint 포맷팅 적용` | +| `test` | 테스트 추가/수정 | `test: Product API 테스트 추가` | +| `docs` | 문서 변경 | `docs: API 문서 업데이트` | + +### Claude 서명 제외 (필수) + +``` +❌ Co-Authored-By: Claude — 포함 금지 +❌ 🤖 Generated with Claude Code — 포함 금지 +``` + +- Git hooks로 자동 제거됨 +- 간결하고 명확한 한글 커밋 메시지만 유지 + +### 푸시 정책 + +- **사용자가 수동으로 푸시 진행** +- 자동 푸시 하지 않음 +- 커밋 후 푸시 여부를 묻지 않음 + +### Claude Code 설정 파일도 커밋 대상 + +다음 파일들이 변경되면 반드시 커밋: + +| 파일/폴더 | 설명 | 커밋 예시 | +|-----------|------|----------| +| `CLAUDE.md` | 프로젝트 설정 | `docs: CLAUDE.md 규칙 업데이트` | +| `claudedocs/` | Claude 관련 문서 | `docs: 기능 분석 문서 추가` | +| `.claude/settings.json` | Claude 설정 | `chore: Claude 설정 변경` | +| `agents/`, `skills/` | 커스텀 에이전트/스킬 | `feat: [claude] 새 스킬 추가` | + +### 커밋 전 체크리스트 + +- [ ] `./vendor/bin/pint` 실행 (코드 포맷팅, 해당 시) +- [ ] `git diff`로 변경사항 검토 +- [ ] 불필요한 파일 제외 (.env, node_modules 등) +- [ ] 변경된 파일이 있는 저장소에서 git add → git commit +- [ ] CLAUDE.md, claudedocs/, agents/, skills/ 변경 확인 → git commit +- [ ] 커밋 메시지: `type: [scope] 한글 작업내용` 형식 준수 +- [ ] Co-Authored-By 서명 미포함 확인 + +--- + +## 주요 프로젝트 경로 + +| 경로 | 설명 | Git 저장소 | +|------|------|-----------| +| `/home/aweso/sam/mng` | 관리자 웹 (Laravel) | 독립 저장소 | +| `/home/aweso/sam/api` | API 서버 (Laravel) | 독립 저장소 | +| `/home/aweso/sam/react` | 프론트엔드 (Next.js) | 독립 저장소 | + +**각 폴더는 독립적인 Git 저장소입니다. 해당 폴더에서 git 명령을 실행해야 합니다.** + +--- + +## 서버 직접 접근 금지 (최우선 필수 규칙) + +> **경고: 운영/개발 서버에 SSH로 직접 접속하여 파일을 수정하거나 명령을 실행하지 마세요!** +> **2026-02-21 사고**: Claude가 서버에 SSH로 직접 접속하여 설정을 변경한 결과 502 Bad Gateway 발생. 개발팀장이 복구함. + +### 핵심 원칙 + +서버는 **개발팀장이 관리**한다. Claude는 서버에 절대 직접 접근하지 않는다. + +### 금지 사항 + +``` +❌ ssh pro@114.203.209.83 ... 로 서버 접속 금지 +❌ ssh hskwon@114.203.209.83 ... 로 서버 접속 금지 +❌ 서버에서 파일 수정, 프로세스 종료/시작, 설정 변경 금지 +❌ 서버에서 npm run build, npm start, node server.js 등 실행 금지 +❌ 서버에서 git pull, composer install, php artisan 등 실행 금지 +❌ scp, rsync로 서버에 파일 직접 전송 금지 +``` + +### 허용 사항 + +``` +✅ 로컬에서 코드 작성 및 수정 +✅ 로컬에서 git add → git commit +✅ 사용자에게 git push 안내 (사용자가 수동으로 실행) +✅ 사용자에게 서버 배포 절차 안내 (사용자가 수동으로 실행) +``` + +### 배포 흐름 + +``` +Claude 역할 사용자/팀장 역할 +┌─────────────────┐ ┌─────────────────┐ +│ 코드 작성/수정 │ │ │ +│ git add │ │ │ +│ git commit │──push──→ │ git pull │ +│ │ │ 서버 배포 │ +│ │ │ 서비스 재시작 │ +└─────────────────┘ └─────────────────┘ +``` + +### 서버 작업이 필요한 경우 + +사용자에게 명령어를 안내만 한다: + +``` +서버에서 다음 명령을 실행해주세요: +cd /home/webservice/api && git pull && composer install && php artisan migrate +``` + +### 체크리스트 (모든 작업 시) + +- [ ] SSH 명령 사용하지 않음 +- [ ] 서버 파일 직접 수정하지 않음 +- [ ] 배포가 필요하면 사용자에게 안내만 제공 +- [ ] git push까지만 Claude 역할 + +--- + +## React 빌드/배포 정책 (필수 규칙) + +> **경고: React(Next.js) 빌드는 반드시 로컬에서 실행합니다. 서버에서 빌드 절대 금지!** + +### 배경 + +서버 스펙(2코어, 3.8GB RAM, Swap 없음)으로는 Next.js 빌드 시 메모리 부족으로 20분 이상 소요되거나 실패한다. +로컬(WSL)에서 빌드 후 결과물만 서버에 배포한다. + +### 금지 사항 + +``` +❌ 서버에서 npm run build 실행 금지 +❌ 서버 SSH 접속 후 빌드 명령 실행 금지 +❌ Claude가 직접 npm run build 실행 금지 (로컬 포함) +``` + +### 빌드/배포 방법 + +``` +Claude 역할 사용자/팀장 역할 +┌─────────────────┐ ┌─────────────────┐ +│ 코드 작성/수정 │ │ │ +│ git commit │──push──→ │ git pull │ +│ │ │ npm run build │ +│ │ │ 서비스 재시작 │ +└─────────────────┘ └─────────────────┘ +``` + +### 빌드가 필요한 상황 + +사용자에게 다음과 같이 안내한다: + +``` +React 코드가 변경되었습니다. git push 후 서버에서 배포해주세요. +``` + +--- + +## 데이터베이스 아키텍처 (필수 규칙) + +> **경고: 이 규칙을 반드시 준수하세요!** + +### 핵심 원칙 + +**모든 데이터베이스 관련 파일은 API 프로젝트에서만 관리합니다.** + +| 항목 | API (`/home/aweso/sam/api`) | MNG (`/home/aweso/sam/mng`) | +|------|----------------------------|----------------------------| +| 마이그레이션 | ✅ 여기에 생성 | ❌ 생성 금지 | +| 시더 | ✅ 여기에 생성 | ⚠️ MNG 전용만 허용 | +| 팩토리 | ✅ 여기에 생성 | ❌ 생성 금지 | + +### 금지 사항 + +``` +❌ /home/aweso/sam/mng/database/migrations/ 에 파일 생성 금지 +❌ MNG에서 테이블 생성/수정 마이그레이션 작성 금지 +``` + +### 허용 사항 + +``` +✅ /home/aweso/sam/api/database/migrations/ 에 모든 마이그레이션 생성 +✅ MNG에서는 MngMenuSeeder 같은 MNG 전용 시더만 허용 +``` + +### 마이그레이션 실행 + +```bash +# 마이그레이션은 반드시 API 컨테이너에서 실행 +docker exec sam-api-1 php artisan migrate + +# MNG 컨테이너에서 마이그레이션 실행 금지 +# docker exec sam-mng-1 php artisan migrate ← 사용하지 않음 +``` + +### 이유 + +- MNG: 프론트엔드/관리자 화면 담당 (컨트롤러, 뷰, 라우트) +- API: 백엔드/데이터베이스 담당 (마이그레이션, 모델 정의, API) +- 단일 DB를 두 프로젝트가 공유하므로 마이그레이션은 한 곳에서만 관리 + +--- + +## 메뉴 관리 규칙 (필수) + +> **경고: 메뉴 시더(Seeder)를 절대 실행하지 마세요!** + +### 배경 + +메뉴 시더 실행 시 부서별 권한 설정(permission_overrides)이 초기화되는 문제가 반복 발생합니다. +메뉴 ID가 변경되면 기존 부서-메뉴 권한 매핑이 깨지기 때문입니다. + +### 금지 사항 + +``` +❌ php artisan db:seed --class=MngMenuSeeder 실행 금지 +❌ php artisan db:seed --class=*MenuSeeder 실행 금지 +❌ 메뉴 시더 파일 생성 금지 +❌ 메뉴 데이터를 일괄 삭제 후 재생성하는 방식 금지 +``` + +### 메뉴 변경 시 올바른 절차 + +메뉴 추가/수정/삭제/이동이 필요할 때는 **사용자에게 수동 실행 안내**를 제공합니다: + +1. **tinker 명령어를 안내** (사용자가 직접 실행) +2. **또는 SQL 쿼리를 안내** (사용자가 phpMyAdmin 등에서 직접 실행) +3. **절대 시더를 만들어 실행하지 않음** + +### 안내 예시 + +``` +메뉴를 추가하려면 아래 명령을 서버에서 실행해 주세요: + +ssh sam-server "cd /home/webservice/mng && php artisan tinker --execute=\" +App\\Models\\Commons\\Menu::create([ + 'tenant_id' => 1, + 'parent_id' => <부모ID>, + 'name' => '새 메뉴', + 'url' => '/new-menu', + 'icon' => 'icon-name', + 'sort_order' => 1, + 'is_active' => true, +]); +\"" +``` + +### 체크리스트 (메뉴 변경 요청 시) + +- [ ] 시더 파일 생성하지 않음 +- [ ] 시더 실행하지 않음 +- [ ] tinker 또는 SQL로 개별 레코드만 수정 +- [ ] 변경 후 부서 권한 설정이 유지되는지 확인 + +--- + +## Docker 환경 (필수 인지) + +> **중요: 로컬 개발 환경은 Docker 기반입니다!** + +### 왜 Docker를 통해 실행하나? + +PHP, Laravel, Node.js 등이 **Docker 컨테이너 안에** 설치되어 있습니다. +로컬 PC(WSL)에는 이런 도구들이 없으므로, 반드시 Docker 컨테이너를 통해 실행해야 합니다. + +``` +로컬 PC (WSL) +└── Docker + ├── sam-mng-1 ← PHP + Laravel (MNG 앱) + ├── sam-api-1 ← PHP + Laravel (API 앱) + ├── sam-mysql-1 ← MySQL DB + └── sam-nginx-1 ← Nginx 웹서버 +``` + +### Docker 명령어 패턴 + +```bash +# MNG 앱에서 artisan 명령 실행 +docker exec sam-mng-1 php artisan <명령어> + +# API 앱에서 artisan 명령 실행 +docker exec sam-api-1 php artisan <명령어> + +# 예시: 시더 실행 +docker exec sam-mng-1 php artisan db:seed --class=MngMenuSeeder + +# 예시: 마이그레이션 실행 (API에서만!) +docker exec sam-api-1 php artisan migrate + +# 예시: 캐시 클리어 +docker exec sam-mng-1 php artisan cache:clear +``` + +### 체크리스트 (명령 실행 시) + +- [ ] `php artisan` 명령 → `docker exec sam-mng-1 php artisan` 또는 `sam-api-1` 사용 +- [ ] `composer` 명령 → `docker exec sam-mng-1 composer` 또는 `sam-api-1` 사용 +- [ ] DB 시더 실행 필요 시 → Docker를 통해 실행 +- [ ] **마이그레이션은 반드시 API에서 실행** → `docker exec sam-api-1 php artisan migrate` + +--- + +## 공동 개발 워크플로우 (필수) + +> **중요: 코드를 pull 받은 후 반드시 필요한 명령을 실행하세요!** + +### 로컬 환경 (Docker) 업데이트 + +```bash +# 1. 코드 받기 (WSL에서 실행) +cd /home/aweso/sam/api +git pull + +cd /home/aweso/sam/mng +git pull + +# 2. 의존성 업데이트 (composer.json 변경 시) +docker exec sam-api-1 composer install +docker exec sam-mng-1 composer install + +# 3. DB 마이그레이션 (API에서만!) +docker exec sam-api-1 php artisan migrate + +# 4. 캐시 클리어 (설정 변경 시) +docker exec sam-api-1 php artisan config:clear +docker exec sam-mng-1 php artisan config:clear +``` + +### 서버 환경 업데이트 + +```bash +# API 프로젝트 +cd /home/webservice/api +git pull +composer install +php artisan migrate +php artisan config:clear + +# MNG 프로젝트 (마이그레이션 없음) +cd /home/webservice/mng +git pull +composer install +php artisan config:clear +``` + +### 요약 표 + +| 작업 | 로컬 (Docker) | 서버 | +|------|--------------|------| +| git pull | WSL에서 직접 | 서버에서 직접 | +| composer install | `docker exec sam-api-1 composer install` | `composer install` | +| migrate | `docker exec sam-api-1 php artisan migrate` | `php artisan migrate` | +| config:clear | `docker exec sam-api-1 php artisan config:clear` | `php artisan config:clear` | + +### 체크리스트 (pull 후) + +- [ ] API: `git pull` → `composer install` → `php artisan migrate` → `config:clear` +- [ ] MNG: `git pull` → `composer install` → `config:clear` (마이그레이션 없음) + +--- + +## 사용 가능한 Agents + +`~/.claude/agents/` 폴더에 있는 에이전트들: + +### 코드 품질 & 개발 + +| Agent | 모델 | 설명 | 출처 | +|-------|------|------|------| +| `code-reviewer` | sonnet | 코드 리뷰 (품질/보안/유지보수성), 메모리 학습 지원 | 공식 문서 패턴 | +| `debugger` | sonnet | 에러/테스트 실패 근본 원인 분석 및 수정 | 공식 문서 패턴 | +| `test-runner` | haiku | 테스트 실행 및 결과 분석/요약 | 커뮤니티 인기 | +| `security-auditor` | sonnet | OWASP Top 10 기반 보안 취약점 감사 | 커뮤니티 인기 | +| `performance-optimizer` | sonnet | N+1 쿼리, 알고리즘, 캐싱 최적화 | 커뮤니티 인기 | +| `refactoring-agent` | sonnet | 코드 구조 개선, SOLID 원칙, DRY 위반 제거 | 커뮤니티 인기 | +| `laravel-expert` | sonnet | Laravel 전문가 (SAM 프로젝트 환경 인지) | 커스텀 | + +### 워크플로우 & 문서 + +| Agent | 모델 | 설명 | 출처 | +|-------|------|------|------| +| `git-manager` | haiku | Git 브랜치/커밋/머지/PR 관리 | 커뮤니티 인기 | +| `doc-writer` | haiku | API 문서, README, 기술 가이드 작성 | 커뮤니티 인기 | +| `research-agent` | sonnet | 웹 리서치 및 자료 조사 | 기존 | +| `organizer-agent` | - | 프로젝트 구조화 및 정리 | 기존 | +| `proposal-agent` | - | 제안서 작성 | 기존 | + +--- + +## 사용 가능한 Skills + +`~/.claude/skills/` 폴더에 있는 스킬들 (슬래시 명령어로 사용): + +### 문서/프레젠테이션 + +| Skill | 설명 | +|-------|------| +| `pptx-skill` | PowerPoint 생성 | +| `ppt-auto-generator` | 마크다운/텍스트에서 PPT 생성 | +| `pdf-template-skill` | PDF 템플릿 분석/생성 | +| `text-analyzer-skill` | 텍스트 분석 및 PDF 구조 매핑 | +| `proposal-skill` | 제안서 생성 | +| `storyboard-generator` | 스토리보드 생성 | +| `design-skill` | 프레젠테이션 HTML 디자인 | + +### 코드 분석/시각화 + +| Skill | 설명 | +|-------|------| +| `code-flow-web-report` | 웹 앱 런타임 흐름 시각화 리포트 | +| `code-flow-web-doc-generator` | 소스 코드 호출/데이터 흐름 다이어그램 HTML 생성 | +| `codebase-analysis-web-report` | 코드베이스 아키텍처 인터랙티브 HTML 리포트 | +| `uml-generator` | UML 다이어그램 생성 | + +### 코드 품질 (levnikolaevich/claude-code-skills) + +| Skill | 설명 | 출처 | +|-------|------|------| +| `code-bug-finder` | 버그 자동 탐지 및 보고서 생성 | 기존 | +| `code-refactoring` | 리팩토링 권장사항/성능 분석/코드 패치 | 기존 | +| `code-commenter` | 소스 코드에 이해하기 쉬운 주석 추가 | 기존 | +| `async-await-keyword-fixer` | JS/TS 누락된 async/await 수정 | 기존 | +| `code-quality-checker` | DRY/KISS/YAGNI 위반 탐지 | levnikolaevich | +| `code-quality-auditor` | 코드 복잡도, 매직넘버 분석 | levnikolaevich | +| `code-principles-auditor` | DRY/KISS/YAGNI, TODO, DI 패턴 검사 | levnikolaevich | +| `dead-code-auditor` | 미사용 코드 탐지 | levnikolaevich | +| `build-auditor` | 컴파일러/타입 에러 검사 | levnikolaevich | +| `concurrency-auditor` | 레이스 컨디션 탐지 | levnikolaevich | +| `layer-boundary-auditor` | 레이어 위반, I/O 격리 검사 | levnikolaevich | +| `observability-auditor` | 로깅, 메트릭 적절성 검사 | levnikolaevich | +| `query-efficiency-auditor` | DB 쿼리 효율성 분석 | levnikolaevich | +| `dependencies-auditor` | 오래된 패키지, CVE 취약점 검사 | levnikolaevich | +| `regression-checker` | 기존 테스트 실행으로 사이드이펙트 탐지 | levnikolaevich | +| `story-quality-gate` | 코드리뷰 + 테스트 2단계 품질 검증 | levnikolaevich | + +### 테스트/커버리지 + +| Skill | 설명 | 출처 | +|-------|------|------| +| `app-comprehensive-test-generator` | 테스트 시나리오 생성/실행, QA 리포트 | 기존 | +| `coverage-improvement-planner` | 테스트 커버리지 분석 및 개선 계획 | 기존 | +| `test-coverage-auditor` | 테스트 커버리지 측정/분석 | levnikolaevich | +| `test-isolation-auditor` | 테스트 독립성/격리 검사 | levnikolaevich | +| `webapp-testing` | Playwright 기반 웹 앱 UI 테스트 | anthropics 공식 | + +### 보안 (Trail of Bits) + +| Skill | 설명 | 출처 | +|-------|------|------| +| `security-auditor` | 시크릿 노출, Injection, XSS 탐지 | levnikolaevich | +| `static-analysis` | CodeQL/Semgrep/SARIF 정적 분석 (3개 하위 스킬) | Trail of Bits | +| `insecure-defaults` | 위험한 기본 설정, 하드코딩 자격증명 탐지 | Trail of Bits | +| `sharp-edges` | 에러 유발 API, 위험한 디자인 패턴 탐지 | Trail of Bits | +| `differential-review` | 보안 중심 코드 변경 리뷰 | Trail of Bits | + +### 디버깅/로깅 + +| Skill | 설명 | +|-------|------| +| `system-debug-logger` | 에러/예외 자동 캡처 디버그 로깅 | +| `node-debug-logging-middleware` | Node.js Express/Koa 디버깅 로그 미들웨어 | + +### 프론트엔드/UI + +| Skill | 설명 | 출처 | +|-------|------|------| +| `frontend-design` | 프론트엔드 디자인 품질 향상 (AI slop 방지) | anthropics 공식 | +| `flutter-ux-hardening` | Flutter 앱 UI/UX 강화 | 기존 | +| `웹문서` | SAM 프로젝트 웹문서 디자인 표준 | 기존 | + +### 유틸리티 + +| Skill | 설명 | +|-------|------| +| `duplicate-file-cleaner` | 중복 이미지/미디어 파일 정리 | +| `npm-release-manager` | NPM 패키지 배포 자동화 | + +**사용 방법**: `/skill-name` 형식으로 호출 (예: `/code-quality-checker`) + +--- + +## 문서 작성 규칙 (개발팀 협약 - 필수 준수) + +> **경고: 개발자들이 `sam/docs`의 문서 작성 기법을 준용하기로 협약했습니다. 모든 문서 작성 시 반드시 따르세요!** + +### 참조 경로 + +- **인덱스**: `/home/aweso/sam/docs/INDEX.md` (전체 문서 목록 및 폴더 구조) +- **작업 전 확인**: 작업 유형에 맞는 문서를 `INDEX.md`에서 찾아 먼저 읽고 시작 + +### 폴더 선택 기준 (의미 기반 분류) + +| 폴더 | 질문 | 설명 | +|------|------|------| +| `plans/` | "무슨 작업을 할 것인가?" | 임시 개발 계획 (완료 후 삭제) | +| `standards/` | "어떻게 코드를 작성할 것인가?" | 코딩 컨벤션, 스타일 가이드 | +| `architecture/` | "왜 이렇게 설계하는가?" | 시스템 설계, 아키텍처 결정 | +| `rules/` | "무엇이 유효한 데이터인가?" | 비즈니스 규칙, 검증 규칙 | +| `specs/` | "무엇을 구현할 것인가?" | 기술 스펙, DB 스키마 | +| `guides/` | "어떻게 구현할 것인가?" | 단계별 구현 매뉴얼 | +| `features/` | 기능별 상세 | 기능 단위 심층 문서 | +| `changes/` | "무엇이 변경되었는가?" | 완료된 변경 이력 | + +### 파일명 규칙 + +- **일반 문서**: `kebab-case.md` (소문자 + 하이픈) 예: `api-rules.md`, `item-policy.md` +- **변경 이력**: `YYYYMMDD_short_description.md` 예: `20260109_handover_report_api.md` +- **폴더 인덱스**: `README.md` (대문자) +- **크기 목표**: 10KB 이하 +- **새 문서 작성 시**: 반드시 `docs/INDEX.md`에 추가 + +### 문서 구조 템플릿 + +#### 정책/규칙 문서 (`rules/`, `standards/`) + +```markdown +# 제목 + +> **작성일**: YYYY-MM-DD +> **상태**: 설계 확정 + +--- + +## 1. 개요 +### 1.1 목적 +### 1.2 핵심 원칙 + +--- + +## 2. 테이블 구조 (해당 시) +### 2.1 ERD 개요 + +--- + +## N. 비즈니스 규칙 +### N.1 검증 규칙 + +--- + +## N. API 엔드포인트 + +--- + +## 관련 문서 + +--- + +**최종 업데이트**: YYYY-MM-DD +``` + +#### 변경 이력 문서 (`changes/`) + +```markdown +# 변경 내용 요약 + +**날짜:** YYYY-MM-DD +**작업자:** Claude Code + +## 변경 개요 + +## 수정된 파일 +| 파일 | 변경 내용 | +|------|----------| + +## 상세 변경 사항 + +## 테스트 체크리스트 +- [x] 완료 항목 +- [ ] 미완료 항목 + +## 관련 문서 +``` + +### 작성 스타일 규칙 + +| 항목 | 규칙 | +|------|------| +| **언어** | 한글 기본, 코드/경로/기술 식별자만 영어 | +| **어조** | 서술형 ("X를 해야 한다" 아닌 "X 한다") | +| **경고** | `> **경고: ...**` 블록인용 형식 | +| **금지/필수** | `❌` 금지, `✅` 필수 접두사 | +| **우선순위** | `🔴 필수`, `🟡 중요`, `🟢 권장` | +| **섹션 번호** | `## 1.`, `### 1.1` 번호 매기기 | +| **규칙 번호** | R1, R2, R3... 순차 라벨 | +| **코드 블록** | 반드시 언어 지정 (```php, ```bash, ```json, ```sql) | +| **인라인 코드** | 파일 경로, 메서드명, 변수명, 컬럼명에 백틱 | +| **다이어그램** | `┌─┐│└─┘` 박스 문자, `→` 화살표 사용 | +| **구분선** | `---` 주요 섹션 사이마다 | +| **테이블** | API: `| Method | Path | 설명 |`, 필드: `| 필드 | 타입 | 설명 |` | + +### plans/ 워크플로우 + +1. 개발 계획 문서를 `plans/`에 작성 +2. 작업 진행 +3. 완료 후 결과물을 해당 폴더(`features/`, `changes/` 등)에 정리 +4. plan 문서 삭제 + +### 체크리스트 (문서 작성 시) + +- [ ] 적절한 폴더에 배치 (위 폴더 선택 기준 참고) +- [ ] `kebab-case.md` 파일명 사용 +- [ ] 문서 구조 템플릿 준수 +- [ ] 한글 기본, 기술 용어만 영어 +- [ ] 코드 블록에 언어 지정 +- [ ] `docs/INDEX.md`에 새 문서 등록 +- [ ] 10KB 이하 크기 유지 + +--- + +## PPT / 프레젠테이션 제작 규칙 (필수 준수) + +> **경고: 모든 프레젠테이션 및 문서 제작 시 반드시 따르세요!** + +### 회사 정보 + +| 항목 | 값 | +|------|------| +| **공식 회사명** | **(주)코드브릿지엑스** | +| **서비스명** | **SAM** (Smart Automation Management) | +| **푸터 표기 예시** | `SAM 서비스 요금 안내 | (주)코드브릿지엑스` | + +### 금지 사항 + +``` +❌ "주일/경동" — 문서, 슬라이드, 푸터 어디에도 사용 금지 +❌ "주일", "경동" 단독 사용 금지 +❌ 내부 제조사(주일/경동) 이름을 외부 문서에 노출 금지 +``` + +> **배경**: 주일/경동은 SAM을 기반으로 만든 내부 제조업체 이름이며, 대외 문서에 노출되어서는 안 된다. +> 모든 대외 문서의 회사명은 **(주)코드브릿지엑스**를 사용한다. + +### SAM BI (Brand Identity) 이미지 + +**프로젝트 내 경로**: `/home/aweso/sam/docs/assets/bi/` + +| 파일 | 용도 | 배경 | +|------|------|------| +| `sam_bi_black.png` | 밝은 배경 슬라이드 | 투명 배경, 검정 로고 | +| `sam_bi_white.png` | 다크 배경 슬라이드 | 투명 배경, 흰색 로고 | +| `sam_bi_blue.png` | 청색 테마 슬라이드 | 투명 배경, 파란 로고 | +| `sam_bi_green.png` | 녹색 테마 슬라이드 | 녹색 배경, 흰색 로고 | +| `sam_bi_red.png` | 적색/대외비 슬라이드 | 적색 배경, 흰색 로고 | +| `sam_bi_orange.png` | 주황 포인트 슬라이드 | 주황 배경, 흰색 로고 | +| `sam_bi_purple.png` | 보라 테마 슬라이드 | 보라 배경, 흰색 로고 | + +### PPT 슬라이드 제작 시 적용 규칙 + +1. **표지(slide-01)에 BI 로고 필수** — 배경색에 맞는 BI 이미지 사용 +2. **푸터에 회사명**: `(주)코드브릿지엑스` (주일/경동 절대 금지) +3. **BI 로고 + "SAM" 텍스트** 조합 사용 권장 +4. **배경색별 BI 선택**: + - 다크 배경 → `sam_bi_white.png` + - 밝은 배경 → `sam_bi_black.png` + - 테마 컬러 배경 → 해당 색상 BI (green, blue, red 등) + +### 체크리스트 (PPT 제작 시) + +- [ ] 회사명: (주)코드브릿지엑스 사용 +- [ ] "주일/경동" 미포함 확인 +- [ ] 표지에 SAM BI 로고 포함 +- [ ] 푸터에 (주)코드브릿지엑스 표기 +- [ ] 배경색에 맞는 BI 색상 선택 From c726e0852ec6924e65c8b6e71b684af1c701b4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Feb 2026 19:51:36 +0900 Subject: [PATCH 14/69] =?UTF-8?q?docs:=20[plans]=20=EC=9A=B4=EC=98=81=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B0=B0=ED=8F=AC=20=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 12개 섹션 구성: 환경 전략, 서버 아키텍처, CI/CD, DB, 보안 등 - Jenkins CI/CD 파이프라인 설계 (4개 저장소) - 단계별 마이그레이션 체크리스트 (Phase 1~4) - INDEX.md에 문서 등록 --- INDEX.md | 14 + plans/production-deployment-plan.md | 1099 +++++++++++++++++++++++++++ 2 files changed, 1113 insertions(+) create mode 100644 plans/production-deployment-plan.md diff --git a/INDEX.md b/INDEX.md index 572b412..a3d9121 100644 --- a/INDEX.md +++ b/INDEX.md @@ -19,6 +19,7 @@ | **품목관리** | `rules/item-policy.md` | 품목 정책 (유형, 예약어, API 규칙) | | **게시판** | `specs/board-system-spec.md` | 게시판 시스템 설계 | | **단가관리** | `rules/pricing-policy.md` | 원가/판매가 계산, 리비전 관리 | +| **운영 배포** | `plans/production-deployment-plan.md` | 운영 환경 배포 계획 (CI/CD, 서버 아키텍처) | | **과금정책 (고객용)** | `rules/customer-pricing.md` | 고객 안내용 서비스 요금표 | | **과금정책 (파트너)** | `rules/partner-commission.md` | 영업파트너 수당 체계 및 정산 | | **과금정책 (내부용)** | `rules/billing-policy.md` | 내부용 원가/마진/코드참조 (CONFIDENTIAL) | @@ -42,6 +43,7 @@ docs/ ├── features/ # 기능별 상세 문서 ├── projects/ # 프로젝트별 문서 (MES, Legacy) ├── history/ # 히스토리 및 로드맵 +├── contracts/ # 전자계약서 버전 관리 ├── changes/ # 변경 이력 └── data/ # 데이터 분석 ``` @@ -125,6 +127,18 @@ docs/ |------|------| | [analysis/item-db-analysis.md](data/analysis/item-db-analysis.md) | Item DB/API 분석 최종본 | +### contracts/ - 전자계약서 버전 관리 +> DOCX 배포본 + Markdown 추적본 + 자동화 스크립트 + +| 문서 | 설명 | +|------|------| +| [CHANGELOG.md](contracts/CHANGELOG.md) | 전체 개정이력 | +| [revisions.json](contracts/revisions.json) | 개정 데이터 | +| [docx/](contracts/docx/) | DOCX 배포본 (전자서명용 4종, 바로 사용 가능) | +| [markdown/](contracts/markdown/) | Markdown 추적본 (Git diff용 4종) | +| [scripts/extract_to_markdown.py](contracts/scripts/extract_to_markdown.py) | DOCX → Markdown 추출 | +| [scripts/sync_check.py](contracts/scripts/sync_check.py) | DOCX ↔ Markdown 동기화 검증 | + ### features/ - 기능별 문서 | 문서 | 설명 | diff --git a/plans/production-deployment-plan.md b/plans/production-deployment-plan.md new file mode 100644 index 0000000..bbcde30 --- /dev/null +++ b/plans/production-deployment-plan.md @@ -0,0 +1,1099 @@ +# SAM 운영 환경 배포 계획서 + +> **작성일**: 2026-02-22 +> **상태**: 계획 수립 +> **대상**: MS3 정식 런칭 (2026-02-28) +> **작성자**: 개발팀 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM 프로젝트의 MS3(정식 런칭, 2026-02-28)을 위해 개발 환경(`dev.codebridge-x.com`)에서 운영 환경(`codebridge-x.com`)으로의 전환을 체계적으로 수행한다. 수동 배포 방식에서 Jenkins CI/CD 기반 자동화 배포로 전환하여 안정적인 운영 체계를 구축한다. + +### 1.2 핵심 원칙 + +- 🔴 **무중단 전환**: 개발 환경 서비스에 영향 없이 운영 환경을 구축한다 +- 🔴 **롤백 가능**: 모든 배포는 즉시 롤백 가능해야 한다 +- 🔴 **자동화 우선**: 반복 작업은 Jenkins 파이프라인으로 자동화한다 +- 🟡 **점진적 전환**: 한 번에 전환하지 않고 Phase별로 검증한다 + +### 1.3 현재 환경 vs 목표 환경 + +| 항목 | 현재 (개발) | 목표 (운영) | +|------|------------|------------| +| **서버** | 114.203.209.83 (2코어/3.8GB) | 신규 서버 (4코어/8GB 이상) | +| **도메인** | `dev.codebridge-x.com` | `codebridge-x.com` | +| **배포 방식** | 수동 (git pull + SSH) | Jenkins CI/CD 자동화 | +| **SSL** | 자체 서명 인증서 | Let's Encrypt | +| **DB** | `samdb` (개발/운영 공용) | `sam_prod` (운영 전용) | +| **모니터링** | 없음 | 헬스체크 + Slack 알림 | +| **백업** | 수동 | 자동 일일 백업 | + +### 1.4 관련 문서 + +| 문서 | 경로 | +|------|------| +| 런칭 로드맵 | `guides/project-launch-roadmap.md` | +| .env 동기화 | `guides/production-env-sync.md` | +| Docker 환경 스펙 | `specs/docker-setup.md` | +| 보안 정책 | `architecture/security-policy.md` | + +--- + +## 2. 환경 전략 + +### 2.1 3-Tier 환경 분리 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 로컬 (WSL) │ │ 개발 서버 │ │ 운영 서버 │ +│ Docker 기반 │ │ Bare-metal │ │ Bare-metal │ +│ │ │ │ │ │ +│ dev.sam.kr │────→│ dev.codebridge │────→│ codebridge-x │ +│ (hosts 매핑) │ │ -x.com │ │ .com │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + 개발/테스트 스테이징/CI/CD 정식 서비스 +``` + +### 2.2 도메인 매핑 + +| 서비스 | 로컬 (WSL Docker) | 개발 서버 | 운영 서버 | +|--------|-------------------|----------|----------| +| **React (사용자)** | `dev.sam.kr` | `dev.codebridge-x.com` | `codebridge-x.com` | +| **API** | `api.sam.kr` | `api.dev.codebridge-x.com` | `api.codebridge-x.com` | +| **MNG (관리자)** | `mng.sam.kr` | `mng.dev.codebridge-x.com` | `mng.codebridge-x.com` | +| **Sales** | `sales.sam.kr` | `sales.dev.codebridge-x.com` | `sales.codebridge-x.com` | +| **5130 (레거시)** | `5130.sam.kr` | - | - | +| **Gitea** | - | `114.203.209.83:3000` | - | + +### 2.3 .env 분기 전략 + +> 상세 동기화 절차는 `guides/production-env-sync.md` 참조 + +| 환경 변수 | 로컬 (Docker) | 개발 서버 | 운영 서버 | +|-----------|--------------|----------|----------| +| `APP_ENV` | `local` | `development` | `production` | +| `APP_DEBUG` | `true` | `true` | `false` | +| `APP_URL` | `https://api.sam.kr` | `https://api.dev.codebridge-x.com` | `https://api.codebridge-x.com` | +| `DB_HOST` | `sam-mysql-1` | `localhost` | `localhost` | +| `DB_DATABASE` | `samdb` | `samdb` | `sam_prod` | +| `LOG_CHANNEL` | `stack` | `stack` | `stack` | +| `LOG_LEVEL` | `debug` | `debug` | `warning` | +| `BAROBILL_TEST_MODE` | `true` | `true` | `false` | + +--- + +## 3. 운영 서버 아키텍처 + +### 3.1 서버 스펙 권장 + +| 항목 | 최소 사양 | 권장 사양 | 사유 | +|------|----------|----------|------| +| **CPU** | 4코어 | 8코어 | PHP-FPM 3풀 + Node.js 동시 운영 | +| **RAM** | 8GB | 16GB | PHP-FPM 풀당 ~1.5GB + MySQL ~2GB | +| **디스크** | 100GB SSD | 200GB SSD | DB + 로그 + 파일 스토리지 | +| **OS** | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS | 장기 지원 | + +> **경고: 현재 개발 서버(2코어/3.8GB)에서는 React 빌드 시 메모리 부족으로 실패한다. 운영 서버는 최소 8GB를 확보해야 한다.** + +### 3.2 Bare-metal 운영 결정 + +운영 서버는 Docker를 사용하지 않고 Bare-metal로 구성한다 (현재 개발 서버와 동일 방식). + +| 항목 | Docker | Bare-metal (선택) | +|------|--------|------------------| +| 리소스 오버헤드 | 15~20% | 없음 | +| 서버 스펙 요구 | 높음 | 낮음 | +| 운영 복잡도 | 중간 | 낮음 | +| 현재 개발 서버 | - | 이미 이 방식 사용 중 | + +### 3.3 서비스 레이아웃 + +``` +┌────────────────────────────────────────────────────────────┐ +│ 운영 서버 │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Nginx (Reverse Proxy + Static Files) │ │ +│ │ :80 → HTTPS redirect │ │ +│ │ :443 → PHP-FPM / Node.js │ │ +│ └──────────┬──────────┬──────────┬────────────────────┘ │ +│ │ │ │ │ +│ ┌──────────┴┐ ┌──────┴──────┐ ┌┴───────────┐ │ +│ │ PHP-FPM │ │ PHP-FPM │ │ PHP-FPM │ │ +│ │ pool: api │ │ pool: mng │ │ pool: sales│ │ +│ │ :9001 │ │ :9002 │ │ :9003 │ │ +│ └───────────┘ └─────────────┘ └────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────────────────────┐ │ +│ │ Node.js │ │ Supervisor │ │ +│ │ (React SSR) │ │ - API Queue Worker (x1) │ │ +│ │ :3000 │ │ - MNG Queue Worker (x2) │ │ +│ └──────────────┘ │ - API Scheduler │ │ +│ └──────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ MySQL 8.0 (sam_prod) │ │ +│ │ :3306 (localhost only) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +### 3.4 PHP-FPM 풀 설정 + +현재 Docker Supervisor 설정 기반으로 운영 서버 PHP-FPM 풀을 구성한다. + +**API 풀** (`/etc/php/8.4/fpm/pool.d/api.conf`): + +```ini +[api] +user = www-data +group = www-data +listen = /run/php/php8.4-fpm-api.sock +pm = dynamic +pm.max_children = 10 +pm.start_servers = 3 +pm.min_spare_servers = 2 +pm.max_spare_servers = 5 +pm.max_requests = 500 +request_terminate_timeout = 300 +chdir = /home/webservice/api +``` + +**MNG 풀** (`/etc/php/8.4/fpm/pool.d/mng.conf`): + +```ini +[mng] +user = www-data +group = www-data +listen = /run/php/php8.4-fpm-mng.sock +pm = dynamic +pm.max_children = 15 +pm.start_servers = 5 +pm.min_spare_servers = 3 +pm.max_spare_servers = 8 +pm.max_requests = 500 +request_terminate_timeout = 300 +chdir = /home/webservice/mng +``` + +**Sales 풀** (`/etc/php/8.4/fpm/pool.d/sales.conf`): + +```ini +[sales] +user = www-data +group = www-data +listen = /run/php/php8.4-fpm-sales.sock +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 500 +chdir = /home/webservice/sales +``` + +### 3.5 Supervisor 프로세스 설정 + +현재 Docker 컨테이너의 `supervisord.conf`를 운영 서버용으로 변환한다. + +**API Queue Worker** (`/etc/supervisor/conf.d/sam-api-worker.conf`): + +```ini +[program:sam-api-worker] +command=php /home/webservice/api/artisan queue:work database --queue=api,default --sleep=3 --tries=3 --timeout=1800 --max-jobs=100 --max-time=3600 +process_name=%(program_name)s_%(process_num)02d +numprocs=1 +directory=/home/webservice/api +autostart=true +autorestart=true +startsecs=5 +startretries=3 +stopwaitsecs=1830 +user=www-data +stdout_logfile=/var/log/sam/api-queue-worker.log +stdout_logfile_maxbytes=5MB +stderr_logfile=/var/log/sam/api-queue-worker-error.log +stderr_logfile_maxbytes=5MB +``` + +**MNG Queue Worker** (`/etc/supervisor/conf.d/sam-mng-worker.conf`): + +```ini +[program:sam-mng-worker] +command=php /home/webservice/mng/artisan queue:work database --queue=mng,default --sleep=3 --tries=1 --timeout=1800 --max-jobs=10 --max-time=3600 +process_name=%(program_name)s_%(process_num)02d +numprocs=2 +directory=/home/webservice/mng +autostart=true +autorestart=true +startsecs=5 +startretries=3 +stopwaitsecs=1830 +user=www-data +stdout_logfile=/var/log/sam/mng-queue-worker.log +stdout_logfile_maxbytes=5MB +stderr_logfile=/var/log/sam/mng-queue-worker-error.log +stderr_logfile_maxbytes=5MB +``` + +**API Scheduler** (`/etc/supervisor/conf.d/sam-api-scheduler.conf`): + +```ini +[program:sam-api-scheduler] +command=bash -c "while true; do php /home/webservice/api/artisan schedule:run --no-interaction; sleep 60; done" +process_name=%(program_name)s +numprocs=1 +directory=/home/webservice/api +autostart=true +autorestart=true +startsecs=0 +user=www-data +stdout_logfile=/var/log/sam/api-scheduler.log +stdout_logfile_maxbytes=5MB +stderr_logfile=/var/log/sam/api-scheduler-error.log +stderr_logfile_maxbytes=5MB +``` + +### 3.6 필수 패키지 설치 + +현재 Docker Dockerfile 기반으로 운영 서버에 설치할 패키지 목록: + +```bash +# PHP 8.4 + 확장 모듈 (API + MNG 공통) +apt install php8.4-fpm php8.4-mysql php8.4-zip php8.4-intl \ + php8.4-xml php8.4-soap php8.4-mbstring php8.4-curl + +# MNG 전용 (GD + LibreOffice + FFmpeg) +apt install php8.4-gd libreoffice-writer-nogui \ + fonts-nanum fonts-nanum-extra ffmpeg + +# Node.js 20 LTS (React SSR) +curl -fsSL https://deb.nodesource.com/setup_20.x | bash - +apt install nodejs + +# 기타 +apt install nginx mysql-server supervisor git unzip +``` + +--- + +## 4. Jenkins CI/CD 파이프라인 + +### 4.1 Jenkins 설치 위치 + +Jenkins는 **개발 서버(114.203.209.83)**에 설치한다. 서버 메모리 한계를 고려하여 Swap을 추가한다. + +```bash +# Swap 4GB 추가 +sudo fallocate -l 4G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab + +# Jenkins 설치 +wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo apt-key add - +echo "deb https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list +sudo apt update && sudo apt install jenkins +``` + +### 4.2 Gitea Webhook 연동 + +각 저장소에서 Push 이벤트 발생 시 Jenkins 빌드가 자동 트리거된다. + +| 저장소 | Gitea URL | Jenkins Job | +|--------|-----------|-------------| +| sam-api | `http://114.203.209.83:3000/SamProject/sam-api.git` | `sam-api-deploy` | +| sam-manage | `http://114.203.209.83:3000/SamProject/sam-manage.git` | `sam-mng-deploy` | +| sam-react-prod | `http://114.203.209.83:3000/SamProject/sam-react-prod.git` | `sam-react-deploy` | +| sam-sales | `http://114.203.209.83:3000/SamProject/sam-sales.git` | `sam-sales-deploy` | +| sam-docs | `http://114.203.209.83:3000/SamProject/sam-docs.git` | - (배포 없음) | + +### 4.3 브랜치 전략 + +``` +feature/* ──→ develop ──→ main/master + (자동배포) (승인 후 배포) + ↓ ↓ + 개발 서버 운영 서버 +``` + +| 브랜치 | 배포 대상 | 트리거 | 승인 | +|--------|----------|--------|------| +| `develop` | 개발 서버 | Push 자동 | 불필요 | +| `main`/`master` | 운영 서버 | PR 머지 | 팀장 승인 필수 | + +### 4.4 저장소별 Jenkinsfile + +#### sam-api 파이프라인 + +```groovy +pipeline { + agent any + + environment { + DEPLOY_SERVER = credentials('prod-server-ssh') + DEPLOY_PATH = '/home/webservice/api' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Lint') { + steps { + sh 'composer install --no-interaction' + sh './vendor/bin/pint --test' + } + } + + stage('Test') { + steps { + sh 'php artisan test --parallel' + } + } + + stage('Deploy') { + when { + branch 'main' + } + steps { + sshagent(['prod-server-ssh']) { + sh """ + ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} ' + cd ${DEPLOY_PATH} && + git pull origin main && + composer install --no-dev --optimize-autoloader && + php artisan migrate --force && + php artisan config:clear && + php artisan cache:clear && + php artisan route:cache && + php artisan view:cache && + sudo supervisorctl restart sam-api-worker:* + ' + """ + } + } + } + } + + post { + success { + slackSend channel: '#sam-deploy', + message: "API 배포 성공: ${env.BUILD_URL}" + } + failure { + slackSend channel: '#sam-alerts', + message: "API 배포 실패: ${env.BUILD_URL}" + } + } +} +``` + +#### sam-manage 파이프라인 + +```groovy +pipeline { + agent any + + environment { + DEPLOY_SERVER = credentials('prod-server-ssh') + DEPLOY_PATH = '/home/webservice/mng' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Lint') { + steps { + sh 'composer install --no-interaction' + sh './vendor/bin/pint --test' + } + } + + stage('Build Assets') { + steps { + sh 'npm ci && npx tailwindcss -o public/css/app.css --minify' + } + } + + stage('Deploy') { + when { + branch 'master' + } + steps { + sshagent(['prod-server-ssh']) { + sh """ + ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} ' + cd ${DEPLOY_PATH} && + git pull origin master && + composer install --no-dev --optimize-autoloader && + php artisan config:clear && + php artisan cache:clear && + php artisan view:cache && + sudo supervisorctl restart sam-mng-worker:* + ' + """ + } + } + } + } + + post { + success { + slackSend channel: '#sam-deploy', + message: "MNG 배포 성공: ${env.BUILD_URL}" + } + failure { + slackSend channel: '#sam-alerts', + message: "MNG 배포 실패: ${env.BUILD_URL}" + } + } +} +``` + +> **참고**: MNG는 마이그레이션을 실행하지 않는다. 모든 마이그레이션은 API에서만 실행한다. + +#### sam-react-prod 파이프라인 + +```groovy +pipeline { + agent any + + environment { + DEPLOY_SERVER = credentials('prod-server-ssh') + DEPLOY_PATH = '/home/webservice/react' + BUILD_FILE = 'next-standalone.tar.gz' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Install') { + steps { + sh 'npm ci' + } + } + + stage('Lint') { + steps { + sh 'npm run lint' + } + } + + stage('Build') { + steps { + sh ''' + # .env.local 백업 (.env.production 으로 빌드) + [ -f .env.local ] && mv .env.local .env.local.bak + + npm run build + + # .env.local 복원 + [ -f .env.local.bak ] && mv .env.local.bak .env.local + + # standalone 빌드 확인 + test -f .next/standalone/server.js + ''' + } + } + + stage('Package') { + steps { + sh """ + rm -f ${BUILD_FILE} + COPYFILE_DISABLE=1 tar -czf ${BUILD_FILE} \ + .next/standalone \ + .next/static \ + public + """ + } + } + + stage('Deploy') { + when { + branch 'master' + } + steps { + sshagent(['prod-server-ssh']) { + sh """ + scp ${BUILD_FILE} ${DEPLOY_SERVER}:${DEPLOY_PATH}/ + ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} ' + cd ${DEPLOY_PATH} && + lsof -ti:3000 | xargs kill 2>/dev/null || true && + sleep 2 && + rm -rf .next.bak && + mv .next .next.bak 2>/dev/null || true && + tar xzf ${BUILD_FILE} && + cp -r .next/static .next/standalone/.next/static && + cp -r public .next/standalone/public && + cp .env.production .next/standalone/.env.production 2>/dev/null || true && + cd .next/standalone && + PORT=3000 HOSTNAME=0.0.0.0 nohup node server.js > /tmp/sam-react.log 2>&1 & && + sleep 3 && + cd ${DEPLOY_PATH} && + rm -f ${BUILD_FILE} && + rm -rf .next.bak + ' + """ + } + } + } + } + + post { + success { + slackSend channel: '#sam-deploy', + message: "React 배포 성공: ${env.BUILD_URL}" + } + failure { + slackSend channel: '#sam-alerts', + message: "React 배포 실패: ${env.BUILD_URL}" + } + } +} +``` + +> **경고: React 빌드는 Jenkins 서버(Swap 추가 후)에서 수행한다. Jenkins 서버에서도 메모리 부족 시 로컬(WSL)에서 빌드 후 `deploy.sh`로 배포한다.** + +#### sam-sales 파이프라인 (간소화) + +```groovy +pipeline { + agent any + + stages { + stage('Deploy') { + when { + branch 'main' + } + steps { + sshagent(['prod-server-ssh']) { + sh """ + ssh -o StrictHostKeyChecking=no ${DEPLOY_SERVER} ' + cd /home/webservice/sales && + git pull origin main && + composer install --no-dev --optimize-autoloader && + php artisan config:clear && + php artisan cache:clear + ' + """ + } + } + } + } +} +``` + +--- + +## 5. 데이터베이스 전략 + +### 5.1 개발/운영 DB 물리 분리 + +| 항목 | 개발 DB | 운영 DB | +|------|---------|---------| +| **DB명** | `samdb` | `sam_prod` | +| **위치** | 개발 서버 (114.203.209.83) | 운영 서버 | +| **접속** | `samuser`/`sampass` | 별도 운영 계정 | +| **용도** | 개발/테스트 | 정식 서비스 | + +### 5.2 마이그레이션 규칙 + +> **경고: 모든 마이그레이션은 API 프로젝트(`/home/webservice/api`)에서만 실행한다. MNG에서 마이그레이션 실행 금지.** + +```bash +# 운영 서버 마이그레이션 (API에서만) +cd /home/webservice/api +php artisan migrate --force +``` + +### 5.3 초기 데이터 마이그레이션 절차 + +```bash +# 1. 개발 DB 덤프 (구조 + 필수 데이터) +mysqldump -u samuser -p samdb \ + --single-transaction \ + --routines \ + --triggers \ + --add-drop-table \ + > sam_initial_dump.sql + +# 2. 운영 서버로 전송 +scp sam_initial_dump.sql user@prod-server:/tmp/ + +# 3. 운영 DB에 복원 +mysql -u sam_prod_user -p sam_prod < /tmp/sam_initial_dump.sql + +# 4. 운영 전용 설정 적용 +mysql -u sam_prod_user -p sam_prod << 'EOF' +-- 바로빌 운영 모드 전환 +UPDATE barobill_configs SET is_active = 0 WHERE environment = 'test'; +UPDATE barobill_configs SET is_active = 1 WHERE environment = 'production'; +UPDATE barobill_members SET server_mode = 'production'; + +-- 테스트 데이터 정리 (필요 시) +-- DELETE FROM ... WHERE is_test = 1; +EOF +``` + +### 5.4 백업 체계 + +| 항목 | 주기 | 보관 기간 | 방법 | +|------|------|----------|------| +| **전체 백업** | 매일 03:00 | 30일 | `mysqldump --single-transaction` | +| **증분 백업** | 매 6시간 | 7일 | `mysqlbinlog` | +| **배포 전 스냅샷** | 배포 시 | 다음 배포까지 | Jenkins 파이프라인 내 자동 실행 | + +**자동 백업 스크립트** (`/etc/cron.d/sam-backup`): + +```bash +# 매일 03:00 전체 백업 +0 3 * * * root /home/webservice/scripts/db-backup.sh >> /var/log/sam/backup.log 2>&1 +``` + +```bash +#!/bin/bash +# /home/webservice/scripts/db-backup.sh +BACKUP_DIR="/home/webservice/backups/db" +DATE=$(date +%Y%m%d_%H%M%S) +KEEP_DAYS=30 + +mkdir -p ${BACKUP_DIR} + +mysqldump -u sam_prod_user --single-transaction --routines --triggers sam_prod \ + | gzip > ${BACKUP_DIR}/sam_prod_${DATE}.sql.gz + +# 30일 이상 된 백업 삭제 +find ${BACKUP_DIR} -name "*.sql.gz" -mtime +${KEEP_DAYS} -delete + +# Slack 알림 +curl -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"DB 백업 완료: sam_prod_${DATE}.sql.gz\"}" \ + ${SLACK_WEBHOOK_URL} +``` + +--- + +## 6. SSL/도메인 설정 + +### 6.1 Let's Encrypt 인증서 발급 + +```bash +# Certbot 설치 +apt install certbot python3-certbot-nginx + +# 인증서 발급 (4개 도메인) +certbot --nginx -d codebridge-x.com \ + -d api.codebridge-x.com \ + -d mng.codebridge-x.com \ + -d sales.codebridge-x.com + +# 자동 갱신 확인 +certbot renew --dry-run + +# 자동 갱신 cron (이미 certbot이 자동 설정) +# /etc/cron.d/certbot +``` + +### 6.2 Nginx 운영 설정 + +현재 Docker Nginx 설정(`docker/nginx/nginx.conf`)을 기반으로 운영 서버용으로 변환한다. + +핵심 변경 사항: + +| 항목 | 개발 (Docker) | 운영 (Bare-metal) | +|------|--------------|------------------| +| upstream | `proxy_pass http://react:3000` | `proxy_pass http://127.0.0.1:3000` | +| PHP-FPM | `fastcgi_pass api:9000` | `fastcgi_pass unix:/run/php/php8.4-fpm-api.sock` | +| SSL | 자체 서명 | Let's Encrypt | +| 도메인 | `*.sam.kr` | `*.codebridge-x.com` | + +**보안 헤더** (개발 서버 Sales 설정 기반): + +```nginx +# 공통 보안 헤더 +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-XSS-Protection "1; mode=block" always; + +# 보안: 악의적 경로 패턴 차단 +if ($request_uri ~* "(\.\.\/|\.\.\\|etc\/passwd|\.env|\.git|\.htaccess|\.sql|@fs\/)") { + return 403; +} + +# 보안: 의심스러운 User-Agent 차단 +if ($http_user_agent ~* "(sqlmap|nikto|nmap|masscan|metasploit|nessus)") { + return 403; +} +``` + +--- + +## 7. 모니터링 및 로깅 + +### 7.1 로그 집중화 + +``` +/var/log/sam/ +├── api-laravel.log # API Laravel 로그 (심볼릭 링크) +├── mng-laravel.log # MNG Laravel 로그 (심볼릭 링크) +├── api-queue-worker.log # API Queue Worker +├── api-queue-worker-error.log +├── mng-queue-worker.log # MNG Queue Worker +├── mng-queue-worker-error.log +├── api-scheduler.log # API Scheduler +├── react.log # React SSR 로그 +├── backup.log # DB 백업 로그 +└── healthcheck.log # 헬스체크 로그 +``` + +```bash +# 심볼릭 링크 설정 +ln -sf /home/webservice/api/storage/logs/laravel.log /var/log/sam/api-laravel.log +ln -sf /home/webservice/mng/storage/logs/laravel.log /var/log/sam/mng-laravel.log +``` + +### 7.2 헬스체크 스크립트 + +**5분 주기 실행** (`/etc/cron.d/sam-healthcheck`): + +```bash +*/5 * * * * root /home/webservice/scripts/healthcheck.sh >> /var/log/sam/healthcheck.log 2>&1 +``` + +```bash +#!/bin/bash +# /home/webservice/scripts/healthcheck.sh + +SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}" +SERVICES=( + "https://codebridge-x.com|React" + "https://api.codebridge-x.com/up|API" + "https://mng.codebridge-x.com|MNG" + "https://sales.codebridge-x.com|Sales" +) + +for service in "${SERVICES[@]}"; do + IFS='|' read -r url name <<< "$service" + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url") + + if [ "$HTTP_CODE" -ne 200 ] && [ "$HTTP_CODE" -ne 302 ]; then + echo "[$(date)] ALERT: ${name} DOWN (HTTP ${HTTP_CODE})" + curl -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"${name} 서비스 다운! HTTP ${HTTP_CODE} - ${url}\"}" \ + "$SLACK_WEBHOOK" + fi +done + +# MySQL 체크 +if ! mysqladmin ping -u sam_prod_user --silent 2>/dev/null; then + echo "[$(date)] ALERT: MySQL DOWN" + curl -X POST -H 'Content-type: application/json' \ + --data '{"text":"MySQL 서비스 다운!"}' \ + "$SLACK_WEBHOOK" +fi + +# 디스크 사용량 체크 (90% 이상 경고) +DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | tr -d '%') +if [ "$DISK_USAGE" -ge 90 ]; then + curl -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"디스크 사용량 경고: ${DISK_USAGE}%\"}" \ + "$SLACK_WEBHOOK" +fi +``` + +### 7.3 Slack 채널 구성 + +| 채널 | 용도 | 알림 내용 | +|------|------|----------| +| `#sam-deploy` | 배포 알림 | 배포 성공/실패 결과 | +| `#sam-alerts` | 장애 알림 | 서비스 다운, 디스크 부족, DB 연결 실패 | +| `#sam-errors` | 에러 로그 | Laravel 500 에러, Queue 실패 | + +--- + +## 8. 롤백 전략 + +### 8.1 API/MNG 롤백 + +```bash +# 1. 이전 커밋으로 코드 복원 +cd /home/webservice/api +git log --oneline -5 # 이전 커밋 확인 +git checkout <이전_커밋_해시> + +# 2. 의존성 복원 +composer install --no-dev --optimize-autoloader + +# 3. 캐시 초기화 +php artisan config:clear +php artisan cache:clear +php artisan route:cache + +# 4. Queue Worker 재시작 +sudo supervisorctl restart sam-api-worker:* +``` + +### 8.2 React 롤백 + +```bash +# .next.bak 이 남아있는 경우 (배포 직후) +cd /home/webservice/react +lsof -ti:3000 | xargs kill 2>/dev/null || true +rm -rf .next +mv .next.bak .next +cd .next/standalone +PORT=3000 HOSTNAME=0.0.0.0 nohup node server.js > /tmp/sam-react.log 2>&1 & +``` + +### 8.3 DB 롤백 + +| 우선순위 | 방법 | 설명 | +|---------|------|------| +| **1순위** | 코드 롤백 | 마이그레이션 문제가 아니면 코드만 롤백 | +| **2순위** | `migrate:rollback` | 마지막 마이그레이션 배치 되돌리기 | +| **3순위** | 스냅샷 복원 | 배포 전 자동 스냅샷에서 복원 | + +```bash +# 마이그레이션 롤백 +cd /home/webservice/api +php artisan migrate:rollback --step=1 + +# 스냅샷 복원 (최후의 수단) +mysql -u sam_prod_user -p sam_prod < /home/webservice/backups/db/pre-deploy-snapshot.sql.gz +``` + +--- + +## 9. 보안 강화 + +### 9.1 방화벽 (UFW) + +```bash +# 기본 정책 +ufw default deny incoming +ufw default allow outgoing + +# 허용 포트 +ufw allow 22/tcp # SSH +ufw allow 80/tcp # HTTP +ufw allow 443/tcp # HTTPS + +# MySQL은 localhost만 (외부 차단) +# 기본 deny에 의해 자동 차단됨 + +# 활성화 +ufw enable +``` + +### 9.2 SSH 보안 + +```bash +# /etc/ssh/sshd_config +PermitRootLogin no +PasswordAuthentication no +PubkeyAuthentication yes +MaxAuthTries 3 +AllowUsers deploy +``` + +### 9.3 fail2ban + +```bash +apt install fail2ban + +# /etc/fail2ban/jail.local +[sshd] +enabled = true +port = 22 +maxretry = 5 +bantime = 3600 + +[nginx-http-auth] +enabled = true + +[nginx-limit-req] +enabled = true +``` + +### 9.4 운영 .env 관리 규칙 + +``` +❌ .env 파일을 Git에 커밋 금지 +❌ .env 파일을 Slack/메신저로 공유 금지 +✅ 서버에서 직접 편집 (vi /home/webservice/api/.env) +✅ 변경 시 팀 채널에 "어떤 키를 변경했는지"만 공유 +``` + +### 9.5 보안 체크리스트 + +| # | 항목 | 확인 | +|---|------|------| +| 1 | `APP_DEBUG=false` | [ ] | +| 2 | `APP_ENV=production` | [ ] | +| 3 | `APP_KEY` 운영 전용 키 생성 | [ ] | +| 4 | DB 비밀번호 강력한 값으로 변경 | [ ] | +| 5 | MySQL 외부 접속 차단 (bind-address=127.0.0.1) | [ ] | +| 6 | UFW 방화벽 활성화 | [ ] | +| 7 | SSH 키 인증만 허용 (비밀번호 금지) | [ ] | +| 8 | fail2ban 설치 및 활성화 | [ ] | +| 9 | Nginx 보안 헤더 적용 (HSTS, X-Frame 등) | [ ] | +| 10 | Nginx 악의적 경로/UA 차단 규칙 적용 | [ ] | +| 11 | SSL 인증서 발급 및 자동 갱신 설정 | [ ] | +| 12 | `.env` 파일 권한 600 설정 | [ ] | +| 13 | `storage/`, `bootstrap/cache/` 권한 확인 | [ ] | +| 14 | phpMyAdmin 운영 서버에 설치하지 않음 | [ ] | +| 15 | Sanctum 토큰 만료 시간 설정 확인 | [ ] | +| 16 | `LOG_SLACK_WEBHOOK_URL` 설정 (에러 알림) | [ ] | + +--- + +## 10. 단계별 마이그레이션 체크리스트 + +### 10.1 Phase 1: 인프라 구축 (1주) + +| # | 작업 | 담당 | 확인 | +|---|------|------|------| +| 1 | 운영 서버 호스팅 계약 및 OS 설치 | 팀장 | [ ] | +| 2 | 기본 패키지 설치 (Nginx, PHP 8.4, MySQL 8.0, Node.js 20) | 팀장 | [ ] | +| 3 | PHP 확장 모듈 설치 (zip, intl, xml, soap, gd 등) | 팀장 | [ ] | +| 4 | LibreOffice, FFmpeg 설치 (MNG용) | 팀장 | [ ] | +| 5 | Supervisor 설치 및 설정 | 팀장 | [ ] | +| 6 | MySQL `sam_prod` 데이터베이스 생성 | 팀장 | [ ] | +| 7 | MySQL 운영 계정 생성 (외부 접속 차단) | 팀장 | [ ] | +| 8 | UFW 방화벽 설정 (22, 80, 443만 허용) | 팀장 | [ ] | +| 9 | SSH 키 인증 설정 (비밀번호 로그인 차단) | 팀장 | [ ] | +| 10 | fail2ban 설치 | 팀장 | [ ] | +| 11 | DNS 레코드 추가 (A 레코드 4개) | 팀장 | [ ] | +| 12 | Let's Encrypt SSL 인증서 발급 | 팀장 | [ ] | +| 13 | Nginx 운영 설정 배포 (4개 도메인) | 팀장 | [ ] | +| 14 | PHP-FPM 3개 풀 설정 (api, mng, sales) | 팀장 | [ ] | +| 15 | 로그 디렉토리 생성 (`/var/log/sam/`) | 팀장 | [ ] | +| 16 | 백업 스크립트 설치 및 cron 등록 | 팀장 | [ ] | + +### 10.2 Phase 2: CI/CD 파이프라인 구축 (1주) + +| # | 작업 | 담당 | 확인 | +|---|------|------|------| +| 1 | 개발 서버 Swap 4GB 추가 | 팀장 | [ ] | +| 2 | Jenkins 설치 및 초기 설정 | 팀장 | [ ] | +| 3 | Gitea → Jenkins Webhook 연동 (4개 저장소) | 팀장 | [ ] | +| 4 | Jenkins SSH Credential 등록 (운영 서버) | 팀장 | [ ] | +| 5 | sam-api Jenkinsfile 작성 및 테스트 | 팀장 | [ ] | +| 6 | sam-manage Jenkinsfile 작성 및 테스트 | 팀장 | [ ] | +| 7 | sam-react-prod Jenkinsfile 작성 및 테스트 | 팀장 | [ ] | +| 8 | sam-sales Jenkinsfile 작성 및 테스트 | 팀장 | [ ] | +| 9 | Slack Webhook 연동 (배포/장애 알림) | 팀장 | [ ] | +| 10 | 헬스체크 스크립트 설치 및 cron 등록 | 팀장 | [ ] | +| 11 | develop → 개발 서버 자동 배포 테스트 | 팀장 | [ ] | + +### 10.3 Phase 3: 스테이징 배포 (3일) + +| # | 작업 | 담당 | 확인 | +|---|------|------|------| +| 1 | 프로젝트 소스 코드 클론 (4개 저장소) | 팀장 | [ ] | +| 2 | 운영 `.env` 파일 생성 (API, MNG, Sales, React) | 팀장 | [ ] | +| 3 | `composer install` (API, MNG, Sales) | 팀장 | [ ] | +| 4 | 개발 DB → 운영 DB 데이터 마이그레이션 | 팀장 | [ ] | +| 5 | `php artisan migrate --force` (API에서만) | 팀장 | [ ] | +| 6 | 바로빌 운영 설정 전환 (DB + .env) | 팀장 | [ ] | +| 7 | Google 서비스 어카운트 파일 배치 | 팀장 | [ ] | +| 8 | React 빌드 및 배포 (standalone) | 팀장 | [ ] | +| 9 | Supervisor 프로세스 시작 | 팀장 | [ ] | +| 10 | 전체 서비스 기동 확인 | 전원 | [ ] | +| 11 | 기능 테스트 (로그인, 견적, 세금계산서 등) | 전원 | [ ] | +| 12 | 외부 서비스 연동 확인 (바로빌, FCM, Gemini) | 전원 | [ ] | +| 13 | 성능 기본 테스트 (응답 속도 < 500ms) | 팀장 | [ ] | + +### 10.4 Phase 4: 운영 전환 (1일) + +| # | 작업 | 담당 | 확인 | +|---|------|------|------| +| 1 | 전환 일시 공지 (사용자/팀) | 팀장 | [ ] | +| 2 | 개발 DB 최종 덤프 → 운영 DB 동기화 | 팀장 | [ ] | +| 3 | DNS 최종 전환 (운영 서버 IP로 변경) | 팀장 | [ ] | +| 4 | SSL 인증서 최종 확인 | 팀장 | [ ] | +| 5 | 운영 환경 최종 기동 | 팀장 | [ ] | +| 6 | Jenkins 운영 파이프라인 활성화 | 팀장 | [ ] | +| 7 | 모니터링/헬스체크 최종 확인 | 팀장 | [ ] | +| 8 | 사용자 접속 안내 (URL 변경) | 팀장 | [ ] | +| 9 | 2시간 집중 모니터링 | 전원 | [ ] | +| 10 | 전환 완료 공지 | 팀장 | [ ] | + +**운영 전환 후 검증 체크리스트:** + +``` +□ React 메인 페이지 로딩 확인 +□ 로그인/로그아웃 정상 +□ MNG 관리자 화면 접속 확인 +□ API 엔드포인트 응답 확인 (/up) +□ 파일 업로드/다운로드 정상 +□ 바로빌 세금계산서 발행 테스트 +□ FCM 푸시 알림 전송 확인 +□ Queue Worker 정상 동작 (failed_jobs 확인) +□ Scheduler 정상 동작 +□ Slack 알림 수신 확인 +``` + +--- + +## 11. 일정 요약 + +``` +Week 1 (02/22~02/28) Week 2 (03/01~03/07) Week 3 (03/08~03/14) + │ │ │ + Phase 1 Phase 2 Phase 3 → Phase 4 + 인프라 구축 CI/CD 구축 스테이징 운영 전환 + ├─ 서버 셋업 ├─ Jenkins 설치 ├─ 데이터 ├─ DNS 전환 + ├─ 패키지 설치 ├─ Webhook 연동 │ 마이그 ├─ 모니터링 + ├─ 방화벽/SSL ├─ 파이프라인 작성 │ 레이션 └─ 전환 공지 + └─ Nginx 설정 └─ Slack 연동 └─ 기능 + 테스트 +``` + +> **참고**: MS3 목표일(2026-02-28)은 Phase 1 완료 시점이다. 실제 운영 전환은 Week 3에 수행한다. 필요 시 Phase 1~2를 병렬로 진행하여 일정을 단축할 수 있다. + +--- + +## 12. 위험 요소 및 완화 방안 + +| # | 위험 요소 | 영향도 | 발생 확률 | 완화 방안 | +|---|----------|--------|----------|----------| +| 1 | **RAM 부족으로 React 빌드 실패** | 🔴 높음 | 중간 | Jenkins 서버 Swap 추가, 폴백으로 로컬 빌드 사용 | +| 2 | **DNS 전파 지연** | 🟡 중간 | 높음 | TTL 사전 단축 (300초), 전환 24시간 전 TTL 변경 | +| 3 | **바로빌 운영 전환 실패** | 🔴 높음 | 낮음 | DB + .env 동시 전환, 즉시 롤백 절차 준비 (`production-env-sync.md` 참조) | +| 4 | **운영 서버 호스팅 지연** | 🔴 높음 | 낮음 | 대안 호스팅 사전 조사, 최소 1주 여유 확보 | +| 5 | **Jenkins 메모리 부족** | 🟡 중간 | 중간 | Swap 4GB 추가, 동시 빌드 제한 (1개), 빌드 후 workspace 정리 | +| 6 | **마이그레이션 충돌** | 🟡 중간 | 낮음 | 배포 전 DB 스냅샷, `migrate:rollback` 준비 | +| 7 | **Google 서비스 어카운트 경로 불일치** | 🟡 중간 | 중간 | 운영 서버 경로 통일, .env 교차 검증 | + +--- + +## 관련 문서 + +- [런칭 로드맵](../guides/project-launch-roadmap.md) +- [.env 동기화 절차](../guides/production-env-sync.md) +- [Docker 환경 스펙](../specs/docker-setup.md) +- [보안 정책](../architecture/security-policy.md) +- [시스템 아키텍처](../architecture/system-overview.md) + +--- + +**최종 업데이트**: 2026-02-22 From 86ea901de09a60fefda2494fd079ac862f930850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Feb 2026 20:51:33 +0900 Subject: [PATCH 15/69] =?UTF-8?q?docs:=20[guides]=20Jenkins=20=EC=85=8B?= =?UTF-8?q?=EC=97=85=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Jenkins 이해하기 (용어, CI/CD 개념) - 사전 준비 (Swap, Java, 방화벽) - 설치 및 초기 설정 - 필수 플러그인 설치 - Credential 설정 (SSH, Gitea, Slack) - Gitea Webhook 연동 - Jenkinsfile 작성 가이드 - 트러블슈팅 & FAQ --- INDEX.md | 4 + guides/jenkins-setup-guide.md | 274 ++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 guides/jenkins-setup-guide.md diff --git a/INDEX.md b/INDEX.md index a3d9121..384016e 100644 --- a/INDEX.md +++ b/INDEX.md @@ -20,6 +20,7 @@ | **게시판** | `specs/board-system-spec.md` | 게시판 시스템 설계 | | **단가관리** | `rules/pricing-policy.md` | 원가/판매가 계산, 리비전 관리 | | **운영 배포** | `plans/production-deployment-plan.md` | 운영 환경 배포 계획 (CI/CD, 서버 아키텍처) | +| **서버 동작 원리** | `guides/server-how-it-works.md` | 요청 흐름, 배포 원리 이해 | | **과금정책 (고객용)** | `rules/customer-pricing.md` | 고객 안내용 서비스 요금표 | | **과금정책 (파트너)** | `rules/partner-commission.md` | 영업파트너 수당 체계 및 정산 | | **과금정책 (내부용)** | `rules/billing-policy.md` | 내부용 원가/마진/코드참조 (CONFIDENTIAL) | @@ -102,6 +103,8 @@ docs/ | [item-management-migration.md](guides/item-management-migration.md) | Item 시스템 전환 가이드 | 마이그레이션 작업 전 | | [project-launch-roadmap.md](guides/project-launch-roadmap.md) | 런칭 준비 현황 | 런칭 관련 작업 시 | | [production-env-sync.md](guides/production-env-sync.md) | 운영 전환 시 .env 동기화 절차 | 테스트→운영 전환 시 | +| [server-how-it-works.md](guides/server-how-it-works.md) | 서버 동작 원리 초보자 가이드 | 신규 합류 시 | +| [jenkins-setup-guide.md](guides/jenkins-setup-guide.md) | Jenkins CI/CD 셋업 가이드 | Jenkins 설치/설정 시 | ### quickstart/ - 빠른 시작 > 핵심 규칙 요약, 자주 쓰는 명령어 @@ -148,6 +151,7 @@ docs/ | [boards/mng-implementation.md](features/boards/mng-implementation.md) | MNG 게시판 구현 상세 | | [hr/hr-api-analysis.md](features/hr/hr-api-analysis.md) | HR API 분석 (근태/직원/부서) | | [quotes/README.md](features/quotes/README.md) | 견적 시스템 분석 (BOM 계산, 10단계 로직) | +| [academy/fire-shutter-image-prompts.md](features/academy/fire-shutter-image-prompts.md) | 방화셔터 백과사전 이미지 생성 프롬프트 (Gemini용) | ### projects/ - 프로젝트별 문서 diff --git a/guides/jenkins-setup-guide.md b/guides/jenkins-setup-guide.md new file mode 100644 index 0000000..d40ac35 --- /dev/null +++ b/guides/jenkins-setup-guide.md @@ -0,0 +1,274 @@ +# Jenkins CI/CD 셋업 가이드 + +> **작성일**: 2026-02-22 +> **상태**: 설계 확정 +> **대상**: SAM 프로젝트 개발팀장 + +--- + +## 1. Jenkins 이해하기 + +### 1.1 Jenkins란 + +Jenkins는 오픈소스 자동화 서버다. 코드를 Push하면 자동으로 빌드, 테스트, 배포를 수행한다. + +### 1.2 현재 수동 vs 자동화 비교 + +``` +현재: 개발자 → git push → SSH 접속 → git pull → composer install → 재시작 (수동, 5~10분) +목표: 개발자 → git push → Jenkins 자동 감지 → 빌드/테스트/배포 (자동, Slack 알림) +``` + +### 1.3 핵심 용어 + +| 용어 | 설명 | +|------|------| +| **Job** | 하나의 작업 단위 (예: `sam-api-deploy`) | +| **Pipeline** | Stage를 순서대로 실행하는 흐름 | +| **Stage / Step** | Pipeline의 단계 / 단계 내 개별 명령 | +| **Credential** | Jenkins에 저장하는 비밀 정보 (SSH 키, 토큰) | +| **Webhook** | Gitea가 Push 이벤트를 Jenkins에 알려주는 HTTP 호출 | + +--- + +## 2. 사전 준비 + +| 항목 | 값 | +|------|------| +| IP | `114.203.209.83` | +| CPU/RAM | 2코어 / 3.8GB | +| Gitea | `http://114.203.209.83:3000` | + +> **경고: RAM이 부족하므로 Swap 추가가 필수다.** + +### 2.1 Swap 4GB 추가 + +```bash +sudo fallocate -l 4G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab +free -h # 확인 +``` + +### 2.2 Java 17 + 방화벽 + +```bash +sudo apt update && sudo apt install -y openjdk-17-jdk +java -version # 17.x.x 확인 + +sudo ufw allow 8080/tcp # Jenkins 웹 UI 포트 +``` + +--- + +## 3. Jenkins 설치 + +### 3.1 패키지 설치 + +```bash +curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee \ + /usr/share/keyrings/jenkins-keyring.asc > /dev/null + +echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \ + https://pkg.jenkins.io/debian-stable binary/" | sudo tee \ + /etc/apt/sources.list.d/jenkins.list > /dev/null + +sudo apt update && sudo apt install -y jenkins +sudo systemctl start jenkins && sudo systemctl enable jenkins +``` + +### 3.2 초기 설정 + +```bash +sudo cat /var/lib/jenkins/secrets/initialAdminPassword # 초기 비밀번호 +``` + +1. `http://114.203.209.83:8080` 접속 → 비밀번호 입력 +2. **Install suggested plugins** 선택 → 설치 대기 (3~5분) +3. 관리자 계정 생성 (Username: `admin`) +4. Jenkins URL: `http://114.203.209.83:8080/` → **Save and Finish** + +--- + +## 4. 필수 플러그인 설치 + +**Jenkins 관리 → Plugins → Available plugins** 에서 설치한다. + +| 플러그인 | 역할 | 필수 | +|---------|------|------| +| **Git plugin** | 소스 코드 체크아웃 | 🔴 | +| **Pipeline** | Jenkinsfile 지원 | 🔴 | +| **SSH Agent** | SSH 키로 운영 서버 배포 | 🔴 | +| **Generic Webhook Trigger** | Gitea Push 이벤트 수신 | 🔴 | +| **Slack Notification** | 배포 결과 Slack 알림 | 🟡 | +| **NodeJS** | React 빌드용 Node.js | 🟡 | + +> **참고**: Git plugin, Pipeline은 suggested plugins에 포함되어 이미 설치되었을 수 있다. + +NodeJS 설정: **Jenkins 관리 → Tools → NodeJS installations → Add NodeJS** → Name: `NodeJS-20`, Version: `20.x` + +--- + +## 5. Credential 설정 + +### 5.1 SSH 키 생성 (Jenkins → 운영 서버) + +```bash +# Jenkins 서버에서 실행 +sudo su - jenkins +ssh-keygen -t ed25519 -C "jenkins@sam" -f ~/.ssh/id_ed25519 -N "" +cat ~/.ssh/id_ed25519.pub # 이 값을 운영 서버에 등록 +exit + +# 운영 서버에서 실행 (공개키 등록) +echo "ssh-ed25519 AAAA... jenkins@sam" >> /home/deploy/.ssh/authorized_keys +``` + +### 5.2 Jenkins Credential 등록 + +**Jenkins 관리 → Credentials → (global) → Add Credentials** + +| Credential | Kind | ID | 내용 | +|-----------|------|-----|------| +| SSH 키 | SSH Username with private key | `prod-server-ssh` | `~jenkins/.ssh/id_ed25519` 비밀키 | +| Gitea 토큰 | Username with password | `gitea-token` | Gitea 사용자명 + API 토큰 | +| Slack URL | Secret text | `slack-webhook` | Slack Incoming Webhook URL | + +```bash +# SSH 비밀키 확인 (Jenkins에 붙여넣기) +sudo cat /var/lib/jenkins/.ssh/id_ed25519 +``` + +``` +❌ Jenkinsfile에 비밀번호/토큰/키를 하드코딩 금지 +✅ 모든 비밀 정보는 Jenkins Credential에 등록 후 credentials('ID')로 참조 +``` + +--- + +## 6. Gitea Webhook 연동 + +### 6.1 Jenkins Pipeline Job 생성 + +1. **New Item** → 이름: `sam-api-deploy` → **Pipeline** 선택 +2. **Build Triggers**: Generic Webhook Trigger 체크, Token: `sam-api` +3. **Pipeline**: Pipeline script from SCM → Git + - URL: `http://114.203.209.83:3000/SamProject/sam-api.git` + - Credentials: `gitea-token` + - Branch: `*/main` + - Script Path: `Jenkinsfile` + +### 6.2 전체 Job 목록 + +| Job 이름 | 저장소 | 브랜치 | Token | +|---------|--------|--------|-------| +| `sam-api-deploy` | `sam-api.git` | `*/main` | `sam-api` | +| `sam-mng-deploy` | `sam-manage.git` | `*/master` | `sam-mng` | +| `sam-react-deploy` | `sam-react-prod.git` | `*/master` | `sam-react` | +| `sam-sales-deploy` | `sam-sales.git` | `*/main` | `sam-sales` | + +### 6.3 Gitea Webhook 설정 + +각 저장소: **Settings → Webhooks → Add Webhook → Gitea** + +| 항목 | 값 | +|------|------| +| Target URL | `http://114.203.209.83:8080/generic-webhook-trigger/invoke?token=sam-api` | +| Content Type | `application/json` | +| Trigger On | **Push Events** | +| Branch filter | `main` | + +**Test Delivery** → 응답 200이면 성공 + +--- + +## 7. Jenkinsfile 작성 가이드 + +### 7.1 기본 구조 + +```groovy +pipeline { + agent any + environment { KEY = 'value' } + stages { + stage('단계명') { + steps { sh 'command' } + } + } + post { + success { slackSend channel: '#sam-deploy', message: "성공" } + failure { slackSend channel: '#sam-alerts', message: "실패" } + } +} +``` + +### 7.2 SAM 저장소별 Jenkinsfile + +> 상세 코드는 `plans/production-deployment-plan.md` 4.4절 참조 + +| 저장소 | Stage 흐름 | 특이사항 | +|--------|-----------|---------| +| **sam-api** | Checkout → Lint → Test → Deploy | `migrate --force` 포함 | +| **sam-manage** | Checkout → Lint → Build Assets → Deploy | 마이그레이션 없음 | +| **sam-react-prod** | Checkout → Install → Lint → Build → Package → Deploy | `tar.gz`로 전송 | +| **sam-sales** | Deploy | 간소화 (git pull + composer) | + +### 7.3 배치 방법 + +각 저장소 **루트**에 `Jenkinsfile` 생성 → `git add Jenkinsfile && git commit -m "chore: Jenkinsfile 추가"` → push + +--- + +## 8. 트러블슈팅 + +### 8.1 빌드 실패 + +Jenkins 대시보드 → Job → 빌드 번호 → **Console Output** 에서 에러 로그 확인 + +### 8.2 SSH 권한 오류 (`Permission denied`) + +```bash +sudo su - jenkins && ssh deploy@운영서버IP # 수동 테스트 +# 운영 서버에서 authorized_keys 등록 확인 +chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys +``` + +### 8.3 메모리 부족 + +```bash +# Jenkins 힙 메모리 제한: /etc/default/jenkins에 JAVA_ARGS="-Xmx512m" 추가 +sudo systemctl restart jenkins +# Job 설정 → Discard old builds → 최대 빌드 수: 10 +``` + +### 8.4 Webhook 미동작 + +```bash +# 수동 트리거 테스트 +curl -X POST "http://114.203.209.83:8080/generic-webhook-trigger/invoke?token=sam-api" +# Gitea: Webhooks → Recent Deliveries → 응답 코드 확인 (200=정상, 403=Token 불일치) +``` + +### 8.5 React 빌드 OOM + +```bash +# Jenkinsfile에서 메모리 증가 +sh 'export NODE_OPTIONS="--max-old-space-size=2048" && npm run build' +# 실패 시 로컬(WSL)에서 react/deploy.sh 사용 +``` + +> **경고: 개발 서버에서 React 빌드 실패 시 로컬에서 `deploy.sh`를 사용한다.** + +--- + +## 관련 문서 + +- [운영 환경 배포 계획서](../plans/production-deployment-plan.md) - Jenkinsfile 상세, 브랜치 전략 +- [.env 동기화 절차](production-env-sync.md) - 환경 변수 분리 +- [Docker 환경 스펙](../specs/docker-setup.md) - 현재 개발 환경 + +--- + +**최종 업데이트**: 2026-02-22 From d406e54fcf1e03b45a19349f8f3a596d410f7635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Feb 2026 21:39:15 +0900 Subject: [PATCH 16/69] =?UTF-8?q?docs:=20[guides]=20PHP-FPM=20=EC=B4=88?= =?UTF-8?q?=EB=B3=B4=EC=9E=90=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PHP-FPM 개념, 역사(CGI→mod_php→PHP-FPM), 구조 설명 - Nginx-PHP-FPM 관계 및 FastCGI 프로토콜 - SAM 컨테이너별 설정(www.conf) 상세 - 자주 묻는 질문 (502 에러, 워커 부족, 재시작 등) --- INDEX.md | 1 + guides/php-fpm-guide.md | 261 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 guides/php-fpm-guide.md diff --git a/INDEX.md b/INDEX.md index 384016e..a0e650c 100644 --- a/INDEX.md +++ b/INDEX.md @@ -104,6 +104,7 @@ docs/ | [project-launch-roadmap.md](guides/project-launch-roadmap.md) | 런칭 준비 현황 | 런칭 관련 작업 시 | | [production-env-sync.md](guides/production-env-sync.md) | 운영 전환 시 .env 동기화 절차 | 테스트→운영 전환 시 | | [server-how-it-works.md](guides/server-how-it-works.md) | 서버 동작 원리 초보자 가이드 | 신규 합류 시 | +| [php-fpm-guide.md](guides/php-fpm-guide.md) | PHP-FPM 초보자 가이드 | PHP-FPM 개념 이해 시 | | [jenkins-setup-guide.md](guides/jenkins-setup-guide.md) | Jenkins CI/CD 셋업 가이드 | Jenkins 설치/설정 시 | ### quickstart/ - 빠른 시작 diff --git a/guides/php-fpm-guide.md b/guides/php-fpm-guide.md new file mode 100644 index 0000000..ef19a06 --- /dev/null +++ b/guides/php-fpm-guide.md @@ -0,0 +1,261 @@ +# PHP-FPM 초보자 가이드 + +> **작성일**: 2026-02-22 +> **대상**: SAM 프로젝트에 새로 합류한 개발자 + +--- + +## 1. 개요 + +### 1.1 PHP-FPM이란 + +**PHP-FPM**(FastCGI Process Manager)은 PHP 코드를 실행하는 **프로세스 관리자**다. +Nginx는 PHP를 직접 실행하지 못하므로, PHP-FPM이 대신 실행하고 결과를 돌려준다. + +### 1.2 이 문서의 목적 + +[서버 동작 원리 가이드](server-how-it-works.md)에서 PHP-FPM을 간략히 소개했다. +이 문서는 **왜 필요한지**, **어떻게 동작하는지**, **SAM에서 어떤 설정으로 쓰이는지**를 다룬다. + +--- + +## 2. PHP가 웹에서 동작하는 방식의 역사 + +### 2.1 CGI → mod_php → PHP-FPM + +``` +[1세대 CGI] 요청마다 PHP 프로세스 생성/종료 → 느림 +[2세대 mod_php] Apache에 PHP 내장 → 빠르지만 Nginx 불가, 메모리 낭비 +[3세대 PHP-FPM] Nginx와 분리, 워커 풀 재사용 → 빠르고 유연 ← SAM이 사용 +``` + +**비유로 이해하기**: + +| 세대 | 비유 | +|------|------| +| CGI | 손님마다 직원을 채용하고 해고하는 식당 | +| mod_php | 직원이 주방장(Apache)과 한 몸 — 따로 관리 불가 | +| **PHP-FPM** | 안내 데스크(Nginx)와 업무 창구(PHP-FPM) 분리된 은행 | + +### 2.2 요약 + +| 세대 | 방식 | 장점 | 단점 | +|------|------|------|------| +| CGI | 매번 프로세스 생성 | 단순 | 느림, 리소스 낭비 | +| mod_php | Apache에 내장 | CGI보다 빠름 | Nginx 불가, 메모리 낭비 | +| **PHP-FPM** | 독립 프로세스 관리 | 빠름, 유연 | 설정 필요 | + +--- + +## 3. PHP-FPM의 구조 + +### 3.1 Master / Worker 모델 + +PHP-FPM은 **Master 프로세스** 1개와 **Worker 프로세스** 여러 개로 구성된다. + +``` +PHP-FPM +┌──────────────────────────────────┐ +│ Master 프로세스 (관리자) │ +│ ├── Worker 1 (대기 중) │ +│ ├── Worker 2 (요청 처리 중) │ +│ ├── Worker 3 (대기 중) │ +│ ├── Worker 4 (요청 처리 중) │ +│ └── Worker 5 (대기 중) │ +└──────────────────────────────────┘ +``` + +**은행 창구 비유**: + +| PHP-FPM | 은행 | +|---------|------| +| Master 프로세스 | 지점장 (직원 수 조절, 감독) | +| Worker 프로세스 | 창구 직원 (실제 업무 처리) | +| `pm.max_children` | 최대 창구 수 | +| 요청 큐 | 대기 번호표 줄 | + +- **Master**: 워커 생성/종료, 비정상 워커 재시작, 포트 9000 대기 +- **Worker**: PHP 코드 실행, 1 Worker = 1 요청, 완료 후 다음 요청 대기 + +### 3.2 포트 9000과 프로세스 관리 모드 + +PHP-FPM은 **TCP 포트 9000**에서 요청을 기다린다. + +``` +Nginx ──── TCP 9000 ────→ PHP-FPM Master ──→ 빈 Worker에 배정 +``` + +Master가 Worker 수를 관리하는 3가지 모드: + +| 모드 | 설명 | SAM | +|------|------|-----| +| `static` | 항상 고정 수 유지 | - | +| **`dynamic`** | 트래픽에 따라 조절 | **사용 중** | +| `ondemand` | 요청 올 때만 생성 | - | + +--- + +## 4. Nginx와 PHP-FPM의 관계 + +### 4.1 왜 Nginx는 PHP를 직접 못 실행하는가 + +Nginx는 정적 파일 서빙과 리버스 프록시 전용이다. PHP 해석 엔진이 없으므로 `.php` 파일을 실행할 수 없다. + +### 4.2 FastCGI 프로토콜 + +Nginx와 PHP-FPM은 **FastCGI** 프로토콜로 통신한다. + +``` +브라우저 ──HTTP──→ Nginx ──FastCGI──→ PHP-FPM ──→ PHP 실행 + │ +브라우저 ←──HTTP── Nginx ←──FastCGI── PHP-FPM ←────┘ +``` + +Nginx가 FastCGI로 전달하는 주요 정보: +- `SCRIPT_FILENAME` — 실행할 PHP 파일 경로 (`/var/www/mng/public/index.php`) +- `REQUEST_METHOD` — GET, POST 등 +- `QUERY_STRING` — URL 파라미터 + +### 4.3 역할 분담 + +``` +┌─────────────────────┐ ┌─────────────────────┐ +│ Nginx │ │ PHP-FPM │ +│ │ TCP │ │ +│ • SSL 종료 │ 9000 │ • PHP 코드 실행 │ +│ • 도메인 라우팅 │─────→│ • Laravel 구동 │ +│ • 정적 파일 서빙 │ │ • DB 조회 │ +│ • 보안 필터링 │ │ • HTML/JSON 생성 │ +└─────────────────────┘ └─────────────────────┘ +``` + +--- + +## 5. SAM에서의 PHP-FPM + +### 5.1 컨테이너 구조 + +``` +Docker +├── sam-nginx-1 ── 외부 리버스 프록시 +├── sam-api-1 +│ └── Supervisor +│ ├── php-fpm ← 포트 9000 +│ ├── nginx ← 컨테이너 내부 웹서버 +│ ├── queue-worker ← 백그라운드 작업 +│ └── scheduler ← 60초 예약 작업 +├── sam-mng-1 +│ └── Supervisor +│ ├── php-fpm ← 포트 9000 +│ ├── nginx ← 컨테이너 내부 웹서버 +│ └── queue-worker x2 +└── sam-mysql-1 +``` + +Supervisor가 PHP-FPM과 Nginx를 함께 관리한다. 컨테이너 시작 시 Supervisor가 모든 프로세스를 기동한다. + +### 5.2 PHP-FPM 설정 (`www.conf`) + +SAM 설정 파일 위치: `docker/mng/www.conf` + +```ini +[www] +user = www-data +group = www-data +listen = 0.0.0.0:9000 + +pm = dynamic +pm.max_children = 20 +pm.start_servers = 5 +pm.min_spare_servers = 5 +pm.max_spare_servers = 10 +pm.max_requests = 500 +``` + +**각 설정의 의미**: + +| 설정 | 값 | 의미 | +|------|-----|------| +| `pm` | `dynamic` | 트래픽에 따라 워커 수 조절 | +| `pm.max_children` | `20` | 최대 동시 처리 수 (= 최대 창구 20개) | +| `pm.start_servers` | `5` | 시작 시 워커 수 | +| `pm.min_spare_servers` | `5` | 최소 대기 워커 수 | +| `pm.max_spare_servers` | `10` | 최대 대기 워커 수 | +| `pm.max_requests` | `500` | 500건 처리 후 워커 재시작 (메모리 누수 방지) | + +### 5.3 워커 수의 동적 변화 + +``` +워커 수 +20 ┤ ■■■■ (피크) +10 ┤ ■■■■■ ■■■■■ + 5 ┤ ■■■■■■■■■■■■■ ■■■■■■■■ + 0 ┤────────────────────────────────────────────── + 새벽 오전 점심 오후 +``` + +### 5.4 Docker 이미지 + +SAM은 `php:8.4-fpm` 공식 이미지를 기반으로 한다. 이미지 이름의 `fpm`이 PHP-FPM 내장을 의미한다. + +```dockerfile +FROM php:8.4-fpm +# Supervisor로 php-fpm + nginx 동시 기동 +CMD ["/usr/bin/supervisord"] +``` + +--- + +## 6. 자주 묻는 질문 + +### 6.1 "502 Bad Gateway 에러가 뭔가요?" + +Nginx가 PHP-FPM에 연결 실패 시 발생한다. PHP-FPM이 죽었거나, 컨테이너가 비정상 시작된 경우다. + +```bash +# 컨테이너 상태 확인 +docker ps + +# 로그 확인 +docker logs sam-mng-1 +``` + +### 6.2 "워커가 부족하면?" + +20개 워커가 모두 처리 중이면 새 요청은 대기열에 들어간다. 너무 오래 대기하면 504 Gateway Timeout이 발생한다. `pm.max_children`을 올리거나 느린 코드를 최적화한다. + +### 6.3 "PHP-FPM을 재시작하려면?" + +```bash +# 컨테이너 전체 재시작 +docker restart sam-mng-1 + +# PHP-FPM만 재시작 +docker exec sam-mng-1 supervisorctl restart php-fpm +``` + +### 6.4 "프로세스 상태를 확인하려면?" + +```bash +docker exec sam-mng-1 supervisorctl status +# nginx RUNNING pid 8, uptime 2:30:00 +# php-fpm RUNNING pid 9, uptime 2:30:00 +# queue-worker_00 RUNNING pid 10, uptime 2:30:00 +``` + +### 6.5 "php artisan serve와 뭐가 다른가요?" + +`php artisan serve`는 개발용 간이 서버로 동시 요청 1개만 처리한다. PHP-FPM은 워커 수만큼 동시 처리가 가능한 프로덕션용이다. SAM은 PHP-FPM을 사용한다. + +--- + +## 관련 문서 + +| 문서 | 설명 | +|------|------| +| [server-how-it-works.md](server-how-it-works.md) | 서버 동작 원리 전체 가이드 | +| [docker-setup.md](../specs/docker-setup.md) | Docker 환경 설정값 상세 | + +--- + +**최종 업데이트**: 2026-02-22 From 07fb6bb5354f55a0d871f9f2985419e712ffcc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Feb 2026 21:45:51 +0900 Subject: [PATCH 17/69] =?UTF-8?q?docs:=20[guides]=20PHP-FPM=20=EC=B4=88?= =?UTF-8?q?=EB=B3=B4=EC=9E=90=20=EA=B0=80=EC=9D=B4=EB=93=9C=20PPTX=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- guides/php-fpm-guide.pptx | Bin 0 -> 325454 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 guides/php-fpm-guide.pptx diff --git a/guides/php-fpm-guide.pptx b/guides/php-fpm-guide.pptx new file mode 100644 index 0000000000000000000000000000000000000000..1534b936495af237635dd1bf3a955b1c573c1a13 GIT binary patch literal 325454 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 KR0K`u+?t~a+ELOw`d_=! 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 Date: Sun, 22 Feb 2026 22:14:02 +0900 Subject: [PATCH 18/69] =?UTF-8?q?docs:=20[guides]=20Nginx=20&=20FastCGI=20?= =?UTF-8?q?=EC=B4=88=EB=B3=B4=EC=9E=90=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nginx 개념 (웹서버, 리버스 프록시, SSL 종료) - FastCGI 프로토콜 (CGI→FastCGI 발전, 바이너리 프로토콜) - nginx.conf 설정 해부 (try_files, fastcgi_pass, SCRIPT_FILENAME) - SAM 2계층 Nginx 구조 및 도메인별 라우팅 - FastCGI vs HTTP 프록시 비교 - PPTX 프레젠테이션 포함 --- INDEX.md | 1 + guides/nginx-fastcgi-guide.md | 260 ++++++++++++++++++++++++++++++++ guides/nginx-fastcgi-guide.pptx | Bin 0 -> 327602 bytes 3 files changed, 261 insertions(+) create mode 100644 guides/nginx-fastcgi-guide.md create mode 100644 guides/nginx-fastcgi-guide.pptx diff --git a/INDEX.md b/INDEX.md index a0e650c..a572f3d 100644 --- a/INDEX.md +++ b/INDEX.md @@ -105,6 +105,7 @@ docs/ | [production-env-sync.md](guides/production-env-sync.md) | 운영 전환 시 .env 동기화 절차 | 테스트→운영 전환 시 | | [server-how-it-works.md](guides/server-how-it-works.md) | 서버 동작 원리 초보자 가이드 | 신규 합류 시 | | [php-fpm-guide.md](guides/php-fpm-guide.md) | PHP-FPM 초보자 가이드 | PHP-FPM 개념 이해 시 | +| [nginx-fastcgi-guide.md](guides/nginx-fastcgi-guide.md) | Nginx & FastCGI 초보자 가이드 | Nginx/FastCGI 개념 이해 시 | | [jenkins-setup-guide.md](guides/jenkins-setup-guide.md) | Jenkins CI/CD 셋업 가이드 | Jenkins 설치/설정 시 | ### quickstart/ - 빠른 시작 diff --git a/guides/nginx-fastcgi-guide.md b/guides/nginx-fastcgi-guide.md new file mode 100644 index 0000000..ad93cca --- /dev/null +++ b/guides/nginx-fastcgi-guide.md @@ -0,0 +1,260 @@ +# Nginx & FastCGI 초보자 가이드 + +> **작성일**: 2026-02-22 +> **대상**: SAM 프로젝트에 새로 합류한 개발자 + +--- + +## 1. 개요 + +### 1.1 이 문서의 목적 + +"Nginx가 뭐지?", "FastCGI가 뭐지?" — 이 두 질문에 답하는 문서다. +[서버 동작 원리 가이드](server-how-it-works.md)와 [PHP-FPM 가이드](php-fpm-guide.md)에서 간략히 언급한 내용을 **깊이 파고든다**. + +### 1.2 핵심 한 줄 정리 + +- **Nginx** = 요청을 받아서 적절한 곳에 전달하는 **교통 경찰** +- **FastCGI** = Nginx와 PHP-FPM이 대화하는 **통신 규약(프로토콜)** + +--- + +## 2. Nginx란? + +### 2.1 웹서버가 하는 일 + +웹서버는 브라우저의 요청을 받아서 응답을 돌려주는 프로그램이다. + +``` +브라우저: "index.html 주세요" +웹서버: "여기 있습니다" (파일 내용 전송) +``` + +### 2.2 Nginx의 정체 + +Nginx(엔진엑스)는 **고성능 웹서버이자 리버스 프록시**다. 2004년 러시아 개발자 이고르 시소예프가 만들었다. + +**비유**: 대형 호텔의 프런트 데스크 + +``` +┌─────────────────────────────────────────────┐ +│ Nginx (프런트 데스크) │ +│ │ +│ 손님(브라우저)이 오면: │ +│ │ +│ "이미지 주세요" → 직접 서빙 (정적 파일) │ +│ "PHP 실행해줘" → PHP-FPM에 전달 (FastCGI) │ +│ "React 페이지" → Node.js에 전달 (프록시) │ +│ "API 호출" → API 서버에 전달 (프록시) │ +└─────────────────────────────────────────────┘ +``` + +### 2.3 Nginx vs Apache + +| 항목 | Nginx | Apache | +|------|-------|--------| +| 아키텍처 | **이벤트 기반** (비동기) | 프로세스/스레드 기반 | +| 동시 접속 | 수만 개 가능 | 수천 개 수준 | +| 메모리 | 적게 사용 | 많이 사용 | +| 정적 파일 | **매우 빠름** | 보통 | +| PHP 실행 | 직접 불가 (FastCGI 필요) | mod_php로 직접 가능 | +| SAM에서 | **사용 중** | 사용 안 함 | + +**왜 SAM은 Nginx를 쓰는가**: 서버 스펙(2코어/3.8GB)이 제한적이므로, 적은 메모리로 5개 서비스를 동시 라우팅할 수 있는 Nginx가 적합하다. + +### 2.4 Nginx의 3가지 역할 + +``` +역할 1 — 정적 파일 서빙: logo.png → Nginx가 직접 전송 (PHP 개입 없음) +역할 2 — 리버스 프록시: PHP 요청 → PHP-FPM, React 요청 → Node.js +역할 3 — SSL 종료: HTTPS(암호화) → Nginx에서 해독 → 내부는 HTTP(평문) +``` + +--- + +## 3. FastCGI란? + +### 3.1 먼저 CGI를 이해하자 + +**CGI**(Common Gateway Interface)는 웹서버가 외부 프로그램을 실행하는 규약이다. 1993년에 만들어졌다. + +``` +브라우저 → 웹서버 → "PHP 프로그램을 실행해서 결과를 줘" + ↓ + [새 프로세스 생성] → PHP 실행 → 결과 → [프로세스 종료] +``` + +**문제**: 요청마다 프로세스를 새로 생성하고 종료한다. 100명이 동시에 접속하면 100개 프로세스가 생겼다 사라진다. **느리고 비효율적**이다. + +### 3.2 FastCGI가 해결한 것 + +**FastCGI**는 1996년에 CGI의 문제를 해결하기 위해 만들어졌다. 핵심 차이: + +``` +[CGI] 요청 → 프로세스 생성 → 실행 → 종료 → 요청 → 생성 → 실행 → 종료 +[FastCGI] 프로세스가 미리 떠 있음 → 요청 → 실행 → 대기 → 요청 → 실행 → 대기 +``` + +**비유**: CGI는 택시(매번 부르고 보냄), FastCGI는 전용 기사(항상 대기 중). + +### 3.3 FastCGI는 프로토콜이다 + +FastCGI는 프로그램이 아니라 **통신 규약(프로토콜)**이다. HTTP처럼 "이렇게 데이터를 주고받자"는 약속이다. + +``` +┌──────────┐ ┌──────────┐ +│ Nginx │ ── FastCGI 규약 ── │ PHP-FPM │ +│ (클라이언트) │ │ (서버) │ +└──────────┘ └──────────┘ +``` + +- HTTP: 브라우저와 웹서버 사이의 규약 +- **FastCGI**: 웹서버와 애플리케이션 서버 사이의 규약 +- 둘은 다른 프로토콜이다 (FastCGI는 바이너리, HTTP는 텍스트) + +### 3.4 FastCGI가 전달하는 정보 + +Nginx가 PHP-FPM에 보내는 주요 파라미터: + +| 파라미터 | 의미 | 예시 | +|---------|------|------| +| `SCRIPT_FILENAME` | 실행할 PHP 파일 | `/var/www/mng/public/index.php` | +| `REQUEST_METHOD` | HTTP 메서드 | `GET`, `POST` | +| `QUERY_STRING` | URL 파라미터 | `page=1&sort=name` | +| `HTTP_HOST` | 도메인 | `mng.sam.kr` | + +PHP에서 `$_SERVER['REQUEST_METHOD']`, `$_GET` 등으로 접근하는 값이 바로 이것이다. + +--- + +## 4. Nginx + FastCGI 동작 원리 + +### 4.1 전체 흐름 + +`https://mng.sam.kr/orders?page=2` 접속 시: + +``` +브라우저 →① HTTPS → Nginx(도메인 라우팅) →② try_files → index.php +→③ FastCGI → PHP-FPM(:9000) →④ Laravel → DB → HTML →⑤ 응답 역순 +``` + +### 4.2 Nginx 설정 해부 + +SAM의 MNG 설정에서 FastCGI 관련 부분: + +```nginx +# docker/nginx/nginx.conf (외부 Nginx — MNG 섹션) + +location / { + try_files $uri $uri/ /index.php?$query_string; + # ① ② ③ +} + +location ~ \.php$ { + include fastcgi_params; # ④ + fastcgi_pass mng:9000; # ⑤ + fastcgi_param SCRIPT_FILENAME /var/www/mng/public$fastcgi_script_name; # ⑥ + fastcgi_param PATH_INFO $fastcgi_path_info; # ⑦ + fastcgi_param HTTPS on; # ⑧ + fastcgi_read_timeout 300s; # ⑨ +} +``` + +**핵심 줄 해설**: + +- ①②③ `try_files`: 파일 있으면 직접 서빙, 없으면 `index.php`로 (Laravel 진입점) +- ⑤ `fastcgi_pass mng:9000`: **핵심! FastCGI 요청을 mng 컨테이너 9000번 포트로 전달** +- ⑥ `SCRIPT_FILENAME`: PHP-FPM이 실행할 파일의 절대 경로 +- ⑧ `HTTPS on`: PHP에서 HTTPS 요청으로 인식하도록 설정 +- ⑨ `fastcgi_read_timeout`: PHP 응답 대기 최대 300초 + +### 4.3 왜 index.php 하나로 모든 요청을 처리하는가 + +Laravel은 **프론트 컨트롤러 패턴**을 사용한다. `/orders`, `/users/123` 등 모든 URL이 `public/index.php`를 통과하고, Laravel 라우터가 적절한 컨트롤러로 분배한다. `try_files` 설정이 이를 가능하게 한다. + +--- + +## 5. SAM의 Nginx 구조 + +### 5.1 2계층 Nginx + +SAM은 Nginx가 **2단계**로 작동한다: + +``` +브라우저 → [1계층: 외부 Nginx (sam-nginx-1)] → SSL 종료 + 도메인 라우팅 + │ │ + ▼ ▼ + [2계층: sam-mng-1] [2계층: sam-api-1] + Nginx(:80) Nginx(:80) + ↓ FastCGI ↓ FastCGI + PHP-FPM(:9000) PHP-FPM(:9000) +``` + +### 5.2 도메인별 라우팅 정리 + +| 도메인 | 1계층(외부 Nginx) 동작 | 프로토콜 | +|--------|----------------------|---------| +| `mng.sam.kr` | `fastcgi_pass mng:9000` | **FastCGI** | +| `api.sam.kr` | `fastcgi_pass api:9000` | **FastCGI** | +| `dev.sam.kr` | `proxy_pass react:3000` | HTTP 프록시 | +| `sales.sam.kr` | `proxy_pass sales:80` | HTTP 프록시 | +| `5130.sam.kr` | `proxy_pass php73:80` | HTTP 프록시 | + +> **핵심**: PHP 서비스는 **FastCGI**로, Node.js/레거시 서비스는 **HTTP 프록시**로 연결한다. + +--- + +## 6. 자주 묻는 질문 + +### 6.1 "FastCGI와 HTTP 프록시의 차이는?" + +| 항목 | FastCGI | HTTP 프록시 | +|------|---------|-----------| +| 프로토콜 | 바이너리 (FastCGI) | 텍스트 (HTTP) | +| 용도 | PHP-FPM 등 CGI 호환 앱 | Node.js, 일반 웹서버 | +| Nginx 설정 | `fastcgi_pass` | `proxy_pass` | +| SAM에서 | MNG, API | React, Sales, 5130 | + +### 6.2 "try_files가 뭔가요?" + +Nginx가 파일을 찾는 순서를 지정한다: + +```nginx +try_files $uri $uri/ /index.php?$query_string; +``` + +1. `$uri` — `/orders.html` 파일이 있는가? → 없다 +2. `$uri/` — `/orders/` 디렉토리가 있는가? → 없다 +3. `/index.php?$query_string` — index.php로 넘긴다 (Laravel이 처리) + +### 6.3 "SCRIPT_FILENAME은 왜 설정하나?" + +PHP-FPM은 어떤 PHP 파일을 실행할지 알아야 한다. Nginx가 FastCGI 파라미터로 알려준다. + +```nginx +fastcgi_param SCRIPT_FILENAME /var/www/mng/public$fastcgi_script_name; +# /orders 요청 시 → /var/www/mng/public/index.php +``` + +이 설정이 잘못되면 "File not found" 에러가 발생한다. + +### 6.4 "502/504 에러가 나면?" + +| 에러 | 원인 | 대응 | +|------|------|------| +| **502 Bad Gateway** | PHP-FPM이 죽었거나 연결 불가 | `docker ps`, `docker logs` 확인 | +| **504 Gateway Timeout** | PHP 처리가 너무 오래 걸림 | `fastcgi_read_timeout` 증가 또는 코드 최적화 | + +--- + +## 관련 문서 + +| 문서 | 설명 | +|------|------| +| [server-how-it-works.md](server-how-it-works.md) | 서버 동작 원리 전체 가이드 | +| [php-fpm-guide.md](php-fpm-guide.md) | PHP-FPM 초보자 가이드 | +| [docker-setup.md](../specs/docker-setup.md) | Docker 환경 설정값 상세 | + +--- + +**최종 업데이트**: 2026-02-22 diff --git a/guides/nginx-fastcgi-guide.pptx b/guides/nginx-fastcgi-guide.pptx new file mode 100644 index 0000000000000000000000000000000000000000..625e7c4fd52413d34557318b1ac165a07734c313 GIT binary patch literal 327602 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 BpRWJ_ literal 0 HcmV?d00001 From d2b4264b1315960d333318daeeb9123241d87f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Feb 2026 22:20:58 +0900 Subject: [PATCH 19/69] =?UTF-8?q?docs:=20[guides]=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9D=B8=ED=94=84=EB=9D=BC=20=ED=95=99=EC=8A=B5=20=EC=8B=9C?= =?UTF-8?q?=EB=A6=AC=EC=A6=88=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3개 가이드에 시리즈 배너 추가 (Part 1/2/3) - 관련 문서 섹션에 시리즈 네비게이션 추가 - INDEX.md에 시리즈 순서 반영 --- INDEX.md | 6 +- guides/nginx-fastcgi-guide.md | 14 +- guides/php-fpm-guide.md | 13 +- guides/server-how-it-works.md | 259 ++++++++++++++++++++++++++++++++++ 4 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 guides/server-how-it-works.md diff --git a/INDEX.md b/INDEX.md index a572f3d..e52b993 100644 --- a/INDEX.md +++ b/INDEX.md @@ -103,9 +103,9 @@ docs/ | [item-management-migration.md](guides/item-management-migration.md) | Item 시스템 전환 가이드 | 마이그레이션 작업 전 | | [project-launch-roadmap.md](guides/project-launch-roadmap.md) | 런칭 준비 현황 | 런칭 관련 작업 시 | | [production-env-sync.md](guides/production-env-sync.md) | 운영 전환 시 .env 동기화 절차 | 테스트→운영 전환 시 | -| [server-how-it-works.md](guides/server-how-it-works.md) | 서버 동작 원리 초보자 가이드 | 신규 합류 시 | -| [php-fpm-guide.md](guides/php-fpm-guide.md) | PHP-FPM 초보자 가이드 | PHP-FPM 개념 이해 시 | -| [nginx-fastcgi-guide.md](guides/nginx-fastcgi-guide.md) | Nginx & FastCGI 초보자 가이드 | Nginx/FastCGI 개념 이해 시 | +| [server-how-it-works.md](guides/server-how-it-works.md) | 서버 인프라 시리즈 ① 서버 동작 원리 | 신규 합류 시 | +| [nginx-fastcgi-guide.md](guides/nginx-fastcgi-guide.md) | 서버 인프라 시리즈 ② Nginx & FastCGI | Nginx/FastCGI 개념 이해 시 | +| [php-fpm-guide.md](guides/php-fpm-guide.md) | 서버 인프라 시리즈 ③ PHP-FPM | PHP-FPM 개념 이해 시 | | [jenkins-setup-guide.md](guides/jenkins-setup-guide.md) | Jenkins CI/CD 셋업 가이드 | Jenkins 설치/설정 시 | ### quickstart/ - 빠른 시작 diff --git a/guides/nginx-fastcgi-guide.md b/guides/nginx-fastcgi-guide.md index ad93cca..acccca0 100644 --- a/guides/nginx-fastcgi-guide.md +++ b/guides/nginx-fastcgi-guide.md @@ -3,6 +3,9 @@ > **작성일**: 2026-02-22 > **대상**: SAM 프로젝트에 새로 합류한 개발자 +> **서버 인프라 학습 시리즈** | Part 2 of 3 +> [1. 서버 동작 원리](server-how-it-works.md) → **2. Nginx & FastCGI** → [3. PHP-FPM](php-fpm-guide.md) + --- ## 1. 개요 @@ -249,10 +252,17 @@ fastcgi_param SCRIPT_FILENAME /var/www/mng/public$fastcgi_script_name; ## 관련 문서 +**학습 시리즈**: + +| 순서 | 문서 | 설명 | +|------|------|------| +| Part 1 | [server-how-it-works.md](server-how-it-works.md) | 서버 동작 원리 전체 흐름 (이전) | +| Part 3 | [php-fpm-guide.md](php-fpm-guide.md) | PHP-FPM 프로세스 관리 심화 (다음) | + +**참고 문서**: + | 문서 | 설명 | |------|------| -| [server-how-it-works.md](server-how-it-works.md) | 서버 동작 원리 전체 가이드 | -| [php-fpm-guide.md](php-fpm-guide.md) | PHP-FPM 초보자 가이드 | | [docker-setup.md](../specs/docker-setup.md) | Docker 환경 설정값 상세 | --- diff --git a/guides/php-fpm-guide.md b/guides/php-fpm-guide.md index ef19a06..827406a 100644 --- a/guides/php-fpm-guide.md +++ b/guides/php-fpm-guide.md @@ -3,6 +3,9 @@ > **작성일**: 2026-02-22 > **대상**: SAM 프로젝트에 새로 합류한 개발자 +> **서버 인프라 학습 시리즈** | Part 3 of 3 +> [1. 서버 동작 원리](server-how-it-works.md) → [2. Nginx & FastCGI](nginx-fastcgi-guide.md) → **3. PHP-FPM** + --- ## 1. 개요 @@ -251,9 +254,17 @@ docker exec sam-mng-1 supervisorctl status ## 관련 문서 +**학습 시리즈**: + +| 순서 | 문서 | 설명 | +|------|------|------| +| Part 1 | [server-how-it-works.md](server-how-it-works.md) | 서버 동작 원리 전체 흐름 | +| Part 2 | [nginx-fastcgi-guide.md](nginx-fastcgi-guide.md) | Nginx와 FastCGI 프로토콜 심화 (이전) | + +**참고 문서**: + | 문서 | 설명 | |------|------| -| [server-how-it-works.md](server-how-it-works.md) | 서버 동작 원리 전체 가이드 | | [docker-setup.md](../specs/docker-setup.md) | Docker 환경 설정값 상세 | --- diff --git a/guides/server-how-it-works.md b/guides/server-how-it-works.md new file mode 100644 index 0000000..9e1ee73 --- /dev/null +++ b/guides/server-how-it-works.md @@ -0,0 +1,259 @@ +# SAM 서버 동작 원리 초보자 가이드 + +> **작성일**: 2026-02-22 +> **대상**: SAM 프로젝트에 새로 합류한 개발자 + +> **서버 인프라 학습 시리즈** | Part 1 of 3 +> **1. 서버 동작 원리** → [2. Nginx & FastCGI](nginx-fastcgi-guide.md) → [3. PHP-FPM](php-fpm-guide.md) + +--- + +## 1. 개요 + +### 1.1 이 문서의 목적 + +SAM 시스템에서 **웹 요청이 어떤 경로로 흐르는지**, **git push 후 서버에서 무슨 일이 일어나는지**를 설명한다. +설정값 나열이 아닌, **"왜 이런 구조인가"**에 초점을 맞춘다. + +### 1.2 SAM 전체 구조 + +``` +브라우저 → Nginx (SSL 종료, 도메인별 라우팅) + │ + ┌────┬───┴───┬─────┬─────┐ + ▼ ▼ ▼ ▼ ▼ + MNG API React Sales 5130 ← 5개 서비스 + (PHP)(PHP) (Node) (PHP) (PHP7.3) + └────┴───┬───┴─────┴─────┘ + ▼ + MySQL 8.0 ← 단일 DB 공유 +``` + +--- + +## 2. 웹 요청의 여정: URL에서 화면까지 + +### 2.1 전체 흐름 + +`https://mng.sam.kr/orders` 접속 시: + +``` +브라우저 →① Nginx →② PHP-FPM →③ Laravel →④ MySQL + │ +브라우저 ←────────────────────────────── ⑤ 응답 +``` + +### 2.2 Step 1: 브라우저 → Nginx + +Nginx는 **도메인 이름**을 보고 어떤 서비스로 보낼지 결정한다. + +- `mng.sam.kr` → MNG 컨테이너의 PHP-FPM (포트 9000) +- `api.sam.kr` → API 컨테이너의 PHP-FPM (포트 9000) +- `dev.sam.kr` → React 컨테이너의 Node.js (포트 3000) + +또한 HTTP(80) 요청을 HTTPS(443)로 리다이렉트하고, SSL 인증서를 처리한다. +이를 **SSL 종료**(SSL Termination)라 한다. 내부 통신은 암호화 없이 빠르게 진행된다. + +### 2.3 Step 2: Nginx → PHP-FPM + +Nginx는 PHP 코드를 직접 실행하지 못한다. 대신 **FastCGI 프로토콜**로 PHP-FPM에 요청을 전달한다. + +``` +Nginx: "이 PHP 파일을 실행해줘" → fastcgi_pass mng:9000 +PHP-FPM: "결과 HTML이야" → Nginx → 브라우저 +``` + +PHP-FPM은 여러 **워커 프로세스**를 미리 만들어 두고, 요청이 오면 빈 워커에 할당한다. +MNG의 경우 최대 20개 워커(`pm.max_children = 20`)가 동시에 요청을 처리할 수 있다. + +### 2.4 Step 3: PHP-FPM → Laravel + +PHP-FPM이 실행하는 진입점은 `public/index.php`다. 여기서 Laravel 프레임워크가 시작된다. + +``` +public/index.php + → Bootstrap (설정 로드, 서비스 등록) + → 미들웨어 (인증, 권한, 로깅) + → 라우터 (URL → 컨트롤러 매핑) + → 컨트롤러 (비즈니스 로직) + → 뷰 렌더링 (Blade 템플릿 → HTML) +``` + +### 2.5 Step 4: Laravel → MySQL + +컨트롤러에서 Eloquent ORM으로 DB를 조회한다. 예를 들어: + +```php +// 코드: Order::where('status', 'active')->get(); +// 실제 SQL: SELECT * FROM orders WHERE status = 'active' AND tenant_id = 1; +``` + +`tenant_id`는 글로벌 스코프로 자동 추가되어, 다른 테넌트의 데이터가 섞이지 않는다. + +### 2.6 Step 5: 응답이 돌아오는 길 + +MySQL → Laravel(HTML 생성) → PHP-FPM → Nginx → 브라우저 순으로 돌아온다. +MNG는 HTMX를 사용하므로, 이후 상호작용은 **HTML 조각**(partial)만 주고받아 페이지 전체를 새로고침하지 않는다. + +--- + +## 3. 각 구성 요소의 역할 + +| 구성 요소 | 역할 | 비유 | +|-----------|------|------| +| **Nginx** | 리버스 프록시, SSL, 정적 파일 | 안내 데스크 | +| **PHP-FPM** | PHP 워커 풀 관리 | 창구 직원 팀 | +| **Laravel** | MVC, 라우팅, 비즈니스 로직 | 업무 매뉴얼 | +| **MySQL** | 데이터 저장/조회 | 서류 보관실 | +| **Supervisor** | 프로세스 감시, 자동 재시작 | 관리 감독관 | + +### 3.1 Supervisor가 관리하는 프로세스 + +각 컨테이너 안에서 Supervisor가 여러 프로세스를 관리한다. + +**API 컨테이너** (`sam-api-1`): +- `php-fpm` — PHP 요청 처리 +- `nginx` — 컨테이너 내부 웹서버 +- `queue-worker` — 백그라운드 작업 (이메일, 알림 등) +- `scheduler` — 60초마다 예약 작업 실행 (`schedule:run`) + +**MNG 컨테이너** (`sam-mng-1`): +- `php-fpm`, `nginx` — 위와 동일 +- `queue-worker` x2 — 2개 워커가 병렬 처리 + +--- + +## 4. 로컬 환경 vs 서버 환경 + +### 4.1 비교 + +``` +[로컬 - Docker] [서버 - Bare-metal] +┌───────────────┐ ┌───────────────┐ +│ sam-nginx-1 │ │ Nginx │ +├───────────────┤ ├───────────────┤ +│ sam-mng-1 │ │ MNG (직접) │ +│ sam-api-1 │ │ API (직접) │ +├───────────────┤ ├───────────────┤ +│ sam-mysql-1 │ │ MySQL (직접) │ +└───────────────┘ └───────────────┘ + 네트워크: samnet 네트워크: localhost +``` + +### 4.2 핵심 차이 + +| 항목 | 로컬 (Docker) | 서버 (Bare-metal) | +|------|--------------|-------------------| +| **DB 접속** | `DB_HOST=sam-mysql-1` | `DB_HOST=127.0.0.1` | +| **코드 반영** | 볼륨 마운트 (실시간) | `git pull` 필요 | +| **명령 실행** | `docker exec sam-api-1 php artisan ...` | `php artisan ...` | + +--- + +## 5. "git push하면 무슨 일이 일어나는가?" + +### 5.1 배포 흐름 다이어그램 + +``` +개발자 PC (WSL) Gitea 서버 운영 서버 +┌──────────┐ push ┌──────────┐ pull ┌──────────┐ +│ 코드 수정 │ ──────────→ │ 원격 │ ←───────── │ 서버에서 │ +│ git add │ │ 저장소 │ │ 수동 pull │ +│ git commit│ └──────────┘ └──────────┘ +└──────────┘ +``` + +> **주의**: 자동 배포(CI/CD)가 없다. 서버에서 **수동으로 `git pull`** 해야 반영된다. + +### 5.2 PHP 앱 배포 (MNG, API) + +```bash +# 서버에서 실행하는 명령 (개발팀장이 수행) +cd /home/webservice/api +git pull # ① 최신 코드 받기 +composer install # ② 패키지 의존성 동기화 +php artisan migrate # ③ DB 구조 변경 적용 +php artisan config:clear # ④ 설정 캐시 초기화 +``` + +**각 명령이 필요한 이유**: + +| 명령 | 왜 필요한가 | +|------|------------| +| `git pull` | 코드를 최신 상태로 동기화 | +| `composer install` | 새로 추가된 PHP 패키지 설치 (`composer.json` 변경 시) | +| `php artisan migrate` | 새 테이블/컬럼 생성 등 DB 스키마 적용 (API만) | +| `php artisan config:clear` | `.env` 또는 `config/` 변경 시 캐시된 설정 갱신 | + +### 5.3 React 앱 배포 (Next.js) + +서버 스펙(2코어, 3.8GB RAM)으로는 Next.js 빌드가 메모리 부족으로 실패한다. +따라서 **로컬에서 빌드 → 결과물을 서버에 업로드**하는 방식을 사용한다. + +```bash +# deploy.sh가 수행하는 5단계 +① 로컬에서 npm run build # standalone 빌드 +② tar.gz로 압축 # .next/standalone + static + public +③ scp로 서버 업로드 # 압축 파일 전송 +④ 서버에서 압축 해제 + 시작 # node server.js (포트 3001) +⑤ 로컬 정리 # 임시 파일 삭제 +``` + +--- + +## 6. SAM 도메인별 요청 경로 + +### 6.1 도메인 → 서비스 매핑 + +| 도메인 | 서비스 | 기술 스택 | 응답 형태 | +|--------|--------|-----------|-----------| +| `mng.sam.kr` | MNG | Laravel + Blade + HTMX | HTML (서버 렌더링) | +| `api.sam.kr` | API | Laravel | JSON | +| `dev.sam.kr` | React | Next.js | HTML (SSR/CSR) | +| `sales.sam.kr` | Sales | Laravel | HTML | +| `5130.sam.kr` | 5130 | PHP 7.3 (레거시) | HTML | + +### 6.2 서비스별 요청 흐름 + +**MNG** (관리자 화면 — Blade + HTMX): +``` +브라우저 → Nginx(:443) → MNG PHP-FPM(:9000) → Laravel → Blade HTML +이후 HTMX가 HTML 조각을 Ajax로 교체 (전체 새로고침 없음) +``` + +**API** (REST API — JSON 응답): +``` +React/외부 → Nginx(:443) → API PHP-FPM(:9000) → Laravel → JSON +인증: Bearer 토큰 (Authorization 헤더) +``` + +**React** (Next.js — SSR + CSR): +``` +브라우저 → Nginx(:443) → Node.js(:3000) → SSR HTML +이후 React 하이드레이션 → CSR (클라이언트 렌더링) +API 호출 시 → Next.js API Route 프록시 → api.sam.kr +``` + +--- + +## 관련 문서 + +**학습 시리즈 — 다음 문서**: + +| 순서 | 문서 | 설명 | +|------|------|------| +| Part 2 | [nginx-fastcgi-guide.md](nginx-fastcgi-guide.md) | Nginx와 FastCGI 프로토콜 심화 | +| Part 3 | [php-fpm-guide.md](php-fpm-guide.md) | PHP-FPM 프로세스 관리 심화 | + +**참고 문서**: + +| 문서 | 설명 | +|------|------| +| [docker-setup.md](../specs/docker-setup.md) | Docker 환경 설정값 상세 | +| [system-overview.md](../architecture/system-overview.md) | 시스템 아키텍처 레퍼런스 | +| [production-deployment-plan.md](../plans/production-deployment-plan.md) | 운영 배포 계획 | +| [dev-commands.md](../quickstart/dev-commands.md) | 개발 명령어 모음 | + +--- + +**최종 업데이트**: 2026-02-22 From 472f6a1b4470889bf8be9e4f6fbc657a9856039b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Feb 2026 22:29:11 +0900 Subject: [PATCH 20/69] =?UTF-8?q?docs:=20[guides]=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9D=B8=ED=94=84=EB=9D=BC=20=ED=95=99=EC=8A=B5=20=EC=8B=9C?= =?UTF-8?q?=EB=A6=AC=EC=A6=88=20=ED=86=B5=ED=95=A9=20PPTX=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- guides/server-infra-series.pptx | Bin 0 -> 399715 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 guides/server-infra-series.pptx diff --git a/guides/server-infra-series.pptx b/guides/server-infra-series.pptx new file mode 100644 index 0000000000000000000000000000000000000000..d197c0650c9b004085065d1a44f5bae89b084d25 GIT binary patch literal 399715 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 Date: Sun, 22 Feb 2026 22:50:14 +0900 Subject: [PATCH 21/69] =?UTF-8?q?docs:=20[architecture]=20SAM=2010,000=20?= =?UTF-8?q?=ED=85=8C=EB=84=8C=ED=8A=B8=20=EC=8A=A4=EC=BC=80=EC=9D=BC?= =?UTF-8?q?=EB=A7=81=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5단계 로드맵: 캐시→수평확장→마이크로서비스→샤딩→엔터프라이즈 - SAM 특수 고려사항: tenant_id 샤딩, 권한 캐싱, 219개 테이블 분류 - INDEX.md에 문서 항목 추가 --- INDEX.md | 1 + architecture/scaling-roadmap-10k-tenants.md | 214 ++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 architecture/scaling-roadmap-10k-tenants.md diff --git a/INDEX.md b/INDEX.md index e52b993..b7fff60 100644 --- a/INDEX.md +++ b/INDEX.md @@ -69,6 +69,7 @@ docs/ |------|------|--------------| | [system-overview.md](architecture/system-overview.md) | 전체 시스템 아키텍처 | 새 기능 설계 전 | | [security-policy.md](architecture/security-policy.md) | 인증/인가, 보안 규칙 | 보안 관련 작업 전 | +| [scaling-roadmap-10k-tenants.md](architecture/scaling-roadmap-10k-tenants.md) | 10,000 테넌트 스케일링 로드맵 | 확장 전략 검토 시 | ### rules/ - 비즈니스 규칙 > 도메인 로직, 검증 규칙, 상태 전이 diff --git a/architecture/scaling-roadmap-10k-tenants.md b/architecture/scaling-roadmap-10k-tenants.md new file mode 100644 index 0000000..35c204a --- /dev/null +++ b/architecture/scaling-roadmap-10k-tenants.md @@ -0,0 +1,214 @@ +# SAM 10,000 테넌트 스케일링 로드맵 + +> **작성일**: 2026-02-22 +> **성격**: 가상 시나리오 — 세계 최고 수준 엔지니어링 팀이 설계한다는 가정 + +--- + +## 1. 현재 상태 진단 + +### 1.1 현재 아키텍처 요약 + +``` +브라우저 → Nginx → PHP-FPM(20 workers) → Laravel → MySQL 8.0 (단일) +서버: 2코어/3.8GB RAM | 배포: 수동 git pull | 모니터링: 없음 | 캐시: 없음 +``` + +### 1.2 핵심 병목 지점 + +| 영역 | 현재 | 10,000 테넌트 시 문제 | 심각도 | +|------|------|---------------------|--------| +| DB | MySQL 단일 | 커넥션 폭발, 슬로우 쿼리 | 치명적 | +| 컴퓨팅 | FPM 20워커 | 동시 요청 20개 제한 | 치명적 | +| 캐시 | 없음 | 모든 요청이 DB 직행 | 치명적 | +| 큐 | DB 드라이버 | 큐 자체가 DB 압박 | 심각 | +| 검색 | SQL LIKE | 219개 테이블 풀스캔 | 심각 | +| 배포 | 수동 git pull | 다운타임, 롤백 불가 | 높음 | +| 모니터링 | 없음 | 장애 인지 불가 | 높음 | + +### 1.3 가장 먼저 죽는 곳 + +- 동시 사용자 **20명** → FPM 워커 전부 점유 → 504 Timeout +- 동시 사용자 **200명** → MySQL `max_connections`(151) 고갈 +- 테넌트 **1,000개** → 권한 UNION 3개 쿼리가 매 요청마다 실행 + +--- + +## 2. 5단계 로드맵 개요 + +``` +Phase 1 (0~3개월) 캐시 + 모니터링 + 서버 업그레이드 → 100 테넌트 +Phase 2 (3~6개월) DB 복제 + K8s + S3 + 검색 엔진 → 1,000 테넌트 +Phase 3 (6~9개월) 마이크로서비스 + 이벤트 아키텍처 → 3,000 테넌트 +Phase 4 (9~12개월) DB 샤딩 + 테넌트 티어링 + 멀티리전 → 10,000 테넌트 +Phase 5 (12~18개월) 관측성 고도화 + 카오스 엔지니어링 → 10,000+ 테넌트 +``` + +--- + +## 3. Phase 1: 기초 체력 (0~3개월) → 100 테넌트 + +### 3.1 Redis 도입 (최우선) + +| 대상 | 캐시 TTL | 효과 | +|------|---------|------| +| 세션 저장소 | 2시간 | DB 세션 테이블 부하 제거 | +| 권한 캐시 | 5분 | UNION 3개 쿼리 제거 (매 요청) | +| 메뉴 트리 | 10분 | 중첩 쿼리 제거 | +| 공통 코드 | 1시간 | `common_codes` 반복 조회 제거 | +| Laravel 큐 | - | `database` → `redis` 드라이버 전환 | + +**예상 효과**: DB 쿼리 **60~70% 감소**, 응답 시간 **3~5배 개선**. + +### 3.2 모니터링 구축 + +Grafana + Prometheus + Laravel Telescope + MySQL slow_log. + +**핵심 알림**: FPM 워커 사용률 >80%, MySQL 커넥션 >100, 응답 시간 >2초, 큐 적체 >1000건 → Slack 알림. + +### 3.3 서버 업그레이드 + CI/CD + +| 항목 | 현재 | Phase 1 | +|------|------|---------| +| CPU/RAM | 2코어/3.8GB | 8코어/32GB | +| 스토리지 | HDD | NVMe SSD | +| FPM | 20 워커 | 100 워커 | +| 배포 | 수동 git pull | Jenkins CI/CD (무중단 rolling) | + +--- + +## 4. Phase 2: 수평 확장 (3~6개월) → 1,000 테넌트 + +### 4.1 아키텍처 + +``` +브라우저 → LB(L7) → App Server ×N → Redis Cluster + → MySQL Primary + Replica ×2 + → S3 + CDN (정적/업로드 파일) +``` + +### 4.2 핵심 변경 + +| 영역 | 변경 | 효과 | +|------|------|------| +| **K8s** | HPA 기반 오토스케일링 (API 3~10 pods) | 트래픽에 따라 자동 확장 | +| **DB R/W 분리** | `config/database.php`에 read/write 설정 | 읽기 부하 80% Replica로 분산 | +| **파일 → S3** | Laravel Filesystem → S3 + CloudFront | 서버 간 파일 공유, CDN 가속 | +| **검색 엔진** | SQL LIKE → Meilisearch | 밀리초 응답, 형태소 분석, 오타 허용 | + +--- + +## 5. Phase 3: 마이크로서비스 (6~9개월) → 3,000 테넌트 + +### 5.1 서비스 분리 + +``` +모놀리스(sam-api) → Auth | Product | Order | MES | Finance | Notification +``` + +**분리 순서**: ① 알림 (독립적, 비동기) → ② MES (변경 빈도 높음) → ③ 인증 (가용성 최우선) + +### 5.2 이벤트 기반 전환 + +동기 호출 체인을 이벤트 발행으로 전환한다. 주문 생성 시 재고 차감/알림/회계를 비동기 처리. + +**메시지 브로커**: Redis Streams 또는 RabbitMQ. 응답 시간 2초 → 200ms. + +### 5.3 API Gateway + +Kong/Traefik으로 테넌트별 Rate Limiting, 인증 검증, 요청 라우팅, 응답 캐싱 통합. + +--- + +## 6. Phase 4: 대규모 멀티테넌시 (9~12개월) → 10,000 테넌트 + +### 6.1 DB 샤딩 + +``` +Shard Router (tenant_id 기반) → Shard 0 | Shard 1 | Shard 2 ... + 각 Shard = Primary + Replica +``` + +`tenant_id`가 파티션 키이므로 크로스 샤드 조인 불필요 — SAM의 강점. + +### 6.2 테넌트 티어링 + +| Tier | 자원 | SLA | Rate Limit | +|------|------|-----|-----------| +| Enterprise | 전용 DB/Redis/Pod | 99.99% | 1000/분 | +| Business | 공유 (높은 우선순위) | 99.9% | 300/분 | +| Standard | 공유 | 99.5% | 60/분 | + +### 6.3 멀티리전 + +한국(Primary) + 일본/동남아(Secondary) 리전. DB 복제 + Global Load Balancer로 지리적 라우팅. + +--- + +## 7. Phase 5: 엔터프라이즈 성숙 (12~18개월) → 10,000+ + +| 영역 | 도입 | 목적 | +|------|------|------| +| **관측성** | Jaeger(분산추적) + Loki(중앙로그) + PagerDuty | 전체 서비스 체인 추적 | +| **카오스 엔지니어링** | DB 다운, Pod Kill, 네트워크 지연 주입 | 복원력 검증 | +| **데이터 파이프라인** | CDC(Debezium) → 데이터 웨어하우스 | 운영 DB에서 분석 쿼리 분리 | +| **배포** | Blue-Green + Canary (5% → 100%) | 30초 이내 롤백 | + +--- + +## 8. 기술 스택 진화 요약 + +| 영역 | 현재 | Phase 1 | Phase 2 | Phase 4 | +|------|------|---------|---------|---------| +| 서버 | 단일 2코어 | 단일 8코어 | K8s 3+ 노드 | 멀티리전 | +| DB | MySQL 단일 | + 쿼리 최적화 | Primary + Replica ×2 | Shard ×N | +| 캐시 | 없음 | Redis 단일 | Redis Cluster | 테넌트별 격리 | +| 큐 | DB | Redis | Redis | 티어별 큐 | +| 배포 | 수동 | Jenkins CI/CD | K8s Rolling | Canary | +| 모니터링 | 없음 | Grafana | + Telescope | + 카오스 | + +--- + +## 9. 가장 중요한 3가지 + +**1. Redis (=산소)**: 캐시 없이 10,000 테넌트는 **절대 불가능**. 권한 UNION 쿼리 3개 + 메뉴 + 세션이 매 요청마다 실행된다. Redis 하나로 DB 부하 60% 감소. + +**2. DB R/W 분리 (=심장)**: ERP 읽기:쓰기 = 8:2. Replica 2대 추가로 DB 부하 1/3 분산. Laravel `config/database.php` 변경만으로 적용. + +**3. 관측성 (=눈)**: 모니터링 없이 스케일링은 눈 감고 운전하는 것. 슬로우 쿼리 + 응답 시간 + 알림부터 시작. + +--- + +## 10. SAM 특수 고려사항 + +### 10.1 tenant_id 기반 격리 + +- **강점**: `BelongsToTenant` 스코프 일관 적용, 크로스 테넌트 조인 불필요 → 샤딩에 유리 +- **개선 필요**: `tenant_id` 인덱스 첫 번째 컬럼 여부 전수 검사, `deleted_at` 누적 데이터 파티셔닝 + +### 10.2 권한 시스템 최적화 + +3중 UNION 쿼리가 매 요청 실행된다. 개선: ① Redis 권한 캐시(TTL 5분) → ② 변경 시 해당 사용자 캐시만 무효화 → ③ JWT 클레임에 권한 포함(DB 조회 0회). + +### 10.3 219개 테이블 샤딩 분류 + +| 분류 | 예시 | 처리 | +|------|------|------| +| 테넌트 데이터 | orders, products | 샤딩 대상 | +| 시스템 공통 | permissions, common_codes | 공유 DB 유지 | +| 감사 로그 | audit_logs | 별도 시계열 DB | + +--- + +## 관련 문서 + +| 문서 | 설명 | +|------|------| +| [system-overview.md](system-overview.md) | 현재 시스템 아키텍처 | +| [security-policy.md](security-policy.md) | 현재 보안 구조 | +| [docker-setup.md](../specs/docker-setup.md) | 현재 Docker 구성 | +| [server-how-it-works.md](../guides/server-how-it-works.md) | 서버 동작 원리 | + +--- + +**최종 업데이트**: 2026-02-22 From 920d83c27a072711ebfe5f0edd58ddf54f0e7217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 23 Feb 2026 00:38:06 +0900 Subject: [PATCH 22/69] =?UTF-8?q?docs:=20[plans]=20ERP=20=ED=9A=8C?= =?UTF-8?q?=EA=B3=84=EA=B4=80=EB=A6=AC=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20D1.6=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EB=B3=80=ED=99=98=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 65페이지 PDF를 AI 참조용 마크다운으로 변환 - 대시보드, 회계관리(7개 화면), 기준정보(5개 화면) 전체 수록 - 이관 기초자료 CSV 스펙 4종(거래처/거래내역/계좌내역/세금계산서) 포함 - INDEX.md에 plans/ 섹션 추가 --- INDEX.md | 8 + plans/SAM_ERP_회계관리_Storyboard_D1.6.md | 1288 +++++++++++++++++++++ 2 files changed, 1296 insertions(+) create mode 100644 plans/SAM_ERP_회계관리_Storyboard_D1.6.md diff --git a/INDEX.md b/INDEX.md index b7fff60..2fe5f8b 100644 --- a/INDEX.md +++ b/INDEX.md @@ -145,6 +145,14 @@ docs/ | [scripts/extract_to_markdown.py](contracts/scripts/extract_to_markdown.py) | DOCX → Markdown 추출 | | [scripts/sync_check.py](contracts/scripts/sync_check.py) | DOCX ↔ Markdown 동기화 검증 | +### plans/ - 개발 계획 +> 임시 개발 계획 문서 (작업 완료 후 정리 → 삭제) + +| 문서 | 설명 | +|------|------| +| [SAM_ERP_회계관리_Storyboard_D1.6.md](plans/SAM_ERP_회계관리_Storyboard_D1.6.md) | ERP 회계관리 스토리보드 D1.6 (65p PDF → 마크다운 변환) | +| [production-deployment-plan.md](plans/production-deployment-plan.md) | 운영 환경 배포 계획 (CI/CD, 서버 아키텍처) | + ### features/ - 기능별 문서 | 문서 | 설명 | diff --git a/plans/SAM_ERP_회계관리_Storyboard_D1.6.md b/plans/SAM_ERP_회계관리_Storyboard_D1.6.md new file mode 100644 index 0000000..1ab7db7 --- /dev/null +++ b/plans/SAM_ERP_회계관리_Storyboard_D1.6.md @@ -0,0 +1,1288 @@ +# SAM ERP 회계관리 스토리보드 D1.6 + +> **작성일**: 2026-02-20 +> **버전**: D1.6 +> **상태**: 프론트 작성 +> **원본**: `SAM_ERP_회계관리_Storyboard_D1.6_260220.pdf` (65페이지) + +--- + +## 문서 이력 + +| 날짜 | 버전 | 주요 내용 | 상세 | +|------|------|----------|------| +| 2026-02-13 | D1.5 | 프론트 작성 | 세금계산서 관리, 계좌 입출금 내역, 계좌 관리, 상품권 관리, 바로빌 연동 수정 및 추가 | +| 2026-02-20 | D1.6 | 프론트 작성 | 거래처 관리(사업자등록증 OCR), 일일일보, 대시보드(생산, 시공), 이관 기초자료, 달력 관리, 즐겨찾기, 신용평가 수정 및 추가 | + +--- + +## 메뉴 구조 + +``` +SAM ERP +├── 로그인 +├── 회원가입 +├── 대시보드 +├── MES +│ ├── 판매관리 +│ ├── 구매관리 +│ ├── 발주관리 +│ ├── 공사관리 +│ ├── 생산관리 +│ ├── 품질관리 +│ ├── 자재관리 +│ ├── 장비관리 +│ └── 차량관리 +├── 인사관리 +├── 전자결재 +├── 게시판 +├── 회계관리 ★ (본 문서 범위) +│ ├── 거래처 관리 +│ ├── 세금계산서 발행 +│ ├── 세금계산서 관리 +│ ├── 계좌 입출금 내역 +│ ├── 카드 사용 내역 +│ ├── 상품권 관리 +│ ├── 일반 전표 입력 +│ └── 일일일보 +├── 기준정보 ★ (본 문서 범위) +│ ├── 바로빌 연동 관리 +│ ├── 계좌 관리 +│ ├── 카드 관리 +│ ├── 달력 관리 +│ └── 이관 기초자료 +├── 보고서 및 분석 +└── 운영 + ├── 회사정보 + ├── 계정정보 + ├── 구독관리 + ├── 결제내역 + └── 고객센터 +``` + +--- + +## 화면 목록 (페이지 인덱스) + +| 페이지 | 경로 | 화면명 | +|--------|------|--------| +| 4 | 공통 | 섹션 구분 | +| 5 | 공통 | 즐겨찾기 | +| 6 | 대시보드 | 섹션 구분 | +| 7 | 대시보드 | 대시보드 (자금 현황, 오늘의 이슈, AI 리포트) | +| 8 | 대시보드 | 대시보드 (매출 현황) | +| 9 | 대시보드 | 대시보드 (매입 현황) | +| 10 | 대시보드 | 대시보드 (생산 현황) | +| 11 | 대시보드 | 대시보드 (시공 현황, 미출고 내역) | +| 12 | 대시보드 | 대시보드 (근태 현황) | +| 13-14 | 대시보드 > 항목 설정 팝업 | 항목 설정_대시보드 팝업 | +| 15 | 회계관리 | 섹션 구분 | +| 16 | 회계관리 > 거래처 관리 | 거래처 관리 (목록) | +| 17-18 | 회계관리 > 거래처 관리 > 거래처 상세 | 거래처 상세 (등록/수정) | +| 19 | 회계관리 > 거래처 관리 > 거래처 상세 | 신용분석 리포트 팝업 | +| 20 | 회계관리 > 세금계산서 발행 | 세금계산서 발행 (목록) | +| 21-22 | 회계관리 > 세금계산서 발행 | 세금계산서 발행_확장 (발행 입력) | +| 23 | 회계관리 > 세금계산서 발행 | 공급자 기초정보 설정 팝업 | +| 24 | 회계관리 > 세금계산서 발행 | 거래처 검색 팝업 | +| 25 | 회계관리 > 세금계산서 관리 | 세금계산서 관리 (매출) | +| 26 | 회계관리 > 세금계산서 관리 | 세금계산서 관리 (매입) | +| 27 | 회계관리 > 세금계산서 관리 | 세금계산서 수기 입력 팝업 | +| 28 | 회계관리 > 세금계산서 관리 | 카드 내역 불러오기 팝업 | +| 29 | 회계관리 > 세금계산서 관리 | 분개 수정 팝업 | +| 30 | 회계관리 > 계좌 입출금 내역 | 계좌 입출금 내역 (목록) | +| 31 | 회계관리 > 계좌 입출금 내역 | 입출금 수기 입력 팝업 | +| 32 | 회계관리 > 카드 사용 내역 | 카드 사용 내역 (목록) | +| 33 | 회계관리 > 카드 사용 내역 | 카드사용 수기 입력 팝업 | +| 34 | 회계관리 > 카드 사용 내역 | 거래 분개 팝업 | +| 35 | 회계관리 > 상품권 관리 | 상품권 관리 (목록) | +| 36 | 회계관리 > 상품권 관리 > 상품권 상세 | 상품권 상세 (등록/수정) | +| 37 | 회계관리 > 일반 전표 입력 | 일반 전표 입력 (목록) | +| 38 | 회계관리 > 일반 전표 입력 | 계정과목 설정 팝업 | +| 39 | 회계관리 > 일반 전표 입력 | 수기 전표 입력 팝업 | +| 40 | 회계관리 > 일반 전표 입력 | 분개 수정 팝업 | +| 41 | 기준정보 | 섹션 구분 | +| 42 | 기준정보 > 바로빌 연동 관리 | 바로빌 연동 관리 | +| 43 | 기준정보 > 바로빌 연동 관리 | 바로빌 로그인 정보 등록 팝업 | +| 44 | 기준정보 > 바로빌 연동 관리 | 바로빌 회원가입 정보 등록 팝업 | +| 45 | 기준정보 > 바로빌 연동 관리 | 은행 빠른조회 서비스 등록 팝업 | +| 46 | 기준정보 > 계좌 관리 | 계좌 관리 (목록) | +| 47 | 기준정보 > 계좌 관리 > 계좌 상세 | 계좌 상세_은행 | +| 48 | 기준정보 > 계좌 관리 > 계좌 상세 | 계좌 상세_대출 | +| 49 | 기준정보 > 계좌 관리 > 계좌 상세 | 계좌 상세_증권 | +| 50 | 기준정보 > 계좌 관리 > 계좌 상세 | 계좌 상세_보험_단체보험 | +| 51 | 기준정보 > 계좌 관리 > 계좌 상세 | 계좌 상세_보험_화재보험 | +| 52 | 기준정보 > 계좌 관리 > 계좌 상세 | 계좌 상세_보험_CEO보험 | +| 53 | 기준정보 > 카드 관리 | 카드 관리 (목록) | +| 54 | 기준정보 > 카드 관리 > 카드 상세 | 카드 상세 | +| 55 | 기준정보 > 달력 관리 | 달력 관리 (달력 뷰) | +| 56 | 기준정보 > 달력 관리 | 달력 관리 (목록 뷰) | +| 57 | 기준정보 > 달력 관리 | 달력 상세 팝업 | +| 58 | 기준정보 > 달력 관리 | 대량 등록 팝업 | +| 59-60 | 기준정보 > 이관 기초자료 | 이관 기초자료 (거래처 탭) | +| 61 | 기준정보 > 이관 기초자료 | 이관 기초자료_거래처 CSV 스펙 | +| 62 | 기준정보 > 이관 기초자료 | 이관 기초자료_거래 내역 CSV 스펙 | +| 63 | 기준정보 > 이관 기초자료 | 이관 기초자료_계좌 내역 CSV 스펙 | +| 64 | 기준정보 > 이관 기초자료 | 이관 기초자료_세금계산서 내역 CSV 스펙 | +| 65 | 기준정보 > 이관 기초자료 | 이관 기초자료_업로드 이력 | + +--- + +## 1. 공통 + +### 1.1 즐겨찾기 (P5) + +**경로**: 공통 (사이드바) + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 즐겨찾기 버튼 | 메뉴명에 마우스 롤 오버 시 표시. 즐겨찾기 설정 상태일 경우 상시 표시. 클릭: 즐겨찾기 설정/해제 토글. 디폴트: 해제 상태 | +| 2 | 즐겨찾기 폴더 버튼 | 클릭: 즐겨찾기 설정 목록 표시 | + +**사이드바 메뉴 구조** (회계관리 하위): +- 거래처관리 +- 세금계산서발행 +- 세금계산서관리 +- 계좌입출금내역 +- 카드사용내역 +- 상품권관리 +- 일반전표입력 +- 일일일보 + +--- + +## 2. 대시보드 + +### 2.1 대시보드 - 메인 (P7) + +**경로**: 대시보드 +**설명**: 종합 정보를 조회합니다 + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 항목 설정 버튼 | 클릭: 항목 설정_대시보드 팝업 표시 | +| 2 | 오늘의 이슈 영역 | 당일 이슈 발생 시 알림 처리. 목록 길 경우 영역 내 페이지네이션. 알림 상태에서 즉시 승인/보류 처리 가능 | +| 3 | 필터 셀렉트 박스 | 종류: 전체, 수주 성공, 추심 이슈, 적정 재고, 결재 요청, 세금 신고, 신규 업체 등록, 근태, 발주 완료. 디폴트: 전체. 숫자도 함께 표시 | +| 4 | 이슈 목록 | 클릭: 해당 상세 화면으로 이동. 화면 가로 길이에 따라 4, 3, 2, 1열로 반응형 표시 | +| 5 | 일일일보 영역 | 현금성 자산합계 표시. 클릭: 일일일보 화면으로 이동 | +| 6 | 매출채권 잔액 영역 | 미수금 잔액 합계 표시. 클릭: 미수금 현황 화면으로 이동 | +| 7 | 매입채무 잔액 영역 | = 세금계산서 매입 합계 - 거래처별 일반전표 출금 합계 (또는 거래처별 미지급금 합계) | +| 8 | AI 리포트 | 핵심 키워드 강조 표시 (빨간색: 경고, 주황: 주의, 녹색: 긍정, 파랑: 양호) | + +**이슈 케이스**: +- 신규 업체 등록 +- 재고 미달 알림 +- 채권 추심 등록, 상태 변경 +- 발주, 수주 등록 +- 지출결의서 등 전자결재 상신 +- 세금 신고 알림 등 + +**자금 현황 카드** (예시 데이터): +- 일일일보: 30.5억원 +- 매출채권 잔액: 30.5억원 +- 매입채무 잔액: 30.5억원 +- 운영자금 잔여: 6.2개월 + +**AI 리포트 예시**: +- "어제 3.5억원 출금했습니다. 최근 7일 평균 대비 2배 이상으로 점검이 필요합니다." +- "10.2억원이 입금되었습니다. 대한건설 선수금 입금이 주요 원인입니다." +- "총 현금성 자산이 300.2억원입니다. 월 운영비용 대비 18개월분이 확보되어 안정적입니다." + +--- + +### 2.2 대시보드 - 매출 현황 (P8) + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 거래처 필터 셀렉트 박스 | 검색 & 다중 선택. 종류: 전체, 매출 거래처명. 디폴트: 전체 | +| 2 | 정렬 셀렉트 박스 | 종류: 최신순, 등록순, 금액 높은순, 금액 낮은순. 디폴트: 최신순 | + +**표시 정보**: +- 누적 매출 금액 +- 목표 대비 달성률 (%) +- 전년 동기 대비 증감률 (%) +- 당월 매출 금액 +- 거래처별 매출 (차트) +- 월별 매출 추이 (1~7월 차트) +- 당월 매출 내역 테이블: No., 매출일, 거래처, 매출금액 + +--- + +### 2.3 대시보드 - 매입 현황 (P9) + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 거래처 필터 셀렉트 박스 | 검색 & 다중 선택. 종류: 전체, 매입 거래처명. 디폴트: 전체 | +| 2 | 정렬 셀렉트 박스 | 종류: 최신순, 등록순, 금액 높은순, 금액 낮은순. 디폴트: 최신순 | + +**표시 정보**: +- 누적 매입 금액 +- 미결제 금액 +- 전년 동기 대비 증감률 (%) +- 자재 유형별 구매 비율 (파이 차트: 원자재 55%, 부자재 35%, 소모품 10%) +- 월별 매입 추이 차트 +- 당월 매입 내역 테이블: No., 매출일, 거래처, 매입금액 + +--- + +### 2.4 대시보드 - 생산 현황 (P10) + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 생산 현황 탭 | 종류: 스크린 공정, 슬랫 공정, 절곡 공정. 디폴트: 스크린 공정 | +| 2 | 요약 정보 | 선택한 공정별 요약 정보 표시 (전체 작업, 할일, 작업중, 완료) | +| 3 | 수주 목록 | 클릭: 작업자 화면으로 이동 (해당 수주 작업 선택 상태). 긴급/우선/일반 구분 | +| 4 | 작업자 현황 목록 | 작업자별 당일 생산 현황 표시 (작업중/작업대기 상태, 완료 건수) | +| 5 | 출고 현황 정보 | 클릭: 출고 목록 화면으로 이동 (해당 기간 설정 상태). 예상 출고 7일/30일 이내, 건수 표시 | + +--- + +### 2.5 대시보드 - 시공 현황 (P11) + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 거래처 필터 셀렉트 박스 | 검색 & 다중 선택. 종류: 전체, 매출 거래처명. 디폴트: 전체 | +| 2 | 정렬 셀렉트 박스 | 종류: 납기일 가까운순, 납기일 먼순, 잔량 많은순, 잔량 적은순. 디폴트: 납기일 가까운순 | +| 3 | 시공 현황별 요약 정보 | 시공 진행(7일 이내), 시공 완료(7일 이내) 건수. 클릭: 시공관리 화면으로 이동 | +| 4 | 시공 상세 목록 | 클릭: 해당 시공 상세 화면으로 이동. 컬럼: No., 로트번호, 현장명, 수주처, 잔량, 납기일(D-N) | + +**미출고 내역 테이블**: 시공진행 상태의 현장명, 로트번호 목록 + +--- + +### 2.6 대시보드 - 근태 현황 (P12) + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 근태 현황별 요약 정보 | 오늘 출근, 오늘 휴가, 어제 지각, 어제 결근 인원. 클릭: 근태관리 화면으로 이동 | +| 2 | 상태 필터 셀렉트 박스 | 종류: 전체, 출근, 휴가. 디폴트: 전체 | + +**근태 목록 테이블**: No., 부서, 직급, 이름, 상태(출근/휴가) + +--- + +### 2.7 항목 설정_대시보드 팝업 (P13-14) + +**경로**: 대시보드 > 항목 설정_대시보드 팝업 + +#### 2.7.1 접대비 한도 관리 + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 접대비 한도 관리 셀렉트 박스 | 종류: 연간, 반기, 분기, 월. 디폴트: 연간. 선택 값으로 총 한도를 분할 계산 | +| 1-1 | 기업 구분 셀렉트 박스 | 종류: 일반법인, 중소기업. 디폴트: 일반법인 | +| 1-2 | 기업 구분 방법 영역 | 클릭: 확대/축소 토글. 디폴트: 축소 상태 | + +**중소기업 판단 기준표**: + +| 조건 | 기준 | 충족 요건 | +|------|------|----------| +| 매출액 | 업종별 상이 | 업종별 기준 금액 이하 | +| 자산총액 | 5,000억원 | 미만 | +| 독립성 | 소유/경영 | 대기업 계열 아님 | + +> 3가지 조건 모두 충족 시 중소기업 + +**업종별 매출액 기준** (최근 3개년 평균): + +| 업종 분류 | 기준 매출액 | +|-----------|------------| +| 제조업 | 1,500억원 이하 | +| 건설업 | 1,000억원 이하 | +| 운수업 | 1,000억원 이하 | +| 도매업 | 1,000억원 이하 | +| 소매업 | 600억원 이하 | +| 정보통신업 | 600억원 이하 | +| 전문서비스업 | 600억원 이하 | +| 숙박/음식점업 | 400억원 이하 | +| 기타 서비스업 | 400억원 이하 | + +**접대비 기본한도 판정**: + +| 판정 | 조건 | 접대비 기본한도 | +|------|------|----------------| +| 중소기업 | 3가지 모두 충족 | 3,600만원 | +| 일반법인 | 하나라도 미충족 | 1,200만원 | + +#### 2.7.2 복리후생비 한도 관리 + +| # | 요소 | 설명 | +|---|------|------| +| 2 | 복리후생비 한도 관리 셀렉트 박스 | 종류: 연간, 반기, 분기, 월. 디폴트: 연간. 선택 값으로 총 한도를 분할 계산 | +| 3 | 계산 방식 셀렉트 박스 | 종류: 직원당 정액 금액 방식, 연봉 총액 X 비율 방식. 디폴트: 직원당 정액 금액 방식 | +| 4 | 직원당 정액 금액/월 인풋박스 | 직원당 정액 금액 방식일 경우에만 표시 | +| 5 | 비율 인풋박스 | 연봉 총액 X 비율 방식일 경우에만 표시 | +| 6 | 연간 복리후생비 | 계산된 연간 복리후생비 표시 | + +#### 2.7.3 설정 항목 ON/OFF 목록 (P14) + +| 항목 | 기본값 | +|------|--------| +| 접대비 현황 | ON | +| 카드/가지급금 관리 | ON | +| 복리후생비 현황 | ON | +| 미수금 상위 회사 현황 | ON | +| 미수금 현황 | ON | +| 채권추심 현황 | ON | +| 부가세 현황 | ON | +| 캘린더 | ON | +| 매출 현황 | ON | +| 일별 매출 내역 | ON | +| 매입 현황 | ON | +| 일별 매입 현황 | ON | +| 생산 현황 | ON | +| 출고 현황 | ON | +| 미출고 내역 | ON | +| 시공 현황 | ON | +| 근태 현황 | ON | + +--- + +## 3. 회계관리 + +### 3.1 거래처 관리 (P16) + +**경로**: 회계관리 > 거래처 관리 +**설명**: 거래처 정보 및 신용등급을 관리합니다 + +**기능 설명**: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 거래처 등록 버튼 | 클릭: 거래처 상세_등록 화면으로 이동 | +| 2 | 구분 필터 셀렉트 박스 | 종류: 전체, 매출, 매입, 매입매출. 디폴트: 전체 | +| 3 | 사용 필터 셀렉트 박스 | 종류: 전체, 사용, 미사용. 디폴트: 전체 | +| 4 | 정렬 셀렉트 박스 | 종류: 최신순, 등록순. 디폴트: 최신순 | + +**기간 필터**: 전전월, 어제, 오늘, 전월, 당월, 당해년도 + +**목록 테이블 컬럼**: + +| 컬럼 | 설명 | +|------|------| +| No. | 순번 | +| 구분 | 매출, 매입, 매입매출 | +| 거래처명 | 회사명 | +| 매입 결제일 | 예: 10일 | +| 매출 결제일 | 예: 15일 | +| 신용등급 | 외부 신용평가 (AAA~D) | +| 거래등급 | 자사 기준 (A(우수)~E(위험)) | +| 미수금 | 금액 | +| 악성채권 | 악성채권 여부 | +| 상태 | 사용/미사용 | + +--- + +### 3.2 거래처 상세 (P17-18) + +**경로**: 회계관리 > 거래처 관리 > 거래처 상세 +**설명**: 거래처 상세 정보 및 신용등급을 관리합니다 + +#### 3.2.1 기본 정보 (P17) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 파일 등록 영역 (사업자등록증 OCR) | 클릭: 파일탐색기 팝업 표시. 사업자등록증 파일 등록 시 관련 정보 자동 입력 처리 | +| 2 | 구분 셀렉트 박스 | 종류: 매출, 매입, 매입매출 | +| 3 | 사용 셀렉트 박스 | 종류: 사용, 미사용 | + +**기본 정보 필드**: + +| 필드 | 설명 | +|------|------| +| 사업자등록증 | 파일 업로드 → OCR 자동 인식 | +| 거래처 코드 | 자동 생성 | +| 사업자등록번호 | OCR 또는 수기 입력 | +| 거래처명 | 회사명 | +| 대표자명 | 대표이사 | +| 업태 | 사업자등록증 업태 | +| 업종 | 사업자등록증 업종 | +| 거래처 유형 | 매출/매입/매입매출 | +| 상태 | 사용/미사용 | + +**연락처 정보 필드**: 주소(우편번호 찾기), 모바일, 전화번호, 팩스, 이메일 + +**담당자 정보 필드**: 담당자명, 담당자 전화 + +#### 3.2.2 신용/거래 정보 (P18) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 신용정보 보기 버튼 | 클릭: 신용분석 리포트 팝업표시 | +| 2 | 매입 결제일 셀렉트 박스 | 종류: 1일~31일, 말일. 디폴트: 10일. 거래처 유형이 매입 또는 매입매출일 경우 표시 | +| 3 | 매출 결제일 셀렉트 박스 | 종류: 1일~31일, 말일. 디폴트: 15일. 거래처 유형이 매출 또는 매입매출일 경우 표시 | +| 4 | 신용등급 인풋박스 | 외부 신용평가 등급. 예: AAA, AA, A, BBB, BB, B, CCC, CC, C, D | +| 5 | 거래등급 셀렉트 박스 | 종류: A(우수), B(양호), C(보통), D(주의), E(위험). 디폴트: A(우수). 자사 기준 거래처 평가 | +| 6 | 미수금 표시 영역 | 해당 거래처의 현재 미수금 합계 표시. 읽기 전용 | +| 7 | 연체 토글 | ON: 연체 상태, 연체일수 표시. OFF: 정상 상태. 미수금 현황에서 연체 설정과 연동 | +| 8 | 악성채권 토글 | ON: 악성채권으로 등록, 추심관리 목록에 표시. OFF: 정상. 디폴트: OFF | +| 9 | 미지급 표시 영역 | 해당 거래처에 대한 미지급금 합계 표시. 읽기 전용 | + +**추가 필드**: 입금계좌 은행, 계좌, 예금주, 세금계산서 이메일, 메모 + +--- + +### 3.3 신용분석 리포트 팝업 (P19) + +**경로**: 회계관리 > 거래처 관리 > 거래처 상세 > 신용분석 리포트 팝업 +**설명**: 현행화 (기존 화면 유지) + +--- + +### 3.4 세금계산서 발행 (P20-24) + +**경로**: 회계관리 > 세금계산서 발행 +**설명**: 바로빌 API를 통하여 전자세금계산서를 발행하고 관리합니다 + +#### 3.4.1 목록 화면 (P20) + +**상단 요약 카드**: 발행건수, 총 합계금액, 총 공급가액, 총 세액, 발행/전송 건수 + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 공급자 설정 버튼 | 클릭: 공급자 설정 팝업 표시 | +| 2 | 새로 발행 버튼 | 클릭: 전자세금계산서 발행 세부 입력 영역 표시 | +| 3 | 일자 셀렉트 박스 | 종류: 작성일자, 전송일자. 디폴트: 작성일자 | +| 4 | 상태 셀렉트 박스 | 종류: 전체, 작성중, 발행완료, 국세청 전송완료, 취소됨 | +| 5 | 정렬 셀렉트 박스 | 종류: 작성일자, 전송일자, 공급받는자, 합계금액. 디폴트: 작성일자 | +| 6 | 정렬2 셀렉트 박스 | 종류: 내림차순, 오름차순. 디폴트: 내림차순 | +| 7 | 조회 버튼 | 클릭: 조회 결과 표시 | + +**기간 필터**: 1주일, 1개월, 3개월, 날짜 범위 직접 선택 + +**거래처 검색**: 사업자 번호 또는 사업자명 + +**목록 테이블 컬럼**: 발행번호, 공급받는 자, 작성일자, 전송일자, 공급가액, 세액, 합계금액, 상태, 작업 + +#### 3.4.2 발행 세부 입력 (P21-22) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 검색 버튼 | 클릭: 거래처 검색 팝업표시, 선택 시 공급받는자 목록에 자동 입력 | +| 2 | 품목 추가 버튼 | 클릭: 품목 행 추가 | +| 3 | 전자세금계산서 발행 세부 입력 영역 | - | + +**공급자 영역 필드**: 등록번호, 종사업장, 상호, 대표자, 사업장주소, 업태, 종목, 담당자, 연락처, 이메일 + +**공급받는자 영역 필드**: 등록번호, 종사업장, 상호, 대표자, 사업장주소, 업태, 종목, 담당자, 연락처, 이메일, 세금계산서 수신 이메일 + +**품목 정보 테이블**: 월, 일, 품목, 수량, 단가, 공급가액, 세액, 합계, 과세유형(과세) + +**기타 필드**: 작성일자, 비고, 추가 메모사항 + +#### 3.4.3 공급자 기초정보 설정 팝업 (P23) + +| 필드 | 설명 | +|------|------| +| 사업자번호 | 회사정보 기본값 | +| 상호명 * | 필수 | +| 대표자명 * | 필수 | +| 업태 | - | +| 종목 | - | +| 주소 | - | +| 담당자명 | - | +| 연락처 | - | +| 이메일 | - | + +#### 3.4.4 거래처 검색 팝업 (P24) + +- 거래처명, 사업자번호, 담당자명으로 검색 +- 목록 표시: 거래처명, 사업자번호 + +--- + +### 3.5 세금계산서 관리 (P25-29) + +**경로**: 회계관리 > 세금계산서 관리 +**설명**: 홈택스에 신고된 세금계산서 매입/매출 내역을 조회하고 관리합니다 + +#### 3.5.1 매출 탭 (P25) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 일자 셀렉트 박스 | 종류: 작성일자, 전송일자. 디폴트: 작성일자 | +| 2 | 세금계산서 관리 탭 | 종류: 매출+수, 매입+수. 디폴트: 매출+수 | +| 3 | 수기 입력 버튼 | 클릭: 세금계산서 수기 입력 팝업표시 | + +**기간 필터**: 1분기, 2분기, 3분기, 4분기, 날짜 범위 직접 선택 + +**상단 요약 카드**: 매출 공급가액, 매출 세액, 매입 과세 공급가액, 매입 면세 공급가액, 매입 세액 + +**기간 요약**: 매출 합계(공급가액 + 세액), 매입 합계(공급가액 + 세액), 예상 부가세 + +**구분 표시**: 수기 세금계산서(색상 표시), 홈택스 연동 세금계산서(색상 표시) + +**목록 테이블 컬럼**: 작성일자, 발급일자, 거래처, 사업자번호(주민번호), 과세형태, 품목, 공급가액, 세액, 합계, 영수청구, 문서형태, 발급형태, 상태, 분개 + +**엑셀 다운로드** 기능 제공 + +#### 3.5.2 매입 탭 (P26) + +매출 탭과 동일 구조. 추가 기능: + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 분개 버튼 | 클릭: 분개 수정 팝업표시. 분개 저장 시 [분개 완료] 버튼으로 변경 | + +#### 3.5.3 세금계산서 수기 입력 팝업 (P27) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 구분 셀렉트 박스 | 종류: 매출, 매입. 디폴트: 매출 | +| 2 | 카드 내역 불러오기 버튼 | 클릭: 카드 내역 불러오기 팝업 표시. 선택 시 공급자 정보 및 금액 자동 입력, 수정 가능 | +| 3 | 과세유형 셀렉트 박스 | 종류: 과세, 영세, 면세. 디폴트: 과세 | + +**입력 필드**: 구분, 작성일자, 공급자명, 사업자 번호, 품목, 과세유형, 공급가액, 세액, 합계, 비고 + +#### 3.5.4 카드 내역 불러오기 팝업 (P28) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 카드 내역 목록 표시 | 기간 검색 후 목록 표시 | +| 2 | 선택 버튼 | 클릭: 세금계산서 수기 입력 팝업에 공급자 정보 및 금액 자동 입력 | + +**검색**: 가맹점/승인번호, 기간 검색 + +**목록 컬럼**: 날짜, 가맹점, 금액, 승인번호, 선택 + +#### 3.5.5 분개 수정 팝업 (P29) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 구분 영역 | 클릭: 차변/대변 토글 | +| 2 | 계정과목 셀렉트 박스 | 검색 가능. 종류: 계정과목 목록 | + +**세금계산서 정보**: 구분(매입/매출), 공급가액, 거래처, 세액 + +**분개 내역**: 구분(차변/대변), 계정과목, 금액, 합계 + +**버튼**: 분개 수정, 취소, 분개 삭제 + +--- + +### 3.6 계좌 입출금 내역 (P30-31) + +**경로**: 회계관리 > 계좌 입출금 내역 +**설명**: 계좌 입출금 내역을 조회하고 관리합니다 + +#### 3.6.1 목록 화면 (P30) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 저장 버튼 | 클릭: 인풋박스 영역의 변경값 저장 | +| 2 | 입출금 수기 입력 버튼 | 클릭: 입출금 수기 입력_등록 팝업 표시 | +| 3 | 계좌 입출금 내역 목록 | 클릭: 입출금 수기 입력 팝업 표시 | +| 4 | 수정 영역 | 수정된 영역에 하이라이트 표시 | +| 5 | 구분 셀렉트 박스 | 종류: 전체, 은행계좌, 대출계좌, 증권계좌, 보험계좌. 디폴트: 전체 | +| 6 | 금융기관 셀렉트 박스 | 종류: 전체, 금융기관명 목록. 디폴트: 전체 | + +**기간 필터**: 지난달, D-5월, D-4월, D-3월, D-2월, 이번달 + +**상단 요약 카드**: 입금, 출금, 잔액, 계좌 수, 거래 건수 + +**구분 표시**: 수기 계좌(색상 표시), 연동 계좌(색상 표시) + +**목록 테이블 컬럼**: No., 거래일시, 구분, 계좌정보(금융기관+계좌번호), 적요/내용, 입금, 출금, 잔액, 취급점, 상대계좌 예금주명 + +**엑셀 다운로드** 기능 제공 + +#### 3.6.2 입출금 수기 입력 팝업 (P31) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 계좌 셀렉트 박스 | 종류: 설정된 계좌 목록 | +| 2 | 수정 스티커 | 수정되었을 경우에만 표시 | +| 3 | 원본으로 복원 버튼 | 클릭: 원본 데이터로 변경, 수정 스티커 삭제 | + +**입력 필드**: + +| 필드 | 필수 | 설명 | +|------|------|------| +| 계좌 | * | 계좌 선택 | +| 거래일 | * | 날짜 선택 | +| 거래시간 | - | HH:MM:SS | +| 거래유형 | * | 입금/출금 | +| 금액 | * | 금액 입력 | +| 잔액 | - | 자동계산 | +| 적요 | - | 내용 | +| 상대계좌 예금주명 | - | - | +| 메모 | - | - | +| 취급점 | - | - | + +--- + +### 3.7 카드 사용 내역 (P32-34) + +**경로**: 회계관리 > 카드 사용 내역 +**설명**: 카드 사용 내역을 조회하고 관리합니다 + +#### 3.7.1 목록 화면 (P32) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 숨김 데이터 보기 버튼 | 클릭: 숨김 처리된 거래 영역 표시/숨김 토글. 디폴트: 숨김 | +| 2 | 저장 버튼 | 표 수정 사항 저장 처리 | +| 3 | 카드사용 수기 입력 버튼 | 클릭: 카드사용 수기 입력 팝업표시 | +| 4 | 카드사 셀렉트 박스 | 종류: 전체, 카드사명 목록. 디폴트: 전체 | +| 5 | 공제 셀렉트 박스 (필터) | 종류: 전체, 공제, 불공제. 디폴트: 전체 | +| 6 | 공제 셀렉트 박스 (행 내) | 종류: 공제, 불공제. 디폴트: 공제 | +| 7 | 텍스트 인풋박스 영역 | 인라인 수정 가능 | +| 8 | 숫자 인풋박스 영역 | 인라인 수정 가능 | +| 9 | 계정과목 셀렉트 박스 | 검색 가능. 종류: 계정과목 목록 | +| 10 | 분개 버튼 | 클릭: 거래 분개 팝업표시 | +| 11 | 숨김 버튼 | 클릭: 숨김 데이터 영역에 추가 | +| 12 | 복원 버튼 | 클릭: 원래 위치로 복원 | + +**기간 필터**: 지난달, D-5월, D-4월, D-3월, D-2월, 이번달 + +**상단 요약 카드**: 사용금액, 공제, 불공제, 등록된 카드 수 + +**구분 표시**: 수기 카드(색상 표시), 연동 카드(색상 표시) + +**목록 테이블 컬럼**: No., 사용일시, 카드사, 카드번호, 카드명, 공제, 사업자번호, 가맹점명/증빙/판매자상호, 내역, 합계금액, 공급가액, 세액, 계정과목, 분개, 숨김 + +**숨김 처리된 거래 영역** (별도 테이블): No., 사용일시, 카드사, 카드번호, 카드명, 사업자번호, 가맹점명, 합계금액, 숨김일시, 복원 + +**엑셀 다운로드** 기능 제공 + +#### 3.7.2 카드사용 수기 입력 팝업 (P33) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 카드 셀렉트 박스 | 종류: 설정된 카드 목록 (예: 신한카드 123123 카드명) | +| 2 | 공제 셀렉트 박스 | 종류: 공제, 불공제. 디폴트: 공제 | +| 3 | 계정과목 셀렉트 박스 | 검색 가능. 종류: 계정과목 목록 | + +**입력 필드**: + +| 필드 | 필수 | 설명 | +|------|------|------| +| 카드 선택 | * | 카드 셀렉트 | +| 사용일 | * | 날짜 | +| 사용시간 | - | HH:MM:SS | +| 승인유형 | * | 승인/취소 | +| 승인번호 | - | - | +| 가맹점명 | - | - | +| 사업자번호 | - | - | +| 공제여부 | * | 공제/불공제 | +| 계정과목 | - | 선택 | +| 증빙/판매자상호 | - | - | +| 내역 | - | - | +| 공급가액 | * | 금액 | +| 세액 | * | 금액 | +| 메모 | - | - | + +**합계 금액** = 공급가액 + 세액 (자동 계산 표시) + +#### 3.7.3 거래 분개 팝업 (P34) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 계정과목 셀렉트 박스 | 검색 가능. 종류: 계정과목 목록 | +| 2 | 공제 셀렉트 박스 | 종류: 공제, 불공제. 디폴트: 공제 | +| 3 | 분개 항목 추가 버튼 | 클릭: 분개 항목 영역 추가 | +| 4 | 삭제 버튼 | 클릭: 해당 분개 항목영역 삭제. 1개만 있을 경우 버튼 비활성화 | + +**거래 정보**: 가맹점, 사용일시, 공급가액, 세액, 합계금액 + +**분개 항목 필드**: 계정과목, 공제, 내역, 증빙/판매자상호, 공급가액, 세액, 합계금액, 내역, 메모 + +**분개 합계** 표시 + +--- + +### 3.8 상품권 관리 (P35-36) + +**경로**: 회계관리 > 상품권 관리 +**설명**: 상품권을 등록하고 관리합니다 + +> **상품권 접대비 기준**: +> 1. 50만원 미만: 일반 복리후생비/판촉비. 일반 경비로 처리 가능 +> 2. 50만원 이상: 접대비로 자동 분류. 사용처/수령인 기록 필수. 세법상 접대비 한도 관리 대상 + +#### 3.8.1 목록 화면 (P35) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 상품권 등록 버튼 | 클릭: 상품권 상세화면으로 이동 | +| 2 | 접대비 셀렉트 박스 | 종류: 전체, 해당, 해당없음. 디폴트: 전체 | +| 3 | 상태 셀렉트 박스 | 종류: 전체, 보유, 사용, 폐기. 디폴트: 전체 | + +**기간 필터**: 지난달, D-5월, D-4월, D-3월, D-2월, 이번달 + +**상단 요약 카드**: 전체 상품권 건수, 보유 상품권(건수/금액), 사용 상품권(건수/금액), 접대비 해당(건수/금액) + +**목록 테이블 컬럼**: No., 일련번호, 상품권명, 액면가, 구입일, 사용일, 접대비, 상태 + +#### 3.8.2 상품권 상세 (P36) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 액면가 인풋박스 | 50만원 이상 입력 시 필수 정보 표시 (사용처/수령인 등) | +| 2 | 구입처 셀렉트 박스 | 종류: 매입 거래처명 목록 | +| 3 | 구입목적 셀렉트 박스 | 종류: 판촉, 선물, 접대, 기타 | +| 4 | 상태 셀렉트 박스 | 종류: 보유, 사용, 폐기 | + +**기본 정보 필드**: 상품권명, 일련번호, 액면가, 구입처, 구입목적, 구입일, 접대비(해당/해당없음) + +**상품권 정보** (액면가 50만원 이상 필수): + +| 필드 | 설명 | +|------|------| +| 사용처/용도 | 내용 | +| 수령인 | 이름 | +| 수령인 소속 | 회사명 | +| 사용일 | 날짜 | +| 상태 | 보유/사용/폐기 | +| 비고 | - | + +--- + +### 3.9 일반 전표 입력 (P37-40) + +**경로**: 회계관리 > 일반 전표 입력 +**설명**: 계좌입출금내역을 기반으로 분개 전표를 생성합니다 + +#### 3.9.1 목록 화면 (P37) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 계정과목 설정 버튼 | 클릭: 계정과목 설정 팝업표시 | +| 2 | 수기 전표 입력 버튼 | 클릭: 수기 전표 입력 팝업표시 | +| 3 | 분개 버튼 | 클릭: 분개 수정 팝업표시 | + +**기간 필터**: 지난달, D-5월, D-4월, D-3월, D-2월, 이번달 + +**상단 요약 카드**: 전체 건수, 입금, 출금, 분개완료 건수, 미분개 건수 + +**구분 표시**: 수기 전표(색상 표시), 연동 전표(색상 표시) + +**목록 테이블 컬럼**: 날짜, 적요, 입금, 출금, 잔액, 분개 내역(차변/대변), 분개(차변 합계/대변 합계), 분개 버튼 + +**분개 내역 예시**: +``` +차 현금 6,000 + 대 외상매출금 6,000 + +차 현금 3,000 +차 복리후생비 3,000 + 대 외상매출금 6,000 +``` + +#### 3.9.2 계정과목 설정 팝업 (P38) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 분류 셀렉트 박스 (추가) | 종류: 자산, 부채, 자본, 수익, 비용. 디폴트: 자산 | +| 2 | 추가 버튼 | 클릭: 계정과목 행 추가 | +| 3 | 분류 셀렉트 박스 (필터) | 종류: 전체, 자산, 부채, 자본, 수익, 비용. 디폴트: 전체 | +| 4 | 상태 버튼 | 클릭: 사용중/미사용 토글 | +| 5 | 삭제 버튼 | 클릭: 해당 계정과목 삭제 처리 | + +**계정과목 추가 필드**: 코드(예: 101), 분류(자산/부채/자본/수익/비용), 계정과목명(예: 현금) + +**목록 테이블 컬럼**: 코드, 계정과목명, 분류, 상태, 작업(삭제) + +**검색**: 코드 또는 이름 검색 + +#### 3.9.3 수기 전표 입력 팝업 (P39) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 구분 영역 | 클릭: 차변/대변 토글 | +| 2 | 계정과목 셀렉트 박스 | 검색 가능. 종류: 계정과목 목록 | +| 3 | 거래처 셀렉트 박스 | 검색 가능. 종류: 거래처 목록 | +| 4 | 행 추가 버튼 | 클릭: 아래 위치에 행 추가 | + +**거래 정보 필드**: 전표일자 *, 적요, 전표번호(자동생성, 예: JE-20260213-002) + +**분개 내역 필드** (행 단위): 구분(차변/대변), 계정과목, 금액, 거래처, 적요 + +**합계**: 차변 합계, 대변 합계 + +#### 3.9.4 분개 수정 팝업 (P40) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 차변 합계 표시 | - | +| 2 | 대변 합계 표시 | - | +| 3 | 균형 표시 | 차변=대변이면 "대차 균형", 다르면 "차이: {금액}" 표시 | + +**거래 정보**: 날짜, 금액, 구분(입금/출금), 적요, 계좌, 전표 적요 + +**분개 내역 필드** (행 단위): 구분(차변/대변), 계정과목, 금액, 거래처, 적요 + +**버튼**: 분개 수정, 취소, 분개 삭제, 행추가 + +--- + +## 4. 기준정보 + +### 4.1 바로빌 연동 관리 (P42-45) + +**경로**: 기준정보 > 바로빌 연동 관리 +**설명**: 바로빌 연동 정보를 관리합니다 + +#### 4.1.1 메인 화면 (P42) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 바로빌 로그인 정보 등록 버튼 | 클릭: 바로빌 로그인 정보 등록 팝업 표시 | +| 2 | 바로빌 회원가입 정보 등록 버튼 | 클릭: 바로빌 회원가입 정보 등록 팝업 표시 | +| 3 | 은행 빠른조회 서비스 등록 버튼 | 클릭: 은행 빠른조회 서비스 등록 팝업 표시 | +| 4 | 계좌 연동 등록 버튼 | 바로빌 연동 정보 없으면 Alert, 있으면 바로빌 계좌 등록 팝업 표시 | +| 5 | 카드 연동 등록 버튼 | 바로빌 연동 정보 없으면 Alert, 있으면 바로빌 카드 등록 팝업 표시 | +| 6 | 공인인증서 등록 버튼 | 바로빌 연동 정보 없으면 Alert, 있으면 바로빌 공인인증서 등록 팝업 표시 | + +**연동 플로우**: + +``` +바로빌 연동 +├── 바로빌 회원인 경우 → 로그인 정보 등록 +├── 바로빌 비회원인 경우 → 회원가입 정보 등록 +├── 계좌 연동 (2단계) +│ ├── 1단계: 각 은행 인터넷뱅킹 접속 → 빠른조회/간편서비스 → 계좌 등록 +│ └── 2단계: 바로빌 연동 계좌 정보 등록 → 은행/계좌번호/비밀번호 → 조회 주기 설정 +├── 카드 연동 → 카드사/아이디/비밀번호 입력 +└── 공인인증서 등록 → 홈택스 세금계산서 연동용 +``` + +#### 4.1.2 바로빌 로그인 정보 등록 팝업 (P43) + +| 필드 | 필수 | 설명 | +|------|------|------| +| 바로빌 아이디 | * | Barobill_id | +| 비밀번호 | * | - | + +**버튼**: 등록하기 (바로빌 로그인 처리), 취소 + +#### 4.1.3 바로빌 회원가입 정보 등록 팝업 (P44) + +| 필드 | 필수 | 설명 | +|------|------|------| +| 사업자등록번호 | * | 123-12-12345 | +| 상호명 | * | 회사명 | +| 대표자명 | * | 홍길동 | +| 업태 | - | - | +| 업종 | - | - | +| 주소 | - | - | +| 바로빌 아이디 | * | Barobill_id | +| 비밀번호 | * | - | +| 담당자명 | - | - | +| 담당자 연락처 | - | - | +| 담당자 이메일 | - | - | + +**버튼**: 등록하기 (바로빌 회원가입 처리), 취소 + +#### 4.1.4 은행 빠른조회 서비스 등록 팝업 (P45) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 은행 셀렉트 박스 | 종류: 은행 목록. 디폴트: 첫번째 은행 | +| 2 | 구분 셀렉트 박스 | 종류: 기업, 개인. 디폴트: 기업 | +| 3 | 바로가기 버튼 | 클릭: 선택한 은행+구분에 해당하는 URL로 링크 | + +--- + +### 4.2 계좌 관리 (P46-52) + +**경로**: 기준정보 > 계좌 관리 +**설명**: 연동 계좌 및 수기 계좌를 등록하고 관리합니다 + +#### 4.2.1 목록 화면 (P46) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 수기 계좌 등록 버튼 | 클릭: 계좌 상세_등록 화면으로 이동 | +| 2 | 구분 셀렉트 박스 | 종류: 전체, 은행계좌, 대출계좌, 증권계좌, 보험계좌. 디폴트: 전체 | +| 3 | 금융기관 셀렉트 박스 | 종류: 전체, 금융기관명 목록. 디폴트: 전체 | + +**상단 요약 카드**: 전체 계좌, 국내/외환 계좌, 대출 계좌, 증권 계좌, 보험 계좌 (각 개수) + +**구분 표시**: 수기 계좌(색상 표시), 연동 계좌(색상 표시) + +**목록 테이블 컬럼**: No., 구분, 유형, 금융기관, 계좌번호, 계좌명, 상태(사용/중지) + +#### 4.2.2 계좌 상세_은행 (P47) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 구분 셀렉트 박스 | 종류: 은행계좌, 대출계좌, 증권계좌, 보험계좌 | +| 2 | 유형 셀렉트 박스 | 은행: 보통예금, 정기예금, 정기적금, 외화예금, 기타 | +| 3 | 사용 셀렉트 박스 | 종류: 사용, 중지 | +| 4 | 계좌 정보 영역 | 은행 계좌일 경우에만 표시 | + +**기본 정보 필드**: 계좌번호, 구분, 유형, 금융기관, 예금주, 계좌명(상품명), 상태 + +**계좌 정보 필드** (은행 전용): 시작일, 만기일, 이율, 계약금액, 이월잔액, 비고 + +#### 4.2.3 계좌 상세_대출 (P48) + +**기본 정보**: 은행과 동일 구조 +**유형**: 시설자금, 운영자금, 기타 + +**대출 정보 필드** (대출 전용): + +| 필드 | 설명 | +|------|------| +| 시작일 | 대출 시작일 | +| 만기일 | 대출 만기일 | +| 이율 | 이자율 (%) | +| 대출금액 | 총 대출 금액 | +| 대출잔액 | 현재 남은 잔액 | +| 상환 방식 | 원리금균등 등 | +| 이자 납입 주기 | 매월 등 | +| 거치 기간 | 개월 수 | +| 월 상환액 | 월 상환 금액 | +| 담보물 | 내용 | +| 비고 | - | + +#### 4.2.4 계좌 상세_증권 (P49) + +**유형**: 직접투자, 펀드, 신탁, 기타 + +**증권 정보 필드** (증권 전용): + +| 필드 | 설명 | +|------|------| +| 시작일 | 투자 시작일 | +| 만기일 | - | +| 수익율 | % | +| 투자금액 | 총 투자 금액 | +| 평가액 | 현재 평가 금액 | +| 비고 | - | + +#### 4.2.5 계좌 상세_보험_단체보험 (P50) + +**유형**: 단체보험 + +**보험 정보 필드**: + +| 필드 | 설명 | +|------|------| +| 시작일 | 계약 시작일 | +| 만기일 | 계약 만기일 | +| 이율 | - | +| 계약금액 | 총 계약 금액 | +| 해약환급금 | 현재 환급금 | +| 납입 주기 | 월납, 분기납, 반기납, 연납, 일시납 | +| 증권번호 | - | +| 가입 인원 | 명 | +| 1인당 보험료 | 금액 | +| 납입 주기당 보험료 | 금액 | +| 비고 | - | + +#### 4.2.6 계좌 상세_보험_화재보험 (P51) + +**유형**: 화재보험 + +**보험 정보 필드**: 단체보험과 유사. 추가 필드: + +| 필드 | 설명 | +|------|------| +| 보험 대상물 | 내용 | +| 대상물 주소 | 주소 | + +#### 4.2.7 계좌 상세_보험_CEO보험 (P52) + +**유형**: CEO 보험 + +**보험 정보 필드**: 단체보험과 유사. 추가 필드: + +| 필드 | 설명 | +|------|------| +| 피보험자 | 이름 (기본정보의 예금주 대신) | +| 수익자 | 이름 | + +--- + +### 4.3 카드 관리 (P53-54) + +**경로**: 기준정보 > 카드 관리 +**설명**: 연동 카드 및 수기 카드를 등록하고 관리합니다 + +#### 4.3.1 목록 화면 (P53) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 수기 카드 등록 버튼 | 클릭: 카드 상세_등록 화면으로 이동 | +| 2 | 카드사 셀렉트 박스 | 종류: 전체, 카드사명 목록. 디폴트: 전체 | +| 3 | 상태 셀렉트 박스 | 종류: 전체, 사용, 중지. 디폴트: 전체 | + +**상단 요약 카드**: 전체 카드 수, 총 한도, 사용금액, 잔여한도 + +**구분 표시**: 수기 카드(색상 표시), 연동 카드(색상 표시) + +**목록 테이블 컬럼**: No., 카드사, 카드번호, 카드명, 부서, 사용자, 사용현황(금액 + 사용률 %), 상태(사용/중지) + +#### 4.3.2 카드 상세 (P54) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 카드사 셀렉트 박스 | 종류: 카드사명 목록 | +| 2 | 종류 셀렉트 박스 | 종류: 신용카드, 체크카드 | +| 3 | 결제일 셀렉트 박스 | 종류: 매월 1일, 5일, 10일, 14일, 15일, 20일, 25일, 27일 | +| 4 | 상태 셀렉트 박스 | 종류: 사용, 중지 | +| 5 | 부서 셀렉트 박스 | 검색 가능. 종류: 부서 목록 | +| 6 | 사용자 셀렉트 박스 | 검색 가능. 종류: 선택 부서 사원 목록 | +| 7 | 품의서 작성 버튼 | 클릭: 문서 작성_품의서 화면으로 이동 (품의 사유에 현재 카드사, 카드번호, 카드명 표시) | + +> **선결제 신청 플로우**: 품의서 작성 → 결재선 승인 → 출금 처리 → 장표 등록 → 연동카드일 경우 잔여한도 반영 + +**기본 정보 필드**: 카드명, 종류, 카드사, 카드번호, 유효기간(년도/월), 카드 명의자, CSV + +**사용자 정보 필드**: 부서, 사용자, 직책 + +**한도 정보**: 총 한도, 사용 금액, 잔여한도, 결제일 + +**기타**: 메모, 상태, 선결제 신청, 품의서 작성 안내 + +--- + +### 4.4 달력 관리 (P55-58) + +**경로**: 기준정보 > 달력 관리 +**설명**: 달력을 관리합니다 + +#### 4.4.1 달력 뷰 (P55) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 달력 탭 | 종류: 달력, 목록. 디폴트: 달력 | +| 2 | 대량 등록 버튼 | 클릭: 대량 등록팝업 표시 | +| 3 | 달력 일정 등록 버튼 | 클릭: 달력 상세_등록팝업 표시 | +| 4 | 월별 달력 영역 | 클릭: 달력 상세 팝업 표시. 연간 달력 표시 (1~12월) | + +**상단 요약**: 등록 건수, 총 휴일 일수, 공휴일 건수 + +#### 4.4.2 목록 뷰 (P56) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 유형 필터 셀렉트 박스 | 종류: 전체, 공휴일, 임시휴일, 대체휴일, 세무일, 회사일정. 디폴트: 전체 | +| 2 | 달력 목록 | 클릭: 달력 상세 팝업 표시 | + +**목록 테이블 컬럼**: No., 유형, 일정명, 시작일, 종료일, 일수, 반복, 메모 + +#### 4.4.3 달력 상세 팝업 (P57) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 유형 셀렉트 박스 | 종류: 공휴일, 임시휴일, 대체휴일, 세무일, 회사일정 | +| 2 | 매년 반복 체크박스 | 클릭: 체크 설정/해제 토글. 디폴트: 해제. 체크 설정 시 매년 등록 | + +**입력 필드**: 일정명 *, 유형 *, 기간 *, 메모, 매년 반복 + +**버튼**: 수정, 삭제 + +#### 4.4.4 대량 등록 팝업 (P58) + +**입력 형식**: +- `YYYY-MM-DD 일정명` - 단일 일자 +- `YYYY-MM-DD~YYYY-MM-DD 일정명` - 기간 (일정) +- `YYYY-MM-DD 일정명 [유형]` - 유형 지정 (공휴일/세무일정/회사지정/대체휴일/임시휴일) + +**예시 입력**: +``` +2026-01-01 신정 +2026-01-28~2026-01-30 설날연휴 +2026-03-01 삼일절 +2026-05-05 어린이날 +2026-05-15 부처님오신날 +2026-06-06 현충일 +2026-08-15 광복절 +2026-10-03 개천절 +2026-10-05~2026-10-07 추석연휴 +2026-10-09 한글날 +2026-12-25 크리스마스 +``` + +**버튼**: {N건} 등록, 취소 + +--- + +### 4.5 이관 기초자료 (P59-65) + +**경로**: 기준정보 > 이관 기초자료 +**설명**: 이관 기초자료를 관리합니다 + +#### 4.5.1 메인 화면 (P59-60) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 이관 기초자료 탭 | 종류: 거래처, 거래 내역, 계좌 내역, 세금계산서 내역, 업로드 이력. 디폴트: 거래처 | +| 2 | 양식 다운로드 버튼 | 클릭: 등록된 양식 CSV 다운로드 | +| 3 | 파일 선택 버튼 | 클릭: 파일 탐색기 팝업. CSV 1개만 등록. 50MB 이하 | +| 4 | 파일 변환 버튼 | 클릭: CSV 데이터를 정보 등록 영역에 변환값 표시 | + +**파일 변환 후 상태** (P60): + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 파일명 버튼 | 클릭: 파일 다운로드 처리 | +| 2 | 전체/개별 체크박스 | 클릭: 체크 설정/해제 토글. 디폴트: 전체 설정 | +| 3 | 등록 버튼 | 파일변환 완료 & 체크 항목 있을 경우만 활성화. 클릭: 확인 Alert → 등록 처리 | +| 4 | 오류 하이라이트 | 오류 사항 색상 표시. 마우스 롤 오버 시 문구 표시 | + +#### 4.5.2 거래처 CSV 스펙 (P61) + +| 컬럼명 | 필수 | 타입 | 최대길이 | 입력형식/규칙 | 예시 | 검증 | +|--------|------|------|----------|--------------|------|------| +| 사업자등록번호 | O | 텍스트 | 12자 | NNN-NN-NNNNN (하이픈유무무관) | 123-45-67890 | 10자리숫자, 하이픈자동처리, 중복검사 | +| 거래처명 | O | 텍스트 | 100자 | 법인명 또는 상호명 | (주)한국건설 | 빈값불가, 앞뒤공백 자동 trim | +| 대표자명 | - | 텍스트 | 50자 | 대표이사 성명 | 김대표 | - | +| 거래처유형 | O | 텍스트 | 10자 | 매출처/매입처/기타 | 매출 | 3가지 값만 허용 (대소문자 무관) | +| 업태 | - | 텍스트 | 50자 | 사업자등록증 업태 | 건설업 | - | +| 업종 | - | 텍스트 | 50자 | 사업자등록증 종목 | 시설공사, 토목공사 | - | +| 우편번호 | - | 텍스트 | 5자 | 5자리 숫자 | 06134 | 숫자 5자리 | +| 주소 | - | 텍스트 | 200자 | 도로명 또는 지번 주소 | 서울시 강남구 테헤란로 123 | - | +| 상세주소 | - | 텍스트 | 100자 | 건물명, 층, 호 | 삼성빌딩 5층 501호 | - | +| 대표전화번호 | - | 텍스트 | 20자 | NNN-NNNN-NNNN | 02-1234-5678 | 하이픈 자동 포맷 | +| 팩스번호 | - | 텍스트 | 20자 | NNN-NNNN-NNNN | 02-1234-5679 | 하이픈 자동 포맷 | +| 담당자명 | - | 텍스트 | 50자 | 실무 담당자 이름 | 홍길동 | - | +| 담당자연락처 | - | 텍스트 | 20자 | 휴대폰 또는 직통번호 | 010-1234-5678 | 하이픈 자동 포맷 | +| 이메일 | - | 텍스트 | 100자 | name@domain.com | hk@hancon.co.kr | 이메일 형식 검증 | + +> 사업자등록번호 중복 상태일 경우 마우스 롤오버 시: "기존 거래처정보가 업데이트됩니다." 문구 표시 + +#### 4.5.3 거래 내역 CSV 스펙 (P62) + +| 컬럼명 | 필수 | 타입 | 최대길이 | 입력형식/규칙 | 예시 | 검증 | +|--------|------|------|----------|--------------|------|------| +| 거래유형 | O | 텍스트 | 10자 | 매출/매입 | 매출 | 2가지 값만 허용 | +| 일자 | O | 날짜 | - | YYYY-MM-DD (엑셀 날짜셀 가능) | 2025-12-31 | - | +| 거래처명 | O | 텍스트 | 100자 | 거래처에 등록된 이름과 매칭 | (주)한국건설 | 미등록 → 오류 | +| 사업자등록번호 | O | 텍스트 | 12자 | 등록된 거래처 자동 매칭 | 123-45-67890 | 미등록 → 오류 | +| 품목명N | - | 텍스트 | 100자 | 품목명 | 품목명 | - | +| 공급가액 | O | 숫자 | - | 양의정수 (콤마/원기호 자동 제거) | 10000000 | 0 이하 불가, 콤마 허용 | +| 부가세 | - | 숫자 | - | 미입력 시 공급가액 x 10% 자동계산 | 1000000 | 음수 불가, 빈값 = 자동계산 | +| 적요 | - | 텍스트 | 200자 | 거래 내용 설명 | 12월 기성금 청구 | - | + +> - 거래처 관리에 해당 거래처 등록 필수 +> - 부가세 미입력 시 자동계산 (공급가액 x 10%), 직접 입력 시 자동계산 무시 +> - 품목명~적요까지 추가 입력 가능 +> - 거래처 유효하지 않을 경우: "등록되지 않은 거래처입니다." 문구 표시 +> - 공급가액/부가세 양수 아닐 경우: "금액은 0보다 커야합니다." 문구 표시 + +#### 4.5.4 계좌 내역 CSV 스펙 (P63) + +| 컬럼명 | 필수 | 타입 | 최대길이 | 입력형식/규칙 | 예시 | 검증 | +|--------|------|------|----------|--------------|------|------| +| 거래일시 | O | 날짜 | - | YYYY-MM-DD HH:MM:SS | 2025-12-31 12:21:12 | - | +| 금융기관명 | O | 텍스트 | 30자 | 정식 은행명 (약어 자동 매칭) | 우리은행 | 등록된 은행 목록과 매칭 | +| 계좌번호 | O | 텍스트 | 30자 | 계좌번호 (하이픈 포함/제외 모두 가능) | 1005-301-123456 | 등록된 계좌와 매칭 | +| 적요 | - | 텍스트 | 200자 | 거래 내용 설명 | 기성금 입금 | - | +| 입금 | - | 숫자 | - | 양의정수 (콤마 자동 제거) | 5000000 | 0 이하 불가 | +| 출금 | - | 숫자 | - | 양의정수 (콤마 자동 제거) | 5000000 | 0 이하 불가 | +| 잔액 | O | 숫자 | - | 해당 거래 후 계좌 잔액 | 25000000 | 잔액 정합성 검증용 | +| 취급점 | - | 텍스트 | 100자 | 증미점 | 증미점 | - | +| 상대계좌예금주명 | - | 텍스트 | 20자 | 예금주명 | (주)한국건설 | - | + +> - 계좌 관리에 해당 계좌 등록 필수 +> - 금융기관명/계좌번호 유효하지 않을 경우: "등록되지 않은 계좌입니다." 문구 표시 +> - 잔액 정합성 유효하지 않을 경우: "잔액이 입출금 누적과 불일치합니다." 문구 표시 +> - 금액 양수 아닐 경우: "금액은 0보다 커야합니다." 문구 표시 + +#### 4.5.5 세금계산서 내역 CSV 스펙 (P64) + +| 컬럼명 | 필수 | 타입 | 최대길이 | 입력형식/규칙 | 예시 | 검증 | +|--------|------|------|----------|--------------|------|------| +| 발행유형 | O | 텍스트 | 10자 | 매출/매입 | 매출 | 2가지 값만 허용 | +| 작성일자 | O | 날짜 | - | 세금계산서 작성일 (발행일과 다를 수 있음) | 2025-12-31 | - | +| 발급일자 | O | 날짜 | - | YYYY-MM-DD | 2025-12-31 | - | +| 거래처 | O | 텍스트 | 100자 | 거래처에 등록된 이름과 매칭 | (주)한국건설 | 미등록 → 오류 | +| 사업자번호 | O | 텍스트 | 12자 | 등록된 거래처 자동 매칭 | 123-45-67890 | 미등록 → 오류 | +| 과세형태 | - | 텍스트 | 10자 | 과세/면세 | 과세 | 미입력 시 "과세" 기본값 | +| 품목 | - | 텍스트 | 200자 | 대표 품목/서비스명 | 시설공사 | - | +| 공급가액 | O | 숫자 | - | 양의정수 | 50000000 | 0 이하 불가 | +| 세액 | - | 숫자 | - | 미입력 시 공급가액 x 10% 자동 | 5000000 | - | +| 합계 | - | 숫자 | - | 합계 | 55000000 | 공급가액 + 세액 검증 | +| 영수청구 | - | 텍스트 | 10자 | 영수/청구 | 청구 | 미입력 시 "청구" 기본값 | + +> - 거래처 유효하지 않을 경우: "등록되지 않은 거래처입니다." 문구 표시 +> - 공급가액/부가세 양수 아닐 경우: "금액은 0보다 커야합니다." 문구 표시 + +#### 4.5.6 업로드 이력 (P65) + +| # | 요소 | 설명 | +|---|------|------| +| 1 | 이관 유형 필터 셀렉트 박스 | 종류: 전체, 거래처, 거래 내역, 계좌 내역, 세금계산서 내역. 디폴트: 전체 | +| 2 | 파일명 버튼 | 클릭: 파일 다운로드 처리 | + +**기간 필터**: 지난달, D-5월, D-4월, D-3월, D-2월, 이번달 + +**목록 테이블 컬럼**: No., 업로드 일시, 이관 유형, 전체(건수), 성공(건수), 파일명, 등록자 + +--- + +## 공통 UI 패턴 정리 + +### 기간 필터 유형 + +| 유형 | 사용 화면 | +|------|----------| +| 전전월/어제/오늘/전월/당월/당해년도 | 거래처 관리 | +| 1주일/1개월/3개월 + 날짜범위 | 세금계산서 발행 | +| 1분기/2분기/3분기/4분기 + 날짜범위 | 세금계산서 관리 | +| 지난달/D-5월~D-2월/이번달 | 계좌 입출금, 카드 사용, 상품권, 일반전표, 계좌관리, 카드관리, 이관기초자료 | + +### 데이터 구분 표시 (색상) + +모든 연동 가능한 화면에서 수기 데이터와 연동 데이터를 색상으로 구분: +- 수기 데이터 (한 색상) +- 연동 데이터 (다른 색상) + +### 엑셀 다운로드 + +다음 화면에서 엑셀 다운로드 기능 제공: +- 세금계산서 관리 +- 계좌 입출금 내역 +- 카드 사용 내역 + +### 분개 관련 + +분개 기능이 있는 화면: +- 세금계산서 관리 (매입) +- 카드 사용 내역 +- 일반 전표 입력 + +분개 공통 요소: +- 차변/대변 토글 +- 계정과목 셀렉트 박스 (검색 가능) +- 대차 균형 표시 +- 분개 수정/삭제 + +--- + +## 외부 연동 + +### 바로빌 API + +| 연동 항목 | 용도 | +|-----------|------| +| 전자세금계산서 발행 | 세금계산서 발행 화면에서 바로빌 API를 통한 발행 | +| 홈택스 세금계산서 조회 | 세금계산서 관리에서 홈택스 신고 내역 조회 | +| 계좌 연동 | 은행 빠른조회 서비스를 통한 실시간 계좌 조회 | +| 카드 연동 | 카드사 연동을 통한 카드 사용 내역 자동 수집 | +| 공인인증서 | 홈택스 세금계산서 연동용 인증서 등록 | + +### 사업자등록증 OCR + +거래처 등록 시 사업자등록증 파일 업로드 → OCR 자동 인식 → 거래처 정보 자동 입력 + +--- + +**최종 업데이트**: 2026-02-23 From 113bf826ee2dc5fd5ee8936fc5d57902ae9ccd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 23 Feb 2026 01:24:16 +0900 Subject: [PATCH 23/69] =?UTF-8?q?docs:=20[plans]=20SAM=20ERP=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B3=B4=EB=93=9C=20D1.4=20=EB=A7=88?= =?UTF-8?q?=ED=81=AC=EB=8B=A4=EC=9A=B4=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 167페이지 PDF에서 추출한 원본 텍스트를 구조화된 마크다운으로 변환 - 14개 섹션, 146개 화면, 4개 플로우차트 포함 - UI 목업 노이즈 제거 및 Description 내용 보존 - INDEX.md에 문서 등록 --- INDEX.md | 2 + plans/SAM_ERP_Storyboard_D1.4_260116.md | 3049 +++++++++++++++++++++++ 2 files changed, 3051 insertions(+) create mode 100644 plans/SAM_ERP_Storyboard_D1.4_260116.md diff --git a/INDEX.md b/INDEX.md index 2fe5f8b..d561312 100644 --- a/INDEX.md +++ b/INDEX.md @@ -150,7 +150,9 @@ docs/ | 문서 | 설명 | |------|------| +| [SAM_ERP_Storyboard_D1.4_260116.md](plans/SAM_ERP_Storyboard_D1.4_260116.md) | ERP 전체 스토리보드 D1.4 (167p PDF → 마크다운 변환, 14개 섹션 146개 화면) | | [SAM_ERP_회계관리_Storyboard_D1.6.md](plans/SAM_ERP_회계관리_Storyboard_D1.6.md) | ERP 회계관리 스토리보드 D1.6 (65p PDF → 마크다운 변환) | +| [SAM_General_Rule_Storyboard_D1.0.md](plans/SAM_General_Rule_Storyboard_D1.0.md) | General Rule 스토리보드 D1.0 (43p PDF → 마크다운 변환, UIUX 공통 규칙) | | [production-deployment-plan.md](plans/production-deployment-plan.md) | 운영 환경 배포 계획 (CI/CD, 서버 아키텍처) | ### features/ - 기능별 문서 diff --git a/plans/SAM_ERP_Storyboard_D1.4_260116.md b/plans/SAM_ERP_Storyboard_D1.4_260116.md new file mode 100644 index 0000000..b94396e --- /dev/null +++ b/plans/SAM_ERP_Storyboard_D1.4_260116.md @@ -0,0 +1,3049 @@ +# SAM ERP 스토리보드 + +> **버전**: D1.4 +> **날짜**: 2026.01.16 +> **프로젝트**: SAM_ERP +> **제작**: CODE-BRIDGE X + +--- + +## Document History + +| Date | Version | Main Contents | Detailed Contents | +|------|---------|---------------|-------------------| +| 2025.12.01 | D0.6 | 프론트 초안 | 프론트 PC - ERP - 인사관리&전자결재 작성 | +| 2025.12.01 | D0.7 | 프론트 작성 | 프론트 PC - ERP - 인사관리&전자결재 피드백 반영 | +| 2025.12.16 | D0.8 | 프론트 작성 | 프론트 PC - ERP - 회계&보고서 작성. 변경: 목록화면 기간 설정 추가, GPS 출퇴근 추가, 회계관리 추가, 카드/계좌관리 및 보고서 추가 등 | +| 2025.12.18 | D1.0 | 프론트 작성 | 프론트 PC - ERP - 구독&고객센터 작성. 변경: 게시판 추가, 악성채권 추심관리 상세 추가, 팝업관리/게시판관리/알림설정 추가, 계정정보/회사정보/구독관리/결제내역/고객센터 추가 | +| 2025.12.22 | D1.1 | 프론트 작성 | 97p 카드 내역 관리 수정 | +| 2025.12.31 | D1.2 | 프론트 작성 | 116-120p 알림 소리 설정 추가, 123p 접대비 현황 수정 | +| 2026.01.07 | D1.3 | 프론트 작성 | 150p 보고서->대시보드 이동, 31p/34-51p 화면 추가, 116p 누적 미수금/메모 추가, 142p 항목 설정 버튼 추가, 147-148p 화면 추가 | +| 2026.01.16 | D1.4 | 프론트 작성 | 31-32p 오늘의 이슈/현황판 수정, 36p 5-1번 추가, 38p 현황판/3번 추가, 99/100/104/106/108/123p 계정과목명 변경 | + +--- + +## Menu Structure + +**ERP 메뉴:** +- 운영 (로그인, 회사정보, 계정정보, 구독관리, 결제내역, 고객센터) +- 대시보드 +- 인사관리 (부서관리, 사원관리, 근태관리, 휴가관리) +- 전자결재 (기안함, 결재함, 참조함) +- 게시판 +- 회계관리 (거래처관리, 매출관리, 매입관리, 입금관리, 출금관리, 어음관리, 거래처원장, 일일 일보, 지출 예상 내역서, 미수금 현황, 악성채권 추심관리, 입출금 계좌 조회, 카드 내역 관리) +- 기준정보 (직급관리, 직책관리, 권한관리, 출퇴근관리, 팝업관리, 게시판관리, 알림설정, 계좌관리, 카드관리) +- 보고서 및 분석 + +**MES 메뉴:** +- 판매관리, 구매관리, 생산관리, 품질관리, 자재관리, 장비관리, 차량관리 +- 발주관리, 공사관리 + +--- + +--- + +# 공통 + + +## 페이지 5 - Interaction (제스처/마크) + +| Type | Description | Apply | +|------|-------------|-------| +| Tap | 일정영역을 사용자가 터치합니다. | Yes | +| Touch & Hold | 화면을 터치한 후 계속 누르고 있는 상태입니다. 해당영역 혹은 개체가 홀드 됩니다. | No | +| Double Tap | 일정영역을 두 번 터치합니다. 두 번 터치 시 액션이 실행됩니다. | No | +| Drag & Drop | 터치 혹은 홀드 상태에서 오브젝트를 이동하여 원하는 위치에 배치시킵니다. | Yes | +| Scroll Up | 아래에서 위로 누르는 동작을 유지하면서 이동하였다가 뗍니다. | Yes | +| Scroll Down | 위에서 아래로 누르는 동작을 유지하면서 이동하였다가 뗍니다. | Yes | +| Swipe Left | 오른쪽에서 왼쪽으로 누르는 동작을 유지하면서 이동하였다가 뗍니다. | Yes | +| Swipe Right | 왼쪽에서 오른쪽으로 누르는 동작을 유지하면서 이동하였다가 뗍니다. | Yes | +| Pinch Zoom out | 오브젝트 또는 화면을 축소합니다. | Yes | +| Pinch Zoom in | 오브젝트 혹은 화면을 확대합니다. | Yes | + + +## 페이지 6 - Responsive Web + +- **PC Web**: Contents + Footer +- **Mobile Web**: Contents + Footer + +**브레이크 포인트:** +- 모바일: < 640px (기본) +- 태블릿: 768px ~ 1023px (md) +- 데스크탑: 1024px+ (lg) +- 대형 모니터: 1280px+ (xl) + + +## 페이지 7 - Screen Template + +| 영역 | 설명 | +|------|------| +| A - Status bar | 안테나, 통화, 배터리 등 시스템 OS 관리 영역. 모든 페이지 상단에 존재 | +| B - Browser 영역 | 브라우저 기능 영역 | +| C - Title 영역 | 텍스트 또는 기능 버튼으로 구현됨. 텍스트는 기본 가운데 정렬 | +| D - Content 영역 | 컨텐츠 내용 표시. 컨텐츠 길이가 길어질 경우 스크롤 제공 | +| E - Browser bar 영역 | 브라우저 유틸 바 영역 | +| F - Keypad 영역 | 키보드 입력할 때 활성화. 모든 페이지 위에 덮어쓰기 구현 | + + +## 페이지 8 - 메시지 유형 + +| Type | Description | +|------|-------------| +| 알림 Alert | 사용자에게 상황을 알려주기 위한 팝업 | +| 확인 Alert | 사용자에게 확인이 필요할 경우 제공되는 팝업 | +| 토스트 메시지 | 단순 Notify (2~3)초 후 페이지 내에서 Fade out | + + +## 페이지 9 - GNB, LNB, 푸터 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 알림 버튼 + - 클릭: 알림 팝업 표시 +2. 개인 정보 버튼 + - 항목: 디폴트 이미지, 이름, 직급 + - 클릭: 마이페이지 팝업 표시 +3. 회사 로고 + - 회사정보 화면에서 등록한 로고 표시 + - 회사 변경 선택 시 해당 로고 변경 +4. 메뉴 영역 + - 메뉴 클릭: + 1) 하위 메뉴 있을 경우 + : 하위 메뉴 하단에 표시 + 2) 하위 메뉴 없을 경우 + : 해당 메뉴 화면으로 이동 + - 목록 길 경우 해당 영역 내 스크롤 처리 +5. MES 메뉴 영역 + - 영업관리, 판매관리, 구매관리 등 해당하는 + MES 메뉴 영역 표시 +6. 푸터 영역 + - 모든 화면 하단 공통 표시 +7. SAM AI 채팅 버튼 + - 클릭: SAM AI 채팅 팝업 표시 + + +## 페이지 10 - 알림 팝업 +**버전**: D1.3 | **경로**: `메인> 알림 팝업` + +**Description:** + +1. 알림 목록 + - 항목: 각 디폴트 썸네일, 종류(공지사항, 안 + 내), 제목/내용, 전송일시 표시 + - 클릭: 해당 상세 화면으로 이동 + - 최신순 10개까지 표시 +2. New 아이콘 + - 새 알림일 경우 New 아이콘 표시 + - 해당 알림 클릭 시 사라짐 +2-1. 붉은 점 아이콘 + - 새 알림이 있을 경우 표시 + - 해당 알림 모두 클릭 시 사라짐 + + +## 페이지 11 - 마이페이지 팝업 +**버전**: D1.3 | **경로**: `메인> 마이페이지 팝업` + +**Description:** + +1. 계정 아이디 (이메일) 표시 +2. 회사 셀렉트 박스 + - 종류: 회사명, 회사명, … (해당 계정이 생성 + 한 회사(테넌트) 목록 표시) + - 정렬: 등록순 + - 한 회사만 소유중일 경우에는 해당 영역 +3. 로그아웃 버튼 + - 클릭: “정말 로그아웃하시겠습니까?” 로그 + 아웃 확인 Alert 표시, 확인 버튼 클릭시 로 + 그아웃 처리 + + +## 페이지 12 - - +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 셀렉트 박스 + - 클릭: 하단에 종류 목록 표시 +2. 종류 목록 + - 목록 중 하나만 선택 가능 +3. 다중 선택 셀렉트 박스 + - 선택된 첫번째 항목명 + 추가 수 표시 + - 텍스트 영역 부족할 경우 ‘항목..+3’ 형태 + 로 표시 +4. 다중 선택 종류 목록 + - 목록 중 복수 선택 가능 + - 전체 선택 시 전체 선택/해제 토글 +5. 검색 영역 + - 검색어 입력 후 엔터 또는 검색 아이콘 클 + 릭 시 (5-1) 형태로 표시되며 (5-2) 영역에 + 검색 결과 표시 +5-3. 삭제 버튼 + - 클릭: 검색어 삭제 처리, 전체 종류 목록 + 표시 + + +## 페이지 13 - - +**버전**: D1.3 | **경로**: `-` + +**Description:** + +*. 상황에 따라 입력 필드 하단 또는 Alert에 + 가이드 메시지 표시 +1. 가이드 메시지 표시 위치 + - 긍정일 경우 녹색 + - (1-1) 부정일 경우 붉은색 + 가이드 메시지 표시 + + +## 페이지 14 - - +**버전**: D1.3 | **경로**: `-` + +**Description:** + +*. 공지 팝업 + - 대상: 전체, 설정 부서 + - 내용: 설정 기간동안 대상에게 팝업 표시 +1. 팝업 내용 영역 + - 이미지, 텍스트 +2. 1일간 이 창을 열지 않음 체크박스 + - 클릭: 체크 설정/해제 토글 + - 디폴트: 체크 해제 상태 + - 체크 설정 시 1일 동안 팝업 미표시 (자정 + 기준) + + +--- + +# 운영 (영업) + + +### Flowchart – 가입및 로그인 +**페이지**: 16 + +- 운영 로그인 +- 영업사원 + - [Yes] + - [No] +- 이메일로 URL 발송 +- 자료 확인 +- 사업자번호 +- 조회? +- 고객사 +- 가입 신청 완료 +- 사업자등록번호 입력 +- 관리자 +- 승인? +- 거절 알림 +- 약관 동의 +- SAM 로그인 +- 테넌트 추가? +- 테넌트 추가 알림 +- 비밀번호 설정 +- 사업자등록번호 입력 +- 실물 계약 서류 및 +- 필요 서류 전달 +- 계약금 50% +- 입금 확인 +- 매니저 + +## 페이지 17 - 로그인 +**버전**: D1.3 | **경로**: `운영 로그인` + +**Description:** + +1. 아이디 인풋박스 + - 테넌트 생성자일 경우 이메일, + 사용자일 경우 이메일 또는 아이디 + - (1-1) 상황별 가이드 메시지 +2. 비밀번호 인풋박스 + - 입력 시 마지막 글자 제외 후 마스킹 처리 + - (2-1) 상황별 가이드 메시지 +2-2. 열람 버튼 + - 클릭: 열람/숨김 토글 + - 디폴트: 숨김 상태 + - 열람 상태일 시 (2) 영역 마스킹 해제 처리 +3. 자동 로그인 체크박스 + - 클릭: 체크 설정/해제 토글 + - 체크 시 로그아웃 전까지 세션 유지 +4. 로그인 버튼 + - 클릭: 유효할 경우사업자등록번호 조회 + 화면으로 이동 + +| 상황 | 가이드 메시지 | +|------|------------| +| 필수 정보 미 입력 시 | 필수 정보입니다. | +| 4글자 미만 입력 시 | 이메일은 4자 이상 가능합 니다. 이메일 형식에 유효 | +| 하지 않을 경우 | 이메일 주소를 다시 확인해 주세요. | +| 필수 정보 미 입력 시 | 필수 정보입니다. | +| 8자 미만 입력 시 | 8자 이상으로 만들어주세 요. 8영문+숫자+특수문 | +| 자 조합이 아닐 경우 | 영문, 숫자, 특수문자를 모 두 조합하여 구성해주세요. 단, 다음의 특수기호는 보 안상 사용 불가합니다. ' ; - - < ( ) \ / | + + +## 페이지 18 - 사업자등록번호 조회 +**버전**: D1.3 | **경로**: `사업자등록번호 조회` + +**Description:** + +1. 제조 데모 + - 클릭: 제조 데모 화면으로 이동 +2. 시공 데모 + - 클릭: 시공 데모 화면으로 이동 +3. 사업자등록번호 인풋박스 + - 숫자만 가능, 10자리 +4. 다음 버튼 + - 클릭: + 1) 바로빌 사업자등록번호 조회 후 + 사용 불가 경우 + : “휴폐업 상태인 사업자입니다.” + 알림 Alert 표시 + 2) 바로빌 사업자등록번호 조회 후 + 사용 가능한 경우 + [1] 테넌트 등록된 사업자등록번호일 경우, + 테넌트 등록 전이어도 다른 영업사원이 + 등록했을 경우에는 사업자등록번호 + 사용 불가 (어드민에서는 해제 가능) + : “등록된 사업자등록번호 입니다.” + 알림 Alert 표시 + [2] 등록되지 않은 사업자등록번호일 경우 + : 회사정보 등록 화면으로 이동 + + +## 페이지 19 - 등록 +**버전**: D1.3 | **경로**: `사업자등록번호 조회> 회사정보` + +**Description:** + +*. 회사(테넌트) 상태 + - 신청: 신청 완료 상태 + - 승인: 계약 완료 및 계약금 50% 입금, + 이메일로 URL 발송 상태, + 최초 로그인 시 ERP만 표시 + - 거절: 영업사원이 직접 거절 내용 전달 + - 운영: 프로그램 설정 완료, 잔금 50% 입금 + 및 인도, 당월 말일까지는 무료, 익월부터 익 + 월 말일까지 사용하고 구독료 청구 + - 만료: 기간 종료, 종료일~3일동안 연장 결 + 제 없음, 만료와 연체 상태 구분?? 영업사원 + 에게 알림, 서비스에는 경고 배너, + - 해지 대기: 90일 대기?? 단계 필요?? + - 해지: 서비스 해지, 복구 불가?? + - 제재: 서비스 이용 불가, + - 탈퇴: 로그인 불가, 복구 불가?? +1. 회사 로고 이미지 영역 + - 디폴트 이미지 표시 + - 클릭: 파일탐색기 팝업 표시, 10MB 이하의 + PNG, JPEG, GIF 중 하나 선택 가능 +2. 우편번호 찾기 버튼 + - 클릭: 선정한 주소 팝업 표시 +3. 찾기 버튼 + - 클릭: 파일탐색기 팝업 표시, 이미지 또는 + 파일 하나 선택 가능 +4. 이전 버튼 + - 클릭: 사업자등록번호 조회화면으로 이동 +5. 가입 신청 버튼 + - 회사 로고만 선택, 나머지는 필수 정보 + - 클릭: 가입 신청 완료화면으로 이동 + + +## 페이지 20 - 등록> 가입 신청 완료 +**버전**: D1.3 | **경로**: `사업자등록번호 조회> 회사정보` + +**Description:** + +1. 가입 신청 완료 안내 문구 표시 +2. 가입 신청 취소 버튼 + - 클릭: “가입 신청 취소 시 등록한 모든 정 + 보가 삭제됩니다. 정말 가입 신청을 취소하 + 시겠습니까?” 확인 Alert 표시 + + +## 페이지 21 - 가입 신청 승인 성공 이메일 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 계정 활성화 버튼 + - 클릭: 약관 동의화면으로 이동 +2. 지원, 블로그 버튼 + - 클릭: 해당 운영 노션 링크로 이동 + + +## 페이지 22 - 약관 동의 +**버전**: D1.3 | **경로**: `약관 동의` + +**Description:** + +1. 약관 영역 + - 클릭: (1-1) 약관 내용 영역 열림/닫힘 토글 + - 디폴트: 닫힘 +2. 체크박스 + - 클릭: 체크설정/해제 토글 + - 디폴트: 체크 설정 해제 +3. 약관에 동의합니다 버튼 + - 모든 필수 약관 동의 시 버튼 활성화 + - 클릭: 비밀번호 설정화면으로 이동 +4. 약관에 동의합니다 버튼 + - 클릭: 모든 필수, 선택 약관에 동의 처리, + 비밀번호 설정화면으로 이동 + + +## 페이지 23 - 비밀번호 설정 +**버전**: D1.3 | **경로**: `약관 동의> 비밀번호 설정` + +**Description:** + +1. 계정 활성화 버튼 + - 클릭: 로그인 화면으로 이동 + + +--- + +# GPS 출퇴근 + + +## 페이지 25 - 출퇴근하기 +**버전**: D1.3 | **경로**: `마이페이지 팝업` + +**Description:** + +1. 출퇴근 버튼 + - GPS 출퇴근 사용 시에만 표시 + - 모바일일 경우에만 버튼 활성화 + - 클릭: 출퇴근하기 화면으로 이동 +2. 출퇴근 허용 반경 + - 기준 좌표로부터의 출퇴근 허용 반경을 원 + 형으로 표시 (기준정보> 출퇴근관리에서 설 +3. 현재 위치 버튼 + - 클릭: (3-1) 해당 현재 위치를 지도 중심으 + 로 표시 +4. [+] 버튼 + - 클릭: 지도 영역 확대 +5. 확대/축소 슬라이드바 + - 드래그&드랍또는 클릭: 지도 영역 확대/ +6. [-] 버튼 + - 클릭: 지도 영역 축소 +7. 개인 정보 영역 + - 항목: 프로필 이미지, 이름, 부서명, 직급명 +8. 현재 시:분:초 표시 + - HH:MM:SS +9. 출근하기 버튼 + - 클릭: + 1) 출근 위치 미설정 상태일 경우 + : “출근 위치를 설정해주세요.” + 알림 Alert 표시 + 2) 출근 위치 설정 상태일 경우 + [1] 출근 위치 기준 설정 반경 초과일 경우 + : “출근 가능 위치가 아닙니다. + 출근 위치를 확인해주세요.” + 알림 Alert 표시 + [2] GPS 출근 위치 기준 설정 반경 이내 + : 출근하기 화면으로 이동 + (출근 기록 저장) + 메인> 마이페이지 팝업> 출퇴근하 + + +## 페이지 26 - 출근하기 +**버전**: D1.3 | **경로**: `출퇴근하기` + +**Description:** + +1. 퇴근하기 버튼 + - 클릭: + 1) 퇴근 위치 미설정 상태일 경우 + : “퇴근 위치를 설정해주세요.” + 알림 Alert 표시 + 2) 퇴근 위치 설정 상태일 경우 + [1] 퇴근 위치 기준 설정 반경 초과일 경우 + : “퇴근 가능 위치가 아닙니다. + 퇴근 위치를 확인해주세요.” + 알림 Alert 표시 + [2] GPS 퇴근 위치 기준 설정 반경 이내 + : 퇴근하기 화면으로 이동 + (퇴근 기록 저장) +2. 출근 완료 아이콘 이미지 표시 +3. 출근 완료 정보 + - 항목: 출근 완료 문구, 시:분:초, 일자(요일) +4. 출근 좌표의 본사/현장명 표시 +5. 확인 버튼 + - 클릭: 대시보드로 이동 + 카메라> 출퇴근하기> 출근하기 + + +## 페이지 27 - 퇴근하기 +**버전**: D1.3 | **경로**: `출퇴근하기` + +**Description:** + +1. 퇴근 완료 아이콘 이미지 표시 +2. 퇴근 완료 정보 + - 항목: 퇴근 완료 문구, 시:분:초, 일자(요일) +3. 퇴근 좌표의 본사/현장명 표시 +4. 확인 버튼 + - 클릭: 대시보드로 이동 + 카메라> 출퇴근하기> 퇴근하기 + + +## 페이지 28 - 판매관리> 현장등록 +**버전**: D1.3 | **경로**: `현장등록` + +**Description:** + +1. 위치 정보 설정 + - 각 현장의 GPS 중심값으로 설정 + + +--- + +# 대시보드 + + +## 페이지 30 - 로그인 +**버전**: D1.3 | **경로**: `로그인` + +**Description:** + +1. 아이디 인풋박스 + - 테넌트 생성자일 경우 이메일, + 사용자일 경우 이메일 또는 아이디 + - (1-1) 상황별 가이드 메시지 +2. 비밀번호 인풋박스 + - 입력 시 마지막 글자 제외 후 마스킹 처리 + - (2-1) 상황별 가이드 메시지 +2-2. 열람 버튼 + - 클릭: 열람/숨김 토글 + - 디폴트: 숨김 상태 + - 열람 상태일 시 (2) 영역 마스킹 해제 처리 +3. 자동 로그인 체크박스 + - 클릭: 체크 설정/해제 토글 + - 체크 시 로그아웃 전까지 세션 유지 +4. 로그인 버튼 + - 클릭: 유효할 경우대시보드 화면으로 이 + +| 상황 | 가이드 메시지 | +|------|------------| +| 필수 정보 미 입력 시 | 필수 정보입니다. | +| 4글자 미만 입력 시 | 이메일은 4자 이상 가능합 니다. 이메일 형식에 유효 | +| 하지 않을 경우 | 이메일 주소를 다시 확인해 주세요. | +| 필수 정보 미 입력 시 | 필수 정보입니다. | +| 8자 미만 입력 시 | 8자 이상으로 만들어주세 요. 8영문+숫자+특수문 | +| 자 조합이 아닐 경우 | 영문, 숫자, 특수문자를 모 두 조합하여 구성해주세요. 단, 다음의 특수기호는 보 안상 사용 불가합니다. ' ; - - < ( ) \ / | + + +## 페이지 31 - Description +**버전**: D1.3 + +**Description:** + +1. 항목 설정 버튼 + - 클릭: 항목 설정_대시보드 팝업 표시 +2. 오늘의 이슈 영역 + - 당일 이슈 발생 시 알림 처리 + - 목록 길 경우 영역 내 페이지네이션 + - 알림 상태에서 즉시 승인/보류 처리 가능 + - 이슈 케이스 + - 신규 업체 등록 + - 결근 등 근태 이벤트 + - 재고 미달 알림 + - 채권 추심 등록, 상태 변경 + - 발주, 수주 등록 + - 지출결의서 등 전자결재 상신 + - 세금 신고 알림 등 +3. 필터 셀렉트 박스 + - 종류: 전체, 수주 성공, 추심 이슈, 적정 재 + 고, 결재 요청, 세금 신고, 신규 업체 등록, + 근태, 발주 완료 + - 디폴트: 전체 + - 숫자도 함께 표시 (예) 수주 성공 3) +4. 이슈 목록 + - 클릭: 해당 상세 화면으로 이동 + - 화면 가로 길이에 따라 4, 3, 2, 1열로 표시 +5. 승인/반려 버튼 + - 해당 건에 대해 즉시 승인/반려 처리 가능 +6. 일일 일보 정보 목록 + - 클릭: 일일 일보 화면으로 이동 +7. AI 리포트 + - 핵심 키워드 강조 표시 (빨간색: 경고, 주황: + 주의, 녹색: 긍정, 파랑: 양호) + + +## 페이지 32 - Description +**버전**: D1.3 + +**Description:** + +1. 현황 목록 + - 클릭: 해당 상세 화면으로 이동 +2. 경고 하이라이트 + - 경고 상태일 경우 해당 영역에 색상 하이 + 라이트로 표시 + + +## 페이지 33 - Description +**버전**: D1.3 + +**Description:** + +*. 가지급금 + - 법인카드(지출결의서) 미정리, 접대비 불인 + 정, 증빙미비, 업무관련성 소명 불가 (주말/ + 심야 카드 사용, 불인정 가맹점(귀금속, 상품 + 권, 유흥업소)), 대표자 개인 대여 등 + - 가지급금 인정이자 4.6% +1. 매입 정보 영역 + - 클릭: 당월 매입 상세 팝업 표시 +2. 카드 정보 영역 + - 클릭: 당월 카드 상세 팝업 표시 +3. 발행어음 정보 영역 + - 클릭: 당월 발행어음 상세 팝업 표시 +4. 총 예상 지출 합계 영역 + - 클릭: 당월 지출 예상 상세 팝업 표시 +5. 가지급금 영역 + - 클릭: 가지급금 상세 팝업 표시 +6. 법인세 예상 가중 영역 + - 클릭: 법인세 예상 가중 상세 팝업 표시 +7. 대표자 종합세 예상 가중 영역 + - 클릭: 대표자 종합소득세 예상 가중 상세 + 팝업 표시 + + +## 페이지 34 - Description +**버전**: D1.3 + +**Description:** + +1. 매출 영역 + - 클릭: 당해 매출 상세 팝업 표시 +2. 접대비 목록 + - 클릭: 접대비 상세 팝업 표시 +3. 복리후생비 목록 + - 클릭: 복리후생비 상세 팝업 표시 +4. 미수금 현황 목록 + - 클릭: 미수금 현황 화면으로 이동 +5. 미수금 상위 회사 목록 + - 1, 2위 표시 + + +## 페이지 35 - Description +**버전**: D1.3 + +**Description:** + +1. 채권추심 현황 목록 + - 클릭: 악성채권 추심관리 화면으로 이동 +2. 부가세 현황 목록 + - 클릭: 예상 납부세액 상세 팝업 표시 + + +## 페이지 36 - Description +**버전**: D1.3 + +**Description:** + +1. 이번주+좌우 화살표 버튼 + - 좌우 화살표 클릭: 이전월/다음월 스케줄 + 표시 +2. 캘린더 탭 + - 종류: 주, 월 + - 디폴트: 월 + - 주 선택 시 (1) 영역 ‘2025년 12월 2주’ 형 + 태로 표시 +3. 부서 필터 셀렉트 박스 + - 종류: 전체, 부서, 개인 + - 디폴트: 전체 +4. 업무 필터 셀렉트 박스_다중선택 + - 종류: 전체, 일정, 발주, 시공, 수주 성공, 추 + 심 이슈, 적정 재고, 결재 요청, 세금 신고, + 신규 업체 등록, 근태, 발주 완료 + - 디폴트: 전체 +5. 일정 영역 + - 캘린더항목: [부서/이름] 제목 + - 일정 목록 항목: 제목, 부서명, 기간, 시간 + - 클릭: + 1) 일정일 경우 + : 일정 상세 화면으로 이동 + 2) 스케줄일 경우 + : 해당 발주/시공 상세 화면으로 이동 +5-1. 일정 영역_이슈 + - 항목: [구분] 제목 + - 클릭: 해당 상세 화면으로 이동 +6. 일자 영역 + - 당일일 경우 외곽선 하이라이트 표시 + - 지난 일자일 경우 색상으로 구분 표시 + - 당일 선택 시 바탕색상 하이라이트 표시 + - 클릭: (8) 영역에 목록으로 스케줄 표시 +7. +N 버튼 + - 해당 일자에 스케줄이 2건 초과 시 초과건 + 에 대한 숫자 표시 +8. 일정 목록 영역 +9. 일정 등록 버튼 + - 클릭: 일정 상세 팝업 표시 + + +## 페이지 37 - Description +**버전**: D1.3 + +**Description:** + +1. 부서 셀렉트 박스_검색 + - 종류: 전체, 부서 목록 + - 디폴트: 전체 +2. 기간 영역 + - 클릭: 기간 설정 달력 팝업 표시 + - 디폴트: 해당 일자~해당 일자 +3. 체크박스버튼 + - 클릭: 체크 설정/해제 토글 + - 디폴트: 미설정 상태 + - 미설정 상태일 경우 종일 체크 + - 설정 상태일 경우 (4) 영역 활성화 +4. 시간 영역 + - 클릭: 시간 범위 피커 팝업 표시 + - 디폴트: 09:00~10:00 + + +## 페이지 38 - ON +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 전체 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 +2. 개별 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 +3. 현황판의 항목 정보는 오늘의 이슈 항목 정 + 보와 연동 처리 + + +## 페이지 39 - Description +**버전**: D1.3 + +**Description:** + +1. 접대비 한도 관리 셀렉트 박스 + - 종류: 연간, 반기, 분기, 월 + - 디폴트: 연간 + - 선택 값으로 총 한도를 분할해서 계산 + : 연간, 상반기, 하반기, 1~4분기, 1~12월 +1-1. 기업 구분 셀렉트 박스 + - 종류: 일반법인, 중소기업 + - 디폴트: 일반법인 +1-2. 기업 구분 방법 영역 + - 클릭: 확대/축소 토글 + - 디폴트: 축소 상태 +2. 복리후생비 한도 관리 셀렉트 박스 + - 종류: 연간, 반기, 분기, 월 + - 디폴트: 연간 + - 선택 값으로 총 한도를 분할해서 계산 + : 연간, 상반기, 하반기, 1~4분기, 1~12월 +3. 계산 방식 셀렉트 박스 + - 종류: 직원당 정액 금액 방식, 연봉 총액 X + 비율 방식 + - 디폴트: 직원당 정액 금액 방식 +4. 직원당 정액 금액/월 인풋박스 + - (3) 직원당 정액 금액 방식일 경우에만 표 +5. 비율 인풋박스 + - (3) 연봉 총액 X 비율 방식일 경우에만 표 +6. 연간 복리후생비 표시 + - 계산된 연간 복리후생비 표시 + + +## 페이지 40 - Description +**버전**: D1.3 + +**Description:** + +1. 매입유형 필터 셀렉트 박스_다중 선택 + - 종류: 전체, 원재료매입, 부재료매입, 상품 + 매입, 외주가공비, 소모품비, 수선비, 운반비, + 사무용품비, 임차료, 수도광열비, 통신비, 차 + 량유지비, 접대비, 보험료, 기타용역비, 미설 + 정 + - 디폴트: 전체 +2. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 41 - Description +**버전**: D1.3 + +**Description:** + +1. 사용자 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 사용자명 목록 + - 디폴트: 전체 +2. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 42 - Description +**버전**: D1.3 + +**Description:** + +1. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +2. 상태 필터 셀렉트 박스_검색 + - 종류: 전체, 보관중, 만기임박(만기일 7일 + 전), 만기 경과, 결제완료, 부도 + - 디폴트: 전체 +3. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 43 - Description +**버전**: D1.3 + +**Description:** + +1. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +2. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순 + - 디폴트: 최신순 + + +## 페이지 44 - Description +**버전**: D1.3 + +**Description:** + +1. 대상 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 대상 목록 + - 디폴트: 전체 +2. 구분 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 카드명, 계좌명 + - 디폴트: 전체 +3. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 +4. 가지급금 분류 기준 + - 법인카드(지출결의서) 미정리, 접대비 불인 + 정, 증빙미비, 업무관련성 소명 불가 (주말/ + 심야 카드 사용, 불인정 가맹점(귀금속, 상품 + 권, 유흥업소)), 대표자 개인 대여 등 + - AI 분류 + + +## 페이지 45 - Description +**버전**: D1.3 + +**Description:** + +1. 접대비 초과 금액 및 가지급금 인정이자가 + 정리된 법인세 영역 + - 접대비 초과 금액 및 가지급금 인정이자를 + 0으로 계산한 값 표시 +2. 차액 표시 +*. 계산 + - 과세표준 계산 = 당기순이익+손금불산입- + 손금산입 + - 접대비 한도 초과 금액은 손금불산입 + - 인정이자 전액 손금불산입 + + +## 페이지 46 - 4대 보험 -1,000,000원 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 가지급금 인정이자가 정리된 종합소득세 영 + 역 + - 가지급금 인정이자를 0으로 계산한 값 표 +2. 차액 표시 +*. 계산 + - 과세표준 = 근로소득 + 상여 + - 인정이자가 상여로 처리 + + +## 페이지 47 - Description +**버전**: D1.3 + +**Description:** + +1. 매출유형 필터 셀렉트 박스_다중 선택 + - 종류: 전체, 제품 매출, 상품 매출, 부품 매 + 출, 용역 매출, 공사 매출, 임대 수익, 기타 + 매출, 미설정 + - 디폴트: 전체 +2. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 48 - {1사분기} 접대비 초과 금액 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 당해년도 기준 접대비 계산 정보 목록 +2. 설정 기준 접대비 계산 정보 목록 +3. 사용자 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 사용자명 목록 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 49 - Description +**버전**: D1.3 + +**Description:** + +1. 접대비 기본한도 계산 + - 회사 정보> 기업 구분 정보 항목에서 결정 +2. 수입금액별 추가한도 계산 + - 당해 매출 기준 계산 +3. 접대비 현황 + - 연간 한도를 항목 설정 기준으로 구분 표 + + +## 페이지 50 - {1사분기} 복리후생비 초과 금액 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 당해년도 기준 복리후생비 계산 정보 목록 +2. 설정 기준 복리후생비 계산 정보 목록 +3. 사용자 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 사용자명 목록 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 51 - Description +**버전**: D1.3 + +**Description:** + +1. 복리후생비 계산 + - 연봉 총액 비율 방식일 경우(1-1) 형태로 + 표시 +2. 복리후생비 현황 + - 연간 한도를 항목 설정 기준으로 구분 표 + + +## 페이지 52 - Description +**버전**: D1.3 + +**Description:** + +1. 년도 셀렉트 박스 + - 종류: 2026년 + - 디폴트: 2026년 +2. 분기 셀렉트 박스 + - 종류: 전체, 1사분기, 2사분기, 3사분기, 4 + 사분기 + - 디폴트: 전체 + - (1), (2)에 해당하는 정보로 화면의 모든 정 +3. 구분 필터 셀렉트 박스 + - 종류: 전체, 매출, 매입 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 53 - Description +**버전**: D1.3 + + +## 페이지 54 - Description +**버전**: D1.3 + + +## 페이지 55 - Description +**버전**: D1.3 + + +--- + +# 인사관리 + + +## 페이지 57 - 부서관리 +**버전**: D1.3 | **경로**: `인사관리> 부서관리` + +**Description:** + +1. 전체 선택 체크박스 + - 클릭: 전체 선택설정/해제 토글 + - 디폴트: 설정 해제 상태 +2. 개별 선택 체크박스 + - 클릭: 개별 선택설정/해제 토글 + - 디폴트: 설정 해제 상태 +3. 추가 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: 선택한 부서의 하위 부서 일괄 생성 +4. 삭제 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: “선택한 부서 N개를 삭제하시겠습니 + 까?” 확인 Alert 표시, 확인 선택 시 삭제된 + 부서의 인원은 회사(기본) 인원으로 변경 +5. 축소 버튼 + - 클릭: (6) 확대 버튼으로 변경, 하위 부서 + 숨김 처리 +6. 확대 버튼 + - 클릭: (5) 축소 버튼으로 변경, 하위 부서 + 표시 처리 +7. 추가 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: 부서 추가 팝업 표시 +8. 수정 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: 부서 수정 팝업 표시 +9. 삭제 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: “{부서명} 부서를 삭제하시겠습니까?” + 확인 Alert 표시, 확인 선택 시 삭제된 부서 + 의 인원은 회사(기본) 인원으로 변경 + + +## 페이지 58 - 부서 추가 팝업, 부서 수정 팝업 +**버전**: D1.3 | **경로**: `인사관리> 부서관리` + +**Description:** + +1. 부서명 인풋박스 + - 기존 부서명 표시, 수정 가능 + + +## 페이지 59 - 사원관리 +**버전**: D1.3 | **경로**: `인사관리> 사원관리` + +**Description:** + +1. 기간 설정 영역 + - 입사일 기준 +1-1. 기간 설정 버튼 영역 + - 종류: 당해년도, 전전월, 전월, 당월, 어제, + 오늘 + - 클릭: 해당 기간이 (1) 영역에 설정되며 화 + 면 전체에 적용 처리 +2. CSV 일괄 등록 버튼 + - 클릭: CSV 일괄 등록화면으로 이동 +3. 사원 등록 버튼 + - 클릭: 사원 상세화면으로 이동 +4. 사용자 초대 버튼 + - 클릭: 사용자 초대 팝업표시 +5. 필터 셀렉트 박스 + - 종류: 전체, 사용자 아이디 보유, 사용자 아 + 이디 미보유, 재직, 휴직, 퇴직 + - 디폴트: 전체 +6. 정렬 셀렉트 박스 + - 종류: 직급순, 입사일 최신순, 입사일 등록 + 순, 부서 오름차순, 부서 내림차순, 이름 오 + 름차순, 이름 내림차순 +7. 수정 버튼 + - 클릭: 사원 상세 화면으로 이동 + + +## 페이지 60 - 사원 상세 +**버전**: D1.3 | **경로**: `인사관리> 사원관리> 사원 상세` + +**Description:** + +*. 사원 상세 + - 사원 정보와 사용자 정보를 함께 관리 + - 둘 중 하나만 있어도 등록 가능 +1. 항목 설정 버튼 + - 사원 상세 및 인사 정보 (선택 정보) 설정 + - 클릭: 항목 설정 팝업 표시 +2. 등록 버튼 + - 최초 등록 시에는 등록 버튼 + - 정보 입력 후에는삭제, 수정 버튼으로 표 +3. 사원 정보 영역 + - 사원 정보 등록 시 필수 정보 +4. 사용자 정보 영역 + - 사용자 정보 등록 시 필수 정보 +5. 권한 셀렉트 박스_검색 + - 종류: 권한관리의 목록 표시 +6. 상태 셀렉트 박스 + - 종류: 정상, 제재, 중지 + - 제재 상태인 경우 로그아웃 처리, 로그인 + 시 “제재중인 아이디입니다.” 팝업 + + +## 페이지 61 - 항목 설정 팝업 +**버전**: D1.3 | **경로**: `인사관리> 사원관리> 사원 상세>` + +**Description:** + +1. 전체 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 +2. 개별 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 + + +## 페이지 62 - 사원 상세 +**버전**: D1.3 | **경로**: `인사관리> 사원관리> 사원 상세` + +**Description:** + +1. 사원 상세 영역(선택 정보) + + +## 페이지 63 - 사원 상세 +**버전**: D1.3 | **경로**: `인사관리> 사원관리> 사원 상세` + +**Description:** + +*. 인사 정보 영역(선택 정보) +1. 고용 형태 셀렉트 박스 + - 종류: 정규직, 계약직, 파견직, 용역직, 시간 + 제 근로자 + - 디폴트: 정규직 +2. 직급 셀렉트 박스 + - 종류: 사원, 대리, 과장, 차장, 부장, 이사, + 상무, 전무, 부사장, 사장, 회장 (직급관리 화 + 면에서 설정) +3. 상태 셀렉트 박스 + - 종류: 재직, 병가휴직, 육아휴직, 개인사정 + 휴직, 무급휴직, 퇴사, 해고, 권고사직, 계약 + 만료, 정년퇴직 +4. 부서 셀렉트 박스_검색 + - 종류: 회사명, 부서명, 부서명(부서관리 화 + 면에서 설정) +5. 직책 셀렉트 박스 + - 종류: 없음, 팀장, 파트장, 실장, 부서장, 본 + 부장, 센터장, 매니저, 리더 (직책관리 화면 + 에서 설정) + - 부서별 직책 하나 선택 가능 +6. 추가 버튼 + - 부서, 직책 셀렉트 박스 영역 하단에 추가 +7. 삭제 버튼 + - 클릭: “{부서명} {직책명}을 삭제하시겠습니 + 까?” 확인 Alert 표시 +8. 출근 위치 셀렉트 박스_검색 + - 종류: 본사, 현장 목록 + - 출근 체크 시 해당 위치 좌표 기준으로 설 + 정된 {거리} m 내에서 가능 +9. 퇴근 위치 셀렉트 박스_검색 + - 종류: 본사, 현장 목록 + - 퇴근 체크 시 위치 좌표 기준으로 설정된 + {거리} m 내에서 가능 + + +## 페이지 64 - 팝업 +**버전**: D1.3 | **경로**: `인사관리> 사원관리> 사용자 초대` + +**Description:** + +*. 사용자 초대 프로세스 + - 초대 이메일로 발송→약관 동의 (아이디 + 를 이메일로 사용) →비밀번호 설정 →로 + 그인 + - 이메일 주소 기준 사원 정보가 있을 경우 + 에는 매핑하여 사용자 등록 처리, 없을 경우 + 에는 사용자에만 등록하고 나머지 사원 정 + 보는 직접 입력 필요 + - 사용자 아이디는 다른 테넌트와는 중복 + 가능 (사용자가 여러 테넌트에 등록 가능) +1. 초대할 이메일 주소 인풋박스 + - ‘,’로구분하여 여러 주소 입력 가능 +2. 권한 셀렉트 박스_검색 + - 종류: 권한관리의 목록 표시 +3. 초대 메시지 인풋박스 +4. 초대 버튼 + - 클릭: 사용자 초대 이메일 발송 + + +## 페이지 65 - 사용자 초대 메일 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 초대 메시지 내용 + - 등록한 초대 메시지 표시 +2. 회사 초대 수락 버튼 + - 클릭: 약관 동의 화면으로 이동 + + +## 페이지 66 - 록 +**버전**: D1.3 | **경로**: `인사관리> 사원관리> CSV 일괄 등` + +**Description:** + +1. 양식 다운로드 버튼 + - 클릭: 등록된 양식 CSV 다운로드 +2. 파일 선택 버튼 + - 클릭: 파일 탐색기 팝업, CSV 1개만 등록 +3. 파일 변환 버튼 + - 클릭: CSV 데이터를 (3-1) 정보 등록 영역 + 에 변환값 표시 +3-1. 정보 등록 영역 + - 범위: 사원 상세 화면의 전체 항목 + + +## 페이지 67 - Description +**버전**: D1.3 + +**Description:** + +1. 파일명 버튼 + - 클릭: 파일 다운로드 처리 +2. 전체/개별 체크박스 + - 클릭: 체크박스 설정/해제 토글 + - 디폴트: 전체 설정 상태 +3. 등록 버튼 + - 파일변환 완료& (2) 체크 설정 항목 있을 + 경우에만 버튼 활성화 + - 클릭: “{3}개의 정보를 정말 등록하시겠습 + 니까?” 확인 Alert 표시, 확인 클릭 시 (2) 체 + 크된 정보만 등록 처리, “정보 등록이 완료 + 되었습니다.” 알림 Alert 표시 + + +## 페이지 68 - 근태관리 +**버전**: D1.3 | **경로**: `인사관리> 근태관리` + +**Description:** + +*. 근태관리 + - 관리 권한이 있는 경우에만 모든 선택 가 + 능 + - 관리 권한이 없을 경우 본인의 정보만 선 + 택 및 편집 가능 + - 근태관리 자동 설정 시: 모든 사원이 정시 + 출퇴근한 것으로 기록, 예외사항일 경우 작 +1. 근태 등록 버튼 + - 클릭: 근태 정보 팝업 표시 +2. 사유 등록 버튼 + - 클릭: 사유 정보 팝업 표시 +3. 필터 셀렉트 박스 + - 종류: 전체, 정시 출근, 지각, 결근, 휴가, 출 + 장, 외근, 연장근무 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 직급순, 부서 오름차순, 부서 내림차 + 순, 이름 오름차순, 이름 내림차순 +5. 수정 버튼 + - 클릭: 근태 정보 팝업 표시 +6. 사유명 버튼 + - 클릭: 사유 정보 팝업 표시 + + +## 페이지 69 - 팝업, 사유 정보 팝업 +**버전**: D1.3 | **경로**: `인사관리> 근태관리> 근태 정보` + +**Description:** + +1. 사원 셀렉트 박스_검색&다중 선택 + - 항목: 부서명, 직급명, 사원명 표시 + - 종류: 모든 사원 목록 + - 권한이 없을 경우 자신만 선택 가능 +2. 기준일 설정 영역 + - 클릭: 달력 팝업 표시 + - 일자 다중 선택 가능 + - 디폴트: 당일 +3. 야간 연장 시간 설정 영역 + - 주당 연장 근로 시간에서주말 연장 시간 + 을 차감한 이내에만 설정 가능 +4. 주말 연장 시간 설정 영역 + - 주당 연장 근로 시간에서야간 연장 시간 + 을 차감한 이내에만 설정 가능 +5. 내용 인풋박스 + + +## 페이지 70 - 휴가관리 +**버전**: D1.3 | **경로**: `인사관리> 휴가관리` + +**Description:** + +1. 휴가관리 탭 + - 종류: 휴가 사용 현황, 휴가 부여 현황, 휴 + 가 신청 현황 + - 디폴트: 휴가 사용 현황 +2. 사원 셀렉트 박스_검색&다중 선택 + - 항목: 부서명, 직급명, 사원명 표시 + - 종류: 전체, 모든 사원 목록 + - 디폴트: 전체 +3. 정렬 셀렉트 박스 + - 종류: 직급순, 부서 오름차순, 부서 내림차 + 순, 이름 오름차순, 이름 내림차순 + + +## 페이지 71 - 휴가관리_휴가 부여 현황 +**버전**: D1.3 | **경로**: `인사관리> 휴가관리` + +**Description:** + +1. 부여 등록 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: 휴가 부여 팝업 표시 +2. 부여 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: 휴가 부여 팝업 표시 + + +## 페이지 72 - 휴가관리_휴가 신청 현황 +**버전**: D1.3 | **경로**: `인사관리> 휴가관리` + +**Description:** + +1. 휴가 신청 버튼 + - 클릭: 휴가 신청 팝업 표시 +2. 승인 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: “정말 {1}건을 승인하시겠습니까?” + 확인 Alert 표시, 확인 버튼 클릭 시 “승인이 + 완료되었습니다.” 알림 Alert 표시 +3. 거절 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: “정말 {1}건을 거절하시겠습니까?” + 확인 Alert 표시, 확인 버튼 클릭 시 “거절이 + 완료되었습니다.” 알림 Alert 표시 + + +## 페이지 73 - 팝업, 휴가 신청 팝업 +**버전**: D1.3 | **경로**: `인사관리> 휴가관리> 휴가 부여` + +**Description:** + +1. 유형 셀렉트 박스 + - 종류: 연차, 보상, 경조, 보건, 병가, 반차, + 회수 (차감) + - 디폴트: 연차 + - 회수 선택 시에는 설정 일시만큼 차감 +2. 사유 인풋박스 +3. 부여 버튼 + - 클릭: 휴가 부여 목록 최상단에 추가 +4. 휴가 잔여 일시 표시 +5. 유형 셀렉트 박스 + - 종류: 연차, 보상, 경조, 보건, 병가, 반차 + - 디폴트: 연차 + - 반차 선택 시에는 시간으로 변경 +6. 기간 설정 영역 + - 클릭: 기간 설정 팝업 표시 + - 디폴트: 당일 + - (5) 반차 선택 시 + 1) 기간→시간 으로 변경 + 2) 기간 설정 →시간 셀렉트 박스로 변경 + - 종류: 1시간~7시간 +7. 신청 버튼 + - 클릭: + 1) 잔여 일시 >= 신청 일시 (사용 가능) + : “휴가 신청 완료되었습니다.” + 알림 Alert 표시, + 휴가 신청 목록 최상단에 표시 + 2) 잔여 일시 < 신청 일시 (사용 불가능) + : “휴가 잔여 일시를 초과했습니다.” + 알림 Alert 표시 + + +--- + +# 전자결재 + + +## 페이지 75 - 기안함 +**버전**: D1.3 | **경로**: `전자결재> 기안함` + +**Description:** + +*. 문서 상태 + - 임시저장: 문서 작성 중 임시저장 상태 + - 진행: 상신 및 결재자 중 일부 승인된 상태 + - 완료: 모든 승인 완료 상태 + - 반려: 결재자중 한 명이 반려한 상태 +1. 문서 작성 버튼 + - 클릭: 문서 작성 화면으로 이동 +2. 상신 버튼 + - 클릭: + 1) 임시저장 상태일 경우 + : “정말 {1}건을 상신 처리하시겠습니까?” 확 + 인 Alert 표시 + 2) 임시저장 상태가 아닐 경우 + : “임시저장 상태만 상신이 가능합니다.” 알 + 림 Alert 표시 +3. 삭제 버튼 + - 클릭: + 1) 임시저장 상태일 경우 + : “정말 {1}건을 삭제 처리하시겠습니까?” 확 + 인 Alert 표시 + 2) 임시저장 상태가 아닐 경우 + : “임시저장 상태만 삭제가 가능합니다.” 알 + 림 Alert 표시 +4. 필터 셀렉트 박스 + - 종류: 전체, 임시저장, 진행, 완료, 반려 + - 디폴트: 전체 +5. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순 + - 디폴트: 최신순 +6. 수정 버튼 + - 클릭: + 1) 임시저장 상태일 경우: 문서 작성 화면으 + 로 이동 + 2) 임시저장 상태가 아닐 경우 + : 문서 상세 팝업 표시 + + +## 페이지 76 - 문서 작성_품의서 +**버전**: D1.3 | **경로**: `전자결재> 기안함> 문서 작성` + +**Description:** + +1. 상세 버튼 + - 클릭: 문서 상세 팝업표시 +2. 문서 유형 셀렉트 박스_검색 + - 종류: 품의서, 지출결의서, 지출 예상 내역 + 서 + - 디폴트: 품의서 + - 선택한 문서 유형의 화면으로 변경 표시 +3. 결재자 셀렉트 박스_검색&다중 선택 + - 항목: 부서명, 직책명, 사원명 표시 + - 종류: 전체, 모든 사원 목록 + - 디폴트: 전체 +4. 참조자 셀렉트 박스_검색&다중 선택 + - 항목: 부서명, 직책명, 사원명 표시 + - 종류: 전체, 모든 사원 목록 + - 디폴트: 전체 + + +## 페이지 77 - 문서 작성_품의서 +**버전**: D1.3 | **경로**: `전자결재> 기안함> 문서 작성` + +**Description:** + +1. 녹음 버튼 + - 클릭: 마이크 사용 가능할 경우에만 버튼 + 활성화 + - 클릭: 녹음 중지 버튼으로 변경, 인식된 음 + 성 내용을 텍스트로 변경하여 (1-1) 인풋박 + 스 영역에 표시 + + +## 페이지 78 - 문서 작성_지출결의서 +**버전**: D1.3 | **경로**: `전자결재> 기안함> 문서 작성` + +**Description:** + +1. 문서 유형 셀렉트 박스_검색 + - 종류: 품의서, 지출결의서, 지출 예상 내역 + 서 + - 디폴트: 품의서 + - 선택한 문서 유형의 화면으로 변경 표시 + + +## 페이지 79 - 문서 작성_지출결의서 +**버전**: D1.3 | **경로**: `전자결재> 기안함> 문서 작성` + +**Description:** + +1. 카드 셀렉트 박스 + - 종류: 등록된 카드 목록 + - 디폴트: 첫번째 카드 +2. 총 비용 정보 영역 + - 지출결의서 정보의 금액 합계 표시 + + +## 페이지 80 - 문서 작성_지출 예상 내역서 +**버전**: D1.3 | **경로**: `전자결재> 기안함> 문서 작성` + +**Description:** + +1. 문서 유형 셀렉트 박스_검색 + - 종류: 품의서, 지출결의서, 지출 예상 내역 + + +## 페이지 81 - 문서 작성_지출 예상 내역서 +**버전**: D1.3 | **경로**: `전자결재> 기안함> 문서 작성` + +**Description:** + +1. 지출 예상 내역서 목록 + - 체크 설정/해제 표시 + 1) 지출 예상 내역서 화면에서 왔을 경우 + : 설정했던 체크 상태 유지 + 2) 문서 작성 화면에서 설정했을 경우 + : 모든 체크 항목 설정된 상태 + + +## 페이지 82 - 결재함 +**버전**: D1.3 | **경로**: `전자결재> 결재함` + +**Description:** + +*. 상태 + - 진행 하위 상태 + - 예정: 결재 순번에 의한 대기 + - 결재요청: 결재 요청을 받은 상태 +1. 승인 버튼 + - 클릭: “정말 {1}건을 승인하시겠습니까?” + 확인 Alert 표시, 확인 버튼 클릭 시 “승인이 + 완료되었습니다.” 알림 Alert 표시 +2. 반려 버튼 + - 클릭: “정말 {1}건을 반려하시겠습니까?” + 확인 Alert 표시, 확인 버튼 클릭 시 “반려가 + 완료되었습니다.” 알림 Alert 표시 +3. 필터 셀렉트 박스 + - 종류: 전체, 결재 요청, 예정, 완료, 반려 + - 디폴트: 전체 + - 1/3 완료: 결재선 승인 진행도에 따라 표시 +4. 수정 버튼 + - 클릭: 문서 상세 팝업 표시 + + +## 페이지 83 - 업 +**버전**: D1.3 | **경로**: `전자결재> 결재함> 문서 상세 팝` + +**Description:** + +1. 복제 버튼 + - 클릭: 문서 작성 화면으로 이동 (새글) +2. 수정 버튼 + - 결재선 중에서는 누구나 수정 가능 + - 클릭: 해당 문서 작성 화면으로 이동 +3. 반려 버튼 + - 결재선 아닐 경우 숨김 + - 클릭: “정말 반려하시겠습니까?” 확인 + Alert 표시 +4. 승인 버튼 + - 결재선 아닐 경우 숨김 + - 클릭: “정말 승인하시겠습니까?” 확인 + Alert 표시 +5. 공유 버튼 + - 클릭: (5-1) 팝업 표시 +6. 결재선 영역 + - 승인/반려 시 해당 아이콘 표시 +7. 품의서 정보 표시 + + +## 페이지 84 - 업 +**버전**: D1.3 | **경로**: `전자결재> 결재함> 문서 상세 팝` + +**Description:** + +1. 지출결의서 정보 표시 + + +## 페이지 85 - 업 +**버전**: D1.3 | **경로**: `전자결재> 결재함> 문서 상세 팝` + +**Description:** + +1. 지출 예상 내역서 정보 표시 + 결 + 재 + 예상 지급일 + 항목 + 지출금액 + 거래처 + 계좌 + 2025-11-12 + 품의 사유… + 1,000,000 + 회사명 + 국민 1234 홍길동 + 2025-11-12 + 적요 내용 + 1,000,000 + 회사명 + 국민 1234 홍길동 + 2025-11-12 + 품의 사유… + 1,000,000 + 회사명 + 국민 1234 홍길동 + 2025-11-12 + 적요 내용 + 1,000,000 + 회사명 + 국민 1234 홍길동 + 2025/11 계 + 4,000,000 + 2025-12-12 + 거래처명 12월분 + 1,000,000 + 회사명 + 국민 1234 홍길동 + 2025-12-12 + 품의 사유… + 1,000,000 + 회사명 + 국민 1234 홍길동 + 2025/12 계 + 2,000,000 + 지출 합계 . + 6,000,000 + 계좌 잔액 . + 10,000,000 + 최종 차액 . + 4,000,000 + + +## 페이지 86 - 참조함 +**버전**: D1.3 | **경로**: `전자결재> 참조함` + +**Description:** + +1. 열람 버튼 + - 클릭: “정말 {1}건을 열람 처리하시겠습니 + 까?” 확인 Alert 표시, 확인 버튼 클릭 시 “열 + 람 처리가 완료되었습니다.” 알림 Alert 표시 +2. 미열람 버튼 + - 클릭: “정말 {1}건을 미열람 처리하시겠습 + 니까?” 확인 Alert 표시, 확인 버튼 클릭 시 + “미열람 처리가 완료되었습니다.” 알림 Alert + 표시 +3. 필터 셀렉트 박스 + - 종류: 전체, 열람, 미열람 + - 디폴트: 전체 + + +--- + +# 게시판 + + +## 페이지 88 - Description +**버전**: D1.3 + +**Description:** + +1. 게시글 등록 버튼 + - 클릭: 게시글 상세_등록 화면으로 이동 +2. 게시판 탭 + - 종류: 공지사항, 게시판명, …, 나의 게시글 + (기준정보> 게시판관리 화면에서 설정한 + 게시판 목록) + - 디폴트: 공지사항 + - 대상(전사, 부서, 팀)에 따라 소속에 맞는 + 게시판만 표시 +3. 게시글 정보 영역 + - 항목: 번호(상단 노출 아이콘 또는 번호), + 제목, 작성자, 등록일, 조회수 + - 클릭: 게시글 상세 화면으로 이동 + + +## 페이지 89 - 게시글 상세_등록 +**버전**: D1.3 | **경로**: `게시판> 게시글 상세` + +**Description:** + +1. 게시판 셀렉트 박스 + - 종류: 공지사항, 게시판명, … + (운영 관리_게시판 관리 화면에서 설정한 + 게시판 목록) + - 디폴트: 공지사항 + - 대상(전사, 부서, 팀)에 따라 소속에 맞는 + 게시판만 표시 +2. 상단 노출 라디오 버튼 + - 종류: 사용함, 사용안함 + - 디폴트: 사용안함 + - 사용함 설정 시 해당 게시판 화면에서 최 + 상단에 위치 + - 상단 노출 + 1) 최대 5개까지 설정 가능 + - 초과 시 “상단 노출은 5개까지 + 설정 가능합니다.” 알림 Alert 표시 + 2) 최신순 정렬 + 3) 일반 공지(상단 공지 사용안함) 보다 + 상단에 표시 +3. 댓글 라디오 버튼 + - 종류: 사용함, 사용안함 + - 디폴트: 사용함 + - 사용함 설정 시 게시글 상세 화면에서 댓 + 글 영역 표시 + + +## 페이지 90 - 게시글 상세 +**버전**: D1.3 | **경로**: `게시판> 게시글 상세` + +**Description:** + +1. 삭제/수정 버튼 영역 + - 본인이 작성한 글일 경우에만 표시 +2. 댓글 등록 영역 + - 게시글 작성 화면에서 댓글 사용함 설정 + 시에만 표시 + + +## 페이지 91 - 게시글 상세 +**버전**: D1.3 | **경로**: `게시판> 게시글 상세` + +**Description:** + +1. 댓글 정보 영역 + - 항목: 프로필 이미지, 부서명 이름 직책, 댓 + 글 내용, 등록일시 표시 +2. 수정 버튼 + - 본인이 작성한 댓글일 경우에만 표시 + - 클릭: (2-1) 인풋박스에 기존 댓글 내용 입 + 력 상태로 변경, 수정 가능 +3. 삭제 버튼 + - 본인이 작성한 댓글일 경우에만 표시 + - 클릭: “정말 삭제하시겠습니까?” 확인 + Alert 표시, 확인 클릭 시 삭제 처리 + + +--- + +# 회계관리 + + +### Flowchart – 회계 관리 +**페이지**: 93 + +- 거래처 선택 +- 매출 + - [Yes] + - [No] +- 거래처 선택 +- 입금 +- 매입 +- 출금 +- 추심 +- 매출 등록 +- 매입 등록 +- 세금계산서 발행 +- 세금계산서 수취 +- 입금 등록 +- 출금 등록 +- 장부/보고서 +- 전액 입금? +- 어음 수취? +- 전액 출금? +- 어음 발행? +- 거래처원장 +- 바로빌 API 자동 등록 예정 +- 미수금 현황 +- 악성 추심 +- 미지급 알림 +- 조회 +- 입출금 계좌 조회 +- 카드 내역 관리 +- 악성추심? +- 연체? +- 지출 예상 내역서 +- 일일 일보 + +## 페이지 94 - 거래처관리 +**버전**: D1.3 | **경로**: `회계관리> 거래처관리` + +**Description:** + +1. 삭제 버튼 + - 관리 권한이 없을 경우 숨김 + - 클릭: "선택한 거래처 N개를 삭제하시겠습 + 니까?" 확인 Alert 표시 +2. 구분 필터 셀렉트 박스 + - 종류: 전체, 매출, 매입, 매입매출 + - 디폴트: 전체 +3. 신용등급 필터 셀렉트 박스 + - 종류: 전체, AAA, AA, A, BBB, BB, B, CCC, + CC, C, D + - 디폴트: 전체 +4. 거래등급 필터 셀렉트 박스 + - 종류: 전체, A(우수), B(양호), C(보통), D(주 + 의), E(위험) + - 디폴트: 전체 +5. 악성채권 필터 셀렉트 박스 + - 종류: 전체, 악성채권, 정상 + - 디폴트: 전체 +6. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 거래처명 오름차순, + 거래처명 내림차순, 미수금 높은순, 미수금 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 95 - 세 +**버전**: D1.3 | **경로**: `회계관리> 거래처관리> 거래처 상` + +**Description:** + +*. 회계_거래처 정보 + - 판매, 구매 등의 거래처 등록 정보가 모두 + 표시 + - 회계에 필요한 거래처 정보도 추가로 표시 +1. 삭제 버튼 + - 클릭: "{거래처명}을 삭제하시겠습니까?" + 확인 Alert 표시, 확인 클릭 시 거래처관리 + 목록 화면으로 이동 +2. 수정 버튼 + - 클릭: 정말 수정하시겠습니까?” 확인 Alert + 표시, 확인 클릭 시 “수정이 완료되었습니다.” + 알림 Alert 표시 + + +## 페이지 96 - 세 +**버전**: D1.3 | **경로**: `회계관리> 거래처관리> 거래처 상` + +**Description:** + +1. 회사 로고 이미지 영역 + - 클릭: 파일탐색기 팝업 표시 + - 750 X 250px, 10MB 이하의 PNG, JPEG, + GIF 중 하나 선택 가능 +2. 매입 결제일 셀렉트 박스 + - 종류: 1일~31일, 말일 + - 디폴트: 10일 + - 거래처 유형이 '매입' 또는 '매입매출'일 경 + 우 표시 +3. 매출 결제일 셀렉트 박스 + - 종류: 1일~31일, 말일 + - 디폴트: 15일 + - 거래처 유형이 '매출' 또는 '매입매출'일 경 + 우 표시 + + +## 페이지 97 - Description +**버전**: D1.3 + +**Description:** + +1. 신용등급 인풋박스 + - 외부 신용평가 등급 표시 + - 예: AAA, AA, A, BBB, BB, B, CCC, CC, C, D +2. 거래등급 셀렉트 박스 + - 종류: A(우수), B(양호), C(보통), D(주의), + E(위험) + - 디폴트: A(우수) + - 자사 기준 거래처 평가 등급 +3. 미수금 표시 영역 + - 해당 거래처의 현재 미수금 합계 표시 + - 읽기 전용 +4. 연체 토글 + - ON: 연체 상태로 표시, 연체일수 표시 + - OFF: 정상 상태 + - 미수금 현황에서 연체 설정과 연동 + - (4-1) 연체 등록 이후부터 경과일 표시 +5. 미지급 표시 영역 + - 해당 거래처에 대한 미지급금 합계 표시 + - 읽기 전용 +6. 악성채권 토글 + - ON: 악성채권으로 등록, 악성채권 추심관 + 리 목록에 표시 + - OFF: 정상 상태 + - 디폴트: OFF + - 악성채권 추심관리에서 설정과 연동 + - (6-1) 악성채권의 상태 표시 + + +### Flowchart – 매출 / 입금 +**페이지**: 98 + +- 수주 확정 +- 직원 + - [Yes] + - [No] +- 경리 +- 세금계산서 발행 +- 매출 자동 등록 +- 거래명세서 발행 +- 결정권자 +- 미수금 현황 +- 수금일 변경? +- 바로빌 등자동화 예정 +- 입금 상세 등록 +- 연체? +- 연체 관리 +- 악성채권? +- 악성채권 관리 +- 입금 예정일 +- 입금 완료? +- 별도 매출 +- 매출 수동 등록 + +## 페이지 99 - 매출관리 +**버전**: D1.3 | **경로**: `회계관리> 매출관리` + +**Description:** + +*. 매출 등록 + - 수주 확정 시 매출 자동 등록 (삭제 불가) + - 별도 매출 시 매출 직접 등록 +1. 매출 등록 버튼 + - 클릭: 매출 상세_직접 등록화면으로 이동 + - 수주 연동 없는 별도 매출 발생 시 사용 +2. 매출유형명 셀렉트 박스_검색 + - 종류: 미설정, 제품 매출, 상품 매출, 부품 + 매출, 용역 매출, 공사 매출, 임대 수익, 기타 + 매출 + - 디폴트: 미설정 +2-1. 저장 버튼 + - 클릭: “N개의 매출유형을 {매출유형명}으 + 로 모두 변경하시겠습니까?” 확인 Alert 표 +3. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +4. 매출유형 필터 셀렉트 박스_다중 선택 + - 종류: 전체, 제품 매출, 상품 매출, 부품 매 + 출, 용역 매출, 공사 매출, 임대 수익, 기타 + 매출, 미설정 + - 디폴트: 전체 +5. 발행여부 필터 셀렉트 박스_다중 선택 + - 종류: 전체, 세금계산서 미발행, 거래명세 + 서 미발행 + - 디폴트: 전체 +6. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 +7. 매출번호 + - 형식: 로트번호 + 현장명 + 넘버링 조합 + - 견적서/수주 정보 참조하여 자동 생성 + + +## 페이지 100 - 매출 상세 +**버전**: D1.3 | **경로**: `회계관리> 매출관리> 매출 상세` + +**Description:** + +1. 매출유형명 셀렉트 박스_검색 + - 종류: 미설정, 제품 매출, 상품 매출, 부품 + 매출, 용역 매출, 공사 매출, 임대 수익, 기타 + 매출 + - 디폴트: 제품 매출 + + +## 페이지 101 - 매출 상세 +**버전**: D1.3 | **경로**: `회계관리> 매출관리> 매출 상세` + +**Description:** + +1. 세금계산서 발행 토글 버튼 + - 클릭: 미발행/발행완료 토글 + - 세금계산서 수동 발행 후 발행 상태로 변 +2. 거래명세서 발행 토글 버튼 + - 클릭: 미발행/발행완료 토글 + - (4) 거래명세서 발행하기 버튼 클릭 후 발 + 행 상태로 자동 변경 +3. 거래명세서 조회 버튼 + - 클릭: 문서 상세_거래명세서 팝업 표시 +4. 거래명세서 발행하기 버튼 + - 클릭: 해당 거래명세서를 거래처 이메일로 + 자동 발송 처리, “거래명세서가 + abc@email.com으로 발송되었습니다.” 알림 + Alert 표시 + + +## 페이지 102 - 매출 상세_직접 등록 +**버전**: D1.3 | **경로**: `회계관리> 매출관리> 매출 상세` + +**Description:** + +*. 매출 상세 + - 별도 매출 시 매출 직접 등록 (삭제 가능) + - 별도 매출: 용역 매출, 공사 매출, 임대 수 + 익, 기타 매출 +1. 매출번호 + - 자동 채번 + + +### Flowchart – 매입 / 출금 +**페이지**: 103 + +- 품의서 작성 +- 직원 + - [Yes] + - [No] +- 경리 +- 전자결재 상신 +- 결정권자 +- 지출예상내역서 +- 지급일 가능? +- 승인? +- 예상 지급일 수정 +- 반려 +- 완료 +- 지출결의서 작성 +- 전자결재 상신 +- 매입 상세 작성 +- 지출결의서? +- 출금 +- 출금 상세 등록 +- 승인? +- 매입 자동 등록 + +## 페이지 104 - 매입관리 +**버전**: D1.3 | **경로**: `회계관리> 매입관리` + +**Description:** + +*. 매입 등록 + - 지출예상내역서 승인 완료 시 매입 자동 + 등록 (삭제 불가) +1. 매입유형명 셀렉트 박스_검색 + - 종류: 미설정, 원재료매입, 부재료매입, 상 + 품매입, 외주가공비, 소모품비, 수선비, 운반 + 비, 사무용품비, 임차료, 수도광열비, 통신비, + 차량유지비, 접대비, 보험료, 기타용역비 + - 디폴트: 미설정 +1-1. 저장 버튼 + - 클릭: “N개의 매입유형을 {매입유형명}으 + 로 모두 변경하시겠습니까?” 확인 Alert 표 +2. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +3. 매입유형 필터 셀렉트 박스_다중 선택 + - 종류: 전체, 원재료매입, 부재료매입, 상품 + 매입, 외주가공비, 소모품비, 수선비, 운반비, + 사무용품비, 임차료, 수도광열비, 통신비, 차 + 량유지비, 접대비, 보험료, 기타용역비, 미설 + 정 + - 디폴트: 전체 +4. 발행여부 필터 셀렉트 박스_다중 선택 + - 종류: 전체, 세금계산서 미수취 + - 디폴트: 전체 +5. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 +6. 매입번호 + - 형식: 품의서/지출결의서 문서번호 + 넘버 + 링 조합 + - 품의서/지출결의서 정보 참조하여 자동 생 + + +## 페이지 105 - 매입 상세 +**버전**: D1.3 | **경로**: `회계관리> 매입관리> 매입 상세` + +**Description:** + +1. 근거 문서명 + - 품의서 또는 지출결의서 +2. 열람 버튼 + - 클릭: 해당 문서 상세 팝업 표시 +3. 예상 비용 표시 + - 품의서/지출결의서 예상/총 비용 표시 +4. 매입번호 + - 형식: 품의서/지출결의서 문서번호 + 넘버 + 링 조합 + - 품의서/지출결의서 정보 참조하여 자동 생 +5. 출금계좌 셀렉트 박스 + - 종류: 등록한 계좌 정보 목록 + - 항목: 은행명+ 계좌 번호 마지막 4자리 + +6. 거래처 셀렉트 박스_검색 + - 종류: 거래처 목록 +7. 매입 유형 셀렉트 박스 + - 종류: 원재료매입, 부재료매입, 상품매입, + 외주가공비, 소모품비, 수선비, 운반비, 사무 + 용품비, 임차료, 수도광열비, 통신비, 차량유 + 지비, 접대비, 보험료, 기타용역비, 미설정 +8. 세금계산서 수취 토글 버튼 + - 클릭: 미수취/수취완료 토글 + - 세금계산서 수취 완료 후 완료 상태로 변 + + +## 페이지 106 - 입금관리 +**버전**: D1.3 | **경로**: `회계관리> 입금관리` + +**Description:** + +*. 입금 관리 + - 기준 정보> 계좌 관리에 등록된 계좌의 자 + 동 입금 내역 수집 +1. 입금유형명 셀렉트 박스_검색 + - 종류: 미설정, 매출대금, 선수금, 가수금, 임 + 대수익, 이자수익, 보증금 반환, 차입금, 자본 + 금, 부가세 환급, 기타 + - 디폴트: 미설정 +1-1. 저장 버튼 + - 클릭: “N개의 입금 유형을 {입금유형명}으 + 로 모두 변경하시겠습니까?” 확인 Alert 표 +2. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +3. 입금유형 필터 셀렉트 박스_검색&다중 선 + 택 + - 종류: 전체, 매출대금, 선수금, 가수금, 임대 + 수익, 이자수익, 보증금 반환, 차입금, 자본금, + 부가세 환급, 기타, 미설정 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 +5. 새로고침 버튼 + - 클릭: 은행 계좌 입금 내역 최신 데이터 조 + 회 + - 바로빌 API 연동 시 실시간 조회 + + +## 페이지 107 - 입금 상세 +**버전**: D1.3 | **경로**: `회계관리> 입금관리> 입금 상세` + +**Description:** + +1. 거래처 셀렉트 박스_검색 + - 종류: 거래처 목록 +2. 입금 유형 셀렉트 박스 + - 종류: 매출대금, 선수금, 가수금, 임대수익, + 이자수익, 보증금 반환, 차입금, 자본금, 부가 + 세 환급, 기타, 미설정 + + +## 페이지 108 - 출금관리 +**버전**: D1.3 | **경로**: `회계관리> 출금관리` + +**Description:** + +*. 출금 관리 + - 기준 정보> 계좌 관리에 등록된 계좌의 자 + 동 출금 내역 수집 +1. 출금유형명 셀렉트 박스_검색 + - 종류: 미설정, 매입대금, 선급금, 가지급금, + 임대료, 이자비용, 보증금 지급, 차입금 상환, + 배당금 지급, 부가세 납부, 급여, 4대보험, 세 + 금, 공과금, 경비, 기타 + - 디폴트: 미설정 +1-1. 저장 버튼 + - 클릭: “N개의 출금 유형을 {출금유형명}으 + 로 모두 변경하시겠습니까?” 확인 Alert 표 +2. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +3. 출금유형 필터 셀렉트 박스_검색&다중 선 + 택 + - 종류: 전체, 매입대금, 선급금, 가지급금, 임 + 대료, 이자비용, 보증금 지급, 차입금 상환, + 배당금 지급, 부가세 납부, 급여, 4대보험, 세 + 금, 공과금, 경비, 기타, 미설정 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 109 - 출금 상세 +**버전**: D1.3 | **경로**: `회계관리> 출금관리> 출금 상세` + +**Description:** + +1. 거래처 셀렉트 박스_검색 + - 종류: 거래처 목록 +2. 출금 유형 셀렉트 박스 + - 종류: 매입대금, 선급금, 가지급금, 임대료, + 이자비용, 보증금 지급, 차입금 상환, 배당금 + 지급, 부가세 납부, 급여, 4대보험, 세금, 공 + 과금, 경비, 기타, 미설정 + + +## 페이지 110 - 어음관리 +**버전**: D1.3 | **경로**: `회계관리> 어음관리` + +**Description:** + +1. 어음 등록 버튼 + - 클릭: 어음 상세 화면으로 이동 +2. 상태 셀렉트 박스_검색 + - (3) 구분 종류에 따라 종류 표시 + - 발행 어음 종류: 보관중, 만기임박(만기일 + 7일 전), 만기 경과, 결제완료, 부도 + - 수취 어음 종류: 보관중, 만기임박(만기일 + 7일 전), 추심의뢰, 추심완료, 추심중, 부도 +2-1. 저장 버튼 + - 클릭: “N개의 상태를 {상태명}으로 모두 변 + 경하시겠습니까?” 확인 Alert 표시 +3. 구분 라디오 버튼 + - 종류: 수취, 발행 + - 디폴트: 수취 +4. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +5. 상태 필터 셀렉트 박스_검색 + - (3) 구분 종류에 따라 종류 표시 + - 발행 어음 종류: 전체, 보관중, 만기임박(만 + 기일 7일 전), 만기 경과, 결제완료, 부도 + - 수취 어음 종류: 전체, 보관중, 만기임박(만 + 기일 7일 전), 추심의뢰, 추심완료, 추심중, +6. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 + + +## 페이지 111 - 어음 상세 +**버전**: D1.3 | **경로**: `회계관리> 어음관리> 어음 상세` + +**Description:** + +*. 수취 어음 + - 거래처원장 상세, 일일 일보, 미수금 현황 + 에 반영 + - 미수금에 대한 약정으로만 표시 + - 추심완료되어 입금 시에만 회계에 반영 +*. 발행 어음 + - 지출예상내역서에 반영 + - 지출에 대한 약정으로만 표시 + - 결제완료되어 출금 시에만 회계에 반영 +1. 어음번호 + - 실물어음번호 또는 금융결제원에서 부여 + 하는 전자어음번호 등록 +2. 구분 셀렉트 박스 + - 종류: 수취, 발행 + - 디폴트: 수취 +3. 상태 셀렉트 박스 + - (2) 설정에 따른 종류 표시 + - 발행 어음 종류: 보관중, 만기임박(만기일 + 7일 전), 만기 경과, 결제완료, 부도 + - 수취 어음 종류: 보관중, 만기임박(만기일 + 7일 전), 추심의뢰, 추심완료, 추심중, 부도 +4. 차수 관리 + - 총 금액에 대한 차수로 상환 계획 작성 + + +## 페이지 112 - 거래처원장 +**버전**: D1.3 | **경로**: `회계관리> 거래처원장` + +**Description:** + +1. 거래처원장 목록 + - 거래처별 기간별 합계 금액 표시 + - 클릭: 거래처원장 상세 화면으로 이동 + + +## 페이지 113 - 장 상세 +**버전**: D1.3 | **경로**: `회계관리> 거래처원장> 거래처원` + +**Description:** + +1. 이월잔액 표시 +2. 수취 어음 정보 표시 + - 클릭: 해당 어음 상세 화면으로 이동 +3. 거래명세서 정보 표시 + - 클릭: 문서 상세_거래명세서 팝업 표시 +4. 거래명세서 하위 전체 품목별 판매금액 표 + 시 + - 세금계산서 미발행 상태일 경우 붉은색 하 + 이라이트 표시 +5. 누계 금액 표시 + + +## 페이지 114 - 일일 일보 +**버전**: D1.3 | **경로**: `회계관리> 일일 일보` + +**Description:** + +1. (수취어음) + 거래처명 + 어음번호 표시 +2. 당일의 외국환 및 현금성 자산 내역 표시 + - 전체 계좌 내역 표시 + + +## 페이지 115 - 지출 예상 내역서 +**버전**: D1.3 | **경로**: `회계관리> 지출 예상 내역서` + +**Description:** + +*. 지출 예상 내역서 + - 카드 및 승인/반려가 확정된 목록은 삭제 +1. 예상 지급일 변경 버튼 + - 클릭: 예상 지급일 변경 팝업 표시 +2. 전자결재 버튼 + - 클릭: 문서 작성_지출 예상 내역서 화면으 + 로 이동 +3. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순 + - 디폴트: 최신순 +5. 예상 지급일 + - 매입 거래처 등록 시 자동 입력 + - 그 외 거래처는 입력값 반영 +6. 품의서/지출결의서/발행어음 목록 + - 클릭: 해당 문서/어음 상세 화면으로 이동 +7. 거래처 월 지출 목록 + - 클릭: 해당 거래처원장 상세화면으로 이 + + +--- + +# 기준정보 + + +## 페이지 117 - 미수금 현황 +**버전**: D1.3 | **경로**: `회계관리> 미수금 현황` + +**Description:** + +1. 수취 어음 등록 시 표시 + - 회계에는 미반영 +2. 메모 인풋박스 + - 입력 후 저장 버튼으로 저장 +3. 연체 토글 + - ON: 연체 상태로 표시, 연체일수 시작 + - OFF: 정상 상태 + - 거래처 상세에서 연체 설정과 연동 +4. 확대 버튼 + - 클릭: 확대/축소 토글 + - 디폴트: 축소 상태 + + +## 페이지 118 - 악성채권 추심관리 +**버전**: D1.3 | **경로**: `회계관리> 악성채권 추심관리` + +**Description:** + +1. 거래처 필터 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 거래처 목록 + - 디폴트: 전체 +2. 상태 셀렉트 박스 + - 종류: 전체, 추심중, 법적조치, 회수완료, 대 + 손처리 + - 디폴트: 전체 + - 추심중: 악성채권 설정 시 디폴트 상태 +3. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순 + - 디폴트: 최신순 + + +## 페이지 119 - 성채권 추심관리 상세 +**버전**: D1.3 | **경로**: `회계관리> 악성채권 추심관리> 악` + +**Description:** + +1. 추심 대상 업체 정보 표시 + + +## 페이지 120 - 악성채권 추심관리 상세 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 찾기 버튼 + - 클릭: 파일탐색기 팝업 표시 + abc.pdf + + +## 페이지 121 - 악성채권 추심관리 상세 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 상태 셀렉트 박스 + - 종류: 추심중, 법적조치, 회수완료, 대손처 +2. 본사 담당자 셀렉트 박스_검색 + - 항목: 부서명 이름 직급명 연락처 + - 종류: 사원 목록 +3. 수취 어음 현황 버튼 + - 클릭: 어음관리 화면으로 이동 (해당 거래 + 처의 수취 어음으로 필터링된 상태) +4. 거래처 미수금 현황 버튼 + - 클릭: 미수금 현황 화면으로 이동 (해당 거 + 래처에 하이라이트 표시) + abc.pdf + abc.pdf + 거래처 미수금 현황 + + +## 페이지 122 - 입출금 계좌 조회 +**버전**: D1.3 | **경로**: `회계관리> 입출금 계좌 조회` + +**Description:** + +*. 입출금 계좌 조회 + - 기준 정보> 계좌 관리에 등록된 계좌의 자 + 동 입출금 내역 수집 +1. 새로고침 버튼 + - 클릭: 은행 계좌 입출금 내역 최신 데이터 + 조회 + - 바로빌 API 연동 시 실시간 조회 +2. 구분 필터 셀렉트 박스 + - 종류: 전체, 출금, 입금 + - 디폴트: 전체 +3. 계정과목 필터 셀렉트 박스_검색&다중 선 + 택 + - (2) 선택에 따른 계정과목 목록 표시 + - 입금 종류: 전체, 매출대금, 선수금, 가수금, + 임대수익, 이자수익, 보증금 반환, 차입금, 자 + 본금, 부가세 환급, 기타, 미설정 + - 출금 종류: 전체, 매입대금, 선급금, 가지급 + 금, 임대료, 이자비용, 보증금 지급, 차입금 + 상환, 배당금 지급, 부가세 납부, 급여, 4대보 + 험, 세금, 공과금, 경비, 기타, 미설정 + - 디폴트: 전체 +4. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액순 + - 디폴트: 최신순 +5. 수정 버튼 + - 클릭: 해당 입금/출금 상세 화면으로 이동 + + +## 페이지 123 - 카드 내역 관리 +**버전**: D1.3 | **경로**: `회계관리> 카드 내역 관리` + +**Description:** + +*. 카드 내역 관리 + - 기준 정보> 카드 관리에 등록된 카드의 자 + 동 사용 내역 수집 + - 사용자의 경우 본인의 내역 조회 및 사용 + 유형/적요 작성 가능 +1. 카드명 필터 셀렉트 박스 + - 종류: 전체, 카드명 목록 + - 디폴트: 전체 +2. 정렬 셀렉트 박스 + - 종류: 최신순, 등록순, 금액 높은순, 금액 + 낮은순 + - 디폴트: 최신순 +3. 사용유형 셀렉트 박스 + - 종류: 미설정, 복리후생비, 접대비, 여비교 + 통비, 차량유지비, 소모품비, 운반비, 통신비, + 도서인쇄비, 교육훈련비, 보험료, 광고선전 + 비, 회비, 지급수수료, 세금과공과, 수선비, + 임차료, 잡비 + - 디폴트: 미설정 + + +## 페이지 124 - 내역 상세 +**버전**: D1.3 | **경로**: `회계관리> 카드 내역 관리> 카드` + +**Description:** + +1. 적요 인풋박스 +2. 사용유형 셀렉트 박스 + - 종류: 미설정, 복리후생비, 접대비, 여비교 + 통비, 차량유지비, 소모품비, 운반비, 통신비, + 도서인쇄비, 교육훈련비, 보험료, 광고선전 + 비, 회비, 지급수수료, 세금과공과, 수선비, + 임차료, 잡비 + + +--- + +# 보고서 및 분석 + + +## 페이지 126 - 직급관리 +**버전**: D1.3 | **경로**: `기준정보> 직급관리` + +**Description:** + +1. 직급 인풋박스 +2. 추가 버튼 + - 클릭: (2-1) 직급목록 최하단에 표시 +2-1. 직급 + - 디폴트: 사원, 대리, 과장, 차장, 부장, 이사, + 상무, 전무, 부사장, 사장, 회장 +3. 순서 변경 버튼 + - 드래그&드랍: 해당 위치로 순서 변경 +4. 수정 버튼 + - 클릭: 직급 수정 팝업 표시 +5. 삭제 버튼 + - 클릭: + 1) 해당 직급으로 사원 설정된 경우 + : “{직급명}을 사용하고 있는 사원이 + 있습니다. 모두 변경 후 삭제가 + 가능합니다.” 알림 Alert 표시 + 2) 해당 직급으로 사원 미설정된 경우 + : “정말 삭제하시겠습니까?” 확인 Alert + 표시, 확인 클릭시 “삭제가 + 완료되었습니다.” 알림 Alert 표시 + + +## 페이지 127 - 직책관리 +**버전**: D1.3 | **경로**: `기준정보> 직책관리` + +**Description:** + +1. 직책 인풋박스 +2. 추가 버튼 + - 클릭: (2-1) 직책목록 최하단에 표시 +2-1. 직책 + - 디폴트: 없음(기본), 팀장, 파트장, 실장, 부 + 서장, 본부장, 센터장, 매니저, 리더 +3. 순서 변경 버튼 + - 드래그&드랍: 해당 위치로 순서 변경 +4. 수정 버튼 + - 클릭: 직책 수정 팝업 표시 +5. 삭제 버튼 + - 클릭: + 1) 해당 직책으로 사원 설정된 경우 + : “{직책명}을 사용하고 있는 사원이 + 있습니다. 모두 변경 후 삭제가 + 가능합니다.” 알림 Alert 표시 + 2) 해당 직책으로 사원 미설정된 경우 + : “정말 삭제하시겠습니까?” 확인 Alert + 표시, 확인 클릭시 “삭제가 + 완료되었습니다.” 알림 Alert 표시 + + +## 페이지 128 - 직급 수정 팝업, 직책 수정 팝업 +**버전**: D1.3 | **경로**: `기준정보> 직급관리, 직책관리>` + +**Description:** + +1. 직급명 인풋박스 + - 기존 직급명 표시, 수정 가능 +2. 직책명 인풋박스 + - 기존 직책명 표시, 수정 가능 + + +## 페이지 129 - 권한관리 +**버전**: D1.3 | **경로**: `기준정보> 권한관리` + +**Description:** + +1. 권한 등록 버튼 + - 클릭: 권한 상세 화면으로 이동 +2. 수정 버튼 + - 클릭: 권한 상세 화면으로 이동 + + +## 페이지 130 - 권한 상세 +**버전**: D1.3 | **경로**: `기준정보> 권한관리> 권한 상세` + +**Description:** + +1. 권한명 인풋박스 +2. 상태 셀렉트 박스 + - 종류: 공개, 숨김 +3. 메뉴 목록 + - 상위 및 하위 메뉴 목록 표시 + - 각 메뉴의 관리 목록 모두 설정 가능 + + +## 페이지 131 - 근무관리 +**버전**: D1.3 | **경로**: `기준정보> 근무관리` + +**Description:** + +1. 고용 형태 셀렉트 박스 + - 종류: 정규직, 계약직, 파견직, 용역직, 시간 + 제 근로자 + - 디폴트: 정규직 +2. 주간 근무일 체크박스 + - 체크 시 해당 요일은 근무일 +3. 출근 시간 설정 영역 +4. 퇴근 시간 설정 영역 +5. 법정 주당 기준 근로시간 표시 +6. 법정 주당 연장 근로시간 표시 +7. 휴게 시작 시간 설정 영역 +8. 휴게 종료 시간 설정 영역 + + +## 페이지 132 - 출퇴근관리 +**버전**: D1.3 | **경로**: `기준정보> 출퇴근관리` + +**Description:** + +*. 출퇴근관리 + - GPS 출퇴근과 자동 출퇴근은 독립적으로 + 설정 가능 + - 자동 출퇴근 기능은 정시 출퇴근 처리를 +1. GPS 출퇴근 셀렉트 박스 + - 종류: GPS 출퇴근을 사용합니다, GPS 출퇴 + 근을 사용하지 않습니다. + - 디폴트: GPS 출퇴근을 사용하지 않습니다 + - GPS 미사용 선택 시 (2) 연동 부서, (3) 출 + 퇴근 허용 반경비활성화 +2. 연동 부서 셀렉트 박스_검색&다중 선택 + - 종류: 전체, 부서명 목록 + - 디폴트: 전체 +3. 출퇴근 허용 반경 셀렉트 박스 + - 종류: 50M, 100M, 300M, 500M + - 디폴트: 100M + - 본사 또는 현장 GPS 좌표 기준으로 설정 + 된 반경 내에서만 출퇴근 기록 가능 + - 반경 외 위치에서 출퇴근 시도 시 오류 메 + 시지 표시 +4. 자동 출퇴근 셀렉트 박스 + - 종류: 자동 출퇴근을 사용합니다, 자동 출 + 퇴근을 사용하지 않습니다 + - 디폴트: 자동 출퇴근을 사용합니다. + - 자동 출퇴근 사용 시 (4-1) 연동 부서 셀렉 + 트 박스 활성화 + + +## 페이지 133 - 휴가관리 +**버전**: D1.3 | **경로**: `기준정보> 휴가관리` + +**Description:** + +1. 기준 셀렉트 박스 + - 종류: 회계연도, 입사일 + - 디폴트: 회계연도 + - 입사일 선택 시 (2) 영역 비활성화 + - 기본 연차 설정 반영 +2. 기준일 월/일 설정 영역 +*. 기본 연차 설정 + - 1년간 출근율 80%이상이면 15일 + - 3년 이상 근속 시 2년에 1일 추가 (최대 + 25일) 자동 부여 + - 1년 미만 또는 출근율 80% 미만일 경우 1 + 개월 개근 시 1일씩 연차 발생 (최대 11일) + - 입사일+1년+1일 시점: 전년도 출근율 + 80% 이상이면 15일 부여, 이후 2년에 1일 + 가산 (최대 25일) + - 입사일→회계연도 기준으로 전환할 때는 + 취업규칙 변경, 노사 의견수렴, 전환 시 중복 + ·누락 연차 정산(입사일 기준 vs 회계연도 기 + 준 비교 후 부족분 보전)을 반드시 검토 필 + + +## 페이지 134 - 카드관리 +**버전**: D1.3 | **경로**: `기준정보> 카드관리` + +**Description:** + +*. 카드관리 + - 카드사 코드, 카드 인증 정보, 비밀번호를 + 바로빌 API에 전달하여 카드 내역 자동 수집 + - 연동 성공 시 해당 카드의 사용 내역이 자 + 동으로 시스템에 반영됨 +1. 카드 등록 버튼 + - 클릭: 카드 상세 화면으로 이동 (등록 화면) +2. 삭제 버튼 + - 클릭: “선택하신 N개의 카드를 정말 삭제 + 하시겠습니까?" 확인 팝업 표시 + - 확인 시 해당 카드 삭제 처리 + - 삭제된 카드의 과거 사용 내역은 보존 +3. 수정 버튼 + - 클릭: 카드 상세 화면으로 이동 +4. 삭제 버튼 + - 클릭: “카드를 정말 삭제하시겠습니까?" 확 + 인 팝업 표시 + - 확인 시 해당 카드 삭제 처리 + - 삭제된 카드의 과거 사용 내역은 보존 + + +## 페이지 135 - 카드 상세 +**버전**: D1.3 | **경로**: `기준정보> 카드관리> 카드 상세` + +**Description:** + +1. 카드 비밀번호 앞 2자리 인풋박스 + - 입력 시 마스킹 처리 +2. 상태 셀렉트 박스 + - 종류: 사용, 정지 + - 정지 시 해당 카드의 자동 조회 중단 +3. 사용자 정보 셀렉트 박스_검색 + - 종류: 부서명 / 이름/ 직책 + - 선택 시 해당 카드의 사용자로 설정 + + +## 페이지 136 - 계좌관리 +**버전**: D1.3 | **경로**: `기준정보> 계좌관리` + +**Description:** + +*. 계좌관리 + - 계좌 인증 정보, 비밀번호(빠른 조회 서비 + 스)를 바로빌 API에 전달하여 계좌 내역 자 + 동 수집 + - 연동 성공 시 해당 계좌의 사용 내역이 자 + 동으로 시스템에 반영됨 + - 해당 테넌트는 은행에서 빠른 조회 서비스 + 를 사전 등록 필수 +1. 계좌 등록 버튼 + - 클릭: 계좌 상세 화면으로 이동 (등록 화면) +2. 삭제 버튼 + - 클릭: “선택하신 N개의 계좌를 정말 삭제 + 하시겠습니까?" 확인 팝업 표시 + - 확인 시 해당 계좌 삭제 처리 + - 삭제된 계좌의 과거 사용 내역은 보존 +3. 수정 버튼 + - 클릭: 계좌 상세 화면으로 이동 +4. 삭제 버튼 + - 클릭: “계좌를 정말 삭제하시겠습니까?" 확 + 인 팝업 표시 + - 확인 시 해당 계좌 삭제 처리 + - 삭제된 계좌의 과거 사용 내역은 보존 + + +## 페이지 137 - 계좌 상세 +**버전**: D1.3 | **경로**: `기준정보> 계좌관리> 계좌 상세` + +**Description:** + +1. 계좌 비밀번호 (빠른 조회 서비스) 인풋박스 + - 입력 시 마스킹 처리 +2. 상태 셀렉트 박스 + - 종류: 사용, 정지 + - 정지 시 해당 계좌의 자동 조회 중지 + + +## 페이지 138 - 계좌 상세 +**버전**: D1.3 | **경로**: `기준정보> 계좌관리> 계좌 상세` + +**Description:** + +1. 계좌 비밀번호 (빠른 조회 서비스) 인풋박스 + - 입력 시 마스킹 처리 +2. 상태 셀렉트 박스 + - 종류: 사용, 정지 + - 정지 시 해당 계좌의 자동 조회 중지 + + +## 페이지 139 - 팝업관리 +**버전**: D1.3 | **경로**: `기준정보> 팝업관리` + +**Description:** + +1. 팝업 등록 버튼 + - 클릭: 팝업 상세 화면으로 이동 + + +## 페이지 140 - 팝업 상세 +**버전**: D1.3 | **경로**: `기준정보> 팝업관리> 팝업 상세` + +**Description:** + +1. 대상 셀렉트 박스 + - 종류: 전사, 부서명, … + - 디폴트: 전사 +2. 기간 설정 영역 + - 노출 기간 설정 + - (3) 사용함 상태여도 해당 기간이 아닐 경 + 우 팝업 미노출 +3. 상태 라디오 버튼 + - 종류: 사용함, 사용안함 + - 디폴트: 사용안함 + + +## 페이지 141 - 게시판관리 +**버전**: D1.3 | **경로**: `기준정보> 게시판관리` + +**Description:** + +1. 게시판관리 + - 모든 테넌트 디폴트: 공지사항, 나의 게시 + 글 (수정, 삭제 불가) +2. 게시판 등록 버튼 + - 클릭: 게시판관리 상세 화면으로 이동 + + +## 페이지 142 - 리 상세 +**버전**: D1.3 | **경로**: `기준정보> 게시판관리> 게시판관` + +**Description:** + +1. 대상 셀렉트 박스 + - 종류: 전사, 부서명, … + - 디폴트: 전사 + 게시판 정보 * + 전사 ▼ + 대상 + 등록 + 게시판명을 입력해주세요 + 게시판명 + 2025-09-09 12:20 + + +## 페이지 143 - 알림설정 +**버전**: D1.3 | **경로**: `기준정보> 알림설정` + +**Description:** + +1. 전체 알림 설정 버튼 + - 클릭: ON/OFF 토글 + - 디폴트: OFF 상태 +2. 개별 알림 설정 버튼 + - 클릭: ON/OFF 토글 + - 디폴트: OFF 상태 +3. 알림 소리 선택 셀렉트 박스 + - 종류: 기본 알림음, SAM 보이스, …, 무음 + (샘 관리자에 등록된 음원목록) +3-1. 미리듣기 버튼 + - 클릭: 해당 음원 재생/일시정지 토글 +4. 하위 알림 설정 체크박스 + - 클릭: 체크 설정/해제토글 + - 디폴트: 해제 +5. 항목 설정 버튼 + - 클릭: 항목 설정_알림 팝업 표시 + 공지 알림 +  이메일 + 공지사항 알림 + 알림설정 + 알림 설정을 관리합니다 + 저장 + - 근무관리 + + +## 페이지 144 - - +**버전**: D1.3 + +**Description:** + +1. 유형별 알림 설정 영역 + - 근무관리 + 거래처 알림 + 신규 업체 등록 알림 + 기준정보> 알림설정 + 알림설정 + + +## 페이지 145 - - +**버전**: D1.3 + +**Description:** + +1. 유형별 알림 설정 영역 + - 근무관리 + 기준정보> 알림설정 + 알림설정 + + +## 페이지 146 - 알림설정 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 유형별 알림 설정 영역 + + +## 페이지 147 - 알림설정 +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 유형별 알림 설정 영역 + + +## 페이지 148 - ON +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 전체 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 +2. 개별 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 + + +## 페이지 149 - ON +**버전**: D1.3 | **경로**: `-` + +**Description:** + +1. 전체 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 +2. 개별 설정 ON/OFF 버튼 + - 클릭: 설정 ON/OFF 토글 + - 디폴트: 설정 OFF 상태 + + +--- + +# 계정정보 + + +## 페이지 151 - - +**버전**: D1.3 + +**Description:** + +1. 업체별 신용평가 및 보고서 검색? +2. 업체별 보고서 및 분석 상세 제공? + + +--- + +# 회사정보 + + +## 페이지 153 - Description +**버전**: D1.3 + +**Description:** + +1. 탈퇴 버튼 + - 테넌트 마스터가 아닐 경우에만 버튼 활성 + 화 + - 클릭: “정말 탈퇴하시겠습니까?” 확인 + Alert 표시, 확인 버튼 클릭 시 탈퇴 처리 (모 + 든 테넌트에서 탈퇴처리, SAM 탈퇴 처리) +2. 사용중지 버튼 + - 테넌트 마스터가 아닐 경우에만 버튼 활성 + 화 + - 클릭: “정말 사용중지하시겠습니까?” 확인 + Alert 표시, 확인 버튼 클릭 시 사용중지 처 + 리 (해당 테넌트의 사용중지처리) +3. 변경 버튼 + - 클릭: 비밀번호 설정화면으로 이동 + + +## 페이지 154 - Description +**버전**: D1.3 + +**Description:** + +*. - 테넌트 마스터에게만 표시 +1. 회사 추가 + - 클릭: 회사 추가 팝업표시 +2. 회사 정보 + - 운영(영업)에서 입력된 정보 표시, 수정 가 + + +--- + +# 구독관리 + + +## 페이지 156 - 회사 추가 팝업 +**버전**: D1.3 | **경로**: `회사정보> 회사 추가 팝업` + +**Description:** + +1. 사업자등록번호 인풋박스 + - 숫자만 가능, 10자리 +2. 다음 버튼 + - 클릭: + 1) 바로빌 사업자등록번호 조회 후 + 사용 불가 경우 + : “휴폐업 상태인 사업자입니다.” + 알림 Alert 표시 + 2) 바로빌 사업자등록번호 조회 후 + 사용 가능한 경우 + [1] 테넌트 등록된 사업자등록번호일 경우, + 테넌트 등록 전이어도 다른 영업사원이 + 등록했을 경우에는 사업자등록번호 + 사용 불가 (어드민에서는 해제 가능) + : “등록된 사업자등록번호 입니다.” + 알림 Alert 표시 + [2] 등록되지 않은 사업자등록번호일 경우 + : “매니저에게 회사 추가 신청 알림을 + 발송했습니다. 연락을 기다려주세요.” + 알림 Alert 표시, 매니저에게 알림 처리 + + +## 페이지 157 - Description +**버전**: D1.3 + +**Description:** + +*. - 테넌트 마스터에게만 표시 +1. 자료 내보내기 버튼 + - 클릭: 자료 다운로드 처리 +2. 서비스 해지 버튼 + - 클릭: “모든 데이터가 삭제되며 복구할 수 + 없습니다. 정말 서비스를 해지하시겠습니 + 까?” 확인 Alert 표시, 확인 버튼 클릭 시 서 + 비스 해지 처리 +3. 구독 정보 영역 + - 월 구독 형태 + - 각 용량 한도는 추후 확정 + + +## 페이지 158 - Description +**버전**: D1.3 + +**Description:** + +*. - 테넌트 마스터에게만 표시 +1. 결제내역 표시 + - 최신순 정렬 +2. 거래명세서 버튼 + - 클릭: 문서 상세_거래명세서 팝업 표시 + + +## 페이지 159 - 공지사항 +**버전**: D1.3 | **경로**: `고객센터> 공지사항` + +**Description:** + +*. SAM 공지사항 +1. 게시글 정보 영역 + - 항목: 번호(상단 노출 아이콘 또는 번호), + 제목, 작성자, 등록일, 조회수 + - 클릭: 게시글 상세 화면으로 이동 + + +## 페이지 160 - 세 +**버전**: D1.3 | **경로**: `고객센터> 공지사항> 공지사항 상` + +**Description:** + +*. SAM 공지사항 + + +--- + +# 고객센터 + + +## 페이지 162 - 이벤트 상세 +**버전**: D1.3 | **경로**: `고객센터> 이벤트> 이벤트 상세` + +**Description:** + +*. SAM 이벤트 + + +## 페이지 163 - FAQ +**버전**: D1.3 | **경로**: `고객센터> FAQ` + +**Description:** + +*. SAM FAQ +1. FAQ 탭 + - 종류: 전체, 카테고리명, … + - 디폴트: 전체 +2. FAQ 목록 + - 디폴트: 답변 영역 닫힘 + - 클릭: 답변 영역 열림/닫힘 토글 +3. 답변 영역 + - 클릭: 답변 닫힘 + + +## 페이지 164 - 1:1 문의 +**버전**: D1.3 | **경로**: `고객센터> 1:1 문의` + +**Description:** + +1. 문의 등록 버튼 + - 클릭: 1:1 문의 상세_등록 화면으로 이동 +2. 상담분류 필터 셀렉트 박스 + - 종류: 전체, 문의하기, 신고하기, 건의사항, + 서비스 오류 + - 디폴트: 전체 +3. 상태 필터 셀렉트 박스 + - 종류: 전체, 답변대기, 답변완료 + - 디폴트: 전체 + + +## 페이지 165 - 세 +**버전**: D1.3 | **경로**: `고객센터> 1:1 문의> 1:1 문의 상` + +**Description:** + +1. 상담분류 셀렉트 박스 + - 종류: 문의하기, 신고하기, 건의사항, 서비 + 스 오류 + - 디폴트: 문의하기 + + +## 페이지 166 - 세 +**버전**: D1.3 | **경로**: `고객센터> 1:1 문의> 1:1 문의 상` + +**Description:** + +1. 문의영역 정보 +2. 수정 버튼 + - 답변완료 후에는 수정 버튼 비활성화 + + +## 페이지 167 - Description +**버전**: D1.3 + +**Description:** + +1. 댓글 정보 영역 + - 항목: 프로필 이미지, 이름, 댓글 내용, 등 + 록일시 표시 From f4ae2ee53a81c2c07e1bee037001908cb54e13bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 23 Feb 2026 07:58:42 +0900 Subject: [PATCH 24/69] =?UTF-8?q?docs:=20[plans]=20ERP=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B3=B4=EB=93=9C=20D1.4=20=EB=A7=88?= =?UTF-8?q?=ED=81=AC=EB=8B=A4=EC=9A=B4=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INDEX.md | 1 + plans/SAM_ERP_Storyboard_D1.4.md | 1150 ++++++++++++++++++++++++++++++ 2 files changed, 1151 insertions(+) create mode 100644 plans/SAM_ERP_Storyboard_D1.4.md diff --git a/INDEX.md b/INDEX.md index d561312..f36570d 100644 --- a/INDEX.md +++ b/INDEX.md @@ -151,6 +151,7 @@ docs/ | 문서 | 설명 | |------|------| | [SAM_ERP_Storyboard_D1.4_260116.md](plans/SAM_ERP_Storyboard_D1.4_260116.md) | ERP 전체 스토리보드 D1.4 (167p PDF → 마크다운 변환, 14개 섹션 146개 화면) | +| [SAM_ERP_Storyboard_D1.4.md](plans/SAM_ERP_Storyboard_D1.4.md) | ERP 스토리보드 D1.4 AI 최적화 버전 (구조화된 한글 마크다운, 15개 섹션) | | [SAM_ERP_회계관리_Storyboard_D1.6.md](plans/SAM_ERP_회계관리_Storyboard_D1.6.md) | ERP 회계관리 스토리보드 D1.6 (65p PDF → 마크다운 변환) | | [SAM_General_Rule_Storyboard_D1.0.md](plans/SAM_General_Rule_Storyboard_D1.0.md) | General Rule 스토리보드 D1.0 (43p PDF → 마크다운 변환, UIUX 공통 규칙) | | [production-deployment-plan.md](plans/production-deployment-plan.md) | 운영 환경 배포 계획 (CI/CD, 서버 아키텍처) | diff --git a/plans/SAM_ERP_Storyboard_D1.4.md b/plans/SAM_ERP_Storyboard_D1.4.md new file mode 100644 index 0000000..8b8f4d7 --- /dev/null +++ b/plans/SAM_ERP_Storyboard_D1.4.md @@ -0,0 +1,1150 @@ +# SAM ERP 스토리보드 D1.4 + +> **작성일**: 2026-01-16 +> **버전**: D1.4 +> **상태**: 프론트 작성 +> **문서 ID**: SAM_ERP +> **원본**: `SAM_ERP_Storyboard_D1.4_260116.pdf` (167페이지) + +--- + +## 1. 문서 이력 (Document History) + +| 날짜 | 버전 | 주요 내용 | 상세 내용 | +|------|------|----------|----------| +| 2025-12-01 | D0.6 | 프론트 초안 | PC ERP - 인사관리 & 전자결재 작성 | +| 2025-12-01 | D0.7 | 프론트 작성 | PC ERP - 인사관리 & 전자결재 피드백 반영 | +| 2025-12-16 | D0.8 | 프론트 작성 | PC ERP - 회계 & 보고서 작성. 목록화면 기간 설정 영역 추가, GPS 출퇴근 추가, 급여관리/상세 삭제, 회계관리 추가, 출퇴근관리 추가, 카드/계좌관리 및 보고서 추가 | +| 2025-12-18 | D1.0 | 프론트 작성 | PC ERP - 구독 & 고객센터 작성. 게시판 추가, 악성채권 추심관리 상세 추가, 팝업관리/게시판관리/알림설정 추가, 계정정보/회사정보/구독관리/결제내역/고객센터 추가 | +| 2025-12-22 | D1.1 | 프론트 작성 | 카드 내역 관리 수정 | +| 2025-12-31 | D1.2 | 프론트 작성 | 알림 소리 설정 추가, 접대비 현황 수정 | +| 2026-01-07 | D1.3 | 프론트 작성 | 보고서 정보를 대시보드로 이동, SAM AI 채팅 버튼 추가, 화면 추가 다수, 항목 설정 버튼 추가 | +| 2026-01-16 | D1.4 | 프론트 작성 | 오늘의 이슈/현황판 화면 수정, 현황판 영역 및 3번 추가, 계정과목명 변경 (p99,100,104,106,108,123) | + +--- + +## 2. 메뉴 구조 (Menu Structure) + +``` +SAM +├── 로그인 / 회원가입 +├── 대시보드 +├── MES +│ ├── 판매관리 +│ ├── 구매관리 +│ ├── 발주관리 +│ ├── 공사관리 +│ ├── 생산관리 +│ ├── 품질관리 +│ ├── 자재관리 +│ ├── 장비관리 +│ └── 차량관리 +├── ERP +│ ├── 인사관리 +│ │ ├── 부서관리 +│ │ ├── 사원관리 +│ │ ├── 근태관리 +│ │ └── 휴가관리 +│ ├── 전자결재 +│ │ ├── 기안함 +│ │ ├── 결재함 +│ │ └── 참조함 +│ ├── 게시판 +│ ├── 회계관리 +│ │ ├── 거래처관리 +│ │ ├── 매출관리 +│ │ ├── 매입관리 +│ │ ├── 입금관리 +│ │ ├── 출금관리 +│ │ ├── 어음관리 +│ │ ├── 거래처원장 +│ │ ├── 일일 일보 +│ │ ├── 지출 예상 내역서 +│ │ ├── 미수금 현황 +│ │ ├── 악성채권 추심관리 +│ │ ├── 입출금 계좌 조회 +│ │ └── 카드 내역 관리 +│ ├── 기준정보 +│ │ ├── 직급관리 +│ │ ├── 직책관리 +│ │ ├── 권한관리 +│ │ ├── 근무관리 +│ │ ├── 출퇴근관리 +│ │ ├── 휴가관리 +│ │ ├── 카드관리 +│ │ ├── 계좌관리 +│ │ ├── 팝업관리 +│ │ ├── 게시판관리 +│ │ └── 알림설정 +│ └── 보고서 및 분석 +├── 계정정보 +├── 회사정보 +├── 구독관리 +├── 결제내역 +└── 고객센터 + ├── 공지사항 + ├── 이벤트 + ├── FAQ + └── 1:1 문의 +``` + +--- + +## 3. 공통 요소 + +### 3.1 제스처/인터랙션 + +| Type | 설명 | 적용 | +|------|------|------| +| Tap | 일정영역을 사용자가 터치 | Yes | +| Touch & Hold | 화면을 터치한 후 계속 누르고 있는 상태 | No | +| Double Tap | 일정영역을 두 번 터치 | No | +| Drag & Drop | 터치 혹은 홀드 상태에서 오브젝트를 이동하여 원하는 위치에 배치 | Yes | +| Scroll Up/Down | 위/아래로 스크롤 | Yes | +| Swipe Left/Right | 좌/우로 스와이프 | Yes | +| Pinch Zoom in/out | 오브젝트 또는 화면을 확대/축소 | Yes | + +### 3.2 반응형 웹 브레이크 포인트 + +| 구분 | 크기 | +|------|------| +| 모바일 | < 640px (기본) | +| 태블릿 | 768px ~ 1023px (md) | +| 데스크탑 | 1024px+ (lg) | +| 대형 모니터 | 1280px+ (xl) | + +### 3.3 화면 템플릿 + +- **A**: Status bar - 안테나, 통화, 배터리 등 시스템 OS 관리 영역 +- **B**: Browser 영역 - 브라우저 기능 영역 +- **C**: Title 영역 - 텍스트 또는 기능 버튼, 기본 가운데 정렬 +- **D**: Content 영역 - 컨텐츠 내용 표시, 길어질 경우 스크롤 +- **E**: Browser bar 영역 - 브라우저 유틸 바 영역 +- **F**: Keypad 영역 - 키보드 입력할 때 활성화 + +### 3.4 메시지 유형 + +| Type | 설명 | +|------|------| +| 알림 Alert | 사용자에게 상황을 알려주기 위한 팝업 (확인 버튼) | +| 확인 Alert | 사용자에게 확인이 필요할 경우 제공 (취소/확인 버튼) | +| 토스트 메시지 | 단순 Notify, 2~3초 후 Fade out | + +### 3.5 셀렉트 박스 + +- **기본**: 클릭 시 하단에 종류 목록 표시, 목록 중 하나만 선택 +- **다중 선택**: 복수 선택 가능, 전체 선택/해제 토글, 첫번째 항목명 + 추가 수 표시 +- **검색**: 검색어 입력 후 엔터 또는 검색 아이콘 클릭 시 검색 결과 표시 +- **검색 & 다중 선택**: 검색 + 복수 선택 기능 결합 + +### 3.6 가이드 메시지 + +- 긍정일 경우: 녹색 +- 부정일 경우: 붉은색 +- 입력 필드 하단 또는 Alert에 표시 + +### 3.7 공지 팝업 + +- 대상: 전체 또는 설정 부서 +- 설정 기간동안 대상에게 팝업 표시 +- "1일간 이 창을 열지 않음" 체크박스 (자정 기준) + +--- + +## 4. GNB, LNB, 푸터 (p9) + +### 4.1 GNB (Global Navigation Bar) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 알림 버튼 | 클릭 시 알림 팝업 표시 | +| 2 | 개인 정보 버튼 | 디폴트 이미지, 이름, 직급 표시. 클릭 시 마이페이지 팝업 | +| 3 | 회사 로고 | 회사정보 화면에서 등록한 로고 표시, 회사 변경 시 해당 로고 변경 | +| 4 | 메뉴 영역 | 하위 메뉴 있을 경우 하단에 표시, 없을 경우 해당 화면으로 이동 | +| 5 | MES 메뉴 영역 | 영업관리, 판매관리, 구매관리 등 MES 메뉴 영역 | +| 6 | 푸터 영역 | 모든 화면 하단 공통 표시 | +| 7 | SAM AI 채팅 버튼 | 클릭 시 SAM AI 채팅 팝업 표시 | + +### 4.2 알림 팝업 (p10) + +- 각 디폴트 썸네일, 종류(공지사항, 안내), 제목/내용, 전송일시 표시 +- 클릭 시 해당 상세 화면으로 이동 +- 최신순 10개까지 표시 +- New 아이콘: 새 알림일 경우 표시, 클릭 시 사라짐 +- 붉은 점 아이콘: 새 알림이 있을 경우 표시, 모두 클릭 시 사라짐 + +### 4.3 마이페이지 팝업 (p11) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 계정 아이디 | 이메일 표시 | +| 2 | 회사 셀렉트 박스 | 해당 계정이 생성한 회사(테넌트) 목록 표시, 등록순 정렬, 한 회사만 소유 시 숨김 | +| 3 | 로그아웃 버튼 | "정말 로그아웃하시겠습니까?" 확인 Alert | + +--- + +## 5. 운영 (영업) + +### 5.1 가입 및 로그인 플로우 + +``` +영업사원 → 사업자등록번호 입력 → 조회 + ├── 미등록 → 회사정보 등록 → 가입 신청 완료 + └── 등록됨 → 알림 Alert + +관리자(매니저) → 승인/거절 + ├── 승인 → 이메일로 URL 발송 → 약관 동의 → 비밀번호 설정 → SAM 로그인 + └── 거절 → 거절 알림 +``` + +### 5.2 운영 로그인 (p17) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 아이디 인풋박스 | 테넌트 생성자: 이메일, 사용자: 이메일 또는 아이디 | +| 2 | 비밀번호 인풋박스 | 마지막 글자 제외 마스킹 처리 | +| 2-2 | 열람 버튼 | 열람/숨김 토글, 디폴트 숨김 | +| 3 | 자동 로그인 체크박스 | 체크 시 로그아웃 전까지 세션 유지 | +| 4 | 로그인 버튼 | 유효할 경우 사업자등록번호 조회 화면으로 이동 | + +**아이디 가이드 메시지:** + +| 상황 | 메시지 | +|------|--------| +| 필수 정보 미입력 | "필수 정보입니다." | +| 4글자 미만 | "이메일은 4자 이상 가능합니다." | +| 이메일 형식 유효하지 않음 | "이메일 주소를 다시 확인해주세요." | + +**비밀번호 가이드 메시지:** + +| 상황 | 메시지 | +|------|--------| +| 필수 정보 미입력 | "필수 정보입니다." | +| 8자 미만 | "8자 이상으로 만들어주세요." | +| 영문+숫자+특수문자 조합 아님 | "영문, 숫자, 특수문자를 모두 조합하여 구성해주세요. 단, `' ; -- < ( ) \ /` 보안상 사용 불가" | + +### 5.3 사업자등록번호 조회 (p18) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 제조 데모 | 클릭 시 제조 데모 화면으로 이동 | +| 2 | 시공 데모 | 클릭 시 시공 데모 화면으로 이동 | +| 3 | 사업자등록번호 인풋박스 | 숫자만 가능, 10자리 | +| 4 | 다음 버튼 | 바로빌 조회 후: 휴폐업 시 알림, 사용가능+등록됨 시 알림, 사용가능+미등록 시 회사정보 등록 이동 | + +### 5.4 회사정보 등록 (p19) + +**회사(테넌트) 상태:** + +| 상태 | 설명 | +|------|------| +| 신청 | 신청 완료 상태 | +| 승인 | 계약 완료 및 계약금 50% 입금, 이메일로 URL 발송, 최초 로그인 시 ERP만 표시 | +| 거절 | 영업사원이 직접 거절 내용 전달 | +| 운영 | 프로그램 설정 완료, 잔금 50% 입금 및 인도, 당월 말일까지 무료, 익월부터 구독료 청구 | +| 만료 | 기간 종료, 종료일~3일동안 연장 결제 없음, 영업사원에게 알림, 서비스에 경고 배너 | +| 해지 대기 | 90일 대기 단계 | +| 해지 | 서비스 해지, 복구 불가 | +| 제재 | 서비스 이용 불가 | +| 탈퇴 | 로그인 불가, 복구 불가 | + +**등록 필드:** +- 회사 로고 (750x250px, 10MB 이하 PNG/JPEG/GIF) +- 회사명, 대표자명, 업태, 업종 +- 주소 (우편번호 찾기) +- 이메일(아이디), 세금계산서 이메일 +- 담당자명, 담당자 연락처 +- 사업자등록증 (파일 첨부) + +### 5.5 가입 신청 완료 (p20) + +- 가입 신청 완료 안내 문구 표시 +- 가입 신청 취소 버튼: "가입 신청 취소 시 등록한 모든 정보가 삭제됩니다." 확인 Alert + +### 5.6 가입 신청 승인 이메일 (p21) + +- 계정 활성화 버튼: 약관 동의 화면으로 이동 +- 지원, 블로그 버튼: 운영 노션 링크로 이동 + +### 5.7 약관 동의 (p22) + +- 필수 약관: 서비스 이용약관, 개인정보 취급방침, 기타 약관 +- 선택 약관: 마케팅 정보 수신 동의 (이메일, SMS) +- "약관에 동의합니다" 버튼: 모든 필수 약관 동의 시 활성화 → 비밀번호 설정 화면 이동 +- "전체 약관에 동의합니다" 버튼: 모든 필수+선택 약관 동의 처리 → 비밀번호 설정 화면 이동 + +### 5.8 비밀번호 설정 (p23) + +- 최소 8자 이상 영문+숫자+특수문자 조합 +- 비밀번호 확인 +- 계정 활성화 버튼: 로그인 화면으로 이동 + +--- + +## 6. GPS 출퇴근 + +### 6.1 출퇴근하기 (p25) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 출퇴근 버튼 | GPS 출퇴근 사용 시에만 표시, 모바일일 경우에만 활성화 | +| 2 | 출퇴근 허용 반경 | 기준 좌표로부터의 허용 반경을 원형으로 표시 | +| 3 | 현재 위치 버튼 | 현재 위치를 지도 중심으로 표시 | +| 4-6 | 지도 컨트롤 | 확대(+), 축소(-), 슬라이드바 | +| 7 | 개인 정보 영역 | 프로필 이미지, 이름, 부서명, 직급명 | +| 8 | 현재 시각 | HH:MM:SS | +| 9 | 출근하기 버튼 | 위치 미설정 시 알림, 반경 초과 시 알림, 반경 이내 시 출근 기록 저장 | + +### 6.2 출근/퇴근 완료 (p26-27) + +- 출근/퇴근 완료 아이콘 이미지 표시 +- 완료 정보: 시:분:초, 일자(요일) +- 출근/퇴근 좌표의 본사/현장명 표시 +- 확인 버튼: 대시보드로 이동 + +### 6.3 현장등록 - 위치 정보 설정 (p28) + +- 위도/경도 입력 +- 주소 또는 경위도 값으로 설정 +- 각 현장의 GPS 중심값으로 설정 + +--- + +## 7. 대시보드 + +### 7.1 로그인 (p30) + +- 운영 로그인과 동일 구조 +- 로그인 버튼 클릭 시 대시보드 화면으로 이동 + +### 7.2 대시보드 메인 (p31-36) + +#### 7.2.1 오늘의 이슈 (p31) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 항목 설정 버튼 | 항목 설정_대시보드 팝업 표시 | +| 2 | 오늘의 이슈 영역 | 당일 이슈 발생 시 알림, 즉시 승인/보류 처리 가능 | +| 3 | 필터 셀렉트 박스 | 전체, 수주 성공, 추심 이슈, 적정 재고, 결재 요청, 세금 신고, 신규 업체 등록, 근태, 발주 완료 | +| 4 | 이슈 목록 | 클릭 시 해당 상세 화면으로 이동, 화면 가로 길이에 따라 4/3/2/1열 표시 | +| 5 | 승인/반려 버튼 | 해당 건에 대해 즉시 승인/반려 처리 | +| 6 | 일일 일보 정보 | 현금성 자산 합계, 외국환(USD) 합계, 입금 합계, 출금 합계 | +| 7 | AI 리포트 | 핵심 키워드 강조 표시 (빨간색: 경고, 주황: 주의, 녹색: 긍정, 파랑: 양호) | + +**이슈 케이스:** +- 신규 업체 등록 +- 결근 등 근태 이벤트 +- 재고 미달 알림 +- 채권 추심 등록, 상태 변경 +- 발주, 수주 등록 +- 지출결의서 등 전자결재 상신 +- 세금 신고 알림 + +#### 7.2.2 현황판 (p32) + +- 수주, 채권 추심, 안전 재고, 세금 신고, 신규 업체 등록, 연차, 발주, 결재 요청 +- 경고 상태일 경우 해당 영역에 색상 하이라이트 +- 클릭 시 해당 상세 화면으로 이동 + +#### 7.2.3 당월 예상 지출 내역 (p32-33) + +- 매입, 카드, 발행어음, 총 예상 지출 합계 (전월 대비 %) +- AI 분석 메시지 (예상 지출 증감 원인 분석) + +#### 7.2.4 카드/가지급금 관리 (p33) + +**가지급금 정의:** +- 법인카드(지출결의서) 미정리 +- 접대비 불인정 +- 증빙미비 +- 업무관련성 소명 불가 (주말/심야 카드 사용, 불인정 가맹점) +- 대표자 개인 대여 +- 가지급금 인정이자 4.6% + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 매입 정보 | 클릭 시 당월 매입 상세 팝업 | +| 2 | 카드 정보 | 클릭 시 당월 카드 상세 팝업 | +| 3 | 발행어음 정보 | 클릭 시 당월 발행어음 상세 팝업 | +| 4 | 총 예상 지출 합계 | 클릭 시 당월 지출 예상 상세 팝업 | +| 5 | 가지급금 | 클릭 시 가지급금 상세 팝업 | +| 6 | 법인세 예상 가중 | 클릭 시 법인세 예상 가중 상세 팝업 | +| 7 | 대표자 종합세 예상 가중 | 클릭 시 대표자 종합소득세 예상 가중 상세 팝업 | + +#### 7.2.5 접대비 현황 (p33-34) + +- 매출, 접대비 총 한도, 접대비 잔여한도, 접대비 사용금액 +- AI 분석 메시지 (한도 대비 사용률, 초과 경고, 거래처 정보 누락 등) + +#### 7.2.6 복리후생비 현황 (p34) + +- 당해년도 한도, 기간별 한도/잔여/사용금액 +- AI 분석 (1인당 월 복리후생비, 식대 비과세 한도 초과 등) + +#### 7.2.7 미수금 현황 (p34) + +- 누적 미수금, 당월 미수금 +- 미수금 상위 회사 1, 2위 표시 +- AI 분석 (장기 미수금 경고, 리스크 분산 필요 등) + +#### 7.2.8 채권추심 현황 (p35) + +- 누적 악성채권, 추심중, 법적조치, 회수완료 +- 세금계산서 미발행 건수 +- AI 분석 (지급명령 신청 상태, 대손 처리 검토 등) + +#### 7.2.9 부가세 현황 (p35-36) + +- 매출세액, 매입세액, 예상 납부세액 +- AI 분석 (예상 환급세액/납부세액, 전기 대비 증감 분석) + +#### 7.2.10 캘린더 (p36) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 이번주 + 좌우 화살표 | 이전월/다음월 스케줄 표시 | +| 2 | 캘린더 탭 | 주, 월 (디폴트: 월) | +| 3 | 부서 필터 | 전체, 부서, 개인 | +| 4 | 업무 필터 (다중선택) | 전체, 일정, 발주, 시공, 수주 성공, 추심 이슈 등 | +| 5 | 일정 영역 | [부서/이름] 제목 형태, 클릭 시 상세 이동 | +| 5-1 | 이슈 영역 | [구분] 제목 형태, 클릭 시 상세 이동 | +| 6 | 일자 영역 | 당일 외곽선 하이라이트, 지난 일자 색상 구분 | +| 7 | +N 버튼 | 해당 일자에 스케줄 2건 초과 시 초과건 숫자 표시 | +| 8 | 일정 목록 영역 | 선택된 일자의 일정 목록 | +| 9 | 일정 등록 버튼 | 일정 상세 팝업 표시 | + +### 7.3 일정 상세 팝업 (p37) + +- 부서 셀렉트 박스 (검색) +- 기간 영역: 기간 설정 달력 팝업 +- 시간 체크박스: 미설정 시 종일, 설정 시 시간 범위 활성화 +- 색상 선택 + +### 7.4 항목 설정_대시보드 팝업 (p38-39) + +**ON/OFF 설정 항목:** +- 오늘의 이슈: 수주, 채권 추심, 안전 재고, 세금 신고, 신규 업체 등록, 연차, 지각, 결근, 발주, 결재 요청 +- 현황판 (오늘의 이슈 항목 정보와 연동) +- 당월 예상 지출 내역, 카드/가지급금 관리, 일일 일보 +- 접대비 현황, 복리후생비 현황, 미수금 현황, 미수금 상위 회사 현황 +- 채권추심 현황, 부가세 현황, 캘린더 + +**접대비 한도 관리:** +- 기간 구분: 연간, 반기, 분기, 월 (총 한도를 분할 계산) +- 기업 구분: 일반법인, 중소기업 + +**복리후생비 한도 관리:** +- 계산 방식: 직원당 정액 금액 방식 / 연봉 총액 X 비율 방식 + +**중소기업 판단 기준표:** + +| 조건 | 기준 | 충족 요건 | +|------|------|----------| +| 매출액 | 업종별 상이 | 업종별 기준 금액 이하 | +| 자산총액 | 5,000억원 | 미만 | +| 독립성 | 소유/경영 | 대기업 계열 아님 | + +**접대비 기본한도:** + +| 판정 | 조건 | 접대비 기본한도 | +|------|------|---------------| +| 중소기업 | 3가지 모두 충족 | 3,600만원 | +| 일반법인 | 하나라도 미충족 | 1,200만원 | + +### 7.5 당월 매입 상세 팝업 (p40) + +- 자재 유형별 구매 비율 차트 (원자재/부자재/포장재) +- 월별 매입 추이 차트 +- 일별 매입 내역 테이블: 매입일, 거래처, 매입금액, 매입유형 +- 필터: 매입유형 (원재료매입, 부재료매입, 상품매입, 외주가공비, 소모품비 등) +- 정렬: 최신순, 등록순, 금액순 + +### 7.6 당월 카드 상세 팝업 (p41) + +- 사용자별 카드 사용 비율 차트 +- 월별 카드 사용 추이 차트 +- 일별 카드 사용 내역: 카드명, 사용자, 사용일시, 가맹점명, 사용금액, 사용유형 +- 미정리 건수 표시 + +### 7.7 당월 발행어음 상세 팝업 (p42) + +- 월별 발행어음 추이 차트 +- 당월 거래처별 발행어음 차트 +- 상태: 보관중, 만기임박(만기일 7일 전), 만기 경과, 결제완료, 부도 + +### 7.8 당월 지출 예상 상세 팝업 (p43) + +- 당월 지출 예상 금액, 전월 대비, 총 계좌 잔액 +- 당월 지출 승인 내역서: 예상 지급일, 항목, 지출금액, 거래처, 계좌 +- 지출 합계, 계좌 잔액, 최종 차액 + +### 7.9 가지급금 상세 팝업 (p44) + +- 가지급금, 인정이자 4.6%, 미설정 건수 +- 내역: 발생일시, 대상, 구분(카드/계좌), 금액, 상태, 내용 +- AI 분류 기준: 미정리, 불인정 가맹점, 접대비 불인정, 주말/심야 카드 사용 + +### 7.10 법인세 예상 가중 상세 팝업 (p45) + +**법인세 과세표준 (2024년 기준):** + +| 과세표준 | 세율 | 누진공제 | +|---------|------|---------| +| 2억원 이하 | 9% | - | +| 2억 초과 ~ 200억 이하 | 19% | 2,000만원 | +| 200억 초과 ~ 3,000억 이하 | 21% | 42,000만원 | +| 3,000억 초과 | 24% | 942,000만원 | + +- 접대비 초과 금액 + 가지급금 인정이자 반영/미반영 비교 +- 과세표준 계산: 당기순이익 + 손금불산입 - 손금산입 + +### 7.11 대표자 종합소득세 예상 가중 상세 팝업 (p46) + +**종합소득세 과세표준 (2024년 기준):** + +| 과세표준 | 세율 | 누진공제 | +|---------|------|---------| +| 1,400만원 이하 | 6% | - | +| 1,400만 초과 ~ 5,000만 이하 | 15% | 126만원 | +| 5,000만 초과 ~ 8,800만 이하 | 24% | 576만원 | +| 8,800만 초과 ~ 1.5억 이하 | 35% | 1,544만원 | +| 1.5억 초과 ~ 3억 이하 | 38% | 1,994만원 | +| 3억 초과 ~ 5억 이하 | 40% | 2,594만원 | +| 5억 초과 ~ 10억 이하 | 42% | 3,594만원 | +| 10억 초과 | 45% | 6,594만원 | + +- 인정이자가 상여로 처리, 종합소득세/지방소득세/4대보험 차액 표시 + +### 7.12 당해 매출 상세 팝업 (p47) + +- 월별 매출 추이 차트, 당해년도 거래처별 매출 차트 +- 매출유형: 제품 매출, 상품 매출, 부품 매출, 용역 매출, 공사 매출, 임대 수익, 기타 매출 + +### 7.13 접대비 상세 팝업 (p48-49) + +**접대비 손금한도 계산:** + +| 법인 유형 | 연간 기본한도 | 월 환산 | +|----------|-------------|--------| +| 일반법인 | 12,000,000원 | 1,000,000원 | +| 중소기업 | 36,000,000원 | 3,000,000원 | + +**수입금액별 추가한도:** + +| 수입금액 구간 | 추가한도 계산식 | +|-------------|--------------| +| 100억원 이하 | 수입금액 x 0.2% | +| 100억 초과 ~ 500억 이하 | 2,000만원 + (수입금액 - 100억) x 0.1% | +| 500억원 초과 | 6,000만원 + (수입금액 - 500억) x 0.03% | + +### 7.14 복리후생비 상세 팝업 (p50-51) + +- 항목별 사용 비율 차트 (식대, 건강검진 등) +- 계산 방식: 직원당 정액 금액 / 연봉 총액 비율 + +**법정 외 복리후생비 예시:** + +| 항목 | 금액(원) | 비고 | +|------|---------|------| +| 식대 (비과세) | 200,000 | 1인당 월 20만원 | +| 교통비/차량유지비 | 100,000 | 1인당 월 10만원 | +| 경조사비 | 50,000 | 1인당 월 5만원 적립 | +| 건강검진비 | 30,000 | 연 1회 기준 월 환산 | +| 교육훈련비 | 80,000 | 1인당 월 8만원 | +| 복지포인트/기타 | 100,000 | 1인당 월 10만원 | + +### 7.15 예상 납부세액 상세 팝업 (p52) + +- 매출세액, 매입세액, 경감/공제세액 +- 세금계산서 미발행/미수취 내역 + +### 7.16 가지급금 인정이자 계산 (p54) + +**계산 공식 (법인세법 기준):** +- 경과일수 = 정산일 - 지급일 +- 일이자율 = 연이자율 / 365 +- 인정이자 = 가지급금 x 일이자율 x 경과일수 +- 정산차액 = 가지급금 총액 - 실사용 총액 + +**계산 예시 (2024년 기준, 인정이자율 4.6%):** + +| 항목 | 금액 | 계산식 | +|------|------|--------| +| 가지급금 잔액 | 15,200,000원 | - | +| 인정이자 | 699,200원 | 잔액 x 0.046 | +| 법인세 추가 (19%) | 132,848원 | 인정이자 x 0.19 | +| 대표자 소득세 추가 (35%) | 244,720원 | 인정이자 x 0.35 | +| 대표자 지방소득세 (10%) | 24,472원 | - | +| 총 세금 부담 | 402,040원 | - | + +--- + +## 8. 인사관리 + +### 8.1 부서관리 (p57-58) + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 전체 선택 체크박스 | 전체 선택/해제 토글 | +| 3 | 추가 버튼 | 선택한 부서의 하위 부서 일괄 생성 (관리 권한 필요) | +| 4 | 삭제 버튼 | 삭제된 부서의 인원은 회사(기본) 인원으로 변경 | +| 5/6 | 축소/확대 버튼 | 하위 부서 숨김/표시 토글 | +| 7 | 추가 버튼 | 부서 추가 팝업 표시 | +| 8 | 수정 버튼 | 부서 수정 팝업 표시 | +| 9 | 삭제 버튼 | 개별 부서 삭제 | + +### 8.2 사원관리 (p59) + +**상단 정보:** +- 재직 인원, 휴직 인원, 퇴직 인원, 평균근속년수 + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 기간 설정 | 입사일 기준, 당해년도/전전월/전월/당월/어제/오늘 | +| 2 | CSV 일괄 등록 | CSV 일괄 등록 화면으로 이동 | +| 3 | 사원 등록 | 사원 상세 화면으로 이동 | +| 4 | 사용자 초대 | 사용자 초대 팝업 표시 | +| 5 | 필터 | 전체, 사용자 아이디 보유/미보유, 재직, 휴직, 퇴직 | +| 6 | 정렬 | 직급순, 입사일순, 부서순, 이름순 | + +**목록 항목:** 사원코드, 부서, 직책, 이름, 직급, 휴대폰, 이메일, 입사일, 상태, 사용자 아이디, 권한 + +### 8.3 사원 상세 (p60-63) + +**사원 정보 (필수):** +- 주민등록번호, 이름, 휴대폰, 이메일 +- 급여계좌 (은행, 계좌, 예금주), 연봉 + +**사용자 정보 (필수):** +- 아이디 (이메일 또는 아이디), 비밀번호 +- 권한 (권한관리 목록), 상태 (정상, 제재, 중지) + +**사원 상세 (선택 - 항목 설정으로 관리):** +- 프로필 사진, 사원코드, 성별, 주소 + +**인사 정보 (선택):** +- 고용 형태: 정규직, 계약직, 파견직, 용역직, 시간제 근로자 +- 직급: 사원, 대리, 과장, 차장, 부장, 이사, 상무, 전무, 부사장, 사장, 회장 +- 상태: 재직, 병가휴직, 육아휴직, 개인사정휴직, 무급휴직, 퇴사, 해고, 권고사직, 계약만료, 정년퇴직 +- 부서 (검색), 직책 +- 출근 위치, 퇴근 위치 (본사, 현장 목록 중 선택) +- 퇴사일, 퇴사 사유 + +### 8.4 사용자 초대 팝업 (p64-65) + +**초대 프로세스:** +1. 초대 이메일 발송 +2. 약관 동의 (아이디를 이메일로 사용) +3. 비밀번호 설정 +4. 로그인 + +- 이메일 주소 기준 사원 정보가 있을 경우 매핑하여 사용자 등록 +- 사용자 아이디는 다른 테넌트와 중복 가능 + +### 8.5 CSV 일괄 등록 (p66-67) + +- 양식 다운로드 → 파일 선택 (CSV 50MB 이하) → 파일 변환 → 정보 등록 영역에 표시 → 체크 항목 등록 + +### 8.6 근태관리 (p68-69) + +- 관리 권한이 있으면 모든 사원 편집 가능, 없으면 본인만 +- 근태관리 자동 설정 시: 모든 사원 정시 출퇴근 기록, 예외사항만 작성 + +**상단 정보:** 정시 출근, 지각, 결근, 휴가 + +**근태 정보 팝업:** +- 사원 (검색 & 다중 선택), 기준일 (다중 선택 가능) +- 출근/퇴근 시간, 야간 연장 시간, 주말 연장 시간 + +### 8.7 휴가관리 (p70-73) + +**탭:** 휴가 사용 현황, 휴가 부여 현황, 휴가 신청 현황 + +**상단 정보:** 휴가 승인 대기, 연차 인원, 경조사 인원, 연간 연차 사용률 + +**휴가 유형:** 연차, 보상, 경조, 보건, 병가, 반차, 회수(차감) + +**휴가 신청:** +- 잔여 일시 >= 신청 일시: 휴가 신청 완료 +- 잔여 일시 < 신청 일시: "휴가 잔여 일시를 초과했습니다." 알림 + +--- + +## 9. 전자결재 + +### 9.1 기안함 (p75) + +**문서 상태:** +- 임시저장: 문서 작성 중 임시저장 +- 진행: 상신 및 결재자 중 일부 승인 +- 완료: 모든 승인 완료 +- 반려: 결재자 중 한 명이 반려 + +| 번호 | 요소 | 설명 | +|------|------|------| +| 1 | 문서 작성 | 문서 작성 화면으로 이동 | +| 2 | 상신 버튼 | 임시저장 상태만 상신 가능 | +| 3 | 삭제 버튼 | 임시저장 상태만 삭제 가능 | + +### 9.2 문서 작성 (p76-81) + +**문서 유형:** + +#### 9.2.1 품의서 (p76-77) + +- 기본 정보: 작성일, 기안자, 문서유형, 문서번호 +- 결재선: 부서/직책/이름 (검색 & 다중 선택) +- 참조: 부서/직책/이름 (검색 & 다중 선택) +- 품의서 정보: 구매처, 구매처 결제일, 제목, 품의 내역, 품의 사유, 예상 비용 +- 녹음 버튼: 음성 인식 → 텍스트 변환 → 인풋박스에 표시 +- 참고 이미지 첨부 + +#### 9.2.2 지출결의서 (p78-79) + +- 기본 정보: 품의서와 동일 +- 지출 정보: 결제일, 지출 요청일 +- 결제 정보: 카드 (등록된 카드 목록), 총 비용 +- 지출결의서 정보: 적요, 금액, 비고 (행 추가 가능) +- 참고 이미지 첨부 + +#### 9.2.3 지출 예상 내역서 (p80-81) + +- 기본 정보: 품의서와 동일 +- 지출 예상 내역서 정보: 제목 +- 목록: 예상 지급일, 항목, 지출금액, 거래처, 계좌 +- 월별 소계, 지출 합계, 계좌 잔액, 최종 차액 + +### 9.3 결재함 (p82) + +**상태:** +- 결재요청: 결재 요청을 받은 상태 +- 예정: 결재 순번에 의한 대기 +- 완료: 승인 완료 +- 반려: 반려 완료 + +### 9.4 문서 상세 팝업 (p83-85) + +**공통 기능:** 복제(새글), 수정(결재선 누구나 가능), 반려/승인(결재선만), 공유(PDF/이메일/팩스/카카오톡), 인쇄 +- 품의서: 구매처 정보, 품의 내역/사유, 예상 비용 +- 지출결의서: 지출 요청일/결제일, 적요/금액/비고, 법인카드, 총 비용 +- 지출예상내역서: 예상 지급일별 항목/지출금액/거래처/계좌 + +### 9.5 참조함 (p86) + +- 열람/미열람 상태 관리 +- 열람 버튼: 일괄 열람 처리 +- 미열람 버튼: 일괄 미열람 처리 + +--- + +## 10. 게시판 + +### 10.1 게시판 목록 (p88) + +- 게시판 탭: 공지사항, 게시판명, ..., 나의 게시글 +- 기준정보 > 게시판관리에서 설정한 게시판 목록 +- 대상(전사, 부서, 팀)에 따라 소속에 맞는 게시판만 표시 +- 게시글: 번호(상단 노출 아이콘 또는 번호), 제목, 작성자, 등록일, 조회수 + +### 10.2 게시글 상세 (p89-91) + +**등록:** +- 게시판 선택, 상단 노출 (최대 5개, 최신순), 댓글 사용/미사용 +- 제목, 내용, 첨부파일 + +**조회:** +- 본인 작성글만 수정/삭제 버튼 표시 +- 댓글 등록/수정/삭제 (본인 댓글만) + +--- + +## 11. 회계관리 + +### 11.1 회계 관리 플로우 + +``` +매출 흐름: + 거래처 선택 → 매출 등록 → 세금계산서 발행 → 입금 등록 + ├── 전액 입금? → 거래처원장 + └── 미입금 → 미수금 현황 → 연체? → 악성 추심 + +매입 흐름: + 거래처 선택 → 매입 등록 → 세금계산서 수취 → 출금 등록 + ├── 전액 출금? → 거래처원장 + └── 미출금 → 미지급 알림 +``` + +### 11.2 거래처관리 (p94-97) + +**목록 필터:** +- 구분: 전체, 매출, 매입, 매입매출 +- 신용등급: AAA ~ D +- 거래등급: A(우수), B(양호), C(보통), D(주의), E(위험) +- 악성채권: 전체, 악성채권, 정상 + +**거래처 상세:** +- 기본 정보: 거래처명, 거래처 코드, 사업자등록번호, 대표자명, 거래처 유형, 업태, 업종, 주소 +- 연락처: 전화번호, 모바일, 팩스, 이메일 +- 담당자 정보: 시스템 관리자, 담당자명, 담당자 전화 +- 회사 정보: 회사 로고 +- 결제일: 매입 결제일(1~31일/말일), 매출 결제일(1~31일/말일) +- 추가 정보: 신용등급, 거래등급, 세금계산서 이메일, 입금계좌 +- 미수금 표시 (읽기 전용) +- 연체 토글 (ON/OFF, 연체일수 표시) +- 미지급 표시 (읽기 전용) +- 악성채권 토글 (ON/OFF) +- 메모 (일시, 작성자, 내용) + +### 11.3 매출관리 (p99-102) + +**매출 등록:** +- 수주 확정 시 자동 등록 (삭제 불가) +- 별도 매출 시 직접 등록 (삭제 가능) + +**매출유형:** 미설정, 제품 매출, 상품 매출, 부품 매출, 용역 매출, 공사 매출, 임대 수익, 기타 매출 + +**매출 상세:** +- 기본 정보: 거래처명, 매출일, 매출번호, 매출유형 +- 품목 정보: 품목명, 수량, 단가, 공급가액, 부가세, 합계, 적요 +- 세금계산서: 발행 토글 (미발행/발행완료) +- 거래명세서: 발행 토글, 조회 버튼, 발행하기 버튼 (거래처 이메일로 자동 발송) + +### 11.4 매입/출금 플로우 (p103) + +``` +직원: 품의서 작성 → 전자결재 상신 → 승인? + ├── 승인 → 지출예상내역서 → 지급일 가능? + │ ├── Yes → 지출결의서 작성 → 전자결재 상신 → 승인? + │ │ ├── 승인 → 매입 자동 등록 → 출금 상세 등록 + │ │ └── 반려 + │ └── No → 예상 지급일 수정 + └── 반려 +``` + +### 11.5 매입관리 (p104-105) + +**매입 등록:** 지출예상내역서 승인 완료 시 자동 등록 (삭제 불가) + +**매입유형:** 미설정, 원재료매입, 부재료매입, 상품매입, 외주가공비, 소모품비, 수선비, 운반비, 사무용품비, 임차료, 수도광열비, 통신비, 차량유지비, 접대비, 보험료, 기타용역비 + +**매입 상세:** +- 근거 문서(품의서/지출결의서), 예상 비용 표시 +- 매입번호: 문서번호 + 넘버링 조합 +- 출금계좌, 거래처, 매입유형 +- 세금계산서 수취 토글 + +### 11.6 입금관리 (p106-107) + +- 기준정보 > 계좌관리에 등록된 계좌의 자동 입금 내역 수집 +- 바로빌 API 연동 시 실시간 조회 + +**입금유형:** 미설정, 매출대금, 선수금, 가수금, 임대수익, 이자수익, 보증금 반환, 차입금, 자본금, 부가세 환급, 기타 + +### 11.7 출금관리 (p108-109) + +- 기준정보 > 계좌관리에 등록된 계좌의 자동 출금 내역 수집 + +**출금유형:** 미설정, 매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타 + +### 11.8 어음관리 (p110-111) + +**구분:** 수취, 발행 + +**발행 어음 상태:** 보관중, 만기임박(만기일 7일 전), 만기 경과, 결제완료, 부도 +**수취 어음 상태:** 보관중, 만기임박(만기일 7일 전), 추심의뢰, 추심완료, 추심중, 부도 + +**수취 어음:** +- 거래처원장 상세, 일일 일보, 미수금 현황에 반영 +- 미수금에 대한 약정으로만 표시 +- 추심완료되어 입금 시에만 회계에 반영 + +**발행 어음:** +- 지출예상내역서에 반영 +- 지출에 대한 약정으로만 표시 +- 결제완료되어 출금 시에만 회계에 반영 + +**차수 관리:** 총 금액에 대한 차수로 상환 계획 작성 + +### 11.9 거래처원장 (p112-113) + +- 거래처별 기간별 합계 금액 표시 +- 목록: 거래처명, 이월잔액, 매출, 수금, 잔액, 결제일 + +**거래처원장 상세:** +- 이월잔액, 수취 어음 정보, 거래명세서 정보 +- 하위 전체 품목별 판매금액 표시 +- 세금계산서 미발행 시 붉은색 하이라이트 +- 누계 금액 표시 + +### 11.10 일일 일보 (p114) + +- 어음 및 외상매출채권 현황: 수취어음 거래처명, 금액, 발행일, 만기일 +- 현금성 자산: 계좌별 전월 이월, 수입, 지출, 잔액 +- 외국환(USD) 합계, 현금성 자산 합계 + +### 11.11 지출 예상 내역서 (p115-116) + +- 카드 및 승인/반려 확정 목록은 삭제 불가 +- 예상 지급일: 매입 거래처 등록 시 자동 입력 +- 품의서/지출결의서/발행어음 목록 (클릭 시 상세 이동) +- 거래처 월 지출 목록 (클릭 시 거래처원장 상세 이동) +- 전자결재 버튼: 문서 작성_지출 예상 내역서 화면으로 이동 +- 예상 지급일 변경 팝업 + +### 11.12 미수금 현황 (p117) + +- 거래처별 월별 미수금 현황 (매출/입금/어음/누적 미수금) +- 메모 저장 기능 +- 연체 토글 (거래처 상세와 연동) +- 확대/축소 토글 + +### 11.13 악성채권 추심관리 (p118-121) + +**상태:** 추심중, 법적조치, 회수완료, 대손처리 + +**상세:** +- 기본 정보: 거래처 기본 정보 표시 +- 악성채권 등록 ON/OFF +- 담당자 정보, 연락처 정보 +- 필요 서류: 사업자등록증, 세금계산서, 추가 서류 (파일 첨부) +- 악성 채권 정보: 미수금, 상태, 악성채권 발생일/종료일, 연체일수, 본사 담당자, 메모 +- 수취 어음 현황 (어음관리 화면으로 이동) +- 거래처 미수금 현황 (미수금 현황 화면으로 이동) + +### 11.14 입출금 계좌 조회 (p122) + +- 기준정보 > 계좌관리에 등록된 계좌의 자동 입출금 내역 수집 +- 바로빌 API 연동 시 실시간 조회 +- 목록: 은행명, 계좌명, 거래일시, 구분, 적요, 거래처, 입금자/수취인, 입금, 출금, 잔액, 입출금 유형 + +### 11.15 카드 내역 관리 (p123-124) + +- 기준정보 > 카드관리에 등록된 카드의 자동 사용 내역 수집 +- 사용자는 본인 내역 조회 및 사용유형/적요 작성 가능 + +**사용유형:** 미설정, 복리후생비, 접대비, 여비교통비, 차량유지비, 소모품비, 운반비, 통신비, 도서인쇄비, 교육훈련비, 보험료, 광고선전비, 회비, 지급수수료, 세금과공과, 수선비, 임차료, 잡비 + +--- + +## 12. 기준정보 + +### 12.1 직급관리 (p126) + +- 디폴트: 사원, 대리, 과장, 차장, 부장, 이사, 상무, 전무, 부사장, 사장, 회장 +- 추가, 순서 변경 (드래그 & 드랍), 수정, 삭제 +- 사원 설정된 직급은 모두 변경 후 삭제 가능 + +### 12.2 직책관리 (p127) + +- 디폴트: 없음(기본), 팀장, 파트장, 실장, 부서장, 본부장, 센터장, 매니저, 리더 +- 추가, 순서 변경, 수정, 삭제 + +### 12.3 권한관리 (p129-130) + +- 권한 등록/삭제 +- 권한 상세: 권한명, 상태(공개/숨김) +- 메뉴별 권한 설정: 조회, 생성, 수정, 삭제, 승인, 내보내기, 관리 + +### 12.4 근무관리 (p131) + +- 고용 형태: 정규직, 계약직, 파견직, 용역직, 시간제 근로자 +- 주간 근무일: 월~일 체크박스 +- 출근/퇴근 시간 설정 +- 법정 주당 기준 근로시간 (40시간), 주당 연장 근로시간 (12시간) +- 휴게 시작/종료 시간 설정 + +### 12.5 출퇴근관리 (p132) + +**GPS 출퇴근:** +- 사용/미사용 설정 +- 연동 부서 (검색 & 다중 선택) +- 출퇴근 허용 반경: 50M, 100M, 300M, 500M + +**자동 출퇴근:** +- 사용/미사용 설정 +- 연동 부서 + +### 12.6 휴가관리 (p133) + +- 기준: 회계연도 / 입사일 +- 기준일: 월/일 설정 (회계연도 선택 시) + +**기본 연차 설정:** +- 1년간 출근율 80% 이상: 15일 +- 3년 이상 근속 시 2년에 1일 추가 (최대 25일) +- 1년 미만 또는 출근율 80% 미만: 1개월 개근 시 1일씩 (최대 11일) + +### 12.7 카드관리 (p134-135) + +- 카드사 코드, 카드 인증 정보, 비밀번호를 바로빌 API에 전달하여 자동 수집 +- 카드 상세: 카드번호, 카드사, 카드명, 카드 비밀번호 앞 2자리, 유효기간 +- 사용자 정보: 부서/이름/직책 +- 상태: 사용, 정지 (정지 시 자동 조회 중단) + +### 12.8 계좌관리 (p136-138) + +- 계좌 인증 정보, 비밀번호를 바로빌 API에 전달하여 자동 수집 +- 해당 테넌트는 은행에서 빠른 조회 서비스 사전 등록 필수 +- 계좌 상세: 계좌번호, 은행, 계좌명, 계좌 비밀번호, 예금주 +- 상태: 사용, 정지 (정지 시 자동 조회 중지) + +### 12.9 팝업관리 (p139-140) + +- 목록: 대상, 제목, 상태, 작성자, 등록일, 기간 +- 팝업 상세: 대상(전사/부서), 제목, 내용, 기간, 상태(사용함/사용안함) +- 사용함이어도 기간이 아닐 경우 팝업 미노출 + +### 12.10 게시판관리 (p141-142) + +- 모든 테넌트 디폴트: 공지사항, 나의 게시글 (수정/삭제 불가) +- 게시판 등록: 대상(전사/부서), 게시판명, 상태(사용함/사용안함) + +### 12.11 알림설정 (p143-149) + +**전체/개별 ON/OFF 토글** + +**알림 소리 선택:** 기본 알림음, SAM 보이스, ..., 무음 (관리자 등록 음원 목록) +**추가 알림:** 이메일 체크박스 + +**알림 유형:** + +| 카테고리 | 알림 항목 | +|---------|----------| +| 공지 알림 | 공지사항 알림, 이벤트 알림 | +| 거래처 알림 | 일정 알림, 부가세 신고 알림, 종합소득세 신고 알림, 신규 업체 등록 알림, 신용등급 등록 알림 | +| 근태 알림 | 연차 알림, 출근 알림, 지각 알림, 결근 알림 | +| 수주/발주 알림 | 수주 알림, 발주 알림 | +| 전자결재 알림 | 결재요청 알림, 기안>승인 알림, 기안>반려 알림, 기안>완료 알림 | +| 생산 알림 | 안전재고 알림, 생산완료 알림 | + +**항목 설정_알림 팝업:** 개별 알림 ON/OFF 토글 + +--- + +## 13. 보고서 및 분석 (p150-151) + +- TBD (추후 확정) +- 업체별 신용평가 및 보고서 검색 +- 업체별 보고서 및 분석 상세 제공 + +--- + +## 14. 계정정보/회사정보/구독관리/결제내역/고객센터 + +### 14.1 계정정보 (p153) + +- 아이디(이메일), 권한, 상태 +- 비밀번호 변경 버튼 +- 프로필 사진 (250x250px) +- 약관 동의 정보 (동의일시, 철회일시) +- 탈퇴 버튼: 테넌트 마스터가 아닐 경우에만 (모든 테넌트 + SAM 탈퇴) +- 사용중지 버튼: 테넌트 마스터가 아닐 경우에만 (해당 테넌트 사용중지) + +### 14.2 회사정보 (p154-155) + +- 테넌트 마스터에게만 표시 +- 회사 추가 버튼 +- 회사 정보: 운영(영업)에서 입력된 정보 표시, 수정 가능 +- 결제 계좌 정보: SAM 관리자가 등록 (효성 CMS 실물 계약서 기반) + +### 14.3 회사 추가 팝업 (p156) + +- 사업자등록번호 입력 (숫자 10자리) +- 바로빌 조회: 휴폐업 → 알림, 등록됨 → 알림, 미등록 → 매니저에게 알림 발송 + +### 14.4 구독관리 (p157) + +- 테넌트 마스터에게만 표시 +- 구독 정보: 플랜명, 최근/다음 결제일시, 구독금액 +- 사용량: 사용자 수, 저장 공간, AI API 호출 +- 자료 내보내기 버튼 +- 서비스 해지 버튼: "모든 데이터가 삭제되며 복구할 수 없습니다." 확인 Alert + +### 14.5 결제내역 (p158) + +- 테넌트 마스터에게만 표시 +- 목록: 결제일, 구독명, 결제 수단, 구독 기간, 금액, 거래명세서 + +### 14.6 고객센터 - 공지사항 (p159-160) + +- SAM 공지사항 +- 목록: 번호, 제목, 작성자, 등록일, 조회수 +- 상세: 제목, 작성자, 등록일시, 내용, 첨부파일 + +### 14.7 고객센터 - 이벤트 (p161-162) + +- 탭: 진행중인 이벤트, 종료된 이벤트 +- 목록: 번호, 제목, 작성자, 기간, 조회수 + +### 14.8 고객센터 - FAQ (p163) + +- 탭: 전체, 카테고리별 +- 질문 클릭 시 답변 영역 열림/닫힘 토글 + +### 14.9 고객센터 - 1:1 문의 (p164-167) + +- 문의 등록: 상담분류(문의하기/신고하기/건의사항/서비스 오류), 제목, 내용, 첨부파일 +- 문의 상세: 문의 내용 + 답변 내용 +- 수정 버튼: 답변완료 후 비활성화 +- 댓글 등록/조회 + +--- + +## 15. 참조 테이블 + +### 15.1 매출유형 목록 + +| 코드 | 매출유형명 | +|------|----------| +| - | 미설정 | +| 1 | 제품 매출 | +| 2 | 상품 매출 | +| 3 | 부품 매출 | +| 4 | 용역 매출 | +| 5 | 공사 매출 | +| 6 | 임대 수익 | +| 7 | 기타 매출 | + +### 15.2 매입유형 목록 + +| 코드 | 매입유형명 | +|------|----------| +| - | 미설정 | +| 1 | 원재료매입 | +| 2 | 부재료매입 | +| 3 | 상품매입 | +| 4 | 외주가공비 | +| 5 | 소모품비 | +| 6 | 수선비 | +| 7 | 운반비 | +| 8 | 사무용품비 | +| 9 | 임차료 | +| 10 | 수도광열비 | +| 11 | 통신비 | +| 12 | 차량유지비 | +| 13 | 접대비 | +| 14 | 보험료 | +| 15 | 기타용역비 | + +### 15.3 입금유형 목록 + +매출대금, 선수금, 가수금, 임대수익, 이자수익, 보증금 반환, 차입금, 자본금, 부가세 환급, 기타, 미설정 + +### 15.4 출금유형 목록 + +매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타, 미설정 + +### 15.5 카드 사용유형 목록 + +미설정, 복리후생비, 접대비, 여비교통비, 차량유지비, 소모품비, 운반비, 통신비, 도서인쇄비, 교육훈련비, 보험료, 광고선전비, 회비, 지급수수료, 세금과공과, 수선비, 임차료, 잡비 + +--- + +## 관련 문서 + +- SAM 프로젝트 개요: `/home/aweso/sam/docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` +- 원본 PDF: `/home/aweso/sam/docs/plans/SAM_ERP_Storyboard_D1.4_260116.pdf` + +--- + +**최종 업데이트**: 2026-01-16 (D1.4) From 95bbc90f936fedff1316799af226ac143025a408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 23 Feb 2026 10:17:40 +0900 Subject: [PATCH 25/69] =?UTF-8?q?docs:=20[guides]=20.env=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=AC=B8=EC=84=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 키 관리 방식 변경 반영 (DB → .env 전환) - 공유 API 키 동기화 항목 추가 (Gemini, Vertex AI 등) - .env.example 참조 안내 추가 - 누락 항목 현황 최신화 --- guides/production-env-sync.md | 142 +++++++++++++++++----------------- 1 file changed, 70 insertions(+), 72 deletions(-) diff --git a/guides/production-env-sync.md b/guides/production-env-sync.md index 452c6fc..c94d161 100644 --- a/guides/production-env-sync.md +++ b/guides/production-env-sync.md @@ -2,6 +2,7 @@ > **작성일**: 2026-02-21 > **상태**: 설계 확정 +> **최종 업데이트**: 2026-02-23 --- @@ -15,8 +16,19 @@ SAM 프로젝트는 MNG과 API가 **독립적인 `.env` 파일**을 사용하되 ### 1.2 핵심 원칙 - MNG과 API는 **각각 독립된 `.env`** 파일을 보유 +- **모든 API 키는 `.env`에서 관리** (DB `ai_configs` 테이블 사용하지 않음) - 바로빌 설정은 **DB 우선, `.env` 폴백** 구조 - 동기화 필수 항목과 프로젝트 전용 항목을 명확히 구분 +- 각 프로젝트에 `.env.example` 파일로 필요한 키를 문서화 + +### 1.3 `.env.example` 파일 + +| 프로젝트 | 파일 | 설명 | +|---------|------|------| +| MNG | `mng/.env.example` | MNG 프로젝트 전체 환경 변수 템플릿 | +| API | `api/.env.example` | API 프로젝트 전체 환경 변수 템플릿 | + +> 새 서버 설정 시 `.env.example`을 `.env`로 복사한 후 실제 값을 입력한다. --- @@ -40,7 +52,17 @@ SAM 프로젝트는 MNG과 API가 **독립적인 `.env` 파일**을 사용하되 └──────────┘ └──────────┘ ``` -### 2.2 설정 로드 우선순위 (바로빌) +### 2.2 API 키 관리 방식 (2026-02-23 변경) + +``` +변경 전: DB ai_configs 테이블 → .env 폴백 +변경 후: .env 전용 (ai_configs 테이블 미사용) +``` + +모든 외부 API 키(Gemini, Claude, Notion, GCS)는 `.env`에서 직접 관리한다. +`AiConfig` 모델의 정적 메서드(`getActiveGemini()` 등)가 `config('services.*')`를 읽어 인스턴스를 생성하므로, 기존 서비스 코드 변경 없이 동작한다. + +### 2.3 설정 로드 우선순위 (바로빌) ``` 1순위: DB barobill_members.server_mode (테넌트별 모드) @@ -56,7 +78,27 @@ SAM 프로젝트는 MNG과 API가 **독립적인 `.env` 파일**을 사용하되 > **경고: 아래 항목은 MNG과 API 양쪽에서 동일한 값이어야 한다.** -### 3.1 바로빌 SOAP API +### 3.1 공유 API 키 (양쪽 동일 값 필수) + +| 환경 변수 | MNG `.env` | API `.env` | 설명 | +|-----------|-----------|-----------|------| +| `GEMINI_API_KEY` | ✅ 설정됨 | ✅ 설정됨 | Gemini AI API 키 | +| `GEMINI_MODEL` | ✅ `gemini-2.0-flash` | ✅ `gemini-2.0-flash` | Gemini 모델 | +| `GEMINI_BASE_URL` | ✅ 설정됨 | ✅ 설정됨 | Gemini API URL | +| `GEMINI_PROJECT_ID` | ✅ `codebridge-chatbot` | ✅ `codebridge-chatbot` | GCP 프로젝트 ID | +| `VERTEX_AI_PROJECT_ID` | ✅ 설정됨 | ✅ 설정됨 | Vertex AI 프로젝트 | +| `VERTEX_AI_LOCATION` | ✅ `us-central1` | ✅ `us-central1` | Vertex AI 리전 | +| `GOOGLE_STORAGE_BUCKET` | ✅ 설정됨 | ✅ 설정됨 | GCS 버킷 이름 | + +### 3.2 내부 통신 키 + +| 환경 변수 | MNG `.env` | API `.env` | 설명 | +|-----------|-----------|-----------|------| +| `INTERNAL_EXCHANGE_SECRET` | ✅ 설정됨 | ✅ 설정됨 | HMAC 서버 간 검증 | + +> **경고: 불일치 시 MNG → API HTTP 호출이 인증 실패한다.** + +### 3.3 바로빌 SOAP API | 환경 변수 | MNG `.env` | API `.env` | 설명 | |-----------|-----------|-----------|------| @@ -65,17 +107,9 @@ SAM 프로젝트는 MNG과 API가 **독립적인 `.env` 파일**을 사용하되 | `BAROBILL_CORP_NUM` | ⚠️ 현재 미설정 | ✅ 설정됨 | 파트너 사업자번호 | | `BAROBILL_TEST_MODE` | ⚠️ 현재 미설정 | `true` | 테스트/운영 전환 플래그 | -> **참고**: MNG의 `.env`에 바로빌 항목이 누락되어 있지만, MNG는 **DB(`barobill_configs`)를 우선 참조**하므로 현재 정상 동작한다. 단, DB에 설정이 없는 경우 `.env` 폴백이 작동하지 않는다. +> **참고**: MNG는 **DB(`barobill_configs`)를 우선 참조**하므로 현재 정상 동작한다. -### 3.2 내부 통신 키 - -| 환경 변수 | MNG `.env` | API `.env` | 설명 | -|-----------|-----------|-----------|------| -| `INTERNAL_EXCHANGE_SECRET` | ⚠️ 현재 미설정 | ✅ 설정됨 | HMAC 서버 간 검증 | - -> **경고: 불일치 시 MNG → API HTTP 호출이 인증 실패한다.** - -### 3.3 공유 DB 접속 정보 +### 3.4 공유 DB 접속 정보 | 환경 변수 | 동기화 필수 | 설명 | |-----------|-----------|------| @@ -92,10 +126,12 @@ SAM 프로젝트는 MNG과 API가 **독립적인 `.env` 파일**을 사용하되 | 환경 변수 | 설명 | 비고 | |-----------|------|------| -| `API_BASE_URL` | API 서버 URL | 운영: `https://api.sam.kr` | +| `API_BASE_URL` | API 서버 URL | 운영: `https://api.codebridge-x.com` | | `FLOW_TESTER_API_KEY` | API 테스터 키 | API의 `api_keys` 테이블과 일치 필요 | | `MENU_SYNC_API_KEY` | 메뉴 동기화 키 | MNG 전용 | -| `CHANDJ_DB_*` | 레거시 DB 접속 | MNG에서만 사용 | +| `NOTION_API_KEY` | Notion API 키 | MNG에서만 사용 | +| `NOTION_VERSION` | Notion API 버전 | MNG에서만 사용 | +| `KMA_SERVICE_KEY` | 기상청 API 키 | MNG에서만 사용 | ### 4.2 API 전용 @@ -104,17 +140,16 @@ SAM 프로젝트는 MNG과 API가 **독립적인 `.env` 파일**을 사용하되 | `CLAUDE_API_KEY` | Claude AI API 키 | API에서만 사용 | | `SANCTUM_*` | 토큰 만료 설정 | API 인증 전용 | | `LOG_SLACK_WEBHOOK_URL` | Slack 로그 알림 | API 전용 | +| `CHANDJ_DB_*` | 레거시 DB 접속 | API에서만 사용 | +| `BAROBILL_*` | 바로빌 SOAP API | API 중심 (MNG는 DB 폴백) | +| `L5_SWAGGER_*` | Swagger 문서 설정 | API 전용 | -### 4.3 양쪽 독립 설정 (동일 서비스, 각자 관리) +### 4.3 양쪽 독립 설정 (동일 서비스, 경로가 다름) -| 환경 변수 | MNG 용도 | API 용도 | -|-----------|---------|---------| -| `GEMINI_API_KEY` | 음성 어시스턴트, OCR | AI 리포트 생성 | -| `VERTEX_AI_*` | 영상 생성 | 영상 생성 | -| `GOOGLE_APPLICATION_CREDENTIALS` | STT, GCS | STT, GCS | -| `FCM_*` | 푸시 알림 | 푸시 알림 | - -> **참고**: 같은 키를 사용해도 무방하지만, 서비스 어카운트 **파일 경로**가 다르므로 주의 +| 환경 변수 | 주의사항 | +|-----------|---------| +| `GOOGLE_APPLICATION_CREDENTIALS` | 컨테이너 내부 **파일 경로**가 다르므로 각자 설정 | +| `FCM_SA_PATH` | 컨테이너 내부 **파일 경로**가 다르므로 각자 설정 | --- @@ -128,6 +163,7 @@ SAM 프로젝트는 MNG과 API가 **독립적인 `.env` 파일**을 사용하되 □ Google 서비스 어카운트 운영 경로 확인 □ Firebase 서비스 어카운트 운영 경로 확인 □ DB 백업 완료 +□ .env.example 참조하여 누락 키 없는지 확인 ``` ### 5.2 Step 1: DB 설정 전환 (바로빌) @@ -190,7 +226,7 @@ WHERE server_mode = 'test'; ### 5.4 Step 3: API `.env` 수정 ```bash -# /home/aweso/sam/api/.env +# /home/webservice/api/.env (서버) # 변경 전 BAROBILL_TEST_MODE=true @@ -202,48 +238,25 @@ BAROBILL_TEST_MODE=false ### 5.5 Step 4: MNG `.env` 수정 및 누락 항목 추가 ```bash -# /home/aweso/sam/mng/.env 에 추가/수정 +# /home/webservice/mng/.env (서버)에 추가/수정 # ─── 바로빌 SOAP API (누락 항목 추가) ─── BAROBILL_CERT_KEY_TEST=<테스트_CERTKEY> BAROBILL_CERT_KEY_PROD=<운영_CERTKEY> BAROBILL_CORP_NUM=<사업자번호> BAROBILL_TEST_MODE=false - -# ─── 내부 통신 키 (누락 항목 추가) ─── -INTERNAL_EXCHANGE_SECRET=k8sJ2mN4pQ7rT9wX3yB6cF1hL5vZ0aE8 ``` -### 5.6 Step 5: Google 서비스 어카운트 경로 통일 - -| 프로젝트 | 현재 경로 (레거시) | 권장 경로 | -|---------|-----------------|----------| -| MNG | `/var/www/sales/apikey/` | `/var/www/mng/apikey/` | -| API | `/var/www/mng/apikey/` | `/var/www/mng/apikey/` (유지) | +### 5.6 Step 5: 캐시 클리어 및 재시작 ```bash -# MNG .env 수정 (레거시 경로 → 통일 경로) -# 변경 전 -GOOGLE_APPLICATION_CREDENTIALS=/var/www/sales/apikey/google_service_account.json +# Docker 환경 +docker exec sam-api-1 php artisan config:clear && docker exec sam-api-1 php artisan cache:clear +docker exec sam-mng-1 php artisan config:clear && docker exec sam-mng-1 php artisan cache:clear -# 변경 후 -GOOGLE_APPLICATION_CREDENTIALS=/var/www/mng/apikey/google_service_account.json -``` - -### 5.7 Step 6: 캐시 클리어 및 재시작 - -```bash -# API 캐시 클리어 -docker exec sam-api-1 php artisan config:clear -docker exec sam-api-1 php artisan cache:clear - -# MNG 캐시 클리어 -docker exec sam-mng-1 php artisan config:clear -docker exec sam-mng-1 php artisan cache:clear - -# 서버 환경에서는 -ssh sam-server "cd /home/webservice/api && php artisan config:clear && php artisan cache:clear" -ssh sam-server "cd /home/webservice/mng && php artisan config:clear && php artisan cache:clear" +# bare-metal 환경 +cd /home/webservice/api && php artisan config:clear && php artisan cache:clear +cd /home/webservice/mng && php artisan config:clear && php artisan cache:clear ``` --- @@ -264,6 +277,7 @@ ssh sam-server "cd /home/webservice/mng && php artisan config:clear && php artis ``` □ Gemini AI 음성 어시스턴트 동작 확인 (MNG) +□ Notion 검색 AI 동작 확인 (MNG) □ FCM 푸시 알림 전송 확인 (MNG/API) □ Google STT/GCS 파일 업로드 확인 □ MNG → API HTTP 호출 인증 성공 확인 @@ -290,22 +304,6 @@ docker exec sam-mng-1 php artisan config:clear --- -## 7. MNG `.env` 현재 누락 항목 요약 - -> **경고: 현재 MNG `.env`에 아래 항목이 누락되어 있다. DB 폴백으로 동작하지만, 안정성을 위해 추가를 권장한다.** - -| 환경 변수 | 상태 | 위험도 | 설명 | -|-----------|------|--------|------| -| `BAROBILL_CERT_KEY_TEST` | ❌ 누락 | 🟡 중요 | DB 폴백 시 빈값 | -| `BAROBILL_CERT_KEY_PROD` | ❌ 누락 | 🟡 중요 | DB 폴백 시 빈값 | -| `BAROBILL_CORP_NUM` | ❌ 누락 | 🟡 중요 | DB 폴백 시 빈값 | -| `BAROBILL_TEST_MODE` | ❌ 누락 | 🟡 중요 | 기본값 `true` 사용 중 | -| `INTERNAL_EXCHANGE_SECRET` | ❌ 누락 | 🔴 필수 | 서버 간 HMAC 검증 실패 가능 | -| `GEMINI_API_KEY` | 빈값 | 🟡 중요 | 음성 어시스턴트 미작동 | -| `FCM_PROJECT_ID` | 빈값 | 🟡 중요 | 푸시 알림 미작동 | - ---- - ## 관련 문서 - [Docker 환경 구성](../specs/docker-setup.md) @@ -314,4 +312,4 @@ docker exec sam-mng-1 php artisan config:clear --- -**최종 업데이트**: 2026-02-21 +**최종 업데이트**: 2026-02-23 From 34f9cf41e7a403e85c6d3521362dc91042ba9cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 24 Feb 2026 19:44:05 +0900 Subject: [PATCH 26/69] =?UTF-8?q?docs:=20[barobill-kakaotalk]=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=ED=86=A1=20=EC=97=B0=EB=8F=99=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94=20(=ED=8A=B8=EB=9F=AC=EB=B8=94?= =?UTF-8?q?=EC=8A=88=ED=8C=85,=20v2=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/barobill-kakaotalk/README.md | 511 +++++++++++++++----------- 1 file changed, 289 insertions(+), 222 deletions(-) diff --git a/features/barobill-kakaotalk/README.md b/features/barobill-kakaotalk/README.md index 88466e9..84b923f 100644 --- a/features/barobill-kakaotalk/README.md +++ b/features/barobill-kakaotalk/README.md @@ -1,9 +1,9 @@ # 바로빌 카카오톡 (알림톡/친구톡) 연동 -> **문서 버전**: 0.1 (초안) +> **문서 버전**: 1.0 > **작성일**: 2026-02-14 -> **최종 수정**: 2026-02-14 -> **상태**: 개발 중 (사전 준비 단계) +> **최종 수정**: 2026-02-24 +> **상태**: 운영 중 (전자계약 알림톡 발송 완료) > **대상 프로젝트**: MNG --- @@ -23,7 +23,9 @@ | 카카오톡 채널 개설 | **완료** (2026-02-20) | 채널 ID: `@codebridge`, 채널명: (주)코드브릿지엑스 | | 바로빌 카카오톡 서비스 신청 | **완료** (2026-02-20) | 바로빌 관리자 페이지에서 카카오톡 서비스 활성화 | | 채널 연동 (바로빌↔카카오) | **완료** (2026-02-20) | 바로빌 관리 URL에서 채널 연동 처리 | -| 알림톡 템플릿 등록/검수 | **심사 중** (2026-02-20 접수) | 2종 접수, 카카오 검수 영업일 기준 최대 3일 | +| 바로빌 파트너 과금 설정 | **완료** (2026-02-23) | 바로빌 측에서 파트너사 과금 설정 완료 | +| 알림톡 템플릿 v1 검수 | **완료** (2026-02-22) | `전자계약_서명요청`, `전자계약_리마인드` 2종 승인 | +| 알림톡 템플릿 v2 검수 | **심사 중** (2026-02-24 접수) | 버튼 URL에 `#{토큰}` 변수 포함 2종 재등록 | > 상세 등록 가이드: [카카오톡 알림톡 채널 및 템플릿 등록 가이드](../../guides/카카오톡-알림톡-채널-템플릿-등록.md) @@ -51,8 +53,11 @@ SAM MNG (브라우저) ├─ [페이지] /barobill/kakaotalk/* ← Blade 뷰 │ KakaotalkController (페이지 렌더링) │ - └─ [API] /api/admin/barobill/kakaotalk/* ← AJAX 호출 - BarobillKakaotalkController + ├─ [API] /api/admin/barobill/kakaotalk/* ← AJAX 호출 + │ BarobillKakaotalkController + │ + └─ [전자계약] /esign/* ← 자동 발송 + EsignApiController::sendAlimtalk() │ └─ BarobillService (SOAP 클라이언트) │ @@ -70,47 +75,293 @@ SAM MNG (브라우저) --- -## 3. 구현 현황 +## 3. 전자계약 알림톡 연동 (핵심) -### 3.1 완료된 항목 +### 3.1 발송 흐름 -| 구분 | 파일 | 설명 | -|------|------|------| -| SOAP 서비스 | `app/Services/Barobill/BarobillService.php` | kakaotalk SOAP 클라이언트 + 15개 API 메서드 추가 | -| API 컨트롤러 | `app/Http/Controllers/Api/Admin/Barobill/BarobillKakaotalkController.php` | 15개 API 엔드포인트 | -| 페이지 컨트롤러 | `app/Http/Controllers/Barobill/KakaotalkController.php` | 6개 페이지 (index, channels, templates, send, history, guide) | -| 라우트 (web) | `routes/web.php` | `/barobill/kakaotalk/*` 6개 라우트 | -| 라우트 (api) | `routes/api.php` | `/api/admin/barobill/kakaotalk/*` 14개 라우트 | -| 대시보드 뷰 | `views/barobill/kakaotalk/index.blade.php` | 채널 상태 요약, 빠른 메뉴 | -| 채널 관리 뷰 | `views/barobill/kakaotalk/channels/index.blade.php` | 채널 목록, 관리 URL 연결 | -| 템플릿 관리 뷰 | `views/barobill/kakaotalk/templates/index.blade.php` | 채널별 템플릿 조회, 상세 모달 | -| 발송 뷰 | `views/barobill/kakaotalk/send/index.blade.php` | 알림톡/친구톡 탭, 발송 폼 | -| 전송내역 뷰 | `views/barobill/kakaotalk/history/index.blade.php` | 전송키 조회, 예약 취소 | -| 사용법 가이드 뷰 | `views/barobill/kakaotalk/guide.blade.php` | 초보자용 8단계 가이드 | -| 메뉴 등록 | DB (menus 테이블) | 로컬/서버 모두 등록 완료 | +``` +전자계약 생성 (E-Sign) + │ + ├─ [1단계] EsignApiController::sendAlimtalk() + │ │ + │ ├─ 채널 ID 조회 (getKakaotalkChannelId) + │ ├─ 템플릿 본문 + 버튼 조회 (getTemplateData) + │ ├─ 변수 치환 (#{이름}, #{계약명}, #{기한}) + │ └─ SendATKakaotalkEx 호출 + │ + ├─ [2단계] 바로빌 접수 → SendKey 반환 + │ + ├─ [3단계] 3초 대기 후 GetSendKakaotalk으로 전달 결과 확인 + │ │ + │ ├─ ResultCode = 1 → 성공 + │ └─ ResultCode != 1 → 실패 (에러 반환) + │ + └─ [이메일 폴백] 알림톡 실패 시 이메일로 자동 전환 +``` -### 3.2 미완료 / 검증 필요 항목 +### 3.2 등록된 템플릿 (v1 — 현재 운영) -| 항목 | 상태 | 비고 | -|------|------|------| -| 채널 API 실제 호출 테스트 | **대기** | 카카오 채널 개설 후 가능 | -| 템플릿 조회 테스트 | **대기** | 템플릿 등록/검수 후 가능 | -| 알림톡 발송 테스트 | **대기** | 채널+템플릿 준비 후 가능 | -| 친구톡 발송 테스트 | **대기** | 채널 친구 추가 후 가능 | -| SMS 대체발송 테스트 | **대기** | 바로빌 SMS 서비스 활성화 필요 | -| 대량 발송 테스트 | **대기** | 단건 테스트 완료 후 | -| 에러 핸들링 고도화 | **대기** | 실제 API 응답 확인 후 개선 | +**`전자계약_서명요청`** + +``` + 안녕하세요, #{이름}님. + 전자계약 서명 요청이 도착했습니다. + + ■ 계약명: #{계약명} + ■ 서명 기한: #{기한} + + 아래 버튼을 눌러 계약서를 확인하고 서명해 주세요. +``` + +- 버튼: `계약서 확인하기` (WL) +- Url1/Url2: `https://mng.codebridge-x.com` + +**`전자계약_리마인드`** + +``` +안녕하세요, #{이름}님. +아직 서명이 완료되지 않은 전자계약이 있습니다. + + ■ 계약명: #{계약명} + ■ 서명 기한: #{기한} + + 기한 내에 서명을 완료해 주세요. +``` + +- 버튼: `계약서 확인하기` (WL) +- Url1/Url2: `https://mng.codebridge-x.com` + +### 3.3 등록 예정 템플릿 (v2 — 심사 중) + +> **2026-02-24 재등록**: 버튼 URL에 `#{토큰}` 변수를 포함하여 동적 서명 URL 지원 + +- Url1/Url2: `https://mng.codebridge-x.com/esign/sign/#{토큰}` + +v2 승인 후 코드 변경 필요: +- `EsignApiController::sendAlimtalk()`에서 동적 `$signUrl`을 버튼 URL로 전달 +- 현재 코드의 등록된 URL 그대로 사용 → 동적 URL 사용으로 전환 + +### 3.4 임시 우회: 로그인 페이지 서명 확인 + +v1 템플릿의 버튼 URL이 대시보드(`https://mng.codebridge-x.com`)로 고정되어 있어, +로그인 페이지에 전화번호 기반 서명 확인 기능을 추가하였다. + +``` +알림톡 버튼 클릭 → https://mng.codebridge-x.com → 로그인 페이지 + │ + └─ "전자계약 서명하기" 섹션 + │ + ├─ 전화번호 입력 + ├─ POST /esign/verify-phone + └─ 대기 중인 계약 조회 → /esign/sign/{token} 리다이렉트 +``` + +- 라우트: `POST /esign/verify-phone` +- 컨트롤러: `EsignPublicController::verifyPhone()` +- v2 템플릿 승인 후에도 유지 (비로그인 사용자 대응) + +### 3.5 관련 파일 + +| 파일 | 역할 | +|------|------| +| `app/Http/Controllers/ESign/EsignApiController.php` | `sendAlimtalk()`, `getTemplateData()` | +| `app/Http/Controllers/ESign/EsignPublicController.php` | `verifyPhone()` 전화번호 확인 | +| `app/Services/Barobill/BarobillService.php` | SOAP 클라이언트, `sendATKakaotalkEx()` | +| `resources/views/auth/login.blade.php` | 로그인 페이지 서명 확인 UI | +| `routes/web.php` | `/esign/verify-phone` 라우트 | --- -## 4. API 메서드 목록 +## 4. 트러블슈팅 (실전 경험) -### 4.1 BarobillService 카카오톡 메서드 +> **경고: 아래 내용은 실제 연동 과정에서 발견한 핵심 이슈다. 반드시 숙지할 것.** + +### 4.1 바로빌 API 응답 구조 + +바로빌 SOAP 응답은 `stdClass` 객체로 반환된다. 배열이 아니므로 주의: + +```php +// ❌ 잘못된 접근 +$channels = $result['data']; // 배열이 아님 + +// ✅ 올바른 접근 +$data = $result['data']; // stdClass +$channels = is_array($data->KakaotalkChannel) + ? $data->KakaotalkChannel + : [$data->KakaotalkChannel]; // 1건이면 객체, N건이면 배열 +``` + +### 4.2 SendKey vs ResultCode (2단계 검증 필수) + +> **핵심**: 바로빌이 SendKey를 반환해도 **실제 카카오톡 전달이 실패할 수 있다.** + +``` +[1단계] SendATKakaotalkEx 호출 + → SendKey 반환 (예: BB_6648603713_AT_3044107_260224) + → 이것은 "접수 성공"이지 "전달 성공"이 아님! + +[2단계] 3초 후 GetSendKakaotalk(SendKey) 호출 + → ResultCode = 1: 전달 성공 ✅ + → ResultCode = 4: 템플릿 데이터 일치 오류 ❌ + → ResultCode != 1: 기타 실패 ❌ +``` + +```php +// 반드시 2단계 검증 필요 +if ($result['success'] && is_string($result['data'])) { + $sendKey = $result['data']; + sleep(3); // 카카오톡 전달 대기 + $sendResult = $barobill->getSendKakaotalk($member->biz_no, $sendKey); + $resultCode = $sendResult['data']->ResultCode ?? null; + if ($resultCode != 1) { + // 실패 처리! + } +} +``` + +### 4.3 템플릿 URL 정확 일치 규칙 + +> **핵심**: 버튼 URL은 등록된 템플릿의 URL과 **정확히 일치**해야 한다. 1글자라도 다르면 실패. + +| 등록된 URL | 전송 시 URL | 결과 | +|------------|------------|------| +| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com` | ResultCode=1 (성공) | +| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com/esign/sign/xxx` | ResultCode=4 (실패) | +| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com?sign=xxx` | ResultCode=4 (실패) | +| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com#sign=xxx` | ResultCode=4 (실패) | + +- 경로 추가: 실패 +- 쿼리 파라미터 추가: 실패 +- URL 프래그먼트(#) 추가: 실패 +- **동적 URL을 사용하려면 템플릿에 `#{변수}` 포함하여 재등록 필요** + +### 4.4 SmsReply 오류 (-31325) + +`SmsReply` 파라미터가 `'S'`(대체발송 사용)인데 `SmsSenderNum`이 비어있으면 `-31325` 오류 발생. + +```php +// ❌ 오류 발생 +'SmsReply' => empty($smsMessage) ? 'N' : 'S', // SmsSenderNum이 비어도 S로 설정 + +// ✅ 수정 +'SmsReply' => (empty($smsMessage) || empty($smsSenderNum)) ? 'N' : 'S', +``` + +### 4.5 SOAP 파라미터 구조 + +바로빌 SOAP API의 파라미터 구조에 주의: + +```php +// 올바른 구조 +$params = [ + 'CorpNum' => $bizNo, // 사업자번호 (하이픈 포함: 123-45-67890) + 'SenderID' => $barobillId, // 바로빌 계정 ID + 'YellowId' => $channelId, // 카카오 채널 ID (@codebridge) + 'TemplateName' => '전자계약_서명요청', + 'SendDT' => '', // 즉시발송: 빈 문자열 + 'SmsReply' => 'N', // SMS 발신번호 없으면 반드시 'N' + 'SmsSenderNum' => '', + 'KakaotalkMessage' => [ + 'ReceiverName' => $name, + 'ReceiverNum' => $phone, // 하이픈 없이: 01012345678 + 'Title' => '', + 'Message' => $message, // 템플릿 변수 치환 완료된 본문 + 'SmsMessage' => '', + 'SmsSubject' => '', + 'Buttons' => ['KakaotalkButton' => $buttons], // 버튼 배열 + ], +]; +``` + +### 4.6 에러 코드 정리 + +| 코드 | 메시지 | 원인 | 해결 | +|------|--------|------|------| +| 1 | 성공 | 정상 전달 | - | +| 4 | 템플릿 데이터 일치 오류 | 본문/버튼 URL이 등록 템플릿과 불일치 | 등록된 템플릿과 동일하게 전송 | +| -31325 | 대체문자 유형 오류 | SmsReply=S인데 SmsSenderNum 비어있음 | SmsReply를 N으로 설정 | +| 음수값 | 바로빌 API 오류 | 파라미터 오류 또는 서비스 미설정 | 바로빌 에러코드 문서 참조 | + +--- + +## 5. 구현 현황 + +### 5.1 완료된 항목 + +| 구분 | 파일 | 설명 | +|------|------|------| +| SOAP 서비스 | `app/Services/Barobill/BarobillService.php` | kakaotalk SOAP 클라이언트 + 15개 API 메서드 | +| 전자계약 알림톡 | `app/Http/Controllers/ESign/EsignApiController.php` | `sendAlimtalk()`, `getTemplateData()` | +| 서명 확인 | `app/Http/Controllers/ESign/EsignPublicController.php` | `verifyPhone()` 전화번호 기반 서명 확인 | +| API 컨트롤러 | `app/Http/Controllers/Api/Admin/Barobill/BarobillKakaotalkController.php` | 15개 API 엔드포인트 | +| 페이지 컨트롤러 | `app/Http/Controllers/Barobill/KakaotalkController.php` | 6개 관리 페이지 | +| 로그인 페이지 | `resources/views/auth/login.blade.php` | 전자계약 서명하기 섹션 | +| 라우트 | `routes/web.php` | `/esign/verify-phone`, `/barobill/kakaotalk/*` | +| 메뉴 등록 | DB (menus 테이블) | 로컬/서버 모두 등록 완료 | + +### 5.2 검증 완료 항목 + +| 항목 | 결과 | 날짜 | +|------|------|------| +| 채널 API 호출 | **성공** | 2026-02-22 | +| 템플릿 조회 | **성공** | 2026-02-22 | +| 알림톡 발송 (본문) | **성공** (ResultCode=1) | 2026-02-24 | +| 알림톡 버튼 URL | **성공** (등록된 URL 사용 시) | 2026-02-24 | +| 전달 결과 확인 (2단계) | **구현 완료** | 2026-02-24 | +| 로그인 페이지 서명 확인 | **성공** | 2026-02-24 | + +### 5.3 대기 중인 항목 + +| 항목 | 상태 | 비고 | +|------|------|------| +| 템플릿 v2 승인 | **심사 중** | 버튼 URL에 `#{토큰}` 변수 포함 | +| v2 승인 후 코드 수정 | **대기** | 동적 서명 URL을 버튼에 전달 | +| `전자계약_완료` 템플릿 | **미등록** | 서명 완료 알림 발송용 | +| SMS 대체발송 | **미설정** | 발신번호 등록 필요 | +| 친구톡 발송 | **대기** | 채널 친구 추가 후 가능 | +| 대량 발송 | **대기** | 단건 안정화 후 | + +--- + +## 6. v2 템플릿 승인 후 코드 변경 가이드 + +### 6.1 변경 대상 + +`EsignApiController::sendAlimtalk()` (약 1059~1063행) + +### 6.2 현재 코드 (v1) + +```php +// 등록된 버튼 URL을 그대로 사용 (동적 URL 사용 시 템플릿 불일치 오류) +$buttons = ! empty($templateButtons) ? $templateButtons : [ + ['Name' => '계약서 확인하기', 'ButtonType' => 'WL', + 'Url1' => 'https://mng.codebridge-x.com', 'Url2' => 'https://mng.codebridge-x.com'], +]; +``` + +### 6.3 변경 코드 (v2 승인 후) + +```php +// v2 템플릿: 버튼 URL에 동적 서명 URL 사용 +$buttons = [ + ['Name' => '계약서 확인하기', 'ButtonType' => 'WL', + 'Url1' => $signUrl, 'Url2' => $signUrl], +]; +``` + +- `$signUrl`은 1033행에서 이미 생성됨: `config('app.url').'/esign/sign/'.$signer->access_token` +- `getTemplateData()`에서 등록된 버튼 조회는 더 이상 필요 없음 (제거 가능) + +--- + +## 7. API 메서드 목록 + +### 7.1 BarobillService 카카오톡 메서드 | 메서드 | SOAP Action | 설명 | |--------|-------------|------| | `getKakaotalkChannels` | `GetKakaotalkChannels` | 채널 목록 조회 | -| `getKakaotalkChannelManagementUrl` | `GetKakaotalkChannelManagementURL` | 채널 관리 URL (바로빌 페이지) | +| `getKakaotalkChannelManagementUrl` | `GetKakaotalkChannelManagementURL` | 채널 관리 URL | | `getKakaotalkTemplates` | `GetKakaotalkTemplates` | 템플릿 목록 조회 | | `getKakaotalkTemplateManagementUrl` | `GetKakaotalkTemplateManagementURL` | 템플릿 관리 URL | | `sendATKakaotalk` | `SendATKakaotalk` | 알림톡 단건 발송 | @@ -124,199 +375,14 @@ SAM MNG (브라우저) | `getSendKakaotalks` | `GetSendKakaotalks` | 전송 결과 다건 조회 | | `cancelReservedKakaotalk` | `CancelReservedKakaotalk` | 예약 전송 취소 | -### 4.2 REST API 엔드포인트 - -| Method | URL | 설명 | -|--------|-----|------| -| GET | `/api/admin/barobill/kakaotalk/channels` | 채널 목록 | -| GET | `/api/admin/barobill/kakaotalk/channels/management-url` | 채널 관리 URL | -| GET | `/api/admin/barobill/kakaotalk/templates` | 템플릿 목록 | -| GET | `/api/admin/barobill/kakaotalk/templates/management-url` | 템플릿 관리 URL | -| POST | `/api/admin/barobill/kakaotalk/send/alimtalk` | 알림톡 단건 | -| POST | `/api/admin/barobill/kakaotalk/send/alimtalk-bulk` | 알림톡 대량 | -| POST | `/api/admin/barobill/kakaotalk/send/friendtalk` | 친구톡 텍스트 | -| POST | `/api/admin/barobill/kakaotalk/send/friendtalk-image` | 친구톡 이미지 | -| POST | `/api/admin/barobill/kakaotalk/send/friendtalk-wide` | 친구톡 와이드 | -| GET | `/api/admin/barobill/kakaotalk/send/{sendKey}` | 전송 결과 단건 | -| POST | `/api/admin/barobill/kakaotalk/send/results` | 전송 결과 다건 | -| DELETE | `/api/admin/barobill/kakaotalk/send/{sendKey}/cancel` | 예약 취소 | - --- -## 5. WSDL 데이터 타입 - -### 5.1 핵심 타입 - -``` -KakaotalkChannel -├── ChannelId (string) 채널 ID (@로 시작) -├── ChannelName (string) 채널명 -└── Status (int) 상태 - -KakaotalkTemplate -├── ChannelId (string) 채널 ID -├── TemplateName (string) 템플릿 이름 -├── TemplateContent (string) 템플릿 본문 -├── TemplateExtra (string) 부가 정보 -├── Status (int) 검수 상태 -└── Buttons (array) 버튼 목록 - -KakaotalkATMessage (알림톡) -├── ReceiverName (string) 수신자 이름 -├── ReceiverNum (string) 수신자 번호 (01012345678) -├── Title (string) 제목 (강조 표시용) -├── Message (string) 메시지 (템플릿 변수 치환 후) -├── SmsMessage (string) SMS 대체 메시지 -└── SmsSubject (string) SMS 대체 제목 - -KakaotalkFTMessage (친구톡) -├── ReceiverName (string) 수신자 이름 -├── ReceiverNum (string) 수신자 번호 -├── Message (string) 메시지 (자유 형식) -├── SmsMessage (string) SMS 대체 메시지 -├── SmsSubject (string) SMS 대체 제목 -└── Buttons (array) 버튼 목록 - -KakaotalkButton -├── Name (string) 버튼 텍스트 -├── ButtonType (string) WL(웹링크), AL(앱링크), BK(봇키워드), MD(메시지전달) -├── Url1 (string) 모바일 URL -└── Url2 (string) PC URL -``` - ---- - -## 6. 메뉴 구조 - -### 6.1 사이드바 메뉴 - -``` -바로빌 > 카카오톡 -├── 카카오톡 (대시보드) /barobill/kakaotalk -├── 채널관리 /barobill/kakaotalk/channels -├── 템플릿관리 /barobill/kakaotalk/templates -├── 발송 /barobill/kakaotalk/send -├── 전송내역 /barobill/kakaotalk/history -└── 사용법 /barobill/kakaotalk/guide -``` - -### 6.2 메뉴 DB 정보 - -| 환경 | 부모 메뉴 ID (카카오톡) | 하위 메뉴 ID 범위 | -|------|------------------------|-------------------| -| 로컬 | 15614 | 15615 ~ 15619 | -| 서버 | 15470 | 15471 ~ 15475 | - ---- - -## 7. 다음 단계 (TODO) - -### 7.1 사전 준비 (비개발) - -1. [ ] 법인 명의 휴대폰으로 카카오톡 채널 개설 -2. [ ] 바로빌 관리자에서 카카오톡 서비스 신청 -3. [ ] 바로빌 채널 관리 URL에서 카카오 채널 연동 -4. [ ] 알림톡 템플릿 등록 및 카카오 검수 대기 - -### 7.2 개발 (채널 연동 후) - -1. [ ] 테스트 서버에서 `GetKakaotalkChannels` 호출 → 채널 목록 확인 -2. [ ] `GetKakaotalkTemplates` 호출 → 템플릿 목록 확인 -3. [ ] 알림톡 테스트 발송 → 응답/상태 확인 -4. [ ] 친구톡 테스트 발송 -5. [ ] 에러 응답 코드 정리 및 핸들링 보강 -6. [ ] SMS 대체발송 테스트 -7. [ ] 대량 발송 테스트 (수신자 다건) -8. [ ] 운영 서버 전환 (`testws` → `ws`) - -### 7.3 추후 고도화 - -- [ ] 발송 이력 DB 저장 (현재는 바로빌 API 조회만) -- [ ] 자동 발송 연동 (주문 확인, 배송 알림 등) -- [ ] 발송 통계 대시보드 -- [ ] 엑셀 업로드 대량 발송 -- [ ] 주소록(고객 DB) 연동 - ---- - -## 8. 활용 계획: 전자계약(E-Sign) 알림톡 연동 - -> **상세 구현 계획서**: [plans/esign-alimtalk-integration.md](../../plans/esign-alimtalk-integration.md) -> UI/UX 변경, 백엔드 로직, DB 변경, 카카오 템플릿 3종, 구현 순서 등 포함 - -### 8.1 배경 - -현재 전자계약은 **이메일**로 발송하고 있으나, 열람률이 낮고 확인이 지연되는 문제가 있다. -카카오톡 알림톡으로 전환하면 즉시 알림이 도달하여 계약 체결 속도를 크게 개선할 수 있다. - -### 8.2 발송 흐름 - -``` -전자계약 생성 (E-Sign) - │ - ├─ [기존] 이메일 발송 - │ - └─ [추가] 알림톡 발송 (SendATKakaotalkEx) - │ - ├─ 메시지: "{회사명}에서 전자계약서가 도착했습니다." - ├─ 내용: 계약명, 발신자, 마감일 등 - └─ 버튼: [계약서 확인하기] → 전자서명 페이지 URL - (ButtonType: WL, 웹링크) -``` - -### 8.3 알림톡 템플릿 (안) - -``` -[전자계약 도착 안내] - -안녕하세요, #{수신자명}님. -#{발신회사명}에서 전자계약서가 도착했습니다. - -■ 계약명: #{계약명} -■ 발신자: #{발신자명} -■ 마감일: #{마감일} - -아래 버튼을 눌러 계약서를 확인하고 서명해주세요. - -[계약서 확인하기] ← 웹링크 버튼 -``` - -> 카카오 템플릿 검수 시 정보성 메시지로 분류되어 승인 가능성 높음 - -### 8.4 기술 구현 포인트 - -| 항목 | 내용 | -|------|------| -| **사용 API** | `SendATKakaotalkEx` (버튼 포함 알림톡) | -| **버튼 타입** | `WL` (웹링크) - 모바일/PC URL 모두 설정 | -| **SMS 대체발송** | 카카오톡 미사용자에게 자동 SMS 전환 | -| **구현 위치** | 전자계약 발송 컨트롤러에서 `BarobillService::sendATKakaotalkEx()` 호출 추가 | -| **이메일 병행** | 알림톡 + 이메일 동시 발송 (선택 가능하게) | - -### 8.5 기대 효과 - -- **열람률 향상**: 이메일(20~30%) → 카카오톡(80%+) -- **체결 속도**: 이메일 확인 지연(수시간~1일) → 카카오톡 즉시 확인 -- **접근성**: 별도 앱 설치 없이 카카오톡에서 바로 계약서 페이지 이동 -- **미사용자 대응**: SMS 대체발송으로 카카오톡 미사용자도 커버 - -### 8.6 준비 순서 - -| 순서 | 내용 | 비고 | -|------|------|------| -| 1 | 카카오 채널 개설 | 법인 명의 휴대폰 필요 | -| 2 | 바로빌에 채널 연동 | 바로빌 관리 페이지 | -| 3 | "전자계약 도착 안내" 템플릿 등록 | 카카오 검수 1~3 영업일 | -| 4 | 전자계약 컨트롤러에 알림톡 발송 로직 추가 | 코드 구현 | -| 5 | 테스트 발송 → 운영 전환 | testws → ws | - ---- - -## 9. 참고 자료 +## 8. 참고 자료 - [바로빌 API 문서](https://dev.barobill.co.kr) - [카카오비즈니스 채널 관리](https://business.kakao.com) - [카카오 알림톡 가이드](https://kakaobusiness.gitbook.io) +- 바로빌 템플릿 관리: 로그인 후 `https://www.barobill.co.kr` → 카카오톡 템플릿 관리 --- @@ -324,5 +390,6 @@ KakaotalkButton | 날짜 | 버전 | 변경 내용 | |------|------|----------| +| 2026-02-24 | 1.0 | 전자계약 알림톡 연동 완료, 트러블슈팅 문서화, v2 템플릿 가이드 추가 | | 2026-02-14 | 0.2 | 전자계약(E-Sign) 알림톡 연동 활용 계획 추가 | | 2026-02-14 | 0.1 | 초안 작성 - 코드 구현 완료, 실 서비스 연동 대기 | From 6b2d990d6ab4179f4bc3fb583b4359fa3954d7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 26 Feb 2026 20:35:37 +0900 Subject: [PATCH 27/69] =?UTF-8?q?docs:=20[hr]=20=EA=B7=BC=ED=83=9C?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=ED=9A=8D=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 구성, 데이터 구조, 비즈니스 규칙, API 엔드포인트 정리 - 1차 구현 완료 12건, 2차 고도화 예정 8건 목록 - INDEX.md에 문서 등록 --- INDEX.md | 2 + features/hr/attendance-management-spec.md | 340 ++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 features/hr/attendance-management-spec.md diff --git a/INDEX.md b/INDEX.md index f36570d..7f840d4 100644 --- a/INDEX.md +++ b/INDEX.md @@ -163,8 +163,10 @@ docs/ | [barobill-kakaotalk/README.md](features/barobill-kakaotalk/README.md) | 바로빌 카카오톡 (알림톡/친구톡) 연동 | | [boards/README.md](features/boards/README.md) | 게시판 시스템 구현 | | [boards/mng-implementation.md](features/boards/mng-implementation.md) | MNG 게시판 구현 상세 | +| [hr/attendance-management-spec.md](features/hr/attendance-management-spec.md) | 근태관리 기획서 (화면/데이터/비즈니스규칙/API) | | [hr/hr-api-analysis.md](features/hr/hr-api-analysis.md) | HR API 분석 (근태/직원/부서) | | [quotes/README.md](features/quotes/README.md) | 견적 시스템 분석 (BOM 계산, 10단계 로직) | +| [business-card-request.md](features/business-card-request.md) | 명함신청 관리 (3단계 워크플로우: 요청→제작의뢰→처리완료) | | [academy/fire-shutter-image-prompts.md](features/academy/fire-shutter-image-prompts.md) | 방화셔터 백과사전 이미지 생성 프롬프트 (Gemini용) | ### projects/ - 프로젝트별 문서 diff --git a/features/hr/attendance-management-spec.md b/features/hr/attendance-management-spec.md new file mode 100644 index 0000000..eb8e9b3 --- /dev/null +++ b/features/hr/attendance-management-spec.md @@ -0,0 +1,340 @@ +# 근태관리 기획서 + +> **작성일**: 2026-02-26 +> **상태**: 1차 구현 완료 / 2차 고도화 예정 +> **담당**: MNG 인사관리 모듈 + +--- + +## 1. 개요 + +### 1.1 목적 + +사원의 일별 출퇴근 및 근태 상태를 체계적으로 관리하여, 급여 산정·인사 평가·법정 근로시간 준수의 기초 데이터를 확보한다. + +### 1.2 핵심 원칙 + +| 원칙 | 설명 | +|------|------| +| **정확성** | 출퇴근 시간·상태를 실시간으로 정확히 기록 | +| **자동화** | 지각/정시 자동 판정, 근무시간 자동 계산 | +| **유연성** | 휴가·출장·외근·재택 등 다양한 근무 형태 지원 | +| **감사 추적** | 모든 등록·수정·삭제에 작업자 기록 | + +--- + +## 2. 화면 구성 + +### 2.1 메뉴 위치 + +``` +인사관리 +├── 사원관리 +├── 근태관리 ← 현재 문서 +├── 달력 관리 +└── (향후 확장) +``` + +### 2.2 메인 화면 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 근태현황 [+ 근태 등록] │ +│ 2026년 2월 현재 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐│ +│ │ 정시출근 │ │ 지각 │ │ 결근 │ │ 휴가 │ │ 기타 ││ +│ │ 42건 │ │ 3건 │ │ 0건 │ │ 5건 │ │ 2건 ││ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └────────┘│ +├─────────────────────────────────────────────────────────────┤ +│ [검색: 사원명] [부서 ▼] [상태 ▼] [시작일] [종료일] [검색] │ +├─────────────────────────────────────────────────────────────┤ +│ 날짜 │ 사원 │ 부서 │ 상태 │ 출근 │ 퇴근 │ 비고 │ +│──────────┼────────┼────────┼────────┼───────┼───────┼──────│ +│ 02-26 │ 홍길동 │ 개발팀 │ 정시 │ 08:55 │ 18:10 │ │ +│ 02-26 │ 김철수 │ 영업팀 │ 지각 │ 09:15 │ 18:30 │ │ +│ 02-25 │ 이영희 │ 경영 │ 휴가 │ - │ - │ 연차 │ +├─────────────────────────────────────────────────────────────┤ +│ ◀ 1 2 3 4 5 ▶ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.3 근태 등록/수정 모달 + +``` +┌──────────────── 근태 등록 ─────────────────┐ +│ │ +│ 사원 [홍길동 ▼] │ +│ 날짜 [2026-02-26] │ +│ 상태 [정시출근 ▼] │ +│ 출근시간 [09:00] │ +│ 퇴근시간 [18:00] │ +│ 비고 [ ] │ +│ │ +│ [취소] [저장] │ +└─────────────────────────────────────────────┘ +``` + +--- + +## 3. 근태 상태 유형 + +| 상태 | 코드 | 색상 | 설명 | +|------|------|------|------| +| 정시출근 | `onTime` | 🟢 emerald | 정해진 출근 시간 내 출근 | +| 지각 | `late` | 🟡 amber | 출근 시간 초과 후 출근 | +| 결근 | `absent` | 🔴 red | 무단 미출근 | +| 휴가 | `vacation` | 🔵 blue | 연차·반차·병가 등 | +| 출장 | `businessTrip` | 🟣 purple | 사외 업무 수행 | +| 외근 | `fieldWork` | 🟤 indigo | 외부 근무 | +| 야근 | `overtime` | 🟠 orange | 정규시간 초과 근무 | +| 재택 | `remote` | 🟦 teal | 원격 근무 | + +--- + +## 4. 데이터 구조 + +### 4.1 attendances 테이블 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | 고유 ID | +| `tenant_id` | BIGINT FK | 테넌트 ID | +| `user_id` | BIGINT FK | 사용자 ID | +| `base_date` | DATE | 기준 일자 | +| `status` | ENUM | 근태 상태 (8종) | +| `json_details` | JSON | 상세 정보 (출퇴근 시간, GPS 등) | +| `remarks` | VARCHAR(500) | 비고 | +| `created_by` | BIGINT | 등록자 | +| `updated_by` | BIGINT | 수정자 | +| `deleted_by` | BIGINT | 삭제자 | +| `created_at` | TIMESTAMP | 등록일시 | +| `updated_at` | TIMESTAMP | 수정일시 | +| `deleted_at` | TIMESTAMP | 삭제일시 (Soft Delete) | + +**제약 조건**: +- `UNIQUE (tenant_id, user_id, base_date)` — 일자별 사용자당 1건만 허용 + +### 4.2 json_details 구조 + +```json +{ + "check_in": "09:00:00", + "check_out": "18:00:00", + "check_ins": [ + { "time": "09:00:00", "recorded_at": "2026-02-26T09:00:00+09:00" } + ], + "check_outs": [ + { "time": "18:00:00", "recorded_at": "2026-02-26T18:00:00+09:00" } + ], + "gps_data": { + "check_in": { "lat": 37.5665, "lng": 126.9780, "accuracy": 10 }, + "check_out": { "lat": 37.5665, "lng": 126.9780, "accuracy": 10 } + }, + "work_minutes": 480, + "break_minutes": 60, + "overtime_minutes": 0, + "late_minutes": 0, + "early_leave_minutes": 0, + "vacation_type": "annual", + "external_work": { + "location": "고객사", + "purpose": "미팅", + "start_time": "14:00", + "end_time": "16:00" + } +} +``` + +### 4.3 attendance_settings 테이블 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `tenant_id` | BIGINT UNIQUE | 테넌트별 1건 | +| `use_gps` | BOOLEAN | GPS 출퇴근 사용 여부 | +| `use_auto` | BOOLEAN | 자동 출퇴근 사용 여부 | +| `allowed_radius` | INT | 허용 반경 (m, 기본 100) | +| `hq_address` | VARCHAR(255) | 사업장 주소 | +| `hq_latitude` | DECIMAL(10,8) | 사업장 위도 | +| `hq_longitude` | DECIMAL(11,8) | 사업장 경도 | + +--- + +## 5. 비즈니스 규칙 + +### 5.1 출퇴근 기록 + +| 규칙 | 설명 | +|------|------| +| R1 | 같은 날짜 + 사용자 조합은 1건만 존재 (Upsert) | +| R2 | 출근 기록 시 WorkSetting의 `start_time` 기준으로 지각/정시 자동 판정 | +| R3 | 퇴근 기록 시 근무시간 자동 계산: (최초 출근 ~ 최종 퇴근) - 휴게시간 | +| R4 | 다중 출퇴근 기록 지원 — `check_ins`/`check_outs` 배열로 이력 관리 | +| R5 | GPS 기록 시 사업장 반경 내 여부 검증 (Haversine 공식) | + +### 5.2 상태 자동 판정 + +``` +출근 시간 기록 시: +├── WorkSetting.start_time 이전 → 상태: 'onTime' (정시출근) +├── WorkSetting.start_time 이후 → 상태: 'late' (지각) +└── WorkSetting 미설정 → 상태: 'onTime' (기본값) +``` + +### 5.3 근무시간 계산 + +``` +총 근무시간 = (가장 늦은 퇴근시간 - 가장 빠른 출근시간) - 휴게시간 + +휴게시간 산출: +├── WorkSetting에 break_start/break_end 설정 있음 +│ └── 근무시간이 휴게 시간대를 포함하면 차감 +└── 미설정 → 휴게시간 0분 +``` + +### 5.4 권한 + +| 역할 | 조회 | 등록 | 수정 | 삭제 | +|------|:----:|:----:|:----:|:----:| +| 관리자 (admin) | ✅ 전체 | ✅ | ✅ | ✅ | +| 부서장 | ✅ 소속 부서 | ✅ | ✅ | ❌ | +| 일반 사원 | ✅ 본인 | ❌ | ❌ | ❌ | + +--- + +## 6. 기능 목록 + +### 6.1 1차 구현 (완료) + +| 기능 | 상태 | 설명 | +|------|:----:|------| +| 근태 목록 조회 | ✅ | 페이지네이션, 필터링, HTMX 테이블 | +| 근태 등록/수정 | ✅ | 모달 기반 CRUD | +| 근태 삭제 | ✅ | Soft Delete + 확인 대화상자 | +| 월별 통계 카드 | ✅ | 정시/지각/결근/휴가/기타 집계 | +| 필터링 | ✅ | 사원명 검색, 부서/상태/날짜 범위 필터 | +| 출근/퇴근 기록 API | ✅ | check-in/check-out 엔드포인트 | +| 근무시간 자동 계산 | ✅ | 출퇴근 시간 차 - 휴게시간 | +| 상태 자동 판정 | ✅ | WorkSetting 기준 지각/정시 판별 | +| 엑셀 내보내기 | ✅ | 월별 데이터 Excel 다운로드 | +| GPS 설정 | ✅ | 사업장 좌표, 허용 반경 설정 | +| 다중 출퇴근 기록 | ✅ | check_ins/check_outs 배열 관리 | +| 감사 로그 | ✅ | created_by, updated_by, deleted_by | + +### 6.2 2차 고도화 (예정) + +| 기능 | 우선순위 | 설명 | +|------|:--------:|------| +| 월간 캘린더 뷰 | 🔴 높음 | 달력 형태로 사원별 근태 현황 표시 | +| 일괄 등록 | 🔴 높음 | 다수 사원의 근태를 한 번에 등록 (CSV/엑셀 업로드) | +| 근태 승인 워크플로우 | 🟡 중간 | 휴가/출장 신청 → 부서장 승인 → 확정 | +| 초과근무 알림 | 🟡 중간 | 주 52시간 초과 시 관리자 알림 | +| 사원별 월간 요약 | 🟡 중간 | 개인별 월간 근무일수, 총 근무시간, 지각 횟수 등 | +| GPS 출퇴근 (모바일) | 🟢 낮음 | 모바일 앱에서 GPS 기반 자동 출퇴근 | +| 자동 결근 처리 | 🟢 낮음 | 영업일에 출근 기록 없으면 자동으로 결근 표시 | +| 연차 관리 연동 | 🟢 낮음 | 휴가 상태 등록 시 잔여 연차 자동 차감 | + +--- + +## 7. API 엔드포인트 + +### 7.1 MNG 내부 API (HTMX) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/api/admin/hr/attendances` | 목록 조회 (HTML/JSON) | +| POST | `/api/admin/hr/attendances` | 등록 | +| PUT | `/api/admin/hr/attendances/{id}` | 수정 | +| DELETE | `/api/admin/hr/attendances/{id}` | 삭제 | + +### 7.2 외부 API (sam/api) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/api/v1/attendances` | 목록 조회 (페이지네이션) | +| GET | `/api/v1/attendances/{id}` | 상세 조회 | +| POST | `/api/v1/attendances` | 등록 | +| PATCH | `/api/v1/attendances/{id}` | 수정 | +| DELETE | `/api/v1/attendances/{id}` | 삭제 | +| POST | `/api/v1/attendances/bulk-delete` | 일괄 삭제 | +| POST | `/api/v1/attendances/check-in` | 출근 기록 | +| POST | `/api/v1/attendances/check-out` | 퇴근 기록 | +| GET | `/api/v1/attendances/monthly-stats` | 월별 통계 | +| GET | `/api/v1/attendances/export` | 엑셀 내보내기 | + +--- + +## 8. 프로세스 흐름 + +### 8.1 관리자 근태 등록 + +``` +관리자 → [근태 등록] 클릭 → 모달 열림 + → 사원 선택, 날짜·상태·시간 입력 + → [저장] 클릭 + → Fetch POST /api/admin/hr/attendances + → 같은 날짜+사용자 기존 기록 있으면 Upsert + → 성공 → 테이블 HTMX 새로고침 +``` + +### 8.2 사원 출근 (API) + +``` +사원(모바일) → [출근] 버튼 + → POST /api/v1/attendances/check-in + { user_id, check_in_time, gps? } + → 서버: WorkSetting.start_time 기준 지각/정시 판정 + → 기존 기록 없으면 신규 생성 + → 기존 기록 있으면 check_ins 배열에 추가 + → 응답: 근태 레코드 반환 +``` + +### 8.3 사원 퇴근 (API) + +``` +사원(모바일) → [퇴근] 버튼 + → POST /api/v1/attendances/check-out + { user_id, check_out_time, gps? } + → 서버: 근무시간 자동 계산 + (가장 빠른 출근 ~ 가장 늦은 퇴근) - 휴게시간 + → check_outs 배열에 추가 + → work_minutes, overtime_minutes 업데이트 + → 응답: 근태 레코드 반환 +``` + +--- + +## 9. 관련 파일 + +### MNG + +| 파일 | 설명 | +|------|------| +| `app/Http/Controllers/HR/AttendanceController.php` | 페이지 렌더링 | +| `app/Http/Controllers/Api/Admin/HR/AttendanceController.php` | HTMX API | +| `app/Models/HR/Attendance.php` | 모델 (Accessor 포함) | +| `app/Services/HR/AttendanceService.php` | 비즈니스 로직 | +| `resources/views/hr/attendances/index.blade.php` | 메인 페이지 | +| `resources/views/hr/attendances/partials/table.blade.php` | 테이블 partial | + +### API + +| 파일 | 설명 | +|------|------| +| `app/Http/Controllers/Api/V1/AttendanceController.php` | RESTful API | +| `app/Models/Tenants/Attendance.php` | 모델 | +| `app/Models/Tenants/AttendanceSetting.php` | GPS/자동 설정 | +| `app/Services/AttendanceService.php` | 서비스 (642줄) | +| `database/migrations/2025_12_09_*` | 테이블 생성 | + +### 문서 + +| 파일 | 설명 | +|------|------| +| `docs/rules/attendance-api.md` | API 비즈니스 규칙 | +| `docs/specs/erp-analysis/03-gps-attendance.md` | GPS 출퇴근 스펙 | +| `docs/specs/erp-analysis/04-hr-management.md` | HR 시스템 분석 | + +--- + +**최종 업데이트**: 2026-02-26 From 73f7811da3bb575b69f9b7a27c2a30eab0f7cfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 26 Feb 2026 22:07:36 +0900 Subject: [PATCH 28/69] =?UTF-8?q?docs:=20[leave]=20=ED=9C=B4=EA=B0=80?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=AA=A8=EB=93=88=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phase 1: 기본 휴가관리 (신청/승인/잔여연차/사용현황) - Phase 2: 연차 정책 및 자동 계산 (근로기준법 기반) - Phase 3: 연차 촉진, 알림, 리포트 - 기존 API 테이블 4개 활용 - 근태현황 vacation 기능 분리 전략 포함 --- INDEX.md | 2 + plans/leave-management-plan.md | 459 +++++++++++++++++++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 plans/leave-management-plan.md diff --git a/INDEX.md b/INDEX.md index 7f840d4..1ef7b30 100644 --- a/INDEX.md +++ b/INDEX.md @@ -155,6 +155,8 @@ docs/ | [SAM_ERP_회계관리_Storyboard_D1.6.md](plans/SAM_ERP_회계관리_Storyboard_D1.6.md) | ERP 회계관리 스토리보드 D1.6 (65p PDF → 마크다운 변환) | | [SAM_General_Rule_Storyboard_D1.0.md](plans/SAM_General_Rule_Storyboard_D1.0.md) | General Rule 스토리보드 D1.0 (43p PDF → 마크다운 변환, UIUX 공통 규칙) | | [production-deployment-plan.md](plans/production-deployment-plan.md) | 운영 환경 배포 계획 (CI/CD, 서버 아키텍처) | +| [attendance-management-plan.md](plans/attendance-management-plan.md) | 근태현황 개발 계획 (Phase 1~2, HTMX 기반) | +| [leave-management-plan.md](plans/leave-management-plan.md) | 휴가관리 모듈 개발 계획 (연차 발생/신청/승인/정책) | ### features/ - 기능별 문서 diff --git a/plans/leave-management-plan.md b/plans/leave-management-plan.md new file mode 100644 index 0000000..62384c3 --- /dev/null +++ b/plans/leave-management-plan.md @@ -0,0 +1,459 @@ +# 휴가관리 모듈 개발 계획서 + +> **작성일**: 2026-02-26 +> **상태**: 설계 중 + +--- + +## 1. 개요 + +### 1.1 목적 + +근태현황에 포함된 휴가/연차 기능을 **독립된 휴가관리 모듈**로 분리한다. +근로기준법 기반 연차 자동 계산, 휴가 신청/승인, 잔여 연차 관리를 체계적으로 지원한다. + +### 1.2 핵심 원칙 + +- 근태현황의 `vacation` 상태는 **결과 기록**으로만 유지 (휴가 승인 완료 시 자동 기록) +- 휴가 신청/승인/잔여일수 관리는 모두 **휴가관리 모듈**에서 수행 +- 기존 API 테이블/모델(`leaves`, `leave_balances`, `leave_policies`, `leave_grants`)을 최대한 활용 +- MNG에서 관리자 인터페이스(Blade + HTMX) 구현 + +### 1.3 현재 상태 분석 + +| 항목 | API (DB/모델) | MNG (UI/서비스) | 비고 | +|------|:------------:|:--------------:|------| +| `leaves` 테이블 | ✅ 마이그레이션 완료 | ❌ 미구현 | 핵심 테이블 | +| `leave_balances` 테이블 | ✅ 마이그레이션 완료 | ⚠️ 단순 모델만 | 연차 잔액 | +| `leave_policies` 테이블 | ✅ 마이그레이션 완료 | ❌ 미구현 | 연차 정책 설정 | +| `leave_grants` 테이블 | ✅ 마이그레이션 완료 | ❌ 미구현 | 연차 부여 이력 | +| `attendance_requests` 테이블 | ✅ 마이그레이션 완료 | ✅ 신청/승인 구현 | 근태현황에 포함 | +| 연차 자동 차감 | — | ⚠️ 단순 구현 | 복원 로직 없음 | + +### 1.4 근태현황과의 역할 분리 + +| 기능 | 근태현황 (유지) | 휴가관리 (신규) | +|------|:--------------:|:--------------:| +| 출퇴근 기록 | ✅ | — | +| 출장/재택/외근 신청·승인 | ✅ | — | +| `vacation` 상태 표시 | ✅ (결과만) | — | +| 연차 부여/발생 규칙 | — | ✅ | +| 휴가 신청·승인 워크플로우 | — | ✅ | +| 잔여 연차 관리 | — | ✅ | +| 연차 촉진 알림 | — | ✅ (Phase 3) | + +--- + +## 2. 기존 DB 스키마 + +> API에 이미 마이그레이션 완료된 테이블들. MNG에서 모델만 생성하여 활용한다. + +### 2.1 `leaves` 테이블 + +``` +┌──────────────────────────────────────────────────────────┐ +│ leaves │ +├────────────────┬─────────────┬────────────────────────────┤ +│ id │ bigint PK │ Auto Increment │ +│ tenant_id │ bigint FK │ → tenants.id │ +│ user_id │ bigint FK │ → users.id │ +│ leave_type │ enum │ annual, half_am, half_pm, │ +│ │ │ sick, family, maternity, │ +│ │ │ parental │ +│ start_date │ date │ 시작일 │ +│ end_date │ date │ 종료일 │ +│ days │ decimal(3,1)│ 사용일수 (0.5 = 반차) │ +│ reason │ text │ 사유 │ +│ status │ enum │ pending, approved, rejected, │ +│ │ │ cancelled │ +│ approved_by │ bigint │ 승인자 ID │ +│ approved_at │ datetime │ 승인 일시 │ +│ reject_reason │ text │ 반려 사유 │ +│ created_by/ │ bigint │ 감사 필드 │ +│ updated_by/ │ │ │ +│ deleted_by │ │ │ +│ timestamps │ │ │ +│ soft_deletes │ │ │ +└────────────────┴─────────────┴────────────────────────────┘ +INDEX: (tenant_id, user_id), status, (start_date, end_date) +``` + +### 2.2 `leave_balances` 테이블 + +``` +┌──────────────────────────────────────────────────────────┐ +│ leave_balances │ +├────────────────┬─────────────┬────────────────────────────┤ +│ id │ bigint PK │ │ +│ tenant_id │ bigint FK │ │ +│ user_id │ bigint FK │ │ +│ year │ int │ 연도 │ +│ total_days │ decimal(4,1)│ 부여일수 (기본 15) │ +│ used_days │ decimal(4,1)│ 사용일수 │ +│ remaining_days │ decimal(4,1)│ storedAs(total - used) │ +└────────────────┴─────────────┴────────────────────────────┘ +UNIQUE: (tenant_id, user_id, year) +``` + +### 2.3 `leave_policies` 테이블 + +``` +┌──────────────────────────────────────────────────────────┐ +│ leave_policies │ +├──────────────────────┬─────────────┬─────────────────────┤ +│ tenant_id │ bigint UNIQUE│ 테넌트당 1개 │ +│ standard_type │ enum │ fiscal / hire │ +│ fiscal_start_month │ tinyint │ 회계연도 시작월 │ +│ fiscal_start_day │ tinyint │ 회계연도 시작일 │ +│ default_annual_leave │ int │ 기본 연차 (15) │ +│ additional_leave_per_year │ int │ 근속 가산 (+1) │ +│ max_annual_leave │ int │ 최대 연차 (25) │ +│ carry_over_enabled │ boolean │ 이월 허용 │ +│ carry_over_max_days │ int │ 이월 한도 │ +│ carry_over_expiry_months │ int │ 이월 소멸 개월 │ +└──────────────────────┴─────────────┴─────────────────────┘ +``` + +### 2.4 `leave_grants` 테이블 + +``` +┌──────────────────────────────────────────────────────────┐ +│ leave_grants │ +├────────────────┬─────────────┬────────────────────────────┤ +│ tenant_id │ bigint FK │ │ +│ user_id │ bigint FK │ │ +│ grant_type │ enum │ annual, monthly, reward, │ +│ │ │ condolence, other │ +│ grant_date │ date │ 부여일 │ +│ grant_days │ decimal(4,1)│ 부여일수 │ +│ reason │ text │ 부여 사유 │ +└────────────────┴─────────────┴────────────────────────────┘ +INDEX: (tenant_id, user_id), grant_date, grant_type +``` + +--- + +## 3. Phase 1: 기본 휴가관리 (핵심) + +> 🔴 **필수** — 연차 조회, 휴가 신청/승인, 잔여일수 관리 + +### 3.1 MNG 모델 생성 + +| 모델 | 파일 | 대상 테이블 | +|------|------|------------| +| `Leave` | `app/Models/HR/Leave.php` | `leaves` | +| `LeavePolicy` | `app/Models/HR/LeavePolicy.php` | `leave_policies` | +| `LeaveGrant` | `app/Models/HR/LeaveGrant.php` | `leave_grants` | +| `LeaveBalance` | (기존 수정) | `leave_balances` | + +### 3.2 LeaveService 생성 + +**파일**: `app/Services/HR/LeaveService.php` + +| 메서드 | 설명 | +|--------|------| +| `getLeaves(array $filters, int $perPage)` | 휴가 목록 조회 (필터: 사원, 유형, 상태, 기간) | +| `storeLeave(array $data)` | 휴가 신청 등록 (잔여일수 검증 포함) | +| `approve(int $id)` | 승인 처리 → `leave_balances` 차감 → `attendances` 자동 기록 | +| `reject(int $id, ?string $reason)` | 반려 처리 | +| `cancel(int $id)` | 취소 처리 → `leave_balances` 복원 → `attendances` 삭제 | +| `getBalance(int $userId, ?int $year)` | 사원별 연차 잔여일수 조회 | +| `getBalanceSummary(?int $year)` | 전체 사원 잔여일수 요약 | +| `calculateDays(string $type, string $startDate, string $endDate)` | 신청 일수 자동 계산 (주말 제외, 반차=0.5) | + +### 3.3 LeaveController (API) 생성 + +**파일**: `app/Http/Controllers/Api/Admin/HR/LeaveController.php` + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/admin/hr/leaves` | 휴가 목록 (HTMX/JSON) | +| POST | `/admin/hr/leaves` | 휴가 신청 등록 | +| POST | `/admin/hr/leaves/{id}/approve` | 승인 | +| POST | `/admin/hr/leaves/{id}/reject` | 반려 | +| POST | `/admin/hr/leaves/{id}/cancel` | 취소 | +| GET | `/admin/hr/leaves/balance` | 전체 사원 잔여일수 요약 | +| GET | `/admin/hr/leaves/balance/{userId}` | 개별 사원 잔여일수 | +| GET | `/admin/hr/leaves/export` | 엑셀(CSV) 내보내기 | + +### 3.4 MNG 뷰 컨트롤러 + +**파일**: `app/Http/Controllers/HR/LeaveController.php` + +``` +GET /hr/leaves → index 페이지 (휴가관리 메인) +``` + +### 3.5 뷰 구성 + +**파일**: `resources/views/hr/leaves/index.blade.php` + +``` +┌───────────────────────────────────────────────────────┐ +│ 휴가관리 │ +├───────────┬───────────┬─────────────┬─────────────────┤ +│ 휴가신청 │ 잔여연차 │ 사용현황 │ (Phase 2) 설정 │ +│ (탭 1) │ (탭 2) │ (탭 3) │ (탭 4) │ +└───────────┴───────────┴─────────────┴─────────────────┘ +``` + +**탭 1: 휴가신청 목록** + +``` +┌─────────────────────────────────────────────────┐ +│ [+ 휴가 신청] [엑셀 내보내기] │ +│ │ +│ 필터: [사원 ▼] [유형 ▼] [상태 ▼] [기간 ~] │ +│ │ +│ ┌──────┬──────┬──────┬──────┬─────┬──────┬─────┐ │ +│ │ 사원 │ 유형 │ 기간 │ 일수 │사유 │ 상태 │ 처리│ │ +│ ├──────┼──────┼──────┼──────┼─────┼──────┼─────┤ │ +│ │홍길동│ 연차 │2/24~ │ 1.0 │개인 │ 대기 │승인 │ │ +│ │ │ │ 2/24 │ │사유 │ │반려 │ │ +│ │김영희│ 반차 │2/25 │ 0.5 │병원 │ 승인 │취소 │ │ +│ │ │(오전)│ │ │ │ │ │ │ +│ └──────┴──────┴──────┴──────┴─────┴──────┴─────┘ │ +│ │ +│ [페이지네이션] │ +└─────────────────────────────────────────────────┘ +``` + +**탭 2: 잔여연차 현황** + +``` +┌─────────────────────────────────────────────────┐ +│ 연도: [2026 ▼] │ +│ │ +│ ┌──────┬──────┬──────┬──────┬──────┬──────────┐ │ +│ │ 사원 │ 부서 │ 입사일│ 부여 │ 사용 │ 잔여 │ │ +│ ├──────┼──────┼──────┼──────┼──────┼──────────┤ │ +│ │홍길동│ 개발 │21.03 │ 20.0 │ 5.0 │ 15.0 │ │ +│ │김영희│ 영업 │23.08 │ 15.0 │ 3.5 │ 11.5 │ │ +│ │이민수│ 총무 │25.06 │ 11.0 │ 2.0 │ 9.0 │ │ +│ └──────┴──────┴──────┴──────┴──────┴──────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +**탭 3: 사용현황 통계** + +``` +┌─────────────────────────────────────────────────┐ +│ 기간: [2026 ▼] [전체/부서별 ▼] │ +│ │ +│ 유형별 집계: 연차 45건 | 반차 12건 | 병가 3건 │ +│ │ +│ 월별 사용 추이 차트 (선택적) │ +│ │ +│ ┌──────┬──────┬──────┬──────┬──────┬──────────┐ │ +│ │ 사원 │ 연차 │ 반차 │ 병가 │ 경조 │ 합계 │ │ +│ ├──────┼──────┼──────┼──────┼──────┼──────────┤ │ +│ │홍길동│ 3.0 │ 1.0 │ 1.0 │ 0.0 │ 5.0 │ │ +│ │김영희│ 2.0 │ 1.5 │ 0.0 │ 0.0 │ 3.5 │ │ +│ └──────┴──────┴──────┴──────┴──────┴──────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +### 3.6 휴가 신청 모달 + +``` +┌──────────────────────────────────────────┐ +│ 휴가 신청 │ +├──────────────────────────────────────────┤ +│ 사원: [홍길동 ▼] 잔여: 15.0일 │ +│ 유형: [연차 ▼] │ +│ 기간: [2026-02-27] ~ [2026-02-28] │ +│ 일수: 2.0일 (자동 계산, 주말 제외) │ +│ 사유: [ ] │ +│ │ +│ [취소] [신청] │ +└──────────────────────────────────────────┘ +``` + +### 3.7 근태현황 연동 로직 + +``` +휴가 승인 시: +┌──────────┐ ┌──────────────┐ ┌──────────────┐ +│ Leave │──→ │ LeaveBalance │──→ │ Attendance │ +│ approved │ │ used_days +N │ │ status= │ +│ │ │ │ │ vacation │ +└──────────┘ └──────────────┘ └──────────────┘ + +휴가 취소 시: +┌──────────┐ ┌──────────────┐ ┌──────────────┐ +│ Leave │──→ │ LeaveBalance │──→ │ Attendance │ +│ cancelled │ │ used_days -N │ │ 해당 날짜 │ +│ │ │ │ │ 레코드 삭제 │ +└──────────┘ └──────────────┘ └──────────────┘ +``` + +### 3.8 근태현황 정리 작업 + +Phase 1 구현 후 근태현황에서 다음을 정리: + +- `AttendanceRequest`의 `vacation` 유형 → 휴가관리로 이관 (출장/재택/외근만 유지) +- `AttendanceService.deductLeaveBalance()` 제거 → `LeaveService`로 일원화 +- `AttendanceController.leaveBalance()` 제거 → `LeaveController`로 이관 + +--- + +## 4. Phase 2: 연차 정책 및 자동 계산 + +> 🟡 **중요** — 근로기준법 기반 연차 자동 발생, 정책 설정 + +### 4.1 연차 정책 설정 UI + +**탭 4: 휴가 설정** (Phase 2에서 활성화) + +``` +┌──────────────────────────────────────────┐ +│ 연차 기준 │ +│ ○ 입사일 기준 ● 회계연도 기준 │ +│ 회계연도 시작: [1]월 [1]일 │ +│ │ +│ 연차 일수 │ +│ 기본 연차: [15]일 │ +│ 2년 초과 시 가산: [1]일/2년 │ +│ 최대 연차: [25]일 │ +│ │ +│ 이월 설정 │ +│ □ 잔여 연차 이월 허용 │ +│ 이월 한도: [5]일 │ +│ 이월 소멸: [3]개월 후 │ +│ │ +│ [저장] │ +└──────────────────────────────────────────┘ +``` + +### 4.2 LeavePolicyService + +**파일**: `app/Services/HR/LeavePolicyService.php` + +| 메서드 | 설명 | +|--------|------| +| `getPolicy()` | 현재 테넌트 연차 정책 조회 | +| `updatePolicy(array $data)` | 정책 저장/수정 | +| `calculateAnnualLeave(int $userId)` | 사원별 연차 자동 계산 (입사일 + 근속년수 기반) | +| `generateAnnualLeaves()` | 전체 사원 연차 일괄 발생 (연초/입사일 기준) | +| `processCarryOver()` | 이월 처리 (연말) | + +### 4.3 연차 발생 규칙 (근로기준법) + +```php +// 입사일 기준 연차 계산 로직 +function calculateAnnualDays(Carbon $hireDate): float +{ + $years = $hireDate->diffInYears(now()); + + if ($years < 1) { + // 1년 미만: 매월 개근 시 1일 (최대 11일) + $months = $hireDate->diffInMonths(now()); + return min($months, 11); + } + + // 1년 이상: 15일 + (근속년수-1)/2 가산 (최대 25일) + $base = 15; + $additional = max(0, floor(($years - 1) / 2)); + return min($base + $additional, 25); +} +``` + +### 4.4 연차 부여 이력 관리 + +`leave_grants` 테이블을 활용하여 부여 이력 추적: + +| grant_type | 설명 | 예시 | +|-----------|------|------| +| `annual` | 연차 자동 발생 | 2026년 연차 15일 부여 | +| `monthly` | 1년 미만 월차 | 2026-03 월차 1일 부여 | +| `reward` | 포상 휴가 | 우수사원 포상 2일 | +| `condolence` | 경조사 | 결혼 경조 5일 | +| `other` | 기타 | 회사 지정 휴가 | + +--- + +## 5. Phase 3: 고급 기능 + +> 🟢 **권장** — 연차 촉진, 알림, 리포트 + +### 5.1 연차 촉진제도 (근로기준법 제61조) + +| 시기 | 내용 | 자동화 | +|------|------|--------| +| 만료 6개월 전 | 1차 촉진 통보 (미사용 일수 안내) | 카카오 알림톡 발송 | +| 근로자 미응답 시 | 사용 시기 지정 촉구 | 리마인더 알림 | +| 만료 2개월 전 | 2차 촉진 통보 (회사 지정) | 카카오 알림톡 + 이력 보관 | + +### 5.2 알림 기능 + +- 휴가 승인/반려 시 신청자에게 알림톡 +- 잔여 연차 N일 이하 시 사용 권고 알림 +- 연차 소멸 D-30, D-7 자동 알림 + +### 5.3 리포트 + +- 부서별/사원별 연차 사용율 대시보드 +- 연차 대장 엑셀 출력 (노무감사 대비) +- 월별 사용 추이 통계 + +--- + +## 6. 구현 순서 + +### Phase 1 (기본 — 약 1~2일) + +| Step | 작업 | 파일 | +|------|------|------| +| 1 | MNG 모델 생성 (Leave, LeavePolicy, LeaveGrant) | `app/Models/HR/` | +| 2 | LeaveService 생성 | `app/Services/HR/LeaveService.php` | +| 3 | LeaveController (API) 생성 | `app/Http/Controllers/Api/Admin/HR/` | +| 4 | API 라우트 등록 | `routes/api.php` | +| 5 | 뷰 컨트롤러 생성 | `app/Http/Controllers/HR/LeaveController.php` | +| 6 | 웹 라우트 등록 | `routes/web.php` | +| 7 | index.blade.php 작성 (3개 탭) | `resources/views/hr/leaves/` | +| 8 | 파셜 뷰 작성 (목록, 잔여, 통계) | `resources/views/hr/leaves/partials/` | +| 9 | 근태현황 vacation 연동 정리 | AttendanceService 수정 | +| 10 | 메뉴 등록 안내 | tinker 명령 제공 | + +### Phase 2 (정책 — 약 1일) + +| Step | 작업 | +|------|------| +| 1 | LeavePolicyService 생성 | +| 2 | 연차 자동 계산 로직 구현 | +| 3 | 설정 UI (탭 4) 추가 | +| 4 | 연차 일괄 발생 Artisan 명령 | +| 5 | 이월 처리 로직 | + +### Phase 3 (고급 — 추후) + +| Step | 작업 | +|------|------| +| 1 | 연차 촉진 스케줄러 | +| 2 | 카카오 알림톡 연동 | +| 3 | 연차 대장 리포트 | +| 4 | 대시보드 위젯 | + +--- + +## 7. 핵심 설계 결정 + +| 항목 | 결정 | 이유 | +|------|------|------| +| 신규 테이블 | 불필요 | API에 4개 테이블 이미 존재 | +| 휴가 신청 테이블 | `leaves` 사용 | `attendance_requests`에서 vacation 분리 | +| 반차 처리 | `leave_type`=`half_am`/`half_pm`, `days`=0.5 | 기존 enum 활용 | +| 연차 차감 시점 | 승인 시 즉시 차감 | 잔여일수 실시간 반영 | +| 근태 연동 | 승인 시 Attendance 자동 생성 | 기존 패턴 유지 | +| 취소 시 복원 | used_days 복원 + Attendance 삭제 | 데이터 일관성 | + +--- + +## 관련 문서 + +- `plans/attendance-management-plan.md` — 근태현황 개발 계획 +- `rules/attendance-api.md` — 근태 API 규칙 + +--- + +**최종 업데이트**: 2026-02-26 From 730266f06978a512334a5f56c2de7fdafae38649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 26 Feb 2026 15:48:15 +0900 Subject: [PATCH 29/69] =?UTF-8?q?docs:=20plans=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20-=2053=EA=B0=9C=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C/=ED=86=B5=ED=95=A9,=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=8B=A0=EA=B7=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5단계 분류(ACTIVE/PLANNED/SUPERSEDED/COMPLETED/OBSOLETE) 기반 정리 - SUPERSEDED 4개: 최신 문서에 대체 확인 후 삭제 - COMPLETED 3개: HISTORY.md에 요약 후 삭제 - OBSOLETE 1개: 일회성 핫픽스 이력 삭제 - archive/ 37개 개별 파일 → HISTORY.md 1파일로 통합 (40건 요약) - sub/ 7개 + clodeCheck/ 7개 폴더 삭제 - GUIDE.md 신규: 문서 관리 최소 원칙 6개 (명명규칙, 필수섹션, 상태표기, 생명주기, 폴더구조, 인덱스관리) - index_plans.md 재작성: ACTIVE 18개 + PLANNED 19개만 도메인별 그룹핑 - 정리 계획 문서(docs-plans-cleanup-plan.md) 포함 Before: 95파일 → After: 42파일 (56% 감소) Co-Authored-By: Claude Opus 4.6 --- plans/GUIDE.md | 127 ++ plans/archive/5130-bom-migration-plan.md | 446 ----- plans/archive/5130-sam-data-migration-plan.md | 828 ---------- .../AI_리포트_키워드_색상체계_가이드_v1.4.md | 406 ----- plans/archive/HISTORY.md | 88 + plans/archive/SEEDERS_LIST.md | 128 -- plans/archive/api-analysis-report.md | 434 ----- .../archive/bending-lot-pipeline-dev-plan.md | 1097 ------------- .../bending-worklog-reimplementation-plan.md | 860 ---------- .../bidding-api-implementation-plan.md | 817 ---------- .../construction-api-integration-plan.md | 480 ------ plans/archive/docs-update-plan.md | 309 ---- .../document-management-system-changelog.md | 31 - .../document-system-product-inspection.md | 375 ----- .../erp-api-development-plan-d1.0-changes.md | 559 ------- .../fcm-user-targeted-notification-plan.md | 369 ----- .../archive/formula-engine-real-data-plan.md | 1077 ------------ plans/archive/items-table-unification-plan.md | 589 ------- plans/archive/kd-items-migration-plan.md | 1293 --------------- .../archive/l2-permission-management-plan.md | 378 ----- .../material-input-per-item-mapping-plan.md | 482 ------ .../archive/mes-integration-analysis-plan.md | 525 ------ .../mng-item-formula-integration-plan.md | 837 ---------- plans/archive/mng-item-management-plan.md | 1447 ----------------- .../mng-quote-formula-development-plan.md | 553 ------- .../archive/notification-sound-system-plan.md | 424 ----- .../archive/order-location-management-plan.md | 831 ---------- plans/archive/order-management-plan.md | 335 ---- ...der-workorder-shipment-integration-plan.md | 659 -------- plans/archive/process-management-plan.md | 397 ----- ...quote-auto-calculation-development-plan.md | 743 --------- .../quote-v2-auto-calculation-fix-plan.md | 262 --- .../react-fcm-push-notification-plan.md | 543 ------- .../react-server-component-audit-plan.md | 147 -- .../archive/sam-stat-database-design-plan.md | 1294 --------------- .../simulator-calculation-logic-mapping.md | 1057 ------------ plans/archive/stock-integration-plan.md | 421 ----- plans/archive/welfare-section-plan.md | 1021 ------------ plans/archive/work-order-plan.md | 409 ----- plans/bending-preproduction-stock-plan.md | 838 ---------- ...tendance-management_2026-01-14_23-30-00.md | 206 --- ...ank-transactions_2026-01-15_test-report.md | 231 --- ...ard-transactions_2026-01-15_test-report.md | 351 ---- .../employee-register_2026-01-14_20-00-00.md | 179 -- .../salary-management_2026-01-15_10-30-00.md | 175 -- ...sales-management_2026-01-15_test-report.md | 226 --- ...rawal-management_2026-01-15_test-report.md | 299 ---- plans/db-trigger-audit-system-plan.md | 1294 --------------- plans/docs-plans-cleanup-plan.md | 326 ++++ plans/document-management-system-plan.md | 1119 ------------- plans/hotfix-20260119-action-plan.md | 286 ---- plans/index_plans.md | 297 ++-- plans/items-migration-kyungdong-plan.md | 1399 ---------------- plans/quote-management-url-migration-plan.md | 1282 --------------- plans/quote-system-development-plan.md | 319 ---- plans/react-mock-remaining-tasks.md | 637 -------- plans/sub/archive/handover-report-plan.md | 154 -- plans/sub/archive/labor-plan.md | 145 -- plans/sub/categories-plan.md | 149 -- plans/sub/contract-plan.md | 171 -- plans/sub/items-plan.md | 144 -- plans/sub/order-management-plan.md | 153 -- plans/sub/pricing-plan.md | 141 -- plans/sub/site-management-plan.md | 148 -- plans/sub/structure-review-plan.md | 138 -- 65 files changed, 647 insertions(+), 33238 deletions(-) create mode 100644 plans/GUIDE.md delete mode 100644 plans/archive/5130-bom-migration-plan.md delete mode 100644 plans/archive/5130-sam-data-migration-plan.md delete mode 100644 plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md create mode 100644 plans/archive/HISTORY.md delete mode 100644 plans/archive/SEEDERS_LIST.md delete mode 100644 plans/archive/api-analysis-report.md delete mode 100644 plans/archive/bending-lot-pipeline-dev-plan.md delete mode 100644 plans/archive/bending-worklog-reimplementation-plan.md delete mode 100644 plans/archive/bidding-api-implementation-plan.md delete mode 100644 plans/archive/construction-api-integration-plan.md delete mode 100644 plans/archive/docs-update-plan.md delete mode 100644 plans/archive/document-management-system-changelog.md delete mode 100644 plans/archive/document-system-product-inspection.md delete mode 100644 plans/archive/erp-api-development-plan-d1.0-changes.md delete mode 100644 plans/archive/fcm-user-targeted-notification-plan.md delete mode 100644 plans/archive/formula-engine-real-data-plan.md delete mode 100644 plans/archive/items-table-unification-plan.md delete mode 100644 plans/archive/kd-items-migration-plan.md delete mode 100644 plans/archive/l2-permission-management-plan.md delete mode 100644 plans/archive/material-input-per-item-mapping-plan.md delete mode 100644 plans/archive/mes-integration-analysis-plan.md delete mode 100644 plans/archive/mng-item-formula-integration-plan.md delete mode 100644 plans/archive/mng-item-management-plan.md delete mode 100644 plans/archive/mng-quote-formula-development-plan.md delete mode 100644 plans/archive/notification-sound-system-plan.md delete mode 100644 plans/archive/order-location-management-plan.md delete mode 100644 plans/archive/order-management-plan.md delete mode 100644 plans/archive/order-workorder-shipment-integration-plan.md delete mode 100644 plans/archive/process-management-plan.md delete mode 100644 plans/archive/quote-auto-calculation-development-plan.md delete mode 100644 plans/archive/quote-v2-auto-calculation-fix-plan.md delete mode 100644 plans/archive/react-fcm-push-notification-plan.md delete mode 100644 plans/archive/react-server-component-audit-plan.md delete mode 100644 plans/archive/sam-stat-database-design-plan.md delete mode 100644 plans/archive/simulator-calculation-logic-mapping.md delete mode 100644 plans/archive/stock-integration-plan.md delete mode 100644 plans/archive/welfare-section-plan.md delete mode 100644 plans/archive/work-order-plan.md delete mode 100644 plans/bending-preproduction-stock-plan.md delete mode 100644 plans/clodeCheck/attendance-management_2026-01-14_23-30-00.md delete mode 100644 plans/clodeCheck/bank-transactions_2026-01-15_test-report.md delete mode 100644 plans/clodeCheck/card-transactions_2026-01-15_test-report.md delete mode 100644 plans/clodeCheck/employee-register_2026-01-14_20-00-00.md delete mode 100644 plans/clodeCheck/salary-management_2026-01-15_10-30-00.md delete mode 100644 plans/clodeCheck/sales-management_2026-01-15_test-report.md delete mode 100644 plans/clodeCheck/withdrawal-management_2026-01-15_test-report.md delete mode 100644 plans/db-trigger-audit-system-plan.md create mode 100644 plans/docs-plans-cleanup-plan.md delete mode 100644 plans/document-management-system-plan.md delete mode 100644 plans/hotfix-20260119-action-plan.md delete mode 100644 plans/items-migration-kyungdong-plan.md delete mode 100644 plans/quote-management-url-migration-plan.md delete mode 100644 plans/quote-system-development-plan.md delete mode 100644 plans/react-mock-remaining-tasks.md delete mode 100644 plans/sub/archive/handover-report-plan.md delete mode 100644 plans/sub/archive/labor-plan.md delete mode 100644 plans/sub/categories-plan.md delete mode 100644 plans/sub/contract-plan.md delete mode 100644 plans/sub/items-plan.md delete mode 100644 plans/sub/order-management-plan.md delete mode 100644 plans/sub/pricing-plan.md delete mode 100644 plans/sub/site-management-plan.md delete mode 100644 plans/sub/structure-review-plan.md diff --git a/plans/GUIDE.md b/plans/GUIDE.md new file mode 100644 index 0000000..3d0ed3a --- /dev/null +++ b/plans/GUIDE.md @@ -0,0 +1,127 @@ +# docs/plans 문서 가이드 (최소 원칙) + +> **작성일**: 2026-02-26 +> **상태**: 최소 원칙 (정리 완료 후 보강 예정) +> **참조**: `docs/INDEX.md`, `CLAUDE.md`에 링크 예정 + +--- + +## 1. 파일 명명 규칙 + +``` +[도메인]-[기능]-plan.md + +예시: + bending-preproduction-stock-plan.md + quote-order-sync-improvement-plan.md + document-system-work-log-plan.md +``` + +- 영문 소문자, 하이픈(`-`) 구분 +- 접미사 `-plan.md` 고정 +- 도메인 접두사 통일: + +| 도메인 | 접두사 | 예시 | +|--------|--------|------| +| 견적 | `quote-` | quote-calculation-api-plan.md | +| 수주 | `order-` | order-location-management-plan.md | +| 품목/BOM | `item-`, `bom-` | item-master-data-alignment-plan.md | +| 절곡/생산 | `bending-` | bending-preproduction-stock-plan.md | +| 문서/서식 | `document-` | document-system-master-plan.md | +| 관리자(mng) | `mng-` | mng-menu-system-plan.md | +| 시스템/인프라 | `db-`, `tenant-` | db-backup-system-plan.md | +| 프론트엔드 | `react-` | react-api-integration-plan.md | +| 마이그레이션 | `[출처]-migration-` | kd-orders-migration-plan.md | + +> 도메인 분류는 정리 완료 후 실제 남은 문서 기반으로 확정 예정 + +--- + +## 2. 문서 필수 섹션 + +| 섹션 | 필수 | 내용 | +|------|:----:|------| +| **목적** (상단 1줄) | ✅ | 왜 이 작업이 필요한가 | +| **현재 진행 상태** | ✅ | 마지막 완료 작업, 다음 작업, 진행률 | +| **대상 범위** | ✅ | Phase별 작업 항목 테이블 | +| **변경 이력** | ✅ | 날짜 + 변경 내용 | +| 참고 문서 | ⚪ | 관련 문서 링크 | +| 검증 결과 | ⚪ | 완료 시 작성 | + +--- + +## 3. 상태 표기법 + +### 문서 상태 (인덱스용) + +| 표기 | 의미 | +|------|------| +| 🟡 진행중 | 현재 작업중 | +| ⚪ 대기 | 미착수 / 선행조건 대기 | +| ✅ 완료 | 개발 완료 | + +### 항목 상태 (문서 내부용) + +| 표기 | 의미 | +|------|------| +| ⏳ | 대기 | +| 🔄 | 진행중 | +| ✅ | 완료 | +| ⚠️ | 컨펌 필요 | + +### 진행률 표기 + +``` +완료/전체 (%) +예: 5/8 (63%) +``` + +--- + +## 4. 문서 생명주기 + +``` +생성 (PLANNED) ← 개발 계획 수립 + ↓ 착수 +진행 (ACTIVE) ← 인덱스에 노출, 진행 상태 추적 + ↓ 개발 완료 +완료 (COMPLETED) ← 인덱스에서 완료 표기 + ↓ docs/ 구조화 시 +정식 문서에 반영 ← plan의 설계 결정/구현 상세를 docs/ 정식 문서로 이관 +``` + +- **plan 문서**: 개발 계획 수립 및 진행 추적 용도 +- **완료 후**: 유용한 내용(설계 결정, 구현 상세)은 `docs/` 정식 문서에 반영 +- **plan 파일 보관/삭제**: `docs/` 구조화 시 확정 + +--- + +## 5. 폴더 구조 + +``` +docs/plans/ +├── GUIDE.md ← 이 가이드 +├── index_plans.md ← ACTIVE + PLANNED 문서 인덱스 +├── [도메인]-*-plan.md ← 현행 계획 문서 +├── archive/ +│ └── HISTORY.md ← 완료 작업 요약 (기능별 섹션) +├── flow-tests/ ← JSON 테스트 케이스 (별도 관리) +└── SAM_ERP_Storyboard*/ ← 디자인 참조 (별도 관리) +``` + +--- + +## 6. 인덱스 관리 + +- 문서 생성/삭제 시 `index_plans.md` **동시 업데이트** +- **ACTIVE + PLANNED** 문서만 인덱스에 포함 +- 도메인별 섹션으로 그룹핑 +- 각 문서의 상태/진행률 표기 + +--- + +> **TODO (정리 완료 후 보강)** +> - 도메인 분류 체계 확정 (실제 남은 문서 기반) +> - 문서 간 관계 규칙 (상위/하위, 참조 관계) +> - 인덱스 관리 주기 및 방법 +> - docs/ 전체 구조와의 연계 정책 \ No newline at end of file diff --git a/plans/archive/5130-bom-migration-plan.md b/plans/archive/5130-bom-migration-plan.md deleted file mode 100644 index a970d91..0000000 --- a/plans/archive/5130-bom-migration-plan.md +++ /dev/null @@ -1,446 +0,0 @@ -# 5130 → SAM BOM 데이터 마이그레이션 계획 - -> **작성일**: 2025-01-20 -> **목적**: 5130 레거시 시스템의 BOM 데이터를 SAM items 테이블의 bom 컬럼에 마이그레이션 -> **기준 문서**: `api/app/Services/Quote/FormulaEvaluatorService.php` -> **상태**: ✅ 완료 (Serena ID: 5130-bom-migration-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | BOM 마이그레이션 실행 완료 (61건) | -| **다음 작업** | 견적 페이지에서 실제 테스트 (사용자 수동 확인) | -| **진행률** | 4/4 (100%) | -| **마지막 업데이트** | 2025-01-20 | - ---- - -## 1. 개요 - -### 1.1 배경 - -5130 레거시 시스템에서 SAM으로 품목(items) 마이그레이션이 완료되었으나, 완제품(FG)의 BOM 데이터가 마이그레이션되지 않아 다음 문제가 발생: - -``` -문제 현상: -- 견적 페이지에서 "국민방화스크린 (일체형) (S0001)" 선택 후 자동 견적 산출 → 합계 0원 -- 원인: S0001의 bom 컬럼이 NULL -- items 테이블에서 확인: SELECT bom FROM items WHERE code = 'S0001' → NULL -``` - -**기존 마이그레이션 상태:** -- Items: 608건 (KDunitprice → items) -- Orders: 24,424건 -- Order Items: 43,900건 -- ❌ BOM 데이터: 마이그레이션 안됨 - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. FormulaEvaluatorService 호환 BOM JSON 형식 생성 │ -│ 2. 동적 수량 계산을 위한 quantityFormula 필드 지원 │ -│ 3. childItemCode 기반 참조 (child_item_id 아님) │ -│ 4. 기존 SAM BOM 패턴과 일관성 유지 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | BOM JSON 데이터 추가, 매핑 테이블 생성 | 불필요 | -| ⚠️ 컨펌 필요 | 기존 items 데이터 수정, 새 마이그레이션 스크립트 | **필수** | -| 🔴 금지 | items 테이블 구조 변경, 기존 BOM 삭제 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `api/app/Services/Quote/FormulaEvaluatorService.php` - BOM 계산 로직 - ---- - -## 2. 데이터 구조 분석 - -### 2.1 5130 BOM 구조 - -``` -5130 DB (chandj) -├── KDunitprice (품목 마스터) -│ ├── prodcode: 품목 코드 -│ ├── item_name: 품목명 -│ └── item_div: [제품], [상품], [부재료], [원재료], [반제품] -│ -├── models (모델 마스터) -│ ├── model_id: PK -│ ├── model_name: KSS01, KSE01, KWE01... (모델 코드) -│ ├── major_category: 스크린 | 철재 -│ ├── finishing_type: SUS마감 | EGI마감 -│ └── guiderail_type: 벽면형 | 측면형 -│ -├── parts (1단계 BOM - 모델별 부품) -│ ├── part_id: PK -│ ├── model_id: FK → models -│ ├── part_name: 가이드레일, 하단마감재 등 -│ ├── spec: 120*70, 60*40 등 -│ ├── quantity: 수량 -│ ├── unit: SET, EA 등 -│ └── unitprice: 단가 (문자열, 콤마 포함) -│ -└── parts_sub (2단계 BOM - 부품별 원자재) - ├── subpart_id: PK - ├── part_id: FK → parts - ├── subpart_name: 1번(마감제), 2번(본체) 등 - ├── material: SUS 1.2T, EGI 1.55T 등 - ├── quantity: 수량 - ├── bendSum, plateSum, finalSum: 가공 관련 - └── unitPrice, computedPrice, lineTotal: 금액 -``` - -**5130 model_id별 데이터 현황:** -| model_id | model_name | category | finishing | guiderail | parts 수 | -|----------|------------|----------|-----------|-----------|----------| -| 12 | KSS01 | 스크린 | SUS마감 | 벽면형 | 2 | -| 13 | KSS01 | 스크린 | SUS마감 | 측면형 | 2 | -| 14 | KSE01 | 스크린 | SUS마감 | 벽면형 | 2 | -| ... | ... | ... | ... | ... | ... | - -**5130 KDunitprice item_div 분포:** -| item_div | 건수 | SAM item_type 매핑 | -|----------|------|-------------------| -| [제품] | 194건 | FG (완제품) | -| [상품] | 260건 | SM (부자재) | -| [부재료] | 48건 | SM (부자재) | -| [원재료] | 24건 | RM (원자재) | -| [반제품] | 73건 | SF (반제품) | -| [무형상품] | 4건 | CS (서비스) | - -### 2.2 SAM BOM 구조 - -```sql --- SAM items 테이블 BOM 컬럼 -items.bom: JSON -``` - -**SAM BOM JSON 형식 (FormulaEvaluatorService 호환):** -```json -[ - { - "childItemCode": "SF-SCR-F01", // 필수: 하위 품목 코드 - "quantity": 1, // 필수: 기본 수량 - "quantityFormula": "W*H/1000000", // 선택: 동적 수량 계산식 - "unit": "M2", // 선택: 단위 - "note": "스크린 원단" // 선택: 비고 - }, - { - "childItemCode": "SF-SCR-M01", - "quantity": 1, - "quantityFormula": "", - "unit": "EA", - "note": "소형용 모터" - } -] -``` - -**기존 SAM BOM 예시 (FG-SCR-001):** -```json -[ - {"unit":"M2","quantity":1,"childItemCode":"SF-SCR-F01","quantityFormula":"W*H/1000000"}, - {"unit":"M","quantity":1,"childItemCode":"SF-SCR-F02","quantityFormula":"H/1000"}, - {"unit":"EA","quantity":1,"childItemCode":"SF-SCR-M01","quantityFormula":"","note":"소형용"}, - {"unit":"EA","quantity":20,"childItemCode":"SM-B002","quantityFormula":"","note":"조립용"} -] -``` - -### 2.3 핵심 차이점 - -| 항목 | 5130 | SAM | -|------|------|-----| -| **BOM 저장 위치** | parts/parts_sub 테이블 | items.bom JSON 컬럼 | -| **연결 기준** | model_id (모델 기준) | childItemCode (품목 코드 기준) | -| **수량 계산** | 고정값 + estimate.detailJson | quantityFormula 동적 계산 | -| **단가 계산** | parts.unitprice 고정 | FormulaEvaluatorService 동적 | -| **계층 구조** | 2단계 (parts → parts_sub) | 1단계 (flat JSON array) | - ---- - -## 3. 마이그레이션 전략 - -### 3.1 접근 방식: 수동 매핑 + 템플릿 기반 - -5130의 BOM 구조와 SAM의 BOM 구조가 근본적으로 다르기 때문에, 자동 변환이 아닌 **수동 매핑 + 템플릿 기반** 접근 필요: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 전략: 완제품(FG) 유형별 BOM 템플릿 정의 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. SCREEN 완제품 → screen_bom_template │ -│ 2. STEEL 완제품 → steel_bom_template │ -│ 3. BENDING 완제품 → bending_bom_template │ -│ │ -│ 각 템플릿은 FormulaEvaluatorService 호환 JSON 형식으로 정의 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 완제품-모델 매핑 - -**매핑 대상 (SAM items WHERE item_type='FG' AND source='5130'):** -```sql --- SAM에서 5130에서 마이그레이션된 완제품 목록 -SELECT id, code, name, item_category -FROM items -WHERE item_type = 'FG' - AND (legacy_code IS NOT NULL OR code LIKE 'S%'); -``` - -**주요 완제품 매핑 예시:** -| SAM code | SAM name | item_category | 5130 model | -|----------|----------|---------------|------------| -| S0001 | 국민방화스크린(일체형) | SCREEN | KSS01 (스크린/SUS/벽면형) | -| S0002 | 국민방화스크린(분리형) | SCREEN | KSE01 (스크린/SUS/벽면형) | -| ... | ... | ... | ... | - -### 3.3 BOM 템플릿 정의 - -**SCREEN 완제품 BOM 템플릿:** -```json -[ - {"childItemCode": "RM-SCR-FABRIC", "quantity": 1, "quantityFormula": "W*H/1000000", "unit": "M2", "note": "스크린 원단"}, - {"childItemCode": "PT-SCR-GUIDE", "quantity": 1, "quantityFormula": "H/1000", "unit": "M", "note": "가이드레일"}, - {"childItemCode": "PT-SCR-BOTTOM", "quantity": 1, "quantityFormula": "W/1000", "unit": "M", "note": "하단바"}, - {"childItemCode": "PT-SCR-CASE", "quantity": 1, "quantityFormula": "W/1000", "unit": "M", "note": "케이스"}, - {"childItemCode": "PT-SCR-MOTOR", "quantity": 1, "quantityFormula": "", "unit": "EA", "note": "모터"} -] -``` - ---- - -## 4. 작업 절차 - -### 4.1 Phase 1: 하위 품목 확인 및 생성 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | BOM에 필요한 하위 품목(SF, PT, RM) 목록 정의 | ✅ | 52개 품목 정의됨 | -| 1.2 | SAM items 테이블에 하위 품목 존재 여부 확인 | ✅ | 52개 모두 존재 확인 | -| 1.3 | 누락된 하위 품목 생성 (필요시) | ✅ | 누락 품목 없음 (생성 불필요) | - -### 4.2 Phase 2: BOM 템플릿 정의 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | SCREEN 완제품용 BOM 템플릿 정의 | ✅ | FG-SCR-001 (14개 항목) | -| 2.2 | STEEL 완제품용 BOM 템플릿 정의 | ✅ | FG-STL-001 (12개 항목) | -| 2.3 | BENDING 완제품용 BOM 템플릿 정의 | ✅ | FG-BND-001 (6개 항목) | - -### 4.3 Phase 3: 마이그레이션 스크립트 작성 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | Migrate5130Bom 커맨드 생성 | ✅ | `api/app/Console/Commands/Migrate5130Bom.php` | -| 3.2 | 완제품-템플릿 매핑 로직 구현 | ✅ | item_category 기반 매핑 | -| 3.3 | items.bom 컬럼 업데이트 로직 구현 | ✅ | DB::table 직접 업데이트 | -| 3.4 | 검증 로직 구현 | ✅ | dry-run, verbose 옵션 지원 | - -### 4.4 Phase 4: 검증 및 테스트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | Migrate5130Bom 커맨드 실행 | ✅ | 61건 처리 완료 | -| 4.2 | 견적 페이지에서 실제 테스트 | ⏳ | 사용자 수동 확인 필요 | -| 4.3 | 결과 문서화 | ✅ | 본 문서 업데이트 | - ---- - -## 5. 기술 상세 - -### 5.1 FormulaEvaluatorService BOM 처리 로직 - -```php -// api/app/Services/Quote/FormulaEvaluatorService.php - -// BOM JSON 필드 사용 위치: -// 1. getBomItems() - bom JSON 파싱 -// 2. calculateBomQuantity() - quantityFormula 평가 -// 3. childItemCode로 하위 품목 조회 - -// 주요 변수: -// - W0, H0: 개구부 치수 (입력값) -// - W1, H1: 제작 치수 (계산값) -// - W, H: W1, H1과 동일 -// - M: 면적 (m²) -// - K: 중량 (kg) -``` - -### 5.2 마이그레이션 스크립트 구조 - -```php -// api/app/Console/Commands/Migrate5130Bom.php - -class Migrate5130Bom extends Command -{ - protected $signature = 'migration:migrate-5130-bom - {--dry-run : 실제 변경 없이 시뮬레이션} - {--code= : 특정 품목 코드만 처리}'; - - // 1. item_category별 BOM 템플릿 정의 - private array $bomTemplates = [ - 'SCREEN' => [...], - 'STEEL' => [...], - 'BENDING' => [...] - ]; - - // 2. 완제품 조회 (5130 마이그레이션된 FG) - // 3. 템플릿 기반 BOM JSON 생성 - // 4. items.bom 컬럼 업데이트 -} -``` - -### 5.3 검증 쿼리 - -```sql --- 마이그레이션 전: BOM이 NULL인 완제품 -SELECT code, name, item_category -FROM items -WHERE item_type = 'FG' - AND item_category IN ('SCREEN', 'STEEL', 'BENDING') - AND (bom IS NULL OR bom = '[]'); - --- 마이그레이션 후: BOM이 있는 완제품 -SELECT code, name, item_category, JSON_LENGTH(bom) as bom_count -FROM items -WHERE item_type = 'FG' - AND item_category IN ('SCREEN', 'STEEL', 'BENDING') - AND bom IS NOT NULL - AND JSON_LENGTH(bom) > 0; -``` - ---- - -## 6. 컨펌 대기 목록 - -> 모든 승인 항목 완료 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | BOM 템플릿 확정 | SCREEN/STEEL/BENDING별 템플릿 | 견적 계산 | ✅ 완료 | -| 2 | 하위 품목 코드 확정 | childItemCode 명명 규칙 | items 테이블 | ✅ 완료 | -| 3 | 마이그레이션 실행 | items.bom 업데이트 | 완제품 61건 | ✅ 완료 | - ---- - -## 7. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-20 | 초안 | 계획 문서 작성 | - | - | -| 2025-01-20 | 분석 | 5130/SAM BOM 구조 분석 완료 | - | - | -| 2025-01-20 | 스크립트 | Migrate5130Bom 커맨드 생성 | `api/app/Console/Commands/Migrate5130Bom.php` | ✅ | -| 2025-01-20 | 실행 | BOM 마이그레이션 실행 (61건) | items.bom 컬럼 | ✅ | -| 2025-01-20 | 문서화 | 결과 문서화 완료 | 본 문서 | ✅ | - ---- - -## 8. 참고 문서 - -- **FormulaEvaluatorService**: `api/app/Services/Quote/FormulaEvaluatorService.php` -- **기존 마이그레이션**: `api/app/Console/Commands/Migrate5130PriceItems.php` -- **검증 커맨드**: `api/app/Console/Commands/Verify5130Calculation.php` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - ---- - -## 9. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 9.1 세션 시작 시 (Load Strategy) -```javascript -// 순차적 로드 -read_memory("5130-bom-migration-state") // 1. 상태 파악 -read_memory("5130-bom-migration-rules") // 2. 규칙 확인 -read_memory("5130-bom-migration-mappings") // 3. 매핑 확인 -``` - -### 9.2 Serena 메모리 구조 -- `5130-bom-migration-state`: { phase, progress, next_step, last_decision } -- `5130-bom-migration-rules`: BOM 템플릿 정의, 변환 규칙 -- `5130-bom-migration-mappings`: 완제품-모델 매핑 테이블 - ---- - -## 10. 검증 결과 - -> 2025-01-20 마이그레이션 실행 완료 - -### 10.1 마이그레이션 실행 결과 - -``` -📊 카테고리별 BOM 적용 현황 (tenant_id=287): - SCREEN: 35건 - STEEL: 11건 - BENDING: 15건 - -✅ BOM 적용 완료: 61건 -⏳ BOM 미적용: 0건 -``` - -### 10.2 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| S0001 BOM JSON 확인 | childItemCode 5개 이상 | 14개 항목 적용됨 | ✅ | -| S0001 + W0=2500, H0=2000 | 견적 금액 > 0 | 사용자 확인 필요 | ⏳ | - -### 10.3 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 완제품 BOM NULL → JSON 변환 | ✅ | 61건 변환 완료 | -| BOM JSON 형식 호환 | ✅ | FormulaEvaluatorService 호환 형식 | -| 견적 계산 정상 동작 | ⏳ | 사용자 수동 확인 필요 | - -### 10.4 BOM 템플릿 상세 - -| 카테고리 | 소스 템플릿 | BOM 항목 수 | 적용 완제품 수 | -|----------|------------|------------|--------------| -| SCREEN | FG-SCR-001 | 14개 | 35건 | -| STEEL | FG-STL-001 | 12개 | 11건 | -| BENDING | FG-BND-001 | 6개 | 15건 | - ---- - -## 11. 자기완결성 점검 결과 - -> Phase 5.5에서 수행된 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | S0001 등 BOM NULL → 견적 0원 문제 해결 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | SCREEN/STEEL/BENDING 완제품 대상 | -| 4 | 의존성이 명시되어 있는가? | ✅ | FormulaEvaluatorService, 하위 품목 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 참조 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 참조 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10.1 참조 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/조건 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 4. 작업 절차 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5.2 마이그레이션 스크립트 | -| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/5130-sam-data-migration-plan.md b/plans/archive/5130-sam-data-migration-plan.md deleted file mode 100644 index 5451064..0000000 --- a/plans/archive/5130-sam-data-migration-plan.md +++ /dev/null @@ -1,828 +0,0 @@ -# 5130 → SAM 자재/수주 데이터 마이그레이션 계획 - -> **작성일**: 2025-01-19 -> **목적**: 5130 레거시 시스템의 품목(KDunitprice, price_*) 및 수주(output, output_extra) 데이터를 SAM 구조(items, orders, order_items)로 마이그레이션 -> **기준 문서**: 5130/output/_row.php, 5130/KDunitprice/_row.php, api/database/migrations/* -> **상태**: ✅ 마이그레이션 완료 (Phase 1-4 완료) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 - 전체 데이터 마이그레이션 실행 완료 | -| **다음 작업** | 완료 (운영 검증 후 문서 아카이브) | -| **진행률** | 14/14 (100%) | -| **마지막 업데이트** | 2026-01-20 | - ---- - -## 1. 개요 - -### 1.1 배경 - -5130 레거시 시스템에서 운영 중인 자재/수주 데이터를 SAM 신규 시스템으로 마이그레이션해야 합니다. -- 5130: 플랫 테이블 구조 + JSON 컬럼으로 데이터 저장 -- SAM: 정규화된 관계형 테이블 구조 + JSON attributes 필드 - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 📊 데이터 (값): 5130 우선 - 실제 운영 중인 사이트 │ -│ 🏗️ 구조: SAM 우선 - 신규 정규화 설계 │ -│ 🧮 견적 수식: 동일성 유지 - 5130과 SAM 결과값 일치 필수 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------:| -| ✅ 즉시 가능 | 필드 추가/변경, 마이그레이션 스크립트 작성, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 테이블 구조 변경, 새 컬럼 추가, 데이터 타입 변경 | **필수** | -| 🔴 금지 | 기존 데이터 삭제, 운영 DB 직접 수정, 스키마 파괴적 변경 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - 데이터베이스 스키마 -- `api/CLAUDE.md` - API 개발 규칙 - ---- - -## 2. 테이블 매핑 개요 - -### 2.1 5130 소스 테이블 - -| 테이블 | 용도 | 주요 필드 | -|--------|------|----------| -| `KDunitprice` | 단가표 (Ecount 연동) | prodcode, item_name, item_div, spec, unit, unitprice | -| `price_raw_materials` | 원자재 단가 | JSON itemList | -| `price_bend` | 절곡 단가 | JSON itemList | -| `output` | 수주 마스터 | ~80개 필드, JSON (screenlist, slatlist, motorList 등) | -| `output_extra` | 수주 부가정보 | ~30개 필드 (parent_num으로 연결) | - -### 2.2 SAM 대상 테이블 - -| 테이블 | 용도 | item_type | -|--------|------|-----------| -| `items` | 통합 품목 마스터 | FG, PT, SM, RM, CS | -| `orders` | 수주 마스터 | - | -| `order_items` | 수주 상세 | - | -| `order_item_components` | 자재 투입 | - | - -### 2.3 매핑 관계 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 5130 → SAM │ -├─────────────────────────────────────────────────────────────────┤ -│ KDunitprice → items (SM, RM, CS) │ -│ price_raw_materials.itemList → items (RM) │ -│ price_bend.itemList → items (PT) + price tables │ -│ output → orders │ -│ output.screenlist/slatlist → order_items │ -│ output_extra → order_items.attributes │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: 품목 마스터 마이그레이션 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | KDunitprice → items 매핑 분석 | ✅ | 10개 필드 매핑 완료 | -| 1.2 | price_raw_materials → items 매핑 | ✅ | RM 타입, itemList JSON 15개 필드 매핑 | -| 1.3 | price_bend → items 매핑 | ✅ | PT 타입, itemList JSON 18개 필드 매핑 | -| 1.4 | 품목 마이그레이션 스크립트 작성 | ✅ | `Migrate5130PriceItems.php` | -| 1.5 | 품목 데이터 검증 | ✅ | dry-run 621건 성공, item_type 분류 검증 완료 | - -### 3.2 Phase 2: 수주 마스터 마이그레이션 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | output → orders 필드 매핑 | ✅ | 69개 필드 분석, 상세 매핑 완료 | -| 2.2 | output JSON → order_items 변환 | ✅ | screenlist, slatlist 구조 분석 완료 | -| 2.3 | output_extra → order_items.attributes | ✅ | 33개 필드, motorList/bendList 등 | -| 2.4 | 수주 마이그레이션 스크립트 작성 | ✅ | `Migrate5130Orders.php` + `order_id_mappings` 테이블 | -| 2.5 | 수주 데이터 검증 | ✅ | dry-run 100건 성공, 필드 매핑 검증 완료 | - -### 3.3 Phase 3: 견적 로직 검증 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 5130 견적 수식 분석 | ✅ | write_form_script.php + fetch_unitprice.php 분석 완료 | -| 3.2 | SAM 견적 수식 구현/검증 | ✅ | Legacy5130Calculator.php + Verify5130Calculation.php | -| 3.3 | 검증 테스트 실행 | ✅ | 5/5 테스트 케이스 통과, 100% 일치 | - ---- - -## 4. 상세 필드 매핑 - -### 4.1 KDunitprice → items - -| 5130 필드 | SAM 필드 | 타입 | 비고 | -|-----------|----------|------|------| -| prodcode | code | string | 품목코드 | -| item_name | name | string | 품목명 | -| item_div | item_type 판별 기준 | - | SM/RM/CS 분류 | -| spec | attributes.spec | JSON | 규격 | -| unit | unit | string | 단위 | -| unitprice | attributes.unit_price | JSON | 단가 | - -### 4.2 output → orders (상세 매핑) - -#### 4.2.1 기본 정보 매핑 - -| 5130 필드 | SAM 필드 | 타입 변환 | 비고 | -|-----------|----------|----------|------| -| num | options.legacy_num | int→JSON | 5130 원본 PK 보존 | -| - | id | auto | SAM 신규 PK | -| - | tenant_id | 287 | 경동기업 고정 | -| outdate | received_at | date→datetime | 수주일 | -| orderdate | options.order_date | date | 발주일 | -| outworkplace | site_name | varchar(50) | 현장명 | -| orderman | options.orderman | varchar(20) | 수주담당자 | -| con_num | client_id | int→FK | 거래처 (조회 필요) | -| outputplace | options.output_place | varchar(50) | 출고장소 | -| receiver | options.receiver | varchar(20) | 수령인 | -| phone | client_contact | varchar(15) | 연락처 | -| comment | memo | varchar(250) | 메모 | -| delivery | delivery_method_code | varchar(15) | 배송방법 | - -#### 4.2.2 상태 필드 매핑 - -| 5130 필드 | SAM 필드 | 변환 규칙 | 비고 | -|-----------|----------|----------|------| -| regist_state | status_code | '등록'→'REGISTERED' | 주 상태 | -| screen_state | options.screen_state | 그대로 | 방충망 상태 | -| slat_state | options.slat_state | 그대로 | 슬랫 상태 | -| bend_state | options.bend_state | 그대로 | 절곡 상태 | -| motor_state | options.motor_state | 그대로 | 모터 상태 | - -#### 4.2.3 수량/금액 필드 - -| 5130 필드 | SAM 필드 | 비고 | -|-----------|----------|------| -| screen_su | quantity (합산) | 방충망 수량 | -| slat_su | quantity (합산) | 슬랫 수량 | -| screen_m2 | options.screen_m2 | 방충망 면적 | -| slat_m2 | options.slat_m2 | 슬랫 면적 | -| output_extra.EstimateFinalSum | total_amount | 최종금액 | -| output_extra.EstimateDiscount | discount_amount | 할인금액 | -| output_extra.EstimateDiscountRate | discount_rate | 할인율 | - -#### 4.2.4 JSON → order_items 변환 대상 - -| 5130 JSON 필드 | order_items 유형 | 비고 | -|----------------|-----------------|------| -| screenlist | item_type='SCREEN' | 방충망 품목 | -| slatlist | item_type='SLAT' | 슬랫 품목 | -| output_extra.motorList | item_type='MOTOR' | 모터 품목 | -| output_extra.bendList | item_type='BEND' | 절곡 품목 | -| output_extra.etcList | item_type='ETC' | 기타 품목 | -| output_extra.controllerList | item_type='CTRL' | 컨트롤러 | -| deliveryfeeList | item_type='DELIVERY' | 배송비 | - -#### 4.2.5 options JSON에 보존할 필드 - -```json -{ - "legacy_num": "5130 num", - "legacy_extra_num": "output_extra num", - "orderman": "수주담당자", - "output_place": "출고장소", - "receiver": "수령인", - "secondord": "2차 주문처", - "secondordman": "2차 주문 담당자", - "secondordmantel": "2차 주문 연락처", - "screen_state": "방충망 상태", - "slat_state": "슬랫 상태", - "bend_state": "절곡 상태", - "motor_state": "모터 상태", - "screen_m2": "방충망 면적", - "slat_m2": "슬랫 면적", - "warranty": "보증서 여부", - "warrantyNum": "보증서 번호", - "lotNum": "로트번호", - "prodCode": "제품코드", - "ACI": { - "regDate": "인정검사 등록일", - "askDate": "인정검사 요청일", - "doneDate": "인정검사 완료일", - "memo": "인정검사 메모", - "check": "인정검사 체크", - "groupCode": "인정검사 그룹코드", - "groupName": "인정검사 그룹명" - }, - "pjnum": "프로젝트 번호", - "major_category": "대분류", - "position": "위치", - "makeWidth": "제작폭", - "makeHeight": "제작높이", - "maguriWing": "마구리날개" -} -``` - -### 4.3 screenlist/slatlist → order_items - -#### 4.3.1 screenlist JSON 구조 - -```json -{ - "floors": "층수", - "text1": "표시텍스트1", - "text2": "표시텍스트2 (요약)", - "memo": "메모 (재질)", - "cutwidth": "절단폭", - "cutheight": "절단높이", - "number": "수량", - "exititem": "출고여부", - "printside": "인쇄면", - "direction": "방향", - "intervalnum": "간격수", - "intervalnumsecond": "2차간격수", - "exitinterval": "출고간격", - "cover": "커버", - "drawbottom1": "하부도면1", - "drawbottom2": "하부도면2", - "drawbottom3": "하부도면3", - "draw": "도면파일", - "done_check": "완료체크", - "remain_check": "잔여체크", - "mid_check": "중간체크", - "left_check": "좌측체크", - "right_check": "우측체크" -} -``` - -#### 4.3.2 screenlist → order_items 매핑 - -| screenlist 필드 | order_items 필드 | 비고 | -|-----------------|-----------------|------| -| - | serial_no | 순번 (1부터) | -| cutwidth + 'x' + cutheight | specification | 규격 (예: 3260x4000) | -| floors | floor_code | 층수 | -| text1 | symbol_code | 기호 | -| number | quantity | 수량 | -| memo | remarks | 메모 (재질 등) | -| text2 | note | 요약 텍스트 | -| (전체) | attributes | 원본 JSON 보존 | - -#### 4.3.3 slatlist JSON 구조 - -```json -{ - "floors": "층수", - "text1": "기호 (FST-1 등)", - "text2": "요약텍스트", - "memo": "메모 (재질 EGI 1.6T 등)", - "cutwidth": "절단폭", - "cutheight": "절단높이 (총H)", - "number": "수량", - "exititem": "출고여부", - "intervalnum": "간격수 (매수)", - "hinge": "힌지", - "hingenum": "힌지수량", - "hinge_direction": "힌지방향", - "done_check": "완료체크" -} -``` - -### 4.4 output_extra 상세 매핑 - -#### 4.4.1 금액 관련 필드 - -| 5130 필드 | SAM 필드 | 비고 | -|-----------|----------|------| -| estimateTotal | orders.supply_amount | 공급가액 | -| EstimateFirstSum | options.estimate_first | 최초견적 | -| EstimateUpdatetSum | options.estimate_update | 변경견적 | -| EstimateDiffer | options.estimate_diff | 차액 | -| EstimateDiscountRate | orders.discount_rate | 할인율 | -| EstimateDiscount | orders.discount_amount | 할인금액 | -| EstimateFinalSum | orders.total_amount | 최종금액 | -| estimateSurang | options.estimate_quantity | 견적수량 | -| inspectionFee | options.inspection_fee | 검사비용 | - -#### 4.4.2 JSON 리스트 필드 (→ order_items) - -| 5130 필드 | 건수 | 구조 | SAM 변환 | -|-----------|------|------|----------| -| motorList | 7건 | col1~col8 | order_items (MOTOR) | -| bendList | 10건 | col1~col8 | order_items (BEND) | -| etcList | - | col1~col5 | order_items (ETC) | -| controllerList | - | col1~col4 | order_items (CTRL) | - -#### 4.4.3 motorList col 매핑 - -| col | 내용 | order_items 필드 | -|-----|------|-----------------| -| col1 | 품명 (전동개폐기_단상 220V) | item_name | -| col2 | 용량 (300kg) | specification | -| col3 | 규격 (380*180) | attributes.dimension | -| col4 | 인치 (5인치) | attributes.inch | -| col5 | 수량 | quantity | -| col6 | 형태 (신형) | attributes.type | -| col7 | 옵션 | attributes.option | -| col8 | 전원 (단상) | attributes.power | - -#### 4.4.4 bendList col 매핑 - -| col | 내용 | order_items 필드 | -|-----|------|-----------------| -| col1 | 품명 (가이드레일) | item_name | -| col2 | 재질 (EGI 1.6T) | specification | -| col3 | 길이 (3000) | attributes.length | -| col5 | 폭 (332) | attributes.width | -| col6 | 도면이미지 | attributes.drawing | -| col7 | 수량 | quantity | -| col8 | 비고 | remarks | - -### 4.5 견적 수식 분석 (Phase 3.1) - -> **분석 대상**: `5130/output/write_form_script.php` (JS), `5130/estimate/fetch_unitprice.php` (PHP) - -#### 4.5.1 절곡품 단가 계산 - -**함수**: `getBendPlatePrice(material, thickness, length, width, qty)` - -```javascript -// 5130/output/write_form_script.php (lines 5780-5822) -// item_bend 배열: { col1: 재질, col5: 두께, col17: 면적당단가(원/m²) } - -// 1. 재질/두께 정규화 -EGI: 1.15 → 1.2, 1.55 → 1.6 -SUS: 1.15 → 1.2, 1.55 → 1.5 - -// 2. 면적 계산 (mm² → m²) -areaM² = (length × width) / 1,000,000 - -// 3. 총액 계산 (절삭) -total = Math.floor(unitPricePerM² × areaM² × qty) -``` - -**데이터 소스**: `price_bend.itemList` → `window.item_bend` (JS 전역) - -#### 4.5.2 비인정 스크린 단가 계산 - -**함수**: 익명 함수 (tables 배열 내) - -```javascript -// 5130/output/write_form_script.php (lines 6794-6822) -// materialBasePrice에서 재질(material)로 단가 조회 - -// 1. 단가 조회 -unitprice = materialBasePrice[material] || 0 - -// 2. 수량 계산 (타입별 분기) -if (원단류) { - // 세로 기준 1000mm 단위 - surang = height / 1000 -} else { - // 일반 면적 기준 - surang = (width × height) / 1,000,000 × qty -} - -// 3. 총액 -total = unitprice × surang -``` - -**데이터 소스**: `price_raw_materials.itemList` → `window.materialBasePrice` (JS 전역) - -#### 4.5.3 철재 스라트 비인정 단가 - -**함수**: 익명 함수 (tables 배열 내) - -```javascript -// 5130/output/write_form_script.php (lines 6824-6881) - -// 1. 유형별 단가 조회 -type = 방화셔터/방범셔터/단열셔터/이중파이프/조인트바 -unitprice = materialBasePrice[type] || 0 - -// 2. 수량 계산 (유형별 분기) -if (면적 기준: 방화/방범/단열/이중파이프) { - surang = (width × height) / 1,000,000 × qty -} else if (수량 기준: 조인트바) { - surang = qty -} - -// 3. 총액 -total = unitprice × surang -``` - -#### 4.5.4 전동 개폐기/제어기 조회 - -**함수**: `lookupMotorPrice(row)`, `lookupControllerPrice(row)` - -```javascript -// 5130/output/write_form_script.php (lines 6886-6920) - -// KDunitprice 테이블에서 조회 -// unitInfo: { prodcode → unitprice } 매핑 - -// 전동 개폐기 -unitprice = lookupMotorPrice(row) -// → row 데이터(용량, 전원, 형태 등)로 KDunitprice 조회 - -// 제어기 -unitprice = lookupControllerPrice(row) -// → row 데이터(유형, 규격)로 KDunitprice 조회 -``` - -**데이터 소스**: `KDunitprice` → `window.unitInfo` (JS 전역) - -#### 4.5.5 모터 용량 계산 (핵심 로직) - -**함수**: `calculateMotorSpec($item, $weight, $BracketInch)` (PHP) - -```php -// 5130/estimate/fetch_unitprice.php (lines 200-350) - -// 1. 품목 유형 판별 -$ItemSel = (substr($item['col4'], 0, 2) === 'KS' || - substr($item['col4'], 0, 2) === 'KW') - ? '스크린' : '철재'; - -// 2. 용량 결정 테이블 -// 스크린: 150K ~ 600K -// 철재: 300K ~ 1000K -// Weight + BracketInch 조합으로 용량 결정 - -// 3. 브라켓 사이즈 매핑 -300-400K → 530×320 -500-600K → 600×350 -800-1000K → 690×390 -``` - -#### 4.5.6 기타 계산 함수 - -| 함수 | 용도 | 계산식 | -|------|------|--------| -| `calculateGuidrail()` | 가이드레일 수량 | `col17 / 3490` (기본 길이) | -| `calculateShaft()` | 샤프트 단가 | `col19 × 수량`, 길이별 조회 | -| `calculatePipe()` | 파이프 단가 | `col4(길이)`, `col2(규격)`으로 `col8(단가)` 조회 | -| `slatPrice()` | 인정 슬랫 단가 | `price_raw_materials.col13` | -| `unapprovedSlatPrice()` | 비인정 슬랫 단가 | `price_raw_materials.col15` | - -#### 4.5.7 전역 데이터 구조 (JS) - -```javascript -// 5130/output/write_form.php에서 PHP→JS 전달 - -// 비인정 자재 단가 (재질 → 단가) -window.materialBasePrice = { - "실리카": 12000, - "폴리에스터": 8500, - // ... -}; - -// 비인정 자재 코드 (재질 → 코드) -window.materialBaseCode = { - "실리카": "RM001", - // ... -}; - -// 절곡품 단가표 -var item_bend = [ - { col1: "EGI", col5: 1.2, col17: 45000 }, - { col1: "SUS", col5: 1.5, col17: 85000 }, - // ... -]; - -// KDunitprice 단가 (prodcode → unitprice) -window.unitInfo = { - "MOT300": 250000, - "MOT500": 380000, - // ... -}; -``` - -#### 4.5.8 SAM 구현 시 고려사항 - -| 구분 | 5130 방식 | SAM 구현 방향 | -|------|----------|--------------| -| 단가 조회 | JS 전역 변수 | Service 클래스 + DB 쿼리 | -| 면적 계산 | JS (mm² → m²) | PHP Helper 함수 | -| 두께 매핑 | JS 하드코딩 | 설정 테이블 or Enum | -| 모터 용량 | PHP 조건문 | 룰 엔진 or 매핑 테이블 | -| 반올림/절삭 | `Math.floor()` | `floor()` 동일 적용 | - ---- - -## 5. 작업 절차 - -### 5.1 단계별 절차 - -``` -Step 1: 품목 마스터 분석 (Phase 1.1-1.3) -├── KDunitprice 테이블 구조 상세 분석 -├── price_raw_materials JSON 구조 분석 -├── price_bend JSON 구조 분석 -└── SAM items 테이블과 매핑 확정 - -Step 2: 품목 마이그레이션 (Phase 1.4-1.5) -├── 마이그레이션 스크립트 작성 (Artisan Command) -├── 테스트 데이터로 검증 -└── 전체 데이터 마이그레이션 - -Step 3: 수주 마스터 분석 (Phase 2.1-2.3) -├── output 테이블 80개 필드 분석 -├── JSON 필드 (screenlist 등) 구조 분석 -├── output_extra 연결 관계 분석 -└── SAM orders/order_items 매핑 확정 - -Step 4: 수주 마이그레이션 (Phase 2.4-2.5) -├── 마이그레이션 스크립트 작성 -├── JSON → 관계형 변환 로직 구현 -├── 테스트 데이터로 검증 -└── 전체 데이터 마이그레이션 - -Step 5: 견적 로직 검증 (Phase 3) -├── 5130 견적 계산 JS 분석 -├── SAM에서 동일 로직 구현/검증 -└── 샘플 데이터로 결과 비교 -``` - -### 5.2 분석 템플릿 - -```markdown -### [테이블명] 분석 - -**현재 상태 (5130):** -- 테이블: [테이블명] -- 필드 수: [N]개 -- 레코드 수: [N]건 - -**목표 상태 (SAM):** -- 테이블: [테이블명] -- 매핑 필드: [N]개 - -**필드 매핑:** -| 5130 | SAM | 변환 로직 | -|------|-----|----------| -| | | | - -**특이사항:** -- [ ] JSON 변환 필요 여부 -- [ ] 타입 변환 필요 여부 -- [ ] 기본값 처리 방법 -``` - ---- - -## 6. 컨펌 대기 목록 - -> 테이블 구조 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| - | - | - | - | - | - ---- - -## 7. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-19 | 초안 | 문서 초안 작성 | - | - | -| 2025-01-19 | Phase 1.1 | KDunitprice → items 매핑 분석 완료 | - | - | -| 2025-01-19 | Phase 1.2 | price_raw_materials → items 매핑 분석 완료 (itemList JSON 15필드) | - | - | -| 2025-01-19 | Phase 1.3 | price_bend → items 매핑 분석 완료 (itemList JSON 18필드) | - | - | -| 2025-01-19 | Phase 1.4 | 품목 마이그레이션 스크립트 작성 완료 | `api/app/Console/Commands/Migrate5130PriceItems.php` | - | -| 2026-01-19 | Phase 2.4 | 수주 마이그레이션 스크립트 작성 완료 | `api/app/Console/Commands/Migrate5130Orders.php`, `api/database/migrations/2026_01_19_202830_create_order_id_mappings_table.php` | - | -| 2026-01-19 | Phase 3.1 | 5130 견적 수식 분석 완료 | `5130/output/write_form_script.php`, `5130/estimate/fetch_unitprice.php` | - | -| 2026-01-19 | Phase 3.2 | SAM 견적 수식 구현 완료 | `api/app/Helpers/Legacy5130Calculator.php`, `api/app/Console/Commands/Verify5130Calculation.php` | - | -| 2026-01-19 | Phase 3.3 | 견적 수식 검증 테스트 실행 | 5/5 테스트 케이스 100% 일치 | - | -| 2026-01-20 | 준비 완료 | Phase 1-3 모든 준비 작업 완료, 실행 대기 | 13/13 작업 완료 | - | -| 2026-01-20 | Phase 4 | 전체 마이그레이션 실행 완료 | items 608건, orders 24,424건, order_items 43,900건 | ✅ | - ---- - -## 8. 참고 문서 - -### 8.1 5130 소스 코드 - -- **수주 폼**: `5130/output/write_form.php` (1176줄) -- **견적 계산 JS**: `5130/output/write_form_script.php` (302KB, ~7000줄) -- **단가 조회 PHP**: `5130/estimate/fetch_unitprice.php` (875줄) -- **output 필드**: `5130/output/_row.php` (~80개 필드) -- **output_extra 필드**: `5130/output/_row_extra.php` (~30개 필드) -- **단가표 필드**: `5130/KDunitprice/_row.php` - -### 8.2 SAM 스키마 - -- **items 테이블**: `api/database/migrations/2025_12_13_152507_create_items_table.php` -- **orders 테이블**: `api/database/migrations/2024_11_19_000001_create_orders_table.php` -- **order_items 테이블**: `api/database/migrations/2024_11_19_000002_create_order_items_table.php` - -### 8.3 SAM 모델 - -- **Order 모델**: `api/app/Models/Orders/Order.php` -- **OrderItem 모델**: `api/app/Models/Orders/OrderItem.php` -- **Item 모델**: `api/app/Models/Items/Item.php` - ---- - -## 9. 세션 및 메모리 관리 정책 - -### 9.1 세션 시작 시 (Load Strategy) -```javascript -// 순차적 로드 -read_memory("5130-migration-state") // 1. 상태 파악 -read_memory("5130-migration-mappings") // 2. 매핑 정보 로드 -read_memory("5130-migration-rules") // 3. 규칙 확인 -``` - -### 9.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 **Snapshot** | `write_memory("5130-migration-snapshot", "진행상황")` | -| **20% 이하** | 🧹 **Context Purge** | `write_memory("5130-migration-active", "현재 작업")` | -| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 | - -### 9.3 Serena 메모리 구조 -- `5130-migration-state`: { phase, progress, next_step } (JSON 구조) -- `5130-migration-mappings`: 테이블/필드 매핑 정보 (Text) -- `5130-migration-rules`: 변환 규칙, 타입 매핑 (Text) - ---- - -## 10. 검증 결과 - -### 10.1 Phase 1 품목 마이그레이션 검증 (2025-01-19) - -#### 소스 데이터 카운트 -| 테이블 | 총 건수 | 활성 건수 | 최신 버전 | -|--------|---------|----------|----------| -| KDunitprice | 603 | 601 (NULL/0) | - | -| price_raw_materials | 14 | 6 | 2025-06-18 | -| price_bend | 3 | 3 | 2025-03-09 | - -#### dry-run 검증 결과 -| 테이블 | Total | Migrated | Skipped | 결과 | -|--------|-------|----------|---------|:----:| -| KDunitprice | 601 | 601 | 0 | ✅ | -| price_raw_materials | 13 | 13 | 0 | ✅ | -| price_bend | 7 | 7 | 0 | ✅ | -| **합계** | **621** | **621** | **0** | ✅ | - -#### item_type 분류 검증 -| item_div | 예상 | 실제 | 결과 | -|----------|------|------|:----:| -| [상품] | FG | FG | ✅ | -| [제품] | FG | FG | ✅ | -| [반제품] | PT | PT | ✅ | -| [부재료] | SM | SM | ✅ | -| [원재료] | RM | RM | ✅ | -| [무형상품] | CS | CS | ✅ | - -#### item_div 분포 (KDunitprice 601건) -| item_div | 건수 | item_type | -|----------|------|-----------| -| [상품] | 259 | FG | -| [제품] | 193 | FG | -| [반제품] | 73 | PT | -| [부재료] | 48 | SM | -| [원재료] | 24 | RM | -| [무형상품] | 4 | CS | - -### 10.2 Phase 2 수주 마이그레이션 검증 (2026-01-19) - -#### 소스 데이터 현황 -| 테이블/필드 | 총 건수 | 비고 | -|-------------|---------|------| -| output | 24,584 | 전체 수주 | -| output (screenlist 있음) | 9,392 | 방충망 포함 | -| output (slatlist 있음) | 1,955 | 슬랫 포함 | -| output_extra (motorList 있음) | 7 | 모터 포함 | -| output_extra (bendList 있음) | 10 | 절곡 포함 | - -#### dry-run 검증 결과 -| 항목 | 건수 | 결과 | 비고 | -|------|------|:----:|------| -| orders | 100 | ✅ | 100건 테스트 성공 | -| order_items (screen) | - | ⏳ | 실제 실행 후 확인 | -| order_items (slat) | - | ⏳ | 실제 실행 후 확인 | -| order_items (motor) | 0 | ✅ | motorList 없는 범위 | -| order_items (bend) | 0 | ✅ | bendList 없는 범위 | - -#### 샘플 데이터 매핑 검증 -**샘플 num=25810** -| 5130 필드 | 값 | SAM 필드 | 변환 결과 | 검증 | -|-----------|-----|----------|----------|:----:| -| outdate | 2025-12-15 | received_at | 2025-12-15 00:00:00 | ✅ | -| outworkplace | IFC | site_name | IFC | ✅ | -| regist_state | 등록 | status_code | REGISTERED | ✅ | -| phone | 010-5231-3134 | client_contact | 010-5231-3134 | ✅ | -| comment | 실리카1틀/... | memo | 실리카1틀/... | ✅ | -| delivery | 직접배차 | delivery_method_code | 직접배차 | ✅ | -| screenlist[0].cutwidth×cutheight | 3260×4000 | specification | 3260x4000 | ✅ | -| screenlist[0].number | 1 | quantity | 1 | ✅ | -| screenlist[0].memo | 실리카 | remarks | 실리카 | ✅ | - -**motorList/bendList 구조 검증** -| col | motorList 매핑 | bendList 매핑 | 검증 | -|-----|---------------|--------------|:----:| -| col1 | item_name (전동개폐기_단상 220V) | item_name (가이드레일) | ✅ | -| col2 | specification (300kg) | specification (EGI 1.6T) | ✅ | -| col3 | attributes.dimension (380*180) | attributes.length (3000) | ✅ | -| col5 | quantity (2) | attributes.width (332) | ✅ | -| col6 | attributes.type (신형) | attributes.drawing (이미지경로) | ✅ | -| col7 | attributes.option | quantity (1) | ✅ | -| col8 | attributes.power (단상) | remarks | ✅ | - -### 10.3 데이터 정합성 요약 - -| 테이블 | 5130 건수 | SAM 건수 | 일치 | 비고 | -|--------|----------|----------|:----:|------| -| KDunitprice → items | 601 | (dry-run) | ✅ | Phase 1 검증 완료 | -| price_raw_materials → items | 13 | (dry-run) | ✅ | 최신 버전만 | -| price_bend → items | 7 | (dry-run) | ✅ | 최신 버전만 | -| output → orders | 24,584 | (dry-run) | ✅ | 100건 테스트 성공 | -| screenlist → order_items | 9,392+ | (대기) | ⏳ | 실제 마이그레이션 후 확인 | -| slatlist → order_items | 1,955+ | (대기) | ⏳ | 실제 마이그레이션 후 확인 | - -### 10.4 견적 수식 검증 (2026-01-19) - -#### 검증 도구 -- **Legacy5130Calculator.php**: 5130 호환 계산 헬퍼 클래스 -- **Verify5130Calculation.php**: 검증 Artisan 커맨드 -- **실행**: `php artisan migration:verify-5130-calculation --W0=3000 --H0=2500 --type=screen` - -#### 테스트 결과 - -| 케이스 | W0×H0 | 유형 | W1 (5130/SAM) | H1 (5130/SAM) | M (m²) | K (kg) | 결과 | -|--------|-------|------|---------------|---------------|--------|--------|:----:| -| 스크린 소형 | 1500×1200 | screen | 1640/1640 | 1550/1550 | 2.542 | 26.34 | ✅ | -| 스크린 중형 | 3000×2500 | screen | 3140/3140 | 2850/2850 | 8.949 | 60.41 | ✅ | -| 스크린 대형 | 5000×4000 | screen | 5140/5140 | 4350/4350 | 22.359 | 115.57 | ✅ | -| 철재 중형 | 2000×1800 | steel | 2110/2110 | 2150/2150 | 4.5365 | 113.41 | ✅ | -| 철재 대형 | 4000×3500 | steel | 4110/4110 | 3850/3850 | 15.8235 | 395.59 | ✅ | - -#### 검증 수식 - -``` -스크린 (screen): -├── W1 = W0 + 140 (마진) -├── H1 = H0 + 350 (마진) -├── M = (W1 × H1) / 1,000,000 (m²) -└── K = (M × 2) + (W0 / 1000 × 14.17) (kg) - -철재 (steel): -├── W1 = W0 + 110 (마진) -├── H1 = H0 + 350 (마진) -├── M = (W1 × H1) / 1,000,000 (m²) -└── K = M × 25 (kg) -``` - -#### 모터 용량/브라켓 사이즈 검증 - -| 케이스 | 중량(K) | 브라켓인치 | 모터용량 | 브라켓사이즈 | -|--------|---------|-----------|---------|-------------| -| 스크린 중형 | 60.41 | 124" | 600K | 600×350 | -| 철재 중형 | 113.41 | 84" | 1000K | 690×390 | - -**결과**: 5/5 테스트 케이스 통과 → ✅ **견적 수식 100% 일치 확인** - ---- - -## 11. 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 5130→SAM 데이터 마이그레이션 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 데이터 정합성 + 견적 동일성 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3 정의됨 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 5130 소스 + SAM 스키마 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 참조 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 참조 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10 참조 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 테이블/필드 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 5.1 단계별 절차 | -| Q3. 어떤 테이블을 매핑해야 하는가? | ✅ | 2. 테이블 매핑 개요 | -| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md b/plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md deleted file mode 100644 index aedaf24..0000000 --- a/plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md +++ /dev/null @@ -1,406 +0,0 @@ -# SAM ERP 대시보드 -## AI 리포트 핵심 키워드 색상 체계 가이드 -### (임계값 명확화 버전 v1.4) - -> 버전: D1.4 | 작성일: 2026년 1월 - ---- - -## 1. AI 리포트 색상 체계 개요 - -AI 리포트는 각 섹션별 핵심 키워드에 색상을 적용하여 사용자가 즉시 상태를 파악할 수 있도록 합니다. 모든 기준은 명확한 수치로 정의되어 일관된 적용이 가능합니다. - -### 1.1 색상 정의 - -| 색상 | 의미 | 적용 원칙 | 우선순위 | -|:---:|:---:|:---|:---:| -| 🔴 빨간색 | 경고 | 즉각 조치 필요, 한도/기준 초과, 손실 발생 | 1순위 (최우선) | -| 🟠 주황색 | 주의 | 기준의 80~100% 도달, 기한 임박, 검토 필요 | 2순위 | -| 🟢 녹색 | 긍정 | 목표 달성, 정상 완료, 개선, 입금/회수 | 3순위 | -| 🔵 파란색 | 양호 | 안정적 유지, 정상 진행 중, 충분히 확보 | 4순위 | - -### 1.2 공통 임계값 원칙 - -| 구분 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 | -|:---|:---|:---|:---|:---| -| 한도 사용률 | 100% 초과 | 85~100% | - | 85% 미만 | -| 전월/전기 대비 증감 | ±20% 이상 | ±10~20% | 개선 방향 변동 | ±10% 이내 | -| 예산 대비 | 100% 초과 | 90~100% | - | 90% 미만 | -| 연체 기간 | 90일 초과 | 30~90일 | 정상 회수 | 만기 전 | -| 운영자금 확보 | 3개월 미만 | 3~6개월 | - | 6개월 이상 | - ---- - -## 2. 일일 일보 섹션 - -일일 일보는 당일 자금 현황을 요약하여 보여주며, 현금 흐름에 대한 AI 분석 리포트가 함께 제공됩니다. - -### 2.1 현금 자산 - 출금 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **출금** | 🔴 빨간색 | 7일 평균 대비 200% 이상 | 당일출금 ÷ 7일평균출금 ≥ 2.0 | -| **점검이 필요** | 🔴 빨간색 | 7일 평균 대비 200% 이상 | 출금 키워드와 함께 사용 | -| **출금 증가** | 🟠 주황색 | 7일 평균 대비 150~200% | 당일출금 ÷ 7일평균출금 1.5~2.0 | -| **정상 출금** | 🔵 파란색 | 7일 평균 대비 150% 미만 | 당일출금 ÷ 7일평균출금 < 1.5 | - -#### 적용 예시 -- 어제 🔴**3.5억원 출금**했습니다. 최근 7일 평균(1.7억원) 대비 206%로 🔴**점검이 필요**합니다. - -### 2.2 현금 자산 - 입금 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **입금** | 🟢 녹색 | 입금 발생 시 (금액 무관) | 당일 입금 > 0 | -| **대규모 입금** | 🟢 녹색 | 월평균 입금의 200% 이상 | 당일입금 ÷ 월평균입금 ≥ 2.0 | -| **주요 원인** | 🟢 녹색 | 입금 원인 설명 시 | 입금 키워드와 함께 사용 | - -#### 적용 예시 -- 어제 🟢**10.2억원이 입금**되었습니다. 대한건설 선수금 🟢**입금**이 🟢**주요 원인**입니다. - -### 2.3 현금 자산 - 운영자금 안정성 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **자금 부족 우려** | 🔴 빨간색 | 월 운영비용 대비 3개월 미만 | 현금자산 ÷ 월운영비 < 3 | -| **자금 관리 필요** | 🟠 주황색 | 월 운영비용 대비 3~6개월 | 현금자산 ÷ 월운영비 3~6 | -| **확보되어 안정적** | 🔵 파란색 | 월 운영비용 대비 6개월 이상 | 현금자산 ÷ 월운영비 ≥ 6 | - -#### 적용 예시 -- 총 현금성 자산이 300.2억원입니다. 월 운영비용(16.7억원) 대비 🔵**18개월 분이 확보되어 안정적**입니다. - -### 2.4 외화 현황 - 환율 변동 - -| 키워드 | 색상 | 임계값 기준 (일일) | 임계값 기준 (주간) | -|:---|:---:|:---|:---| -| **환율 급등** | 🔴 빨간색 | 전일 대비 ±1.5% 이상 또는 ±20원 이상 | 전주 대비 ±3% 이상 | -| **환율 급락** | 🔴 빨간색 | 전일 대비 ±1.5% 이상 또는 ±20원 이상 | 전주 대비 ±3% 이상 | -| **환율 변동 주의** | 🟠 주황색 | 전일 대비 ±1.0~1.5% 또는 ±10~20원 | 전주 대비 ±2~3% | -| **환율 안정** | 🔵 파란색 | 전일 대비 ±1.0% 미만 또는 ±10원 미만 | 전주 대비 ±2% 미만 | - -### 2.5 외화 현황 - 환차손익 - -| 키워드 | 색상 | 임계값 기준 (금액) | 임계값 기준 (비율) | -|:---|:---:|:---|:---| -| **환차손 발생** | 🔴 빨간색 | 평가손실 1,000만원 이상 | 외화보유액 대비 2% 이상 손실 | -| **환리스크 주의** | 🟠 주황색 | 평가손실 500~1,000만원 | 외화보유액 대비 1~2% 손실 | -| **환차익 발생** | 🟢 녹색 | 평가이익 500만원 이상 | 외화보유액 대비 1% 이상 이익 | -| **환율 영향 미미** | 🔵 파란색 | 평가손익 ±500만원 미만 | 외화보유액 대비 ±1% 미만 | - -#### 적용 예시 -- 전일 대비 환율이 🔴**1.8% 상승(+24원)**했습니다. 외화자산 평가손실 🔴**약 1,500만원 환차손 발생**이 예상됩니다. -- 전일 대비 환율 변동 0.3%(+4원)으로 🔵**환율 안정**적인 상태입니다. 🔵**환율 영향 미미**합니다. - ---- - -## 3. 당월 예상 지출 내역 섹션 - -당월 예상되는 지출 항목(매입, 카드, 발행어음 등)을 분석하여 전월 대비 및 예산 대비 현황을 제공합니다. - -### 3.1 전월 대비 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **전월 대비 N% 증가** | 🔴 빨간색 | 전월 대비 15% 이상 증가 | (당월-전월) ÷ 전월 ≥ 0.15 | -| **지출 증가 추이** | 🟠 주황색 | 전월 대비 10~15% 증가 | (당월-전월) ÷ 전월 0.10~0.15 | -| **전월 대비 N% 감소** | 🟢 녹색 | 전월 대비 5% 이상 감소 | (당월-전월) ÷ 전월 ≤ -0.05 | -| **전월과 유사** | 🔵 파란색 | 전월 대비 ±10% 이내 | |(당월-전월) ÷ 전월| < 0.10 | - -#### 적용 예시 -- 이번 달 예상 지출이 🔴**전월 대비 15% 증가**했습니다. 매입 비용 증가가 주요 원인입니다. -- 이번 달 예상 지출이 🟢**전월 대비 8% 감소**했습니다. 외주비용 절감이 주요 원인입니다. - -### 3.2 예산 대비 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **예산을 N% 초과** | 🔴 빨간색 | 예산 대비 100% 초과 | 예상지출 ÷ 예산 > 1.0 | -| **예산 임박** | 🟠 주황색 | 예산 대비 90~100% | 예상지출 ÷ 예산 0.9~1.0 | -| **예산 내 운영** | 🟢 녹색 | 예산 대비 90% 미만 | 예상지출 ÷ 예산 < 0.9 | - -#### 적용 예시 -- 이번 달 예상 지출이 🔴**예산을 12% 초과**했습니다. 비용 항목별 점검이 필요합니다. -- 이번 달 예상 지출이 🟢**예산 내 운영** 중입니다. (예산 대비 82%) - -### 3.3 항목별 지출 분석 기준 - -| 지출 항목 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 | -|:---|:---|:---|:---|:---| -| 매입 | 전월 대비 20% 이상 증가 | 전월 대비 10~20% 증가 | 전월 대비 감소 | ±10% 이내 | -| 카드 | 한도 100% 초과 | 한도 80~100% 사용 | - | 한도 80% 미만 | -| 발행어음 | 만기 초과 또는 부도 위험 | 만기 D-7일 이내 | 정상 결제 완료 | 만기 D-8일 이상 | -| 인건비 | 예산 대비 100% 초과 | 예산 대비 90~100% | - | 예산 대비 90% 미만 | - ---- - -## 4. 카드/가지급금 관리 섹션 - -법인카드 사용 현황과 가지급금 발생 현황을 분석하여 세무 리스크를 사전에 안내합니다. - -### 4.1 가지급금 전환 - -| 키워드 | 색상 | 임계값 기준 | 세무 영향 | -|:---|:---:|:---|:---| -| **가지급금으로 전환** | 🔴 빨간색 | 미정리 법인카드 사용 100만원 이상 | 인정이자 4.6% 발생 | -| **인정이자가 발생** | 🔴 빨간색 | 가지급금 잔액 × 4.6% | 법인세 증가 | -| **연간 N만원의 인정이자** | 🔴 빨간색 | 연간 인정이자 100만원 이상 | 가지급금 × 4.6% | -| **가지급금 정리 필요** | 🟠 주황색 | 미정리 법인카드 사용 50~100만원 | 정리 권고 | - -#### 적용 예시 -- 법인카드 사용 중 850만원이 🔴**가지급금으로 전환**되었습니다. 🔴**연 4.6% 인정이자가 발생**합니다. -- 현재 가지급금 3.5억 × 4.6% = 🔴**연간 약 1,610만원의 인정이자가 발생** 중입니다. - -### 4.2 업무관련성 소명 필요 - -| 키워드 | 색상 | 임계값 기준 | 발생 사유 | -|:---|:---:|:---|:---| -| **불인정 가맹점 결제** | 🔴 빨간색 | 유흥업소, 귀금속, 상품권 등 결제 | 가지급금 전환 대상 | -| **본인 청구 결제 감지** | 🟠 주황색 | 상품권, 귀금속, 면세점 등 1건 이상 | 소명 자료 필요 | -| **주말 사용 감지** | 🟠 주황색 | 토/일요일 결제 50만원 이상 | 업무관련성 검토 | -| **심야 사용 감지** | 🟠 주황색 | 22시~06시 결제 30만원 이상 | 업무관련성 검토 | -| **해외 사용 감지** | 🟠 주황색 | 해외 결제 발생 시 | 출장 증빙 필요 | - -#### 적용 예시 -- 상품권 구매 🟠**본인 청구 결제 감지**. 가지급금 처리 예정입니다. -- 🟠**주말 사용 감지** - 토요일 120만원 결제. 업무관련성 소명이 어려울 수 있으니 기록을 남겨주세요. - -### 4.3 법인세/종합소득세 예상 가중 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **법인세 예상 가중** | 🔴 빨간색 | 추가 법인세 100만원 이상 예상 | 가지급금 인정이자 × 법인세율 | -| **대표자 종합소득세 예상 가중** | 🔴 빨간색 | 추가 종합소득세 50만원 이상 예상 | 인정상여 × 소득세율 | -| **세무 리스크 주의** | 🟠 주황색 | 추가 세금 50만원 미만 예상 | 정리 권고 | - -#### 적용 예시 -- 가지급금으로 인한 🔴**법인세 예상 가중 약 320만원**이 발생합니다. -- 🔴**대표자 종합소득세 예상 가중 약 180만원**이 예상됩니다. (추가 사용 +10.5%) - ---- - -## 5. 접대비 현황 섹션 - -접대비 사용 현황과 한도 대비 사용률을 분석하여 세법상 한도 초과 여부를 사전에 안내합니다. - -### 5.1 한도 사용률 기준 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **한도 초과 N만원 발생** | 🔴 빨간색 | 한도 사용률 100% 초과 | 사용액 > 한도액 | -| **손금 불산입** | 🔴 빨간색 | 한도 초과액 발생 시 | 초과액 = 사용액 - 한도액 | -| **법인세 부담이 증가** | 🔴 빨간색 | 한도 초과로 인한 법인세 증가 | 초과액 × 법인세율 | -| **잔여 한도 N원** | 🟠 주황색 | 한도 사용률 85~100% | 잔여 = 한도액 - 사용액 | -| **사용 계획을 점검** | 🟠 주황색 | 한도 사용률 85% 이상 | 사용액 ÷ 한도액 ≥ 0.85 | -| **여유 있게 운영** | 🟢 녹색 | 한도 사용률 75% 미만 | 사용액 ÷ 한도액 < 0.75 | -| **정상 운영** | 🔵 파란색 | 한도 사용률 75~85% | 사용액 ÷ 한도액 0.75~0.85 | - -#### 세법상 접대비 한도 계산 -- 기본한도: 중소기업 3,600만원, 일반기업 2,400만원 (연간) -- 추가한도: 수입금액 × 적용률 (100억 이하 0.3%, 100~500억 0.2%, 500억 초과 0.03%) - -#### 적용 예시 -- (1분기) 접대비 사용 1,000만원 / 한도 4,012만원 (25%). 🟢**여유 있게 운영** 중입니다. -- 접대비 한도 85% 도달. 🟠**잔여 한도 600만원**입니다. 🟠**사용 계획을 점검**해 주세요. -- 🔴**접대비 한도 초과 320만원 발생**. 초과분은 🔴**손금 불산입**되어 🔴**법인세 부담이 증가**합니다. - -### 5.2 증빙 관리 - -| 키워드 | 색상 | 임계값 기준 | 필수 정보 | -|:---|:---:|:---|:---| -| **거래처 정보가 누락** | 🔴 빨간색 | 거래처명 또는 참석자 미입력 1건 이상 | 거래처명, 참석자, 목적 | -| **증빙 누락** | 🔴 빨간색 | 영수증 또는 카드전표 미첨부 1건 이상 | 적격증빙 필수 | -| **기록 보완 필요** | 🟠 주황색 | 상세 내용 미기재 (목적, 장소 등) | 상세 기록 권고 | -| **증빙 완비** | 🟢 녹색 | 모든 필수 정보 입력 완료 | - | - -#### 적용 예시 -- 접대비 사용 중 3건(45만원)의 🔴**거래처 정보가 누락**되었습니다. 🟠**기록 보완 필요**합니다. - ---- - -## 6. 복리후생비 현황 섹션 - -복리후생비 사용 현황을 분석하여 비과세 한도 초과 여부와 업계 평균 대비 적정성을 안내합니다. - -### 6.1 1인당 복리후생비 - -| 키워드 | 색상 | 임계값 기준 | 업계 평균 | -|:---|:---:|:---|:---| -| **과다 지출** | 🔴 빨간색 | 1인당 월 30만원 초과 | 업계 평균의 150% 초과 | -| **지출 증가 추이** | 🟠 주황색 | 1인당 월 25~30만원 | 업계 평균의 120~150% | -| **업계 평균 내 정상 운영** | 🟢 녹색 | 1인당 월 15~25만원 | 업계 평균 범위 내 | -| **적정 운영** | 🔵 파란색 | 1인당 월 15만원 미만 | 업계 평균 미만 | - -#### 적용 예시 -- 1인당 월 복리후생비 20만원. 🟢**업계 평균(15~25만원) 내 정상 운영** 중입니다. - -### 6.2 항목별 비과세 한도 - -| 항목 | 비과세 한도 | 🔴 경고 기준 | 🟢 정상 기준 | -|:---|:---|:---|:---| -| 식대 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 자가운전보조금 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 출산/보육수당 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 연구보조비 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 야근식대/숙직비 | 실비 정산 | 과다 지급 시 과세 위험 | 실비 범위 내 | - -### 6.3 비과세 초과 시 - -| 키워드 | 색상 | 임계값 기준 | 세무 처리 | -|:---|:---:|:---|:---| -| **비과세 한도를 초과** | 🔴 빨간색 | 항목별 비과세 한도 초과 시 | 초과분 근로소득 과세 | -| **근로소득 과세됩니다** | 🔴 빨간색 | 비과세 초과분 발생 시 | 원천세 추가 징수 | -| **초과분 N만원 과세 처리** | 🔴 빨간색 | 과세 금액 명시 | 급여에 합산 | -| **한도 임박** | 🟠 주황색 | 비과세 한도의 90% 이상 사용 | 사용 주의 | - -#### 적용 예시 -- 식대가 월 25만원으로 🔴**비과세 한도(20만원)를 초과**했습니다. 🔴**초과분 5만원 근로소득 과세됩니다**. - ---- - -## 7. 미수금 현황 섹션 - -미수금 현황을 분석하여 연체 상태, 회수 필요성, 리스크 집중도를 안내합니다. - -### 7.1 연체 기간별 분류 - -| 키워드 | 색상 | 연체 기간 | 조치 수준 | -|:---|:---:|:---|:---| -| **장기 미수금 발생** | 🔴 빨간색 | 90일 초과 | 법적 조치 검토 | -| **회수 조치가 필요** | 🔴 빨간색 | 60~90일 | 적극적 독촉/추심 | -| **연체 발생** | 🟠 주황색 | 30~60일 | 독촉장 발송 | -| **연체 임박** | 🟠 주황색 | 만기 D-7일 ~ 만기 후 30일 | 사전 연락 | -| **정상 거래** | 🟢 녹색 | 만기 전 | 정상 관리 | -| **회수 완료** | 🟢 녹색 | 전액 회수 시 | 완료 처리 | - -#### 적용 예시 -- 90일 이상 🔴**장기 미수금 3건(2,500만원) 발생**. 🔴**회수 조치가 필요**합니다. - -### 7.2 리스크 집중도 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **전체의 N%를 차지** | 🔴 빨간색 | 상위 1개사 미수금 비중 30% 이상 | 거래처 미수금 ÷ 총 미수금 | -| **리스크 분산이 필요** | 🔴 빨간색 | 상위 1개사 비중 30% 이상 | 집중 리스크 경고 | -| **리스크 관리 필요** | 🟠 주황색 | 상위 3개사 비중 50% 이상 | 분산 권고 | -| **리스크 분산 양호** | 🟢 녹색 | 상위 1개사 비중 20% 미만 | 정상 분산 | -| **리스크 관리 양호** | 🔵 파란색 | 상위 3개사 비중 40% 미만 | 양호한 분산 | - -#### 적용 예시 -- (주)대한전자 미수금 1,500만원으로 🔴**전체의 35%를 차지**합니다. 🔴**리스크 분산이 필요**합니다. -- 상위 3개사 미수금 비중 38%. 🔵**리스크 관리 양호**합니다. - -### 7.3 미수금 금액 기준 - -| 키워드 | 색상 | 임계값 기준 | 비고 | -|:---|:---:|:---|:---| -| **대형 미수금** | 🔴 빨간색 | 단일 건 3,000만원 이상 | 집중 관리 대상 | -| **주요 미수금** | 🟠 주황색 | 단일 건 1,000~3,000만원 | 관리 주의 | -| **일반 미수금** | 🔵 파란색 | 단일 건 1,000만원 미만 | 정상 관리 | - ---- - -## 8. 채권추심 현황 섹션 - -채권추심 진행 현황을 분석하여 법적 조치 상태와 회수 가능성을 안내합니다. - -### 8.1 추심 진행 상태 - -| 키워드 | 색상 | 임계값 기준 | 다음 단계 | -|:---|:---:|:---|:---| -| **회수 불가 판정** | 🔴 빨간색 | 채무자 무자력 확인 또는 소멸시효 완성 | 대손 처리 | -| **파산/회생 신청** | 🔴 빨간색 | 채무자 파산/회생 신청 확인 시 | 채권 신고 | -| **대손 처리 검토가 필요** | 🟠 주황색 | 회수 가능성 30% 미만 판단 시 | 세무 검토 | -| **법적 조치 진행 중** | 🔵 파란색 | 소송/강제집행 진행 중 | 결과 대기 | -| **지급명령 신청 완료** | 🟢 녹색 | 지급명령 신청 접수 완료 | 법원 결정 대기 | -| **회수 완료** | 🟢 녹색 | 채권 전액 또는 일부 회수 | 종결 처리 | - -#### 적용 예시 -- (주)대한전자 건 🟢**지급명령 신청 완료**. 법원 결정까지 약 2주 소요 예정입니다. -- (주)삼성테크 건 🔴**회수 불가 판정**. 🟠**대손 처리 검토가 필요**합니다. - -### 8.2 예상 소요 기간 - -| 키워드 | 색상 | 임계값 기준 | 비고 | -|:---|:---:|:---|:---| -| **장기 소송 예상** | 🔴 빨간색 | 예상 소요 기간 6개월 이상 | 비용/효익 검토 필요 | -| **소송 진행 중** | 🟠 주황색 | 예상 소요 기간 3~6개월 | 진행 상황 모니터링 | -| **법원 결정까지 약 N주 소요 예정** | 🔵 파란색 | 예상 소요 기간 3개월 미만 | 정상 진행 | -| **조기 회수 예상** | 🟢 녹색 | 예상 소요 기간 1개월 미만 | 신속 처리 | - -### 8.3 회수율 기준 - -| 키워드 | 색상 | 임계값 기준 | 판단 기준 | -|:---|:---:|:---|:---| -| **회수 불가** | 🔴 빨간색 | 예상 회수율 10% 미만 | 대손 처리 대상 | -| **회수 곤란** | 🔴 빨간색 | 예상 회수율 10~30% | 적극 추심 필요 | -| **부분 회수 예상** | 🟠 주황색 | 예상 회수율 30~70% | 협상 검토 | -| **회수 가능성 높음** | 🟢 녹색 | 예상 회수율 70% 이상 | 정상 추심 | -| **전액 회수 예상** | 🟢 녹색 | 예상 회수율 90% 이상 | 양호 | - ---- - -## 9. 부가세 현황 섹션 - -부가세 예정/확정 신고 현황을 분석하여 납부/환급 예상액과 세금계산서 발행 현황을 안내합니다. - -### 9.1 납부/환급 세액 - -| 키워드 | 색상 | 임계값 기준 | 판단 근거 | -|:---|:---:|:---|:---| -| **납부세액 급증** | 🔴 빨간색 | 전기 대비 30% 이상 증가 (매출 증가율 대비 초과) | 비정상 증가 | -| **매입세액 누락 의심** | 🔴 빨간색 | 매입세액 ÷ 매출세액 < 업종 평균의 70% | 누락 가능성 | -| **납부세액 증가** | 🟠 주황색 | 전기 대비 15~30% 증가 | 검토 필요 | -| **예상 환급세액** | 🟢 녹색 | 매입세액 > 매출세액 | 환급 발생 | -| **매입세액 증가가 주요 원인** | 🟢 녹색 | 설비투자 등 정당한 매입 증가 시 | 정상 사유 | -| **정상적인 증가로 판단** | 🟢 녹색 | 매출 증가율과 납부세액 증가율 유사 | 정상 범위 | -| **전기 대비 N% 증가** | 🔵 파란색 | 전기 대비 15% 미만 증가 | 정상 변동 | - -#### 적용 예시 -- 2026년 1기 예정신고 기준, 🟢**예상 환급세액**은 5,200,000원입니다. 설비투자에 따른 🟢**매입세액 증가가 주요 원인**입니다. -- 예상 납부세액 110,100,000원. 🔵**전기 대비 12.9% 증가**했으며, 매출 증가(11.5%)에 따른 🟢**정상적인 증가로 판단**됩니다. - -### 9.2 세금계산서 발행 관리 - -| 키워드 | 색상 | 임계값 기준 | 가산세 | -|:---|:---:|:---|:---| -| **세금계산서 미발행** | 🔴 빨간색 | 발행 기한 경과 후 미발행 1건 이상 | 공급가액의 2% | -| **발행 기한 초과** | 🔴 빨간색 | 공급일 다음 달 10일 경과 | 지연 발급 1% | -| **가산세 발생 위험** | 🔴 빨간색 | 미발행 또는 지연 발행 시 | 최대 2% | -| **발행 기한 임박** | 🟠 주황색 | 발행 기한 D-3일 이내 | 발행 권고 | -| **정상 발행** | 🟢 녹색 | 공급일 다음 달 10일 이내 발행 | 정상 | - -#### 적용 예시 -- 🔴**세금계산서 미발행** 3건 발생. 🔴**가산세 발생 위험**이 있습니다. 공급가액 1,500만원 × 2% = 30만원 -- 12월 매출분 세금계산서 🟠**발행 기한 임박** (D-2일). 1월 10일까지 발행 필요합니다. - ---- - -## 10. 종합 색상 적용 기준 매트릭스 - -모든 AI 리포트 섹션에 대한 색상 적용 기준을 요약한 종합 매트릭스입니다. - -| 섹션 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 | -|:---|:---|:---|:---|:---| -| 일일 일보 (출금) | 7일 평균 대비 200% 이상 | 7일 평균 대비 150~200% | - | 7일 평균 대비 150% 미만 | -| 일일 일보 (입금) | - | - | 입금 발생 시 | - | -| 일일 일보 (운영자금) | 3개월 미만 확보 | 3~6개월 확보 | - | 6개월 이상 확보 | -| 일일 일보 (환율) | 일 ±1.5% 이상 | 일 ±1.0~1.5% | 환차익 발생 | 일 ±1.0% 미만 | -| 일일 일보 (환차손익) | 손실 1,000만원 이상 | 손실 500~1,000만원 | 이익 500만원 이상 | ±500만원 미만 | -| 당월 지출 (전월 대비) | 15% 이상 증가 | 10~15% 증가 | 5% 이상 감소 | ±10% 이내 | -| 당월 지출 (예산 대비) | 100% 초과 | 90~100% | - | 90% 미만 | -| 카드/가지급금 (전환) | 100만원 이상 전환 | 50~100만원 전환 | - | - | -| 카드/가지급금 (소명) | 불인정 가맹점 | 주말/심야 50만원 이상 | - | - | -| 접대비 (한도) | 100% 초과 | 85~100% | 75% 미만 | 75~85% | -| 접대비 (증빙) | 거래처 정보 누락 | 상세 내용 미기재 | 증빙 완비 | - | -| 복리후생비 | 1인당 월 30만원 초과 | 1인당 월 25~30만원 | 1인당 월 15~25만원 | 1인당 월 15만원 미만 | -| 복리후생비 (비과세) | 한도 초과 시 과세 | 한도 90% 이상 | - | 한도 이하 | -| 미수금 (연체) | 90일 초과 | 30~90일 | 회수 완료 | 만기 전 | -| 미수금 (집중도) | 1개사 30% 이상 | 3개사 50% 이상 | 1개사 20% 미만 | 3개사 40% 미만 | -| 채권추심 (상태) | 회수 불가, 파산 | 대손 검토 필요 | 지급명령 완료, 회수 | 법적 조치 진행 중 | -| 채권추심 (회수율) | 10% 미만 | 10~30% | 70% 이상 | 30~70% | -| 부가세 (납부세액) | 전기 대비 30% 이상 증가 | 전기 대비 15~30% 증가 | 환급 발생, 정상 증가 | 전기 대비 15% 미만 | -| 부가세 (세금계산서) | 미발행/기한 초과 | 기한 D-3일 이내 | 정상 발행 | - | - ---- - -*— 문서 끝 —* diff --git a/plans/archive/HISTORY.md b/plans/archive/HISTORY.md new file mode 100644 index 0000000..9693f6f --- /dev/null +++ b/plans/archive/HISTORY.md @@ -0,0 +1,88 @@ +# 완료 작업 히스토리 + +> docs/plans 완료 문서 요약. 상세 내용은 git 이력 참조. + +## 견적/수주 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 견적 자동 산출 개발 | 2025-12 | MNG 수식 설정 + React 자동산출 기능 구현 | +| MNG 수식 관리 개발 | 2025-12 | 수식 CRUD/카테고리/시뮬레이터/범위/매핑/품목 UI 완료 | +| 시뮬레이터 로직 동기화 | 2025-12 | Design/MNG 시뮬레이터 동일 결과 동기화 | +| 견적 V2 자동산출 오류 수정 | 2026-01 | 자동산출 4가지 오류 분석 및 수정 | +| 입찰관리 API 구현 | 2026-01 | 견적→입찰 전환 API 및 더미데이터 생성 | +| 시공사 페이지 API 연동 | 2026-01 | 8개 시공사 페이지 Mock→API 연동 완료 | +| 견적 URL 마이그레이션 | 2026-01 | test-new/test 경로→정식 경로 정비 | +| 수식 엔진 실제 데이터 연동 | 2026-02 | 테스트 데이터를 실제 품목으로 재구성 | + +## 수주/작업지시 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 수주관리 API 연동 | 2026-01 | 수주 목록/등록/수정/삭제 API 연동 완료 | +| 수주-작업지시-출하 연동 | 2026-01 | Order→WorkOrder→Shipment FK 연결 및 상태 동기화 | +| 작업지시 API | 2026-01 | 작업지시 목록/등록/상세 API 연동 완료 | +| 수주 하위 구조 관리 | 2026-02 | N-depth 트리 구조(개소/구역/공정) 하이브리드 설계 | + +## 품목/BOM + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| Items 테이블 통합 | 2025-12 | products/materials를 items로 통합 (Item-Master) | +| 5130 BOM 마이그레이션 | 2026-01 | 5130 레거시 BOM 61건을 SAM items.bom으로 마이그레이션 | +| 5130 자재/수주 마이그레이션 | 2026-01 | KDunitprice/output 데이터를 items/orders/order_items로 이관 | +| 경동 품목/단가 마이그레이션 | 2026-01 | 5130 ~1,500건 품목/단가/BOM 데이터 이관 | +| MNG 품목관리 페이지 | 2026-02 | 3-Panel 품목관리 (좌측 리스트+중앙 BOM+우측 상세) 구현 | +| MNG 품목-수식 연동 | 2026-02 | FormulaEvaluatorService 연동으로 동적 BOM 산출 | + +## 생산/절곡 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 공정관리 API | 2026-01 | 공정 CRUD + 분류 규칙 + 품목 연결 API 완료 | +| 재고 통합 시스템 | 2026-01 | 입고/생산/견적/출하 시 재고 자동 증감 및 FIFO 차감 | +| 절곡 작업일지 재구현 | 2026-02 | PHP 원본(~1400줄)을 React BendingWorkLogContent로 재구현 | +| 절곡 LOT 파이프라인 | 2026-02 | 절곡 세부품목 동적 BOM + LOT 추적 파이프라인 구축 | +| 개소별 자재 투입 매핑 | 2026-02 | 개소별 자재 투입 추적 및 LOT 매핑 기능 완료 | +| 절곡 선재고 관리 | 2026-02 | 선재고 입고 흐름 14/14 완료 | + +## 문서/서식 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 문서 업데이트 계획 | 2025-12 | docs/architecture 문서 동기화 (admin→mng 전환 반영) | +| 문서관리 시스템 변경이력 | 2026-02 | 검사 양식 템플릿 4종 + FQC/중간검사 구현 31개 이력 | +| 제품검사(FQC) 폼 | 2026-02 | 제품검사 양식 템플릿 설계 및 5.2 Phase 구현 | + +## 시스템/인프라 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| ERP API D1.0 개발 | 2025-12 | ERP API Phase 5~8 (12개 기능, ~71개 API) 완료 | +| API 전체 분석 보고서 | 2026-01 | 710+ API 중복/통합/미사용 분석 (React 실제 사용 ~80개) | +| 통계 DB 설계 | 2026-01 | 확장 가능한 전용 통계 DB(sam_stat) 설계 | +| MES 통합 흐름 분석 | 2026-01 | 견적→수주→작업지시 모듈 간 데이터 흐름 분석 | +| DB 트리거 감사 시스템 | 2026-02 | 감사 트리거 15/16 완료, 94% | + +## 사용자/권한 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| L2 권한관리 API | 2025-12 | React 권한관리 Mock→API 연동 (Spatie Permission) | +| 시더 목록 | 2026-01 | 사용자/부서/거래처 등 13개 시더 명령어 정리 | + +## 프론트엔드/알림 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| React FCM 푸시 알림 | 2025-12 | mng FCM.js를 React에 포팅, Capacitor 앱 지원 | +| FCM 사용자별 알림 | 2026-01 | 테넌트 전체 브로드캐스트→사용자별 타겟 발송 전환 | +| 알림음 시스템 | 2026-01 | FCM 알림 타입별 커스텀 알림음 (6개 채널) | +| React 서버컴포넌트 점검 | 2026-01 | 'use client' 정책 준수 여부 점검 (0개 오류) | + +## 기타 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| AI 리포트 색상체계 가이드 | 2026-01 | AI 리포트 섹션별 색상 임계값 정의 (v1.4) | +| 복리후생비 섹션 | 2026-01 | CEO 대시보드 복리후생비 현황 4개 카드 구현 | diff --git a/plans/archive/SEEDERS_LIST.md b/plans/archive/SEEDERS_LIST.md deleted file mode 100644 index b8a90b2..0000000 --- a/plans/archive/SEEDERS_LIST.md +++ /dev/null @@ -1,128 +0,0 @@ -# SAM API 시더 목록 - -> 생성일: 2025-01-05 -> 대상 테넌트: ID 287 - -## 개별 실행 방법 - -```bash -# Docker 컨테이너 접속 후 -php artisan db:seed --class=시더클래스명 - -# Dummy 폴더 시더는 네임스페이스 포함 -php artisan db:seed --class=Dummy\\DummyClientSeeder -``` - ---- - -## 1. 메인 시더 - -| # | 시더 | 설명 | 실행 명령어 | -|---|------|------|-------------| -| 1 | `DatabaseSeeder` | 기본 시더 (테스트 유저 + 메뉴) | `php artisan db:seed` | -| 2 | `DummyDataSeeder` | 전체 더미 데이터 (모든 Dummy 호출) | `php artisan db:seed --class=DummyDataSeeder` | - ---- - -## 2. 기본 데이터 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 3 | `DummyUserSeeder` | users | 15 | `php artisan db:seed --class=Dummy\\DummyUserSeeder` | -| 4 | `DummyDepartmentSeeder` | departments | 11 | `php artisan db:seed --class=Dummy\\DummyDepartmentSeeder` | -| 5 | `DummyClientGroupSeeder` | client_groups | 5 | `php artisan db:seed --class=Dummy\\DummyClientGroupSeeder` | -| 6 | `DummyBankAccountSeeder` | bank_accounts | 5 | `php artisan db:seed --class=Dummy\\DummyBankAccountSeeder` | -| 7 | `DummyClientSeeder` | clients | 20 | `php artisan db:seed --class=Dummy\\DummyClientSeeder` | - ---- - -## 3. 회계 데이터 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 8 | `DummyDepositSeeder` | deposits | 60 | `php artisan db:seed --class=Dummy\\DummyDepositSeeder` | -| 9 | `DummyWithdrawalSeeder` | withdrawals | 60 | `php artisan db:seed --class=Dummy\\DummyWithdrawalSeeder` | -| 10 | `DummySaleSeeder` | sales | 80 | `php artisan db:seed --class=Dummy\\DummySaleSeeder` | -| 11 | `DummyPurchaseSeeder` | purchases | 70 | `php artisan db:seed --class=Dummy\\DummyPurchaseSeeder` | -| 12 | `DummyBadDebtSeeder` | bad_debts | 18 | `php artisan db:seed --class=Dummy\\DummyBadDebtSeeder` | -| 13 | `DummyBillSeeder` | bills | 30 | `php artisan db:seed --class=Dummy\\DummyBillSeeder` | - ---- - -## 4. HR 데이터 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 14 | `DummyWorkSettingSeeder` | work_settings | 1 | `php artisan db:seed --class=Dummy\\DummyWorkSettingSeeder` | -| 15 | `DummyAttendanceSettingSeeder` | attendance_settings | 1 | `php artisan db:seed --class=Dummy\\DummyAttendanceSettingSeeder` | -| 16 | `DummyAttendanceSeeder` | attendances | ~300 | `php artisan db:seed --class=Dummy\\DummyAttendanceSeeder` | -| 17 | `DummyLeaveGrantSeeder` | leave_grants | ~200 | `php artisan db:seed --class=Dummy\\DummyLeaveGrantSeeder` | -| 18 | `DummyLeaveSeeder` | leaves | ~50 | `php artisan db:seed --class=Dummy\\DummyLeaveSeeder` | -| 19 | `DummyCardSeeder` | cards | 5 | `php artisan db:seed --class=Dummy\\DummyCardSeeder` | -| 20 | `DummySalarySeeder` | salaries | 15 | `php artisan db:seed --class=Dummy\\DummySalarySeeder` | - ---- - -## 5. 기타 더미 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 21 | `DummyItemSeeder` | items | 10,000 | `php artisan db:seed --class=Dummy\\DummyItemSeeder` | -| 22 | `DummyPopupSeeder` | popups | 8 | `php artisan db:seed --class=Dummy\\DummyPopupSeeder` | -| 23 | `DummyPaymentSeeder` | payments | 13 | `php artisan db:seed --class=Dummy\\DummyPaymentSeeder` | -| 24 | `ApprovalTestDataSeeder` | approvals | ~60 | `php artisan db:seed --class=ApprovalTestDataSeeder` | - ---- - -## 6. 시스템/설정 시더 - -| # | 시더 | 설명 | 실행 명령어 | -|---|------|------|-------------| -| 25 | `GlobalMenuTemplateSeeder` | 글로벌 메뉴 템플릿 | `php artisan db:seed --class=GlobalMenuTemplateSeeder` | -| 26 | `ReactMenuSeeder` | React 메뉴 | `php artisan db:seed --class=ReactMenuSeeder` | -| 27 | `CategorySeeder` | 카테고리 | `php artisan db:seed --class=CategorySeeder` | -| 28 | `ItemTypeSeeder` | 품목 유형 | `php artisan db:seed --class=ItemTypeSeeder` | -| 29 | `ItemMasterSeeder` | 품목 마스터 | `php artisan db:seed --class=ItemMasterSeeder` | -| 30 | `PositionSeeder` | 직급 | `php artisan db:seed --class=PositionSeeder` | -| 31 | `FolderSeeder` | 폴더 | `php artisan db:seed --class=FolderSeeder` | -| 32 | `CapabilityProfileSeeder` | 역량 프로필 | `php artisan db:seed --class=CapabilityProfileSeeder` | -| 33 | `StockReceivingSeeder` | 입고 | `php artisan db:seed --class=StockReceivingSeeder` | -| 34 | `ComprehensiveAnalysisSeeder` | 종합분석 | `php artisan db:seed --class=ComprehensiveAnalysisSeeder` | -| 35 | `SystemFieldDefinitionSeeder` | 시스템 필드 정의 | `php artisan db:seed --class=SystemFieldDefinitionSeeder` | -| 36 | `DemoSystemSeeder` | 데모 시스템 | `php artisan db:seed --class=DemoSystemSeeder` | -| 37 | `BpMesCategoryFieldsSeeder` | MES 카테고리 필드 | `php artisan db:seed --class=BpMesCategoryFieldsSeeder` | -| 38 | `BpMesTenantStatFieldsSeeder` | MES 테넌트 통계 필드 | `php artisan db:seed --class=BpMesTenantStatFieldsSeeder` | - ---- - -## 7. 견적 관련 시더 - -| # | 시더 | 설명 | 실행 명령어 | -|---|------|------|-------------| -| 39 | `QuoteFormulaSeeder` | 견적 계산식 | `php artisan db:seed --class=QuoteFormulaSeeder` | -| 40 | `QuoteFormulaCategorySeeder` | 견적 계산 카테고리 | `php artisan db:seed --class=QuoteFormulaCategorySeeder` | -| 41 | `QuoteFormulaItemSeeder` | 견적 계산 품목 | `php artisan db:seed --class=QuoteFormulaItemSeeder` | -| 42 | `QuoteFormulaMappingSeeder` | 견적 계산 매핑 | `php artisan db:seed --class=QuoteFormulaMappingSeeder` | - ---- - -## 요약 - -| 카테고리 | 개수 | -|----------|------| -| 메인 시더 | 2 | -| 기본 데이터 (Dummy) | 5 | -| 회계 데이터 (Dummy) | 6 | -| HR 데이터 (Dummy) | 7 | -| 기타 더미 (Dummy) | 4 | -| 시스템/설정 | 14 | -| 견적 관련 | 4 | -| **총계** | **42** | - ---- - -## 주의사항 - -1. **Dummy 시더**는 `TENANT_ID = 287` 하드코딩 -2. **의존성 순서**: 기본 데이터 → 회계 → HR → 기타 순서로 실행 권장 -3. **중복 주의**: 이미 데이터가 있는 경우 중복 생성됨 (특히 `DummyItemSeeder` 10,000개) \ No newline at end of file diff --git a/plans/archive/api-analysis-report.md b/plans/archive/api-analysis-report.md deleted file mode 100644 index ae48343..0000000 --- a/plans/archive/api-analysis-report.md +++ /dev/null @@ -1,434 +0,0 @@ -# SAM API 전체 분석 보고서 - -> **작성일**: 2026-01-29 -> **목적**: api/, mng/, react/ 프로젝트 간 API 중복/통합/미사용 분석 및 관계 정리 -> **기준 문서**: api/routes/api/v1/*.php, mng/routes/api.php, mng/routes/web.php, react/src/lib/api/* -> **상태**: ✅ 분석 완료 - ---- - -## 📍 분석 결과 요약 - -| 항목 | 수치 | -|------|------| -| **api/ 엔드포인트** | ~710+ | -| **mng/ 엔드포인트** | ~300+ | -| **React 실제 사용** | ~80+ (api/ 전체의 ~15%) | -| **중복 도메인** | 10개 | -| **즉시 정리 가능** | 3건 | -| **통합 가능 그룹** | 6개 | - ---- - -## 1. 개요 - -### 1.1 배경 - -SAM 프로젝트는 api/(REST API), mng/(관리자 패널), react/(프론트엔드) 3개 프로젝트로 구성되어 있으며, 각 프로젝트가 독립적으로 발전하면서 동일 도메인에 대한 API가 중복 생성되었다. 본 분석은 전체 API를 파악하고 정리 방안을 제시한다. - -### 1.2 분석 범위 - -| 프로젝트 | 역할 | 분석 대상 | -|---------|------|----------| -| **api/** | REST API 서버 | routes/api/v1/*.php (14개 라우트 파일), 컨트롤러 138개 | -| **mng/** | 관리자 패널 | routes/api.php, routes/web.php, 컨트롤러 90+개 | -| **react/** | 프론트엔드 | src/lib/api/*, src/hooks/*, src/app/api/* | - ---- - -## 2. 프로젝트별 API 구조 - -### 2.1 API 프로젝트 (api/) - -**라우트 파일**: `api/routes/api/v1/` - -| 도메인 | 라우트 파일 | 엔드포인트 수 | 소비자 | -|--------|-----------|:----------:|--------| -| 인증 | `auth.php` | 8 | React, MNG | -| 사용자 | `users.php` | 25 | React | -| 테넌트 | `tenants.php` | 18 | React | -| 관리자 | `admin.php` | 22 | React, MNG | -| 공통 | `common.php` | 95+ | React, MNG | -| HR | `hr.php` | 85+ | React | -| 재무 | `finance.php` | 130+ | React | -| 영업 | `sales.php` | 85+ | React | -| 재고 | `inventory.php` | 65+ | React | -| 생산 | `production.php` | 35+ | React | -| 설계 | `design.php` | 55+ | React | -| 파일 | `files.php` | 15 | React | -| 게시판 | `boards.php` | 70+ | React | -| 문서 | `documents.php` | 5+ | React | - -### 2.2 MNG 프로젝트 (mng/) - -**API 소비 방식**: 자체 모델로 DB 직접 접근 (api/ REST API를 거치지 않음) - -| 도메인 | 엔드포인트 수 | 비고 | -|--------|:----------:|------| -| 사용자/역할/권한 | 30+ | api/와 중복 | -| 메뉴/글로벌메뉴 | 25+ | api/와 중복 | -| 게시판/필드 | 20+ | api/와 중복 | -| 카테고리/글로벌 | 15+ | api/와 중복 | -| 바로빌 (전체) | 60+ | MNG 전용 (외부 서비스) | -| 프로젝트 관리 | 25+ | MNG 전용 | -| 견적 공식 | 30+ | MNG 전용 | -| 품목 필드 | 25+ | MNG 전용 | -| 문서/템플릿 | 12+ | api/와 중복 | -| 계좌/자금일정 | 18+ | api/와 중복 | -| 기타 (회의록, 신용, 영업, Lab) | 40+ | MNG 전용 | - -### 2.3 React 프론트엔드 (react/) - -**API 호출 방식**: Next.js Proxy (`/api/proxy/*`) → Backend (`/api/v1/*`) - -| 카테고리 | 주요 엔드포인트 | 사용 빈도 | -|---------|---------------|:--------:| -| 인증 | login, logout, refresh, signup | 높음 | -| 품목 CRUD | items, items/{id}, items/bom | 높음 | -| 품목기준관리 | item-master/* (pages, sections, fields) | 높음 | -| 견적 계산 | quotes/calculate, quotes/calculate/bom | 높음 | -| 공통코드 | settings/common/{group} | 높음 | -| 대시보드 | card-transactions/dashboard, loans/dashboard | 중간 | -| 알림 | today-issues/unread, unread/count | 중간 | -| 거래처 | clients, client-groups | 중간 | -| 재고 | stocks, work-results | 중간 | -| 일괄작업 | bulk-update-account-code | 낮음 | -| 내보내기 | attendances/export, salaries/export | 낮음 | - ---- - -## 3. 중복 API 분석 - -### 3.1 중복 컨트롤러 목록 (api/ vs mng/) - -| # | 도메인 | api/ 컨트롤러 | mng/ 컨트롤러 | 중복 수준 | -|---|--------|-------------|-------------|:--------:| -| 1 | 사용자 관리 | `Api\V1\Admin\AdminController` | `Api\Admin\UserController` | 🔴 높음 | -| 2 | 역할 관리 | `Api\V1\RoleController` | `Api\Admin\RoleController` | 🔴 높음 | -| 3 | 메뉴 관리 | `Api\V1\MenuController` | `Api\Admin\MenuController` | 🔴 높음 | -| 4 | 카테고리 | `Api\V1\CategoryController` | `Api\Admin\CategoryApiController` | 🔴 높음 | -| 5 | 계좌 관리 | `Api\V1\BankAccountController` | `Api\Admin\BankAccountController` | 🔴 높음 | -| 6 | 권한 관리 | `Api\V1\PermissionController` | `Api\Admin\PermissionController` | 🟡 중간 | -| 7 | 부서 관리 | `Api\V1\DepartmentController` | `Api\Admin\DepartmentController` | 🟡 중간 | -| 8 | 게시판 | `Api\V1\BoardController` | `Api\Admin\BoardController` | 🟡 중간 | -| 9 | 문서 | `Api\V1\Documents\DocumentController` | `Api\Admin\DocumentApiController` | 🟡 중간 | -| 10 | 테넌트 | `Api\V1\TenantController` | `Api\Admin\TenantController` | 🟡 중간 | - -### 3.2 상세 비교 - -#### (1) 사용자 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 복구 (restore) | ✅ | ✅ | 동일 | -| 비밀번호 초기화 | ✅ | ✅ | 동일 | -| 활성화/비활성화 | ✅ (PATCH /status) | ❌ | api/만 | -| 역할 부여/해제 | ✅ (POST/DELETE /roles) | ❌ | api/만 | -| 영구삭제 | ❌ | ✅ (DELETE /force) | mng/만 (슈퍼관리자) | -| 개발용 로그인토큰 | ❌ | ✅ (POST /login-token) | mng/만 | -| 모달 데이터 | ❌ | ✅ (GET /modal) | mng/만 | - -#### (2) 역할 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 통계 (stats) | ✅ | ❌ | api/만 | -| 활성 목록 (active) | ✅ | ❌ | api/만 | - -#### (3) 메뉴 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 순서변경 (reorder) | ✅ | ✅ | 동일 | -| 복구 (restore) | ✅ | ✅ | 동일 | -| 활성화 토글 | ✅ (toggle) | ✅ (toggle-active) | 동일 기능 | -| 동기화 | ✅ (sync, sync-new, sync-updates) | ❌ | api/만 | -| 트리 구조 | ❌ | ✅ (tree) | mng/만 | -| 글로벌 복사 | ❌ | ✅ (copy-from-global) | mng/만 | -| 일괄 작업 | ❌ | ✅ (bulk-delete/restore/force) | mng/만 | -| 숨김 토글 | ❌ | ✅ (toggle-hidden) | mng/만 | -| 영구삭제 | ❌ | ✅ (force) | mng/만 (슈퍼관리자) | - -#### (4) 카테고리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 트리/순서변경/이동 | ✅ | ✅ | 동일 | -| 활성화 토글 | ✅ | ✅ | 동일 | -| 필드 관리 | ✅ (fields CRUD, bulk-upsert) | ❌ | api/만 | -| 템플릿 관리 | ✅ (templates, apply, preview, diff) | ❌ | api/만 | -| 로그 조회 | ✅ (logs) | ❌ | api/만 | -| 글로벌 관리 | ❌ | ✅ (global-categories) | mng/만 | - -#### (5) 계좌 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 활성화 토글 | ✅ | ✅ | 동일 | -| 활성 목록 (active) | ✅ | ❌ | api/만 | -| 대표계좌 설정 | ✅ (set-primary) | ❌ | api/만 | -| 전체 조회 (all) | ❌ | ✅ | mng/만 | -| 요약 (summary) | ❌ | ✅ | mng/만 | -| 거래내역 | ❌ | ✅ (transactions) | mng/만 | -| 일괄 작업 | ❌ | ✅ (bulk-*) | mng/만 | -| 영구삭제/복구 | ❌ | ✅ (force/restore) | mng/만 | - -#### (6) 권한 관리 🟡 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 권한 매트릭스 조회 | ✅ (dept/role/user menu-matrix) | ❌ | api/만 (특화) | -| 기본 CRUD | ❌ | ✅ | mng/만 | - -> **분석**: api/는 매트릭스 조회 전용, mng/는 CRUD 전용으로 기능 분리된 상태. 완전 중복은 아님. - ---- - -## 4. 통합 가능 API - -### 4.1 통합 대상 그룹 - -| # | 대상 | 현재 상태 | 통합 방안 | 우선순위 | -|---|------|----------|----------|:--------:| -| 1 | **인증 API** | signup + register 중복 | register 제거 (signup 유지) | 🔴 | -| 2 | **사용자 관리** | api/ + mng/ 각각 CRUD | mng/ → api/ 호출로 전환 | 🔴 | -| 3 | **역할 관리** | api/ + mng/ 각각 CRUD | api/에 통합, mng/는 호출만 | 🟡 | -| 4 | **메뉴 관리** | api/ 동기화 + mng/ 관리 분리 | 관리: mng/, 조회+동기화: api/ | 🟡 | -| 5 | **대시보드 데이터** | 개별 엔드포인트 분산 | 통합 대시보드 API 제공 | 🟢 | -| 6 | **일괄 업데이트** | withdrawals/deposits/sales 각각 | 공통 bulk-update 패턴 | 🟢 | - -### 4.2 인증 API 중복 상세 - -``` -현재: - POST /v1/login → 로그인 - POST /v1/logout → 로그아웃 - POST /v1/signup → 회원가입 (1) - POST /v1/register → 회원가입 (2) ← 중복! - POST /v1/token-login → 토큰 로그인 (MNG→DEV) - POST /v1/refresh → 토큰 갱신 - POST /v1/internal/exchange-token → 내부 서버 토큰 교환 - GET /v1/debug-apikey → 디버그용 ← 프로덕션 제거 필요 - -권장: - - register 제거 (signup 유지) - - debug-apikey 프로덕션 비활성화 -``` - ---- - -## 5. 미사용 API - -### 5.1 React에서 호출하지 않는 api/ 도메인 - -| 도메인 | 엔드포인트 수 | 미사용 이유 | -|--------|:----------:|-----------| -| HR 전체 (employees, attendance, leave, approval) | ~80+ | MNG에서 직접 관리 또는 React 미구현 | -| 생산 대부분 (processes, work-orders, inspections) | ~35+ | work-results만 사용 | -| 설계 전체 (models, versions, bom-templates) | ~55+ | 견적 계산 시 간접 사용만 | -| 재무 대부분 (cards, payroll, bad-debts 등) | ~100+ | CEO 대시보드 일부만 사용 | -| 사용자 초대 (invitations) | ~5 | React 미구현 | -| 알림 설정 (notification-settings) | ~5 | React 미구현 | -| 프로필 관리 (profiles) | ~5 | React 미구현 | -| 팝업 관리 (popups) | ~5 | React 미구현 | -| AI 리포트 (reports/ai) | ~4 | React 미구현 | -| 구독/결제 (subscriptions, payments) | ~20+ | React 미구현 | -| 현장/시공 (sites, construction) | ~30+ | React 미구현 | -| 검사 관리 (inspections) | ~8 | React 미구현 | - -> **참고**: "미사용"은 React 프론트엔드 기준. MNG에서 Blade UI로 직접 사용하거나 향후 구현 예정인 경우 포함. - -### 5.2 완전 미사용 가능성 높은 API - -| 엔드포인트 | 이유 | 조치 권장 | -|-----------|------|----------| -| `GET /v1/debug-apikey` | 디버그 전용 | 프로덕션 비활성화 | -| `POST /v1/register` | signup과 중복 | 제거 | -| `GET /v1/welfare/*` | React/MNG 모두 미호출 확인 필요 | 사용 여부 확인 | -| `GET /v1/entertainment/*` | React/MNG 모두 미호출 확인 필요 | 사용 여부 확인 | -| `GET /v1/calendar/schedules` | React 미호출 | 사용 여부 확인 | -| `GET /v1/comprehensive-analysis` | React 미호출 | 사용 여부 확인 | - -### 5.3 MNG 전용 기능 (정상) - -| 기능 | 설명 | 상태 | -|------|------|:----:| -| 바로빌 (Barobill) | 전자세금계산서, 카드, 홈택스 연동 | ✅ 정상 | -| 프로젝트 관리 | 프로젝트, 태스크, 이슈 | ✅ 정상 | -| 데일리 로그 | 일일 스크럼 | ✅ 정상 | -| 견적 공식 | 견적 계산 공식 관리 | ✅ 정상 | -| 회의록 | 녹음, AI 요약 (Google Cloud) | ✅ 정상 | -| 신용 평가 | Coocon API 연동 | ✅ 정상 | -| 영업 관리 | 매니저, 전망, 기록 | ✅ 정상 | -| DevTools | API 탐색기, 흐름 테스터 | ✅ 정상 | -| Lab/R&D | AI, 전략 실험 | ✅ 정상 | - ---- - -## 6. 프로젝트 간 API 관계도 - -### 6.1 시스템 구조 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 사용자 (브라우저) │ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ │ -│ │ React App │ │ MNG Admin │ │ -│ │ (dev.sam.kr) │ │ (mng.sam.kr) │ │ -│ └──────┬───────┘ └──────┬───────────┘ │ -│ │ │ │ -│ Next.js Proxy 자체 모델 직접 사용 │ -│ (/api/proxy/*) + 일부 api/ 호출 │ -│ │ │ │ -│ ▼ │ │ -│ ┌──────────────┐ │ │ -│ │ API 서버 │◄─────────────────┘ │ -│ │ (api.sam.kr) │ token-login, │ -│ │ │ DevTools API 탐색 │ -│ └──────┬───────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────┐ │ -│ │ Database │◄──── MNG도 동일 DB 직접 접근 │ -│ │ (MySQL) │ │ -│ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - -외부 API: -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Google │ │ Coocon │ │ FCM │ │ NTS │ -│ Cloud │ │ (신용) │ │ (푸시) │ │ (홈택스) │ -└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - └────────────┴────────────┴─────────────┘ - │ - MNG에서 호출 -``` - -### 6.2 데이터 흐름 - -| 흐름 | 방식 | 설명 | -|------|------|------| -| React → API | HTTP (Proxy) | 모든 비즈니스 로직 API 호출 | -| MNG → DB | 직접 모델 | 관리 기능은 DB 직접 접근 | -| MNG → API | HTTP | token-login, DevTools, 일부 동기화 | -| MNG → 외부 | HTTP | Barobill, Google Cloud, Coocon, NTS | -| API → DB | 직접 모델 | 모든 비즈니스 로직 | - -### 6.3 중복 발생 원인 - -``` -문제: MNG가 api/를 호출하지 않고 DB 직접 접근 - → 동일 도메인에 대해 api/, mng/ 각각 독립 컨트롤러 보유 - → 비즈니스 로직 분산, 유지보수 부담 증가 - -현재: - React → api/ (REST API) → DB - MNG → DB 직접 ← 여기가 문제 - -이상적: - React → api/ (REST API) → DB - MNG → api/ (REST API) → DB (관리자 전용 엔드포인트 추가) -``` - ---- - -## 7. 개선 권장사항 - -### 7.1 즉시 정리 (Quick Wins) 🔴 - -| # | 작업 | 영향 | 노력 | -|---|------|------|:----:| -| 1 | `POST /v1/register` 제거 (signup 유지) | 코드 정리 | 소 | -| 2 | `GET /v1/debug-apikey` 프로덕션 비활성화 | 보안 강화 | 소 | -| 3 | 미사용 Swagger 문서 정리 | 문서 정확성 | 소 | - -### 7.2 중복 해소 (Medium Term) 🟡 - -| # | 작업 | 현재 | 목표 | -|---|------|------|------| -| 1 | 사용자 관리 통합 | api/ + mng/ 각각 | api/ 마스터, mng/ 관리자 기능만 추가 | -| 2 | 역할 관리 통합 | api/ + mng/ 각각 | api/ 단일 소스 | -| 3 | 카테고리 통합 | api/ + mng/ 각각 | api/ 마스터, mng/ 글로벌 관리만 유지 | -| 4 | 계좌 관리 통합 | api/ + mng/ 각각 | 하나로 통합 | -| 5 | 메뉴 관리 정리 | api/ 동기화 + mng/ 관리 | 역할 분리 명확화 | - -### 7.3 아키텍처 개선 (Long Term) 🟢 - -| # | 작업 | 설명 | -|---|------|------| -| 1 | MNG → API 호출 전환 | MNG가 DB 직접 접근 대신 api/ REST API 호출 | -| 2 | API Gateway 도입 | 인증/권한/레이트리밋 중앙 관리 | -| 3 | 미사용 API 비활성화 | deprecation 헤더 추가 후 단계적 제거 | -| 4 | API v2 전환 | 중복 정리 포함한 v2 설계 | - ---- - -## 8. 전체 엔드포인트 도메인별 수 - -### API 프로젝트 - -| 도메인 | 파일 | 수 | -|--------|------|:--:| -| 인증 | auth.php | 8 | -| 사용자 | users.php | 25 | -| 테넌트 | tenants.php | 18 | -| 관리자 | admin.php | 22 | -| 공통 | common.php | 95+ | -| HR | hr.php | 85+ | -| 재무 | finance.php | 130+ | -| 영업 | sales.php | 85+ | -| 재고 | inventory.php | 65+ | -| 생산 | production.php | 35+ | -| 설계 | design.php | 55+ | -| 파일 | files.php | 15 | -| 게시판 | boards.php | 70+ | -| 문서 | documents.php | 5+ | -| **합계** | | **~710+** | - -### MNG 프로젝트 - -| 그룹 | 수 | -|------|:--:| -| 사용자/역할/권한 | 30+ | -| 메뉴/글로벌메뉴 | 25+ | -| 게시판/필드 | 20+ | -| 카테고리/글로벌 | 15+ | -| 바로빌 | 60+ | -| 프로젝트 관리 | 25+ | -| 견적 공식 | 30+ | -| 품목 필드 | 25+ | -| 문서/템플릿 | 12+ | -| 계좌/자금일정 | 18+ | -| 기타 | 40+ | -| **합계** | **~300+** | - ---- - -## 9. 참고 문서 - -- `docs/standards/api-rules.md` - API 규칙 -- `docs/architecture/system-overview.md` - 시스템 아키텍처 -- `docs/specs/database-schema.md` - DB 스키마 -- `api/routes/api/v1/*.php` - API 라우트 파일 -- `mng/routes/api.php` - MNG API 라우트 -- `react/src/lib/api/` - React API 클라이언트 - ---- - -## 10. 결론 - -1. **api/와 mng/의 10개 도메인에서 컨트롤러 중복** 발생 - 동일 DB를 각각 직접 접근하는 구조적 문제 -2. **React는 api/ 전체의 약 15%만 사용** - 나머지는 MNG 전용이거나 미구현 기능 -3. **인증 API에 signup/register 중복** 존재 - 즉시 정리 가능 -4. **장기적으로 MNG → API 호출 전환**이 이상적이나, 현재 아키텍처도 기능적으로 동작 -5. **Quick Wins(register 제거, debug-apikey 비활성화)부터 시작** 권장 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/bending-lot-pipeline-dev-plan.md b/plans/archive/bending-lot-pipeline-dev-plan.md deleted file mode 100644 index a9d2833..0000000 --- a/plans/archive/bending-lot-pipeline-dev-plan.md +++ /dev/null @@ -1,1097 +0,0 @@ -# 절곡 자재투입 LOT 매핑 파이프라인 개발 계획 - -> **작성일**: 2026-02-22 -> **목적**: 절곡 세부품목(BD-XX-NN)의 동적 BOM 생성 및 LOT 추적 파이프라인 구축 -> **기준 문서**: `docs/plans/bending-material-input-mapping-plan.md` -> **상태**: ✅ 완료 (Serena ID: bending-lot-pipeline-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 5.2 완료 — 전체 파이프라인 완성 | -| **다음 작업** | 없음 (전체 완료) | -| **진행률** | 13/13 (100%) ✅ | -| **마지막 업데이트** | 2026-02-22 | - ---- - -## 1. 개요 - -### 1.1 배경 - -절곡 작업일지에는 4대 카테고리(가이드레일/하단마감재/셔터박스/연기차단재)의 세부품목이 표시되나, 현재 SAM에서 이 세부품목들이 items 테이블의 BOM과 연결되지 않아 **자재투입 시 세부품목별 LOT 매핑이 불가능**하다. - -**방안 B(동적 BOM 생성)** 확정: 작업지시 생성 시 BendingInfoBuilder를 확장하여 `work_order_items.options.dynamic_bom`에 세부품목 정보를 저장하고, `getMaterials()` API가 이를 우선 참조하도록 수정한다. - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 견적 로직(QuoteCalculationService) 수정 없음 │ -│ 2. DB 스키마 변경 없음 — 기존 options JSON 컬럼 활용 │ -│ 3. 하위 호환성 — dynamic_bom 없는 기존 데이터도 정상 동작 │ -│ 4. bending_info와 dynamic_bom은 동일 Builder에서 동시 생성 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | JSON 필드 추가, 새 Service 클래스 생성, 유틸 함수, 테스트 | 불필요 | -| ⚠️ 컨펌 필요 | getMaterials() 로직 변경, registerMaterialInput API 통일, 프론트 모달 동작 변경 | **필수** | -| 🔴 금지 | items.bom 컬럼 직접 수정, 견적 로직 변경, work_order_material_inputs 스키마 변경 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/standards/api-rules.md` — Service-First, FormRequest, ApiResponse -- `docs/standards/quality-checklist.md` — 품질 체크리스트 -- `docs/rules/item-policy.md` — 품목 정책 (BD-* 명명 규칙) -- `api/CLAUDE.md` — SAM API 개발 규칙 - -### 1.5 성공 기준 - -| 기준 | 측정 방법 | -|------|----------| -| 작업지시 생성 시 dynamic_bom JSON 자동 생성 | work_order_items.options에 dynamic_bom 존재 확인 | -| getMaterials API가 세부품목(BD-RS-43 등) 반환 | API 응답에 세부품목 리스트 포함 확인 | -| 세부품목별 LOT 선택 → 재고 차감 정상 | stock_transactions + work_order_material_inputs 레코드 확인 | -| 자재투입 이력에 work_order_item_id 기록 | WorkOrderMaterialInput 레코드의 work_order_item_id NOT NULL | -| 레거시 5130과 동일한 LOT prefix 체계 | prefix × lengthCode 전체 조합 매칭 검증 | - -### 1.6 현재 구현 컨텍스트 (새 세션 필독) - -> 이 섹션은 새 세션에서 별도 파일을 읽지 않고도 작업을 시작할 수 있도록 핵심 코드 구조를 인라인합니다. - -#### 1.6.1 전체 데이터 흐름 - -``` -[견적/수주] - QuoteCalculationService.calculateBom() - → order_nodes.options.bom_result에 부모 품목 저장 - → 예: BD-가이드레일-KSS01-SUS-120*70, qty=8.5m - ↓ -[작업지시 생성] - WorkOrderService.store() (L266-316) - → salesOrder.items 순회 → work_order_items에 복사 - → nodeOptions에서 bending_info 복사: work_order_items.options.bending_info - → ⭐ [신규] dynamic_bom도 여기서 저장: work_order_items.options.dynamic_bom - ↓ - BendingInfoBuilder.build(Order, processId) (L29-69) - → 절곡 공정 확인 → rootNodes 필터링 → productCode 파싱 - → getMaterialMapping() → aggregateNodes() → assembleBendingInfo() - → ⭐ [신규] buildDynamicBom() → 길이 버킷팅 결과로 BD-XX-NN 세부품목 매핑 - ↓ -[자재투입 조회] - getMaterials(workOrderId) (L1183-1317) - → work_order_items 순회 - → ⭐ [신규] options.dynamic_bom 있으면 세부품목 사용 / 없으면 item.bom fallback - → 세부품목별 Stock → StockLot (FIFO) 조회 - ↓ -[자재투입 등록] - registerMaterialInputForItem(workOrderId, itemId, inputs) (L2821-2907) - → StockService.decreaseFromLot() — 재고 차감 - → WorkOrderMaterialInput::create() — 투입 이력 기록 - ↓ -[생산완료] - updateStatus(workOrderId, 'completed') (L520-602) - → sales_order_id 있으면: createShipmentFromWorkOrder() (출하 직행) - → sales_order_id 없으면: stockInFromProduction() → stock_lots 생성 -``` - -#### 1.6.2 BendingInfoBuilder 핵심 구조 - -**파일**: `api/app/Services/Production/BendingInfoBuilder.php` - -```php -// 진입점 -public function build(Order $order, int $processId, ?array $nodeIds = null): ?array - -// BOM 아이템 카테고리 분류 (L96-130) -private function categorizeBomItem(array $bomItem): ?string -// 반환: 'guideRail', 'shutterBox_case', 'shutterBox_finCover', 'bottomBar', -// 'smokeBarrier_rail', 'smokeBarrier_case', 'detail_lbar', 'detail_reinforce', 'motor' - -// 노드 집계 (L135-175) -private function aggregateNodes(Collection $nodes): array -// 반환: { dimensionGroups: [{height, width, qty}], totalNodeQty, bomCategories: {category => bomItem} } - -// 높이 기준 버킷팅 (L760-763) — 가이드레일용 -private function heightLengthData(array $dimGroups): array -// 반환: [{ length: 2438, quantity: 5 }, { length: 3000, quantity: 3 }] -// 표준 길이: [2438, 3000, 3500, 4000, 4300] - -// 하단마감재 배분 (L801-834) -private function bottomBarDistribution(int $openWidth): array -// 반환: [3000mm수량, 4000mm수량] -// 예: openWidth=7000 → [1, 1] (3000×1 + 4000×1) - -// 셔터박스 배분 (L411-548) -private function shutterBoxDistribution(int $openWidth): array -// 반환: [1219 => qty, 2438 => qty, 3000 => qty, 3500 => qty, 4000 => qty, 4150 => qty] - -// 가이드레일 섹션 (L251-299) -private function buildGuideRail(string $guideType, string $baseSize, array $materials, array $dimGroups, string $productCode): array -// guideType: '벽면형', '측면형', '혼합형' -// 반환: { wall: {baseSize, baseDimension, lengthData}, side: {...} | null } - -// 표준 길이 버킷팅 (L856-865) — ⚠️ 초과 시 원본 반환 -private function bucketToStandardLength(int $dimension, array $buckets): int -``` - -#### 1.6.3 getMaterials() 현재 로직 - -**파일**: `api/app/Services/WorkOrderService.php` L1183-1317 - -``` -Phase 1: 유니크 자재 수집 - for each workOrder.items: - if item.bom 존재: ← 절곡 부모 품목은 bom=null이므로 여기 안 탐 - BOM 자식 순회 → uniqueMaterials[childItemId] += qty - else: ← 현재 절곡은 여기로 빠짐 (부모 품목 자체가 자재로) - uniqueMaterials[itemId] = qty - -Phase 2: StockLot 조회 - for each uniqueMaterial: - stock = Stock.find(itemId) → StockLot.where(available) → FIFO 정렬 - -⚠️ 문제: 절곡 부모 품목(BD-가이드레일-KSS01-SUS-120*70)의 bom이 null - → 세부품목(BD-RS-43 등)이 자재 목록에 나오지 않음 - → dynamic_bom으로 해결 -``` - -#### 1.6.4 registerMaterialInput 두 메서드 차이 - -| 항목 | registerMaterialInput (L1330) | registerMaterialInputForItem (L2821) | -|------|-------------------------------|--------------------------------------| -| 파라미터 | workOrderId, inputs | workOrderId, **itemId**, inputs | -| 재고 차감 | ✅ decreaseFromLot | ✅ decreaseFromLot | -| WorkOrderMaterialInput | ❌ 미생성 | ✅ 생성 (work_order_item_id 포함) | -| 용도 | 전체 작업지시 단위 | 개소(품목) 단위 | - -#### 1.6.5 프론트엔드 현재 구조 - -**MaterialInputModal** (`react/src/components/production/WorkerScreen/MaterialInputModal.tsx`) - -```typescript -// Props — workOrderItemId 유무로 API 경로 분기 -interface MaterialInputModalProps { - order: WorkOrder | null; - workOrderItemId?: number; // 있으면 개소별 API, 없으면 전체 API - workOrderItemName?: string; -} - -// 품목 그룹핑 (L102-119): itemId 기준 Map -// FIFO 배분 (L121-138): selectedLotKeys → 가용량 순서로 자동 배분 -// 등록 (L261-307): -// workOrderItemId ? registerMaterialInputForItem() : registerMaterialInput() -``` - -**API 엔드포인트** (`react/src/components/production/WorkerScreen/actions.ts`) - -| 메서드 | 경로 | 함수명 | -|--------|------|--------| -| GET | `/api/v1/work-orders/{id}/materials` | getMaterialsForWorkOrder | -| GET | `/api/v1/work-orders/{id}/items/{itemId}/materials` | getMaterialsForItem | -| POST | `/api/v1/work-orders/{id}/material-inputs` | registerMaterialInput | -| POST | `/api/v1/work-orders/{id}/items/{itemId}/material-inputs` | registerMaterialInputForItem | -| GET | `/api/v1/work-orders/{id}/items/{itemId}/material-inputs` | getMaterialInputsForItem | -| DELETE | `/api/v1/work-orders/{id}/material-inputs/{inputId}` | deleteMaterialInput | -| PATCH | `/api/v1/work-orders/{id}/material-inputs/{inputId}` | updateMaterialInput | - -**절곡 유틸리티** (`react/.../documents/bending/utils.ts`) - -- `getSLengthCode(length, category)` — 길이→코드 변환 -- `getMaterialMapping(productCode, finishMaterial)` — 재질 매핑 -- `buildWallGuideRailRows()`, `buildSideGuideRailRows()`, `buildBottomBarRows()`, `buildShutterBoxRows()`, `buildSmokeBarrierRows()` — 각 섹션 파트 행 생성 (lotPrefix 포함) - -#### 1.6.6 LOT Prefix 전체 맵 (PrefixResolver 구현 기준) - -**가이드레일 벽면형 (Wall)** - -| 세부품목 | KSS01(SUS) | KSE01/KWE01(EGI마감) | KSE01/KWE01(SUS마감) | KTE01(철재) | -|---------|-----------|-------------------|-------------------|-----------| -| 마감재 | RS | RE | RE | RS | -| 본체 | RM | RM | RM | **RT** | -| C형 | RC | RC | RC | RC | -| D형 | RD | RD | RD | RD | -| 별도마감 | - | - | **YY** | - | -| 하부BASE | XX | XX | XX | XX | - -**가이드레일 측면형 (Side)** - -| 세부품목 | KSS01(SUS) | KSE01/KWE01(EGI마감) | KSE01/KWE01(SUS마감) | KTE01(철재) | -|---------|-----------|-------------------|-------------------|-----------| -| 마감재 | SS | SE | SE | SS | -| 본체 | SM | SM | SM | **ST** | -| C형 | SC | SC | SC | SC | -| D형 | SD | SD | SD | SD | -| 별도마감 | - | - | **YY** | - | -| 하부BASE | XX | XX | XX | XX | - -**하단마감재** - -| 세부품목 | EGI마감 | SUS마감 | 철재 | -|---------|--------|--------|------| -| 메인 | BE | BS | TS | -| L-Bar | LA | LA | LA | -| 보강평철 | HH | HH | HH | -| 별도마감 | - | YY | - | - -**셔터박스** (표준 500*380 사이즈만 개별 prefix) - -| 세부품목 | 표준 prefix | 비표준 prefix | -|---------|-----------|-------------| -| 전면부 | CF | XX | -| 린텔부 | CL | XX | -| 점검구 | CP | XX | -| 후면코너부 | CB | XX | -| 상부덮개 | XX | XX | -| 마구리 | XX | XX | - -**연기차단재**: W50, W80 모두 → GI - -#### 1.6.7 길이코드 매핑 (getSLengthCode) - -| 길이(mm) | 코드 | 비고 | -|---------|------|------| -| 1219 | 12 | 셔터박스 | -| 2438 | 24 | 셔터박스 | -| 3000 | 30 | 공통 | -| 3500 | 35 | 공통 | -| 4000 | 40 | 공통 | -| 4150 | 41 | 셔터박스 | -| 4200 | 42 | - | -| 4300 | 43 | 가이드레일 | -| 3000 | **53** | 연기차단재50 전용 | -| 4000 | **54** | 연기차단재50 전용 | -| 3000 | **83** | 연기차단재80 전용 | -| 4000 | **84** | 연기차단재80 전용 | - -**코드 생성 규칙**: `BD-{prefix}-{lengthCode}` → 예: `BD-RS-43` = 가이드레일 벽면 SUS 마감재 4300mm - -#### 1.6.8 BD-* 마스터 현황 (items 테이블, 총 148개) - -**A. 제품 마스터형 (58개)** — 부모 품목 (견적 BOM에 사용) -``` -BD-가이드레일-KSS01-SUS-120*70 등 (20개: 제품코드별) -BD-하단마감재-KSE01-EGI-60*40 등 (10개) -BD-케이스-500*380 등 (10개), BD-마구리-505*355 등 (10개) -BD-L-BAR-*, BD-보강평철-*, BD-연기차단재 (8개) -``` - -**B. LOT prefix형 (90개 등록, XX/YY/HH 미등록)** — 세부품목 (자재투입 대상) - -| prefix | 수량 | prefix | 수량 | prefix | 수량 | -|--------|:----:|--------|:----:|--------|:----:| -| BD-RS | 5 | BD-SS | 4 | BD-BE | 2 | -| BD-RM | 6 | BD-SM | 5 | BD-BS | 5 | -| BD-RC | 6 | BD-SC | 5 | BD-TS | 1 | -| BD-RD | 6 | BD-SD | 5 | BD-LA | 2 | -| BD-RT | 2 | BD-ST | 1 | BD-CF | 6 | -| | | BD-SU | 4 | BD-CL | 6 | -| | | | | BD-CP | 6 | -| | | | | BD-CB | 6 | -| | | | | BD-GI | 7 | - -**미등록**: BD-XX (하부BASE/셔터 상부/마구리), BD-YY (별도SUS마감), BD-HH (보강평철) → Phase 0.1에서 등록 - -#### 1.6.9 dynamic_bom JSON 목표 구조 - -`work_order_items.options.dynamic_bom` 에 저장: - -```json -[ - { - "child_item_id": 15812, - "child_item_code": "BD-RS-43", - "lot_prefix": "RS", - "part_type": "마감재", - "category": "guideRail", - "material_type": "SUS", - "length_mm": 4300, - "qty": 1 - }, - { - "child_item_id": 15809, - "child_item_code": "BD-RS-40", - "lot_prefix": "RS", - "part_type": "마감재", - "category": "guideRail", - "material_type": "SUS", - "length_mm": 4000, - "qty": 1 - }, - { - "child_item_id": 15826, - "child_item_code": "BD-RM-43", - "lot_prefix": "RM", - "part_type": "본체", - "category": "guideRail", - "material_type": "EGI", - "length_mm": 4300, - "qty": 1 - } -] -``` - -**필드 설명**: -- `child_item_id`: items 테이블 PK (getMaterials에서 Stock/StockLot 조회용) -- `child_item_code`: items.code (표시용) -- `lot_prefix`: LOT prefix (프론트 작업일지 매핑용) -- `part_type`: 세부품명 한글 (마감재, 본체, C형 등) -- `category`: 4대 카테고리 (guideRail, bottomBar, shutterBox, smokeBarrier) -- `material_type`: 재질 (SUS, EGI 등) -- `length_mm`: 표준 길이 (mm) -- `qty`: 수량 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 0: 선행 준비 (마스터 데이터) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 0.1 | XX/YY/HH 미등록 품목 items 등록 | ✅ | 22건 등록 (13+9 추가 누락) | -| 0.2 | 마스터 데이터 검증 스크립트 작성 | ✅ | 101/101 전체 통과 | - -### 2.2 Phase 1: GAP #1 해결 — API 통일 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | registerMaterialInput → registerMaterialInputForItem 통일 | ✅ | work_order_item_id 분기 + fallback + N+1 수정 | -| 1.2 | 프론트 workOrderItemId 전달 보장 | ✅ | actions.ts + MaterialInputModal work_order_item_id 전달 | - -### 2.3 Phase 2: 방안 B 핵심 — dynamic_bom 생성 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | PrefixResolver 클래스 구현 | ✅ | `app/Services/Production/PrefixResolver.php` | -| 2.2 | BendingInfoBuilder 확장 — dynamic_bom 생성 | ✅ | `build()` 리턴 변경 + `buildDynamicBomForItem()` 추가, OrderService 연동 | -| 2.3 | DynamicBomEntry DTO 구현 | ✅ | `app/DTOs/Production/DynamicBomEntry.php` | - -### 2.4 Phase 3: getMaterials 연동 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | getMaterials() dynamic_bom 우선 체크 | ✅ | dynamic_bom → BOM fallback, (item_id, woItem_id) 쌍 합산, 추가 필드 반환 | -| 3.2 | N+1 쿼리 최적화 + uniqueMaterials 합산 단위 변경 | ✅ | 3.1에서 함께 해결: Item/Stock/StockLot 모두 배치 조회 | - -### 2.5 Phase 4: 프론트엔드 연동 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | 자재투입 모달 세부품목 단위 표시 | ✅ | MaterialInputModal groupKey + category badge + actions.ts 필드 추가 | -| 4.2 | 작업일지 LOT NO 표시 연동 | ✅ | 4개 섹션 lotNoMap prop + WorkLogModal lotNoMap 빌드 | - -### 2.6 Phase 5: 테스트 및 검증 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | PrefixResolver + dynamic_bom 단위 테스트 | ✅ | 58 tests / 256 assertions 통과 | -| 5.2 | getMaterials → 자재투입 통합 테스트 | ✅ | 6 tests (4 pass + 2 skip — dynamic_bom 작업지시 미생성), 마스터 품목 전체 검증 | - -### 2.7 별도 과제 (이 계획 범위 밖) - -| # | 항목 | 시점 | -|---|------|------| -| X.1 | GAP #4: 수주 연결 생산완료 → stock_lots 입고 통일 | 출하 시스템 설계 시 | -| X.2 | GAP #3: lot_genealogy (투입↔산출 LOT 직접 연결) | 향후 고도화 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Phase 0: 선행 준비 -├── 0.1 XX/YY/HH 품목 등록 (items 테이블 INSERT) -└── 0.2 검증 스크립트 (Artisan Command) - └── 19종 prefix × 7-12 lengthCode 조합 → items 존재 확인 - -Phase 1: API 통일 (GAP #1) — Phase 0 완료 후 -├── 1.1 registerMaterialInput() 내부에서 registerMaterialInputForItem() 호출하도록 통일 -│ ├── WorkOrderService.php L1330-1388 수정 -│ └── 기존 프론트 호출 호환성 유지 -└── 1.2 프론트 workOrderItemId 전달 - └── WorkerScreen/index.tsx → MaterialInputModal Props - -Phase 2: dynamic_bom 생성 — Phase 0 완료 후 (Phase 1과 병행 가능) -├── 2.1 PrefixResolver 클래스 -│ ├── productCode + finishMaterial + guideType → prefix 결정 -│ ├── prefix + lengthMm → BD-XX-NN 코드 생성 -│ └── BD-XX-NN → items.id 조회 (캐시) -├── 2.2 BendingInfoBuilder 확장 -│ ├── build() 반환값에 dynamic_bom 추가 -│ ├── bending_info와 동시 생성 (정합성 보장) -│ └── work_order_items.options.dynamic_bom에 저장 -└── 2.3 DynamicBomValidator - └── dynamic_bom JSON 구조 검증 (child_item_id 필수 등) - -Phase 3: getMaterials 수정 — Phase 2 완료 후 -├── 3.1 dynamic_bom 우선 체크 -│ ├── WorkOrderService.php getMaterials() L1198 이후 -│ ├── options.dynamic_bom 있으면 → 세부품목 리스트 사용 -│ └── 없으면 → 기존 item.bom fallback (하위 호환) -└── 3.2 N+1 최적화 - ├── Item::whereIn() 배치 조회 - └── uniqueMaterials 합산 단위: (item_id, work_order_item_id) 쌍 - -Phase 4: 프론트엔드 — Phase 3 완료 후 -├── 4.1 자재투입 모달 수정 -│ ├── materialGroups가 세부품목 단위로 표시 (이미 itemId 기준 그룹핑) -│ └── 그룹 헤더에 세부품목명(BD-RS-43) 표시 -└── 4.2 작업일지 LOT NO 표시 - ├── dynamic_bom에서 lotPrefix + lengthCode 조합 - └── 투입 이력(getMaterialInputsForItem)에서 실제 LOT NO 반영 - -Phase 5: 테스트 — Phase 3 완료 후 (Phase 4와 병행 가능) -├── 5.1 단위 테스트 -│ ├── PrefixResolver: 7종 productCode × 3종 finishMaterial × 3종 guideType -│ ├── dynamic_bom 생성: 실제 bom_result 데이터 기반 -│ └── DynamicBomValidator: 필수/선택 필드 검증 -└── 5.2 통합 테스트 - ├── 작업지시 생성 → dynamic_bom 저장 확인 - ├── getMaterials → 세부품목 반환 확인 - └── 자재투입 → stock_transactions + work_order_material_inputs 확인 -``` - -### 3.2 의존성 맵 - -``` -Phase 0 ──→ Phase 1 (독립 진행 가능) - │ - └──→ Phase 2 ──→ Phase 3 ──→ Phase 4 - │ - └──→ Phase 5 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 0: 선행 준비 - -#### 0.1 XX/YY/HH 미등록 품목 등록 - -**현재 상태**: BD-* 품목 148개 중 XX(하부BASE), YY(별도SUS마감), HH(보강평철) 미등록 - -**목표 상태**: BD-XX-NN, BD-YY-NN, BD-HH-NN 패턴으로 items 테이블에 등록 - -**등록 대상**: - -| prefix | 설명 | 등록할 길이코드 | 예상 수량 | -|--------|------|---------------|----------| -| BD-XX | 하부BASE, 셔터박스 상부덮개/마구리 | 12, 24, 30, 35, 40, 41, 43 | 7개 | -| BD-YY | 별도 SUS 마감 (SUS마감 시만) | 30, 35, 40, 43 | 4개 | -| BD-HH | 보강평철 | 30, 40 | 2개 | - -**수정 파일**: 없음 (DB INSERT — Seeder 또는 Artisan Command) - -**생성 파일**: -- `api/database/seeders/BendingItemSeeder.php` — BD-XX/YY/HH 품목 등록 - -**검증**: `items` 테이블에서 `code LIKE 'BD-XX-%'` 조회로 13개 확인 - ---- - -#### 0.2 마스터 데이터 검증 스크립트 - -**목적**: 19종 prefix × 가능 lengthCode 전체 조합이 items에 존재하는지 확인 - -**생성 파일**: -- `api/app/Console/Commands/ValidateBendingItems.php` - -**로직**: -``` -전체 prefix 목록 정의 (RS, RM, RC, RD, RT, SS, SM, SC, SD, ST, SU, BE, BS, TS, LA, CF, CL, CP, CB, GI, XX, YY, HH) -각 prefix별 유효 lengthCode 정의 -조합별 items.code = "BD-{prefix}-{code}" 존재 확인 -누락 항목 리스트 출력 -``` - -**실행**: `php artisan bending:validate-items` - -**검증**: 출력이 "All items registered" (누락 0건) - ---- - -### 4.2 Phase 1: GAP #1 해결 — API 통일 - -#### 1.1 registerMaterialInput → registerMaterialInputForItem 통일 - -**현재 상태**: -- `registerMaterialInput()` (L1330): 재고 차감만, WorkOrderMaterialInput 레코드 미생성 -- `registerMaterialInputForItem()` (L2821): 재고 차감 + WorkOrderMaterialInput 레코드 생성 - -**목표 상태**: 모든 자재투입이 `work_order_material_inputs`에 기록 - -**수정 파일**: -- `api/app/Services/WorkOrderService.php` - -**수정 내용**: -``` -registerMaterialInput(int $workOrderId, array $inputs) 수정: - ├── $inputs 배열에 work_order_item_id 필드 추가 지원 - │ { stock_lot_id: N, qty: N, work_order_item_id?: N } - ├── work_order_item_id가 있으면 → registerMaterialInputForItem() 위임 - └── work_order_item_id가 없으면 → 기존 동작 + WorkOrderMaterialInput 레코드 생성 추가 - (work_order_item_id = 첫 번째 work_order_item의 id로 fallback) -``` - -**N+1 개선**: `registerMaterialInputForItem()` L2860-2861의 `StockLot::find()` → `$lot->stock->item_id` 호출을 `StockLot::with('stock')->find()` Eager Loading으로 변경 - -**검증**: -- POST `/work-orders/{id}/material-inputs` 호출 후 `work_order_material_inputs` 테이블에 레코드 존재 확인 -- 기존 호출 형식(work_order_item_id 미포함)도 정상 동작 확인 - ---- - -#### 1.2 프론트 workOrderItemId 전달 보장 - -**현재 상태**: `WorkerScreen/index.tsx`에서 `MaterialInputModal`에 `workOrderItemId` Props를 전달하지만, 완료 플로우에서는 미지정 가능 - -**수정 파일**: -- `react/src/components/production/WorkerScreen/index.tsx` - -**수정 내용**: -- 자재투입 모달 호출 시 `workOrderItemId`가 항상 전달되도록 보장 -- 완료 플로우에서도 `selectedItemId` 설정 - -**검증**: MaterialInputModal이 항상 `registerMaterialInputForItem()` 경로로 호출되는지 확인 - ---- - -### 4.3 Phase 2: 방안 B 핵심 — dynamic_bom 생성 - -#### 2.1 PrefixResolver 클래스 구현 - -**목적**: 제품코드 + 마감재질 + 가이드타입 → LOT prefix 결정 로직을 단일 클래스로 집중 - -**생성 파일**: -- `api/app/Services/Production/PrefixResolver.php` - -**클래스 구조**: -```php -class PrefixResolver -{ - // 벽면형 prefix 맵 - private const WALL_PREFIXES = [ - 'finish' => ['KSS' => 'RS', 'KSE' => 'RE', 'KWE' => 'RE'], - 'body' => 'RM', - 'c_type' => 'RC', - 'd_type' => 'RD', - 'extra_finish' => 'YY', // SUS 마감 시만 - 'base' => 'XX', - ]; - - // 측면형 prefix 맵 - private const SIDE_PREFIXES = [ - 'finish' => ['KSS' => 'SS', 'KSE' => 'SE', 'KWE' => 'SE'], - 'body' => 'SM', - 'c_type' => 'SC', - 'd_type' => 'SD', - 'extra_finish' => 'YY', - 'base' => 'XX', - ]; - - // 철재형 override - private const STEEL_OVERRIDES = [ - 'wall_body' => 'RT', - 'side_body' => 'ST', - ]; - - // 하단마감재 prefix 맵 - private const BOTTOM_BAR_PREFIXES = [ - 'EGI' => 'BE', - 'SUS' => 'BS', - 'STEEL_SUS' => 'TS', - ]; - - // 셔터박스 prefix 맵 (표준 사이즈만) - private const SHUTTER_BOX_PREFIXES = [ - 'front' => 'CF', - 'lintel' => 'CL', - 'inspection' => 'CP', - 'rear_corner' => 'CB', - 'top_cover' => 'XX', - 'fin_cover' => 'XX', - ]; - - // 연기차단재 - private const SMOKE_PREFIXES = [ - 'w50' => 'GI', - 'w80' => 'GI', - ]; - - /** - * 가이드레일 세부품목의 prefix 결정 - */ - public function resolveGuideRailPrefix( - string $partType, // 'finish', 'body', 'c_type', 'd_type', 'extra_finish', 'base' - string $guideType, // 'wall', 'side' - string $productCode, // 'KSS01', 'KSE01', ... - ): string - - /** - * 하단마감재 세부품목의 prefix 결정 - */ - public function resolveBottomBarPrefix( - string $partType, // 'main', 'lbar', 'reinforce', 'extra' - string $finishMaterial, // 'EGI 1.55T', 'SUS 1.2T' - string $productCode, - ): string - - /** - * 셔터박스 세부품목의 prefix 결정 - */ - public function resolveShutterBoxPrefix( - string $partType, // 'front', 'lintel', 'inspection', 'rear_corner', 'top_cover', 'fin_cover' - bool $isStandardSize, // 500*380인지 - ): string - - /** - * 연기차단재 세부품목의 prefix 결정 - */ - public function resolveSmokeBarrierPrefix(string $partType): string - - /** - * prefix + 길이(mm) → BD-XX-NN 코드 생성 - */ - public function buildItemCode(string $prefix, int $lengthMm, ?string $smokeCategory = null): string - - /** - * BD-XX-NN 코드 → items.id 조회 (캐시 사용) - */ - public function resolveItemId(string $itemCode): ?int - - /** - * 길이(mm) → 길이코드 변환 (getSLengthCode 동일) - */ - public static function lengthToCode(int $lengthMm, ?string $smokeCategory = null): ?string -} -``` - -**의존성**: `App\Models\Items\Item` (코드→ID 조회용) - -**검증**: 단위 테스트에서 productCode × guideType × partType 전 조합 테스트 - ---- - -#### 2.2 BendingInfoBuilder 확장 — dynamic_bom 생성 - -**수정 파일**: -- `api/app/Services/Production/BendingInfoBuilder.php` - -**수정 범위**: - -1. **build() 메서드 (L29-69)**: 반환값에 `dynamic_bom` 배열 추가 - ``` - 현재: return assembleBendingInfo(...) // bending_info만 - 변경: return [ - 'bending_info' => assembleBendingInfo(...), - 'dynamic_bom' => buildDynamicBom(...) // 신규 - ] - ``` - -2. **buildDynamicBom() 신규 메서드**: bending_info 생성과 동일한 길이 버킷팅 결과를 사용 - ``` - private function buildDynamicBom( - array $aggregated, // aggregateNodes() 결과 - string $productCode, - array $materials, // getMaterialMapping() 결과 - PrefixResolver $resolver, - ): array - ``` - - **로직**: - ``` - dynamic_bom = [] - - // 1. 가이드레일 세부품목 - for each guideType (wall, side): - lengthData = heightLengthData(dimGroups) // 기존 버킷팅 재사용 - for each (length, qty) in lengthData: - for each partType in [finish, body, c_type, d_type, extra_finish, base]: - prefix = resolver.resolveGuideRailPrefix(partType, guideType, productCode) - if prefix is empty: skip - itemCode = resolver.buildItemCode(prefix, length) - itemId = resolver.resolveItemId(itemCode) - dynamic_bom[] = { - child_item_id: itemId, - child_item_code: itemCode, - lot_prefix: prefix, - part_type: partType의 한글명, - category: 'guideRail', - material_type: materials[partType], - length_mm: length, - qty: qty - } - - // 2. 하단마감재 세부품목 - for each dimGroup: - [qty3000, qty4000] = bottomBarDistribution(openWidth) - for each (length, qty) in [(3000, qty3000), (4000, qty4000)]: - if qty == 0: skip - for each partType in [main, lbar, reinforce, extra]: - prefix = resolver.resolveBottomBarPrefix(partType, finishMaterial, productCode) - ... dynamic_bom 추가 ... - - // 3. 셔터박스 세부품목 - for each dimGroup: - distribution = shutterBoxDistribution(openWidth) - for each (length, qty) in distribution: - if qty == 0: skip - isStandard = (boxSize == '500*380') - for each partType in [front, lintel, inspection, rear_corner, top_cover, fin_cover]: - prefix = resolver.resolveShutterBoxPrefix(partType, isStandard) - ... dynamic_bom 추가 ... - - // 4. 연기차단재 세부품목 - for each smokeType (w50, w80): - for each (length, qty) in smokeLengthData: - prefix = resolver.resolveSmokeBarrierPrefix(smokeType) - smokeCategory = smokeType == 'w50' ? '연기차단재50' : '연기차단재80' - itemCode = resolver.buildItemCode(prefix, length, smokeCategory) - ... dynamic_bom 추가 ... - - return dynamic_bom - ``` - -3. **work_order_items.options 저장 위치 수정**: - - `WorkOrderService.php` L275-306 (작업지시 품목 복사 로직)에서 build() 반환값의 `dynamic_bom`을 `options.dynamic_bom`에 저장 - -**주의사항**: -- `aggregateNodes()` L164의 `!isset` 체크: 첫 노드에서만 BOM 메타 추출 → 노드별 BOM이 다를 수 있으므로 주의 -- `bucketToStandardLength()` L862-864: 표준 길이 초과 시 원본 반환 → PrefixResolver.resolveItemId()에서 null 반환 시 경고 로그 + fallback -- 혼합형 가이드레일: wall + side 각각 독립 dynamic_bom 생성 - -**검증**: -- 작업지시 생성 API 호출 후 `work_order_items.options` JSON에 `dynamic_bom` 배열 존재 확인 -- dynamic_bom의 각 항목에 `child_item_id`가 NOT NULL인지 확인 -- bending_info의 lengthData와 dynamic_bom의 length_mm/qty가 일치하는지 확인 - ---- - -#### 2.3 DynamicBomValidator DTO 구현 - -**생성 파일**: -- `api/app/DTOs/Production/DynamicBomEntry.php` - -**구조**: -```php -class DynamicBomEntry -{ - public function __construct( - public readonly int $child_item_id, - public readonly string $child_item_code, - public readonly string $lot_prefix, - public readonly string $part_type, - public readonly string $category, // guideRail, bottomBar, shutterBox, smokeBarrier - public readonly string $material_type, - public readonly int $length_mm, - public readonly int|float $qty, - ) {} - - public static function fromArray(array $data): self - public function toArray(): array - public static function validate(array $data): bool // child_item_id 필수 등 -} -``` - -**검증**: 단위 테스트에서 필수 필드 누락 시 예외 발생 확인 - ---- - -### 4.4 Phase 3: getMaterials 연동 - -#### 3.1 getMaterials() dynamic_bom 우선 체크 - -**수정 파일**: -- `api/app/Services/WorkOrderService.php` - -**수정 위치**: `getMaterials()` L1198 이후 - -**수정 내용**: -``` -현재 (L1198-1238): - foreach (workOrderItems as woItem): - item = woItem.item - if (item.bom): - ... BOM 순회 ... - else: - ... item 자체를 자재로 ... - -변경: - // Phase 1: dynamic_bom 대상 item_id 일괄 수집 - allDynamicItemIds = [] - foreach (workOrderItems as woItem): - dynamicBom = woItem.options['dynamic_bom'] ?? null - if (dynamicBom): - allDynamicItemIds += array_column(dynamicBom, 'child_item_id') - - // Phase 2: 배치 조회 (N+1 방지) - dynamicItems = Item::whereIn('id', array_unique(allDynamicItemIds)) - ->get()->keyBy('id') - - // Phase 3: 유니크 자재 수집 - foreach (workOrderItems as woItem): - dynamicBom = woItem.options['dynamic_bom'] ?? null - if (dynamicBom): - foreach (dynamicBom as bomEntry): - childItem = dynamicItems[bomEntry['child_item_id']] - // 합산 키: (item_id, work_order_item_id) 쌍 - key = bomEntry['child_item_id'] . '_' . woItem.id - uniqueMaterials[key] = { - item_id: bomEntry['child_item_id'], - work_order_item_id: woItem.id, - bom_qty: bomEntry['qty'], - item: childItem, - ... - } - elseif (item.bom): - ... 기존 BOM 로직 (하위 호환) ... - else: - ... 기존 fallback ... -``` - -**반환 형식 변경**: -``` -기존: { stock_lot_id, item_id, lot_no, bom_qty, required_qty, ... } -추가: { ..., work_order_item_id, lot_prefix, part_type, category } -``` - -**검증**: -- dynamic_bom 있는 work_order → 세부품목(BD-RS-43 등) 반환 확인 -- dynamic_bom 없는 work_order → 기존 동작 그대로 (하위 호환) -- 동일 item_id가 다른 work_order_item에 속한 경우 별도 행으로 반환 - ---- - -#### 3.2 N+1 쿼리 최적화 + uniqueMaterials 합산 단위 변경 - -**수정 파일**: `api/app/Services/WorkOrderService.php` - -**수정 내용**: -1. `Item::find()` 개별 호출 → `Item::whereIn()` 배치 조회 -2. `uniqueMaterials` 합산 키를 `item_id` → `(item_id, work_order_item_id)` 쌍으로 변경 -3. StockLot 조회도 `Stock::whereIn()` 배치 처리 - -**기대 효과**: 쿼리 수 30-50회 → 3-5회로 감소 - -**검증**: Laravel Debugbar 또는 DB 쿼리 로그로 쿼리 수 확인 - ---- - -### 4.5 Phase 4: 프론트엔드 연동 - -#### 4.1 자재투입 모달 세부품목 단위 표시 - -**수정 파일**: -- `react/src/components/production/WorkerScreen/MaterialInputModal.tsx` - -**현재 상태**: `materialGroups`가 `itemId` 기준 그룹핑 (L102-119). getMaterials 응답이 세부품목을 반환하면 자동으로 세부품목 단위 그룹핑됨. - -**수정 내용**: -- 그룹 헤더에 세부품목명(BD-RS-43 등) + part_type(마감재 등) + category(가이드레일 등) 표시 -- 기존 `materialCode`/`materialName` 필드로 충분하나, 카테고리별 시각적 구분 추가 - -**수정 규모**: 소규모 — 그룹 헤더 렌더링 수정 - -**검증**: 자재투입 모달에서 세부품목별 그룹이 표시되고, 각 그룹 내 LOT 선택이 정상 동작 - ---- - -#### 4.2 작업일지 LOT NO 표시 연동 - -**수정 파일**: -- `react/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx` -- 해당 폴더의 다른 Section 컴포넌트 (BottomBarSection, ShutterBoxSection 등) - -**현재 상태**: LOT NO 컬럼이 `"-"`로 하드코딩 - -**수정 내용**: -- `getMaterialInputsForItem()` API로 투입 이력 조회 -- lotPrefix + lengthCode 매칭으로 실제 LOT NO 표시 -- 투입 전이면 "-", 투입 후이면 실제 LOT 번호 - -**수정 규모**: 중규모 — 각 Section 컴포넌트에 LOT 조회 로직 추가 - -**검증**: 자재투입 완료 후 작업일지에 실제 LOT NO 표시 - ---- - -### 4.6 Phase 5: 테스트 및 검증 - -#### 5.1 단위 테스트 - -**생성 파일**: -- `api/tests/Unit/Services/Production/PrefixResolverTest.php` -- `api/tests/Unit/Services/Production/BendingInfoBuilderDynamicBomTest.php` - -**테스트 케이스**: - -| 테스트 | 입력 | 기대 결과 | -|--------|------|----------| -| KSS01 벽면형 마감재 4300mm | ('finish', 'wall', 'KSS01') | prefix='RS', code='BD-RS-43' | -| KSE01 측면형 본체 3000mm | ('body', 'side', 'KSE01') | prefix='SM', code='BD-SM-30' | -| KTE01 벽면형 본체 (철재) | ('body', 'wall', 'KTE01') | prefix='RT' | -| 하단마감재 EGI | ('main', 'EGI 1.55T', 'KSE01') | prefix='BE' | -| 셔터박스 비표준 사이즈 | ('front', false) | prefix='XX' | -| 연기차단재 W50 3000mm | resolveSmokeBarrierPrefix('w50') | prefix='GI', code='BD-GI-53' | -| 표준 길이 초과 (4500mm) | buildItemCode('RS', 4500) | 경고 로그 + null 반환 | - ---- - -#### 5.2 통합 테스트 - -**생성 파일**: -- `api/tests/Feature/Production/BendingMaterialInputFlowTest.php` - -**테스트 시나리오**: - -``` -1. 작업지시 생성 → dynamic_bom 저장 확인 - - Order (KSS01, SUS마감, 오픈높이=4300, 오픈폭=3000) - - 작업지시 생성 → work_order_items.options.dynamic_bom 확인 - - dynamic_bom에 RS-43, RM-43, RC-43, RD-43 세부품목 존재 - -2. getMaterials → 세부품목 반환 확인 - - getMaterials(workOrderId) 호출 - - 응답에 BD-RS-43, BD-RM-43 등 세부품목 반환 - - 각 세부품목의 StockLot 정보 포함 - -3. 자재투입 → 이력 기록 확인 - - registerMaterialInputForItem() 호출 - - stock_transactions에 OUT 기록 - - work_order_material_inputs에 레코드 생성 - - stock_lots.available_qty 감소 -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | registerMaterialInput API 통일 | 기존 API에 WorkOrderMaterialInput 레코드 생성 추가 | 프론트 호출 호환 유지 | ⏳ | -| 2 | BendingInfoBuilder.build() 반환값 변경 | 기존 array → { bending_info, dynamic_bom } | WorkOrderService 호출처 수정 필요 | ⏳ | -| 3 | getMaterials() 로직 변경 | dynamic_bom 우선 체크 + 합산 단위 변경 | MaterialInputModal 응답 형식 변경 | ⏳ | - ---- - -## 6. 변경 이력 - -| 날짜 | 변경 내용 | -|------|----------| -| 2026-02-22 | 문서 초안 작성 | -| 2026-02-22 | Phase 0 완료: BD-* 22건 등록 + 검증 101/101 통과 | -| 2026-02-22 | Phase 2 완료: PrefixResolver, BendingInfoBuilder 확장(build→context+bending_info, buildDynamicBomForItem), DynamicBomEntry DTO, OrderService 연동 | -| 2026-02-22 | Phase 1.1 + 3.1/3.2 완료: registerMaterialInput 통일 (work_order_item_id 분기+fallback+WorkOrderMaterialInput 레코드 생성), getMaterials dynamic_bom 우선체크 + N+1 배치최적화 | - ---- - -## 7. 참고 문서 - -| 문서 | 경로 | -|------|------| -| **분석 기준 문서** | `docs/plans/bending-material-input-mapping-plan.md` | -| 선생산 재고 계획 | `docs/plans/bending-preproduction-stock-plan.md` | -| BendingInfoBuilder | `api/app/Services/Production/BendingInfoBuilder.php` | -| WorkOrderService | `api/app/Services/WorkOrderService.php` | -| StockService | `api/app/Services/StockService.php` | -| WorkOrderMaterialInput 모델 | `api/app/Models/Production/WorkOrderMaterialInput.php` | -| MaterialInputModal | `react/src/components/production/WorkerScreen/MaterialInputModal.tsx` | -| WorkerScreen actions | `react/src/components/production/WorkerScreen/actions.ts` | -| Bending types/utils | `react/src/components/production/WorkOrders/documents/bending/` | -| API 개발 규칙 | `docs/standards/api-rules.md` | -| 품질 체크리스트 | `docs/standards/quality-checklist.md` | - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("bending-lot-pipeline-state") // 1. 상태 파악 -read_memory("bending-lot-pipeline-snapshot") // 2. 사고 흐름 복구 -read_memory("bending-lot-pipeline-active-symbols") // 3. 작업 대상 파악 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | Snapshot | `write_memory("bending-lot-pipeline-snapshot", "코드변경+논의요약")` | -| **20% 이하** | Context Purge | `write_memory("bending-lot-pipeline-active-symbols", "주요 수정 파일/함수")` | -| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 | - -### 8.3 Serena 메모리 구조 -- `bending-lot-pipeline-state`: { phase, progress, next_step, last_decision } -- `bending-lot-pipeline-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `bending-lot-pipeline-rules`: 해당 작업에서 결정된 불변의 규칙들 -- `bending-lot-pipeline-active-symbols`: 현재 수정 중인 파일/심볼 리스트 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| KSS01 + SUS + 벽면형 + 4300mm | BD-RS-43 (item_id 존재) | | ⏳ | -| getMaterials (dynamic_bom 있는 WO) | 세부품목 리스트 반환 | | ⏳ | -| 자재투입 등록 | work_order_material_inputs 레코드 생성 | | ⏳ | -| getMaterials (dynamic_bom 없는 WO) | 기존 동작 (하위 호환) | | ⏳ | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|:----:|------| -| dynamic_bom 자동 생성 | ⏳ | Phase 2 완료 후 | -| getMaterials 세부품목 반환 | ⏳ | Phase 3 완료 후 | -| 세부품목별 LOT 입력 가능 | ⏳ | Phase 4 완료 후 | -| 자재투입 이력 100% 기록 | ⏳ | Phase 1 완료 후 | -| LOT prefix 체계 일치 | ⏳ | Phase 0.2 검증 후 | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 1.5 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 대상 범위 (13개 태스크) | -| 4 | 의존성이 명시되어 있는가? | ✅ | 3.2 의존성 맵 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 코드 분석 기반 확인 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 상세 작업 내용 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 각 태스크별 검증 항목 | -| 8 | 모호한 표현이 없는가? | ✅ | 라인 번호, 메서드명, 파일 경로 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 0 + 📍 현재 진행 상태 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 섹션 4 (각 태스크별 수정/생성 파일 명시) | -| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 + 각 태스크별 검증 항목 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* diff --git a/plans/archive/bending-worklog-reimplementation-plan.md b/plans/archive/bending-worklog-reimplementation-plan.md deleted file mode 100644 index 1da3252..0000000 --- a/plans/archive/bending-worklog-reimplementation-plan.md +++ /dev/null @@ -1,860 +0,0 @@ -# 절곡 작업일지 완전 재구현 계획 - -> **작성일**: 2026-02-19 -> **목적**: PHP viewBendingWork_slat.php와 동일한 구조로 React BendingWorkLogContent.tsx 완전 재구현 -> **기준 문서**: `5130/output/viewBendingWork_slat.php` (~1400줄) -> **상태**: ✅ 구현 완료 (커밋: 59b9b1b) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 1~5 전체 구현 완료 + 슬랫 입고 LOT NO 개소별 표시 버그 수정 | -| **다음 작업** | 실 데이터 테스트 (bending_info가 채워진 작업지시로 화면 확인) | -| **진행률** | 15/15 (100%) | -| **마지막 업데이트** | 2026-02-19 | -| **Git 커밋** | `59b9b1b` feat(WEB): 절곡 작업일지 완전 재구현 + 슬랫 입고 LOT NO 개소별 표시 수정 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 React `BendingWorkLogContent.tsx`는 **빈 껍데기 상태**로, 단순 테이블에 `item.productName`, `item.specification`, `item.quantity`만 평면 나열함. PHP 원본(`viewBendingWork_slat.php`)의 4개 카테고리 구조를 전혀 지원하지 않음. - -**현재 React 컴포넌트 상태:** -- 헤더 + 결재란 (ConstructionApprovalTable 사용) ✅ -- 신청업체 / 신청내용 테이블 ✅ -- 제품 정보 테이블 (빈 칸) ❌ 데이터 바인딩 없음 -- 작업내역 (유형명/세부품명/재질/LOT/길이/수량) ❌ 단순 flat 리스트 -- 생산량 합계 [kg] SUS/EGI ❌ 빈 칸 -- **4개 카테고리 섹션 완전 부재** ❌ - -**PHP 원본 구조 (구현 목표):** -- 가이드레일: 벽면형/측면형 분류, 이미지 + 세부품명별 길이/수량/LOT NO/무게 계산 -- 하단마감재: 3000/4000mm 길이별 수량, 별도마감재 -- 셔터박스: 동적 이미지 + 구성요소(전면부/린텔부/점검구/후면코너부/상부덮개/측면부) -- 연기차단재: W50 레일용, W80 케이스용 -- 생산량 합계: SUS(7.93g/cm3) / EGI(7.85g/cm3) 무게 자동 계산 - -### 1.2 데이터 흐름 (전체 파이프라인) - -``` -[수주 시스템] -order_nodes.options.bending_info (JSON) - │ - ▼ WorkOrderService.php (Line 276) - │ $nodeOptions['bending_info'] ?? null - │ - ▼ -work_order_items.options (JSON) - │ { floor, code, width, height, bending_info, slat_info, cutting_info, wip_info } - │ - ▼ API GET /work-orders/{id} → items[].options.bending_info - │ - ▼ Frontend getWorkOrderById() → WorkOrder.items - │ - ▼ WorkLogModal.tsx (Line 207-213) - │ - │ ※ materialLots 미전달 (bending은 slat과 다르게 LOT를 별도로 안 받음) - │ - ▼ BendingWorkLogContent.tsx (재작성 대상) -``` - -**핵심**: `bending_info`는 `work_order_items.options` JSON 안에 저장되며, 현재 프론트엔드 `WorkOrderItem` 타입에는 `bendingInfo` 필드가 **없음** (slatInfo처럼 추가 필요). - -### 1.3 현재 bending_info 구조 (SAM에 정의된 것) - -```typescript -// react/src/components/production/WorkerScreen/types.ts (Lines 91-107) -export interface BendingInfo { - drawingUrl?: string; - common: BendingCommonInfo; - detailParts: BendingDetailPart[]; -} - -export interface BendingCommonInfo { - kind: string; // "혼합형 120X70" - type: string; // "혼합형" | "벽면형" | "측면형" - lengthQuantities: { length: number; quantity: number }[]; -} - -export interface BendingDetailPart { - partName: string; // "엘바", "하장바" - material: string; // "EGI 1.6T" - barcyInfo: string; // "16 I 75" -} -``` - -### 1.4 현재 WorkOrderItem 타입 (types.ts Lines 106-120) - -```typescript -// react/src/components/production/WorkOrders/types.ts -export interface WorkOrderItem { - id: string; - no: number; - status: ItemStatus; - productName: string; - floorCode: string; - specification: string; - width?: number; - height?: number; - quantity: number; - unit: string; - orderNodeId: number | null; - orderNodeName: string; - slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number }; - // ❌ bendingInfo 없음 → 추가 필요 -} -``` - -**transform 함수** (types.ts Lines 457-474): `slatInfo`는 `item.options.slat_info`에서 파싱하지만, `bending_info`는 아직 매핑하지 않음. - -### 1.5 PHP col → SAM 매핑 (완전 테이블) - -PHP에서 데이터는 `estimateSlatList` JSON의 각 아이템에 `col{N}` 키로 저장됨. - -| PHP 컬럼 | 의미 | SAM bending_info 필드 | 상태 | -|---------|------|----------------------|------| -| `col4` | 제품코드 (KQTS01, KTE01 등) | `productCode` | ⚠️ item_code로 별도 존재, bending_info에도 추가 | -| `col6` | 가이드레일 유형 | `common.type` | ✅ 존재 | -| `col7` | 마감유형 (SUS마감/EGI마감) | `finishMaterial` | ❌ 추가 필요 | -| `col24` | 유효 길이 (mm) | `common.lengthQuantities` | ✅ 존재 | -| `col32` | 연기차단재 W50 수량 - 2438mm | `smokeBarrier.w50[].quantity` | ❌ 추가 필요 | -| `col33` | 연기차단재 W50 수량 - 3000mm | 상동 | ❌ | -| `col34` | 연기차단재 W50 수량 - 3500mm | 상동 | ❌ | -| `col35` | 연기차단재 W50 수량 - 4000mm | 상동 | ❌ | -| `col36` | 연기차단재 W50 수량 - 4300mm | 상동 | ❌ | -| `col37` | 셔터박스 크기 (500*380 등) | `shutterBox[].size` | ❌ 추가 필요 | -| `col37_custom` | 셔터박스 커스텀 크기 | `shutterBox[].size` (custom일 때) | ❌ | -| `col37_railwidth` | 셔터박스 레일 폭 | `shutterBox[].railWidth` | ❌ | -| `col37_frontbottom` | 셔터박스 전면 하단 치수 | `shutterBox[].frontBottom` | ❌ | -| `col37_boxdirection` | 셔터박스 방향 (양면/밑면/후면) | `shutterBox[].direction` | ❌ | -| `col39` | 셔터박스 수량 - 1219mm | `shutterBox[].lengthData` | ❌ | -| `col40` | 셔터박스 수량 - 2438mm | 상동 | ❌ | -| `col41` | 셔터박스 수량 - 3000mm | 상동 | ❌ | -| `col42` | 셔터박스 수량 - 3500mm | 상동 | ❌ | -| `col43` | 셔터박스 수량 - 4000mm | 상동 | ❌ | -| `col44` | 셔터박스 수량 - 4150mm | 상동 | ❌ | -| `col45` | 상부덮개 수량 | `shutterBox[].coverQty` | ❌ | -| `col47` | 마구리 수량 | `shutterBox[].finCoverQty` | ❌ | -| `col48` | 연기차단재 W80 수량 | `smokeBarrier.w80Qty` | ❌ | -| `col50` | 하단마감재 3000mm 수량 | `bottomBar.length3000Qty` | ❌ | -| `col51` | 하단마감재 4000mm 수량 | `bottomBar.length4000Qty` | ❌ | - -### 1.6 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - options JSON 확장 (컬럼 추가 금지 - 멀티테넌시 원칙) │ -│ - PHP 원본과 동일한 계산 로직 (calWeight, 길이 버킷팅) │ -│ - 이미지는 정적 파일로 서빙 (셔터박스만 SVG/Canvas 대체) │ -│ - 카테고리별 독립 컴포넌트 (가이드레일/하단마감/셔터박스/연기차단재)│ -│ - 현재 WorkOrderItem에 bendingInfo 필드 추가 (slatInfo 패턴) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.7 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | React 컴포넌트 추가/수정, 타입 정의 추가, 이미지 복사 | 불필요 | -| ⚠️ 컨펌 필요 | bending_info JSON 스키마 변경, API 응답 구조 변경, 계산 로직 변경 | **필수** | -| 🔴 금지 | work_order_items 테이블 컬럼 추가, 기존 API 삭제 | 별도 협의 | - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 데이터 스키마 확장 (백엔드) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | bending_info JSON 스키마 확장 설계 | ✅ | BendingInfoExtended 타입 정의 완료 | -| 1.2 | WorkOrderService.php - options 매핑 확인/수정 | ✅ | Line 277에서 bending_info 정상 전달 확인 | -| 1.3 | API 응답에 확장된 bending_info 포함 확인 | ✅ | transform 함수에 bendingInfo 매핑 추가 완료 | - -### 2.2 Phase 2: 이미지 서빙 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 5130/img/ → api/public/images/bending/ 복사 | ✅ | guiderail(12) + bottombar(6) + part(1) + box source(3) = 22개 | -| 2.2 | 이미지 URL 빌더 유틸 (프론트) | ✅ | bending/utils.ts getBendingImageUrl() | - -### 2.3 Phase 3: 프론트엔드 타입 & 유틸리티 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | BendingWorkLog 타입 정의 확장 | ✅ | bending/types.ts + WorkOrderItem.bendingInfo 추가 | -| 3.2 | 무게 계산 유틸리티 (`calcWeight`) | ✅ | bending/utils.ts (calcWeight, getMaterialMapping 등 11개 함수) | -| 3.3 | WorkOrderItem transform에 bendingInfo 매핑 추가 | ✅ | item.options.bending_info → bendingInfo | - -### 2.4 Phase 4: 프론트엔드 컴포넌트 구현 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | GuideRailSection 컴포넌트 | ✅ | 벽면형/측면형 분류, 이미지+파트테이블 | -| 4.2 | BottomBarSection 컴포넌트 | ✅ | 하단마감재 + 별도마감재 | -| 4.3 | ShutterBoxSection 컴포넌트 | ✅ | 방향별(양면/밑면/후면) 구성요소, source 이미지 | -| 4.4 | SmokeBarrierSection 컴포넌트 | ✅ | W50 레일용 + W80 케이스용 | -| 4.5 | ProductionSummarySection 컴포넌트 | ✅ | SUS/EGI/합계 표시 | -| 4.6 | BendingWorkLogContent 통합 | ✅ | 헤더 + 신청업체/내용 + 제품정보 + 4섹션 + 합계 + 비고 | - -### 2.5 Phase 5: 검증 & 정리 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | PHP 원본과 출력 비교 검증 | ✅ | TypeScript 타입 체크 통과, 실 데이터 테스트 대기 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Phase 1: 데이터 스키마 확장 (백엔드) -├── 1.1 bending_info 확장 스키마 설계 -│ ├── guideRail: { wall, side } (길이 버킷팅 + 수량 + baseSize) -│ ├── bottomBar: { material, extraFinish, length3000Qty, length4000Qty } -│ ├── shutterBox: [{ size, direction, railWidth, frontBottom, coverQty, finCoverQty, lengthData }] -│ └── smokeBarrier: { w50: [...], w80Qty } -├── 1.2 WorkOrderService.php 매핑 확인 (Line 276) -└── 1.3 API 응답 검증 (curl로 직접 확인) - -Phase 2: 이미지 서빙 -├── 2.1 정적 이미지 복사 (guiderail 12jpg + bottombar 6jpg + part 1jpg = 19개) -└── 2.2 이미지 URL 헬퍼 유틸 - -Phase 3: 프론트엔드 타입 & 유틸 -├── 3.1 타입 정의 (bending/types.ts 신규 + WorkOrderItem.bendingInfo 추가) -├── 3.2 calcWeight + getMaterialMapping 유틸 (bending/utils.ts) -└── 3.3 transform 함수에 bendingInfo 매핑 추가 (slatInfo 패턴 동일) - -Phase 4: 컴포넌트 구현 -├── 4.1 GuideRailSection (가장 복잡 - 벽면/측면 분리, 파트 구성, 무게 계산) -├── 4.2 BottomBarSection (3000/4000 수량, 별도마감) -├── 4.3 ShutterBoxSection (방향별 구성요소, SVG 다이어그램) -├── 4.4 SmokeBarrierSection (W50 길이별 + W80 고정) -├── 4.5 ProductionSummarySection (SUS/EGI 누적 합계) -└── 4.6 BendingWorkLogContent 통합 (헤더+신청+4섹션+합계 조립) - -Phase 5: 검증 -└── 5.1 PHP 원본과 비교 (num=24822) -``` - ---- - -## 4. 상세 작업 내용 (PHP 로직 완전 인라인) - -### 4.1 Phase 1: bending_info 확장 스키마 - -#### 1.1 확장된 bending_info JSON 구조 - -```typescript -interface BendingInfoExtended { - // === 기존 필드 (유지) === - drawingUrl?: string; - common: BendingCommonInfo; // { kind, type, lengthQuantities } - detailParts: BendingDetailPart[]; // [{ partName, material, barcyInfo }] - - // === 신규 필드 === - productCode: string; // "KTE01", "KQTS01", "KSE01", "KSS01", "KWE01" - finishMaterial: string; // "EGI마감", "SUS마감" - - guideRail: { - wall: { - lengthData: { length: number; quantity: number }[]; - baseSize: string; // "135*80" 또는 "135*130" - } | null; - side: { - lengthData: { length: number; quantity: number }[]; - baseSize: string; // "135*130" - } | null; - }; - - bottomBar: { - material: string; // "EGI 1.55T" 또는 "SUS 1.5T" - extraFinish: string; // "SUS 1.2T" 또는 "없음" - length3000Qty: number; - length4000Qty: number; - }; - - shutterBox: { - size: string; // "500*380" 등 - direction: string; // "양면" | "밑면" | "후면" - railWidth: number; - frontBottom: number; - coverQty: number; // 상부덮개 수량 - finCoverQty: number; // 마구리 수량 - lengthData: { length: number; quantity: number }[]; - }[]; // 배열 (여러 사이즈 가능) - - smokeBarrier: { - w50: { length: number; quantity: number }[]; // 레일용 W50 - w80Qty: number; // 케이스용 W80 수량 - }; -} -``` - -#### 1.2 calWeight 함수 (PHP 원본 Lines 27-55 → TypeScript 구현) - -```typescript -// PHP 원본: -// $volume_cm3 = ($thickness * $calWidth * $calHeight) / 1000; -// $weight_kg = ($volume_cm3 * $density) / 1000; -// SUS → $SUS_total += $weight_kg, EGI → $EGI_total += $weight_kg - -function calcWeight( - material: string, // "SUS 1.2T", "EGI 1.55T", "EGI 0.8T" 등 - width: number, // mm - height: number // mm (= 길이) -): { weight: number; type: 'SUS' | 'EGI' } { - const thickness = parseFloat(material.match(/\d+(\.\d+)?/)?.[0] || '0'); - const isSUS = material.includes('SUS'); - const density = isSUS ? 7.93 : 7.85; // g/cm3 - const volume_cm3 = (thickness * width * height) / 1000; - const weight_kg = (volume_cm3 * density) / 1000; - return { - weight: Math.round(weight_kg * 100) / 100, - type: isSUS ? 'SUS' : 'EGI', - }; -} -``` - -#### 1.3 제품코드별 재질 매핑 (PHP Lines 330-366) - -```typescript -function getMaterialMapping(productCode: string, finishMaterial: string) { - // Group 1: KQTS01 - if (productCode === 'KQTS01') { - return { - guideRailFinish: 'SUS 1.2T', // ①②마감재 - bodyMaterial: 'EGI 1.55T', // ③본체, ④C형, ⑤D형 - guideRailExtraFinish: '', // 별도마감 없음 - bottomBarFinish: 'SUS 1.5T', // 하단마감재 - bottomBarExtraFinish: '없음', // 별도마감 없음 - }; - } - // Group 2: KTE01 - if (productCode === 'KTE01') { - const isSUS = finishMaterial === 'SUS마감'; - return { - guideRailFinish: 'EGI 1.55T', - bodyMaterial: 'EGI 1.55T', - guideRailExtraFinish: isSUS ? 'SUS 1.2T' : '', - bottomBarFinish: 'EGI 1.55T', - bottomBarExtraFinish: isSUS ? 'SUS 1.2T' : '없음', - }; - } - // 기타 제품코드 (KSE01, KSS01, KWE01 등) - KTE01 + EGI마감과 동일 패턴 - return { - guideRailFinish: 'EGI 1.55T', - bodyMaterial: 'EGI 1.55T', - guideRailExtraFinish: '', - bottomBarFinish: 'EGI 1.55T', - bottomBarExtraFinish: '없음', - }; -} -``` - -#### 1.4 가이드레일 길이 버킷팅 알고리즘 (PHP Lines 384-413) - -```typescript -// 고정 버킷: [2438, 3000, 3500, 4000, 4300] -// 각 아이템의 col24(유효길이)를 "첫 번째로 수용 가능한 버킷"에 넣음 (first-fit) - -const LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300]; - -function bucketGuideRails(items: Array<{ validLength: number; railType: string }>) { - const buckets = LENGTH_BUCKETS.map(len => ({ - length: len, wallSum: 0, sideSum: 0, - wallBaseSize: null as string | null, sideBaseSize: null as string | null, - })); - - for (const item of items) { - for (const bucket of buckets) { - if (item.validLength <= bucket.length) { - if (item.railType === '혼합형(130*75)(130*125)') { - bucket.wallSum += 1; - bucket.sideSum += 1; - bucket.wallBaseSize = '135*80'; - bucket.sideBaseSize = '135*130'; - } else if (item.railType === '벽면형(130*75)') { - bucket.wallSum += 2; - bucket.wallBaseSize = '135*130'; - } else if (item.railType === '측면형(130*125)') { - bucket.sideSum += 2; - bucket.sideBaseSize = '135*130'; - } - break; // first-fit: 한 버킷에 넣으면 다음 아이템으로 - } - } - } - return buckets.filter(b => b.wallSum > 0 || b.sideSum > 0); -} -``` - -#### 1.5 가이드레일 세부품명 + LOT 접두사 + 무게 계산 폭 - -**벽면형 [130*75] 파트 구성:** - -| 세부품명 | LOT 접두사 | 재질 | 무게 계산 폭 (mm) | -|---------|-----------|------|-----------------| -| ①②마감재 | XX | `guideRailFinish` | 412 | -| ③본체 | RT | `bodyMaterial` | 412 | -| ④C형 | RC | `bodyMaterial` | 412 | -| ⑤D형 | RD | `bodyMaterial` | 412 | -| ⑥별도마감 (SUS마감 시만) | RS | `guideRailExtraFinish` | 412 | -| 하부BASE | XX | EGI 1.55T (고정) | 135 (높이=80) | - -무게: `calcWeight(재질, 412, 길이)` / 하부BASE: `calcWeight('EGI 1.55T', 135, 80)` -baseSize는 `135*80` (혼합형) 또는 `135*130` (벽면형 단독) - -**측면형 [130*125] 파트 구성:** - -| 세부품명 | LOT 접두사 | 재질 | 무게 계산 폭 (mm) | -|---------|-----------|------|-----------------| -| ①②마감재 | SS | `guideRailFinish` | 462 | -| ③본체 | ST | `bodyMaterial` | 462 | -| ④C형 | SC | `bodyMaterial` | 462 | -| ⑤D형 | SD | `bodyMaterial` | 462 | -| 하부BASE | XX | EGI 1.55T (고정) | 135 (높이=130) | - -무게: `calcWeight(재질, 462, 길이)` / 하부BASE: `calcWeight('EGI 1.55T', 135, 130)` - -#### 1.6 하단마감재 세부품명 - -| 세부품명 | LOT 접두사 | 재질 | 무게 계산 폭 (mm) | 길이 옵션 | -|---------|-----------|------|-----------------|---------| -| ①하단마감재 | TE(EGI)/TS(SUS) | `bottomBarFinish` | 184 | 3000, 4000 | -| ④별도마감재 | TE/TS | `bottomBarExtraFinish` | 238 | 3000, 4000 | - -별도마감재는 `bottomBarExtraFinish !== '없음'`일 때만 표시. - -#### 1.7 셔터박스 구성요소 (방향별 - PHP Lines 819-1190) - -**셔터박스 재질**: 항상 `EGI 1.55T` (= `$BoxFinish`) - -**표준 사이즈 (500*380) 구성:** - -| 구성요소 | LOT 접두사 | 치수 공식 | -|---------|-----------|----------| -| ①전면부 | CF | `boxHeight + 122` | -| ②린텔부 | CL | `boxWidth - 330` | -| ③⑤점검구 | CP | `boxWidth - 200` | -| ④후면코너부 | CB | `170` (고정) | - -**비표준 사이즈 - 양면 구성:** - -| 구성요소 | LOT 접두사 | 치수 공식 | -|---------|-----------|----------| -| ①전면부 | XX | `boxHeight + 122` | -| ②린텔부 | CL | `boxWidth - 330` | -| ③점검구 | XX | `boxWidth - 200` | -| ④후면코너부 | CB | `170` (고정) | -| ⑤점검구 | XX | `boxHeight - 100` | -| ⑥상부덮개 | XX | `1219 * (boxWidth - 111)` | -| ⑦측면부(마구리) | XX | `(boxWidthFin+5) * (boxHeightFin+5)` 표시 | - -**비표준 사이즈 - 밑면 구성:** - -| 구성요소 | LOT 접두사 | 치수 공식 | -|---------|-----------|----------| -| ①전면부 | XX | `boxHeight + 122` | -| ②린텔부 | CL | `boxWidth - 330` | -| ③점검구 | XX | `boxWidth - 200` | -| ④후면부 | CB | `boxHeight + 85*2` | -| ⑤상부덮개 | XX | `1219 * (boxWidth - 111)` | -| ⑥측면부(마구리) | XX | `(boxWidthFin+5) * (boxHeightFin+5)` 표시 | - -**비표준 사이즈 - 후면 구성:** - -| 구성요소 | LOT 접두사 | 치수 공식 | -|---------|-----------|----------| -| ①전면부 | XX | `boxHeight + 122` | -| ②린텔부 | CL | `boxWidth + 85*2` | -| ③점검구 | XX | `boxHeight - 200` | -| ④후면코너부 | CB | `boxHeight + 85*2` | -| ⑤상부덮개 | XX | `1219 * (boxWidth - 111)` | -| ⑥측면부(마구리) | XX | `(boxWidthFin+5) * (boxHeightFin+5)` 표시 | - -**공통 사항:** -- 상부덮개 무게: `calcWeight('EGI 1.55T', boxWidth - 111, 1219)` × coverQty -- 마구리 무게: `calcWeight('EGI 1.55T', boxWidthFin, boxHeightFin)` × finCoverQty -- 셔터박스 길이 버킷: [1219, 2438, 3000, 3500, 4000, 4150] - -#### 1.8 연기차단재 (PHP Lines 1195-1321) - -| 파트 | 재질 | 무게 계산 폭 (mm) | 길이 버킷 | -|-----|------|-----------------|---------| -| 레일용 [W50] | EGI 0.8T | 26 | 2438, 3000, 3500, 4000, 4300 | -| 케이스용 [W80] | EGI 0.8T | 26 | 3000 (고정) | - -LOT 접두사: 모두 `GI` -LOT 코드 생성: `GI-{getSLengthCode(length, category)}` - -#### 1.9 getSLengthCode 함수 (PHP Lines 56-100) - -```typescript -function getSLengthCode(length: number, category: string): string | null { - if (category === '연기차단재50') { - return length === 3000 ? '53' : length === 4000 ? '54' : null; - } - if (category === '연기차단재80') { - return length === 3000 ? '83' : length === 4000 ? '84' : null; - } - // category === '기타' (일반) - const map: Record = { - 1219: '12', 2438: '24', 3000: '30', 3500: '35', - 4000: '40', 4150: '41', 4200: '42', 4300: '43', - }; - return map[length] || null; -} -``` - ---- - -### 4.2 Phase 2: 이미지 서빙 - -#### 복사 대상 (총 19개 JPG 파일) - -**가이드레일 (12개):** -``` -5130/img/guiderail/ → api/public/images/bending/guiderail/ -├── guiderail_KQTS01_wall_130x75.jpg -├── guiderail_KQTS01_side_130x125.jpg -├── guiderail_KTE01_wall_130x75.jpg -├── guiderail_KTE01_side_130x125.jpg -├── guiderail_KSE01_wall_120x70.jpg -├── guiderail_KSE01_side_120x120.jpg -├── guiderail_KSS01_wall_120x70.jpg -├── guiderail_KSS01_side_120x120.jpg -├── guiderail_KSS02_wall_120x70.jpg -├── guiderail_KSS02_side_120x120.jpg -├── guiderail_KWE01_wall_120x70.jpg -└── guiderail_KWE01_side_120x120.jpg -``` - -**하단마감재 (6개):** -``` -5130/img/bottombar/ → api/public/images/bending/bottombar/ -├── bottombar_KQTS01.jpg -├── bottombar_KTE01.jpg -├── bottombar_KSE01.jpg -├── bottombar_KSS01.jpg -├── bottombar_KSS02.jpg -└── bottombar_KWE01.jpg -``` - -**연기차단재 (1개):** -``` -5130/img/part/ → api/public/images/bending/part/ -└── smokeban.jpg -``` - -**셔터박스 이미지**: PHP에서 GD 라이브러리로 동적 생성 → React에서는 SVG/Canvas로 대체 -- 소스 이미지: `5130/img/box/source/box_{both|bottom|rear}.jpg` -- 치수 텍스트를 오버레이하는 구조 → SVG 컴포넌트로 재구현 - -#### 이미지 URL 패턴 - -```typescript -const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://api.sam.kr'; - -function getBendingImageUrl(category: string, productCode: string, type?: string): string { - switch (category) { - case 'guiderail': { - // PHP: guiderail_{prodCode}_{wall|side}_{size}.jpg - // KQTS01, KTE01 → 130x75 (wall) / 130x125 (side) - // KSE01, KSS01, KSS02, KWE01 → 120x70 (wall) / 120x120 (side) - const size = ['KQTS01', 'KTE01'].includes(productCode) - ? (type === 'wall' ? '130x75' : '130x125') - : (type === 'wall' ? '120x70' : '120x120'); - return `${API_BASE}/images/bending/guiderail/guiderail_${productCode}_${type}_${size}.jpg`; - } - case 'bottombar': - return `${API_BASE}/images/bending/bottombar/bottombar_${productCode}.jpg`; - case 'smokebarrier': - return `${API_BASE}/images/bending/part/smokeban.jpg`; - default: - return ''; - } -} -``` - ---- - -### 4.3 Phase 3: 프론트엔드 타입 & 유틸리티 - -#### 파일 구조 - -``` -react/src/components/production/WorkOrders/documents/ -├── BendingWorkLogContent.tsx ← 기존 파일 (재작성) -├── bending/ -│ ├── types.ts ← 절곡 작업일지 전용 타입 -│ ├── utils.ts ← calcWeight, getMaterialMapping, getBendingImageUrl, getSLengthCode -│ ├── GuideRailSection.tsx ← 가이드레일 섹션 -│ ├── BottomBarSection.tsx ← 하단마감재 섹션 -│ ├── ShutterBoxSection.tsx ← 셔터박스 섹션 -│ ├── SmokeBarrierSection.tsx ← 연기차단재 섹션 -│ └── ProductionSummarySection.tsx ← 생산량 합계 -``` - -#### WorkOrderItem.bendingInfo 추가 (slatInfo 패턴 참고) - -```typescript -// types.ts에 추가 -export interface WorkOrderItem { - // ... 기존 필드 ... - slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number }; - bendingInfo?: BendingInfoExtended; // ← 신규 추가 -} - -// transform 함수에 추가 (slatInfo 패턴 동일) -bendingInfo: item.options?.bending_info - ? (item.options.bending_info as BendingInfoExtended) - : undefined, -``` - ---- - -### 4.4 Phase 4: 컴포넌트 구현 상세 - -#### 4.1 GuideRailSection 레이아웃 - -``` -┌──────────────────────────────────────────────────────────────────────────────┐ -│ 1.1 벽면형 [130*75] │ -│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ -│ │ [guiderail 이미지] │ │ 세부품명 │ 재질 │ 길이 │ 수량 │ LOT NO │ 무게 ││ -│ │ │ │──────────┼──────────┼──────┼──────┼────────┼──────││ -│ │ │ │ ①②마감재 │ SUS 1.2T │ 4000 │ 6 │ ____ │ XX.X ││ -│ │ 입고&생산 LOT NO: │ │ ③본체 │ EGI 1.55T│ 4000 │ 6 │ ____ │ XX.X ││ -│ │ ___________ │ │ ④C형 │ EGI 1.55T│ 4000 │ 6 │ ____ │ XX.X ││ -│ └─────────────────────┘ │ ⑤D형 │ EGI 1.55T│ 4000 │ 6 │ ____ │ XX.X ││ -│ │ ⑥별도마감 │ SUS 1.2T │ 4000 │ 6 │ ____ │ XX.X ││ -│ │ 하부BASE │ EGI 1.55T│135*80│ N │ ____ │ XX.X ││ -│ └──────────────────────────────────────────────────┘│ -├──────────────────────────────────────────────────────────────────────────────┤ -│ 1.2 측면형 [130*125] (동일 구조, 폭=462mm, baseSize=135*130) │ -└──────────────────────────────────────────────────────────────────────────────┘ -``` - -각 길이 버킷(2438/3000/3500/4000/4300)별로 수량이 있는 행만 표시. -각 파트의 무게는 `calcWeight(재질, 폭, 길이)` × 수량으로 계산. - -#### 4.2 BottomBarSection 레이아웃 - -``` -┌──────────────────────────────────────────────────────────────────────────────┐ -│ 2. 하단마감재 │ -│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ -│ │ [bottombar 이미지] │ │ 세부품명 │ 재질 │ 길이 │ 수량 │ LOT │ 무게 ││ -│ │ │ │─────────────┼──────────┼──────┼──────┼──────┼──────││ -│ │ │ │ ①하단마감재 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ -│ └─────────────────────┘ │ ①하단마감재 │ EGI 1.55T│ 4000 │ N │ ____ │ XX.X ││ -│ │ ④별도마감재 │ SUS 1.2T │ 3000 │ N │ ____ │ XX.X ││ -│ │ ④별도마감재 │ SUS 1.2T │ 4000 │ N │ ____ │ XX.X ││ -│ └──────────────────────────────────────────────────┘│ -└──────────────────────────────────────────────────────────────────────────────┘ -``` - -#### 4.3 ShutterBoxSection 레이아웃 - -``` -┌──────────────────────────────────────────────────────────────────────────────┐ -│ 3. 셔터박스 [500*380] 양면 │ -│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ -│ │ [SVG 다이어그램] │ │ 구성요소 │ 재질 │ 길이 │ 수량 │ LOT │ 무게 ││ -│ │ (치수 텍스트 포함) │ │────────────┼──────────┼──────┼──────┼──────┼──────││ -│ │ boxHeight+122 │ │ ①전면부 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ -│ │ boxWidth-330 │ │ ②린텔부 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ -│ │ boxWidth-200 │ │ ③점검구 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ -│ └─────────────────────┘ │ ④후면코너부 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ -│ │ ⑤점검구 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ -│ │ ⑥상부덮개 │ EGI 1.55T│ 1219 │ N │ ____ │ XX.X ││ -│ │ ⑦마구리 │ EGI 1.55T│ - │ N │ ____ │ XX.X ││ -│ └──────────────────────────────────────────────────┘│ -└──────────────────────────────────────────────────────────────────────────────┘ -``` - -#### 4.4 SmokeBarrierSection 레이아웃 - -``` -┌──────────────────────────────────────────────────────────────────────────────┐ -│ 4. 연기차단재 │ -│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ -│ │ [smokeban.jpg] │ │ 파트 │ 재질 │ 길이 │ 수량 │ LOT │ 무게 ││ -│ │ │ │───────────────┼─────────┼──────┼──────┼──────┼──────││ -│ └─────────────────────┘ │ 레일용 [W50] │EGI 0.8T │ 3000 │ N │ ____ │ XX.X ││ -│ │ 레일용 [W50] │EGI 0.8T │ 4000 │ N │ ____ │ XX.X ││ -│ │ 케이스용 [W80]│EGI 0.8T │ 3000 │ N │ ____ │ XX.X ││ -│ └──────────────────────────────────────────────────┘│ -└──────────────────────────────────────────────────────────────────────────────┘ -``` - -#### 4.5 ProductionSummarySection 레이아웃 - -``` -┌──────────────────────────────────────────────────────┐ -│ 생산량 합계(KG) │ SUS │ EGI │ 합계 │ -│ │ XX.XX kg │ XX.XX kg │ XX.XX kg │ -└──────────────────────────────────────────────────────┘ -``` - -SUS_total과 EGI_total은 4개 섹션의 모든 calcWeight 호출에서 누적. - ---- - -## 5. 모든 하드코딩 상수 (PHP 원본 기준) - -| 상수 | 값 | 용도 | -|------|-----|------| -| SUS 밀도 | 7.93 g/cm3 | calWeight | -| EGI 밀도 | 7.85 g/cm3 | calWeight | -| 벽면형 파트 폭 | 412 mm | 가이드레일 무게 계산 | -| 측면형 파트 폭 | 462 mm | 가이드레일 무게 계산 | -| 벽면형 하부BASE | 135 × 80 mm | 가이드레일 | -| 측면형 하부BASE | 135 × 130 mm | 가이드레일 | -| 하단마감재 폭 | 184 mm | 하단마감재 무게 | -| 별도마감재 폭 | 238 mm | 별도마감재 무게 | -| 연기차단재 폭 (W50/W80) | 26 mm | 연기차단재 무게 | -| 상부덮개 길이 | 1219 mm (고정) | 셔터박스 | -| 상부덮개 폭 | boxWidth - 111 | 셔터박스 | -| 전면부 치수 | boxHeight + 122 | 셔터박스 | -| 린텔부 치수 | boxWidth - 330 | 셔터박스 | -| 점검구 치수 | boxWidth - 200 | 셔터박스 | -| 후면코너부 치수 (표준/양면) | 170 | 셔터박스 | -| 가이드레일 길이 버킷 | [2438, 3000, 3500, 4000, 4300] | 길이 분류 | -| 셔터박스 길이 버킷 | [1219, 2438, 3000, 3500, 4000, 4150] | 길이 분류 | -| 하단마감재 길이 | [3000, 4000] | 길이 분류 | -| 연기차단재 W50 길이 버킷 | [2438, 3000, 3500, 4000, 4300] | 길이 분류 | -| 케이스용 W80 길이 | 3000 (고정) | 연기차단재 | -| 마구리 표시 크기 보정 | +5 mm (양쪽) | 셔터박스 | - ---- - -## 6. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | bending_info 스키마 확장 | guideRail, bottomBar, shutterBox, smokeBarrier 필드 추가 | api options JSON | ⚠️ 컨펌 필요 | -| 2 | 이미지 파일 복사 | 5130/img/ → api/public/images/bending/ (19개 JPG) | api 서버 | ⚠️ 컨펌 필요 | -| 3 | 셔터박스 이미지 처리 | SVG 컴포넌트로 클라이언트 렌더링 (PHP GD 대체) | react | ⚠️ 컨펌 필요 | - ---- - -## 7. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-19 | - | 문서 초안 작성 | - | - | -| 2026-02-19 | - | 자기완결성 보완 (PHP 로직 완전 인라인, 이미지 목록, 상수 테이블, 데이터 흐름) | - | - | - ---- - -## 8. 참고 문서 & 핵심 파일 경로 - -### 수정 대상 파일 - -| 파일 | 역할 | 작업 | -|------|------|------| -| `react/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx` | 메인 컴포넌트 | **재작성** | -| `react/src/components/production/WorkOrders/types.ts` | WorkOrderItem 타입 | `bendingInfo` 필드 추가 + transform 함수 수정 | -| `react/src/components/production/WorkOrders/documents/bending/` | 신규 디렉토리 | **6개 파일 생성** (types, utils, 4개 섹션 + 합계) | - -### 참조 파일 (읽기 전용) - -| 파일 | 역할 | -|------|------| -| `5130/output/viewBendingWork_slat.php` | PHP 원본 (~1400줄) | -| `react/src/components/production/WorkerScreen/types.ts` | BendingInfo 인터페이스 (Lines 91-107) | -| `react/src/components/production/WorkerScreen/WorkLogModal.tsx` | 작업일지 모달 - BendingWorkLogContent 호출 (Lines 207-213) | -| `api/app/Services/WorkOrderService.php` | options에 bending_info 저장 (Line 276) | -| `react/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx` | 슬랫 작업일지 참고 (유사 패턴) | -| `react/src/components/production/WorkOrders/documents/index.ts` | export 파일 (BendingWorkLogContent 등록됨) | - -### 이미지 원본 경로 - -| 소스 | 대상 | 파일 수 | -|------|------|---------| -| `5130/img/guiderail/*.jpg` | `api/public/images/bending/guiderail/` | 12개 | -| `5130/img/bottombar/*.jpg` | `api/public/images/bending/bottombar/` | 6개 | -| `5130/img/part/smokeban.jpg` | `api/public/images/bending/part/` | 1개 | - -**참고**: `api/public/images/bending/` 디렉토리는 아직 존재하지 않음 → 생성 필요. - ---- - -## 9. 세션 관리 - -### Serena 메모리 ID -- `bending-worklog-state`: 진행 상태 -- `bending-worklog-snapshot`: 스냅샷 -- `bending-worklog-active-symbols`: 수정 중 파일 - ---- - -## 10. 검증 결과 - -### 10.1 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 4개 카테고리 섹션이 PHP와 동일한 레이아웃으로 렌더링 | ⏳ | | -| SUS/EGI 무게 계산이 PHP calWeight와 동일한 결과 | ⏳ | calcWeight(SUS 1.2T, 412, 4000) 등으로 검증 | -| 생산량 합계(KG)가 SUS/EGI 별도 + 합산으로 표시 | ⏳ | | -| 가이드레일/하단마감재/연기차단재 이미지가 정상 표시 | ⏳ | | -| 셔터박스 SVG 다이어그램에 치수 텍스트 표시 | ⏳ | | -| 제품코드/마감유형에 따라 세부품명 동적 변경 | ⏳ | KQTS01 vs KTE01+SUS vs KTE01+EGI | -| 가이드레일 길이 버킷팅이 PHP first-fit과 동일 | ⏳ | | -| 빌드 에러 없음 | ⏳ | | - -### 10.2 검증 방법 -- PHP 원본: `5130/output/viewBendingWork_slat.php?num=24822` 출력과 비교 -- 무게 계산 단위 테스트: `calcWeight('SUS 1.2T', 412, 4000)` → 예상값과 비교 - - `thickness=1.2, width=412, height=4000, density=7.93` - - `volume_cm3 = (1.2 * 412 * 4000) / 1000 = 1977.6` - - `weight_kg = (1977.6 * 7.93) / 1000 = 15.68` - ---- - -## 11. 자기완결성 점검 결과 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | PHP 동일 구조 재구현 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.1 (8개 기준) | -| 3 | 작업 범위가 구체적인가? | ✅ | 5 Phase, 15개 작업 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성, 데이터 흐름 섹션 1.2 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 (수정 대상 + 참조 파일 분리) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | PHP 로직 완전 인라인 (섹션 4) | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | PHP num=24822 비교 + 단위 테스트 예시 | -| 8 | 모호한 표현이 없는가? | ✅ | 모든 상수/공식/조건 구체적으로 명시 | - -### 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------:| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 데이터가 어디서 어떻게 오는가? | ✅ | 1.2 데이터 흐름 | -| Q3. 어디서부터 시작해야 하는가? | ✅ | 3.1 단계별 절차 | -| Q4. 어떤 파일을 수정/생성해야 하는가? | ✅ | 8 핵심 파일 경로 | -| Q5. PHP 원본의 계산 로직은? | ✅ | 4.1 (calWeight, 버킷팅, 재질매핑 전부 인라인) | -| Q6. 이미지 파일은 어디에 있는가? | ✅ | 4.2 (19개 파일 목록 + URL 패턴) | -| Q7. 모든 하드코딩 상수 값은? | ✅ | 섹션 5 (완전 테이블) | -| Q8. 작업 완료 확인 방법은? | ✅ | 10.1 성공 기준 + 10.2 검증 방법 | -| Q9. 막혔을 때 참고 문서는? | ✅ | 8 참고 문서 | - -**결과**: 9/9 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/bidding-api-implementation-plan.md b/plans/archive/bidding-api-implementation-plan.md deleted file mode 100644 index e0c3135..0000000 --- a/plans/archive/bidding-api-implementation-plan.md +++ /dev/null @@ -1,817 +0,0 @@ -# 입찰관리(Bidding) API 구현 계획 - -> **작성일**: 2026-01-19 -> **목적**: 견적 → 입찰 전환 기능 구현 및 테스트용 더미데이터 생성 -> **기준 문서**: React 목업 타입 (`react/src/components/business/construction/bidding/types.ts`) -> **상태**: ✅ 완료 (Serena ID: bidding-api-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4.3 - Pint 코드 포맷팅 및 Swagger 재생성 | -| **다음 작업** | 사용자 수동 실행 (마이그레이션, 시더) | -| **진행률** | 12/12 (100%) | -| **마지막 업데이트** | 2026-01-19 | - ---- - -## 1. 개요 - -### 1.1 배경 - -**업무 흐름:** -``` -현장설명회 → 견적관리 → [견적완료] → 입찰관리 → 계약관리 → 기성/정산 - ↑ - 전환 기능 필요 -``` - -현재 React 프론트엔드의 입찰관리(`/construction/project/bidding`)는 **목업 데이터**를 사용 중입니다. -견적(Quote) API는 이미 구현되어 있으므로, 입찰(Bidding) API를 새로 구현하고 견적 → 입찰 전환 기능을 추가해야 합니다. - -**현재 상태:** -| 구분 | 견적(Estimate/Quote) | 입찰(Bidding) | -|------|---------------------|---------------| -| API Model | ✅ `Estimate.php` | ❌ 없음 | -| API Migration | ✅ `estimates` 테이블 | ❌ 없음 | -| API Endpoint | ✅ `/api/v1/quotes` | ❌ 없음 | -| React | ✅ API 연동 완료 | ❌ 목업 상태 | - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. SAM API Rules 엄격 준수 (Service-First, FormRequest) │ -│ 2. Multi-tenancy 필수 (BelongsToTenant) │ -│ 3. React 목업 타입과 100% 호환 │ -│ 4. 견적 데이터 참조 (복사가 아닌 FK 연결) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 새 테이블 생성, 새 API 추가, Seeder 작성 | 불필요 | -| ⚠️ 컨펌 필요 | 기존 quotes 테이블 수정, 비즈니스 로직 변경 | **필수** | -| 🔴 금지 | 기존 API 삭제, 파괴적 변경 | 별도 협의 | - -### 1.4 준수 규칙 - -- `api/CLAUDE.md` - SAM API Development Rules -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/guides/swagger-guide.md` - Swagger 문서화 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: Database & Model (Day 1) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | `biddings` 테이블 마이그레이션 생성 | ✅ | `2026_01_19_100000_create_biddings_table.php` | -| 1.2 | `Bidding` Model 생성 | ✅ | BelongsToTenant, SoftDeletes | -| 1.3 | 더미데이터 Seeder 생성 | ✅ | 10건 테스트 데이터 | - -### 2.2 Phase 2: API Implementation (Day 2) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | BiddingService 생성 | ✅ | CRUD + 통계 | -| 2.2 | BiddingController 생성 | ✅ | | -| 2.3 | FormRequest 생성 | ✅ | Filter, Update, Status, BulkDelete | -| 2.4 | Routes 등록 | ✅ | `/api/v1/biddings` | - -### 2.3 Phase 3: 견적 → 입찰 전환 (Day 2-3) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | QuoteService에 `convertToBidding()` 추가 | ✅ | 기존 코드에 메서드 추가 | -| 3.2 | 전환 API 엔드포인트 추가 | ✅ | `POST /quotes/{id}/convert-to-bidding` | - -### 2.4 Phase 4: Swagger & 검증 (Day 3) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | Swagger 문서 작성 | ✅ | `BiddingApi.php` | -| 4.2 | i18n 메시지 추가 | ✅ | message.php, error.php | -| 4.3 | Pint 코드 포맷팅 | ✅ | 9 style issues fixed | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: Database Schema -├── biddings 테이블 마이그레이션 작성 -├── 마이그레이션 실행 -└── Seeder로 더미데이터 생성 - -Step 2: Model & Service -├── Bidding Model 생성 (BelongsToTenant, SoftDeletes) -├── BiddingService 생성 (CRUD, stats, filter) -└── BiddingController 생성 - -Step 3: API Routes -├── routes/api.php에 biddings 라우트 추가 -├── FormRequest 클래스 생성 -└── API 테스트 - -Step 4: 견적 → 입찰 전환 -├── QuoteService에 convertToBidding() 추가 -├── 전환 API 엔드포인트 추가 -└── 전환 테스트 - -Step 5: Documentation -├── Swagger 문서 작성 -├── API 문서 검증 -└── Pint 실행 -``` - -### 3.2 데이터베이스 스키마 - -```sql --- biddings 테이블 -CREATE TABLE biddings ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', - - -- 기본 정보 - bidding_code VARCHAR(50) NOT NULL COMMENT '입찰번호', - quote_id BIGINT UNSIGNED NULL COMMENT '연결된 견적 ID (quotes.id)', - - -- 거래처/현장 - client_id BIGINT UNSIGNED NULL COMMENT '거래처 ID', - client_name VARCHAR(100) NULL COMMENT '거래처명 (스냅샷)', - project_name VARCHAR(200) NULL COMMENT '현장명', - - -- 입찰 정보 - bidding_date DATE NULL COMMENT '입찰일', - bid_date DATE NULL COMMENT '입찰일 (레거시 호환)', - submission_date DATE NULL COMMENT '투찰일', - confirm_date DATE NULL COMMENT '확정일', - total_count INT DEFAULT 0 COMMENT '총 개소', - bidding_amount DECIMAL(15,2) DEFAULT 0 COMMENT '입찰금액', - - -- 상태 - status VARCHAR(20) DEFAULT 'waiting' COMMENT '상태 (waiting/submitted/failed/invalid/awarded/hold)', - - -- 입찰자 - bidder_id BIGINT UNSIGNED NULL COMMENT '입찰자 ID', - bidder_name VARCHAR(50) NULL COMMENT '입찰자명 (스냅샷)', - - -- 공사기간 - construction_start_date DATE NULL COMMENT '공사 시작일', - construction_end_date DATE NULL COMMENT '공사 종료일', - vat_type VARCHAR(20) DEFAULT 'excluded' COMMENT '부가세 (included/excluded)', - - -- 비고 - remarks TEXT NULL COMMENT '비고', - - -- 견적 데이터 스냅샷 (JSON) - expense_items JSON NULL COMMENT '공과 항목 스냅샷', - estimate_detail_items JSON NULL COMMENT '견적 상세 항목 스냅샷', - - -- 감사 - created_by BIGINT UNSIGNED NULL COMMENT '생성자', - updated_by BIGINT UNSIGNED NULL COMMENT '수정자', - deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자', - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - deleted_at TIMESTAMP NULL, - - -- 인덱스 - INDEX idx_tenant_id (tenant_id), - INDEX idx_status (status), - INDEX idx_bidding_date (bidding_date), - INDEX idx_quote_id (quote_id), - UNIQUE INDEX idx_bidding_code (tenant_id, bidding_code) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 3.3 API 엔드포인트 설계 - -| Method | Path | 설명 | -|--------|------|------| -| GET | `/api/v1/biddings` | 목록 조회 (필터, 페이지네이션) | -| GET | `/api/v1/biddings/stats` | 통계 조회 | -| GET | `/api/v1/biddings/{id}` | 단건 조회 | -| PUT | `/api/v1/biddings/{id}` | 수정 | -| DELETE | `/api/v1/biddings/{id}` | 삭제 | -| DELETE | `/api/v1/biddings/bulk` | 일괄 삭제 | -| POST | `/api/v1/quotes/{id}/convert-to-bidding` | 견적 → 입찰 전환 | - -**참고**: 입찰은 별도 등록 없음 (견적완료 시 자동 전환) - -### 3.4 타입 매핑 (React → API) - -| React (camelCase) | API (snake_case) | DB Column | -|-------------------|------------------|-----------| -| `id` | `id` | `id` | -| `biddingCode` | `bidding_code` | `bidding_code` | -| `partnerId` | `client_id` | `client_id` | -| `partnerName` | `client_name` | `client_name` | -| `projectName` | `project_name` | `project_name` | -| `biddingDate` | `bidding_date` | `bidding_date` | -| `totalCount` | `total_count` | `total_count` | -| `biddingAmount` | `bidding_amount` | `bidding_amount` | -| `bidDate` | `bid_date` | `bid_date` | -| `submissionDate` | `submission_date` | `submission_date` | -| `confirmDate` | `confirm_date` | `confirm_date` | -| `status` | `status` | `status` | -| `bidderId` | `bidder_id` | `bidder_id` | -| `bidderName` | `bidder_name` | `bidder_name` | -| `remarks` | `remarks` | `remarks` | -| `estimateId` | `quote_id` | `quote_id` | -| `estimateCode` | `quote_number` | (join) | - -### 3.5 상태값 매핑 - -| 값 | 한글 | 설명 | -|----|------|------| -| `waiting` | 입찰대기 | 견적 전환 후 초기 상태 | -| `submitted` | 투찰 | 투찰서 제출 완료 | -| `failed` | 탈락 | 입찰 실패 | -| `invalid` | 유찰 | 입찰 무효 | -| `awarded` | 낙찰 | 입찰 성공 | -| `hold` | 보류 | 검토 대기 | - -### 3.6 기존 quotes 테이블 스키마 (연결용) - -> `biddings.quote_id` → `quotes.id` FK 연결 - -```sql --- quotes 테이블 핵심 컬럼 (api/database/migrations/2025_12_04_164542_create_quotes_table.php) -quotes ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, - quote_type ENUM('manufacturing', 'construction'), -- 'construction' 필터 - quote_number VARCHAR(50), -- 견적번호 (예: KD-SC-251204-01) - registration_date DATE, - client_id BIGINT, -- 거래처 ID - client_name VARCHAR(100), -- 거래처명 - site_name VARCHAR(200), -- 현장명 - total_amount DECIMAL(15,2), -- 최종 금액 - status ENUM('pending','draft','sent','approved','rejected','finalized','converted'), - site_briefing_id BIGINT, -- 현장설명회 연결 - options JSON, -- { summary_items, expense_items, detail_items, price_adjustment_data } - ... -) -``` - -**Quote 상태 상수** (api/app/Models/Quote/Quote.php): -- `pending` → 견적대기 (현장설명회에서 자동생성) -- `finalized` → 확정 (입찰 전환 가능) -- `converted` → 전환완료 - -### 3.7 API 응답 형식 (JSON) - -#### 목록 조회 응답 (GET /biddings) -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "data": [ - { - "id": 1, - "bidding_code": "BID-2025-001", - "client_id": 1, - "client_name": "이사대표", - "project_name": "광장 아파트", - "bidding_date": "2025-01-25", - "total_count": 15, - "bidding_amount": 71000000, - "bid_date": "2025-01-20", - "submission_date": "2025-01-22", - "confirm_date": "2025-01-25", - "status": "awarded", - "bidder_id": 1, - "bidder_name": "홍길동", - "remarks": "", - "quote_id": 1, - "quote_number": "EST-2025-001", - "created_at": "2025-01-01T00:00:00.000000Z" - } - ], - "current_page": 1, - "per_page": 20, - "total": 10, - "last_page": 1 - } -} -``` - -#### 통계 응답 (GET /biddings/stats) -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "total": 10, - "waiting": 3, - "awarded": 3 - } -} -``` - -#### 단건 조회 응답 (GET /biddings/{id}) -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "id": 1, - "bidding_code": "BID-2025-001", - "client_id": 1, - "client_name": "이사대표", - "project_name": "광장 아파트", - "bidding_date": "2025-01-25", - "total_count": 15, - "bidding_amount": 71000000, - "status": "awarded", - "construction_start_date": "2025-02-01", - "construction_end_date": "2025-04-30", - "vat_type": "excluded", - "expense_items": [ - { "id": "1", "name": "설계비", "amount": 5000000 }, - { "id": "2", "name": "운반비", "amount": 3000000 } - ], - "estimate_detail_items": [ - { "id": "1", "no": 1, "name": "방화문", "material": "SUS304", "width": 1000, "height": 2100, "quantity": 10, ... } - ], - "quote": { - "id": 1, - "quote_number": "EST-2025-001" - } - } -} -``` - -### 3.8 convertToBidding() 상세 로직 - -```php -/** - * 견적 → 입찰 전환 - * - * @param int $quoteId 견적 ID - * @return Bidding 생성된 입찰 - */ -public function convertToBidding(int $quoteId): Bidding -{ - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 1. 견적 조회 (quote_type=construction, status=finalized) - $quote = Quote::where('tenant_id', $tenantId) - ->where('id', $quoteId) - ->where('quote_type', 'construction') - ->where('status', 'finalized') - ->firstOrFail(); - - // 2. 이미 입찰이 존재하는지 확인 - $existingBidding = Bidding::where('quote_id', $quoteId)->first(); - if ($existingBidding) { - throw new BadRequestHttpException(__('error.bidding_already_exists')); - } - - // 3. 입찰 데이터 생성 - $bidding = Bidding::create([ - 'tenant_id' => $tenantId, - 'bidding_code' => $this->generateBiddingCode($tenantId), - 'quote_id' => $quote->id, - - // 거래처/현장 정보 복사 - 'client_id' => $quote->client_id, - 'client_name' => $quote->client_name, - 'project_name' => $quote->site_name, - - // 금액 정보 - 'bidding_amount' => $quote->total_amount, - 'total_count' => $quote->items->count(), - - // 날짜 - 'bidding_date' => now()->toDateString(), - - // 상태 - 'status' => 'waiting', - - // 현장설명회에서 공사기간 가져오기 - 'construction_start_date' => $quote->siteBriefing?->construction_start_date, - 'construction_end_date' => $quote->siteBriefing?->construction_end_date, - 'vat_type' => $quote->siteBriefing?->vat_type ?? 'excluded', - - // 견적 옵션 데이터 스냅샷 - 'expense_items' => $quote->options['expense_items'] ?? [], - 'estimate_detail_items' => $quote->options['detail_items'] ?? [], - - 'created_by' => $userId, - ]); - - // 4. 견적 상태 업데이트 (선택적) - // $quote->update(['status' => 'converted']); - - return $bidding; -} - -/** - * 입찰번호 자동 생성 (BID-YYYY-NNN) - */ -private function generateBiddingCode(int $tenantId): string -{ - $year = now()->format('Y'); - $prefix = "BID-{$year}-"; - - $lastBidding = Bidding::where('tenant_id', $tenantId) - ->where('bidding_code', 'like', "{$prefix}%") - ->orderBy('id', 'desc') - ->first(); - - $sequence = 1; - if ($lastBidding) { - $lastNum = (int) substr($lastBidding->bidding_code, -3); - $sequence = $lastNum + 1; - } - - return $prefix . str_pad($sequence, 3, '0', STR_PAD_LEFT); -} -``` - -### 3.9 Service/Controller 패턴 (SAM 표준) - -**Controller 패턴** (api/app/Http/Controllers): -```php - $this->service->index($request->validated())); - } - - public function show(int $id) - { - return ApiResponse::handle(fn () => $this->service->show($id)); - } - - public function update(BiddingUpdateRequest $request, int $id) - { - return ApiResponse::handle(fn () => $this->service->update($id, $request->validated())); - } - - public function destroy(int $id) - { - return ApiResponse::handle(fn () => $this->service->destroy($id)); - } - - public function stats() - { - return ApiResponse::handle(fn () => $this->service->stats()); - } -} -``` - -**Service 패턴** (api/app/Services): -```php -tenantId(); // 필수 - $query = Bidding::where('tenant_id', $tenantId); - // ... 필터, 정렬, 페이지네이션 - return $query->paginate($params['size'] ?? 20); - } - - public function show(int $id): Bidding - { - $tenantId = $this->tenantId(); - return Bidding::where('tenant_id', $tenantId) - ->with(['quote']) - ->findOrFail($id); - } - - public function stats(): array - { - $tenantId = $this->tenantId(); - return [ - 'total' => Bidding::where('tenant_id', $tenantId)->count(), - 'waiting' => Bidding::where('tenant_id', $tenantId)->where('status', 'waiting')->count(), - 'awarded' => Bidding::where('tenant_id', $tenantId)->where('status', 'awarded')->count(), - ]; - } -} -``` - -### 3.10 더미데이터 (Seeder용 10건) - -> React 목업 기준 (`react/src/components/business/construction/bidding/actions.ts`) - -```php -// api/database/seeders/BiddingSeeder.php -$biddings = [ - [ - 'bidding_code' => 'BID-2025-001', - 'client_name' => '이사대표', - 'project_name' => '광장 아파트', - 'bidding_date' => '2025-01-25', - 'total_count' => 15, - 'bidding_amount' => 71000000, - 'bid_date' => '2025-01-20', - 'submission_date' => '2025-01-22', - 'confirm_date' => '2025-01-25', - 'status' => 'awarded', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-002', - 'client_name' => '야사건설', - 'project_name' => '대림아파트', - 'bidding_date' => '2025-01-20', - 'total_count' => 22, - 'bidding_amount' => 100000000, - 'bid_date' => '2025-01-18', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'waiting', - 'bidder_name' => '김철수', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-003', - 'client_name' => '여의건설', - 'project_name' => '현장아파트', - 'bidding_date' => '2025-01-18', - 'total_count' => 18, - 'bidding_amount' => 85000000, - 'bid_date' => '2025-01-15', - 'submission_date' => '2025-01-16', - 'confirm_date' => '2025-01-18', - 'status' => 'awarded', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-004', - 'client_name' => '이사대표', - 'project_name' => '송파타워', - 'bidding_date' => '2025-01-15', - 'total_count' => 30, - 'bidding_amount' => 120000000, - 'bid_date' => '2025-01-12', - 'submission_date' => '2025-01-13', - 'confirm_date' => '2025-01-15', - 'status' => 'failed', - 'bidder_name' => '이영희', - 'remarks' => '가격 경쟁력 부족', - ], - [ - 'bidding_code' => 'BID-2025-005', - 'client_name' => '야사건설', - 'project_name' => '강남센터', - 'bidding_date' => '2025-01-12', - 'total_count' => 25, - 'bidding_amount' => 95000000, - 'bid_date' => '2025-01-10', - 'submission_date' => '2025-01-11', - 'confirm_date' => null, - 'status' => 'submitted', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-006', - 'client_name' => '여의건설', - 'project_name' => '목동센터', - 'bidding_date' => '2025-01-10', - 'total_count' => 12, - 'bidding_amount' => 78000000, - 'bid_date' => '2025-01-08', - 'submission_date' => '2025-01-09', - 'confirm_date' => '2025-01-10', - 'status' => 'invalid', - 'bidder_name' => '김철수', - 'remarks' => '입찰 조건 미충족', - ], - [ - 'bidding_code' => 'BID-2025-007', - 'client_name' => '이사대표', - 'project_name' => '서초타워', - 'bidding_date' => '2025-01-08', - 'total_count' => 35, - 'bidding_amount' => 150000000, - 'bid_date' => '2025-01-05', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'waiting', - 'bidder_name' => '이영희', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-008', - 'client_name' => '야사건설', - 'project_name' => '청담프로젝트', - 'bidding_date' => '2025-01-05', - 'total_count' => 40, - 'bidding_amount' => 200000000, - 'bid_date' => '2025-01-03', - 'submission_date' => '2025-01-04', - 'confirm_date' => '2025-01-05', - 'status' => 'awarded', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-009', - 'client_name' => '여의건설', - 'project_name' => '잠실센터', - 'bidding_date' => '2025-01-03', - 'total_count' => 20, - 'bidding_amount' => 88000000, - 'bid_date' => '2025-01-01', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'hold', - 'bidder_name' => '김철수', - 'remarks' => '검토 대기 중', - ], - [ - 'bidding_code' => 'BID-2025-010', - 'client_name' => '이사대표', - 'project_name' => '역삼빌딩', - 'bidding_date' => '2025-01-01', - 'total_count' => 10, - 'bidding_amount' => 65000000, - 'bid_date' => '2024-12-28', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'waiting', - 'bidder_name' => '이영희', - 'remarks' => '', - ], -]; - -// 통계 요약: -// - total: 10건 -// - waiting: 3건 (BID-002, 007, 010) -// - awarded: 3건 (BID-001, 003, 008) -// - submitted: 1건 (BID-005) -// - failed: 1건 (BID-004) -// - invalid: 1건 (BID-006) -// - hold: 1건 (BID-009) -``` - ---- - -## 4. 상세 작업 내용 - -> 각 Phase 진행 후 이 섹션에 상세 내용 추가 - -### 4.1 Phase 1: Database & Model - -#### 1.1 마이그레이션 파일 생성 -- **상태**: ⏳ 대기 -- **파일**: `api/database/migrations/2026_01_19_XXXXXX_create_biddings_table.php` - -#### 1.2 Model 생성 -- **상태**: ⏳ 대기 -- **파일**: `api/app/Models/Bidding/Bidding.php` - -#### 1.3 Seeder 생성 -- **상태**: ⏳ 대기 -- **파일**: `api/database/seeders/BiddingSeeder.php` -- **데이터**: React 목업 기준 10건 - ---- - -## 5. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | QuoteService 수정 | `convertToBidding()` 메서드 추가 | api/Quote | ⏳ 대기 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-19 | - | 문서 초안 작성 | - | - | - ---- - -## 7. 참고 문서 - -- **SAM API Rules**: `api/CLAUDE.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **Swagger 가이드**: `docs/guides/swagger-guide.md` -- **React 목업 타입**: `react/src/components/business/construction/bidding/types.ts` -- **React 목업 데이터**: `react/src/components/business/construction/bidding/actions.ts` -- **기존 견적 API**: `react/src/components/business/construction/estimates/actions.ts` - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("bidding-api-state") // 1. 상태 파악 -read_memory("bidding-api-snapshot") // 2. 사고 흐름 복구 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 Snapshot | 현재까지 코드 변경점 저장 | -| **20% 이하** | 🧹 Context Purge | 활성 심볼 저장 | -| **10% 이하** | 🛑 Stop & Save | 최종 상태 저장 | - -### 8.3 Serena 메모리 구조 -- `bidding-api-state`: { phase, progress, next_step } -- `bidding-api-snapshot`: 현재까지의 코드 변경점 요약 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 API 테스트 케이스 - -| 엔드포인트 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|-----------|------|----------|----------|------| -| GET /biddings | - | 목록 반환 | | ⏳ | -| GET /biddings/stats | - | 통계 반환 | | ⏳ | -| GET /biddings/{id} | id=1 | 단건 반환 | | ⏳ | -| PUT /biddings/{id} | 수정 데이터 | 수정 성공 | | ⏳ | -| POST /quotes/{id}/convert-to-bidding | quote_id | 입찰 생성 | | ⏳ | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| Bidding API CRUD 동작 | ⏳ | | -| 견적 → 입찰 전환 동작 | ⏳ | | -| 더미데이터 10건 생성 | ⏳ | | -| Swagger 문서 완성 | ⏳ | | -| Pint 통과 | ⏳ | | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 견적→입찰 전환 + 더미데이터 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-4 정의 | -| 4 | 의존성이 명시되어 있는가? | ✅ | quotes API 의존 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 7. 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.1 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/API 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태 + 3.1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/construction-api-integration-plan.md b/plans/archive/construction-api-integration-plan.md deleted file mode 100644 index f217f7a..0000000 --- a/plans/archive/construction-api-integration-plan.md +++ /dev/null @@ -1,480 +0,0 @@ -# 시공사 페이지 API 연동 계획 - -> **작성일**: 2026-01-08 -> **목적**: 시공사 8개 페이지 Mock → API 연동 -> **기준 문서**: `docs/standards/api-rules.md`, `docs/guides/swagger-guide.md` -> **상태**: ✅ 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3.4: 노임관리 API 연동 완료 ✅ | -| **다음 작업** | 🎉 **전체 완료** | -| **진행률** | 8/8 (100%) | -| **마지막 업데이트** | 2026-01-12 | - ---- - -## 0. 전제 조건 (Prerequisites) - -### 0.1 환경 확인 -```bash -# Docker 컨테이너 상태 확인 -docker ps | grep sam - -# API 서버 접속 확인 -curl -I http://api.sam.kr/api/health - -# React 개발 서버 확인 -curl -I http://react.sam.kr -``` - -**체크리스트:** -- [ ] Docker 컨테이너 실행 중 (api, react, mysql) -- [ ] api.sam.kr 접속 가능 (200 응답) -- [ ] react.sam.kr 접속 가능 (200 응답) -- [ ] 데이터베이스 연결 정상 - -### 0.2 권한 및 인증 -- [ ] API 개발 권한 (`api/` 디렉토리 수정 가능) -- [ ] React 개발 권한 (`react/` 디렉토리 수정 가능) -- [ ] Sanctum 토큰 발급 방법 숙지 (테스트용) - -### 0.3 필수 도구 -- PHP 8.4+, Composer -- Node.js 20+, pnpm -- Git - ---- - -## 1. 개요 - -### 1.1 배경 -시공사 메뉴의 8개 페이지가 현재 모두 Mock 데이터를 사용하고 있으며, 실제 API 연동이 필요함. -(물량검토관리는 Frontend/기획 미존재로 제외) - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - Service-First: 비즈니스 로직 → Service Layer │ -│ - Multi-tenancy: BelongsToTenant 필수 │ -│ - FormRequest: Controller 검증 금지 │ -│ - Server Actions: React에서 'use server' 패턴 사용 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | actions.ts Mock→API 변경, 타입 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 새 API 엔드포인트 생성, DB 스키마 변경 | **필수** | -| 🔴 금지 | 기존 API 삭제, 테이블 구조 변경 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/standards/api-rules.md` - API 개발 규칙 ✅ 존재 -- `docs/guides/swagger-guide.md` - Swagger 작성 가이드 ✅ 존재 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 ✅ 존재 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 계약관리 (Contract) - -| # | 작업 항목 | 상태 | 서브 문서 | -|---|----------|:----:|----------| -| 1.1 | 계약관리 (contract) | ✅ | [contract-plan.md](./sub/contract-plan.md) | -| 1.2 | 인수인계보고서관리 (handover-report) | ✅ | [handover-report-plan.md](./sub/handover-report-plan.md) | - -### 2.2 Phase 2: 발주관리 (Order) - -| # | 작업 항목 | 상태 | 서브 문서 | -|---|----------|:----:|----------| -| 2.1 | 현장관리 (site-management) | ✅ | [site-management-plan.md](./sub/site-management-plan.md) | -| 2.2 | 구조검토관리 (structure-review) | ✅ | [structure-review-plan.md](./sub/structure-review-plan.md) | -| 2.3 | 물량검토관리 (quantity-review) | ❌ 제외 | Frontend/기획 미존재 | - -### 2.3 Phase 3: 기준정보 (Base Info) - -| # | 작업 항목 | 상태 | 서브 문서 | -|---|----------|:----:|----------| -| 3.1 | 카테고리관리 (categories) | ✅ | [categories-plan.md](./sub/categories-plan.md) | -| 3.2 | 품목관리 (items) | ✅ | [items-plan.md](./sub/items-plan.md) | -| 3.3 | 단가관리 (pricing) | ✅ | [pricing-plan.md](./sub/pricing-plan.md) | -| 3.4 | 노임관리 (labor) | ✅ | [labor-plan.md](./sub/labor-plan.md) | - ---- - -## 3. API 현황 분석 - -### 3.1 기존 API (연동 가능) - -| API | 경로 | 상태 | 대상 컴포넌트 | -|-----|------|:----:|--------------| -| categories | `/api/construction/categories` | ✅ 존재 | 카테고리관리 | -| pricing | `/api/construction/pricing` | ✅ 존재 | 단가관리 | - -### 3.2 신규 개발 필요 API - -| API | 예상 경로 | 우선순위 | 대상 컴포넌트 | -|-----|----------|:--------:|--------------| -| contracts | `/api/construction/contracts` | ✅ 완료 | 계약관리 | -| handover-reports | `/api/construction/handover-reports` | ✅ 완료 | 인수인계보고서 | -| sites | `/api/construction/sites` | ✅ 완료 | 현장관리 | -| structure-reviews | `/api/construction/structure-reviews` | ✅ 완료 | 구조검토관리 | -| quantity-reviews | `/api/construction/quantity-reviews` | ❌ 제외 | 물량검토관리 (Frontend/기획 미존재) | -| items | `/api/construction/items` | 🟢 낮음 | 품목관리 | -| labor | `/api/construction/labor` | 🟢 낮음 | 노임관리 | - ---- - -## 4. 작업 절차 - -### 4.1 단계별 절차 (상세) - -``` -Step 1: 서브 문서 확인 -├── docs/plans/sub/{module}-plan.md 읽기 -├── 현재 Mock 데이터 구조 확인 -└── 필요한 API 엔드포인트 파악 - -Step 2: API 엔드포인트 확인/생성 -├── api/routes/api.php에서 기존 API 확인 -├── 없으면: -│ ├── Controller 생성: php artisan make:controller Api/Construction/{Name}Controller -│ ├── Service 생성: app/Services/Construction/{Name}Service.php -│ ├── FormRequest 생성: php artisan make:request Api/Construction/{Name}Request -│ └── Model 확인/생성 -└── Swagger 문서 작성 - -Step 3: React actions.ts 수정 -├── react/src/components/business/construction/{module}/actions.ts 열기 -├── Mock 데이터 상수 제거 (MOCK_XXX) -├── API 호출 로직 구현: -│ └── const response = await fetch('/api/construction/{endpoint}', {...}) -└── 에러 핸들링 추가 - -Step 4: 타입 정합성 확인 -├── API 응답과 프론트엔드 타입 매칭 -├── types.ts 수정 (snake_case → camelCase 변환 등) -└── 컴포넌트 수정 (필요시) - -Step 5: 테스트 및 검증 -├── API 직접 호출 테스트 (curl/Postman) -├── UI 동작 확인 (브라우저) -└── 에러 케이스 테스트 -``` - -### 4.2 첫 번째 작업 시작점 - -**Phase 1.1 계약관리 시작:** -```bash -# 1. 서브 문서 읽기 -cat docs/plans/sub/contract-plan.md - -# 2. 현재 Mock 확인 -cat react/src/components/business/construction/contract/actions.ts - -# 3. API 존재 여부 확인 -grep -n "contracts" api/routes/api.php - -# 4. 없으면 Controller 생성 -cd api && php artisan make:controller Api/Construction/ContractController --resource -``` - ---- - -## 5. 환경 정보 - -### 5.1 프로젝트 구조 - -``` -SAM/ -├── api/ # Laravel 12 REST API -│ ├── app/Http/Controllers/Api/Construction/ -│ ├── app/Services/Construction/ -│ └── routes/api.php -│ -├── react/ # Next.js 15 Frontend -│ └── src/ -│ ├── app/[locale]/(protected)/construction/ -│ │ ├── project/contract/ # 계약관리 -│ │ ├── project/contract/handover-report/ # 인수인계 -│ │ ├── order/site-management/ # 현장관리 -│ │ ├── order/structure-review/ # 구조검토 -│ │ ├── order/order-management/ # 발주관리 -│ │ └── order/base-info/ # 기준정보 -│ │ ├── categories/ -│ │ ├── items/ -│ │ ├── pricing/ -│ │ └── labor/ -│ └── components/business/construction/ -│ -└── docs/plans/ # 계획 문서 - ├── construction-api-integration-plan.md # 메인 (현재 문서) - └── sub/ # 서브 문서 (9개) -``` - -### 5.2 개발 환경 - -| 항목 | 값 | -|------|-----| -| 도메인 | sam.kr (로컬) | -| API | api.sam.kr | -| React | react.sam.kr | -| PHP | 8.4+ | -| Laravel | 12 | -| Next.js | 15 | - ---- - -## 6. 컴포넌트 분석 요약 - -### 6.1 계약관리 (Contract) - -| 컴포넌트 | Mock 상태 | 주요 기능 | -|----------|:--------:|----------| -| ContractListClient | ✅ Mock | 목록, 검색, 삭제, 필터 | -| 인수인계보고서 | ✅ Mock | 목록, 상세, 삭제 | - -### 6.2 발주관리 (Order) - -| 컴포넌트 | Mock 상태 | 주요 기능 | -|----------|:--------:|----------| -| SiteManagementListClient | ✅ Mock | 현장 목록, 통계, 삭제 | -| StructureReviewListClient | ✅ Mock | 구조검토 목록, 상태 관리 | -| OrderManagementClient | ✅ Mock | 발주 목록, 필터, 삭제 | - -### 6.3 기준정보 (Base Info) - -| 컴포넌트 | Mock 상태 | API 존재 | 주요 기능 | -|----------|:--------:|:-------:|----------| -| CategoryManagementClient | ✅ Mock | ✅ | 카테고리 CRUD, 순서 변경 | -| ItemManagementClient | ✅ Mock | ❌ | 품목 CRUD, 카테고리 연결 | -| PricingListClient | ✅ Mock | ✅ | 단가 CRUD, 버전 관리 | -| LaborManagementClient | ✅ Mock | ❌ | 노임 CRUD, 단가 관리 | - ---- - -## 7. 성공 기준 - -### 7.1 각 페이지 완료 조건 - -| # | 조건 | 확인 방법 | -|---|------|----------| -| 1 | Mock 데이터 완전 제거 | `grep -r "MOCK_" actions.ts` 결과 없음 | -| 2 | API 호출 성공 | 네트워크 탭에서 200 응답 확인 | -| 3 | UI에서 데이터 정상 표시 | 목록에 실제 데이터 표시 | -| 4 | CRUD 동작 정상 | 생성/조회/수정/삭제 모두 동작 | -| 5 | 에러 핸들링 동작 | 네트워크 끊김 시 에러 메시지 표시 | - -### 7.2 전체 완료 조건 - -- [ ] 8개 페이지 모두 API 연동 완료 (4/8) -- [ ] Swagger 문서 작성 완료 -- [ ] 기본 동작 테스트 통과 -- [ ] 코드 리뷰 완료 - -### 7.3 품질 기준 - -- API 응답 시간: < 500ms -- 에러 발생 시 사용자 친화적 메시지 표시 -- TypeScript 타입 에러 0개 -- ESLint 경고 0개 - ---- - -## 8. 검증 방법 - -### 8.1 API 테스트 (curl) - -```bash -# 1. 인증 토큰 획득 (테스트용) -TOKEN=$(curl -s -X POST "http://api.sam.kr/api/auth/login" \ - -H "Content-Type: application/json" \ - -d '{"email":"test@test.com","password":"password"}' | jq -r '.token') - -# 2. 계약 목록 조회 -curl -X GET "http://api.sam.kr/api/construction/contracts" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Accept: application/json" - -# 3. 계약 상세 조회 -curl -X GET "http://api.sam.kr/api/construction/contracts/1" \ - -H "Authorization: Bearer $TOKEN" - -# 4. 계약 생성 -curl -X POST "http://api.sam.kr/api/construction/contracts" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"title":"테스트 계약","partner_id":1}' -``` - -### 8.2 UI 테스트 체크리스트 - -``` -□ 페이지 접속 시 로딩 스피너 표시 -□ 데이터 로딩 완료 후 목록 표시 -□ 검색 기능 동작 -□ 필터 기능 동작 -□ 페이지네이션 동작 -□ 상세 보기 동작 -□ 생성 폼 동작 -□ 수정 폼 동작 -□ 삭제 확인 및 동작 -□ 에러 발생 시 메시지 표시 -``` - -### 8.3 에러 케이스 테스트 - -| 케이스 | 예상 동작 | 확인 방법 | -|--------|----------|----------| -| 네트워크 끊김 | 에러 메시지 표시 | 네트워크 탭에서 Offline 모드 | -| 401 인증 오류 | 로그인 페이지 리다이렉트 | 토큰 만료 상태에서 접속 | -| 404 데이터 없음 | "데이터 없음" 표시 | 존재하지 않는 ID 접근 | -| 500 서버 오류 | 에러 메시지 표시 | API 강제 에러 발생 | - ---- - -## 9. 세션 관리 - -### 9.1 새 세션 시작 시 - -```bash -# 1. 메인 문서 읽기 (현재 진행 상태 확인) -cat docs/plans/construction-api-integration-plan.md | head -30 - -# 2. "다음 작업" 확인 -grep "다음 작업" docs/plans/construction-api-integration-plan.md - -# 3. 해당 서브 문서 읽기 -cat docs/plans/sub/{다음작업}-plan.md - -# 4. 작업 시작 -``` - -### 9.2 작업 중 체크포인트 - -| 시점 | 행동 | -|------|------| -| 작업 완료 시 | 메인 문서 "현재 진행 상태" 업데이트 | -| 서브 작업 완료 시 | 서브 문서 상태 (⏳→✅) 업데이트 | -| 컨펌 필요 시 | "컨펌 대기 목록"에 추가 | -| 세션 종료 전 | 변경 이력에 기록 | - -### 9.3 세션 종료 시 - -```bash -# 1. 진행 상태 업데이트 -# - 📍 현재 진행 상태 섹션의 "마지막 완료 작업", "다음 작업" 수정 -# - 대상 범위의 상태 아이콘 수정 (⏳ → ✅ 또는 🔄) - -# 2. 변경 이력 추가 -# | 2026-01-08 | 1.1 | 계약관리 API 연동 완료 | contract/actions.ts | - | - -# 3. 커밋 (승인 후) -git add . && git commit -m "feat: [시공사] 1.1 계약관리 - API 연동" -``` - -### 9.4 컨텍스트 관리 (Serena 메모리) - -```javascript -// 세션 시작 시 로드 -read_memory("construction-api-state") - -// 작업 중 저장 (30분마다 또는 주요 완료 시) -write_memory("construction-api-state", { - phase: "1.1", - status: "진행중", - lastCompleted: "Controller 생성", - nextStep: "Service 로직 구현" -}) - -// 컨텍스트 30% 이하 시 -write_memory("construction-api-snapshot", "현재까지 진행 상황 요약...") -``` - ---- - -## 10. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-08 | 초안 | 문서 초안 작성, 9개 컴포넌트 분석 | - | - | -| 2026-01-08 | 보완 | 전제조건, 성공기준, 검증방법, 세션관리 추가 | - | - | -| 2026-01-09 | 1.1 | 계약관리 API 연동 완료 (Backend + Frontend) | api/, react/ | ✅ | -| 2026-01-09 | 1.2 | 인수인계보고서 Frontend API 연동 완료 | react/ | ✅ | -| 2026-01-09 | 2.1 | 현장관리 API 연동 완료 (Backend + Frontend) | api/, react/ | ✅ | -| 2026-01-09 | 2.2 | 구조검토관리 API 연동 완료 (Backend + Frontend) | api/, react/ | ✅ | -| 2026-01-09 | 2.3 | 물량검토관리 제외 (Frontend/기획 미존재) | docs/ | ✅ | -| 2026-01-09 | 3.1 | 카테고리관리 API 연동 완료 (HTTP 메서드 수정) | react/ | ✅ | -| 2026-01-09 | 3.2 | 품목관리 API 연동 완료 (apiClient.delete body 지원 추가) | react/ | ✅ | -| 2026-01-09 | 3.3 | 단가관리 Backend API 보완 (stats, bulkDestroy 추가) | api/ | ✅ | - ---- - -## 11. 참고 문서 - -| 문서 | 경로 | 용도 | -|------|------|------| -| API 규칙 | `docs/standards/api-rules.md` | API 개발 표준 | -| Swagger 가이드 | `docs/guides/swagger-guide.md` | API 문서화 | -| 품질 체크리스트 | `docs/standards/quality-checklist.md` | 완료 전 점검 | -| 빠른 시작 | `docs/quickstart/quick-start.md` | 환경 설정 | -| 개발 명령어 | `docs/quickstart/dev-commands.md` | 자주 쓰는 명령어 | - ---- - -## 12. 서브 문서 링크 - -| Phase | 문서 | 경로 | API 상태 | -|-------|------|------|:--------:| -| 1.1 | 계약관리 | [./sub/contract-plan.md](./sub/contract-plan.md) | ✅ 완료 | -| 1.2 | 인수인계보고서 | [./sub/handover-report-plan.md](./sub/handover-report-plan.md) | ❌ 신규 | -| 2.1 | 현장관리 | [./sub/site-management-plan.md](./sub/site-management-plan.md) | ⚠️ 확인필요 | -| 2.2 | 구조검토관리 | [./sub/structure-review-plan.md](./sub/structure-review-plan.md) | ❌ 신규 | -| 2.3 | 발주관리 | [./sub/order-management-plan.md](./sub/order-management-plan.md) | ❌ 신규 | -| 3.1 | 카테고리관리 | [./sub/categories-plan.md](./sub/categories-plan.md) | ✅ 존재 | -| 3.2 | 품목관리 | [./sub/items-plan.md](./sub/items-plan.md) | ❌ 신규 | -| 3.3 | 단가관리 | [./sub/pricing-plan.md](./sub/pricing-plan.md) | ✅ 존재 | -| 3.4 | 노임관리 | [./sub/labor-plan.md](./sub/labor-plan.md) | ❌ 신규 | - ---- - -## 13. 자기완결성 점검 결과 - -### 13.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 참조 섹션 | -|---|----------|:----:|----------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 7. 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 0. 전제 조건 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 11. 참고 문서 (검증됨) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 4. 작업 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 8. 검증 방법 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 명령어 포함 | - -### 13.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 4.2 첫 번째 작업 시작점 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5.1 프로젝트 구조 + 서브 문서 | -| Q4. 작업 완료 확인 방법은? | ✅ | 7. 성공 기준, 8. 검증 방법 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 11. 참고 문서 | - -**결과: 5/5 통과 → ✅ 자기완결성 확보** - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* -*보완일: 2026-01-08* \ No newline at end of file diff --git a/plans/archive/docs-update-plan.md b/plans/archive/docs-update-plan.md deleted file mode 100644 index 1713e06..0000000 --- a/plans/archive/docs-update-plan.md +++ /dev/null @@ -1,309 +0,0 @@ -# docs/architecture 문서 업데이트 계획 - -> **작성일**: 2025-12-26 -> **목적**: 현재 시스템 상태와 문서 동기화 -> **기준 문서**: docs/INDEX.md -> **상태**: 🔄 진행중 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 전체 완료 | -| **다음 작업** | 없음 (완료) | -| **진행률** | 13/13 (100%) ✅ | -| **마지막 업데이트** | 2025-12-26 | - ---- - -## 1. 개요 - -### 1.1 배경 -- 2025-12-13 admin 프로젝트 → mng 프로젝트 전환 완료 -- 문서에 아직 admin 참조가 남아있어 동기화 필요 -- 기술 스택 버전 업데이트 반영 필요 - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 문서 업데이트 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - 현재 시스템 상태와 100% 동기화 │ -│ - admin → mng 전환 완전 반영 │ -│ - 버전 정보 최신화 (React 19.2.1, Next.js 15.5.7) │ -│ - 상호 참조 링크 일관성 유지 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 날짜 갱신, 오타 수정, 버전 업데이트 | 불필요 | -| ⚠️ 컨펌 필요 | 구조 변경, 새 섹션 추가, 문서 삭제 | **필수** | -| 🔴 금지 | 비즈니스 로직 변경, 정책 변경 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/INDEX.md` - 문서 인덱스 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 핵심 문서 업데이트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | system-overview.md - admin→mng 전환 | ✅ | 완료 | -| 1.2 | dev-commands.md - admin→mng 변경 | ✅ | 완료 | -| 1.3 | quick-start.md - claudedocs→docs 경로 수정 | ✅ | 완료 | - -### 2.2 Phase 2: 보조 문서 업데이트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | INDEX.md - 프로젝트 구조 미세 조정 | ✅ | Admin 참조 제거 | -| 2.2 | quality-checklist.md - 날짜 갱신 | ✅ | 2025-12-26 | -| 2.3 | swagger-guide.md - 날짜 갱신 | ✅ | 2025-12-26 | - -### 2.3 Phase 3: 검증 및 정리 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | security-policy.md - 날짜 갱신 | ✅ | 2025-12-26 | -| 3.2 | database-schema.md - 테이블 수 업데이트 | ✅ | 92개→171개 | - -### 2.4 Phase 4: 오래된 파일 정리/아카이브 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | history/2025-09/ 문서 검토 | ✅ | 참조용 유지 | -| 4.2 | history/2025-11/ 문서 검토 | ✅ | 아카이브로 적절 | -| 4.3 | admin 참조 파일 식별 및 정리 | ✅ | 4개 파일 수정 완료 | -| 4.4 | 완료된 plans/ 문서 정리 | ✅ | D0.8→history, index 업데이트 | -| 4.5 | 중복/불필요 문서 정리 | ✅ | 빈 디렉토리 6개 삭제 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: Phase 1 - 핵심 문서 업데이트 -├── 1.1 system-overview.md 전면 업데이트 -│ ├── admin/ 설명 → mng/ 설명 -│ ├── Filament v4 → Pure Blade + Tailwind -│ ├── Docker 서비스 구성 업데이트 -│ └── 저장소 구조 업데이트 -├── 1.2 dev-commands.md 수정 -│ ├── Admin Application → MNG Application -│ └── admin/ 경로 → mng/ 경로 -└── 1.3 quick-start.md 수정 - ├── claudedocs/ → docs/ 경로 - └── 프로젝트 구조 업데이트 - -Step 2: Phase 2 - 보조 문서 업데이트 -├── 2.1 INDEX.md 미세 조정 -├── 2.2 quality-checklist.md 날짜 갱신 -└── 2.3 swagger-guide.md 날짜 갱신 - -Step 3: Phase 3 - 검증 및 정리 -├── 3.1 security-policy.md 날짜 갱신 -├── 3.2 database-schema.md 테이블 수 확인 -└── 3.3 모든 문서 일관성 검증 - -Step 4: Phase 4 - 오래된 파일 정리/아카이브 -├── 4.1 history/2025-09/ 문서 검토 -│ └── 구버전 스키마, 체크포인트 확인 -├── 4.2 history/2025-11/ 문서 검토 -│ └── item-master 관련 아카이브 정리 -├── 4.3 admin 참조 파일 정리 -│ └── mng로 미전환된 파일 식별/수정 -├── 4.4 완료된 plans/ 문서 정리 -│ └── 완료된 계획 문서 삭제/아카이브 -└── 4.5 중복/불필요 문서 정리 - └── 통합 가능 문서 식별 및 처리 -``` - -### 3.2 문서 업데이트 템플릿 - -```markdown -### [항목 ID] 항목명 - -**현재 상태:** -- [현재 상태 설명] - -**목표 상태:** -- [목표 상태 설명] - -**변경 사항:** -- [ ] ✅ [즉시 가능 항목] -- [ ] ⚠️ [컨펌 필요 항목] -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: 핵심 문서 업데이트 - -#### 1.1 system-overview.md -- **상태**: ⏳ 대기 -- **주요 변경**: - - [ ] admin/ 섹션 → mng/ 섹션으로 전환 - - [ ] 기술 스택: Filament v4 → Pure Blade + Tailwind CSS 3.x - - [ ] Docker 서비스: design, php73 추가 - - [ ] React 버전: 19.2.0 → 19.2.1 - - [ ] Next.js 버전: 15 → 15.5.7 - - [ ] 도메인 매핑: admin.sam.kr → mng 서비스 설명 - - [ ] 저장소 구조: admin → mng - -#### 1.2 dev-commands.md -- **상태**: ⏳ 대기 -- **주요 변경**: - - [ ] "Admin Application (admin/)" → "MNG Application (mng/)" - - [ ] admin/ 경로 → mng/ 경로 - - [ ] 업데이트 날짜 갱신 - -#### 1.3 quick-start.md -- **상태**: ⏳ 대기 -- **주요 변경**: - - [ ] claudedocs/SAM/ 경로 → docs/ 경로 - - [ ] 프로젝트 구조에 mng, design, planning 추가 - - [ ] admin/ 참조 → mng/ 참조 - - [ ] 업데이트 날짜 갱신 - -### 4.2 Phase 4: 오래된 파일 정리/아카이브 - -#### 4.1 history/2025-09/ 문서 검토 -- **상태**: ⏳ 대기 -- **대상 파일**: - - `history/2025-09/checkpoint.md` - 구버전 체크포인트 - - `history/2025-09/database-schema.md` - 구버전 스키마 (참조용 유지 검토) -- **조치**: 아카이브 적합성 검토, 불필요시 삭제 - -#### 4.2 history/2025-11/ 문서 검토 -- **상태**: ⏳ 대기 -- **대상 파일**: - - `history/2025-11/item-master-gap-analysis.md` - - `history/2025-11/item-master-spec.md` - - `history/2025-11/front-requests/` 디렉토리 - - `history/2025-11/item-master-archived/` 디렉토리 -- **조치**: 현재 유효성 검토, 아카이브 정리 - -#### 4.3 admin 참조 파일 식별 및 정리 -- **상태**: ⏳ 대기 -- **검색 대상**: docs/ 전체에서 "admin" 키워드 포함 파일 -- **조치**: mng로 전환 또는 deprecated 표시 - -#### 4.4 완료된 plans/ 문서 정리 -- **상태**: ⏳ 대기 -- **대상 파일**: - - 완료된 계획 문서 식별 - - 현재 진행중인 문서 유지 -- **조치**: 완료된 계획은 삭제 또는 history/로 이동 - -#### 4.5 중복/불필요 문서 정리 -- **상태**: ⏳ 대기 -- **검토 대상**: - - 내용이 중복된 문서 - - 더 이상 유효하지 않은 문서 - - 통합 가능한 문서 -- **조치**: 통합, 삭제, 또는 아카이브 - ---- - -## 5. 컨펌 대기 목록 - -> 구조 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| - | - | - | - | - | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-12-26 | - | 계획 문서 초안 작성 | - | - | -| 2025-12-26 | Phase 4 | 오래된 파일 정리/아카이브 작업 추가 | docs-update-plan.md | - | -| 2025-12-26 | Phase 1 | 핵심 문서 3개 업데이트 완료 | system-overview.md, dev-commands.md, quick-start.md | ✅ | -| 2025-12-26 | Phase 2 | 보조 문서 3개 업데이트 완료 | INDEX.md, quality-checklist.md, swagger-guide.md | ✅ | -| 2025-12-26 | Phase 3 | 검증 및 정리 완료 | security-policy.md, database-schema.md | ✅ | -| 2025-12-26 | Phase 4.1-4.2 | history/ 문서 검토 완료 | - | ✅ | -| 2025-12-26 | Phase 4.4 | plans/ 정리 완료 | D0.8→history, index_plans.md 업데이트 | ✅ | -| 2025-12-26 | Phase 4.3 | admin 참조 파일 정리 | docker-setup, git-conventions, project-launch-roadmap, remote-work-setup | ✅ | - ---- - -## 7. 참고 문서 - -- **문서 인덱스**: `docs/INDEX.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **Serena 메모리**: `docs-update-analysis.md` - ---- - -## 8. 세션 관리 정책 - -### 8.1 세션 시작 시 -``` -list_memories() → 기존 상태 확인 -read_memory("docs-update-analysis") → 분석 결과 로드 -이 계획 문서 읽기 → 컨텍스트 로드 -``` - -### 8.2 작업 중 -- 변경 이력 실시간 업데이트 -- Phase/항목별 상태 업데이트 -- 컨펌 필요 시 대기 목록 추가 - -### 8.3 세션 종료 시 -``` -변경 이력에 최종 업데이트 기록 -write_memory("docs-update-progress") → Serena에 저장 -``` - -### 8.4 Serena 메모리 구조 -``` -docs-update-analysis.md # 분석 결과 (완료) -docs-update-progress.md # 진행 상황 (작업 중 업데이트) -``` - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 문서 일관성 체크 - -| 문서 | admin 참조 | mng 반영 | 날짜 최신화 | 링크 유효 | -|------|:----------:|:--------:|:-----------:|:---------:| -| system-overview.md | | | | | -| dev-commands.md | | | | | -| quick-start.md | | | | | -| INDEX.md | | | | | -| quality-checklist.md | | | | | -| swagger-guide.md | | | | | -| security-policy.md | | | | | -| database-schema.md | | | | | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| admin 참조 완전 제거 | | | -| mng 반영 완료 | | | -| 버전 정보 최신화 | | | -| 상호 참조 링크 유효 | | | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* diff --git a/plans/archive/document-management-system-changelog.md b/plans/archive/document-management-system-changelog.md deleted file mode 100644 index ee81f29..0000000 --- a/plans/archive/document-management-system-changelog.md +++ /dev/null @@ -1,31 +0,0 @@ -# 문서관리 시스템 - 변경 이력 - -> **본 문서**: `docs/plans/document-management-system-plan.md`의 변경 이력 -> **최종 업데이트**: 2026-02-12 - ---- - -## 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 관련 섹션 | 승인 | -|------|------|----------|----------|------| -| 2026-01-31 | 초안 | 기존 시스템 분석 기반 계획 문서 전면 재작성 | 본 문서 | - | -| 2026-01-31 | Phase 1.1 완료 | 양식 편집 UI 5개 탭 전체 CRUD 확인 (사실상 완료) | 섹션 3.1, 11.1 | - | -| 2026-01-31 | Phase 1.2 완료 | viewJS.php 라우팅 분석 + EGI/SUS 대표 2종 상세 분석 + 공통패턴 추출 | 섹션 3.1, 11.2 | - | -| 2026-01-31 | Phase 1.3 완료 | IncomingInspectionTemplateSeeder 생성. EGI(ID:7), SUS(ID:8) 2종 시드 완료. 결재2+기본필드10+섹션+항목+컬럼 전체 | 섹션 3.1 | - | -| 2026-01-31 | Phase 1.4 완료 | 미리보기 기능 기존 구현 확인. 모달로 결재란+기본정보+검사이미지+검사테이블(complex)+Footer 모두 렌더링 | 섹션 3.1 | - | -| 2026-01-31 | Phase 1.5 완료 | 양식 복제 기능. duplicate() 메서드 + 라우트 + 테이블 버튼 + JS 함수 추가 | 섹션 3.1 | - | -| 2026-01-31 | Phase 2.1 완료 | 문서 생성 기능 보완. ①문서번호 카테고리별 prefix(IQC/PRD/SLS/PUR, YYMMDD-순번) ②결재라인 초기화(template.approvalLines→document_approvals) ③기본필드 뷰 속성 불일치 수정(field_type/label/default_value 매핑, Str::slug로 field_key 생성) ④섹션 title 참조 수정 | 섹션 3.2 | - | -| 2026-01-31 | Phase 2.2 완료 | 문서 데이터 입력 UI. ①섹션별 동적 검사 테이블 렌더링(complex/select/check/measurement/text 컬럼 타입 지원) ②서브 라벨 행(complex 컬럼의 n1/n2/n3) ③정적 컬럼 자동 매핑(NO/검사항목/검사기준/검사방식/검사주기→item속성) ④종합판정+비고 Footer ⑤JS 폼 데이터 수집(기본필드+섹션데이터+체크박스) ⑥백엔드 saveDocumentData() 공통 메서드(section_id/column_id/row_index EAV 저장) | 섹션 3.2 | - | -| 2026-01-31 | Phase 2.3 완료 | 결재 워크플로우. ①API: submit(DRAFT→PENDING), approve(단계별 승인, 전체 완료 시 APPROVED), reject(반려 사유 필수, REJECTED) ②edit.blade: 결재 제출 버튼 + JS ③show.blade: 승인/반려 버튼, 반려 모달, 결재 현황 속성 수정(step/role/acted_at), 상태 배지 CSS ④재제출 시 결재라인 상태 초기화 ⑤라우트: submit/approve/reject 3개 추가 | 섹션 3.2 | - | -| 2026-01-31 | Phase 2.4 완료 | 문서 목록/검색/필터. ①날짜 범위 필터(date_from/date_to) API + UI 추가 ②DRAFT 문서 삭제 버튼 + deleteDocument() JS (showDeleteConfirm + fetch DELETE) ③기존 구현 확인: 상태/템플릿/검색/페이징 정상 동작 | 섹션 3.2 | - | -| 2026-01-31 | Phase 3.1 완료 | 중간검사 양식 구조 설계. ①5130 레거시 4종(절곡/스크린/슬랫/조인트바) viewMidInspect*.php 전체 분석 ②검사항목·기준·판정방식·공차·이미지 문서화 ③컬럼 구조(check/complex/select) 매핑 설계 ④4종 비교표 + 양식 시스템 매핑 전략(Option A/B/C) ⑤공통 구조(결재3단계, 기본필드7개, Footer) 정의 | 섹션 5.2 | - | -| 2026-01-31 | Phase 3.2 완료 | 5130 중간검사 데이터 이관 설계. ①JSON 공통 배열 구조 분석([0]결재/[1]입력값/[2]num/[3]table/[4]log/[5]checkbox) ②JSON→EAV 매핑 테이블(결재→document_approvals, 기본필드/측정값/체크박스→document_data) ③데이터 변환 규칙(날짜mm/dd→datetime, boolean→string, 이름→user_id) ④6단계 이관 프로세스 설계 ⑤절곡품 inputValue named object vs 나머지 flat array 차이 문서화 ⑥주의사항 5건 | 섹션 5.3 | - | -| 2026-01-31 | Phase 3.3 완료 | 중간검사 양식 시드 데이터. MidInspectionTemplateSeeder 생성. ①조인트바(ID:10, 1섹션6항목8컬럼, 고정기준값4개) ②슬랫(ID:11, 1섹션5항목7컬럼, 고정2+도면1) ③스크린(ID:12, 1섹션6항목8컬럼, 겉모양3+치수3) ④절곡품(ID:13, 4섹션11항목7컬럼, 구성품별 분리) ⑤공통: 결재3단계(판매→생산→품질), 기본필드7개, Footer(부적합+종합판정) | 섹션 3.3 | - | -| 2026-01-31 | Phase 3.4 완료 | 검사 기준 이미지 이관. 5130/img/inspection/ → mng/public/img/inspection/ (27개 파일). 가이드레일(벽면/측면×6변형), 하단마감재(4), 케이스(4), 절곡기준서(2), 스크린/슬랫/조인트바(각1), L-BAR(1), 연기차단재(1) | 섹션 5.4 | - | -| 2026-01-31 | Phase 4.1 완료 | API 엔드포인트 설계. ①DocumentTemplate 모델 6개(Template+ApprovalLine+BasicField+Section+SectionItem+Column) ②DocumentTemplateService(list+show) ③DocumentTemplateController(index+show) ④IndexRequest FormRequest ⑤라우트 2개(GET /v1/document-templates, GET /v1/document-templates/{id}) ⑥DocumentTemplateApi.php Swagger(7개 스키마) ⑦Document 결재 워크플로우 활성화(submit/approve/reject/cancel 4개 엔드포인트) ⑧ApproveRequest+RejectRequest FormRequest ⑨DocumentApi.php Swagger에 결재 4개 추가 ⑩Document.template() 참조 경로 수정 | 섹션 3.4, 4.1, 7 | - | -| 2026-01-31 | Phase 4.2 완료 | mng JSON 기반 문서 화면. ①show.blade.php 섹션 테이블 읽기전용 렌더링(complex/select/check/measurement/text 5가지 컬럼 타입) ②select 판정값 배지(적합=초록, 부적합=빨강) ③check 체크마크 SVG ④measurement mono 폰트 ⑤정적 컬럼 매핑(NO/검사항목/기준/방식/주기/규격/분류) ⑥종합판정+비고 Footer(마지막 섹션에 표시) ⑦검사 기준 이미지 표시 ⑧버그 3건 수정: field_key→Str::slug, field_type→field_type, section.name→title | 섹션 3.4 | - | -| 2026-01-31 | Phase 4.3 완료 | 문서 데이터 입력/저장 연동 검증. Phase 2.2~2.3에서 이미 완전 구현 확인: ①edit.blade.php JS 폼 수집(기본필드+섹션데이터+체크박스) ②fetch POST/PATCH→DocumentApiController ③saveDocumentData() EAV 저장(section_id/column_id/row_index) ④판정(적합/부적합) select+종합판정 Footer 저장 정상 ⑤6.2 결정사항 #2(프론트 입력, 결과만 저장) 적용됨. 추가 코드 작업 없음 | 섹션 3.4 | - | -| 2026-02-10 | Phase 5 계획 수립 | Phase 5 확장 계획 수립. ①마스터 진행 관리 문서 신규 생성(document-system-master.md) ②중간검사(PQC) 상세 계획(document-system-mid-inspection.md) ③제품검사(FQC) 상세 계획(document-system-product-inspection.md) ④작업일지 상세 계획(document-system-work-log.md) ⑤핵심 결정사항 5건: 조인트바=슬랫하위유지, 제품검사=개소별1문서, 작업일지=하이브리드, 제품검사=품질검사 동일, 기타문서=추후정의 ⑥기존 plan 문서 Phase 5 섹션 업데이트 | 섹션 3.5, 마스터 문서 | - | -| 2026-02-10 | 방안1 채택 | 검사기준서↔테이블컬럼 연동 분석 및 방안1 결정. ①edit.blade.php 분석(검사기준서 탭=section_fields+items, 테이블컬럼 탭=columns, 완전 독립) ②이슈 수정: 스키마 불일치→section_fields 누락이 실제 원인(컬럼은 모두 존재) ③방안1 채택: items.measurement_type→columns 자동 파생, 테이블컬럼 탭은 확인/미세조정용 ④Phase 5.0 신설(3개 작업: 자동파생 JS, 시더 section_fields 추가, 탭 모드 전환) ⑤결정사항 #9/#10 추가 ⑥4개 문서 업데이트(master, mid-inspection, product-inspection, changelog) | 마스터 섹션 7.5, 결정사항 | - | -| 2026-02-12 | Phase 5.2 전체 완료 | 제품검사(FQC) 폼 구현 5/5 완료. ①5.2.1 ProductInspectionTemplateSeeder(template_id:65, 결재3+기본필드7+섹션2+항목11) ②5.2.2 mng 양식 편집/미리보기 검증 ③5.2.3 API bulk-create-fqc+fqc-status 엔드포인트(DocumentService.bulkCreateFqc/fqcStatus) ④5.2.4 React fqcActions.ts+FqcDocumentContent.tsx 신규, InspectionReportModal/ProductInspectionInputModal 듀얼모드(FQC양식/legacy하드코딩) 전환 ⑤5.2.5 InspectionDetail FQC 진행현황 통계바+개소별 상태뱃지(합격/불합격/진행중/미생성)+조회버튼. OrderSettingItem.orderId 기반 자동 활성화, 없으면 legacy fallback | Phase 5.2, 마스터 문서 | - | \ No newline at end of file diff --git a/plans/archive/document-system-product-inspection.md b/plans/archive/document-system-product-inspection.md deleted file mode 100644 index e43682b..0000000 --- a/plans/archive/document-system-product-inspection.md +++ /dev/null @@ -1,375 +0,0 @@ -# Phase 5.2: 제품검사(FQC) 폼 구현 계획 - -> **작성일**: 2026-02-10 -> **마스터 문서**: [`document-system-master.md`](./document-system-master.md) -> **상태**: 🔄 진행 중 -> **선행 조건**: Phase 5.0 (공통: 검사기준서↔컬럼 연동) 완료 필요, Phase 5.1과 병렬 진행 가능 -> **최종 분석일**: 2026-02-12 - ---- - -## 1. 개요 - -### 1.1 목적 -mng에서 제품검사(FQC) 양식 템플릿을 관리하고, React 품질관리 화면(`/quality/inspections`)에서 수주건의 **개소별** 제품검사 문서를 생성/입력/결재할 수 있도록 한다. - -### 1.2 제품검사 = 품질검사 -- 동일 개념. "제품검사(FQC: Final Quality Control)"로 통일 -- 수주건(Order) + 개소(OrderItem) 단위로 관리 -- **전수검사**: 수주 50개소 → 제품검사 문서 50건 생성 - -### 1.3 현재 상태 (2026-02-12 분석) - -| 항목 | 상태 | 비고 | -|------|:----:|------| -| React InspectionManagement | ✅ | `components/quality/InspectionManagement/` - 요청관리 CRUD (목록/등록/상세/캘린더) | -| React ProductInspectionDocument | ✅ | `quality/qms/components/documents/` - 하드코딩 11개 항목 | -| React 제품검사 모달 | ✅ | InspectionReportModal, ProductInspectionInputModal | -| React 문서시스템 뷰어 | ✅ | `components/document-system/` - DocumentViewer, TemplateInspectionContent | -| API Inspection 모델 | ✅ | `/api/v1/inspections` - JSON 기반, 단순 status (waiting→completed) | -| API Document 모델 | ✅ | EAV 정규화, 결재 워크플로우 (DRAFT→APPROVED) | -| mng 양식 템플릿 | ❌ | 미존재 (신규 생성 필요) | -| 개소별 문서 자동생성 | ❌ | 미구현 | - -### 1.4 핵심 발견 사항 - -**두 개의 독립적 검사 시스템 존재:** - -| 시스템 | 데이터 모델 | 특징 | -|--------|------------|------| -| InspectionManagement | `inspections` 테이블 (JSON) | 요청관리, 단순 상태, 결재 없음 | -| Document System | `documents` 테이블 (EAV) | 양식 기반, 결재 워크플로우, 이력 관리 | - -**세 가지 검사항목 세트 발견:** - -| 출처 | 항목 | 용도 | -|------|------|------| -| types.ts ProductInspectionData | 겉모양(가공/재봉/조립/연기차단재/하단마감재), 모터, 재질/치수, 시험 | 공장출하검사 | -| 계획문서 (이 문서) | 외관, 작동, 개폐속도, 방연/차연/내화, 안전, 비상개방, 전기배선, 설치, 부속 | **설치 후 최종검사 ← 채택** | -| QMS ProductInspectionDocument | 가공상태, 외관검사, 절단면, 도포상태, 조립, 슬릿, 규격치수, 마감처리, 내벽/마감/배색시트 | 제조품질검사 | - -### 1.5 통합 전략 (확정) - -> **InspectionManagement의 요청관리 흐름(목록/등록/상세/캘린더)은 유지하고, -> 검사 성적서 생성/입력/결재만 documents 시스템으로 전환한다.** - -- `inspections` 테이블: 검사 요청/일정/상태 관리 (meta 정보) → **유지** -- `documents` 테이블: 검사 성적서 (양식 기반 상세 데이터, 결재) → **신규 연동** -- 연결: `documents.linkable_type = 'order_item'`, `document_links`로 Order/Inspection 연결 -- 기존 InspectionReportModal/ProductInspectionInputModal → TemplateInspectionContent 기반 전환 - -### 1.6 성공 기준 -1. mng에서 제품검사 양식 편집/미리보기 정상 동작 -2. 수주 1건 선택 시 개소(OrderItem) 수만큼 Document 자동생성 -3. 각 Document에 해당 개소의 정보(층-부호, 규격, 수량) 자동매핑 -4. 개소별 검사 데이터 입력/저장/조회 가능 -5. 결재 워크플로우 정상 동작 -6. 기존 InspectionManagement 요청관리 기능 정상 유지 - ---- - -## 2. 데이터 흐름 - -``` -Order (수주) -├─ order_no: "KD-TS-260210-01" -├─ client_name: "발주처명" -├─ site_name: "현장명" -├─ quantity: 50 (총 개소 수) -└─ items: OrderItem[] (50건) - ├─ [0] floor_code="1F", symbol_code="A", specification="W7400×H2950" - ├─ [1] floor_code="1F", symbol_code="B", specification="W5200×H3100" - └─ [49] ... - -제품검사 요청 시: - ↓ -Document (50건 자동생성) -├─ Document[0] -│ ├─ template_id → 제품검사 양식 -│ ├─ linkable_type = 'App\Models\OrderItem' -│ ├─ linkable_id = OrderItem[0].id -│ ├─ document_no = "FQC-260210-01" -│ ├─ title = "제품검사 - 1F-A (W7400×H2950)" -│ └─ document_data (EAV) -│ ├─ 기본필드: 납품명, 제품명, 발주처, LOT NO, 로트크기, 검사일자, 검사자 -│ ├─ 검사데이터: 11개 항목별 적합/부적합 -│ └─ Footer: 종합판정(합격/불합격) -├─ Document[1] → OrderItem[1] -└─ Document[49] → OrderItem[49] - -+ document_links 연결: - ├─ link_key="order" → Order.id - └─ link_key="inspection" → Inspection.id (있는 경우) -``` - -### 2.1 linkable 다형성 연결 - -| 필드 | 값 | 설명 | -|------|-----|------| -| `linkable_type` | `App\Models\OrderItem` | OrderItem 모델 | -| `linkable_id` | OrderItem.id | 개소 PK | - -추가로 `document_links` 테이블을 통해: -- Order(수주) 연결: link_key="order" -- Inspection(검사요청) 연결: link_key="inspection" (InspectionManagement에서 연결 시) -- Process(공정) 연결: link_key="process" (해당되는 경우) - ---- - -## 3. 작업 항목 - -| # | 작업 | 상태 | 완료 기준 | 비고 | -|---|------|:----:|----------|------| -| 5.2.1 | mng 제품검사 양식 시더 생성 | ✅ | ProductInspectionTemplateSeeder 작성 (template_id: 65). 결재3+기본필드7+섹션2+항목11+section_fields | 2026-02-12 | -| 5.2.2 | mng 양식 편집/미리보기 검증 | ✅ | 양식 edit → 미리보기 → 저장 정상 동작 확인 | 2026-02-12 | -| 5.2.3 | API 개소별 문서 일괄생성 | ✅ | `POST /api/v1/documents/bulk-create-fqc` + `GET /api/v1/documents/fqc-status`. DocumentService에 bulkCreateFqc/fqcStatus 추가 | 2026-02-12 | -| 5.2.4 | React 제품검사 모달 → 양식 기반 전환 | ✅ | fqcActions.ts + FqcDocumentContent.tsx 신규. InspectionReportModal/ProductInspectionInputModal 듀얼모드(FQC/legacy) | 2026-02-12 | -| 5.2.5 | 개소 목록/진행현황 UI | ✅ | InspectionDetail에 FQC 진행현황 통계 바 + 개소별 상태 뱃지(합격/불합격/진행중/미생성) + 조회 버튼 | 2026-02-12 | - ---- - -## 4. 제품검사 항목 (설치 후 최종검사 11항목 - 확정) - -| # | 카테고리 | 검사항목 | 검사기준 | 검사방식 | 측정유형 | -|---|---------|---------|---------|---------|---------| -| 1 | 외관 | 외관검사 | 사용상 결함이 없을 것 | visual | checkbox | -| 2 | 기능 | 작동상태 | 정상 작동 | visual | checkbox | -| 3 | 기능 | 개폐속도 | 규정 속도 범위 이내 | visual | checkbox | -| 4 | 성능 | 방연성능 | 기준 적합 | visual | checkbox | -| 5 | 성능 | 차연성능 | 기준 적합 | visual | checkbox | -| 6 | 성능 | 내화성능 | 기준 적합 | visual | checkbox | -| 7 | 안전 | 안전장치 | 정상 작동 | visual | checkbox | -| 8 | 안전 | 비상개방 | 정상 작동 | visual | checkbox | -| 9 | 설치 | 전기배선 | 규정 적합 | visual | checkbox | -| 10 | 설치 | 설치상태 | 규정 적합 | visual | checkbox | -| 11 | 부속 | 부속품 | 누락 없음 | visual | checkbox | - -**특성:** -- 모든 항목이 visual/checkbox (적합/부적합) -- numeric 측정값 없음 → columns 구조가 중간검사보다 훨씬 단순 -- **columns 자동 파생(방안1)**: checkbox → 판정(select) 컬럼 - -**결재라인**: 작성(품질) → 검토(품질QC) → 승인(경영) -**Footer**: 부적합 내용 + 종합판정(합격/불합격) -**자동판정**: 모든 항목 적합 → 합격, 1개라도 부적합 → 불합격 - -### 4.1 양식 시더 구조 (MidInspectionTemplateSeeder 패턴) - -```php -// ProductInspectionTemplateSeeder -[ - 'name' => '제품검사 성적서', - 'category' => '품질/제품검사', - 'title' => '제 품 검 사 성 적 서', - 'company_name' => '케이디산업', - 'footer_remark_label' => '부적합 내용', - 'footer_judgement_label' => '종합판정', - 'footer_judgement_options' => ['합격', '불합격'], - - 'approval_lines' => [ - ['name' => '작성', 'dept' => '품질', 'role' => '담당자', 'sort_order' => 1], - ['name' => '검토', 'dept' => '품질', 'role' => 'QC', 'sort_order' => 2], - ['name' => '승인', 'dept' => '경영', 'role' => '대표', 'sort_order' => 3], - ], - - 'basic_fields' => [ - ['label' => '납품명', 'field_type' => 'text'], - ['label' => '제품명', 'field_type' => 'text'], - ['label' => '발주처', 'field_type' => 'text'], - ['label' => 'LOT NO', 'field_type' => 'text'], - ['label' => '로트크기', 'field_type' => 'text'], - ['label' => '검사일자', 'field_type' => 'date'], - ['label' => '검사자', 'field_type' => 'text'], - ], - - 'sections' => [ - [ - 'title' => '제품검사 기준서', - 'items' => [], // 기준서 섹션 (빈 섹션, 향후 확장) - ], - [ - 'title' => '제품검사 DATA', - 'items' => [ - ['category' => '외관', 'item' => '외관검사', ...], - // ... 11개 항목 (모두 visual/checkbox) - ], - ], - ], - - // columns는 자동 파생 (Phase 5.0 방안1) - // checkbox → [NO, 검사항목, 검사기준, 판정(select)] -] -``` - ---- - -## 5. 개소별 문서 일괄생성 로직 - -### 5.1 API 엔드포인트 (계획) - -``` -POST /api/v1/orders/{orderId}/create-fqc -Request: { template_id: number } -Response: { documents: Document[], created_count: number } -``` - -### 5.2 생성 로직 - -```php -// 1. Order + OrderItems 조회 -$order = Order::with('items')->findOrFail($orderId); - -// 2. 개소별 Document 생성 -foreach ($order->items as $index => $orderItem) { - $document = Document::create([ - 'template_id' => $templateId, - 'document_no' => "FQC-" . date('ymd') . "-" . str_pad($index + 1, 2, '0', STR_PAD_LEFT), - 'title' => "제품검사 - {$orderItem->floor_code}-{$orderItem->symbol_code} ({$orderItem->specification})", - 'status' => DocumentStatus::DRAFT, - 'linkable_type' => OrderItem::class, - 'linkable_id' => $orderItem->id, - ]); - - // 3. 기본필드 자동매핑 - $autoFillData = [ - '납품명' => $order->title, - '제품명' => $orderItem->item_name, - '발주처' => $order->client_name, - 'LOT NO' => $order->order_no, - '로트크기' => "1 EA", - ]; - - // 4. document_data에 기본필드 저장 - foreach ($autoFillData as $key => $value) { - DocumentData::create([ - 'document_id' => $document->id, - 'field_key' => Str::slug($key), - 'field_value' => $value, - ]); - } - - // 5. document_links 연결 - DocumentLink::create([ - 'document_id' => $document->id, - 'link_key' => 'order', - 'linkable_type' => Order::class, - 'linkable_id' => $order->id, - ]); - - // 6. 결재라인 초기화 - // ... (기존 패턴 재사용) -} -``` - -### 5.3 개소 진행현황 조회 - -``` -GET /api/v1/orders/{orderId}/fqc-status -Response: { - total: 50, - inspected: 30, - passed: 28, - failed: 2, - pending: 20, - items: [ - { order_item_id: 1, floor_code: "1F", symbol_code: "A", document_id: 101, status: "APPROVED", result: "합격" }, - { order_item_id: 2, floor_code: "1F", symbol_code: "B", document_id: 102, status: "DRAFT", result: null }, - ... - ] -} -``` - ---- - -## 6. 핵심 파일 경로 - -### mng -| 파일 | 용도 | 상태 | -|------|------|:----:| -| `mng/database/seeders/ProductInspectionTemplateSeeder.php` | 제품검사 양식 시더 | 🔄 작성 중 | -| `mng/database/seeders/MidInspectionTemplateSeeder.php` | 참조 패턴 (중간검사) | ✅ | - -### api -| 파일 | 용도 | 상태 | -|------|------|:----:| -| `api/app/Models/Order.php` | 수주 모델 | ✅ | -| `api/app/Models/OrderItem.php` | 수주 상세(개소) 모델 | ✅ | -| `api/app/Models/Documents/Document.php` | 문서 모델 | ✅ | -| `api/app/Models/Qualitys/Inspection.php` | 기존 검사 모델 (IQC/PQC/FQC) | ✅ | -| `api/app/Http/Controllers/Api/V1/OrderController.php` | 수주 컨트롤러 (createFqc 추가 필요) | ⏳ | -| `api/app/Services/DocumentService.php` | 문서 생성 서비스 | ✅ | - -### react -| 파일 | 용도 | 상태 | -|------|------|:----:| -| `react/src/components/quality/InspectionManagement/` | 품질검사 요청관리 (15+ 파일) | ✅ 유지 | -| `react/src/components/quality/InspectionManagement/InspectionList.tsx` | 검사 목록 | ✅ 유지 | -| `react/src/components/quality/InspectionManagement/InspectionDetail.tsx` | 검사 상세 | 🔄 수정 필요 | -| `react/src/components/quality/InspectionManagement/modals/InspectionReportModal.tsx` | 성적서 모달 | 🔄 전환 필요 | -| `react/src/components/quality/InspectionManagement/modals/ProductInspectionInputModal.tsx` | 입력 모달 | 🔄 전환 필요 | -| `react/src/components/document-system/viewer/DocumentViewer.tsx` | 문서 뷰어 | ✅ | -| `react/src/components/document-system/content/TemplateInspectionContent.tsx` | 양식 기반 렌더링 | ✅ | -| `react/src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx` | 하드코딩 문서 | ❌ 대체 예정 | - ---- - -## 7. 기존 Inspection 모델과의 관계 (통합 전략) - -### 7.1 현재 구조 - -``` -inspections 테이블 (JSON 기반) -├─ inspection_type: IQC/PQC/FQC -├─ status: waiting → in_progress → completed -├─ meta: { ... } (JSON) -├─ items: { ... } (JSON - 검사 결과) -└─ extra: { ... } (JSON) - -documents 테이블 (EAV 정규화) -├─ template_id → document_templates -├─ status: DRAFT → PENDING → APPROVED/REJECTED -├─ linkable_type + linkable_id (다형성) -├─ document_data (EAV - 섹션/컬럼/행 기반) -└─ document_approvals (결재 이력) -``` - -### 7.2 통합 후 구조 - -``` -InspectionManagement (요청관리 레이어) - 유지 -├─ 검사 목록/등록/상세/캘린더 -├─ inspections 테이블 (요청/일정/상태) -└─ API: /api/v1/inspections (CRUD) - -Document System (성적서 레이어) - 신규 연동 -├─ 양식 기반 검사 데이터 입력 -├─ documents 테이블 (EAV + 결재) -├─ linkable → OrderItem (개소별) -└─ document_links → Order, Inspection - -연결 포인트: -├─ InspectionDetail에서 "성적서 작성/조회" 시 → Document System 호출 -├─ InspectionReportModal → TemplateInspectionContent 기반 전환 -└─ ProductInspectionInputModal → 양식 기반 입력으로 전환 -``` - ---- - -## 8. 변경 이력 - -| 날짜 | 내용 | -|------|------| -| 2026-02-10 | Phase 5.2 계획 문서 신규 생성 | -| 2026-02-10 | 방안1 반영: 시더에 section_fields 필수, columns 자동 파생. 선행조건 Phase 5.0 추가 | -| 2026-02-12 | 코드베이스 분석 반영: InspectionManagement 발견, 3개 검사항목 세트 정리, 통합 전략 확정 | -| 2026-02-12 | 설치 후 최종검사 11항목 확정, documents 기반 통합 방향 확정 | -| 2026-02-12 | 5.2.1 ProductInspectionTemplateSeeder 작성 완료 (template_id: 65) | -| 2026-02-12 | 5.2.2 mng 양식 편집/미리보기 검증 완료 | -| 2026-02-12 | 5.2.3 API bulk-create-fqc + fqc-status 엔드포인트 구현 완료 | -| 2026-02-12 | 5.2.4 React fqcActions.ts + FqcDocumentContent + 모달 듀얼모드 전환 완료 | -| 2026-02-12 | 5.2.5 InspectionDetail FQC 진행현황 통계 바 + 개소별 상태/조회 UI 완료 | -| 2026-02-12 | **Phase 5.2 전체 완료 (5/5)** | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/erp-api-development-plan-d1.0-changes.md b/plans/archive/erp-api-development-plan-d1.0-changes.md deleted file mode 100644 index d0920b6..0000000 --- a/plans/archive/erp-api-development-plan-d1.0-changes.md +++ /dev/null @@ -1,559 +0,0 @@ -# SAM ERP API 개발 작업 계획 - D1.0 변경사항 - -> **작성일**: 2025-12-19 -> **기준 문서**: SAM_ERP_Storyboard_D1.0_251218 (38페이지) -> **이전 버전**: SAM_ERP_Storyboard_D0.8_251216 (85페이지) -> **상태**: ✅ Phase 5 완료 | ✅ Phase 6 완료 | ✅ Phase 7 완료 | ✅ Phase 8 완료 - ---- - -## 📚 참고 문서 - -### 핵심 참고 문서 -| 문서 | 경로 | 용도 | -|------|------|------| -| **기존 개발 계획** | [`erp-api-development-plan.md`](./erp-api-development-plan.md) | D0.8 기준 Phase 1-4 | -| **개발 공통 정책** | [`../guides/PROJECT_DEVELOPMENT_POLICY.md`](../guides/PROJECT_DEVELOPMENT_POLICY.md) | 개발 표준 및 정책 | -| **D0.8 스토리보드** | [`SAM_ERP_Storyboard_D0.8_251216/`](./SAM_ERP_Storyboard_D0.8_251216/) | 이전 버전 UI 참조 | -| **D1.0 스토리보드** | [`SAM_ERP_Storyboard_D1.0_251218/`](./SAM_ERP_Storyboard_D1.0_251218/) | 최신 UI/UX 참조 | - -### 기존 코드 참조 -| 항목 | 경로 | 상태 | -|------|------|------| -| `Board` 모델 | `api/app/Models/Boards/Board.php` | ✅ 존재 | -| `BoardSetting` 모델 | `api/app/Models/Boards/BoardSetting.php` | ✅ 존재 | -| `BoardComment` 모델 | `api/app/Models/Boards/BoardComment.php` | ✅ 존재 | -| `Plan` 모델 | `api/app/Models/Tenants/Plan.php` | ✅ 존재 | -| `Subscription` 모델 | `api/app/Models/Tenants/Subscription.php` | ✅ 존재 | -| `PushNotificationSetting` | `api/app/Models/PushNotificationSetting.php` | ✅ 존재 | - ---- - -## 📊 D1.0 개발 범위 요약 - -| Phase | 구분 | 항목수 | 신규 테이블 | API 수 | 상태 | -|-------|------|--------|------------|--------|------| -| Phase 5 | 기본 확장 | 4개 | 1개 | ~14개 | ✅ 완료 | -| Phase 6 | 핵심 신규 | 2개 | 4개 | ~17개 | ✅ 완료 | -| Phase 7 | 게시판 연동 | 2개 | 0개 | ~15개 | ✅ 완료 | -| Phase 8 | SaaS 확장 | 3개 | 1개 | ~10개 | ✅ 완료 | -| **합계** | | **12개** | **~5개** | **~71개** | | - ---- - -## 🚀 Phase 5: D1.0 기본 확장 ✅ 완료 - -> 기존 테이블/모델 활용, API 추가 중심 -> **완료일: 2025-12-22** (기존 구현 확인) - -### 5.1 사용자 초대 기능 ✅ -> 슬라이드: 2 | 경로: 인사관리 > 사원관리 > 사용자 초대 -> **완료일: 2025-12-19** - -- [x] **테이블 생성** - - [x] `user_invitations` 마이그레이션 (2025_12_19_100001) - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** - - [x] `UserInvitation` 모델 (BelongsToTenant) - - [x] 관계 정의 (inviter, role, tenant) - - [x] 토큰 생성 헬퍼 (`generateToken()`) - - [x] 상태 상수 (pending, accepted, expired, cancelled) - -- [x] **서비스 구현** - - [x] `UserInvitationService` 생성 - - [x] 이메일 초대 발송 로직 (`invite()`) - - [x] 초대 수락 로직 (`accept()`) - - [x] 토큰 만료 처리 (`expirePendingInvitations()`) - - [x] 초대 재발송 로직 (`resend()`) - -- [x] **API 엔드포인트** (5개) - - [x] `POST /v1/users/invite` - 사용자 초대 (이메일 발송) - - [x] `GET /v1/users/invitations` - 초대 목록 - - [x] `POST /v1/users/invitations/{token}/accept` - 초대 수락 - - [x] `DELETE /v1/users/invitations/{id}` - 초대 취소 - - [x] `POST /v1/users/invitations/{id}/resend` - 초대 재발송 - -- [x] **Swagger 문서** - - [x] `UserInvitationApi.php` 작성 - - [x] 스키마 정의 (UserInvitation, InviteRequest, AcceptRequest) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 5.2 알림설정 확장 ✅ -> 슬라이드: 19-22 | 경로: 기준정보 > 알림설정 -> **완료일: 2025-12-19** - -- [x] **테이블 확장** - - [x] `notification_settings` 테이블 확인/생성 - -- [x] **모델 생성/수정** - - [x] `NotificationSetting` 모델 (BelongsToTenant) - - [x] 카테고리별 그룹화 메서드 - -- [x] **서비스 구현** - - [x] `NotificationSettingService` 생성 - - [x] 카테고리별 조회/수정 로직 - - [x] 사용자별 기본값 생성 로직 - -- [x] **API 엔드포인트** (3개) - - [x] `GET /v1/users/me/notification-settings` - 알림 설정 조회 - - [x] `PUT /v1/users/me/notification-settings` - 알림 설정 수정 - - [x] `PUT /v1/users/me/notification-settings/bulk` - 알림 일괄 설정 - -- [x] **Swagger 문서** - - [x] `NotificationSettingApi.php` 작성 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 5.3 계정정보 수정 (탈퇴/사용중지) ✅ -> 슬라이드: 24 | 경로: 계정정보 -> **완료일: 2025-12-19** - -- [x] **서비스 구현** - - [x] `AccountService` 생성/확장 - - [x] 회원 탈퇴 로직 (`withdraw()`) - - [x] 사용 중지 로직 (`suspend()`) - - [x] 약관 동의 정보 관리 (`getAgreements()`, `updateAgreements()`) - -- [x] **API 엔드포인트** (4개) - - [x] `POST /v1/account/withdraw` - 회원 탈퇴 - - [x] `POST /v1/account/suspend` - 사용 중지 (특정 테넌트) - - [x] `GET /v1/account/agreements` - 약관 동의 정보 조회 - - [x] `PUT /v1/account/agreements` - 약관 동의 정보 수정 - -- [x] **Swagger 문서** - - [x] `AccountApi.php` 확장 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 5.4 매출 상세 확장 (거래명세서) ✅ -> 슬라이드: 9 | 경로: 회계관리 > 매출관리 > 매출 상세 -> **완료일: 2025-12-19** - -**기존 구성요소:** -- `Sale` 모델, `SaleService` 존재 -- `TaxInvoice` 모델 존재 (세금계산서) - -- [x] **서비스 확장** - - [x] `SaleService` 확장 - - [x] 거래명세서 조회 로직 (`getStatement()`) - - [x] 거래명세서 발행 로직 (`issueStatement()`) - - [x] 거래명세서 이메일 발송 로직 (`sendStatement()`) - -- [x] **API 엔드포인트** (3개) - - [x] `GET /v1/sales/{id}/statement` - 거래명세서 조회 - - [x] `POST /v1/sales/{id}/statement/issue` - 거래명세서 발행 - - [x] `POST /v1/sales/{id}/statement/send` - 거래명세서 이메일 발송 - -- [x] **Swagger 문서** - - [x] `SaleApi.php` 확장 (거래명세서 관련 추가) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -## 🔨 Phase 6: D1.0 핵심 신규 개발 (예상 2-3주) - -> 신규 테이블 + API 전체 신규 구현 - -### 6.1 악성채권 추심관리 ✅ -> 슬라이드: 10-13 | 경로: 회계관리 > 악성채권 추심관리 -> **완료일: 2025-12-19** (commit: c0af888) - -- [x] **테이블 생성** (3개) - - [x] `bad_debts` 마이그레이션 (2025_12_19_160001) - - [x] `bad_debt_documents` 마이그레이션 (2025_12_19_160002) - - [x] `bad_debt_memos` 마이그레이션 (2025_12_19_160003) - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** (3개) - - [x] `BadDebt` 모델 (BelongsToTenant, SoftDeletes) - - 상태 상수: collecting, legal_action, recovered, bad_debt - - 관계: client, assignedUser, creator, documents, memos - - [x] `BadDebtDocument` 모델 - - 문서 유형: business_license, tax_invoice, additional - - [x] `BadDebtMemo` 모델 - -- [x] **서비스 구현** - - [x] `BadDebtService` 생성 (307줄) - - [x] 악성채권 등록/수정/삭제 로직 - - [x] 상태 전이 로직 (추심중→법적조치→회수완료/대손처리) - - [x] 요약 통계 (총 채권, 상태별 금액) - - [x] 서류 첨부/삭제 로직 - - [x] 메모 추가/삭제 로직 - -- [x] **API 엔드포인트** (11개) - - [x] `GET /v1/bad-debts` - 악성채권 목록 - - [x] `POST /v1/bad-debts` - 악성채권 등록 - - [x] `GET /v1/bad-debts/summary` - 상단 요약 (총 채권, 상태별 금액) - - [x] `GET /v1/bad-debts/{id}` - 악성채권 상세 - - [x] `PUT /v1/bad-debts/{id}` - 악성채권 수정 - - [x] `DELETE /v1/bad-debts/{id}` - 악성채권 삭제 - - [x] `PATCH /v1/bad-debts/{id}/toggle` - 설정 ON/OFF - - [x] `POST /v1/bad-debts/{id}/documents` - 서류 첨부 - - [x] `DELETE /v1/bad-debts/{id}/documents/{docId}` - 서류 삭제 - - [x] `POST /v1/bad-debts/{id}/memos` - 메모 추가 - - [x] `DELETE /v1/bad-debts/{id}/memos/{memoId}` - 메모 삭제 - -- [x] **Swagger 문서** - - [x] `BadDebtApi.php` 작성 (433줄) - - [x] 스키마 정의 (BadDebt, BadDebtDocument, BadDebtMemo, Summary) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 6.2 팝업관리 ✅ -> 슬라이드: 15-16 | 경로: 기준정보 > 팝업관리 -> **완료일: 2025-12-19** - -- [x] **테이블 생성** (1개) - - [x] `popups` 마이그레이션 - ```sql - -- popups (팝업) - id, tenant_id, target_type, target_id, - title, content, status, - started_at, ended_at, options, - created_by, updated_by, deleted_by, - created_at, updated_at, deleted_at - ``` - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** - - [x] `Popup` 모델 (BelongsToTenant, SoftDeletes) - - target_type: all, department - - status: active, inactive - - 활성 팝업 스코프 (기간 + 상태 체크) - -- [x] **서비스 구현** - - [x] `PopupService` 생성 - - [x] 팝업 CRUD 로직 - - [x] 활성 팝업 조회 로직 (로그인 후 노출용) - - [x] 기간 유효성 검사 로직 - -- [x] **API 엔드포인트** (6개) - - [x] `GET /v1/popups` - 팝업 목록 (관리자용) - - [x] `POST /v1/popups` - 팝업 등록 - - [x] `GET /v1/popups/active` - 활성 팝업 목록 (사용자용) - - [x] `GET /v1/popups/{id}` - 팝업 상세 - - [x] `PUT /v1/popups/{id}` - 팝업 수정 - - [x] `DELETE /v1/popups/{id}` - 팝업 삭제 - -- [x] **Swagger 문서** - - [x] `PopupApi.php` 작성 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -## 📋 Phase 7: D1.0 게시판 연동 ✅ 완료 -> **완료일: 2025-12-19** - -> 기존 Board 모델 활용, API 엔드포인트 추가 - -**기존 구성요소 (api 프로젝트):** -- `Board` 모델: is_system, board_type, board_code, name, extra_settings -- `BoardSetting` 모델: 커스텀 필드 정의 -- `BoardComment` 모델: 댓글 -- `Post` 모델: 게시글 - -### 7.1 게시판관리 ✅ -> 슬라이드: 17-18 | 경로: 기준정보 > 게시판관리 -> **완료일: 2025-12-19** (기존 구현 활용) - -- [x] **기존 모델 확인/확장** - - [x] `Board` 모델 확인 - - [x] `BoardSetting` 모델 확인 - - [x] 필요 필드 이미 존재 - -- [x] **서비스 구현** - - [x] `BoardService` 존재 (테넌트별 게시판 CRUD 로직) - -- [x] **API 엔드포인트** (5개) - - [x] `GET /v1/boards` - 게시판 목록 - - [x] `POST /v1/boards` - 게시판 생성 - - [x] `GET /v1/boards/{id}` - 게시판 상세 - - [x] `PUT /v1/boards/{id}` - 게시판 수정 - - [x] `DELETE /v1/boards/{id}` - 게시판 삭제 - -- [x] **Swagger 문서** - - [x] `BoardApi.php` 작성 완료 - ---- - -### 7.2 게시판 (사용자용) ✅ -> 슬라이드: 3-7 | 경로: 게시판 -> **완료일: 2025-12-19** - -- [x] **기존 모델 확인/확장** - - [x] `Post` 모델 확인 - - [x] 상단 노출 필드 (is_notice) - - [x] 조회수 필드 (views) - -- [x] **서비스 구현** - - [x] `PostService` 존재 - - [x] 게시글 CRUD 로직 - - [x] 상단 노출 로직 - - [x] 조회수 증가 로직 - - [x] 나의 게시글 조회 로직 ✅ 추가됨 - -- [x] **API 엔드포인트** (10개) - - [x] `GET /v1/boards` - 게시판 목록 (탭용) - - [x] `GET /v1/boards/{code}/posts` - 게시글 목록 - - [x] `POST /v1/boards/{code}/posts` - 게시글 등록 - - [x] `GET /v1/boards/{code}/posts/{id}` - 게시글 상세 - - [x] `PUT /v1/boards/{code}/posts/{id}` - 게시글 수정 - - [x] `DELETE /v1/boards/{code}/posts/{id}` - 게시글 삭제 - - [x] `GET /v1/posts/my` - 나의 게시글 ✅ 신규 추가 - - [x] `GET /v1/boards/{code}/posts/{id}/comments` - 댓글 목록 - - [x] `POST /v1/boards/{code}/posts/{id}/comments` - 댓글 등록 - - [x] `PUT /v1/boards/{code}/posts/{id}/comments/{commentId}` - 댓글 수정 - - [x] `DELETE /v1/boards/{code}/posts/{id}/comments/{commentId}` - 댓글 삭제 - -- [x] **Swagger 문서** - - [x] `BoardApi.php` 작성 완료 - - [x] `PostApi.php` 작성 완료 - ---- - -### 7.3 고객센터 → 게시판관리로 대체 ⏭️ -> 슬라이드: 30-38 | 경로: 고객센터 - -**결정사항:** 고객센터 기능은 기존 게시판관리 시스템으로 구현 -- 공지사항, 이벤트, FAQ, 1:1 문의 → 게시판 유형(board_code)으로 관리 -- 별도 SupportAPI 불필요, 기존 Board/Post API 활용 - ---- - -## 💼 Phase 8: D1.0 SaaS 확장 (예상 1-2주) - -> 기존 Plan/Subscription/Payment 모델 활용 - -### 8.1 구독관리 ✅ -> 슬라이드: 28 | 경로: 구독관리 -> **완료일: 2025-12-22** (기존 구현 확인) - -**기존 구성요소:** -- `Plan` 모델: name, code, price, features(json) -- `Subscription` 모델: tenant_id, plan_id, started_at, ended_at, status -- `DataExport` 모델: 데이터 내보내기 - -- [x] **서비스 확장** - - [x] `SubscriptionService` 확장 (432줄) - - [x] 현재 구독 정보 조회 로직 (`current()`) - - [x] 사용량 조회 로직 (`usage()`) - - [x] 자료 내보내기 로직 (`createExport()`, `getExport()`) - - [x] 서비스 해지 로직 (`cancel()`) - -- [x] **API 엔드포인트** (5개 + 추가 6개) - - [x] `GET /v1/subscriptions/current` - 현재 구독 정보 - - [x] `GET /v1/subscriptions/usage` - 사용량 조회 - - [x] `POST /v1/subscriptions/export` - 자료 내보내기 요청 - - [x] `GET /v1/subscriptions/export/{id}` - 내보내기 상태 조회 - - [x] `POST /v1/subscriptions/{id}/cancel` - 서비스 해지 - - [x] `GET /v1/subscriptions` - 구독 목록 (추가) - - [x] `POST /v1/subscriptions` - 구독 등록 (추가) - - [x] `GET /v1/subscriptions/{id}` - 구독 상세 (추가) - - [x] `POST /v1/subscriptions/{id}/renew` - 구독 갱신 (추가) - - [x] `POST /v1/subscriptions/{id}/suspend` - 일시정지 (추가) - - [x] `POST /v1/subscriptions/{id}/resume` - 재개 (추가) - -- [x] **Swagger 문서** - - [x] `SubscriptionApi.php` 작성 (526줄) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- -### 8.2 결제내역 ✅ -> 슬라이드: 29 | 경로: 결제내역 -> **완료일: 2025-12-22** (기존 구현 확인) - -**기존 구성요소:** -- `Payment` 모델: subscription_id, amount, payment_method, paid_at, status - -- [x] **서비스 확장** - - [x] `PaymentService` 확장 (357줄) - - [x] 결제 내역 목록 조회 로직 (`index()`) - - [x] 거래명세서 생성 로직 (`statement()`) - - [x] 결제 요약 통계 (`summary()`) - -- [x] **API 엔드포인트** (2개 + 추가 6개) - - [x] `GET /v1/payments` - 결제 내역 목록 - - [x] `GET /v1/payments/{id}/statement` - 거래명세서 조회 - - [x] `GET /v1/payments/summary` - 결제 요약 통계 (추가) - - [x] `GET /v1/payments/{id}` - 결제 상세 (추가) - - [x] `POST /v1/payments` - 결제 등록 (추가) - - [x] `POST /v1/payments/{id}/complete` - 완료 처리 (추가) - - [x] `POST /v1/payments/{id}/cancel` - 취소 (추가) - - [x] `POST /v1/payments/{id}/refund` - 환불 (추가) - -- [x] **Swagger 문서** - - [x] `PaymentApi.php` 작성 (455줄) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 8.3 회사 추가 ✅ -> 슬라이드: 25-27 | 경로: 회사정보 -> **완료일: 2025-12-22** - -- [x] **테이블 생성** (1개) - - [x] `company_requests` 마이그레이션 - ```sql - -- company_requests (회사 추가 신청) - id, user_id, business_number, company_name, ceo_name, - address, phone, email, status, message, reject_reason, - barobill_response(json), approved_by, created_tenant_id, - processed_at, created_at, updated_at - ``` - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** - - [x] `CompanyRequest` 모델 - - 상태 상수: pending, approved, rejected - - 관계: user, approver, createdTenant - - 스코프: pending(), approved(), rejected() - -- [x] **서비스 구현** - - [x] `CompanyService` 생성 - - [x] 사업자등록번호 유효성 검사 (바로빌 연동 + 체크섬 검증) - - [x] 회사 추가 신청 로직 - - [x] 신청 승인 로직 (테넌트 자동 생성 + 사용자 연결) - - [x] 신청 반려 로직 - - [x] 신청 목록 조회 (관리자용/사용자용) - -- [x] **API 엔드포인트** (7개) - - [x] `POST /v1/companies/check` - 사업자등록번호 유효성 검사 - - [x] `POST /v1/companies/request` - 회사 추가 신청 - - [x] `GET /v1/companies/requests` - 신청 목록 (관리자용) - - [x] `GET /v1/companies/requests/{id}` - 신청 상세 - - [x] `POST /v1/companies/requests/{id}/approve` - 승인 - - [x] `POST /v1/companies/requests/{id}/reject` - 반려 - - [x] `GET /v1/companies/my-requests` - 내 신청 목록 - -- [x] **Swagger 문서** - - [x] `CompanyApi.php` 작성 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -## 📋 기획 확인 필요 항목 - -> ⚠️ API 구현 전 비즈니스 로직 확정 필요 - -### D1.0 신규 확인 필요 -- [ ] 사용자 초대 시 권한 범위 (테넌트 단위 vs 전사) -- [ ] 악성채권 자동 판정 조건 (연체일수 기준, 기본 90일?) -- [ ] 팝업 노출 우선순위 (복수 팝업 시) -- [ ] 서비스 해지 시 데이터 보관 기간 -- [ ] 자료 내보내기 포맷 (Excel, CSV, JSON) -- [ ] 상단 노출 게시글 최대 개수 (기본 5개) -- [ ] 1:1 문의 상담분류 목록 (문의하기, 신고하기, 건의사항, 서비스 오류) - -### 기존 확인 사항 (D0.8) -- [ ] 테넌트: 신청→승인→만료→해지 전이 조건 -- [ ] 전자결재→회계: 지출결의서 승인 시 출금 자동 생성? -- [ ] 바로빌 API 비용 확인 - ---- - -## 📝 작업 일지 - -### 2025-12-19 -- [x] D1.0 스토리보드 분석 완료 (38페이지) -- [x] D0.8 대비 변경사항 식별 (신규 8개, 수정 4개) -- [x] D1.0 개발 계획 문서 작성 (Phase 5-8) -- [x] 기존 코드베이스 분석 (Board, Plan, Subscription 모델 확인) -- [x] Phase 6.1 악성채권 추심관리 API 개발 완료 (commit: c0af888) -- [x] Phase 6.2 팝업관리 API 개발 완료 -- [x] Phase 7.1 게시판관리 - 기존 구현 확인 완료 -- [x] Phase 7.2 게시판(사용자용) - 기존 구현 확인 + `/posts/my` API 추가 (commit: c15a245) -- [x] Phase 7.3 고객센터 → 게시판관리로 대체 결정 -- [x] Phase 8 SaaS 확장 분석 시작 - -### 2025-12-22 -- [x] Phase 8.1 구독관리 - 기존 구현 확인 완료 -- [x] Phase 8.2 결제내역 - 기존 구현 확인 완료 -- [x] Phase 8.3 회사 추가 API 개발 완료 (commit: 7781253) - - company_requests 테이블 생성 - - CompanyRequest 모델 생성 - - CompanyService 생성 (바로빌 연동 + 테넌트 생성) - - 7개 API 엔드포인트 구현 - - Swagger 문서 작성 -- [x] Phase 5 전체 기존 구현 확인 완료 - - 5.1 사용자 초대: 5개 API (invite, invitations, accept, cancel, resend) - - 5.2 알림설정: 3개 API (notification-settings, update, bulk) - - 5.3 계정정보: 4개 API (withdraw, suspend, agreements) - - 5.4 매출 거래명세서: 3개 API (statement, issue, send) -- [x] D1.0 Phase 5-8 전체 API 개발 완료! - ---- - -## ✅ 완료 기준 - -### Phase 5 완료 조건 (기본 확장) ✅ -- [x] 사용자 초대 API 구현 완료 ✅ 2025-12-19 -- [x] 알림설정 API 확장 완료 ✅ 2025-12-19 -- [x] 계정정보 API 확장 완료 ✅ 2025-12-19 -- [x] 매출 거래명세서 API 구현 완료 ✅ 2025-12-19 -- [x] Swagger 문서 완성 ✅ 2025-12-19 -- [x] Pint 코드 포맷팅 완료 ✅ - -### Phase 6 완료 조건 (핵심 신규) -- [x] 악성채권 추심관리 전체 구현 ✅ 2025-12-18 -- [x] 팝업관리 전체 구현 ✅ 2025-12-19 -- [x] 마이그레이션 검증 완료 -- [x] Swagger 문서 완성 - -### Phase 7 완료 조건 (게시판 연동) ✅ -- [x] 게시판관리 API 구현 완료 ✅ 2025-12-19 -- [x] 게시판 (사용자용) API 구현 완료 ✅ 2025-12-19 -- [x] 고객센터 → 게시판관리로 대체 결정 ✅ 2025-12-19 - -### Phase 8 완료 조건 (SaaS 확장) ✅ -- [x] 구독관리 API 구현 완료 ✅ 2025-12-22 -- [x] 결제내역 API 구현 완료 ✅ 2025-12-22 -- [x] 회사 추가 API 구현 완료 ✅ 2025-12-22 -- [x] 자료 내보내기 기능 구현 ✅ (SubscriptionService에 포함) - -### 전체 완료 조건 -- [ ] 모든 D1.0 API 구현 완료 (~71개) -- [ ] Swagger 문서 100% -- [ ] 통합 테스트 통과 -- [ ] 프론트엔드 연동 준비 완료 - ---- - -## 🔗 관련 링크 - -- **기존 개발 계획**: [`erp-api-development-plan.md`](./erp-api-development-plan.md) -- **API Swagger UI**: http://sam.kr/api-docs/index.html -- **개발 공통 정책**: [`../guides/PROJECT_DEVELOPMENT_POLICY.md`](../guides/PROJECT_DEVELOPMENT_POLICY.md) -- **D1.0 스토리보드**: [`SAM_ERP_Storyboard_D1.0_251218/`](./SAM_ERP_Storyboard_D1.0_251218/) \ No newline at end of file diff --git a/plans/archive/fcm-user-targeted-notification-plan.md b/plans/archive/fcm-user-targeted-notification-plan.md deleted file mode 100644 index 59389e2..0000000 --- a/plans/archive/fcm-user-targeted-notification-plan.md +++ /dev/null @@ -1,369 +0,0 @@ -# FCM 사용자별 알림 발송 계획 - -> **작성일**: 2026-01-28 -> **목적**: FCM 푸시 알림을 테넌트 전체 브로드캐스트에서 사용자별 타겟 발송으로 변경 -> **상태**: ✅ 구현 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 - FCM 발송 로직 수정 완료 | -| **다음 작업** | 테스트 검증 | -| **진행률** | 8/8 (100%) | -| **마지막 업데이트** | 2026-01-28 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 TodayIssue 생성 시 FCM 푸시 알림이 **테넌트 전체 사용자** 중 알림 설정이 켜진 모든 사용자에게 발송됨. - -**문제점**: -- 결재요청 알림이 결재자가 아닌 사람에게도 발송됨 -- 기안 승인/반려/완료 알림이 기안자가 아닌 사람에게도 발송됨 -- 불필요한 알림으로 사용자 경험 저하 - -### 1.2 목표 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 목표 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 이슈 타입에 따라 특정 대상자에게만 FCM 발송 │ -│ 2. 사용자별 알림 설정(ON/OFF)이 정상 동작하도록 보장 │ -│ 3. 근태 알림은 제외 (정책 미확정) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 발송 대상 정책 - -| 이슈 타입 | 현재 | 변경 후 대상 | -|-----------|------|-------------| -| **결재요청** | 테넌트 전체 | **결재자(나)** - ApprovalStep.user_id | -| **기안 승인** | 테넌트 전체 | **기안자** - Approval.drafter_id | -| **기안 반려** | 테넌트 전체 | **기안자** - Approval.drafter_id | -| **기안 완료** | 테넌트 전체 | **기안자** - Approval.drafter_id | -| 수주등록 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 추심이슈 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 안전재고 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 지출승인 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 세금신고 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 신규업체 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 입금 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 출금 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| **근태 알림** | - | **제외** (정책 미확정) | - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 필드 추가, 로직 수정, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션, 새 테이블/컬럼 | **필수** | -| 🔴 금지 | 기존 테이블 구조 변경, 파괴적 변경 | 별도 협의 | - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 데이터베이스 변경 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | TodayIssue 테이블에 `target_user_id` 컬럼 추가 | ✅ | nullable, FK | -| 1.2 | 마이그레이션 파일 생성 | ✅ | 2026_01_28_132426 | - -### 2.2 Phase 2: 모델 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | TodayIssue 모델에 target_user_id 추가 | ✅ | fillable, relation, scopes | -| 2.2 | TodayIssue::createIssue() 메서드에 targetUserId 파라미터 추가 | ✅ | | - -### 2.3 Phase 3: Observer 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | handleApprovalStepChange() - 결재요청 시 결재자 지정 | ✅ | step->user_id 전달 | -| 3.2 | 기안 승인/반려/완료 알림 추가 (기안자 지정) | ✅ | ApprovalIssueObserver 신규 | - -### 2.4 Phase 4: FCM 발송 로직 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | sendFcmNotification() - target_user_id 있으면 해당 사용자만 | ✅ | | -| 4.2 | getEnabledUserTokens() - 특정 사용자 필터링 로직 추가 | ✅ | | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: 데이터베이스 변경 -├── today_issues 테이블에 target_user_id 컬럼 추가 -├── 마이그레이션 실행 -└── 검증: 테이블 구조 확인 - -Step 2: TodayIssue 모델 수정 -├── target_user_id fillable 추가 -├── targetUser() relation 추가 -└── createIssue() 파라미터 추가 - -Step 3: TodayIssueObserverService 수정 -├── createIssueWithFcm() 파라미터 추가 -├── handleApprovalStepChange() 수정 - 결재자 지정 -├── 기안 상태 변경 알림 추가 (신규) -└── 근태 알림 비활성화 - -Step 4: FCM 발송 로직 수정 -├── sendFcmNotification() 수정 -├── getEnabledUserTokens() 수정 - targetUserId 파라미터 추가 -└── 검증: 대상자만 수신 확인 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: 데이터베이스 변경 - -**마이그레이션 파일**: -```php -// database/migrations/xxxx_add_target_user_id_to_today_issues_table.php - -Schema::table('today_issues', function (Blueprint $table) { - $table->unsignedBigInteger('target_user_id') - ->nullable() - ->after('source_id') - ->comment('특정 대상 사용자 ID (null이면 테넌트 전체)'); - - $table->foreign('target_user_id') - ->references('id') - ->on('users') - ->onDelete('cascade'); - - $table->index(['tenant_id', 'target_user_id']); -}); -``` - -### 4.2 Phase 2: TodayIssue 모델 수정 - -```php -// app/Models/Tenants/TodayIssue.php - -protected $fillable = [ - // ... 기존 필드 - 'target_user_id', // 추가 -]; - -public function targetUser(): BelongsTo -{ - return $this->belongsTo(User::class, 'target_user_id'); -} - -public static function createIssue( - int $tenantId, - string $sourceType, - ?int $sourceId, - string $badge, - string $content, - ?string $path = null, - bool $needsApproval = false, - ?\DateTime $expiresAt = null, - ?int $targetUserId = null // 추가 -): self { - // ... 기존 로직 + target_user_id 저장 -} -``` - -### 4.3 Phase 3: Observer 수정 - -**결재요청 - 결재자에게만**: -```php -// handleApprovalStepChange() 수정 - -$this->createIssueWithFcm( - tenantId: $approval->tenant_id, - sourceType: TodayIssue::SOURCE_APPROVAL, - sourceId: $step->id, - badge: TodayIssue::BADGE_APPROVAL_REQUEST, - content: __('message.today_issue.approval_pending', [...]), - path: '/approval/inbox', - needsApproval: true, - expiresAt: null, - targetUserId: $step->user_id // 결재자 -); -``` - -**기안 승인/반려/완료 - 기안자에게만** (신규): -```php -// handleApprovalStatusChange() 신규 메서드 - -public function handleApprovalStatusChange(Approval $approval): void -{ - $badge = match($approval->status) { - 'approved' => TodayIssue::BADGE_DRAFT_APPROVED, - 'rejected' => TodayIssue::BADGE_DRAFT_REJECTED, - 'completed' => TodayIssue::BADGE_DRAFT_COMPLETED, - default => null, - }; - - if (!$badge) return; - - $this->createIssueWithFcm( - tenantId: $approval->tenant_id, - sourceType: TodayIssue::SOURCE_APPROVAL, - sourceId: $approval->id, - badge: $badge, - content: __('message.today_issue.'.$approval->status, [...]), - path: '/approval/draft', - needsApproval: false, - expiresAt: Carbon::now()->addDays(7), - targetUserId: $approval->drafter_id // 기안자 - ); -} -``` - -### 4.4 Phase 4: FCM 발송 로직 수정 - -```php -// sendFcmNotification() 수정 - -public function sendFcmNotification(TodayIssue $issue): void -{ - // target_user_id가 있으면 해당 사용자만, 없으면 테넌트 전체 - $tokens = $this->getEnabledUserTokens( - $issue->tenant_id, - $issue->notification_type, - $issue->target_user_id // 추가 - ); - - // ... 기존 발송 로직 -} - -// getEnabledUserTokens() 수정 - -private function getEnabledUserTokens( - int $tenantId, - string $notificationType, - ?int $targetUserId = null // 추가 -): array { - $query = PushDeviceToken::withoutGlobalScopes() - ->where('tenant_id', $tenantId) - ->where('is_active', true) - ->whereNull('deleted_at'); - - // 특정 대상자가 지정된 경우 - if ($targetUserId !== null) { - $query->where('user_id', $targetUserId); - } - - $tokens = $query->get(); - - // 알림 설정 확인 후 필터링 - $enabledTokens = []; - foreach ($tokens as $token) { - if ($this->isNotificationEnabledForUser($tenantId, $token->user_id, $notificationType)) { - $enabledTokens[] = $token->token; - } - } - - return $enabledTokens; -} -``` - ---- - -## 5. 제외 항목 - -### 5.1 근태 알림 (정책 미확정) - -다음 알림 타입은 이번 작업에서 **제외**: -- 연차 알림 -- 출근 알림 -- 지각 알림 -- 결근 알림 - -**사유**: 정책이 모호하여 추후 별도 작업 - -### 5.2 알림 소리 커스터마이징 - -현재는 **하드코딩된 채널별 알림음** 사용: -- `push_urgent`: 긴급 (신규업체) -- `push_payment`: 결재 -- `push_sales_order`: 수주 -- `push_default`: 기타 - -**추후 작업**: 사용자별 알림 설정의 `soundType` 값 기준으로 발송 - ---- - -## 6. 영향받는 파일 - -### API (api/) - -| 파일 | 변경 내용 | -|------|----------| -| `database/migrations/2026_01_28_132426_add_target_user_id_to_today_issues_table.php` | 신규 - 마이그레이션 | -| `app/Models/Tenants/TodayIssue.php` | target_user_id 추가, 신규 뱃지 상수, targetUser 관계, forUser/targetedTo 스코프 | -| `app/Services/TodayIssueObserverService.php` | createIssueWithFcm, sendFcmNotification, getEnabledUserTokens 수정, handleApprovalStatusChange 추가 | -| `app/Observers/TodayIssue/ApprovalIssueObserver.php` | 신규 - 기안 상태 변경 Observer | -| `app/Providers/AppServiceProvider.php` | ApprovalIssueObserver 등록 | -| `lang/ko/message.php` | 신규 메시지 키 추가 (draft_approved/rejected/completed) | - -### React (react/) - 변경 없음 - -프론트엔드 알림 설정 UI는 이미 사용자별로 구현되어 있음. - ---- - -## 7. 검증 방법 - -### 7.1 테스트 시나리오 - -| # | 시나리오 | 예상 결과 | -|---|----------|----------| -| 1 | A가 B에게 결재 요청 | B에게만 FCM 발송 | -| 2 | B가 A의 기안 승인 | A에게만 FCM 발송 | -| 3 | B가 A의 기안 반려 | A에게만 FCM 발송 | -| 4 | 수주 등록 | 테넌트 전체 (알림 ON인 사용자만) | -| 5 | A가 알림 OFF → 수주 등록 | A에게는 발송 안됨 | - -### 7.2 성공 기준 - -- [ ] 결재요청 알림이 결재자에게만 발송됨 -- [ ] 기안 상태 변경 알림이 기안자에게만 발송됨 -- [ ] 사용자별 알림 설정(ON/OFF)이 정상 동작함 -- [ ] 기존 브로드캐스트 이슈(수주, 입금 등)는 정상 동작함 - ---- - -## 8. 참고 문서 - -- `api/app/Services/TodayIssueObserverService.php` - 현재 발송 로직 -- `api/app/Models/NotificationSetting.php` - 알림 설정 모델 -- `react/src/components/settings/NotificationSettings/types.ts` - 프론트엔드 알림 설정 타입 - ---- - -## 9. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-28 | - | 계획 문서 초안 작성 | - | - | -| 2026-01-28 | Phase 1 | target_user_id 컬럼 추가 마이그레이션 | migrations/2026_01_28_132426_* | ✅ | -| 2026-01-28 | Phase 2 | TodayIssue 모델 수정 (fillable, relation, scopes) | TodayIssue.php | ✅ | -| 2026-01-28 | Phase 3 | Observer 수정 (결재자/기안자 타겟팅) | TodayIssueObserverService.php, ApprovalIssueObserver.php | ✅ | -| 2026-01-28 | Phase 4 | FCM 발송 로직 수정 | TodayIssueObserverService.php | ✅ | -| 2026-01-28 | 신규 | ApprovalIssueObserver 생성 | ApprovalIssueObserver.php | ✅ | -| 2026-01-28 | i18n | 기안 상태 알림 메시지 추가 | lang/ko/message.php | ✅ | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/formula-engine-real-data-plan.md b/plans/archive/formula-engine-real-data-plan.md deleted file mode 100644 index 7114c42..0000000 --- a/plans/archive/formula-engine-real-data-plan.md +++ /dev/null @@ -1,1077 +0,0 @@ -# 수식 엔진 실제 데이터 연동 계획 - -> **작성일**: 2026-02-19 -> **목적**: FormulaEvaluatorService의 테스트 데이터(SF-/SM-)를 실제 품목(BD-)으로 재구성 -> **기준 문서**: `docs/features/quotes/README.md`, `docs/rules/item-policy.md` -> **상태**: ✅ 완료 (Phase 1-3,5 완료 / Phase 4 후순위 보류) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 문서 최종 업데이트 및 검증 결과 반영 | -| **다음 작업** | 없음 (Phase 4 Generic 데이터는 후순위 보류) | -| **진행률** | 4/5 완료 (Phase 1-3,5 ✅ / Phase 4 ⏭️ 후순위) | -| **마지막 업데이트** | 2026-02-20 17:00 | - ---- - -## 1. 개요 - -### 1.1 배경 - -수식 엔진(FormulaEvaluatorService)에는 두 가지 실행 경로가 있다: -- **Generic 경로**: `quote_formula_*` 4개 테이블 기반 (데이터 드리븐) -- **Kyungdong 경로**: `KyungdongFormulaHandler` 코드 기반 (tenant_id=287 전용) - -**현재 문제:** -1. Generic 경로의 `quote_formula_items` (24건)이 모두 삭제된 SF-/SM- 테스트 품목을 참조 -2. `quote_formula_ranges` (12건)도 모두 SF- 코드 반환 -3. `quote_formula_mappings`는 비어있음 -4. Mapping 수식(id:20,21)이 참조하는 product_id 468, 473도 삭제됨 -5. Kyungdong 핸들러는 BD- 품목을 참조하지만, EST- 코드 일부가 items 테이블에 미등록 -6. 핸들러가 `KyungdongFormulaHandler`로 하드코딩 → 업체 추가 시 확장 불가 구조 - -### 1.2 두 경로 비교 - -| 구분 | Generic 경로 | Kyungdong 경로 | -|------|-------------|---------------| -| **진입 조건** | 전용 핸들러 없는 tenant | 전용 핸들러 있는 tenant | -| **BOM 구성** | quote_formula_items + items.bom 전개 | 코드 기반 동적 조립 | -| **모델 인식** | 없음 (단일 수식 세트) | 모델/마감/타입별 분기 | -| **아이템 참조** | SF-/SM- (삭제됨) | BD- 동적 코드 조합 + EST- 코드 | -| **단가 조회** | prices 테이블 + items.attributes | EstimatePriceService | -| **핸들러 해석** | FormulaHandlerFactory → null → Generic | FormulaHandlerFactory → Tenant{id}/FormulaHandler | -| **상태** | ⏭️ FG.bom 비어있음 (후순위) | ✅ 정비 완료 | - -### 1.3 실행 흐름 (MNG → API) - -#### 현재 (Before) -``` -FormulaEvaluatorService::calculateBomWithDebug() - │ - ├─ if ($tenantId === 287) ← 하드코딩! - │ └─ new KyungdongFormulaHandler() ← 직접 생성! - │ - └─ else → Generic 10단계 -``` - -#### 목표 (After) - Strategy + Factory, Zero Config -``` -[MNG 품목관리 UI] - │ 사용자가 FG 선택 + W0/H0/QTY/MP 입력 - ▼ -ItemManagementApiController::calculateFormula() (mng, 라인 60-86) - │ $item->code, {W0, H0, QTY, MP}, session('selected_tenant_id') - ▼ -FormulaApiService::calculateBom() (mng, 라인 24-82) - │ POST https://nginx/api/v1/quotes/calculate/bom - │ Headers: X-API-KEY, X-TENANT-ID - ▼ -FormulaEvaluatorService::calculateBomWithDebug() (api, 라인 592-596) - │ - ├─ FormulaHandlerFactory::make($tenantId) - │ │ class_exists("Handlers\Tenant{$tenantId}\FormulaHandler") ? - │ │ - │ ├─ 핸들러 존재 → calculateTenantBom($handler, ...) - │ │ └─ Tenant287/FormulaHandler::calculateDynamicItems() - │ │ ├─ calculateSteelItems() → BD- 절곡품 (10종) - │ │ ├─ calculatePartItems() → EST- 부자재 (5종) - │ │ └─ 모터/제어기/주자재/검사비 - │ │ - │ └─ 핸들러 없음 (null) → 10단계 Generic 계산 (라인 613-791) - │ └─ quote_formula_* 테이블 (DB 드리븐) - │ - ▼ -[BOM 결과 JSON 반환] -``` - -#### 핸들러 자동 발견 원리 -``` -FormulaHandlerFactory::make(287) - → class_exists("App\Services\Quote\Handlers\Tenant287\FormulaHandler") - → YES → new Tenant287\FormulaHandler() - → 인터페이스 TenantFormulaHandler 구현 보장 - -FormulaHandlerFactory::make(999) - → class_exists("App\Services\Quote\Handlers\Tenant999\FormulaHandler") - → NO → return null → Generic DB 경로 -``` - -**업체 추가 시**: `Handlers/Tenant{id}/FormulaHandler.php` 파일 1개만 생성. 설정/매핑 불필요. - -### 1.4 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 업체별 핸들러 구조화 (Tenant{id} 기반 자동 발견, Zero Config) │ -│ 2. 경동(287) 핸들러가 실제 운영 로직 (우선 정비) │ -│ 3. Generic 경로는 핸들러 없는 테넌트용 (DB 드리븐, 후순위) │ -│ 4. 품목 마스터에 실제 품목이 모두 등록되어야 함 │ -│ 5. 수식 데이터는 실제 품목 코드만 참조 │ -│ 6. 기존 테스트 데이터는 삭제하지 않음 (완전 이관 후 별도 삭제) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.5 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | items 테이블에 EST- 품목 등록, 핸들러 디렉토리 구조 변경(이동) | 불필요 | -| ⚠️ 컨펌 필요 | 인터페이스/팩토리 신규 생성, FormulaEvaluatorService 분기 로직 변경, quote_formula_* 데이터 추가 | **필수** | -| 🔴 금지 | 테이블 스키마 변경, 핸들러 핵심 계산 로직 변경 | 별도 협의 | - ---- - -## 2. 현황 분석 - -### 2.1 items 테이블 현황 (tenant_id=287) - -| 코드 접두어 | item_type | 건수 | 설명 | 상태 | -|------------|-----------|------|------|------| -| FG- | FG | 18 | 완제품 (7모델 × 타입/마감 조합) | ✅ 정상 | -| BD- | PT | 58 | 절곡물 (모델별 가이드레일/케이스/마구리 등) | ✅ 정상 | -| PT- (레거시) | PT | ~650 | 레거시 부품 (5자리 숫자 코드) | ✅ 정상 | -| RM- | RM | 28 | 원자재 | ✅ 정상 | -| SM- | SM | 61 | 부자재 (레거시) | ✅ 정상 | -| CS- | CS | 4 | 소모품 | ✅ 정상 | -| SF- | - | 0 | 삭제됨 (테스트 데이터) | ❌ 삭제 완료 | -| EST- | PT | 72 | 부자재 (모터/제어기/샤프트/앵글/파이프/원자재 등) | ✅ 등록 완료 | - -### 2.2 KyungdongFormulaHandler가 참조하는 미등록 품목 - -> **중요**: 핸들러는 `EST-` 접두어를 사용 (이전 문서의 `ST-`는 오류) - -#### EST- 코드 (items 미등록, 핸들러가 동적 생성) - -| 코드 패턴 | 라인 | 메서드 | 용도 | 대안 | -|-----------|------|--------|------|------| -| `EST-SMOKE-케이스용` | 519 | calculateSteelItems | 케이스용 연기차단재 | `BD-케이스용 연기차단재` (id:15587) | -| `EST-SMOKE-레일용` | 557 | calculateSteelItems | 가이드레일용 연기차단재 | `BD-가이드레일용 연기차단재` (id:15572) | -| `EST-SHAFT-{size}인치-{length}` | 795 | calculatePartItems | 감기샤프트 | 신규 등록 | -| `EST-PIPE-1.4-{length}` | 854,868 | calculatePartItems | 앵글파이프 | 신규 등록 | -| `EST-ANGLE-BRACKET-{type}` | 891 | calculatePartItems | 모터받침 앵글 | 신규 등록 | -| `EST-ANGLE-MAIN-{type}-{size}` | 912 | calculatePartItems | 부자재 앵글 | 신규 등록 | -| `EST-INSPECTION` | 1010 | calculateDynamicItems | 검사비 | 신규 등록 | -| `EST-RAW-스크린-{type}` | 1019 | calculateDynamicItems | 스크린 원단 | 신규 등록 | -| `EST-RAW-슬랫-{type}` | 1025 | calculateDynamicItems | 슬랫 원단 | 신규 등록 | -| `EST-MOTOR-{voltage}-{capacity}` | 1044 | calculateDynamicItems | 모터 | 신규 등록 | -| `EST-CTRL-{type}` | 1062 | calculateDynamicItems | 제어기 | 신규 등록 | -| `EST-CTRL-뒷박스` | 1087 | calculateDynamicItems | 뒷박스 제어기 | 신규 등록 | - -#### 레거시 숫자 코드 (items 등록됨) - -| 코드 | 라인 | items.id | items.name | item_type | unit | 용도 | -|------|------|----------|-----------|-----------|------|------| -| `00035` | 564 | 14939 | 철재용하장바(SUS)3000 | PT | EA | 하장바 SUS | -| `00036` | 564 | 14940 | 철재용하장바(SUS1.2T) | SM | M | 하장바 EGI | -| `00021` | 619 | 14928 | 평철12T | PT | M | 무게평철12T | -| `90201` | 631 | 15188 | KD환봉(30파이) | PT | EA | 환봉 30파이 (기본) | -| `90202` | 628 | 15189 | KD환봉 | PT | EA | 환봉 35파이 | -| `90203` | 629 | 15190 | KD환봉 | PT | EA | 환봉 45파이 | -| `90204` | 630 | 15191 | KD환봉 | PT | EA | 환봉 50파이 | -| `00013` | - | 14922 | 점검구3 | PT | EA | 점검구 (핸들러에서 미사용) | - -### 2.3 quote_formula_* 현황 - -#### quote_formulas (21건, tenant_id=1) - -| id | type | variable | name | formula | output_type | -|----|------|----------|------|---------|-------------| -| 1 | input | PC | 제품 카테고리 | (없음) | variable | -| 2 | input | W0 | 오픈사이즈 폭 | (없음) | variable | -| 3 | input | H0 | 오픈사이즈 높이 | (없음) | variable | -| 4 | input | GT | 가이드레일 설치유형 | (없음) | variable | -| 5 | input | MP | 모터 전원 | (없음) | variable | -| 6 | input | CT | 연동제어기 | (없음) | variable | -| 7 | input | QTY | 수량 | (없음) | variable | -| 8 | calculation | W1_SCREEN | 제작폭 W1 (스크린) | W0 + 140 | variable | -| 9 | calculation | W1_STEEL | 제작폭 W1 (철재) | W0 + 110 | variable | -| 10 | calculation | H1 | 제작높이 H1 | H0 + 350 | variable | -| 11 | calculation | W | 제작폭 (W) | IF(PC=="스크린", W0+140, W0+110) | variable | -| 12 | calculation | H | 제작높이 (H) | H0 + 350 | variable | -| 13 | calculation | M | 면적 (M) | W * H / 1000000 | variable | -| 14 | calculation | K_SCREEN | 중량 K (스크린) | M * 2 + W0 / 1000 * 14.17 | variable | -| 15 | calculation | K_STEEL | 중량 K (철재) | M * 25 | variable | -| 16 | calculation | K | 중량 (K) | IF(PC=="스크린", M*2+W0/1000*14.17, M*25) | variable | -| 17 | range | MOTOR | 모터 자동선택 | K | item | -| 18 | range | GUIDE | 가이드레일 자동선택 | H | item | -| 19 | range | CASE | 케이스 자동선택 | W | item | -| 20 | mapping | BOM_SCR_001 | FG-SCR-001 BOM 매핑 | (없음) | item | -| 21 | mapping | BOM_STL_001 | FG-STL-001 BOM 매핑 | (없음) | item | - -- id 20: product_id=468 (삭제됨) -- id 21: product_id=473 (삭제됨) - -#### quote_formula_items (24건) - 전부 삭제된 코드 - -| id | formula_id | item_code | item_name | sort | -|----|-----------|-----------|-----------|------| -| 1 | 20 | SF-SCR-F01 | 스크린 원단 | 1 | -| 2 | 20 | SF-SCR-F02 | 가이드레일 (좌) | 2 | -| 3 | 20 | SF-SCR-F03 | 가이드레일 (우) | 3 | -| 4 | 20 | SF-SCR-F04 | 케이스 | 4 | -| 5 | 20 | SF-SCR-F05 | 하부프레임 | 5 | -| 6 | 20 | SF-SCR-M01 | 모터 (소형) | 6 | -| 7 | 20 | SF-SCR-C01 | 제어반 | 7 | -| 8 | 20 | SF-SCR-S01 | 셋팅박스 | 8 | -| 9 | 20 | SF-SCR-SW01 | 권선드럼 | 9 | -| 10 | 20 | SF-SCR-B01 | 브라켓 세트 | 10 | -| 11 | 20 | SF-SCR-SW01 | 스위치 | 11 | -| 12 | 20 | SM-B002 | 볼트 M8x25 | 12 | -| 13 | 20 | SM-N002 | 너트 M8 | 13 | -| 14 | 20 | SM-W002 | 와셔 M8 | 14 | -| 15 | 21 | SF-STL-P01 | 도어 패널 | 1 | -| 16 | 21 | SF-STL-F01 | 문틀 프레임 | 2 | -| 17 | 21 | SF-STL-G01 | 유리창 | 3 | -| 18 | 21 | SF-STL-H01 | 힌지 | 4 | -| 19 | 21 | SF-STL-L01 | 잠금장치 | 5 | -| 20 | 21 | SF-STL-C01 | 도어클로저 | 6 | -| 21 | 21 | SF-STL-S01 | 실링재 | 7 | -| 22 | 21 | SF-STL-PT01 | 파우더 도장 | 8 | -| 23 | 21 | SM-B002 | 볼트 M8x25 | 9 | -| 24 | 21 | SM-N002 | 너트 M8 | 10 | - -#### quote_formula_ranges (12건) - 전부 삭제된 코드 - -| id | formula_id | condition_variable | min | max | result_value | -|----|-----------|-------------------|-----|-----|--------------| -| 1 | 17 (MOTOR) | K | 0 | 30 | SF-SCR-M01 | -| 2 | 17 | K | 30 | 50 | SF-SCR-M02 | -| 3 | 17 | K | 50 | 80 | SF-SCR-M03 | -| 4 | 17 | K | 80 | 9999 | SF-SCR-M04 | -| 5 | 18 (GUIDE) | H | 0 | 2500 | SF-SCR-F02 | -| 6 | 18 | H | 2500 | 3500 | SF-SCR-F02 | -| 7 | 18 | H | 3500 | 4500 | SF-SCR-F02 | -| 8 | 18 | H | 4500 | 9999 | SF-SCR-F02 | -| 9 | 19 (CASE) | W | 0 | 2000 | SF-SCR-F04 | -| 10 | 19 | W | 2000 | 3000 | SF-SCR-F04 | -| 11 | 19 | W | 3000 | 4000 | SF-SCR-F04 | -| 12 | 19 | W | 4000 | 9999 | SF-SCR-F04 | - -#### quote_formula_mappings (0건) - 비어있음 - -### 2.4 FG 모델 매트릭스 - -| 모델 | 카테고리 | 마감 | 가이드레일 타입 | BD 부품 수 | -|------|---------|------|---------------|-----------| -| KSS01 | 스크린 | SUS | 벽면/측면 | 4 (가이드레일×2, 하단마감재, L-BAR) | -| KSS02 | 스크린 | SUS | 벽면/측면 | 4 | -| KSE01 | 스크린 | SUS+EGI | 벽면/측면 | 8 | -| KWE01 | 스크린 | SUS+EGI | 벽면/측면 | 8 | -| KQTS01 | 철재 | SUS | 벽면/측면 | 3 (가이드레일×2, 하단마감재) | -| KTE01 | 철재 | SUS+EGI | 벽면/측면 | 6 | -| KDSS01 | (FG없음) | SUS | 벽면/측면 | 4 | - -### 2.5 가이드레일 규격 매핑 (모델별) - -``` -KSS01/KSS02/KSE01/KWE01 → 벽면: 120*70, 측면: 120*120 -KTE01/KQTS01 → 벽면: 130*75, 측면: 130*125 -KDSS01 → 벽면: 150*150, 측면: 150*212 -``` - ---- - -## 3. 대상 범위 - -### Phase 1: 누락 품목 등록 (items 테이블) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | EST-SMOKE 코드 → Phase 3.1로 이관 (핸들러 코드 수정) | ⏭️ | Phase 3에서 처리 | -| 1.2 | EST-MOTOR 품목 등록 (150K~2000K, 전압별) | ✅ | 21건 확인 (220V 8종 + 380V 13종) | -| 1.3 | EST-CTRL 품목 등록 (제어기 종류별) | ✅ | 20건 확인 (기본3 + 방범9 + 방화4 + 기타4) | -| 1.4 | EST-SHAFT 품목 등록 (인치×길이별) | ✅ | 16건 확인 (3~12인치) | -| 1.5 | EST-PIPE 품목 등록 | ✅ | 3건 확인 (1.4T×2 + 2T×1) | -| 1.6 | EST-ANGLE 품목 등록 | ✅ | 8건 확인 (BRACKET 4 + MAIN 4) | -| 1.7 | EST-INSPECTION 품목 등록 | ✅ | 1건 확인 | -| 1.8 | EST-RAW 원자재 품목 등록 | ✅ | 6건 확인 (스크린3 + 슬랫3) | - -### Phase 2: 핸들러 구조화 (Strategy + Factory, Zero Config) ✅ 완료 - -> **설계 원칙**: tenant_id 기반 자동 발견. 설정/매핑/options 없이 클래스 존재 여부만으로 라우팅. - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | `TenantFormulaHandler` 인터페이스 생성 | ✅ | `Contracts/TenantFormulaHandler.php` | -| 2.2 | `FormulaHandlerFactory` 생성 (class_exists 자동 발견) | ✅ | `FormulaHandlerFactory.php` (35줄) | -| 2.3 | `KyungdongFormulaHandler` → `Tenant287/FormulaHandler`로 이동 | ✅ | namespace + implements 완료, 원본 삭제 | -| 2.4 | `FormulaEvaluatorService` 분기 로직 변경 | ✅ | KYUNGDONG_TENANT_ID 상수 제거, Factory::make() 사용 | -| 2.5 | `calculateKyungdongBom()` → `calculateTenantBom()` 일반화 | ✅ | 메서드명 + 파라미터(handler) + 문자열 일반화 | - -### Phase 3: 핸들러 아이템 코드 정비 (Tenant287/FormulaHandler) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | EST-SMOKE 코드 → BD- 코드로 변경 | ✅ | BD-케이스용 연기차단재(id:15587), BD-가이드레일용 연기차단재(id:15572) | -| 3.2 | 레거시 숫자 코드(00035, 00036 등) 유지 | ✅ | items 테이블에 등록됨, 변경 불필요 | -| 3.3 | lookupItem 실패 시 Log::warning() 추가 | ✅ | tenant_id, code 포함 경고 로그 | -| 3.4 | tinker E2E 테스트 통과 | ✅ | 17건, 1,167,934원 (KQTS01-SUS-벽면형) | - -### Phase 4: Generic 수식 데이터 재구성 (quote_formula_* 테이블) ⏭️ 후순위 - -> **분석 결과**: Generic 경로는 `items.bom` JSON 필드 기반이나, FG 품목의 bom 필드가 비어있음. -> `quote_formula_*` 테이블은 독립 수식 평가 기능용으로, 메인 BOM 계산 경로에서 직접 사용하지 않음. -> Tenant 287은 핸들러 경로를 사용하므로 현재 실질적 영향 없음. 다른 테넌트 추가 시 진행. - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | 실제 FG 제품용 mapping 수식 신규 생성 | ⏭️ | 다른 테넌트 추가 시 | -| 4.2 | quote_formula_items에 실제 BD- 코드 BOM 세트 추가 | ⏭️ | FG.bom 필드 구성 선행 필요 | -| 4.3 | quote_formula_ranges에 실제 BD- 코드 범위 추가 | ⏭️ | | -| 4.4 | quote_formula_mappings 구성 (FG → BD 모델별 매핑) | ⏭️ | | -| 4.5 | FormulaEvaluatorService 모델 인식 로직 추가 | ⏭️ | | - -### Phase 5: 통합 테스트 및 검증 ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | 7모델 전수 BOM 계산 테스트 (벽면형) | ✅ | 7모델 전부 PASS (18건씩, 1.1M~1.3M원) | -| 5.1b | 측면형 + 대형 규격 테스트 (3000×3000, QTY=2) | ✅ | 3모델 PASS (18건씩, 2.9M~3.2M원) | -| 5.2 | Factory 엣지 케이스 테스트 | ✅ | tenant 0/-1/999999→null, 287→Handler | -| 5.3 | SF-/SM- 잔여 참조 점검 (코드 기준) | ✅ | api/Services/Quote/ 내 참조 0건 | -| 5.4 | React 견적관리 BOM 테스트 | ⏭️ | Phase 4 후순위와 함께 | - ---- - -## 4. 작업 절차 - -### 4.1 단계별 절차 - -``` -Phase 1: 누락 품목 등록 -├── 1.1 EST-SMOKE → BD- 매핑 (코드만 변경, 품목 신규 등록 불필요) -├── 1.2~1.8 EST- 품목 등록 (items 테이블 INSERT) -│ ├── 코드: EST- 접두어 유지 (핸들러 코드와 일치) -│ ├── item_type: PT, tenant_id: 287 -│ └── options: { lot_managed: false, consumption_method: "none" } -└── 등록 후 lookupItem() 호출로 매핑 확인 - -Phase 2: 핸들러 구조화 (Strategy + Factory, Zero Config) -├── 2.1 TenantFormulaHandler 인터페이스 생성 -│ └── Contracts/TenantFormulaHandler.php (신규) -├── 2.2 FormulaHandlerFactory 생성 -│ └── class_exists("Handlers\Tenant{$tenantId}\FormulaHandler") 자동 발견 -├── 2.3 KyungdongFormulaHandler → Tenant287/FormulaHandler 이동 -│ ├── namespace 변경: Handlers → Handlers\Tenant287 -│ ├── implements TenantFormulaHandler 추가 -│ └── 클래스 docblock에 "경동기업 (tenant_id: 287)" 명시 -├── 2.4 FormulaEvaluatorService 분기 로직 변경 -│ ├── 제거: private const KYUNGDONG_TENANT_ID = 287 -│ ├── 제거: if ($tenantId === self::KYUNGDONG_TENANT_ID) -│ └── 추가: $handler = FormulaHandlerFactory::make($tenantId) -└── 2.5 calculateKyungdongBom() → calculateTenantBom($handler, ...) 일반화 - -Phase 3: 핸들러(Tenant287) 아이템 코드 정비 -├── 3.1 EST-SMOKE 코드 변경 (2곳) -│ ├── 라인 519: 'EST-SMOKE-케이스용' → 'BD-케이스용 연기차단재' -│ └── 라인 557: 'EST-SMOKE-레일용' → 'BD-가이드레일용 연기차단재' -├── 3.2 레거시 코드 검토 (00035, 00036, 00021, 90201~90204) -│ └── 현재 items 테이블에 등록되어 있으므로 동작함. 변경 여부 검토만. -├── 3.3 lookupItem()에 미등록 품목 경고 로깅 추가 -│ └── 라인 42-48: null 반환 시 Log::warning() -└── 3.4 MNG 연동 테스트 (https://mng.sam.kr/item-management) - -Phase 4: Generic 수식 데이터 재구성 (기존 데이터 유지, 실제 데이터 추가) -├── 4.1 실제 FG 제품용 mapping 수식 신규 생성 (quote_formulas INSERT) -├── 4.2~4.4 실제 데이터 INSERT (기존 테스트 데이터와 병행) -│ ├── quote_formula_items: BD-/EST- 코드 기반 BOM 구성 -│ ├── quote_formula_ranges: 실제 규격별 BD- 코드 반환 -│ └── quote_formula_mappings: FG 모델 → BD 부품 매핑 -└── 4.5 FormulaEvaluatorService에 모델 인식 로직 추가 - -Phase 5: 통합 테스트 -├── 5.1 MNG 품목관리 - 7모델 전수 테스트 -├── 5.2 React 견적관리 - BOM 계산 테스트 -├── 5.3 단가 정합성 검증 -└── 5.4 잔여 테스트 데이터 참조 점검 -``` - -### 4.2 EST- 품목 등록 상세 - -#### items INSERT 템플릿 - -```sql -INSERT INTO items (tenant_id, item_type, code, name, unit, is_active, created_at, updated_at) -VALUES (287, 'PT', '{code}', '{name}', '{unit}', 1, NOW(), NOW()); -``` - -#### 등록 대상 품목 목록 - -``` -EST-MOTOR-{voltage}-{capacity}: 모터 (전압-용량) -├── EST-MOTOR-220V-150K 150K 모터 220V -├── EST-MOTOR-220V-300K 300K 모터 220V -├── EST-MOTOR-220V-400K 400K 모터 220V -├── EST-MOTOR-220V-500K 500K 모터 220V -├── EST-MOTOR-220V-600K 600K 모터 220V -├── EST-MOTOR-380V-500K 500K 모터 380V -├── EST-MOTOR-380V-600K 600K 모터 380V -├── EST-MOTOR-380V-800K 800K 모터 380V -├── EST-MOTOR-380V-1000K 1000K 모터 380V -└── item_type: PT, unit: EA - -EST-CTRL-{type}: 제어기 -├── EST-CTRL-뒷박스 뒷박스 제어기 -├── EST-CTRL-일반 일반 제어기 -├── EST-CTRL-동보 동보 제어기 -├── EST-CTRL-자탈 자탈 제어기 -├── EST-CTRL-셋팅 셋팅 박스 -└── item_type: PT, unit: EA - -EST-SHAFT-{inch}인치-{length}: 감기샤프트 -├── EST-SHAFT-3인치-300 3인치 300mm -├── EST-SHAFT-4인치-3000 4인치 3000mm -├── EST-SHAFT-4인치-4500 4인치 4500mm -├── EST-SHAFT-4인치-6000 4인치 6000mm -├── EST-SHAFT-5인치-6000 5인치 6000mm -├── EST-SHAFT-5인치-7000 5인치 7000mm -├── EST-SHAFT-5인치-8200 5인치 8200mm -└── item_type: PT, unit: EA - -EST-PIPE-1.4-{length}: 앵글파이프 -├── EST-PIPE-1.4-3000 1.4T 3000mm -├── EST-PIPE-1.4-4500 1.4T 4500mm (핸들러에 없지만 패턴상 추가) -├── EST-PIPE-1.4-6000 1.4T 6000mm -└── item_type: PT, unit: EA - -EST-ANGLE-BRACKET-{type}: 모터받침 앵글 -├── EST-ANGLE-BRACKET-스크린용 -├── EST-ANGLE-BRACKET-철제300K -├── EST-ANGLE-BRACKET-철제400K -├── EST-ANGLE-BRACKET-철제500K이상 -└── item_type: PT, unit: EA - -EST-ANGLE-MAIN-{type}-{size}: 부자재 앵글 -├── EST-ANGLE-MAIN-앵글3T-2.5 -├── EST-ANGLE-MAIN-앵글3T-10 -├── EST-ANGLE-MAIN-앵글4T-2.5 -└── item_type: PT, unit: EA - -EST-INSPECTION: 검사비 -└── item_type: PT, unit: EA - -EST-RAW-스크린-{type}: 스크린 원단 -├── EST-RAW-스크린-실리카 -└── item_type: PT, unit: ㎡ - -EST-RAW-슬랫-{type}: 슬랫 원단 -├── EST-RAW-슬랫-방화 -└── item_type: PT, unit: ㎡ -``` - -> **참고**: 핸들러가 동적으로 코드를 조합하므로, 실제 사용되는 코드 조합만 등록. -> 등록 후 `lookupItem()` 호출 시 item_id/name이 정상 반환되는지 확인. - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 핸들러 구조화 | 인터페이스 + 팩토리 신규, 핸들러 이동 | Services/Quote/ 전체 | ✅ 완료 | -| 2 | FormulaEvaluatorService 분기 변경 | if(287) → Factory::make() | 전체 테넌트 | ✅ 완료 | -| 3 | EST- 품목 코드 체계 | 72건 이미 등록 확인 | items 테이블 | ✅ 완료 (사전 등록됨) | -| 4 | EST-SMOKE → BD- 코드 변경 | 핸들러 라인 519, 557 변경 | Tenant287/FormulaHandler | ✅ 완료 | -| 5 | 레거시 숫자코드 유지 | 00035, 00036 등 유지 결정 | Tenant287/FormulaHandler | ✅ 유지 (items에 등록됨) | -| 6 | Generic 경로에 모델 인식 추가 | 후순위 보류 (Phase 4) | 핸들러 없는 테넌트 | ⏭️ 후순위 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-19 | - | 문서 초안 작성 | - | - | -| 2026-02-19 | - | 자기완결성 보완 (부록 추가) | - | - | -| 2026-02-20 | Phase 1 | EST- 품목 72건 이미 등록 확인 → Phase 1 완료 | items 테이블 | ✅ | -| 2026-02-20 | Phase 2 | TenantFormulaHandler 인터페이스 + FormulaHandlerFactory 생성 | Contracts/TenantFormulaHandler.php, FormulaHandlerFactory.php | ✅ | -| 2026-02-20 | Phase 2 | KyungdongFormulaHandler → Tenant287/FormulaHandler 이동 | Handlers/Tenant287/FormulaHandler.php (신규), Handlers/KyungdongFormulaHandler.php (삭제) | ✅ | -| 2026-02-20 | Phase 2 | FormulaEvaluatorService 분기 로직 변경 (if(287) → Factory::make()) | FormulaEvaluatorService.php | ✅ | -| 2026-02-20 | Phase 2 | calculateKyungdongBom() → calculateTenantBom() 일반화 | FormulaEvaluatorService.php | ✅ | -| 2026-02-20 | Phase 3 | EST-SMOKE-케이스용 → BD-케이스용 연기차단재 (id:15587) | Tenant287/FormulaHandler.php | ✅ | -| 2026-02-20 | Phase 3 | EST-SMOKE-레일용 → BD-가이드레일용 연기차단재 (id:15572) | Tenant287/FormulaHandler.php | ✅ | -| 2026-02-20 | Phase 3 | lookupItem() 미등록 품목 Log::warning() 추가 | Tenant287/FormulaHandler.php | ✅ | -| 2026-02-20 | Phase 4 | Generic 경로 분석 → items.bom 기반, FG.bom 비어있음 → 후순위 결정 | - | ⏭️ | -| 2026-02-20 | Phase 5 | 벽부형 7모델 + 측면형 3모델 tinker 통합 테스트 PASS | - | ✅ | -| 2026-02-20 | Phase 5 | Factory 엣지케이스 + SF-/SM- 잔존 참조 점검 완료 | - | ✅ | -| 2026-02-20 | - | 문서 최종 업데이트 (검증결과, 변경이력, 상태 반영) | formula-engine-real-data-plan.md | ✅ | - ---- - -## 7. 참고 문서 - -- **견적 시스템**: `docs/features/quotes/README.md` -- **품목 정책**: `docs/rules/item-policy.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **빠른 시작**: `docs/quickstart/quick-start.md` - ---- - -## 8. 관련 파일 및 코드 위치 - -### 8.1 API (api/) - 핵심 코드 위치 - -| 파일 | 메서드 | 라인 | 역할 | -|------|--------|------|------| -| `Services/Quote/FormulaEvaluatorService.php` | `calculateBomWithDebug()` | 592-596 | 메인 엔트리 | -| 같은 파일 | (경동 분기 if문) | 609-611 | **Phase 2에서 Factory로 교체** | -| 같은 파일 | `calculateKyungdongBom()` | 1574-1881 | **Phase 2에서 calculateTenantBom()으로 일반화** | -| 같은 파일 | `KYUNGDONG_TENANT_ID` | 35 | **Phase 2에서 제거** | -| 같은 파일 | `expandBomWithFormulas()` | 1261-1333 | items.bom 재귀 전개 (Generic, 유지) | -| 같은 파일 | `calculateCategoryPrice()` | 812-862 | 카테고리 그룹 기반 단가 (유지) | -| 같은 파일 | `getItemPrice()` | 1066-1097 | 단가 조회 (유지) | -| **신규** `Contracts/TenantFormulaHandler.php` | - | - | **Phase 2에서 생성** | -| **신규** `FormulaHandlerFactory.php` | `make()` | - | **Phase 2에서 생성** | -| `Handlers/KyungdongFormulaHandler.php` | - | - | **→ `Handlers/Tenant287/FormulaHandler.php`로 이동** | -| `Handlers/Tenant287/FormulaHandler.php` | `calculateDynamicItems()` | 963 | **메인 엔트리** (이동 후) | -| 같은 파일 | `calculateSteelItems()` | 448 | 절곡품 10종 계산 | -| 같은 파일 | `calculatePartItems()` | 778 | 부자재 5종 계산 | -| 같은 파일 | `lookupItem()` | 35-49 | 품목 코드 → id/name 조회 (캐싱) | -| 같은 파일 | `withItemMapping()` | 72-87 | 아이템에 item_code/item_id 매핑 | -| 같은 파일 | `getGuideRailSpecs()` | 666-672 | 모델별 가이드레일 규격 매핑 | -| 같은 파일 | `calculateGuideRails()` | 675-730 | 가이드레일 타입별 계산 | -| `Services/Quote/EstimatePriceService.php` | (전체) | - | 단가 조회 서비스 (유지) | -| `Services/FormulaApiService.php` | `calculateBom()` | - | API 서버 호출 래퍼 (유지) | - -### 8.2 MNG (mng/) - -| 파일 | 메서드 | 라인 | 역할 | -|------|--------|------|------| -| `Controllers/Api/Admin/ItemManagementApiController.php` | `calculateFormula()` | 60-86 | 수식 BOM 계산 API | -| `Services/FormulaApiService.php` | `calculateBom()` | 24-82 | POST /api/v1/quotes/calculate/bom | -| `Services/ItemManagementService.php` | `getBomTree()` | - | BOM 트리 조회 (items.bom) | -| `views/item-management/index.blade.php` | JS `calculateFormula()` | - | 프론트 수식 계산 호출 | - -### 8.3 DB 테이블 스키마 - -#### items 테이블 - -| 컬럼 | 타입 | NULL | 설명 | -|------|------|------|------| -| id | bigint unsigned | NO | PK | -| tenant_id | bigint unsigned | NO | 테넌트 | -| item_type | varchar(15) | NO | FG/PT/SM/RM/CS | -| code | varchar(100) | NO | 품목 코드 | -| name | varchar(255) | NO | 품목명 | -| unit | varchar(20) | YES | 단위 (EA/M/㎡) | -| category_id | bigint unsigned | YES | 카테고리 FK | -| process_type | varchar(20) | YES | 공정 유형 | -| item_category | varchar(50) | YES | 품목 카테고리 | -| bom | json | YES | BOM JSON (FG는 현재 NULL) | -| attributes | json | YES | 동적 속성 | -| options | json | YES | 관리 옵션 | -| is_active | tinyint(1) | NO | 활성 (기본 1) | - -#### quote_formula_items 테이블 - -| 컬럼 | 타입 | NULL | 설명 | -|------|------|------|------| -| id | bigint unsigned | NO | PK | -| formula_id | bigint unsigned | NO | quote_formulas FK | -| item_code | varchar(50) | NO | 품목 코드 | -| item_name | varchar(200) | NO | 품목명 | -| specification | varchar(100) | YES | 규격 | -| unit | varchar(20) | NO | 단위 | -| quantity_formula | varchar(500) | NO | 수량 수식 | -| unit_price_formula | varchar(500) | YES | 단가 수식 | -| sort_order | int unsigned | NO | 정렬 | - -#### quote_formula_ranges 테이블 - -| 컬럼 | 타입 | NULL | 설명 | -|------|------|------|------| -| id | bigint unsigned | NO | PK | -| formula_id | bigint unsigned | NO | quote_formulas FK | -| min_value | decimal(15,4) | NO | 최소값 | -| max_value | decimal(15,4) | NO | 최대값 | -| condition_variable | varchar(50) | NO | 조건 변수 (K/H/W) | -| result_value | varchar(500) | NO | 결과값 (품목 코드) | -| result_type | enum('fixed','formula') | NO | 결과 유형 | -| sort_order | int unsigned | NO | 정렬 | - -#### quote_formula_mappings 테이블 - -| 컬럼 | 타입 | NULL | 설명 | -|------|------|------|------| -| id | bigint unsigned | NO | PK | -| formula_id | bigint unsigned | NO | quote_formulas FK | -| source_variable | varchar(50) | NO | 원본 변수 | -| source_value | varchar(200) | NO | 원본 값 | -| result_value | varchar(500) | NO | 결과값 | -| result_type | enum('fixed','formula') | NO | 결과 유형 | -| sort_order | int unsigned | NO | 정렬 | - -#### quote_formulas 테이블 - -| 컬럼 | 타입 | NULL | 설명 | -|------|------|------|------| -| id | bigint unsigned | NO | PK | -| tenant_id | bigint unsigned | NO | 테넌트 | -| category_id | bigint unsigned | NO | 카테고리 FK | -| product_id | bigint unsigned | YES | 매핑 대상 제품 FK | -| name | varchar(200) | NO | 수식명 | -| variable | varchar(50) | NO | 변수명 | -| type | enum('input','calculation','range','mapping') | NO | 유형 | -| formula | text | YES | 수식 표현식 | -| output_type | enum('variable','item') | NO | 출력 유형 | -| sort_order | int unsigned | NO | 정렬 | -| is_active | tinyint(1) | NO | 활성 | - ---- - -## 9. 검증 결과 - -### 9.1 테스트 케이스 (tinker 수동 실행) - -#### 벽부형 7모델 (W0=2000, H0=2500, QTY=1) - -| 모델 | FG 코드 | BOM 항목수 | 총 금액 | 상태 | -|------|---------|----------|--------|------| -| KQTS01 | FG-KQTS01-벽면형-SUS | 18건 | 1,167,934원 | ✅ | -| KSS01 | FG-KSS01-벽면형-SUS | 18건 | ~1.1M원 | ✅ | -| KSS02 | FG-KSS02-벽면형-SUS | 18건 | ~1.1M원 | ✅ | -| KSE01 | FG-KSE01-벽면형-SUS | 18건 | ~1.2M원 | ✅ | -| KSE01-EGI | FG-KSE01-벽면형-EGI | 18건 | ~1.2M원 | ✅ | -| KWE01 | FG-KWE01-벽면형-SUS | 18건 | ~1.2M원 | ✅ | -| KTE01 | FG-KTE01-벽면형-SUS | 18건 | ~1.3M원 | ✅ | - -#### 측면형 + 대형 규격 (W0=4000, H0=5000, QTY=2) - -| 모델 | FG 코드 | BOM 항목수 | 총 금액 | 상태 | -|------|---------|----------|--------|------| -| KQTS01 | FG-KQTS01-측면형-SUS | 18건 | ~2.9M원 | ✅ | -| KSE01 | FG-KSE01-측면형-SUS | 18건 | ~3.1M원 | ✅ | -| KTE01-EGI | FG-KTE01-측면형-EGI | 18건 | ~3.2M원 | ✅ | - -#### Factory 엣지 케이스 - -| tenant_id | 예상 | 실제 | 상태 | -|-----------|------|------|------| -| 287 | Tenant287\FormulaHandler 인스턴스 | ✅ 정상 반환 | ✅ | -| 0 | null | null | ✅ | -| -1 | null | null | ✅ | -| 999999 | null | null | ✅ | - -#### SF-/SM- 잔존 참조 점검 - -| 검색 범위 | 패턴 | 결과 | 상태 | -|-----------|------|------|------| -| api/app/Services/Quote/ | SF- / SM- 코드 참조 | 0건 | ✅ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| FormulaHandlerFactory::make(287)이 Tenant287 핸들러 반환 | ✅ | 자동 발견 정상 동작 | -| FormulaHandlerFactory::make(999)이 null 반환 → Generic 경로 | ✅ | 미등록 테넌트 정상 | -| tinker에서 FG 선택 시 BOM 계산 성공 | ✅ | 벽부 7모델 + 측면 3모델 전수 PASS | -| BOM 결과의 모든 item_code가 items에 존재 | ✅ | BD- 코드 정상 매핑 (lookupItem null 없음) | -| React 견적관리 BOM 벌크 계산 정상 | ⏭️ | Phase 4 후순위와 함께 | -| SF-/SM- 코드 참조 잔존 없음 | ✅ | api/Services/Quote/ 내 0건 확인 | - ---- - -## 부록 A. FG 품목 전체 목록 (18건) - -| id | code | model | guiderail | finishing | major_category | legacy_model_id | -|----|------|-------|-----------|-----------|---------------|-----------------| -| 15515 | FG-KSS01-벽면형-SUS | KSS01 | 벽면형 | SUS마감 | 스크린 | 12 | -| 15516 | FG-KSS01-측면형-SUS | KSS01 | 측면형 | SUS마감 | 스크린 | 13 | -| 15517 | FG-KSE01-벽면형-SUS | KSE01 | 벽면형 | SUS마감 | 스크린 | 14 | -| 15518 | FG-KSE01-벽면형-EGI | KSE01 | 벽면형 | EGI마감 | 스크린 | 15 | -| 15519 | FG-KSE01-측면형-SUS | KSE01 | 측면형 | SUS마감 | 스크린 | 16 | -| 15520 | FG-KSE01-측면형-EGI | KSE01 | 측면형 | EGI마감 | 스크린 | 17 | -| 15521 | FG-KWE01-벽면형-SUS | KWE01 | 벽면형 | SUS마감 | 스크린 | 18 | -| 15522 | FG-KWE01-벽면형-EGI | KWE01 | 벽면형 | EGI마감 | 스크린 | 19 | -| 15523 | FG-KWE01-측면형-SUS | KWE01 | 측면형 | SUS마감 | 스크린 | 20 | -| 15524 | FG-KWE01-측면형-EGI | KWE01 | 측면형 | EGI마감 | 스크린 | 21 | -| 15525 | FG-KQTS01-벽면형-SUS | KQTS01 | 벽면형 | SUS마감 | 철재 | 22 | -| 15526 | FG-KQTS01-측면형-SUS | KQTS01 | 측면형 | SUS마감 | 철재 | 23 | -| 15527 | FG-KTE01-측면형-SUS | KTE01 | 측면형 | SUS마감 | 철재 | 24 | -| 15528 | FG-KTE01-벽면형-SUS | KTE01 | 벽면형 | SUS마감 | 철재 | 25 | -| 15529 | FG-KTE01-측면형-EGI | KTE01 | 측면형 | EGI마감 | 철재 | 26 | -| 15530 | FG-KTE01-벽면형-EGI | KTE01 | 벽면형 | EGI마감 | 철재 | 27 | -| 15531 | FG-KSS02-측면형-SUS | KSS02 | 측면형 | SUS마감 | 스크린 | 28 | -| 15532 | FG-KSS02-벽면형-SUS | KSS02 | 벽면형 | SUS마감 | 스크린 | 29 | - ---- - -## 부록 B. BD- 품목 전체 목록 (58건, 모두 item_type=PT) - -### 가이드레일 (17건) - -| id | code | name | -|----|------|------| -| 15589 | BD-가이드레일-KDSS01-SUS-150*150 | 가이드레일 KDSS01 SUS 150*150 | -| 15590 | BD-가이드레일-KDSS01-SUS-150*212 | 가이드레일 KDSS01 SUS 150*212 | -| 15592 | BD-가이드레일-KQTS01-SUS-130*125 | 가이드레일 KQTS01 SUS 130*125 | -| 15593 | BD-가이드레일-KQTS01-SUS-130*75 | 가이드레일 KQTS01 SUS 130*75 | -| 15596 | BD-가이드레일-KSE01-SUS-120*120 | 가이드레일 KSE01 SUS 120*120 | -| 15597 | BD-가이드레일-KSE01-SUS-120*70 | 가이드레일 KSE01 SUS 120*70 | -| 15598 | BD-가이드레일-KSE01-EGI-120*120 | 가이드레일 KSE01 EGI 120*120 | -| 15599 | BD-가이드레일-KSE01-EGI-120*70 | 가이드레일 KSE01 EGI 120*70 | -| 15603 | BD-가이드레일-KSS01-SUS-120*120 | 가이드레일 KSS01 SUS 120*120 | -| 15604 | BD-가이드레일-KSS01-SUS-120*70 | 가이드레일 KSS01 SUS 120*70 | -| 15607 | BD-가이드레일-KSS02-SUS-120*120 | 가이드레일 KSS02 SUS 120*120 | -| 15608 | BD-가이드레일-KSS02-SUS-120*70 | 가이드레일 KSS02 SUS 120*70 | -| 15610 | BD-가이드레일-KTE01-SUS-130*125 | 가이드레일 KTE01 SUS 130*125 | -| 15611 | BD-가이드레일-KTE01-SUS-130*75 | 가이드레일 KTE01 SUS 130*75 | -| 15612 | BD-가이드레일-KTE01-EGI-130*125 | 가이드레일 KTE01 EGI 130*125 | -| 15613 | BD-가이드레일-KTE01-EGI-130*75 | 가이드레일 KTE01 EGI 130*75 | -| 15617 | BD-가이드레일-KWE01-SUS-120*120 | 가이드레일 KWE01 SUS 120*120 | -| 15618 | BD-가이드레일-KWE01-SUS-120*70 | 가이드레일 KWE01 SUS 120*70 | -| 15619 | BD-가이드레일-KWE01-EGI-120*120 | 가이드레일 KWE01 EGI 120*120 | -| 15620 | BD-가이드레일-KWE01-EGI-120*70 | 가이드레일 KWE01 EGI 120*70 | - -### 하단마감재 (10건) - -| id | code | name | -|----|------|------| -| 15591 | BD-하단마감재-KDSS01-SUS-140*78 | 하단마감재 KDSS01 SUS 140*78 | -| 15594 | BD-하단마감재-KQTS01-SUS-60*30 | 하단마감재 KQTS01 SUS 60*30 | -| 15600 | BD-하단마감재-KSE01-SUS-64*43 | 하단마감재 KSE01 SUS 64*43 | -| 15601 | BD-하단마감재-KSE01-EGI-60*40 | 하단마감재 KSE01 EGI 60*40 | -| 15605 | BD-하단마감재-KSS01-SUS-60*40 | 하단마감재 KSS01 SUS 60*40 | -| 15609 | BD-하단마감재-KSS02-SUS-60*40 | 하단마감재 KSS02 SUS 60*40 | -| 15614 | BD-하단마감재-KTE01-SUS-64*34 | 하단마감재 KTE01 SUS 64*34 | -| 15615 | BD-하단마감재-KTE01-EGI-60*30 | 하단마감재 KTE01 EGI 60*30 | -| 15621 | BD-하단마감재-KWE01-SUS-64*43 | 하단마감재 KWE01 SUS 64*43 | -| 15622 | BD-하단마감재-KWE01-EGI-60*40 | 하단마감재 KWE01 EGI 60*40 | - -### L-BAR (5건) - -| id | code | name | -|----|------|------| -| 15588 | BD-L-BAR-KDSS01-17*100 | L-BAR KDSS01 17*100 | -| 15595 | BD-L-BAR-KSE01-17*60 | L-BAR KSE01 17*60 | -| 15602 | BD-L-BAR-KSS01-17*60 | L-BAR KSS01 17*60 | -| 15606 | BD-L-BAR-KSS02-17*60 | L-BAR KSS02 17*60 | -| 15616 | BD-L-BAR-KWE01-17*60 | L-BAR KWE01 17*60 | - -### 케이스 (11건) - -| id | code | name | -|----|------|------| -| 15577 | BD-케이스-500*350 | 케이스 500*350 | -| 15578 | BD-케이스-500*380 | 케이스 500*380 | -| 15579 | BD-케이스-600*500 | 케이스 600*500 | -| 15580 | BD-케이스-600*550 | 케이스 600*550 | -| 15581 | BD-케이스-650*500 | 케이스 650*500 | -| 15582 | BD-케이스-650*550 | 케이스 650*550 | -| 15583 | BD-케이스-700*550 | 케이스 700*550 | -| 15584 | BD-케이스-700*600 | 케이스 700*600 | -| 15585 | BD-케이스-780*600 | 케이스 780*600 | -| 15586 | BD-케이스-780*650 | 케이스 780*650 | -| 15587 | BD-케이스용 연기차단재 | 케이스용 연기차단재 | - -### 마구리 (10건) - -| id | code | name | -|----|------|------| -| 15565 | BD-마구리-505*355 | 마구리 505*355 | -| 15566 | BD-마구리-505*385 | 마구리 505*385 | -| 15567 | BD-마구리-605*555 | 마구리 605*555 | -| 15568 | BD-마구리-655*555 | 마구리 655*555 | -| 15569 | BD-마구리-705*605 | 마구리 705*605 | -| 15570 | BD-마구리-785*685 | 마구리 785*685 | -| 15573 | BD-마구리-655*505 | 마구리 655*505 | -| 15574 | BD-마구리-705*555 | 마구리 705*555 | -| 15575 | BD-마구리-785*605 | 마구리 785*605 | -| 15576 | BD-마구리-785*655 | 마구리 785*655 | - -### 기타 (5건) - -| id | code | name | -|----|------|------| -| 15571 | BD-보강평철-50 | 보강평철 50 | -| 15572 | BD-가이드레일용 연기차단재 | 가이드레일용 연기차단재 | - ---- - -## 부록 C. 코드 변경 포인트 - -### C.1 EST-SMOKE → BD- 변경 (Phase 3.1) - -**파일**: `api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php` (이동 후) - -``` -라인 519: 'EST-SMOKE-케이스용' → 'BD-케이스용 연기차단재' (id: 15587) -라인 557: 'EST-SMOKE-레일용' → 'BD-가이드레일용 연기차단재' (id: 15572) -``` - -### C.2 레거시 숫자 코드 매핑 (Phase 3.2 검토 대상) - -| 라인 | 현재 코드 | items.id | items.name | 비고 | -|------|----------|----------|-----------|------| -| 564 | 00035 | 14939 | 철재용하장바(SUS)3000 | 하장바 SUS | -| 564 | 00036 | 14940 | 철재용하장바(SUS1.2T) | 하장바 EGI (SM타입) | -| 619 | 00021 | 14928 | 평철12T | 무게평철12T | -| 631 | 90201 | 15188 | KD환봉(30파이) | 환봉 기본 | -| 628 | 90202 | 15189 | KD환봉 | 환봉 35파이 | -| 629 | 90203 | 15190 | KD환봉 | 환봉 45파이 | -| 630 | 90204 | 15191 | KD환봉 | 환봉 50파이 | - -> 모두 items 테이블에 존재하므로 lookupItem() 정상 동작. -> 변경 여부는 코드 가독성 차원에서 검토 (기능적 문제 없음). - -### C.3 lookupItem 로깅 추가 (Phase 3.3) - -**파일**: `api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php` -**위치**: 라인 42-48 `lookupItem()` 메서드 - -```php -// 변경 전 (라인 46) -$cache[$code] = ['id' => $item?->id, 'name' => $item?->name]; - -// 변경 후 -$cache[$code] = ['id' => $item?->id, 'name' => $item?->name]; -if (!$item) { - \Log::warning("[Tenant287\FormulaHandler] 미등록 품목: {$code}"); -} -``` - ---- - -## 부록 D. calculateDynamicItems 입력 파라미터 - -KyungdongFormulaHandler의 메인 엔트리 `calculateDynamicItems()` (라인 963)가 수신하는 파라미터: - -```php -$inputs = [ - // 기본 치수 - 'W0' => float, // 폭 (mm) - 'H0' => float, // 높이 (mm) - 'QTY' => int, // 수량 - - // 제품 정보 - 'product_type' => string, // 'screen' | 'slat' | 'steel' - 'model_name' => string, // 'KSS01' | 'KSE01' | ... - 'finishing_type' => string, // 'SUS마감' | 'EGI마감' (→ 내부에서 '마감' 제거) - - // 가이드레일 - 'guide_type' => string, // '벽면형' | '측면형' | '혼합형' - - // 케이스 - 'case_spec' => string, // '500*380' 등 - - // 모터/제어기 - 'bracket_inch' => string, // '4' | '5' | '6' | '8' - 'motor_power' => string, // 'single' | 'three' - 'controller_type' => string, // '일반' | '동보' | '자탈' 등 - - // 기타 (선택) - 'weight_plate_qty' => int, - 'round_bar_qty' => int, - 'round_bar_phi' => int, // 30 | 35 | 45 | 50 -]; -``` - -**반환값** (아이템 배열): - -```php -[ - [ - 'category' => string, // 'steel' | 'parts' | 'inspection' | 'material' | 'motor' | 'controller' - 'item_name' => string, - 'item_code' => string, // EST-*, BD-*, 또는 레거시 숫자코드 - 'item_id' => int|null, // items.id (lookupItem 결과) - 'specification' => string, - 'unit' => string, // 'EA' | 'm' | '㎡' - 'quantity' => float, - 'unit_price' => float, - 'total_price' => float, - ], - // ... -] -``` - ---- - -## 부록 E. 핸들러 구조화 설계 (Phase 2 상세) - -### E.1 디렉토리 구조 (Before → After) - -``` -Before: -api/app/Services/Quote/ -├── FormulaEvaluatorService.php ← if (287) 하드코딩 -├── EstimatePriceService.php -└── Handlers/ - └── KyungdongFormulaHandler.php ← 독립 클래스, 인터페이스 없음 - -After: -api/app/Services/Quote/ -├── FormulaEvaluatorService.php ← Factory::make($tenantId) 사용 -├── FormulaHandlerFactory.php ← 신규: 자동 발견 팩토리 -├── EstimatePriceService.php -├── Contracts/ -│ └── TenantFormulaHandler.php ← 신규: 인터페이스 -└── Handlers/ - └── Tenant287/ ← 경동기업 (tenant_id: 287) - └── FormulaHandler.php ← KyungdongFormulaHandler 이동 - └── Tenant{N}/ ← 향후 업체 추가 시 - └── FormulaHandler.php -``` - -### E.2 인터페이스 설계 - -```php -// api/app/Services/Quote/Contracts/TenantFormulaHandler.php -namespace App\Services\Quote\Contracts; - -interface TenantFormulaHandler -{ - /** - * 동적 BOM 항목 계산 (메인 엔트리) - */ - public function calculateDynamicItems(array $inputs): array; - - /** - * 모터 용량 계산 - */ - public function calculateMotorCapacity(string $productType, float $weight, string $bracketInch): string; - - /** - * 브라켓 사이즈 계산 - */ - public function calculateBracketSize(float $weight, ?string $bracketInch = null): string; -} -``` - -### E.3 팩토리 설계 - -```php -// api/app/Services/Quote/FormulaHandlerFactory.php -namespace App\Services\Quote; - -use App\Services\Quote\Contracts\TenantFormulaHandler; - -class FormulaHandlerFactory -{ - /** - * tenant_id로 핸들러 자동 발견. - * Handlers/Tenant{id}/FormulaHandler.php가 존재하면 인스턴스 반환. - * 없으면 null → Generic DB 경로. - */ - public static function make(int $tenantId): ?TenantFormulaHandler - { - $class = "App\\Services\\Quote\\Handlers\\Tenant{$tenantId}\\FormulaHandler"; - - if (!class_exists($class)) { - return null; - } - - $handler = new $class(); - - if (!$handler instanceof TenantFormulaHandler) { - throw new \RuntimeException( - "Tenant{$tenantId} FormulaHandler must implement TenantFormulaHandler" - ); - } - - return $handler; - } -} -``` - -### E.4 핸들러 이동 (Tenant287) - -```php -// api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php -namespace App\Services\Quote\Handlers\Tenant287; - -use App\Services\Quote\Contracts\TenantFormulaHandler; -use App\Services\Quote\EstimatePriceService; - -/** - * 경동기업 수식 핸들러 (tenant_id: 287) - * - * 방화셔터/스크린/철재 제품의 BOM 동적 계산. - * KyungdongFormulaHandler에서 이동됨. - */ -class FormulaHandler implements TenantFormulaHandler -{ - private const TENANT_ID = 287; - - // ... 기존 KyungdongFormulaHandler 코드 그대로 유지 -} -``` - -### E.5 FormulaEvaluatorService 변경 포인트 - -```php -// 변경 전 (라인 35) -private const KYUNGDONG_TENANT_ID = 287; - -// 변경 전 (라인 609-611) -if ($tenantId === self::KYUNGDONG_TENANT_ID) { - return $this->calculateKyungdongBom($finishedGoodsCode, $inputVariables, $tenantId); -} - -// ───────────────────────────────────────── - -// 변경 후 (라인 35 제거) -// KYUNGDONG_TENANT_ID 상수 제거 - -// 변경 후 (라인 609-611) -$handler = FormulaHandlerFactory::make($tenantId); -if ($handler) { - return $this->calculateTenantBom($handler, $finishedGoodsCode, $inputVariables, $tenantId); -} -// else → 기존 Generic 10단계 그대로 실행 - -// calculateKyungdongBom() → calculateTenantBom() 리네이밍 -// $handler 파라미터 추가, 내부의 new KyungdongFormulaHandler() 제거 -``` - -### E.6 향후 업체 추가 절차 - -``` -1. Handlers/Tenant{id}/FormulaHandler.php 파일 1개 생성 -2. implements TenantFormulaHandler -3. 끝. (설정 파일, DB 옵션, 매핑 테이블 변경 없음) -``` - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 | -| 3 | 작업 범위가 구체적인가? | ✅ | 4 Phase + 부록 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성 | -| 5 | 참고 파일 경로 + 라인번호가 정확한가? | ✅ | 섹션 8 + 부록 C/E | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 4.1 + 4.2 (SQL), 부록 E (코드 설계) | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/건수/라인번호 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 3 Phase 1, 4.1 단계별 절차 | -| Q3. 어떤 파일의 몇 번째 줄을 수정해야 하는가? | ✅ | 8.1 코드 위치, 부록 C/E | -| Q4. 어떤 품목을 등록해야 하는가? | ✅ | 4.2 등록 상세, 부록 A/B | -| Q5. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q6. 핸들러가 어떤 파라미터를 받는가? | ✅ | 부록 D | -| Q7. DB INSERT 어떻게 하는가? | ✅ | 4.2 SQL 템플릿 | -| Q8. 기존 데이터 건드려도 되는가? | ✅ | 1.4 원칙 6번 (삭제 금지) | -| Q9. 핸들러 구조는 어떻게 만드는가? | ✅ | 부록 E (인터페이스/팩토리/이동 상세) | -| Q10. 향후 업체 추가 시 절차는? | ✅ | 부록 E.6 (파일 1개 생성, 끝) | - -**결과**: 10/10 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* diff --git a/plans/archive/items-table-unification-plan.md b/plans/archive/items-table-unification-plan.md deleted file mode 100644 index eee1f67..0000000 --- a/plans/archive/items-table-unification-plan.md +++ /dev/null @@ -1,589 +0,0 @@ -# Items 테이블 통합 마이그레이션 계획 - -## 참조 문서 - -### 필수 확인 - -| 문서 | 경로 | 내용 | -|------|------|------| -| **ItemMaster 연동 설계서** | [specs/item-master-integration.md](../specs/item-master-integration.md) | source_table, EntityRelationship 구조 | -| **DB 스키마** | [specs/database-schema.md](../specs/database-schema.md) | 테이블 구조, Multi-tenant 아키텍처 | - -### 참고 문서 - -| 문서 | 경로 | 내용 | -|------|------|------| -| **품목관리 마이그레이션 가이드** | [projects/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md](../projects/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md) | 프론트엔드 마이그레이션 | -| **API 품목 분석 요약** | [projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md](../projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md) | 기존 API 분석, price_histories | -| **Swagger 가이드** | [guides/swagger-guide.md](../guides/swagger-guide.md) | API 문서화 규칙 | - -### 관련 코드 - -| 파일 | 경로 | 역할 | -|------|------|------| -| ItemPage 모델 | `api/app/Models/ItemMaster/ItemPage.php` | source_table 매핑 | -| EntityRelationship 모델 | `api/app/Models/ItemMaster/EntityRelationship.php` | 엔티티 관계 관리 | -| ItemMasterService | `api/app/Services/ItemMaster/ItemMasterService.php` | init API, 메타데이터 조회 | -| ProductService | `api/app/Services/ProductService.php` | 기존 Products API (제거 예정) | -| MaterialService | `api/app/Services/MaterialService.php` | 기존 Materials API (제거 예정) | - ---- - -## 개요 - -### 목적 -`products`/`materials` 테이블을 `items` 테이블로 통합하여: -- BOM 관리 시 `child_item_type` 불필요 (ID만으로 유일 식별) -- 단일 쿼리로 모든 품목 조회 가능 -- Item-Master 시스템과 일관된 구조 - -### 현재 상황 -- **개발 단계**: 미오픈 (레거시 호환 불필요) -- **Item-Master**: 메타데이터 시스템 운영 중 (pages, sections, fields) -- **이전 시도**: 12/11 items 생성 → 12/12 롤백 (정책 정리 필요) - -### 현재 시스템 구조 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Item-Master (메타데이터) │ -├─────────────────────────────────────────────────────────────┤ -│ item_pages (source_table: 'products'|'materials') │ -│ ↓ EntityRelationship │ -│ item_sections → item_fields, item_bom_items │ -└─────────────────────────────────────────────────────────────┘ - ↓ 참조 -┌─────────────────────────────────────────────────────────────┐ -│ 실제 데이터 테이블 │ -├─────────────────────────────────────────────────────────────┤ -│ products (808건) ← ProductController, ProductService │ -│ materials (417건) ← MaterialController, MaterialService │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 목표 구조 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Item-Master (메타데이터) │ -├─────────────────────────────────────────────────────────────┤ -│ item_pages (source_table: 'items') │ -│ ↓ EntityRelationship │ -│ item_sections → item_fields, item_bom_items │ -└─────────────────────────────────────────────────────────────┘ - ↓ 참조 -┌─────────────────────────────────────────────────────────────┐ -│ 통합 데이터 테이블 │ -├─────────────────────────────────────────────────────────────┤ -│ items ← ItemController, ItemService │ -│ item_type: FG, PT, SM, RM, CS │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Phase 0: 데이터 정규화 - -### 0.1 item_type 표준화 - -개발 중이므로 비표준 데이터는 삭제 처리. 품목관리 완료 후 경동기업 데이터 전체 재세팅 예정. - -**표준 item_type 체계**: - -| 코드 | 설명 | 출처 | -|------|------|------| -| FG | 완제품 (Finished Goods) | products | -| PT | 부품 (Parts) | products | -| SM | 부자재 (Sub-materials) | materials | -| RM | 원자재 (Raw Materials) | materials | -| CS | 소모품 (Consumables) | materials만 | - -**비표준 데이터 삭제**: -```sql --- products에서 비표준 타입 삭제 (PRODUCT, SUBASSEMBLY, PART, CS) -DELETE FROM products WHERE product_type NOT IN ('FG', 'PT'); - --- materials는 이미 표준 타입만 사용 (SM, RM, CS) -``` - -### 0.2 BOM 데이터 정리 - -통합 시 문제되는 BOM 데이터 삭제: -```sql --- 삭제될 products/materials를 참조하는 BOM 항목 제거 --- (Phase 1 이관 전에 실행) -``` - -### 0.3 체크리스트 - -- [x] products 비표준 타입 삭제 -- [x] 관련 BOM 데이터 정리 -- [x] 삭제 건수 확인 - ---- - -## Phase 1: items 테이블 생성 + 데이터 이관 - -### 1.1 items 테이블 - -```sql -CREATE TABLE items ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - - -- 기본 정보 - item_type VARCHAR(15) NOT NULL COMMENT 'FG, PT, SM, RM, CS', - code VARCHAR(100) NOT NULL, - name VARCHAR(255) NOT NULL, - unit VARCHAR(20) NULL, - category_id BIGINT UNSIGNED NULL, - - -- BOM (JSON) - bom JSON NULL COMMENT '[{child_item_id, quantity}, ...]', - - -- 상태 - is_active TINYINT(1) DEFAULT 1, - - -- 감사 필드 - created_by BIGINT UNSIGNED NULL, - updated_by BIGINT UNSIGNED NULL, - deleted_by BIGINT UNSIGNED NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - deleted_at TIMESTAMP NULL, - - -- 인덱스 - INDEX idx_items_tenant_type (tenant_id, item_type), - INDEX idx_items_tenant_code (tenant_id, code), - INDEX idx_items_tenant_category (tenant_id, category_id), - UNIQUE KEY uq_items_tenant_code (tenant_id, code, deleted_at), - - FOREIGN KEY (tenant_id) REFERENCES tenants(id), - FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 1.2 item_details 테이블 (확장 필드) - -```sql -CREATE TABLE item_details ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - item_id BIGINT UNSIGNED NOT NULL, - - -- Products 전용 필드 - is_sellable TINYINT(1) DEFAULT 1, - is_purchasable TINYINT(1) DEFAULT 0, - is_producible TINYINT(1) DEFAULT 0, - safety_stock INT NULL, - lead_time INT NULL, - is_variable_size TINYINT(1) DEFAULT 0, - product_category VARCHAR(50) NULL, - part_type VARCHAR(50) NULL, - - -- Materials 전용 필드 - is_inspection VARCHAR(1) DEFAULT 'N', - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uq_item_details_item_id (item_id), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 1.3 item_attributes 테이블 (동적 속성) - -```sql -CREATE TABLE item_attributes ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - item_id BIGINT UNSIGNED NOT NULL, - - attributes JSON NULL, - options JSON NULL, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uq_item_attributes_item_id (item_id), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 1.4 데이터 이관 스크립트 - -```php -// Products → Items -DB::statement(" - INSERT INTO items (tenant_id, item_type, code, name, unit, category_id, bom, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at) - SELECT tenant_id, product_type, code, name, unit, category_id, bom, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at - FROM products -"); - -// Materials → Items -DB::statement(" - INSERT INTO items (tenant_id, item_type, code, name, unit, category_id, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at) - SELECT tenant_id, material_type, material_code, name, unit, category_id, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at - FROM materials -"); -``` - -### 1.5 체크리스트 - -- [x] items 마이그레이션 생성 -- [x] item_details 마이그레이션 생성 -- [x] item_attributes 마이그레이션 생성 -- [x] 데이터 이관 스크립트 실행 -- [x] 건수 검증 (1,225건) - ---- - -## Phase 2: Item 모델 + Service 생성 - -### 2.1 Item 모델 - -```php -// app/Models/Item.php -class Item extends Model -{ - use BelongsToTenant, ModelTrait, SoftDeletes; - - protected $fillable = [ - 'tenant_id', 'item_type', 'code', 'name', 'unit', - 'category_id', 'bom', 'is_active', - ]; - - protected $casts = [ - 'bom' => 'array', - 'is_active' => 'boolean', - ]; - - // 1:1 관계 - public function details() { return $this->hasOne(ItemDetail::class); } - public function attributes() { return $this->hasOne(ItemAttribute::class); } - - // 타입별 스코프 - public function scopeProducts($q) { - return $q->whereIn('item_type', ['FG', 'PT']); - } - public function scopeMaterials($q) { - return $q->whereIn('item_type', ['SM', 'RM', 'CS']); - } -} -``` - -### 2.2 ItemService - -```php -// app/Services/ItemService.php -class ItemService extends Service -{ - public function index(array $params): LengthAwarePaginator - { - $query = Item::where('tenant_id', $this->tenantId()); - - // item_type 필터 - if ($itemType = $params['item_type'] ?? null) { - $query->where('item_type', strtoupper($itemType)); - } - - // 검색 - if ($search = $params['search'] ?? null) { - $query->where(fn($q) => $q - ->where('code', 'like', "%{$search}%") - ->orWhere('name', 'like', "%{$search}%") - ); - } - - return $query->with(['details', 'attributes'])->paginate($params['per_page'] ?? 15); - } -} -``` - -### 2.3 체크리스트 - -- [x] Item 모델 생성 -- [x] ItemDetail 모델 생성 -- [x] ItemAttribute 모델 생성 -- [x] ItemService 생성 -- [x] ItemRequest 생성 - ---- - -## Phase 3: Item-Master 연동 수정 - -### 3.1 ItemPage.source_table 변경 - -```php -// app/Models/ItemMaster/ItemPage.php - -// 기존 -$mapping = [ - 'products' => \App\Models\Product::class, - 'materials' => \App\Models\Material::class, -]; - -// 변경 -$mapping = [ - 'items' => \App\Models\Item::class, -]; -``` - -### 3.2 item_pages 데이터 업데이트 - -```sql --- source_table 통합 -UPDATE item_pages SET source_table = 'items' WHERE source_table IN ('products', 'materials'); -``` - -### 3.3 체크리스트 - -- [x] ItemPage 모델 수정 (getTargetModelClass) -- [x] item_pages.source_table 마이그레이션 -- [x] ItemMasterService 연동 테스트 - ---- - -## Phase 4: API 통합 - -### 4.1 API 구조 변경 - -``` -기존 (분리): - /api/v1/products → ProductController - /api/v1/products/materials → MaterialController - -통합 후: - /api/v1/items → ItemController - /api/v1/items?item_type=FG → Products 조회 - /api/v1/items?item_type=SM → Materials 조회 -``` - -### 4.2 ItemController - -```php -// app/Http/Controllers/Api/V1/ItemController.php -class ItemController extends Controller -{ - public function __construct(private ItemService $service) {} - - public function index(ItemIndexRequest $request) - { - return ApiResponse::handle(fn() => [ - 'data' => $this->service->index($request->validated()), - ], __('message.fetched')); - } - - public function store(ItemStoreRequest $request) - { - return ApiResponse::handle(fn() => [ - 'data' => $this->service->store($request->validated()), - ], __('message.created')); - } -} -``` - -### 4.3 라우트 - -```php -// routes/api_v1.php -Route::prefix('items')->group(function () { - Route::get('/', [ItemController::class, 'index']); - Route::post('/', [ItemController::class, 'store']); - Route::get('/{id}', [ItemController::class, 'show']); - Route::patch('/{id}', [ItemController::class, 'update']); - Route::delete('/{id}', [ItemController::class, 'destroy']); -}); -``` - -### 4.4 체크리스트 - -- [x] ItemController 생성 -- [x] ItemIndexRequest, ItemStoreRequest 등 생성 -- [x] 라우트 등록 -- [x] Swagger 문서 작성 -- [x] 기존 ProductController, MaterialController 제거 - ---- - -## Phase 5: 참조 테이블 마이그레이션 - -### 5.1 변경 대상 - -| 테이블 | 기존 | 변경 | -|--------|------|------| -| product_components | ref_type + ref_id | child_item_id | -| bom_template_items | ref_type + ref_id | item_id | -| orders | product_id | item_id | -| order_items | product_id | item_id | -| material_receipts | material_id | item_id | -| lots | material_id | item_id | -| price_histories | item_type + item_id | item_id | -| item_fields | source_table 'products'\|'materials' | source_table 'items' | - -### 5.2 체크리스트 - -- [x] 각 참조 테이블 마이그레이션 작성 -- [x] 관련 모델 관계 업데이트 -- [x] 데이터 검증 - ---- - -## Phase 6: 정리 - -### 6.1 체크리스트 - -- [x] CRUD 테스트 (전체 item_type) -- [x] BOM 계산 테스트 -- [x] Item-Master 연동 테스트 -- [x] 참조 무결성 테스트 -- [x] products 테이블 삭제 -- [x] materials 테이블 삭제 -- [x] 기존 Product, Material 모델 삭제 -- [x] 기존 ProductService, MaterialService 삭제 - ---- - -## 테이블 구조 요약 - -``` -┌─────────────────────────────────────────────────────┐ -│ items (핵심) │ -├─────────────────────────────────────────────────────┤ -│ id, tenant_id, item_type, code, name, unit │ -│ category_id, bom (JSON), is_active │ -│ timestamps + soft deletes │ -└─────────────────────┬───────────────────────────────┘ - │ 1:1 - ┌───────────────┴───────────────┐ - ▼ ▼ -┌─────────────┐ ┌─────────────┐ -│item_details │ │item_attrs │ -├─────────────┤ ├─────────────┤ -│ is_sellable │ │ attributes │ -│ is_purch... │ │ options │ -│ safety_stk │ └─────────────┘ -│ lead_time │ -│ is_inspect │ -└─────────────┘ -``` - ---- - -## BOM 계산 로직 - -### 통합 전 -```php -foreach ($bom as $item) { - if ($item['child_item_type'] === 'product') { - $child = Product::find($item['child_item_id']); - } else { - $child = Material::find($item['child_item_id']); - } -} -``` - -### 통합 후 -```php -$childIds = collect($bom)->pluck('child_item_id'); -$children = Item::whereIn('id', $childIds)->get()->keyBy('id'); -``` - ---- - -## 프론트엔드 전달 사항 - -### API 엔드포인트 변경 - -| 기존 | 통합 | -|------|------| -| `GET /api/v1/products` | `GET /api/v1/items?item_type=FG` | -| `GET /api/v1/products?product_type=PART` | `GET /api/v1/items?item_type=PART` | -| `GET /api/v1/products/materials` | `GET /api/v1/items?item_type=SM` | - -### 응답 필드 변경 - -| 기존 | 통합 | -|------|------| -| `product_type` | `item_type` | -| `material_type` | `item_type` | -| `material_code` | `code` | - -### BOM 요청/응답 변경 - -**요청 (Request)**: -```json -// 기존: BOM 저장 시 ref_type 지정 필요 -{ - "bom": [ - { "ref_type": "PRODUCT", "ref_id": 5, "quantity": 2 }, - { "ref_type": "MATERIAL", "ref_id": 10, "quantity": 1 } - ] -} - -// 통합: item_id만 사용 -{ - "bom": [ - { "child_item_id": 5, "quantity": 2 }, - { "child_item_id": 10, "quantity": 1 } - ] -} -``` - -**응답 (Response)**: -```json -// 기존 -{ "child_item_type": "product", "child_item_id": 5, "quantity": 2 } - -// 통합 -{ "child_item_id": 5, "quantity": 2 } -``` - -**프론트엔드 수정 포인트**: -- BOM 구성품 추가 시 `ref_type` 선택 UI 제거 -- 품목 검색 시 `/api/v1/items` 단일 엔드포인트 사용 -- BOM 저장 payload에서 `ref_type`, `ref_id` → `child_item_id`로 변경 - ---- - -## 일정 - -| Phase | 작업 | 상태 | -|-------|------|------| -| 0 | 데이터 정규화 (비표준 item_type/BOM 삭제) | ✅ 완료 | -| 1 | items 테이블 생성 + 데이터 이관 | ✅ 완료 | -| 2 | Item 모델 + Service 생성 | ✅ 완료 | -| 3 | Item-Master 연동 수정 | ✅ 완료 | -| 4 | API 통합 | ✅ 완료 | -| 5 | 참조 테이블 마이그레이션 | ✅ 완료 | -| 6 | 정리 | ✅ 완료 | - -> **완료일**: 2025-12-15 -> **관련 커밋**: `039fd62` (products/materials 테이블 삭제), `a93dfe7` (Phase 6 완료) - ---- - -## 리스크 - -| 리스크 | 대응 | -|--------|------| -| 데이터 이관 누락 | 이관 전후 건수 검증 | -| Item-Master 연동 오류 | source_table 변경 전 테스트 | -| BOM 순환 참조 | 저장 시 검증 로직 추가 | -| Code 중복 (products↔materials) | 개발 중이므로 품목관리 완료 후 경동기업 데이터 전체 삭제 후 재세팅 예정. 중복 데이터는 삭제 처리 | - ---- - -## 롤백 계획 - -각 Phase는 독립적 마이그레이션으로 구성: -```bash -# Phase 1 롤백 -php artisan migrate:rollback --step=3 - -# 데이터 복구 (products/materials 테이블 유지 상태에서) -# 신규 테이블만 삭제하면 됨 -``` \ No newline at end of file diff --git a/plans/archive/kd-items-migration-plan.md b/plans/archive/kd-items-migration-plan.md deleted file mode 100644 index 7710c32..0000000 --- a/plans/archive/kd-items-migration-plan.md +++ /dev/null @@ -1,1293 +0,0 @@ -# 경동기업(5130) 품목/단가 마이그레이션 계획 - -> **작성일**: 2026-01-28 -> **목적**: 경동기업 레거시 시스템(5130/)의 **품목(items), 단가(prices), BOM** 데이터를 SAM으로 이관 -> **기준 문서**: `5130/` 폴더 분석 결과 -> **상태**: 🔄 분석 완료, 구현 대기 -> **데이터 규모**: ~1,500 레코드 (items ~800 + prices ~500 + BOM ~200) - ---- - -## 🚀 새 세션 시작 가이드 (Quick Start) - -### 이 문서만 보고 작업을 재개하려면: - -```bash -# 1. Docker 서비스 확인 -docker ps | grep sam - -# 2. 레거시 DB (chandj) 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" - -# 3. 현재 진행 상태 확인 -# → 아래 "📍 현재 진행 상태" 섹션 참조 - -# 4. 다음 작업 시작 -# → "📍 현재 진행 상태" > "다음 작업" 참조 -``` - -### 환경 정보 - -| 항목 | 값 | -|------|-----| -| **프로젝트 루트** | `/Users/kent/Works/@KD_SAM/SAM` | -| **레거시 소스** | `5130/` (프로젝트 루트 직하) | -| **API 프로젝트** | `api/` | -| **Docker 컨테이너** | `sam-mysql-1` | -| **레거시 DB** | `chandj` (MySQL) | -| **SAM DB** | `samdb` (MySQL) ⚠️ | -| **대상 테넌트 ID** | `287` (경동기업) | -| **생성자 사용자 ID** | `1` | - -### DB 접속 명령어 - -```bash -# 레거시 DB (chandj) 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot chandj - -# SAM DB 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot samdb - -# 레거시 테이블 목록 확인 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SHOW TABLES;" - -# SAM items 테이블 확인 -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" -``` - -### 전제 조건 (작업 전 확인) - -- [x] Docker 서비스 실행 중 -- [x] `sam-mysql-1` 컨테이너 실행 중 -- [x] chandj 데이터베이스 접근 가능 -- [ ] SAM items 마이그레이션 실행 완료 (`php artisan migrate`) -- [ ] SAM prices 마이그레이션 실행 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | ✅ **정적 데이터 마이그레이션 완료** | -| **다음 작업** | 동적 BOM/견적 로직 구현 → [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) | -| **진행률** | 4/4 (100%) - 정적 데이터 완료 | -| **마지막 업데이트** | 2026-01-28 | - -> ⚠️ **주의**: 이 문서는 **정적 품목/단가 데이터 이관**만 다룹니다. -> 동적 BOM 계산, 모터/제어기/부자재 자동 추가 등 **견적 로직**은 별도 문서 참조: -> → [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) - -### Phase 1~3 실행 결과 ✅ - -| 소스 | 타입 | 건수 | -|------|------|------| -| KDunitprice | FG/PT/SM/RM/CS | 601건 | -| models | FG | +18건 | -| item_list | PT | +9건 | -| BDmodels.seconditem | PT (누락 부품) | +6건 | -| price_motor | SM (누락 품목) | +13건 | -| price_raw_materials | RM (누락 품목) | +4건 | -| **items 합계** | | **651건** | -| **prices 합계** | | **651건** | -| **BOM 연결** | items.bom JSON | **18건** | - -**Phase 2 상세:** -- Phase 2.1: BDmodels.seconditem → PT items 6건 추가 - - L-BAR, 보강평철, 케이스, 하단마감재, 가이드레일용 연기차단재, 케이스용 연기차단재 -- Phase 2.2: BDmodels → items.bom JSON 연결 18건 - - FG items (models 기반) ↔ PT items (seconditem) 연결 - -**Phase 3 상세:** -- Phase 3.1: price_motor → SM items 13건 추가 - - PM-020~PM-032: 제어기 (6P~18P, 20회선~100회선) - - PM-033~PM-035: 방화/방범 콘트롤박스, 스위치 -- Phase 3.2: price_raw_materials → RM items 4건 추가 - - RM-007: 신설비상문 (3x2 300*200) - - RM-008~RM-009: 제연커튼 (연기차단원단, 불투명) - - RM-010~RM-011: 화이바원단, 와이어원단 -- 중복 확인: KDunitprice 기존 품목과 명칭 비교로 중복 제외 - -### Phase 4 검증 결과 ✅ - -**로컬 검증 완료 (2026-01-28):** - -| 검증 항목 | 기대값 | 실제값 | 상태 | -|-----------|--------|--------|------| -| items 총 건수 | 651건 | 651건 | ✅ | -| prices 총 건수 | 651건 | 651건 | ✅ | -| BOM 연결 | 18건 | 18건 | ✅ | -| code 중복 | 0건 | 0건 | ✅ | - -**item_type 분포:** -| item_type | 건수 | -|-----------|------| -| FG (완제품) | 470건 | -| PT (부품) | 88건 | -| SM (부자재) | 61건 | -| RM (원자재) | 28건 | -| CS (소모품) | 4건 | - -### 후속 작업 - -**이 문서 범위 (정적 데이터):** -- ✅ 완료 - 개발서버 배포 대기 중 - -**별도 문서 (동적 로직):** -- → [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) -- 5130 견적 로직 분석 -- 동적 BOM 계산 (모터/제어기/부자재) -- 파라미터 기반 절곡품 산출 - -### Seeder 재실행 방법 - -```bash -# Docker 컨테이너 내부에서 실행 -docker exec sam-api-1 php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder" -``` - ---- - -## 0. 성공 기준 - -| 기준 | 목표값 | 확인 방법 | -|------|-------|----------| -| **items 합계** | **~800건** | `SELECT COUNT(*) FROM items WHERE tenant_id=287` | -| items (FG) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='FG'` | -| items (PT) | ~250건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='PT'` | -| items (SM) | ~300건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='SM'` | -| items (RM) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='RM'` | -| items (CS) | ~50건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='CS'` | -| **prices 합계** | **~500건** | `SELECT COUNT(*) FROM prices WHERE tenant_id=287` | -| **BOM 관계** | ~300건 | `SELECT COUNT(*) FROM item_bom_items WHERE tenant_id=287` | -| code 유일성 | 100% | `SELECT code, COUNT(*) FROM items WHERE tenant_id=287 GROUP BY code HAVING COUNT(*) > 1` (0건) | -| API 테스트 | 100% | `/api/v1/items` 목록 조회 성공 | - ---- - -## 1. 개요 - -### 1.1 배경 - -경동기업은 **모델(Model) + 제품(Product)** 분리 구조를 사용하지만, SAM은 **통합 items 테이블** 구조로 기획됨. 레거시 5130/ 폴더의 데이터를 SAM 구조에 맞게 변환하여 이관 필요. - -### 1.2 핵심 차이점 - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 레거시 (chandj) → SAM (samdb) │ -├────────────────────────────────────────────────────────────────────────────┤ -│ 📦 품목 마스터 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ KDunitprice (603건, 핵심!) → items (마스터, code로 구분) │ -│ models (18건) → items (FG) │ -│ parts, parts_sub (170건) → item_bom_items │ -│ category_l1~l4 → items 카테고리 참조 │ -│ guiderail, bottombar, bending 등 → item_details │ -│ │ -│ 💰 단가 정보 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ price_* (10개 테이블) → prices │ -│ KDunitprice.출고가/입고가 → prices (기본가) │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.2.1 중복 제거 전략 ⭐ - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심: KDunitprice가 마스터, code 필드로 중복 방지 │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1️⃣ KDunitprice (603건) → items 먼저 생성 │ -│ - item_div로 item_type 결정 │ -│ - code = prodcode 그대로 사용 ⭐ │ -│ │ -│ 2️⃣ price_* 테이블 → items 중복 확인 후 prices만 생성 │ -│ - code로 items 조회 │ -│ - 존재하면 → prices만 추가 (item_id 연결) │ -│ - 없으면 → items 생성 후 prices 추가 │ -│ │ -│ 3️⃣ 매핑 테이블 불필요 │ -│ - item_id_mappings ❌ (양방향 조회 불필요) │ -│ - chandj는 손 안댐, samdb에만 밀어 넣음 │ -│ │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 SAM items 구조 (Target) - -```sql --- items 테이블 (tenant_id=287 for 경동기업) --- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) -CREATE TABLE items ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - item_type VARCHAR(15) NOT NULL, -- FG, PT, SM, RM, CS - code VARCHAR(100) NOT NULL, -- 품목코드 (← KDunitprice.prodcode) - name VARCHAR(255) NOT NULL, -- 품목명 (← KDunitprice.item_name) - unit VARCHAR(20), -- 단위 (← KDunitprice.unit) - category_id BIGINT, -- 카테고리 ID - process_type VARCHAR(50), -- 공정 타입 - item_category VARCHAR(50), -- 품목 분류 - bom JSON, -- BOM 정보 - attributes JSON, -- 동적 필드 값 (spec 등) - attributes_archive JSON, -- 속성 아카이브 - options JSON, -- 추가 옵션 - description TEXT, -- 설명 - is_active BOOLEAN DEFAULT TRUE, - created_by BIGINT, - updated_by BIGINT, - deleted_by BIGINT, - created_at TIMESTAMP, - updated_at TIMESTAMP, - deleted_at TIMESTAMP -- Soft Delete -); -``` - -### 1.4 item_type 분류 - -| SAM item_type | 설명 | 레거시 소스 | -|---------------|------|-------------| -| **FG** | 완제품 (Finished Goods) | KDunitprice[제품], KDunitprice[상품], models | -| **PT** | 부품 (Parts) | KDunitprice[반제품], parts, parts_sub, category_l4 | -| **SM** | 부자재 (Sub-Materials) | KDunitprice[부재료], price_* 테이블들 | -| **RM** | 원자재 (Raw Materials) | KDunitprice[원재료], price_raw_materials | -| **CS** | 소모품 (Consumables) | KDunitprice[무형상품], 기타 | - -### 1.4.1 KDunitprice.item_div → item_type 매핑 ⭐ - -```sql --- KDunitprice.item_div 값 목록 (603건 중) --- [제품], [상품], [부재료], [원재료], [반제품], [무형상품] - -CASE item_div - WHEN '[제품]' THEN 'FG' -- 완제품 - WHEN '[상품]' THEN 'FG' -- 상품도 완제품으로 분류 - WHEN '[반제품]' THEN 'PT' -- 반제품 = 부품 - WHEN '[부재료]' THEN 'SM' -- 부자재 - WHEN '[원재료]' THEN 'RM' -- 원자재 - WHEN '[무형상품]' THEN 'CS' -- 소모품/무형 - ELSE 'SM' -- 기본값 -END AS item_type -``` - ---- - -## 2. 레거시 DB 구조 분석 - -### 2.1 핵심 테이블 및 레코드 수 - -#### 📦 품목 마스터 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`KDunitprice`** ⭐ | **603** | **품목 마스터 (핵심!)** | **items (마스터)** | -| `models` | 18 | 모델 마스터 (스크린/철재) | items (FG) | -| `BDmodels` | 59 | 모델별 BOM + 단가 (JSON) | item_bom_items + prices | -| `parts` | 36 | 부품 | item_bom_items | -| `parts_sub` | 134 | 하위 부품 | item_bom_items | -| `category_l1` | 2 | 1단계 카테고리 (스크린/철재) | 참조용 | -| `category_l2` | 14 | 2단계 카테고리 | 참조용 | -| `category_l3` | 24 | 3단계 카테고리 | 참조용 | -| `category_l4` | 37 | 4단계 카테고리 (부품) | items (PT) | -| `item_list` | 5+ | 품목 마스터 | items (PT) | - -#### 💰 단가 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| `price_motor` | 2 (JSON) | 모터 단가 | prices | -| `price_shaft` | 2 (JSON) | 감기샤프트 단가 | prices | -| `price_pipe` | 2 (JSON) | 파이프 단가 | prices | -| `price_angle` | 2 (JSON) | 앵글 단가 | prices | -| `price_raw_materials` | 6 (JSON) | 주자재 단가 | prices | -| `price_bend` | 3 (JSON) | 절곡 단가 | prices | -| `price_pole` | 2 (JSON) | 폴 단가 | prices | -| `price_screenplate` | 2 (JSON) | 스크린플레이트 단가 | prices | -| `price_smokeban` | 2 (JSON) | 연기차단 단가 | prices | - -### 2.2 KDunitprice 테이블 구조 ⭐ (핵심 마스터) - -```sql --- KDunitprice: 품목 마스터 (603건) - 가장 중요한 테이블! --- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) -num INT PRIMARY KEY, -- PK -is_deleted INT, -- 삭제 여부 -prodcode VARCHAR(50), -- items.code (유니크 키!) ⭐ -item_name VARCHAR(255), -- items.name ⭐ -item_div VARCHAR(20), -- [제품]/[상품]/[부재료]/[원재료]/[반제품]/[무형상품] → item_type ⭐ -spec VARCHAR(100), -- items.attributes.spec -unit VARCHAR(20), -- items.unit -unitprice DECIMAL, -- prices.sales_price (단일 컬럼, 입고가/출고가 구분 없음!) ⭐ -searchtag TEXT, -- 검색 태그 -update_log TEXT -- 변경 이력 -``` - -**item_div 분포 확인 쿼리**: -```sql -SELECT item_div, COUNT(*) FROM KDunitprice WHERE is_deleted=0 GROUP BY item_div; --- [제품] ~100건 → FG --- [상품] ~50건 → FG --- [반제품] ~100건 → PT --- [부재료] ~200건 → SM --- [원재료] ~100건 → RM --- [무형상품] ~53건 → CS -``` - -### 2.3 BDmodels 테이블 구조 (BOM + 단가) - -```sql --- BDmodels: 모델별 BOM 및 단가 정보 -num INT PRIMARY KEY, -major_category VARCHAR(10), -- 스크린/철재 -spec VARCHAR(30), -- 규격 (60*40, 120*70 등) -model_name VARCHAR(255), -- 모델명 -finishing_type ENUM('SUS마감','EGI마감'), -check_type VARCHAR(20), -- 벽면형/측면형/혼합형 -seconditem VARCHAR(30), -- 부품명 (가이드레일, 하단마감재, L-BAR 등) -unitprice TEXT, -- 단가 (문자열) -savejson TEXT, -- BOM 상세 JSON -description TEXT, -is_deleted, priceDate DATE -``` - -**savejson 예시** (가이드레일 BOM): -```json -[ - {"col1":"1번(마감재)","col2":"SUS 1.2T","col3":"-3","col4":"203","col5":"206","col6":"51,000","col7":"10,506","col8":"2","col9":"21,012","col10":"삭제"}, - {"col1":"2번(본체)","col2":"EGI 1.55T","col3":"-5","col4":"294","col5":"299","col6":"27,000","col7":"8,073","col8":"1","col9":"8,073","col10":"삭제"} -] -``` - -### 2.4 단가 시스템 상세 분석 ⭐ - -#### 2.4.1 레거시 단가 테이블 전체 목록 (10개) - -| 테이블명 | 레코드 수 | 최신 날짜 | 용도 | -|---------|----------|----------|------| -| `price_motor` | 2 | 2024-08-25 | 전동개폐기, 제어기 단가 | -| `price_shaft` | 2 | 2024-08-25 | 감기샤프트 단가 | -| `price_pipe` | 2 | 2024-08-26 | 파이프 단가 | -| `price_angle` | 2 | 2024-08-26 | 앵글 단가 | -| `price_raw_materials` | 6 | 2025-06-18 | 슬랫, 스크린 원자재 단가 | -| `price_bend` | 3 | 2025-03-09 | 절곡 단가 | -| `price_pole` | 2 | 2024-08-26 | 폴 단가 | -| `price_screenplate` | 2 | 2024-08-26 | 스크린플레이트 단가 | -| `price_smokeban` | 2 | 2024-08-26 | 연기차단 단가 | -| `price_etc` | 0 | - | 기타 (개별 컬럼 방식, 비활성) | - -#### 2.4.2 SAM prices 테이블 구조 (Target) - -```sql -CREATE TABLE prices ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - - -- 품목 연결 - item_type_code VARCHAR(20), -- FG/PT/SM/RM/CS - item_id BIGINT, -- items.id FK - client_group_id BIGINT NULL, -- NULL = 기본가 - - -- 원가 정보 - purchase_price DECIMAL(15,4), -- 매입단가 (원가) - processing_cost DECIMAL(15,4), -- 가공비 - loss_rate DECIMAL(5,2), -- LOSS율 (%) - - -- 판매가 정보 - margin_rate DECIMAL(5,2), -- 마진율 (%) - sales_price DECIMAL(15,4), -- 판매단가 ⭐ - rounding_rule ENUM('round','ceil','floor'), - rounding_unit INT DEFAULT 1, -- 반올림 단위 - - -- 메타 정보 - supplier VARCHAR(255), -- 공급업체 - effective_from DATE, -- 적용 시작일 ⭐ - effective_to DATE NULL, -- 적용 종료일 - note TEXT, - - -- 상태 관리 - status ENUM('draft','active','inactive','finalized'), - is_final BOOLEAN DEFAULT FALSE, - - -- 감사 컬럼 - created_by, updated_by, deleted_by, timestamps, soft_deletes -); -``` - ---- - -## 3. 매핑 설계 - -### 3.1 models → items (FG 완제품) - -| 레거시 (models) | SAM (items) | 비고 | -|----------------|-------------|------| -| model_id | (신규 생성) | | -| model_name | code | KSS01 → FG-KSS01 | -| - | name | 모델명 + 마감타입 + 가이드타입 조합 | -| major_category | attributes.major_category | 스크린/철재 | -| finishing_type | attributes.finishing_type | SUS마감/EGI마감 | -| guiderail_type | attributes.guiderail_type | 벽면형/측면형/혼합형 | -| - | item_type | 'FG' | -| - | tenant_id | 287 | - -**코드 생성 규칙**: -``` -FG-{model_name}-{guiderail_type}-{finishing_type} -예: FG-KSS01-벽면형-SUS -``` - -### 3.2 price_* → prices 테이블 (단가 연동) ⭐ - -> **중요**: 단가 데이터는 items.attributes가 아닌 **prices 테이블**에 별도 관리 - -| 레거시 (price_*) | SAM (prices) | 비고 | -|-----------------|--------------|------| -| registedate | effective_from | 적용 시작일 | -| itemList.col13 (판매가) | sales_price | | -| itemList.col11 (원가) | purchase_price | | -| - | item_type_code | FG/PT/SM/RM/CS | -| - | item_id | items.id FK | -| - | client_group_id | NULL (기본가) | -| - | status | 'active' | - ---- - -## 4. 대상 범위 - -### 4.1 Phase 1: 마스터 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| **1.0** | **KDunitprice → items (마스터)** ⭐ | ⏳ | **603건 (최우선!)** | -| 1.1 | models → items (FG) INSERT 쿼리 작성 | ⏳ | 18건 (중복 확인 후) | -| 1.2 | item_list → items (PT) INSERT 쿼리 작성 | ⏳ | 5건+ (중복 확인 후) | -| 1.3 | category_l4 → items (PT) INSERT 쿼리 작성 | ⏳ | 37건 (중복 확인 후) | -| 1.4 | price_motor 파싱 → prices 연결 | ⏳ | code로 items 조회 후 prices 생성 | -| 1.5 | price_shaft 파싱 → prices 연결 | ⏳ | ~15건 | -| 1.6 | price_raw_materials 파싱 → prices 연결 | ⏳ | ~20건 | -| 1.7 | ⚠️ **사용자 승인**: Phase 1 INSERT 실행 | ⏳ | | - -### 4.2 Phase 2: BOM 및 상세 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | BDmodels.savejson → item_bom_items | ⏳ | 59건 | -| 2.2 | parts → item_bom_items | ⏳ | 36건 | -| 2.3 | parts_sub → item_bom_items | ⏳ | 134건 | -| 2.4 | guiderail/bottombar/bending 등 → item_details | ⏳ | 제품 상세 | -| 2.5 | parent_item_id, child_item_id 매핑 | ⏳ | code 기반 조회 | - -### 4.3 Phase 3: 단가 데이터 이관 ⭐ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | price_motor → items (SM) + prices | ⏳ | ~35건 품목 + 단가 | -| 3.2 | price_shaft → items (SM) + prices | ⏳ | ~15건 | -| 3.3 | price_pipe → items (SM) + prices | ⏳ | ~10건 | -| 3.4 | price_angle → items (SM) + prices | ⏳ | ~10건 | -| 3.5 | price_raw_materials → items (RM) + prices | ⏳ | ~20건 | -| 3.6 | price_bend → items (SM) + prices | ⏳ | ~10건 | -| 3.7 | price_pole → items (SM) + prices | ⏳ | ~5건 | -| 3.8 | price_screenplate → items (SM) + prices | ⏳ | ~5건 | -| 3.9 | price_smokeban → items (SM) + prices | ⏳ | ~5건 | -| 3.10 | 단가 버전 이력 정리 | ⏳ | effective_from/to 설정 | -| 3.11 | ⚠️ **사용자 승인**: 단가 INSERT 실행 | ⏳ | | - -### 4.4 Phase 4: 검증 및 배포 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | 로컬 테스트 | ⏳ | | -| 4.2 | API 테스트 | ⏳ | | -| 4.3 | 개발서버 배포 | ⏳ | ⚠️ 컨펌 필요 | - ---- - -## 5. Seeder 파일 - -### 5.0 Seeder 구조 및 실행 방법 - -**파일 위치**: `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` - -**실행 명령어**: -```bash -# 로컬 실행 (tenant_id=287만 삭제 후 INSERT) -cd /Users/kent/Works/@KD_SAM/SAM/api -php artisan db:seed --class=Database\\Seeders\\Kyungdong\\KyungdongItemSeeder - -# 개발서버 실행 (TRUNCATE 후 INSERT) - ⚠️ 컨펌 필요 -php artisan db:seed --class=Database\\Seeders\\Kyungdong\\KyungdongItemSeeder --env=development -``` - -**환경별 삭제 전략**: -| 환경 | 삭제 방식 | 비고 | -|------|----------|------| -| 로컬 (local) | `DELETE WHERE tenant_id=287` | 다른 테넌트 데이터 보존 | -| 개발 (development) | `TRUNCATE` | 전체 초기화 | - ---- - -### 5.1 KyungdongItemSeeder.php (전체 코드) - -```php -command->info('🚀 경동기업 품목/단가 마이그레이션 시작...'); - - // 1. 기존 데이터 삭제 - $this->cleanupExistingData(); - - // 2. KDunitprice → items - $itemCount = $this->migrateItems(); - - // 3. KDunitprice → prices - $priceCount = $this->migratePrices(); - - $this->command->info("✅ 완료: items {$itemCount}건, prices {$priceCount}건"); - } - - /** - * 기존 데이터 삭제 - */ - private function cleanupExistingData(): void - { - if (App::environment('local')) { - // 로컬: tenant_id=287만 삭제 - $this->command->info(' 🧹 로컬 환경: tenant_id=287 데이터 삭제...'); - DB::table('prices')->where('tenant_id', self::TENANT_ID)->delete(); - DB::table('items')->where('tenant_id', self::TENANT_ID)->delete(); - } else { - // 개발/운영: TRUNCATE (⚠️ 주의) - $this->command->info(' 🧹 개발 환경: TRUNCATE...'); - DB::statement('SET FOREIGN_KEY_CHECKS=0'); - DB::table('prices')->truncate(); - DB::table('items')->truncate(); - DB::statement('SET FOREIGN_KEY_CHECKS=1'); - } - } - - /** - * KDunitprice → items 마이그레이션 - */ - private function migrateItems(): int - { - $this->command->info(' 📦 KDunitprice → items 마이그레이션...'); - - // chandj.KDunitprice에서 데이터 조회 - $kdItems = DB::connection('legacy') // config/database.php에 'legacy' 연결 필요 - ->table('KDunitprice') - ->where('is_deleted', 0) - ->whereNotNull('prodcode') - ->where('prodcode', '!=', '') - ->get(); - - $items = []; - $now = now(); - - foreach ($kdItems as $kd) { - $items[] = [ - 'tenant_id' => self::TENANT_ID, - 'item_type' => $this->mapItemType($kd->item_div), - 'code' => $kd->prodcode, - 'name' => $kd->item_name, - 'unit' => $kd->unit, - 'attributes' => json_encode([ - 'spec' => $kd->spec, - 'item_div' => $kd->item_div, - 'legacy_source' => 'KDunitprice', - 'legacy_num' => $kd->num, - ]), - 'is_active' => true, - 'created_by' => self::USER_ID, - 'updated_by' => self::USER_ID, - 'created_at' => $now, - 'updated_at' => $now, - ]; - - // 500건씩 배치 INSERT - if (count($items) >= 500) { - DB::table('items')->insert($items); - $items = []; - } - } - - // 남은 데이터 INSERT - if (!empty($items)) { - DB::table('items')->insert($items); - } - - return $kdItems->count(); - } - - /** - * KDunitprice → prices 마이그레이션 - */ - private function migratePrices(): int - { - $this->command->info(' 💰 KDunitprice → prices 마이그레이션...'); - - // items와 KDunitprice 조인하여 prices 생성 - $count = DB::statement(" - INSERT INTO prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, - effective_from, status, - created_by, updated_by, created_at, updated_at - ) - SELECT - ? AS tenant_id, - i.item_type AS item_type_code, - i.id AS item_id, - NULL AS client_group_id, - 0 AS purchase_price, - COALESCE(k.unitprice, 0) AS sales_price, - CURDATE() AS effective_from, - 'active' AS status, - ? AS created_by, - ? AS updated_by, - NOW(), NOW() - FROM items i - JOIN " . config('database.connections.legacy.database') . ".KDunitprice k - ON k.prodcode = i.code - WHERE i.tenant_id = ? - AND k.is_deleted = 0 - AND k.prodcode IS NOT NULL - AND k.prodcode != '' - ", [self::TENANT_ID, self::USER_ID, self::USER_ID, self::TENANT_ID]); - - return DB::table('prices')->where('tenant_id', self::TENANT_ID)->count(); - } - - /** - * item_div → item_type 매핑 - */ - private function mapItemType(?string $itemDiv): string - { - return match ($itemDiv) { - '[제품]', '[상품]' => 'FG', - '[반제품]' => 'PT', - '[부재료]' => 'SM', - '[원재료]' => 'RM', - '[무형상품]' => 'CS', - default => 'SM', - }; - } -} -``` - ---- - -### 5.2 Legacy DB 연결 설정 - -**config/database.php에 추가**: -```php -'connections' => [ - // ... 기존 연결들 - - 'legacy' => [ - 'driver' => 'mysql', - 'host' => env('LEGACY_DB_HOST', '127.0.0.1'), - 'port' => env('LEGACY_DB_PORT', '3306'), - 'database' => env('LEGACY_DB_DATABASE', 'chandj'), - 'username' => env('LEGACY_DB_USERNAME', 'root'), - 'password' => env('LEGACY_DB_PASSWORD', 'root'), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - ], -], -``` - -**.env에 추가**: -```env -LEGACY_DB_HOST=127.0.0.1 -LEGACY_DB_PORT=3306 -LEGACY_DB_DATABASE=chandj -LEGACY_DB_USERNAME=root -LEGACY_DB_PASSWORD=root -``` - ---- - -### 5.3 참고: SQL 쿼리 (직접 실행용) - -#### 5.3.1 KDunitprice → items (마스터) - -```sql --- ⚠️ 참고용 SQL (Seeder 사용 권장) --- KDunitprice: 품목 마스터 (603건) → SAM items - -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, description, is_active, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - -- item_div → item_type 매핑 - CASE item_div - WHEN '[제품]' THEN 'FG' - WHEN '[상품]' THEN 'FG' - WHEN '[반제품]' THEN 'PT' - WHEN '[부재료]' THEN 'SM' - WHEN '[원재료]' THEN 'RM' - WHEN '[무형상품]' THEN 'CS' - ELSE 'SM' - END AS item_type, - prodcode AS code, -- 유니크 키! ⭐ - item_name AS name, -- ⭐ - unit AS unit, - JSON_OBJECT( - 'spec', spec, -- ⭐ - 'item_div', item_div, - 'legacy_source', 'KDunitprice', - 'legacy_num', num - ) AS attributes, - NULL AS description, -- 비고 컬럼 없음 - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice -WHERE is_deleted = 0 - AND prodcode IS NOT NULL AND prodcode != ''; - --- 결과 확인 -SELECT item_type, COUNT(*) -FROM samdb.items -WHERE tenant_id = 287 -GROUP BY item_type; -``` - -#### 5.3.2 KDunitprice → prices (기본 단가) - -```sql --- ⚠️ 참고용 SQL (Seeder 사용 권장) --- unitprice 단일 컬럼 → sales_price, purchase_price는 0 -INSERT INTO samdb.prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, - effective_from, status, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - i.item_type AS item_type_code, - i.id AS item_id, - NULL AS client_group_id, -- 기본가 - 0 AS purchase_price, -- 입고가 컬럼 없음, 0으로 설정 - COALESCE(k.unitprice, 0) AS sales_price, -- ⭐ unitprice 사용 - CURDATE() AS effective_from, -- 적용일 - 'active' AS status, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice k -JOIN samdb.items i ON i.code = k.prodcode AND i.tenant_id = 287 -- ⭐ prodcode 사용 -WHERE k.is_deleted = 0 - AND k.prodcode IS NOT NULL AND k.prodcode != ''; -``` - -### 5.4 models → items (FG) - 추가 SQL 참고용 - -```sql --- ⚠️ 참고용 SQL (Seeder 확장 시 사용) --- 레거시 chandj.models → SAM items (FG) --- KDunitprice에 없는 것만 추가 (중복 확인 필요) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'FG' AS item_type, - CONCAT('FG-', model_name, '-', - COALESCE(guiderail_type, 'STD'), '-', - CASE finishing_type - WHEN 'SUS마감' THEN 'SUS' - WHEN 'EGI마감' THEN 'EGI' - ELSE 'STD' - END - ) AS code, - CONCAT(model_name, ' ', major_category, ' ', finishing_type, ' ', COALESCE(guiderail_type, '')) AS name, - 'EA' AS unit, - JSON_OBJECT( - 'major_category', major_category, - 'finishing_type', finishing_type, - 'guiderail_type', guiderail_type, - 'legacy_model_id', model_id - ) AS attributes, - CASE WHEN is_deleted = 0 THEN 1 ELSE 0 END AS is_active, - 1 AS created_by, - created_at, - updated_at -FROM chandj.models -WHERE is_deleted = 0; -``` - -### 5.5 category_l4 → items (PT) - 추가 SQL 참고용 - -```sql --- ⚠️ 참고용 SQL (Seeder 확장 시 사용) --- 레거시 4단계 카테고리 → SAM items (PT) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'PT' AS item_type, - CONCAT('PT-', l1.name, '-', l2.name, '-', l3.name, '-', l4.name) AS code, - l4.name AS name, - 'EA' AS unit, - JSON_OBJECT( - 'category_l1', l1.name, - 'category_l2', l2.name, - 'category_l3', l3.name, - 'category_l4', l4.name, - 'legacy_l4_id', l4.id - ) AS attributes, - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.category_l4 l4 -JOIN chandj.category_l3 l3 ON l4.parent_id = l3.id -JOIN chandj.category_l2 l2 ON l3.parent_id = l2.id -JOIN chandj.category_l1 l1 ON l2.parent_id = l1.id; -``` - -### 5.6 price_motor → items (SM) + prices - PHP 스크립트 참고용 - -```php -query(" - SELECT num, registedate, itemList - FROM price_motor - WHERE is_deleted = 0 - ORDER BY registedate DESC -"); -$priceRecords = $stmt->fetchAll(PDO::FETCH_ASSOC); - -// 최신 단가의 itemList 파싱 → items 생성 -$latestRecord = $priceRecords[0]; -$itemList = json_decode($latestRecord['itemList'], true); - -foreach ($itemList as $idx => $item) { - $voltage = $item['col1']; // 220, 380, 제어기, 방화, 방범 - $capacity = $item['col2']; // 150K(S), 300K, 노출형, 매립형... - $purchasePrice = (float)str_replace(',', '', $item['col11'] ?? '0'); - $salesPrice = (float)str_replace(',', '', $item['col13'] ?? '0'); - - // 품목 코드 생성 - $code = "SM-MOTOR-" . preg_replace('/[^A-Za-z0-9가-힣]/', '', $voltage) - . "-" . preg_replace('/[^A-Za-z0-9가-힣()]/', '', $capacity); - - // 품목명 생성 - if (in_array($voltage, ['220', '380'])) { - $name = "전동개폐기 {$voltage}V {$capacity}"; - $itemType = 'SM'; - } elseif ($voltage === '제어기') { - $name = "연동제어기 {$capacity}"; - $itemType = 'SM'; - } else { - $name = "{$voltage} {$capacity}"; - $itemType = 'SM'; - } - - // 1단계: items INSERT - $itemStmt = $pdo->prepare(" - INSERT INTO items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at - ) VALUES (?, ?, ?, ?, 'EA', ?, 1, ?, NOW(), NOW()) - ON DUPLICATE KEY UPDATE name = VALUES(name) - "); - $attributes = json_encode([ - 'voltage' => $voltage, - 'capacity' => $capacity, - 'legacy_source' => 'price_motor', - 'legacy_col_index' => $idx - ]); - $itemStmt->execute([$tenantId, $itemType, $code, $name, $attributes, $userId]); - $itemId = $pdo->lastInsertId(); - - // 2단계: prices INSERT (모든 버전) - foreach ($priceRecords as $priceIdx => $priceRecord) { - $priceItemList = json_decode($priceRecord['itemList'], true); - if (!isset($priceItemList[$idx])) continue; - - $priceItem = $priceItemList[$idx]; - $pPrice = (float)str_replace(',', '', $priceItem['col11'] ?? '0'); - $sPrice = (float)str_replace(',', '', $priceItem['col13'] ?? '0'); - $effectiveFrom = $priceRecord['registedate']; - - // 다음 레코드가 있으면 effective_to 설정 - $effectiveTo = isset($priceRecords[$priceIdx + 1]) - ? date('Y-m-d', strtotime($effectiveFrom . ' -1 day')) - : null; - - $status = ($priceIdx === 0) ? 'active' : 'inactive'; - - $priceStmt = $pdo->prepare(" - INSERT INTO prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, effective_from, effective_to, - status, created_by, created_at, updated_at - ) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) - "); - $priceStmt->execute([ - $tenantId, $itemType, $itemId, - $pPrice, $sPrice, $effectiveFrom, $effectiveTo, - $status, $userId - ]); - } - - echo "✓ {$code} - items + prices 생성 완료\n"; -} -``` - ---- - -## 6. 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 📦 데이터 전략 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - KDunitprice(603건)가 품목 마스터 → items 최우선 생성 │ -│ - code 필드로 중복 방지 (ON DUPLICATE KEY UPDATE) │ -│ - BOM은 item_bom_items 테이블 사용 (items.bom JSON ❌) │ -│ - 단가 정보는 prices 테이블에 별도 저장 (items.attributes ❌) │ -│ │ -│ ❌ 불필요한 것 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - item_id_mappings 테이블 (양방향 조회 불필요) │ -│ - chandj 수정 (손 안댐, samdb에만 밀어 넣음) │ -│ - 레거시 소스 확인 (마이그레이션 후 검증만) │ -│ │ -│ ✅ 필수 사항 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - 경동기업 기준으로 맞춤 (이미 사용중인 시스템) │ -│ - 전체 이관 (items + prices + BOM) │ -│ - SQL 쿼리 + PHP 스크립트 혼용 (JSON 파싱 필요) │ -│ - 로컬 검증 완료 후 개발서버 배포 │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.1 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | SELECT 쿼리, 분석, 매핑 설계 | 불필요 | -| ⚠️ 컨펌 필요 | INSERT 실행, TRUNCATE, 개발서버 배포 | **필수** | -| 🔴 금지 | 운영서버 직접 작업 | 별도 협의 | - ---- - -## 7. 데이터 규모 예상 - -### 7.1 items 테이블 예상 - -| 소스 | 레코드 수 | SAM item_type | 예상 items 건수 | -|------|----------|---------------|----------------| -| **KDunitprice** ⭐ | **603** | FG/PT/SM/RM/CS | **~603 (마스터)** | -| models | 18 | FG | ~0 (중복 제외) | -| category_l4 | 37 | PT | ~20 (일부 신규) | -| item_list | 5 | PT | ~0 (중복 제외) | -| price_* 테이블 | ~130 항목 | SM/RM | ~100 (신규만) | -| **items 합계** | - | - | **~700~800건** | - -**item_type별 분포 예상**: -| item_type | 설명 | 예상 건수 | -|-----------|------|----------| -| FG | 완제품 | ~100건 | -| PT | 부품 | ~250건 | -| SM | 부자재 | ~300건 | -| RM | 원자재 | ~100건 | -| CS | 소모품 | ~50건 | - -### 7.2 prices 테이블 예상 ⭐ - -| 소스 | 버전 수 | 품목당 단가 | 예상 prices 건수 | -|------|--------|------------|-----------------| -| KDunitprice | 1 | 603 | ~603 | -| price_motor | 2 | 35 | ~70 | -| price_shaft | 2 | 15 | ~30 | -| price_pipe | 2 | 10 | ~20 | -| price_angle | 2 | 10 | ~20 | -| price_raw_materials | 6 | 20 | ~120 | -| price_bend | 3 | 10 | ~30 | -| 기타 price_* | 2 | 15 | ~30 | -| **prices 합계** | - | - | **~500건** (중복 제외) | - ---- - -## 8. 체크리스트 - -### Phase 1: 마스터 데이터 이관 ✅ 완료 -- [x] 레거시 DB 구조 분석 완료 -- [x] KDunitprice 테이블 발견 및 분석 (603건, 핵심 마스터) -- [x] 중복 제거 전략 수립 (code 기반, 매핑 테이블 불필요) -- [x] Seeder 기반 마이그레이션 계획 수립 -- [x] ~~config/database.php에 'legacy' 연결 추가~~ → 기존 'chandj' 연결 사용 -- [x] ~~.env에 LEGACY_DB_* 환경변수 추가~~ → 기존 CHANDJ_DB_* 사용 -- [x] **Phase 1.0**: KDunitprice → items 601건, prices 601건 ✅ -- [x] **Phase 1.1**: models → items (FG) 18건 ✅ -- [x] **Phase 1.2**: item_list → items (PT) 9건 ✅ -- [x] ~~Phase 1.3: category_l4~~ → 스킵 (카테고리 데이터) -- [x] **Phase 1 결과**: items 628건, prices 628건 ✅ - -### Phase 2: BOM 데이터 이관 ✅ 완료 -- [x] BDmodels.seconditem → PT items 누락 부품 6건 추가 ✅ -- [x] ~~child_item_id 매핑 테이블 생성~~ → code 기반 직접 조회 -- [x] items.bom JSON 생성 (18건 FG ↔ PT 연결) ✅ -- [x] **최종 결과**: items 634건, prices 634건, BOM 18건 ✅ (2026-01-28) - -### Phase 3: 단가 데이터 이관 ✅ 완료 -- [x] 레거시 price_* 테이블 구조 분석 (10개) -- [x] 각 테이블별 JSON 스키마 분석 -- [x] SAM prices 테이블 구조 확인 -- [x] Legacy → SAM 단가 매핑 전략 수립 -- [x] price_motor → items (SM) 누락 품목 13건 추가 ✅ -- [x] price_raw_materials → items (RM) 누락 품목 4건 추가 ✅ -- [x] 기타 price_* 테이블 분석 완료 (대부분 계산 참조용, 품목 마스터 아님) - - price_shaft, price_pipe, price_angle, price_bend, price_pole, price_screenplate: 계산 참조용 - - 220V/380V 모터: KDunitprice에 "KD모터*Kg단상/삼상"으로 이미 존재 -- [x] **사용자 승인**: 완료 (2026-01-28) - -### Phase 4: 검증 및 배포 ✅ 로컬 검증 완료 -- [x] 건수 검증 ✅ (items 651건, prices 651건, BOM 18건) -- [x] 데이터 조회 테스트 ✅ (artisan tinker, MySQL 직접 쿼리) -- [x] code 중복 검증 ✅ (0건) -- [x] Phase 3 추가 품목 확인 ✅ (PM-* 13건, RM-* 4건) -- [ ] ⚠️ **사용자 승인**: 개발서버 배포 - ---- - -## 9. 참고 문서 - -- **레거시 소스**: `5130/` 폴더 -- **SAM items 마이그레이션**: `api/database/migrations/2025_12_13_152507_create_items_table.php` -- **SAM prices 마이그레이션**: `api/database/migrations/2025_12_08_154633_create_prices_table.php` -- **SAM price_revisions 마이그레이션**: `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php` -- **품목 분석**: `docs/data/analysis/item-db-analysis.md` -- **DummyItemSeeder**: `api/database/seeders/Dummy/DummyItemSeeder.php` -- **DummyDataSeeder**: `api/database/seeders/DummyDataSeeder.php` (TENANT_ID=287, USER_ID=1 상수 참조) -- **prices item_type_code 마이그레이션**: `api/database/migrations/2025_12_21_165524_update_prices_item_type_code_to_actual_item_type.php` -- **연관 문서**: `docs/plans/kd-orders-migration-plan.md` (입고/재고/주문 마이그레이션) - ---- - -## 10. 세션 및 메모리 관리 정책 - -### 10.1 세션 시작 시 (Load Strategy) -```bash -# 1. Docker 확인 -docker ps | grep sam - -# 2. DB 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" - -# 3. 현재 진행 상태 확인 -# → 이 문서의 "📍 현재 진행 상태" 섹션 참조 - -# 4. 마이그레이션 상태 확인 (API 프로젝트) -cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status -``` - -### 10.2 작업 중 관리 - -| 작업 완료 시 | 조치 | -|-------------|------| -| Phase 완료 | "📍 현재 진행 상태" 업데이트 | -| INSERT 실행 | "12. 변경 이력" 추가 | -| 스키마 변경 | 관련 섹션 업데이트 + 변경 이력 추가 | -| 오류 발생 | 체크리스트에 메모 추가 | - -### 10.3 컨텍스트 관리 - -| 컨텍스트 잔량 | 조치 | -|--------------|------| -| **30% 이하** | 현재 작업 중단점 문서에 기록 | -| **20% 이하** | "📍 현재 진행 상태" 최종 업데이트 | -| **10% 이하** | 세션 정리 및 다음 세션 가이드 작성 | - ---- - -## 11. 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 0 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Quick Start 전제 조건 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 SQL 쿼리 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 0 확인 방법 SQL | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 건수, 테이블명, 컬럼 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 문서 헤더 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 → 다음 작업 상세 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 9. 참고 문서 | -| Q4. 작업 완료 확인 방법은? | ✅ | 0. 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서, Quick Start DB 접속 명령어 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - -### 11.3 핵심 정보 요약 (새 세션용) - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 📋 핵심 정보 요약 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 🎯 목표: 경동기업 레거시(chandj) → SAM(samdb) 품목/단가 이관 │ -│ │ -│ 📊 데이터 규모 (총 ~1,500건): │ -│ - items: ~800건 (KDunitprice 603 + 추가) │ -│ - prices: ~500건 │ -│ - item_bom_items: ~200건 │ -│ │ -│ 🔑 핵심 상수: │ -│ - tenant_id = 287 (경동기업) │ -│ - user_id = 1 (생성자) │ -│ - Docker: sam-mysql-1 │ -│ - 레거시 DB: chandj / SAM DB: samdb ⚠️ │ -│ │ -│ ⭐ KDunitprice 실제 컬럼명 (2026-01-28 확인): │ -│ - prodcode (품목코드) → items.code │ -│ - item_name (품목명) → items.name │ -│ - spec (규격) → items.attributes.spec │ -│ - unit (단위) → items.unit │ -│ - item_div ([제품] 등) → items.item_type │ -│ - unitprice (단가, 단일 컬럼!) → prices.sales_price │ -│ │ -│ ⭐ 마이그레이션 순서 (Seeder 기반): │ -│ 1. config/database.php에 'legacy' 연결 추가 │ -│ 2. .env에 LEGACY_DB_* 환경변수 추가 │ -│ 3. KyungdongItemSeeder.php 파일 생성 ← 최우선! │ -│ 4. Seeder 실행 (items 603건 + prices 603건) │ -│ 5. 추가 items/BOM은 확장 Seeder로 처리 │ -│ │ -│ 📍 현재 상태: Phase 1 대기 (Seeder 파일 생성 및 실행) │ -│ │ -│ ❌ 불필요한 것: item_id_mappings (양방향 조회 불필요, chandj 손 안댐) │ -│ │ -│ ⚠️ 주의: 모든 INSERT 실행 전 사용자 승인 필요 │ -│ │ -│ 📎 연관 문서: docs/plans/kd-orders-migration-plan.md (입고/재고/주문) │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 12. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-28 | 문서 분리 | items-migration-kyungdong-plan.md에서 품목/단가 부분 분리 | - | - | -| 2026-01-28 | 문서 생성 | kd-items-migration-plan.md 신규 생성 | - | - | -| 2026-01-28 | 컬럼명 수정 | 실제 DB 컬럼명으로 업데이트 (품목코드→prodcode, 품목명→item_name 등) | - | - | -| 2026-01-28 | Seeder 전환 | SQL → Seeder 방식으로 전환, 섹션 5.0~5.6 구조 정리 | - | - | - ---- - -## 13. 트러블슈팅 가이드 - -### 13.1 일반적인 문제 - -| 문제 | 원인 | 해결책 | -|------|------|--------| -| Docker 컨테이너 없음 | Docker 미실행 | `docker-compose up -d` 실행 | -| DB 접속 실패 | 컨테이너명 변경 | `docker ps`로 정확한 컨테이너명 확인 | -| chandj DB 없음 | 레거시 DB 미설정 | Docker 볼륨 확인 또는 덤프 복원 | -| tenant_id 287 없음 | 경동기업 테넌트 미생성 | SAM에서 테넌트 생성 필요 | -| items 테이블 없음 | 마이그레이션 미실행 | `php artisan migrate` 실행 | -| **SAM DB 이름 오류** | `sam` 대신 `samdb` | 모든 쿼리에서 `samdb` 사용 확인 | -| KDunitprice 테이블 없음 | 레거시 덤프 불완전 | chandj 전체 덤프 확인 | - -### 13.2 JSON 파싱 오류 - -```php -// price_* 테이블의 itemList 파싱 시 주의사항 -$itemList = json_decode($record['itemList'], true); - -// 빈 값 또는 잘못된 JSON 처리 -if (empty($itemList) || !is_array($itemList)) { - // 스킵하고 로그 기록 - error_log("Invalid itemList in {$table} num={$record['num']}"); - continue; -} - -// 숫자 형식 변환 (콤마 제거) -$price = (float)str_replace(',', '', $item['col13'] ?? '0'); -``` - -### 13.3 중복 코드 처리 (code 기반) - -```sql --- 이미 존재하는 품목 확인 (code 유일성 검사) -SELECT code, COUNT(*) AS cnt -FROM samdb.items -WHERE tenant_id=287 -GROUP BY code -HAVING cnt > 1; - --- INSERT 시 ON DUPLICATE KEY UPDATE 사용 --- ⚠️ items 테이블에 (tenant_id, code) UNIQUE 인덱스 필요 -INSERT INTO samdb.items (...) VALUES (...) -ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW(); - --- KDunitprice와 price_* 중복 확인 (⭐ 실제 컬럼명 사용) -SELECT k.prodcode, '모터 150K' AS price_item -FROM chandj.KDunitprice k -WHERE k.item_name LIKE '%모터%150K%'; --- → KDunitprice가 마스터, price_*는 가격만 추가 -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/l2-permission-management-plan.md b/plans/archive/l2-permission-management-plan.md deleted file mode 100644 index e7490a2..0000000 --- a/plans/archive/l2-permission-management-plan.md +++ /dev/null @@ -1,378 +0,0 @@ -# L-2 권한관리 Mock → API 연동 계획 - -> **작성일**: 2025-12-30 -> **목적**: React 권한관리 페이지의 Mock 데이터를 API 연동으로 전환 -> **기준 문서**: mng.sam.kr/role-permissions -> **상태**: ✅ 완료 - Phase 1~4 전체 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 React 연동 완료 | -| **다음 작업** | 완료 (테스트 후 운영 배포) | -| **진행률** | 12/12 (100%) | -| **마지막 업데이트** | 2025-12-30 - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 React의 권한관리 페이지(`/settings/permissions`)는 `localStorage`와 `defaultPermissions` Mock 데이터를 사용하고 있습니다. mng 프로젝트에는 이미 완전한 역할-권한 관리 시스템이 구현되어 있으므로, api 프로젝트에 동일한 API를 개발하고 React에서 연동해야 합니다. - -**문제점:** -- React는 `localStorage`에 권한 데이터 저장 (새로고침/브라우저 변경 시 데이터 손실) -- 실제 DB 연동 없음 -- 역할 숨김(is_hidden) 기능이 DB 스키마에 없음 - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. React → api.sam.kr만 호출 (mng 직접 호출 금지) │ -│ 2. mng의 RoleService/RolePermissionService 로직 참조하여 api에 재구현 │ -│ 3. Spatie Permission 패키지 활용 (기존 테이블 구조 유지) │ -│ 4. Multi-tenant 지원 필수 (BelongsToTenant) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | API 엔드포인트 추가, 타입 정의, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | DB 마이그레이션 (is_hidden 컬럼), 기존 API 수정 | **필수** | -| 🔴 금지 | roles 테이블 구조 대폭 변경, 기존 권한 삭제 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/api-rules.md` - API 개발 규칙 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - DB 스키마 - ---- - -## 2. 현재 상태 분석 - -### 2.1 mng 프로젝트 (기준) - -| 파일 | 역할 | 주요 기능 | -|------|------|----------| -| `RoleController.php` | 역할 CRUD 화면 | index, create, edit | -| `RoleService.php` | 역할 비즈니스 로직 | getRoles, createRole, updateRole, deleteRole | -| `RolePermissionController.php` | 권한 매트릭스 화면 | index (테넌트별 역할 목록) | -| `RolePermissionService.php` | 권한 매트릭스 로직 | togglePermission, allowAll, denyAll, getMenuTree | -| `Role.php` (Model) | 역할 모델 | tenant, permissions, users 관계 | - -**mng의 역할 필드:** -```php -$fillable = ['tenant_id', 'name', 'description', 'guard_name']; -``` - -**⚠️ 숨김 기능 없음**: mng에도 `is_hidden` 필드가 없음 - -### 2.2 React 프로젝트 (현재) - -| 파일 | 현재 상태 | 문제점 | -|------|----------|--------| -| `index.tsx` | `localStorage` + `defaultPermissions` | 실제 DB 연동 없음 | -| `types.ts` | `Permission` 타입 정의 | `status: 'active' | 'hidden'` 있음 | -| `PermissionDetail.tsx` | 메뉴별 권한 설정 | Mock 데이터 사용 | - -**React의 Permission 타입:** -```typescript -interface Permission { - id: number; - name: string; - status: 'active' | 'hidden'; // ← DB에 없음! - menuPermissions: MenuPermission[]; - createdAt: string; -} -``` - -### 2.3 api 프로젝트 (현재) - -- **Role 관련 API 없음** (개발 필요) -- `shared/Models/Role.php` 존재 여부 확인 필요 - -### 2.4 DB 스키마 (roles 테이블) - -```sql -roles (11 컬럼): -- id (PK) -- tenant_id (FK → tenants.id) -- name -- guard_name (default: 'web') -- description -- created_by, updated_by, deleted_by -- created_at, updated_at, deleted_at - --- ⚠️ is_hidden 컬럼 없음! 추가 필요 -``` - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: DB 스키마 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | roles 테이블에 `is_hidden` 컬럼 추가 | ✅ | `2025_12_30_160802_add_is_hidden_to_roles_table.php` 생성완료, 실행대기 | -| 1.2 | 기존 역할 데이터 기본값 설정 (is_hidden = false) | ✅ | 마이그레이션에 포함 | - -### 3.2 Phase 2: api 프로젝트 - Role CRUD API - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | Role 모델 생성/수정 | ✅ | shared/Models/Role.php | -| 2.2 | RoleService 생성 | ✅ | `api/app/Services/RoleService.php` | -| 2.3 | RoleController 생성 | ✅ | `api/app/Http/Controllers/Api/V1/RoleController.php` | -| 2.4 | RoleFormRequest 생성 | ⏳ | StoreRoleRequest, UpdateRoleRequest 미생성 | -| 2.5 | routes/api.php 라우트 추가 | ✅ | 5개 CRUD 라우트 등록완료 | -| 2.6 | Swagger 문서 작성 | ✅ | `api/app/Swagger/v1/RoleApi.php` | - -### 3.3 Phase 3: api 프로젝트 - 권한 매트릭스 API - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | RolePermissionController 생성 | ✅ | `api/app/Http/Controllers/Api/V1/RolePermissionController.php` | -| 3.2 | 권한 목록 조회 API | ✅ | GET /roles/{id}/permissions | -| 3.3 | 권한 부여 API | ✅ | POST /roles/{id}/permissions | -| 3.4 | 권한 회수/동기화 API | ✅ | DELETE, PUT /roles/{id}/permissions/sync | -| 3.5 | Swagger 문서 작성 | ✅ | `api/app/Swagger/v1/RolePermissionApi.php` | - -### 3.4 Phase 4: React 연동 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | actions.ts 생성 | ✅ | 12개 Server Actions (fetchRoles, createRole, updateRole, deleteRole 등) | -| 4.2 | types.ts 수정 | ✅ | ApiResponse, Role, RoleStats, MenuTreeItem, PermissionMatrix 타입 추가 | -| 4.3 | index.tsx 수정 (목록) | ✅ | localStorage → API 연동, 로딩/에러 상태, toast 알림 | -| 4.4 | PermissionDetailClient.tsx 수정 (상세/권한매트릭스) | ✅ | 역할 CRUD, 권한 토글, 전체 허용/거부/초기화 | -| 4.5 | Mock 데이터 제거 | ✅ | defaultPermissions 삭제, API 기반으로 전환 | - ---- - -## 4. API 설계 - -### 4.1 Role CRUD API - -| Method | Endpoint | 설명 | Request | Response | -|--------|----------|------|---------|----------| -| GET | `/api/v1/roles` | 역할 목록 | `?search=&is_hidden=` | `{ data: Role[], meta: Pagination }` | -| GET | `/api/v1/roles/{id}` | 역할 상세 | - | `{ data: Role }` | -| POST | `/api/v1/roles` | 역할 생성 | `{ name, description, is_hidden }` | `{ data: Role }` | -| PUT | `/api/v1/roles/{id}` | 역할 수정 | `{ name, description, is_hidden }` | `{ data: Role }` | -| DELETE | `/api/v1/roles/{id}` | 역할 삭제 | - | `{ message }` | - -### 4.2 권한 매트릭스 API - -| Method | Endpoint | 설명 | Request | Response | -|--------|----------|------|---------|----------| -| GET | `/api/v1/roles/{id}/menus` | 메뉴 트리 + 권한 상태 | - | `{ data: MenuWithPermissions[] }` | -| POST | `/api/v1/roles/{id}/permissions/toggle` | 권한 토글 | `{ menu_id, permission_type }` | `{ data: { value: boolean } }` | -| POST | `/api/v1/roles/{id}/permissions/allow-all` | 전체 허용 | - | `{ message }` | -| POST | `/api/v1/roles/{id}/permissions/deny-all` | 전체 거부 | - | `{ message }` | -| POST | `/api/v1/roles/{id}/permissions/reset` | 기본값 초기화 | - | `{ message }` | - -### 4.3 Role 응답 타입 - -```typescript -interface Role { - id: number; - tenant_id: number; - name: string; - description: string | null; - guard_name: string; - is_hidden: boolean; // ← 신규 필드 - permissions_count: number; // ← 권한 개수 - users_count: number; // ← 사용자 수 - created_at: string; - updated_at: string; -} - -interface MenuWithPermissions { - id: number; - name: string; - parent_id: number | null; - depth: number; - has_children: boolean; - permissions: { - view: boolean; - create: boolean; - update: boolean; - delete: boolean; - approve: boolean; - export: boolean; - manage: boolean; - }; -} -``` - ---- - -## 5. 상세 작업 내용 - -### 5.1 Phase 1: DB 스키마 수정 ✅ - -#### 1.1 roles 테이블에 is_hidden 컬럼 추가 -- **상태**: ✅ 파일 생성완료 (실행 대기) -- **마이그레이션 파일**: `2025_12_30_160802_add_is_hidden_to_roles_table.php` -- **컬럼 정의**: `boolean is_hidden default false after description` -- **영향**: api, mng 모두 적용 - -### 5.2 Phase 2: Role CRUD API ✅ - -#### 생성된 파일 -| 파일 | 경로 | -|------|------| -| RoleController | `api/app/Http/Controllers/Api/V1/RoleController.php` | -| RoleService | `api/app/Services/RoleService.php` | -| RoleApi Swagger | `api/app/Swagger/v1/RoleApi.php` | - -#### 등록된 라우트 (5개) -``` -GET /api/v1/roles → index -POST /api/v1/roles → store -GET /api/v1/roles/{id} → show -PATCH /api/v1/roles/{id} → update -DELETE /api/v1/roles/{id} → destroy -``` - -### 5.3 Phase 3: 권한 매트릭스 API ✅ - -#### 생성된 파일 -| 파일 | 경로 | -|------|------| -| RolePermissionController | `api/app/Http/Controllers/Api/V1/RolePermissionController.php` | -| RolePermissionApi Swagger | `api/app/Swagger/v1/RolePermissionApi.php` | - -#### 등록된 라우트 (4개) -``` -GET /api/v1/roles/{id}/permissions → index -POST /api/v1/roles/{id}/permissions → grant -DELETE /api/v1/roles/{id}/permissions → revoke -PUT /api/v1/roles/{id}/permissions/sync → sync -``` - ---- - -## 6. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | is_hidden 컬럼 추가 | roles 테이블 마이그레이션 | api, mng | ⏳ 대기 | - ---- - -## 7. 파일 구조 (예상) - -### 7.1 api 프로젝트 - -``` -api/app/ -├── Http/ -│ ├── Controllers/ -│ │ └── RoleController.php ← 🆕 생성 -│ └── Requests/ -│ ├── StoreRoleRequest.php ← 🆕 생성 -│ └── UpdateRoleRequest.php ← 🆕 생성 -├── Models/ -│ └── Role.php ← 🔄 수정 (is_hidden 추가) -└── Services/ - ├── RoleService.php ← 🆕 생성 - └── RolePermissionService.php ← 🆕 생성 - -api/database/migrations/ -└── xxxx_add_is_hidden_to_roles_table.php ← 🆕 생성 - -api/routes/ -└── api.php ← 🔄 수정 (라우트 추가) -``` - -### 7.2 React 프로젝트 - -``` -react/src/components/settings/PermissionManagement/ -├── index.tsx ← 🔄 수정 (API 연동) -├── types.ts ← 🔄 수정 (타입 매핑) -├── actions.ts ← 🆕 생성 -├── PermissionDetail.tsx ← 🔄 수정 (API 연동) -├── PermissionDetailClient.tsx ← 🔄 수정 -└── PermissionDialog.tsx ← 🔄 수정 -``` - ---- - -## 8. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-12-30 | Phase 1~3 | API 개발 완료 (마이그레이션, Controller, Service, Swagger, 라우트) | 다수 | ✅ | -| 2025-12-30 | Phase 4 | React 연동 완료 (actions.ts, types.ts, index.tsx, PermissionDetailClient.tsx) | react 4개 파일 | ✅ | -| 2025-12-30 | 문서 | 계획 문서 초안 작성 | - | - | -| 2025-12-30 | 문서 | Phase 4 완료 반영 업데이트 | - | - | - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **mng 권한관리**: `mng/app/Services/RoleService.php`, `RolePermissionService.php` - ---- - -## 10. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 10.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("l2-permission-state") // 1. 상태 파악 -read_memory("l2-permission-snapshot") // 2. 사고 흐름 복구 -``` - -### 10.2 Serena 메모리 구조 -- `l2-permission-state`: { phase, progress, next_step, last_decision } -- `l2-permission-snapshot`: 현재까지의 논의 및 코드 변경점 요약 - ---- - -## 11. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 11.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| GET /api/v1/roles | 역할 목록 반환 | | ⏳ | -| POST /api/v1/roles | 역할 생성 | | ⏳ | -| PUT /api/v1/roles/{id} | 역할 수정 | | ⏳ | -| DELETE /api/v1/roles/{id} | 역할 삭제 | | ⏳ | -| GET /api/v1/roles/{id}/menus | 메뉴+권한 매트릭스 | | ⏳ | -| POST /api/v1/roles/{id}/permissions/toggle | 권한 토글 | | ⏳ | - -### 11.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| localStorage 제거 | ⏳ | | -| 역할 CRUD API 동작 | ⏳ | | -| 권한 매트릭스 API 동작 | ⏳ | | -| 숨김 기능 동작 | ⏳ | | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/material-input-per-item-mapping-plan.md b/plans/archive/material-input-per-item-mapping-plan.md deleted file mode 100644 index e40c15b..0000000 --- a/plans/archive/material-input-per-item-mapping-plan.md +++ /dev/null @@ -1,482 +0,0 @@ -# 개소별 자재 투입 매핑 계획 - -> **작성일**: 2026-02-12 -> **목적**: Worker Screen 자재 투입 시 개소(work_order_item)별 매핑 추적 기능 구현 -> **기준 문서**: `docs/specs/database-schema.md`, `docs/standards/api-rules.md` -> **상태**: 🔄 진행중 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 1~3 전체 구현 완료 | -| **다음 작업** | 테스트 및 검증 | -| **진행률** | 8/8 (100%) | -| **마지막 업데이트** | 2026-02-12 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 자재 투입은 **작업지시(WorkOrder) 단위**로만 처리됨: -- `POST /api/v1/work-orders/{id}/material-inputs` → `{inputs: [{stock_lot_id, qty}]}` -- `stock_transactions.reference_id` = `work_order_id` (개소 정보 없음) -- 어떤 개소(work_order_item)에 어떤 자재가 투입되었는지 추적 불가 - -**필요**: 개소별로 자재 투입을 추적하여: -- 개소별 투입 완료 여부 확인 -- 개소별 필요 자재 vs 실투입 비교 -- 검사서에 개소별 투입 자재 LOT 번호 기록 - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 신규 테이블(work_order_material_inputs)로 개소별 매핑 추적 │ -│ 2. 기존 stock_transactions 구조 변경 없음 (재고 이력은 그대로) │ -│ 3. 기존 작업지시 단위 API는 유지, 개소별 API를 추가 │ -│ 4. BOM 기반 필요 자재 계산은 기존 로직 재활용 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 타입 정의 추가, 프론트 UI 변경 | 불필요 | -| ⚠️ 컨펌 필요 | 새 마이그레이션, 새 API 엔드포인트, 서비스 로직 변경 | **필수** | -| 🔴 금지 | 기존 stock_transactions 구조 변경, 기존 API 삭제 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/standards/api-rules.md` - Service-First, FormRequest, ApiResponse::handle() -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - DB 스키마 규칙 -- MEMORY.md: 멀티테넌시 원칙 (FK/조인키만 컬럼, 나머지 options JSON) - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: Database & Model (백엔드 기반) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | `work_order_material_inputs` 마이그레이션 생성 | ✅ | api/ 프로젝트에서 | -| 1.2 | `WorkOrderMaterialInput` 모델 생성 | ✅ | BelongsToTenant 필수 | -| 1.3 | 관계 설정 (WorkOrderItem, WorkOrder) | ✅ | | - -### 2.2 Phase 2: Backend API (서비스 + 컨트롤러) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | `getMaterialsForItem()` 서비스 메서드 | ✅ | 개소별 BOM 자재 조회 | -| 2.2 | `registerMaterialInputForItem()` 서비스 메서드 | ✅ | 개소별 투입 + 매핑 저장 | -| 2.3 | `getMaterialInputsForItem()` 서비스 메서드 | ✅ | 개소별 투입 이력 조회 | -| 2.4 | 컨트롤러 엔드포인트 추가 | ✅ | 3개 엔드포인트 | -| 2.5 | FormRequest 생성 | ✅ | 투입 요청 검증 | -| 2.6 | 라우트 등록 | ✅ | production.php | - -### 2.3 Phase 3: Frontend (React) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | Server Actions 추가 | ✅ | 개소별 API 호출 함수 | -| 3.2 | MaterialInputModal props 확장 | ✅ | workOrderItemId 추가 | -| 3.3 | 자재투입 버튼 → 개소별 호출 연결 | ✅ | WorkerScreen에서 | -| 3.4 | 투입 이력/상태 표시 | ✅ | 개소 카드에 투입 완료 표시 | - ---- - -## 3. 상세 설계 - -### 3.1 신규 테이블: `work_order_material_inputs` - -```sql -CREATE TABLE work_order_material_inputs ( - id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - work_order_id BIGINT UNSIGNED NOT NULL COMMENT '작업지시 ID', - work_order_item_id BIGINT UNSIGNED NOT NULL COMMENT '개소(작업지시품목) ID', - stock_lot_id BIGINT UNSIGNED NOT NULL COMMENT '투입 로트 ID', - item_id BIGINT UNSIGNED NOT NULL COMMENT '자재 품목 ID', - qty DECIMAL(12,3) NOT NULL COMMENT '투입 수량', - input_by BIGINT UNSIGNED NULL COMMENT '투입자 ID', - input_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '투입 시각', - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - -- FK - FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON DELETE CASCADE, - FOREIGN KEY (work_order_item_id) REFERENCES work_order_items(id) ON DELETE CASCADE, - - -- Index - INDEX idx_womi_tenant (tenant_id), - INDEX idx_womi_wo_item (work_order_id, work_order_item_id), - INDEX idx_womi_lot (stock_lot_id) -) COMMENT='개소별 자재 투입 이력'; -``` - -**설계 근거**: -- `work_order_id`: 작업지시 단위 조회용 (기존 호환) -- `work_order_item_id`: 개소별 매핑 핵심 -- `stock_lot_id`: 어떤 LOT에서 투입했는지 -- `item_id`: 어떤 자재(품목)인지 -- `qty`: 투입 수량 -- `input_by`, `input_at`: 투입자/시간 추적 - -### 3.2 API 엔드포인트 - -#### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/materials` -- **용도**: 특정 개소의 BOM 기반 필요 자재 + 재고 LOT 조회 -- **응답**: 기존 `MaterialForInput[]`과 동일 구조 -- **로직**: 기존 `getMaterials()` 중 해당 item_id의 BOM만 추출 - -#### POST `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs` -- **용도**: 특정 개소에 자재 투입 등록 -- **요청**: -```json -{ - "inputs": [ - { "stock_lot_id": 456, "qty": 100 } - ] -} -``` -- **처리 순서**: - 1. `StockService::decreaseFromLot()` 호출 (기존 재고 차감 로직 재사용) - 2. `work_order_material_inputs` 레코드 생성 (개소 매핑) - 3. 감사 로그 기록 -- **응답**: -```json -{ - "work_order_id": 123, - "work_order_item_id": 789, - "material_count": 2, - "input_results": [...], - "input_at": "2026-02-12T14:30:00" -} -``` - -#### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs` -- **용도**: 특정 개소의 투입 이력 조회 -- **응답**: -```json -{ - "data": [ - { - "id": 1, - "stock_lot_id": 456, - "lot_no": "LOT-2026-001", - "item_id": 100, - "material_code": "MAT-001", - "material_name": "내화실", - "qty": 100, - "unit": "EA", - "input_by": 5, - "input_by_name": "홍길동", - "input_at": "2026-02-12T14:30:00" - } - ] -} -``` - -### 3.3 서비스 메서드 설계 - -#### WorkOrderService::getMaterialsForItem(int $workOrderId, int $itemId): array - -``` -1. WorkOrderItem 조회 (workOrderId + itemId 검증) -2. 해당 item의 BOM 추출 -3. BOM child_item별 required_qty = bom_qty × item.quantity -4. 각 자재의 StockLot 조회 (FIFO) -5. 이미 투입된 수량 차감 계산 (work_order_material_inputs에서 SUM) -6. 반환: MaterialForInput[] (remaining_required_qty 포함) -``` - -#### WorkOrderService::registerMaterialInputForItem(int $workOrderId, int $itemId, array $inputs): array - -``` -DB::transaction { - 1. WorkOrderItem 조회 + 검증 - 2. foreach (inputs as input): - a. StockService::decreaseFromLot() (기존 로직 재사용) - b. WorkOrderMaterialInput::create({ - tenant_id, work_order_id, work_order_item_id, - stock_lot_id, item_id (로트의 품목), - qty, input_by, input_at - }) - 3. 감사 로그 기록 - 4. 결과 반환 -} -``` - -### 3.4 프론트엔드 변경 - -#### MaterialInputModal Props 확장 -```typescript -interface MaterialInputModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - order: WorkOrder | null; - workOrderItemId?: number; // ← 추가: 개소 ID - workOrderItemName?: string; // ← 추가: 개소명 (모달 헤더용) - isCompletionFlow?: boolean; - onComplete?: () => void; - onSaveMaterials?: (...) => void; - savedMaterials?: MaterialInput[]; -} -``` - -#### Server Actions 추가 -```typescript -// 개소별 자재 조회 -getMaterialsForItem(workOrderId: string, itemId: number): Promise<{ - success: boolean; - data: MaterialForInput[]; -}> - -// 개소별 자재 투입 -registerMaterialInputForItem(workOrderId: string, itemId: number, inputs: ...): Promise<{ - success: boolean; -}> - -// 개소별 투입 이력 -getMaterialInputsForItem(workOrderId: string, itemId: number): Promise<{ - success: boolean; - data: MaterialInputHistory[]; -}> -``` - -#### MaterialInputModal 로직 변경 -``` -useEffect에서: - if (workOrderItemId) { - getMaterialsForItem(order.id, workOrderItemId) // 개소별 조회 - } else { - getMaterialsForWorkOrder(order.id) // 기존 전체 조회 (하위호환) - } - -handleSubmit에서: - if (workOrderItemId) { - registerMaterialInputForItem(order.id, workOrderItemId, inputs) - } else { - registerMaterialInput(order.id, inputs) - } -``` - -### 3.5 기존 API와의 관계 - -``` -기존 API (유지, 하위 호환): - GET /work-orders/{id}/materials → 전체 자재 조회 - POST /work-orders/{id}/material-inputs → 전체 단위 투입 - -신규 API (추가): - GET /work-orders/{id}/items/{itemId}/materials → 개소별 자재 조회 - POST /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입 - GET /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입 이력 -``` - ---- - -## 4. 작업 절차 - -### Step 1: 마이그레이션 + 모델 (Phase 1) -``` -1.1 api/ 프로젝트에서 마이그레이션 파일 생성 - - 파일: api/database/migrations/2026_02_12_XXXXXX_create_work_order_material_inputs_table.php - - 테이블: work_order_material_inputs (섹션 3.1 참조) - -1.2 WorkOrderMaterialInput 모델 생성 - - 파일: api/app/Models/Production/WorkOrderMaterialInput.php - - traits: BelongsToTenant, SoftDeletes (선택) - - $fillable: tenant_id, work_order_id, work_order_item_id, stock_lot_id, item_id, qty, input_by, input_at - - 관계: belongsTo(WorkOrder), belongsTo(WorkOrderItem), belongsTo(StockLot) - -1.3 기존 모델에 역관계 추가 - - WorkOrderItem: hasMany(WorkOrderMaterialInput) - - WorkOrder: hasMany(WorkOrderMaterialInput) - -검증: docker exec sam-api-1 php artisan migrate → 테이블 생성 확인 -``` - -### Step 2: Backend Service (Phase 2.1-2.3) -``` -2.1 WorkOrderService에 getMaterialsForItem() 추가 - - 기존 getMaterials() 로직 재활용 - - 해당 item의 BOM만 필터링 - - 이미 투입된 수량 차감 표시 - -2.2 WorkOrderService에 registerMaterialInputForItem() 추가 - - 기존 registerMaterialInput() 로직 기반 - - work_order_material_inputs 레코드 추가 생성 - - 트랜잭션 내에서 처리 - -2.3 WorkOrderService에 getMaterialInputsForItem() 추가 - - work_order_material_inputs 조회 - - lot_no, material_name 등 조인 - -검증: API 테스트 (curl 또는 Swagger) -``` - -### Step 3: Controller + Route (Phase 2.4-2.6) -``` -2.4 WorkOrderController에 3개 메서드 추가 - - materialsForItem(int $workOrderId, int $itemId) - - registerMaterialInputForItem(Request, int $workOrderId, int $itemId) - - materialInputsForItem(int $workOrderId, int $itemId) - -2.5 MaterialInputForItemRequest FormRequest 생성 (투입 검증) - - inputs: required|array|min:1 - - inputs.*.stock_lot_id: required|integer - - inputs.*.qty: required|numeric|gt:0 - -2.6 라우트 등록: api/routes/api/v1/production.php - - Route::get('work-orders/{id}/items/{itemId}/materials', ...) - - Route::post('work-orders/{id}/items/{itemId}/material-inputs', ...) - - Route::get('work-orders/{id}/items/{itemId}/material-inputs', ...) - -검증: php artisan route:list | grep material -``` - -### Step 4: Frontend (Phase 3) -``` -3.1 actions.ts에 3개 Server Action 추가 - - getMaterialsForItem() - - registerMaterialInputForItem() - - getMaterialInputsForItem() - -3.2 MaterialInputModal 수정 - - workOrderItemId prop 추가 - - useEffect에서 조건부 API 호출 - - handleSubmit에서 조건부 API 호출 - - 모달 헤더에 개소명 표시 - -3.3 WorkerScreen에서 개소별 자재투입 연결 - - 자재투입 버튼 클릭 시 workOrderItemId 전달 - -3.4 개소 카드에 투입 상태 표시 - - 투입 완료/미완료 뱃지 - -검증: dev.sam.kr에서 실제 플로우 테스트 -``` - ---- - -## 5. 핵심 파일 참조 - -### Backend (api/) -| 파일 | 역할 | -|------|------| -| `app/Services/WorkOrderService.php` | getMaterials() (line 1117), registerMaterialInput() (line 1264) | -| `app/Services/StockService.php` | decreaseFromLot() (line 618) - 재고 차감 | -| `app/Http/Controllers/Api/V1/WorkOrderController.php` | materials(), registerMaterialInput() | -| `routes/api/v1/production.php` (line 67-70) | 자재 관련 라우트 | -| `app/Models/Production/WorkOrderItem.php` | 작업지시 품목 모델 | - -### Frontend (react/) -| 파일 | 역할 | -|------|------| -| `src/components/production/WorkerScreen/MaterialInputModal.tsx` | 자재 투입 모달 UI | -| `src/components/production/WorkerScreen/actions.ts` | getMaterialsForWorkOrder(), registerMaterialInput() | -| `src/components/production/WorkerScreen/types.ts` | MaterialForInput, MaterialInput 타입 | - -### Database -| 테이블 | 역할 | -|--------|------| -| `work_order_items` | 작업지시 품목(개소). options JSON에 공정별 상세 | -| `stock_lots` | 재고 LOT. available_qty, fifo_order | -| `stock_transactions` | 재고 거래 이력. reference_type='work_order_input' | -| `work_order_material_inputs` | **신규** - 개소별 투입 매핑 | - ---- - -## 6. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 마이그레이션 | work_order_material_inputs 테이블 생성 | DB | ⚠️ 컨펌 필요 | -| 2 | API 엔드포인트 3개 추가 | 개소별 자재 조회/투입/이력 | api | ⚠️ 컨펌 필요 | -| 3 | 기존 API 유지 여부 | 작업지시 단위 API 유지 (하위호환) | api | ✅ 유지 | - ---- - -## 7. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-12 | - | 문서 초안 작성 | - | - | - ---- - -## 8. 참고 문서 - -- **API 규칙**: `docs/standards/api-rules.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **기존 분석**: Explore Agent 분석 결과 (세션 내) -- **품목 정책**: `docs/rules/item-policy.md` (BOM, lot_managed 등) -- **MEMORY.md**: 멀티테넌시 원칙, 품목 options 체계 - ---- - -## 9. 검증 결과 - -### 9.1 테스트 케이스 - -| # | 시나리오 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|----------|----------|------| -| 1 | 개소별 자재 조회 (BOM 있는 품목) | 해당 개소 BOM의 자재 + LOT 목록 반환 | | ✅ | -| 2 | 개소별 자재 조회 (BOM 없는 품목) | 품목 자체를 자재로 반환 | | ✅ | -| 3 | 개소별 자재 투입 | stock_lot 차감 + material_inputs 레코드 생성 | | ✅ | -| 4 | 이미 투입된 자재 재조회 | remaining_required_qty 감소 확인 | | ✅ | -| 5 | 가용수량 초과 투입 시도 | 에러 반환 (재고 부족) | | ✅ | -| 6 | 투입 이력 조회 | lot_no, 자재명, 수량, 투입자 확인 | | ✅ | -| 7 | 프론트 자재투입 모달에서 개소별 투입 | 해당 개소 자재만 표시, 투입 성공 | | ✅ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 개소별 자재 조회 API 동작 | ✅ | BOM 기반 필터링 | -| 개소별 자재 투입 API 동작 | ✅ | 재고 차감 + 매핑 저장 | -| 프론트에서 개소별 투입 플로우 | ✅ | MaterialInputModal 연동 | -| 기존 작업지시 단위 API 호환 유지 | ✅ | 기존 기능 미파손 | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 개소별 자재 투입 매핑 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3, 8개 작업 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 API, StockService 재사용 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 5 핵심 파일 참조 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 Step 1-4 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | SQL, API 스키마 구체적 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 4. 작업 절차 Step 1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5. 핵심 파일 참조 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/mes-integration-analysis-plan.md b/plans/archive/mes-integration-analysis-plan.md deleted file mode 100644 index 3b9bc28..0000000 --- a/plans/archive/mes-integration-analysis-plan.md +++ /dev/null @@ -1,525 +0,0 @@ -# MES 모듈 통합 흐름 분석 계획 - -> **작성일**: 2025-01-09 -> **목적**: 견적 → 수주 → 작업지시 + 공정관리 모듈 간 연동 상태 점검 및 문제점 분석 -> **기준 문서**: `docs/plans/process-management-plan.md`, `docs/plans/order-management-plan.md`, `docs/plans/work-order-plan.md` -> **상태**: ✅ 분석 완료 + 개선 방향 **재결정됨** (2025-01-09 추가 분석) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 공정 관리 페이지 확인 + 개념 명확화 | -| **다음 작업** | WorkOrder `process_type` → `process_id` FK 변경 구현 | -| **진행률** | 7/7 (100%) | -| **마지막 업데이트** | 2025-01-09 | - -### ✅ 결정된 개선 방향 (재결정) - -| 결정 사항 | 내용 | -|----------|------| -| **WorkOrder.process_type** | `process_type` (varchar) → `process_id` (FK) **변경** | -| **Process.process_type** | 공정 구분 → `common_codes`에서 관리 | -| **개념 정리** | 공정명(WorkOrder) ≠ 공정구분(Process) 명확히 구분 | - ---- - -## 1. 개요 - -### 1.1 배경 -MES 시스템의 핵심 모듈인 공정관리, 수주관리, 작업지시가 개별적으로 개발 완료되었으나, -모듈 간 통합 흐름이 제대로 설계되었는지 검증이 필요합니다. - -### 1.2 분석 목표 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 분석 목표 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 모듈 간 데이터 흐름 검증 │ -│ 2. API 연동 상태 점검 │ -│ 3. 프론트엔드 연동 상태 점검 │ -│ 4. 설계 문제점 및 개선 방안 도출 │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. 분석 대상 - -### 2.1 모듈 구성 - -| 모듈 | 역할 | API 상태 | Frontend 상태 | -|------|------|:--------:|:------------:| -| **견적관리 (Quote)** | 견적서 작성 및 수주 변환 | ✅ 완료 | ✅ 완료 | -| **수주관리 (Order)** | 견적→수주 변환, 생산지시 생성 | ✅ 완료 | ✅ 완료 | -| **작업지시 (WorkOrder)** | 실제 생산 작업 관리 | ✅ 완료 | ✅ 완료 | -| **공정관리 (Process)** | 공정 템플릿 및 품목 분류 규칙 관리 | ✅ 완료 | ✅ 완료 | - -### 2.2 기대 데이터 흐름 - -``` -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ 견적관리 │ │ 수주관리 │ │ 작업지시 │ │ 공정관리 │ -│ (Quote) │ ──→ │ (Order) │ ──→ │ (WorkOrder) │ ? │ (Process) │ -└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ - │ │ │ │ - ▼ ▼ ▼ ▼ - - 견적서 작성 - 수주 확정 - 작업 상태 관리 - 공정 템플릿 - - 품목/단가 구성 - 생산지시 생성 - 담당자 배정 - 품목 분류 규칙 - - 고객 승인 - 납기 관리 - 공정별 진행 - 작업 단계 정의 -``` - ---- - -## 3. 분석 결과 - -### 3.0 ✅ 견적관리 → 수주관리 연동 (정상 작동) - -**API 연동 구현**: -``` -POST /api/v1/orders/from-quote/{quoteId} -→ Order 생성 + Quote 상태 변경 (finalized → converted) -``` - -**연결 관계**: -| 항목 | 내용 | -|------|------| -| FK 연결 | `orders.quote_id` → `quotes.id` | -| 상태 연동 | Quote `finalized` 시에만 수주 변환 가능 | -| 중복 방지 | 동일 Quote에 대해 중복 변환 불가 | - -**Quote 상태 흐름**: -``` -draft → sent → approved → finalized → converted -(임시저장) (발송) (승인) (확정) (수주변환) -``` - -**API 핵심 로직** (`api/app/Services/OrderService.php`): -```php -public function createFromQuote(int $quoteId): Order -{ - $quote = Quote::findOrFail($quoteId); - - // 변환 가능 상태 검증 (finalized만 가능) - if ($quote->status !== Quote::STATUS_FINALIZED) { - throw new BadRequestHttpException(__('error.quote.must_be_finalized')); - } - - // 중복 변환 방지 - $existingOrder = Order::where('quote_id', $quoteId)->first(); - if ($existingOrder) { - throw new BadRequestHttpException(__('error.order.already_exists_from_quote')); - } - - // Order 생성 + Quote 품목 자동 복사 - $order = Order::create([ - 'quote_id' => $quote->id, - 'client_id' => $quote->client_id, - 'status_code' => Order::STATUS_DRAFT, - // ... 견적 정보 복사 - ]); - - // Quote 상태 변경 - $quote->status = Quote::STATUS_CONVERTED; - $quote->save(); - - return $order; -} -``` - -**프론트엔드 구현**: -```typescript -// react/src/components/orders/actions.ts -export async function createOrderFromQuote( - quoteId: string | number -): Promise - -// react/src/components/quotes/QuotationSelectDialog.tsx -// 견적 선택 → 수주 변환 UI 컴포넌트 -``` - -**데이터 변환**: -| Quote 필드 | Order 필드 | 변환 방식 | -|-----------|-----------|----------| -| `id` | `quote_id` (FK) | 참조 | -| `client_id` | `client_id` | 복사 | -| `project_name` | `project_name` | 복사 | -| `quote_items` | `order_items` | 품목 복사 | -| `product_category` | - | 참조용 | - -**평가**: ✅ **정상 구현됨** - FK 관계, 상태 연동, 중복 방지 모두 정상 - ---- - -### 3.1 ✅ 수주관리 → 작업지시 연동 (정상 작동) - -**API 연동 구현**: -``` -POST /api/v1/orders/{id}/production-order -→ WorkOrder 생성 + Order 상태 변경 (CONFIRMED → IN_PROGRESS) -``` - -**연결 관계**: -| 항목 | 내용 | -|------|------| -| FK 연결 | `work_orders.sales_order_id` → `orders.id` | -| 상태 연동 | Order CONFIRMED 시에만 생산지시 가능 | -| 중복 방지 | 동일 Order에 대해 중복 생성 불가 | - -**프론트엔드 구현**: -```typescript -// react/src/components/orders/actions.ts -export async function createProductionOrder( - orderId: string, - data?: CreateProductionOrderData -): Promise - -// CreateProductionOrderData 타입 -interface CreateProductionOrderData { - processType?: 'screen' | 'slat' | 'bending'; - priority?: 'urgent' | 'high' | 'normal' | 'low'; - assigneeId?: number; - teamId?: number; - scheduledDate?: string; - memo?: string; -} -``` - -**평가**: ✅ **정상 구현됨** - ---- - -### 3.2 🔴 공정관리 → 작업지시 연동 (설계 문제 발견 → 해결 방향 결정) - -#### 3.2.0 ✅ 개념 명확화 (2025-01-09 추가 분석) - -**공정 관리 페이지 확인** (`/master-data/process-management`): - -| 공정코드 | 공정명 | 구분 | 담당부서 | 상태 | -|---------|-------|------|---------|------| -| P-001 | 슬랫 | 생산 | 경영본부 | 사용중 | -| P-002 | 스크린 | 생산 | 개발팀 | 사용중 | - -**핵심 발견**: -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 💡 개념 정리 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ WorkOrder.process_type = "공정명" (스크린, 슬랫, 절곡) │ -│ → 공정 관리 테이블(processes)에서 등록된 공정 │ -│ → 하드코딩 ❌ → 공정 테이블 FK로 연결해야 함 ✅ │ -│ │ -│ Process.process_type = "공정 구분" (생산, 검사, 포장, 조립) │ -│ → 공정의 분류/카테고리 │ -│ → common_codes에서 관리해야 함 ✅ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**최종 정리**: - -| 구분 | 필드명 | 실제 의미 | 현재 상태 | 올바른 상태 | -|------|--------|----------|----------|------------| -| **WorkOrder** | `process_type` | 공정명 | 하드코딩 (screen/slat/bending) | **공정 테이블 FK** | -| **Process** | `process_type` | 공정 구분 | 하드코딩 (생산/검사/포장/조립) | common_codes | - ---- - -#### 3.2.1 process_type 불일치 문제 (기존 분석) - -| 구분 | 공정관리 (Process) | 작업지시 (WorkOrder) | -|------|:------------------:|:-------------------:| -| **필드명** | `process_type` | `process_type` | -| **값 (Frontend)** | '생산', '검사', '포장', '조립' | 'screen', 'slat', 'bending' | -| **값 개수** | 4개 (한글) | 3개 (영문) | -| **실제 의미** | 공정 **구분** (카테고리) | 공정 **명** (공정 테이블 데이터) | - -**문제점**: -- 동일한 필드명(`process_type`)을 사용하지만 **완전히 다른 의미** -- WorkOrder는 **공정 테이블을 참조해야 하는데** 하드코딩되어 있음 -- **FK 관계가 없음** - Process 테이블과 WorkOrder 테이블 연결 없음 - -#### 3.2.2 코드 증거 - -**공정관리 타입** (`react/src/types/process.ts`): -```typescript -export type ProcessType = '생산' | '검사' | '포장' | '조립'; -``` - -**작업지시 타입** (`react/src/components/production/WorkOrders/types.ts`): -```typescript -export type ProcessType = 'screen' | 'slat' | 'bending'; - -export const PROCESS_TYPE_LABELS: Record = { - screen: '스크린', - slat: '슬랫', - bending: '절곡', -}; -``` - -**API 모델** (`api/app/Models/Production/WorkOrder.php`): -```php -const PROCESS_SCREEN = 'screen'; -const PROCESS_SLAT = 'slat'; -const PROCESS_BENDING = 'bending'; -``` - -#### 3.2.3 영향도 분석 - -| 기능 | 현재 상태 | 문제점 | -|------|----------|--------| -| 공정 선택 | WorkOrder 생성 시 하드코딩된 3개 옵션만 사용 | Process 테이블 활용 안됨 | -| 분류 규칙 | Process에만 존재 | WorkOrder에서 품목 자동 분류 불가 | -| 작업 단계 | Process와 WorkOrder 각각 별도 정의 | 데이터 중복 | -| 메타데이터 | Process에 풍부한 정보 (인원, 설비, 템플릿) | WorkOrder에서 미활용 | - ---- - -### 3.3 🟡 공정관리 → 수주관리 연동 (연결 없음) - -**현재 상태**: -- Process와 Order 간 직접적인 연결 관계 없음 -- 이는 **의도된 설계**로 보임 (공정은 생산 단계에서 적용) - ---- - -## 4. 문제점 요약 - -### 4.1 핵심 문제: process_type 이중 정의 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🔴 핵심 문제 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 공정관리(Process)와 작업지시(WorkOrder)가 │ -│ 동일한 필드명(process_type)을 사용하지만 │ -│ 완전히 다른 값 체계와 목적을 가지고 있음 │ -│ │ -│ ┌─────────────┐ ┌─────────────┐ │ -│ │ Process │ ❌ │ WorkOrder │ │ -│ │ (생산/검사) │ ─────── │ (screen/slat) │ │ -│ └─────────────┘ 연결없음 └─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 4.2 문제 유형 분류 - -| # | 문제 | 심각도 | 영향 | -|---|------|:------:|------| -| 1 | process_type 값 체계 불일치 | 🔴 높음 | 데이터 일관성, 확장성 | -| 2 | Process ↔ WorkOrder FK 부재 | 🔴 높음 | 메타데이터 활용 불가 | -| 3 | 공정 정보 중복 정의 | 🟡 중간 | 유지보수 복잡성 | -| 4 | 새 공정 추가 시 코드 수정 필요 | 🟡 중간 | 확장성 제한 | - ---- - -## 5. 해결 방안 (검토 필요) - -### 5.1 Option A: 현행 유지 (의도된 분리) - -**전제**: 공정관리와 작업지시가 **서로 다른 도메인**임을 인정 - -``` -공정관리 (Process) 작업지시 (WorkOrder) -───────────────── ───────────────── -목적: 품목 분류 자동화 목적: 실제 생산 작업 관리 -대상: 모든 품목 유형 대상: 특화 제조품 (스크린/슬랫/절곡) -사용자: 품질/물류팀 사용자: 생산팀 -``` - -**장점**: -- 현재 코드 변경 불필요 -- 각 도메인의 독립성 유지 - -**단점**: -- `process_type` 필드명 혼란 지속 -- 공정 메타데이터 재활용 불가 - -**권장 조치**: -- WorkOrder의 `process_type`을 `manufacturing_type` 또는 `product_line`으로 **리네이밍** -- 문서에 두 개념의 차이 명확히 기술 - ---- - -### 5.2 Option B: 통합 연결 (FK 추가) - -**전제**: 공정관리가 작업지시의 **상위 템플릿** 역할을 해야 함 - -``` -Process (공정 템플릿) - │ - │ process_id (FK) - ▼ -WorkOrder (작업지시) -``` - -**필요 변경**: -1. `work_orders` 테이블에 `process_id` FK 추가 -2. Process 모델에 제조 공정 유형 추가 (screen, slat, bending) -3. WorkOrder 생성 시 Process 선택 UI 추가 -4. 공정별 메타데이터 (작업단계, 인원, 설비) 자동 적용 - -**장점**: -- 데이터 일관성 확보 -- 공정 메타데이터 재활용 -- 새 공정 추가 시 코드 수정 불필요 - -**단점**: -- DB 마이그레이션 필요 -- 기존 데이터 마이그레이션 필요 -- API 및 프론트엔드 수정 필요 - ---- - -### 5.3 Option C: 하이브리드 (권장) - -**전제**: 점진적 통합으로 위험 최소화 - -**Phase 1**: 명명 정리 (즉시) -- WorkOrder의 `process_type` → `manufacturing_type` 리네이밍 -- 문서 정리 및 팀 공유 - -**Phase 2**: 연결 준비 (중기) -- Process 모델에 `is_manufacturing` 플래그 추가 -- 제조 전용 공정 구분 (screen, slat, bending) - -**Phase 3**: 통합 (장기) -- WorkOrder에 `process_id` FK 추가 (optional) -- 메타데이터 연동 구현 - ---- - -## 6. 컨펌 결과 (✅ 결정 완료 → 재결정) - -| # | 항목 | ~~이전 결정~~ | **최종 결정** | 결정일 | -|---|------|-------------|--------------|--------| -| 1 | **설계 방향** | ~~Option C (하이브리드)~~ | **Option B** (FK 추가) | 2025-01-09 | -| 2 | **필드 변경** | ~~리네이밍만~~ | **FK로 변경** | 2025-01-09 | -| 3 | **FK 추가 여부** | ~~❌ 불필요~~ | **✅ 필요** - 공정 테이블 FK | 2025-01-09 | -| 4 | **도메인 연결** | ~~독립 도메인~~ | **Process → WorkOrder 연결** | 2025-01-09 | - -### 6.0 재결정 사유 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 💡 핵심 발견 (공정 관리 페이지 확인) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ WorkOrder.process_type 값 (screen, slat, bending)이 │ -│ 실제로는 공정 관리 페이지에서 등록된 "공정명"임을 확인 │ -│ │ -│ /master-data/process-management 등록 현황: │ -│ - P-001: 슬랫 (slat) │ -│ - P-002: 스크린 (screen) │ -│ │ -│ ∴ 하드코딩된 값이 아닌 공정 테이블 FK로 연결해야 함 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 6.1 다음 작업 (FK 추가 구현) - -``` -WorkOrder `process_type` (varchar) → `process_id` (FK) 변경 작업 범위: - -1. DB 마이그레이션 - - work_orders.process_type (varchar) 제거 - - work_orders.process_id (FK) 추가 → processes.id 참조 - - 기존 데이터 마이그레이션 (screen→P-002, slat→P-001, bending→신규등록) - -2. API 수정 - - api/app/Models/Production/WorkOrder.php - - PROCESS_* 상수 제거 - - process_type 필드 → process_id FK 필드 - - process() BelongsTo 관계 추가 - - api/app/Services/OrderService.php (생산지시 생성 로직) - - api/app/Services/WorkOrderService.php (비즈니스 로직) - - 관련 FormRequest, Resource 클래스 - -3. Frontend 수정 - - react/src/components/production/WorkOrders/types.ts - - ProcessType enum 제거 - - process_id: number 필드 추가 - - process 관계 데이터 타입 추가 - - 관련 컴포넌트 (actions.ts, components) - - 공정 선택 드롭다운 → API에서 공정 목록 조회 -``` - ---- - -## 7. 참고 문서 - -- **공정관리 계획**: `docs/plans/process-management-plan.md` -- **수주관리 계획**: `docs/plans/order-management-plan.md` -- **작업지시 계획**: `docs/plans/work-order-plan.md` -- **시스템 아키텍처**: `docs/architecture/system-overview.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - ---- - -## 8. 분석 파일 참조 - -### 8.1 API 레이어 -| 파일 | 역할 | -|------|------| -| `api/app/Http/Controllers/Api/V1/QuoteController.php` | 견적 CRUD | -| `api/app/Http/Controllers/Api/V1/OrderController.php` | 수주 CRUD + 생산지시 생성 | -| `api/app/Http/Controllers/V1/ProcessController.php` | 공정 CRUD | -| `api/app/Http/Controllers/Api/V1/WorkOrderController.php` | 작업지시 CRUD | -| `api/app/Services/QuoteService.php` | 견적 비즈니스 로직 | -| `api/app/Services/OrderService.php` | 견적→수주 변환, 수주→작업지시 연동 | -| `api/app/Services/WorkOrderService.php` | 작업지시 비즈니스 로직 | - -### 8.2 모델 레이어 -| 파일 | 핵심 필드 | -|------|----------| -| `api/app/Models/Quote/Quote.php` | `status` (draft/sent/approved/finalized/converted), `product_category` | -| `api/app/Models/Order.php` | `status_code`, `quote_id` (FK) | -| `api/app/Models/Process.php` | `process_type` (생산/검사/포장/조립) | -| `api/app/Models/Production/WorkOrder.php` | `process_type` (screen/slat/bending), `sales_order_id` (FK) | - -### 8.3 프론트엔드 레이어 -| 파일 | 역할 | -|------|------| -| `react/src/components/quotes/types.ts` | Quote 타입 정의 | -| `react/src/components/quotes/QuotationSelectDialog.tsx` | 견적 선택 UI | -| `react/src/types/process.ts` | Process 타입 정의 | -| `react/src/components/production/WorkOrders/types.ts` | WorkOrder 타입 정의 | -| `react/src/components/orders/actions.ts` | Order API 호출 + 생산지시 생성 + 견적변환 | -| `react/src/components/process-management/actions.ts` | Process API 호출 | -| `react/src/components/production/WorkOrders/actions.ts` | WorkOrder API 호출 | - ---- - -## 9. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-09 | 문서 생성 | MES 통합 흐름 분석 완료 | - | - | -| 2025-01-09 | 견적 분석 추가 | Quote → Order 연동 분석 (섹션 3.0) | - | - | -| 2025-01-09 | 결정 반영 | Option C 선택, 리네이밍 진행, FK 미추가 결정 | - | ✅ | -| 2025-01-09 | **재결정** | 공정 관리 페이지 확인 후 **Option B (FK 추가)로 변경** | - | ✅ | - -### 9.1 재결정 상세 - -**재결정 배경**: -- 공정 관리 페이지(`/master-data/process-management`) 실제 확인 -- `screen`, `slat`, `bending` 값이 공정명(Process Name)임을 확인 -- P-001: 슬랫, P-002: 스크린 등록 확인 - -**이전 결정 → 최종 결정**: -| 항목 | 이전 | 최종 | -|------|------|------| -| 설계 방향 | Option C (하이브리드) | **Option B (FK 추가)** | -| 필드 처리 | 리네이밍만 | **FK로 변경** | -| FK 추가 | 불필요 | **필요** | -| 도메인 관계 | 독립 | **연결** | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/mng-item-formula-integration-plan.md b/plans/archive/mng-item-formula-integration-plan.md deleted file mode 100644 index 54261a4..0000000 --- a/plans/archive/mng-item-formula-integration-plan.md +++ /dev/null @@ -1,837 +0,0 @@ -# MNG 품목관리 - 견적수식 엔진(FormulaEvaluatorService) 연동 계획 - -> **작성일**: 2026-02-19 -> **목적**: 가변사이즈 완제품(FG) 선택 시 오픈사이즈(W,H) 입력 → FormulaEvaluatorService로 동적 자재 산출 → 중앙 패널에 트리 표시 -> **기준 문서**: docs/plans/mng-item-management-plan.md, api/app/Services/Quote/FormulaEvaluatorService.php -> **선행 작업**: 3-Panel 품목관리 페이지 구현 완료 (Phase 1~2 of mng-item-management-plan.md) -> **상태**: 🔄 진행중 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 2.5: 로딩/에러 상태 처리 (전체 구현 완료) | -| **다음 작업** | 검증 (브라우저 테스트) | -| **진행률** | 8/8 (100%) | -| **마지막 업데이트** | 2026-02-19 | - ---- - -## 1. 개요 - -### 1.1 배경 - -MNG 품목관리 페이지(`mng.sam.kr/item-management`)에서 완제품(FG) `FG-KQTS01-벽면형-SUS`를 선택하면 `items.bom` JSON에 등록된 정적 BOM(PT 2개: 가이드레일, 하단마감재)만 표시된다. -그러나 견적관리(`dev.sam.kr/sales/quote-management/46`)에서는 `FormulaEvaluatorService`가 W=3000, H=3000 입력으로 17종 51개의 자재를 동적 산출한다. - -**핵심 문제**: items.bom(정적)과 FormulaEvaluatorService(동적) 두 시스템이 분리되어 있어, 품목관리 페이지에서 실제 필요 자재를 볼 수 없다. - -**해결**: 가변사이즈 품목(`item_details.is_variable_size = true`) 선택 시 오픈사이즈(W, H) 입력 UI를 제공하고, API의 기존 엔드포인트(`POST /api/v1/quotes/calculate/bom`)를 MNG에서 HTTP로 호출하여 산출 결과를 중앙 패널에 표시한다. - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - MNG에서 마이그레이션 파일 생성 금지 (API에서만) │ -│ - MNG ↔ API 통신은 HTTP API 호출 (코드 공유/직접 서비스 호출 X) │ -│ - 기존 API 엔드포인트 재사용 (POST /api/v1/quotes/calculate/bom)│ -│ - Docker nginx 내부 라우팅 + SSL 우회 패턴 사용 │ -│ - Blade + HTMX + Tailwind + Vanilla JS (Alpine.js 미사용) │ -│ - 정적 BOM과 수식 산출 결과를 탭으로 전환 가능하게 │ -│ - Controller에서 직접 DB 쿼리 금지 (Service-First) │ -│ - Controller에서 직접 validate() 금지 (FormRequest 필수) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 서비스 생성, 컨트롤러 메서드 추가, Blade 수정, JS 추가 | 불필요 | -| ⚠️ 컨펌 필요 | API 라우트 추가, 기존 Blade 구조 변경 | **필수** | -| 🔴 금지 | mng에서 마이그레이션 생성, API 소스 수정 | 별도 협의 | - -### 1.4 MNG 절대 금지 규칙 - -``` -❌ mng/database/migrations/ 에 파일 생성 금지 -❌ docker exec sam-mng-1 php artisan migrate 실행 금지 -❌ php artisan db:seed --class=*MenuSeeder 실행 금지 -❌ Controller에서 직접 DB 쿼리 금지 (Service-First) -❌ Controller에서 직접 validate() 금지 (FormRequest 필수) -❌ api/ 프로젝트 소스 코드 수정 금지 -``` - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: MNG 백엔드 (HTTP API 호출 서비스) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | FormulaApiService 생성 (MNG→API HTTP 호출 래퍼) | ✅ | 신규 파일 | -| 1.2 | ItemManagementApiController에 calculateFormula 메서드 추가 | ✅ | 기존 파일 수정 | -| 1.3 | API 라우트 추가 (POST /api/admin/items/{id}/calculate-formula) | ✅ | 기존 파일 수정 | - -### 2.2 Phase 2: MNG 프론트엔드 (UI 연동) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 중앙 패널 헤더에 탭 UI 추가 (정적 BOM / 수식 산출) | ✅ | index.blade.php 수정 | -| 2.2 | 오픈사이즈 입력 폼 (W, H, 수량) + 산출 버튼 | ✅ | index.blade.php 수정 | -| 2.3 | 수식 산출 결과 트리 렌더링 (카테고리 그룹별) | ✅ | JS 추가 | -| 2.4 | 가변사이즈 품목 감지 → 자동 탭 전환 | ✅ | item-detail.blade.php + JS 수정 | -| 2.5 | 로딩/에러 상태 처리 | ✅ | JS 추가 | - ---- - -## 3. 이미 구현된 코드 (선행 작업 - 수정 대상) - -> 새 세션에서 현재 코드 상태를 파악할 수 있도록 이미 존재하는 파일 전체 목록과 핵심 구조를 기록. - -### 3.1 파일 구조 (이미 존재) - -``` -mng/ -├── app/ -│ ├── Http/Controllers/ -│ │ ├── ItemManagementController.php # Web (HX-Redirect 패턴) -│ │ └── Api/Admin/ -│ │ └── ItemManagementApiController.php # API (index, bomTree, detail) -│ ├── Models/ -│ │ ├── Items/ -│ │ │ ├── Item.php # BelongsToTenant, 관계, 스코프, 상수 -│ │ │ └── ItemDetail.php # 1:1 확장 (is_variable_size 필드 포함) -│ │ └── Commons/ -│ │ └── File.php # 파일 모델 -│ ├── Services/ -│ │ └── ItemManagementService.php # getItemList, getBomTree, getItemDetail -│ └── Traits/ -│ └── BelongsToTenant.php # 테넌트 격리 Trait -├── resources/views/item-management/ -│ ├── index.blade.php # 3-Panel 메인 (★ 수정 대상) -│ └── partials/ -│ ├── item-list.blade.php # 좌측 패널 (변경 없음) -│ ├── bom-tree.blade.php # 중앙 패널 초기 상태 (변경 없음) -│ └── item-detail.blade.php # 우측 패널 (★ 수정 대상) -├── routes/ -│ ├── web.php # Route: GET /item-management (변경 없음) -│ └── api.php # Route: items group (★ 수정 대상 - 라우트 추가) -└── config/ - └── api-explorer.php # FLOW_TESTER_API_KEY 설정 참조 -``` - -### 3.2 현재 ItemManagementApiController 전체 (수정 대상) - -```php -service->getItemList([ - 'search' => $request->input('search'), - 'item_type' => $request->input('item_type'), - 'per_page' => $request->input('per_page', 50), - ]); - return view('item-management.partials.item-list', compact('items')); - } - - public function bomTree(int $id, Request $request): JsonResponse - { - $maxDepth = $request->input('max_depth', 10); - $tree = $this->service->getBomTree($id, $maxDepth); - return response()->json($tree); - } - - public function detail(int $id): View - { - $data = $this->service->getItemDetail($id); - return view('item-management.partials.item-detail', [ - 'item' => $data['item'], - 'bomChildren' => $data['bom_children'], - ]); - } -} -``` - -### 3.3 현재 API 라우트 (items 그룹, mng/routes/api.php:866~) - -```php -Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/items')->name('api.admin.items.')->group(function () { - Route::get('/search', [ItemApiController::class, 'search'])->name('search'); - - // 품목관리 페이지 API - Route::get('/', [ItemManagementApiController::class, 'index'])->name('index'); - Route::get('/{id}/bom-tree', [ItemManagementApiController::class, 'bomTree'])->name('bom-tree'); - Route::get('/{id}/detail', [ItemManagementApiController::class, 'detail'])->name('detail'); - // ★ 여기에 calculate-formula 라우트 추가 예정 -}); -``` - -### 3.4 현재 index.blade.php 중앙 패널 (수정 대상 부분) - -```html - -
-
-

BOM 구성 (재귀 트리)

-
-
-

좌측에서 품목을 선택하세요.

-
-
-``` - -### 3.5 현재 JS 구조 (index.blade.php @push('scripts')) - -핵심 함수: -- `loadItemList()` - 좌측 품목 리스트 HTMX 로드 -- `selectItem(itemId, updateTree)` - 품목 선택 (좌측 하이라이트 + 중앙 트리 fetch + 우측 상세 HTMX) -- `selectTreeNode(itemId)` - 중앙 트리 노드 클릭 (우측만 갱신, 트리 유지) -- `renderBomTree(node, container)` - BOM 트리 재귀 렌더링 -- `getTypeBadgeClass(type)` - 유형별 뱃지 CSS 클래스 - -### 3.6 테넌트 필터링 패턴 (중요) - -MNG의 HQ 관리자는 헤더에서 테넌트를 선택하며, `session('selected_tenant_id')`에 저장된다. -그러나 `BelongsToTenant`의 `TenantScope`는 `request->attributes`, `X-TENANT-ID 헤더`, `auth user`에서 tenant_id를 읽으므로 **세션 값과 불일치**할 수 있다. - -**따라서 Service에서는 `Item::withoutGlobalScopes()->where('tenant_id', session('selected_tenant_id'))` 패턴을 사용한다.** - -```php -// ✅ 올바른 패턴 (현재 ItemManagementService에서 사용 중) -Item::withoutGlobalScopes() - ->where('tenant_id', session('selected_tenant_id')) - ->findOrFail($id); - -// ❌ 잘못된 패턴 (HQ 관리자 세션과 불일치) -Item::findOrFail($id); // TenantScope가 auth user의 tenant_id 사용 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: MNG 백엔드 - -#### 1.1 FormulaApiService 생성 - -**파일 경로**: `mng/app/Services/FormulaApiService.php` (신규 생성) - -**역할**: MNG에서 API 프로젝트의 `POST /api/v1/quotes/calculate/bom` 엔드포인트를 HTTP로 호출하는 래퍼 - -**호출 대상 API 엔드포인트 상세**: - -``` -POST /api/v1/quotes/calculate/bom -라우트 정의: api/routes/api/v1/sales.php:64 -미들웨어: 글로벌(ApiKeyMiddleware, CorsMiddleware, ApiRateLimiter) -FormRequest: QuoteBomCalculateRequest (authorize = true, 제한 없음) -``` - -**API 인증 요구사항** (확인 완료): - -| 헤더 | 필수 | 설명 | -|------|:----:|------| -| `X-API-KEY` | ✅ 필수 | `api_keys` 테이블에 `is_active=true`로 등록된 키 | -| `Authorization: Bearer {token}` | ❌ 선택 | Sanctum 토큰, 있으면 tenant_id 자동 설정 | -| `X-TENANT-ID` | ❌ 선택 | 테넌트 식별 (Bearer 없을 때 대안) | - -**API Key 취득 방법**: `env('FLOW_TESTER_API_KEY')` (mng/.env에 설정됨, `config/api-explorer.php:26`에서 참조) - -**요청 페이로드**: -```json -{ - "finished_goods_code": "FG-KQTS01", - "variables": { - "W0": 3000, - "H0": 3000, - "QTY": 1 - }, - "tenant_id": 287 -} -``` - -**응답 구조** (FormulaEvaluatorService::calculateBomWithDebug 반환값): -```json -{ - "success": true, - "finished_goods": { "code": "FG-KQTS01", "name": "벽면형-SUS", "id": 123 }, - "variables": { "W0": 3000, "H0": 3000, "QTY": 1 }, - "items": [ - { - "item_code": "PT-강재-C형강", - "item_name": "C형강 65×32×10t", - "specification": "65×32×10t", - "unit": "mm", - "quantity": 6038, - "unit_price": 1.0, - "total_price": 6038, - "category_group": "steel" - } - ], - "grouped_items": { - "steel": [ ... ], - "part": [ ... ], - "motor": [ ... ] - }, - "subtotals": { "steel": 123456, "part": 78900, "motor": 50000 }, - "grand_total": 252356, - "debug_steps": [ ... ] -} -``` - -**구현 코드**: -```php -withoutVerifying() - ->withHeaders([ - 'Host' => 'api.sam.kr', - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - 'X-API-KEY' => $apiKey, - 'X-TENANT-ID' => (string) $tenantId, - ]) - ->post('https://nginx/api/v1/quotes/calculate/bom', [ - 'finished_goods_code' => $finishedGoodsCode, - 'variables' => $variables, - 'tenant_id' => $tenantId, - ]); - - if ($response->successful()) { - $json = $response->json(); - // ApiResponse::handle()는 {success, message, data} 구조로 래핑 - return $json['data'] ?? $json; - } - - Log::warning('FormulaApiService: API 호출 실패', [ - 'status' => $response->status(), - 'body' => $response->body(), - 'code' => $finishedGoodsCode, - ]); - - return [ - 'success' => false, - 'error' => 'API 응답 오류: HTTP ' . $response->status(), - ]; - } catch (\Exception $e) { - Log::error('FormulaApiService: 예외 발생', [ - 'message' => $e->getMessage(), - 'code' => $finishedGoodsCode, - ]); - - return [ - 'success' => false, - 'error' => '수식 계산 서버 연결 실패: ' . $e->getMessage(), - ]; - } - } -} -``` - -**트러블슈팅 가이드**: -- `401 Unauthorized` → API Key 확인: `docker exec sam-mng-1 php artisan tinker --execute="echo env('FLOW_TESTER_API_KEY');"` -- `Connection refused` → nginx 컨테이너 확인: `docker ps | grep nginx` -- `SSL certificate problem` → `withoutVerifying()` 누락 확인 -- `422 Validation` → finished_goods_code가 items 테이블에 존재하는지 확인 - -#### 1.2 ItemManagementApiController::calculateFormula 추가 - -**파일**: `mng/app/Http/Controllers/Api/Admin/ItemManagementApiController.php` - -**변경**: 기존 컨트롤러에 메서드 1개 추가 + use 문 추가 - -```php -// 파일 상단 use 추가 -use App\Services\FormulaApiService; - -// 기존 메서드 아래에 추가 -/** - * 수식 기반 BOM 산출 (API 서버의 FormulaEvaluatorService HTTP 호출) - */ -public function calculateFormula(Request $request, int $id): JsonResponse -{ - $item = \App\Models\Items\Item::withoutGlobalScopes() - ->where('tenant_id', session('selected_tenant_id')) - ->findOrFail($id); - - $width = (int) $request->input('width', 1000); - $height = (int) $request->input('height', 1000); - $qty = (int) $request->input('qty', 1); - - $variables = [ - 'W0' => $width, - 'H0' => $height, - 'QTY' => $qty, - ]; - - $formulaService = new FormulaApiService(); - $result = $formulaService->calculateBom( - $item->code, - $variables, - (int) session('selected_tenant_id') - ); - - return response()->json($result); -} -``` - -#### 1.3 API 라우트 추가 - -**파일**: `mng/routes/api.php` (라인 866~ 기존 items 그룹 내) - -**추가 위치**: 기존 detail 라우트 아래 - -```php -// 기존 라우트 아래에 추가 -Route::post('/{id}/calculate-formula', [ItemManagementApiController::class, 'calculateFormula'])->name('calculate-formula'); -``` - ---- - -### 4.2 Phase 2: MNG 프론트엔드 - -#### 2.1 중앙 패널 탭 UI - -**수정 파일**: `mng/resources/views/item-management/index.blade.php` - -**변경 대상 (현재 HTML)**: -```html -
-

BOM 구성 (재귀 트리)

-
-
-``` - -**변경 후**: -```html -
-
- - -
-
- - - - - -
-

좌측에서 품목을 선택하세요.

-
- - - -``` - -#### 2.2 item-detail.blade.php에 메타 데이터 추가 - -**수정 파일**: `mng/resources/views/item-management/partials/item-detail.blade.php` - -**파일 맨 위에 추가** (기존 `
` 앞): -```html - - -``` - -#### 2.3 JS 추가 (index.blade.php @push('scripts')) - -**기존 IIFE 내부에 추가할 변수와 함수**: - -```javascript -// ── 추가 변수 ── -let currentBomTab = 'static'; // 'static' | 'formula' -let currentItemId = null; -let currentItemCode = null; - -// ── 탭 전환 ── -window.switchBomTab = function(tab) { - currentBomTab = tab; - - // 탭 버튼 스타일 - document.querySelectorAll('.bom-tab').forEach(btn => { - btn.classList.remove('bg-blue-100', 'text-blue-800'); - btn.classList.add('bg-gray-100', 'text-gray-600'); - }); - const activeBtn = document.getElementById(tab === 'static' ? 'tab-static-bom' : 'tab-formula-bom'); - if (activeBtn) { - activeBtn.classList.remove('bg-gray-100', 'text-gray-600'); - activeBtn.classList.add('bg-blue-100', 'text-blue-800'); - } - - // 콘텐츠 영역 전환 - document.getElementById('bom-tree-container').style.display = (tab === 'static') ? '' : 'none'; - document.getElementById('formula-input-panel').style.display = (tab === 'formula') ? '' : 'none'; - document.getElementById('formula-result-container').style.display = (tab === 'formula') ? '' : 'none'; -}; - -// ── 가변사이즈 탭 표시/숨김 ── -function showFormulaTab() { - document.getElementById('tab-formula-bom').style.display = ''; - switchBomTab('formula'); // 자동으로 수식 산출 탭으로 전환 -} - -function hideFormulaTab() { - document.getElementById('tab-formula-bom').style.display = 'none'; - document.getElementById('formula-input-panel').style.display = 'none'; - document.getElementById('formula-result-container').style.display = 'none'; - switchBomTab('static'); -} - -// ── 상세 로드 완료 후 가변사이즈 감지 ── -document.body.addEventListener('htmx:afterSwap', function(event) { - if (event.detail.target.id === 'item-detail') { - const meta = document.getElementById('item-meta-data'); - if (meta) { - currentItemId = meta.dataset.itemId; - currentItemCode = meta.dataset.itemCode; - if (meta.dataset.isVariableSize === 'true') { - showFormulaTab(); - } else { - hideFormulaTab(); - } - } - } -}); - -// ── 수식 산출 API 호출 ── -window.calculateFormula = function() { - if (!currentItemId) return; - - const width = parseInt(document.getElementById('input-width').value) || 1000; - const height = parseInt(document.getElementById('input-height').value) || 1000; - const qty = parseInt(document.getElementById('input-qty').value) || 1; - - // 입력값 범위 검증 - if (width < 100 || width > 10000 || height < 100 || height > 10000) { - alert('폭과 높이는 100~10000 범위로 입력하세요.'); - return; - } - - const container = document.getElementById('formula-result-container'); - container.innerHTML = '
'; - - fetch(`/api/admin/items/${currentItemId}/calculate-formula`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfToken, - }, - body: JSON.stringify({ width, height, qty }), - }) - .then(res => res.json()) - .then(data => { - if (data.success === false) { - container.innerHTML = ` -
-

${data.error || '산출 실패'}

- -
`; - return; - } - renderFormulaTree(data, container); - }) - .catch(err => { - container.innerHTML = ` -
-

서버 연결 실패

- -
`; - }); -}; - -// ── 수식 산출 결과 트리 렌더링 ── -function renderFormulaTree(data, container) { - container.innerHTML = ''; - - // 카테고리 그룹 한글 매핑 - const groupLabels = { steel: '강재', part: '부품', motor: '모터/컨트롤러' }; - const groupIcons = { steel: '🏗️', part: '🔧', motor: '⚡' }; - const groupedItems = data.grouped_items || {}; - - // 합계 영역 - if (data.grand_total) { - const totalDiv = document.createElement('div'); - totalDiv.className = 'mb-4 p-3 bg-blue-50 rounded-lg flex justify-between items-center'; - totalDiv.innerHTML = ` - - ${data.finished_goods?.name || ''} (${data.finished_goods?.code || ''}) - W:${data.variables?.W0} H:${data.variables?.H0} - - 합계: ${Number(data.grand_total).toLocaleString()}원 - `; - container.appendChild(totalDiv); - } - - // 카테고리 그룹별 렌더링 - Object.entries(groupedItems).forEach(([group, items]) => { - if (!items || items.length === 0) return; - - const groupDiv = document.createElement('div'); - groupDiv.className = 'mb-3'; - - const subtotal = data.subtotals?.[group] || 0; - - // 그룹 헤더 - const header = document.createElement('div'); - header.className = 'flex items-center gap-2 py-2 px-3 bg-gray-50 rounded-t-lg cursor-pointer'; - header.innerHTML = ` - - ${groupIcons[group] || '📦'} - ${groupLabels[group] || group} - (${items.length}건) - 소계: ${Number(subtotal).toLocaleString()}원 - `; - - const listDiv = document.createElement('div'); - listDiv.className = 'border border-gray-100 rounded-b-lg divide-y divide-gray-50'; - - // 그룹 접기/펼치기 - header.onclick = function() { - const toggle = header.querySelector('.text-gray-400'); - if (listDiv.style.display === 'none') { - listDiv.style.display = ''; - toggle.textContent = '▼'; - } else { - listDiv.style.display = 'none'; - toggle.textContent = '▶'; - } - }; - - // 아이템 목록 - items.forEach(item => { - const row = document.createElement('div'); - row.className = 'flex items-center gap-2 py-1.5 px-3 hover:bg-gray-50 cursor-pointer text-sm'; - row.innerHTML = ` - PT - ${item.item_code || ''} - ${item.item_name || ''} - ${item.quantity || 0} ${item.unit || ''} - ${Number(item.total_price || 0).toLocaleString()}원 - `; - // 아이템 클릭 시 items 테이블에서 해당 코드로 검색하여 상세 표시 - row.onclick = function() { - // item_code로 좌측 검색 → 해당 품목 상세 로드 - const searchInput = document.getElementById('item-search'); - searchInput.value = item.item_code; - loadItemList(); - }; - listDiv.appendChild(row); - }); - - groupDiv.appendChild(header); - groupDiv.appendChild(listDiv); - container.appendChild(groupDiv); - }); - - if (Object.keys(groupedItems).length === 0) { - container.innerHTML = '

산출된 자재가 없습니다.

'; - } -} -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | ~~API 인증 방식~~ | X-API-KEY 필수 (FLOW_TESTER_API_KEY) | MNG→API 통신 | ✅ 확인 완료 | -| 2 | API 라우트 추가 | POST /api/admin/items/{id}/calculate-formula | mng/routes/api.php | ✅ 즉시 가능 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-19 | - | 계획 문서 초안 작성 | - | - | -| 2026-02-19 | - | 자기완결성 보강 (기존 코드 현황, API 인증 분석, 트러블슈팅) | - | - | -| 2026-02-19 | 1.1~1.3 | Phase 1 백엔드 구현 완료 (FormulaApiService, Controller, Route) | FormulaApiService.php, ItemManagementApiController.php, api.php | ✅ | -| 2026-02-19 | 2.1~2.5 | Phase 2 프론트엔드 구현 완료 (탭 UI, 입력 폼, 트리 렌더링, 감지, 에러) | index.blade.php, item-detail.blade.php | ✅ | - ---- - -## 7. 참고 문서 - -- **기존 품목관리 계획**: `docs/plans/mng-item-management-plan.md` -- **FormulaEvaluatorService**: `api/app/Services/Quote/FormulaEvaluatorService.php` - - 메서드: `calculateBomWithDebug(string $finishedGoodsCode, array $inputVariables, ?int $tenantId): array` - - tenant_id=287 자동 감지 → KyungdongFormulaHandler 라우팅 -- **KyungdongFormulaHandler**: `api/app/Services/Quote/Handlers/KyungdongFormulaHandler.php` - - `calculateDynamicItems(array $inputs)` → steel(10종), part(3종), motor/controller 산출 -- **API 라우트**: `api/routes/api/v1/sales.php:64` → `QuoteController::calculateBom` -- **QuoteBomCalculateRequest**: `api/app/Http/Requests/V1/QuoteBomCalculateRequest.php` - - `finished_goods_code` (required|string) - - `variables` (required|array), `variables.W0` (required|numeric), `variables.H0` (required|numeric) - - `tenant_id` (nullable|integer) -- **MNG-API HTTP 패턴**: `mng/app/Services/FlowTester/HttpClient.php` -- **API Key 설정**: `mng/config/api-explorer.php:26` → `env('FLOW_TESTER_API_KEY')` -- **현재 품목관리 뷰**: `mng/resources/views/item-management/index.blade.php` -- **MNG 프로젝트 규칙**: `mng/CLAUDE.md` - ---- - -## 8. 세션 및 메모리 관리 정책 - -### 8.1 세션 시작 시 (Load Strategy) -``` -1. 이 문서 읽기 (docs/plans/mng-item-formula-integration-plan.md) -2. 📍 현재 진행 상태 확인 → 다음 작업 파악 -3. 섹션 3 "이미 구현된 코드" 확인 → 수정 대상 파일 파악 -4. 필요시 Serena 메모리 로드: - read_memory("item-formula-state") - read_memory("item-formula-snapshot") - read_memory("item-formula-active-symbols") -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | Snapshot | `write_memory("item-formula-snapshot", "코드변경+논의요약")` | -| **20% 이하** | Context Purge | `write_memory("item-formula-active-symbols", "주요 수정 파일/함수")` | -| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 | - ---- - -## 9. 검증 결과 - -### 9.1 테스트 케이스 - -| # | 테스트 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---|--------|------|----------|----------|------| -| 1 | FG 가변사이즈 품목 선택 | FG-KQTS01 클릭 | 수식 산출 탭 자동 표시, 입력 폼 노출 | | ⏳ | -| 2 | 오픈사이즈 입력 후 산출 | W:3000, H:3000, QTY:1 | 17종 자재 트리 (steel/part/motor 그룹별), 소계/합계 표시 | | ⏳ | -| 3 | 비가변사이즈 품목 선택 | PT 품목 클릭 | 수식 산출 탭 숨김, 정적 BOM만 표시 | | ⏳ | -| 4 | 정적 BOM ↔ 수식 산출 탭 전환 | 탭 클릭 | 각 탭 콘텐츠 전환, 입력 폼 표시/숨김 | | ⏳ | -| 5 | 산출 결과에서 품목 클릭 | 트리 노드 클릭 | 좌측 검색에 품목코드 입력 → 품목 리스트 필터링 | | ⏳ | -| 6 | API Key 미설정 | FLOW_TESTER_API_KEY 없음 | 에러 메시지 "API 응답 오류: HTTP 401" + 재시도 버튼 | | ⏳ | -| 7 | 입력값 범위 초과 | W:0, H:-1 | alert 표시, API 호출 안 함 | | ⏳ | -| 8 | 서버 연결 실패 | nginx 중지 상태 | 에러 메시지 "서버 연결 실패" + 재시도 버튼 | | ⏳ | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| FG 가변사이즈 품목에서 수식 산출 가능 | ⏳ | | -| 산출 결과가 견적관리와 동일한 17종 자재 표시 | ⏳ | | -| 정적 BOM과 수식 산출 탭 전환 작동 | ⏳ | | -| 비가변사이즈 품목은 기존 정적 BOM만 표시 | ⏳ | | -| 에러 처리 및 로딩 상태 표시 | ⏳ | | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 가변사이즈 FG 품목의 동적 자재 산출 표시 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 성공 기준 5개 항목 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1 백엔드 3건, Phase 2 프론트 5건 | -| 4 | 의존성이 명시되어 있는가? | ✅ | API 엔드포인트 인증 분석 완료, Docker 라우팅 패턴 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 파일 경로 검증 완료 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 전체 구현 코드 포함 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 8개 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/코드로 기술 | -| 9 | 기존 코드 현황이 명시되어 있는가? | ✅ | 섹션 3에 전체 파일 구조 + 핵심 코드 인라인 | -| 10 | API 인증 방식이 확정되었는가? | ✅ | X-API-KEY 필수 (FLOW_TESTER_API_KEY) | -| 11 | 트러블슈팅 가이드가 있는가? | ✅ | 4.1 FormulaApiService 트러블슈팅 섹션 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 + 4.1 Phase 1 | -| Q3. 어떤 파일을 수정/생성해야 하는가? | ✅ | 3.1 파일 구조 + 2. 대상 범위 | -| Q4. 현재 코드 상태는 어떤가? | ✅ | 3.2~3.6 기존 코드 현황 | -| Q5. API 인증은 어떻게 하는가? | ✅ | 4.1 FormulaApiService (인증 테이블) | -| Q6. 테넌트 필터링은 어떻게 동작하는가? | ✅ | 3.6 테넌트 필터링 패턴 | -| Q7. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q8. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 + 4.1 트러블슈팅 | - -**결과**: 8/8 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다. 자기완결성 보강: 2026-02-19* diff --git a/plans/archive/mng-item-management-plan.md b/plans/archive/mng-item-management-plan.md deleted file mode 100644 index 172f216..0000000 --- a/plans/archive/mng-item-management-plan.md +++ /dev/null @@ -1,1447 +0,0 @@ -# MNG 품목관리 페이지 계획 - -> **작성일**: 2026-02-19 -> **목적**: MNG 관리자 패널에 3-Panel 품목관리 페이지 추가 (좌측 리스트 + 중앙 BOM 트리 + 우측 상세) -> **기준 문서**: docs/rules/item-policy.md, docs/specs/item-master-integration.md -> **상태**: ✅ 기본 구현 완료 (미커밋) → Phase 3 수식 연동은 별도 계획 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 1~2 전체 구현 완료 (미커밋 상태) | -| **다음 작업** | 수식 엔진 연동 → `docs/plans/mng-item-formula-integration-plan.md` 참조 | -| **진행률** | 12/12 (100%) - 기본 3-Panel 구현 완료 | -| **마지막 업데이트** | 2026-02-19 | -| **후속 작업** | FormulaEvaluatorService 연동 (별도 계획 문서) | - ---- - -## 1. 개요 - -### 1.1 배경 - -MNG 관리자 패널에 품목(Items)을 관리하고 BOM 연결관계를 시각적으로 파악할 수 있는 페이지가 필요하다. -현재 items 테이블은 products + materials 통합 구조로, `items.bom` JSON 필드에 BOM 구성을 저장한다. - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - MNG에서 마이그레이션 파일 생성 금지 (API에서만) │ -│ - Service-First (비즈니스 로직은 Service 클래스에만) │ -│ - FormRequest 필수 (Controller 검증 금지) │ -│ - BelongsToTenant (테넌트 격리) │ -│ - Blade + HTMX + Tailwind (Alpine.js 미사용) │ -│ - 세션 기반 테넌트 필터링: session('selected_tenant_id') │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 모델/서비스/뷰/컨트롤러/라우트 생성 | 불필요 | -| ⚠️ 컨펌 필요 | 기존 라우트 수정, 사이드바 메뉴 추가 | **필수** | -| 🔴 금지 | mng에서 마이그레이션 생성, 테이블 구조 변경 | 별도 협의 | - -### 1.4 MNG 절대 금지 규칙 (인라인) - -``` -❌ mng/database/migrations/ 에 파일 생성 금지 -❌ docker exec sam-mng-1 php artisan migrate 실행 금지 -❌ php artisan db:seed --class=*MenuSeeder 실행 금지 -❌ 메뉴 시더 파일 생성/실행 금지 (부서별 권한 초기화됨) -❌ Controller에서 직접 DB 쿼리 금지 (Service-First) -❌ Controller에서 직접 validate() 금지 (FormRequest 필수) -``` - ---- - -## 2. 기능 설계 - -### 2.1 3-Panel 레이아웃 - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Header (64px) - 테넌트 선택 (session 기반 필터링) │ -├──────────┬─────────────────────────────┬────────────────────────────┤ -│ 좌측 │ 중앙 │ 우측 │ -│ (280px) │ (flex-1) │ (380px) │ -│ │ │ │ -│ [검색] │ │ ┌──────────────────────┐ │ -│ ________│ │ │ 기본정보 │ │ -│ │ BOM 재귀 트리 │ │ 코드: P-001 │ │ -│ 품목 1 ◀│ ┌ 완제품A │ │ 이름: 스크린 제품 │ │ -│ 품목 2 │ ├─ 부품B │ │ 유형: FG │ │ -│ 품목 3 │ │ ├─ 원자재C │ │ 단위: EA │ │ -│ 품목 4 │ │ └─ 부자재D │ │ 카테고리: ... │ │ -│ 품목 5 │ ├─ 부품E │ ├──────────────────────┤ │ -│ ... │ │ ├─ 원자재F │ │ BOM 구성 (1depth) │ │ -│ │ │ └─ 소모품G │ │ - 부품B (2ea) │ │ -│ │ └─ 원자재H │ │ - 부품E (1ea) │ │ -│ │ │ │ - 원자재H (0.5kg) │ │ -│ │ ← 전체 재귀 트리 → │ ├──────────────────────┤ │ -│ │ (좌측 선택 품목 기준) │ │ 절곡 정보 │ │ -│ │ │ │ (bending_details) │ │ -│ │ │ ├──────────────────────┤ │ -│ │ │ │ 이미지/파일 │ │ -│ │ │ │ 📎 도면.pdf │ │ -│ │ │ │ 📎 인증서.pdf │ │ -│ │ │ └──────────────────────┘ │ -├──────────┴─────────────────────────────┴────────────────────────────┤ -│ ← 클릭 시 어디서든 → 우측 상세 갱신 │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 패널별 상세 동작 - -#### 좌측 패널 (품목 리스트) -- **상단 검색**: `` debounce 300ms, 코드+이름 동시 검색 -- **리스트**: 스크롤 가능, 선택된 항목 하이라이트 -- **표시 정보**: 품목코드, 품목명, 유형(FG/PT/SM/RM/CS) 뱃지 -- **테넌트 필터**: 헤더에서 선택된 테넌트 자동 적용 (BelongsToTenant) -- **클릭 시**: 중앙 트리 갱신 + 우측 상세 갱신 - -#### 중앙 패널 (BOM 재귀 트리) -- **데이터 소스**: `items.bom` JSON → child_item_id 재귀 탐색 -- **트리 깊이**: 전체 재귀 (BOM → BOM → BOM ...) -- **노드 표시**: 품목코드 + 품목명 + 수량 + 유형 뱃지 -- **펼침/접힘**: 노드별 토글 가능 -- **클릭 시**: 해당 품목으로 우측 상세 갱신 (좌측 선택은 변경 안 함) - -#### 우측 패널 (선택 품목 상세) -- **기본정보**: 코드, 이름, 유형, 단위, 카테고리, 활성 여부, options -- **BOM 구성 (1depth)**: 직접 연결된 자식 품목만 (재귀 X) -- **절곡 정보**: item_details.bending_details JSON (해당 시) -- **파일/이미지**: 연결된 files 목록 -- **scope**: 선택된 품목에 직접 연결된 정보만 (1depth) - -### 2.3 데이터 흐름 - -``` -[좌측 검색/선택] - │ - ├──→ HTMX GET /api/admin/items?search=xxx - │ → 좌측 리스트 갱신 - │ - ├──→ fetch GET /api/admin/items/{id}/bom-tree - │ → 중앙 트리 갱신 (재귀 JSON 반환 → Vanilla JS 렌더링) - │ - └──→ HTMX GET /api/admin/items/{id}/detail - → 우측 상세 갱신 - -[중앙 트리 노드 클릭] - │ - └──→ HTMX GET /api/admin/items/{id}/detail - → 우측 상세만 갱신 (중앙 트리 유지) -``` - ---- - -## 3. 기술 설계 - -### 3.1 DB 스키마 (기존 테이블 활용, 변경 없음) - -```sql --- items (통합 품목) - 이미 존재하는 테이블 --- item_type: FG(완제품), PT(부품), SM(부자재), RM(원자재), CS(소모품) --- item_category: SCREEN, STEEL, BENDING, ALUMINUM 등 -CREATE TABLE items ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - item_type VARCHAR(10) NOT NULL, -- FG/PT/SM/RM/CS - item_category VARCHAR(50) NULL, -- SCREEN/STEEL/BENDING/ALUMINUM 등 - code VARCHAR(50) NOT NULL, - name VARCHAR(200) NOT NULL, - unit VARCHAR(20) NULL, - category_id BIGINT UNSIGNED NULL, -- FK → categories.id - bom JSON NULL, -- [{child_item_id: 5, quantity: 2.5}, ...] - attributes JSON NULL, -- 동적 필드 (migration 등에서 가져온 데이터) - attributes_archive JSON NULL, -- 아카이브 - options JSON NULL, -- {lot_managed, consumption_method, ...} - description TEXT NULL, - is_active TINYINT(1) DEFAULT 1, - created_by BIGINT UNSIGNED NULL, - updated_by BIGINT UNSIGNED NULL, - deleted_at TIMESTAMP NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - INDEX (tenant_id), INDEX (item_type), INDEX (code), INDEX (category_id) -); - --- item_details (1:1 확장) - 이미 존재하는 테이블 -CREATE TABLE item_details ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - item_id BIGINT UNSIGNED NOT NULL UNIQUE, -- FK → items.id (1:1) - -- Products 전용 - is_sellable TINYINT(1) DEFAULT 0, - is_purchasable TINYINT(1) DEFAULT 0, - is_producible TINYINT(1) DEFAULT 0, - safety_stock DECIMAL(10,2) NULL, - lead_time INT NULL, - is_variable_size TINYINT(1) DEFAULT 0, - product_category VARCHAR(50) NULL, - part_type VARCHAR(50) NULL, - bending_diagram VARCHAR(255) NULL, -- 절곡 도면 파일 경로 - bending_details JSON NULL, -- 절곡 상세 정보 JSON - specification_file VARCHAR(255) NULL, - specification_file_name VARCHAR(255) NULL, - certification_file VARCHAR(255) NULL, - certification_file_name VARCHAR(255) NULL, - certification_number VARCHAR(100) NULL, - certification_start_date DATE NULL, - certification_end_date DATE NULL, - -- Materials 전용 - is_inspection CHAR(1) NULL, -- 'Y'/'N' - item_name VARCHAR(200) NULL, - specification VARCHAR(500) NULL, - search_tag VARCHAR(500) NULL, - remarks TEXT NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL -); - --- files (폴리모픽) - 이미 존재하는 테이블 --- 품목 파일: document_id = items.id, document_type = '1' (ITEM_GROUP_ID) -CREATE TABLE files ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NULL, - document_id BIGINT UNSIGNED NOT NULL, -- 연결 대상 ID (items.id) - document_type VARCHAR(10) NOT NULL, -- '1' = ITEM_GROUP_ID - original_name VARCHAR(255) NOT NULL, - stored_name VARCHAR(255) NOT NULL, - path VARCHAR(500) NOT NULL, - mime_type VARCHAR(100) NULL, - size BIGINT UNSIGNED NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL -); - --- categories - 이미 존재하는 테이블 --- 품목 카테고리 (code_group으로 구분, 계층 구조) -CREATE TABLE categories ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - parent_id BIGINT UNSIGNED NULL, -- 자기 참조 (트리) - code_group VARCHAR(50) NOT NULL, -- 카테고리 그룹 - profile_code VARCHAR(50) NULL, - code VARCHAR(50) NOT NULL, - name VARCHAR(200) NOT NULL, - is_active TINYINT(1) DEFAULT 1, - sort_order INT DEFAULT 0, - description TEXT NULL, - created_by BIGINT UNSIGNED NULL, - updated_by BIGINT UNSIGNED NULL, - deleted_by BIGINT UNSIGNED NULL, - deleted_at TIMESTAMP NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL -); -``` - -### 3.2 BOM 트리 재귀 로직 - -```php -// ItemManagementService::getBomTree(int $itemId, int $maxDepth = 10): array -public function getBomTree(int $itemId, int $maxDepth = 10): array -{ - $item = Item::with('details')->findOrFail($itemId); - return $this->buildBomNode($item, 0, $maxDepth, []); -} - -private function buildBomNode(Item $item, int $depth, int $maxDepth, array $visited): array -{ - // 순환 참조 방지: visited 배열 + maxDepth 이중 안전장치 - if (in_array($item->id, $visited) || $depth >= $maxDepth) { - return $this->formatNode($item, $depth, []); - } - - $visited[] = $item->id; - $children = []; - - $bomData = $item->bom ?? []; - if (!empty($bomData)) { - $childIds = array_column($bomData, 'child_item_id'); - $childItems = Item::whereIn('id', $childIds)->get()->keyBy('id'); - - foreach ($bomData as $bom) { - $childItem = $childItems->get($bom['child_item_id']); - if ($childItem) { - $childNode = $this->buildBomNode($childItem, $depth + 1, $maxDepth, $visited); - $childNode['quantity'] = $bom['quantity'] ?? 1; - $children[] = $childNode; - } - } - } - - return $this->formatNode($item, $depth, $children); -} - -private function formatNode(Item $item, int $depth, array $children): array -{ - return [ - 'id' => $item->id, - 'code' => $item->code, - 'name' => $item->name, - 'item_type' => $item->item_type, - 'unit' => $item->unit, - 'depth' => $depth, - 'has_children' => count($children) > 0, - 'children' => $children, - ]; -} -``` - -### 3.3 API 엔드포인트 설계 - -| Method | Endpoint | 설명 | 반환 | -|--------|----------|------|------| -| GET | `/api/admin/items` | 품목 목록 (검색, 페이지네이션) | HTML partial | -| GET | `/api/admin/items/{id}/bom-tree` | BOM 재귀 트리 | JSON | -| GET | `/api/admin/items/{id}/detail` | 품목 상세 (1depth BOM, 파일, 절곡) | HTML partial | - -#### GET /api/admin/items - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| search | string | 코드+이름 검색 (LIKE) | -| item_type | string | 유형 필터 (FG,PT,SM,RM,CS 쉼표 구분) | -| per_page | int | 페이지 크기 (default: 50) | -| page | int | 페이지 번호 | - -#### GET /api/admin/items/{id}/bom-tree - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| max_depth | int | 최대 재귀 깊이 (default: 10) | - -**응답 (JSON)**: -```json -{ - "id": 1, - "code": "SCREEN-001", - "name": "스크린 제품", - "item_type": "FG", - "unit": "EA", - "depth": 0, - "has_children": true, - "children": [ - { - "id": 5, - "code": "SLAT-001", - "name": "슬랫", - "item_type": "PT", - "quantity": 2.5, - "depth": 1, - "has_children": true, - "children": [ - { - "id": 12, - "code": "STEEL-001", - "name": "강판", - "item_type": "RM", - "quantity": 1.0, - "depth": 2, - "has_children": false, - "children": [] - } - ] - } - ] -} -``` - -#### GET /api/admin/items/{id}/detail - -**응답 (HTML partial)**: 기본정보 + BOM 1depth + 절곡정보 + 파일 목록 - -### 3.4 파일 구조 - -``` -mng/ -├── app/ -│ ├── Http/ -│ │ └── Controllers/ -│ │ ├── ItemManagementController.php # Web (Blade 화면) -│ │ └── Api/Admin/ -│ │ └── ItemManagementApiController.php # API (HTMX) -│ ├── Models/ -│ │ ├── Category.php # ⚠️ 이미 존재 (수정 불필요) -│ │ └── Items/ -│ │ ├── Item.php # ⚠️ 이미 존재 → 보완 필요 -│ │ └── ItemDetail.php # 신규 생성 -│ ├── Services/ -│ │ └── ItemManagementService.php # BOM 트리, 검색, 상세 -│ └── Traits/ -│ └── BelongsToTenant.php # ⚠️ 이미 존재 (수정 불필요) -├── resources/ -│ └── views/ -│ └── item-management/ -│ ├── index.blade.php # 메인 (3-Panel) -│ └── partials/ -│ ├── item-list.blade.php # 좌측 리스트 -│ ├── bom-tree.blade.php # 중앙 트리 (JS 렌더링) -│ └── item-detail.blade.php # 우측 상세 -└── routes/ - ├── web.php # + items 라우트 추가 - └── api.php # + items API 라우트 추가 -``` - -### 3.5 트리 렌더링 방식 - -**Vanilla JS + Tailwind (라이브러리 미사용)** - MNG 기존 패턴 유지 - -```javascript -// BOM 트리 JSON → HTML 변환 -function renderBomTree(node, container) { - const li = document.createElement('li'); - li.className = 'ml-4'; - - // 노드 렌더링 - const nodeEl = document.createElement('div'); - nodeEl.className = 'flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-blue-50'; - nodeEl.onclick = () => selectTreeNode(node.id); - - // 펼침/접힘 토글 - if (node.has_children) { - const toggle = document.createElement('span'); - toggle.className = 'text-gray-400 cursor-pointer'; - toggle.textContent = '▶'; - toggle.onclick = (e) => { e.stopPropagation(); toggleNode(toggle, childList); }; - nodeEl.appendChild(toggle); - } else { - // 빈 공간 (들여쓰기 맞춤) - const spacer = document.createElement('span'); - spacer.className = 'w-4 inline-block'; - nodeEl.appendChild(spacer); - } - - // 유형 뱃지 + 코드 + 이름 + 수량 - nodeEl.innerHTML += ` - ${node.item_type} - ${node.code} - ${node.name} - ${node.quantity ? `(${node.quantity})` : ''} - `; - li.appendChild(nodeEl); - - // 자식 노드 재귀 렌더링 - if (node.children && node.children.length > 0) { - const childList = document.createElement('ul'); - childList.className = 'border-l border-gray-200'; - node.children.forEach(child => renderBomTree(child, childList)); - li.appendChild(childList); - } - - container.appendChild(li); -} - -// 트리 노드 펼침/접힘 -function toggleNode(toggle, childList) { - if (childList.style.display === 'none') { - childList.style.display = ''; - toggle.textContent = '▼'; - } else { - childList.style.display = 'none'; - toggle.textContent = '▶'; - } -} -``` - ---- - -## 4. 대상 범위 - -### Phase 1: 백엔드 (모델 + 서비스 + API) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | Item 모델 보완 (mng/app/Models/Items/Item.php) | ✅ | BelongsToTenant, 관계, 스코프, 상수, 헬퍼 추가 | -| 1.2 | ItemDetail 모델 생성 (mng/app/Models/Items/ItemDetail.php) | ✅ | 1:1 관계, is_variable_size 포함 | -| 1.3 | ItemManagementService 생성 | ✅ | getItemList, getBomTree(재귀), getItemDetail | -| 1.4 | ItemManagementApiController 생성 | ✅ | index(HTML), bomTree(JSON), detail(HTML) | -| 1.5 | API 라우트 등록 (routes/api.php) | ✅ | /api/admin/items/* (3개 라우트) | -| 1.6 | File 모델 생성 (mng/app/Models/Commons/File.php) | ✅ | Item.files() 관계용 | - -### Phase 2: 프론트엔드 (Blade + JS) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 메인 페이지 (index.blade.php) - 3-Panel 레이아웃 | ✅ | Tailwind flex, 3-Panel | -| 2.2 | 좌측 패널 (item-list.blade.php) + 실시간 검색 | ✅ | HTMX + debounce 300ms + 유형 필터 | -| 2.3 | 중앙 패널 (bom-tree.blade.php) + JS 트리 렌더링 | ✅ | Vanilla JS 재귀 렌더링 | -| 2.4 | 우측 패널 (item-detail.blade.php) | ✅ | 기본정보+BOM 1depth+절곡+파일 | -| 2.5 | ItemManagementController (Web) 생성 | ✅ | HX-Redirect 패턴 | -| 2.6 | Web 라우트 등록 (routes/web.php) | ✅ | GET /item-management | -| 2.7 | 유형별 뱃지 스타일 + 트리 라인 CSS | ✅ | Tailwind inline + JS getTypeBadgeClass | - -### Phase 3: 수식 엔진 연동 (후속 작업) - -> 별도 계획 문서: `docs/plans/mng-item-formula-integration-plan.md` -> -> 가변사이즈 FG 품목 선택 시 오픈사이즈(W,H) 입력 → FormulaEvaluatorService 동적 산출 → 중앙 패널 탭 전환 표시 - ---- - -## 5. 작업 절차 - -### Step 1: 모델 보완/생성 (Phase 1.1, 1.2) -``` -├── mng/app/Models/Items/Item.php 보완 (기존 파일 존재) -│ 현재 상태: SoftDeletes만 있음, BelongsToTenant 없음, 관계 없음 -│ 추가 필요: -│ - use App\Traits\BelongsToTenant 추가 -│ - $fillable에 category_id, bom, attributes, options, description 추가 -│ - $casts에 bom→array, options→array 추가 -│ - 관계: details(), category(), files() -│ - 스코프: type(), active(), search() -│ - 상수: TYPE_FG 등, PRODUCT_TYPES, MATERIAL_TYPES -│ - 헬퍼: isProduct(), isMaterial(), getBomChildIds() -│ -└── mng/app/Models/Items/ItemDetail.php 생성 (신규) - - item() belongsTo 관계 - - $fillable: 전체 필드 (섹션 A.3 참고) - - $casts: bending_details→array, is_sellable→boolean 등 -``` - -### Step 2: 서비스 생성 (Phase 1.3) -``` -├── mng/app/Services/ItemManagementService.php 생성 -│ - getItemList(array $filters): LengthAwarePaginator -│ └ Item::query()->search($search)->active()->orderBy('code')->paginate($perPage) -│ - getBomTree(int $itemId, int $maxDepth = 10): array -│ └ 재귀 buildBomNode() (섹션 3.2 코드) -│ - getItemDetail(int $itemId): array -│ └ Item::with(['details', 'category', 'files'])->findOrFail($id) -│ └ BOM 1depth: items.bom JSON에서 child_item_id 추출 → Item::whereIn() -│ -└── 테넌트 스코프 자동 적용 (BelongsToTenant가 글로벌 스코프 등록) -``` - -### Step 3: API 컨트롤러 + 라우트 (Phase 1.4, 1.5) -``` -├── mng/app/Http/Controllers/Api/Admin/ItemManagementApiController.php -│ - __construct(private readonly ItemManagementService $service) -│ - index(Request $request): View -│ └ HTMX 요청 시 HTML partial 반환 (Blade view render) -│ - bomTree(int $id): JsonResponse -│ └ JSON 반환 (JS에서 트리 렌더링) -│ - detail(int $id): View -│ └ HTML partial 반환 (item-detail.blade.php) -│ -└── routes/api.php에 라우트 추가 (기존 그룹 내) - // 기존 Route::middleware(['web', 'auth', 'hq.member']) - // ->prefix('admin')->name('api.admin.')->group(function () { ... }); - // 내부에 추가: - Route::prefix('items')->name('items.')->group(function () { - Route::get('/', [ItemManagementApiController::class, 'index'])->name('index'); - Route::get('/{id}/bom-tree', [ItemManagementApiController::class, 'bomTree'])->name('bom-tree'); - Route::get('/{id}/detail', [ItemManagementApiController::class, 'detail'])->name('detail'); - }); -``` - -### Step 4: Blade 뷰 생성 (Phase 2.1~2.4) -``` -├── index.blade.php: 3-Panel 메인 레이아웃 -│ @extends('layouts.app'), @section('content'), @push('scripts') -│ HTMX 페이지이므로 HX-Redirect 필요 (JS가 @push('scripts')에 있음) -│ -├── partials/item-list.blade.php: 좌측 품목 리스트 -│ @foreach($items as $item) → 품목코드, 품목명, 유형 뱃지 -│ data-item-id="{{ $item->id }}" onclick="selectItem({{ $item->id }})" -│ -├── partials/bom-tree.blade.php: 중앙 트리 (빈 컨테이너) -│
품목을 선택하세요
-│ -└── partials/item-detail.blade.php: 우측 상세정보 - 기본정보 테이블 + BOM 1depth 리스트 + 절곡 정보 + 파일 목록 -``` - -### Step 5: Web 컨트롤러 + 라우트 (Phase 2.5, 2.6) -``` -├── mng/app/Http/Controllers/ItemManagementController.php -│ - __construct(private readonly ItemManagementService $service) -│ - index(Request $request): View|Response -│ └ HX-Request 체크 → HX-Redirect (JS 포함 페이지이므로) -│ └ return view('item-management.index') -│ -└── routes/web.php에 라우트 추가 - // 기존 인증 미들웨어 그룹 내에 추가: - Route::get('/item-management', [ItemManagementController::class, 'index']) - ->name('item-management.index'); -``` - -### Step 6: 스타일 + 트리 인터랙션 (Phase 2.7) -``` -├── 유형별 뱃지 색상 (Tailwind inline) -│ FG: bg-blue-100 text-blue-800 (완제품) -│ PT: bg-green-100 text-green-800 (부품) -│ SM: bg-yellow-100 text-yellow-800 (부자재) -│ RM: bg-orange-100 text-orange-800 (원자재) -│ CS: bg-gray-100 text-gray-800 (소모품) -│ -└── 트리 라인 CSS (border-l + ml-4 indent) -``` - ---- - -## 6. 상세 구현 명세 - -### 6.1 Item 모델 보완 (기존 파일 수정) - -**기존 파일**: `mng/app/Models/Items/Item.php` - -**현재 상태 (보완 전)**: -```php - 'boolean', - 'attributes' => 'array', - ]; -} -``` - -**보완 후 (목표 상태)**: -```php - 'array', - 'attributes' => 'array', - 'attributes_archive' => 'array', - 'options' => 'array', - 'is_active' => 'boolean', - ]; - - // 유형 상수 - const TYPE_FG = 'FG'; // 완제품 - const TYPE_PT = 'PT'; // 부품 - const TYPE_SM = 'SM'; // 부자재 - const TYPE_RM = 'RM'; // 원자재 - const TYPE_CS = 'CS'; // 소모품 - - const PRODUCT_TYPES = ['FG', 'PT']; - const MATERIAL_TYPES = ['SM', 'RM', 'CS']; - - // ── 관계 ── - - public function details() - { - return $this->hasOne(ItemDetail::class, 'item_id'); - } - - public function category() - { - return $this->belongsTo(Category::class, 'category_id'); - } - - /** - * 파일 (document_id/document_type 기반) - * document_id = items.id, document_type = '1' (ITEM_GROUP_ID) - */ - public function files() - { - return $this->hasMany(\App\Models\Commons\File::class, 'document_id') - ->where('document_type', '1'); - } - - // ── 스코프 ── - - public function scopeType($query, string $type) - { - return $query->where('items.item_type', strtoupper($type)); - } - - public function scopeActive($query) - { - return $query->where('is_active', true); - } - - public function scopeSearch($query, ?string $search) - { - if (!$search) return $query; - return $query->where(function ($q) use ($search) { - $q->where('code', 'like', "%{$search}%") - ->orWhere('name', 'like', "%{$search}%"); - }); - } - - // ── 헬퍼 ── - - public function isProduct(): bool - { - return in_array($this->item_type, self::PRODUCT_TYPES); - } - - public function isMaterial(): bool - { - return in_array($this->item_type, self::MATERIAL_TYPES); - } - - public function getBomChildIds(): array - { - return collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray(); - } -} -``` - -> **주의**: files() 관계에서 `\App\Models\Commons\File::class` 경로를 사용한다. -> 만약 mng에 File 모델이 없다면, 단순 모델로 신규 생성해야 한다. -> 확인 필요: `mng/app/Models/Commons/File.php` 존재 여부. 없으면 생성. - -### 6.2 ItemDetail 모델 (신규 생성) - -```php - 'boolean', - 'is_purchasable' => 'boolean', - 'is_producible' => 'boolean', - 'is_variable_size' => 'boolean', - 'bending_details' => 'array', - 'certification_start_date' => 'date', - 'certification_end_date' => 'date', - ]; - - public function item() - { - return $this->belongsTo(Item::class); - } -} -``` - -### 6.3 좌측 검색 - Debounce + HTMX - -```javascript -// index.blade.php @push('scripts') -let searchTimer = null; -const searchInput = document.getElementById('item-search'); - -searchInput.addEventListener('input', function() { - clearTimeout(searchTimer); - searchTimer = setTimeout(() => { - const search = this.value.trim(); - htmx.ajax('GET', `/api/admin/items?search=${encodeURIComponent(search)}&per_page=50`, { - target: '#item-list', - swap: 'innerHTML', - headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content} - }); - }, 300); // 300ms debounce -}); -``` - -### 6.4 품목 선택 시 중앙+우측 갱신 - -```javascript -// 품목 선택 함수 (좌측/중앙 공용) -function selectItem(itemId, updateTree = true) { - // 선택 하이라이트 - document.querySelectorAll('.item-row').forEach(el => el.classList.remove('bg-blue-50', 'border-blue-300')); - const selected = document.querySelector(`[data-item-id="${itemId}"]`); - if (selected) selected.classList.add('bg-blue-50', 'border-blue-300'); - - // 중앙 트리 갱신 (좌측에서 클릭 시에만) - if (updateTree) { - fetch(`/api/admin/items/${itemId}/bom-tree`, { - headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content} - }) - .then(res => res.json()) - .then(tree => { - const container = document.getElementById('bom-tree-container'); - container.innerHTML = ''; - if (tree.has_children) { - const ul = document.createElement('ul'); - renderBomTree(tree, ul); - container.appendChild(ul); - } else { - container.innerHTML = '

BOM 구성이 없습니다.

'; - } - }); - } - - // 우측 상세 갱신 (항상) - htmx.ajax('GET', `/api/admin/items/${itemId}/detail`, { - target: '#item-detail', - swap: 'innerHTML', - headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content} - }); -} - -// 중앙 트리 노드 클릭 (트리는 유지, 우측만 갱신) -function selectTreeNode(itemId) { - selectItem(itemId, false); // updateTree = false -} -``` - ---- - -## 7. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 사이드바 메뉴 추가 | "품목관리" 메뉴 항목 추가 | menus 테이블 (DB) | ⏳ tinker 안내 필요 | - ---- - -## 8. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-19 | - | 문서 초안 작성 | - | - | -| 2026-02-19 | - | 자기완결성 보강 (Appendix A~C 추가) | - | - | -| 2026-02-19 | Phase 1 | Item 모델 보완, ItemDetail/File 모델 생성 | Item.php, ItemDetail.php, File.php | ✅ | -| 2026-02-19 | Phase 1 | ItemManagementService 생성 | ItemManagementService.php | ✅ | -| 2026-02-19 | Phase 1 | ItemManagementApiController 생성 + API 라우트 | ItemManagementApiController.php, api.php | ✅ | -| 2026-02-19 | Phase 2 | 3-Panel Blade 뷰 전체 생성 | index.blade.php + 3 partials | ✅ | -| 2026-02-19 | Phase 2 | Web 컨트롤러 + 라우트 등록 | ItemManagementController.php, web.php | ✅ | -| 2026-02-19 | - | Phase 1~2 완료, Phase 3 수식 연동 계획 별도 문서 분리 | mng-item-formula-integration-plan.md | - | - ---- - -## 9. 참고 문서 - -- **품목 정책**: `docs/rules/item-policy.md` -- **품목 연동 설계**: `docs/specs/item-master-integration.md` -- **MNG 절대 규칙**: `mng/docs/MNG_CRITICAL_RULES.md` -- **MNG 프로젝트 문서**: `mng/docs/INDEX.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **API Item 모델**: `api/app/Models/Items/Item.php` -- **API ItemDetail 모델**: `api/app/Models/Items/ItemDetail.php` - ---- - -## 10. 검증 결과 - -### 10.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| 좌측 검색: "스크린" | "스크린" 포함 품목만 표시 | 정상 동작 | ✅ | -| FG 품목 클릭 | 중앙에 BOM 트리, 우측에 상세 | 정상 동작 (정적 BOM 2개 표시) | ✅ | -| BOM 없는 품목 클릭 | 중앙 "BOM 없음", 우측 상세 표시 | 정상 동작 | ✅ | -| 중앙 트리 노드 클릭 | 우측 상세만 변경 (트리 유지) | 정상 동작 | ✅ | -| 테넌트 전환 | 좌측 리스트가 해당 테넌트 품목으로 변경 | 확인 필요 | ⏳ | -| 순환 참조 BOM | 무한 루프 없이 maxDepth에서 중단 | 로직 구현 완료, 실제 데이터 미검증 | ⏳ | - -### 10.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 3-Panel 레이아웃 정상 렌더링 | ✅ | 좌측 280px + 중앙 flex-1 + 우측 384px | -| 실시간 검색 (debounce 300ms) | ✅ | 코드+이름 동시 검색 | -| BOM 재귀 트리 정상 표시 (전체 depth) | ✅ | 펼침/접힘 토글 포함 | -| 어디서든 클릭 → 우측 상세 갱신 | ✅ | selectItem + selectTreeNode | -| 테넌트 필터링 정상 동작 | ⏳ | withoutGlobalScopes + session 패턴 사용 | -| 순환 참조 방지 (maxDepth) | ✅ | visited 배열 + maxDepth 이중 안전장치 | - ---- - -## 11. 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 3-Panel 품목관리 페이지 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.2 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 (12개 작업 항목) | -| 4 | 의존성이 명시되어 있는가? | ✅ | items 테이블 존재 전제 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 + Appendix | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 (6 Step) | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10.1 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/구조 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 5. 작업 절차 Step 1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 3.4 파일 구조 + 6.1 기존 파일 현황 | -| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 + Appendix A~C | -| Q6. MNG 코딩 패턴은 무엇인가? | ✅ | Appendix A (인라인 패턴) | -| Q7. 테넌트 필터링은 어떻게 동작하는가? | ✅ | Appendix B (BelongsToTenant 전문) | -| Q8. API 모델의 정확한 필드는? | ✅ | Appendix C (API 모델 전문) | - -**결과**: 8/8 통과 → ✅ 자기완결성 확보 - ---- - -## Appendix A: MNG 코딩 패턴 레퍼런스 - -> 새 세션에서 외부 파일을 읽지 않고도 MNG 패턴을 따를 수 있도록 인라인화한 레퍼런스. - -### A.1 Web Controller 패턴 - -Web Controller는 Blade 뷰 렌더링만 담당한다. 비즈니스 로직은 Service에 위임. - -```php -header('HX-Request')) { - return response('', 200)->header('HX-Redirect', route('item-management.index')); - } - return view('item-management.index'); -} -``` - -### A.2 API Controller 패턴 - -API Controller는 HTMX 요청 시 HTML partial, 일반 요청 시 JSON 반환. - -```php -departmentService->getDepartments( - $request->all(), - $request->integer('per_page', 10) - ); - - // HTMX 요청 시 HTML partial 반환 - if ($request->header('HX-Request')) { - return view('departments.partials.table', compact('departments')); - } - - // 일반 요청 시 JSON - return response()->json([ - 'success' => true, - 'data' => $departments->items(), - 'meta' => [ - 'current_page' => $departments->currentPage(), - 'last_page' => $departments->lastPage(), - 'per_page' => $departments->perPage(), - 'total' => $departments->total(), - ], - ]); - } -} -``` - -### A.3 Service 패턴 - -모든 DB 쿼리 로직은 Service에서 처리. `session('selected_tenant_id')`로 테넌트 격리. - -```php -with('parent'); - - // 검색 필터 - if (!empty($filters['search'])) { - $search = $filters['search']; - $query->where(function ($q) use ($search) { - $q->where('name', 'like', "%{$search}%") - ->orWhere('code', 'like', "%{$search}%"); - }); - } - - return $query->orderBy('sort_order')->paginate($perPage); - } -} -``` - -> **중요**: BelongsToTenant trait이 모델에 있으면 tenant_id 필터가 자동 적용된다. -> Service에서 수동으로 `where('tenant_id', ...)` 할 필요 없음. - -### A.4 Blade + HTMX 패턴 - -Index 페이지는 빈 셸이고, 데이터는 HTMX `hx-get` + `hx-trigger="load"`로 로드. - -```blade -{{-- 참고: mng/resources/views/departments/index.blade.php 패턴 --}} -@extends('layouts.app') - -@section('title', '부서 관리') - -@section('content') -
-

부서 관리

-
- - {{-- HTMX 테이블: 초기 로드 + 이벤트 재로드 --}} -
- {{-- 로딩 스피너 --}} -
-
-
-
-@endsection - -@push('scripts') - -@endpush -``` - -### A.5 라우트 패턴 - -**routes/web.php** 구조: -```php -// 인증 필요 라우트 그룹 -Route::middleware(['auth', 'hq.member', 'password.changed'])->group(function () { - // ... 기존 라우트들 ... - - // 품목관리 (신규 추가할 위치) - Route::get('/item-management', [ItemManagementController::class, 'index']) - ->name('item-management.index'); -}); -``` - -**routes/api.php** 구조: -```php -// MNG API는 세션 기반 (token 아님) -Route::middleware(['web', 'auth', 'hq.member']) - ->prefix('admin') - ->name('api.admin.') - ->group(function () { - // ... 기존 API 라우트들 ... - - // 품목관리 API (신규 추가할 위치) - Route::prefix('items')->name('items.')->group(function () { - Route::get('/', [ItemManagementApiController::class, 'index'])->name('index'); - Route::get('/{id}/bom-tree', [ItemManagementApiController::class, 'bomTree'])->name('bom-tree'); - Route::get('/{id}/detail', [ItemManagementApiController::class, 'detail'])->name('detail'); - }); - }); -``` - -> **주의**: MNG API는 `['web', 'auth', 'hq.member']` 미들웨어 사용 (세션 기반, Sanctum 아님). -> 고정 라우트(`/all`, `/summary`)를 `/{id}` 파라미터 라우트보다 먼저 정의해야 충돌 방지. - -### A.6 모델 패턴 - -```php -// 참고: mng/app/Models/Category.php 패턴 -use App\Traits\BelongsToTenant; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; - -class Category extends Model -{ - use BelongsToTenant, SoftDeletes; - - protected $fillable = [ - 'tenant_id', 'parent_id', 'code_group', 'profile_code', - 'code', 'name', 'is_active', 'sort_order', 'description', - 'created_by', 'updated_by', 'deleted_by', - ]; - - protected $casts = [ - 'is_active' => 'boolean', - 'sort_order' => 'integer', - ]; - - // 자기 참조 트리 - public function parent() { return $this->belongsTo(self::class, 'parent_id'); } - public function children() { return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); } - - // 스코프 - public function scopeActive($query) { return $query->where('is_active', true); } -} -``` - ---- - -## Appendix B: BelongsToTenant 동작 방식 - -### B.1 Trait (mng/app/Traits/BelongsToTenant.php) - -```php -runningInConsole()) { - return; - } - - // 요청당 1회만 tenant_id 조회 (캐시) - if (!self::$cacheInitialized) { - $request = app(Request::class); - self::$cachedTenantId = $request->attributes->get('tenant_id') - ?? $request->header('X-TENANT-ID') - ?? auth()->user()?->tenant_id; - self::$cacheInitialized = true; - } - - if (self::$cachedTenantId !== null) { - $builder->where($model->getTable() . '.tenant_id', self::$cachedTenantId); - } - } - - public static function clearCache(): void - { - self::$cachedTenantId = null; - self::$cacheInitialized = false; - } -} -``` - -**동작 요약**: -1. 모델에 `use BelongsToTenant` 선언하면 자동으로 TenantScope 등록 -2. 모든 쿼리에 `WHERE items.tenant_id = ?` 조건 자동 추가 -3. tenant_id 결정 우선순위: request attributes → X-TENANT-ID 헤더 → auth user -4. console 환경(migrate 등)에서는 스킵 -5. **Service에서 수동 tenant_id 필터 불필요** (자동 적용) - ---- - -## Appendix C: API 모델 전문 (참조용) - -> 구현 시 API 모델의 정확한 필드 목록과 관계를 참고하기 위한 인라인 전문. - -### C.1 api/app/Models/Items/Item.php (전체) - -```php - 'array', - 'attributes' => 'array', - 'attributes_archive' => 'array', - 'options' => 'array', - 'is_active' => 'boolean', - ]; - - const TYPE_FINISHED_GOODS = 'FG'; - const TYPE_PARTS = 'PT'; - const TYPE_SUB_MATERIALS = 'SM'; - const TYPE_RAW_MATERIALS = 'RM'; - const TYPE_CONSUMABLES = 'CS'; - const PRODUCT_TYPES = ['FG', 'PT']; - const MATERIAL_TYPES = ['SM', 'RM', 'CS']; - - public function details() { return $this->hasOne(ItemDetail::class); } - public function stock() { return $this->hasOne(\App\Models\Tenants\Stock::class); } - public function category() { return $this->belongsTo(Category::class, 'category_id'); } - - // files: document_id = item_id, document_type = '1' (ITEM_GROUP_ID) - public function files() - { - return $this->hasMany(File::class, 'document_id')->where('document_type', '1'); - } - - public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } - - // BOM 자식 조회 (JSON bom 필드에서 child_item_id 추출) - public function bomChildren() - { - $childIds = collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray(); - return self::whereIn('id', $childIds); - } - - // 스코프 - public function scopeType($query, string $type) - { - return $query->where('items.item_type', strtoupper($type)); - } - public function scopeProducts($query) { return $query->whereIn('items.item_type', self::PRODUCT_TYPES); } - public function scopeMaterials($query) { return $query->whereIn('items.item_type', self::MATERIAL_TYPES); } - public function scopeActive($query) { return $query->where('is_active', true); } - - // 헬퍼 - public function isProduct(): bool { return in_array($this->item_type, self::PRODUCT_TYPES); } - public function isMaterial(): bool { return in_array($this->item_type, self::MATERIAL_TYPES); } - public function getBomChildIds(): array - { - return collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray(); - } -} -``` - -### C.2 api/app/Models/Items/ItemDetail.php (전체) - -```php - 'boolean', - 'is_purchasable' => 'boolean', - 'is_producible' => 'boolean', - 'is_variable_size' => 'boolean', - 'bending_details' => 'array', - 'certification_start_date' => 'date', - 'certification_end_date' => 'date', - ]; - - public function item() { return $this->belongsTo(Item::class); } - public function isSellable(): bool { return $this->is_sellable ?? false; } - public function isPurchasable(): bool { return $this->is_purchasable ?? false; } - public function isProducible(): bool { return $this->is_producible ?? false; } - public function isCertificationValid(): bool - { - return $this->certification_end_date?->isFuture() ?? false; - } - public function requiresInspection(): bool { return $this->is_inspection === 'Y'; } -} -``` - ---- - -## Appendix D: 구현 시 확인 사항 - -### D.1 File 모델 존재 여부 확인 - -구현 시작 전 `mng/app/Models/Commons/File.php` 존재 여부를 확인해야 한다. -없으면 다음과 같이 간단한 모델 생성 필요: - -```php - 1, - 'parent_id' => <부모메뉴ID>, - 'name' => '품목관리', - 'url' => '/item-management', - 'icon' => 'heroicon-o-cube', - 'sort_order' => 1, - 'is_active' => true, -]); -" -``` - -### D.3 품목 유형 정리 - -| 코드 | 이름 | 설명 | BOM 자식 가능 | -|------|------|------|:------------:| -| FG | 완제품 (Finished Goods) | 최종 판매 제품 | ✅ 주로 있음 | -| PT | 부품 (Parts) | 조립/가공 부품 | ✅ 있을 수 있음 | -| SM | 부자재 (Sub Materials) | 보조 자재 | ❌ 일반적으로 없음 | -| RM | 원자재 (Raw Materials) | 원재료 | ❌ 리프 노드 | -| CS | 소모품 (Consumables) | 소모성 자재 | ❌ 리프 노드 | - -### D.4 items.bom JSON 구조 - -```json -// items.bom 필드 예시 (FG 완제품) -[ - {"child_item_id": 5, "quantity": 2.5}, - {"child_item_id": 8, "quantity": 1}, - {"child_item_id": 12, "quantity": 0.5} -] -// child_item_id는 같은 items 테이블의 다른 행을 참조 -// quantity는 소수점 가능 (단위에 따라 kg, m, EA 등) -``` - -### D.5 items.options JSON 구조 - -```json -{ - "lot_managed": true, // LOT 추적 여부 - "consumption_method": "auto", // auto/manual/none - "production_source": "self_produced", // purchased/self_produced/both - "input_tracking": true // 원자재 투입 추적 -} -``` - ---- - -*이 문서는 /plan 스킬로 생성되었습니다. 자기완결성 보강: 2026-02-19* \ No newline at end of file diff --git a/plans/archive/mng-quote-formula-development-plan.md b/plans/archive/mng-quote-formula-development-plan.md deleted file mode 100644 index a632902..0000000 --- a/plans/archive/mng-quote-formula-development-plan.md +++ /dev/null @@ -1,553 +0,0 @@ -# MNG 견적수식 관리 개발 계획 - -> **작성일**: 2025-12-22 -> **상태**: ✅ 완료 -> **대상**: mng.sam.kr/quote-formulas - ---- - -## 1. 현황 분석 - -### 1.1 MNG 프로젝트 현재 상태 - -#### 구현된 기능 (mng) - -| 기능 | 상태 | 설명 | -|-----|------|-----| -| 수식 목록 | ✅ 완료 | 페이지네이션, 필터링, HTMX 테이블 | -| 수식 생성 | ✅ 완료 | 카테고리, 유형, 변수명, 수식 입력 | -| 수식 수정 | ✅ 완료 | 편집 폼, API 연동 | -| 수식 삭제 | ✅ 완료 | Soft Delete, 복원, 영구삭제 | -| 수식 복제 | ✅ 완료 | 수식 복사 기능 | -| 활성/비활성 | ✅ 완료 | 토글 기능 | -| 카테고리 관리 | ✅ 완료 | CRUD 구현 | -| 시뮬레이터 | ✅ 완료 | 입력값 → 계산 결과 미리보기 | -| 변수 참조 | ✅ 완료 | 사용 가능한 변수 목록 표시 | -| 수식 검증 | ✅ 완료 | 문법 검증 API | -| 범위(Range) 관리 UI | ✅ 완료 | 범위별 결과 설정 화면 (Phase 1) | -| 매핑(Mapping) 관리 UI | ✅ 완료 | 매핑 규칙 설정 화면 (Phase 2) | -| 품목(Item) 관리 UI | ✅ 완료 | 출력 품목 설정 화면 (Phase 3) | - -### 1.2 API 프로젝트 현재 상태 - -#### 모델 구조 (api) - -``` -QuoteFormulaCategory (카테고리) -└── QuoteFormula (수식) - ├── QuoteFormulaRange (범위 조건) - ├── QuoteFormulaMapping (매핑 규칙) - └── QuoteFormulaItem (출력 품목) -``` - -#### 시더 데이터 (api) - -| 시더 | 데이터 수 | 설명 | -|-----|---------|-----| -| QuoteFormulaCategorySeeder | 11개 | 카테고리 (오픈사이즈~단가수식) | -| QuoteFormulaSeeder | 30개 수식, 18개 범위 | 스크린 계산 수식 | -| QuoteFormulaItemSeeder | 25개 | 품목 마스터 | - -#### 서비스 (api) - -| 서비스 | 역할 | -|-------|-----| -| QuoteCalculationService | 자동산출 실행 엔진 | -| FormulaEvaluatorService | 수식 평가, 범위/매핑 처리 | -| QuoteService | 견적 CRUD, 상태 관리 | -| QuoteNumberService | 견적번호 생성 | -| QuoteDocumentService | PDF/이메일/카카오 발송 (TODO) | - ---- - -## 2. MNG vs API 비교 분석 - -### 2.1 데이터 구조 비교 - -| 항목 | MNG | API | 일치 | -|-----|-----|-----|-----| -| quote_formula_categories | ✅ | ✅ | ✅ | -| quote_formulas | ✅ | ✅ | ✅ | -| quote_formula_ranges | ✅ | ✅ | ✅ | -| quote_formula_mappings | ✅ | ✅ | ✅ | -| quote_formula_items | ✅ | ✅ | ✅ | - -**결론**: 모델 구조는 동일함 (같은 DB 사용) - -### 2.2 기능 비교 - -| 기능 | MNG | API | 비고 | -|-----|-----|-----|-----| -| 수식 CRUD | ✅ | ✅ | 동일 | -| 카테고리 CRUD | ✅ | ✅ | 동일 | -| 범위 관리 UI | ✅ | ✅ (시더) | Phase 1 완료 | -| 매핑 관리 UI | ✅ | ✅ (시더) | Phase 2 완료 | -| 품목 관리 UI | ✅ | ✅ (시더) | Phase 3 완료 | -| 시뮬레이터 | ✅ | ✅ | 동일 | -| 자동산출 API | - | ✅ | API 전용 | - ---- - -## 3. 개발 계획 (완료) - -### 3.1 목표 - -MNG에서 **범위(Range), 매핑(Mapping), 품목(Item)** 관리 UI를 추가하여: -1. 시더 없이도 관리자가 직접 수식 규칙 설정 가능 -2. SAM 자체 품목 마스터로 가격 설정 -3. 실시간 시뮬레이션으로 설정 검증 가능 - -### 3.2 개발 범위 (완료) - -#### Phase 1: 범위(Range) 관리 UI ✅ - -**우선순위**: 높음 -**이유**: 모터, 가이드레일, 케이스 자동 선택에 필수 - -**기능 목록**: -1. 수식 상세 페이지에 범위 관리 탭 추가 -2. 범위 목록 표시 (min ~ max → 결과) -3. 범위 추가/수정/삭제 -4. 드래그앤드롭 순서 변경 -5. item_code 연결 (품목 선택) - -**화면 설계**: -``` -[수식 수정] 페이지 -├── [기본 정보] 탭 (기존) -├── [범위 설정] 탭 ← 추가 -│ ├── 조건 변수: [K (중량)] ▼ -│ ├── 범위 목록 -│ │ ┌─────────────────────────────────────────────────┐ -│ │ │ # │ 최소값 │ 최대값 │ 결과값 │ 품목코드 │ -│ │ ├─────────────────────────────────────────────────┤ -│ │ │ 1 │ 0 │ 150 │ 150K │ PT-MOTOR-150│ -│ │ │ 2 │ 150 │ 300 │ 300K │ PT-MOTOR-300│ -│ │ │ 3 │ 300 │ 400 │ 400K │ PT-MOTOR-400│ -│ │ └─────────────────────────────────────────────────┘ -│ └── [+ 범위 추가] -├── [매핑 설정] 탭 -└── [품목 설정] 탭 -``` - -**API 엔드포인트 (MNG 내부)**: -``` -GET /api/admin/quote-formulas/formulas/{id}/ranges -POST /api/admin/quote-formulas/formulas/{id}/ranges -PUT /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId} -DELETE /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId} -POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder -``` - -#### Phase 2: 매핑(Mapping) 관리 UI ✅ - -**우선순위**: 중간 -**이유**: 제어기 유형 등 코드 매핑에 사용 - -**기능 목록**: -1. 수식 상세 페이지에 매핑 관리 탭 추가 -2. 매핑 목록 표시 (소스값 → 결과값) -3. 매핑 추가/수정/삭제 - -**화면 설계**: -``` -[매핑 설정] 탭 -├── 소스 변수: [CONTROL_TYPE] ▼ -├── 매핑 목록 -│ ┌──────────────────────────────────────────────────┐ -│ │ # │ 소스값 │ 결과값 │ 품목코드 │ -│ ├──────────────────────────────────────────────────┤ -│ │ 1 │ EMB │ 매립형 │ PT-CTRL-EMB │ -│ │ 2 │ EXP │ 노출형 │ PT-CTRL-EXP │ -│ │ 3 │ BOX_1P │ 콘트롤박스 │ PT-CTRL-BOX-1P │ -│ └──────────────────────────────────────────────────┘ -└── [+ 매핑 추가] -``` - -#### Phase 3: 품목(Item) 관리 UI ✅ - -**우선순위**: 중간 -**이유**: 수식 결과로 생성되는 품목 정의 - -**기능 목록**: -1. 수식 상세 페이지에 품목 관리 탭 추가 -2. 품목 목록 표시 -3. 품목 추가/수정/삭제 -4. 수량/단가 수식 입력 -5. SAM 품목 마스터에서 가격 참조 - -**화면 설계**: -``` -[품목 설정] 탭 -├── 품목 목록 -│ ┌───────────────────────────────────────────────────────────┐ -│ │ 품목코드 │ 품목명 │ 규격 │ 수량식 │ 단가식│ -│ ├───────────────────────────────────────────────────────────┤ -│ │ PT-MOTOR-150 │ 개폐전동기 150kg│ 150K(S) │ 1 │ 285000│ -│ │ PT-GR-3000 │ 가이드레일 3000 │ 3000mm │ 2 │ 42000 │ -│ └───────────────────────────────────────────────────────────┘ -└── [+ 품목 추가] -``` - -### 3.3 파일 구조 (구현 완료) - -#### Controllers -``` -app/Http/Controllers/ -├── QuoteFormulaController.php (수정: 탭 추가) -└── Api/Admin/Quote/ - ├── QuoteFormulaController.php - ├── QuoteFormulaRangeController.php ✅ - ├── QuoteFormulaMappingController.php ✅ - ├── QuoteFormulaItemController.php ✅ - └── QuoteFormulaCategoryController.php -``` - -#### Services -``` -app/Services/Quote/ -├── QuoteFormulaService.php -├── QuoteFormulaRangeService.php ✅ -├── QuoteFormulaMappingService.php ✅ -├── QuoteFormulaItemService.php ✅ -└── QuoteFormulaCategoryService.php -``` - -#### Views -``` -resources/views/quote-formulas/ -├── index.blade.php -├── create.blade.php -├── edit.blade.php (수정: 탭 구조) -├── simulator.blade.php -└── partials/ - ├── basic-info-tab.blade.php ✅ - ├── ranges-tab.blade.php ✅ - ├── mappings-tab.blade.php ✅ - └── items-tab.blade.php ✅ -``` - ---- - -## 4. 기술 스택 - -### 4.1 Frontend (MNG) -- **Framework**: Laravel Blade + Alpine.js -- **Styling**: Tailwind CSS + DaisyUI -- **AJAX**: HTMX (hx-get, hx-post, hx-delete) -- **Modal**: DaisyUI modal 컴포넌트 - -### 4.2 Backend (MNG) -- **Framework**: Laravel 12 -- **ORM**: Eloquent -- **DB**: MySQL (samdb) -- **Auth**: Session 기반 - -### 4.3 API 연동 -- MNG 내부 API (`/api/admin/quote-formulas/*`) - ---- - -## 5. 검증 계획 - -### 5.1 시뮬레이터 테스트 -``` -입력: W0=3000, H0=2500 -예상 결과: - - CASE: PT-CASE-3600 (S=3270) - - GR: PT-GR-3000 (H1=2770) - - MOTOR: PT-MOTOR-150 (K=41.21kg) -``` - -### 5.2 CRUD 테스트 -- 범위 추가/수정/삭제 후 시뮬레이터 결과 확인 -- 품목 가격 변경 후 합계 확인 - ---- - -## 6. 참고 자료 - -### 6.1 파일 위치 (MNG) -``` -mng/ -├── app/Http/Controllers/ -│ ├── QuoteFormulaController.php -│ └── Api/Admin/Quote/ -│ ├── QuoteFormulaController.php -│ ├── QuoteFormulaRangeController.php -│ ├── QuoteFormulaMappingController.php -│ ├── QuoteFormulaItemController.php -│ └── QuoteFormulaCategoryController.php -├── app/Services/Quote/ -│ ├── QuoteFormulaService.php -│ ├── QuoteFormulaRangeService.php -│ ├── QuoteFormulaMappingService.php -│ ├── QuoteFormulaItemService.php -│ └── QuoteFormulaCategoryService.php -├── app/Models/Quote/ -│ ├── QuoteFormula.php -│ ├── QuoteFormulaCategory.php -│ ├── QuoteFormulaRange.php -│ ├── QuoteFormulaMapping.php -│ └── QuoteFormulaItem.php -└── resources/views/quote-formulas/ - ├── index.blade.php - ├── create.blade.php - ├── edit.blade.php - ├── simulator.blade.php - └── partials/ - ├── basic-info-tab.blade.php - ├── ranges-tab.blade.php - ├── mappings-tab.blade.php - └── items-tab.blade.php -``` - -### 6.2 API 시더 위치 -``` -api/database/seeders/ -├── QuoteFormulaCategorySeeder.php -├── QuoteFormulaSeeder.php -└── QuoteFormulaItemSeeder.php -``` - ---- - -## 7. 코딩 컨벤션 및 예시 코드 - -### 7.1 API Controller 패턴 (MNG) - -```php -rangeService->getRangesByFormula($formulaId); - - return response()->json([ - 'success' => true, - 'data' => $ranges, - ]); - } - - /** - * 범위 생성 - */ - public function store(Request $request, int $formulaId): JsonResponse - { - $validated = $request->validate([ - 'min_value' => 'nullable|numeric', - 'max_value' => 'nullable|numeric', - 'condition_variable' => 'required|string|max:50', - 'result_value' => 'required|string', - 'result_type' => 'in:fixed,formula', - 'sort_order' => 'nullable|integer', - ]); - - $range = $this->rangeService->createRange($formulaId, $validated); - - return response()->json([ - 'success' => true, - 'message' => '범위가 추가되었습니다.', - 'data' => $range, - ]); - } - - /** - * 범위 수정 - */ - public function update(Request $request, int $formulaId, int $rangeId): JsonResponse - { - $validated = $request->validate([ - 'min_value' => 'nullable|numeric', - 'max_value' => 'nullable|numeric', - 'result_value' => 'required|string', - 'result_type' => 'in:fixed,formula', - ]); - - $this->rangeService->updateRange($rangeId, $validated); - - return response()->json([ - 'success' => true, - 'message' => '범위가 수정되었습니다.', - ]); - } - - /** - * 범위 삭제 - */ - public function destroy(int $formulaId, int $rangeId): JsonResponse - { - $this->rangeService->deleteRange($rangeId); - - return response()->json([ - 'success' => true, - 'message' => '범위가 삭제되었습니다.', - ]); - } - - /** - * 순서 변경 - */ - public function reorder(Request $request, int $formulaId): JsonResponse - { - $validated = $request->validate([ - 'range_ids' => 'required|array', - 'range_ids.*' => 'integer', - ]); - - $this->rangeService->reorder($validated['range_ids']); - - return response()->json([ - 'success' => true, - 'message' => '순서가 변경되었습니다.', - ]); - } -} -``` - -### 7.2 Service 패턴 (MNG) - -```php -orderBy('sort_order') - ->get(); - } - - /** - * 범위 생성 - */ - public function createRange(int $formulaId, array $data): QuoteFormulaRange - { - $data['formula_id'] = $formulaId; - - // 순서 자동 설정 - if (!isset($data['sort_order'])) { - $maxOrder = QuoteFormulaRange::where('formula_id', $formulaId)->max('sort_order') ?? 0; - $data['sort_order'] = $maxOrder + 1; - } - - return QuoteFormulaRange::create($data); - } - - /** - * 범위 수정 - */ - public function updateRange(int $rangeId, array $data): QuoteFormulaRange - { - $range = QuoteFormulaRange::findOrFail($rangeId); - $range->update($data); - - return $range->fresh(); - } - - /** - * 범위 삭제 - */ - public function deleteRange(int $rangeId): void - { - QuoteFormulaRange::destroy($rangeId); - } - - /** - * 순서 변경 - */ - public function reorder(array $rangeIds): void - { - foreach ($rangeIds as $order => $id) { - QuoteFormulaRange::where('id', $id)->update(['sort_order' => $order + 1]); - } - } -} -``` - -### 7.3 API 응답 형식 - -```json -// 성공 응답 -{ - "success": true, - "message": "범위가 추가되었습니다.", - "data": { ... } -} - -// 실패 응답 -{ - "success": false, - "message": "이미 사용 중인 변수명입니다." -} - -// 목록 응답 -{ - "success": true, - "data": [ - { - "id": 1, - "formula_id": 5, - "min_value": "0.0000", - "max_value": "150.0000", - "condition_variable": "K", - "result_value": "{\"value\":\"150K\",\"item_code\":\"PT-MOTOR-150\"}", - "result_type": "fixed", - "sort_order": 1 - } - ] -} -``` - ---- - -## 8. 체크리스트 (완료) - -### 개발 완료 확인 - -- [x] mng 프로젝트 디렉토리: `/Users/hskwon/Works/@KD_SAM/SAM/mng` -- [x] `QuoteFormulaRangeController.php` 생성 -- [x] `QuoteFormulaRangeService.php` 생성 -- [x] `QuoteFormulaMappingController.php` 생성 -- [x] `QuoteFormulaMappingService.php` 생성 -- [x] `QuoteFormulaItemController.php` 생성 -- [x] `QuoteFormulaItemService.php` 생성 -- [x] `routes/api.php`에 라우트 추가 -- [x] `edit.blade.php` 탭 구조로 수정 -- [x] `partials/ranges-tab.blade.php` 생성 -- [x] `partials/mappings-tab.blade.php` 생성 -- [x] `partials/items-tab.blade.php` 생성 - ---- - -*문서 버전*: 2.0 -*작성자*: Claude Code -*검토자*: - -*최종 업데이트*: 2025-12-22 (Phase 1-3 완료, 5130 연동 제거) \ No newline at end of file diff --git a/plans/archive/notification-sound-system-plan.md b/plans/archive/notification-sound-system-plan.md deleted file mode 100644 index f2e7e66..0000000 --- a/plans/archive/notification-sound-system-plan.md +++ /dev/null @@ -1,424 +0,0 @@ -# 알림음 시스템 구현 계획 - -> **작성일**: 2025-01-07 -> **목적**: FCM 푸시 알림 타입별 커스텀 알림음 구현 -> **영향 범위**: app (Capacitor), api (Laravel), mng (Laravel) -> **상태**: ✅ 핵심 기능 완료 (4.3 알림 설정 테이블은 후순위) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 5 - 테스트 및 검증 완료 ✅ | -| **다음 작업** | 완료 (4.3 알림 설정 테이블은 후순위) | -| **진행률** | 10/11 (91%) - 핵심 기능 완료 | -| **마지막 업데이트** | 2025-01-07 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM 앱은 FCM 푸시 알림 시 2개 채널(`push_default`, `push_urgent`)만 지원합니다. -비즈니스 요구사항에 따라 알림 타입별로 다른 알림음이 필요합니다: - -- 결제 알림 → 결제 전용 알림음 -- 수주 알림 → 수주 전용 알림음 -- 발주 알림 → 발주 전용 알림음 -- 계약 알림 → 계약 전용 알림음 -- 일반 알림 → 기본 알림음 -- 신규업체 등록 → 긴급 알림음 - -### 1.2 목표 구조 - -| 타입 | 채널 ID | 알림음 파일 | 설명 | -|------|---------|------------|------| -| 결제 | `push_payment` | `push_payment.wav` | 결제 관련 알림 | -| 수주 | `push_sales_order` | `push_sales_order.wav` | 수주 관련 알림 | -| 발주 | `push_purchase_order` | `push_purchase_order.wav` | 발주 관련 알림 | -| 계약 | `push_contract` | `push_contract.wav` | 계약 관련 알림 | -| 일반 | `push_default` | `push_default.wav` | 일반 알림 (기존) | -| 신규업체 등록 | `push_urgent` | `push_urgent.wav` | 신규업체 등록 (기존) | - -### 1.3 현재 상태 분석 - -#### App (Capacitor Android) -- **파일**: `app/android/app/src/main/java/com/codebridgex/webapp/MainActivity.java` -- **현재**: 2개 채널 (`push_default`, `push_urgent`) -- **알림음**: `res/raw/push_default.wav`, `res/raw/push_urgent.wav` - -#### API (Laravel) -- **파일**: `api/app/Services/Fcm/FcmSender.php` -- **현재**: `channel_id` 파라미터 지원, 사운드는 `'default'` 하드코딩 -- **문제**: 커스텀 사운드 미지원 - -#### MNG (Laravel) -- **파일**: `mng/app/Http/Controllers/FcmController.php` -- **현재**: `sound_key` 파라미터 존재하나 실제 활용 안됨 - -### 1.4 시스템 흐름 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ FCM 알림음 시스템 흐름 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ MNG (발송 UI) │ -│ ┌─────────────────┐ │ -│ │ 타입 선택 │ ← 결제/수주/발주/계약/일반/신규업체 │ -│ │ channel_id 설정 │ │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ API (FCM 발송) │ -│ ┌─────────────────┐ │ -│ │ FcmSender │ │ -│ │ channel_id → │ │ -│ │ android.channel │ │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ Firebase Cloud Messaging │ -│ ┌─────────────────┐ │ -│ │ FCM Server │ │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ App (Capacitor) │ -│ ┌─────────────────┐ │ -│ │ NotificationChannel │ ← channel_id로 매칭 │ -│ │ 채널별 사운드 재생 │ ← push_payment.wav 등 │ -│ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: App - 채널 및 알림음 추가 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 1.1 | 알림음 파일 준비 (4개) | ✅ | `res/raw/*.wav` | -| 1.2 | MainActivity.java 채널 추가 (4개) | ✅ | `MainActivity.java` | - -### 2.2 Phase 2: API - FcmSender 수정 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 2.1 | buildMessage() 사운드 동적 처리 | ✅ | `FcmSender.php` | -| 2.2 | 채널-사운드 매핑 (FcmSender 내부 통합) | ✅ | `FcmSender.php` | - -### 2.3 Phase 3: MNG - 발송 UI 수정 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 3.1 | 타입 선택 드롭다운 추가 | ✅ | `fcm/send.blade.php` | -| 3.2 | 타입-채널 매핑 로직 | ✅ | `FcmController.php` | - -### 2.4 Phase 4: 이벤트 기반 자동 푸시 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 4.1 | PushNotificationService 생성 | ✅ | `api/app/Services/PushNotificationService.php` | -| 4.2 | 신규 거래처 등록 시 푸시 | ✅ | `api/app/Services/ClientService.php` | -| 4.3 | 알림 설정 테이블 (추후) | ⏭️ | 후순위 | - -### 2.5 Phase 5: 테스트 및 검증 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | 각 타입별 푸시 발송 테스트 | ✅ | 6개 타입 | -| 5.2 | 알림음 재생 확인 | ✅ | Android 실기기 | - ---- - -## 3. 상세 작업 내용 - -### 3.1 Phase 1: App - 채널 및 알림음 추가 - -#### 1.1 알림음 파일 준비 - -**위치**: `app/android/app/src/main/res/raw/` - -| 파일명 | 상태 | 비고 | -|--------|------|------| -| `push_default.wav` | ✅ | 일반 알림 | -| `push_urgent.wav` | ✅ | 신규업체 등록 | -| `push_payment.wav` | ✅ | 결제 알림 | -| `push_sales_order.wav` | ✅ | 수주 알림 | -| `push_purchase_order.wav` | ✅ | 발주 알림 | -| `push_contract.wav` | ✅ | 계약 알림 | - -> **완료**: 6개 알림음 파일 모두 준비됨 (2025-01-07) - -#### 1.2 MainActivity.java 수정 - -**현재 코드** (2개 채널): -```java -public static final String CHANNEL_DEFAULT = "push_default"; -public static final String CHANNEL_URGENT = "push_urgent"; -``` - -**목표 코드** (6개 채널): -```java -public static final String CHANNEL_DEFAULT = "push_default"; -public static final String CHANNEL_URGENT = "push_urgent"; -public static final String CHANNEL_PAYMENT = "push_payment"; -public static final String CHANNEL_SALES_ORDER = "push_sales_order"; -public static final String CHANNEL_PURCHASE_ORDER = "push_purchase_order"; -public static final String CHANNEL_CONTRACT = "push_contract"; -``` - -### 3.2 Phase 2: API - FcmSender 수정 - -#### 2.1 buildMessage() 수정 - -**현재** (`FcmSender.php:112`): -```php -'android' => [ - 'notification' => [ - 'channel_id' => $channelId, - 'sound' => 'default', // 하드코딩 - ], -], -``` - -**목표**: -```php -'android' => [ - 'notification' => [ - 'channel_id' => $channelId, - 'sound' => $this->getSoundForChannel($channelId), - ], -], -``` - -#### 2.2 채널-사운드 매핑 - -```php -// config/fcm.php 또는 FcmSender 내부 -private function getSoundForChannel(string $channelId): string -{ - return match($channelId) { - 'push_payment' => 'push_payment', - 'push_sales_order' => 'push_sales_order', - 'push_purchase_order' => 'push_purchase_order', - 'push_contract' => 'push_contract', - 'push_urgent' => 'push_urgent', - default => 'push_default', - }; -} -``` - -### 3.3 Phase 3: MNG - 발송 UI 수정 - -#### 3.1 타입 선택 UI - -```html - -``` - -#### 3.2 타입 → 채널 매핑 - -```php -$channelMap = [ - 'general' => 'push_default', - 'payment' => 'push_payment', - 'sales_order' => 'push_sales_order', - 'purchase_order' => 'push_purchase_order', - 'contract' => 'push_contract', - 'new_company' => 'push_urgent', -]; -``` - -### 3.4 Phase 4: 이벤트 기반 자동 푸시 - -#### 4.1 PushNotificationService 생성 - -**파일**: `api/app/Services/PushNotificationService.php` - -```php -getChannelForEvent($event); - - // 해당 테넌트의 활성 토큰 조회 - $tokens = PushDeviceToken::where('tenant_id', $tenantId) - ->where('is_active', true) - ->pluck('token') - ->toArray(); - - if (empty($tokens)) { - return; - } - - $this->fcmSender->sendToMany( - $tokens, - $title, - $body, - $channelId, - $data - ); - } - - /** - * 이벤트 → 채널 매핑 - */ - private function getChannelForEvent(string $event): string - { - return match($event) { - 'payment' => 'push_payment', - 'sales_order' => 'push_sales_order', - 'purchase_order' => 'push_purchase_order', - 'contract' => 'push_contract', - 'new_client' => 'push_urgent', - default => 'push_default', - }; - } -} -``` - -#### 4.2 ClientService에서 푸시 호출 - -**파일**: `api/app/Services/ClientService.php` (store 메서드) - -```php -/** 생성 */ -public function store(array $data) -{ - $tenantId = $this->tenantId(); - - $data['client_code'] = $this->generateClientCode($tenantId); - $data['tenant_id'] = $tenantId; - $data['is_active'] = $data['is_active'] ?? true; - - $client = Client::create($data); - - // 신규 거래처 등록 푸시 발송 - app(PushNotificationService::class) - ->setTenantId($tenantId) - ->sendByEvent( - 'new_client', - $tenantId, - '신규 거래처 등록', - "새로운 거래처 '{$client->name}'이(가) 등록되었습니다.", - ['client_id' => $client->id] - ); - - return $client; -} -``` - -#### 4.3 이벤트 타입 정의 - -| 이벤트 | 채널 | 발생 시점 | -|--------|------|----------| -| `new_client` | `push_urgent` | 거래처 신규 등록 | -| `payment` | `push_payment` | 결제 완료/요청 | -| `sales_order` | `push_sales_order` | 수주 등록/변경 | -| `purchase_order` | `push_purchase_order` | 발주 등록/변경 | -| `contract` | `push_contract` | 계약 등록/만료 | - ---- - -## 4. 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 알림음 파일 추가, 채널 추가 | 불필요 | -| ⚠️ 컨펌 필요 | FcmSender 로직 변경, UI 수정 | **필수** | -| 🔴 금지 | FCM 구조 변경, 기존 채널 삭제 | 별도 협의 | - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 알림음 파일 | 6개 wav 파일 준비 | app | ✅ 완료 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-07 | - | 계획 문서 초안 작성 | - | - | -| 2025-01-07 | 1.2 | MainActivity.java 6개 채널 추가 | `MainActivity.java` | ✅ | -| 2025-01-07 | 2.1/2.2 | FcmSender 사운드 동적 처리 + getSoundForChannel 추가 | `FcmSender.php` | ✅ | -| 2025-01-07 | 3.1 | MNG 알림 타입 드롭다운 추가 (6개 타입) | `fcm/send.blade.php` | ✅ | -| 2025-01-07 | 3.2 | FcmController channel_id 검증 + sound_key 제거 | `FcmController.php` | ✅ | -| 2025-01-07 | 4.1 | PushNotificationService 생성 (이벤트 기반 푸시) | `PushNotificationService.php` | ✅ | -| 2025-01-07 | 4.2 | ClientService.store()에 푸시 알림 연동 | `ClientService.php` | ✅ | -| 2025-01-07 | 5.1/5.2 | 테스트 및 검증 완료 | 서버 배포 후 실기기 테스트 | ✅ | - ---- - -## 7. 참고 문서 - -- **FCM 푸시 계획**: `docs/plans/react-fcm-push-notification-plan.md` -- **API 규칙**: `docs/standards/api-rules.md` - ---- - -## 8. 알림음 파일 준비 가이드 - -### 요구사항 -- **포맷**: WAV (권장) 또는 MP3 -- **길이**: 1-3초 권장 -- **샘플레이트**: 44.1kHz -- **비트레이트**: 16bit - -### 임시 방안 -알림음 파일이 준비되지 않은 경우, 기존 파일을 복사하여 사용: - -```bash -cd app/android/app/src/main/res/raw/ -cp push_default.wav push_payment.wav -cp push_default.wav push_sales_order.wav -cp push_default.wav push_purchase_order.wav -cp push_default.wav push_contract.wav -``` - -### 무료 알림음 리소스 -- [Pixabay Sound Effects](https://pixabay.com/sound-effects/) -- [Freesound](https://freesound.org/) -- [Zapsplat](https://www.zapsplat.com/) - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/order-location-management-plan.md b/plans/archive/order-location-management-plan.md deleted file mode 100644 index cac3da9..0000000 --- a/plans/archive/order-location-management-plan.md +++ /dev/null @@ -1,831 +0,0 @@ -# 수주 하위 구조 관리 시스템 구축 계획 - -> **작성일**: 2026-02-06 -> **목적**: 수주(Order) 하위에 범용 N-depth 트리 구조를 구축하여 개소/구역/공정 등 다양한 하위 단위를 자유롭게 관리 -> **기준 문서**: `docs/features/quotes/README.md`, `docs/specs/database-schema.md`, `docs/standards/api-rules.md` -> **상태**: 🔄 진행중 -> **설계 결정**: 하이브리드 (고정 코어 컬럼 + options JSON) — 통계 쿼리 성능과 유연성 균형 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4.2 - 프론트엔드 노드별 그룹 UI | -| **다음 작업** | 완료 (테스트 검증 필요) | -| **진행률** | 13/13 (100%) | -| **마지막 업데이트** | 2026-02-06 | - ---- - -## 1. 개요 - -### 1.1 배경 - -**즉시 문제**: 견적→수주 전환(`QuoteService::convertToOrder`)에서 개소 정보가 매핑되지 않아 `order_items.floor_code`, `symbol_code`가 null로 저장됨. 반면 `OrderService::syncFromQuote`에는 이미 파싱 로직이 있어 정상 동작. - -**구조적 문제**: 현재 수주 하위 구조는 `order_items` 플랫 테이블뿐이며, 개소/구역/공정 등 다양한 그루핑 단위를 관리할 수 없음. 5130(경동)은 개소별 관리가 필요하지만, 향후 다른 테넌트에서는 구역별, 층별, 공정별 등 다양한 트리 구조가 필요. - -**현재 데이터 흐름 문제**: -``` -견적 저장: - quotes.calculation_inputs.items[] → 개소별 데이터 ✅ - quote_items.note → "4F FSS-01" ✅ - -수주 전환 (convertToOrder): - order_items.floor_code → null ❌ ← $productMapping이 빈 배열 - order_items.symbol_code → null ❌ - -수주 동기화 (syncFromQuote): - order_items.floor_code → "4F" ✅ ← note 파싱 로직 있음 - order_items.symbol_code → "FSS-01" ✅ -``` - -### 1.2 목표 - -1. 견적→수주 전환 시 개소 정보가 정확히 매핑되도록 즉시 수정 (Quick Fix) -2. `order_nodes` 테이블을 신규 생성하여 **범용 N-depth 트리 구조** 제공 -3. 노드별 독립 상태 추적 (대기/진행중/완료/취소) -4. 프론트엔드에서 노드별 그룹 UI 제공 (경동은 개소별 표시) - -### 1.3 아키텍처 결정 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 설계 결정: 하이브리드 (고정 코어 + options JSON) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ❌ 순수 EAV → 통계 쿼리 시 JOIN 폭발, 성능 문제 │ -│ ❌ 고정 컬럼 전용 → 경동 개소에만 맞고 범용성 없음 │ -│ ✅ 하이브리드 → 통계용 고정 컬럼 + 유형별 상세는 options JSON │ -│ │ -│ 근거: │ -│ - SAM 프로젝트에서 이미 options JSON 패턴 사용 중 │ -│ (work_order_items.options, quotes.calculation_inputs) │ -│ - MySQL 8 JSON path 쿼리 지원 (options->>'$.floor' 등) │ -│ - 통계 집계는 고정 컬럼(code, name, status, quantity, price)으로 │ -│ - sam_stat 일간/월간 집계에도 고정 컬럼 기반으로 수월 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 핵심 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. 범용 트리: N-depth 자기참조(parent_id)로 어떤 구조든 표현 가능 │ -│ 2. 통계 친화: code, name, status, quantity, price는 고정 컬럼 │ -│ 3. 유형 자유: node_type으로 구분, 유형별 상세는 options JSON │ -│ 4. 역호환성: 기존 수주(order_nodes 없는)도 정상 동작 │ -│ 5. SAM 패턴 준수: BelongsToTenant, Auditable, SoftDeletes │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.5 적용 예시 - -**경동 (1-depth: 개소)**: -``` -Order: ORD-260206-001 -├── Node (type:location, code:"1F-FSS-01", name:"1F FSS-01") -│ ├── options: { floor:"1F", symbol:"FSS-01", product_code:"KSS01", -│ │ open_width:5000, open_height:3000, guide_rail:"wall" } -│ └── OrderItems (자재 N개) -│ -└── Node (type:location, code:"2F-SD-02", name:"2F SD-02") - ├── options: { floor:"2F", symbol:"SD-02", product_code:"KWE01", - │ open_width:2800, open_height:2400 } - └── OrderItems (자재 N개) -``` - -**다른 테넌트 (3-depth: 동→층→실)**: -``` -Order: ORD-260206-005 -├── Node (type:zone, code:"A", name:"A동") -│ ├── Node (type:floor, code:"1F", name:"1층") -│ │ ├── Node (type:room, code:"101", name:"회의실") -│ │ │ └── OrderItems -│ │ └── Node (type:room, code:"102", name:"사무실") -│ │ └── OrderItems -│ └── Node (type:floor, code:"2F", name:"2층") -│ └── ... -└── Node (type:zone, code:"B", name:"B동") - └── ... -``` - -### 1.6 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | convertToOrder 로직 수정, 모델 관계 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션 생성, 신규 테이블, API 엔드포인트 추가 | **필수** | -| 🔴 금지 | 기존 order_items.floor_code/symbol_code 삭제, 기존 API 스키마 변경 | 별도 협의 | - -### 1.7 준수 규칙 - -- `docs/standards/api-rules.md` - Service-First, FormRequest, i18n -- `docs/specs/database-schema.md` - 공통 컬럼 패턴 (tenant_id, audit, softDeletes) -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `react/CLAUDE.md` - 'use client' 필수, Server Actions - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: convertToOrder 개소 파싱 (Quick Fix) - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 1.1 | convertToOrder에 개소 파싱 로직 추가 | `api/app/Services/Quote/QuoteService.php` | ✅ | syncFromQuote 로직 재사용 | -| 1.2 | 개소 파싱 공통 메소드 추출 | `api/app/Services/Quote/QuoteService.php` | ✅ | 중복 코드 제거 | - -### 2.2 Phase 2: order_nodes 테이블 (DB 스키마) - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 2.1 | order_nodes 마이그레이션 생성 | `api/database/migrations/XXXX_create_order_nodes_table.php` | ✅ | 신규 테이블 | -| 2.2 | order_items에 order_node_id 추가 | `api/database/migrations/XXXX_add_order_node_id_to_order_items.php` | ✅ | nullable FK | -| 2.3 | OrderNode 모델 생성 | `api/app/Models/Orders/OrderNode.php` | ✅ | BelongsToTenant, SoftDeletes, 자기참조 | -| 2.4 | Order 모델에 nodes() 관계 추가 | `api/app/Models/Orders/Order.php` | ✅ | HasMany | -| 2.5 | OrderItem 모델에 node() 관계 추가 | `api/app/Models/Orders/OrderItem.php` | ✅ | BelongsTo, fillable 추가 | - -### 2.3 Phase 3: 전환 로직 연동 (Service) - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 3.1 | convertToOrder에 OrderNode 생성 로직 추가 | `api/app/Services/Quote/QuoteService.php` | ✅ | Phase 1.1 위에 구축 | -| 3.2 | syncFromQuote에 OrderNode 동기화 추가 | `api/app/Services/OrderService.php` | ✅ | 기존 items 삭제→재생성 패턴 동일 | -| 3.3 | 수주 상세 조회에 nodes eager loading | `api/app/Services/OrderService.php` | ✅ | show() 메소드 수정 | - -### 2.4 Phase 4: 프론트엔드 노드별 UI - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 4.1 | OrderNode 타입 + 서버 액션 추가 | `react/src/components/orders/actions.ts` | ✅ | 타입 정의, API 호출 | -| 4.2 | 수주 상세 뷰 노드별 그룹 UI | `react/src/components/orders/OrderSalesDetailView.tsx` | ✅ | 트리/아코디언 형식 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Phase 1: Quick Fix (convertToOrder 개소 파싱) -├── 1.1 syncFromQuote의 개소 파싱 로직을 공통 메소드로 추출 -├── 1.2 convertToOrder에서 공통 메소드 호출하여 $productMapping 전달 -└── 검증: 견적→수주 전환 후 order_items.floor_code/symbol_code 값 확인 - -Phase 2: DB 스키마 (order_nodes 테이블) -├── 2.1 order_nodes 마이그레이션 작성 -│ ├── 트리 구조: parent_id 자기참조 (nullable = 루트) -│ ├── 고정 코어: node_type, code, name, status_code, quantity, unit_price, total_price -│ └── 유연 확장: options JSON -├── 2.2 order_items에 order_node_id 컬럼 마이그레이션 작성 -├── 2.3 OrderNode 모델 생성 (BelongsToTenant, Auditable, SoftDeletes) -│ ├── 자기참조 관계: parent(), children() -│ └── items() HasMany -├── 2.4 Order 모델에 nodes() HasMany 관계 추가 -├── 2.5 OrderItem 모델에 node() BelongsTo 관계 추가 -└── 검증: php artisan migrate 성공, 트리 관계 정상 동작 - -Phase 3: 전환 로직 연동 -├── 3.1 convertToOrder에 OrderNode 생성 로직 삽입 -│ ├── calculation_inputs.items[] 순회하여 OrderNode (type:location) 생성 -│ ├── bomResults[]에서 금액 정보 매핑 -│ └── OrderItem 생성 시 order_node_id 연결 -├── 3.2 syncFromQuote에 OrderNode 동기화 추가 -│ ├── 기존 nodes 소프트삭제 → 신규 생성 -│ └── OrderItem 재생성 시 node 연결 -├── 3.3 수주 상세 조회에 nodes eager loading 추가 -└── 검증: API 호출로 노드 데이터 정상 반환 확인 - -Phase 4: 프론트엔드 UI -├── 4.1 타입 + 서버 액션 -│ ├── OrderNode 인터페이스 정의 -│ └── 수주 상세 조회 응답에 nodes 포함 -├── 4.2 수주 상세 뷰 노드별 그룹 UI -│ ├── 노드별 카드/아코디언 레이아웃 -│ ├── 노드 헤더 (유형/코드/이름/상태/금액) -│ ├── 노드 내 자재 테이블 -│ ├── 하위 노드 중첩 표시 (재귀 컴포넌트) -│ └── 역호환: nodes 없는 수주는 기존 플랫 테이블 유지 -└── 검증: 수주 상세 화면에서 노드별 그룹 표시 확인 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: Quick Fix (변경 없음) - -#### 1.1 convertToOrder 개소 파싱 로직 추가 - -**현재 코드** (`QuoteService.php` Line 600-607): -```php -$serialIndex = 1; -foreach ($quote->items as $quoteItem) { - $orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex); - $orderItem->created_by = $userId; - $orderItem->save(); - $serialIndex++; -} -``` - -**수정 코드**: -```php -$calculationInputs = $quote->calculation_inputs ?? []; -$productItems = $calculationInputs['items'] ?? []; - -$serialIndex = 1; -foreach ($quote->items as $quoteItem) { - $productMapping = $this->resolveLocationMapping($quoteItem, $productItems); - $orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex, $productMapping); - $orderItem->created_by = $userId; - $orderItem->save(); - $serialIndex++; -} -``` - -#### 1.2 공통 메소드 추출 - -```php -/** - * 견적 품목에서 개소(층/부호) 정보 추출 - */ -private function resolveLocationMapping(QuoteItem $quoteItem, array $productItems): array -{ - $floorCode = null; - $symbolCode = null; - - // 1순위: note에서 파싱 ("4F FSS-01") - $note = trim($quoteItem->note ?? ''); - if ($note !== '') { - $parts = preg_split('/\s+/', $note, 2); - $floorCode = $parts[0] ?? null; - $symbolCode = $parts[1] ?? null; - } - - // 2순위: formula_source → calculation_inputs - if (empty($floorCode) && empty($symbolCode)) { - $productIndex = 0; - $formulaSource = $quoteItem->formula_source ?? ''; - if (preg_match('/product_(\d+)/', $formulaSource, $matches)) { - $productIndex = (int) $matches[1]; - } - if (isset($productItems[$productIndex])) { - $floorCode = $productItems[$productIndex]['floor'] ?? null; - $symbolCode = $productItems[$productIndex]['code'] ?? null; - } elseif (count($productItems) === 1) { - $floorCode = $productItems[0]['floor'] ?? null; - $symbolCode = $productItems[0]['code'] ?? null; - } - } - - return ['floor_code' => $floorCode, 'symbol_code' => $symbolCode]; -} -``` - ---- - -### 4.2 Phase 2: DB 스키마 - -#### 2.1 order_nodes 마이그레이션 - -```php -Schema::create('order_nodes', function (Blueprint $table) { - $table->id()->comment('ID'); - $table->foreignId('tenant_id')->comment('테넌트 ID'); - $table->foreignId('order_id')->comment('수주 ID'); - - // ---- 트리 구조 ---- - $table->foreignId('parent_id')->nullable()->comment('상위 노드 ID (NULL=루트)'); - - // ---- 고정 코어 (통계/집계용) ---- - $table->string('node_type', 50)->comment('노드 유형 (location, zone, floor, room, process...)'); - $table->string('code', 100)->comment('식별 코드'); - $table->string('name', 200)->comment('표시명'); - $table->string('status_code', 30)->default('PENDING') - ->comment('상태 (PENDING/CONFIRMED/IN_PRODUCTION/PRODUCED/SHIPPED/COMPLETED/CANCELLED)'); - $table->integer('quantity')->default(1)->comment('수량'); - $table->decimal('unit_price', 15, 2)->default(0)->comment('단가'); - $table->decimal('total_price', 15, 2)->default(0)->comment('합계'); - - // ---- 유연 확장 (유형별 상세) ---- - $table->json('options')->nullable()->comment('유형별 동적 속성 JSON'); - - // ---- 정렬 ---- - $table->integer('depth')->default(0)->comment('트리 깊이 (0=루트)'); - $table->integer('sort_order')->default(0)->comment('정렬 순서'); - - // ---- 감사 ---- - $table->foreignId('created_by')->nullable()->comment('생성자 ID'); - $table->foreignId('updated_by')->nullable()->comment('수정자 ID'); - $table->foreignId('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // ---- 인덱스 ---- - $table->index('tenant_id'); - $table->index('parent_id'); - $table->index(['order_id', 'depth', 'sort_order']); - $table->index(['order_id', 'node_type']); - $table->index(['tenant_id', 'node_type', 'status_code']); // 통계용 -}); -``` - -**통계 쿼리 예시**: -```sql --- 1. 노드 유형별 수주 현황 (고정 컬럼만으로 가능) -SELECT node_type, status_code, COUNT(*), SUM(total_price) -FROM order_nodes WHERE tenant_id = 287 -GROUP BY node_type, status_code; - --- 2. 경동 개소별 상세 (필요 시 JSON path) -SELECT code, name, total_price, - options->>'$.floor' AS floor, - options->>'$.symbol' AS symbol -FROM order_nodes -WHERE order_id = 123 AND node_type = 'location'; -``` - -#### 2.2 order_items에 order_node_id 추가 - -```php -Schema::table('order_items', function (Blueprint $table) { - $table->foreignId('order_node_id') - ->nullable() - ->after('order_id') - ->comment('수주 노드 ID (order_nodes)'); - $table->index('order_node_id'); -}); -``` - -#### 2.3 OrderNode 모델 - -```php -namespace App\Models\Orders; - -class OrderNode extends Model -{ - use Auditable, BelongsToTenant, SoftDeletes; - - protected $table = 'order_nodes'; - - // 상태 코드 (Order와 동일 체계) - public const STATUS_PENDING = 'PENDING'; - public const STATUS_CONFIRMED = 'CONFIRMED'; - public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION'; - public const STATUS_PRODUCED = 'PRODUCED'; - public const STATUS_SHIPPED = 'SHIPPED'; - public const STATUS_COMPLETED = 'COMPLETED'; - public const STATUS_CANCELLED = 'CANCELLED'; - - protected $fillable = [ - 'tenant_id', 'order_id', 'parent_id', - 'node_type', 'code', 'name', - 'status_code', 'quantity', 'unit_price', 'total_price', - 'options', 'depth', 'sort_order', - 'created_by', 'updated_by', 'deleted_by', - ]; - - protected $casts = [ - 'quantity' => 'integer', - 'unit_price' => 'decimal:2', - 'total_price' => 'decimal:2', - 'options' => 'array', - 'depth' => 'integer', - ]; - - // ---- 트리 관계 ---- - public function parent(): BelongsTo - { - return $this->belongsTo(self::class, 'parent_id'); - } - - public function children(): HasMany - { - return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); - } - - // ---- 비즈니스 관계 ---- - public function order(): BelongsTo - { - return $this->belongsTo(Order::class); - } - - public function items(): HasMany - { - return $this->hasMany(OrderItem::class, 'order_node_id'); - } - - // ---- 트리 헬퍼 ---- - public function isRoot(): bool - { - return $this->parent_id === null; - } - - public function isLeaf(): bool - { - return $this->children()->count() === 0; - } - - /** - * 하위 노드 포함 전체 트리 재귀 로드 - */ - public function scopeWithRecursiveChildren($query) - { - return $query->with(['children' => function ($q) { - $q->orderBy('sort_order')->with('children', 'items'); - }, 'items']); - } -} -``` - -#### 2.4-2.5 기존 모델 수정 - -**Order 모델**: -```php -public function nodes(): HasMany -{ - return $this->hasMany(OrderNode::class)->orderBy('depth')->orderBy('sort_order'); -} - -public function rootNodes(): HasMany -{ - return $this->hasMany(OrderNode::class)->whereNull('parent_id')->orderBy('sort_order'); -} -``` - -**OrderItem 모델** - fillable + 관계: -```php -// fillable에 추가 -'order_node_id', - -// 관계 -public function node(): BelongsTo -{ - return $this->belongsTo(OrderNode::class, 'order_node_id'); -} -``` - ---- - -### 4.3 Phase 3: 전환 로직 연동 - -#### 3.1 convertToOrder OrderNode 생성 - -**수정 위치**: `QuoteService::convertToOrder()` (Line 590~623) - -```php -return DB::transaction(function () use ($quote, $userId, $tenantId) { - $orderNo = $this->generateOrderNumber($tenantId); - $order = Order::createFromQuote($quote, $orderNo); - $order->created_by = $userId; - $order->save(); - - // ---- OrderNode 생성 (개소별) ---- - $calculationInputs = $quote->calculation_inputs ?? []; - $productItems = $calculationInputs['items'] ?? []; - $bomResults = $calculationInputs['bomResults'] ?? []; - - $nodeMap = []; // productIndex → OrderNode - foreach ($productItems as $idx => $locItem) { - $bomResult = $bomResults[$idx] ?? null; - $grandTotal = $bomResult['grand_total'] ?? 0; - $qty = (int) ($locItem['quantity'] ?? 1); - $floor = $locItem['floor'] ?? ''; - $symbol = $locItem['code'] ?? ''; - - $node = OrderNode::create([ - 'tenant_id' => $tenantId, - 'order_id' => $order->id, - 'parent_id' => null, // 루트 노드 (경동은 1-depth) - 'node_type' => 'location', - 'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}", - 'name' => trim("{$floor} {$symbol}") ?: "개소 ".($idx + 1), - 'status_code' => OrderNode::STATUS_PENDING, - 'quantity' => $qty, - 'unit_price' => $grandTotal, - 'total_price' => $grandTotal * $qty, - 'options' => [ - 'floor' => $floor, - 'symbol' => $symbol, - 'product_code' => $locItem['productCode'] ?? null, - 'product_name' => $locItem['productName'] ?? null, - 'open_width' => $locItem['openWidth'] ?? null, - 'open_height' => $locItem['openHeight'] ?? null, - 'guide_rail_type' => $locItem['guideRailType'] ?? null, - 'motor_power' => $locItem['motorPower'] ?? null, - 'controller' => $locItem['controller'] ?? null, - 'wing_size' => $locItem['wingSize'] ?? null, - 'inspection_fee' => $locItem['inspectionFee'] ?? null, - 'bom_result' => $bomResult, - ], - 'depth' => 0, - 'sort_order' => $idx, - 'created_by' => $userId, - ]); - $nodeMap[$idx] = $node; - } - - // ---- OrderItem 생성 (노드 연결) ---- - $serialIndex = 1; - foreach ($quote->items as $quoteItem) { - $mapping = $this->resolveLocationMapping($quoteItem, $productItems); - $locIdx = $this->resolveLocationIndex($quoteItem, $productItems); - - $productMapping = array_merge($mapping, [ - 'order_node_id' => isset($nodeMap[$locIdx]) ? $nodeMap[$locIdx]->id : null, - ]); - - $orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex, $productMapping); - $orderItem->created_by = $userId; - $orderItem->save(); - $serialIndex++; - } - - // 합계 재계산 + 견적 상태 변경 (기존 로직 유지) - $order->load('items'); - $order->recalculateTotals(); - $order->save(); - - $quote->update([ - 'status' => Quote::STATUS_CONVERTED, - 'order_id' => $order->id, - 'updated_by' => $userId, - ]); - - return $quote->refresh()->load(['items', 'client', 'order']); -}); -``` - -**resolveLocationIndex 헬퍼**: -```php -private function resolveLocationIndex(QuoteItem $quoteItem, array $productItems): int -{ - $formulaSource = $quoteItem->formula_source ?? ''; - if (preg_match('/product_(\d+)/', $formulaSource, $matches)) { - return (int) $matches[1]; - } - - $note = trim($quoteItem->note ?? ''); - if ($note !== '') { - $parts = preg_split('/\s+/', $note, 2); - $floor = $parts[0] ?? ''; - $code = $parts[1] ?? ''; - foreach ($productItems as $idx => $item) { - if (($item['floor'] ?? '') === $floor && ($item['code'] ?? '') === $code) { - return $idx; - } - } - } - - return 0; -} -``` - -#### 3.2 syncFromQuote OrderNode 동기화 - -**수정 위치**: `OrderService::syncFromQuote()` (Line 559~659) - -기존 `$order->items()->delete()` 다음에: -```php -// 기존 노드 삭제 후 재생성 -$order->nodes()->delete(); - -// OrderNode 생성 (convertToOrder와 동일 로직) -$nodeMap = []; -foreach ($productItems as $idx => $locItem) { - // ... (convertToOrder와 동일) - $nodeMap[$idx] = $node; -} - -// OrderItem 생성 시 order_node_id 연결 -foreach ($quote->items as $index => $quoteItem) { - $locIdx = $this->resolveLocationIndex($quoteItem, $productItems); - $order->items()->create([ - // ... 기존 필드 ... - 'order_node_id' => $nodeMap[$locIdx]->id ?? null, - ]); -} -``` - -#### 3.3 수주 상세 조회 nodes eager loading - -```php -$order = Order::where('tenant_id', $tenantId) - ->with([ - 'items', - 'rootNodes' => function ($q) { - $q->withRecursiveChildren(); // 재귀 트리 로드 - }, - 'client', - 'quote', - ]) - ->find($id); -``` - ---- - -### 4.4 Phase 4: 프론트엔드 노드별 UI - -#### 4.1 타입 + 서버 액션 - -**OrderNode 타입** (`react/src/components/orders/actions.ts`): -```typescript -export interface OrderNode { - id: number; - parentId: number | null; - nodeType: string; // 'location', 'zone', 'floor', 'room', 'process'... - code: string; - name: string; - statusCode: string; - quantity: number; - unitPrice: number; - totalPrice: number; - options: Record | null; // 유형별 동적 속성 - depth: number; - sortOrder: number; - children: OrderNode[]; // 하위 노드 (재귀) - items: OrderItem[]; // 해당 노드의 자재 -} - -export interface OrderDetail extends Order { - nodes: OrderNode[]; // 루트 노드 배열 (children 재귀 포함) -} -``` - -#### 4.2 수주 상세 뷰 노드별 그룹 UI - -**레이아웃 (경동 1-depth 예시)**: -``` -┌─ 수주 기본 정보 ────────────────────────────────────────┐ -│ 수주번호: ORD-260206-001 | 현장명: 삼성 빌딩 신축 │ -│ 거래처: 삼성물산 | 총금액: 15,000,000원 │ -└─────────────────────────────────────────────────────────┘ - -┌─ 구조 (3개 노드) ──────────────────────────────────────┐ -│ │ -│ ┌─ [location] 1F FSS-01 ──────────────────────────┐ │ -│ │ KSS01(고정스크린) | 5000×3000 | 수량:1 │ │ -│ │ 상태: [PENDING ▾] | 금액: 1,250,000원 │ │ -│ ├──────────────────────────────────────────────────┤ │ -│ │ # | 품목코드 | 품명 | 수량 | 단가 | 금액 │ │ -│ │ 1 | MT-SUS-01 | 슬랫 | 15.5 | 12,000 | 186K │ │ -│ │ 소계: 1,250,000원 │ │ -│ └──────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ [location] 2F SD-02 ──────────────────────────┐ │ -│ │ ... │ │ -│ └─────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - -**재귀 컴포넌트 (N-depth)**: -```typescript -function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number }) { - return ( -
- {/* 노드 헤더 */} - - - {/* 해당 노드의 자재 테이블 */} - {node.items.length > 0 && } - - {/* 하위 노드 재귀 렌더링 */} - {node.children.map(child => ( - - ))} -
- ); -} -``` - -**역호환**: -```typescript -{order.nodes && order.nodes.length > 0 ? ( - order.nodes.map(node => ) -) : ( - -)} -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | order_nodes 테이블 생성 | N-depth 자기참조 트리 + 하이브리드 JSON | DB 스키마 | ⚠️ 컨펌 필요 | -| 2 | order_items에 order_node_id 추가 | nullable FK 컬럼 | DB 스키마 | ⚠️ 컨펌 필요 | -| 3 | 노드 상태 코드 체계 | Order와 동일 체계 사용 | 비즈니스 로직 | ⚠️ 컨펌 필요 | -| 4 | 경동 node_type 값 | "location" 사용 | 비즈니스 로직 | ⚠️ 컨펌 필요 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-06 | - | 문서 초안 작성 (order_locations 전용 설계) | - | - | -| 2026-02-06 | 아키텍처 변경 | order_locations → order_nodes (N-depth 트리 + 하이브리드) | - | ✅ 사용자 승인 | - ---- - -## 7. 참고 문서 - -- **견적 시스템 분석**: `docs/features/quotes/README.md` -- **DB 스키마 규칙**: `docs/specs/database-schema.md` -- **API 개발 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - -### 핵심 소스 파일 - -| 파일 | 역할 | 핵심 라인 | -|------|------|----------| -| `api/app/Services/Quote/QuoteService.php` | 견적→수주 전환 | L574-623 (`convertToOrder`) | -| `api/app/Services/OrderService.php` | 수주 동기화 | L559-659 (`syncFromQuote`) | -| `api/app/Models/Orders/Order.php` | 수주 모델 | L23-59 (상태 코드) | -| `api/app/Models/Orders/OrderItem.php` | 수주 품목 모델 | L162-190 (`createFromQuoteItem`) | -| `react/src/components/orders/actions.ts` | 수주 프론트 타입 | L281-300 (OrderItem) | -| `react/src/components/orders/OrderSalesDetailView.tsx` | 수주 상세 뷰 | L386-424 (테이블) | -| `react/src/components/quotes/types.ts` | 견적 타입 | L661-684 (LocationItem) | - ---- - -## 8. 세션 및 메모리 관리 정책 - -### 8.1 세션 시작 시 - -``` -1. read_memory("order-nodes-state") → 진행 상태 파악 -2. 이 문서의 "📍 현재 진행 상태" 섹션 확인 -3. 마지막 완료 작업 확인 후 다음 작업 착수 -``` - -### 8.2 Serena 메모리 구조 - -- `order-nodes-state`: `{ phase, progress, next_step, last_decision }` -- `order-nodes-snapshot`: 현재까지의 코드 변경점 요약 -- `order-nodes-active-symbols`: 수정 중인 파일/함수 목록 - ---- - -## 9. 검증 결과 - -### 9.1 테스트 케이스 - -| # | 시나리오 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|----------|----------|------| -| 1 | 견적 3개소 → 수주 전환 | order_nodes 3행(type:location) 생성, 각 order_items에 node_id 연결 | - | ⏳ | -| 2 | 수주 상세 조회 | rootNodes + children 재귀 + items eager loading 정상 | - | ⏳ | -| 3 | 견적 수정 → 수주 동기화 | 기존 nodes 삭제 후 재생성, items 재연결 | - | ⏳ | -| 4 | 기존 수주 (nodes 없음) 조회 | 기존 플랫 테이블 정상 표시, 에러 없음 | - | ⏳ | -| 5 | 프론트 노드별 그룹 표시 | 노드 카드 내 자재 테이블, 역호환 플랫 뷰 | - | ⏳ | -| 6 | 통계 쿼리 성능 | 고정 컬럼(node_type, status, total_price) 기반 GROUP BY 정상 | - | ⏳ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 전환 시 order_nodes 생성됨 | ⏳ | Phase 2+3 | -| N-depth 트리 구조 지원 | ⏳ | Phase 2 (parent_id 자기참조) | -| order_items에 order_node_id 연결됨 | ⏳ | Phase 3 | -| 프론트 노드별 그룹 표시 | ⏳ | Phase 4 | -| 기존 수주 역호환 정상 | ⏳ | Phase 4 | -| 통계 쿼리가 고정 컬럼으로 가능 | ⏳ | Phase 2 (인덱스) | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 범용 N-depth 트리 + 통계 친화 하이브리드 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 (6개 기준) | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 (13개 작업 항목) | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 1→2→3→4 순서 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7 (라인번호 포함) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3+4 (코드 포함) | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 (6개 테스트 케이스) | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/파일/라인 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 1.2 목표 | -| Q2. 왜 하이브리드 구조를 선택했는가? | ✅ | 1.3 아키텍처 결정 | -| Q3. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 | -| Q4. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 | -| Q5. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /plan 스킬 + Sequential Thinking MCP로 생성되었습니다.* -*아키텍처: N-depth 트리(order_nodes) + 하이브리드(고정 코어 + options JSON)* \ No newline at end of file diff --git a/plans/archive/order-management-plan.md b/plans/archive/order-management-plan.md deleted file mode 100644 index ecb5f87..0000000 --- a/plans/archive/order-management-plan.md +++ /dev/null @@ -1,335 +0,0 @@ -# 수주관리 (Order Management) API 연동 계획 - -> **작성일**: 2025-01-08 -> **목적**: 수주관리 페이지 Mock 데이터 → API 연동 -> **상태**: ✅ Phase 3 완료 (100% 완료) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 버그 수정 - 목록 페이지 서버 에러 해결 (3건) | -| **다음 작업** | 완료 | -| **진행률** | 3/3 Phase (100%) + 버그 수정 완료 | -| **마지막 업데이트** | 2025-01-09 | -| **커밋** | 버그 수정 커밋 완료 | - ---- - -## 1. 개요 - -### 1.1 배경 -수주관리 페이지는 프론트엔드 UI가 구현되어 있으나, **하드코딩된 Mock 데이터(SAMPLE_ORDERS)**를 사용 중입니다. -실제 비즈니스 운영을 위해 API 연동이 필요합니다. - -### 1.2 현재 구현 상태 분석 - -#### API (Laravel) - ✅ Phase 1 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|------| -| Model | `api/app/Models/Orders/Order.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderItem.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderHistory.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderVersion.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderItemComponent.php` | ✅ 존재 | -| Controller | `api/app/Http/Controllers/Api/V1/OrderController.php` | ✅ **완료** | -| Service | `api/app/Services/OrderService.php` | ✅ **완료** | -| FormRequest | `api/app/Http/Requests/Order/*.php` | ✅ **완료** (3개) | -| Route | `/api/v1/orders` | ✅ **완료** (7개 엔드포인트) | -| Swagger | `api/app/Swagger/v1/OrderApi.php` | ✅ **완료** | - -#### Frontend (React/Next.js) - ✅ Phase 2 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|------| -| 목록 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx` | ✅ API 연동 | -| 등록 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx` | ✅ API 연동 | -| 상세 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx` | ✅ API 연동 | -| 수정 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` | ✅ API 연동 | -| 생산지시 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` | ✅ 완료 | -| 등록 컴포넌트 | `react/src/components/orders/OrderRegistration.tsx` | ✅ 완료 | -| 견적선택 다이얼로그 | `react/src/components/orders/QuotationSelectDialog.tsx` | ✅ 완료 | -| 품목추가 다이얼로그 | `react/src/components/orders/ItemAddDialog.tsx` | ✅ 완료 | -| **actions.ts** | `react/src/components/orders/actions.ts` | ✅ **완료** | - -### 1.3 연관관계 -``` -┌─────────────────┐ ┌─────────────────┐ -│ Quote │────── quote_id ────▶│ Order │ -│ (견적서) │ │ (수주) │ -└─────────────────┘ └─────────────────┘ - │ - │ sales_order_id - ▼ - ┌─────────────────┐ - │ WorkOrder │ - │ (작업지시) │ - └─────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 필드 추가/변경, API 엔드포인트 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 테이블 구조 변경, 기존 API 수정 | **필수** | -| 🔴 금지 | 기존 Order 모델 구조 변경 | 별도 협의 | - ---- - -## 2. 대상 범위 - -### Phase 1: API 개발 (✅ 완료) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | OrderController 생성 | ✅ | CRUD + 상태관리 (7개 메서드) | -| 1.2 | OrderService 생성 | ✅ | 비즈니스 로직 (index, stats, show, store, update, destroy, updateStatus) | -| 1.3 | FormRequest 생성 | ✅ | Store, Update, UpdateStatus (3개) | -| 1.4 | API 라우트 등록 | ✅ | routes/api.php (7개 엔드포인트) | -| 1.5 | Swagger 문서 작성 | ✅ | OrderApi.php (스키마 8개) | - -### Phase 2: Frontend 연동 (✅ 완료) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | actions.ts 생성 | ✅ | API 호출 함수 + 타입 정의 + 변환 함수 | -| 2.2 | 목록 페이지 연동 | ✅ | getOrders(), getOrderStats() 연동 | -| 2.3 | 상세 페이지 연동 | ✅ | getOrderById() 연동 + 타입 오류 수정 | -| 2.4 | 등록 페이지 연동 | ✅ | createOrder() 연동 | -| 2.5 | 수정 페이지 연동 | ✅ | updateOrder() 연동 + 타입 오류 수정 | - -### Phase 3: 고급 기능 (✅ 완료) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 견적서 → 수주 변환 | ✅ | QuotationSelectDialog + createOrderFromQuote() | -| 3.2 | 생산지시 생성 연동 | ✅ | createProductionOrder() + production-order 페이지 | -| 3.3 | 상태 흐름 관리 | ✅ | 수주확정 다이얼로그 + updateOrderStatus() | - ---- - -## 3. API 엔드포인트 설계 - -### 3.1 REST API - -| Method | Endpoint | 설명 | 우선순위 | -|--------|----------|------|:--------:| -| GET | `/api/v1/orders` | 수주 목록 조회 (페이징/필터) | 🔴 | -| GET | `/api/v1/orders/stats` | 수주 통계 | 🔴 | -| GET | `/api/v1/orders/{id}` | 수주 상세 조회 | 🔴 | -| POST | `/api/v1/orders` | 수주 생성 | 🔴 | -| PUT | `/api/v1/orders/{id}` | 수주 수정 | 🟡 | -| DELETE | `/api/v1/orders/{id}` | 수주 삭제 | 🟡 | -| PATCH | `/api/v1/orders/{id}/status` | 상태 변경 | 🟡 | -| POST | `/api/v1/orders/{id}/production-order` | 생산지시 생성 | 🟢 | -| POST | `/api/v1/orders/from-quote/{quoteId}` | 견적→수주 변환 | 🟢 | - -### 3.2 데이터 스키마 - -#### Order (수주) - 기존 모델 기반 -```typescript -interface Order { - id: number; - tenantId: number; - quoteId?: number; // 원본 견적 - orderNo: string; // 수주번호 (KD-TS-YYMMDD-NN) - orderTypeCode: 'ORDER' | 'PURCHASE'; - statusCode: 'DRAFT' | 'CONFIRMED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'; - clientId?: number; - clientName?: string; - siteName?: string; // 현장명 - quantity: number; - supplyAmount: number; - taxAmount: number; - totalAmount: number; - deliveryDate?: Date; - deliveryMethodCode?: string; - memo?: string; - createdBy?: number; - updatedBy?: number; - createdAt: Date; - updatedAt: Date; - // Relations - items?: OrderItem[]; - client?: Client; -} -``` - -#### OrderItem (수주 품목) -```typescript -interface OrderItem { - id: number; - orderId: number; - itemId?: number; - itemName: string; - specification?: string; - quantity: number; - unit?: string; - unitPrice: number; - supplyAmount: number; - taxAmount: number; - totalAmount: number; - sortOrder: number; -} -``` - ---- - -## 4. 작업 절차 - -### Step 1: API 개발 (Backend) - -``` -1. OrderService 생성 - ├── index(): 목록 조회 (페이징, 필터링) - ├── show(): 상세 조회 - ├── store(): 생성 - ├── update(): 수정 - ├── destroy(): 삭제 - ├── updateStatus(): 상태 변경 - ├── stats(): 통계 조회 - └── createFromQuote(): 견적→수주 변환 - -2. OrderController 생성 - ├── FormRequest DI - └── ApiResponse::handle() 사용 - -3. FormRequest 생성 - ├── StoreOrderRequest - └── UpdateOrderRequest - -4. 라우트 등록 - └── Route::prefix('orders')->group(...) - -5. Swagger 문서 작성 - └── app/Swagger/v1/OrderApi.php -``` - -### Step 2: Frontend 연동 - -``` -1. actions.ts 생성 - ├── getOrders(): 목록 조회 - ├── getOrderById(): 상세 조회 - ├── createOrder(): 생성 - ├── updateOrder(): 수정 - ├── deleteOrder(): 삭제 - ├── updateOrderStatus(): 상태 변경 - └── getOrderStats(): 통계 조회 - -2. 페이지별 연동 - ├── page.tsx: SAMPLE_ORDERS → getOrders() - ├── [id]/page.tsx: Mock → getOrderById() - ├── new/page.tsx: Mock → createOrder() - └── [id]/edit/page.tsx: Mock → updateOrder() -``` - ---- - -## 5. 의존성 - -### 5.1 필수 선행 작업 -- **없음** - Order 모델 이미 존재, 바로 작업 가능 - -### 5.2 연관 기능 (선택적) -- **견적관리 (Quote)**: 견적→수주 변환 시 필요 -- **거래처관리 (Client)**: 거래처 연동 -- **품목관리 (Item)**: 품목 마스터 연동 - -### 5.3 후속 연동 -- **작업지시 (WorkOrder)**: 생산지시 생성 시 `sales_order_id` 연결 -- **출하관리**: 수주 완료 후 출하 처리 - ---- - -## 6. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **Swagger 가이드**: `docs/guides/swagger-guide.md` - -### 참고 코드 -- **작업지시 API (참고용)**: `api/app/Http/Controllers/Api/V1/WorkOrderController.php` -- **공정관리 actions.ts (참고용)**: `react/src/components/process-management/actions.ts` - ---- - -## 7. 검증 방법 - -### 7.1 API 테스트 -```bash -# 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/orders" -H "X-Api-Key: ..." - -# 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/orders/1" -H "X-Api-Key: ..." - -# 통계 조회 -curl -X GET "http://api.sam.kr/api/v1/orders/stats" -H "X-Api-Key: ..." -``` - -### 7.2 성공 기준 -| 기준 | 측정 방법 | -|------|----------| -| API CRUD 동작 | Swagger UI 테스트 통과 | -| 목록 페이지 | 실제 데이터 표시 | -| 상세 페이지 | 수주 정보 정상 표시 | -| 등록/수정 | 데이터 저장 및 조회 | -| 상태 변경 | DRAFT → CONFIRMED 전환 | - ---- - -## 8. 자기완결성 점검 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | Mock → API 연동 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 7 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3 단계별 정의 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 선행 작업 없음 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 경로 검증됨 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | Step 1-2 상세 정의 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | curl 테스트 + 기준 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/엔드포인트 명시 | - ---- - -## 9. 버그 수정 이력 - -### 2025-01-09: 목록 페이지 서버 에러 수정 - -| # | 파일 | 문제 | 수정 내용 | -|---|------|------|----------| -| 1 | `react/.../page.tsx:120` | API 응답 데이터 구조 불일치 | `ordersResult.data` → `ordersResult.data.items` | -| 2 | `api/.../OrderService.php:113` | Quote 필드명 오류 | `quote:id,quote_no,site_name` → `quote:id,quote_number,site_name` | -| 3 | `react/.../actions.ts:384` | Quote 필드명 오류 | `apiData.quote?.quote_no` → `apiData.quote?.quote_number` | - -**원인 분석:** -- `getOrders()` 함수는 `{ items: Order[], total, page, totalPages }` 구조를 반환하나, 페이지에서 `ordersResult.data`를 직접 사용하여 타입 불일치 발생 -- Quote 모델의 필드명이 `quote_number`인데 `quote_no`로 잘못 참조 - -**영향 범위:** -- 수주 목록 페이지 접근 시 서버 에러 발생 -- 견적 연동 수주의 견적번호 표시 오류 - -### 2025-01-09: 수주 등록 페이지 거래처 API 연동 - -| # | 파일 | 변경 내용 | -|---|------|----------| -| 1 | `react/.../OrderRegistration.tsx` | `SAMPLE_CLIENTS` 하드코딩 제거 | -| 2 | `react/.../OrderRegistration.tsx` | `useClientList` 훅으로 실제 API 연동 | -| 3 | `react/.../OrderRegistration.tsx` | 로딩 상태 처리 ("불러오는 중...") | -| 4 | `react/.../OrderRegistration.tsx` | 견적 선택 시 발주처 필드 비활성화 | - -**개선 내용:** -- 발주처(거래처) 드롭다운이 `/api/proxy/clients` API에서 실제 데이터 조회 -- 견적 선택 시 발주처가 자동 설정되고 필드 비활성화 -- 로딩 중 "불러오는 중..." 플레이스홀더 표시 - ---- - -*이 문서는 독립 세션에서 바로 작업 시작 가능하도록 설계되었습니다.* \ No newline at end of file diff --git a/plans/archive/order-workorder-shipment-integration-plan.md b/plans/archive/order-workorder-shipment-integration-plan.md deleted file mode 100644 index 105c5c3..0000000 --- a/plans/archive/order-workorder-shipment-integration-plan.md +++ /dev/null @@ -1,659 +0,0 @@ -# 수주-작업지시-출하 하이브리드 연동 구조 구현 계획 - -> **작성일**: 2025-01-19 -> **목적**: Order → WorkOrder → Shipment 간 FK 연결 강화 및 상태 동기화 로직 구현 -> **기준 문서**: `api/app/Models/Orders/Order.php`, `api/app/Models/Production/WorkOrder.php`, `api/app/Models/Tenants/Shipment.php` -> **상태**: 📋 계획 수립 완료 (Serena ID: order-integration-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4: 작업완료 시 자동 출하 생성 기능 구현 | -| **다음 작업** | ✅ 모든 Phase 완료 | -| **진행률** | 4/4 Phase (100%) | -| **마지막 업데이트** | 2025-01-19 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM 시스템은 수주(Order), 작업지시(WorkOrder), 출하(Shipment)가 독립적으로 운영되고 있습니다. - -**현재 문제점:** -- `shipments` 테이블에 `work_order_id` FK가 없음 -- 작업 완료 시 출하로 자동 연결되지 않음 -- Order의 전체 진행 상태를 추적할 수 없음 -- 데이터 정합성 보장이 어려움 - -**목표:** -- 하이브리드 마스터-디테일 구조로 전환 -- `orders.status_code`로 전체 진행 상태 추적 -- 각 단계별 상태 변경 시 연관 테이블 자동 동기화 - -### 1.2 목표 구조 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 목표 구조 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ orders (마스터) │ -│ ├─ status_code: 전체 진행상태 추적 │ -│ │ DRAFT → CONFIRMED → IN_PRODUCTION → PRODUCED │ -│ │ → SHIPPING → SHIPPED → COMPLETED │ -│ │ │ -│ ├──(1:N)──▶ work_orders (생산 상세) │ -│ │ ├─ sales_order_id FK ✅ (기존) │ -│ │ └─ status: 생산 프로세스 상태 │ -│ │ │ -│ └──(1:N)──▶ shipments (출하 상세) │ -│ ├─ order_id FK ✅ (기존) │ -│ ├─ work_order_id FK 🆕 (신규 추가) │ -│ └─ status: 출하 프로세스 상태 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. orders.status_code = 전체 프로세스의 Single Source of Truth │ -│ 2. 하위 테이블(work_orders, shipments)은 상세 정보만 관리 │ -│ 3. 상태 변경 시 상위 테이블 자동 동기화 │ -│ 4. 기존 데이터 호환성 유지 (work_order_id는 nullable) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 모델 관계 추가, 상수 추가, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션, 서비스 로직 변경, 상태 동기화 | **필수** | -| 🔴 금지 | 기존 테이블 구조 파괴적 변경, 기존 API 삭제 | 별도 협의 | - -### 1.5 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `CLAUDE.md` - SAM API Development Rules - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: DB 스키마 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | `shipments` 테이블에 `work_order_id` FK 추가 마이그레이션 | ⏳ | nullable, index 포함 | - -### 2.2 Phase 2: 모델 관계 추가 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | Order 모델에 `shipments()` HasMany 관계 추가 | ⏳ | | -| 2.2 | WorkOrder 모델에 `shipments()` HasMany 관계 추가 | ⏳ | | -| 2.3 | Shipment 모델에 `workOrder()` BelongsTo 관계 추가 | ⏳ | | -| 2.4 | Shipment 모델에 `work_order_id` fillable 추가 | ⏳ | | - -### 2.3 Phase 3: Order 상태 확장 및 동기화 로직 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | Order 모델에 생산/출하 관련 상태 상수 추가 | ⏳ | IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED | -| 3.2 | WorkOrderService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 | -| 3.3 | ShipmentService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 | - -### 2.4 Phase 4: 연동 기능 (선택) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | ShipmentService.store()에 work_order_id 연결 로직 추가 | ⏳ | 출하 생성 시 WorkOrder 선택 가능 | -| 4.2 | WorkOrder 완료 시 Shipment 자동 생성 옵션 | ⏳ | 선택적 기능 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Phase 1: DB 스키마 수정 -└── 1.1 마이그레이션 생성 및 실행 - ├── add_work_order_id_to_shipments_table.php - ├── work_order_id FK (nullable) - └── index 추가 - -Phase 2: 모델 관계 추가 -├── 2.1 Order.php - shipments() HasMany -├── 2.2 WorkOrder.php - shipments() HasMany -├── 2.3 Shipment.php - workOrder() BelongsTo -└── 2.4 Shipment.php - fillable에 work_order_id 추가 - -Phase 3: 상태 동기화 -├── 3.1 Order.php - 상태 상수 확장 -│ ├── STATUS_IN_PRODUCTION = 'IN_PRODUCTION' -│ ├── STATUS_PRODUCED = 'PRODUCED' -│ ├── STATUS_SHIPPING = 'SHIPPING' -│ └── STATUS_SHIPPED = 'SHIPPED' -├── 3.2 WorkOrderService.php - syncOrderStatus() 메서드 추가 -│ ├── in_progress → Order: IN_PRODUCTION -│ ├── completed → Order: PRODUCED -│ └── shipped → Order: (Shipment 생성 시) -└── 3.3 ShipmentService.php - syncOrderStatus() 메서드 추가 - ├── scheduled/ready → Order: SHIPPING (첫 출하 생성 시) - └── completed → Order: SHIPPED (모든 출하 완료 시) - -Phase 4: 연동 기능 (선택) -├── 4.1 ShipmentService.store() - work_order_id 파라미터 추가 -└── 4.2 WorkOrderService.updateStatus() - 자동 Shipment 생성 옵션 -``` - -### 3.2 상태 흐름도 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 전체 상태 흐름 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ [Order] │ -│ DRAFT ──▶ CONFIRMED ──▶ IN_PRODUCTION ──▶ PRODUCED │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ WorkOrder WorkOrder WorkOrder │ -│ 생성 in_progress completed │ -│ │ │ -│ ▼ │ -│ ──────────────────────▶ SHIPPING ──▶ SHIPPED ──▶ COMPLETED │ -│ │ │ │ -│ ▼ ▼ │ -│ Shipment Shipment │ -│ 생성 completed │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: DB 스키마 수정 - -#### 1.1 마이그레이션: shipments 테이블에 work_order_id 추가 - -**파일**: `api/database/migrations/2025_01_19_XXXXXX_add_work_order_id_to_shipments_table.php` - -```php -foreignId('work_order_id') - ->nullable() - ->after('order_id') - ->comment('작업지시 ID'); - - $table->index(['tenant_id', 'work_order_id']); - }); - } - - public function down(): void - { - Schema::table('shipments', function (Blueprint $table) { - $table->dropIndex(['tenant_id', 'work_order_id']); - $table->dropColumn('work_order_id'); - }); - } -}; -``` - ---- - -### 4.2 Phase 2: 모델 관계 추가 - -#### 2.1 Order 모델 - shipments() 관계 - -**파일**: `api/app/Models/Orders/Order.php` - -```php -use App\Models\Tenants\Shipment; - -/** - * 출하 목록 - */ -public function shipments(): HasMany -{ - return $this->hasMany(Shipment::class, 'order_id'); -} -``` - -#### 2.2 WorkOrder 모델 - shipments() 관계 - -**파일**: `api/app/Models/Production/WorkOrder.php` - -```php -use App\Models\Tenants\Shipment; - -/** - * 출하 목록 - */ -public function shipments(): HasMany -{ - return $this->hasMany(Shipment::class); -} -``` - -#### 2.3-2.4 Shipment 모델 수정 - -**파일**: `api/app/Models/Tenants/Shipment.php` - -```php -use App\Models\Production\WorkOrder; - -// fillable에 추가 -protected $fillable = [ - // ... 기존 필드들 - 'work_order_id', // 추가 -]; - -// casts에 추가 -protected $casts = [ - // ... 기존 캐스트들 - 'work_order_id' => 'integer', // 추가 -]; - -/** - * 작업지시 관계 - */ -public function workOrder(): BelongsTo -{ - return $this->belongsTo(WorkOrder::class); -} -``` - ---- - -### 4.3 Phase 3: Order 상태 확장 및 동기화 로직 - -#### 3.1 Order 모델 - 상태 상수 확장 - -**파일**: `api/app/Models/Orders/Order.php` - -```php -// 기존 상태 -public const STATUS_DRAFT = 'DRAFT'; -public const STATUS_CONFIRMED = 'CONFIRMED'; -public const STATUS_IN_PROGRESS = 'IN_PROGRESS'; -public const STATUS_COMPLETED = 'COMPLETED'; -public const STATUS_CANCELLED = 'CANCELLED'; - -// 신규 상태 추가 -public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION'; // 생산중 -public const STATUS_PRODUCED = 'PRODUCED'; // 생산완료 -public const STATUS_SHIPPING = 'SHIPPING'; // 출하중 -public const STATUS_SHIPPED = 'SHIPPED'; // 출하완료 - -/** - * 전체 상태 목록 - */ -public const STATUSES = [ - self::STATUS_DRAFT, - self::STATUS_CONFIRMED, - self::STATUS_IN_PRODUCTION, - self::STATUS_PRODUCED, - self::STATUS_SHIPPING, - self::STATUS_SHIPPED, - self::STATUS_COMPLETED, - self::STATUS_CANCELLED, -]; - -/** - * 상태 라벨 - */ -public const STATUS_LABELS = [ - self::STATUS_DRAFT => '임시저장', - self::STATUS_CONFIRMED => '확정', - self::STATUS_IN_PRODUCTION => '생산중', - self::STATUS_PRODUCED => '생산완료', - self::STATUS_SHIPPING => '출하중', - self::STATUS_SHIPPED => '출하완료', - self::STATUS_COMPLETED => '완료', - self::STATUS_CANCELLED => '취소', -]; -``` - -#### 3.2 WorkOrderService - Order 상태 동기화 - -**파일**: `api/app/Services/WorkOrderService.php` - -```php -use App\Models\Orders\Order; - -/** - * Order 상태 동기화 - * WorkOrder 상태 변경 시 Order.status_code 업데이트 - */ -private function syncOrderStatus(WorkOrder $workOrder): void -{ - if (!$workOrder->sales_order_id) { - return; - } - - $order = Order::find($workOrder->sales_order_id); - if (!$order) { - return; - } - - $newStatus = null; - - switch ($workOrder->status) { - case WorkOrder::STATUS_IN_PROGRESS: - case WorkOrder::STATUS_WAITING: - case WorkOrder::STATUS_PENDING: - // 하나라도 진행중이면 생산중 - $newStatus = Order::STATUS_IN_PRODUCTION; - break; - - case WorkOrder::STATUS_COMPLETED: - // 모든 작업지시가 완료되었는지 확인 - $allCompleted = WorkOrder::where('sales_order_id', $order->id) - ->whereNotIn('status', [WorkOrder::STATUS_COMPLETED, WorkOrder::STATUS_SHIPPED]) - ->doesntExist(); - - if ($allCompleted) { - $newStatus = Order::STATUS_PRODUCED; - } - break; - } - - if ($newStatus && $order->status_code !== $newStatus) { - $order->update(['status_code' => $newStatus]); - - $this->auditLogger->log( - $order->tenant_id, - 'order', - $order->id, - 'status_synced_from_work_order', - ['status_code' => $order->getOriginal('status_code')], - ['status_code' => $newStatus, 'work_order_id' => $workOrder->id] - ); - } -} -``` - -**updateStatus() 메서드에 호출 추가:** - -```php -public function updateStatus(int $id, string $status, ?array $resultData = null) -{ - // ... 기존 로직 ... - - return DB::transaction(function () use ($workOrder, $status, $resultData, $tenantId, $userId) { - // ... 기존 상태 변경 로직 ... - - $workOrder->save(); - - // Order 상태 동기화 추가 - $this->syncOrderStatus($workOrder); - - // ... 나머지 로직 ... - }); -} -``` - -#### 3.3 ShipmentService - Order 상태 동기화 - -**파일**: `api/app/Services/ShipmentService.php` - -```php -use App\Models\Orders\Order; - -/** - * Order 상태 동기화 - * Shipment 상태 변경 시 Order.status_code 업데이트 - */ -private function syncOrderStatus(Shipment $shipment): void -{ - if (!$shipment->order_id) { - return; - } - - $order = Order::find($shipment->order_id); - if (!$order) { - return; - } - - $newStatus = null; - - switch ($shipment->status) { - case 'scheduled': - case 'ready': - case 'shipping': - // 출하 프로세스 시작 - if (!in_array($order->status_code, [Order::STATUS_SHIPPING, Order::STATUS_SHIPPED, Order::STATUS_COMPLETED])) { - $newStatus = Order::STATUS_SHIPPING; - } - break; - - case 'completed': - // 모든 출하가 완료되었는지 확인 - $allCompleted = Shipment::where('order_id', $order->id) - ->where('status', '!=', 'completed') - ->doesntExist(); - - if ($allCompleted) { - $newStatus = Order::STATUS_SHIPPED; - } - break; - } - - if ($newStatus && $order->status_code !== $newStatus) { - $order->update(['status_code' => $newStatus]); - } -} -``` - -**store() 및 updateStatus() 메서드에 호출 추가:** - -```php -public function store(array $data): Shipment -{ - // ... 기존 로직 ... - - return DB::transaction(function () use ($data, $tenantId, $userId) { - // ... 기존 생성 로직 ... - - // Order 상태 동기화 추가 - $this->syncOrderStatus($shipment); - - return $shipment->load('items'); - }); -} - -public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment -{ - // ... 기존 로직 ... - - $shipment->update($updateData); - - // Order 상태 동기화 추가 - $this->syncOrderStatus($shipment); - - return $shipment->load('items'); -} -``` - ---- - -### 4.4 Phase 4: 연동 기능 (선택) - -#### 4.1 ShipmentService.store() - work_order_id 연결 - -**파일**: `api/app/Services/ShipmentService.php` - -```php -public function store(array $data): Shipment -{ - return DB::transaction(function () use ($data, $tenantId, $userId) { - $shipment = Shipment::create([ - // ... 기존 필드들 ... - 'work_order_id' => $data['work_order_id'] ?? null, // 추가 - ]); - - // WorkOrder가 있으면 상태를 shipped로 변경 - if ($shipment->work_order_id) { - $workOrder = WorkOrder::find($shipment->work_order_id); - if ($workOrder && $workOrder->status === WorkOrder::STATUS_COMPLETED) { - $workOrder->update([ - 'status' => WorkOrder::STATUS_SHIPPED, - 'shipped_at' => now(), - ]); - } - } - - // ... 나머지 로직 ... - }); -} -``` - -#### 4.2 ShipmentStoreRequest - work_order_id 검증 - -**파일**: `api/app/Http/Requests/Shipment/ShipmentStoreRequest.php` - -```php -public function rules(): array -{ - return [ - // ... 기존 규칙들 ... - 'work_order_id' => ['nullable', 'integer', 'exists:work_orders,id'], - ]; -} -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 마이그레이션 | shipments에 work_order_id FK 추가 | DB | ⏳ 컨펌 필요 | -| 2 | Order 상태 확장 | 4개 상태 추가 (IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED) | Order 모델, API | ⏳ 컨펌 필요 | -| 3 | 상태 동기화 로직 | WorkOrder/Shipment 상태 변경 시 Order 자동 업데이트 | 서비스 로직 | ⏳ 컨펌 필요 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-19 | - | 계획 문서 초안 작성 | - | - | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **SAM API 규칙**: `CLAUDE.md` -- **DB 스키마**: `docs/specs/database-schema.md` - -### 분석된 기존 파일 - -| 파일 | 역할 | -|------|------| -| `api/app/Models/Orders/Order.php` | 수주 마스터 모델 | -| `api/app/Models/Production/WorkOrder.php` | 작업지시 모델 | -| `api/app/Models/Tenants/Shipment.php` | 출하 모델 | -| `api/app/Services/WorkOrderService.php` | 작업지시 비즈니스 로직 | -| `api/app/Services/ShipmentService.php` | 출하 비즈니스 로직 | -| `api/database/migrations/2025_12_26_100000_create_work_orders_table.php` | 작업지시 테이블 | -| `api/database/migrations/2025_12_26_150604_create_shipments_table.php` | 출하 테이블 | - ---- - -## 8. 세션 및 메모리 관리 정책 - -### 8.1 세션 시작 시 -```javascript -read_memory("order-integration-state") // 상태 파악 -read_memory("order-integration-snapshot") // 사고 흐름 복구 -``` - -### 8.2 Serena 메모리 구조 -- `order-integration-state`: { phase, progress, next_step, last_decision } -- `order-integration-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `order-integration-rules`: 해당 작업에서 결정된 규칙들 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 시나리오 | 예상 결과 | 실제 결과 | 상태 | -|----------|----------|----------|------| -| WorkOrder 생성 (in_progress) | Order.status = IN_PRODUCTION | - | ⏳ | -| WorkOrder 완료 (completed) | Order.status = PRODUCED | - | ⏳ | -| Shipment 생성 | Order.status = SHIPPING | - | ⏳ | -| Shipment 완료 | Order.status = SHIPPED | - | ⏳ | -| 모든 프로세스 완료 | Order.status = COMPLETED | - | ⏳ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| shipments.work_order_id FK 추가 완료 | ⏳ | | -| 모델 관계 정상 동작 | ⏳ | | -| Order 상태 자동 동기화 | ⏳ | | -| 기존 데이터 호환성 유지 | ⏳ | | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase별 순서 정의 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3, 4 상세 코드 포함 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드 예시 포함 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태, 3.1 절차 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 섹션 4 상세 작업 내용 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/process-management-plan.md b/plans/archive/process-management-plan.md deleted file mode 100644 index 5c8d7d3..0000000 --- a/plans/archive/process-management-plan.md +++ /dev/null @@ -1,397 +0,0 @@ -# 공정관리 (Process Management) API 연동 계획 - -> **작성일**: 2025-01-08 -> **목적**: 공정관리 기능 검증 및 테스트 -> **상태**: ✅ 검증 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3: 개별 품목 연결 기능 (process_items) | -| **다음 작업** | 완료 (Phase 2는 선택사항) | -| **진행률** | 5/5 (100%) - Phase 1 + Phase 3 완료 | -| **마지막 업데이트** | 2026-01-08 | - ---- - -## 1. 개요 - -### 1.1 기능 설명 -공정관리는 MES 시스템의 기초 데이터로, 생산 공정을 정의하고 관리하는 기능입니다. -작업지시 생성 시 공정 유형(process_type)으로 연결되며, 자동 분류 규칙을 통해 품목별 공정 배정을 자동화합니다. - -### 1.2 현재 구현 상태 분석 - -#### API (Laravel) - ✅ 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| Model | `api/app/Models/Process.php` | ✅ | -| Model | `api/app/Models/ProcessClassificationRule.php` | ✅ | -| Model | `api/app/Models/ProcessItem.php` | ✅ (Phase 3) | -| Migration | `api/database/migrations/2026_01_08_180607_create_process_items_table.php` | ✅ | -| Service | `api/app/Services/ProcessService.php` | ✅ | -| Controller | `api/app/Http/Controllers/V1/ProcessController.php` | ✅ | -| FormRequest | `api/app/Http/Requests/V1/Process/StoreProcessRequest.php` | ✅ | -| FormRequest | `api/app/Http/Requests/V1/Process/UpdateProcessRequest.php` | ✅ | -| Swagger | `api/app/Swagger/v1/ProcessApi.php` | ✅ | -| Route | `/api/v1/processes` | ✅ | - -#### Frontend (React/Next.js) - ✅ API 연동 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| 목록 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/page.tsx` | ✅ | -| 등록 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx` | ✅ | -| 상세 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx` | ✅ | -| 수정 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx` | ✅ | -| 목록 컴포넌트 | `react/src/components/process-management/ProcessListClient.tsx` | ✅ | -| 폼 컴포넌트 | `react/src/components/process-management/ProcessForm.tsx` | ✅ | -| 상세 컴포넌트 | `react/src/components/process-management/ProcessDetail.tsx` | ✅ | -| 규칙 모달 | `react/src/components/process-management/RuleModal.tsx` | ✅ | -| **actions.ts** | `react/src/components/process-management/actions.ts` | ✅ | - -### 1.3 관련 URL -| 화면 | URL | 설명 | -|------|-----|------| -| 공정목록 | `/master-data/process-management` | 토글 기능 포함 | -| 공정등록 | `/master-data/process-management/new` | 모달 - 규칙추가 | -| 공정상세 | `/master-data/process-management/{id}` | 상세 정보 | -| 공정수정 | `/master-data/process-management/{id}/edit` | 수정 폼 | - -### 1.4 연관관계 -``` -┌─────────────────┐ process_type ┌─────────────────┐ -│ Process │ ───────────────────────│ WorkOrder │ -│ (공정관리) │ screen/slat/bending │ (작업지시) │ -└─────────────────┘ └─────────────────┘ - │ - ├── classificationRules (패턴 규칙) - │ ▼ - │ ┌─────────────────────────┐ - │ │ ProcessClassificationRule│ - │ │ (자동 분류 규칙) │ - │ └─────────────────────────┘ - │ - └── processItems (개별 품목) ← Phase 3 - ▼ - ┌─────────────────────────┐ ┌─────────────────┐ - │ ProcessItem │────────│ Item │ - │ (공정-품목 연결) │ │ (품목) │ - └─────────────────────────┘ └─────────────────┘ -``` - ---- - -## 2. API 엔드포인트 - -### 2.1 REST API (구현 완료) -| Method | Endpoint | 설명 | 상태 | -|--------|----------|------|:----:| -| GET | `/api/v1/processes` | 공정 목록 조회 (검색/페이징) | ✅ | -| GET | `/api/v1/processes/{id}` | 공정 상세 조회 | ✅ | -| POST | `/api/v1/processes` | 공정 생성 | ✅ | -| PUT | `/api/v1/processes/{id}` | 공정 수정 | ✅ | -| DELETE | `/api/v1/processes/{id}` | 공정 삭제 | ✅ | -| DELETE | `/api/v1/processes` | 공정 일괄 삭제 | ✅ | -| PATCH | `/api/v1/processes/{id}/toggle` | 공정 상태 토글 | ✅ | -| GET | `/api/v1/processes/options` | 드롭다운용 옵션 목록 | ✅ | -| GET | `/api/v1/processes/stats` | 공정 통계 | ✅ | - -### 2.2 actions.ts 구현 함수 (완료) -```typescript -// 목록/조회 -getProcessList(params) // 목록 조회 -getProcessById(id) // 상세 조회 -getProcessOptions() // 드롭다운 옵션 -getProcessStats() // 통계 조회 - -// CRUD -createProcess(data) // 생성 -updateProcess(id, data) // 수정 -deleteProcess(id) // 삭제 -deleteProcesses(ids) // 일괄 삭제 -toggleProcessActive(id) // 상태 토글 - -// 보조 -getDepartmentOptions() // 부서 옵션 (분류 규칙용) -getItemList(params) // 품목 목록 (분류 규칙용) -``` - ---- - -## 3. 데이터 스키마 - -### 3.1 Process (공정) -```typescript -interface Process { - id: string; - processCode: string; // P-001, P-002 - processName: string; // 공정명 - description?: string; // 공정 설명 - processType: '생산' | '검사' | '포장' | '조립'; - department: string; // 담당 부서 - workLogTemplate?: string; // 작업일지 양식 - classificationRules: ClassificationRule[]; - requiredWorkers: number; // 필요 작업자 수 - equipmentInfo?: string; // 설비 정보 - workSteps: string[]; // 작업 단계 - note?: string; - status: '사용중' | '미사용'; - createdAt: string; - updatedAt: string; -} -``` - -### 3.2 ClassificationRule (자동 분류 규칙) -```typescript -interface ClassificationRule { - id: string; - registrationType: 'pattern' | 'individual'; // 패턴 규칙 vs 개별 품목 - ruleType: '품목코드' | '품목명' | '품목구분'; - matchingType: 'startsWith' | 'endsWith' | 'contains' | 'equals'; - conditionValue: string; - priority: number; - description?: string; - isActive: boolean; - createdAt: string; -} -``` - -### 3.3 ProcessItem (공정-품목 연결) - Phase 3 추가 -```typescript -// API 응답 스키마 -interface ApiProcessItem { - id: number; - process_id: number; - item_id: number; - priority: number; - is_active: boolean; - item?: { - id: number; - code: string; - name: string; - }; -} - -// DB 테이블: process_items -// - id (PK) -// - process_id (FK → processes) -// - item_id (FK → items) -// - priority (정렬 순서) -// - is_active (사용 여부) -// - created_at, updated_at -``` - -### 3.4 API 요청/응답 변환 - -#### 요청 (Frontend → API) -```typescript -// 패턴 규칙과 개별 품목 분리 -{ - classification_rules: [ // 패턴 규칙만 - { rule_type, matching_type, condition_value, ... } - ], - item_ids: [123, 456, 789] // 개별 품목 ID 배열 -} -``` - -#### 응답 (API → Frontend) -```typescript -// process_items를 individual 규칙으로 변환 -{ - classification_rules: [...], // 패턴 규칙 - process_items: [ // 개별 품목 연결 - { id, process_id, item_id, priority, is_active, item: {...} } - ] -} -``` - ---- - -## 4. 작업 범위 - -### Phase 1: 검증 및 테스트 (완료 - 2026-01-08) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | 목록 조회 테스트 | ✅ | 검색, 탭 필터 정상 | -| 1.2 | 등록 기능 테스트 | ✅ | 정상 (담당부서는 DB 데이터 의존) | -| 1.3 | 수정 기능 테스트 | ✅ | 필요인원 변경/저장 정상 | -| 1.4 | 삭제 기능 테스트 | ⏭️ | 데이터 보존으로 생략 | -| 1.5 | 토글 기능 테스트 | ✅ | 사용중↔미사용 전환 정상 | - -### 📋 참고사항 - -- **담당부서 드롭다운**: departments 테이블 데이터에 의존. 데이터 없으면 빈 드롭다운 (정상 동작) - -### Phase 2: 개선 사항 (선택) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 공정 순서 드래그앤드롭 | ⏭️ | 후순위 | -| 2.2 | 작업 지침서 PDF 업로드 | ⏭️ | 후순위 | -| 2.3 | 공정 흐름도 시각화 | ⏭️ | 후순위 | - -### Phase 3: 개별 품목 연결 기능 (완료 - 2026-01-08) - -#### 배경 -- 기존 분류 규칙에서 400개 이상의 품목 코드를 `,` 구분자로 저장 시도 -- `condition_value` VARCHAR(255) 필드 초과 → API 422 에러 발생 -- 해결: 개별 품목은 별도 테이블(`process_items`)로 관계형 저장 - -#### 완료 작업 - -| # | 작업 항목 | 상태 | 파일/위치 | -|---|----------|:----:|----------| -| 3.1 | ProcessItem 모델 생성 | ✅ | `api/app/Models/ProcessItem.php` | -| 3.2 | process_items 마이그레이션 | ✅ | `api/database/migrations/2026_01_08_180607_*` | -| 3.3 | Process 모델 관계 추가 | ✅ | `processItems()` HasMany | -| 3.4 | ProcessService 수정 | ✅ | `syncProcessItems()` 메서드 추가 | -| 3.5 | Validation 업데이트 | ✅ | `item_ids` 배열 검증 추가 | -| 3.6 | Swagger 문서 업데이트 | ✅ | `ProcessItem` 스키마 추가 | -| 3.7 | Frontend actions.ts 수정 | ✅ | 요청/응답 변환 로직 | - -#### 핵심 변경 사항 - -**API 측 (Laravel)** -```php -// ProcessService.php -private function syncProcessItems(Process $process, array $itemIds): void -{ - $process->processItems()->delete(); - foreach ($itemIds as $index => $itemId) { - ProcessItem::create([ - 'process_id' => $process->id, - 'item_id' => $itemId, - 'priority' => $index, - 'is_active' => true, - ]); - } -} -``` - -**Frontend 측 (Next.js)** -```typescript -// actions.ts -// 패턴 규칙과 개별 품목 분리 -const patternRules = data.classificationRules.filter( - (rule) => rule.registrationType === 'pattern' -); -const individualRules = data.classificationRules.filter( - (rule) => rule.registrationType === 'individual' -); -// item_ids 추출 -const itemIds = individualRules.flatMap((rule) => - rule.conditionValue.split(',').map((id) => parseInt(id.trim(), 10)) -); -``` - ---- - -## 5. 주요 기능 상세 - -### 5.1 토글 기능 -- 목록에서 각 공정의 사용/미사용 상태를 토글 -- `PATCH /api/v1/processes/{id}/toggle` 호출 -- 미사용 공정은 작업지시 생성 시 선택 불가 - -### 5.2 규칙 추가 (모달) -- 자동 분류 규칙을 통해 품목별 공정 자동 배정 -- 우선순위(priority)에 따라 규칙 적용 순서 결정 -- include/exclude로 포함/제외 규칙 설정 - -### 5.3 양식 보기 (모달) -- 작업일지 템플릿 미리보기 -- HTML/마크다운 형식 지원 - ---- - -## 6. 의존성 - -### 6.1 필수 선행 작업 -- **없음** (기초 데이터) - -### 6.2 후속 연동 -- **작업지시 (WorkOrder)**: 공정 유형 선택 (process_type: screen/slat/bending) -- **품목관리 (Item)**: 자동 분류 규칙 적용 - ---- - -## 7. 검증 방법 - -### 7.1 테스트 체크리스트 - -| 기능 | 테스트 항목 | 예상 결과 | -|------|-----------|----------| -| 목록 조회 | 페이지 로드 | 공정 목록 표시 | -| 검색 | "생산" 검색 | 필터링된 결과 | -| 탭 필터 | "사용중" 탭 클릭 | 사용중 공정만 표시 | -| 등록 | 새 공정 등록 | 목록에 추가됨 | -| 수정 | 공정명 변경 | 변경 반영됨 | -| 삭제 | 공정 삭제 | 목록에서 제거됨 | -| 토글 | 상태 토글 | 사용중↔미사용 전환 | -| 규칙 추가 | 분류 규칙 추가 | 규칙 저장됨 | - -### 7.2 API 테스트 -```bash -# 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/processes" -H "X-Api-Key: ..." - -# 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/processes/1" -H "X-Api-Key: ..." - -# 통계 조회 -curl -X GET "http://api.sam.kr/api/v1/processes/stats" -H "X-Api-Key: ..." - -# 토글 -curl -X PATCH "http://api.sam.kr/api/v1/processes/1/toggle" -H "X-Api-Key: ..." -``` - ---- - -## 8. 참고 사항 - -### 8.1 공정 유형 (process_type) -현재 작업지시에서 사용하는 공정 유형: -- `screen`: 스크린 공정 -- `slat`: 슬랫 공정 -- `bending`: 절곡 공정 - -### 8.2 Process vs WorkOrder.process_type -- `Process` 모델: 공정의 메타데이터 (이름, 설명, 규칙 등) -- `WorkOrder.process_type`: 실제 작업지시에 적용된 공정 유형 -- 향후 FK 연결로 확장성 확보 가능 - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - -### 참고 코드 -- **Controller**: `api/app/Http/Controllers/V1/ProcessController.php` -- **Service**: `api/app/Services/ProcessService.php` -- **actions.ts**: `react/src/components/process-management/actions.ts` - ---- - -## 10. 자기완결성 점검 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 검증 및 테스트 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 7 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1 테스트 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 선행 작업 없음 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 경로 검증됨 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 테스트 체크리스트 제공 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | curl + 체크리스트 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 경로 명시 | - ---- - -*이 문서는 독립 세션에서 바로 작업 시작 가능하도록 설계되었습니다.* \ No newline at end of file diff --git a/plans/archive/quote-auto-calculation-development-plan.md b/plans/archive/quote-auto-calculation-development-plan.md deleted file mode 100644 index 2034c20..0000000 --- a/plans/archive/quote-auto-calculation-development-plan.md +++ /dev/null @@ -1,743 +0,0 @@ -# 견적 자동산출 개발 계획 - -> **작성일**: 2025-12-22 -> **상태**: ✅ 구현 완료 -> **목표**: MNG 견적수식 데이터 셋팅 + React 견적관리 자동산출 기능 구현 -> **완료일**: 2025-12-22 -> **실제 소요 시간**: 약 2시간 - ---- - -## 0. 빠른 시작 가이드 - -### 폴더 구조 이해 (중요!) - -| 폴더 | 포트 | 역할 | 비고 | -|------|------|------|------| -| `design/` | localhost:3002 | 디자인 프로토타입 | UI 참고용 | -| `react/` | localhost:3000 | **실제 프론트엔드** | 구현 대상 ✅ | -| `mng/` | mng.sam.kr | 관리자 패널 | 수식 데이터 관리 | -| `api/` | api.sam.kr | REST API | 견적 산출 엔진 | - -### 이 문서만으로 작업을 시작하려면: - -```bash -# 1. Docker 서비스 시작 -cd /Users/hskwon/Works/@KD_SAM/SAM -docker-compose up -d - -# 2. MNG 시더 실행 (Phase 1 완료 후) -cd mng -php artisan quote:seed-formulas --tenant=1 - -# 3. React 개발 서버 (실제 구현 대상) -cd react -npm run dev -# http://localhost:3000 접속 -``` - -### 핵심 파일 위치 - -| 구분 | 파일 경로 | 역할 | -|------|----------|------| -| **MNG 시더** | `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | 🆕 생성 필요 | -| **React 자동산출** | `react/src/components/quotes/QuoteRegistration.tsx` | ⚡ 수정 필요 (line 332) | -| **API 클라이언트** | `react/src/lib/api/client.ts` | 참조 | -| **API 엔드포인트** | `api/app/Http/Controllers/Api/V1/QuoteController.php` | ✅ 구현됨 | -| **수식 엔진** | `api/app/Services/Quote/QuoteCalculationService.php` | ✅ 구현됨 | - ---- - -## 1. 현황 분석 - -### 1.1 시스템 구조 - -``` -┌───────────────────────────────────────────────────────────────────────────────┐ -│ SAM 시스템 │ -├───────────────────────────────────────────────────────────────────────────────┤ -│ MNG (mng.sam.kr) │ React (react/ 폴더) │ Design │ -│ ├── 기준정보관리 │ ├── 판매관리 │ (참고용) │ -│ │ └── 견적수식관리 ✅ │ │ └── 견적관리 │ │ -│ │ - 카테고리 CRUD │ │ └── 자동견적산출 │ design/ │ -│ │ - 수식 CRUD │ │ UI 있음 ✅ │ :3002 │ -│ │ - 범위/매핑/품목 탭 │ │ API 연동 ❌ │ │ -│ │ │ │ │ │ -│ └── DB: quote_formulas 테이블 │ └── API 호출: │ │ -│ (데이터 없음! ❌) │ POST /v1/quotes/calculate │ │ -└───────────────────────────────────────────────────────────────────────────────┘ - -※ design/ 폴더 (localhost:3002)는 UI 프로토타입용이며, 실제 구현은 react/ 폴더에서 진행 -``` - -### 1.2 React 견적등록 컴포넌트 현황 - -**파일**: `react/src/components/quotes/QuoteRegistration.tsx` - -```typescript -// 현재 상태 (line 332-335) -const handleAutoCalculate = () => { - toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`); -}; - -// 입력 필드 (이미 구현됨): -interface QuoteItem { - openWidth: string; // W0 (오픈사이즈 가로) - openHeight: string; // H0 (오픈사이즈 세로) - productCategory: string; // screen | steel - quantity: number; - // ... 기타 필드 -} -``` - -### 1.3 API 엔드포인트 현황 - -**파일**: `api/app/Http/Controllers/Api/V1/QuoteController.php` - -```php -// 이미 구현됨 (line 135-145) -public function calculate(QuoteCalculateRequest $request) -{ - return ApiResponse::handle(function () use ($request) { - $validated = $request->validated(); - return $this->calculationService->calculate( - $validated['inputs'] ?? $validated, - $validated['product_category'] ?? null - ); - }, __('message.quote.calculated')); -} -``` - -### 1.4 수식 시더 데이터 (API) - -**파일**: `api/database/seeders/QuoteFormulaSeeder.php` - -| 카테고리 | 수식 수 | 설명 | -|---------|--------|------| -| OPEN_SIZE | 2 | W0, H0 입력값 | -| MAKE_SIZE | 4 | 제작사이즈 계산 | -| AREA | 1 | 면적 = W1 * H1 / 1000000 | -| WEIGHT | 2 | 중량 계산 (스크린/철재) | -| GUIDE_RAIL | 5 | 가이드레일 자동 선택 | -| CASE | 3 | 케이스 자동 선택 | -| MOTOR | 1 | 모터 자동 선택 (범위 9개) | -| CONTROLLER | 2 | 제어기 매핑 | -| EDGE_WING | 1 | 마구리 수량 | -| INSPECTION | 1 | 검사비 | -| PRICE_FORMULA | 8 | 단가 수식 | -| **합계** | **30개** | + 범위 18개 | - ---- - -## 2. 개발 상세 계획 - -### Phase 1: MNG 시더 데이터 생성 (1일) - -#### 2.1 Artisan 명령어 생성 - -**생성할 파일**: `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` - -```php -option('tenant'); - $only = $this->option('only'); - $fresh = $this->option('fresh'); - - if ($fresh) { - $this->warn('기존 데이터를 삭제합니다...'); - $this->truncateTables($tenantId); - } - - if (!$only || $only === 'categories') { - $this->seedCategories($tenantId); - } - - if (!$only || $only === 'formulas') { - $this->seedFormulas($tenantId); - } - - if (!$only || $only === 'ranges') { - $this->seedRanges($tenantId); - } - - $this->info('✅ 견적수식 시드 완료!'); - return Command::SUCCESS; - } - - private function seedCategories(int $tenantId): void - { - $categories = [ - ['code' => 'OPEN_SIZE', 'name' => '오픈사이즈', 'sort_order' => 1], - ['code' => 'MAKE_SIZE', 'name' => '제작사이즈', 'sort_order' => 2], - ['code' => 'AREA', 'name' => '면적', 'sort_order' => 3], - ['code' => 'WEIGHT', 'name' => '중량', 'sort_order' => 4], - ['code' => 'GUIDE_RAIL', 'name' => '가이드레일', 'sort_order' => 5], - ['code' => 'CASE', 'name' => '케이스', 'sort_order' => 6], - ['code' => 'MOTOR', 'name' => '모터', 'sort_order' => 7], - ['code' => 'CONTROLLER', 'name' => '제어기', 'sort_order' => 8], - ['code' => 'EDGE_WING', 'name' => '마구리', 'sort_order' => 9], - ['code' => 'INSPECTION', 'name' => '검사', 'sort_order' => 10], - ['code' => 'PRICE_FORMULA', 'name' => '단가수식', 'sort_order' => 11], - ]; - - foreach ($categories as $cat) { - DB::table('quote_formula_categories')->updateOrInsert( - ['tenant_id' => $tenantId, 'code' => $cat['code']], - array_merge($cat, [ - 'tenant_id' => $tenantId, - 'is_active' => true, - 'created_at' => now(), - 'updated_at' => now(), - ]) - ); - } - - $this->info("카테고리 " . count($categories) . "개 생성됨"); - } - - private function seedFormulas(int $tenantId): void - { - // API 시더와 동일한 데이터 (api/database/seeders/QuoteFormulaSeeder.php 참조) - $formulas = $this->getFormulaData(); - - $categoryMap = DB::table('quote_formula_categories') - ->where('tenant_id', $tenantId) - ->pluck('id', 'code') - ->toArray(); - - $count = 0; - foreach ($formulas as $formula) { - $categoryId = $categoryMap[$formula['category_code']] ?? null; - if (!$categoryId) continue; - - DB::table('quote_formulas')->updateOrInsert( - ['tenant_id' => $tenantId, 'variable' => $formula['variable']], - [ - 'tenant_id' => $tenantId, - 'category_id' => $categoryId, - 'variable' => $formula['variable'], - 'name' => $formula['name'], - 'type' => $formula['type'], - 'formula' => $formula['formula'] ?? null, - 'output_type' => 'variable', - 'description' => $formula['description'] ?? null, - 'sort_order' => $formula['sort_order'] ?? 0, - 'is_active' => $formula['is_active'] ?? true, - 'created_at' => now(), - 'updated_at' => now(), - ] - ); - $count++; - } - - $this->info("수식 {$count}개 생성됨"); - } - - private function getFormulaData(): array - { - return [ - // 오픈사이즈 - ['category_code' => 'OPEN_SIZE', 'variable' => 'W0', 'name' => '오픈사이즈 W0 (가로)', 'type' => 'input', 'formula' => null, 'sort_order' => 1], - ['category_code' => 'OPEN_SIZE', 'variable' => 'H0', 'name' => '오픈사이즈 H0 (세로)', 'type' => 'input', 'formula' => null, 'sort_order' => 2], - - // 제작사이즈 - ['category_code' => 'MAKE_SIZE', 'variable' => 'W1_SCREEN', 'name' => '제작사이즈 W1 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 140', 'sort_order' => 1], - ['category_code' => 'MAKE_SIZE', 'variable' => 'H1_SCREEN', 'name' => '제작사이즈 H1 (스크린)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 2], - ['category_code' => 'MAKE_SIZE', 'variable' => 'W1_STEEL', 'name' => '제작사이즈 W1 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 110', 'sort_order' => 3], - ['category_code' => 'MAKE_SIZE', 'variable' => 'H1_STEEL', 'name' => '제작사이즈 H1 (철재)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 4], - - // 면적 - ['category_code' => 'AREA', 'variable' => 'M', 'name' => '면적 계산', 'type' => 'calculation', 'formula' => 'W1 * H1 / 1000000', 'sort_order' => 1], - - // 중량 - ['category_code' => 'WEIGHT', 'variable' => 'K_SCREEN', 'name' => '중량 계산 (스크린)', 'type' => 'calculation', 'formula' => 'M * 2 + W0 / 1000 * 14.17', 'sort_order' => 1], - ['category_code' => 'WEIGHT', 'variable' => 'K_STEEL', 'name' => '중량 계산 (철재)', 'type' => 'calculation', 'formula' => 'M * 25', 'sort_order' => 2], - - // 가이드레일 - ['category_code' => 'GUIDE_RAIL', 'variable' => 'G', 'name' => '가이드레일 제작길이', 'type' => 'calculation', 'formula' => 'H0 + 250', 'sort_order' => 1], - ['category_code' => 'GUIDE_RAIL', 'variable' => 'GR_AUTO_SELECT', 'name' => '가이드레일 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 2], - - // 케이스 - ['category_code' => 'CASE', 'variable' => 'S_SCREEN', 'name' => '케이스 사이즈 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 220', 'sort_order' => 1], - ['category_code' => 'CASE', 'variable' => 'S_STEEL', 'name' => '케이스 사이즈 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 240', 'sort_order' => 2], - ['category_code' => 'CASE', 'variable' => 'CASE_AUTO_SELECT', 'name' => '케이스 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 3], - - // 모터 - ['category_code' => 'MOTOR', 'variable' => 'MOTOR_AUTO_SELECT', 'name' => '모터 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 1], - - // 제어기 - ['category_code' => 'CONTROLLER', 'variable' => 'CONTROLLER_TYPE', 'name' => '제어기 유형', 'type' => 'input', 'formula' => null, 'sort_order' => 0], - ['category_code' => 'CONTROLLER', 'variable' => 'CTRL_AUTO_SELECT', 'name' => '제어기 자동 선택', 'type' => 'mapping', 'formula' => null, 'sort_order' => 1], - - // 검사 - ['category_code' => 'INSPECTION', 'variable' => 'INSP_FEE', 'name' => '검사비', 'type' => 'calculation', 'formula' => '1', 'sort_order' => 1], - ]; - } - - // ... 나머지 메서드 (seedRanges, truncateTables 등) -} -``` - -#### 2.2 작업 순서 - -```bash -# 1. 명령어 파일 생성 -# mng/app/Console/Commands/SeedQuoteFormulasCommand.php - -# 2. 실행 -cd mng -php artisan quote:seed-formulas --tenant=1 - -# 3. 확인 -php artisan tinker ->>> \App\Models\Quote\QuoteFormula::count() -# 예상: 30 - -# 4. 시뮬레이터 테스트 -# mng.sam.kr/quote-formulas/simulator -# 입력: W0=3000, H0=2500 -``` - ---- - -### Phase 2: React 자동산출 기능 구현 (2-3일) - -#### 2.1 API 클라이언트 추가 - -**수정할 파일**: `react/src/lib/api/quote.ts` (신규) - -```typescript -// react/src/lib/api/quote.ts -import { ApiClient } from './client'; -import { AUTH_CONFIG } from './auth/auth-config'; - -// API 응답 타입 -interface CalculationResult { - inputs: Record; - outputs: Record; - items: Array<{ - item_code: string; - item_name: string; - specification?: string; - unit?: string; - quantity: number; - unit_price: number; - total_price: number; - formula_variable: string; - }>; - costs: { - material_cost: number; - labor_cost: number; - install_cost: number; - subtotal: number; - }; - errors: string[]; -} - -interface CalculateRequest { - inputs: { - W0: number; - H0: number; - QTY?: number; - INSTALL_TYPE?: string; - CONTROL_TYPE?: string; - }; - product_category: 'screen' | 'steel'; -} - -// Quote API 클라이언트 -class QuoteApiClient extends ApiClient { - constructor() { - super({ - mode: 'bearer', - apiKey: AUTH_CONFIG.apiKey, - getToken: () => { - if (typeof window !== 'undefined') { - return localStorage.getItem('auth_token'); - } - return null; - }, - }); - } - - /** - * 자동 견적 산출 - */ - async calculate(request: CalculateRequest): Promise<{ success: boolean; data: CalculationResult; message: string }> { - return this.post('/api/v1/quotes/calculate', request); - } - - /** - * 입력 스키마 조회 - */ - async getCalculationSchema(productCategory?: string): Promise<{ success: boolean; data: Record }> { - const query = productCategory ? `?product_category=${productCategory}` : ''; - return this.get(`/api/v1/quotes/calculation-schema${query}`); - } -} - -export const quoteApi = new QuoteApiClient(); -``` - -#### 2.2 QuoteRegistration.tsx 수정 - -**수정할 파일**: `react/src/components/quotes/QuoteRegistration.tsx` - -```typescript -// 추가할 import -import { quoteApi } from '@/lib/api/quote'; -import { useState } from 'react'; - -// 상태 추가 (컴포넌트 내부) -const [calculationResult, setCalculationResult] = useState(null); -const [isCalculating, setIsCalculating] = useState(false); - -// handleAutoCalculate 수정 (line 332-335) -const handleAutoCalculate = async () => { - const item = formData.items[activeItemIndex]; - - if (!item.openWidth || !item.openHeight) { - toast.error('오픈사이즈(W0, H0)를 입력해주세요.'); - return; - } - - setIsCalculating(true); - try { - const response = await quoteApi.calculate({ - inputs: { - W0: parseFloat(item.openWidth), - H0: parseFloat(item.openHeight), - QTY: item.quantity, - INSTALL_TYPE: item.guideRailType, - CONTROL_TYPE: item.controller, - }, - product_category: item.productCategory as 'screen' | 'steel' || 'screen', - }); - - if (response.success) { - setCalculationResult(response.data); - toast.success('자동 산출이 완료되었습니다.'); - } else { - toast.error(response.message || '산출 중 오류가 발생했습니다.'); - } - } catch (error) { - console.error('자동 산출 오류:', error); - toast.error('서버 연결에 실패했습니다.'); - } finally { - setIsCalculating(false); - } -}; - -// 산출 결과 반영 함수 추가 -const handleApplyCalculation = () => { - if (!calculationResult) return; - - // 산출된 품목을 견적 항목에 반영 - const newItems = calculationResult.items.map((item, index) => ({ - id: `calc-${Date.now()}-${index}`, - floor: formData.items[activeItemIndex].floor, - code: item.item_code, - productCategory: formData.items[activeItemIndex].productCategory, - productName: item.item_name, - openWidth: formData.items[activeItemIndex].openWidth, - openHeight: formData.items[activeItemIndex].openHeight, - guideRailType: formData.items[activeItemIndex].guideRailType, - motorPower: formData.items[activeItemIndex].motorPower, - controller: formData.items[activeItemIndex].controller, - quantity: item.quantity, - wingSize: formData.items[activeItemIndex].wingSize, - inspectionFee: item.unit_price, - unitPrice: item.unit_price, - totalAmount: item.total_price, - })); - - setFormData({ - ...formData, - items: [...formData.items.slice(0, activeItemIndex), ...newItems, ...formData.items.slice(activeItemIndex + 1)], - }); - - setCalculationResult(null); - toast.success(`${newItems.length}개 품목이 반영되었습니다.`); -}; -``` - -#### 2.3 산출 결과 표시 UI 추가 - -```tsx -{/* 자동 견적 산출 버튼 아래에 추가 */} -{calculationResult && ( - - - - - 산출 결과 - - - - {/* 계산 변수 */} -
- {Object.entries(calculationResult.outputs).map(([key, val]) => ( -
-
{val.name}
-
{typeof val.value === 'number' ? val.value.toFixed(2) : val.value}
-
- ))} -
- - {/* 산출 품목 */} - - - - - - - - - - - - {calculationResult.items.map((item, i) => ( - - - - - - - - ))} - - - - - - - -
품목코드품목명수량단가금액
{item.item_code}{item.item_name}{item.quantity}{item.unit_price.toLocaleString()}{item.total_price.toLocaleString()}
합계{calculationResult.costs.subtotal.toLocaleString()}원
- - {/* 반영 버튼 */} - -
-
-)} -``` - ---- - -### Phase 3: 통합 테스트 (1일) - -#### 3.1 테스트 시나리오 - -| 번호 | 테스트 케이스 | 입력값 | 예상 결과 | -|-----|-------------|-------|----------| -| 1 | 기본 스크린 산출 | W0=3000, H0=2500 | 가이드레일 PT-GR-3000, 모터 PT-MOTOR-150 | -| 2 | 대형 스크린 산출 | W0=5000, H0=4000 | 모터 규격 상향 (300K 이상) | -| 3 | 철재 산출 | W0=2000, H0=2000, steel | 중량 M*25 적용 | -| 4 | 품목 반영 | 산출 후 반영 클릭 | 견적 항목에 추가됨 | -| 5 | 에러 처리 | W0/H0 미입력 | "오픈사이즈를 입력해주세요" | - -#### 3.2 검증 체크리스트 - -``` -□ MNG 시뮬레이터에서 수식 계산 정확도 확인 -□ React 자동산출 버튼 클릭 → API 호출 확인 -□ 산출 결과 테이블 정상 표시 -□ "품목에 반영하기" 클릭 → 견적 항목 추가 확인 -□ 견적 저장 시 calculation_inputs 필드 저장 확인 -□ 에러 시 적절한 메시지 표시 -``` - ---- - -## 3. SAM 개발 규칙 요약 - -### 3.1 API 개발 규칙 (CLAUDE.md 참조) - -```php -// Controller: FormRequest + ApiResponse 패턴 -public function calculate(QuoteCalculateRequest $request) -{ - return ApiResponse::handle(function () use ($request) { - return $this->calculationService->calculate($request->validated()); - }, __('message.quote.calculated')); -} - -// Service: 비즈니스 로직 분리 -class QuoteCalculationService extends Service -{ - public function calculate(array $inputs, ?string $productCategory = null): array - { - $tenantId = $this->tenantId(); // 필수 - // ... - } -} - -// 응답 형식 -{ - "success": true, - "message": "견적이 산출되었습니다.", - "data": { ... } -} -``` - -### 3.2 React 개발 패턴 - -```typescript -// API 클라이언트 패턴 (react/src/lib/api/client.ts) -class ApiClient { - async post(endpoint: string, data?: unknown): Promise - async get(endpoint: string): Promise -} - -// 컴포넌트 패턴 -// - shadcn/ui 컴포넌트 사용 -// - toast (sonner) 알림 -// - FormField, Card, Button 등 -``` - -### 3.3 MNG 개발 패턴 - -```php -// Artisan 명령어 패턴 -protected $signature = 'quote:seed-formulas {--tenant=1}'; - -// 모델 사용 -use App\Models\Quote\QuoteFormula; -use App\Models\Quote\QuoteFormulaCategory; - -// 서비스 패턴 -class QuoteFormulaService { - public function __construct( - private FormulaEvaluatorService $evaluator - ) {} -} -``` - ---- - -## 4. 파일 구조 - -``` -SAM/ -├── mng/ -│ ├── app/Console/Commands/ -│ │ └── SeedQuoteFormulasCommand.php # 🆕 Phase 1 -│ ├── app/Models/Quote/ -│ │ ├── QuoteFormula.php # ✅ 있음 -│ │ ├── QuoteFormulaCategory.php # ✅ 있음 -│ │ └── QuoteFormulaRange.php # ✅ 있음 -│ └── app/Services/Quote/ -│ └── FormulaEvaluatorService.php # ✅ 있음 -│ -├── api/ -│ ├── app/Http/Controllers/Api/V1/ -│ │ └── QuoteController.php # ✅ calculate() 있음 -│ ├── app/Services/Quote/ -│ │ ├── QuoteCalculationService.php # ✅ 있음 -│ │ └── FormulaEvaluatorService.php # ✅ 있음 -│ └── database/seeders/ -│ └── QuoteFormulaSeeder.php # 참조용 데이터 -│ -├── react/ -│ ├── src/lib/api/ -│ │ ├── client.ts # ✅ ApiClient 클래스 -│ │ └── quote.ts # 🆕 Phase 2 -│ └── src/components/quotes/ -│ └── QuoteRegistration.tsx # ⚡ Phase 2 수정 -│ -└── docs/plans/ - └── quote-auto-calculation-development-plan.md # 이 문서 -``` - ---- - -## 5. 수식 계산 예시 - -``` -입력: W0=3000mm, H0=2500mm, product_category=screen - -계산 순서: -1. W1 = W0 + 140 = 3140mm (스크린 제작 가로) -2. H1 = H0 + 350 = 2850mm (스크린 제작 세로) -3. M = W1 * H1 / 1000000 = 8.949㎡ (면적) -4. K = M * 2 + W0 / 1000 * 14.17 = 60.41kg (중량) -5. G = H0 + 250 = 2750mm (가이드레일 길이) -6. S = W0 + 220 = 3220mm (케이스 사이즈) - -범위 자동 선택: -- 가이드레일: G=2750 → 2438 < G ≤ 3000 → PT-GR-3000 × 2개 -- 케이스: S=3220 → 3000 < S ≤ 3600 → PT-CASE-3600 × 1개 -- 모터: K=60.41 → 0 < K ≤ 150 → PT-MOTOR-150 × 1개 -``` - ---- - -## 6. 일정 요약 - -| Phase | 작업 | 예상 기간 | 상태 | -|-------|------|----------|------| -| 1 | MNG 시더 명령어 생성 | 1일 | ✅ 완료 | -| 2 | React Quote API 클라이언트 생성 | 0.5일 | ✅ 완료 | -| 3 | React handleAutoCalculate API 연동 | 0.5일 | ✅ 완료 | -| 4 | 산출 결과 UI 추가 | 0.5일 | ✅ 완료 | -| 5 | 문서 업데이트 | 0.5시간 | ✅ 완료 | -| **합계** | | **약 2시간** | ✅ | - ---- - -## 7. 완료된 구현 내역 - -### 생성된 파일 -| 파일 경로 | 역할 | -|----------|------| -| `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | MNG 견적수식 시더 명령어 | -| `react/src/lib/api/quote.ts` | React Quote API 클라이언트 | - -### 수정된 파일 -| 파일 경로 | 변경 내용 | -|----------|----------| -| `react/src/components/quotes/QuoteRegistration.tsx` | handleAutoCalculate API 연동, 산출 결과 UI 추가 | - -### MNG 시더 실행 결과 -``` -✅ 견적수식 시드 완료! -카테고리: 11개 -수식: 18개 -범위: 18개 -``` - -### React 기능 구현 -- `handleAutoCalculate`: API 호출 및 로딩 상태 관리 -- `handleApplyCalculation`: 산출 결과를 견적 항목에 반영 -- 산출 결과 UI: 변수, 품목 테이블, 비용 합계 표시 -- 에러 처리: 입력값 검증, API 에러 토스트 - ---- - -*문서 버전*: 3.0 (구현 완료) -*작성자*: Claude Code -*최종 업데이트*: 2025-12-22 \ No newline at end of file diff --git a/plans/archive/quote-v2-auto-calculation-fix-plan.md b/plans/archive/quote-v2-auto-calculation-fix-plan.md deleted file mode 100644 index 2b372ec..0000000 --- a/plans/archive/quote-v2-auto-calculation-fix-plan.md +++ /dev/null @@ -1,262 +0,0 @@ -# 견적 V2 자동 견적 산출 오류 수정 계획 - -> **작성일**: 2026-01-26 -> **목적**: 자동 견적 산출 기능의 4가지 오류 분석 및 수정 -> **기준 문서**: `QuoteRegistrationV2.tsx`, `LocationDetailPanel.tsx`, `QuoteSummaryPanel.tsx`, `actions.ts` -> **상태**: ✅ 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 테스트 및 검증 완료 | -| **다음 작업** | - | -| **진행률** | 4/4 (100%) ✅ | -| **마지막 업데이트** | 2026-01-26 | - ---- - -## 1. 개요 - -### 1.1 배경 -견적 V2 페이지(`/sales/quote-management/test-new`)에서 자동 견적 산출 버튼 클릭 후 다음 4가지 문제 발생: -1. 오른쪽 패널에 제품 리스트가 표시되지 않음 -2. 개소별 합계(상세소계)가 표시되지 않음 -3. 상세별 합계(그룹)가 표시되지 않음 -4. 예상 견적금액이 0원으로 표시됨 - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - API 응답 구조와 프론트엔드 기대 구조의 일치 확보 │ -│ - Mock 데이터 fallback 로직은 디버깅/테스트용으로만 유지 │ -│ - 실제 BOM 계산 결과가 UI에 정확히 반영되도록 수정 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 타입 캐스팅 수정, 데이터 매핑 로직 수정 | 불필요 | -| ⚠️ 컨펌 필요 | API 응답 구조 변경, 새 인터페이스 정의 | **필수** | -| 🔴 금지 | API 엔드포인트 변경, DB 스키마 변경 | 별도 협의 | - ---- - -## 2. 근본 원인 분석 - -### 2.1 API 응답 구조 불일치 (핵심 원인) - -**API 실제 응답** (`actions.ts:962-965`): -```typescript -return { - success: true, - data: result.data || [], // 배열을 직접 반환 -}; -``` - -**API 서버 응답** (`QuoteCalculationService.php:168-178`): -```php -return [ - 'success' => $failCount === 0, - 'summary' => [ - 'total_count' => count($inputItems), - 'success_count' => $successCount, - 'fail_count' => $failCount, - 'grand_total' => round($grandTotal, 2), - ], - 'items' => $results, // items 배열 안에 결과가 있음 -]; -``` - -**컴포넌트 기대 구조** (`QuoteRegistrationV2.tsx:459-462`): -```typescript -const apiData = result.data as { - summary?: { grand_total: number }; - items?: Array<{ index: number; result: BomCalculationResult }>; -}; -const bomItems = apiData.items || []; // ❌ result.data가 배열이면 items가 없음! -``` - -### 2.2 문제 발생 흐름 - -``` -사용자 → "자동 견적 산출" 클릭 - ↓ -calculateBomBulk(bomItems) 호출 - ↓ -API 서버: { success, summary, items: [...] } 반환 - ↓ -actions.ts: result.data = 전체 응답 객체 (또는 배열로 잘못 파싱) - ↓ -QuoteRegistrationV2.tsx: result.data.items 접근 시도 - ↓ -❌ items가 undefined → bomItems = [] - ↓ -locations에 bomResult 저장 안됨 - ↓ -LocationDetailPanel: bomResult?.items 없음 → Mock 데이터 표시 -QuoteSummaryPanel: bomResult?.subtotals 없음 → Mock 데이터 표시 - ↓ -💥 모든 UI 영역에 데이터 없음 -``` - -### 2.3 영향 받는 컴포넌트 - -| 컴포넌트 | 파일 | 영향 | -|----------|------|------| -| QuoteRegistrationV2 | `QuoteRegistrationV2.tsx:457-481` | bomResult 저장 안됨 | -| LocationDetailPanel | `LocationDetailPanel.tsx:152-184` | Mock 데이터로 fallback | -| QuoteSummaryPanel | `QuoteSummaryPanel.tsx:136-164` | Mock 데이터로 fallback | - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: API 응답 처리 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | `actions.ts` 응답 구조 확인 | ✅ | API 서버 응답과 비교 | -| 1.2 | `actions.ts` BomBulkResponse 타입 추가 | ✅ | 정확한 API 응답 구조 정의 | -| 1.3 | `QuoteRegistrationV2.tsx` handleCalculate 수정 | ✅ | 응답 매핑 로직 및 디버그 로그 추가 | - -### 3.2 Phase 2: 데이터 바인딩 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | `FormulaEvaluatorService.php` items에 process_group 추가 | ✅ | addProcessGroupToItems 메서드 추가 | -| 2.2 | `LocationDetailPanel.tsx` bomItemsByTab 수정 | ✅ | process_group_key 기반 매핑 | -| 2.3 | `QuoteSummaryPanel.tsx` detailTotals 수정 | ✅ | grouped_items에서 items 가져오기 | -| 2.4 | `actions.ts` BomCalculationResult 타입 확장 | ✅ | process_group, grouped_items 필드 추가 | - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1.2: handleCalculate 함수 수정 - -**현재 코드** (`QuoteRegistrationV2.tsx:457-479`): -```typescript -if (result.success && result.data) { - // ❌ 잘못된 타입 캐스팅 - result.data 자체가 API 응답 객체임 - const apiData = result.data as { - summary?: { grand_total: number }; - items?: Array<{ index: number; result: BomCalculationResult }>; - }; - const bomItems = apiData.items || []; // ❌ undefined - // ... -} -``` - -**수정 방안**: -`actions.ts`의 응답 처리를 확인하여 두 가지 접근법 중 선택: - -#### 방안 A: actions.ts 수정 (권장) -```typescript -// actions.ts에서 API 응답 구조 유지 -return { - success: true, - data: { - summary: result.data.summary, - items: result.data.items, - }, -}; -``` - -#### 방안 B: QuoteRegistrationV2.tsx 수정 -```typescript -if (result.success && result.data) { - // result.data가 { summary, items } 구조인지 확인 - const apiData = result.data as unknown as { - summary?: { grand_total: number }; - items?: Array<{ index: number; result: BomCalculationResult }>; - }; - // ... -} -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | API 응답 구조 | actions.ts의 result.data 반환 방식 검토 | react/actions.ts | 대기 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-26 | 분석 | 문서 초안 작성 및 근본 원인 분석 완료 | - | - | -| 2026-01-26 | 수정 | actions.ts BomBulkResponse 타입 추가 | react/src/components/quotes/actions.ts | ✅ | -| 2026-01-26 | 수정 | QuoteRegistrationV2.tsx handleCalculate 수정 | react/src/components/quotes/QuoteRegistrationV2.tsx | ✅ | -| 2026-01-26 | 수정 | FormulaEvaluatorService.php process_group 추가 | api/app/Services/Quote/FormulaEvaluatorService.php | ✅ | -| 2026-01-26 | 수정 | LocationDetailPanel.tsx bomItemsByTab 수정 | react/src/components/quotes/LocationDetailPanel.tsx | ✅ | -| 2026-01-26 | 수정 | QuoteSummaryPanel.tsx detailTotals 수정 | react/src/components/quotes/QuoteSummaryPanel.tsx | ✅ | -| 2026-01-26 | 수정 | ItemService.php has_bom 필드 추가 | api/app/Services/ItemService.php | ✅ | -| 2026-01-26 | 수정 | actions.ts FinishedGoods에 has_bom, bom 필드 추가 | react/src/components/quotes/actions.ts | ✅ | -| 2026-01-26 | 수정 | QuoteRegistrationV2.tsx DevFill BOM 필터링 | react/src/components/quotes/QuoteRegistrationV2.tsx | ✅ | -| 2026-01-26 | 검증 | 브라우저 테스트 완료 - 4가지 문제 모두 해결 확인 | - | ✅ | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **API 규칙**: `docs/standards/api-rules.md` - ---- - -## 8. 검증 결과 - -> 브라우저 자동화 테스트 완료 (2026-01-26) - -### 8.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| DevFill 후 자동 견적 산출 | 제품 리스트 표시 | 볼트 M10×40, 너트 M10, 볼트 M8×30 등 6개 품목 표시 | ✅ | -| 개소 선택 | 개소별 합계 표시 | 1F / SS-01 상세소계: 3,119,555.94원 | ✅ | -| 그룹별 합계 | 상세별 합계 표시 | 절곡 공정: 735,891.24원, 철재 공정: 2,383,364.7원 | ✅ | -| 전체 금액 | 예상 견적금액 > 0 | 예상 견적금액: 3,119,555.94원 | ✅ | - -### 8.2 테스트 환경 - -- **URL**: `http://dev.sam.kr/sales/quote-management/test-new` -- **테스트 방법**: Claude-in-Chrome 브라우저 자동화 -- **데이터**: DevFill로 생성된 테스트 데이터 - -### 8.3 추가 발견 및 해결 사항 - -테스트 중 DevFill이 BOM 없는 제품을 선택하여 계산 결과가 0으로 나오는 문제 발견: - -| 문제 | 원인 | 해결 | -|------|------|------| -| DevFill 후 bomItemsCount: 0 | BOM 없는 제품 선택 | DevFill에서 BOM 있는 제품만 필터링 | -| has_bom 필드 없음 | API 응답에 미포함 | ItemService.php에서 계산 필드 추가 | -| getFinishedGoods에서 필드 누락 | 매핑 시 has_bom, bom 미포함 | FinishedGoods 인터페이스 및 매핑 수정 | - -### 8.4 최종 검증 결과 - -``` -[DevFill] BOM 있는 제품: 15개 / 전체: 2017개 -[BOM 계산 결과] -- bomItemsCount: 6 -- bomGrandTotal: 3,119,555.94 -- 공정별 그룹: 절곡, 철재 -``` - -**모든 4가지 UI 문제 해결 확인 완료** ✅ - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/react-fcm-push-notification-plan.md b/plans/archive/react-fcm-push-notification-plan.md deleted file mode 100644 index 7583ba8..0000000 --- a/plans/archive/react-fcm-push-notification-plan.md +++ /dev/null @@ -1,543 +0,0 @@ -# React FCM 푸시 알림 연동 계획 - -> **작성일**: 2025-12-30 -> **목적**: Capacitor 앱 웹뷰가 dev.sam.kr (Next.js)을 로드할 때 FCM 푸시 알림 지원 -> **기준 문서**: mng/public/js/fcm.js (포팅 대상), api/app/Swagger/v1/PushApi.php -> **상태**: ✅ 구현 완료 (Serena ID: react-fcm-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4: 통합 완료 | -| **다음 작업** | 테스트 (Capacitor 앱에서 확인) | -| **진행률** | 4/4 (100%) ✅ | -| **마지막 업데이트** | 2025-12-30 | - ---- - -## 1. 개요 - -### 1.1 현재 구조 - -``` -Capacitor 앱 (웹뷰) - │ - ▼ - mng (현재) - │ - ├── fcm.js 로드 - │ ├── Capacitor PushNotifications 사용 - │ ├── 토큰 발급 - │ └── api에 토큰 등록 - │ - ▼ - api - │ - └── /push/register-token -``` - -### 1.2 목표 구조 - -``` -Capacitor 앱 (웹뷰) - │ - ▼ - dev.sam.kr (react) ← 변경 - │ - ├── FCM 훅/유틸리티 (포팅) - │ ├── Capacitor PushNotifications 사용 (동일) - │ ├── 토큰 발급 (동일) - │ └── api에 토큰 등록 (동일) - │ - ▼ - api (변경 없음) - │ - └── /push/register-token -``` - -### 1.3 핵심 포인트 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심: mng/public/js/fcm.js를 react에 포팅 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. Capacitor PushNotifications 플러그인 사용 (동일) │ -│ 2. 토큰 발급 → api 등록 로직 (동일) │ -│ 3. 포그라운드 알림 → sonner 토스트로 변경 │ -│ 4. 백엔드 API 변경 없음 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | Capacitor 플러그인 설치, 훅 생성, 유틸리티 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 포그라운드 알림 UX (토스트 디자인) | **필수** | -| 🔴 금지 | API 엔드포인트 변경, DB 스키마 변경 | 별도 협의 | - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: Capacitor 플러그인 설치 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | @capacitor/push-notifications 설치 | ✅ | ^8.0.0 | -| 1.2 | @capacitor/app 설치 | ✅ | ^8.0.0 | -| 1.3 | @capacitor/core 설치 | ✅ | ^8.0.0 | - -### 2.2 Phase 2: FCM 유틸리티 포팅 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | lib/capacitor/fcm.ts 생성 | ✅ | 9.1KB | -| 2.2 | useFCM 훅 생성 | ✅ | 3.3KB | -| 2.3 | FCM Provider 생성 | ✅ | contexts/FCMProvider.tsx | - -### 2.3 Phase 3: 포그라운드 알림 UI ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | sonner 토스트 연동 | ✅ | useFCM에서 처리 | -| 3.2 | 알림 사운드 재생 | ✅ | public/sounds/ | -| 3.3 | 클릭 시 URL 이동 | ✅ | window.location.href | - -### 2.4 Phase 4: 통합 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | layout.tsx에 FCMProvider 추가 | ✅ | (protected)/layout.tsx | -| 4.2 | 로그아웃 시 토큰 해제 | ✅ | logout.ts 수정 | -| 4.3 | 토큰 등록 테스트 | ⏳ | Capacitor 앱에서 확인 필요 | -| 4.4 | 포그라운드/백그라운드 알림 테스트 | ⏳ | Capacitor 앱에서 확인 필요 | - ---- - -## 3. 기술 상세 - -### 3.1 기존 mng/public/js/fcm.js 분석 - -```javascript -// 핵심 기능 요약 -1. Capacitor 네이티브 환경 체크 (ios/android) -2. PushNotifications.requestPermissions() - 권한 요청 -3. PushNotifications.register() - 토큰 발급 -4. registration 이벤트 → api에 토큰 등록 -5. pushNotificationReceived → 포그라운드 알림 (토스트 + 사운드) -6. pushNotificationActionPerformed → 알림 클릭 시 URL 이동 -``` - -### 3.2 FCM 유틸리티 (포팅) - -```typescript -// src/lib/capacitor/fcm.ts -import { Capacitor } from '@capacitor/core'; -import { PushNotifications } from '@capacitor/push-notifications'; -import { App } from '@capacitor/app'; - -const CONFIG = { - apiBaseUrl: process.env.NEXT_PUBLIC_API_URL || 'https://api.codebridge-x.com', - fcmTokenKey: 'fcm_token', - soundBasePath: '/sounds/', - defaultSound: 'default', -}; - -let isAppForeground = true; - -/** - * FCM 초기화 (Capacitor 네이티브 환경에서만 동작) - */ -export async function initializeFCM( - accessToken: string, - onForegroundNotification?: (notification: PushNotification) => void -): Promise { - // 네이티브 환경 체크 - const platform = Capacitor.getPlatform(); - if (platform !== 'ios' && platform !== 'android') { - console.log('[FCM] Not running in native app'); - return false; - } - - if (!Capacitor.isPluginAvailable('PushNotifications')) { - console.log('[FCM] PushNotifications plugin not available'); - return false; - } - - try { - // 앱 상태 리스너 - App.addListener('appStateChange', ({ isActive }) => { - isAppForeground = isActive; - console.log('[FCM] App state:', isActive ? 'foreground' : 'background'); - }); - - // 기존 리스너 제거 - await PushNotifications.removeAllListeners(); - - // 리스너 등록 - PushNotifications.addListener('registration', async (token) => { - console.log('[FCM] Token received:', token.value?.substring(0, 20) + '...'); - await handleTokenRegistration(token.value, accessToken); - }); - - PushNotifications.addListener('registrationError', (err) => { - console.error('[FCM] Registration error:', err); - }); - - PushNotifications.addListener('pushNotificationReceived', (notification) => { - console.log('[FCM] Push received (foreground):', notification); - if (onForegroundNotification) { - onForegroundNotification(notification); - } - handleForegroundSound(notification); - }); - - PushNotifications.addListener('pushNotificationActionPerformed', (action) => { - console.log('[FCM] Push action performed:', action); - const url = action.notification?.data?.url; - if (url) { - window.location.href = url; - } - }); - - // 권한 요청 - const perm = await PushNotifications.requestPermissions(); - console.log('[FCM] Push permission:', perm.receive); - - if (perm.receive !== 'granted') { - console.log('[FCM] Push permission not granted'); - return false; - } - - // 토큰 발급 요청 - await PushNotifications.register(); - return true; - - } catch (error) { - console.error('[FCM] Initialization error:', error); - return false; - } -} - -/** - * 토큰 등록 처리 - */ -async function handleTokenRegistration(newToken: string, accessToken: string): Promise { - const oldToken = sessionStorage.getItem(CONFIG.fcmTokenKey); - - if (oldToken === newToken) { - console.log('[FCM] Token unchanged, skip'); - return; - } - - const success = await registerTokenToServer(newToken, accessToken); - - if (success) { - sessionStorage.setItem(CONFIG.fcmTokenKey, newToken); - console.log('[FCM] Token saved to sessionStorage'); - } -} - -/** - * 서버에 토큰 등록 - */ -async function registerTokenToServer(token: string, accessToken: string): Promise { - try { - const response = await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/register-token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - token, - platform: Capacitor.getPlatform(), - device_name: navigator.userAgent?.substring(0, 100) || null, - app_version: process.env.NEXT_PUBLIC_APP_VERSION || null, - }), - }); - - if (response.ok) { - console.log('[FCM] Token registered successfully'); - return true; - } - - console.error('[FCM] Token registration failed:', response.status); - return false; - - } catch (error) { - console.error('[FCM] Failed to send token:', error); - return false; - } -} - -/** - * 토큰 해제 (로그아웃 시) - */ -export async function unregisterFCMToken(accessToken?: string): Promise { - const token = sessionStorage.getItem(CONFIG.fcmTokenKey); - if (!token) return true; - - try { - if (accessToken) { - await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/unregister-token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - body: JSON.stringify({ token }), - }); - } - } catch (e) { - console.warn('[FCM] Unregister failed'); - } - - sessionStorage.removeItem(CONFIG.fcmTokenKey); - return true; -} - -/** - * 포그라운드 사운드 재생 - */ -function handleForegroundSound(notification: any): void { - if (!isAppForeground) return; - - const soundKey = notification.data?.sound_key; - if (!soundKey) return; - - try { - const audio = new Audio(`${CONFIG.soundBasePath}${soundKey}.wav`); - audio.volume = 0.5; - audio.play().catch(() => { - // 기본 사운드 시도 - const defaultAudio = new Audio(`${CONFIG.soundBasePath}${CONFIG.defaultSound}.wav`); - defaultAudio.volume = 0.5; - defaultAudio.play().catch(() => {}); - }); - } catch (err) { - console.warn('[FCM] Sound error:', err); - } -} - -/** - * Capacitor 네이티브 환경인지 확인 - */ -export function isCapacitorNative(): boolean { - const platform = Capacitor.getPlatform(); - return platform === 'ios' || platform === 'android'; -} - -// 타입 정의 -export interface PushNotification { - title?: string; - body?: string; - data?: { - type?: string; - url?: string; - sound_key?: string; - }; -} -``` - -### 3.3 useFCM 훅 - -```typescript -// src/hooks/useFCM.ts -'use client'; - -import { useEffect, useRef } from 'react'; -import { useSession } from 'next-auth/react'; -import { toast } from 'sonner'; -import { - initializeFCM, - unregisterFCMToken, - isCapacitorNative, - PushNotification, -} from '@/lib/capacitor/fcm'; - -export function useFCM() { - const { data: session } = useSession(); - const initialized = useRef(false); - - useEffect(() => { - // 네이티브 환경이 아니면 무시 - if (!isCapacitorNative()) return; - - // 로그인 안 됐으면 무시 - if (!session?.accessToken) return; - - // 이미 초기화됐으면 무시 - if (initialized.current) return; - - initialized.current = true; - - // FCM 초기화 - initializeFCM(session.accessToken, handleForegroundNotification); - - // 클린업 (로그아웃 시) - return () => { - // 로그아웃 시 토큰 해제는 별도 처리 - }; - }, [session?.accessToken]); - - // 포그라운드 알림 핸들러 - function handleForegroundNotification(notification: PushNotification) { - const { title, body, data } = notification; - const type = data?.type || 'default'; - const url = data?.url; - - // 타입별 토스트 스타일 - const toastFn = getToastFunction(type); - - toastFn(title || '알림', { - description: body, - action: url ? { - label: '보기', - onClick: () => { - window.location.href = url; - }, - } : undefined, - duration: 5000, - }); - } - - // 타입별 토스트 함수 - function getToastFunction(type: string) { - const errorTypes = ['invoice_failed', 'payment_failed', 'order_cancelled']; - const warningTypes = ['approval_required', 'stock_low']; - const successTypes = ['order_completed', 'payment_completed', 'approval_approved']; - - if (errorTypes.includes(type)) return toast.error; - if (warningTypes.includes(type)) return toast.warning; - if (successTypes.includes(type)) return toast.success; - return toast.info; - } - - // 로그아웃 시 호출 - async function cleanup(accessToken?: string) { - await unregisterFCMToken(accessToken); - initialized.current = false; - } - - return { cleanup }; -} -``` - -### 3.4 FCM Provider - -```typescript -// src/providers/FCMProvider.tsx -'use client'; - -import { useFCM } from '@/hooks/useFCM'; - -export function FCMProvider({ children }: { children: React.ReactNode }) { - // FCM 훅 실행 (초기화) - useFCM(); - - return <>{children}; -} -``` - -### 3.5 레이아웃에 Provider 추가 - -```typescript -// src/app/layout.tsx (또는 적절한 위치) -import { FCMProvider } from '@/providers/FCMProvider'; - -export default function RootLayout({ children }) { - return ( - - - - - {children} - - - - - ); -} -``` - ---- - -## 4. 파일 구조 - -``` -react/ -├── public/ -│ └── sounds/ ← 알림 사운드 (mng에서 복사) -│ ├── default.wav -│ └── *.wav -├── src/ -│ ├── lib/ -│ │ └── capacitor/ -│ │ └── fcm.ts ← 🆕 FCM 핵심 로직 (포팅) -│ ├── hooks/ -│ │ └── useFCM.ts ← 🆕 FCM 훅 -│ └── providers/ -│ └── FCMProvider.tsx ← 🆕 FCM Provider -├── capacitor.config.ts ← 확인/수정 필요 -└── package.json ← Capacitor 플러그인 추가 -``` - ---- - -## 5. 의존성 - -| 패키지 | 버전 | 용도 | -|--------|------|------| -| @capacitor/core | (기존) | Capacitor 코어 | -| @capacitor/push-notifications | ^6.0.0 | 푸시 알림 플러그인 | -| @capacitor/app | ^6.0.0 | 앱 상태 감지 | -| sonner | (기존) | 포그라운드 토스트 | - ---- - -## 6. mng vs react 비교 - -| 항목 | mng (기존) | react (포팅) | -|------|-----------|--------------| -| **FCM 플러그인** | Capacitor PushNotifications | 동일 | -| **토큰 저장** | sessionStorage | 동일 | -| **API 호출** | fetch | 동일 | -| **포그라운드 알림** | showToast (커스텀) | sonner 토스트 | -| **사운드 재생** | Audio API | 동일 | -| **URL 이동** | window.location.href | 동일 (또는 router.push) | - ---- - -## 7. 참고 문서 - -| 문서 | 용도 | -|------|------| -| `mng/public/js/fcm.js` | 포팅 원본 | -| `api/app/Swagger/v1/PushApi.php` | 백엔드 API 스펙 | -| [Capacitor Push Notifications](https://capacitorjs.com/docs/apis/push-notifications) | 공식 문서 | - ---- - -## 8. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 포그라운드 알림 UX | sonner 토스트 디자인/위치 | UX | ⏳ | - ---- - -## 9. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-12-30 | 계획 수립 | 계획 문서 작성 | - | - | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/react-server-component-audit-plan.md b/plans/archive/react-server-component-audit-plan.md deleted file mode 100644 index ae0ce56..0000000 --- a/plans/archive/react-server-component-audit-plan.md +++ /dev/null @@ -1,147 +0,0 @@ -# React 서버 컴포넌트 점검 계획 - -> **작성일**: 2025-01-09 -> **목적**: push하지 않은 작업분 중 서버 컴포넌트를 클라이언트 컴포넌트로 변경 -> **상태**: ✅ 점검 완료 - 수정 불필요 - ---- - -## 📍 점검 결과 요약 - -| 항목 | 내용 | -|------|------| -| **점검 대상** | push하지 않은 커밋 (origin/master..HEAD) | -| **커밋 수** | 20개 | -| **점검 파일 수** | 31개 (tsx/ts 파일) | -| **서버 컴포넌트 발견** | 0개 | -| **수정 필요** | ❌ 없음 | - ---- - -## 1. 점검 배경 - -### 1.1 정책 -- 프론트엔드 정책: **서버 컴포넌트 사용 금지** -- 모든 컴포넌트는 **클라이언트 컴포넌트**로 작성해야 함 -- `'use client'` 지시어 필수 - -### 1.2 점검 범위 -- **대상**: react 폴더의 push하지 않은 작업분 -- **제외**: 이미 push된 커밋 (프론트엔드에서 수정 중) - ---- - -## 2. 점검 대상 파일 - -### 2.1 변경된 TSX 파일 (16개) - -| # | 파일 | 'use client' | 상태 | -|---|------|:------------:|:----:| -| 1 | `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` | ✅ | 정상 | -| 2 | `src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx` | ✅ | 정상 | -| 3 | `src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` | ✅ | 정상 | -| 4 | `src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx` | ✅ | 정상 | -| 5 | `src/app/[locale]/(protected)/sales/order-management-sales/page.tsx` | ✅ | 정상 | -| 6 | `src/components/approval/DocumentCreate/ApprovalLineSection.tsx` | ✅ | 정상 | -| 7 | `src/components/approval/DocumentCreate/ReferenceSection.tsx` | ✅ | 정상 | -| 8 | `src/components/hr/EmployeeManagement/EmployeeForm.tsx` | ✅ | 정상 | -| 9 | `src/components/orders/OrderRegistration.tsx` | ✅ | 정상 | -| 10 | `src/components/orders/QuotationSelectDialog.tsx` | ✅ | 정상 | -| 11 | `src/components/process-management/ProcessDetail.tsx` | ✅ | 정상 | -| 12 | `src/components/process-management/RuleModal.tsx` | ✅ | 정상 | -| 13 | `src/components/production/WorkOrders/SalesOrderSelectModal.tsx` | ✅ | 정상 | -| 14 | `src/components/production/WorkOrders/WorkOrderCreate.tsx` | ✅ | 정상 | -| 15 | `src/components/production/WorkOrders/WorkOrderDetail.tsx` | ✅ | 정상 | -| 16 | `src/components/production/WorkOrders/WorkOrderList.tsx` | ✅ | 정상 | - -### 2.2 변경된 TS 파일 (15개) - 검토 불필요 - -TS 파일은 컴포넌트가 아닌 유틸리티/타입/액션 파일로 서버 컴포넌트 대상 아님: - -- `src/components/business/construction/*/actions.ts` (6개) -- `src/components/orders/actions.ts` -- `src/components/orders/index.ts` -- `src/components/process-management/actions.ts` -- `src/components/production/WorkOrders/actions.ts` -- `src/components/production/WorkOrders/types.ts` -- `src/lib/api/common-codes.ts` -- `src/lib/api/index.ts` -- `src/types/process.ts` -- `src/components/business/construction/site-management/types.ts` - ---- - -## 3. Push하지 않은 커밋 목록 - -``` -311ddd9 docs: Phase D~K 마이그레이션 완료 상태 반영 (95%) -6615f39 feat(order-management): Mock → API 연동 및 common-codes 유틸리티 추가 -d472b77 fix(approval): 결재선/참조 Select 값 변경 불가 버그 수정 -5fa20c8 feat(item-management): Mock → API 연동 완료 -749f0ce feat: 거래처관리 API 연동 (Phase 2.2) -273d570 feat(시공사): 2.1 현장관리 - Frontend API 연동 -78e193c refactor(work-orders): process_type을 process_id FK로 변환 -9d30555 feat(시공사): 1.2 인수인계보고서 - Frontend API 연동 -d15a203 feat(work-orders): 다중 담당자 UI 구현 -8172226 Merge remote-tracking branch 'origin/master' -668cde3 Merge remote-tracking branch 'origin/master' -c651e7b feat(WEB): 수주관리 Phase 3 완료 - 고급 기능 구현 -2d7809b feat: [시공관리] 계약관리 Frontend API 연동 -12b4259 refactor(work-orders): 코드 리뷰 기반 프론트엔드 개선 -fde8726 feat(WEB): 수주관리 Phase 2 타입 정의 확장 및 공정관리 개별 품목 표시 수정 -ba36c0e feat: 공정 관리 Frontend actions 업데이트 -d797868 fix(WEB): 공정관리 개별 품목 저장 안되는 버그 수정 -3d2dea6 feat: 수주 관리 Phase 3 - Frontend API 연동 -6632943 Merge remote-tracking branch 'origin/master' -288871c feat(WEB): 직원 관리 폼 직급/부서/직책 Select 드롭다운 연동 -572ffe8 feat(orders): Phase 2 - Frontend API 연동 완료 -``` - ---- - -## 4. 점검 결론 - -### 4.1 결과 -**✅ 모든 TSX 파일에 'use client' 지시어가 있음** - -push하지 않은 작업분에서 서버 컴포넌트가 발견되지 않았습니다. -모든 컴포넌트가 클라이언트 컴포넌트 정책을 준수하고 있습니다. - -### 4.2 수정 필요 항목 -**없음** - ---- - -## 5. 향후 권장사항 - -### 5.1 새 파일 생성 시 체크리스트 -``` -□ TSX 파일 첫 줄에 'use client' 지시어 추가 -□ page.tsx 파일도 예외 없이 'use client' 필수 -□ layout.tsx 파일도 필요시 'use client' 추가 -``` - -### 5.2 코드 리뷰 시 확인 -- PR 리뷰 시 새 TSX 파일의 'use client' 지시어 확인 -- async 컴포넌트 패턴 지양 (useEffect, React Query 등 사용) - -### 5.3 린트 규칙 고려 -향후 ESLint 커스텀 룰 추가 검토: -```javascript -// .eslintrc.js 예시 -rules: { - 'react/enforce-use-client': 'error' // 커스텀 룰 -} -``` - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | -|------|------|----------| -| 2025-01-09 | 문서 생성 | 서버 컴포넌트 점검 완료, 수정 불필요 확인 | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/sam-stat-database-design-plan.md b/plans/archive/sam-stat-database-design-plan.md deleted file mode 100644 index f63455e..0000000 --- a/plans/archive/sam-stat-database-design-plan.md +++ /dev/null @@ -1,1294 +0,0 @@ -# SAM 통계 시스템 (sam_stat DB) 설계 계획 - -> **작성일**: 2026-01-29 -> **목적**: SAM ERP의 확장 가능한 통계 전용 데이터베이스(sam_stat) 설계 -> **기준 문서**: `docs/specs/database-schema.md`, `docs/architecture/system-overview.md` -> **상태**: ✅ 구현 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 6: 문서화 및 마무리 완료 (Swagger, DB 스키마 문서, 계획 문서 완료 처리) | -| **다음 작업** | ✅ 전체 완료 | -| **진행률** | 6/6 Phase (100%) | -| **마지막 업데이트** | 2026-01-30 | - ---- - -## 0. 프로젝트 컨텍스트 (새 세션용) - -> **이 섹션은 새 세션에서 이 문서만으로 작업을 시작할 수 있도록 필요한 모든 컨텍스트를 포함한다.** - -### 0.1 프로젝트 구조 - -``` -/Users/kent/Works/@KD_SAM/SAM/ -├── api/ ← 작업 대상 (Laravel 12 REST API, PHP 8.4+) -│ ├── app/ -│ │ ├── Console/Commands/ # Artisan 커맨드 (19개 존재) -│ │ ├── Http/Controllers/Api/V1/ # API 컨트롤러 -│ │ ├── Models/ # Eloquent 모델 (167개) -│ │ │ ├── Stats/ # ← 새로 생성할 통계 모델 디렉토리 -│ │ │ ├── Tenants/ # 테넌트 스코프 모델 (가장 많음) -│ │ │ ├── Orders/ # 수주 관련 -│ │ │ ├── Production/ # 생산 관련 -│ │ │ └── ... -│ │ └── Services/ # 비즈니스 로직 (Service-First 아키텍처) -│ │ ├── Stats/ # ← 새로 생성할 통계 서비스 디렉토리 -│ │ ├── DashboardService.php # 기존 대시보드 (355줄, 원본 DB 실시간 집계) -│ │ ├── ReportService.php # 기존 보고서 (일일일보, 지출예상) -│ │ ├── DailyReportService.php # 일일 보고서 (어음, 계좌, 요약) -│ │ ├── AiReportService.php # AI 보고서 -│ │ └── ... -│ ├── config/ -│ │ └── database.php # DB 연결 설정 (mysql, chandj 존재) -│ ├── database/ -│ │ └── migrations/ # 279개 마이그레이션 파일 -│ ├── routes/ -│ │ ├── console.php # 스케줄러 정의 (Laravel 12 방식) -│ │ └── api/v1/ -│ │ ├── common.php # dashboard, reports 라우트 -│ │ ├── finance.php # daily-report 라우트 -│ │ └── ... # 14개 라우트 파일 -│ └── .env # 환경변수 -├── mng/ # 관리자 패널 (Plain Laravel + Blade/Tailwind) -├── react/ # Next.js 15 프론트엔드 -├── docker/ -│ └── docker-compose.yml # Docker 설정 -└── docs/ # 기술 문서 - ├── specs/database-schema.md # DB 스키마 문서 - ├── architecture/system-overview.md - └── plans/ # 이 문서의 위치 -``` - -### 0.2 현재 DB 환경 - -``` -# .env (api/) -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 # Docker 내부: sam-mysql-1 -DB_PORT=3306 -DB_DATABASE=samdb # ← 원본 DB (219개 테이블) -DB_USERNAME=samuser -DB_PASSWORD=sampass - -# sam_stat 연결은 아직 없음 → Phase 1에서 추가 -``` - -**config/database.php 현재 연결:** -- `mysql` - 기본 samdb (원본) -- `chandj` - 5130 레거시 DB (사용하지 않음) -- `sam_stat` - **아직 없음** (이 작업에서 추가) - -### 0.3 기존 대시보드/보고서 시스템 (변경 대상) - -| 파일 | 경로 | 역할 | 통계 전환 시 영향 | -|------|------|------|------------------| -| DashboardController | `api/app/Http/Controllers/Api/V1/DashboardController.php` | summary, charts, approvals | Phase 4.5에서 sam_stat 조회로 전환 | -| ReportController | `api/app/Http/Controllers/Api/V1/ReportController.php` | daily, expense-estimate, export | Phase 4.5에서 sam_stat 조회로 전환 | -| DailyReportController | `api/app/Http/Controllers/Api/V1/DailyReportController.php` | note-receivables, accounts, summary | Phase 4.5에서 sam_stat 조회로 전환 | -| DashboardService | `api/app/Services/DashboardService.php` (355줄) | 원본 DB에서 실시간 집계 (Attendance, Approval, Deposit, Sale 등) | **핵심 전환 대상** | -| ReportService | `api/app/Services/ReportService.php` | 일일일보, 지출예상 (Excel 내보내기 포함) | 부분 전환 | -| DailyReportService | `api/app/Services/DailyReportService.php` | 어음/외상채권, 계좌현황 | 부분 전환 | -| AiReportService | `api/app/Services/AiReportService.php` | AI 보고서 생성/조회 | 변경 없음 | - -**현재 API 라우트 (변경 없음, 내부 데이터소스만 전환):** -``` -# common.php -GET /api/v1/dashboard/summary → DashboardController@summary -GET /api/v1/dashboard/charts → DashboardController@charts -GET /api/v1/dashboard/approvals → DashboardController@approvals -GET /api/v1/reports/daily → ReportController@daily -GET /api/v1/reports/daily/export → ReportController@dailyExport -GET /api/v1/reports/expense-estimate → ReportController@expenseEstimate - -# finance.php -GET /api/v1/daily-report/note-receivables → DailyReportController@noteReceivables -GET /api/v1/daily-report/daily-accounts → DailyReportController@dailyAccounts -GET /api/v1/daily-report/summary → DailyReportController@summary -``` - -### 0.4 기존 스케줄러 패턴 (따라야 할 패턴) - -```php -// api/routes/console.php (Laravel 12 방식 - Kernel.php 없음) -use Illuminate\Support\Facades\Schedule; - -// 기존 스케줄러: 매일 03:00 API 로그 정리 -Schedule::command('api-log:prune') - ->dailyAt('03:00') - ->appendOutputTo(storage_path('logs/scheduler.log')) - ->onSuccess(function () { Log::info('...'); }) - ->onFailure(function () { Log::error('...'); }); -``` - -### 0.5 기존 Artisan 커맨드 패턴 - -``` -api/app/Console/Commands/ -├── PruneAuditLogs.php # 감사 로그 정리 (참고 패턴) -├── CleanupExpiredLinks.php # 만료 링크 정리 -├── RecordStorageUsage.php # 저장소 사용량 기록 -├── TenantsBootstrap.php # 테넌트 초기화 -└── ... # 총 19개 -``` - -### 0.6 모델 패턴 (따라야 할 패턴) - -```php -// 기존 모델 예시 - 멀티테넌트 + Soft Delete -namespace App\Models\Tenants; - -use App\Models\Scopes\TenantScope; -use Illuminate\Database\Eloquent\SoftDeletes; - -class Deposit extends Model -{ - use SoftDeletes; - - protected $table = 'deposits'; - - protected static function booted(): void - { - static::addGlobalScope(new TenantScope); - } -} - -// 통계 모델은 다른 DB 연결 사용 -// protected $connection = 'sam_stat'; -// TenantScope 대신 tenant_id를 직접 WHERE 조건으로 사용 -``` - -### 0.7 환경별 구성 - -#### 로컬 환경 (Docker) - -```yaml -# docker/docker-compose.yml 내 MySQL 서비스 -# Docker 내부 호스트: sam-mysql-1 -# sam_stat DB는 같은 MySQL 인스턴스에 생성 (별도 서버 불필요) -``` - -```bash -# 로컬 sam_stat DB 생성 -docker compose exec mysql mysql -u root -proot \ - -e "CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - -# 로컬 마이그레이션 실행 -docker compose exec api php artisan migrate --database=sam_stat - -# 로컬 시딩 -docker compose exec api php artisan db:seed --class=DimDateSeeder -``` - -#### 개발 서버 (non-Docker, codebridge-x.com) - -> **개발 서버는 Docker를 사용하지 않는다.** -> 로컬에서 코드 작업 후 Git push하면 되지만, 개발 서버에서 아래 **1회 세팅이 필요**하다. - -```bash -# 1. sam_stat DB 생성 (개발 서버 MySQL 직접 접속) -mysql -u [user] -p \ - -e "CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - -# 2. .env에 STAT_DB_* 환경변수 추가 (개발 서버의 api/.env) -# STAT_DB_HOST=127.0.0.1 -# STAT_DB_PORT=3306 -# STAT_DB_DATABASE=sam_stat -# STAT_DB_USERNAME=[개발서버 DB 유저] -# STAT_DB_PASSWORD=[개발서버 DB 비밀번호] - -# 3. 마이그레이션 실행 -cd /path/to/api && php artisan migrate --database=sam_stat - -# 4. dim_date 시딩 -php artisan db:seed --class=DimDateSeeder - -# 5. 스케줄러 cron 확인 (이미 등록되어 있다면 추가 불필요) -# * * * * * cd /path/to/api && php artisan schedule:run >> /dev/null 2>&1 -``` - -#### 배포 워크플로우 - -``` -로컬 (Docker, *.sam.kr) - ↓ Git push -개발 서버 (non-Docker, codebridge-x.com) - ↓ 수동 배포 - ↓ 최초 1회: DB 생성 + .env + migrate + seed + cron 확인 - ↓ 이후: git pull → php artisan migrate --database=sam_stat -운영 (TBD) -``` - -**코드에 커밋되는 것:** `config/database.php`, 마이그레이션, 모델, 서비스, 커맨드 -**환경별 수동 설정:** `.env` (STAT_DB_*), DB 생성, cron - -### 0.8 핵심 코딩 규칙 (이 작업에 적용) - -1. **Service-First**: 비즈니스 로직 → Service, Controller는 DI + 호출만 -2. **FormRequest**: Controller에서 직접 검증 금지 -3. **BelongsToTenant**: 원본 모델만 적용, 통계 모델은 tenant_id WHERE 직접 사용 -4. **i18n**: 메시지는 `__('message.xxx')` 형태 -5. **ApiResponse**: `use App\Helpers\ApiResponse;` → `ApiResponse::handle()` -6. **Swagger**: 별도 파일 `api/app/Swagger/v1/{Resource}Api.php`에 작성 -7. **커밋**: 사용자 승인 후에만 커밋 (자동 커밋 금지) - -### 0.9 작업 시작 체크리스트 - -``` -새 세션에서 이 문서를 받았을 때: - -□ 1. 이 문서의 "📍 현재 진행 상태" 확인 -□ 2. Phase별 작업 상태 (⏳/🔄/✅) 확인 -□ 3. Docker 실행 확인: docker compose ps (docker/ 디렉토리) -□ 4. DB 접속 확인: docker compose exec mysql mysql -u root -proot samdb -□ 5. sam_stat DB 존재 여부 확인: SHOW DATABASES LIKE 'sam_stat'; -□ 6. 마이그레이션 상태 확인: cd api && php artisan migrate:status -□ 7. 다음 작업 항목의 "비고" 컬럼 참조하여 작업 시작 -``` - ---- - -## 1. 개요 - -### 1.1 배경 - -SAM ERP는 219개 테이블, 17개 비즈니스 도메인을 가진 종합 제조/건설 ERP 시스템이다. -현재 대시보드(DashboardService, ReportService 등)는 **원본 DB(samdb)에서 실시간 집계**하는 방식으로 동작한다. - -**문제점:** -- 원본 DB에 집계 쿼리 부하 (JOIN, GROUP BY, SUM 등) -- 과거 데이터 추세 분석 불가 (스냅샷 없음) -- 도메인별 KPI 누적 관리 불가 -- 대시보드 응답 속도 저하 가능성 -- 통계 요구사항 증가 시 원본 스키마 오염 - -**해결 방안:** -- `sam_stat` 별도 DB에 사전 집계(pre-aggregated) 통계 데이터 저장 -- 배치/스케줄러로 원본(samdb) → 통계(sam_stat) DB 동기화 -- 원본 DB 부하 분리, 빠른 조회, 이력 보존 - -### 1.2 설계 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 원본 DB 무간섭 - sam_stat은 읽기 전용 파생 데이터 │ -│ 2. 멀티테넌트 유지 - 모든 통계 테이블에 tenant_id 필수 │ -│ 3. 시간축 기반 - 일/주/월/분기/년 단위 집계 지원 │ -│ 4. 확장 가능 - 새 도메인 통계 추가 시 테이블만 추가 │ -│ 5. 멱등성 보장 - 같은 기간 재집계 시 동일 결과 (UPSERT) │ -│ 6. 메타데이터 드리븐 - stat_definitions로 동적 통계 정의 가능 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 통계 필드 추가, 집계 주기 변경, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 새 통계 테이블 생성, 스케줄러 추가, 마이그레이션 | **필수** | -| 🔴 금지 | 원본 DB 스키마 변경, 원본 테이블에 통계 컬럼 추가 | 별도 협의 | - ---- - -## 2. 분석: 필요한 통계 도메인 - -SAM의 17개 비즈니스 도메인을 분석하여 8개 핵심 통계 영역을 도출했다. - -### 2.1 통계 도메인 매핑 - -| # | 통계 도메인 | 원본 테이블 | 핵심 지표 | 우선순위 | -|---|-----------|-----------|----------|---------| -| 1 | **매출/수주** | orders, order_items, sales, clients | 수주액, 매출액, 수주건수, 고객별 매출 | 🔴 P0 | -| 2 | **재무/회계** | deposits, withdrawals, purchases, bills, bank_transactions | 입출금, 미수/미지급, 자금흐름, 어음현황 | 🔴 P0 | -| 3 | **생산/작업** | work_orders, work_order_items, work_results | 생산량, 작업효율, 불량률, 납기준수율 | 🔴 P0 | -| 4 | **재고/자재** | stocks, stock_transactions, material_receipts, shipments | 재고회전율, 입출고량, 안전재고, 로트추적 | 🟡 P1 | -| 5 | **견적/영업** | quotes, quote_items, sales_prospects, biddings | 수주전환율, 견적성공률, 영업파이프라인 | 🟡 P1 | -| 6 | **인사/근태** | attendance, leaves, payrolls, salaries | 출근율, 근태현황, 인건비, 부서별통계 | 🟡 P1 | -| 7 | **건설/프로젝트** | sites, contracts, expected_expenses, labor_distributions | 프로젝트수익률, 공정진행률, 원가분석 | 🟢 P2 | -| 8 | **시스템/감사** | audit_logs, api_request_logs, fcm_send_logs | API사용량, 사용자활동, 알림발송률 | 🟢 P2 | - ---- - -## 3. sam_stat 데이터베이스 설계 - -### 3.1 아키텍처 개요 - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ sam_stat DB │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ -│ │ 메타 테이블 (2) │ │ 이벤트/팩트 테이블 (2) │ │ -│ │ │ │ │ │ -│ │ stat_definitions │ │ stat_events │ │ -│ │ stat_job_logs │ │ stat_snapshots │ │ -│ └─────────────────────┘ └─────────────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 도메인별 집계 테이블 (8 도메인) │ │ -│ │ │ │ -│ │ stat_sales_daily stat_inventory_daily │ │ -│ │ stat_finance_daily stat_quote_pipeline_daily │ │ -│ │ stat_production_daily stat_hr_attendance_daily │ │ -│ │ stat_project_monthly stat_system_daily │ │ -│ │ │ │ -│ │ 요약 테이블 (월간/연간) │ │ -│ │ │ │ -│ │ stat_sales_monthly stat_finance_monthly │ │ -│ │ stat_production_monthly stat_kpi_monthly │ │ -│ │ │ │ -│ └───────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ -│ │ 차원 테이블 (Dim) │ │ KPI/알림 테이블 │ │ -│ │ │ │ │ │ -│ │ dim_date │ │ stat_kpi_targets │ │ -│ │ dim_client │ │ stat_alerts │ │ -│ │ dim_product │ │ │ │ -│ └─────────────────────┘ └─────────────────────────────────┘ │ -│ │ -│ 총 테이블: 18개 │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 데이터 흐름 - -``` -samdb (원본) sam_stat (통계) -┌──────────┐ ┌──────────────┐ -│ orders │──┐ │ │ -│ sales │──┤ Scheduler │ stat_sales_ │ -│ deposits │──┼──(매일 02:00)──→│ daily │ -│ stocks │──┤ │ │ -│ work_ │──┤ │ stat_finance_│ -│ orders │──┘ │ daily │ -│ │ │ │ -│ │ Scheduler │ stat_*_ │ -│ │──(매월 1일)──────→│ monthly │ -│ │ │ │ -│ │ 실시간 이벤트 │ stat_events │ -│ │──(Observer)─────→│ │ -└──────────┘ └──────────────┘ -``` - ---- - -## 4. 테이블 상세 설계 - -### 4.1 메타 테이블 - -#### `stat_definitions` - 통계 정의 (메타데이터 드리븐) - -```sql -CREATE TABLE stat_definitions ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(100) NOT NULL UNIQUE, -- 'sales_daily_revenue' - domain VARCHAR(50) NOT NULL, -- 'sales', 'finance', 'production' - name VARCHAR(200) NOT NULL, -- '일일 매출액' - description TEXT NULL, - source_tables JSON NOT NULL, -- ["orders", "order_items", "sales"] - aggregation VARCHAR(20) NOT NULL DEFAULT 'daily', -- daily, weekly, monthly, quarterly, yearly - query_template TEXT NULL, -- 집계 SQL 템플릿 (선택) - is_active BOOLEAN NOT NULL DEFAULT TRUE, - config JSON NULL, -- 추가 설정 (임계값, 단위 등) - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - INDEX idx_domain (domain), - INDEX idx_aggregation (aggregation), - INDEX idx_active (is_active) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_job_logs` - 집계 작업 이력 - -```sql -CREATE TABLE stat_job_logs ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - job_type VARCHAR(100) NOT NULL, -- 'sales_daily', 'finance_monthly' - target_date DATE NOT NULL, -- 집계 대상 날짜 - status ENUM('pending','running','completed','failed') NOT NULL DEFAULT 'pending', - records_processed INT UNSIGNED DEFAULT 0, - error_message TEXT NULL, - started_at TIMESTAMP NULL, - completed_at TIMESTAMP NULL, - duration_ms INT UNSIGNED NULL, - created_at TIMESTAMP NULL, - - INDEX idx_tenant_job (tenant_id, job_type), - INDEX idx_status (status), - INDEX idx_target_date (target_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.2 차원 테이블 (Dimension) - -#### `dim_date` - 날짜 차원 - -```sql -CREATE TABLE dim_date ( - date_key DATE PRIMARY KEY, -- '2026-01-29' - year SMALLINT NOT NULL, - quarter TINYINT NOT NULL, -- 1~4 - month TINYINT NOT NULL, - week TINYINT NOT NULL, -- ISO week - day_of_week TINYINT NOT NULL, -- 1(월)~7(일) - day_of_month TINYINT NOT NULL, - is_weekend BOOLEAN NOT NULL, - is_holiday BOOLEAN NOT NULL DEFAULT FALSE, - holiday_name VARCHAR(100) NULL, - fiscal_year SMALLINT NULL, -- 회계연도 - fiscal_quarter TINYINT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `dim_client` - 고객 차원 (스냅샷) - -```sql -CREATE TABLE dim_client ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - client_id BIGINT UNSIGNED NOT NULL, -- 원본 clients.id - client_name VARCHAR(200) NOT NULL, - client_group_id BIGINT UNSIGNED NULL, - client_group_name VARCHAR(200) NULL, - client_type VARCHAR(50) NULL, -- 고객/공급업체/양쪽 - region VARCHAR(100) NULL, - valid_from DATE NOT NULL, - valid_to DATE NULL, -- NULL = 현재 유효 - is_current BOOLEAN NOT NULL DEFAULT TRUE, - - INDEX idx_tenant_client (tenant_id, client_id), - INDEX idx_current (is_current) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `dim_product` - 제품 차원 (스냅샷) - -```sql -CREATE TABLE dim_product ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - product_id BIGINT UNSIGNED NOT NULL, -- 원본 products.id - product_code VARCHAR(100) NOT NULL, - product_name VARCHAR(300) NOT NULL, - product_type VARCHAR(50) NULL, -- PRODUCT/PART/SUBASSEMBLY - category_id BIGINT UNSIGNED NULL, - category_name VARCHAR(200) NULL, - valid_from DATE NOT NULL, - valid_to DATE NULL, - is_current BOOLEAN NOT NULL DEFAULT TRUE, - - INDEX idx_tenant_product (tenant_id, product_id), - INDEX idx_current (is_current) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.3 도메인별 집계 테이블 (Fact) - -#### 🔴 P0: `stat_sales_daily` - 매출/수주 일일 통계 - -```sql -CREATE TABLE stat_sales_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 수주 - order_count INT UNSIGNED DEFAULT 0, -- 신규 수주 건수 - order_amount DECIMAL(18,2) DEFAULT 0, -- 수주 금액 - order_item_count INT UNSIGNED DEFAULT 0, -- 수주 품목 수 - - -- 매출 - sales_count INT UNSIGNED DEFAULT 0, -- 매출 건수 - sales_amount DECIMAL(18,2) DEFAULT 0, -- 매출 금액 - sales_tax_amount DECIMAL(18,2) DEFAULT 0, -- 세액 - - -- 고객 - new_client_count INT UNSIGNED DEFAULT 0, -- 신규 고객 수 - active_client_count INT UNSIGNED DEFAULT 0, -- 활성 고객 수 - - -- 수주 상태별 건수 - order_draft_count INT UNSIGNED DEFAULT 0, - order_confirmed_count INT UNSIGNED DEFAULT 0, - order_in_progress_count INT UNSIGNED DEFAULT 0, - order_completed_count INT UNSIGNED DEFAULT 0, - order_cancelled_count INT UNSIGNED DEFAULT 0, - - -- 출하 - shipment_count INT UNSIGNED DEFAULT 0, - shipment_amount DECIMAL(18,2) DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date), - INDEX idx_tenant (tenant_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🔴 P0: `stat_finance_daily` - 재무 일일 통계 - -```sql -CREATE TABLE stat_finance_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 입출금 - deposit_count INT UNSIGNED DEFAULT 0, - deposit_amount DECIMAL(18,2) DEFAULT 0, - withdrawal_count INT UNSIGNED DEFAULT 0, - withdrawal_amount DECIMAL(18,2) DEFAULT 0, - net_cashflow DECIMAL(18,2) DEFAULT 0, -- 입금 - 출금 - - -- 매입 - purchase_count INT UNSIGNED DEFAULT 0, - purchase_amount DECIMAL(18,2) DEFAULT 0, - purchase_tax_amount DECIMAL(18,2) DEFAULT 0, - - -- 미수/미지급 - receivable_balance DECIMAL(18,2) DEFAULT 0, -- 미수금 잔액 - payable_balance DECIMAL(18,2) DEFAULT 0, -- 미지급 잔액 - overdue_receivable DECIMAL(18,2) DEFAULT 0, -- 연체 미수금 - - -- 어음 - bill_issued_count INT UNSIGNED DEFAULT 0, - bill_issued_amount DECIMAL(18,2) DEFAULT 0, - bill_matured_count INT UNSIGNED DEFAULT 0, - bill_matured_amount DECIMAL(18,2) DEFAULT 0, - - -- 카드 - card_transaction_count INT UNSIGNED DEFAULT 0, - card_transaction_amount DECIMAL(18,2) DEFAULT 0, - - -- 은행 - bank_balance_total DECIMAL(18,2) DEFAULT 0, -- 전 계좌 잔액 합계 - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🔴 P0: `stat_production_daily` - 생산 일일 통계 - -```sql -CREATE TABLE stat_production_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 작업지시 - wo_created_count INT UNSIGNED DEFAULT 0, -- 신규 작업지시 - wo_completed_count INT UNSIGNED DEFAULT 0, -- 완료 작업지시 - wo_in_progress_count INT UNSIGNED DEFAULT 0, -- 진행중 - wo_overdue_count INT UNSIGNED DEFAULT 0, -- 납기 초과 - - -- 생산량 - production_qty DECIMAL(18,2) DEFAULT 0, -- 생산 수량 - defect_qty DECIMAL(18,2) DEFAULT 0, -- 불량 수량 - defect_rate DECIMAL(5,2) DEFAULT 0, -- 불량률 (%) - - -- 작업 효율 - planned_hours DECIMAL(10,2) DEFAULT 0, -- 계획 공수 - actual_hours DECIMAL(10,2) DEFAULT 0, -- 실적 공수 - efficiency_rate DECIMAL(5,2) DEFAULT 0, -- 효율 (%) - - -- 작업자 - active_worker_count INT UNSIGNED DEFAULT 0, - issue_count INT UNSIGNED DEFAULT 0, -- 발생 이슈 수 - - -- 납기 - on_time_delivery_count INT UNSIGNED DEFAULT 0, - late_delivery_count INT UNSIGNED DEFAULT 0, - delivery_rate DECIMAL(5,2) DEFAULT 0, -- 납기준수율 (%) - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟡 P1: `stat_inventory_daily` - 재고 일일 통계 - -```sql -CREATE TABLE stat_inventory_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 재고 현황 - total_sku_count INT UNSIGNED DEFAULT 0, -- 총 SKU 수 - total_stock_qty DECIMAL(18,2) DEFAULT 0, -- 총 재고 수량 - total_stock_value DECIMAL(18,2) DEFAULT 0, -- 총 재고 금액 - - -- 입출고 - receipt_count INT UNSIGNED DEFAULT 0, -- 입고 건수 - receipt_qty DECIMAL(18,2) DEFAULT 0, - receipt_amount DECIMAL(18,2) DEFAULT 0, - issue_count INT UNSIGNED DEFAULT 0, -- 출고 건수 - issue_qty DECIMAL(18,2) DEFAULT 0, - issue_amount DECIMAL(18,2) DEFAULT 0, - - -- 안전재고 - below_safety_count INT UNSIGNED DEFAULT 0, -- 안전재고 미달 품목 수 - zero_stock_count INT UNSIGNED DEFAULT 0, -- 재고 0 품목 수 - excess_stock_count INT UNSIGNED DEFAULT 0, -- 과잉 재고 품목 수 - - -- 품질검사 - inspection_count INT UNSIGNED DEFAULT 0, - inspection_pass_count INT UNSIGNED DEFAULT 0, - inspection_fail_count INT UNSIGNED DEFAULT 0, - inspection_pass_rate DECIMAL(5,2) DEFAULT 0, -- 합격률 (%) - - -- 재고회전 - turnover_rate DECIMAL(8,2) DEFAULT 0, -- 재고회전율 - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟡 P1: `stat_quote_pipeline_daily` - 견적/영업 일일 통계 - -```sql -CREATE TABLE stat_quote_pipeline_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 견적 - quote_created_count INT UNSIGNED DEFAULT 0, - quote_amount DECIMAL(18,2) DEFAULT 0, - quote_approved_count INT UNSIGNED DEFAULT 0, - quote_rejected_count INT UNSIGNED DEFAULT 0, - quote_conversion_count INT UNSIGNED DEFAULT 0, -- 수주 전환 건수 - quote_conversion_rate DECIMAL(5,2) DEFAULT 0, -- 전환율 (%) - - -- 영업 기회 - prospect_created_count INT UNSIGNED DEFAULT 0, - prospect_won_count INT UNSIGNED DEFAULT 0, - prospect_lost_count INT UNSIGNED DEFAULT 0, - prospect_amount DECIMAL(18,2) DEFAULT 0, -- 파이프라인 금액 - - -- 입찰 - bidding_count INT UNSIGNED DEFAULT 0, - bidding_won_count INT UNSIGNED DEFAULT 0, - bidding_amount DECIMAL(18,2) DEFAULT 0, - - -- 상담 - consultation_count INT UNSIGNED DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟡 P1: `stat_hr_attendance_daily` - 인사/근태 일일 통계 - -```sql -CREATE TABLE stat_hr_attendance_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 근태 - total_employees INT UNSIGNED DEFAULT 0, -- 전체 직원 수 - attendance_count INT UNSIGNED DEFAULT 0, -- 출근 인원 - late_count INT UNSIGNED DEFAULT 0, -- 지각 - absent_count INT UNSIGNED DEFAULT 0, -- 결근 - attendance_rate DECIMAL(5,2) DEFAULT 0, -- 출근율 (%) - - -- 휴가 - leave_count INT UNSIGNED DEFAULT 0, -- 휴가 사용 - leave_annual_count INT UNSIGNED DEFAULT 0, -- 연차 - leave_sick_count INT UNSIGNED DEFAULT 0, -- 병가 - leave_other_count INT UNSIGNED DEFAULT 0, -- 기타 - - -- 초과근무 - overtime_hours DECIMAL(10,2) DEFAULT 0, - overtime_employee_count INT UNSIGNED DEFAULT 0, - - -- 인건비 (급여 정산 기준) - total_labor_cost DECIMAL(18,2) DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟢 P2: `stat_project_monthly` - 건설/프로젝트 월간 통계 - -```sql -CREATE TABLE stat_project_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - -- 프로젝트 현황 - active_site_count INT UNSIGNED DEFAULT 0, - completed_site_count INT UNSIGNED DEFAULT 0, - new_contract_count INT UNSIGNED DEFAULT 0, - contract_total_amount DECIMAL(18,2) DEFAULT 0, - - -- 원가 - expected_expense_total DECIMAL(18,2) DEFAULT 0, - actual_expense_total DECIMAL(18,2) DEFAULT 0, - labor_cost_total DECIMAL(18,2) DEFAULT 0, - material_cost_total DECIMAL(18,2) DEFAULT 0, - - -- 수익률 - gross_profit DECIMAL(18,2) DEFAULT 0, - gross_profit_rate DECIMAL(5,2) DEFAULT 0, -- 수익률 (%) - - -- 이슈 - handover_report_count INT UNSIGNED DEFAULT 0, - structure_review_count INT UNSIGNED DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟢 P2: `stat_system_daily` - 시스템 일일 통계 - -```sql -CREATE TABLE stat_system_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- API 사용량 - api_request_count INT UNSIGNED DEFAULT 0, - api_error_count INT UNSIGNED DEFAULT 0, - api_avg_response_ms INT UNSIGNED DEFAULT 0, - - -- 사용자 활동 - active_user_count INT UNSIGNED DEFAULT 0, - login_count INT UNSIGNED DEFAULT 0, - - -- 감사 - audit_create_count INT UNSIGNED DEFAULT 0, - audit_update_count INT UNSIGNED DEFAULT 0, - audit_delete_count INT UNSIGNED DEFAULT 0, - - -- 알림 - fcm_sent_count INT UNSIGNED DEFAULT 0, - fcm_failed_count INT UNSIGNED DEFAULT 0, - - -- 파일 - file_upload_count INT UNSIGNED DEFAULT 0, - file_upload_size_mb DECIMAL(10,2) DEFAULT 0, - - -- 결재 - approval_submitted_count INT UNSIGNED DEFAULT 0, - approval_completed_count INT UNSIGNED DEFAULT 0, - approval_avg_hours DECIMAL(8,2) DEFAULT 0, -- 평균 처리 시간 - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.4 월간 요약 테이블 - -#### `stat_sales_monthly` - 매출 월간 요약 - -```sql -CREATE TABLE stat_sales_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - -- 일일 합산 - order_count INT UNSIGNED DEFAULT 0, - order_amount DECIMAL(18,2) DEFAULT 0, - sales_count INT UNSIGNED DEFAULT 0, - sales_amount DECIMAL(18,2) DEFAULT 0, - shipment_count INT UNSIGNED DEFAULT 0, - shipment_amount DECIMAL(18,2) DEFAULT 0, - - -- 월간 고유 지표 - unique_client_count INT UNSIGNED DEFAULT 0, -- 거래 고객 수 - avg_order_amount DECIMAL(18,2) DEFAULT 0, -- 평균 수주 금액 - top_client_id BIGINT UNSIGNED NULL, -- 최다 거래 고객 - top_client_amount DECIMAL(18,2) DEFAULT 0, - mom_growth_rate DECIMAL(8,2) NULL, -- 전월 대비 성장률 (%) - yoy_growth_rate DECIMAL(8,2) NULL, -- 전년동월 대비 (%) - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_finance_monthly` - 재무 월간 요약 - -```sql -CREATE TABLE stat_finance_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - deposit_total DECIMAL(18,2) DEFAULT 0, - withdrawal_total DECIMAL(18,2) DEFAULT 0, - net_cashflow DECIMAL(18,2) DEFAULT 0, - purchase_total DECIMAL(18,2) DEFAULT 0, - card_total DECIMAL(18,2) DEFAULT 0, - - receivable_end DECIMAL(18,2) DEFAULT 0, -- 월말 미수금 - payable_end DECIMAL(18,2) DEFAULT 0, -- 월말 미지급 - bank_balance_end DECIMAL(18,2) DEFAULT 0, -- 월말 잔액 - - mom_cashflow_change DECIMAL(8,2) NULL, -- 전월 대비 현금흐름 변화 (%) - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_production_monthly` - 생산 월간 요약 - -```sql -CREATE TABLE stat_production_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - wo_total_count INT UNSIGNED DEFAULT 0, - wo_completed_count INT UNSIGNED DEFAULT 0, - production_qty DECIMAL(18,2) DEFAULT 0, - defect_qty DECIMAL(18,2) DEFAULT 0, - avg_defect_rate DECIMAL(5,2) DEFAULT 0, - avg_efficiency_rate DECIMAL(5,2) DEFAULT 0, - avg_delivery_rate DECIMAL(5,2) DEFAULT 0, - total_planned_hours DECIMAL(10,2) DEFAULT 0, - total_actual_hours DECIMAL(10,2) DEFAULT 0, - issue_total_count INT UNSIGNED DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.5 KPI/알림 테이블 - -#### `stat_kpi_targets` - KPI 목표 설정 - -```sql -CREATE TABLE stat_kpi_targets ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NULL, -- NULL = 연간 목표 - - domain VARCHAR(50) NOT NULL, -- 'sales', 'production' - metric_code VARCHAR(100) NOT NULL, -- 'monthly_sales_amount' - target_value DECIMAL(18,2) NOT NULL, - unit VARCHAR(20) NOT NULL DEFAULT 'KRW', -- KRW, %, count, hours - description VARCHAR(300) NULL, - - created_by BIGINT UNSIGNED NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_metric (tenant_id, stat_year, stat_month, metric_code), - INDEX idx_domain (domain) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_alerts` - 통계 기반 알림 - -```sql -CREATE TABLE stat_alerts ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - domain VARCHAR(50) NOT NULL, - alert_type VARCHAR(100) NOT NULL, -- 'below_target', 'anomaly', 'threshold' - severity ENUM('info','warning','critical') NOT NULL DEFAULT 'info', - title VARCHAR(300) NOT NULL, - message TEXT NOT NULL, - metric_code VARCHAR(100) NULL, - current_value DECIMAL(18,2) NULL, - threshold_value DECIMAL(18,2) NULL, - is_read BOOLEAN NOT NULL DEFAULT FALSE, - is_resolved BOOLEAN NOT NULL DEFAULT FALSE, - resolved_at TIMESTAMP NULL, - resolved_by BIGINT UNSIGNED NULL, - created_at TIMESTAMP NULL, - - INDEX idx_tenant_unread (tenant_id, is_read), - INDEX idx_severity (severity), - INDEX idx_domain (domain) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.6 이벤트/스냅샷 테이블 - -#### `stat_events` - 실시간 이벤트 로그 (확장용) - -```sql -CREATE TABLE stat_events ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - domain VARCHAR(50) NOT NULL, - event_type VARCHAR(100) NOT NULL, -- 'order_created', 'payment_received' - entity_type VARCHAR(100) NOT NULL, -- 'Order', 'Deposit' - entity_id BIGINT UNSIGNED NOT NULL, - payload JSON NULL, -- 이벤트 데이터 - occurred_at TIMESTAMP NOT NULL, - - INDEX idx_tenant_domain (tenant_id, domain), - INDEX idx_occurred (occurred_at), - INDEX idx_entity (entity_type, entity_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_snapshots` - 상태 스냅샷 (특정 시점 전체 상태) - -```sql -CREATE TABLE stat_snapshots ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - snapshot_date DATE NOT NULL, - domain VARCHAR(50) NOT NULL, - snapshot_type VARCHAR(50) NOT NULL DEFAULT 'daily', -- daily, weekly, monthly - data JSON NOT NULL, -- 전체 스냅샷 데이터 - created_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date_domain (tenant_id, snapshot_date, domain, snapshot_type), - INDEX idx_date (snapshot_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -## 5. 테이블 요약 - -| # | 테이블명 | 유형 | 도메인 | 집계 주기 | 우선순위 | -|---|---------|------|--------|----------|---------| -| 1 | `stat_definitions` | 메타 | 공통 | - | 🔴 P0 | -| 2 | `stat_job_logs` | 메타 | 공통 | - | 🔴 P0 | -| 3 | `dim_date` | 차원 | 공통 | 1회 생성 | 🔴 P0 | -| 4 | `dim_client` | 차원 | 공통 | SCD Type 2 | 🟡 P1 | -| 5 | `dim_product` | 차원 | 공통 | SCD Type 2 | 🟡 P1 | -| 6 | `stat_sales_daily` | 팩트 | 매출/수주 | 일간 | 🔴 P0 | -| 7 | `stat_finance_daily` | 팩트 | 재무/회계 | 일간 | 🔴 P0 | -| 8 | `stat_production_daily` | 팩트 | 생산/작업 | 일간 | 🔴 P0 | -| 9 | `stat_inventory_daily` | 팩트 | 재고/자재 | 일간 | 🟡 P1 | -| 10 | `stat_quote_pipeline_daily` | 팩트 | 견적/영업 | 일간 | 🟡 P1 | -| 11 | `stat_hr_attendance_daily` | 팩트 | 인사/근태 | 일간 | 🟡 P1 | -| 12 | `stat_project_monthly` | 팩트 | 건설/프로젝트 | 월간 | 🟢 P2 | -| 13 | `stat_system_daily` | 팩트 | 시스템/감사 | 일간 | 🟢 P2 | -| 14 | `stat_sales_monthly` | 요약 | 매출/수주 | 월간 | 🔴 P0 | -| 15 | `stat_finance_monthly` | 요약 | 재무/회계 | 월간 | 🔴 P0 | -| 16 | `stat_production_monthly` | 요약 | 생산/작업 | 월간 | 🔴 P0 | -| 17 | `stat_kpi_targets` | KPI | 공통 | 수동 설정 | 🟡 P1 | -| 18 | `stat_alerts` | 알림 | 공통 | 실시간 | 🟡 P1 | -| 19 | `stat_events` | 이벤트 | 공통 | 실시간 | 🟢 P2 | -| 20 | `stat_snapshots` | 스냅샷 | 공통 | 일/월 | 🟢 P2 | - -**총 20개 테이블** (메타 2 + 차원 3 + 일간팩트 6 + 월간팩트 1 + 월간요약 3 + KPI/알림 2 + 이벤트/스냅샷 2 + 시스템 1) - ---- - -## 6. 구현 계획 (Phase) - -### Phase 1: 인프라 구축 (P0) -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 1.1 | sam_stat DB 생성 및 Laravel 연결 설정 | ✅ | ① Docker MySQL에 `CREATE DATABASE sam_stat` 실행 ② `api/config/database.php`에 `sam_stat` 연결 추가 ③ `api/.env`에 `STAT_DB_*` 환경변수 추가 | -| 1.2 | 메타 테이블 마이그레이션 | ✅ | `stat_definitions`, `stat_job_logs` 마이그레이션 생성 (`--database=sam_stat` 옵션) | -| 1.3 | dim_date 테이블 생성 및 시딩 | ✅ | 2020-01-01~2030-12-31 날짜 데이터 Seeder 작성 (4,018건) | -| 1.4 | 기반 모델 클래스 생성 | ✅ | `BaseStatModel`, `StatDefinition`, `StatJobLog`, `DimDate` 생성 | -| 1.5 | 집계 커맨드 기반 구조 | ✅ | `StatAggregateDailyCommand.php`, `StatAggregateMonthlyCommand.php` 생성 | -| 1.6 | StatAggregatorService 골격 | ✅ | `StatAggregatorService.php` + `StatDomainServiceInterface.php` - 테넌트 순회 + 도메인별 서비스 호출 구조 | - -**Phase 1 검증 방법:** -```bash -# DB 생성 확인 -docker compose exec mysql mysql -u root -proot -e "SHOW DATABASES LIKE 'sam_stat';" - -# 마이그레이션 실행 -cd api && php artisan migrate --database=sam_stat - -# dim_date 시딩 -cd api && php artisan db:seed --class=DimDateSeeder - -# 커맨드 확인 -cd api && php artisan stat:aggregate-daily --help -``` - -### Phase 2: P0 도메인 구축 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 2.1 | 매출 테이블 마이그레이션 | ✅ | `stat_sales_daily` + `stat_sales_monthly` 마이그레이션 | -| 2.2 | 매출 모델 + 서비스 | ✅ | `StatSalesDaily`, `StatSalesMonthly`, `SalesStatService` - orders, sales, clients, shipments 집계 | -| 2.3 | 재무 테이블 마이그레이션 | ✅ | `stat_finance_daily` + `stat_finance_monthly` 마이그레이션 | -| 2.4 | 재무 모델 + 서비스 | ✅ | `StatFinanceDaily`, `StatFinanceMonthly`, `FinanceStatService` - deposits, withdrawals, purchases, bills, bank_transactions 집계 | -| 2.5 | 생산 테이블 마이그레이션 | ✅ | `stat_production_daily` + `stat_production_monthly` 마이그레이션 | -| 2.6 | 생산 모델 + 서비스 | ✅ | `StatProductionDaily`, `StatProductionMonthly`, `ProductionStatService` - work_orders, work_results 집계 | -| 2.7 | 스케줄러 등록 | ✅ | `console.php`에 `stat:aggregate-daily` (02:00), `stat:aggregate-monthly` (매월 1일 03:00) 등록 | - -**Phase 2 검증 방법:** -```bash -# 수동 집계 실행 (특정 날짜) -cd api && php artisan stat:aggregate-daily --date=2026-01-28 - -# 데이터 확인 -docker compose exec mysql mysql -u root -proot sam_stat \ - -e "SELECT * FROM stat_sales_daily WHERE stat_date='2026-01-28';" -``` - -### Phase 3: P1 도메인 확장 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 3.1 | 차원 테이블 | ✅ | `dim_client`, `dim_product` 마이그레이션 + 모델 + `DimensionSyncService` (SCD Type 2). 원본: `clients`→`dim_client`, `items`→`dim_product` (products 테이블 없어 items 사용) | -| 3.2 | 재고 통계 | ✅ | `stat_inventory_daily` 마이그레이션 + 모델 + `InventoryStatService` - 원본: `stocks`, `stock_transactions`, `inspections` | -| 3.3 | 견적/영업 통계 | ✅ | `stat_quote_pipeline_daily` 마이그레이션 + 모델 + `QuoteStatService` - 원본: `quotes`, `sales_prospects`, `biddings`, `sales_prospect_consultations` | -| 3.4 | 인사/근태 통계 | ✅ | `stat_hr_attendance_daily` 마이그레이션 + 모델 + `HrStatService` - 원본: `attendances`, `leaves`, `user_tenants` | -| 3.5 | KPI/알림 | ✅ | `stat_kpi_targets`, `stat_alerts` 마이그레이션 + 모델 + `KpiAlertService` + `StatCheckKpiAlertsCommand` + 스케줄러 09:00 | - -### Phase 4: P2 도메인 + API + 대시보드 전환 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 4.1 | 건설/프로젝트 통계 | ✅ | `stat_project_monthly` 마이그레이션 + 모델 + `ProjectStatService` - 원본: `sites`, `contracts`, `expected_expenses`. 월간 전용 도메인 | -| 4.2 | 시스템 통계 | ✅ | `stat_system_daily` 마이그레이션 + 모델 + `SystemStatService` - 원본: `api_request_logs`, `personal_access_tokens`(user_tenants 조인), `audit_logs`, `fcm_send_logs`, `files`, `approvals` | -| 4.3 | 이벤트/스냅샷 | ✅ | `stat_events`, `stat_snapshots` 마이그레이션 + 모델 + `StatEventService` + `StatEventObserver` (Order, Sale, Deposit, Withdrawal, Purchase, Approval에 등록) | -| 4.4 | 통계 API | ✅ | `StatController` (summary/daily/monthly/alerts) + `StatQueryService` + FormRequest 3개 + `routes/api/v1/stats.php`. Swagger는 Phase 5에서 추가 | -| 4.5 | 대시보드 전환 | ✅ | `DashboardService` getFinanceSummary/getSalesSummary → sam_stat 우선 조회 + 원본 DB 폴백. 응답에 `source` 필드 추가 | - -### Phase 5: 최적화 및 안정화 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 5.1 | 백필 스크립트 | ✅ | `StatBackfillCommand` - `stat:backfill --from= --to= --domain= --tenant= --skip-monthly --skip-dimensions`. CarbonPeriod 일간 순회 + 월간 집계 + 프로그레스바 + 에러 리포트. 테스트: 7도메인 0.2초 | -| 5.2 | 정합성 검증 | ✅ | `StatVerifyCommand` - `stat:verify --date= --tenant= --domain= --fix`. sales(수주건수/매출금액), finance(입금액/출금액), system(API요청수/감사로그수) 교차 검증. --fix 시 자동 재집계. 테스트: 6건 전부 일치 | -| 5.3 | 파티셔닝 준비 | ✅ | `2026_01_29_300001_prepare_partitioning_daily_tables.php` - 7개 일간 테이블 RANGE COLUMNS(stat_date) 파티셔닝. PK에 stat_date 포함, p2024~p2028 + p_future. 기존 파티션 여부 체크 후 스킵 | -| 5.4 | Redis 캐싱 | ✅ | `StatQueryService` - Cache::remember TTL 5분. 키 패턴: `stat:{daily\|monthly\|dashboard}:{tenantId}:...`. `invalidateCache()` 정적 메서드: Redis keys 패턴 매칭 삭제. 집계 완료 시 StatAggregatorService에서 자동 호출 | -| 5.5 | 모니터링 알림 | ✅ | `StatMonitorService` - recordAggregationFailure(critical), recordMissingData(warning), recordMismatch(critical), resolveAlerts(). StatAggregatorService catch 블록에서 자동 호출. stat_alerts 테이블 연동 검증 완료 | - -### Phase 6: 문서화 및 마무리 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 6.1 | Swagger API 문서 | ✅ | `app/Swagger/v1/StatApi.php` - Stats 태그, 4개 엔드포인트 (summary/daily/monthly/alerts), StatSalesDaily/StatFinanceDaily/StatDashboardSummary/StatAlert 스키마 정의. `l5-swagger:generate` 성공 | -| 6.2 | DB 스키마 문서 | ✅ | `docs/specs/database-schema.md`에 sam_stat 섹션 추가 - 20개 테이블 (메타 2, 차원 3, 일간 7, 월간 4, KPI/알림/이벤트 4) + Artisan 커맨드 5개 + API 엔드포인트 4개 | -| 6.3 | 계획 문서 완료 | ✅ | Phase 6 섹션 추가, 진행률 100%, 상태 완료 | - ---- - -## 7. 기술 설계 요약 - -### 7.1 Laravel 다중 DB 연결 - -```php -// config/database.php -'connections' => [ - 'mysql' => [ /* 기존 samdb */ ], - 'sam_stat' => [ - 'driver' => 'mysql', - 'host' => env('STAT_DB_HOST', '127.0.0.1'), - 'database' => env('STAT_DB_DATABASE', 'sam_stat'), - 'username' => env('STAT_DB_USERNAME', 'root'), - 'password' => env('STAT_DB_PASSWORD', ''), - // ... 나머지 동일 - ], -], -``` - -### 7.2 모델 구조 - -``` -api/app/Models/Stats/ -├── StatDefinition.php // connection = 'sam_stat' -├── StatJobLog.php -├── Dimensions/ -│ ├── DimDate.php -│ ├── DimClient.php -│ └── DimProduct.php -├── Daily/ -│ ├── StatSalesDaily.php -│ ├── StatFinanceDaily.php -│ ├── StatProductionDaily.php -│ ├── StatInventoryDaily.php -│ ├── StatQuotePipelineDaily.php -│ ├── StatHrAttendanceDaily.php -│ └── StatSystemDaily.php -├── Monthly/ -│ ├── StatSalesMonthly.php -│ ├── StatFinanceMonthly.php -│ ├── StatProductionMonthly.php -│ └── StatProjectMonthly.php -├── StatKpiTarget.php -├── StatAlert.php -├── StatEvent.php -└── StatSnapshot.php -``` - -### 7.3 서비스 구조 - -``` -api/app/Services/Stats/ -├── StatAggregatorService.php // 집계 오케스트레이터 -├── SalesStatService.php // 매출/수주 집계 -├── FinanceStatService.php // 재무 집계 -├── ProductionStatService.php // 생산 집계 -├── InventoryStatService.php // 재고 집계 -├── QuoteStatService.php // 견적/영업 집계 -├── HrStatService.php // 인사/근태 집계 -├── ProjectStatService.php // 건설 집계 -├── SystemStatService.php // 시스템 집계 -└── KpiAlertService.php // KPI 목표 대비 알림 -``` - -### 7.4 스케줄러 구조 - -```php -// app/Console/Kernel.php (또는 routes/console.php) - -// 일간 집계 - 매일 02:00 -Schedule::command('stat:aggregate-daily') - ->dailyAt('02:00') - ->withoutOverlapping(); - -// 월간 집계 - 매월 1일 03:00 -Schedule::command('stat:aggregate-monthly') - ->monthlyOn(1, '03:00') - ->withoutOverlapping(); - -// KPI 알림 체크 - 매일 09:00 -Schedule::command('stat:check-kpi-alerts') - ->dailyAt('09:00'); -``` - -### 7.5 집계 패턴 (UPSERT) - -```php -// 멱등성 보장: 같은 날짜 재실행 시 덮어쓰기 -StatSalesDaily::updateOrCreate( - ['tenant_id' => $tenantId, 'stat_date' => $date], - [ - 'order_count' => $orderCount, - 'order_amount' => $orderAmount, - // ... - ] -); -``` - ---- - -## 8. 참고 문서 - -| 문서 | 경로 | 용도 | -|------|------|------| -| DB 스키마 | `docs/specs/database-schema.md` | 원본 219개 테이블 구조 | -| 시스템 아키텍처 | `docs/architecture/system-overview.md` | 전체 시스템 구조, 미들웨어, Docker | -| API 규칙 | `docs/standards/api-rules.md` | Controller/Service 패턴, ApiResponse | -| 품질 체크리스트 | `docs/standards/quality-checklist.md` | 코드 품질 검증 항목 | -| 빠른 시작 | `docs/quickstart/quick-start.md` | 핵심 개발 규칙 3가지 | -| Swagger 가이드 | `docs/guides/swagger-guide.md` | Swagger 작성 규칙 (Phase 4.4 시) | -| Git 규칙 | `docs/standards/git-conventions.md` | 커밋 메시지 형식 | -| 프로젝트 CLAUDE.md | `/SAM/CLAUDE.md` | 프로젝트 전체 규칙 및 맥락 | -| API CLAUDE.md | `/SAM/api/CLAUDE.md` | API 저장소 상세 규칙 | - ---- - -## 9. 자기완결성 점검 결과 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1: sam_stat 별도 DB로 통계 분리 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 20개 테이블, 8 도메인, Phase별 검증 방법 명시 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4: 테이블별 DDL, 섹션 6: Phase별 구체적 작업 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 섹션 2.1: 원본 테이블 매핑, 섹션 0.2: DB 환경 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 0.1, 0.3: 실제 파일 경로 검증됨 (2026-01-29) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | Phase 1-5 구체적 작업 + bash 검증 커맨드 포함 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | Phase 1, 2에 검증 bash 커맨드 블록 포함 | -| 8 | 모호한 표현이 없는가? | ✅ | 파일 경로, 클래스명, 테이블명 모두 구체적 | - -### 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 0.9 체크리스트 → 6. Phase 1 | -| Q3. 어떤 파일을 수정/생성해야 하는가? | ✅ | 0.1 프로젝트 구조 + 7.1~7.5 기술 설계 | -| Q4. 기존 코드에 어떤 영향이 있는가? | ✅ | 0.3 기존 대시보드/보고서 시스템 | -| Q5. DB 연결은 어떻게 설정하는가? | ✅ | 0.2 현재 DB 환경 + 7.1 Laravel 다중 DB | -| Q6. 코딩 규칙은 무엇인가? | ✅ | 0.8 핵심 코딩 규칙 | -| Q7. 작업 완료 확인 방법은? | ✅ | Phase 1, 2 검증 방법 블록 | -| Q8. 스케줄러는 어떻게 등록하는가? | ✅ | 0.4 기존 스케줄러 패턴 + 7.4 | -| Q9. Docker 환경은 어떻게 구성되어 있는가? | ✅ | 0.7 Docker 환경 | -| Q10. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 (9개 문서 매핑) | - -**결과**: 10/10 통과 → ✅ 자기완결성 확보 - ---- - -## 10. 변경 이력 - -| 날짜 | 항목 | 내용 | -|------|------|------| -| 2026-01-29 | 초안 작성 | 프로젝트 분석 → 8개 도메인 도출 → 20개 테이블 설계 | -| 2026-01-29 | 자기완결성 보완 | 섹션 0 추가 (프로젝트 컨텍스트, DB 환경, 기존 시스템, 코딩 규칙, 체크리스트) | -| 2026-01-29 | 환경별 배포 구분 | 섹션 0.7 확장: 로컬(Docker) vs 개발서버(non-Docker) 구분, 배포 워크플로우 추가 | -| 2026-01-29 | Phase 1 완료 | 인프라 구축: sam_stat DB 생성, 메타/dim_date 마이그레이션, 기반 모델 4개, 커맨드 2개, AggregatorService + Interface | -| 2026-01-29 | Phase 2 완료 | P0 도메인: 매출/재무/생산 일간+월간 테이블 6개, 모델 6개, 서비스 3개, 스케줄러 2개 등록. 실데이터 집계 검증 완료 | -| 2026-01-29 | Phase 3 완료 | P1 도메인: dim_client/dim_product 차원 + 재고/견적/인사 일간 3개 + KPI/알림 2개 = 테이블 7개, 모델 7개, 서비스 4개(Dimension/Inventory/Quote/Hr/KpiAlert), 커맨드 1개, 스케줄러 1개. 실데이터 검증 완료. products→items, client_groups.name→group_name 수정 | -| 2026-01-29 | Phase 4 완료 | P2 도메인 + API + 대시보드: stat_project_monthly/stat_system_daily/stat_events/stat_snapshots 테이블 4개, 모델 4개, 서비스 4개(Project/System/StatEvent/StatQuery), StatController + FormRequest 3개 + routes/stats.php, StatEventObserver(6모델), DashboardService sam_stat 전환(폴백 패턴). 버그: whereHas→DB Builder 제거, User모델경로 수정. sam_stat 총 20테이블 | -| 2026-01-29 | Phase 5 완료 | 최적화 및 안정화: StatBackfillCommand(백필), StatVerifyCommand(정합성 검증+자동 재집계), 파티셔닝 준비 마이그레이션(7테이블 RANGE), StatQueryService Redis 캐싱(TTL 5분+invalidateCache), StatMonitorService(집계 실패/누락/불일치 알림→stat_alerts), StatAggregatorService에 모니터링+캐시 무효화 연동. severity enum 수정(high→critical). 전체 테스트 통과 | -| 2026-01-30 | Phase 6 완료 | 문서화 및 마무리: StatApi.php Swagger 문서(4 엔드포인트, 4 스키마), database-schema.md sam_stat 섹션 추가(20테이블+5커맨드+4API). 전체 6 Phase 100% 완료 | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/simulator-calculation-logic-mapping.md b/plans/archive/simulator-calculation-logic-mapping.md deleted file mode 100644 index 113c198..0000000 --- a/plans/archive/simulator-calculation-logic-mapping.md +++ /dev/null @@ -1,1057 +0,0 @@ -# 견적 시뮬레이터 완전 동기화 계획 - -> **작성일**: 2025-12-23 (업데이트: 2025-12-30) -> **목표**: design.sam.kr 시뮬레이터와 mng 시뮬레이터가 **동일한 결과**를 출력하도록 완전 동기화 - ---- - -## 1. Design 시스템 전체 분석 - -### 1.1 핵심 파일 구조 - -| 파일 | 줄 수 | 역할 | -|------|-------|------| -| `AutoCalculationSimulator.tsx` | 1,068 | 메인 시뮬레이터 UI + 계산 로직 | -| `formulaEvaluator.ts` | 312 | 수식 평가 엔진 | -| `bomCalculatorWithDebug.ts` | 232 | BOM 계산 + 10단계 디버깅 | -| `DataContext.tsx` | 9,859 | 마스터 데이터 타입 + 상태 관리 | -| `sampleQuoteData_Complete.ts` | 600+ | 샘플 품목 데이터 | -| `addProductBoms.ts` | 298 | 완제품 BOM 구성 | - -### 1.2 데이터 구조 (TypeScript 인터페이스) - -#### 품목 마스터 (ItemMaster) -```typescript -interface ItemMaster { - id: string; - itemCode: string; // 품목코드 - itemName: string; // 품목명 - itemType: 'FG' | 'SF' | 'PT' | 'SM' | 'RM' | 'CS'; - productCategory?: 'SCREEN' | 'STEEL'; // 제품 카테고리 - partType?: 'ASSEMBLY' | 'BENDING' | 'PURCHASED'; - unit: string; - salesPrice?: number; // 판매단가 - purchasePrice?: number; // 매입단가 - marginRate?: number; // 마진율 - bom?: BOMLine[]; // 하위 BOM 목록 - // ... 기타 필드 -} -``` - -#### BOM 라인 (BOMLine) -```typescript -interface BOMLine { - childItemCode: string; // 자식 품목 코드 - childItemName: string; // 자식 품목명 - quantity: number; // 기준 수량 - unit: string; // 단위 - quantityFormula?: string; // 수량 수식 (예: "W*H/1000000", "H/1000") - note?: string; // 비고 -} -``` - -#### 단가 관리 (PricingData) -```typescript -interface PricingData { - id: string; - itemId: string; - itemCode: string; - purchasePrice?: number; // 매입단가 - processingCost?: number; // 가공비 - loss?: number; // LOSS(%) - marginRate?: number; // 마진율 - salesPrice?: number; // 판매단가 - effectiveDate: string; // 적용일 - status: 'draft' | 'active' | 'inactive' | 'finalized'; -} -``` - -#### 카테고리 그룹 (CategoryGroup) - MNG에 누락 -```typescript -interface CategoryGroup { - id: string; - name: string; // "면적기반", "중량기반", "수량기반" - categories: string[]; // 소속 카테고리들 - multiplierVariable?: string; // 곱할 변수 (M, K 등) -} -``` - -### 1.3 계산 변수 체계 - -| 변수 | 설명 | 계산식 | -|------|------|--------| -| `W0` | 오픈사이즈 폭 | 사용자 입력 | -| `H0` | 오픈사이즈 높이 | 사용자 입력 | -| `PC` | 제품 카테고리 | "스크린" / "철재" | -| `W1` | 제작폭 | PC=="스크린" ? W0+140 : W0+110 | -| `H1` | 제작높이 | H0 + 350 | -| `W` | 제작폭 (별칭) | = W1 | -| `H` | 제작높이 (별칭) | = H1 | -| `M` | 면적 (㎡) | (W1 × H1) / 1,000,000 | -| `K` | 중량 (kg) | 스크린: M×2 + W0/1000×14.17, 철재: M×25 | -| `GT` | 가이드레일 설치유형 | "벽면형" / "측면형" | -| `MP` | 모터 전원 | "220V" / "380V" | -| `CT` | 연동제어기 | "단독" / "연동" | -| `QTY` | 수량 | 사용자 입력 | - -### 1.4 수식 평가 함수 - -**지원 함수 목록:** -| 함수 | 설명 | 예시 | -|------|------|------| -| `SUM(a, b, ...)` | 합계 | `SUM(W0, H0, 100)` | -| `AVERAGE(a, b, ...)` | 평균 | `AVERAGE(W0, H0)` | -| `MAX(a, b, ...)` | 최대값 | `MAX(W0, 1000)` | -| `MIN(a, b, ...)` | 최소값 | `MIN(H0, 3000)` | -| `ROUND(val, dec)` | 반올림 | `ROUND(M, 2)` | -| `CEIL(val)` | 올림 | `CEIL(H1 / 1000)` | -| `FLOOR(val)` | 내림 | `FLOOR(W1 / 500)` | -| `ABS(val)` | 절대값 | `ABS(W0 - 2000)` | -| `IF(cond, t, f)` | 조건문 | `IF(W0 > 3000, 2, 1)` | -| `SQRT(val)` | 제곱근 | `SQRT(M)` | -| `POWER(base, exp)` | 거듭제곱 | `POWER(W1, 2)` | - -**평가 과정:** -```typescript -// 1. 변수 치환 (긴 변수명부터) -const sortedVars = Object.keys(vars).sort((a, b) => b.length - a.length); -sortedVars.forEach(varName => { - const regex = new RegExp(`\\b${varName}\\b`, 'g'); - formula = formula.replace(regex, String(vars[varName])); -}); - -// 2. 함수 처리 (CEIL, FLOOR, ROUND 등) -formula = processFunctions(formula); - -// 3. 최종 계산 -return new Function(`return (${formula})`)(); -``` - -### 1.5 BOM 계산 10단계 프로세스 - -| 단계 | 항목 | 예시 | -|------|------|------| -| Step 1 | 수량 공식 확인 | `H/1000` | -| Step 2 | 변수 값 확인 | `{W0:2000, H0:2500, W1:2140, H1:2850, M:6.099}` | -| Step 3 | 수량 계산 과정 | `H/1000 = 2850/1000 = 2.85` | -| Step 4 | 계산된 수량 | `2.85` | -| Step 5 | 단가 소스 | `단가관리 (15,000원)` 또는 `품목마스터 (15,000원)` | -| Step 6 | 기본 단가 | `15,000` | -| Step 7 | 카테고리 승수 | `면적단가 (15,000원/㎡ × 6.099㎡)` | -| Step 8 | 최종 단가 | `91,485` | -| Step 9 | 금액 계산 | `2.85 × 91,485 = 260,732` | -| Step 10 | 최종 금액 | `260,732` | - -### 1.6 단가 계산 로직 - -```typescript -// 1. 단가 조회 우선순위 -let unitPrice = 0; -let priceSource = '단가 없음'; - -// 1순위: pricing 테이블에서 조회 -const itemPricing = pricings.find(p => p.itemCode === bomEntry.childItemCode); -if (itemPricing && itemPricing.salesPrice) { - unitPrice = itemPricing.salesPrice; - priceSource = `단가관리 (${unitPrice.toLocaleString()}원)`; -} -// 2순위: 품목마스터에서 조회 -else if (childItem.salesPrice) { - unitPrice = childItem.salesPrice; - priceSource = `품목마스터 (${unitPrice.toLocaleString()}원)`; -} - -// 2. 면적 기반 품목 판단 -const areaBasedCategories = ['원단', '패널', '도장', '표면처리']; -const isAreaBased = areaBasedCategories.some(cat => - itemCategory.includes(cat) || childItem.itemName.includes(cat) -); - -// 3. 최종 단가 계산 -let finalUnitPrice = unitPrice; -if (isAreaBased && calculationVariables.M > 0) { - finalUnitPrice = unitPrice * calculationVariables.M; // 면적 단가 - priceCalculationNote = `면적단가 (${unitPrice}원/㎡ × ${M}㎡)`; -} else { - priceCalculationNote = '수량단가'; -} - -// 4. 최종 금액 -const totalPrice = calculatedQuantity * finalUnitPrice; -``` - ---- - -## 2. Design 샘플 데이터 분석 - -### 2.1 품목 구성 (약 100개) - -| 유형 | 코드 접두사 | 수량 | 설명 | -|------|------------|------|------| -| 원자재 (RM) | RM-* | 20 | 강판, 알루미늄, 원단, 패킹 등 | -| 부자재 (SM) | SM-* | 25 | 볼트, 너트, 전선, 실리콘 등 | -| 스크린 반제품 (SF) | SF-SCR-* | 20 | 원단, 가이드레일, 케이스, 모터 등 | -| 철재 반제품 (SF) | SF-STL-*, SF-BND-* | 20 | 도어, 프레임, 패널, 절곡 부품 등 | -| 스크린 완제품 (FG) | FG-SCR-* | 5 | 소형/중형/대형/특대/맞춤형 | -| 철재 완제품 (FG) | FG-STL-* | 5 | 소형/중형/대형/양개문/특수 | -| 절곡 완제품 (FG) | FG-BND-* | 4 | L형/U형/Z형/ㄷ형 | - -### 2.2 주요 BOM 수식 패턴 - -| 품목 유형 | 수식 | 설명 | -|----------|------|------| -| 스크린 원단 | `W*H/1000000` | 면적 계산 | -| 가이드레일 | `H/1000` | 높이(m) 기준 | -| 엣지윙 | `H/1000` | 높이(m) 기준 | -| 철재 프레임 | `(W+H)*2/1000` | 둘레(m) 기준 | -| 철재 패널 | `W*H/1000000` | 면적 계산 | -| 실링재 | `(W+H)*2/1000` | 둘레(m) 기준 | -| 파우더 도장 | `W*H/1000000` | 면적 계산 | - -### 2.3 완제품 BOM 예시 (FG-SCR-002 중형 스크린) - -```typescript -{ - itemCode: 'FG-SCR-002', - itemName: '방화스크린 중형 (2000x3000)', - bom: [ - { childItemCode: 'SF-SCR-F01', quantity: 6.0, unit: 'M2', quantityFormula: 'W*H/1000000' }, - { childItemCode: 'SF-SCR-F02', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-F03', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-F04', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-F05', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-M02', quantity: 1, unit: 'EA', note: '중형용' }, - { childItemCode: 'SF-SCR-C01', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-S01', quantity: 1, unit: 'SET' }, - { childItemCode: 'SF-SCR-W01', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-B01', quantity: 2, unit: 'SET', note: '중형용 2세트' }, - { childItemCode: 'SF-SCR-E01', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-E02', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-REM01', quantity: 1, unit: 'EA' }, - { childItemCode: 'SM-B002', quantity: 30, unit: 'EA', note: '조립용' }, - { childItemCode: 'SM-N002', quantity: 30, unit: 'EA' }, - { childItemCode: 'SM-A001', quantity: 8, unit: 'EA', note: '고정용' }, - ] -} -``` - ---- - -## 3. MNG 현재 상태 분석 - -### 3.1 테이블 구조 - -| 테이블 | 현재 상태 | Design 대응 | -|--------|----------|-------------| -| `items` | 364개 (RM:133, SM:217, PT:6, FG:3, CS:5) | ItemMaster | -| `item_details` | 품목 상세 정보 | ItemMaster 확장 필드 | -| `prices` | 3개 (거의 없음) | PricingData | -| `quote_formulas` | 57개 (기본 변수 있음) | FormulaRule, CalculationFormula | -| `quote_formula_ranges` | 범위 규칙 | FormulaRule.ranges | -| `quote_formula_items` | 수식 품목 매핑 | BOM 연동 | -| `common_codes` | 코드 그룹 | CategoryGroup (부분) | -| `category_groups` | ❌ 없음 | CategoryGroup 추가 필요 | - -### 3.2 quote_formulas 현재 데이터 (샘플) - -``` -[PC] 제품카테고리 (input) => variable -[W0] 가로 (W0) (input) => variable -[H0] 세로 (H0) (input) => variable -[W1_SCREEN] 제작사이즈 W1 (스크린): W0 + 140 => variable -[H1_SCREEN] 제작사이즈 H1 (스크린): H0 + 350 => variable -[W1_STEEL] 제작사이즈 W1 (철재): W0 + 110 => variable -[H1_STEEL] 제작사이즈 H1 (철재): H0 + 350 => variable -[M] 면적 계산: W1 * H1 / 1000000 => variable -[K_SCREEN] 중량 계산 (스크린): M * 2 + W0 / 1000 * 14.17 => variable -[K_STEEL] 중량 계산 (철재): M * 25 => variable -``` - -### 3.3 누락 항목 - -| 항목 | 설명 | 우선순위 | -|------|------|---------| -| `items.process_type` | 공정유형 (스크린/절곡/전기) | 높음 | -| `items.item_category` | 품목분류 (원단/패널/도장 등) | 높음 | -| `category_groups` 테이블 | 면적/중량 기반 분류 | 높음 | -| Design 샘플 품목 데이터 | 100개 품목 Seeder | 높음 | -| BOM 구성 데이터 | 제품별 BOM Seeder | 높음 | -| 단가 데이터 | 품목별 단가 Seeder | 중간 | - ---- - -## 4. 완전 동기화 구현 계획 - -### Phase 1: DB 스키마 확장 (1일) - -#### 1.1 items 테이블 필드 추가 -```sql -ALTER TABLE items ADD COLUMN process_type VARCHAR(20) DEFAULT NULL - COMMENT '공정유형: screen(스크린), bending(절곡), electric(전기), steel(철재)'; - -ALTER TABLE items ADD COLUMN item_category VARCHAR(50) DEFAULT NULL - COMMENT '품목분류: 원단, 패널, 도장, 표면처리, 가이드레일, 케이스, 모터, 제어반 등'; - -CREATE INDEX idx_items_process_type ON items(process_type); -CREATE INDEX idx_items_item_category ON items(item_category); -``` - -#### 1.2 category_groups 테이블 생성 -```sql -CREATE TABLE category_groups ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - code VARCHAR(50) NOT NULL COMMENT '코드: area_based, weight_based, quantity_based', - name VARCHAR(100) NOT NULL COMMENT '이름: 면적기반, 중량기반, 수량기반', - multiplier_variable VARCHAR(20) COMMENT '곱셈 변수: M, K, null', - categories JSON COMMENT '소속 카테고리 목록', - description TEXT, - sort_order INT DEFAULT 0, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - INDEX idx_tenant (tenant_id), - INDEX idx_code (code) -); -``` - -### Phase 2: Seeder 작성 (2일) - -#### 2.1 품목 마스터 Seeder - -**파일**: `database/seeders/DesignItemSeeder.php` - -```php -class DesignItemSeeder extends Seeder -{ - public function run(): void - { - // 원자재 (20개) - $rawMaterials = [ - ['code' => 'RM-S001', 'name' => '강판 1.2T', 'unit' => 'KG', 'price' => 3500, 'category' => '강판'], - ['code' => 'RM-F001', 'name' => '방화원단 A급', 'unit' => 'M2', 'price' => 28000, 'category' => '원단'], - // ... 18개 더 - ]; - - // 부자재 (25개) - $subMaterials = [ - ['code' => 'SM-B001', 'name' => '볼트 M8x30', 'unit' => 'EA', 'price' => 150, 'category' => '볼트'], - // ... 24개 더 - ]; - - // 스크린 반제품 (20개) - $screenSemiProducts = [ - ['code' => 'SF-SCR-F01', 'name' => '스크린 원단', 'unit' => 'M2', 'price' => 35000, 'category' => '원단', 'process' => 'screen'], - ['code' => 'SF-SCR-F02', 'name' => '가이드레일 (좌)', 'unit' => 'M', 'price' => 42000, 'category' => '가이드레일', 'process' => 'screen'], - // ... 18개 더 - ]; - - // 완제품 (14개) - $finishedProducts = [ - ['code' => 'FG-SCR-001', 'name' => '방화스크린 소형', 'category' => 'SCREEN'], - ['code' => 'FG-SCR-002', 'name' => '방화스크린 중형', 'category' => 'SCREEN'], - // ... 12개 더 - ]; - } -} -``` - -#### 2.2 BOM 구성 Seeder - -**파일**: `database/seeders/DesignBomSeeder.php` - -```php -class DesignBomSeeder extends Seeder -{ - public function run(): void - { - $bomData = [ - 'FG-SCR-002' => [ - ['child' => 'SF-SCR-F01', 'qty' => 1, 'formula' => 'W*H/1000000', 'unit' => 'M2'], - ['child' => 'SF-SCR-F02', 'qty' => 1, 'formula' => 'H/1000', 'unit' => 'M'], - ['child' => 'SF-SCR-F03', 'qty' => 1, 'formula' => 'H/1000', 'unit' => 'M'], - ['child' => 'SF-SCR-F04', 'qty' => 1, 'formula' => '', 'unit' => 'EA'], - // ... 더 많은 BOM 라인 - ], - // ... 다른 제품들 - ]; - } -} -``` - -#### 2.3 CategoryGroup Seeder - -```php -class CategoryGroupSeeder extends Seeder -{ - public function run(): void - { - $groups = [ - [ - 'code' => 'area_based', - 'name' => '면적기반', - 'multiplier_variable' => 'M', - 'categories' => json_encode(['원단', '패널', '도장', '표면처리']), - ], - [ - 'code' => 'weight_based', - 'name' => '중량기반', - 'multiplier_variable' => 'K', - 'categories' => json_encode(['강판', '알루미늄']), - ], - [ - 'code' => 'quantity_based', - 'name' => '수량기반', - 'multiplier_variable' => null, - 'categories' => json_encode(['볼트', '너트', '모터', '제어반']), - ], - ]; - } -} -``` - -### Phase 3: 백엔드 로직 확장 (2일) - -#### 3.1 FormulaEvaluatorService 확장 - -**추가할 메서드:** - -```php -/** - * 카테고리 기반 단가 계산 - */ -private function calculateCategoryPrice( - array $item, - float $basePrice, - array $variables -): array { - $categoryGroup = CategoryGroup::query() - ->whereJsonContains('categories', $item['item_category'] ?? '') - ->first(); - - if (!$categoryGroup || !$categoryGroup->multiplier_variable) { - return [ - 'final_price' => $basePrice, - 'calculation_note' => '수량단가', - 'multiplier' => 1, - ]; - } - - $multiplierVar = $categoryGroup->multiplier_variable; - $multiplierValue = $variables[$multiplierVar] ?? 1; - - return [ - 'final_price' => $basePrice * $multiplierValue, - 'calculation_note' => "{$categoryGroup->name} ({$basePrice}원/{$this->getUnit($multiplierVar)} × {$multiplierValue})", - 'multiplier' => $multiplierValue, - ]; -} - -/** - * 공정별 품목 그룹화 - */ -private function groupItemsByProcess(array $items): array -{ - $processOrder = [ - 'screen' => ['label' => '스크린 공정', 'items' => [], 'subtotal' => 0], - 'bending' => ['label' => '절곡 공정', 'items' => [], 'subtotal' => 0], - 'electric' => ['label' => '전기 공정', 'items' => [], 'subtotal' => 0], - 'assembly' => ['label' => '조립 공정', 'items' => [], 'subtotal' => 0], - 'etc' => ['label' => '기타', 'items' => [], 'subtotal' => 0], - ]; - - foreach ($items as $item) { - $process = $item['process_type'] ?? 'etc'; - if (isset($processOrder[$process])) { - $processOrder[$process]['items'][] = $item; - $processOrder[$process]['subtotal'] += $item['total_price'] ?? 0; - } else { - $processOrder['etc']['items'][] = $item; - $processOrder['etc']['subtotal'] += $item['total_price'] ?? 0; - } - } - - return array_filter($processOrder, fn($g) => count($g['items']) > 0); -} - -/** - * 10단계 디버깅 정보 생성 - */ -private function generateDebugInfo( - array $bomLine, - array $variables, - float $calculatedQty, - float $basePrice, - float $finalPrice, - float $totalPrice, - string $priceSource, - string $calcNote -): array { - return [ - 'step1_formula' => $bomLine['quantity_formula'] ?? '수식 없음', - 'step2_variables' => $variables, - 'step3_quantity_calc' => $this->buildQuantityCalcString($bomLine, $variables, $calculatedQty), - 'step4_quantity' => $calculatedQty, - 'step5_price_source' => $priceSource, - 'step6_base_price' => $basePrice, - 'step7_category_multiplier' => $calcNote, - 'step8_final_price' => $finalPrice, - 'step9_total_calc' => sprintf('%.2f × %s = %s', $calculatedQty, number_format($finalPrice), number_format($totalPrice)), - 'step10_total' => $totalPrice, - ]; -} -``` - -#### 3.2 executeAll() 반환 구조 확장 - -```php -public function executeAll(array $inputVariables): array -{ - // 1. 변수 계산 - $calculatedVariables = $this->calculateVariables($inputVariables); - - // 2. 제품 BOM 조회 - $product = Item::where('code', $inputVariables['PRODUCT_ID'])->first(); - $bomTree = $this->getBomTree($product); - - // 3. BOM 항목별 계산 - $bomItems = []; - foreach ($bomTree as $bomLine) { - $result = $this->calculateBomItem($bomLine, $calculatedVariables); - $bomItems[] = $result; - } - - // 4. 공정별 그룹화 - $groupedByProcess = $this->groupItemsByProcess($bomItems); - - // 5. 총합계 - $totalAmount = array_sum(array_column($bomItems, 'total_price')); - - return [ - 'input_variables' => $inputVariables, - 'calculated_variables' => $calculatedVariables, - 'product' => [ - 'code' => $product->code, - 'name' => $product->name, - 'category' => $product->item_details->product_category ?? null, - ], - 'bom_items' => $bomItems, - 'grouped_by_process' => $groupedByProcess, - 'summary' => [ - 'total_items' => count($bomItems), - 'total_amount' => $totalAmount, - ], - ]; -} -``` - -### Phase 4: 프론트엔드 확장 (1일) - -#### 4.1 simulator.blade.php 결과 표시 개선 - -```blade -{{-- 공정별 그룹화 결과 --}} -@if(isset($result['grouped_by_process'])) -
- @foreach($result['grouped_by_process'] as $processCode => $group) -
-
-

{{ $group['label'] }}

- - 소계: {{ number_format($group['subtotal']) }}원 - -
- - - - - - - - - - - - - @foreach($group['items'] as $item) - - - - - - - - - @endforeach - -
품목코드품목명수량단위단가금액
{{ $item['item_code'] }}{{ $item['item_name'] }}{{ number_format($item['calculated_quantity'], 2) }}{{ $item['unit'] }}{{ number_format($item['final_price']) }}{{ number_format($item['total_price']) }}
-
- @endforeach -
- -{{-- 총합계 --}} -
-
- 총 합계 - - {{ number_format($result['summary']['total_amount']) }}원 - -
-
-``` - -### Phase 5: 검증 및 동기화 (1일) - -#### 5.1 테스트 케이스 - -| 입력값 | Design 결과 | MNG 목표 | -|--------|------------|----------| -| W0=2000, H0=2500, PC=스크린 | W1=2140, H1=2850, M=6.099 | 동일 | -| 스크린 원단 (면적단가) | 35,000 × 6.099 = 213,465원 | 동일 | -| 가이드레일 (길이단가) | 42,000 × 2.85 = 119,700원 | 동일 | -| 모터 (고정단가) | 480,000 × 1 = 480,000원 | 동일 | - -#### 5.2 검증 스크립트 - -```php -// php artisan tinker - -// 동일 입력으로 계산 비교 -$input = [ - 'PC' => '스크린', - 'PRODUCT_ID' => 'FG-SCR-002', - 'W0' => 2000, - 'H0' => 2500, - 'GT' => '벽면형', - 'MP' => '220V', - 'CT' => '단독', - 'QTY' => 1, -]; - -$service = app(\App\Services\Quote\FormulaEvaluatorService::class); -$result = $service->executeAll($input); - -// Design 결과와 비교 -dump([ - 'W1' => $result['calculated_variables']['W1'], // 예상: 2140 - 'H1' => $result['calculated_variables']['H1'], // 예상: 2850 - 'M' => $result['calculated_variables']['M'], // 예상: 6.099 - 'total' => $result['summary']['total_amount'], // Design과 동일해야 함 -]); -``` - ---- - -## 5. 핵심 파일 참조 - -### Design (참조용 - 수정 금지) -``` -/SAM/design/src/ -├── components/ -│ ├── AutoCalculationSimulator.tsx # 메인 시뮬레이터 (1068줄) -│ ├── BomCalculationResults.tsx # 결과 표시 컴포넌트 -│ ├── contexts/ -│ │ └── DataContext.tsx # 마스터 데이터 (9859줄) -│ └── utils/ -│ ├── formulaEvaluator.ts # 수식 평가 (312줄) -│ └── bomCalculatorWithDebug.ts # BOM 계산 (232줄) -└── utils/ - ├── sampleQuoteData_Complete.ts # 샘플 품목 데이터 - └── addProductBoms.ts # BOM 구성 데이터 -``` - -### MNG (수정 대상) -``` -/SAM/mng/ -├── app/Services/Quote/ -│ └── FormulaEvaluatorService.php # 핵심 서비스 확장 대상 -├── database/ -│ ├── migrations/ -│ │ └── 20xx_add_simulator_fields.php # 신규 마이그레이션 -│ └── seeders/ -│ ├── DesignItemSeeder.php # 신규 Seeder -│ ├── DesignBomSeeder.php # 신규 Seeder -│ └── CategoryGroupSeeder.php # 신규 Seeder -├── app/Models/ -│ ├── CategoryGroup.php # 신규 모델 -│ ├── Item.php # 필드 추가 -│ └── Price.php # 기존 모델 -└── resources/views/quote-formulas/ - └── simulator.blade.php # UI 확장 -``` - ---- - -## 6. 작업 일정 요약 - -| Phase | 작업 내용 | 예상 일정 | -|-------|----------|----------| -| Phase 1 | DB 스키마 확장 (마이그레이션) | 1일 | -| Phase 2 | Seeder 작성 (품목/BOM/단가/CategoryGroup) | 2일 | -| Phase 3 | FormulaEvaluatorService 확장 | 2일 | -| Phase 4 | simulator.blade.php UI 개선 | 1일 | -| Phase 5 | 검증 및 동기화 테스트 | 1일 | -| **합계** | | **7일** | - ---- - -## 7. 성공 기준 - -1. **계산 결과 동일**: Design과 MNG에서 동일 입력 시 동일한 금액 산출 -2. **10단계 디버깅**: 각 품목별 계산 과정을 10단계로 확인 가능 -3. **공정별 그룹화**: 스크린/절곡/전기 공정별로 품목 분류 -4. **단가 우선순위**: prices 테이블 > items.salesPrice 순서 적용 -5. **면적/중량 기반 단가**: CategoryGroup 설정에 따라 자동 계산 - ---- - -## 8. Serena 컨텍스트 유지 정책 - -> **목적**: 세션 간 컨텍스트 유지를 위해 Serena MCP 메모리에 역할별 분리 저장 - -### 8.1 메모리 구조 - -``` -simulator-rules.md # 패턴, 규칙, 체크리스트 -simulator-mappings.md # 필드 매핑 상세 (Design ↔ MNG) -simulator-progress.md # 진행 상황 -``` - -### 8.2 메모리 내용 - -#### `simulator-rules.md` -- 계산 변수 체계 (W0, H0, W1, H1, M, K 등) -- 수식 평가 함수 목록 (SUM, CEIL, FLOOR, ROUND, IF 등) -- BOM 10단계 계산 프로세스 -- 단가 우선순위 규칙 -- 체크리스트 - -#### `simulator-mappings.md` -- Design TypeScript 인터페이스 ↔ MNG DB 테이블 매핑 -- 품목 타입 매핑 (RM, SM, SF, FG, PT, CS) -- CategoryGroup 매핑 -- 공정 타입 매핑 (screen, bending, electric, assembly) - -#### `simulator-progress.md` -- Phase별 진행 상태 -- 완료된 작업 목록 -- 남은 작업 및 이슈 - -### 8.3 세션 시작/종료 패턴 - -**세션 시작:** -``` -list_memories() → 기존 상태 확인 -read_memory("simulator-progress.md") → 진행 상황 복원 -read_memory("simulator-rules.md") → 규칙 컨텍스트 로드 -``` - -**세션 종료:** -``` -write_memory("simulator-progress.md", 현재 진행 상황) -``` - -### 8.4 초기 메모리 저장 명령 - -```bash -# 세션 시작 시 아래 명령으로 메모리 초기화 -/sc:save simulator-rules # 규칙 저장 -/sc:save simulator-mappings # 매핑 저장 -/sc:save simulator-progress # 진행 상황 저장 -``` - ---- - -## 9. 검증 결과 (Phase 5) - -> **검증일**: 2025-12-24 -> **테스트 환경**: Docker (sam-mng-1) - -### 9.1 테스트 케이스: FG-SCR-001 (W0=2000, H0=2500) - -#### 변수 계산 (Design 마진 적용) -| 변수 | 계산식 | 결과 | 상태 | -|------|--------|------|------| -| W0 | 입력값 | 2000 | ✅ | -| H0 | 입력값 | 2500 | ✅ | -| W1 | W0 + 140 | 2140 | ✅ | -| H1 | H0 + 350 | 2850 | ✅ | -| M | W1 × H1 / 1,000,000 | 6.099 ㎡ | ✅ | - -#### 품목별 계산 결과 -| 품목코드 | 그룹 | 수량 | 단가 | 금액 | 상태 | -|----------|------|------|------|------|------| -| SF-SCR-F01 | area_based | 6.10 | 35,000 | 213,465원 | ✅ | -| SF-SCR-F02 | quantity_based | 2.85 | 42,000 | 119,700원 | ✅ | -| SF-SCR-F03 | quantity_based | 2.85 | 42,000 | 119,700원 | ✅ | -| SF-SCR-F04 | quantity_based | 1.00 | 145,000 | 145,000원 | ✅ | -| SF-SCR-F05 | (미등록) | 1.00 | 55,000 | 55,000원 | ✅ | -| SF-SCR-M01 | quantity_based | 1.00 | 350,000 | 350,000원 | ✅ | -| SF-SCR-C01 | quantity_based | 1.00 | 280,000 | 280,000원 | ✅ | -| SF-SCR-S01 | (미등록) | 1.00 | 180,000 | 180,000원 | ✅ | -| SF-SCR-W01 | (미등록) | 1.00 | 125,000 | 125,000원 | ✅ | -| SF-SCR-B01 | quantity_based | 1.00 | 78,000 | 78,000원 | ✅ | -| SF-SCR-SW01 | quantity_based | 1.00 | 45,000 | 45,000원 | ✅ | -| SM-B002 | quantity_based | 1.00 | 200 | 200원 | ✅ | -| SM-N002 | quantity_based | 1.00 | 100 | 100원 | ✅ | -| SM-W002 | quantity_based | 1.00 | 60 | 60원 | ✅ | -| **합계** | | | | **1,711,225원** | ✅ | - -### 9.2 10단계 디버깅 검증 - -| 단계 | 항목 | 상태 | -|------|------|------| -| Step 1 | 입력값수집 | ✅ | -| Step 2 | 변수계산 | ✅ | -| Step 3 | 완제품선택 | ✅ | -| Step 4 | BOM전개 | ✅ | -| Step 5 | 단가출처 | ✅ | -| Step 6 | 수량계산 | ✅ | -| Step 7 | 금액계산 | ✅ | -| Step 8 | 공정그룹화 | ✅ | -| Step 9 | 소계계산 | ✅ | -| Step 10 | 최종합계 | ✅ | - -### 9.3 공정별 그룹화 검증 - -| 공정 | 품목 수 | 소계 | 상태 | -|------|---------|------|------| -| screen | 11 | 1,710,865원 | ✅ | -| assembly | 3 | 360원 | ✅ | - -### 9.4 단가 우선순위 검증 - -| 품목 | 단가 출처 | 상태 | -|------|----------|------| -| SF-SCR-F01 | items.salesPrice | ✅ | -| SF-SCR-M01 | items.salesPrice | ✅ | -| SM-B002 | items.salesPrice | ✅ | - -> **참고**: ~~prices 테이블에 active 데이터 없음~~ → **2025-12-29 prices 데이터 85개 추가 완료** - -### 9.5 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 계산 결과 동일 | ✅ | Design 마진 (W+140, H+350) 적용 | -| 10단계 디버깅 | ✅ | 모든 단계 정상 출력 | -| 공정별 그룹화 | ✅ | screen, assembly 분류 | -| 단가 우선순위 | ✅ | prices → items.salesPrice 순서 | -| 면적/중량 기반 단가 | ✅ | CategoryGroup 기반 자동 계산 | - -### 9.6 수정 사항 (Phase 5 중) - -1. **면적기반 단가 중복 계산 수정** - - 문제: `total = quantity × (base_price × multiplier)` (중복) - - 수정: 면적/중량기반은 `total = final_price` (이미 multiplier 적용됨) - -2. **마진값 Design 표준 적용** - - 기존: W+100, H+100 - - 수정: W+140, H+350 (스크린 기준) - ---- - -## 10. Phase 6: prices 테이블 데이터 추가 (2025-12-29) - -### 10.1 작업 내용 - -| 항목 | 내용 | -|------|------| -| 작업일 | 2025-12-29 | -| 목적 | prices 테이블에 시뮬레이터용 단가 데이터 추가 | -| Seeder | `DesignPriceSeeder.php` | -| 대상 품목 | 85개 (RM, SM, SF-SCR, SF-STL, SF-BND) | - -### 10.2 생성된 Seeder - -**파일**: `mng/database/seeders/DesignPriceSeeder.php` - -```php -// items.attributes.salesPrice → prices 테이블 이전 -// 단가 우선순위: prices (1순위) → items.attributes (2순위) -``` - -**실행 명령**: -```bash -php artisan db:seed --class=DesignPriceSeeder -``` - -### 10.3 추가된 데이터 - -| 품목 유형 | 코드 패턴 | 수량 | -|----------|----------|------| -| 원자재 | RM-* | 20개 | -| 부자재 | SM-* | 25개 | -| 스크린 반제품 | SF-SCR-* | 20개 | -| 철재 반제품 | SF-STL-* | 16개 | -| 절곡 반제품 | SF-BND-* | 4개 | -| **합계** | | **85개** | - -### 10.4 단가 우선순위 검증 결과 - -``` -=== prices 우선순위 테스트 === -prices 테이블: 99,999원 (테스트용 변경) -items.attributes: 35,000원 (그대로) -getSalesPriceByItemCode(): 99,999원 - -✓ prices 테이블 우선 적용 확인! -``` - -### 10.5 FormulaEvaluatorService 단가 조회 로직 - -```php -// mng/app/Services/Quote/FormulaEvaluatorService.php:379-410 -private function getItemPrice(string $itemCode): float -{ - // 1순위: Price 모델에서 조회 - $price = Price::getSalesPriceByItemCode($tenantId, $itemCode); - if ($price > 0) { - return $price; - } - - // 2순위: Fallback - items.attributes.salesPrice - $item = DB::table('items')->where('code', $itemCode)->first(); - return (float) ($attributes['salesPrice'] ?? 0); -} -``` - ---- - -## 11. Phase 7: 철재 제품 테스트 케이스 (2025-12-30) - -### 11.1 작업 개요 - -| 항목 | 내용 | -|------|------| -| 작업일 | 2025-12-30 | -| 목적 | 철재 제품(FG-STL-*) 마진값/중량 계산 동기화 및 CategoryGroup 적용 | -| 테스트 완제품 | FG-STL-001 (철재 방화문) | -| 입력값 | W0=2000, H0=2500 | - -### 11.2 수정 사항 - -#### 11.2.1 마진값 동적 적용 (SCREEN/STEEL 분기) - -**파일**: `mng/app/Services/Quote/FormulaEvaluatorService.php` - -| 제품 카테고리 | 마진 W | 마진 H | K 계산식 | -|-------------|-------|-------|---------| -| SCREEN (스크린) | W0+140 | H0+350 | M×2 + W0/1000×14.17 | -| STEEL (철재) | W0+110 | H0+350 | M×25 | - -**변경 내용**: -```php -// 제품 카테고리에 따른 마진값 결정 -if (strtoupper($productCategory) === 'STEEL') { - $marginW = 110; // 철재 마진 - $K = $M * 25; // 철재 중량 -} else { - $marginW = 140; // 스크린 기본 마진 - $K = $M * 2 + ($W0 / 1000) * 14.17; // 스크린 중량 -} -``` - -#### 11.2.2 CategoryGroup 데이터 생성 (tenant 287) - -**문제**: CategoryGroup 데이터가 tenant_id=1에만 존재, tenant_id=287 미등록 - -**해결**: tenant 287용 CategoryGroup 3종 생성 - -| 코드 | 이름 | 승수변수 | 포함 카테고리 | -|------|------|---------|-------------| -| area_based | 면적기반 | M | 원단, 패널, 도장, 표면처리, 유리, 도어, 프레임, 창틀 | -| weight_based | 중량기반 | K | 강판, 알루미늄, 스테인리스, 철재 | -| quantity_based | 수량기반 | (없음) | 볼트, 경첩, 도어락, 도어클로저, 실링재, 문턱, 킥플레이트 등 | - -### 11.3 테스트 결과 - -#### 11.3.1 변수 계산 검증 - -| 변수 | 계산값 | 예상값 | 상태 | -|------|-------|-------|------| -| W1 | 2110 | 2110 (W0+110) | ✅ | -| H1 | 2850 | 2850 (H0+350) | ✅ | -| M | 6.0135 ㎡ | 6.0135 | ✅ | -| K | 150.34 kg | 150.34 (M×25) | ✅ | -| PC | STEEL | STEEL | ✅ | - -#### 11.3.2 CategoryGroup 적용 검증 - -| 품목 | 카테고리 | CategoryGroup | 기준단가 | 승수 | 최종단가 | -|------|---------|--------------|---------|------|---------| -| 철재 도어 | 도어 | area_based | 320,000 | M×6.01 | 1,924,320원 | -| 철재 프레임 | 프레임 | area_based | 58,000 | M×6.01 | 348,783원 | -| 철재 패널 | 패널 | area_based | 68,000 | M×6.01 | 408,918원 | -| 경첩 세트 | 경첩 | quantity_based | 42,000 | - | 42,000원 | -| 도어락 | 도어락 | quantity_based | 95,000 | - | 95,000원 | -| 도어클로저 | 도어클로저 | quantity_based | 115,000 | - | 115,000원 | -| 실링재 | 실링재 | quantity_based | 9,500 | - | 9,500원 | -| 문턱 | 문턱 | quantity_based | 58,000 | - | 58,000원 | -| 킥플레이트 | 킥플레이트 | quantity_based | 45,000 | - | 45,000원 | -| 볼트 세트 | 볼트 | quantity_based | 18,000 | - | 18,000원 | - -**최종 합계**: 3,158,111원 ✅ - -### 11.4 수정된 파일 - -| 파일 | 수정 내용 | -|------|----------| -| `FormulaEvaluatorService.php` | 마진값/K계산 동적 분기, `getItemDetails()`에 item_category 추가 | -| `category_groups` (DB) | tenant 287용 3개 그룹 생성 | - -### 11.5 성공 기준 달성 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 철재 마진 적용 | ✅ | W+110 정상 적용 | -| 철재 중량 계산 | ✅ | M×25 정상 적용 | -| CategoryGroup 매칭 | ✅ | area_based, quantity_based 정상 | -| 면적기반 단가 계산 | ✅ | base_price × M 정상 | -| 수량기반 단가 계산 | ✅ | base_price 그대로 적용 | - -### 11.6 절곡 제품 테스트 (FG-BND-001) - -#### 테스트 결과 - -| 변수 | 계산값 | 상태 | -|------|-------|------| -| W1 | 2110 (W0+110) | ✅ 철재 마진 적용 | -| M | 6.0135 ㎡ | ✅ | -| K | 150.34 kg (M×25) | ✅ 철재 중량 | -| PC | STEEL | ✅ | - -#### CategoryGroup 수정 - -**문제**: "절곡" 카테고리가 CategoryGroup 미등록 → 단가 0원 - -**해결**: `area_based`에 "절곡" 카테고리 추가 - -```json -// area_based categories (수정 후) -["원단","패널","도장","표면처리","스크린원단","유리","도어","프레임","창틀","절곡"] -``` - -#### 수정 후 단가 계산 - -| 품목 | CategoryGroup | 기준단가 | 승수 | 최종단가 | -|------|--------------|---------|------|---------| -| 절곡 | area_based | 28,000 | M×6.01 | 168,378원 | -| 프레임 | area_based | 58,000 | M×6.01 | 348,783원 | -| 도장 | area_based | 32,000 | M×6.01 | 192,432원 | -| 볼트 | quantity_based | 18,000 | - | 18,000원 | - -**최종 합계**: 727,893원 ✅ - -### 11.7 전체 제품 유형 검증 완료 - -| 제품 유형 | 코드 | 마진 | K 계산 | 합계 | -|----------|------|------|--------|------| -| 스크린 | FG-SCR-001 | W+140 ✅ | M×2+W0/1000×14.17 ✅ | 1,711,225원 | -| 철재 | FG-STL-001 | W+110 ✅ | M×25 ✅ | 3,158,111원 | -| 절곡 | FG-BND-001 | W+110 ✅ | M×25 ✅ | 727,893원 | - ---- - -*이 문서는 design.sam.kr 완전 분석을 바탕으로 mng 시뮬레이터 완전 동기화 계획을 상세히 기술합니다.* \ No newline at end of file diff --git a/plans/archive/stock-integration-plan.md b/plans/archive/stock-integration-plan.md deleted file mode 100644 index 5926cd5..0000000 --- a/plans/archive/stock-integration-plan.md +++ /dev/null @@ -1,421 +0,0 @@ -# 재고 통합 시스템 개발 계획 - -> **작성일**: 2025-01-26 -> **목적**: 입고/생산/견적 시스템과 재고(Stock)의 실시간 연동 구현 -> **기준 문서**: `docs/specs/database-schema.md`, `docs/standards/api-rules.md` -> **상태**: 🔄 계획 수립 중 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3 - 견적/출하 → 재고 연동 완료 | -| **다음 작업** | ✅ 모든 Phase 완료 | -| **진행률** | 12/12 (100%) | -| **마지막 업데이트** | 2025-01-26 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM 시스템의 재고 관리는 **조회 전용**으로만 작동합니다: -- 입고(Receiving)가 완료되어도 Stock이 증가하지 않음 -- 생산(WorkOrder)에서 자재를 투입해도 Stock이 감소하지 않음 -- 견적(Order)이 확정되어도 재고 예약이 되지 않음 -- 출하(Shipment)가 완료되어도 Stock이 감소하지 않음 - -**결과**: 재고현황 페이지가 실제 재고를 반영하지 못함 - -### 1.2 목표 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 목표 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 입고 완료 → Stock 자동 증가 + StockLot 생성 │ -│ 2. 자재 투입 → Stock 자동 차감 (FIFO 기반) │ -│ 3. 견적 확정 → reserved_qty 증가 │ -│ 4. 출하 완료 → stock_qty 차감 │ -│ 5. 모든 변경에 대한 감사 로그 기록 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 성공 기준 - -| 기준 | 측정 방법 | -|------|----------| -| 입고 → 재고 연동 | 입고 완료 시 Stock.stock_qty 자동 증가 확인 | -| 생산 → 재고 연동 | 자재 투입 시 Stock.stock_qty 자동 감소 확인 | -| 견적 → 재고 연동 | 견적 확정 시 Stock.reserved_qty 증가 확인 | -| 출하 → 재고 연동 | 출하 완료 시 Stock.stock_qty 감소 확인 | -| 감사 로그 | 모든 재고 변경이 audit_logs에 기록됨 | -| FIFO 적용 | StockLot이 fifo_order 순서대로 차감됨 | - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 메서드 추가, 파라미터 추가, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | Service 로직 변경, 새 이벤트 추가, 마이그레이션 | **필수** | -| 🔴 금지 | 기존 API 응답 구조 변경, Stock 테이블 컬럼 삭제 | 별도 협의 | - -### 1.5 준수 규칙 -- `docs/standards/api-rules.md` - Service-First 패턴 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - DB 스키마 규칙 - ---- - -## 2. 현재 시스템 분석 - -### 2.1 데이터 모델 관계 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 현재 상태 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ Item (품목) │ -│ ↓ 1:1 │ -│ Stock (재고현황) ←── 자동 업데이트 없음 ──┐ │ -│ ↓ 1:N │ │ -│ StockLot (LOT별 상세) ←── 자동 생성 없음 ─┤ │ -│ │ │ -│ Receiving (입고) ─── 연결 끊김 ────────────┤ │ -│ WorkOrder (생산) ─── 연결 없음 ────────────┤ │ -│ Order (견적/수주) ─── 연결 없음 ───────────┤ │ -│ Shipment (출하) ─── 연결 없음 ─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 목표 데이터 흐름 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 목표 상태 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ [입고 완료] ──→ StockLot 생성 ──→ Stock.refreshFromLots() │ -│ │ -│ [자재 투입] ──→ StockLot 차감(FIFO) ──→ Stock.refreshFromLots()│ -│ │ -│ [견적 확정] ──→ Stock.reserved_qty 증가 │ -│ │ -│ [출하 완료] ──→ StockLot 차감 ──→ Stock.refreshFromLots() │ -│ ──→ Stock.reserved_qty 감소 │ -│ │ -│ [모든 변경] ──→ AuditLog 기록 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2.3 핵심 파일 위치 - -| 구분 | 경로 | -|------|------| -| **Stock 모델** | `api/app/Models/Tenants/Stock.php` | -| **StockLot 모델** | `api/app/Models/Tenants/StockLot.php` | -| **StockService** | `api/app/Services/StockService.php` | -| **ReceivingService** | `api/app/Services/ReceivingService.php` | -| **WorkOrderService** | `api/app/Services/WorkOrderService.php` | -| **OrderService** | `api/app/Services/OrderService.php` | - ---- - -## 3. 대상 범위 - -### Phase 1: 입고 → 재고 연동 (우선순위 1) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | StockService에 이벤트 기반 구조 설계 | ✅ | increaseFromReceiving(), getOrCreateStock() | -| 1.2 | ReceivingService.process() 수정 - Stock 연동 | ✅ | StockService 호출 추가 | -| 1.3 | StockLot 자동 생성 로직 구현 | ✅ | FIFO 순서 자동 계산 | -| 1.4 | 감사 로그 통합 | ✅ | logStockChange() 구현 | -| 1.5 | 단위 테스트 작성 | ⏭️ | 수동 테스트로 대체 | - -### Phase 2: 생산 → 재고 연동 (우선순위 2) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | WorkOrderService에 BOM 기반 자재 조회 구현 | ✅ | getMaterials() 실제 재고 연동 | -| 2.2 | 자재 투입 시 Stock 차감 로직 (FIFO) | ✅ | StockService.decreaseFIFO() | -| 2.3 | 작업 완료 시 제품 Stock 증가 로직 | ⏭️ | 추후 구현 (생산품 LOT 생성 시) | -| 2.4 | 단위 테스트 작성 | ⏭️ | 수동 테스트로 대체 | - -### Phase 3: 견적/출하 → 재고 연동 (우선순위 3) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | Order 확정 시 reserved_qty 증가 로직 | ✅ | StockService.reserve(), reserveForOrder() | -| 3.2 | Shipment 출하 시 stock_qty 차감 로직 | ✅ | StockService.decreaseForShipment() | -| 3.3 | 예약 취소/변경 처리 로직 | ✅ | StockService.releaseReservation() | - ---- - -## 4. 상세 설계 - -### 4.1 StockService 이벤트 구조 - -```php -// api/app/Services/StockService.php - -class StockService -{ - /** - * 입고 완료 시 재고 증가 - * @param Receiving $receiving - * @return StockLot - */ - public function increaseFromReceiving(Receiving $receiving): StockLot - { - // 1. StockLot 생성 - // 2. Stock.refreshFromLots() 호출 - // 3. 감사 로그 기록 - } - - /** - * 자재 투입 시 재고 차감 (FIFO) - * @param int $itemId - * @param float $qty - * @param string $reason (work_order, shipment 등) - * @param int $referenceId - * @return array 차감된 LOT 정보 - */ - public function decreaseFIFO(int $itemId, float $qty, string $reason, int $referenceId): array - { - // 1. StockLot을 fifo_order 순서로 조회 - // 2. 필요 수량만큼 차감 (여러 LOT에 걸칠 수 있음) - // 3. Stock.refreshFromLots() 호출 - // 4. 감사 로그 기록 - } - - /** - * 재고 예약 - * @param int $itemId - * @param float $qty - * @param int $orderId - */ - public function reserve(int $itemId, float $qty, int $orderId): void - { - // 1. Stock.reserved_qty 증가 - // 2. Stock.available_qty 재계산 - // 3. 감사 로그 기록 - } - - /** - * 예약 해제 - */ - public function releaseReservation(int $itemId, float $qty, int $orderId): void - { - // reserved_qty 감소 - } -} -``` - -### 4.2 ReceivingService 수정 사항 - -```php -// api/app/Services/ReceivingService.php - process() 메서드 수정 - -public function process(Receiving $receiving, array $data): Receiving -{ - return DB::transaction(function () use ($receiving, $data) { - // 기존 로직 유지 - $receiving->update([ - 'receiving_qty' => $data['receiving_qty'], - 'receiving_date' => $data['receiving_date'], - 'lot_no' => $data['lot_no'], - 'status' => 'completed', - ]); - - // 🆕 재고 연동 추가 - app(StockService::class)->increaseFromReceiving($receiving); - - return $receiving->fresh(); - }); -} -``` - -### 4.3 WorkOrderService 수정 사항 - -```php -// api/app/Services/WorkOrderService.php - registerMaterialInput() 수정 - -public function registerMaterialInput(WorkOrder $workOrder, array $data): void -{ - DB::transaction(function () use ($workOrder, $data) { - // 기존 감사 로그 유지 - - // 🆕 재고 차감 추가 - $stockService = app(StockService::class); - - foreach ($data['materials'] as $material) { - $stockService->decreaseFIFO( - itemId: $material['item_id'], - qty: $material['qty'], - reason: 'work_order_input', - referenceId: $workOrder->id - ); - } - }); -} -``` - -### 4.4 감사 로그 구조 - -| 필드 | 값 | -|------|------| -| `auditable_type` | `Stock` | -| `auditable_id` | Stock ID | -| `event` | `stock_increase`, `stock_decrease`, `stock_reserve` | -| `old_values` | 변경 전 수량 | -| `new_values` | 변경 후 수량 + 사유 + 참조 ID | - ---- - -## 5. 작업 절차 - -### Step 1: Phase 1 - 입고 → 재고 연동 - -``` -1.1 StockService 이벤트 메서드 추가 -├── increaseFromReceiving() 구현 -├── 감사 로그 통합 -└── 단위 테스트 - -1.2 ReceivingService.process() 수정 -├── 기존 로직 분석 -├── StockService 호출 추가 -└── 트랜잭션 보장 - -1.3 StockLot 자동 생성 -├── Receiving 정보로 StockLot 생성 -├── fifo_order 자동 계산 -└── Stock.refreshFromLots() 호출 - -1.4 테스트 및 검증 -├── 입고 생성 → 입고처리 → Stock 확인 -└── 감사 로그 확인 -``` - -### Step 2: Phase 2 - 생산 → 재고 연동 - -``` -2.1 BOM 기반 자재 조회 구현 -├── 품목의 BOM 정보 조회 -├── Mock 데이터 제거 -└── 실제 자재 목록 반환 - -2.2 자재 투입 시 Stock 차감 -├── decreaseFIFO() 구현 -├── 여러 LOT 걸쳐 차감 처리 -└── 재고 부족 시 예외 처리 - -2.3 작업 완료 시 제품 Stock 증가 -├── 생산된 제품의 StockLot 생성 -├── Stock.refreshFromLots() 호출 -└── 감사 로그 기록 -``` - -### Step 3: Phase 3 - 견적/출하 → 재고 연동 - -``` -3.1 Order 확정 시 예약 -├── reserve() 호출 -├── available_qty 감소 -└── 오버부킹 방지 검증 - -3.2 Shipment 출하 시 차감 -├── decreaseFIFO() 호출 -├── reserved_qty 동시 감소 -└── 감사 로그 기록 -``` - ---- - -## 6. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | ReceivingService.process() | Stock 연동 로직 추가 | 입고 프로세스 | ⏳ 대기 | -| 2 | WorkOrderService.registerMaterialInput() | Stock 차감 로직 추가 | 생산 프로세스 | ⏳ 대기 | -| 3 | ShipmentService (신규) | Stock 차감 로직 추가 | 출하 프로세스 | ⏳ 대기 | - ---- - -## 7. 리스크 및 대응 - -### 7.1 데이터 정합성 리스크 - -| 리스크 | 확률 | 영향 | 대응 | -|--------|------|------|------| -| 트랜잭션 실패 시 Stock만 변경됨 | 중 | 높음 | DB 트랜잭션으로 원자성 보장 | -| 동시 요청 시 재고 충돌 | 중 | 높음 | 비관적 락(FOR UPDATE) 적용 | -| 재고 부족 상태에서 차감 시도 | 높음 | 중 | 사전 검증 + 예외 처리 | - -### 7.2 성능 리스크 - -| 리스크 | 확률 | 영향 | 대응 | -|--------|------|------|------| -| LOT가 많을 경우 FIFO 조회 느림 | 낮음 | 중 | fifo_order 인덱스 확인 | -| refreshFromLots() 빈번 호출 | 중 | 낮음 | 필요 시에만 호출 (이미 구현됨) | - ---- - -## 8. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-26 | Phase 3 | 견적/출하→재고 연동 구현 완료 | StockService, OrderService, ShipmentService | ✅ | -| 2025-01-26 | Phase 2 | 생산→재고 연동 구현 완료 | StockService, WorkOrderService | ✅ | -| 2025-01-26 | Phase 1 | 입고→재고 연동 구현 완료 | StockService, ReceivingService | ✅ | -| 2025-01-26 | - | 문서 초안 작성 | - | - | - ---- - -## 9. 참고 문서 - -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경, 1.2 목표 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 1.3 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 3 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 2.3 핵심 파일 위치 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 작업 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 1.3 성공 기준 | -| 8 | 모호한 표현이 없는가? | ✅ | | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 1.2 목표 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 3. 대상 범위 Phase 1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2.3 핵심 파일 위치 | -| Q4. 작업 완료 확인 방법은? | ✅ | 1.3 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/welfare-section-plan.md b/plans/archive/welfare-section-plan.md deleted file mode 100644 index 94541ed..0000000 --- a/plans/archive/welfare-section-plan.md +++ /dev/null @@ -1,1021 +0,0 @@ -# 복리후생비 현황 섹션 개발 계획 - -> **작성일**: 2026-01-22 -> **목적**: CEO 대시보드 복리후생비 현황 섹션 완성 (4개 카드 + 모달 API 연동) -> **기준 문서**: `api/app/Swagger/v1/WelfareApi.php` -> **상태**: 🔄 진행중 (Serena ID: welfare-section-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 2 - 프론트엔드 연동 완료 | -| **다음 작업** | 검증 및 테스트 | -| **진행률** | 6/6 (100%) | -| **마지막 업데이트** | 2026-01-22 | - ---- - -## 1. 개요 - -### 1.1 배경 -CEO 대시보드의 복리후생비 현황 섹션은 4개의 카드로 구성됩니다: -1. **당해년도 복리후생비 한도** - 연간 총 한도 -2. **{분기} 복리후생비 총 한도** - 분기별 한도 -3. **{분기} 복리후생비 잔여한도** - 분기별 남은 한도 -4. **{분기} 복리후생비 사용금액** - 분기별 사용 금액 - -현재 상태: -- ✅ 섹션 UI 컴포넌트: 완료 (`WelfareSection.tsx`) -- ✅ 카드 데이터 API: 완료 (`/api/v1/welfare/summary`) -- ✅ 프론트엔드 Hook: 완료 (`useWelfare()`) -- ⚠️ 모달 상세 데이터: Mock 사용 중 (API 연동 필요) - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. API-First: 백엔드 API 완성 후 프론트엔드 연동 │ -│ 2. 기존 패턴 준수: WelfareService 확장 │ -│ 3. Mock 데이터 구조 유지: 기존 모달 설정 형식 호환 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 모달 설정 Mock → API 변환, Transformer 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 새 API 엔드포인트 추가, 서비스 메서드 추가 | **필수** | -| 🔴 금지 | expense_accounts 테이블 구조 변경 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `api/CLAUDE.md` - SAM API Development Rules - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: API 개발 (Backend) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | 모달 상세 데이터 API 개발 | ✅ | `/api/v1/welfare/detail` | -| 1.2 | Swagger 문서 업데이트 | ✅ | WelfareApi.php | - -### 2.2 Phase 2: 프론트엔드 연동 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 타입 정의 추가 | ✅ | WelfareDetailApiResponse (types.ts) | -| 2.2 | API 함수 추가 | ✅ | useWelfareDetail hook (useCEODashboard.ts) | -| 2.3 | Transformer 추가 | ✅ | transformWelfareDetailResponse (transformers.ts) | -| 2.4 | 모달 설정 동적 생성 | ✅ | CEODashboard.tsx 연동 + fallback | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: API 개발 (Backend) -├── WelfareService에 getDetail() 메서드 추가 -├── WelfareController에 detail() 액션 추가 -├── routes/api.php에 라우트 등록 -└── Swagger 문서 작성 - -Step 2: 프론트엔드 연동 -├── types.ts에 WelfareDetailApiResponse 추가 -├── useCEODashboard.ts에 fetchWelfareDetail 추가 -├── transformers.ts에 transformWelfareDetailResponse 추가 -└── welfareConfigs.ts를 API 응답 기반으로 수정 -``` - ---- - -## 4. 핵심 참조 코드 (인라인) - -### 4.1 DetailModalConfig 타입 정의 - -**파일**: `react/src/components/business/CEODashboard/types.ts` (라인 414-426) - -```typescript -// 상세 모달 전체 설정 타입 -export interface DetailModalConfig { - title: string; - summaryCards: SummaryCardData[]; - barChart?: BarChartConfig; - pieChart?: PieChartConfig; - horizontalBarChart?: HorizontalBarChartConfig; - comparisonSection?: ComparisonSectionConfig; - referenceTable?: ReferenceTableConfig; - referenceTables?: ReferenceTableConfig[]; - calculationCards?: CalculationCardsConfig; - quarterlyTable?: QuarterlyTableConfig; - table?: TableConfig; -} -``` - -### 4.2 관련 서브 타입 정의 - -```typescript -// 요약 카드 타입 (라인 249-255) -export interface SummaryCardData { - label: string; - value: string | number; - isComparison?: boolean; - isPositive?: boolean; - unit?: string; -} - -// 막대 차트 설정 타입 (라인 265-271) -export interface BarChartConfig { - title: string; - data: BarChartDataItem[]; - dataKey: string; - xAxisKey: string; - color?: string; -} - -// 도넛 차트 설정 타입 (라인 282-285) -export interface PieChartConfig { - title: string; - data: PieChartDataItem[]; -} - -// 도넛 차트 데이터 아이템 (라인 274-279) -export interface PieChartDataItem { - name: string; - value: number; - percentage: number; - color: string; -} - -// 테이블 설정 타입 (라인 332-342) -export interface TableConfig { - title: string; - columns: TableColumnConfig[]; - data: Record[]; - filters?: TableFilterConfig[]; - showTotal?: boolean; - totalLabel?: string; - totalValue?: string | number; - totalColumnKey?: string; - footerSummary?: FooterSummaryItem[]; -} - -// 계산 카드 섹션 설정 타입 (라인 391-395) -export interface CalculationCardsConfig { - title: string; - subtitle?: string; - cards: CalculationCardItem[]; -} - -// 계산 카드 아이템 타입 (라인 383-388) -export interface CalculationCardItem { - label: string; - value: number; - unit?: string; - operator?: '+' | '=' | '-' | '×'; -} - -// 분기별 테이블 설정 타입 (라인 408-411) -export interface QuarterlyTableConfig { - title: string; - rows: QuarterlyTableRow[]; -} - -// 분기별 테이블 행 타입 (라인 398-405) -export interface QuarterlyTableRow { - label: string; - q1?: number | string; - q2?: number | string; - q3?: number | string; - q4?: number | string; - total?: number | string; -} -``` - -### 4.3 현재 Mock 데이터 구조 (welfareConfigs.ts 전체) - -**파일**: `react/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts` - -```typescript -import type { DetailModalConfig } from '../types'; - -export function getWelfareModalConfig(calculationType: 'fixed' | 'ratio'): DetailModalConfig { - // 계산 방식에 따른 조건부 calculationCards 생성 - const calculationCards = calculationType === 'fixed' - ? { - // 직원당 정액 금액/월 방식 - title: '복리후생비 계산', - subtitle: '직원당 정액 금액/월 200,000원', - cards: [ - { label: '직원 수', value: 20, unit: '명' }, - { label: '연간 직원당 월급 금액', value: 2400000, unit: '원', operator: '×' as const }, - { label: '당해년도 복리후생비 총 한도', value: 48000000, unit: '원', operator: '=' as const }, - ], - } - : { - // 연봉 총액 비율 방식 - title: '복리후생비 계산', - subtitle: '연봉 총액 기준 비율 20.5%', - cards: [ - { label: '연봉 총액', value: 1000000000, unit: '원' }, - { label: '비율', value: 20.5, unit: '%', operator: '×' as const }, - { label: '당해년도 복리후생비 총 한도', value: 205000000, unit: '원', operator: '=' as const }, - ], - }; - - return { - title: '복리후생비 상세', - - // 1. 요약 카드 (8개) - summaryCards: [ - // 1행: 당해년도 기준 - { label: '당해년도 복리후생비 계정', value: 3123000, unit: '원' }, - { label: '당해년도 복리후생비 한도', value: 600000, unit: '원' }, - { label: '당해년도 복리후생비 사용', value: 6000000, unit: '원' }, - { label: '당해년도 잔여한도', value: 0, unit: '원' }, - // 2행: 분기 기준 - { label: '1사분기 복리후생비 총 한도', value: 3123000, unit: '원' }, - { label: '1사분기 복리후생비 잔여한도', value: 6000000, unit: '원' }, - { label: '1사분기 복리후생비 사용금액', value: 6000000, unit: '원' }, - { label: '1사분기 복리후생비 초과 금액', value: 6000000, unit: '원' }, - ], - - // 2. 월별 사용 추이 (막대 차트) - barChart: { - title: '월별 복리후생비 사용 추이', - data: [ - { name: '1월', value: 1500000 }, - { name: '2월', value: 1800000 }, - { name: '3월', value: 2200000 }, - { name: '4월', value: 1900000 }, - { name: '5월', value: 2100000 }, - { name: '6월', value: 1700000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - - // 3. 항목별 사용 비율 (도넛 차트) - pieChart: { - title: '항목별 사용 비율', - data: [ - { name: '식비', value: 55000000, percentage: 55, color: '#FBBF24' }, - { name: '건강검진', value: 25000000, percentage: 5, color: '#60A5FA' }, - { name: '경조사비', value: 10000000, percentage: 10, color: '#F87171' }, - { name: '기타', value: 10000000, percentage: 30, color: '#34D399' }, - ], - }, - - // 4. 일별 사용 내역 (테이블) - table: { - title: '일별 복리후생비 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'date', label: '사용일자', align: 'center', format: 'date' }, - { key: 'store', label: '가맹점명', align: 'left' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'usageType', label: '사용항목', align: 'center' }, - ], - data: [ - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1000000, usageType: '식비' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1200000, usageType: '건강검진' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1500000, usageType: '경조사비' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1300000, usageType: '기타' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 6000000, usageType: '식비' }, - ], - filters: [ - { - key: 'usageType', - options: [ - { value: 'all', label: '전체' }, - { value: '식비', label: '식비' }, - { value: '건강검진', label: '건강검진' }, - { value: '경조사비', label: '경조사비' }, - { value: '기타', label: '기타' }, - ], - defaultValue: 'all', - }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 11000000, - totalColumnKey: 'amount', - }, - - // 5. 복리후생비 계산 (조건부 - calculationType에 따라) - calculationCards, - - // 6. 분기별 현황 테이블 - quarterlyTable: { - title: '복리후생비 현황', - rows: [ - { label: '한도금액', q1: 12000000, q2: 12000000, q3: 12000000, q4: 12000000, total: 48000000 }, - { label: '이월금액', q1: 0, q2: '', q3: '', q4: '', total: '' }, - { label: '사용금액', q1: 1000000, q2: '', q3: '', q4: '', total: '' }, - { label: '잔여한도', q1: 11000000, q2: '', q3: '', q4: '', total: '' }, - { label: '초과금액', q1: '', q2: '', q3: '', q4: '', total: '' }, - ], - }, - }; -} -``` - -### 4.4 expense_accounts 테이블 스키마 - -**파일**: `api/database/migrations/2026_01_21_103734_create_expense_accounts_table.php` - -```sql -CREATE TABLE expense_accounts ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', - - -- 비용 유형 - account_type VARCHAR(50) NOT NULL COMMENT '계정 유형: welfare, entertainment, etc.', - sub_type VARCHAR(50) NULL COMMENT '세부 유형: meal, gift, etc.', - - -- 비용 정보 - expense_date DATE NOT NULL COMMENT '지출일', - amount DECIMAL(15,2) DEFAULT 0 COMMENT '금액', - description VARCHAR(500) NULL COMMENT '비용 내역', - receipt_no VARCHAR(100) NULL COMMENT '증빙번호', - - -- 거래처 정보 - vendor_id BIGINT UNSIGNED NULL COMMENT '거래처 ID', - vendor_name VARCHAR(200) NULL COMMENT '거래처명 (직접 입력)', - - -- 카드/결제 정보 - payment_method VARCHAR(50) NULL COMMENT '결제수단: card, cash, transfer', - card_no VARCHAR(50) NULL COMMENT '카드 마지막 4자리', - - -- 감사 컬럼 - created_by BIGINT UNSIGNED NULL COMMENT '등록자', - updated_by BIGINT UNSIGNED NULL COMMENT '수정자', - deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자', - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - deleted_at TIMESTAMP NULL, - - -- 인덱스 - INDEX idx_tenant_type_date (tenant_id, account_type, expense_date), - INDEX idx_tenant_date (tenant_id, expense_date), - - -- 외래키 - FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, - FOREIGN KEY (vendor_id) REFERENCES clients(id) ON DELETE SET NULL -); -``` - -**account_type 값**: -- `welfare` - 복리후생비 -- `entertainment` - 접대비 - -**sub_type 값** (welfare의 경우): -- `meal` - 식비 -- `health_check` - 건강검진 -- `congratulation` - 경조사비 -- `other` - 기타 - ---- - -## 5. API → 모달 설정 변환 매핑 - -### 5.1 API 응답 스키마 (제안) - -```typescript -// 백엔드 API 응답: GET /api/v1/welfare/detail -interface WelfareDetailApiResponse { - // 요약 카드 데이터 - summary: { - annual_account: number; // 당해년도 복리후생비 계정 - annual_limit: number; // 당해년도 복리후생비 한도 - annual_used: number; // 당해년도 복리후생비 사용 - annual_remaining: number; // 당해년도 잔여한도 - quarterly_limit: number; // 분기 복리후생비 총 한도 - quarterly_remaining: number; // 분기 복리후생비 잔여한도 - quarterly_used: number; // 분기 복리후생비 사용금액 - quarterly_exceeded: number; // 분기 복리후생비 초과 금액 - }; - - // 월별 사용 추이 - monthly_usage: { - month: number; // 1-12 - amount: number; - }[]; - - // 항목별 분포 - category_distribution: { - category: string; // meal, health_check, congratulation, other - label: string; // 식비, 건강검진, 경조사비, 기타 - amount: number; - ratio: number; // 백분율 (0-100) - }[]; - - // 일별 사용 내역 - transactions: { - id: number; - card_name: string; - user_name: string; - expense_date: string; // YYYY-MM-DD HH:mm - vendor_name: string; - amount: number; - sub_type: string; - sub_type_label: string; - }[]; - - // 계산 정보 - calculation: { - type: 'fixed' | 'ratio'; - employee_count: number; - monthly_amount?: number; // fixed 방식 - total_salary?: number; // ratio 방식 - ratio?: number; // ratio 방식 (%) - annual_limit: number; - }; - - // 분기별 현황 - quarterly: { - quarter: number; // 1-4 - limit: number; - carryover: number; - used: number; - remaining: number; - exceeded: number; - }[]; -} -``` - -### 5.2 변환 매핑 테이블 - -| API 필드 | DetailModalConfig 필드 | 변환 로직 | -|----------|----------------------|----------| -| `summary.annual_account` | `summaryCards[0].value` | 직접 매핑 | -| `summary.annual_limit` | `summaryCards[1].value` | 직접 매핑 | -| `summary.annual_used` | `summaryCards[2].value` | 직접 매핑 | -| `summary.annual_remaining` | `summaryCards[3].value` | 직접 매핑 | -| `summary.quarterly_limit` | `summaryCards[4].value` | 라벨에 분기 동적 삽입 | -| `summary.quarterly_remaining` | `summaryCards[5].value` | 라벨에 분기 동적 삽입 | -| `summary.quarterly_used` | `summaryCards[6].value` | 라벨에 분기 동적 삽입 | -| `summary.quarterly_exceeded` | `summaryCards[7].value` | 라벨에 분기 동적 삽입 | -| `monthly_usage[]` | `barChart.data[]` | `{ name: '${month}월', value: amount }` | -| `category_distribution[]` | `pieChart.data[]` | 색상 매핑 추가 필요 | -| `transactions[]` | `table.data[]` | 필드명 camelCase 변환 | -| `calculation` | `calculationCards` | type에 따라 분기 | -| `quarterly[]` | `quarterlyTable.rows[]` | 행/열 피벗 변환 | - -### 5.3 색상 매핑 (카테고리별) - -```typescript -const CATEGORY_COLORS: Record = { - meal: '#FBBF24', // 식비 - 노란색 - health_check: '#60A5FA', // 건강검진 - 파란색 - congratulation: '#F87171', // 경조사비 - 빨간색 - other: '#34D399', // 기타 - 초록색 -}; -``` - ---- - -## 6. 상세 작업 내용 - -### 6.1 Phase 1: API 개발 - -#### 1.1 WelfareService 확장 - -**파일**: `api/app/Services/WelfareService.php` - -**추가할 메서드**: -```php -/** - * 복리후생비 상세 정보 조회 (모달용) - */ -public function getDetail( - ?string $calculationType = 'fixed', - ?int $fixedAmountPerMonth = 200000, - ?float $ratio = 0.05, - ?int $year = null, - ?int $quarter = null -): array { - // 1. 요약 데이터 조회 - // 2. 월별 사용 추이 조회 - // 3. 항목별 분포 조회 - // 4. 일별 사용 내역 조회 - // 5. 계산 정보 생성 - // 6. 분기별 현황 조회 -} -``` - -**필요한 쿼리**: -```php -// 월별 사용 추이 -DB::table('expense_accounts') - ->select(DB::raw('MONTH(expense_date) as month'), DB::raw('SUM(amount) as amount')) - ->where('tenant_id', $tenantId) - ->where('account_type', 'welfare') - ->whereYear('expense_date', $year) - ->whereNull('deleted_at') - ->groupBy(DB::raw('MONTH(expense_date)')) - ->orderBy('month') - ->get(); - -// 항목별 분포 -DB::table('expense_accounts') - ->select('sub_type', DB::raw('SUM(amount) as amount')) - ->where('tenant_id', $tenantId) - ->where('account_type', 'welfare') - ->whereBetween('expense_date', [$startDate, $endDate]) - ->whereNull('deleted_at') - ->groupBy('sub_type') - ->get(); - -// 일별 사용 내역 -DB::table('expense_accounts') - ->select('id', 'card_no', 'created_by', 'expense_date', 'vendor_name', 'amount', 'sub_type') - ->where('tenant_id', $tenantId) - ->where('account_type', 'welfare') - ->whereBetween('expense_date', [$startDate, $endDate]) - ->whereNull('deleted_at') - ->orderByDesc('expense_date') - ->get(); -``` - -#### 1.2 WelfareController 확장 - -**파일**: `api/app/Http/Controllers/Api/V1/WelfareController.php` - -**추가할 메서드**: -```php -/** - * 복리후생비 상세 조회 (모달용) - */ -public function detail(Request $request): JsonResponse -{ - $calculationType = $request->query('calculation_type', 'fixed'); - $fixedAmountPerMonth = $request->query('fixed_amount_per_month') - ? (int) $request->query('fixed_amount_per_month') - : 200000; - $ratio = $request->query('ratio') - ? (float) $request->query('ratio') - : 0.05; - $year = $request->query('year') ? (int) $request->query('year') : null; - $quarter = $request->query('quarter') ? (int) $request->query('quarter') : null; - - return ApiResponse::handle(function () use ($calculationType, $fixedAmountPerMonth, $ratio, $year, $quarter) { - return $this->welfareService->getDetail( - $calculationType, - $fixedAmountPerMonth, - $ratio, - $year, - $quarter - ); - }, __('message.fetched')); -} -``` - -#### 1.3 라우트 등록 - -**파일**: `api/routes/api.php` - -```php -Route::prefix('welfare')->group(function () { - Route::get('/summary', [WelfareController::class, 'summary']); - Route::get('/detail', [WelfareController::class, 'detail']); // 추가 -}); -``` - -### 6.2 Phase 2: 프론트엔드 연동 - -#### 2.1 타입 정의 추가 - -**파일**: `react/src/lib/api/dashboard/types.ts` - -```typescript -// Welfare Detail API 응답 타입 -export interface WelfareDetailApiResponse { - summary: { - annual_account: number; - annual_limit: number; - annual_used: number; - annual_remaining: number; - quarterly_limit: number; - quarterly_remaining: number; - quarterly_used: number; - quarterly_exceeded: number; - }; - monthly_usage: { - month: number; - amount: number; - }[]; - category_distribution: { - category: string; - label: string; - amount: number; - ratio: number; - }[]; - transactions: { - id: number; - card_name: string; - user_name: string; - expense_date: string; - vendor_name: string; - amount: number; - sub_type: string; - sub_type_label: string; - }[]; - calculation: { - type: 'fixed' | 'ratio'; - employee_count: number; - monthly_amount?: number; - total_salary?: number; - ratio?: number; - annual_limit: number; - }; - quarterly: { - quarter: number; - limit: number; - carryover: number; - used: number; - remaining: number; - exceeded: number; - }[]; -} -``` - -#### 2.2 API 함수 추가 - -**파일**: `react/src/hooks/useCEODashboard.ts` - -```typescript -export async function fetchWelfareDetail( - options: { - calculationType?: 'fixed' | 'ratio'; - fixedAmountPerMonth?: number; - ratio?: number; - year?: number; - quarter?: number; - } -): Promise { - const params = new URLSearchParams(); - if (options.calculationType) params.append('calculation_type', options.calculationType); - if (options.fixedAmountPerMonth) params.append('fixed_amount_per_month', options.fixedAmountPerMonth.toString()); - if (options.ratio) params.append('ratio', options.ratio.toString()); - if (options.year) params.append('year', options.year.toString()); - if (options.quarter) params.append('quarter', options.quarter.toString()); - - return fetchApi(`welfare/detail?${params.toString()}`); -} -``` - -#### 2.3 Transformer 추가 - -**파일**: `react/src/lib/api/dashboard/transformers.ts` - -```typescript -const CATEGORY_COLORS: Record = { - meal: '#FBBF24', - health_check: '#60A5FA', - congratulation: '#F87171', - other: '#34D399', -}; - -export function transformWelfareDetailToModalConfig( - api: WelfareDetailApiResponse, - quarter: number -): DetailModalConfig { - const quarterLabel = `${quarter}사분기`; - - return { - title: '복리후생비 상세', - - summaryCards: [ - { label: '당해년도 복리후생비 계정', value: api.summary.annual_account, unit: '원' }, - { label: '당해년도 복리후생비 한도', value: api.summary.annual_limit, unit: '원' }, - { label: '당해년도 복리후생비 사용', value: api.summary.annual_used, unit: '원' }, - { label: '당해년도 잔여한도', value: api.summary.annual_remaining, unit: '원' }, - { label: `${quarterLabel} 복리후생비 총 한도`, value: api.summary.quarterly_limit, unit: '원' }, - { label: `${quarterLabel} 복리후생비 잔여한도`, value: api.summary.quarterly_remaining, unit: '원' }, - { label: `${quarterLabel} 복리후생비 사용금액`, value: api.summary.quarterly_used, unit: '원' }, - { label: `${quarterLabel} 복리후생비 초과 금액`, value: api.summary.quarterly_exceeded, unit: '원' }, - ], - - barChart: { - title: '월별 복리후생비 사용 추이', - data: api.monthly_usage.map(m => ({ name: `${m.month}월`, value: m.amount })), - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - - pieChart: { - title: '항목별 사용 비율', - data: api.category_distribution.map(c => ({ - name: c.label, - value: c.amount, - percentage: c.ratio, - color: CATEGORY_COLORS[c.category] || '#9CA3AF', - })), - }, - - table: { - title: '일별 복리후생비 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'date', label: '사용일자', align: 'center', format: 'date' }, - { key: 'store', label: '가맹점명', align: 'left' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'usageType', label: '사용항목', align: 'center' }, - ], - data: api.transactions.map((t, i) => ({ - no: i + 1, - cardName: t.card_name, - user: t.user_name, - date: t.expense_date, - store: t.vendor_name, - amount: t.amount, - usageType: t.sub_type_label, - })), - filters: [ - { - key: 'usageType', - options: [ - { value: 'all', label: '전체' }, - { value: '식비', label: '식비' }, - { value: '건강검진', label: '건강검진' }, - { value: '경조사비', label: '경조사비' }, - { value: '기타', label: '기타' }, - ], - defaultValue: 'all', - }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: api.transactions.reduce((sum, t) => sum + t.amount, 0), - totalColumnKey: 'amount', - }, - - calculationCards: api.calculation.type === 'fixed' - ? { - title: '복리후생비 계산', - subtitle: `직원당 정액 금액/월 ${(api.calculation.monthly_amount || 0).toLocaleString()}원`, - cards: [ - { label: '직원 수', value: api.calculation.employee_count, unit: '명' }, - { label: '연간 직원당 월급 금액', value: (api.calculation.monthly_amount || 0) * 12, unit: '원', operator: '×' }, - { label: '당해년도 복리후생비 총 한도', value: api.calculation.annual_limit, unit: '원', operator: '=' }, - ], - } - : { - title: '복리후생비 계산', - subtitle: `연봉 총액 기준 비율 ${api.calculation.ratio}%`, - cards: [ - { label: '연봉 총액', value: api.calculation.total_salary || 0, unit: '원' }, - { label: '비율', value: api.calculation.ratio || 0, unit: '%', operator: '×' }, - { label: '당해년도 복리후생비 총 한도', value: api.calculation.annual_limit, unit: '원', operator: '=' }, - ], - }, - - quarterlyTable: { - title: '복리후생비 현황', - rows: [ - { - label: '한도금액', - q1: api.quarterly.find(q => q.quarter === 1)?.limit || '', - q2: api.quarterly.find(q => q.quarter === 2)?.limit || '', - q3: api.quarterly.find(q => q.quarter === 3)?.limit || '', - q4: api.quarterly.find(q => q.quarter === 4)?.limit || '', - total: api.quarterly.reduce((sum, q) => sum + q.limit, 0), - }, - { - label: '이월금액', - q1: api.quarterly.find(q => q.quarter === 1)?.carryover || '', - q2: api.quarterly.find(q => q.quarter === 2)?.carryover || '', - q3: api.quarterly.find(q => q.quarter === 3)?.carryover || '', - q4: api.quarterly.find(q => q.quarter === 4)?.carryover || '', - total: '', - }, - { - label: '사용금액', - q1: api.quarterly.find(q => q.quarter === 1)?.used || '', - q2: api.quarterly.find(q => q.quarter === 2)?.used || '', - q3: api.quarterly.find(q => q.quarter === 3)?.used || '', - q4: api.quarterly.find(q => q.quarter === 4)?.used || '', - total: api.quarterly.reduce((sum, q) => sum + q.used, 0), - }, - { - label: '잔여한도', - q1: api.quarterly.find(q => q.quarter === 1)?.remaining || '', - q2: api.quarterly.find(q => q.quarter === 2)?.remaining || '', - q3: api.quarterly.find(q => q.quarter === 3)?.remaining || '', - q4: api.quarterly.find(q => q.quarter === 4)?.remaining || '', - total: '', - }, - { - label: '초과금액', - q1: api.quarterly.find(q => q.quarter === 1)?.exceeded || '', - q2: api.quarterly.find(q => q.quarter === 2)?.exceeded || '', - q3: api.quarterly.find(q => q.quarter === 3)?.exceeded || '', - q4: api.quarterly.find(q => q.quarter === 4)?.exceeded || '', - total: '', - }, - ], - }, - }; -} -``` - -#### 2.4 모달 설정 동적 생성 - -**파일**: `react/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts` - -```typescript -import type { DetailModalConfig } from '../types'; -import { fetchWelfareDetail, transformWelfareDetailToModalConfig } from '@/lib/api/dashboard'; - -// 기존 Mock 함수 (fallback용) -export function getWelfareModalConfigMock(calculationType: 'fixed' | 'ratio'): DetailModalConfig { - // ... 기존 Mock 코드 유지 -} - -// 새로운 API 기반 함수 -export async function getWelfareModalConfigFromApi( - options: { - calculationType: 'fixed' | 'ratio'; - fixedAmountPerMonth?: number; - ratio?: number; - year?: number; - quarter?: number; - } -): Promise { - try { - const apiData = await fetchWelfareDetail(options); - return transformWelfareDetailToModalConfig(apiData, options.quarter || getCurrentQuarter()); - } catch (error) { - console.error('[Welfare] Failed to fetch detail, using mock data:', error); - return getWelfareModalConfigMock(options.calculationType); - } -} - -function getCurrentQuarter(): number { - return Math.ceil((new Date().getMonth() + 1) / 3); -} -``` - ---- - -## 7. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 새 API 엔드포인트 | `GET /api/v1/welfare/detail` 추가 | api | ✅ 완료 | -| 2 | WelfareService 확장 | getDetail() 메서드 추가 | api | ✅ 완료 | - ---- - -## 8. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-22 | - | 문서 초안 작성 | - | - | -| 2026-01-22 | - | 자기완결성 보완 (타입, 스키마, 매핑 추가) | - | - | -| 2026-01-22 | Phase 1.1 | getDetail() 메서드 추가 | WelfareService.php | ✅ | -| 2026-01-22 | Phase 1.1 | detail() 액션 추가 | WelfareController.php | ✅ | -| 2026-01-22 | Phase 1.1 | /welfare/detail 라우트 추가 | routes/api.php | ✅ | -| 2026-01-22 | Phase 1.2 | Swagger 스키마 및 엔드포인트 추가 | WelfareApi.php | ✅ | -| 2026-01-22 | Phase 2.1 | WelfareDetailApiResponse 타입 추가 | types.ts | ✅ | -| 2026-01-22 | Phase 2.2 | useWelfareDetail hook 추가 | useCEODashboard.ts | ✅ | -| 2026-01-22 | Phase 2.3 | transformWelfareDetailResponse 추가 | transformers.ts | ✅ | -| 2026-01-22 | Phase 2.4 | 모달 설정 API 연동 + fallback | CEODashboard.tsx, welfareConfigs.ts | ✅ | - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **API 규칙**: `api/CLAUDE.md` (SAM API Development Rules) -- **Swagger 가이드**: `docs/guides/swagger-guide.md` - ---- - -## 10. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 10.1 세션 시작 시 (Load Strategy) -```javascript -// 순차적 로드 -read_memory("welfare-section-state") // 1. 상태 파악 -read_memory("welfare-section-snapshot") // 2. 사고 흐름 복구 -``` - -### 10.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 **Snapshot** | `write_memory("welfare-section-snapshot", "코드변경+논의요약")` | -| **20% 이하** | 🧹 **Context Purge** | `write_memory("welfare-section-active-symbols", "주요 수정 파일/함수")` | -| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 | - -### 10.3 Serena 메모리 구조 -- `welfare-section-state`: { phase, progress, next_step, last_decision } -- `welfare-section-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `welfare-section-active-symbols`: 현재 수정 중인 파일/심볼 리스트 - ---- - -## 11. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 11.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| 모달 클릭 (fixed) | 정액 방식 계산 표시 | - | ⏳ | -| 모달 클릭 (ratio) | 비율 방식 계산 표시 | - | ⏳ | -| 분기 변경 (Q1→Q2) | 해당 분기 데이터 표시 | - | ⏳ | - -### 11.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 4개 카드 데이터 실시간 반영 | ✅ | API 연동 완료 상태 | -| 모달 상세 데이터 API 연동 | ✅ | Backend API + Frontend hook 완료 | -| Mock 데이터 제거 | ✅ | API 우선, Mock fallback 유지 | - ---- - -## 12. 자기완결성 점검 결과 - -### 12.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 모달 데이터 API 연동 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 11.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 참조 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 1 → Phase 2 순서 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 4. 핵심 참조 코드 인라인 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 6. 상세 작업 내용 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 11.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드 스니펫 포함 | - -### 12.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 3.1 단계별 절차 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 6. 상세 작업 내용 | -| Q4. DetailModalConfig 구조는? | ✅ | 4.1, 4.2 타입 정의 | -| Q5. Mock 데이터 구조는? | ✅ | 4.3 현재 Mock 데이터 | -| Q6. DB 테이블 스키마는? | ✅ | 4.4 expense_accounts | -| Q7. API → 모달 변환 방법은? | ✅ | 5. 변환 매핑 | -| Q8. 작업 완료 확인 방법은? | ✅ | 11. 검증 결과 | -| Q9. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 | - -**결과**: 9/9 통과 → ✅ 자기완결성 확보 - -### 12.3 보완 이력 - -| 날짜 | 항목 | 원본 | 보완 내용 | -|------|------|------|----------| -| 2026-01-22 | DetailModalConfig | 없음 | 타입 정의 전체 인라인 | -| 2026-01-22 | Mock 데이터 | 없음 | welfareConfigs.ts 전체 인라인 | -| 2026-01-22 | DB 스키마 | 없음 | expense_accounts 테이블 구조 | -| 2026-01-22 | 변환 매핑 | 없음 | API → 모달 매핑 테이블 및 코드 | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/work-order-plan.md b/plans/archive/work-order-plan.md deleted file mode 100644 index 56c5c1b..0000000 --- a/plans/archive/work-order-plan.md +++ /dev/null @@ -1,409 +0,0 @@ -# 작업지시 (Work Orders) API 연동 계획 - -> **작성일**: 2025-01-08 -> **목적**: 작업지시 기능 검증 및 테스트 -> **상태**: ✅ 전체 테스트 완료 (2025-01-11) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 전체 기능 테스트 완료 (2025-01-11) | -| **다음 작업** | 운영 준비 | -| **진행률** | 5/5 (100%) | -| **마지막 업데이트** | 2025-01-11 | - ---- - -## 1. 개요 - -### 1.1 기능 설명 -작업지시는 MES 시스템의 핵심 기능으로, 수주를 기반으로 실제 생산 작업을 지시하고 추적합니다. -공정 유형별(스크린/슬랫/절곡)로 작업 단계를 관리하며, 담당자 배정 및 작업 상태를 추적합니다. - -### 1.2 현재 구현 상태 분석 - -#### API (Laravel) - ✅ 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| Model | `api/app/Models/Production/WorkOrder.php` | ✅ | -| Model | `api/app/Models/Production/WorkOrderItem.php` | ✅ | -| Model | `api/app/Models/Production/WorkOrderBendingDetail.php` | ✅ | -| Model | `api/app/Models/Production/WorkOrderIssue.php` | ✅ | -| Service | `api/app/Services/WorkOrderService.php` | ✅ | -| Controller | `api/app/Http/Controllers/Api/V1/WorkOrderController.php` | ✅ | -| FormRequest | `api/app/Http/Requests/WorkOrder/*.php` | ✅ | -| Route | `/api/v1/work-orders` | ✅ | - -#### Frontend (React/Next.js) - ✅ API 연동 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| 목록 페이지 | `react/src/app/[locale]/(protected)/production/work-orders/page.tsx` | ✅ | -| 등록 페이지 | `react/src/app/[locale]/(protected)/production/work-orders/create/page.tsx` | ✅ | -| 상세 페이지 | `react/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx` | ✅ | -| 목록 컴포넌트 | `react/src/components/production/WorkOrders/WorkOrderList.tsx` | ✅ | -| 등록 컴포넌트 | `react/src/components/production/WorkOrders/WorkOrderCreate.tsx` | ✅ | -| 상세 컴포넌트 | `react/src/components/production/WorkOrders/WorkOrderDetail.tsx` | ✅ | -| 수주선택 모달 | `react/src/components/production/WorkOrders/SalesOrderSelectModal.tsx` | ✅ | -| 담당자선택 모달 | `react/src/components/production/WorkOrders/AssigneeSelectModal.tsx` | ✅ | -| 타입 정의 | `react/src/components/production/WorkOrders/types.ts` | ✅ | -| **actions.ts** | `react/src/components/production/WorkOrders/actions.ts` | ✅ | - -### 1.3 관련 URL -| 화면 | URL | 설명 | -|------|-----|------| -| 작업지시목록 | `/production/work-orders` | 상태별 필터링, 검색 | -| 작업지시등록 | `/production/work-orders/create` | 모달 - 수주선택 | -| 작업지시상세 | `/production/work-orders/{id}` | 상세 정보 | - -### 1.4 연관관계 -``` -┌─────────────────┐ ┌─────────────────┐ -│ Order │────sales_order_id──▶│ WorkOrder │ -│ (수주) │ │ (작업지시) │ -└─────────────────┘ └─────────────────┘ - │ - ┌───────────────────────────────────────┼───────────────────────────────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ WorkOrderItem │ │WorkOrderBending │ │ WorkOrderIssue │ -│ (작업품목) │ │ Detail │ │ (이슈) │ -└─────────────────┘ │ (절곡상세) │ └─────────────────┘ - └─────────────────┘ - │ - │ work_order_id - ▼ - ┌─────────────────┐ - │ WorkResult │ - │ (작업실적) │ - └─────────────────┘ -``` - ---- - -## 2. API 엔드포인트 - -### 2.1 REST API (구현 완료) -| Method | Endpoint | 설명 | 상태 | -|--------|----------|------|:----:| -| GET | `/api/v1/work-orders` | 목록 조회 (필터/페이징) | ✅ | -| GET | `/api/v1/work-orders/stats` | 통계 조회 | ✅ | -| GET | `/api/v1/work-orders/{id}` | 상세 조회 | ✅ | -| POST | `/api/v1/work-orders` | 작업지시 생성 | ✅ | -| PUT | `/api/v1/work-orders/{id}` | 작업지시 수정 | ✅ | -| DELETE | `/api/v1/work-orders/{id}` | 작업지시 삭제 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/status` | 상태 변경 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/assign` | 담당자 배정 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/bending/toggle` | 절곡 상세 토글 | ✅ | -| POST | `/api/v1/work-orders/{id}/issues` | 이슈 등록 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/issues/{issueId}/resolve` | 이슈 해결 | ✅ | - -### 2.2 actions.ts 구현 함수 (완료) -```typescript -// 목록/조회 -getWorkOrders(params) // 목록 조회 -getWorkOrderStats() // 통계 조회 -getWorkOrderById(id) // 상세 조회 - -// CRUD -createWorkOrder(data) // 생성 -updateWorkOrder(id, data) // 수정 -deleteWorkOrder(id) // 삭제 - -// 상태/배정 -updateWorkOrderStatus(id, status) // 상태 변경 -assignWorkOrder(id, data) // 담당자 배정 - -// 절곡 공정 -toggleBendingField(id, field, value) // 절곡 상세 토글 - -// 이슈 관리 -addWorkOrderIssue(id, data) // 이슈 등록 -resolveWorkOrderIssue(id, issueId) // 이슈 해결 - -// 연동 -getSalesOrdersForWorkOrder() // 수주 목록 (작업지시용) -getDepartmentsWithUsers() // 부서/사용자 목록 (담당자 배정용) -``` - ---- - -## 3. 데이터 스키마 - -### 3.1 WorkOrder (작업지시) -```typescript -interface WorkOrder { - id: string; - workOrderNo: string; // WO202512260001 - lotNo: string; // 수주번호 참조 - processType: 'screen' | 'slat' | 'bending'; - status: WorkOrderStatus; - // 기본 정보 - client: string; // 발주처 - projectName: string; // 현장명 - dueDate: string; // 납기일 - assignee: string; // 작업자 - // 날짜 - orderDate: string; // 지시일 - shipmentDate: string; // 출고예정일 - // 플래그 - isAssigned: boolean; - isStarted: boolean; - priority: number; // 1~9 - // 품목 - items: WorkOrderItem[]; - // 공정 진행 - currentStep: number; - // 절곡 전용 - bendingDetails?: BendingDetail[]; - // 이슈 - issues?: WorkOrderIssue[]; - note?: string; -} -``` - -### 3.2 WorkOrderStatus (상태) -```typescript -type WorkOrderStatus = - | 'unassigned' // 미배정 - | 'pending' // 승인대기 - | 'waiting' // 작업대기 - | 'in_progress' // 작업중 - | 'completed' // 작업완료 - | 'shipped'; // 출하완료 -``` - -### 3.3 ProcessType (공정 유형) -```typescript -type ProcessType = 'screen' | 'slat' | 'bending'; - -// 공정별 작업 단계 -const SCREEN_STEPS = ['원단절단', '미싱', '앤드락작업', '중간검사', '포장']; -const SLAT_STEPS = ['코일절단', '성형', '미미작업', '검사', '포장']; -const BENDING_STEPS = ['가이드레일 제작', '케이스 제작', '하단마감재 제작', '검사']; -``` - ---- - -## 4. 작업 범위 - -### Phase 1: 검증 및 테스트 ✅ 완료 (2025-01-11) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | 목록 조회 테스트 | ✅ | 필터링/검색/페이징 정상 | -| 1.2 | 등록 기능 테스트 | ✅ | 수주 선택 모달 동작 확인 | -| 1.3 | 상세 조회 테스트 | ✅ | 버그 수정 완료 (site_name 컬럼 수정) | -| 1.4 | 상태 변경 테스트 | ✅ | 전체 상태 전이 검증 완료 | -| 1.5 | 담당자 배정 테스트 | ✅ | 배정 시 상태 자동 전이 확인 | - -**Phase 1 테스트 상세:** -- **버그 수정**: WorkOrderService.php:119 - `project_name` → `site_name` (Order 모델에 맞춤) -- **상태 전이**: pending ⇄ waiting ⇄ in_progress ⇄ completed ⇄ shipped 모두 정상 -- **담당자 배정**: 배정 시 unassigned → pending 자동 전이 확인 - -### Phase 2: 공정별 기능 테스트 ✅ 완료 (2025-01-11) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 스크린 공정 작업지시 | ✅ | process_id=2 생성 확인 | -| 2.2 | 슬랫 공정 작업지시 | ✅ | process_id=1 생성 확인 | -| 2.3 | 공정별 필터링 | ✅ | forProcess(), forProcessName() 정상 | -| 2.4 | 작업지시 품목 관리 | ✅ | WorkOrderItem CRUD 확인 | - -**Phase 2 테스트 상세:** -- **공정 목록**: 슬랫(P-001), 스크린(P-002) 활성화 확인 -- **공정별 필터**: `forProcess(1)`, `forProcessName('슬랫')` 정상 동작 -- **품목 관리**: 작업지시별 품목 추가/조회 정상 - -### Phase 3: 이슈 및 연동 ✅ 완료 (2025-01-11) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 이슈 등록 기능 | ✅ | 이슈 생성 정상 | -| 3.2 | 이슈 해결 기능 | ✅ | 해결 상태/시간 저장 확인 | -| 3.3 | 수주 연동 확인 | ✅ | salesOrder 관계 정상 | -| 3.4 | 작업실적 연동 | ⏭️ | 후순위 (별도 기능) | - -**Phase 3 테스트 상세:** -- **이슈 관리**: 등록(open) → 해결(resolved) 전체 흐름 정상 -- **open_issues_count**: 미해결 이슈 카운트 속성 정상 -- **수주 연동**: WorkOrder.salesOrder 관계를 통한 수주 정보 조회 정상 - ---- - -## 5. 주요 기능 상세 - -### 5.1 수주 선택 (모달) -``` -작업지시 등록 - │ - ▼ "수주 선택" 버튼 -┌─────────────────────────────────┐ -│ SalesOrderSelectModal │ -│ - 수주 목록 (for_work_order=1) │ -│ - 검색 기능 │ -│ - 선택 시 정보 자동 채움 │ -└─────────────────────────────────┘ -``` - -### 5.2 상태 흐름 -``` -unassigned (미배정) - │ - ▼ 담당자 배정 -pending (승인대기) - │ - ▼ 승인 -waiting (작업대기) - │ - ▼ 작업 시작 -in_progress (작업중) - │ - ▼ 작업 완료 -completed (작업완료) - │ - ▼ 출하 -shipped (출하완료) -``` - -### 5.3 공정별 작업 단계 - -#### 스크린 공정 (screen) -1. 원단절단 (cutting) -2. 미싱 (sewing) -3. 앤드락작업 (endlock) -4. 중간검사 (inspection) -5. 포장 (packing) - -#### 슬랫 공정 (slat) -1. 코일절단 (coil_cutting) -2. 성형 (forming) -3. 미미작업 (finishing) -4. 검사 (inspection) -5. 포장 (packing) - -#### 절곡 공정 (bending) -1. 가이드레일 제작 (guide_rail) -2. 케이스 제작 (case) -3. 하단마감재 제작 (bottom_finish) -4. 검사 (inspection) - -### 5.4 절곡 상세 토글 -- 절곡 공정의 세부 항목 완료 여부 토글 -- `PATCH /api/v1/work-orders/{id}/bending/toggle` -- 필드: shaft_cutting, bearing, shaft_welding, assembly 등 - -### 5.5 이슈 관리 -- 작업 중 발생한 이슈 등록 -- 우선순위: low, medium, high -- 상태: pending → resolved - ---- - -## 6. 의존성 - -### 6.1 필수 선행 작업 -- **공정관리 (Process)**: 공정 유형 정의 - ✅ 완료 -- **사원관리**: 담당자 배정 (assignee_id) -- **부서관리**: 팀 배정 (team_id) - -### 6.2 관련 의존성 -- **수주관리 (Order)**: 수주 데이터 필요 (sales_order_id) - - ✅ Order API 연동 완료 (2025-01-09) - - 수주 → 생산지시 생성 기능 연동됨 - -### 6.3 후속 연동 -- **작업실적 (WorkResult)**: 작업 완료 후 실적 등록 -- **품질검사**: 검사 공정 연동 -- **출하관리**: 출하 처리 - ---- - -## 7. 검증 방법 - -### 7.1 테스트 체크리스트 - -| 기능 | 테스트 항목 | 예상 결과 | -|------|-----------|----------| -| 목록 조회 | 페이지 로드 | 작업지시 목록 표시 | -| 상태 필터 | "작업중" 탭 클릭 | 해당 상태만 표시 | -| 검색 | 작업지시번호 검색 | 필터링된 결과 | -| 등록 | 새 작업지시 등록 | 목록에 추가됨 | -| 상세 조회 | 행 클릭 | 상세 정보 표시 | -| 상태 변경 | 상태 버튼 클릭 | 상태 전환됨 | -| 담당자 배정 | 배정 버튼 클릭 | 담당자 변경됨 | -| 이슈 등록 | 이슈 추가 | 이슈 목록에 표시 | - -### 7.2 API 테스트 -```bash -# 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/work-orders" -H "X-Api-Key: ..." - -# 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/work-orders/1" -H "X-Api-Key: ..." - -# 통계 조회 -curl -X GET "http://api.sam.kr/api/v1/work-orders/stats" -H "X-Api-Key: ..." - -# 상태 변경 -curl -X PATCH "http://api.sam.kr/api/v1/work-orders/1/status" \ - -H "X-Api-Key: ..." \ - -H "Content-Type: application/json" \ - -d '{"status": "in_progress"}' -``` - ---- - -## 8. 참고 사항 - -### 8.1 작업지시번호 형식 -- 형식: `WO{YYYYMMDD}{NNNN}` -- 예: `WO202512260001` -- 자동 생성: `WorkOrderService::generateWorkOrderNo()` - -### 8.2 Worker Screen (작업자 화면) -- 별도 화면: `/production/worker-screen` -- 작업자가 직접 작업 진행/완료 처리 -- 이슈 보고 기능 -- `react/src/components/production/WorkerScreen/` 참고 - -### 8.3 Production Dashboard -- 생산 현황 대시보드: `/production/dashboard` -- 공정별 작업 현황 시각화 -- `react/src/components/production/ProductionDashboard/` 참고 - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - -### 참고 코드 -- **Controller**: `api/app/Http/Controllers/Api/V1/WorkOrderController.php` -- **Service**: `api/app/Services/WorkOrderService.php` -- **actions.ts**: `react/src/components/production/WorkOrders/actions.ts` - ---- - -## 10. 자기완결성 점검 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 검증 및 테스트 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 7 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3 테스트 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Order API 연동 완료 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 경로 검증됨 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 테스트 체크리스트 제공 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | curl + 체크리스트 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 경로 명시 | - ---- - -*이 문서는 독립 세션에서 바로 작업 시작 가능하도록 설계되었습니다.* \ No newline at end of file diff --git a/plans/bending-preproduction-stock-plan.md b/plans/bending-preproduction-stock-plan.md deleted file mode 100644 index 352ae35..0000000 --- a/plans/bending-preproduction-stock-plan.md +++ /dev/null @@ -1,838 +0,0 @@ -# 절곡품 선생산 → 재고 적재 흐름 통합 개발 계획 - -> **작성일**: 2026-02-21 -> **목적**: 레거시 5130 절곡품(가이드레일/셔터박스/바텀바) 관리를 SAM 기존 재고 시스템에 통합하고, 선생산→재고적재 흐름 구현 -> **기준 문서**: `api/app/Services/StockService.php`, `api/app/Services/WorkOrderService.php`, `docs/plans/bending-info-auto-generation-plan.md` -> **상태**: 🔄 Phase 3 완료 (3.5 마이그레이션 제외) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 3.5 레거시 데이터 마이그레이션 커맨드 작성 완료 | -| **다음 작업** | 마이그레이션 실행 및 검증 | -| **진행률** | 14/14 (100%) | -| **마지막 업데이트** | 2026-02-21 | - ---- - -## 0. 용어 및 비즈니스 배경 - -### 0.1 절곡품이란? -- **절곡(Bending)**: 금속판(철판, SUS, EGI)을 절곡기로 구부려 만드는 부품 -- **주요 절곡품 3종**: - - **가이드레일**: 방화셔터가 상하로 이동하는 레일 (벽면형/측면형, SUS/EGI 마감) - - **셔터박스(케이스)**: 방화셔터가 말려 들어가는 상부 박스 (양면/밑면/후면 점검구) - - **바텀바(하단마감재)**: 방화셔터 하부를 마감하는 부품 (스크린/철재) -- **연기차단재**: 가이드레일/케이스에 부착하는 연기 차단용 부자재 (W50 레일용, W80 케이스용) - -### 0.2 선생산 운영 방식 -- 절곡품은 **수주와 무관하게 미리 대량 생산**하여 재고로 비축 -- 수주 발생 시 비축된 재고에서 **투입(차감)**하여 사용 -- 이유: 절곡 공정은 셋업 시간이 길어 건별 생산보다 일괄 생산이 효율적 - -### 0.3 SAM 프로젝트 구조 -``` -SAM/ -├── api/ # Laravel 12 REST API (백엔드) -├── react/ # Next.js 15 프론트엔드 -├── mng/ # 관리자 패널 (Plain Laravel) -├── 5130/ # 레거시 시스템 소스코드 (참조용) -└── docs/ # 기술 문서 -``` - -### 0.4 SAM 핵심 아키텍처 규칙 -- **Service-First**: 비즈니스 로직은 반드시 Service 레이어 -- **Multi-tenancy**: 모든 모델에 `BelongsToTenant` trait, tenant_id 필수 -- **컬럼 추가 정책**: FK/조인키만 컬럼 추가, 나머지 속성은 `options` JSON 활용 -- **FormRequest**: Controller에서 검증 금지, FormRequest 사용 - ---- - -## 1. 개요 - -### 1.1 배경 - -레거시 5130에서 절곡품(가이드레일, 셔터박스, 바텀바)은 **수주와 무관하게 미리 생산하여 재고로 관리**하는 형태. -수주 발생 시 재고에서 투입(차감)하는 방식으로 운영됨. - -SAM에는 이미 재고 관리 시스템(`stocks` + `stock_lots` + `stock_transactions`)이 구축되어 있으나, -**생산 완료 → 재고 입고** 경로가 없어 절곡품 선생산 흐름을 지원하지 못함. - -### 1.2 레거시 5130 절곡품 관리 구조 - -``` -[5130 시스템] - -┌─────────────────────────────────────────────────────────────┐ -│ 절곡품 마스터 (3종) │ -│ ├── guiderail 테이블 (가이드레일) │ -│ │ ├── 대분류: 스크린/철재 │ -│ │ ├── 인정/비인정, 제품코드(KSS01 등) │ -│ │ ├── 치수: rail_width × rail_length │ -│ │ ├── material_summary (소요자재량 JSON) │ -│ │ └── bending_components (절곡 구성품) │ -│ ├── shutterbox 테이블 (셔터박스) │ -│ │ ├── 점검구 형태: 양면/밑면/후면 │ -│ │ └── 치수: box_width × box_height │ -│ └── bottombar 테이블 (바텀바/하단마감재) │ -│ ├── 대분류: 스크린/철재 │ -│ └── 치수: bar_width × bar_height │ -│ │ -│ 재고 관리 │ -│ ├── lot 테이블 (생산 LOT) │ -│ │ ├── 3코드 식별: prod + spec + slength │ -│ │ ├── lot_number, surang(수량), rawLot(원자재LOT) │ -│ │ └── 재고 = SUM(lot.surang) - SUM(bending_work_log.qty) │ -│ └── bending_work_log 테이블 (사용 이력) │ -│ └── quantity, reg_date, lot_no │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 1.3 SAM 현재 상태 (AS-IS) - -``` -[수주 기반 흐름만 존재] - -Order(수주) ──→ WorkOrder(생산지시) ──→ 자재투입 ──→ 완료 ──→ Shipment(출하) - │ │ │ - │ sales_order_id 필수 │ 재고차감 │ ⚠️ 재고입고 없이 - │ (비즈니스 로직상) │ (기존 OK) │ 바로 출하 - -[구매입고 흐름 (별도)] - -Receiving(입고) ──→ StockService::increaseFromReceiving() (라인 241) - │ Stock + StockLot 생성 - │ StockTransaction(IN, receiving) - └─ FIFO 순서 부여 -``` - -### 1.4 목표 흐름 (TO-BE) - -``` -[선생산 흐름 (신규)] - -선생산 작업지시 ──→ 자재투입 ──→ 생산완료 - │ sales_order_id = NULL │ - │ mode = 'manual' (프론트) │ - ▼ - ⭐ 재고 입고 (신규) - StockService::increaseFromProduction() - Stock + StockLot 생성 - StockTransaction(IN, production_output) - │ - ▼ - [완성품 재고 적재] - LOT 추적, FIFO 관리 - │ - ▼ - [수주 발생 시] - 재고 확인 → reserve() → 부족분만 생산지시 - -[기존 수주 기반 흐름 (변경 없음)] - -Order ──→ WorkOrder ──→ 완료 ──→ Shipment (기존 유지) -``` - -### 1.5 핵심 설계 결정 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 설계 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 기존 재고 시스템(stocks/stock_lots/stock_transactions) 재활용 │ -│ 2. Receiving은 구매입고 전용 유지 → 생산입고는 직접 StockService │ -│ 3. 멀티테넌시 정책: FK만 컬럼, 나머지는 options JSON │ -│ 4. items.options 체계 활용 (production_source, lot_managed 등) │ -│ 5. 절곡품 전용 페이지 불필요 → 기존 재고현황에 필터 추가 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.6 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 상수 추가, 필터 파라미터 추가, options JSON 활용 | 불필요 | -| ⚠️ 컨펌 필요 | 신규 메서드 추가, 비즈니스 로직 분기, 프론트 UI 변경 | **필수** | -| 🔴 금지 | 기존 입출고 로직 변경, stocks 테이블 구조 변경, 기존 API 스펙 변경 | 별도 협의 | - -### 1.7 준수 규칙 -- `CLAUDE.md` - Service-First, FormRequest, BelongsToTenant -- `SAM_QUICK_REFERENCE.md` - API 규칙 -- `docs/plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 참조 -- `docs/plans/bending-worklog-reimplementation-plan.md` - 프론트 절곡 컴포넌트 참조 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 재고 입고 기반 구축 (백엔드) - -| # | 작업 항목 | 상태 | 영향 파일 | -|---|----------|:----:|----------| -| 1.1 | StockTransaction REASON 상수 추가 | ✅ | `api/app/Models/Tenants/StockTransaction.php` (라인 41-57) | -| 1.2 | StockLot에 work_order_id 컬럼 추가 | ✅ | `api/database/migrations/` (신규), `api/app/Models/Tenants/StockLot.php` | -| 1.3 | StockService::increaseFromProduction() 구현 | ✅ | `api/app/Services/StockService.php` (라인 241 참조) | -| 1.4 | WorkOrderService 완료 처리 분기 로직 | ✅ | `api/app/Services/WorkOrderService.php` (라인 563-593) | - -### 2.2 Phase 2: 선생산 작업지시 흐름 (백엔드 + 프론트) - -| # | 작업 항목 | 상태 | 영향 파일 | -|---|----------|:----:|----------| -| 2.1 | 수주 없는 작업지시 API 보완 | ✅ | 이미 지원됨 (sales_order_id nullable, items 직접 전달 가능) | -| 2.2 | items.options 기반 비즈니스 로직 분기 | ✅ | Phase 1에서 shouldStockIn()으로 구현 완료 | -| 2.3 | 작업지시 생성 프론트 UI 보완 (manual 모드) | ✅ | `react/.../WorkOrderCreate.tsx` + `actions.ts` (품목 검색/추가 UI, items 파라미터) | -| 2.4 | 재고현황 item_category 필터 추가 (API) | ✅ | `api/app/Services/StockService.php`, `StockController.php` | -| 2.5 | 재고현황 절곡품 필터 추가 (프론트) | ✅ | `react/.../StockStatusList.tsx` + `actions.ts` (카테고리 필터 드롭다운) | - -### 2.3 Phase 3: 수주 연동 고도화 - -| # | 작업 항목 | 상태 | 영향 파일 | -|---|----------|:----:|----------| -| 3.1 | 수주의 절곡 BOM 품목별 재고 확인 API | ✅ | `api/app/Services/OrderService.php`, `OrderController.php`, `routes/api/v1/sales.php` | -| 3.2 | 가용 재고 자동 예약(reserve) 로직 | ✅ | 기존 `reserveForOrder()` (라인 639-642)에서 이미 처리됨 | -| 3.3 | 부족분 수동 처리 (사용자 결정) | ✅ | 프론트에서 부족 현황 표시 → 사용자가 수동으로 선생산 작업지시 생성 | -| 3.4 | 수주화면 절곡 재고 현황 표시 (프론트) | ✅ | `react/src/components/orders/actions.ts`, `orders/index.ts`, `order-management-sales/[id]/page.tsx` | -| 3.5 | 5130 레거시 데이터 마이그레이션 | ⏳ | `api/database/seeders/` 또는 마이그레이션 스크립트 (별도 진행) | - ---- - -## 3. 작업 절차 - -### 3.1 Phase 1 상세 절차 - -``` -Step 1.1: StockTransaction REASON 상수 추가 -├── 파일: api/app/Models/Tenants/StockTransaction.php -├── 위치: 라인 49 (REASON_ORDER_CANCEL 다음) -├── 추가: const REASON_PRODUCTION_OUTPUT = 'production_output'; -├── REASONS 배열에도 추가 (라인 51-57) -└── 검증: 모델 상수 선언 확인 - -Step 1.2: StockLot에 work_order_id 컬럼 추가 -├── 마이그레이션 파일 생성 -│ └── stock_lots 테이블에 work_order_id (nullable, FK → work_orders.id) 추가 -│ └── 위치: receiving_id (라인 47) 다음 -├── StockLot 모델 수정 (api/app/Models/Tenants/StockLot.php) -│ ├── fillable에 'work_order_id' 추가 (라인 15-34) -│ └── workOrder() 관계 추가: belongsTo(WorkOrder::class) -├── 멀티테넌시 정책: work_order_id는 FK이므로 컬럼 추가 정당 -└── 검증: migrate:status, 모델 관계 확인 - -Step 1.3: StockService::increaseFromProduction() 구현 -├── 파일: api/app/Services/StockService.php -├── 기존 increaseFromReceiving() (라인 241-314) 참고하여 구현 -│ ├── getOrCreateStock() 재사용 (라인 423-466) -│ ├── getNextFifoOrder() 재사용 (라인 474) -│ ├── StockLot 생성 (work_order_id 참조, receiving_id는 null) -│ ├── Stock.refreshFromLots() 호출 (Stock.php 라인 149-164) -│ ├── recordTransaction() 호출 (라인 1232) -│ └── logStockChange() 호출 (라인 1274) -├── 차이점: receiving_id 대신 work_order_id 사용, supplier 관련 필드 null -├── LOT 번호: WorkOrderService::generateLotNo() (라인 845-866) 에서 생성한 것 수신 -└── 검증: 단위 테스트 (입고 후 재고량 증가 확인) - -Step 1.4: WorkOrderService 완료 처리 분기 로직 -├── 파일: api/app/Services/WorkOrderService.php -├── 수정 위치: updateStatus() 라인 591-593 -│ 현재 코드: -│ if ($status === WorkOrder::STATUS_COMPLETED) { -│ $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); -│ } -│ 변경: -│ if ($status === WorkOrder::STATUS_COMPLETED) { -│ if ($workOrder->sales_order_id) { -│ $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); -│ } else { -│ $this->stockInFromProduction($workOrder); -│ } -│ } -├── saveItemResults() (라인 805-840)는 양쪽 모두 실행됨 (라인 563-568, 분기 전에 호출) -├── generateLotNo() (라인 845-866) 에서 LOT 번호 자동 생성 (KD-SA-YYMMDD-NN 형식) -└── 검증: 선생산 WO 완료 시 재고 증가 확인, 기존 수주 WO는 변경 없음 -``` - -### 3.2 Phase 2 상세 절차 - -``` -Step 2.1: 수주 없는 작업지시 API 보완 -├── WorkOrderService::store() 메서드 확인 -│ └── sales_order_id 없이도 items 직접 전달 가능 (기존 경로 활용) -├── work_orders.sales_order_id는 DB에서 이미 nullable -├── 프론트: WorkOrderCreate.tsx의 RegistrationMode (라인 52) -│ └── 현재: type RegistrationMode = 'linked' | 'manual' -│ └── 'manual' 선택 시 수주 연동 없이 생성 가능 -│ └── ⚠️ 주의: 'source_type' 필드는 현재 존재하지 않음 → 필요시 신규 추가 -└── 검증: Postman으로 수주 없는 작업지시 생성 테스트 - -Step 2.2: items.options 기반 비즈니스 로직 분기 -├── Item.options 참조 위치 정리 -│ ├── production_source: 'purchased' | 'self_produced' | 'both' -│ ├── lot_managed: boolean -│ └── consumption_method: 'auto' | 'manual' | 'none' -├── 생산완료 시: production_source === 'self_produced' && lot_managed → 재고 입고 -├── 자재투입 시: consumption_method에 따른 차감 방식 분기 -└── 검증: 절곡 품목의 options 값 시더 데이터 확인 - -Step 2.3: 작업지시 생성 프론트 UI 보완 -├── 파일: react/src/components/production/WorkOrders/WorkOrderCreate.tsx -├── 현재 manual 모드 UI (라인 278-305): -│ └── RadioGroup에 'linked' | 'manual' 선택지, Label: "수동 등록 (재고생산)" -├── 보완 필요: -│ ├── 품목 검색/선택 UI (items 마스터에서 BENDING 카테고리 필터) -│ ├── 수량 입력 -│ └── 공정 선택 (절곡 공정 기본 선택) -├── 생산완료 버튼 UI 변경 (선생산 WO: "재고 입고" / 수주 WO: "출하") -└── 검증: 프론트에서 선생산 작업지시 생성 → 완료 → 재고 확인 - -Step 2.4: 재고현황 item_category 필터 추가 (API) -├── 파일: api/app/Services/StockService.php -├── index() 메서드 (라인 45) 파라미터에 item_category 추가 -│ └── whereHas('item', fn($q) => $q->where('item_category', $category)) -├── StockController 파라미터 바인딩 -└── 검증: API 호출로 BENDING 카테고리 필터링 확인 - -Step 2.5: 재고현황 절곡품 필터 추가 (프론트) -├── 파일: react/src/components/material/StockStatus/StockStatusList.tsx -├── 관련 파일: -│ ├── StockStatusDetail.tsx (상세) -│ ├── stockStatusConfig.ts (설정) -│ ├── actions.ts (API 호출) -│ └── types.ts (타입 정의) -├── 카테고리 탭 또는 드롭다운 추가 -│ └── 전체 | 원자재 | 절곡품(BENDING) | 부자재 | 소모품 -├── API 호출 시 item_category 파라미터 전달 -└── 검증: 절곡품 필터 적용하여 재고 목록 확인 -``` - -### 3.3 Phase 3 상세 절차 - -``` -Step 3.1: 수주 확정 시 재고 자동 확인 -├── OrderService::confirmOrder() 또는 createProductionOrder() 수정 -│ ├── BOM에서 절곡 품목 추출 (item_category === 'BENDING') -│ ├── 각 품목의 가용 재고 조회: StockService::getAvailableStock() (라인 796) -│ └── 재고 현황 반환 (충족/부족 품목별) -├── 프론트에 재고 확인 결과 표시 -└── 검증: 수주 확정 시 재고 현황 표시 확인 - -Step 3.2: 가용 재고 자동 예약 -├── 기존 메서드 활용: -│ ├── StockService::reserve() (라인 832) -│ └── StockService::releaseReservation() (라인 948) -├── 예약 시점: 수주 확정 시 자동 예약 (사용자 확인 후) -├── 예약 해제: 수주 취소 시 releaseReservation() -└── 검증: 예약 후 available_qty 감소 확인 - -Step 3.3: 부족분 자동 생산지시 -├── 수주 확정 시 재고 부족 품목에 대해 자동 생산지시 생성 -│ └── createProductionOrder()에 부족 수량만 반영 -├── 또는 수동: 부족 품목 목록을 사용자에게 표시 → 선생산 지시 유도 -└── 검증: 재고 10개, 필요 15개 → 5개만 생산지시 확인 - -Step 3.4: 수주화면 재고 현황 표시 -├── 수주 상세/편집 화면에 절곡 품목별 재고 현황 표시 -│ └── 품목명 | 필요수량 | 가용재고 | 부족수량 -└── 검증: UI 렌더링 확인 - -Step 3.5: 5130 레거시 데이터 마이그레이션 -├── lot 테이블 → stocks + stock_lots 매핑 -│ ├── prod+spec+slength → items.code (BD-* 패턴) 매핑 -│ ├── surang → stock_lots.qty -│ └── rawLot → stock_lots.options (원자재 LOT 추적) -├── bending_work_log → stock_transactions 매핑 -│ └── quantity → stock_transactions (TYPE_OUT) -├── guiderail/shutterbox/bottombar → items 테이블 매핑 -│ └── item_category = 'BENDING', item_type = 'PT' -└── 검증: 마이그레이션 전후 재고량 일치 확인 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 현재 DB 스키마 (수정 대상) - -#### stocks 테이블 (`2025_12_26_132806_create_stocks_table.php`) -``` -id, tenant_id, item_id, item_code, item_name, item_type, -specification, unit, stock_qty, safety_stock, -reserved_qty, available_qty, lot_count, oldest_lot_date, -location, status, last_receipt_date, last_issue_date, -created_by, updated_by, timestamps, softDeletes, deleted_by -``` - -#### stock_lots 테이블 (`2025_12_26_132842_create_stock_lots_table.php`) -``` -id, tenant_id, stock_id(FK→stocks), lot_no, fifo_order(default:1), -receipt_date, qty(decimal 15,3), reserved_qty, available_qty, -unit(default:'EA'), supplier, supplier_lot, po_number, -location, status(default:'available'), receiving_id(nullable), -created_by, updated_by, timestamps, softDeletes, deleted_by - -인덱스: tenant_id, stock_id, lot_no, status, (stock_id+fifo_order) 복합 -유니크: (tenant_id, stock_id, lot_no) -``` - -#### stock_transactions 테이블 (`2026_01_29_000001_create_stock_transactions_table.php`) -``` -id, tenant_id, stock_id, stock_lot_id, type(IN/OUT/RESERVE/RELEASE), -qty, balance_qty, reference_type, reference_id, lot_no, -reason, remark, item_code, item_name, created_by, timestamps -``` - -### 4.2 현재 코드 레퍼런스 (라인번호 포함) - -#### StockTransaction 상수 (`api/app/Models/Tenants/StockTransaction.php`) -```php -// 라인 25-31: TYPE 상수 -const TYPE_IN = 'IN'; // 라인 25 -const TYPE_OUT = 'OUT'; // 라인 27 -const TYPE_RESERVE = 'RESERVE'; // 라인 29 -const TYPE_RELEASE = 'RELEASE'; // 라인 31 - -// 라인 41-57: REASON 상수 -const REASON_RECEIVING = 'receiving'; // 라인 41 -const REASON_WORK_ORDER_INPUT = 'work_order_input'; // 라인 43 -const REASON_SHIPMENT = 'shipment'; // 라인 45 -const REASON_ORDER_CONFIRM = 'order_confirm'; // 라인 47 -const REASON_ORDER_CANCEL = 'order_cancel'; // 라인 49 -const REASONS = [ ... ]; // 라인 51-57 -``` - -#### StockService 주요 메서드 (`api/app/Services/StockService.php`) -``` -라인 45: index(array $params): LengthAwarePaginator -라인 109: stats(): array -라인 159: show(int $id): Item -라인 176: findByItemCode(string $itemCode): ?Item -라인 192: statsByItemType(): array -라인 241: increaseFromReceiving(Receiving $receiving): StockLot ← 참조 대상 -라인 325: adjustFromReceiving(Receiving $receiving, float $newQty): void -라인 423: getOrCreateStock(int $itemId, ?Receiving $receiving = null): Stock ← 재사용 -라인 474: getNextFifoOrder(int $stockId): int ← 재사용 -라인 493: decreaseFIFO(int $itemId, float $qty, string $reason, int $referenceId): array -라인 618: decreaseFromLot(int $stockLotId, float $qty, string $reason, int $referenceId): array -라인 710: increaseToLot(int $stockLotId, float $qty, string $reason, int $referenceId): array -라인 796: getAvailableStock(int $itemId): ?array -라인 832: reserve(int $itemId, float $qty, int $orderId): void -라인 948: releaseReservation(int $itemId, float $qty, int $orderId): void -라인 1050: reserveForOrder($orderItems, int $orderId): void -라인 1071: releaseReservationForOrder($orderItems, int $orderId): void -라인 1099: decreaseForShipment(int $itemId, float $qty, int $shipmentId, ?int $stockLotId = null): array -라인 1232: [private] recordTransaction(...) -라인 1274: [private] logStockChange(...) -``` - -#### WorkOrderService 완료 처리 (`api/app/Services/WorkOrderService.php`) -```php -// 라인 563-568: completed 케이스 (saveItemResults 호출) -case WorkOrder::STATUS_COMPLETED: - $workOrder->started_at = $workOrder->started_at ?? now(); - $workOrder->completed_at = now(); - $this->saveItemResults($workOrder, $resultData, $userId); - break; - -// 라인 591-593: 완료 후 출하 자동 생성 (← 여기에 분기 삽입) -if ($status === WorkOrder::STATUS_COMPLETED) { - $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); -} - -// 라인 606: 출하 생성 메서드 -private function createShipmentFromWorkOrder(WorkOrder $workOrder, int $tenantId, int $userId): ?Shipment - -// 라인 805: 결과 데이터 저장 (LOT 번호 생성 포함) -private function saveItemResults(WorkOrder $workOrder, ?array $resultData, int $userId): void - -// 라인 845-866: LOT 번호 생성 -private function generateLotNo(WorkOrder $workOrder): string -// 패턴: KD-SA-YYMMDD-NN (예: KD-SA-260221-01) -``` - -#### Stock 모델 refreshFromLots (`api/app/Models/Tenants/Stock.php`) -```php -// 라인 149-164 -public function refreshFromLots(): void -{ - $lots = $this->lots()->where('status', '!=', 'used')->get(); - $this->lot_count = $lots->count(); - $this->stock_qty = $lots->sum('qty'); - $this->reserved_qty = $lots->sum('reserved_qty'); - $this->available_qty = $lots->sum('available_qty'); - $oldestLot = $lots->sortBy('receipt_date')->first(); - $this->oldest_lot_date = $oldestLot?->receipt_date; - $this->last_receipt_date = $lots->max('receipt_date'); - $this->status = $this->calculateStatus(); - $this->save(); -} -``` - -### 4.3 increaseFromReceiving() 실제 코드 (참조용) - -신규 `increaseFromProduction()` 구현 시 아래 코드를 기반으로 작성: - -```php -// api/app/Services/StockService.php 라인 241-314 -public function increaseFromReceiving(Receiving $receiving): StockLot -{ - if (! $receiving->item_id) { - throw new \Exception(__('error.stock.item_id_required')); - } - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - return DB::transaction(function () use ($receiving, $tenantId, $userId) { - $stock = $this->getOrCreateStock($receiving->item_id, $receiving); - $fifoOrder = $this->getNextFifoOrder($stock->id); - - $stockLot = new StockLot; - $stockLot->tenant_id = $tenantId; - $stockLot->stock_id = $stock->id; - $stockLot->lot_no = $receiving->lot_no; - $stockLot->fifo_order = $fifoOrder; - $stockLot->receipt_date = $receiving->receiving_date; - $stockLot->qty = $receiving->receiving_qty; - $stockLot->reserved_qty = 0; - $stockLot->available_qty = $receiving->receiving_qty; - $stockLot->unit = $receiving->order_unit ?? 'EA'; - $stockLot->supplier = $receiving->supplier; // ← 생산입고: null - $stockLot->supplier_lot = $receiving->supplier_lot; // ← 생산입고: null - $stockLot->po_number = $receiving->order_no; // ← 생산입고: null - $stockLot->location = $receiving->receiving_location; - $stockLot->status = 'available'; - $stockLot->receiving_id = $receiving->id; // ← 생산입고: null, work_order_id 대신 사용 - $stockLot->created_by = $userId; - $stockLot->updated_by = $userId; - $stockLot->save(); - - $stock->refreshFromLots(); - - $this->recordTransaction( - stock: $stock, - type: StockTransaction::TYPE_IN, - qty: $receiving->receiving_qty, - reason: StockTransaction::REASON_RECEIVING, // ← 생산입고: REASON_PRODUCTION_OUTPUT - referenceType: 'receiving', // ← 생산입고: 'work_order' - referenceId: $receiving->id, // ← 생산입고: $workOrder->id - lotNo: $receiving->lot_no, - stockLotId: $stockLot->id - ); - - $this->logStockChange(...); - return $stockLot; - }); -} -``` - -### 4.4 increaseFromProduction() 구현 설계 - -```php -/** - * 생산 완료 시 완성품 재고 입고 - * increaseFromReceiving()을 기반으로 구현 - * - * @param WorkOrder $workOrder 선생산 작업지시 - * @param WorkOrderItem $woItem 작업지시 품목 - * @param float $goodQty 양품 수량 (saveItemResults에서 기록) - * @param string $lotNo LOT 번호 (generateLotNo에서 생성) - */ -public function increaseFromProduction( - WorkOrder $workOrder, - WorkOrderItem $woItem, - float $goodQty, - string $lotNo -): StockLot { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - return DB::transaction(function () use ($workOrder, $woItem, $goodQty, $lotNo, $tenantId, $userId) { - // 1. Stock 조회 또는 생성 - // getOrCreateStock()의 두 번째 파라미터(Receiving)는 null - // → specification, unit은 Item에서 가져옴 - $stock = $this->getOrCreateStock($woItem->item_id); - - // 2. FIFO 순서 - $fifoOrder = $this->getNextFifoOrder($stock->id); - - // 3. StockLot 생성 - $stockLot = new StockLot; - $stockLot->tenant_id = $tenantId; - $stockLot->stock_id = $stock->id; - $stockLot->lot_no = $lotNo; - $stockLot->fifo_order = $fifoOrder; - $stockLot->receipt_date = now()->toDateString(); - $stockLot->qty = $goodQty; - $stockLot->reserved_qty = 0; - $stockLot->available_qty = $goodQty; - $stockLot->unit = $woItem->unit ?? 'EA'; - $stockLot->supplier = null; // 구매입고 전용 필드 - $stockLot->supplier_lot = null; - $stockLot->po_number = null; - $stockLot->location = null; - $stockLot->status = 'available'; - $stockLot->receiving_id = null; // 구매입고가 아님 - $stockLot->work_order_id = $workOrder->id; // ★ 생산입고 참조 - $stockLot->created_by = $userId; - $stockLot->updated_by = $userId; - $stockLot->save(); - - // 4. Stock 합계 갱신 - $stock->refreshFromLots(); - - // 5. 거래 이력 기록 - $this->recordTransaction( - stock: $stock, - type: StockTransaction::TYPE_IN, - qty: $goodQty, - reason: StockTransaction::REASON_PRODUCTION_OUTPUT, - referenceType: 'work_order', - referenceId: $workOrder->id, - lotNo: $lotNo, - stockLotId: $stockLot->id - ); - - // 6. 감사 로그 - $this->logStockChange( - stock: $stock, - action: 'production_in', - details: [ - 'work_order_id' => $workOrder->id, - 'work_order_item_id' => $woItem->id, - 'qty' => $goodQty, - 'lot_no' => $lotNo, - ] - ); - - return $stockLot; - }); -} -``` - -### 4.5 WorkOrderService 완료 분기 구현 설계 - -```php -// 라인 591-593 변경: updateStatus() 내부 -if ($status === WorkOrder::STATUS_COMPLETED) { - if ($workOrder->sales_order_id) { - // 기존 로직: 수주 연동 → 출하 자동 생성 - $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); - } else { - // 신규 로직: 선생산 → 재고 입고 - $this->stockInFromProduction($workOrder); - } -} - -// 신규 private 메서드 -private function stockInFromProduction(WorkOrder $workOrder): void -{ - foreach ($workOrder->items as $woItem) { - if ($this->shouldStockIn($woItem)) { - $resultData = $woItem->options['result'] ?? []; - $goodQty = $resultData['good_qty'] ?? $woItem->quantity; - $lotNo = $resultData['lot_no'] ?? ''; - - if ($goodQty > 0 && $lotNo) { - $this->stockService->increaseFromProduction( - $workOrder, $woItem, $goodQty, $lotNo - ); - } - } - } -} - -private function shouldStockIn(WorkOrderItem $woItem): bool -{ - $item = $woItem->item; - $options = $item->options ?? []; - - return ($options['production_source'] ?? null) === 'self_produced' - && ($options['lot_managed'] ?? false) === true; -} -``` - -### 4.6 데이터 매핑 (5130 → SAM) - -#### 절곡품 마스터 매핑 - -| 5130 | SAM | 비고 | -|------|-----|------| -| guiderail.model_name | items.code (BD-가이드레일-*) | item_category=BENDING | -| guiderail.rail_width × rail_length | items.options.dimensions | JSON | -| guiderail.material_summary | items.options.material_summary | JSON | -| guiderail.finishing_type | items.options.finishing_type | JSON | -| shutterbox.box_width × box_height | items.code (BD-케이스-*) | 치수 코드화 | -| bottombar.bar_width × bar_height | items.code (BD-하단마감재-*) | 치수 코드화 | - -#### 재고 매핑 - -| 5130 | SAM | 비고 | -|------|-----|------| -| lot.lot_number | stock_lots.lot_no | 1:1 | -| lot.surang | stock_lots.qty | 생산 수량 | -| lot.prod+spec+slength | items.code → stocks.item_id | 3코드→품목코드 변환 | -| lot.rawLot | stock_lots.options.raw_lot | JSON | -| lot.fabric_lot | stock_lots.options.fabric_lot | JSON | -| bending_work_log.quantity | stock_transactions.qty (TYPE_OUT) | 사용 이력 | - -#### 3코드 → 품목코드 변환 규칙 - -| prod | spec | slength | SAM item_code | -|------|------|---------|---------------| -| R(벽면형) | S(SUS) | 53(W50x3000) | BD-가이드레일-벽면형-SUS-W50x3000 | -| R(벽면형) | E(EGI) | 84(W80x4000) | BD-가이드레일-벽면형-EGI-W80x4000 | -| C(케이스) | M(본체) | 30(3000) | BD-케이스-본체-3000 | -| B(하단마감재스크린) | A(스크린용) | 30(3000) | BD-하단마감재-스크린-3000 | - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| C1 | StockLot에 work_order_id 컬럼 추가 | DB 마이그레이션 | stock_lots 테이블 | ⚠️ 컨펌 필요 | -| C2 | WorkOrderService 완료 로직 분기 | 비즈니스 로직 변경 | 생산 완료 프로세스 | ⚠️ 컨펌 필요 | -| C3 | Phase 3 수주→재고 자동 매칭 설계 | 신규 비즈니스 프로세스 | OrderService | ⚠️ Phase 3 착수 전 별도 협의 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-21 | - | 문서 초안 작성 | - | - | -| 2026-02-21 | 보완 | 용어설명, 파일경로 수정, 코드 레퍼런스 추가, DB 스키마 추가 | - | - | -| 2026-02-21 | Phase 1 구현 | 1.1~1.4 전체 완료 | StockTransaction, StockLot, StockService, WorkOrderService | ✅ | - ---- - -## 7. 참고 문서 - -### 직접 관련 문서 -- `docs/plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 자동 생성 계획 -- `docs/plans/bending-worklog-reimplementation-plan.md` - 절곡 작업일지 프론트 재구현 (완료) -- `docs/projects/legacy-5130/04_PRODUCTION.md` - 레거시 생산 시스템 분석 - -### 핵심 코드 파일 (⚠️ 경로 주의: Models는 Tenants 네임스페이스) - -**백엔드 서비스**: -- `api/app/Services/StockService.php` - 재고 서비스 (increaseFromReceiving 라인 241) -- `api/app/Services/WorkOrderService.php` - 작업지시 서비스 (updateStatus 라인 521, saveItemResults 라인 805) -- `api/app/Services/OrderService.php` - 수주 서비스 (createProductionOrder) -- `api/app/Services/Production/BendingInfoBuilder.php` - 절곡 정보 자동 생성 - -**백엔드 모델** (⚠️ `Models/Tenants/` 경로): -- `api/app/Models/Tenants/Stock.php` - 재고 모델 (refreshFromLots 라인 149) -- `api/app/Models/Tenants/StockLot.php` - 재고 LOT 모델 (fillable 라인 15-34) -- `api/app/Models/Tenants/StockTransaction.php` - 재고 거래 이력 모델 (상수 라인 25-57) - -**DB 마이그레이션**: -- `api/database/migrations/2025_12_26_132806_create_stocks_table.php` -- `api/database/migrations/2025_12_26_132842_create_stock_lots_table.php` -- `api/database/migrations/2026_01_29_000001_create_stock_transactions_table.php` - -### 프론트 코드 파일 -- `react/src/components/production/WorkOrders/WorkOrderCreate.tsx` - 작업지시 생성 (RegistrationMode 라인 52, manual UI 라인 278-305) -- `react/src/components/material/StockStatus/StockStatusList.tsx` - 재고 현황 목록 -- `react/src/components/material/StockStatus/` - 재고 현황 전체 디렉토리 (Detail, Audit, actions, types, config, mockData) -- `react/src/components/production/WorkOrders/documents/bending/` - 절곡 작업일지 컴포넌트 - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("bending-preproduction-state") // 1. 상태 파악 -read_memory("bending-preproduction-snapshot") // 2. 사고 흐름 복구 -read_memory("bending-preproduction-active-symbols") // 3. 작업 대상 파악 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | Snapshot | `write_memory("bending-preproduction-snapshot", "코드변경+논의요약")` | -| **20% 이하** | Context Purge | `write_memory("bending-preproduction-active-symbols", "수정 파일/함수")` | -| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 | - -### 8.3 Serena 메모리 구조 -- `bending-preproduction-state`: { phase, progress, next_step, last_decision } -- `bending-preproduction-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `bending-preproduction-rules`: 불변 규칙 (Receiving 우회, options JSON 정책 등) -- `bending-preproduction-active-symbols`: 현재 수정 중인 파일/심볼 리스트 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 Phase 1 테스트 케이스 - -| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|------|----------|----------|------| -| T1.1 | 선생산 WO 완료 시 재고 입고 | WO(sales_order_id=null) 완료 | Stock/StockLot 생성, qty 증가 | | ⏳ | -| T1.2 | 기존 수주 WO 완료 시 변경 없음 | WO(sales_order_id=43) 완료 | 기존대로 Shipment 생성 | | ⏳ | -| T1.3 | LOT 번호 자동 생성 | 선생산 WO 완료 | KD-SA-YYMMDD-NN 형식 LOT | | ⏳ | -| T1.4 | StockTransaction 기록 | 생산 입고 | TYPE_IN, reason=production_output | | ⏳ | - -### 9.2 Phase 2 테스트 케이스 - -| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|------|----------|----------|------| -| T2.1 | 수주 없이 작업지시 생성 | manual 모드 + 절곡 품목 | WO 생성, sales_order_id=null | | ⏳ | -| T2.2 | 재고현황 절곡품 필터 | item_category=BENDING | 절곡품만 표시 | | ⏳ | -| T2.3 | FIFO 출고 | 재고 투입 | 가장 오래된 LOT부터 차감 | | ⏳ | - -### 9.3 Phase 3 테스트 케이스 - -| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|------|----------|----------|------| -| T3.1 | 수주 확정 시 재고 확인 | 재고 10, 필요 15 | 부족 5 표시 | | ⏳ | -| T3.2 | 가용 재고 자동 예약 | 재고 10, 필요 5 | reserved_qty=5, available_qty=5 | | ⏳ | -| T3.3 | 부족분 생산지시 | 재고 10, 필요 15 | 5개 생산지시 자동 생성 | | ⏳ | - -### 9.4 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 선생산 WO → 재고 입고 정상 동작 | ⏳ | Phase 1 핵심 | -| 기존 수주 WO 흐름 변경 없음 | ⏳ | 회귀 테스트 | -| 절곡품 재고현황 필터링 가능 | ⏳ | Phase 2 | -| 수주 시 재고 자동 매칭 | ⏳ | Phase 3 | -| 5130 데이터 마이그레이션 완료 | ⏳ | Phase 3 | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 0.2 선생산 운영 방식 + 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.4 성공 기준 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1~3, 14개 작업 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 bending 계획 문서 참조 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 검증 완료 (Models/Tenants/, material/StockStatus/) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 라인번호 + 실제 코드 바디 포함 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9 테스트 케이스 참조 | -| 8 | 모호한 표현이 없는가? | ✅ | 코드 수준 상세 기술 + 용어 설명 포함 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 절곡품이 뭔가? 왜 선생산하는가? | ✅ | 0.1, 0.2 용어 및 비즈니스 배경 | -| Q2. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q3. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 1 + 3.1 절차 | -| Q4. 어떤 파일을 수정해야 하는가? | ✅ | 2.1~2.3 영향 파일 (정확한 경로) | -| Q5. 기존 코드 구조가 어떻게 되어 있는가? | ✅ | 4.1~4.3 DB 스키마 + 코드 레퍼런스 | -| Q6. 신규 메서드를 어떻게 구현해야 하는가? | ✅ | 4.4~4.5 구현 설계 (전체 코드) | -| Q7. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q8. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/clodeCheck/attendance-management_2026-01-14_23-30-00.md b/plans/clodeCheck/attendance-management_2026-01-14_23-30-00.md deleted file mode 100644 index 11e028a..0000000 --- a/plans/clodeCheck/attendance-management_2026-01-14_23-30-00.md +++ /dev/null @@ -1,206 +0,0 @@ -# E2E Test Report: 근태관리 테스트 - -**Test ID**: attendance-management -**Executed**: 2026-01-14 23:30:00 -**Duration**: ~15분 -**Status**: ❌ FAIL (3 bugs found) - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 13 | -| Passed | 10 | -| Failed | 3 | -| Pass Rate | 76.9% | - ---- - -## 필수 검증 결과 - -| # | 검증 항목 | 결과 | 비고 | -|---|----------|------|------| -| 1 | 파일 다운로드 | ❌ FAIL | Network API 호출 없음 | -| 2 | 등록/저장 버튼 | ❌ FAIL | 사유 등록 시 404 에러 | -| 3 | 검색/필터 | ✅ PASS | 데이터 필터링 정상 | -| 4 | 모달 등록 완료 | ❌ FAIL | 근태 등록: 서버 에러, 사유 등록: 404 에러 | - ---- - -## Step Results - -| Step | Name | Status | Notes | -|------|------|--------|-------| -| 1 | 인사관리 메뉴 진입 | ✅ PASS | /hr/attendance-management 이동 완료 | -| 2 | 근태 현황 대시보드 확인 | ✅ PASS | 미출근, 정시출근, 지각, 휴가 카드 표시 | -| 3 | 기간 필터 확인 | ✅ PASS | 당해년도~오늘 버튼, 날짜 입력 필드 확인 | -| 4 | 탭 필터 확인 | ✅ PASS | 전체, 미출근, 정시출근 등 9개 탭 확인 | -| 5 | 근태 테이블 구조 확인 | ✅ PASS | 12개 컬럼 구조 확인 | -| 6 | 근태 등록 모달 열기 | ✅ PASS | 모달 열림, 필드 확인 | -| 7 | 근태 등록 실제 저장 (필수 #4) | ❌ FAIL | "Create failed: 서버 에러" | -| 8 | 근태 등록 모달 닫기 | ✅ PASS | 모달 자동 닫힘 | -| 9 | 사유 등록 모달 열기 | ✅ PASS | 모달 열림, 대상/기준일/유형 필드 확인 | -| 10 | 사유 등록 실제 등록 (필수 #4) | ❌ FAIL | 404 페이지 이동 | -| 11 | 검색 기능 확인 (필수 #3) | ✅ PASS | "홍킬동" 검색 → 6건 필터링 | -| 12 | 엑셀 다운로드 (필수 #1) | ❌ FAIL | Console LOG만 출력, API 호출 없음 | -| 13 | 사유 유형 옵션 확인 | ✅ PASS | 4개 옵션 확인 | - ---- - -## 🐛 Bug Report #1: 엑셀 다운로드 미구현 - -**Report ID**: ATT-BUG-001 -**Priority**: High -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\hr\attendance-management\page.tsx` - -### Issue Summary -엑셀 다운로드 버튼 클릭 시 Console LOG만 출력되고 실제 파일 다운로드가 이루어지지 않음 - -### Steps to Reproduce -1. 근태관리 페이지 접속 -2. "엑셀 다운로드" 버튼 클릭 - -### Expected Result -- 근태 데이터가 엑셀 파일로 다운로드됨 -- Network에 `/api/export/excel` 또는 유사 API 호출 발생 - -### Actual Result -- Console: `[LOG] Excel download`만 출력 -- Network: 다운로드 관련 API 호출 없음 -- 파일 다운로드: 발생하지 않음 - -### Error Details -``` -Console Output: [LOG] Excel download -Network Requests: 다운로드 API 호출 없음 -``` - -### Suggested Fix (Reference Only) -엑셀 다운로드 핸들러에 실제 API 호출 로직 구현 필요 - -**영향 범위**: react / api -**변경 승인 정책**: ⚠️ 컨펌 필요 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` - ---- - -## 🐛 Bug Report #2: 사유 등록 404 에러 - -**Report ID**: ATT-BUG-002 -**Priority**: Critical -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\hr\attendance-management\page.tsx` - -### Issue Summary -사유 등록 모달에서 "등록" 버튼 클릭 시 존재하지 않는 페이지로 이동하여 404 에러 발생 - -### Steps to Reproduce -1. 근태관리 페이지 접속 -2. "사유 등록" 버튼 클릭 -3. 대상 선택 (예: 홍킬동) -4. 유형 선택 (예: 출장신청서) -5. "등록" 버튼 클릭 - -### Expected Result -- 사유가 정상적으로 등록됨 -- 성공 토스트 메시지 표시 -- 근태관리 페이지에 유지 - -### Actual Result -- `/hr/documents/new?type=businessTripRequest` 페이지로 이동 -- "페이지를 찾을 수 없습니다" 에러 페이지 표시 -- Console: `📌 경로 존재 여부: false` - -### Error Details -``` -URL Change: /hr/attendance-management → /hr/documents/new?type=businessTripRequest -Error Message: "요청하신 페이지가 존재하지 않거나 접근 권한이 없습니다." -Console Log: 📌 경로 존재 여부: false -``` - -### Suggested Fix (Reference Only) -1. `/hr/documents/new` 페이지 구현 필요 -2. 또는 사유 등록 로직을 API 호출 방식으로 변경 - -**영향 범위**: react / api / 라우팅 -**변경 승인 정책**: ⚠️ 컨펌 필요 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- 시스템 아키텍처: `C:\Users\codeb\docs\architecture\system-overview.md` - ---- - -## 🐛 Bug Report #3: 근태 등록 서버 에러 - -**Report ID**: ATT-BUG-003 -**Priority**: High -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\hr\attendance-management\page.tsx` - -### Issue Summary -근태 등록 모달에서 "저장" 버튼 클릭 시 서버 에러 발생 - -### Steps to Reproduce -1. 근태관리 페이지 접속 -2. "근태 등록" 버튼 클릭 -3. 대상 선택 (예: 홍킬동) -4. 기준일, 출근/퇴근 시간 확인 -5. "저장" 버튼 클릭 - -### Expected Result -- 근태가 정상적으로 등록됨 -- 성공 토스트 메시지 표시 -- 테이블에 새 데이터 표시 - -### Actual Result -- Console: `[ERROR] Create failed: 서버 에러` -- 모달은 닫히지만 데이터 저장 실패 - -### Error Details -``` -Console Error: [ERROR] Create failed: 서버 에러 -Source: page-0ad2723b9ad2d990.js:0 -``` - -### Suggested Fix (Reference Only) -백엔드 근태 등록 API 엔드포인트 확인 및 에러 원인 분석 필요 - -**영향 범위**: react / api / database -**변경 승인 정책**: ⚠️ 컨펌 필요 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` -- DB 스키마: `C:\Users\codeb\docs\specs\database-schema.md` - ---- - -## Test Environment - -- **URL**: https://dev.codebridge-x.com -- **Test Account**: TestUser5 -- **Browser**: Playwright (Chromium) -- **Date**: 2026-01-14 - ---- - -## Conclusion - -근태관리 페이지의 UI 요소와 기본 기능(대시보드, 필터, 검색)은 정상 동작하지만, **핵심 CRUD 기능에서 3건의 버그가 발견**되었습니다: - -1. **엑셀 다운로드**: 미구현 (Console LOG만 존재) -2. **사유 등록**: 404 에러 (페이지 미존재) -3. **근태 등록**: 서버 에러 (API 문제) - -이 버그들은 실제 업무 사용에 영향을 주므로 우선 수정이 필요합니다. - ---- - -*Generated by E2E Test Framework - 2026-01-14* diff --git a/plans/clodeCheck/bank-transactions_2026-01-15_test-report.md b/plans/clodeCheck/bank-transactions_2026-01-15_test-report.md deleted file mode 100644 index 4c3d7e7..0000000 --- a/plans/clodeCheck/bank-transactions_2026-01-15_test-report.md +++ /dev/null @@ -1,231 +0,0 @@ -# E2E Test Report: 은행거래 (Bank Transactions) - -**Test ID**: bank-transactions -**Executed**: 2026-01-15 -**Status**: ⚠️ PARTIAL (8/10 - 1 Critical Bug) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 10 | -| Passed | 8 | -| Failed | 1 | -| Warning | 1 | -| Pass Rate | 80% | - ---- - -## Step Results - -| Step | Test Case | Status | Notes | -|------|-----------|--------|-------| -| 1 | 은행거래 메뉴 진입 | ✅ PASS | /accounting/bank-transactions 접속 확인 | -| 2 | 목록 페이지 구조 검증 | ✅ PASS | 통계 카드 4개, 테이블 컬럼 12개 확인 | -| 3 | 당해년도 버튼 테스트 | ✅ PASS | 2026-01-01 ~ 2026-12-31 변경 확인 | -| 4 | 전전월 버튼 테스트 | ✅ PASS | 2025-11-01 ~ 2025-11-30 변경 확인 | -| 5 | 전월 버튼 테스트 | ✅ PASS | 2025-12-01 ~ 2025-12-31 변경 확인 | -| 6 | 당월 버튼 테스트 | ✅ PASS | 2026-01-01 ~ 2026-01-31 변경 확인 | -| 7 | 어제 버튼 테스트 | ✅ PASS | 2026-01-14 ~ 2026-01-14 변경 확인 | -| 8 | 오늘 버튼 테스트 | ✅ PASS | 2026-01-15 ~ 2026-01-15 변경 확인 | -| 9 | 직접 날짜 입력 테스트 | ✅ PASS | 수동 입력 후 데이터 반영 확인 | -| 10 | 테이블 데이터 표시 | ❌ FAIL | **통계 카드에만 데이터 표시, 테이블은 빈 상태** | - ---- - -## Detailed Test Results - -### 1. 은행거래 메뉴 진입 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/bank-transactions | /accounting/bank-transactions | ✅ | -| 페이지 타이틀 | 입출금 계좌조회 | 입출금 계좌조회 | ✅ | -| 인증 상태 | 로그인됨 | 로그인됨 | ✅ | - ---- - -### 2. 목록 페이지 구조 검증 - -#### 통계 카드 (4개) - -| 카드명 | 값 (2025-12) | 결과 | -|--------|-------------|------| -| 입금 | 47,232,008원 | ✅ | -| 출금 | 178,098,104원 | ✅ | -| 입금 유형 미설정 | 3건 | ✅ | -| 출금 유형 미설정 | 4건 | ✅ | - -#### 필터 드롭다운 (3개) - -| # | 필터명 | 옵션 | -|---|--------|------| -| 1 | 계좌 선택 | 전체, KB국민은행\|운영계좌, NH농협은행\|비상금, 신한은행\|급여계좌, 우리은행\|예비계좌, 하나은행\|법인카드 | -| 2 | 구분 | 전체 (입금/출금 구분 추정) | -| 3 | 정렬 | 최신순 | - -#### 테이블 컬럼 (12개) - -| # | 컬럼명 | 결과 | -|---|--------|------| -| 1 | 체크박스 | ✅ | -| 2 | 은행명 | ✅ | -| 3 | 계좌명 | ✅ | -| 4 | 거래일시 | ✅ | -| 5 | 구분 | ✅ | -| 6 | 적요 | ✅ | -| 7 | 거래처 | ✅ | -| 8 | 입금자/수취인 | ✅ | -| 9 | 입금 | ✅ | -| 10 | 출금 | ✅ | -| 11 | 잔액 | ✅ | -| 12 | 입출금 유형 | ✅ | - ---- - -### 3-8. 기간 버튼 클릭 테스트 (6개) - -| 버튼 | 예상 시작일 | 예상 종료일 | 실제 시작일 | 실제 종료일 | 결과 | -|------|-----------|-----------|-----------|-----------|------| -| 당해년도 | 2026-01-01 | 2026-12-31 | 2026-01-01 | 2026-12-31 | ✅ | -| 전전월 | 2025-11-01 | 2025-11-30 | 2025-11-01 | 2025-11-30 | ✅ | -| 전월 | 2025-12-01 | 2025-12-31 | 2025-12-01 | 2025-12-31 | ✅ | -| 당월 | 2026-01-01 | 2026-01-31 | 2026-01-01 | 2026-01-31 | ✅ | -| 어제 | 2026-01-14 | 2026-01-14 | 2026-01-14 | 2026-01-14 | ✅ | -| 오늘 | 2026-01-15 | 2026-01-15 | 2026-01-15 | 2026-01-15 | ✅ | - -**참고**: 모든 기간 버튼이 정확한 날짜 범위로 변경됨 - -#### 기간별 통계 데이터 - -| 기간 | 입금 | 출금 | 입금 유형 미설정 | 출금 유형 미설정 | -|------|------|------|----------------|----------------| -| 당해년도 (2026) | 0원 | 0원 | 0건 | 0건 | -| 전전월 (2025-11) | 68,956,798원 | 12,123,251원 | 4건 | 4건 | -| 전월 (2025-12) | 47,232,008원 | 178,098,104원 | 3건 | 4건 | -| 당월 (2026-01) | 0원 | 0원 | 0건 | 0건 | -| 어제 (2026-01-14) | 0원 | 0원 | 0건 | 0건 | -| 오늘 (2026-01-15) | 0원 | 0원 | 0건 | 0건 | - ---- - -### 9. 직접 날짜 입력 테스트 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 시작일 입력 | 2025-12-01 | 2025-12-01 | ✅ | -| 종료일 입력 | 2025-12-31 | 2025-12-31 | ✅ | -| 통계 카드 업데이트 | 변경됨 | 입금 47,232,008원, 출금 178,098,104원 | ✅ | - ---- - -### 10. 테이블 데이터 표시 ❌ FAIL - -**BUG-BANK-TRANSACTIONS-20260115-001** - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 통계 카드 데이터 | 표시됨 | 입금 47,232,008원, 출금 178,098,104원 | ✅ | -| 테이블 데이터 | 거래 목록 표시 | "검색 결과가 없습니다." | ❌ | -| 테이블 합계 | 입금/출금 합계 | 0 / 0 | ❌ | - ---- - -## 발견된 버그 - -### BUG-BANK-TRANSACTIONS-20260115-001: 통계 카드와 테이블 데이터 불일치 - -**Priority**: Critical -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\accounting\bank-transactions\page.tsx` - -#### Issue Summary -통계 카드에는 입출금 데이터가 정상적으로 표시되지만, 테이블에는 "검색 결과가 없습니다"로 표시되어 실제 거래 내역을 확인할 수 없음. - -#### Steps to Reproduce -1. 회계관리 > 은행거래 접속 -2. 전월 또는 전전월 버튼 클릭 (2025년 데이터 존재) -3. 통계 카드 확인: 입금/출금 금액 표시됨 -4. 테이블 확인: "검색 결과가 없습니다" 표시 - -#### Expected Result -- 통계 카드에 표시된 입금/출금 금액에 해당하는 거래 내역이 테이블에 표시됨 -- 테이블 합계가 통계 카드 금액과 일치 - -#### Actual Result -- 통계 카드: 입금 47,232,008원, 출금 178,098,104원 (정상) -- 테이블: "검색 결과가 없습니다" (오류) -- 테이블 합계: 0 / 0 (오류) - -#### Error Details -``` -통계 API: 정상 동작 (금액 표시됨) -테이블 API: 데이터 반환 안됨 또는 데이터 매핑 오류 - -가능한 원인: -1. 통계 API와 테이블 API가 다른 데이터 소스 참조 -2. 테이블 렌더링 시 데이터 매핑 로직 오류 -3. 페이지네이션 또는 필터링 로직 오류 -4. 프론트엔드에서 API 응답 파싱 오류 -``` - -#### Suggested Fix (Reference Only) -- 통계 API와 테이블 API의 데이터 소스 일치 확인 -- 프론트엔드 테이블 컴포넌트 데이터 바인딩 확인 -- 브라우저 개발자 도구에서 API 응답 확인 필요 - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - ---- - -## 필터 드롭다운 옵션 - -### 계좌 선택 드롭다운 - -| # | 옵션 | -|---|------| -| 1 | 전체 | -| 2 | KB국민은행\|운영계좌 | -| 3 | NH농협은행\|비상금 | -| 4 | 신한은행\|급여계좌 | -| 5 | 우리은행\|예비계좌 | -| 6 | 하나은행\|법인카드 | - ---- - -## Conclusion - -10개 테스트 케이스 중 8개 통과 (80%) - -### 검증 완료 항목 -1. ✅ 회계관리 > 은행거래 메뉴 접근 -2. ✅ 목록 페이지 구조 (통계 카드 4개, 테이블 컬럼 12개, 필터 3개) -3. ✅ 당해년도 버튼 클릭 (2026년 전체) -4. ✅ 전전월 버튼 클릭 (2025-11) -5. ✅ 전월 버튼 클릭 (2025-12) -6. ✅ 당월 버튼 클릭 (2026-01) -7. ✅ 어제 버튼 클릭 (2026-01-14) -8. ✅ 오늘 버튼 클릭 (2026-01-15) -9. ✅ 직접 날짜 입력 (시작일/종료일 수동 입력) -10. ❌ 테이블 데이터 표시 (BUG-BANK-TRANSACTIONS-20260115-001) - -### 검증 결과 요약 -- **기간 버튼**: 6개 모두 정상 동작 ✅ -- **직접 날짜 입력**: 정상 동작 ✅ -- **통계 카드**: 데이터 정상 표시 ✅ -- **테이블 데이터**: ❌ 표시 안됨 (Critical Bug) - -### 테스트 제외 항목 -- 검색 기능 -- 페이지네이션 -- 행 클릭 상세 보기 -- 체크박스 선택 및 일괄 처리 -- 정렬 기능 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/clodeCheck/card-transactions_2026-01-15_test-report.md b/plans/clodeCheck/card-transactions_2026-01-15_test-report.md deleted file mode 100644 index 9b5f51d..0000000 --- a/plans/clodeCheck/card-transactions_2026-01-15_test-report.md +++ /dev/null @@ -1,351 +0,0 @@ -# E2E Test Report: 카드거래 (Card Transactions) - -**Test ID**: card-transactions -**Executed**: 2026-01-15 -**Status**: ⚠️ PARTIAL (13/15 - 1 Critical Bug) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 15 | -| Passed | 13 | -| Failed | 1 | -| Warning | 1 | -| Pass Rate | 86.7% | - ---- - -## Step Results - -| Step | Test Case | Status | Notes | -|------|-----------|--------|-------| -| 1 | 카드거래 메뉴 진입 | ✅ PASS | /accounting/card-transactions 접속 확인 | -| 2 | 목록 페이지 구조 검증 | ✅ PASS | 통계 카드 2개, 테이블 컬럼 8개 확인 | -| 3 | 2년 기간 설정 | ✅ PASS | 2024-01-15 ~ 2026-01-15 설정, 12행 로드 | -| 4 | 테이블 데이터 존재 확인 | ✅ PASS | 12행, 합계 190,119,372원 | -| 5 | 계정과목명 드롭다운 옵션 확인 | ✅ PASS | 16개 옵션 확인 | -| 6 | 체크박스 선택 | ✅ PASS | 첫 번째 행 선택 | -| 7 | 계정과목명 일괄변경 실행 | ❌ FAIL | API 200 OK 추정, 데이터 미반영 | -| 8 | 일괄변경 결과 확인 | ⚠️ WARN | 데이터 미변경 (미설정 유지) | -| 9 | 행 클릭하여 모달창 열기 | ✅ PASS | 모달 "카드 내역 상세" 표시 | -| 10 | 모달창 필드 상태 확인 | ✅ PASS | 읽기전용 5개, 편집가능 2개 | -| 11 | 모달창에서 적요 수정 | ✅ PASS | "테스트 적요 수정" 입력 | -| 12 | 모달창에서 사용유형 수정 | ✅ PASS | "접대비" 선택, 17개 옵션 확인 | -| 13 | 모달창 저장 버튼 클릭 | ✅ PASS | 저장 성공, 테이블 반영 확인 | -| 14 | 수정 데이터 반영 확인 | ✅ PASS | 사용유형 "접대비"로 변경됨 | -| 15 | 모달창 취소 버튼 동작 확인 | ✅ PASS | 모달 닫힘, 데이터 미변경 | - ---- - -## Detailed Test Results - -### 1. 카드거래 메뉴 진입 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/card-transactions | /accounting/card-transactions | ✅ | -| 페이지 타이틀 | 카드거래 | 카드 내역 조회 | ⚠️ 명칭 상이 | -| 인증 상태 | 로그인됨 | 로그인됨 | ✅ | - ---- - -### 2. 목록 페이지 구조 검증 - -#### 통계 카드 (2개) - -| 카드명 | 값 | 결과 | -|--------|-----|------| -| 전월 사용액 | 0원 | ✅ | -| 당월 사용액 | 0원 | ✅ | - -**참고**: 시나리오에는 "사용금액", "사용유형 미설정" 카드로 정의되어 있으나 실제로는 "전월 사용액", "당월 사용액"으로 구성 - -#### 테이블 컬럼 (8개) - -| # | 컬럼명 | 시나리오 | 결과 | -|---|--------|----------|------| -| 1 | 체크박스 | 체크박스 | ✅ | -| 2 | 카드 | 카드명 | ⚠️ 명칭 상이 | -| 3 | 카드명 | - | 추가 컬럼 | -| 4 | 사용자 | - | 추가 컬럼 | -| 5 | 사용일시 | 사용일시 | ✅ | -| 6 | 가맹점명 | 가맹점명 | ✅ | -| 7 | 사용금액 | 사용금액 | ✅ | -| 8 | 사용유형 | 사용유형 | ✅ | - -**참고**: 시나리오의 "적요" 컬럼이 목록에 없음, 대신 "카드", "카드명", "사용자" 컬럼 존재 - ---- - -### 3. 2년 기간 설정 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 시작일 | 2024-01-15 | 2024-01-15 | ✅ | -| 종료일 | 2026-01-15 | 2026-01-15 | ✅ | -| 데이터 로드 | 있음 | 12행, 190,119,372원 | ✅ | - ---- - -### 4. 테이블 데이터 존재 확인 - -| 항목 | 값 | -|------|-----| -| 총 행 수 | 12 | -| 합계 금액 | 190,119,372원 | -| 표시 기간 | 2025-01-12 ~ 2025-11-19 | - -**데이터 샘플**: -| 사용일시 | 가맹점명 | 사용금액 | 사용유형 | -|----------|----------|----------|----------| -| 2025-11-19 | GS칼텍스 지급 | 3,293,557원 | 미설정 | -| 2025-10-25 | SK이노베이션 지급 | 1,238,454원 | 미설정 | -| 2025-10-10 | 현대제철 지급 | 30,481,719원 | 미설정 | - ---- - -### 5. 계정과목명 드롭다운 옵션 - -**목록 페이지 옵션 (16개)**: -1. 미설정 -2. 매입대금 -3. 선급금 -4. 가지급금 -5. 임대료 -6. 이자비용 -7. 보증금 지급 -8. 차입금 상환 -9. 배당금 지급 -10. 부가세 납부 -11. 급여 -12. 4대보험 -13. 세금 -14. 공과금 -15. 경비 -16. 기타 - -**참고**: 시나리오 정의와 옵션 목록이 다름 (시나리오: 미설정, 접대비, 복리후생비 등) - ---- - -### 6-8. 계정과목명 일괄변경 테스트 ❌ FAIL - -**BUG-CARD-20260115-001** - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 체크박스 선택 | 1개 항목 선택 | 1개 항목 선택됨 | ✅ | -| 계정과목명 선택 | 경비 | 경비 선택됨 | ✅ | -| 저장 버튼 클릭 | 동작 | 동작 | ✅ | -| 확인 다이얼로그 | 표시 | "1개의 카드 사용 내역을 경비(으)로 모두 변경하시겠습니까?" | ✅ | -| 확인 버튼 클릭 | 동작 | 동작 | ✅ | -| 데이터 변경 | 미설정 → 경비 | **미설정 (변경 없음)** | ❌ | - -**버그 상세**: -- **증상**: 확인 다이얼로그까지 정상 표시되나 실제 데이터 변경 안됨 -- **심각도**: Critical -- **영향**: 목록 페이지에서 일괄변경 기능 미동작 -- **관련 버그**: - - BUG-DEPOSIT-20260115-001 (입금관리 동일 증상) - - BUG-WITHDRAWAL-20260115-001 (출금관리 동일 증상) - - BUG-SALES-20260115-001 (매출관리 동일 증상) - ---- - -### 9-10. 모달창 열기 및 필드 검증 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 모달 타이틀 | 카드거래 상세 | 카드 내역 상세 | ⚠️ 명칭 상이 | -| 설명 | - | 카드 사용 상세 내역을 등록합니다 | ✅ | - -#### 모달 필드 상태 - -| 필드명 | 타입 | 상태 | 값 (테스트 행) | -|--------|------|------|----------------| -| 사용일시 | paragraph | disabled | 2025-11-19 | -| 카드 | paragraph | disabled | - (-) | -| 사용자 | paragraph | disabled | - | -| 사용금액 | paragraph | disabled | 3,293,557원 | -| 가맹점 | paragraph | disabled | GS칼텍스 지급 | -| 적요 | textbox | **enabled** | (빈 값) | -| 사용 유형 | combobox | **enabled** | 미설정 | - -#### 모달 버튼 - -| 버튼 | 존재 여부 | -|------|----------| -| 수정 | ✅ | -| Close | ✅ | - -**참고**: 시나리오의 "저장" 버튼은 실제로 "수정" 버튼, "취소" 버튼은 "Close" 버튼 - ---- - -### 11-14. 모달창 수정 및 저장 ✅ PASS - -#### 수정 내용 - -| 필드 | 변경 전 | 변경 후 | -|------|---------|---------| -| 적요 | (빈 값) | 테스트 적요 수정 | -| 사용 유형 | 미설정 | 접대비 | - -#### 모달 사용 유형 드롭다운 옵션 (17개) - -**⚠️ 중요: 목록 페이지 옵션과 다름!** - -1. 미설정 -2. 복리후생비 -3. 접대비 -4. 여비교통비 -5. 차량유지비 -6. 소모품비 -7. 운반비 -8. 통신비 -9. 도서인쇄비 -10. 교육훈련비 -11. 보험료 -12. 광고선전비 -13. 회비 -14. 지급수수료 -15. 세금과공과 -16. 수선비 -17. 임차료 -18. 잡비 - -#### 저장 결과 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 수정 버튼 동작 | 저장 실행 | 저장 실행 | ✅ | -| 모달 닫힘 | 닫힘 | 닫힘 | ✅ | -| URL 유지 | /accounting/card-transactions | /accounting/card-transactions | ✅ | -| 에러 페이지 | 없음 | 없음 | ✅ | -| 테이블 반영 | 접대비 | 접대비 | ✅ | - ---- - -### 15. 모달창 취소 버튼 동작 확인 ✅ PASS - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 다른 행 클릭 | 모달 열림 | 모달 열림 (SK이노베이션 지급) | ✅ | -| Close 버튼 클릭 | 모달 닫힘 | 모달 닫힘 | ✅ | -| 데이터 변경 | 없음 | 미설정 유지 | ✅ | - ---- - -## 발견된 버그 - -### BUG-CARD-20260115-001: 계정과목명 일괄변경 데이터 미반영 - -**Priority**: Critical -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\accounting\card-transactions\page.tsx` - -#### Issue Summary -목록 페이지에서 체크박스로 항목 선택 후 계정과목명을 변경하고 저장 시, 확인 다이얼로그까지 표시되나 실제 데이터는 변경되지 않음. - -#### Steps to Reproduce -1. 회계관리 > 카드거래 접속 -2. 테이블에서 행 체크박스 선택 -3. 계정과목명 드롭다운에서 옵션 선택 (예: 경비) -4. 저장 버튼 클릭 -5. 확인 다이얼로그에서 확인 클릭 -6. 결과: 데이터 미변경 - -#### Expected Result -- 선택된 항목의 사용유형이 변경됨 -- 테이블에 변경된 값 반영 - -#### Actual Result -- 확인 다이얼로그까지 정상 표시 -- 데이터가 변경되지 않음 (미설정 유지) - -#### Error Details -``` -Dialog Message: "1개의 카드 사용 내역을 경비(으)로 모두 변경하시겠습니까?" -Result: 데이터 미변경 (미설정 → 미설정) - -동일 패턴 버그: -- BUG-DEPOSIT-20260115-001 (입금관리) -- BUG-WITHDRAWAL-20260115-001 (출금관리) -- BUG-SALES-20260115-001 (매출관리) -``` - -#### Suggested Fix (Reference Only) -- 확인 버튼 클릭 후 API 호출 로직 점검 -- 요청 페이로드와 실제 DB 업데이트 로직 확인 -- 프론트엔드에서 올바른 파라미터 전송 여부 확인 - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - ---- - -## 시나리오 vs 실제 시스템 차이점 - -| 항목 | 시나리오 정의 | 실제 시스템 | 비고 | -|------|--------------|------------|------| -| 페이지 타이틀 | 카드거래 | 카드 내역 조회 | 명명 규칙 차이 | -| 모달 타이틀 | 카드거래 상세 | 카드 내역 상세 | 명명 규칙 차이 | -| 통계 카드 | 사용금액, 사용유형 미설정 | 전월 사용액, 당월 사용액 | 구조 차이 | -| 테이블 컬럼 | 7개 (체크박스, 카드명, 사용일시, 가맹점명, 사용금액, 적요, 사용유형) | 8개 (체크박스, 카드, 카드명, 사용자, 사용일시, 가맹점명, 사용금액, 사용유형) | 컬럼 차이 | -| 목록 계정과목 옵션 | 9개 | 16개 | 옵션 수 차이 | -| 모달 사용유형 옵션 | 9개 | 17개 | 옵션 수 차이 | -| 저장 버튼 (모달) | 저장 | 수정 | 버튼명 차이 | -| 취소 버튼 (모달) | 취소 | Close | 버튼명 차이 | - ---- - -## 드롭다운 옵션 불일치 ⚠️ 주의 - -**목록 페이지 계정과목명 (16개)**: -미설정, 매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타 - -**모달 사용 유형 (17개)**: -미설정, 복리후생비, 접대비, 여비교통비, 차량유지비, 소모품비, 운반비, 통신비, 도서인쇄비, 교육훈련비, 보험료, 광고선전비, 회비, 지급수수료, 세금과공과, 수선비, 임차료, 잡비 - -**⚠️ 두 드롭다운의 옵션이 완전히 다름!** 이는 의도된 설계인지 확인 필요. - ---- - -## Conclusion - -15개 테스트 케이스 중 13개 통과 (86.7%) - -### 검증 완료 항목 -1. ✅ 회계관리 > 카드거래 메뉴 접근 -2. ✅ 목록 페이지 구조 (통계 카드 2개, 테이블 컬럼 8개) -3. ✅ 2년 기간 설정 (2024-01-15 ~ 2026-01-15) -4. ✅ 테이블 데이터 표시 (12행, 190,119,372원) -5. ✅ 계정과목명 드롭다운 옵션 (16개) -6. ✅ 체크박스 선택 기능 -7. ❌ 계정과목명 일괄변경 (BUG-CARD-20260115-001) -8. ✅ 행 클릭 → 모달창 열기 -9. ✅ 모달창 필드 상태 (읽기전용 5개, 편집가능 2개) -10. ✅ 모달창 적요 수정 -11. ✅ 모달창 사용유형 수정 (17개 옵션) -12. ✅ 모달창 저장 → 테이블 반영 확인 -13. ✅ 모달창 취소(Close) 버튼 동작 - -### 핵심 발견 사항 -- **일괄변경 버그**: 입금/출금/매출/카드거래 4개 메뉴에서 동일 패턴 버그 발생 -- **모달 수정 기능 정상**: 개별 행 수정은 정상 동작 -- **드롭다운 옵션 불일치**: 목록 페이지와 모달의 옵션 목록이 다름 - -### 테스트 제외 항목 -- 검색 기능 -- 필터 기능 (전체/최신순) -- 페이지네이션 -- 기간 버튼 (당해년도, 전전월 등) -- 새로고침 버튼 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/clodeCheck/employee-register_2026-01-14_20-00-00.md b/plans/clodeCheck/employee-register_2026-01-14_20-00-00.md deleted file mode 100644 index 9880be9..0000000 --- a/plans/clodeCheck/employee-register_2026-01-14_20-00-00.md +++ /dev/null @@ -1,179 +0,0 @@ -# E2E Test Report: 직원 등록 테스트 - -**Test ID**: employee-register -**Executed**: 2026-01-14 20:00:00 -**Duration**: ~5분 -**Status**: ❌ FAIL - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 8 | -| Passed | 7 | -| Failed | 1 | - -## Step Results - -| Step | Name | Status | Duration | Notes | -|------|------|--------|----------|-------| -| 1 | 인사관리 메뉴 진입 | ✅ PASS | 2s | 인사관리 > 직원관리 메뉴 이동 성공 | -| 2 | 사원 등록 페이지 이동 | ✅ PASS | 1s | /hr/employee-management/new 이동 성공 | -| 3 | 사원 정보 입력 | ✅ PASS | 3s | 이름, 주민등록번호, 휴대폰, 이메일, 연봉 입력 완료 | -| 4 | 급여계좌 입력 | ✅ PASS | 2s | 은행명, 계좌번호, 예금주 입력 완료 | -| 5 | 사원 상세 입력 | ✅ PASS | 2s | 사원코드, 성별, 주소 입력 완료 | -| 6 | 인사 정보 입력 | ✅ PASS | 3s | 입사일, 고용형태(정규직), 직급(과장) 선택 완료 | -| 7 | 사용자 정보 입력 | ✅ PASS | 2s | 아이디, 비밀번호, 비밀번호 확인 입력 완료 | -| 8 | 등록 완료 | ❌ FAIL | 2s | 서버 에러 발생 | - -## Test Data Used - -| Field | Value | -|-------|-------| -| 이름 | 테스트직원_1768387800 | -| 주민등록번호 | 900101-1234567 | -| 휴대폰 | 010-9876-5432 | -| 이메일 | testemployee_1768387800@codebridge-x.com | -| 연봉 | 50000000 | -| 은행명 | 신한은행 | -| 계좌번호 | 110-123-456789 | -| 예금주 | 테스트직원_1768387800 | -| 사원코드 | EMP2026001 | -| 성별 | 남성 | -| 상세주소 | 123번지 4층 | -| 입사일 | 2026-01-14 | -| 고용형태 | 정규직 | -| 직급 | 과장 | -| 상태 | 재직 | -| 아이디 | testuser_1768387800 | -| 비밀번호 | password123! | -| 권한 | 일반 사용자 | -| 계정상태 | 활성 | - -## Error Details - -### Step 8: 등록 완료 - -**Error Type**: Server Error -**Error Message**: `[EmployeeNewPage] Create failed: 서버 에러` -**Console Log**: -``` -[ERROR] [EmployeeNewPage] Create failed: 서버 에러 -``` - -**Network Request**: -``` -[POST] https://dev.codebridge-x.com/hr/employee-management/new => 서버 에러 -``` - -**Screenshot**: [에러 스크린샷](screenshots/employee-register_error_2026-01-14.png) - -## Assertions - -| Type | Expected | Actual | Result | -|------|----------|--------|--------| -| URL (Step 2) | /hr/employee-management/new | /hr/employee-management/new | ✅ PASS | -| 이름 입력 | 테스트직원_1768387800 | 테스트직원_1768387800 | ✅ PASS | -| 이메일 입력 | testemployee_1768387800@codebridge-x.com | testemployee_1768387800@codebridge-x.com | ✅ PASS | -| 고용형태 선택 | 정규직 | 정규직 | ✅ PASS | -| 직급 선택 | 과장 | 과장 | ✅ PASS | -| 아이디 입력 | testuser_1768387800 | testuser_1768387800 | ✅ PASS | -| 등록 완료 | 목록 페이지 리다이렉트 | 서버 에러 | ❌ FAIL | - -## Test Environment - -- **Browser**: Chromium (Playwright) -- **URL**: https://dev.codebridge-x.com -- **Login User**: TestUser5 / 홍킬동 -- **Test Scenario**: employee-register.json - -## Screenshots - -- [에러 스크린샷](screenshots/employee-register_error_2026-01-14.png) - ---- - -## 🐛 Bug Report for Developer - -**Report ID**: 2026-01-14_20-00-00 -**Priority**: High -**Component**: `C:\Users\codeb\react\app\[locale]\(protected)\hr\employee-management\new\page.tsx` - -### Issue Summary -사원 등록 시 서버 에러 발생 - 모든 필수 필드 입력 완료 후 등록 버튼 클릭 시 "서버 에러" 토스트 메시지 출력 - -### Steps to Reproduce -1. 인사관리 > 직원관리 메뉴 진입 -2. "사원 등록" 버튼 클릭 -3. 모든 필수 필드 입력: - - 이름: 테스트직원_1768387800 - - 이메일: testemployee_1768387800@codebridge-x.com - - 아이디: testuser_1768387800 - - 비밀번호: password123! - - 비밀번호 확인: password123! -4. "등록" 버튼 클릭 - -### Expected Result -- 사원 등록 성공 -- 목록 페이지(/hr/employee-management)로 리다이렉트 -- 성공 토스트 메시지 표시 -- 목록에 신규 등록된 사원 표시 - -### Actual Result -- 서버 에러 발생 -- 토스트 메시지: "서버 에러" -- 페이지 이동 없음 (등록 페이지 유지) - -### Error Details -``` -Console Error: [EmployeeNewPage] Create failed: 서버 에러 -``` - -### Screenshots -- [에러 발생 화면](screenshots/employee-register_error_2026-01-14.png) - -### Suggested Fix (Reference Only) - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - -**가능한 원인 분석**: -1. **API 엔드포인트 문제**: 사원 등록 API가 500 에러 반환 -2. **데이터 검증 실패**: 서버측 데이터 검증에서 예상치 못한 에러 -3. **DB 제약 조건**: 중복 키 또는 외래 키 제약 조건 위반 -4. **필수 필드 누락**: 부서/직책 미선택으로 인한 서버 검증 실패 가능성 - -**조사 필요 사항**: -1. API 서버 로그 확인 (500 에러 상세 내용) -2. 사원 등록 API 요청 payload 검증 -3. DB 테이블 스키마 및 제약 조건 확인 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` -- DB 스키마: `C:\Users\codeb\docs\specs\database-schema.md` - ---- - -## Notes - -### 테스트 실패 원인 분석 -1. **서버 에러**: API 엔드포인트에서 500 에러 반환 추정 -2. **부서/직책 미선택**: "부서/직책을 추가해주세요" 메시지가 표시되어 있으나, 필수 필드인지 확인 필요 -3. **출퇴근 위치 미선택**: 출근/퇴근 위치가 선택되지 않았으나, 필수 여부 확인 필요 - -### UI/UX 확인 사항 -- ✅ 폼 입력 필드 정상 동작 -- ✅ 드롭다운 선택 정상 동작 -- ✅ 라디오 버튼 선택 정상 동작 -- ✅ 날짜 입력 정상 동작 -- ❌ 등록 버튼 클릭 시 서버 에러 - -### 직급 드롭다운 참고 -- 테스트 시 "사원" 옵션을 찾으려 했으나 "과장"만 표시됨 -- 직급 옵션이 "과장"만 있는 것은 기준정보 설정에 따라 다를 수 있음 - ---- - -**Test Result**: ❌ **FAILED** (7/8 steps passed) diff --git a/plans/clodeCheck/salary-management_2026-01-15_10-30-00.md b/plans/clodeCheck/salary-management_2026-01-15_10-30-00.md deleted file mode 100644 index 7b52803..0000000 --- a/plans/clodeCheck/salary-management_2026-01-15_10-30-00.md +++ /dev/null @@ -1,175 +0,0 @@ -# E2E Test Report: 급여관리 테스트 - -**Test ID**: salary-management -**Executed**: 2026-01-15 10:30:00 -**Duration**: ~8분 -**Status**: ⚠️ PARTIAL (4/5 PASS, 1 FAIL) - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 13 | -| Passed | 12 | -| Failed | 1 | -| Pass Rate | 92.3% | - ---- - -## 필수 검증 항목 결과 - -| # | 검증 항목 | 결과 | 비고 | -|---|----------|------|------| -| 1 | 파일 다운로드 (엑셀) | ❌ FAIL | 기능 미구현 - toast.info만 출력 | -| 2 | 등록/저장 버튼 | ✅ PASS | 지급완료/지급예정 상태 변경 성공 | -| 3 | 검색/필터 | ✅ PASS | 16건 → 1건 필터링 정상 동작 | -| 4 | 모달 등록 완료 | ✅ PASS | 급여 상세 다이얼로그 저장 성공 | -| 5 | 목업 페이지 감지 | ✅ PASS | 정상 페이지 (목업 아님) | - ---- - -## Step Results - -| Step | Name | Status | Notes | -|------|------|--------|-------| -| 1 | 로그인 | ✅ PASS | TestUser5 / password123! 로그인 성공 | -| 2 | 인사관리 > 급여관리 메뉴 진입 | ✅ PASS | /hr/salary-management 페이지 진입 | -| 3 | 필수 검증 #5: 목업 페이지 감지 | ✅ PASS | 입력 필드 및 동작하는 버튼 존재 | -| 4 | 급여 현황 대시보드 확인 | ✅ PASS | 6개 카드 표시 확인 (총 실지급액, 기본급, 수당, 초과근무, 상여, 공제) | -| 5 | 급여 테이블 구조 확인 | ✅ PASS | 14개 컬럼 존재 확인 | -| 6 | 날짜 필터 확인 | ✅ PASS | 시작일/종료일 필드 존재 | -| 7 | 필수 검증 #3: 검색 기능 | ✅ PASS | "홍" 검색 → 16건에서 1건으로 필터링 | -| 8 | 정렬 옵션 확인 | ✅ PASS | 직급순/이름순/부서순/지급일순/지급액순 옵션 확인 | -| 9 | 필수 검증 #2: 상태 변경 (지급완료) | ✅ PASS | 체크박스 선택 후 지급완료 버튼 동작 | -| 10 | 수정 버튼 - 상세 다이얼로그 열기 | ✅ PASS | 급여 수정 다이얼로그 정상 열림 | -| 11 | 필수 검증 #4: 상세 다이얼로그 저장 | ✅ PASS | 상태 변경 후 저장 성공, 토스트 "급여 정보가 저장되었습니다." | -| 12 | 다이얼로그 닫기 확인 | ✅ PASS | 저장 후 자동으로 모달 닫힘 | -| 13 | 필수 검증 #1: 엑셀 다운로드 | ❌ FAIL | 기능 미구현 | - ---- - -## Errors - -### ❌ 필수 검증 #1: 엑셀 다운로드 FAIL - -**버그 유형**: 기능 미구현 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 버튼 클릭 | 다운로드 시작 | 토스트만 표시 | ❌ | -| Console LOG | export 로그 | 없음 | ❌ | -| Network API 호출 | /api/export, /api/download | 미호출 | ❌ | -| Download Event | 발생 | 미발생 | ❌ | -| 토스트 메시지 | 다운로드 완료 | "엑셀 다운로드 기능은 준비 중입니다." | ❌ | - -**최종 판정**: ❌ FAIL (Console LOG만 존재, API 미호출, 다운로드 미발생) - -**코드 분석**: -```tsx -// c:/Users/codeb/react/src/components/hr/SalaryManagement/index.tsx:441 - -``` - ---- - -## 🐛 Bug Report for Developer - -**Report ID**: BUG-SALARY-001-2026-01-15 -**Priority**: Medium -**Component**: `c:\Users\codeb\react\src\components\hr\SalaryManagement\index.tsx:441` - -### Issue Summary -엑셀 다운로드 버튼 클릭 시 실제 다운로드가 발생하지 않고 "엑셀 다운로드 기능은 준비 중입니다." 토스트만 표시됨 - -### Steps to Reproduce -1. 급여관리 페이지 (/hr/salary-management) 접속 -2. "엑셀 다운로드" 버튼 클릭 -3. 토스트 메시지만 표시되고 파일 다운로드 없음 - -### Expected Result -- 엑셀 파일(.xlsx) 다운로드 시작 -- Network API 호출 (예: POST /api/salary/export) -- 다운로드 완료 토스트 또는 파일 저장 다이얼로그 - -### Actual Result -- toast.info('엑셀 다운로드 기능은 준비 중입니다.') 출력 -- Network API 호출 없음 -- 파일 다운로드 없음 - -### Error Details -- Console 에러: 없음 -- Network 요청: 미발생 -- 상태: 기능 미구현 - -### Suggested Fix (Reference Only) - -**영향 범위**: react / api -**변경 승인 정책**: ⚠️ 컨펌 필요 - -1. **React 컴포넌트 수정** (`SalaryManagement/index.tsx`) - - toast.info 대신 실제 export API 호출 로직 구현 - - API 응답으로 Blob 받아 다운로드 처리 - -2. **API 엔드포인트 구현** (필요시) - - POST /api/salary/export 또는 GET /api/salary/download - - 급여 데이터를 엑셀 형식으로 변환하여 반환 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` - ---- - -## 추가 발견 사항 - -### ⚠️ 지급항목 추가 버튼 미구현 - -급여 상세 다이얼로그 내 "지급항목 추가" 버튼도 동일하게 미구현 상태입니다. - -```tsx -// c:/Users/codeb/react/src/components/hr/SalaryManagement/index.tsx:227-229 -const handleAddPaymentItem = useCallback(() => { - // TODO: 지급항목 추가 다이얼로그 또는 로직 구현 - toast.info('지급항목 추가 기능은 준비 중입니다.'); -}, []); -``` - ---- - -## 테스트 환경 - -| 항목 | 값 | -|------|-----| -| 테스트 URL | https://dev.codebridge-x.com | -| 테스트 계정 | TestUser5 | -| 시나리오 파일 | tests/e2e/scenarios/salary-management.json | -| 브라우저 | Playwright (Chromium) | - ---- - -## Console Warnings - -| 유형 | 메시지 | 심각도 | -|------|--------|--------| -| WARNING | Missing `Description` or `aria-describedby={undefined}` for {DialogContent} | Low | - -**권장 조치**: 접근성 개선을 위해 Dialog에 aria-describedby 속성 추가 필요 - ---- - -## 결론 - -급여관리 페이지는 전반적으로 정상 동작하지만, **엑셀 다운로드 기능**과 **지급항목 추가 기능**이 미구현 상태입니다. -해당 기능들은 버튼만 존재하고 실제 로직이 toast.info()로 대체되어 있으므로 백엔드 API 연동 및 프론트엔드 로직 구현이 필요합니다. - -| 기능 | 상태 | 우선순위 | -|------|------|----------| -| 엑셀 다운로드 | 미구현 | Medium | -| 지급항목 추가 | 미구현 | Low | - diff --git a/plans/clodeCheck/sales-management_2026-01-15_test-report.md b/plans/clodeCheck/sales-management_2026-01-15_test-report.md deleted file mode 100644 index 0a81c92..0000000 --- a/plans/clodeCheck/sales-management_2026-01-15_test-report.md +++ /dev/null @@ -1,226 +0,0 @@ -# E2E Test Report: 매출관리 (Sales Management) - -**Test ID**: sales-management -**Executed**: 2026-01-15 -**Status**: ❌ FAIL (11/12) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 12 | -| Passed | 11 | -| Failed | 1 | -| Pass Rate | 91.7% | - ---- - -## Step Results - -| Step | Test Case | Status | Duration | Notes | -|------|-----------|--------|----------|-------| -| 1 | 로그인 및 페이지 진입 | ✅ PASS | - | 이미 로그인 상태, /accounting/sales 접속 확인 | -| 2 | 목업 감지 | ✅ PASS | - | 실제 데이터 81건 표시, API 연동 정상 | -| 3 | 테이블 구조 확인 | ✅ PASS | - | 11개 컬럼 확인 (번호~거래명세서) | -| 4 | 계정과목명 드롭박스 변경 | ✅ PASS | - | 8개 옵션 표시, 선택 정상 동작 | -| 5 | 저장 버튼 동작 | ✅ PASS | - | 확인 다이얼로그 + 성공 토스트 표시 | -| 6 | **계정과목명 변경 데이터 반영** | ❌ FAIL | - | **토스트 성공 표시되나 실제 데이터 미변경** | -| 7 | 매출 등록 페이지 이동 | ✅ PASS | - | /accounting/sales/new 이동 확인 | -| 8 | 기본정보 드롭박스 테스트 | ✅ PASS | - | 거래처명 5개, 매출유형 7개 옵션 확인 | -| 9 | 품목 추가/삭제 및 자동계산 | ✅ PASS | - | 동적 추가/삭제 정상, 공급가액/부가세 자동계산 | -| 10 | Switch 버튼 동작 | ✅ PASS | - | 세금계산서/거래명세서 발행 토글 정상 | -| 11 | 취소 버튼 동작 | ✅ PASS | - | 목록 페이지 복귀 확인 | -| 12 | 등록 API 호출 | ⏭️ SKIP | - | 이전 테스트에서 검증 완료 | - ---- - -## Detailed Test Results - -### 1. 목록 페이지 검증 - -#### 목업 감지 검증 -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 데이터 존재 | 있음 | 81건 | ✅ | -| API 연동 | 정상 | 정상 | ✅ | -| 입력 필드 | 있음 | 있음 | ✅ | -| 버튼 동작 | 정상 | 정상 | ✅ | - -**판정**: 정상 페이지 (목업 아님) - -#### 테이블 구조 -| # | 컬럼명 | 존재 여부 | -|---|--------|----------| -| 1 | 번호 | ✅ | -| 2 | 매출번호 | ✅ | -| 3 | 매출일 | ✅ | -| 4 | 거래처 | ✅ | -| 5 | 공급가액 | ✅ | -| 6 | 부가세 | ✅ | -| 7 | 합계금액 | ✅ | -| 8 | 매출유형 | ✅ | -| 9 | 세금계산서 발행완료 | ✅ | -| 10 | 거래명세서 발행완료 | ✅ | -| 11 | (액션) | ✅ | - ---- - -### 2. 계정과목명 일괄 변경 - -#### 드롭박스 옵션 -- 미설정, 제품 매출, 상품 매출, 부품 매출, 용역 매출, 공사 매출, 임대수익, 기타매출 - -#### 저장 동작 검증 -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 확인 다이얼로그 | 표시 | "1개의 매출유형을 제품 매출(으)로 모두 변경하시겠습니까?" | ✅ | -| 성공 토스트 | 표시 | "계정과목명이 변경되었습니다." | ✅ | -| URL 유지 | /accounting/sales | /accounting/sales | ✅ | -| **데이터 변경** | **제품 매출** | **기타 매출 (변경 안됨)** | ❌ | - ---- - -### 3. 매출 등록 페이지 - -#### 페이지 구조 -- 기본 정보: 매출번호(자동생성), 매출일, 거래처명, 매출유형 -- 품목 정보: 테이블 + 추가 버튼 -- 세금계산서: Switch + 상태 표시 -- 거래명세서: Switch + 조회/발행 버튼 + 상태 표시 -- 취소/등록 버튼 - -#### 거래처명 드롭박스 -- 거래처테스트, 아크더레드, 코브라브릿지, 가우스전자, 아크아크 - -#### 매출유형 드롭박스 -- 외상 매출, 제품 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 - ---- - -### 4. 품목 정보 자동계산 검증 - -#### 테스트 데이터 -| 품목 | 수량 | 단가 | 공급가액 | 부가세 | -|------|------|------|----------|--------| -| 테스트 품목 A | 10 | 50,000 | 500,000 | 50,000 | -| 테스트 품목 B | 5 | 30,000 | 150,000 | 15,000 | -| **합계** | - | - | **650,000** | **65,000** | - -#### 자동계산 검증 -| 항목 | 계산식 | 예상 | 실제 | 결과 | -|------|--------|------|------|------| -| 공급가액 A | 10 × 50,000 | 500,000 | 500,000 | ✅ | -| 부가세 A | 500,000 × 10% | 50,000 | 50,000 | ✅ | -| 공급가액 B | 5 × 30,000 | 150,000 | 150,000 | ✅ | -| 부가세 B | 150,000 × 10% | 15,000 | 15,000 | ✅ | -| 합계 공급가액 | 500,000 + 150,000 | 650,000 | 650,000 | ✅ | -| 합계 부가세 | 50,000 + 15,000 | 65,000 | 65,000 | ✅ | - -#### 품목 삭제 검증 -- 두 번째 품목 삭제 후 합계: 500,000 / 50,000 ✅ - ---- - -### 5. Switch 버튼 동작 - -| Switch | 초기 상태 | 클릭 후 상태 | 결과 | -|--------|----------|-------------|------| -| 세금계산서 발행 | 미발행 | 발행완료 | ✅ | -| 거래명세서 발행 | 미발행 | 발행완료 | ✅ | - ---- - -### 6. 취소 버튼 동작 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 클릭 후 URL | /accounting/sales | /accounting/sales | ✅ | -| 페이지 이동 | 목록 페이지 | 목록 페이지 | ✅ | - ---- - -## 🐛 Bug Report: 계정과목명 변경 데이터 미반영 - -**Report ID**: BUG-SALES-20260115-001 -**Priority**: High -**Component**: `C:\Users\codeb\react\src\components\accounting\SalesManagement\` - -### Issue Summary -계정과목명 일괄 변경 기능에서 성공 토스트가 표시되지만 실제 데이터가 변경되지 않음 - -### Steps to Reproduce -1. 매출관리 목록 페이지 (/accounting/sales) 접속 -2. 테이블에서 첫 번째 행의 체크박스 선택 (SL202601150001, 현재 매출유형: "기타 매출") -3. 상단 계정과목명 드롭박스에서 "제품 매출" 선택 -4. "저장" 버튼 클릭 -5. 확인 다이얼로그에서 "확인" 클릭 - -### Expected Result -- 선택된 행의 매출유형이 "제품 매출"로 변경되어야 함 -- 페이지 새로고침 후에도 변경된 값이 유지되어야 함 - -### Actual Result -- ✅ 확인 다이얼로그: "1개의 매출유형을 제품 매출(으)로 모두 변경하시겠습니까?" 표시 -- ✅ 성공 토스트: "계정과목명이 변경되었습니다." 표시 -- ❌ 테이블의 매출유형 값이 여전히 "기타 매출"로 표시됨 -- ❌ 페이지 새로고침 후에도 "기타 매출" 유지 (데이터 미저장) - -### Error Analysis -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 확인 다이얼로그 | 표시 | 표시됨 | ✅ | -| 성공 토스트 | 표시 | 표시됨 | ✅ | -| 매출유형 변경 | 제품 매출 | 기타 매출 (변경 안됨) | ❌ | -| 데이터 영속성 | 저장됨 | 미저장 | ❌ | - -### Suggested Fix (Reference Only) - -**가능한 원인 분석**: -1. **API 미호출**: 프론트엔드에서 저장 API를 호출하지 않을 수 있음 -2. **API 파라미터 오류**: 선택된 ID 또는 변경할 값이 올바르게 전달되지 않을 수 있음 -3. **API 응답 처리 오류**: API는 성공했으나 프론트엔드에서 상태를 갱신하지 않을 수 있음 -4. **백엔드 버그**: API가 성공 응답을 반환하지만 실제 DB 업데이트가 이루어지지 않을 수 있음 - -**영향 범위**: react / api -**변경 승인 정책**: ⚠️ 컨펌 필요 - -**확인 필요 사항**: -1. `actions.ts`의 `updateSale()` 함수가 일괄 변경 시 올바르게 호출되는지 확인 -2. API 요청 payload에 선택된 ID와 변경할 계정과목 값이 포함되는지 확인 -3. 백엔드 `/api/v1/sales/{id}` PUT 엔드포인트의 실제 동작 확인 -4. 네트워크 탭에서 실제 API 호출 여부 및 응답 확인 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` - ---- - -## Conclusion - -11개 테스트 케이스 중 1개 실패 (91.7% 통과율) - -### 검증 완료 항목 (11/12) -1. ✅ 목록 페이지 - 목업 아닌 실제 동작 확인 (81건 데이터) -2. ✅ 테이블 구조 - 11개 컬럼 정상 표시 -3. ✅ 계정과목명 드롭박스 - 8개 옵션 표시, 저장 버튼 동작 정상 -4. ❌ **계정과목명 변경 데이터 반영 - 토스트 성공 표시되나 실제 데이터 미변경 (버그)** -5. ✅ 매출 등록 페이지 - 페이지 이동 정상 -6. ✅ 거래처명 드롭박스 - 5개 옵션 정상 -7. ✅ 매출유형 드롭박스 - 7개 옵션 정상 -8. ✅ 품목 동적 추가/삭제 - 정상 동작 -9. ✅ 자동계산 로직 - 공급가액(수량×단가), 부가세(10%) 정확 -10. ✅ Switch 버튼 - 세금계산서/거래명세서 토글 정상 -11. ✅ 취소 버튼 - 목록 페이지 복귀 정상 - -### 테스트 제외 항목 (사용자 요청) -- 삭제 기능 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/clodeCheck/withdrawal-management_2026-01-15_test-report.md b/plans/clodeCheck/withdrawal-management_2026-01-15_test-report.md deleted file mode 100644 index bf7be19..0000000 --- a/plans/clodeCheck/withdrawal-management_2026-01-15_test-report.md +++ /dev/null @@ -1,299 +0,0 @@ -# E2E Test Report: 출금관리 (Withdrawal Management) - -**Test ID**: withdrawal-management -**Executed**: 2026-01-15 -**Status**: ⚠️ PARTIAL (11/12 - 1 Bug) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 12 | -| Passed | 11 | -| Failed | 1 | -| Pass Rate | 91.7% | - ---- - -## Step Results - -| Step | Test Case | Status | Notes | -|------|-----------|--------|-------| -| 1 | 회계관리 메뉴 진입 | ✅ PASS | /accounting/withdrawals 접속 확인 | -| 2 | 목록 페이지 구조 검증 | ✅ PASS | 통계 카드 4개, 테이블 컬럼 8개 확인 | -| 3 | 계정과목명 드롭다운 옵션 확인 | ✅ PASS | 16개 옵션 확인 (시나리오 14개와 상이) | -| 4 | 계정과목명 일괄변경 테스트 | ❌ FAIL | API 200 OK, 데이터 미반영 | -| 5 | 상세 페이지 진입 | ✅ PASS | /accounting/withdrawals/58 이동 확인 | -| 6 | 상세 페이지 필드 검증 | ✅ PASS | 기본 정보 섹션 7개 필드 확인 | -| 7 | 수정 모드 전환 | ✅ PASS | ?mode=edit URL 변경, 버튼 변경 확인 | -| 8 | 수정 가능 필드 검증 | ✅ PASS | 적요, 거래처, 출금유형 수정 가능 | -| 9 | 필수값 유효성 검증 | ✅ PASS | "거래처를 선택해주세요" 토스트 확인 | -| 10 | 상세 페이지 수정 저장 | ✅ PASS | 거래처, 출금유형 변경 후 저장 성공 | -| 11 | 수정 데이터 반영 확인 | ✅ PASS | 목록에서 변경된 데이터 확인 | -| 12 | 출금유형 미설정 건수 감소 | ✅ PASS | 60건 → 59건 확인 | - ---- - -## Detailed Test Results - -### 1. 회계관리 메뉴 진입 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/withdrawals | /accounting/withdrawals | ✅ | -| 페이지 타이틀 | 출금관리 | 출금관리 | ✅ | -| 인증 상태 | 로그인됨 | 로그인됨 | ✅ | - ---- - -### 2. 목록 페이지 구조 검증 - -#### 통계 카드 (4개) - -| 카드명 | 값 | 결과 | -|--------|-----|------| -| 총 출금 | 1,214,143,687원 | ✅ | -| 당월 출금 | 0원 | ✅ | -| 거래처 미설정 | 0건 | ✅ | -| 출금유형 미설정 | 60건 | ✅ | - -#### 테이블 컬럼 (8개) - -| # | 컬럼명 | 시나리오 | 결과 | -|---|--------|----------|------| -| 1 | 체크박스 | 체크박스 | ✅ | -| 2 | 출금일 | 출금일 | ✅ | -| 3 | 출금계좌 | 출금계좌 | ✅ | -| 4 | 수취인명 | 받는분 | ⚠️ 컬럼명 상이 | -| 5 | 출금금액 | 출금금액 | ✅ | -| 6 | 거래처 | 거래처 | ✅ | -| 7 | 적요 | 적요 | ✅ | -| 8 | 출금유형 | 출금유형 | ✅ | - -**참고**: 시나리오의 "받는분" 컬럼이 실제 시스템에서는 "수취인명"으로 표시됨 - ---- - -### 3. 계정과목명 드롭다운 옵션 - -**실제 옵션 (16개)**: -1. 미설정 -2. 매입대금 -3. 선급금 -4. 가지급금 -5. 임대료 -6. 이자비용 -7. 보증금 지급 -8. 차입금 상환 -9. 배당금 지급 -10. 부가세 납부 -11. 급여 -12. 4대보험 -13. 세금 -14. 공과금 -15. 경비 -16. 기타 - -**참고**: 시나리오에는 14개 옵션으로 정의되어 있으나 실제로는 16개 옵션 존재 - ---- - -### 4. 계정과목명 일괄변경 테스트 ❌ FAIL - -**BUG-WITHDRAWAL-20260115-001** - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 체크박스 선택 | 1개 항목 선택 | 1개 항목 선택됨 | ✅ | -| 계정과목명 선택 | 매입대금 | 매입대금 | ✅ | -| 저장 버튼 클릭 | 동작 | 동작 | ✅ | -| 확인 다이얼로그 | 표시 | "1개의 출금 유형을 매입대금(으)로 모두 변경하시겠습니까?" | ✅ | -| 확인 버튼 클릭 | 동작 | 동작 | ✅ | -| API 호출 | POST /accounting/withdrawals | POST /accounting/withdrawals (200 OK) | ✅ | -| 데이터 변경 | 미설정 → 매입대금 | **미설정 (변경 없음)** | ❌ | -| 출금유형 미설정 건수 | 59건 | **60건 (변경 없음)** | ❌ | - -**버그 상세**: -- **증상**: API 호출은 성공(200 OK)하지만 실제 데이터가 변경되지 않음 -- **심각도**: High -- **영향**: 일괄변경 기능 미동작 -- **버그 유형**: 백엔드 API 로직 오류 또는 프론트엔드-백엔드 데이터 불일치 -- **관련 버그**: - - BUG-DEPOSIT-20260115-001 (입금관리 동일 증상) - - BUG-SALES-20260115-001 (매출관리 동일 증상) - ---- - -### 5-6. 상세 페이지 진입 및 필드 검증 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/withdrawals/{id} | /accounting/withdrawals/58 | ✅ | -| 페이지 타이틀 | 출금 상세 | 출금 상세 | ✅ | -| 버튼 | 목록, 삭제, 수정 | 목록, 삭제, 수정 | ✅ | - -#### 기본 정보 필드 - -| 필드명 | 타입 | 상태 | 값 | 결과 | -|--------|------|------|-----|------| -| 출금일 | textbox | disabled | 2025-12-27 | ✅ | -| 출금계좌 | textbox | disabled | 운영계좌 | ✅ | -| 수취인명 | textbox | disabled | 두산에너빌리티 | ✅ | -| 출금금액 | textbox | disabled | 1,513,170 | ✅ | -| 적요 | textbox | disabled | 두산에너빌리티 지급 | ✅ | -| 거래처 * | combobox | disabled | 선택 ▼ | ✅ | -| 출금 유형 * | combobox | disabled | 미설정 | ✅ | - ---- - -### 7-8. 수정 모드 전환 및 필드 활성화 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | ?mode=edit 추가 | /accounting/withdrawals/58?mode=edit | ✅ | -| 페이지 타이틀 | 출금 수정 | 출금 수정 | ✅ | -| 버튼 변경 | 취소, 저장 | 취소, 저장 | ✅ | - -#### 수정 모드 필드 상태 - -| 필드명 | 읽기 모드 | 수정 모드 | 결과 | -|--------|----------|----------|------| -| 출금일 | disabled | disabled | ✅ | -| 출금계좌 | disabled | disabled | ✅ | -| 수취인명 | disabled | disabled | ✅ | -| 출금금액 | disabled | disabled | ✅ | -| 적요 | disabled | **enabled** | ✅ | -| 거래처 | disabled | **enabled** | ✅ | -| 출금 유형 | disabled | **enabled** | ✅ | - ---- - -### 9. 필수값 유효성 검증 - -| 시나리오 | 입력값 | 예상 결과 | 실제 결과 | 결과 | -|----------|--------|----------|----------|------| -| 거래처 미선택 후 저장 | 거래처: 선택 ▼, 출금유형: 매입대금 | 유효성 에러 | "거래처를 선택해주세요." 토스트 | ✅ | - ---- - -### 10-12. 상세 페이지 수정 및 저장 - -#### 수정 내용 - -| 필드 | 변경 전 | 변경 후 | -|------|---------|---------| -| 거래처 | 선택 ▼ (두산에너빌리티) | 거래처테스트 | -| 출금유형 | 미설정 | 매입대금 | - -#### 저장 결과 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 저장 버튼 동작 | 저장 실행 | 저장 실행 | ✅ | -| 리다이렉트 | /accounting/withdrawals | /accounting/withdrawals | ✅ | -| 거래처 변경 | 거래처테스트 | 거래처테스트 | ✅ | -| 출금유형 변경 | 매입대금 | 매입대금 | ✅ | -| 미설정 건수 | 59건 | 59건 | ✅ | - ---- - -## 발견된 버그 - -### BUG-WITHDRAWAL-20260115-001: 계정과목명 일괄변경 데이터 미반영 - -**Priority**: High -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\accounting\withdrawals\page.tsx` - -#### Issue Summary -목록 페이지에서 체크박스로 항목 선택 후 계정과목명을 변경하고 저장 시, API는 성공 응답(200 OK)을 반환하지만 실제 데이터는 변경되지 않음. - -#### Steps to Reproduce -1. 회계관리 > 출금관리 접속 -2. 테이블에서 행 체크박스 선택 -3. 계정과목명 드롭다운에서 옵션 선택 (예: 매입대금) -4. 저장 버튼 클릭 -5. 확인 다이얼로그에서 확인 클릭 -6. 결과: API 200 OK, 데이터 미변경 - -#### Expected Result -- 선택된 항목의 출금유형이 변경됨 -- 출금유형 미설정 건수가 감소함 - -#### Actual Result -- API 응답은 성공(200 OK) -- 데이터가 변경되지 않음 -- 출금유형 미설정 건수 그대로 유지 - -#### Error Details -``` -Network Request: POST /accounting/withdrawals => 200 OK -Console: No errors -Data: 미설정 → 미설정 (변경 없음) -``` - -#### Related Bugs -- BUG-DEPOSIT-20260115-001: 입금관리 일괄변경 (동일 증상) -- BUG-SALES-20260115-001: 매출관리 일괄변경 (동일 증상) - -#### Suggested Fix (Reference Only) -- 백엔드 API 로직 점검 필요 -- 요청 페이로드와 실제 DB 업데이트 로직 확인 -- 프론트엔드에서 올바른 파라미터 전송 여부 확인 - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - ---- - -## 시나리오 vs 실제 시스템 차이점 - -| 항목 | 시나리오 정의 | 실제 시스템 | 비고 | -|------|--------------|------------|------| -| 테이블 컬럼명 | 받는분 | 수취인명 | 명명 규칙 차이 | -| 계정과목 옵션 수 | 14개 | 16개 | 2개 추가 (4대보험, 공과금) | - ---- - -## 거래처 드롭다운 옵션 (상세 페이지) - -| # | 거래처명 | -|---|----------| -| 1 | 거래처테스트 | -| 2 | 아크더레드 | -| 3 | 코브라브릿지 | -| 4 | 가우스전자 | -| 5 | 아크아크 | - ---- - -## Conclusion - -12개 테스트 케이스 중 11개 통과 (91.7%) - -### 검증 완료 항목 -1. ✅ 회계관리 > 출금관리 메뉴 접근 -2. ✅ 목록 페이지 구조 (통계 카드 4개, 테이블 컬럼 8개) -3. ✅ 계정과목명 드롭다운 옵션 (16개) -4. ❌ 계정과목명 일괄변경 (BUG-WITHDRAWAL-20260115-001) -5. ✅ 상세 페이지 진입 및 정보 표시 -6. ✅ 수정 모드 전환 -7. ✅ 필드 활성화 상태 변경 -8. ✅ 필수값 유효성 검증 -9. ✅ 상세 페이지 데이터 수정 및 저장 -10. ✅ 수정 데이터 목록 반영 - -### 테스트 제외 항목 -- 삭제 기능 -- 검색 기능 -- 필터 기능 (전체/전체/최신순) -- 페이지네이션 -- 날짜 필터 버튼 (당해년도, 전전월 등) -- 취소 버튼 동작 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/db-trigger-audit-system-plan.md b/plans/db-trigger-audit-system-plan.md deleted file mode 100644 index 62da7d9..0000000 --- a/plans/db-trigger-audit-system-plan.md +++ /dev/null @@ -1,1294 +0,0 @@ -# DB 트리거 기반 데이터 변경 추적 시스템 계획 - -> **작성일**: 2026-02-07 -> **목적**: 모든 경로(앱, 직접SQL, AI, phpMyAdmin 등)의 데이터 변경을 DB 레벨에서 추적하고 복구 가능하게 함 -> **기준 문서**: `docs/specs/database-schema.md`, `api/app/Traits/Auditable.php`, `api/config/audit.php` -> **상태**: 🔄 Phase 1-3 완료, Phase 4 핵심 완료 (4.4~4.6 옵션 잔여) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 핵심 (mng 대시보드 + 목록 + 상세 + 이력 + 롤백) | -| **다음 작업** | Phase 4.4 트리거 관리 화면 (옵션) | -| **진행률** | 15/16 (94%) - 핵심 기능 완료, 옵션 3개 잔여 | -| **마지막 업데이트** | 2026-02-07 | - ---- - -## 1. 개요 - -### 1.1 배경 - -SAM 프로젝트에는 이미 Laravel `Auditable` trait 기반 감사 로그가 존재하지만, 이는 **Laravel Eloquent ORM을 통한 변경만 추적**한다. 다음 경로의 변경은 추적 불가: - -- AI(Claude 등)가 직접 실행하는 SQL 쿼리 -- phpMyAdmin, DBeaver 등 DB 클라이언트에서의 직접 수정 -- MySQL CLI에서의 직접 쿼리 -- 다른 애플리케이션/스크립트에서의 DB 접근 -- Laravel `DB::statement()` 등 Eloquent 우회 쿼리 - -**해결책**: MySQL 트리거를 사용하여 DB 엔진 레벨에서 모든 INSERT/UPDATE/DELETE를 포착한다. - -### 1.2 기준 원칙 - -``` -+------------------------------------------------------------------+ -| 계층 분리 (Layered Audit) | -+------------------------------------------------------------------+ -| Layer 1: Laravel Audit (기존 유지) | -| - 비즈니스 액션 (released, cloned, items_replaced 등) | -| - 사용자 컨텍스트 풍부 (IP, UA, 세션 정보) | -| - 실패 시 비즈니스 로직 불영향 (try/catch) | -+------------------------------------------------------------------+ -| Layer 2: MySQL Trigger Audit (신규) | -| - 모든 DML 포착 (직접 쿼리 포함, 누락 불가) | -| - 컬럼 단위 old/new values JSON 저장 | -| - 특정 레코드의 특정 시점으로 복원 가능 | -+------------------------------------------------------------------+ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 트리거 대상 테이블 목록 조정, 제외 컬럼 변경 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션 실행, 트리거 생성/변경, 미들웨어 추가, 새 API 엔드포인트 | **필수** | -| 🔴 금지 | 기존 audit_logs 테이블 구조 변경, 기존 Auditable trait 수정 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - DB 스키마 -- `docs/standards/api-rules.md` - API 규칙 (Audit Logging 섹션) - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: DB 기반 구축 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | trigger_audit_logs 테이블 마이그레이션 (파티셔닝 포함) | ✅ | 15개 파티션, 3개 인덱스 | -| 1.2 | 트리거 대상 테이블 선정 및 확정 | ✅ | 제외 11개 외 전체 적용 | -| 1.3 | 트리거 자동 생성 (PHP 기반, SP 불가) | ✅ | MySQL CREATE TRIGGER는 PREPARE 미지원 → PHP 마이그레이션으로 전환 | -| 1.4 | 대상 테이블별 트리거 생성 | ✅ | 789개 트리거 (263 테이블 × 3) | -| 1.5 | 세션 변수 설정 미들웨어 (Laravel) | ✅ | @sam_actor_id, @sam_session_info | - -### 2.2 Phase 2: 복구 메커니즘 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | TriggerAuditLog 모델 | ✅ | casts, scopes, changed_columns accessor | -| 2.2 | AuditRollbackService 구현 | ✅ | rollback SQL 생성 + 실행 + getRecordStateAt | -| 2.3 | Trigger Audit 조회 API | ✅ | 6개 엔드포인트 (index, show, stats, history, rollback-preview, rollback) | -| 2.4 | Rollback API 엔드포인트 | ✅ | POST /api/v1/trigger-audit-logs/{id}/rollback + confirm 필수 | - -### 2.3 Phase 3: 관리 도구 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 통합 조회 뷰 (v_unified_audit) | ✅ | APP 3,108건 + TRIGGER 2,649건 통합, COLLATE 해결 | -| 3.2 | 파티션 자동 관리 (artisan 커맨드) | ✅ | audit:partitions --add-months --retention-months --drop --dry-run | -| 3.3 | 트리거 재생성 artisan 커맨드 | ✅ | audit:triggers --table --drop-only --dry-run | - -### 2.4 Phase 4: 관리자 대시보드 (mng) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | 변경 이력 목록 화면 (index) | ✅ | 통계카드+필터+목록+파티션현황+트리거수, 페이지네이션 | -| 4.2 | 레코드 상세 변경 이력 (show + history) | ✅ | diff 뷰(old/new 비교, 변경 컬럼 하이라이트) + 레코드 타임라인 | -| 4.3 | 복구 기능 UI (rollback-preview) | ✅ | SQL 미리보기, 확인 체크박스+confirm, @disable_audit_trigger | -| 4.4 | 트리거 관리 화면 | ⏭️ | 옵션 - artisan audit:triggers 커맨드로 CLI 관리 가능 | -| 4.5 | 대시보드 통계 | ✅ | index에 통합 (전체/오늘/DML별 통계, 상위 테이블, 파티션, 저장소) | -| 4.6 | 보관 정책 설정 | ⏭️ | 옵션 - artisan audit:partitions 커맨드로 CLI 관리 가능 | - ---- - -## 3. 작업 절차 - -### 3.1 아키텍처 다이어그램 - -``` -[사용자/AI/phpMyAdmin/스크립트] - │ - ▼ - ┌─────────┐ - │ MySQL │ - │ Engine │ - └────┬────┘ - │ DML (INSERT/UPDATE/DELETE) - ▼ - ┌─────────────────────────┐ - │ 대상 테이블 │ - │ (제외 목록 외 전체 │ - │ 약 207개) │ - └────┬────────────────────┘ - │ AFTER 트리거 발동 - ▼ - ┌─────────────────────────┐ - │ trigger_audit_logs │ - │ (파티셔닝, 13개월 보관) │ - │ - table_name │ - │ - row_id │ - │ - dml_type │ - │ - old_values (JSON) │ - │ - new_values (JSON) │ - │ - tenant_id │ - │ - actor_id │ ← @sam_actor_id 세션변수 - │ - session_info │ ← @sam_session_info 세션변수 - │ - db_user │ ← CURRENT_USER() - │ - created_at │ - └─────────────────────────┘ - │ - ▼ - ┌─────────────────────────┐ - │ AuditRollbackService │ - │ (Laravel) │ - │ - 이력 조회 │ - │ - Rollback SQL 생성 │ - │ - 특정 시점 복원 │ - └─────────────────────────┘ -``` - -### 3.2 트리거 대상 테이블 - -#### 적용 방침: 전체 적용 (제외 목록 방식) - -로컬 개발 환경에서 1인 사용이므로, **제외 대상을 제외한 모든 테이블에 트리거를 적용**한다. -운영 환경 전환 시 필요에 따라 대상을 축소할 수 있다. - -SP(`sp_create_audit_triggers`)가 `INFORMATION_SCHEMA.TABLES`에서 samdb의 전체 테이블을 읽고, -제외 목록에 없는 모든 테이블에 자동으로 트리거를 생성한다. - -#### 제외 대상 (트리거 미적용) - -| 테이블 패턴 | 사유 | -|-------------|------| -| `audit_logs` | 감사 로그 자체 (순환 방지) | -| `trigger_audit_logs` | 트리거 감사 자체 (순환 방지) | -| `personal_access_tokens` | Sanctum 토큰 (대량 생성/삭제, 보안 데이터) | -| `sessions` | 세션 데이터 (빈번한 갱신) | -| `cache`, `cache_locks` | 캐시 데이터 | -| `jobs`, `job_batches` | 큐 작업 | -| `failed_jobs` | 실패 큐 | -| `migrations` | 마이그레이션 기록 | -| `password_reset_tokens` | 비밀번호 리셋 토큰 | -| `telescope_*` | 디버그 도구 (있는 경우) | - -> **예상**: samdb 약 219개 테이블 - 제외 약 12개 = **약 207개 테이블 × 3 트리거 = 약 621개 트리거** -> -> SP가 `INFORMATION_SCHEMA`에서 동적으로 테이블을 읽으므로, 테이블이 추가/삭제되면 -> `artisan audit:regenerate-triggers` 명령으로 트리거를 재생성하면 된다. - -### 3.3 trigger_audit_logs 테이블 구조 - -```sql -CREATE TABLE trigger_audit_logs ( - id BIGINT UNSIGNED AUTO_INCREMENT, - table_name VARCHAR(64) NOT NULL COMMENT '변경된 테이블명', - row_id VARCHAR(64) NOT NULL COMMENT '변경된 레코드 PK (문자열 지원)', - dml_type ENUM('INSERT','UPDATE','DELETE') NOT NULL COMMENT 'DML 유형', - old_values JSON DEFAULT NULL COMMENT '변경 전 값 (INSERT시 NULL)', - new_values JSON DEFAULT NULL COMMENT '변경 후 값 (DELETE시 NULL)', - changed_columns JSON DEFAULT NULL COMMENT 'UPDATE시 변경된 컬럼 목록', - tenant_id BIGINT UNSIGNED DEFAULT NULL COMMENT '테넌트 ID', - actor_id BIGINT UNSIGNED DEFAULT NULL COMMENT '사용자 ID (세션변수)', - session_info VARCHAR(500) DEFAULT NULL COMMENT '세션 정보 JSON (IP, UA 등)', - db_user VARCHAR(100) DEFAULT NULL COMMENT 'CURRENT_USER()', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '변경 시각', - PRIMARY KEY (id, created_at) -) ENGINE=InnoDB - DEFAULT CHARSET=utf8mb4 - COLLATE=utf8mb4_unicode_ci - COMMENT='DB 트리거 기반 데이터 변경 추적' - PARTITION BY RANGE (UNIX_TIMESTAMP(created_at)) ( - PARTITION p202601 VALUES LESS THAN (UNIX_TIMESTAMP('2026-02-01')), - PARTITION p202602 VALUES LESS THAN (UNIX_TIMESTAMP('2026-03-01')), - PARTITION p202603 VALUES LESS THAN (UNIX_TIMESTAMP('2026-04-01')), - PARTITION p202604 VALUES LESS THAN (UNIX_TIMESTAMP('2026-05-01')), - PARTITION p202605 VALUES LESS THAN (UNIX_TIMESTAMP('2026-06-01')), - PARTITION p202606 VALUES LESS THAN (UNIX_TIMESTAMP('2026-07-01')), - PARTITION p202607 VALUES LESS THAN (UNIX_TIMESTAMP('2026-08-01')), - PARTITION p202608 VALUES LESS THAN (UNIX_TIMESTAMP('2026-09-01')), - PARTITION p202609 VALUES LESS THAN (UNIX_TIMESTAMP('2026-10-01')), - PARTITION p202610 VALUES LESS THAN (UNIX_TIMESTAMP('2026-11-01')), - PARTITION p202611 VALUES LESS THAN (UNIX_TIMESTAMP('2026-12-01')), - PARTITION p202612 VALUES LESS THAN (UNIX_TIMESTAMP('2027-01-01')), - PARTITION p202701 VALUES LESS THAN (UNIX_TIMESTAMP('2027-02-01')), - PARTITION p202702 VALUES LESS THAN (UNIX_TIMESTAMP('2027-03-01')), - PARTITION p_future VALUES LESS THAN MAXVALUE - ); - --- 조회 성능 인덱스 -CREATE INDEX ix_trig_table_row_created - ON trigger_audit_logs (table_name, row_id, created_at); - -CREATE INDEX ix_trig_tenant_created - ON trigger_audit_logs (tenant_id, created_at); -``` - -### 3.4 트리거 자동 생성 Stored Procedure - -```sql --- 특정 테이블에 대해 AFTER INSERT/UPDATE/DELETE 트리거 3개를 자동 생성 --- INFORMATION_SCHEMA.COLUMNS에서 컬럼 목록을 읽어 JSON_OBJECT 구문 자동 조립 - -CALL sp_create_audit_triggers('products'); --- → trg_products_ai (AFTER INSERT) --- → trg_products_au (AFTER UPDATE) --- → trg_products_ad (AFTER DELETE) -``` - -**SP 핵심 로직:** -1. `INFORMATION_SCHEMA.COLUMNS`에서 대상 테이블의 컬럼 목록 조회 -2. 제외 컬럼 필터링 (`created_at`, `updated_at`, `deleted_at`, `remember_token` 등) -3. `JSON_OBJECT('col1', NEW.col1, 'col2', NEW.col2, ...)` 구문 자동 조립 -4. UPDATE 트리거: 컬럼별 `OLD.col <> NEW.col` 비교 → changed_columns 배열 생성 -5. 비활성화 플래그 체크 (`@disable_audit_trigger`) -6. `PREPARE + EXECUTE`로 트리거 DDL 실행 - -### 3.5 세션 변수 미들웨어 - -```php -// app/Http/Middleware/SetAuditSessionVariables.php - -class SetAuditSessionVariables -{ - public function handle($request, $next) - { - if (auth()->check()) { - DB::statement("SET @sam_actor_id = ?", [auth()->id()]); - DB::statement("SET @sam_session_info = ?", [ - json_encode([ - 'ip' => $request->ip(), - 'ua' => substr($request->userAgent(), 0, 255), - 'route' => $request->route()?->getName(), - ]) - ]); - } - - return $next($request); - } -} -``` - -### 3.6 복구 서비스 - -```php -// app/Services/Audit/AuditRollbackService.php - -class AuditRollbackService -{ - // 특정 audit 레코드에 대한 역방향 SQL 생성 - public function generateRollbackSQL(int $auditId): string; - - // 실제 복구 실행 (트랜잭션 내에서) - public function executeRollback(int $auditId): bool; - - // 특정 레코드의 특정 시점 상태 조회 - public function getRecordStateAt(string $table, string $rowId, Carbon $at): ?array; - - // 특정 레코드의 변경 이력 조회 - public function getRecordHistory(string $table, string $rowId): Collection; -} -``` - -**복구 로직:** - -| 원본 DML | 복구 SQL | -|----------|---------| -| INSERT | `DELETE FROM {table} WHERE id = {row_id}` | -| UPDATE | `UPDATE {table} SET {old_values 각 컬럼} WHERE id = {row_id}` | -| DELETE | `INSERT INTO {table} ({old_values 컬럼}) VALUES ({old_values 값})` | - ---- - -## 4. 상세 작업 내용 - -> 각 Phase 진행 후 이 섹션에 상세 내용 추가 - -### 4.1 Phase 1: DB 기반 구축 -- **상태**: ⏳ 대기 -- **예상 파일**: - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_trigger_audit_logs_table.php` - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_audit_trigger_stored_procedures.php` - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_audit_triggers_for_tables.php` - - `api/app/Http/Middleware/SetAuditSessionVariables.php` - -### 4.2 Phase 2: 복구 메커니즘 -- **상태**: ⏳ 대기 -- **예상 파일**: - - `api/app/Models/Audit/TriggerAuditLog.php` - - `api/app/Services/Audit/AuditRollbackService.php` - - `api/app/Http/Controllers/Api/V1/Audit/TriggerAuditLogController.php` - - `api/app/Http/Requests/Audit/TriggerAuditLogIndexRequest.php` - - `api/app/Http/Requests/Audit/TriggerAuditRollbackRequest.php` - - `api/app/Swagger/v1/TriggerAuditLogApi.php` - -### 4.3 Phase 3: 관리 도구 -- **상태**: ⏳ 대기 -- **예상 파일**: - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_unified_audit_view.php` - - `api/app/Console/Commands/ManageAuditPartitions.php` - - `api/app/Console/Commands/RegenerateAuditTriggers.php` - -### 4.4 Phase 4: 관리자 대시보드 (mng) -- **상태**: ⏳ 대기 -- **예상 파일**: - - `mng/app/Http/Controllers/Admin/TriggerAuditController.php` - - `mng/resources/views/admin/trigger-audit/index.blade.php` (이력 목록) - - `mng/resources/views/admin/trigger-audit/show.blade.php` (상세 diff 뷰) - - `mng/resources/views/admin/trigger-audit/dashboard.blade.php` (대시보드 통계) - - `mng/resources/views/admin/trigger-audit/triggers.blade.php` (트리거 관리) - - `mng/resources/views/admin/trigger-audit/settings.blade.php` (보관 정책) - - `mng/app/Services/TriggerAuditDashboardService.php` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 트리거 대상 | 제외 목록 외 전체 (약 207개) 적용 | database | ✅ 확정 | -| 2 | 성능 영향 | 로컬 1인 사용, 제한 없음 | database | ✅ 확정 | -| 3 | Phase 4 범위 | 풀 관리 대시보드 (조회/복구/트리거관리/통계/정책) | mng | ✅ 확정 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-07 | 계획 | 문서 초안 작성 | - | - | -| 2026-02-07 | 수정 | 피드백 반영: 전체 테이블 적용, Phase 4 대시보드 추가 | - | ✅ | -| 2026-02-07 | Phase 1 | DB 기반 구축 완료. SP→PHP 전환, 789 트리거 생성, my.cnf 설정 추가 | api/database/migrations/2026_02_07_*, api/app/Http/Middleware/SetAuditSessionVariables.php, docker/mysql/my.cnf | ✅ | -| 2026-02-07 | Phase 2 | 복구 메커니즘 API 완료. 모델/서비스/컨트롤러/라우트 6개 엔드포인트 | TriggerAuditLog.php, TriggerAuditLogService.php, AuditRollbackService.php, TriggerAuditLogController.php, audit.php(route) | ✅ | -| 2026-02-07 | Phase 3 | 관리 도구 완료. 통합 뷰(collation 해결), 파티션 관리, 트리거 재생성 커맨드 | v_unified_audit VIEW, ManageAuditPartitions.php, RegenerateAuditTriggers.php | ✅ | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **API 규칙**: `docs/standards/api-rules.md` (Audit Logging 섹션) -- **기존 Auditable**: `api/app/Traits/Auditable.php` -- **기존 audit 설정**: `api/config/audit.php` -- **기존 audit 마이그레이션**: `api/database/migrations/2025_09_11_000100_create_audit_logs_table.php` - -### 외부 참고자료 - -- [MySQL 8.0 Trigger Syntax](https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html) -- [MySQL 8.0 Partitioning](https://dev.mysql.com/doc/refman/8.0/en/partitioning.html) -- [Percona - MySQL Trigger Performance](https://www.percona.com/blog/why-mysql-stored-procedures-functions-triggers-bad-performance/) - ---- - -## 8. 리스크 및 대응 방안 - -| 리스크 | 영향 | 대응 | -|--------|------|------|| 트리거 성능 오버헤드 (INSERT 약 40-50%) | 쓰기 성능 저하 | 로컬 1인 사용 환경이므로 무관. 운영 전환 시 대상 축소 가능. Bulk 작업 시 `@disable_audit_trigger=1` | -| 트리거 실패 시 원본 DML도 롤백 | 비즈니스 중단 | 트리거 로직 최소화, audit 테이블 구조 안정적 유지 | -| 스키마 변경 시 트리거 유지보수 | 누락 위험 | SP 기반 자동 생성 → `artisan audit:regenerate-triggers` | -| 저장 용량 증가 | 디스크 사용량 | 월별 파티셔닝 + 13개월 자동 삭제 | -| 세션 변수 미설정 (CLI, Queue) | actor_id NULL | NULL 허용, db_user로 보완 추적 | - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| Laravel에서 Product 생성 | audit_logs + trigger_audit_logs 모두 기록 | | ⏳ | -| 직접 SQL로 Product UPDATE | trigger_audit_logs에만 기록 | | ⏳ | -| phpMyAdmin에서 DELETE | trigger_audit_logs에 기록 (actor_id=NULL, db_user 기록) | | ⏳ | -| Bulk INSERT 10,000건 (트리거 활성) | trigger_audit_logs에 10,000건 기록, 성능 측정 | | ⏳ | -| Bulk INSERT 10,000건 (트리거 비활성) | trigger_audit_logs 기록 없음, 기본 성능 | | ⏳ | -| UPDATE 후 rollback API 호출 | old_values로 복원됨 | | ⏳ | -| DELETE 후 rollback API 호출 | 삭제된 레코드 복원됨 | | ⏳ | -| INSERT 후 rollback API 호출 | 삽입된 레코드 삭제됨 | | ⏳ | -| 13개월 이전 파티션 삭제 | 해당 파티션 DROP, 데이터 제거 | | ⏳ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 직접 SQL 변경이 trigger_audit_logs에 기록됨 | ⏳ | | -| old_values/new_values JSON이 정확히 저장됨 | ⏳ | | -| 특정 레코드의 특정 시점 복원이 가능함 | ⏳ | | -| 파티셔닝이 정상 작동함 | ⏳ | | -| 기존 Laravel audit 시스템에 영향 없음 | ⏳ | | -| 트리거 비활성화 플래그가 정상 동작함 | ⏳ | | -| mng 대시보드에서 이력 조회/필터링 가능 | ⏳ | | -| mng에서 특정 변경 복구(rollback) 가능 | ⏳ | | -| mng에서 테이블별 트리거 ON/OFF 가능 | ⏳ | | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경에 명시 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 2.1~2.4 Phase별 작업 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서, 기존 시스템 참조 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 7. 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.3~3.6 상세 구현 명세 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/조건 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 1 → 1.1 테이블 생성 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 4.1~4.4 예상 파일 목록 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 테스트 케이스, 9.2 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -## 부록 A: 환경 정보 - -### A.1 프로젝트 구조 -``` -SAM/ ← 프로젝트 루트 -├── api/ ← Laravel 12 REST API (독립 git) -│ ├── app/ -│ │ ├── Http/ -│ │ │ ├── Controllers/Api/V1/ ← API 컨트롤러 -│ │ │ ├── Middleware/ ← 미들웨어 -│ │ │ └── Requests/ ← FormRequest -│ │ ├── Models/Audit/ ← 감사 모델 (AuditLog.php) -│ │ ├── Services/Audit/ ← 감사 서비스 (AuditLogger, AuditLogService) -│ │ ├── Traits/ ← Auditable.php, BelongsToTenant.php -│ │ ├── Console/Commands/ ← Artisan 커맨드 -│ │ └── Swagger/v1/ ← Swagger 문서 -│ ├── config/audit.php ← 감사 설정 -│ ├── database/migrations/ ← 마이그레이션 -│ ├── routes/ -│ │ ├── api.php ← 메인 라우트 (v1 prefix → 도메인별 분리) -│ │ └── api/v1/ ← 도메인별 라우트 파일 -│ └── bootstrap/app.php ← Laravel 12 미들웨어 등록 -├── mng/ ← Laravel 12 관리자 패널 (독립 git) -│ ├── app/Http/Controllers/ ← Blade 컨트롤러 -│ ├── resources/views/ ← Blade 뷰 (Tailwind + Alpine.js + HTMX) -│ │ └── layouts/app.blade.php ← 메인 레이아웃 -│ └── routes/web.php ← 웹 라우트 (auth 미들웨어 그룹) -├── react/ ← Next.js 15 프론트엔드 -└── docs/plans/ ← 이 문서 -``` - -### A.2 DB 접속 정보 -``` -엔진: MySQL 8.0 -Docker 컨테이너: sam-mysql-1 -데이터베이스: samdb (주), sam_stat (통계) -호스트: 127.0.0.1 (로컬) / sam-mysql-1 (Docker 내부) -포트: 3306 -사용자: samuser / sampass (일반), root / root (관리자) -문자셋: utf8mb4 / utf8mb4_unicode_ci -타임존: Asia/Seoul -``` - -### A.3 주요 명령어 -```bash -# Docker -cd /Users/kent/Works/@KD_SAM/SAM -docker compose up -d mysql - -# API 마이그레이션 -cd api && php artisan migrate -cd api && php artisan migrate:status - -# MySQL 직접 접속 -docker exec -it sam-mysql-1 mysql -u root -proot samdb - -# MNG Vite 빌드 -cd mng && npm run dev -``` - ---- - -## 부록 B: 기존 감사 시스템 코드 (수정 금지, 참조용) - -### B.1 Auditable Trait (`api/app/Traits/Auditable.php`) -```php -isFillable('created_by') && ! $model->created_by) { - $model->created_by = $actorId; - } - if ($model->isFillable('updated_by') && ! $model->updated_by) { - $model->updated_by = $actorId; - } - } - }); - - static::updating(function ($model) { - $actorId = static::resolveActorId(); - if ($actorId && $model->isFillable('updated_by')) { - $model->updated_by = $actorId; - } - }); - - static::deleting(function ($model) { - $actorId = static::resolveActorId(); - if ($actorId && $model->isFillable('deleted_by')) { - $model->deleted_by = $actorId; - $model->saveQuietly(); - } - }); - - static::created(function ($model) { - $model->logAuditEvent('created', null, $model->toAuditSnapshot()); - }); - - static::updated(function ($model) { - $dirty = $model->getChanges(); - $excluded = $model->getAuditExcludedFields(); - $changed = array_diff_key($dirty, array_flip($excluded)); - if (empty($changed)) return; - - $before = []; - $after = []; - foreach ($changed as $key => $value) { - $before[$key] = $model->getOriginal($key); - $after[$key] = $value; - } - $model->logAuditEvent('updated', $before, $after); - }); - - static::deleted(function ($model) { - $model->logAuditEvent('deleted', $model->toAuditSnapshot(), null); - }); - } - - public function getAuditExcludedFields(): array - { - $defaults = ['created_at','updated_at','deleted_at','created_by','updated_by','deleted_by']; - $custom = property_exists($this, 'auditExclude') ? $this->auditExclude : []; - return array_merge($defaults, $custom); - } - - public function getAuditTargetType(): string - { - return Str::snake(class_basename(static::class)); - } - - protected function toAuditSnapshot(): array - { - return array_diff_key($this->attributesToArray(), array_flip($this->getAuditExcludedFields())); - } - - protected function logAuditEvent(string $action, ?array $before, ?array $after): void - { - try { - $tenantId = $this->tenant_id ?? null; - if (! $tenantId) return; - $request = request(); - AuditLog::create([ - 'tenant_id' => $tenantId, - 'target_type' => $this->getAuditTargetType(), - 'target_id' => $this->getKey(), - 'action' => $action, - 'before' => $before, - 'after' => $after, - 'actor_id' => static::resolveActorId(), - 'ip' => $request?->ip(), - 'ua' => $request?->userAgent(), - 'created_at' => now(), - ]); - } catch (\Throwable $e) { - // 감사 로그 실패는 업무 흐름을 방해하지 않음 - } - } - - protected static function resolveActorId(): ?int - { - return auth()->id(); - } -} -``` - -### B.2 AuditLog 모델 (`api/app/Models/Audit/AuditLog.php`) -```php - 'array', - 'after' => 'array', - 'created_at' => 'datetime', - ]; -} -``` - -### B.3 AuditLogService (`api/app/Services/Audit/AuditLogService.php`) -```php -tenantId(); - $q = AuditLog::query()->where('tenant_id', $tenantId); - - if (! empty($filters['target_type'])) $q->where('target_type', $filters['target_type']); - if (! empty($filters['target_id'])) $q->where('target_id', (int) $filters['target_id']); - if (! empty($filters['action'])) $q->where('action', $filters['action']); - if (! empty($filters['actor_id'])) $q->where('actor_id', (int) $filters['actor_id']); - if (! empty($filters['from'])) $q->where('created_at', '>=', $filters['from']); - if (! empty($filters['to'])) $q->where('created_at', '<=', $filters['to']); - - $sort = $filters['sort'] ?? 'created_at'; - $order = $filters['order'] ?? 'desc'; - $size = (int) ($filters['size'] ?? 20); - - return $q->orderBy($sort, $order)->paginate($size); - } -} -``` - -### B.4 Audit Config (`api/config/audit.php`) -```php - env('AUDIT_RETENTION_DAYS', 395), // 13개월 - 'log_reads' => env('AUDIT_LOG_READS', false), -]; -``` - -### B.5 API 컨트롤러 패턴 (`api/app/Http/Controllers/Api/V1/Design/AuditLogController.php`) -```php -service->paginate($request->validated()); - }, __('message.fetched')); - } -} -``` - -### B.6 API Kernel (`api/app/Http/Kernel.php`) -```php - [], - 'api' => [], - ]; - protected $routeMiddleware = []; -} -``` - -> **참고**: Laravel 12에서 미들웨어 추가 시 `bootstrap/app.php`의 `->withMiddleware()` 또는 -> `Kernel.php`의 `$middleware` / `$middlewareGroups`에 등록한다. - -### B.7 API 라우트 패턴 (`api/routes/api.php`) -```php -// 도메인별 분리 구조 -Route::prefix('v1')->group(function () { - require __DIR__.'/api/v1/auth.php'; - require __DIR__.'/api/v1/design.php'; - // ... 기타 도메인 -}); - -// design.php 내 감사 로그 라우트 예시 -Route::prefix('design')->group(function () { - Route::prefix('audit-logs')->group(function () { - Route::get('', [DesignAuditLogController::class, 'index']); - Route::get('/{id}', [DesignAuditLogController::class, 'show'])->whereNumber('id'); - }); -}); -``` - -### B.8 Artisan 커맨드 패턴 (예: `TenantsBootstrap.php`) -```php - - - - - - @yield('title', 'Dashboard') - {{ config('app.name') }} - @vite(['resources/css/app.css', 'resources/js/app.js']) - - - - - - @include('components.sidebar.main') -
- @yield('content') -
- @stack('scripts') - - -``` - -### C.3 MNG 컨트롤러 패턴 (기존 `AuditLogController.php` 요약) -```php -orderByDesc('created_at'); - - // 필터링 (target_type, action, tenant_id, from, to, search) - if ($request->filled('target_type')) $query->where('target_type', $request->target_type); - if ($request->filled('action')) $query->where('action', $request->action); - if ($request->filled('from')) $query->where('created_at', '>=', $request->from.' 00:00:00'); - if ($request->filled('to')) $query->where('created_at', '<=', $request->to.' 23:59:59'); - - // 통계 - $stats = [...]; - - // 페이지네이션 - $logs = $query->paginate(50)->withQueryString(); - - return view('audit-logs.index', compact('logs', 'stats')); - } - - public function show(int $id): View - { - $log = AuditLog::findOrFail($id); - return view('audit-logs.show', compact('log')); - } -} -``` - -### C.4 MNG 뷰 패턴 (데이터 목록 화면) -```blade -@extends('layouts.app') -@section('title', '페이지 제목') -@section('content') - -{{-- 1. 헤더 --}} -
-

페이지 제목

-
- -{{-- 2. 통계 카드 --}} -
-
-
전체 기록
-
{{ number_format($stats['total']) }}
-
-
- -{{-- 3. 필터 폼 --}} -
-
- - - -
-
- -{{-- 4. 데이터 테이블 (HTMX 방식 또는 일반 방식) --}} -
- - - - - - - - @foreach($items as $item) - - - - @endforeach - -
컬럼
{{ $item->field }}
-
{{ $items->links() }}
-
- -@endsection -``` - -### C.5 MNG 라우트 패턴 (`mng/routes/web.php`) -```php -// 인증 필수 라우트 그룹 -Route::middleware(['auth', 'hq.member', 'password.changed'])->group(function () { - - // 감사 로그 (기존) - Route::prefix('audit-logs')->group(function () { - Route::get('/', [AuditLogController::class, 'index'])->name('audit-logs.index'); - Route::get('/{id}', [AuditLogController::class, 'show'])->name('audit-logs.show'); - }); - - // 새 트리거 감사는 여기에 추가: - // Route::prefix('trigger-audit')->name('trigger-audit.')->group(function () { ... }); -}); -``` - -### C.6 MNG 미들웨어 목록 -``` -mng/app/Http/Middleware/ -├── EnsureHQMember.php ← 본사 소속 확인 -├── EnsurePasswordChanged.php ← 비밀번호 변경 확인 -├── EnsureSuperAdmin.php ← 슈퍼관리자 확인 -└── AutoLoginViaRemember.php ← Remember Token 자동 재인증 -``` - ---- - -## 부록 D: SP 구현 상세 (Phase 1.3 참조) - -### D.1 sp_create_audit_triggers 전체 구현 방향 - -```sql -DELIMITER // - -DROP PROCEDURE IF EXISTS sp_create_audit_triggers // - -CREATE PROCEDURE sp_create_audit_triggers( - IN p_table_name VARCHAR(64), - IN p_db_name VARCHAR(64) -) -BEGIN - DECLARE v_col_list TEXT DEFAULT ''; - DECLARE v_json_new TEXT DEFAULT ''; - DECLARE v_json_old TEXT DEFAULT ''; - DECLARE v_change_check TEXT DEFAULT ''; - DECLARE v_changed_cols TEXT DEFAULT ''; - DECLARE v_tenant_col VARCHAR(64) DEFAULT NULL; - DECLARE v_pk_col VARCHAR(64) DEFAULT 'id'; - DECLARE v_done INT DEFAULT 0; - DECLARE v_col_name VARCHAR(64); - DECLARE v_sql TEXT; - - -- 제외 컬럼 - DECLARE v_exclude_cols TEXT DEFAULT 'created_at,updated_at,deleted_at,remember_token'; - - -- 커서: 대상 컬럼 목록 - DECLARE col_cursor CURSOR FOR - SELECT COLUMN_NAME - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_NAME = p_table_name - AND FIND_IN_SET(COLUMN_NAME, v_exclude_cols) = 0 - ORDER BY ORDINAL_POSITION; - - DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1; - - -- tenant_id 컬럼 존재 확인 - SELECT COLUMN_NAME INTO v_tenant_col - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_NAME = p_table_name - AND COLUMN_NAME = 'tenant_id' - LIMIT 1; - - -- PK 컬럼 확인 - SELECT COLUMN_NAME INTO v_pk_col - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_NAME = p_table_name - AND COLUMN_KEY = 'PRI' - LIMIT 1; - - -- 컬럼별 JSON_OBJECT 구문 조립 - OPEN col_cursor; - col_loop: LOOP - FETCH col_cursor INTO v_col_name; - IF v_done THEN LEAVE col_loop; END IF; - - -- JSON 조립 - IF v_json_new != '' THEN - SET v_json_new = CONCAT(v_json_new, ','); - SET v_json_old = CONCAT(v_json_old, ','); - END IF; - SET v_json_new = CONCAT(v_json_new, '''', v_col_name, ''', NEW.`', v_col_name, '`'); - SET v_json_old = CONCAT(v_json_old, '''', v_col_name, ''', OLD.`', v_col_name, '`'); - - -- UPDATE 변경 감지 조립 (NULL-safe 비교) - IF v_change_check != '' THEN - SET v_change_check = CONCAT(v_change_check, ' OR '); - SET v_changed_cols = CONCAT(v_changed_cols, ','); - END IF; - SET v_change_check = CONCAT(v_change_check, - 'NOT(OLD.`', v_col_name, '` <=> NEW.`', v_col_name, '`)'); - SET v_changed_cols = CONCAT(v_changed_cols, - 'IF(NOT(OLD.`', v_col_name, '` <=> NEW.`', v_col_name, '`),''', v_col_name, ''',NULL)'); - END LOOP; - CLOSE col_cursor; - - -- tenant_id 참조 - SET @tenant_expr = IF(v_tenant_col IS NOT NULL, - CONCAT('NEW.`', v_tenant_col, '`'), 'NULL'); - SET @tenant_expr_old = IF(v_tenant_col IS NOT NULL, - CONCAT('OLD.`', v_tenant_col, '`'), 'NULL'); - - -- 1. 기존 트리거 삭제 - SET @drop1 = CONCAT('DROP TRIGGER IF EXISTS trg_', p_table_name, '_ai'); - SET @drop2 = CONCAT('DROP TRIGGER IF EXISTS trg_', p_table_name, '_au'); - SET @drop3 = CONCAT('DROP TRIGGER IF EXISTS trg_', p_table_name, '_ad'); - PREPARE s FROM @drop1; EXECUTE s; DEALLOCATE PREPARE s; - PREPARE s FROM @drop2; EXECUTE s; DEALLOCATE PREPARE s; - PREPARE s FROM @drop3; EXECUTE s; DEALLOCATE PREPARE s; - - -- 2. AFTER INSERT 트리거 - SET v_sql = CONCAT( - 'CREATE TRIGGER trg_', p_table_name, '_ai AFTER INSERT ON `', p_table_name, '` ', - 'FOR EACH ROW BEGIN ', - 'IF @disable_audit_trigger IS NULL OR @disable_audit_trigger != 1 THEN ', - 'INSERT INTO trigger_audit_logs(table_name,row_id,dml_type,old_values,new_values,tenant_id,actor_id,session_info,db_user) ', - 'VALUES(''', p_table_name, ''',NEW.`', v_pk_col, '`,''INSERT'',NULL,', - 'JSON_OBJECT(', v_json_new, '),', - @tenant_expr, ',@sam_actor_id,@sam_session_info,CURRENT_USER()); ', - 'END IF; END' - ); - SET @s = v_sql; - PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; - - -- 3. AFTER UPDATE 트리거 (변경 있을 때만) - SET v_sql = CONCAT( - 'CREATE TRIGGER trg_', p_table_name, '_au AFTER UPDATE ON `', p_table_name, '` ', - 'FOR EACH ROW BEGIN ', - 'IF @disable_audit_trigger IS NULL OR @disable_audit_trigger != 1 THEN ', - 'IF ', v_change_check, ' THEN ', - 'INSERT INTO trigger_audit_logs(table_name,row_id,dml_type,old_values,new_values,changed_columns,tenant_id,actor_id,session_info,db_user) ', - 'VALUES(''', p_table_name, ''',NEW.`', v_pk_col, '`,''UPDATE'',', - 'JSON_OBJECT(', v_json_old, '),', - 'JSON_OBJECT(', v_json_new, '),', - 'JSON_REMOVE(JSON_ARRAY(', v_changed_cols, '),', - -- NULL 값 제거 (변경 안 된 컬럼) - '''$[0]''),', -- 간소화: 실제 구현 시 NULL 필터링 로직 보강 필요 - @tenant_expr, ',@sam_actor_id,@sam_session_info,CURRENT_USER()); ', - 'END IF; END IF; END' - ); - SET @s = v_sql; - PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; - - -- 4. AFTER DELETE 트리거 - SET v_sql = CONCAT( - 'CREATE TRIGGER trg_', p_table_name, '_ad AFTER DELETE ON `', p_table_name, '` ', - 'FOR EACH ROW BEGIN ', - 'IF @disable_audit_trigger IS NULL OR @disable_audit_trigger != 1 THEN ', - 'INSERT INTO trigger_audit_logs(table_name,row_id,dml_type,old_values,new_values,tenant_id,actor_id,session_info,db_user) ', - 'VALUES(''', p_table_name, ''',OLD.`', v_pk_col, '`,''DELETE'',', - 'JSON_OBJECT(', v_json_old, '),NULL,', - @tenant_expr_old, ',@sam_actor_id,@sam_session_info,CURRENT_USER()); ', - 'END IF; END' - ); - SET @s = v_sql; - PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -END // - -DELIMITER ; -``` - -> **주의**: 위 코드는 구현 방향을 보여주는 참조 코드이다. -> 실제 구현 시 changed_columns의 NULL 필터링, 복합 PK 처리, 에러 핸들링 등을 보강해야 한다. - -### D.2 전체 테이블 일괄 트리거 생성 프로시저 - -```sql -DELIMITER // - -CREATE PROCEDURE sp_create_all_audit_triggers(IN p_db_name VARCHAR(64)) -BEGIN - DECLARE v_tbl VARCHAR(64); - DECLARE v_done INT DEFAULT 0; - DECLARE v_count INT DEFAULT 0; - - -- 제외 테이블 목록 - DECLARE v_exclude TEXT DEFAULT - 'audit_logs,trigger_audit_logs,personal_access_tokens,sessions,' - 'cache,cache_locks,jobs,job_batches,failed_jobs,migrations,' - 'password_reset_tokens'; - - DECLARE tbl_cursor CURSOR FOR - SELECT TABLE_NAME - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_TYPE = 'BASE TABLE' - AND TABLE_NAME NOT LIKE 'telescope_%' - AND FIND_IN_SET(TABLE_NAME, v_exclude) = 0 - ORDER BY TABLE_NAME; - - DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1; - - OPEN tbl_cursor; - tbl_loop: LOOP - FETCH tbl_cursor INTO v_tbl; - IF v_done THEN LEAVE tbl_loop; END IF; - - CALL sp_create_audit_triggers(v_tbl, p_db_name); - SET v_count = v_count + 1; - END LOOP; - CLOSE tbl_cursor; - - SELECT CONCAT('Created triggers for ', v_count, ' tables') AS result; -END // - -DELIMITER ; - --- 실행: --- CALL sp_create_all_audit_triggers('samdb'); -``` - ---- - -## 부록 E: 복구 서비스 구현 상세 (Phase 2.2 참조) - -```php -dml_type) { - 'INSERT' => $this->buildDeleteSQL($log), - 'UPDATE' => $this->buildRevertUpdateSQL($log), - 'DELETE' => $this->buildReinsertSQL($log), - }; - } - - /** - * 복구 실행 (트랜잭션) - */ - public function executeRollback(int $auditId): bool - { - $log = TriggerAuditLog::findOrFail($auditId); - - // 트리거 감사 비활성화 (복구 작업 자체는 기록 안 함) - DB::statement('SET @disable_audit_trigger = 1'); - - try { - DB::transaction(function () use ($log) { - $sql = $this->generateRollbackSQL($log->id); - DB::statement($sql); - }); - return true; - } finally { - DB::statement('SET @disable_audit_trigger = NULL'); - } - } - - /** - * 특정 레코드의 특정 시점 상태 조회 - */ - public function getRecordStateAt(string $table, string $rowId, Carbon $at): ?array - { - // 해당 시점 이전의 가장 마지막 상태를 추적 - $log = TriggerAuditLog::where('table_name', $table) - ->where('row_id', $rowId) - ->where('created_at', '<=', $at) - ->orderByDesc('created_at') - ->first(); - - if (! $log) return null; - - return match ($log->dml_type) { - 'INSERT', 'UPDATE' => $log->new_values, - 'DELETE' => null, // 해당 시점에 삭제된 상태 - }; - } - - /** - * 특정 레코드의 변경 이력 - */ - public function getRecordHistory(string $table, string $rowId): Collection - { - return TriggerAuditLog::where('table_name', $table) - ->where('row_id', $rowId) - ->orderByDesc('created_at') - ->get(); - } - - private function buildDeleteSQL(TriggerAuditLog $log): string - { - return "DELETE FROM `{$log->table_name}` WHERE `id` = " . DB::getPdo()->quote($log->row_id); - } - - private function buildRevertUpdateSQL(TriggerAuditLog $log): string - { - $sets = collect($log->old_values) - ->map(fn($val, $col) => "`{$col}` = " . ($val === null ? 'NULL' : DB::getPdo()->quote($val))) - ->implode(', '); - - return "UPDATE `{$log->table_name}` SET {$sets} WHERE `id` = " . DB::getPdo()->quote($log->row_id); - } - - private function buildReinsertSQL(TriggerAuditLog $log): string - { - $cols = collect($log->old_values)->keys()->map(fn($c) => "`{$c}`")->implode(', '); - $vals = collect($log->old_values)->values() - ->map(fn($v) => $v === null ? 'NULL' : DB::getPdo()->quote($v)) - ->implode(', '); - - return "INSERT INTO `{$log->table_name}` ({$cols}) VALUES ({$vals})"; - } -} -``` - ---- - -## 부록 F: 세션 시작 가이드 (새 세션용) - -### 이 문서로 작업을 시작하는 방법 - -``` -1. Serena 메모리 로드 - → read_memory("db-trigger-audit-state") : 진행 상태 확인 - -2. 이 문서의 "📍 현재 진행 상태" 확인 - → 마지막 완료 작업, 다음 작업 확인 - -3. 해당 Phase의 "대상 범위" (섹션 2) 확인 - → 구체적 작업 항목과 상태 확인 - -4. 해당 작업의 구현 코드는 "작업 절차" (섹션 3) + "부록" 참조 - → 부록 B: 기존 코드 패턴 (수정 금지) - → 부록 C: MNG 패턴 (Phase 4용) - → 부록 D: SP 구현 상세 (Phase 1.3용) - → 부록 E: 복구 서비스 상세 (Phase 2.2용) - -5. 작업 완료 후 - → 이 문서의 진행 상태 업데이트 - → Serena 메모리 저장: write_memory("db-trigger-audit-state", ...) -``` - -### 환경 확인 명령어 - -```bash -# Docker MySQL 실행 확인 -docker ps | grep sam-mysql - -# 마이그레이션 상태 -cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status - -# 현재 트리거 목록 확인 -docker exec -it sam-mysql-1 mysql -u root -proot samdb -e "SHOW TRIGGERS" - -# trigger_audit_logs 레코드 수 -docker exec -it sam-mysql-1 mysql -u root -proot samdb -e "SELECT COUNT(*) FROM trigger_audit_logs" -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/docs-plans-cleanup-plan.md b/plans/docs-plans-cleanup-plan.md new file mode 100644 index 0000000..5b0a73e --- /dev/null +++ b/plans/docs-plans-cleanup-plan.md @@ -0,0 +1,326 @@ +# docs/plans 폴더 정리 계획 + +> **작성일**: 2026-02-26 +> **목적**: docs/plans 폴더의 문서 분류, 통폐합, 히스토리 보관, 인덱스 재작성 +> **상태**: ⏳ Phase 1 대기 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 4: 최종 검증 완료 | +| **다음 작업** | 없음 (정리 완료) | +| **진행률** | 4/4 Phase (100%) | +| **마지막 업데이트** | 2026-02-26 | + +--- + +## 1. 개요 + +### 1.1 배경 + +`docs/plans/` 폴더에 문서가 누적되면서 다음 문제 발생: +- 같은 도메인에 신/구 문서가 공존 (방향 전환 등으로 새 문서가 생겼으나 이전 문서 미정리) +- 완료된 문서, 폐기된 문서, 진행중인 문서가 혼재 +- archive에 37개 개별 파일이 산재 (참조 효율 저하) +- sub/, clodeCheck/ 등 부수 폴더의 역할 불명확 + +### 1.2 현재 상태 + +``` +docs/plans/ ← 메인: 44개 md 파일 +├── archive/ ← 완료: 37개 md 파일 +├── sub/ ← 하위계획: 7개 md + archive/ +├── clodeCheck/ ← 코드체크 리포트: 7개 md +├── flow-tests/ ← 플로우 테스트 JSON: 32개 +├── SAM_ERP_Storyboard_D1.0_251218/ ← 스토리보드: 38장 +└── index_plans.md ← 현재 인덱스 +``` + +### 1.3 성공 기준 + +- [ ] 모든 메인 문서(44개)가 5단계 중 하나로 분류됨 +- [ ] SUPERSEDED 문서가 최신 문서에 병합되어 삭제됨 +- [ ] COMPLETED 문서가 archive/HISTORY.md로 요약 통합됨 +- [ ] OBSOLETE 문서가 삭제됨 +- [ ] sub/, clodeCheck/ 각 파일 처리 완료 +- [ ] index_plans.md가 ACTIVE+PLANNED 문서만 반영하여 재작성됨 +- [ ] docs/plans/에 ACTIVE + PLANNED 문서만 존재 + +--- + +## 2. 확정된 정책 + +### 2.1 문서 분류 기준 (5단계) + +| 분류 | 정의 | 처리 | 최종 위치 | +|------|------|------|----------| +| **ACTIVE** | 현재 진행중이거나 곧 착수할 문서 | 유지, 최신화 | `docs/plans/` | +| **PLANNED** | 확정된 예정 작업, 선행조건 대기 | 유지, 최신화 | `docs/plans/` | +| **SUPERSEDED** | 새 문서로 대체된 이전 문서 | 새 문서에 병합 후 **삭제** | 파일 없음 | +| **COMPLETED** | 완료된 작업 | HISTORY.md에 요약 후 **삭제** | `archive/HISTORY.md` | +| **OBSOLETE** | 방향 전환/폐기된 문서 | **삭제** | 파일 없음 | + +### 2.2 SUPERSEDED 판정 기준 + +같은 도메인에 문서 2개 이상일 때: +- **최신 문서(나중 생성)가 기준** → 이전 문서는 SUPERSEDED +- 이전 문서에만 있는 유용한 내용 → 최신 문서에 병합 +- 이전 문서가 최신 문서를 참조하지 않고 독립적 → 내용 비교 후 판단 +- 이전 문서가 최신 문서에 참조됨 → 최신 문서에 해당 내용 통합 + +**통폐합 후보 도메인** (파일명 기반, Phase 1에서 확정): +- 견적: `quote-*` 6개 +- 문서시스템: `document-*` 5개 +- 품목: `item-*`, `bom-*`, `mng-item-*` 등 +- 채번: `tenant-numbering-*`, `mng-numbering-*` + +### 2.3 HISTORY.md 구조 + +```markdown +# 완료 작업 히스토리 + +## 견적/수주 +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 견적 자동계산 | 2025-12 | 경동 수식 엔진 구현, V2 자동계산 적용 | + +## 품목/BOM +| 기능 | 완료시기 | 요약 | +| ... | ... | ... | + +## 생산/절곡 +... +``` + +- 기능 도메인별 섹션으로 구분 +- 각 항목: 기능명 + 완료시기 + 한줄 요약 (상세 불필요) +- 현재 archive/ 37개 + 이번 정리에서 COMPLETED로 분류된 문서 모두 포함 + +### 2.4 sub/, clodeCheck/ 처리 원칙 + +Phase 1에서 **문서별로 판단** (D 옵션): + +**sub/ 각 파일 → 아래 중 택1:** +- A. 메인 승격: 아직 유효 → `docs/plans/`로 이동 +- B. 상위 문서에 병합: 내용이 상위 계획에 포함 가능 +- C. 삭제: 이미 반영되었거나 폐기 + +**clodeCheck/ 각 파일 → 아래 중 택1:** +- A. 삭제: 일회성 리포트 +- B. HISTORY.md에 요약: 한 줄 이력으로 보관 + +### 2.5 변경하지 않는 대상 + +| 폴더 | 이유 | +|------|------| +| `flow-tests/` | 운영 도구 (JSON 테스트 케이스) | +| `SAM_ERP_Storyboard_D1.0_251218/` | 디자인 참조 (스토리보드) | + +--- + +## 3. 실행 계획 (4 Phase) + +### Phase 1: 분류 (읽기 전용) + +**목표**: 모든 문서를 5단계 중 하나로 분류 + +**작업 절차**: +1. 메인 44개 문서의 내용을 읽고 분류 판정 +2. sub/ 7개 문서의 상위 문서 관계 파악 후 분류 판정 +3. clodeCheck/ 7개 리포트의 보관 가치 판정 +4. 현재 archive/ 37개 문서의 요약 정보 추출 (HISTORY.md용) +5. 분류 결과 테이블 작성 → 사용자 확인 + +**산출물**: 아래 테이블 완성 + +#### 3.1.1 메인 문서 분류 결과 + +| # | 파일명 | 분류 | 비고 | +|---|--------|------|------| +| 1 | 5130-to-mng-migration-plan.md | ACTIVE | 13% 진행중 | +| 2 | api-explorer-development-plan.md | PLANNED | 미착수 | +| 3 | bending-info-auto-generation-plan.md | PLANNED | 설계 확정, 착수 대기 | +| 4 | bending-material-input-mapping-plan.md | PLANNED | GAP 분석 완료 | +| 5 | bending-preproduction-stock-plan.md | COMPLETED | 14/14 완료 | +| 6 | bom-item-mapping-plan.md | ACTIVE | 66% Phase 3 검증 잔여 | +| 7 | card-management-section-plan.md | ACTIVE | 50% 모달 연동 진행중 | +| 8 | dashboard-api-integration-plan.md | ACTIVE | 45% Phase 2 예정 | +| 9 | db-backup-system-plan.md | ACTIVE | 79% 서버 작업 3건 잔여 | +| 10 | db-trigger-audit-system-plan.md | COMPLETED | 94% 옵션만 잔여 | +| 11 | dev-toolbar-plan.md | ACTIVE | 38% Phase 2-4 진행중 | +| 12 | document-management-system-plan.md | SUPERSEDED | → document-system-master.md | +| 13 | document-system-master.md | ACTIVE | Phase 4-5 마스터 문서 | +| 14 | document-system-mid-inspection.md | ACTIVE | 5/6 결재만 남음 | +| 15 | document-system-work-log.md | ACTIVE | 3/4+α React 연동 잔여 | +| 16 | dummy-data-seeding-plan.md | PLANNED | 미착수 | +| 17 | employee-user-linkage-plan.md | PLANNED | 미착수 | +| 18 | erp-api-development-plan.md | ACTIVE | Phase L 진행중 | +| 19 | esign-alimtalk-integration.md | PLANNED | 카카오 채널 개설 후 착수 | +| 20 | fg-code-consolidation-plan.md | ACTIVE | 분석완료, Phase 1 착수 전 | +| 21 | hotfix-20260119-action-plan.md | OBSOLETE | 일회성 핫픽스 이력 | +| 22 | incoming-inspection-document-integration-plan.md | PLANNED | 분석만 완료 | +| 23 | incoming-inspection-templates-plan.md | ACTIVE | 83% 4종 품목 대기 | +| 24 | intermediate-inspection-report-plan.md | PLANNED | 검토 대기 | +| 25 | item-inventory-management-plan.md | PLANNED | 설계 확정, 구현 대기 | +| 26 | item-master-data-alignment-plan.md | ACTIVE | 섀도잉 정리 재수행 | +| 27 | items-migration-kyungdong-plan.md | SUPERSEDED | → kd-items-migration-plan.md (archive) | +| 28 | kd-orders-migration-plan.md | PLANNED | 선행조건 미충족 | +| 29 | kd-quote-logic-plan.md | ACTIVE | 80% Phase 5 직전 | +| 30 | mng-item-field-management-plan.md | PLANNED | 미착수 | +| 31 | mng-menu-system-plan.md | ACTIVE | 구현완료, 테스트 잔여 | +| 32 | mng-numbering-rule-management-plan.md | PLANNED | 미착수 | +| 33 | monthly-expense-integration-plan.md | PLANNED | 미착수 | +| ~~34~~ | ~~product-code-traceability-plan.md~~ | **제외** | 진행중 - 정리 대상 아님 | +| 35 | quote-calculation-api-plan.md | PLANNED | 설계 완료, 미착수 | +| 36 | quote-management-8issues-plan.md | PLANNED | 컨펌 대기 | +| 37 | quote-management-url-migration-plan.md | COMPLETED | 92% 잔여 사소 | +| 38 | quote-order-sync-improvement-plan.md | PLANNED | 승인 대기 | +| 39 | quote-system-development-plan.md | SUPERSEDED | → kd-quote-logic-plan.md | +| 40 | react-api-integration-plan.md | ACTIVE | 기능별 API 연동 진행중 | +| 41 | react-mock-remaining-tasks.md | SUPERSEDED | → react-mock-to-api-migration-plan.md | +| 42 | react-mock-to-api-migration-plan.md | ACTIVE | Mock→API 전환 진행중 | +| 43 | receiving-management-analysis-plan.md | PLANNED | 분석 완료, 개발 대기 | +| 44 | simulator-ui-enhancement-plan.md | ACTIVE | 60% Phase 2 진행중 | +| 45 | tenant-id-compliance-plan.md | PLANNED | 실행 대기 | +| 46 | tenant-numbering-system-plan.md | PLANNED | 미착수 | + +#### 3.1.2 sub/ 문서 분류 결과 + +| # | 파일명 | 처리 | 상위 문서 | 비고 | +|---|--------|:----:|----------|------| +| 1 | categories-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 2 | contract-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 3 | items-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 4 | order-management-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 5 | pricing-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 6 | site-management-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 7 | structure-review-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | + +#### 3.1.3 clodeCheck/ 문서 분류 결과 + +| # | 파일명 | 처리 | 비고 | +|---|--------|:----:|------| +| 1 | attendance-management_2026-01-14_23-30-00.md | A (삭제) | 일회성 E2E 리포트 | +| 2 | bank-transactions_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | +| 3 | card-transactions_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | +| 4 | employee-register_2026-01-14_20-00-00.md | A (삭제) | 일회성 테스트 리포트 | +| 5 | salary-management_2026-01-15_10-30-00.md | A (삭제) | 일회성 테스트 리포트 | +| 6 | sales-management_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | +| 7 | withdrawal-management_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | + +**Phase 1 완료 기준**: 위 3개 테이블 완성 + 사용자 승인 + +--- + +### Phase 2: 통폐합 (승인 후) + +**목표**: SUPERSEDED 문서를 최신 문서에 병합 + +**작업 절차**: +1. Phase 1에서 SUPERSEDED로 분류된 문서 목록 확인 +2. 각 SUPERSEDED 문서 → 대응하는 최신 문서 매핑 +3. 이전 문서에만 있는 유용한 내용 추출 +4. 최신 문서에 병합 (필요한 내용만) +5. **건별로 사용자 확인** (또는 일괄 승인 선택) +6. 확인 후 이전 문서 삭제 + +**산출물**: 통폐합 매핑 테이블 + +| SUPERSEDED 문서 | 병합 대상 (최신) | 병합 내용 요약 | 승인 | +|----------------|-----------------|---------------|------| +| (Phase 1 결과) | | | | + +**Phase 2 완료 기준**: 모든 SUPERSEDED 문서 처리 + 사용자 승인 + +--- + +### Phase 3: 정리 + +**목표**: COMPLETED/OBSOLETE 처리, HISTORY.md 작성, 인덱스 재작성 + +**병렬 가능한 작업**: + +**3-A. HISTORY.md 작성** +1. 현재 archive/ 37개 문서에서 기능명 + 완료시기 + 한줄요약 추출 +2. Phase 1에서 COMPLETED로 분류된 메인 문서도 동일 처리 +3. 기능 도메인별로 분류하여 HISTORY.md 작성 +4. archive/ 개별 파일 삭제 + +**3-B. OBSOLETE 삭제** +1. Phase 1에서 OBSOLETE로 분류된 문서 삭제 +2. sub/ 처리 (Phase 1 판정에 따라) +3. clodeCheck/ 처리 (Phase 1 판정에 따라) + +**3-C. index_plans.md 재작성** (3-A, 3-B 완료 후) +1. ACTIVE + PLANNED 문서만 기능 도메인별로 정리 +2. 각 문서의 상태/진행률 반영 +3. HISTORY.md 링크 포함 + +**Phase 3 완료 기준**: 폴더에 ACTIVE+PLANNED만 남음 + index 재작성 완료 + +--- + +### Phase 4: 검증 + +**목표**: 최종 구조 확인 + +**체크리스트**: +- [ ] docs/plans/에 ACTIVE + PLANNED 문서만 존재 +- [ ] archive/에 HISTORY.md만 존재 +- [ ] sub/, clodeCheck/ 정리 완료 +- [ ] index_plans.md가 실제 파일과 일치 +- [ ] 삭제된 문서 중 필요한 내용이 누락되지 않았는지 확인 +- [ ] flow-tests/, Storyboard 폴더 영향 없음 + +--- + +## 4. 작업 시 주의사항 + +### 4.0 정리 제외 대상 + +아래 문서는 정리/분류/통폐합 대상에서 **제외**한다: +- `product-code-traceability-plan.md` — 현재 진행중 +- **이 정리 작업 이후 신규 생성되는 문서** — GUIDE.md 원칙에 따라 생성되므로 정리 불필요 + +### 4.1 삭제 전 확인 원칙 +- 문서 삭제 전 반드시 내용을 읽고 유용한 정보 유무 확인 +- SUPERSEDED 삭제 시 최신 문서에 병합 완료 확인 후 삭제 +- **git에서 복구 가능하므로** 과도한 보수적 판단 불필요 + +### 4.2 판단 기준 우선순위 +- 최신 문서 > 이전 문서 +- 구체적 구현 내용 > 추상적 계획 +- 현재 시스템에 적용된 내용 > 적용 예정이었던 내용 + +### 4.3 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | Phase 1 분류 테이블 작성 | 불필요 (읽기 전용) | +| ⚠️ 컨펌 필요 | 문서 병합, 삭제, HISTORY.md 작성 | **Phase별 사용자 승인** | +| 🔴 금지 | flow-tests/, Storyboard 수정 | 별도 협의 | + +--- + +## 5. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | +|------|------|----------| +| 2026-02-26 | 문서 초안 | 정책 수립 완료, 4 Phase 계획 작성 | +| 2026-02-26 | Phase 1~4 완료 | 분류→통폐합→정리→검증 전 과정 완료 | + +--- + +## 6. 참고 문서 + +- **문서 가이드**: `docs/plans/GUIDE.md` ← 정리 시 준수할 최소 원칙 +- **현재 인덱스**: `docs/plans/index_plans.md` +- **문서 인덱스**: `docs/INDEX.md` +- **프로젝트 구조**: `CLAUDE.md` + +--- + +*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/document-management-system-plan.md b/plans/document-management-system-plan.md deleted file mode 100644 index 7894962..0000000 --- a/plans/document-management-system-plan.md +++ /dev/null @@ -1,1119 +0,0 @@ -# 문서관리 시스템 개발 계획 (Phase 1~4) - -> **작성일**: 2026-01-31 -> **목적**: mng에서 문서양식(템플릿)을 관리하고 문서를 생성하여, SAM(react)에서 JSON으로 소비하는 문서관리 시스템을 구축한다 -> **기준 문서**: `docs/specs/database-schema.md`, `mng/CLAUDE.md` -> **상태**: Phase 1~3 ✅ 완료, Phase 4 🔄 (4.4 미완료) -> -> **📌 이 문서는 Phase 1~4 아카이브입니다.** -> **새 작업은 마스터 문서에서 시작하세요**: [`document-system-master.md`](./document-system-master.md) -> Phase 5 상세는 유형별 개별 문서로 분리되었습니다. - ---- - -## 🚀 새 세션 시작 가이드 - -> **이 섹션은 새 세션에서 이 문서만 보고 작업을 시작할 수 있도록 작성되었습니다.** - -### 프로젝트 정보 - -| 항목 | 내용 | -|------|------| -| **작업 프로젝트** | `mng` (관리자 패널) | -| **절대 경로** | `/Users/kent/Works/@KD_SAM/SAM/mng/` | -| **기술 스택** | Laravel 12 + Plain Blade + DaisyUI + HTMX + Alpine.js | -| **로컬 URL** | `https://mng.sam.kr` (Docker 로컬, `admin.sam.kr`도 동일) | -| **관련 API** | `/Users/kent/Works/@KD_SAM/SAM/api/` (Laravel 12 REST API) | -| **프론트** | `/Users/kent/Works/@KD_SAM/SAM/react/` (Next.js 15, 이 작업에서는 미수정) | -| **5130 레거시** | `/Users/kent/Works/@KD_SAM/SAM/5130/` (참조 전용) | -| **문서 경로** | `/Users/kent/Works/@KD_SAM/SAM/docs/` | - -### mng Git 저장소 - -```bash -cd /Users/kent/Works/@KD_SAM/SAM/mng -git status && git branch -``` - -> **주의**: SAM/ 루트는 Git 저장소가 아님. api/, mng/, react/ 각각 독립 Git 저장소. - -### 세션 시작 체크리스트 - -``` -1. 이 문서를 읽는다 (📍 현재 진행 상태 섹션 확인) -2. mng/CLAUDE.md 를 읽는다 (mng 프로젝트 규칙 확인) -3. 마지막 완료 작업 확인 → 다음 작업 결정 -4. 해당 Phase의 상세 절차(섹션 11)를 읽는다 -5. 작업 시작 전 사용자에게 "Phase X.X 시작할까요?" 확인 -``` - -### 핵심 파일 (작업 빈도순) - -| 파일 | 설명 | 크기 | -|------|------|------| -| `mng/resources/views/document-templates/edit.blade.php` | 양식 편집 UI (메인 작업 대상) | 44.5KB | -| `mng/app/Http/Controllers/DocumentTemplateController.php` | 양식 CRUD 컨트롤러 | | -| `mng/app/Http/Controllers/DocumentController.php` | 문서 CRUD 컨트롤러 | | -| `mng/app/Models/DocumentTemplate.php` | 양식 모델 (관계 정의) | | -| `mng/app/Models/Documents/Document.php` | 문서 모델 (상태 워크플로우) | | -| `mng/routes/web.php` (340-353줄) | 양식/문서 라우트 | | - -### 모델 관계 구조 (코드 참조) - -```php -// DocumentTemplate.php 주요 관계 -class DocumentTemplate extends Model { - use BelongsToTenant, SoftDeletes; - - // 결재라인: template->approval_lines (작성/검토/승인) - public function approvalLines() { return $this->hasMany(DocumentTemplateApprovalLine::class, 'template_id')->orderBy('sort_order'); } - - // 기본필드: template->basic_fields (품명, LOT NO 등) - public function basicFields() { return $this->hasMany(DocumentTemplateBasicField::class, 'template_id')->orderBy('sort_order'); } - - // 섹션: template->sections->items (검사기준서 섹션 + 검사항목) - public function sections() { return $this->hasMany(DocumentTemplateSection::class, 'template_id')->orderBy('sort_order'); } - - // 컬럼: template->columns (데이터 테이블 컬럼 정의) - public function columns() { return $this->hasMany(DocumentTemplateColumn::class, 'template_id')->orderBy('sort_order'); } -} - -// Document.php 주요 관계 -class Document extends Model { - use BelongsToTenant, SoftDeletes; - - // 상태: DRAFT -> PENDING -> APPROVED/REJECTED/CANCELLED - protected $casts = ['status' => DocumentStatus::class]; - - public function template() { return $this->belongsTo(DocumentTemplate::class); } - public function approvals() { return $this->hasMany(DocumentApproval::class); } - public function data() { return $this->hasMany(DocumentData::class); } // EAV 패턴 - public function attachments() { return $this->hasMany(DocumentAttachment::class); } - public function linkable() { return $this->morphTo(); } // 다형성 연결 (수주, 작업지시 등) -} -``` - -### mng 라우트 구조 - -```php -// mng/routes/web.php (340-353줄) -Route::resource('document-templates', DocumentTemplateController::class); // /document-templates -Route::resource('documents', DocumentController::class); // /documents -``` - -> **URL 확인**: `https://mng.sam.kr/document-templates` (양식 관리), `https://mng.sam.kr/documents` (문서 관리) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| - -| **마지막 완료 작업** | Phase 4.3 - mng 문서 데이터 입력/저장 연동 검증 완료 (기존 구현 확인) | -| **다음 작업** | Phase 4.4 - 프론트엔드 담당자 협의 후 react 전환 결정 | -| **진행률** | 16/20 (80%) - Phase 1 ✅, Phase 2 ✅, Phase 3 ✅, Phase 4.1-4.3 ✅ | -| **마지막 업데이트** | 2026-01-31 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM(react)에는 검사 성적서(수입검사, 중간검사), 작업일지 등이 하드코딩된 모달 컴포넌트로 존재한다. 5130 레거시 시스템에도 동일 문서들이 PHP 파일 단위로 구현되어 있다. 이들을 **mng에서 동적으로 양식을 관리**하고, **API를 통해 JSON으로 제공**하여 SAM에서 렌더링하는 구조로 전환한다. - -**핵심 문제:** -- 현재 검사 문서가 React 컴포넌트에 하드코딩되어, 새 양식 추가 시 코드 수정이 필요 -- 5130의 수입검사만 약 40종의 자재별 페이지가 개별 PHP 파일로 존재 -- 검사 기준, 항목, 판정 로직이 코드와 혼재되어 비개발자가 관리 불가 -- 중간검사(절곡/스크린/슬랫/조인트바)도 각각 별도 컴포넌트로 분산 - -**해결 방향:** -- mng에서 문서양식(템플릿)을 동적으로 정의 → 검사 항목/기준/판정 로직 포함 -- 양식 기반으로 실제 문서 인스턴스를 생성 → 데이터 입력/결재/출력 -- SAM에서 API로 양식+데이터를 JSON 수신 → 범용 렌더러로 표시 - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 양식 정의는 mng에서만 관리 (비개발자도 양식 수정 가능하도록) │ -│ 2. SAM(react)은 JSON을 받아 렌더링만 담당 (문서 로직 없음) │ -│ 3. 기존 DB 구조(document_templates 계열) 최대한 활용 │ -│ 4. 5130 레거시의 검사 기준/항목을 데이터로 이관 │ -│ 5. 결재 워크플로우(DRAFT->PENDING->APPROVED) 유지 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| 즉시 가능 | 양식 필드 추가/변경, 검사항목 추가, 기준값 수정, 뷰(Blade) 수정 | 불필요 | -| 컨펌 필요 | 새 DB 테이블 추가, 기존 테이블 컬럼 변경, API 엔드포인트 추가, 마이그레이션 | **필수** | -| 금지 | 기존 document_templates 테이블 구조 파괴적 변경, 기존 API 삭제 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/specs/database-schema.md` - DB 스키마 참조 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `mng/CLAUDE.md` - MNG 프로젝트 규칙 - ---- - -## 2. 현황 분석 - -### 2.1 기존 DB 구조 (이미 생성됨) - -``` -document_templates # 양식 마스터 -├── document_template_approval_lines # 결재라인 (작성/검토/승인) -├── document_template_basic_fields # 기본필드 (품명, LOT NO 등) -├── document_template_sections # 섹션 (검사기준서 섹션) -│ └── document_template_section_items # 섹션 항목 (검사항목) -└── document_template_columns # 데이터 테이블 컬럼 - -documents # 문서 인스턴스 -├── document_approvals # 결재 이력 -├── document_data # 필드 데이터 (EAV 패턴) -└── document_attachments # 첨부 파일 -``` - -**주요 테이블 컬럼:** - -| 테이블 | 핵심 컬럼 | -|--------|----------| -| `document_templates` | tenant_id, name, category, title, company_name, footer_remark_label, footer_judgement_label, footer_judgement_options(json) | -| `document_template_approval_lines` | template_id, name, dept, role, sort_order | -| `document_template_basic_fields` | template_id, label, field_type(text/date), default_value, sort_order | -| `document_template_sections` | template_id, title, image_path, sort_order | -| `document_template_section_items` | section_id, category, item, standard, method, frequency, regulation, sort_order | -| `document_template_columns` | template_id, label, column_type(text/check/measurement/select/complex), group_name, sub_labels(json), width, sort_order | -| `documents` | tenant_id, template_id, document_no, title, status(DRAFT/PENDING/APPROVED/REJECTED/CANCELLED), linkable_type, linkable_id | -| `document_data` | document_id, section_id, column_id, row_index, field_key, field_value | -| `document_approvals` | document_id, user_id, step, role, status(PENDING/APPROVED/REJECTED), comment, acted_at | - -### 2.2 기존 MNG 코드 현황 - -| 항목 | 경로 | 상태 | -|------|------|------| -| DocumentTemplate 모델 | `mng/app/Models/DocumentTemplate.php` | 존재 | -| Document 모델 | `mng/app/Models/Documents/Document.php` | 존재 | -| 관련 하위 모델 6개 | `mng/app/Models/Documents/`, `mng/app/Models/DocumentTemplate*.php` | 존재 | -| DocumentTemplateController | `mng/app/Http/Controllers/DocumentTemplateController.php` | 존재 | -| DocumentController | `mng/app/Http/Controllers/DocumentController.php` | 존재 | -| 라우트 (templates, documents) | `mng/routes/web.php` 340-353줄 | 존재 | -| 양식 편집 Blade | `mng/resources/views/document-templates/edit.blade.php` (44.5KB) | 존재 | -| 문서 Blade (index/edit/show) | `mng/resources/views/documents/` | 존재 | - -### 2.3 5130 레거시 검사 문서 현황 - -#### 수입검사 (instock) - -| 항목 | 내용 | -|------|------| -| 위치 | `5130/instock/` | -| 자재별 검사 페이지 | 40+ PHP 파일 (`i_EGI155.php`, `i_SUSplate.php`, `i_wire.php`, `i_motor.php` 등) | -| 메인 로더 | `fetch_inspection.php` (21.8KB) - 자재코드별 동적 로딩 | -| 검사 필드 | 로트번호, 검사일, 납품업체, 품명, 규격, 단위, 품목코드, 입고량, 자재번호, 제조사 | -| 판정 방식 | 항목별 합격/불합격 -> 종합판정 자동계산 | -| LOT 관리 | `lotnum.txt` 파일 기반, YYMMDD-## 형식 | -| PDF 출력 | html2pdf.js 사용 | - -#### 중간검사 (output) - -| 검사 종류 | 파일 | DB 필드 | -|----------|------|---------| -| 절곡품 중간검사 | `viewMidInspectBending.php` (60.7KB) | `recordbendingMid` (JSON) | -| 스크린 중간검사 | `viewMidInspectScreen.php` (33.6KB) | `recordscreenMid` (JSON) | -| 슬랫 중간검사 | `viewMidInspectSlat.php` | `recordslatMid` (JSON) | -| 조인트바 검사 | `viewinspectionJointbar.php` (34.1KB) | `recordjointbar` (JSON) | - -#### 검사 공통 구조 -- 결재: 작성(판매/Order) -> 검토(생산) -> 승인(품질/QC) -- 검사 기준 이미지: `5130/img/inspection/` (20+ 이미지) -- 데이터: JSON으로 DB 저장 (approval chain + measurements) -- QC 관리자 권한 제어 (이세희, 함신옥, 이경호, 노완호) - -### 2.4 SAM(react) 현재 검사 컴포넌트 - -| 컴포넌트 | 경로 | 용도 | -|---------|------|------| -| ImportInspectionDocument | `react/src/.../quality/qms/components/documents/` | 수입검사 성적서 | -| ScreenInspectionDocument | 동일 경로 | 스크린 중간검사 성적서 | -| SlatInspectionDocument | 동일 경로 | 슬랫 중간검사 성적서 | -| BendingInspectionDocument | 동일 경로 | 절곡품 중간검사 성적서 | -| JointbarInspectionDocument | 동일 경로 | 조인트바 중간검사 성적서 | -| ProductInspectionDocument | 동일 경로 | 제품검사 성적서 | -| WorkLogContent | `react/src/components/production/WorkerScreen/` | 작업일지 | -| InspectionReportModal | `react/src/components/production/WorkOrders/documents/` | 중간검사 모달 | -| DocumentViewer | `react/src/components/document-system/viewer/` | 범용 문서 뷰어 | - -**공통 컴포넌트 (document-system):** -- `DocumentHeader.tsx` - 문서 헤더 (로고, 결재라인) -- `QualityApprovalTable.tsx` - 품질 결재표 -- `InfoTable.tsx` - 정보 테이블 -- `DocumentViewer.tsx` - 문서 뷰어 (zoom, drag, print, download) - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: mng 양식 관리 기능 완성 (수입검사) - -수입검사 양식 20여종을 mng에서 동적으로 관리할 수 있도록 기존 코드를 보강한다. - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 1.1 | 기존 document-templates 편집 UI 점검 및 보완 | ✅ | `mng.sam.kr/document-templates/{id}/edit`에서 결재라인/기본필드/섹션/항목/컬럼 모두 CRUD 가능. 저장 후 DB에 정상 반영 확인 | 5개 탭 전체 CRUD 완료 확인 | -| 1.2 | 5130 수입검사 데이터 분석 및 양식 구조 설계 | ✅ | 라우팅 구조 + 대표 자재 2종(EGI, SUS) 상세 분석 완료. 나머지 21종은 Phase 1.3에서 개별 분석 병행 | viewJS.php 라우팅 + 공통패턴 추출 | -| 1.3 | 수입검사 양식 시드 데이터 생성 | ✅ | EGI(ID:7), SUS(ID:8) 2종 생성 완료. 각각 결재2+기본필드10+섹션1+검사항목7~8+컬럼7. 나머지 자재는 개별 분석 후 시더에 추가 | `IncomingInspectionTemplateSeeder.php` | -| 1.4 | 양식 미리보기 기능 | ✅ | edit.blade.php에 모달 미리보기 구현 완료. 결재란+기본정보+검사이미지+검사테이블(complex 지원)+Footer(비고+판정) 모두 렌더링 | 기존 구현 확인 완료 | -| 1.5 | 양식 복제 기능 | ✅ | API `POST /{id}/duplicate` + 목록 복제 버튼. 이름 입력 prompt → 전체 관계(결재/필드/섹션/항목/컬럼) 복제. 비활성 상태로 생성 | API+UI 구현 완료 | - -### 3.2 Phase 2: mng 문서 생성/관리 기능 - -양식을 기반으로 실제 검사 문서를 생성하고 데이터를 입력/결재하는 기능. - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 2.1 | 문서 생성 (양식 선택 -> 빈 문서 생성) | ✅ | 양식 선택 후 빈 문서(DRAFT)가 documents 테이블에 생성됨. 문서번호 자동 채번 | 카테고리별 prefix (IQC/PRD/SLS/PUR), 결재라인 초기화, 기본필드 뷰 수정 완료 | -| 2.2 | 문서 데이터 입력 UI | ✅ | 양식의 columns/sections 기반 동적 테이블 렌더링. complex/select/check/measurement/text 컬럼 타입 지원. EAV 저장 (section_id, column_id, row_index) | field_key 패턴: s{섹션}_r{행}_c{컬럼}_sub{인덱스} | -| 2.3 | 결재 워크플로우 (제출/승인/반려) | ✅ | DRAFT→PENDING→APPROVED/REJECTED 전체 동작. 단계별 승인, 반려 사유 필수, 재제출 시 결재라인 초기화 | submit/approve/reject API + 승인·반려 UI | -| 2.4 | 문서 목록/검색/필터 | ✅ | 상태별(DRAFT/PENDING/APPROVED), 양식별, 날짜별 필터 동작. 페이징 포함 | 날짜 범위 필터(date_from/date_to) + DRAFT 문서 삭제 기능 추가 | -| 2.5 | 문서 PDF 출력 | ⏭️ | **추후 고려** - react에 이미 html2pdf.js 구현됨 (6.2 결정사항 #1 참고) | | - -### 3.3 Phase 3: 중간검사 양식 추가 - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 3.1 | 중간검사 양식 구조 설계 | ✅ | 절곡/스크린/슬랫/조인트바 4종의 검사항목/기준/판정방식 문서화 완료 | 섹션 5.2에 상세 설계. 절곡품 최고 복잡도(★5), 조인트바 최저(★1) | -| 3.2 | 5130 중간검사 데이터 이관 설계 | ✅ | recordbendingMid 등 JSON→양식 매핑 테이블 완성 | 섹션 5.3에 상세 설계. 6단계 이관 프로세스, 변환 규칙, 주의사항 문서화 | -| 3.3 | 중간검사 양식 시드 데이터 | ✅ | 4종 양식 seeder 생성, `mng.sam.kr/document-templates`에서 확인 가능 | MidInspectionTemplateSeeder: 조인트바(ID:10), 슬랫(ID:11), 스크린(ID:12), 절곡품(ID:13) | -| 3.4 | 검사 기준 이미지 관리 | ✅ | `5130/img/inspection/` 이미지 → `mng/public/img/inspection/`로 이관. 양식에서 참조 가능 | 27개 이미지. URL: `/img/inspection/{filename}.jpg` | - -### 3.4 Phase 4: API 연동 및 mng JSON 화면 구현 - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 4.1 | API 엔드포인트 설계 (양식 조회, 문서 CRUD) | ✅ | DocumentTemplate 읽기 전용 API(모델6+서비스+컨트롤러+FormRequest+라우트+Swagger). Document 결재 워크플로우 4개 엔드포인트 활성화(submit/approve/reject/cancel) | api 저장소 | -| 4.2 | mng에서 JSON 기반 문서 화면 구현 | ✅ | show.blade.php에 섹션 테이블 읽기전용 렌더링 구현(5가지 컬럼 타입). 종합판정+비고 Footer. 기존 버그 3건 수정(field_key/field_type/section.title) | mng 저장소 | -| 4.3 | mng에서 문서 데이터 입력/저장 연동 | ✅ | Phase 2.2~2.3에서 이미 완전 구현 확인. edit.blade.php JS→DocumentApiController.saveDocumentData()→document_data EAV 저장. 판정(적합/부적합) select+종합판정 Footer 저장 정상. 6.2 결정사항 #2(프론트 입력, 결과만 저장) 적용됨 | 추가 코드 작업 없음 | -| 4.4 | 프론트엔드 담당자 협의 후 react 전환 결정 | ⏳ | mng 완성 후 프론트 담당자와 미팅. react 기존 컴포넌트는 미수정 (6.2 결정사항 #4) | 협의 결과 문서화 | - -### 3.5 Phase 5: 문서 유형 확장 - -> **상세 계획은 개별 문서로 분리됨** → [`document-system-master.md`](./document-system-master.md) - -| # | 작업 항목 | 상태 | 상세 문서 | -|---|----------|:----:|----------| -| 5.1 | 중간검사(PQC) 폼 구현 | ⏳ | [`document-system-mid-inspection.md`](./document-system-mid-inspection.md) | -| 5.2 | 제품검사(FQC) 폼 구현 | ⏳ | [`document-system-product-inspection.md`](./document-system-product-inspection.md) | -| 5.3 | 작업일지 폼 구현 | ⏳ | [`document-system-work-log.md`](./document-system-work-log.md) | -| 5.4 | 기타문서 (견적서/거래명세서/발주서 등) | ⏭️ | 추후 정의 | - ---- - -## 4. 아키텍처 설계 - -### 4.1 시스템 흐름 - -``` -[mng - 양식 관리] [api - REST API] [SAM - react 프론트] - -DocumentTemplate CRUD ----------> GET /document-templates 양식 목록/상세 - - 결재라인 설정 GET /document-templates/{id} - - 기본필드 설정 - - 섹션/항목 설정 POST /documents 문서 생성 - - 컬럼 설정 PUT /documents/{id} 데이터 입력 - GET /documents/{id} 문서 조회 -Document 생성/관리 ------------> POST /documents/{id}/submit 결재 제출 - - 데이터 입력 POST /documents/{id}/approve 결재 승인 - - 결재 처리 POST /documents/{id}/reject 결재 반려 - - PDF 출력 GET /documents/{id}/pdf PDF 다운로드 -``` - -### 4.2 JSON 응답 구조 (양식 상세) - -```json -{ - "template": { - "id": 1, - "name": "EGI 1.55T 수입검사", - "category": "incoming_inspection", - "title": "수 입 검 사 성 적 서", - "companyName": "케이디산업", - "approvalLines": [ - { "name": "작성", "dept": "판매/Order", "role": "담당자", "sortOrder": 1 }, - { "name": "검토", "dept": "생산", "role": "담당자", "sortOrder": 2 }, - { "name": "승인", "dept": "품질", "role": "QC", "sortOrder": 3 } - ], - "basicFields": [ - { "label": "품명", "fieldType": "text", "sortOrder": 1 }, - { "label": "규격", "fieldType": "text", "sortOrder": 2 }, - { "label": "LOT NO", "fieldType": "text", "sortOrder": 3 }, - { "label": "검사일자", "fieldType": "date", "sortOrder": 4 }, - { "label": "납품업체", "fieldType": "text", "sortOrder": 5 }, - { "label": "검사자", "fieldType": "text", "sortOrder": 6 } - ], - "sections": [ - { - "title": "가이드레일", - "imagePath": "/storage/inspection/guiderail.jpg", - "items": [ - { - "category": "겉모양", - "item": "사용상 결함이 될 흠이 없을 것", - "standard": "KS D 3506", - "method": "육안검사", - "frequency": "체크검사", - "regulation": "KS D 3506" - }, - { - "category": "치수", - "item": "두께", - "standard": "1.55 +/- 0.15", - "method": "계측", - "frequency": "입고시", - "regulation": "KS D 3506" - } - ] - } - ], - "columns": [ - { "label": "NO", "columnType": "text", "width": "50px", "sortOrder": 1 }, - { "label": "검사항목", "columnType": "text", "width": "120px", "sortOrder": 2 }, - { "label": "검사기준", "columnType": "text", "width": "150px", "sortOrder": 3 }, - { - "label": "검사 DATA", - "columnType": "complex", - "groupName": "검사 DATA", - "subLabels": ["1", "2", "3", "4", "5"], - "width": "300px", - "sortOrder": 4 - }, - { "label": "판정", "columnType": "select", "width": "80px", "sortOrder": 5 } - ], - "footerRemarkLabel": "부적합 내용", - "footerJudgementLabel": "종합판정", - "footerJudgementOptions": ["합격", "불합격"] - } -} -``` - -### 4.3 JSON 응답 구조 (문서 상세) - -```json -{ - "document": { - "id": 1, - "templateId": 1, - "documentNo": "IQC-260131-01", - "title": "EGI 1.55T 수입검사 성적서", - "status": "APPROVED", - "template": { "...위 구조와 동일..." }, - "basicData": { - "품명": "전기 아연도금 강판", - "규격": "EGI 1.55T", - "LOT NO": "260131-01", - "검사일자": "2026-01-31", - "납품업체": "포스코", - "검사자": "이세희" - }, - "tableData": [ - { - "sectionId": 1, - "rows": [ - { - "rowIndex": 0, - "values": { - "NO": "1", - "검사항목": "겉모양", - "검사기준": "사용상 결함 없을 것", - "검사 DATA": { "1": "양호", "2": "양호", "3": "양호", "4": "-", "5": "-" }, - "판정": "적합" - } - } - ] - } - ], - "footerData": { - "remark": "", - "judgement": "합격" - }, - "approvals": [ - { "step": 1, "role": "작성", "userName": "홍길동", "status": "APPROVED", "actedAt": "2026-01-31" }, - { "step": 2, "role": "검토", "userName": "김철수", "status": "APPROVED", "actedAt": "2026-01-31" }, - { "step": 3, "role": "승인", "userName": "이세희", "status": "APPROVED", "actedAt": "2026-01-31" } - ] - } -} -``` - ---- - -## 5. 5130 데이터 이관 계획 - -### 5.1 수입검사 자재 목록 (주요) - -5130의 `instock/fetch_inspection.php`에서 자재코드별로 로딩하는 개별 페이지를 분석하여, 각 자재별 검사항목을 양식 시드 데이터로 변환한다. - -| # | 자재 | 5130 파일 | 검사 항목 수 | 우선순위 | -|---|------|----------|:----------:|:-------:| -| 1 | EGI 1.55T (전기아연도금강판) | `i_EGI155.php` | ~8 | 높음 | -| 2 | SUS Plate (스테인리스강판) | `i_SUSplate.php` | ~6 | 높음 | -| 3 | GI Plate (아연도금강판) | `i_GIplate.php` | ~6 | 높음 | -| 4 | Wire (와이어) | `i_wire.php` | ~4 | 중간 | -| 5 | Motor (모터) | `i_motor.php` | ~5 | 중간 | -| 6 | Angle (앵글) | `i_angle.php` | ~4 | 중간 | -| 7-20+ | 기타 자재 | 개별 PHP 파일 | 다양 | 낮음 | - -### 5.2 중간검사 양식 구조 설계 (Phase 3.1) - -> **5130 레거시 분석 결과** - 4종 중간검사의 검사항목/기준/판정방식을 문서화 - -#### 5.2.0 공통 구조 - -**결재라인 (4종 공통)** - -| step | name | dept | role | -|------|------|------|------| -| 1 | 작성 | 판매/Order | 담당자 | -| 2 | 검토 | 생산 | 담당자 | -| 3 | 승인 | 품질 | QC | - -**기본필드 (4종 공통)** - -| # | label | field_type | 비고 | -|---|-------|-----------|------| -| 1 | 품명 | text | 절곡품/스크린/철재스라트/조인트바 | -| 2 | 규격 | text | 제품 규격 | -| 3 | 로트크기 | text | 개소 수 | -| 4 | 발주처 | text | 고객사명 | -| 5 | 현장명 | text | 설치 현장 | -| 6 | 검사일자 | date | - | -| 7 | 검사자 | text | - | - -**Footer (4종 공통)** -- `footer_remark_label`: "부적합 내용" -- `footer_judgement_label`: "종합판정" -- `footer_judgement_options`: ["합격", "불합격"] -- 종합판정 로직: 모든 행 "적" → 합격, 하나라도 "부" → 불합격 - ---- - -#### 5.2.1 절곡품 중간검사 (Bending Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 절곡품 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 절곡품 - 중간 검사 성적서 | -| 5130 DB필드 | `recordbendingMid` (JSON) | - -**섹션 구조**: 제품 코드별 다른 검사 항목 (동적 구성) - -구성품별 검사항목: - -| 구성품 | 검사 항목 | 비고 | -|--------|----------|------| -| 가이드레일 (벽면형 120×70) | 겉모양(절곡상태), 길이, 너비, 간격(4포인트) | S1: 30/80/45/40mm | -| 가이드레일 (측면형 120×120) | 겉모양(절곡상태), 길이, 너비, 간격(6포인트) | S1: 30/70/45/35/95/90mm | -| 하단마감재 (60×40) | 겉모양, 너비(60mm) | 길이 3000/4000mm | -| 하단 L-BAR (17×60) | 겉모양, 너비(17mm) | - | -| 케이스/셔터박스 | 겉모양, 높이, 하단, 차이, 위치 | 양면/밑면/후면 | -| 연기차단재 (가이드레일용) | 너비(50mm), 간격(12mm) | - | -| 연기차단재 (케이스용) | 너비(80mm), 간격(12mm) | - | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 분류/제품명 | text | 자동매핑 (KSS01 등) | -| 2 | 타입 | text | 벽면형/측면형/규격 | -| 3 | 겉모양(절곡상태) | check | 양호/불량 체크 | -| 4 | 길이 | complex | sub_labels: ['도면치수', '측정값'] | -| 5 | 너비 | complex | sub_labels: ['도면치수', '측정값'] | -| 6 | 간격 | complex | sub_labels: POINT별 ['도면치수', '측정값'] (가변) | -| 7 | 판정(적/부) | select | 자동계산 가능 | - -**허용 공차** -- 길이: ±4mm -- 간격: ±2mm - -**참조 이미지**: `bending_inspection1.jpg`, `bending_inspection2.jpg`, `guiderail_*`, `box_*`, `Lbar_mid`, `smoke` - -**⚠️ 특이사항**: 절곡품은 제품 코드(KSS01/KSS02/KWE01)와 마감유형(S1/S2/S3)에 따라 검사 항목이 동적으로 변경됨. 현재 양식 시스템에서 이를 표현하려면 **가장 포괄적인 구성을 기본 양식으로 만들고**, 실제 문서 생성 시 해당 제품에 맞는 행만 활성화하는 방식 검토 필요. - ---- - -#### 5.2.2 스크린 중간검사 (Screen Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 스크린 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 스크린 - 중간 검사 성적서 | -| 5130 DB필드 | `recordscreenMid` (JSON) | - -**섹션: 스크린 검사 항목** - -| # | 검사항목 | 타입 | 기준 | 비고 | -|---|---------|------|------|------| -| 겉모양-1 | 가공상태 | check | 양호/불량 | - | -| 겉모양-2 | 재봉상태 | check | 양호/불량 | - | -| 겉모양-3 | 조립상태 | check | 양호/불량 | - | -| 치수-① | 길이 | measurement | 도면치수 ±4mm | col10_SW/col10 | -| 치수-② | 높이 | measurement | 도면치수 ±40mm | col11_SH/col11 | -| 치수-③ | 간격 | check | 400 이하 → OK/NG | 고정 기준 | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 일련번호 | text | 자동 (제품 순번) | -| 2 | 가공상태 | check | 양호/불량 | -| 3 | 재봉상태 | check | 양호/불량 | -| 4 | 조립상태 | check | 양호/불량 | -| 5 | ①길이 | complex | sub_labels: ['도면치수', '측정값'] | -| 6 | ②높이 | complex | sub_labels: ['도면치수', '측정값'] | -| 7 | ③간격 | complex | sub_labels: ['기준치', 'OK/NG'] | -| 8 | 판정(적/부) | select | 자동계산 | - -**행 개수**: 발주 제품 수(estimateList)만큼 동적 생성 - ---- - -#### 5.2.3 슬랫(철재스라트) 중간검사 (Slat Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 슬랫 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 슬랫 - 중간 검사 성적서 | -| 5130 DB필드 | `recordslatMid` (JSON) | - -**섹션: 슬랫 검사 항목** - -| # | 검사항목 | 타입 | 기준 | 비고 | -|---|---------|------|------|------| -| 겉모양-1 | 가공상태 | check | 양호/불량 | - | -| 겉모양-2 | 조립상태 | check | 양호/불량 | 재봉상태 없음 (스크린과 차이) | -| 치수-① | 높이(1) | measurement | 16.5 ± 1mm | 고정 기준값 | -| 치수-② | 높이(2) | measurement | 14.5 ± 1mm | 고정 기준값 | -| 치수-③ | 길이(엔드락제외) | measurement | 도면치수 ±4mm | col10 | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 일련번호 | text | 자동 | -| 2 | 가공상태 | check | 양호/불량 | -| 3 | 조립상태 | check | 양호/불량 | -| 4 | ①높이 | complex | sub_labels: ['기준(16.5±1)', '측정값'] | -| 5 | ②높이 | complex | sub_labels: ['기준(14.5±1)', '측정값'] | -| 6 | ③길이 | complex | sub_labels: ['도면치수', '측정값'] | -| 7 | 판정(적/부) | select | 자동계산 | - -**행 개수**: 발주 제품 수(estimateSlatList)만큼 동적 생성 - -**스크린 vs 슬랫 차이점** - -| 항목 | 스크린 | 슬랫 | -|------|--------|------| -| 겉모양 | 3개 (가공/재봉/조립) | 2개 (가공/조립) | -| 치수①② | 길이·높이 (도면치수) | 높이(1)(2) (고정값 16.5/14.5) | -| 치수③ | 간격 (400이하, OK/NG) | 길이 (도면치수 ±4mm) | -| 공차 | ±4mm, ±40mm | ±1mm, ±1mm, ±4mm | - ---- - -#### 5.2.4 조인트바 중간검사 (Jointbar Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 조인트바 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 조인트바 - 중간 검사 성적서 | -| 5130 DB필드 | `recordjointbar` (JSON) | - -**섹션: 조인트바 검사 항목** - -| # | 검사항목 | 타입 | 기준값 | 공차 | -|---|---------|------|-------|------| -| 겉모양-1 | 가공상태 | check | 양호/불량 | - | -| 겉모양-2 | 조립상태 | check | 양호/불량 | - | -| 치수-① | 높이(1) | measurement | 16.5mm | ±1mm | -| 치수-② | 높이(2) | measurement | 14.5mm | ±1mm | -| 치수-③ | 길이(엔드락제외) | measurement | 300mm | ±4mm | -| 치수-④ | 간격 | measurement | 150mm | ±4mm | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 일련번호 | text | 자동 | -| 2 | 가공상태 | check | 양호/불량 | -| 3 | 조립상태 | check | 양호/불량 | -| 4 | ①높이 | complex | sub_labels: ['기준(16.5±1)', '측정값'] | -| 5 | ②높이 | complex | sub_labels: ['기준(14.5±1)', '측정값'] | -| 6 | ③길이 | complex | sub_labels: ['기준(300±4)', '측정값'] | -| 7 | ④간격 | complex | sub_labels: ['기준(150±4)', '측정값'] | -| 8 | 판정(적/부) | select | 자동계산 | - -**행 개수**: 단일 행 (제품 1건 단위 검사) - -**참조 이미지**: `jointbar_inspection.jpg` - ---- - -#### 5.2.5 4종 비교 요약 - -| 항목 | 절곡품 | 스크린 | 슬랫 | 조인트바 | -|------|--------|--------|------|---------| -| 겉모양 수 | 1 (절곡상태) | 3 (가공/재봉/조립) | 2 (가공/조립) | 2 (가공/조립) | -| 치수 항목 | 길이+너비+간격(가변) | 3 (길이/높이/간격) | 3 (높이×2/길이) | 4 (높이×2/길이/간격) | -| 행 구성 | 구성품별 (동적) | 발주제품별 (동적) | 발주제품별 (동적) | 단일 행 | -| 기준값 | 도면치수+포인트별 | 도면치수+고정(400) | 고정(16.5/14.5)+도면 | 전체 고정값 | -| 공차 | ±4mm/±2mm | ±4/±40mm | ±1/±1/±4mm | ±1/±1/±4/±4mm | -| 참조이미지 | 다수 (구성품별) | 별도 | 별도 | 1장 | -| 복잡도 | ★★★★★ (최고) | ★★★ | ★★☆ | ★☆ (최저) | - -#### 5.2.6 양식 시스템 매핑 전략 - -**현재 양식 시스템의 한계와 대응**: - -1. **조인트바/슬랫**: 현재 양식 구조(섹션+항목+컬럼)로 **그대로 표현 가능**. 수입검사와 동일 패턴으로 시더 생성. - -2. **스크린**: 겉모양 check 컬럼 + complex 측정 컬럼 조합으로 표현 가능. 행이 발주 제품별 동적이므로 **문서 생성 시 행 수를 결정**하는 로직 필요. - -3. **절곡품**: 제품 코드별로 검사 항목이 완전히 달라지므로 **가장 복잡**. 접근 방식: - - **Option A**: 포괄 양식 1개 + 문서 생성 시 해당 행만 활성화 - - **Option B**: 제품 유형별 양식 분리 (S1/S2/S3 별도) - - **Option C (권장)**: 기본 양식에 구성품 목록만 정의하고, **문서 생성 시 제품 코드에 따라 동적으로 행 구성** (Phase 3.3에서 구현) - -4. **check 컬럼 타입**: 현재 시스템에 `check` 컬럼 타입이 이미 존재. 양호/불량 체크박스로 사용 가능. - -### 5.3 중간검사 데이터 이관 설계 (Phase 3.2) - -> **5130 JSON 구조 → 새 양식 시스템(EAV) 매핑** - -#### 5.3.1 5130 JSON 공통 배열 구조 - -4종 모두 동일한 배열 인덱스 패턴: - -``` -recordXxxMid = [ - [0]: { approval: { writer: {name,date}, reviewer: {name,date}, approver: {name,date} } } - [1]: { inputValue: { ... } } ← 절곡: named object / 스크린·슬랫·조인트바: flat array - [2]: { num: "주문번호" } - [3]: { tablename: "output" } - [4]: { update_log: "..." } ← 슬랫·조인트바는 없음 - [5]: { checkboxData: [ {good:[], bad:[], judgement:""}, ... ] } ← 슬랫·조인트바는 [4] -] -``` - -#### 5.3.2 JSON → EAV 매핑 테이블 - -**결재 데이터 (JSON[0] → document_approvals)** - -| JSON 경로 | EAV 대상 | 비고 | -|-----------|---------|------| -| `[0].approval.writer.name` | `document_approvals` (step=1, user→name) | 작성자 | -| `[0].approval.writer.date` | `document_approvals` (step=1, acted_at) | mm/dd → datetime | -| `[0].approval.reviewer.name` | `document_approvals` (step=2, user→name) | 검토자 | -| `[0].approval.reviewer.date` | `document_approvals` (step=2, acted_at) | mm/dd → datetime | -| `[0].approval.approver.name` | `document_approvals` (step=3, user→name) | 승인자 | -| `[0].approval.approver.date` | `document_approvals` (step=3, acted_at) | mm/dd → datetime | - -**기본필드 (JSON[1].inputValue → document_data, section_id=null)** - -| JSON 경로 | field_key | 비고 | -|-----------|----------|------| -| `[1].inputValue.inspectdate` | `basic_inspectdate` | 검사일자 | -| `[1].inputValue.reviewer_sub` | `basic_reviewer` | 검사자 | -| `[1].inputValue.*_false_comment` | `footer_remark` | 부적합 내용 | -| `[1].inputValue.resultJudgement` | `footer_judgement` | 종합판정 | - -**절곡품 측정 데이터 (JSON[1].inputValue → document_data)** - -| JSON 경로 | field_key 패턴 | 비고 | -|-----------|---------------|------| -| `[1].inputValue.lengthMeasurement[i]` | `s{섹션}_r{i}_length` | 길이 측정값 | -| `[1].inputValue.widthMeasurement[i]` | `s{섹션}_r{i}_width` | 너비 측정값 | -| `[1].inputValue.gapMeasurement[i]` | `s{섹션}_r{i}_gap_{point}` | 간격 측정값 (포인트별) | - -**스크린/슬랫/조인트바 측정 데이터 (JSON[1].inputValue → document_data)** - -| JSON 경로 | field_key 패턴 | 비고 | -|-----------|---------------|------| -| `[1].inputValue[n]` (col{row}_input_{dim}) | `s{섹션}_r{row}_c{col}_sub{dim}` | 순차 인덱스 → 행·컬럼 매핑 | - -**체크박스 데이터 (JSON[5/4].checkboxData → document_data)** - -| JSON 경로 | field_key 패턴 | 비고 | -|-----------|---------------|------| -| `checkboxData[row].good[col]` | `s{섹션}_r{row}_c{checkCol}_good` | 양호 체크 | -| `checkboxData[row].bad[col]` | `s{섹션}_r{row}_c{checkCol}_bad` | 불량 체크 | -| `checkboxData[row].judgement` | `s{섹션}_r{row}_judgement` | 행별 판정 (적/부) | - -#### 5.3.3 이관 시 데이터 변환 규칙 - -| 변환 항목 | 5130 형식 | 새 시스템 형식 | 변환 로직 | -|----------|----------|-------------|----------| -| 날짜 (결재) | `"1/31"` (mm/dd) | `datetime` | 연도 추정 필요 (output.indate 기준) | -| 날짜 (검사) | `"2026-01-31"` | `date` | 그대로 사용 | -| 체크박스 | `true/false` | `"1"/"0"` | boolean → string | -| 판정 | `"적"/"부"` | `"적"/"부"` | 그대로 사용 | -| 종합판정 | `"합격"/"불합격"` | `"합격"/"불합격"` | 그대로 사용 | -| 측정값 | `number/string` | `string` | EAV field_value는 string | -| 결재자 이름 | `string` | `user_id (FK)` | 이름→사용자 테이블 매칭 필요 | - -#### 5.3.4 이관 프로세스 설계 - -``` -Step 1: output 테이블에서 recordXxxMid IS NOT NULL 레코드 추출 - ↓ -Step 2: 각 레코드에 대해 해당 양식 템플릿 매핑 - - recordbendingMid → 절곡품 중간검사 양식 (template_id) - - recordscreenMid → 스크린 중간검사 양식 - - recordslatMid → 슬랫 중간검사 양식 - - recordjointbar → 조인트바 중간검사 양식 - ↓ -Step 3: documents 테이블에 문서 생성 - - template_id, tenant_id, document_no (MID-YYMMDD-NN) - - title: "{양식명} - {현장명}" - - status: APPROVED (이미 완료된 검사) - - created_at: output.indate 기준 - ↓ -Step 4: document_approvals 생성 - - JSON[0].approval → 3개 결재 레코드 - - 이름→user_id 매칭 (매칭 실패 시 created_by = system) - - status: APPROVED, acted_at: 변환된 날짜 - ↓ -Step 5: document_data (EAV) 생성 - - 기본필드: inspectdate, reviewer → field_key 매핑 - - 체크박스: checkboxData → good/bad/judgement 매핑 - - 측정값: inputValue → 행·컬럼 인덱스 매핑 - - Footer: false_comment → footer_remark, resultJudgement → footer_judgement - ↓ -Step 6: 검증 - - 원본 JSON과 변환 결과 대조 - - 종합판정·행별 판정 일치 확인 -``` - -#### 5.3.5 이관 대상 규모 추정 - -| 검사 종류 | DB 필드 | 조건 | 비고 | -|----------|---------|------|------| -| 절곡품 | recordbendingMid | IS NOT NULL AND != '' AND != '{}' | output 테이블 | -| 스크린 | recordscreenMid | 동일 | output 테이블 | -| 슬랫 | recordslatMid | 동일 | output 테이블 | -| 조인트바 | recordjointbar | 동일 | output 테이블 | - -> ⚠️ 실제 레코드 수는 5130 DB 조회 필요 (Phase 3.2 완료 기준 설계만 완성, 실행은 Phase 4 이후) - -#### 5.3.6 이관 시 주의사항 - -1. **절곡품 inputValue 구조 차이**: 절곡품만 named object (`lengthMeasurement[]`, `widthMeasurement[]`, `gapMeasurement[]`), 나머지 3종은 flat array. 이관 스크립트에서 분기 처리 필요. - -2. **update_log 유무**: 스크린만 별도 `update_log` 컬럼 업데이트. 슬랫·조인트바는 JSON 내부에만 포함 (실제로는 비어있을 수 있음). - -3. **결재자 이름 매칭**: 5130의 결재자는 문자열 이름만 저장. 새 시스템의 user_id(FK)로 변환 시 users 테이블에서 name 매칭 필요. 동명이인 주의. - -4. **행 수 불일치 가능성**: 5130에서 발주 제품 수에 따라 행이 동적 생성됨. 이관 시 원본 행 수 보존 필요. - -5. **이미지 참조**: 5130 JSON에는 이미지 참조명(`guiderail_wall_mid` 등)이 포함됨. 이관 시 새 시스템의 이미지 경로로 변환 필요. - -### 5.4 검사 기준 이미지 이관 (Phase 3.4 완료) - -`5130/img/inspection/` → `mng/public/img/inspection/` (27개 파일) - -| 분류 | 파일명 | 용도 | -|------|--------|------| -| 절곡-기준서 | `bending_inspection1.jpg`, `bending_inspection2.jpg` | 가이드레일/케이스/하단 기준 | -| 가이드레일-벽면 | `guiderail_wall_mid.jpg`, `_KSS02.jpg`, `_add.jpg`, `_slat.jpg`, `_add_slat.jpg`, `_slatKQTS01.jpg` | S1/S2/S3/슬랫변형 | -| 가이드레일-측면 | `guiderail_side_mid.jpg`, `_KSS02.jpg`, `_add.jpg`, `_slat.jpg`, `_add_slat.jpg`, `_slatKQTS01.jpg` | S1/S2/S3/슬랫변형 | -| 하단마감재 | `bottombar_KSS01KWE01.jpg`, `_add.jpg`, `_KTE01KQTS01.jpg`, `_add.jpg` | 표준/특수마감 | -| 케이스 | `box_both.jpg`, `box_both500x380.jpg`, `box_bottom.jpg`, `box_rear.jpg` | 양면/밑면/후면 | -| 기타 | `Lbar_mid.jpg`, `smoke.jpg` | L-BAR, 연기차단재 | -| 스크린 | `screen_inspection.jpg` | 스크린 기준서 | -| 슬랫 | `slat_inspection.jpg` | 슬랫 기준서 | -| 조인트바 | `jointbar_inspection.jpg` | 조인트바 기준서 | - -**접근 URL**: `https://mng.sam.kr/img/inspection/{filename}.jpg` - ---- - -## 6. 기술 결정사항 - -### 6.1 확정된 사항 - -| 항목 | 결정 | 이유 | -|------|------|------| -| 양식 관리 위치 | mng (Laravel + Blade) | 관리자 전용, HTMX 기반 UI 이미 존재 | -| 데이터 저장 패턴 | EAV (document_data 테이블) | 이미 설계됨, 동적 필드 지원 | -| 문서 상태 | DRAFT -> PENDING -> APPROVED/REJECTED/CANCELLED | 이미 구현됨 | -| API 제공 | api 저장소 (Laravel REST API) | SAM 표준 아키텍처 | -| 프론트엔드 소비 | react (Next.js) JSON 렌더링 | 기존 document-system 컴포넌트 확장 | - -### 6.2 검토 완료 사항 (2026-01-31 확정) - -| # | 항목 | 결정 | 근거 | -|---|------|------|------| -| 1 | PDF 생성 | **추후 고려** | react에 이미 구현됨 (html2pdf.js + DocumentViewer). mng 단계에서는 PDF 불필요 | -| 2 | 검사 판정 로직 | **프론트에서 입력, 결과만 저장** | 양식이 검사항목/기준을 정의하고, 프론트에서 사용자가 입력. 저장 시 입력값+판정 결과를 그대로 저장. 별도 판정 엔진 불필요 | -| 3 | 양식 버전 관리 | **수정 시 새 버전 생성** | 요청마다 검사 기준이 다를 수 있으므로 버전 관리 필수. document_templates에 version 컬럼 추가 필요 | -| 4 | 기존 react 컴포넌트 전환 | **기존 react 미수정** | mng에서 JSON 기반 화면 구현까지만 개발. 이후 프론트엔드 담당자와 협의하여 react 전환 여부 결정 | - ---- - -## 7. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | API 엔드포인트 추가 | `/api/v1/document-templates` (2), `/api/v1/documents` (5+4결재) | api 저장소 | ✅ Phase 4.1 완료 | -| 2 | DB 마이그레이션 변경 여부 | 기존 테이블로 충분한지 vs version 컬럼 추가 필요 (6.2 #3 확정) | api 저장소 | ⏳ Phase 1 중 | -| 3 | ~~검사 판정 로직 위치~~ | ~~프론트 vs 백엔드~~ → **프론트 입력, 결과만 저장** | - | ✅ 해결됨 (6.2 #2) | -| 4 | ~~PDF 생성 방식~~ | ~~클라이언트 vs 서버~~ → **추후 고려** (react 기 구현) | - | ✅ 해결됨 (6.2 #1) | - ---- - -## 8. 변경 이력 - -> 📎 별도 파일로 관리: [`document-management-system-changelog.md`](./document-management-system-changelog.md) - ---- - -## 9. 참고 문서 및 파일 - -### 프로젝트 문서 -- `docs/specs/database-schema.md` - DB 스키마 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `mng/CLAUDE.md` - MNG 프로젝트 규칙 - -### 기존 코드 (mng) -- `mng/app/Models/DocumentTemplate.php` - 양식 모델 -- `mng/app/Models/Documents/Document.php` - 문서 모델 -- `mng/app/Http/Controllers/DocumentTemplateController.php` - 양식 컨트롤러 -- `mng/app/Http/Controllers/DocumentController.php` - 문서 컨트롤러 -- `mng/resources/views/document-templates/edit.blade.php` - 양식 편집 UI (44.5KB) -- `mng/routes/web.php` 340-353줄 - 라우트 - -### 기존 코드 (react) -- `react/src/components/document-system/` - 문서 공통 시스템 -- `react/src/app/[locale]/(protected)/quality/qms/components/documents/` - QMS 검사 문서 -- `react/src/components/production/WorkerScreen/WorkLogContent.tsx` - 작업일지 -- `react/src/components/production/WorkOrders/documents/` - 중간검사 - -### 5130 레거시 -- `5130/instock/fetch_inspection.php` - 수입검사 메인 로더 (21.8KB) -- `5130/instock/i_*.php` - 자재별 수입검사 페이지 (40+) -- `5130/output/viewMidInspect*.php` - 중간검사 성적서 -- `5130/output/viewinspectionJointbar.php` - 조인트바 검사 -- `5130/img/inspection/` - 검사 기준 이미지 (20+) - -### DB 마이그레이션 -- `api/database/migrations/2026_01_26_200000_create_document_templates_table.php` -- `api/database/migrations/2026_01_28_200000_create_documents_table.php` - ---- - -## 11. Phase별 상세 실행 절차 - -> 각 Phase 작업 시 이 섹션을 먼저 읽고 진행한다. - -### 11.1 Phase 1.1 - 기존 document-templates 편집 UI 점검 및 보완 - -**목표**: `mng.sam.kr/document-templates/{id}/edit`에서 수입검사 양식에 필요한 모든 구성요소를 관리할 수 있는지 확인하고 부족한 부분을 보완한다. - -**사전 조건**: 없음 (첫 번째 작업) - -**실행 절차**: - -``` -Step 1: 현재 UI 분석 -├── mng/resources/views/document-templates/edit.blade.php (44.5KB) 읽기 -├── 기존 기능 목록 정리: -│ - 양식 기본정보 (이름, 카테고리, 제목, 회사명) 편집 가능? -│ - 결재라인 (approval_lines) CRUD 가능? -│ - 기본필드 (basic_fields) CRUD 가능? -│ - 섹션 (sections) CRUD 가능? -│ - 섹션 항목 (section_items) CRUD 가능? -│ - 컬럼 (columns) CRUD 가능? -│ - footer_remark_label, footer_judgement_label, footer_judgement_options 편집 가능? -└── 누락된 기능 목록화 - -Step 2: 브라우저에서 실제 동작 확인 -├── https://mng.sam.kr/document-templates 접속 -├── 기존 양식 편집 시도 (or 새 양식 생성 후 편집) -├── 각 탭/섹션별 CRUD 동작 확인 -└── JS 에러, 저장 실패 등 이슈 기록 - -Step 3: 보완 작업 -├── 누락된 CRUD 기능 구현 (Blade + HTMX + Alpine.js) -├── DocumentTemplateController 메서드 보강 -├── 유효성 검증 추가 (FormRequest 패턴) -└── 섹션 항목(section_items)의 drag-drop 정렬 (있는 경우 확인, 없으면 sort_order 수동 관리) - -Step 4: 검증 -├── 새 양식 생성 → 모든 하위 요소 추가 → 저장 → DB 확인 -├── 기존 양식 수정 → 저장 → 정상 반영 확인 -└── 양식 삭제 → 하위 요소 cascade 삭제 확인 -``` - -### 11.2 Phase 1.2 - 5130 수입검사 데이터 분석 - -**목표**: 5130의 자재별 수입검사 파일을 분석하여, 양식 시드 데이터로 변환할 수 있는 구조화된 데이터를 생성한다. - -**상태**: ✅ 완료 (2026-01-31, 경량 분석) - -**분석 결과**: - -#### 라우팅 구조 - -`5130/instock/common/viewJS.php`의 `viewBoardInstock()` 함수가 **item_name(품명) 기준 switch-case**로 개별 검사 페이지(`i_*.php`)를 팝업 호출한다. - -- `fetch_inspection.php` = 데이터 입력 폼 (목록에서 호출) -- `i_*.php` = 검사 성적서 뷰 (viewinspection 버튼에서 호출) -- 총 23개 파일, 품명별 1:1 또는 N:1 매핑 - -#### 자재 → 검사파일 매핑 (23개) - -| 품명 | 파일 | 비고 | -|---|---|---| -| EGI1.55T, EGI1.15T, EGI1.6T | `i_EGI155.php` | 전기아연도금강판 | -| SUS1.55T, SUS1.5T, SUS1.2T | `i_SUSplate.php` | 스테인리스강판 | -| GI0.5T, GI0.45T | `i_GIplate.php` | 아연도금강판 | -| 앵글 | `i_angle.php` | | -| 받침용앵글 | `i_anglebottom.php` | | -| 방화유리 | `i_antifireglass.php` | | -| 절곡코일(EGI) | `i_bendingcoil.php` | spec 앞3자=EGI | -| 베어링부 | `i_bracket.php` | | -| 바이오세라크울96K | `i_cerakwool.php` | | -| 연동제어기 | `i_controller.php` | | -| 화이바원단 | `i_fiber.php` | | -| 내화충진재 | `i_Fireproof_sealings.php` | | -| 내화실 | `i_fireproofWire.php` | | -| 전동개폐기 | `i_motor.php` | | -| 평철 | `i_platesteel.php` | | -| 마환봉 | `i_pole.php` | | -| 각파이프 | `i_recpipe.php` | | -| 감기샤프트 | `i_shaft.php` | | -| 실리카원단 | `i_sillica.php` | | -| 슬랫코일 | `i_slatcoil.php` | | -| 절곡코일(SUS) | `i_SUScoil.php` | spec 앞3자=SUS | -| 와이어원단 | `i_wire.php` | 기본 | -| 와이어원단(대한) | `i_wireDaehan.php` | remarks에 '대한' 포함 시 | - -#### 대표 자재 분석: EGI 1.55T (`i_EGI155.php`) - -| NO | 검사항목 | 검사기준 | 검사방식 | 검사주기 | 데이터 타입 | -|---|---|---|---|---|---| -| 1 | 겉모양 | 사용상 해로울 결함이 없을 것 | 육안검사 | n=3, c=0 | OK/NG 체크 ×3 | -| 2 | 치수-두께 | 두께별 허용범위 (±0.07~±0.12, 4구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-너비 | 1250 미만: +7/-0 | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-길이 | 길이별 허용범위 (+10~+20/-0, 3구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 3 | 인장강도 (N/mm²) | 270 이상 | 밀시트 | 입고시 | 단일값 | -| 4 | 연신율 (%) | 두께별 36~38 이상 (3구간) | 밀시트 | 입고시 | 단일값 | -| 5 | 아연 최소 부착량 (g/m²) | 한면 17 이상 | 밀시트 | 입고시 | 단일값 | - -#### 대표 자재 분석: SUS Plate (`i_SUSplate.php`) - -| NO | 검사항목 | 검사기준 | 검사방식 | 검사주기 | 데이터 타입 | -|---|---|---|---|---|---| -| 1 | 겉모양 | 사용상 해로울 결함이 없을 것 | 육안검사 | n=3, c=0 | OK/NG 체크 ×3 | -| 2 | 치수-두께 | 두께별 허용범위 (±0.10~±0.12, 2구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-너비 | 1250 미만: +7/-0 | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-길이 | 길이별 허용범위 (+10~+20/-0, 2구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 3 | 항복강도 (N/mm²) | 205 이상 | 밀시트 | 입고시 | 단일값 | -| 4 | 인장강도 (N/mm²) | 520 이상 | 밀시트 | 입고시 | 단일값 | -| 5 | 연신율 (%) | 40 이상 | 밀시트 | 입고시 | 단일값 | -| 6 | 경도 (HV) | 200 이하 | 밀시트 | 입고시 | 단일값 | - -#### 공통 패턴 요약 - -**공통 구조 (모든 자재 동일):** -- **결재**: 담당 / 부서장 (2단계) -- **기본정보**: 품명, 규격(두께×너비×길이), 납품업체/제조업체, 로트번호, 자재번호, 검사일자, 로트크기, 검사자 -- **검사 테이블 컬럼**: NO / 검사항목 / 검사기준 / 검사방식 / 검사주기 / 측정치(n1,n2,n3) / 판정(적/부) -- **Footer**: 부적합 내용 + 종합판정(합격/불합격) -- **판정 로직**: JS 자동 계산 (모든 항목 적→합격, 하나라도 부→불합격) -- **저장**: JSON(`iList` hidden field) → AJAX POST → `insert_iList.php` - -**자재별 차이점:** -- 검사항목 수/종류 (EGI: 5항목 7행, SUS: 6항목 8행) -- 기준값 범위 (두께별 허용 오차, 강도/경도 기준 등) -- 두께 범위 구간 수 (EGI: 4구간, SUS: 2구간) -- 밀시트 항목 차이 (EGI: 인장+연신+아연, SUS: 항복+인장+연신+경도) - -> **결론**: 나머지 21개 자재는 Phase 1.3 시드 데이터 생성 시 개별 분석하면서 병행 진행 - -### 11.3 Phase 1.3 - 수입검사 양식 시드 데이터 생성 - -**실행 절차**: - -``` -Step 1: Seeder 파일 생성 -├── mng/database/seeders/IncomingInspectionTemplateSeeder.php 생성 -├── 1.2에서 정리한 데이터 기반 -└── 주요 자재 10종 양식 생성 (EGI, SUS, GI, Wire, Motor, Angle 등) - -Step 2: 실행 및 검증 -├── php artisan db:seed --class=IncomingInspectionTemplateSeeder -├── mng.sam.kr/document-templates 에서 목록 확인 -└── 각 양식 편집 화면에서 데이터 정합성 확인 -``` - ---- - -## 12. 자기완결성 점검 결과 - -### 12.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 각 Phase 작업 항목에 "완료 기준" 컬럼 추가됨 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 3 Phase 1-5 + 섹션 11 상세 절차 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 DB/모델/컨트롤러 현황 + 새 세션 가이드 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 절대 경로 + 상대 경로 모두 명시 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 11 Phase별 Step-by-step 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 각 Phase 완료 기준에 검증 방법 포함 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/테이블/컬럼/URL 명시 | - -### 12.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 🚀 새 세션 시작 가이드 + 📍 현재 진행 상태 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 새 세션 가이드 "핵심 파일" + 2.2 + 9. 참고 파일 | -| Q4. 작업 완료 확인 방법은? | ✅ | 각 Phase "완료 기준" 컬럼 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 + 새 세션 가이드 | -| Q6. mng 기술 스택과 로컬 환경은? | ✅ | 새 세션 가이드 "프로젝트 정보" | -| Q7. 모델 관계와 DB 구조는? | ✅ | 새 세션 가이드 "모델 관계 구조" + 2.1 | -| Q8. Phase 1.1의 구체적 첫 단계는? | ✅ | 11.1 상세 실행 절차 | - -**결과**: 8/8 통과 - 자기완결성 확보 - -### 12.3 보완 이력 - -| 날짜 | 항목 | 원본 | 보완 내용 | -|------|------|------|----------| -| 2026-01-31 | 초기 검증 | - | 5/5 통과 | -| 2026-01-31 | 자기완결성 강화 | 새 세션에서 시작 불가 | 🚀 새 세션 시작 가이드 추가, 절대 경로/기술 스택/모델 코드 인라인, Phase 완료 기준 추가, 섹션 11 상세 실행 절차 추가, 컨펌 대기 목록 해결 항목 반영 | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/hotfix-20260119-action-plan.md b/plans/hotfix-20260119-action-plan.md deleted file mode 100644 index c355d72..0000000 --- a/plans/hotfix-20260119-action-plan.md +++ /dev/null @@ -1,286 +0,0 @@ -# Hotfix 단위테스트 분석 및 액션 플랜 (2026-01-19) - -## 개요 - -**분석 대상 커밋**: `121b427c899cd37e273eaf08459dd5a3072da670` -**커밋 메시지**: 1/19 단위테스트 -**분석 일시**: 2026-01-19 -**작성자**: Claude Code - ---- - -## 테스트 결과 요약 - -| 구분 | 건수 | 비율 | -|------|------|------| -| ✅ 통과 (PASS) | 37개 | 92.5% | -| ⚠️ 스킵 - 페이지 미구현 | 2개 | 5.0% | -| ⚠️ 스킵 - 데이터 없음 | 1개 | 2.5% | -| **총계** | **40개** | **100%** | - ---- - -## 🔴 긴급 (P0) - 페이지 미구현 - -### 1. 근태 설정 페이지 - -| 항목 | 내용 | -|------|------| -| **URL** | `/ko/settings/attendance` | -| **현재 상태** | 404 Not Found | -| **우선순위** | P0 (긴급) | -| **담당** | React 프론트엔드 | -| **비고** | API 이미 존재 (WorkSettingController) | - -#### 필요 작업 -- [x] API 존재 확인 완료 (WorkSettingController) -- [ ] React 페이지 개발 -- [ ] API 연동 - -#### 예상 기능 -- 출퇴근 시간 설정 -- 지각/조퇴 기준 설정 -- 휴일 설정 -- 근태 알림 설정 - ---- - -### 2. 미수금현황 페이지 - -| 항목 | 내용 | -|------|------| -| **URL** | `/ko/accounting/receivables` | -| **현재 상태** | 404 Not Found | -| **우선순위** | P0 (긴급) | -| **담당** | React 프론트엔드 | -| **비고** | API 이미 존재 (ReceivablesController) | - -#### 필요 작업 -- [x] API 존재 확인 완료 (ReceivablesController) - - `GET /api/v1/receivables` - 목록 - - `GET /api/v1/receivables/summary` - 요약 - - `PUT /api/v1/receivables/memos` - 메모 업데이트 - - `PUT /api/v1/receivables/overdue-status` - 연체 상태 -- [ ] React 페이지 개발 (프론트엔드) -- [ ] API 연동 - -#### 예상 기능 -- 거래처별 미수금 현황 -- 기간별 미수금 추이 -- 연체 미수금 관리 -- 미수금 알림 설정 - ---- - -## 🟡 중요 (P1) - 데이터 정합성 이슈 - -### 1. 입금관리 - 입금유형 미설정 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/deposits` | -| **문제** | 입금유형 미설정 59건 / 60건 (98.3%) | -| **영향** | 입금 분류 및 통계 정확도 저하 | -| **우선순위** | P1 | - -#### 개선 방안 -- [ ] 입금유형 일괄 설정 기능 추가 -- [ ] 입금 등록 시 유형 필수 선택 옵션 -- [ ] 미설정 데이터 경고 배너 추가 - ---- - -### 2. 출금관리 - 출금유형 미설정 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/withdrawals` | -| **문제** | 출금유형 미설정 58건 / 60건 (96.7%) | -| **영향** | 출금 분류 및 통계 정확도 저하 | -| **우선순위** | P1 | - -#### 개선 방안 -- [ ] 출금유형 일괄 설정 기능 추가 -- [ ] 출금 등록 시 유형 필수 선택 옵션 -- [ ] 미설정 데이터 경고 배너 추가 - ---- - -### 3. 매입관리 - 매입유형/세금계산서 미설정 ✅ 완료 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/purchase` | -| **문제** | 매입유형 미설정 69건, 세금계산서 수취 미확인 69건 / 70건 (98.6%) | -| **영향** | 매입 분류, 세무 처리 누락 가능성 | -| **우선순위** | P1 | -| **상태** | ✅ API 완료 (2026-01-19) | - -#### 개선 방안 -- [x] 매입유형/세금계산서 일괄 설정 기능 → API 완료 - - `POST /api/v1/purchases/bulk-update-type` - 매입유형 일괄 변경 - - `POST /api/v1/purchases/bulk-update-tax-received` - 세금계산서 수취 일괄 설정 -- [ ] 매입 등록 시 필수 항목 검증 강화 -- [ ] 세무 신고 전 미설정 데이터 체크 기능 - ---- - -### 4. 매출관리 - 세금계산서/거래명세서 미발행 ✅ API 완료 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/sales` | -| **문제** | 세금계산서 발행대기 81건, 거래명세서 발행대기 81건 (100%) | -| **영향** | 세금계산서/거래명세서 발행 누락 | -| **우선순위** | P1 | -| **상태** | ✅ API 완료 (2026-01-19) | - -#### 기존 API (개별 발행) -- `POST /api/v1/tax-invoices/{id}/issue` - 세금계산서 개별 발행 -- `POST /api/v1/sales/{id}/statement/issue` - 거래명세서 개별 발행 - -#### 일괄 발행 API (신규) -- [x] `POST /api/v1/tax-invoices/bulk-issue` - 세금계산서 일괄 발행 -- [x] `POST /api/v1/sales/bulk-issue-statement` - 거래명세서 일괄 발행 - -#### 개선 방안 -- [x] 세금계산서 일괄 발행 API 개발 → 완료 -- [x] 거래명세서 일괄 발행 API 개발 → 완료 -- [ ] 자동 발행 로직 검토 (매출 등록 시 자동 발행 옵션) -- [ ] 발행 대기 데이터 대시보드 알림 -- [ ] React 프론트엔드 연동 - ---- - -## 🟢 개선 (P2) - 선택 사항 - -### 1. 관리자 대시보드 알림 강화 -- [ ] 데이터 미설정 건수 위젯 추가 -- [ ] 미발행 문서 건수 알림 -- [ ] 페이지 미구현 상태 모니터링 - -### 2. 데이터 품질 관리 -- [ ] 데이터 미설정 시 경고 아이콘 표시 -- [ ] 일별/주별 데이터 품질 리포트 -- [ ] 자동 데이터 정합성 체크 배치 - ---- - -## 정상 동작 기능 목록 (37개) - -
-전체 목록 펼치기 - -### 결재 시스템 (3개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 결재함 | approval-box | /ko/approval/inbox | -| 기안함 | draft-box | /ko/approval/draft | -| 참조함 | reference-box | /ko/approval/reference | - -### 인사관리 (12개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 근태현황 | attendance-checkin | /hr/attendance | -| 근태관리 | attendance-management | /hr/attendance-management | -| 근태 사유 | attendance-reason | /hr/attendance-management | -| 근태 등록 | attendance-register | /hr/attendance-management | -| 사원관리 | employee-register | /ko/hr/employee-management | -| 부서관리 | department-add | /ko/hr/department-management | -| 직급관리 | rank-management | /ko/settings/ranks | -| 휴가관리 | vacation-management | /ko/hr/vacation-management | -| 휴가정책 | leave-policy | /ko/settings/leave-policy | -| 급여관리 | salary-management | /ko/hr/salary-management | -| 카드관리 | card-add | /ko/hr/card-management | -| 근무일정 | work-schedule | /ko/settings/work-schedule | - -### 회계관리 (10개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 입금관리 | deposit-management | /ko/accounting/deposits | -| 출금관리 | withdrawal-management | /ko/accounting/withdrawals | -| 매입관리 | purchase-management | /ko/accounting/purchase | -| 매출관리 | sales-management | /ko/accounting/sales | -| 거래처관리 | vendor-management | /ko/accounting/vendors | -| 거래처원장 | vendor-ledger | /ko/accounting/vendor-ledger | -| 카드거래 | card-transactions | /ko/accounting/card-transactions | -| 대손채권회수 | bad-debt-collection | /accounting/bad-debt-collection | -| 일일 일보 | daily-report | /ko/accounting/daily-report | -| 지출 예상 내역서 | expected-expenses | /ko/accounting/expected-expenses | - -### 게시판 (4개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 게시판관리 | board-management | /ko/board/board-management | -| 게시판 | board-test | /ko/boards/board_mjsgri54_1fmg | -| 자유게시판 | free-board | /ko/boards/free | -| 1:1 문의 | customer-inquiry | /ko/customer-center/qna | - -### 생산관리 (3개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 품목관리 | item-management | /ko/production/screen-production | -| 생산 현황판 | production-dashboard | /ko/production/dashboard | -| 작업지시 관리 | work-order-management | /ko/production/work-orders | - -### 설정 (4개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 회사정보 | company-info | /ko/company-info | -| 권한관리 | permission-management | /ko/settings/permissions | -| 알림설정 | notification-settings | /ko/settings/notification-settings | -| 팝업관리 | popup-management | /ko/settings/popup-management | - -### 기타 (2개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 로그인 | login | /login | -| 결제내역 | payment-history | /ko/payment-history | - -
- ---- - -## 작업 일정 (권장) - -```mermaid -gantt - title Hotfix 작업 일정 - dateFormat YYYY-MM-DD - section P0 긴급 - 근태 설정 페이지 개발 :2026-01-20, 3d - 미수금현황 페이지 개발 :2026-01-20, 3d - section P1 중요 - 입금/출금 유형 일괄설정 :2026-01-23, 2d - 매입/매출 데이터 정합성 :2026-01-25, 2d - section P2 개선 - 대시보드 알림 강화 :2026-01-27, 2d -``` - ---- - -## 담당자 배정 (제안) - -| 우선순위 | 작업 | 담당 | 상태 | -|----------|------|------|------| -| P0 | 근태 설정 페이지 | React 프론트엔드 | ⬜ 대기 (API 존재) | -| P0 | 미수금현황 페이지 | React 프론트엔드 | ⬜ 대기 (API 존재) | -| P1 | 입금유형 일괄설정 | React 프론트엔드 | ✅ API 이미 존재 | -| P1 | 출금유형 일괄설정 | React 프론트엔드 | ✅ API 이미 존재 | -| P1 | 매입 데이터 정합성 | React 프론트엔드 | ✅ API 완료 (2026-01-19) | -| P1 | 매출 문서 발행 | api 백엔드 + React 프론트엔드 | ✅ API 완료 (2026-01-19) | -| P2 | 대시보드 알림 | React 프론트엔드 | ⬜ 대기 | - ---- - -## 참고 자료 - -- 테스트 결과 파일: `hotfix/*_2026-01-19_test.md` (40개) -- Serena 메모리: `hotfix-test-analysis-20260119.md` -- 관련 커밋: `121b427c899cd37e273eaf08459dd5a3072da670` - ---- - -**문서 버전**: 1.0 -**최종 수정**: 2026-01-19 -**다음 검토**: 작업 완료 후 \ No newline at end of file diff --git a/plans/index_plans.md b/plans/index_plans.md index 9cd0f4d..6b42d60 100644 --- a/plans/index_plans.md +++ b/plans/index_plans.md @@ -1,7 +1,7 @@ # 기획 문서 인덱스 > SAM 시스템 개발 계획 및 기획 문서 모음 -> **최종 업데이트**: 2026-02-22 +> **최종 업데이트**: 2026-02-26 --- @@ -9,242 +9,157 @@ | 분류 | 개수 | 설명 | |------|------|------| -| 진행중/대기 계획서 | 44개 | 기능별 개발 계획 | -| 완료 아카이브 | 37개 | `archive/` 폴더에 보관 | +| 🟡 진행중 (ACTIVE) | 18개 | 현재 작업중인 계획 | +| ⚪ 대기 (PLANNED) | 19개 | 미착수/선행조건 대기 | +| 완료 히스토리 | 40건 | `archive/HISTORY.md`에 요약 | | 스토리보드 | 1개 | ERP 화면 설계 (D1.0) | -| 플로우 테스트 | 32개 | API 검증용 JSON 테스트 케이스 | +| 플로우 테스트 | 32개 | API 검증용 JSON | -> **Note**: 완료된 계획 37개는 `archive/` 폴더로 이동됨 (최종 정리: 2026-02-22) +> **문서 관리 가이드**: [GUIDE.md](./GUIDE.md) --- -## 개발 계획서 (진행중/대기) +## 진행중 (ACTIVE) - 18개 -### ERP API 개발 +### ERP API -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [erp-api-development-plan.md](./erp-api-development-plan.md) | 🟡 진행중 | Phase 3/L | SAM ERP API 전체 개발 계획, L-2 React 연동 대기 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [erp-api-development-plan.md](./erp-api-development-plan.md) | Phase L | SAM ERP API, L-2 React 연동 대기 | -### 견적/수주 (Quote/Order) +### 견적/수주 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) | 🟡 진행중 | 4/5 (80%) | 경동 견적 로직, Phase 5 통합 테스트 미완 | -| [quote-management-url-migration-plan.md](./quote-management-url-migration-plan.md) | 🟡 진행중 | 11/12 (92%) | URL 마이그레이션, 사용자 테스트 잔여 | -| [quote-management-8issues-plan.md](./quote-management-8issues-plan.md) | ⚪ 대기 | 0/8 (0%) | 견적관리 8개 이슈, 컨펌 대기 | -| [quote-calculation-api-plan.md](./quote-calculation-api-plan.md) | ⚪ 대기 | 0/12 (0%) | 견적 계산 API, 미착수 | -| [quote-order-sync-improvement-plan.md](./quote-order-sync-improvement-plan.md) | ⚪ 대기 | 0/4 (0%) | 견적-수주 동기화 개선, 미착수 | -| [quote-system-development-plan.md](./quote-system-development-plan.md) | ⚪ 대기 | - | 견적 시스템 개발, 계획 수립 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) | 80% | 경동 견적 로직, Phase 5 통합테스트 직전 | +| [product-code-traceability-plan.md](./product-code-traceability-plan.md) | - | 제품코드 추적성 개선 | -### 생산/절곡 (Production/Bending) +### 품목/BOM -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [bending-preproduction-stock-plan.md](./bending-preproduction-stock-plan.md) | 🟡 진행중 | 14/14 코드 | 선재고, 마이그레이션 실행/검증 잔여 | -| [bending-info-auto-generation-plan.md](./bending-info-auto-generation-plan.md) | ⚪ 대기 | 0/7 (0%) | 절곡 정보 자동 생성, 분석만 완료 | -| [bending-material-input-mapping-plan.md](./bending-material-input-mapping-plan.md) | ⚪ 대기 | 분석 | 절곡 자재투입 매핑, GAP 분석 완료 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [bom-item-mapping-plan.md](./bom-item-mapping-plan.md) | 66% | BOM 품목 매핑, Phase 3 검증 잔여 | +| [item-master-data-alignment-plan.md](./item-master-data-alignment-plan.md) | - | 품목 마스터 정합, 섀도잉 정리 | +| [fg-code-consolidation-plan.md](./fg-code-consolidation-plan.md) | 분석완료 | FG 코드 통합, Phase 1 착수 전 | -### 품목/BOM (Item/BOM) +### 문서/서식 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [bom-item-mapping-plan.md](./bom-item-mapping-plan.md) | 🟡 진행중 | 2/3 (66%) | BOM 품목 매핑, Phase 3 검증 잔여 | -| [item-master-data-alignment-plan.md](./item-master-data-alignment-plan.md) | 🟡 진행중 | - | 품목 마스터 정합, 섀도잉 정리 잔여 | -| [mng-item-field-management-plan.md](./mng-item-field-management-plan.md) | ⚪ 대기 | 0% | 품목 필드 관리, 미착수 | -| [item-inventory-management-plan.md](./item-inventory-management-plan.md) | ⚪ 대기 | 설계 | 품목 재고 관리, 설계 확정/구현 대기 | -| [fg-code-consolidation-plan.md](./fg-code-consolidation-plan.md) | ⚪ 대기 | 0/8 (0%) | FG 코드 통합, 미착수 | - -### 문서/서식 (Document System) - -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [document-management-system-plan.md](./document-management-system-plan.md) | 🟡 진행중 | 16/20 (80%) | 문서관리 시스템, Phase 4.4 잔여 | -| [document-system-master.md](./document-system-master.md) | 🟡 진행중 | Phase 4-5 | 마스터 문서, 일부 Phase 잔여 | -| [document-system-mid-inspection.md](./document-system-mid-inspection.md) | 🟡 진행중 | 5/6 | 중간검사, 1개 미완 | -| [document-system-work-log.md](./document-system-work-log.md) | 🟡 진행중 | 3/4+α | 작업일지, React 연동 잔여 | -| [incoming-inspection-document-integration-plan.md](./incoming-inspection-document-integration-plan.md) | ⚪ 대기 | 0/8 (0%) | 수입검사 서류 연동, 분석만 완료 | -| [incoming-inspection-templates-plan.md](./incoming-inspection-templates-plan.md) | 🟡 진행중 | 19/23 (83%) | 수입검사 템플릿, 4종 품목 대기 | -| [intermediate-inspection-report-plan.md](./intermediate-inspection-report-plan.md) | ⚪ 대기 | 0/14 (0%) | 중간검사 보고서, 검토 대기 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [document-system-master.md](./document-system-master.md) | Phase 4-5 | 문서 시스템 마스터 | +| [document-system-mid-inspection.md](./document-system-mid-inspection.md) | 5/6 | 중간검사, 결재만 남음 | +| [document-system-work-log.md](./document-system-work-log.md) | 3/4+α | 작업일지, React 연동 잔여 | +| [incoming-inspection-templates-plan.md](./incoming-inspection-templates-plan.md) | 83% | 수입검사 템플릿, 4종 품목 대기 | ### 마이그레이션 & 연동 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [5130-to-mng-migration-plan.md](./5130-to-mng-migration-plan.md) | 🟡 진행중 | 5/38 (13%) | 5130→mng 마이그레이션 | -| [react-api-integration-plan.md](./react-api-integration-plan.md) | 🟡 진행중 | - | React↔API 연동 | -| [react-mock-to-api-migration-plan.md](./react-mock-to-api-migration-plan.md) | 🟡 진행중 | - | Mock→API 전환, 별도 문서 추적 | -| [dashboard-api-integration-plan.md](./dashboard-api-integration-plan.md) | 🟡 진행중 | 5/11 (45%) | CEO Dashboard API 연동 | -| [kd-orders-migration-plan.md](./kd-orders-migration-plan.md) | ⚪ 대기 | 0/2 (0%) | 경동 수주 마이그레이션, 선행조건 미충족 | -| [items-migration-kyungdong-plan.md](./items-migration-kyungdong-plan.md) | 📚 참조 | ARCHIVED | 후속 문서로 이관됨 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [5130-to-mng-migration-plan.md](./5130-to-mng-migration-plan.md) | 13% | 5130→mng 마이그레이션 | +| [react-api-integration-plan.md](./react-api-integration-plan.md) | - | React↔API 연동 | +| [react-mock-to-api-migration-plan.md](./react-mock-to-api-migration-plan.md) | - | Mock→API 전환 | +| [dashboard-api-integration-plan.md](./dashboard-api-integration-plan.md) | 45% | CEO Dashboard API 연동 | ### 시스템/인프라 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [db-trigger-audit-system-plan.md](./db-trigger-audit-system-plan.md) | 🟡 진행중 | 15/16 (94%) | DB 트리거 감사, 옵션 3건 잔여 | -| [db-backup-system-plan.md](./db-backup-system-plan.md) | 🟡 진행중 | 11/14 (79%) | DB 백업, 서버 작업 3건 잔여 | -| [tenant-id-compliance-plan.md](./tenant-id-compliance-plan.md) | ⚪ 대기 | 0/4 (0%) | 테넌트 ID 정합, 실행 대기 | -| [tenant-numbering-system-plan.md](./tenant-numbering-system-plan.md) | ⚪ 대기 | 0/8 (0%) | 테넌트 채번, 미착수 | -| [mng-numbering-rule-management-plan.md](./mng-numbering-rule-management-plan.md) | ⚪ 대기 | 0% | 채번 규칙 관리, 미착수 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [db-backup-system-plan.md](./db-backup-system-plan.md) | 79% | DB 백업, 서버 작업 3건 잔여 | ### 프론트엔드 & UI -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [simulator-ui-enhancement-plan.md](./simulator-ui-enhancement-plan.md) | 🟡 진행중 | 6/10 (60%) | 시뮬레이터 UI 개선 | -| [card-management-section-plan.md](./card-management-section-plan.md) | 🟡 진행중 | 6/12 (50%) | 카드 관리 섹션 | -| [dev-toolbar-plan.md](./dev-toolbar-plan.md) | 🟡 진행중 | 3/8 (38%) | 개발 툴바 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [simulator-ui-enhancement-plan.md](./simulator-ui-enhancement-plan.md) | 60% | 시뮬레이터 UI 개선 | +| [card-management-section-plan.md](./card-management-section-plan.md) | 50% | 카드 관리 섹션 | +| [dev-toolbar-plan.md](./dev-toolbar-plan.md) | 38% | 개발 툴바 | ### 기타 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [hotfix-20260119-action-plan.md](./hotfix-20260119-action-plan.md) | 🟡 진행중 | API 완료 | Hotfix, React P0 2건 대기 | -| [mng-menu-system-plan.md](./mng-menu-system-plan.md) | 🟡 진행중 | 구현 완료 | 메뉴 시스템, Phase 3 테스트 잔여 | -| [monthly-expense-integration-plan.md](./monthly-expense-integration-plan.md) | ⚪ 대기 | 0/8 (0%) | 월별 경비 연동, 미착수 | -| [receiving-management-analysis-plan.md](./receiving-management-analysis-plan.md) | ⚪ 대기 | 분석 | 입고 관리, 분석 완료/개발 대기 | -| [api-explorer-development-plan.md](./api-explorer-development-plan.md) | ⚪ 대기 | 0% | API Explorer, 미착수 | -| [employee-user-linkage-plan.md](./employee-user-linkage-plan.md) | ⚪ 대기 | 0% | 사원-회원 연결, 미착수 | -| [dummy-data-seeding-plan.md](./dummy-data-seeding-plan.md) | ⚪ 대기 | - | 더미 데이터 시딩, 미착수 | -| [react-mock-remaining-tasks.md](./react-mock-remaining-tasks.md) | 📚 참조 | - | Mock 전환 잔여 작업 목록 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [mng-menu-system-plan.md](./mng-menu-system-plan.md) | 구현완료 | 메뉴 시스템, Phase 3 테스트 잔여 | --- -## 완료 아카이브 (archive/) - 37개 +## 대기 (PLANNED) - 19개 -> 완료된 계획 문서들 - 참조용으로 보관 +### 견적/수주 -| 문서 | 완료일 | 설명 | -|------|--------|------| -| [bending-lot-pipeline-dev-plan.md](./archive/bending-lot-pipeline-dev-plan.md) | 2026-02 | 절곡 LOT 매핑 파이프라인 | -| [bending-worklog-reimplementation-plan.md](./archive/bending-worklog-reimplementation-plan.md) | 2026-02 | 절곡 작업일지 재구현 | -| [document-system-product-inspection.md](./archive/document-system-product-inspection.md) | 2026-02 | 제품검사 서식 | -| [formula-engine-real-data-plan.md](./archive/formula-engine-real-data-plan.md) | 2026-02 | 수식 엔진 실데이터 | -| [material-input-per-item-mapping-plan.md](./archive/material-input-per-item-mapping-plan.md) | 2026-02 | 품목별 자재투입 매핑 | -| [mng-item-formula-integration-plan.md](./archive/mng-item-formula-integration-plan.md) | 2026-02 | mng 품목 수식 연동 | -| [mng-item-management-plan.md](./archive/mng-item-management-plan.md) | 2026-02 | mng 품목 관리 | -| [fcm-user-targeted-notification-plan.md](./archive/fcm-user-targeted-notification-plan.md) | 2026-01 | 사용자 타겟 FCM 알림 | -| [docs-update-plan.md](./archive/docs-update-plan.md) | 2026-01 | 문서 업데이트 계획 | -| [order-location-management-plan.md](./archive/order-location-management-plan.md) | 2026-01 | 수주 현장 관리 | -| [quote-v2-auto-calculation-fix-plan.md](./archive/quote-v2-auto-calculation-fix-plan.md) | 2026-01 | 견적 V2 자동계산 수정 | -| [sam-stat-database-design-plan.md](./archive/sam-stat-database-design-plan.md) | 2026-01 | 통계 DB 설계 | -| [stock-integration-plan.md](./archive/stock-integration-plan.md) | 2026-01 | 재고 연동 | -| [welfare-section-plan.md](./archive/welfare-section-plan.md) | 2026-01 | 복리후생 섹션 | -| [order-workorder-shipment-integration-plan.md](./archive/order-workorder-shipment-integration-plan.md) | 2026-01 | 수주-작업지시-출하 연동 | -| [document-management-system-changelog.md](./archive/document-management-system-changelog.md) | 2026-01 | 문서관리 변경 이력 | -| [items-table-unification-plan.md](./archive/items-table-unification-plan.md) | 2025-12 | items 테이블 통합 | -| [kd-items-migration-plan.md](./archive/kd-items-migration-plan.md) | 2025-12 | 경동 품목 마이그레이션 | -| [simulator-calculation-logic-mapping.md](./archive/simulator-calculation-logic-mapping.md) | 2025-12 | 시뮬레이터 로직 매핑 | -| [AI_리포트_키워드_색상체계_가이드_v1.4.md](./archive/AI_리포트_키워드_색상체계_가이드_v1.4.md) | 2025-12 | AI 리포트 색상 가이드 | -| [SEEDERS_LIST.md](./archive/SEEDERS_LIST.md) | 2025-12 | 시더 참조 목록 | -| [api-analysis-report.md](./archive/api-analysis-report.md) | 2025-12 | API 분석 보고서 | -| [erp-api-development-plan-d1.0-changes.md](./archive/erp-api-development-plan-d1.0-changes.md) | 2025-12 | D1.0 변경사항 | -| [mng-quote-formula-development-plan.md](./archive/mng-quote-formula-development-plan.md) | 2025-12 | mng 견적 수식 관리 | -| [quote-auto-calculation-development-plan.md](./archive/quote-auto-calculation-development-plan.md) | 2025-12 | 견적 자동 계산 | -| [order-management-plan.md](./archive/order-management-plan.md) | 2025-01 | 수주관리 API 연동 | -| [work-order-plan.md](./archive/work-order-plan.md) | 2025-01 | 작업지시 검증 | -| [process-management-plan.md](./archive/process-management-plan.md) | 2025-12 | 공정관리 API 연동 | -| [construction-api-integration-plan.md](./archive/construction-api-integration-plan.md) | 2026-01 | 시공사 API 연동 | -| [notification-sound-system-plan.md](./archive/notification-sound-system-plan.md) | 2025-01 | 알림음 시스템 | -| [l2-permission-management-plan.md](./archive/l2-permission-management-plan.md) | 2025-12 | L2 권한 관리 | -| [react-fcm-push-notification-plan.md](./archive/react-fcm-push-notification-plan.md) | 2025-12 | FCM 푸시 알림 | -| [react-server-component-audit-plan.md](./archive/react-server-component-audit-plan.md) | 2025-12 | Server Component 점검 | -| [5130-bom-migration-plan.md](./archive/5130-bom-migration-plan.md) | 2025-12 | 5130 BOM 마이그레이션 | -| [5130-sam-data-migration-plan.md](./archive/5130-sam-data-migration-plan.md) | 2025-12 | 5130 데이터 마이그레이션 | -| [bidding-api-implementation-plan.md](./archive/bidding-api-implementation-plan.md) | 2025-12 | 입찰 API 구현 | -| [mes-integration-analysis-plan.md](./archive/mes-integration-analysis-plan.md) | 2025-01 | MES 연동 분석 | +| 문서 | 설명 | +|------|------| +| [quote-management-8issues-plan.md](./quote-management-8issues-plan.md) | 견적관리 8개 이슈, 컨펌 대기 | +| [quote-calculation-api-plan.md](./quote-calculation-api-plan.md) | 견적 계산 API, 설계 완료 | +| [quote-order-sync-improvement-plan.md](./quote-order-sync-improvement-plan.md) | 견적-수주 동기화 개선, 승인 대기 | +| [kd-orders-migration-plan.md](./kd-orders-migration-plan.md) | 경동 수주 마이그레이션, 선행조건 미충족 | +| [receiving-management-analysis-plan.md](./receiving-management-analysis-plan.md) | 입고 관리, 분석 완료/개발 대기 | +| [monthly-expense-integration-plan.md](./monthly-expense-integration-plan.md) | 월별 경비 연동 | + +### 품목/BOM + +| 문서 | 설명 | +|------|------| +| [mng-item-field-management-plan.md](./mng-item-field-management-plan.md) | 품목 필드 관리 | +| [item-inventory-management-plan.md](./item-inventory-management-plan.md) | 품목 재고 관리, 설계 확정 | + +### 생산/절곡 + +| 문서 | 설명 | +|------|------| +| [bending-info-auto-generation-plan.md](./bending-info-auto-generation-plan.md) | 절곡 정보 자동 생성, 설계 확정 | +| [bending-material-input-mapping-plan.md](./bending-material-input-mapping-plan.md) | 절곡 자재투입 매핑, GAP 분석 완료 | + +### 문서/서식 + +| 문서 | 설명 | +|------|------| +| [incoming-inspection-document-integration-plan.md](./incoming-inspection-document-integration-plan.md) | 수입검사 서류 연동, 분석만 완료 | +| [intermediate-inspection-report-plan.md](./intermediate-inspection-report-plan.md) | 중간검사 보고서, 검토 대기 | + +### 시스템/인프라 + +| 문서 | 설명 | +|------|------| +| [tenant-id-compliance-plan.md](./tenant-id-compliance-plan.md) | 테넌트 ID 정합, 실행 대기 | +| [tenant-numbering-system-plan.md](./tenant-numbering-system-plan.md) | 테넌트 채번 | +| [mng-numbering-rule-management-plan.md](./mng-numbering-rule-management-plan.md) | 채번 규칙 관리 | + +### 기타 + +| 문서 | 설명 | +|------|------| +| [api-explorer-development-plan.md](./api-explorer-development-plan.md) | API Explorer | +| [employee-user-linkage-plan.md](./employee-user-linkage-plan.md) | 사원-회원 연결 | +| [esign-alimtalk-integration.md](./esign-alimtalk-integration.md) | 전자서명/알림톡, 카카오 채널 개설 후 착수 | +| [dummy-data-seeding-plan.md](./dummy-data-seeding-plan.md) | 더미 데이터 시딩 | + +--- + +## 완료 히스토리 + +> 40건 완료 작업 요약 → [archive/HISTORY.md](./archive/HISTORY.md) --- ## 스토리보드 -### SAM_ERP_Storyboard_D1.0_251218 (현재 버전) - -**경로**: `docs/plans/SAM_ERP_Storyboard_D1.0_251218/` -**일자**: 2025-12-18 -**슬라이드 수**: 38장 - -**내용**: D0.8 대비 변경/추가된 화면 (D1.0 버전) +**경로**: `SAM_ERP_Storyboard_D1.0_251218/` +**내용**: D0.8 대비 변경/추가된 화면 (D1.0, 2025-12-18, 38장) --- ## 플로우 테스트 -**경로**: `docs/plans/flow-tests/` -**용도**: Flow Tester (mng.sam.kr/dev-tools/flow-tester) 검증용 JSON - -### 인증/권한 - -| 파일 | 설명 | -|------|------| -| [auth-api-flow.json](./flow-tests/auth-api-flow.json) | 인증 API 플로우 | -| [auth-legacy-flow.json](./flow-tests/auth-legacy-flow.json) | 레거시 인증 플로우 | -| [user-invitation-flow.json](./flow-tests/user-invitation-flow.json) | 사용자 초대 | - -### 품목/BOM - -| 파일 | 설명 | -|------|------| -| [items-crud-api-flow.json](./flow-tests/items-crud-api-flow.json) | 품목 CRUD | -| [items-bom-api-flow.json](./flow-tests/items-bom-api-flow.json) | BOM API | -| [items-bom-test.json](./flow-tests/items-bom-test.json) | BOM 테스트 | -| [item-master-page-api-flow.json](./flow-tests/item-master-page-api-flow.json) | 품목 마스터 페이지 | -| [item-master-full-api-flow.json](./flow-tests/item-master-full-api-flow.json) | 품목 마스터 전체 | -| [item-master-init-api-flow.json](./flow-tests/item-master-init-api-flow.json) | 품목 마스터 초기화 | -| [item-master-field-api-flow.json](./flow-tests/item-master-field-api-flow.json) | 품목 필드 | -| [item-master-legacy-flow.json](./flow-tests/item-master-legacy-flow.json) | 레거시 품목 | -| [item-delete-legacy-flow.json](./flow-tests/item-delete-legacy-flow.json) | 품목 삭제 (레거시) | -| [item-delete-force-delete.json](./flow-tests/item-delete-force-delete.json) | 품목 강제 삭제 | -| [item-fields-is-active-test.json](./flow-tests/item-fields-is-active-test.json) | 필드 활성화 테스트 | - -### 거래처/영업 - -| 파일 | 설명 | -|------|------| -| [client-api-flow.json](./flow-tests/client-api-flow.json) | 거래처 API | -| [client-legacy-flow.json](./flow-tests/client-legacy-flow.json) | 레거시 거래처 | -| [client-group-api-flow.json](./flow-tests/client-group-api-flow.json) | 거래처 그룹 | -| [pricing-crud-flow.json](./flow-tests/pricing-crud-flow.json) | 단가 CRUD | -| [pricing-validation-test.json](./flow-tests/pricing-validation-test.json) | 단가 검증 | - -### 인사/급여 - -| 파일 | 설명 | -|------|------| -| [employee-api-crud.json](./flow-tests/employee-api-crud.json) | 사원 CRUD | -| [attendance-api-crud.json](./flow-tests/attendance-api-crud.json) | 근태 CRUD | -| [department-tree-api.json](./flow-tests/department-tree-api.json) | 부서 트리 | - -### 회계/재무 - -| 파일 | 설명 | -|------|------| -| [account-management-flow.json](./flow-tests/account-management-flow.json) | 계정 관리 | -| [sales-statement-flow.json](./flow-tests/sales-statement-flow.json) | 매출 전표 | -| [payment-flow.json](./flow-tests/payment-flow.json) | 결제 플로우 | -| [bad-debt-flow.json](./flow-tests/bad-debt-flow.json) | 대손 처리 | - -### 기타 - -| 파일 | 설명 | -|------|------| -| [popup-flow.json](./flow-tests/popup-flow.json) | 팝업 플로우 | -| [company-request-flow.json](./flow-tests/company-request-flow.json) | 회사 요청 | -| [notification-settings-flow.json](./flow-tests/notification-settings-flow.json) | 알림 설정 | -| [subscription-flow.json](./flow-tests/subscription-flow.json) | 구독 플로우 | -| [branching-example-flow.json](./flow-tests/branching-example-flow.json) | 분기 예제 | +**경로**: `flow-tests/` +**용도**: Flow Tester (mng.sam.kr/dev-tools/flow-tester) 검증용 JSON, 32개 --- ## 관련 문서 - [docs/INDEX.md](../INDEX.md) - 전체 문서 인덱스 -- [docs/projects/index_projects.md](../projects/index_projects.md) - 프로젝트 문서 인덱스 +- [GUIDE.md](./GUIDE.md) - 문서 관리 가이드 --- -**범례**: -- 🟡 진행중: 현재 작업 중 또는 일부 완료 -- ⚪ 대기: 미착수 또는 선행조건 대기 -- 📚 참조: 분석/참조용 문서 +**범례**: 🟡 진행중 | ⚪ 대기 \ No newline at end of file diff --git a/plans/items-migration-kyungdong-plan.md b/plans/items-migration-kyungdong-plan.md deleted file mode 100644 index 6995ecc..0000000 --- a/plans/items-migration-kyungdong-plan.md +++ /dev/null @@ -1,1399 +0,0 @@ -# [ARCHIVED] 경동기업(5130) 레거시 → SAM 전체 데이터 마이그레이션 계획 - -> ⚠️ **이 문서는 분리되었습니다** (2026-01-28) -> -> 이 통합 문서는 다음 2개 문서로 분리되었습니다: -> -> 1. **📦 품목/단가/BOM**: [`kd-items-migration-plan.md`](./kd-items-migration-plan.md) ← **먼저 작업** -> 2. **📋 입고/재고/주문**: [`kd-orders-migration-plan.md`](./kd-orders-migration-plan.md) ← 품목 완료 후 작업 -> -> 아래 내용은 참고용으로 보존됩니다. - ---- - -> **작성일**: 2026-01-28 -> **목적**: 경동기업 레거시 시스템(5130/)의 **전체 운영 데이터**를 SAM으로 이관 -> **기준 문서**: `5130/` 폴더 분석 결과 -> **상태**: ✅ 문서 분리 완료 (2026-01-28) -> **데이터 규모**: ~30,000+ 레코드 (items + prices + receipts + orders) - ---- - -## 🚀 새 세션 시작 가이드 (Quick Start) - -### 이 문서만 보고 작업을 재개하려면: - -```bash -# 1. Docker 서비스 확인 -docker ps | grep sam - -# 2. 레거시 DB (chandj) 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM models;" - -# 3. 현재 진행 상태 확인 -# → 아래 "📍 현재 진행 상태" 섹션 참조 - -# 4. 다음 작업 시작 -# → "📍 현재 진행 상태" > "다음 작업" 참조 -``` - -### 환경 정보 - -| 항목 | 값 | -|------|-----| -| **프로젝트 루트** | `/Users/kent/Works/@KD_SAM/SAM` | -| **레거시 소스** | `5130/` (프로젝트 루트 직하) | -| **API 프로젝트** | `api/` | -| **Docker 컨테이너** | `sam-mysql-1` | -| **레거시 DB** | `chandj` (MySQL) | -| **SAM DB** | `samdb` (MySQL) ⚠️ | -| **대상 테넌트 ID** | `287` (경동기업) | -| **생성자 사용자 ID** | `1` | - -### DB 접속 명령어 - -```bash -# 레거시 DB (chandj) 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot chandj - -# SAM DB 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot samdb - -# 레거시 테이블 목록 확인 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SHOW TABLES;" - -# SAM items 테이블 확인 -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" -``` - -### 전제 조건 (작업 전 확인) - -- [x] Docker 서비스 실행 중 -- [x] `sam-mysql-1` 컨테이너 실행 중 -- [x] chandj 데이터베이스 접근 가능 -- [ ] SAM items 마이그레이션 실행 완료 (`php artisan migrate`) -- [ ] SAM prices 마이그레이션 실행 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 전체 범위 분석 완료 (KDunitprice 603건, output 24,564건 발견) | -| **다음 작업** | Phase 1.0: KDunitprice → items 마스터 INSERT | -| **진행률** | 2/6 (33%) - 분석 완료, 구현 대기 | -| **마지막 업데이트** | 2026-01-28 | - -### 다음 작업 상세 - -**Phase 1.0: KDunitprice → items (마스터) INSERT** ⭐ 최우선! - -1. KDunitprice 데이터 확인: - ```bash - docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*), item_div FROM KDunitprice GROUP BY item_div;" - ``` - -2. 섹션 5.0의 SQL 쿼리를 SAM DB에서 실행: - - KDunitprice → items (603건) - - KDunitprice → prices (603건) - -3. 중복 확인 후 추가 items 생성: - - models, category_l4 중 KDunitprice에 없는 것만 추가 - -4. ⚠️ 실행 전 사용자 승인 필요 - ---- - -## 0. 성공 기준 - -| 기준 | 목표값 | 확인 방법 | -|------|-------|----------| -| **items 합계** | **~800건** | `SELECT COUNT(*) FROM items WHERE tenant_id=287` | -| items (FG) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='FG'` | -| items (PT) | ~250건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='PT'` | -| items (SM) | ~300건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='SM'` | -| items (RM) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='RM'` | -| items (CS) | ~50건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='CS'` | -| **prices 합계** | **~500건** | `SELECT COUNT(*) FROM prices WHERE tenant_id=287` | -| **BOM 관계** | ~300건 | `SELECT COUNT(*) FROM item_bom_items WHERE tenant_id=287` | -| **입고 기록** | ~2,300건 | `SELECT COUNT(*) FROM item_receipts WHERE tenant_id=287` | -| **주문 기록** | ~24,600건 | `SELECT COUNT(*) FROM orders WHERE tenant_id=287` | -| **로트 기록** | ~200건 | `SELECT COUNT(*) FROM lots WHERE tenant_id=287` | -| code 유일성 | 100% | `SELECT code, COUNT(*) FROM items WHERE tenant_id=287 GROUP BY code HAVING COUNT(*) > 1` (0건) | -| API 테스트 | 100% | `/api/v1/items` 목록 조회 성공 | - ---- - -## 1. 개요 - -### 1.1 배경 - -경동기업은 **모델(Model) + 제품(Product)** 분리 구조를 사용하지만, SAM은 **통합 items 테이블** 구조로 기획됨. 레거시 5130/ 폴더의 데이터를 SAM 구조에 맞게 변환하여 이관 필요. - -### 1.2 핵심 차이점 - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 레거시 (chandj) → SAM (samdb) │ -├────────────────────────────────────────────────────────────────────────────┤ -│ 📦 품목 마스터 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ KDunitprice (603건, 핵심!) → items (마스터, code로 구분) │ -│ models (18건) → items (FG) │ -│ parts, parts_sub (170건) → item_bom_items │ -│ category_l1~l4 → items 카테고리 참조 │ -│ guiderail, bottombar, bending 등 → item_details │ -│ │ -│ 💰 단가 정보 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ price_* (10개 테이블) → prices │ -│ KDunitprice.출고가/입고가 → prices (기본가) │ -│ │ -│ 📥 입고/재고 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ instock (2,286건) → item_receipts + stocks │ -│ lot, lot_sales → lots + lot_sales │ -│ │ -│ 📋 주문/출고 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ output (24,564건) → orders + order_items │ -│ output.iList (JSON 파일 참조) → orders.options │ -│ estimate → orders (type=견적) │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.2.1 중복 제거 전략 ⭐ - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심: KDunitprice가 마스터, code 필드로 중복 방지 │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1️⃣ KDunitprice (603건) → items 먼저 생성 │ -│ - item_div로 item_type 결정 │ -│ - code = 품목코드 그대로 사용 │ -│ │ -│ 2️⃣ price_* 테이블 → items 중복 확인 후 prices만 생성 │ -│ - code로 items 조회 │ -│ - 존재하면 → prices만 추가 (item_id 연결) │ -│ - 없으면 → items 생성 후 prices 추가 │ -│ │ -│ 3️⃣ 매핑 테이블 불필요 │ -│ - item_id_mappings ❌ (양방향 조회 불필요) │ -│ - chandj는 손 안댐, samdb에만 밀어 넣음 │ -│ │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 SAM items 구조 (Target) - -```sql --- items 테이블 (tenant_id=287 for 경동기업) -CREATE TABLE items ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - item_type VARCHAR(15) NOT NULL, -- FG, PT, SM, RM, CS - code VARCHAR(100) NOT NULL, -- 품목코드 - name VARCHAR(255) NOT NULL, -- 품목명 - unit VARCHAR(20), -- 단위 - category_id BIGINT, -- 카테고리 ID - bom JSON, -- [{child_item_id, quantity}, ...] - attributes JSON, -- 동적 필드 값 - options JSON, -- 추가 옵션 - description TEXT, -- 설명 - is_active BOOLEAN DEFAULT TRUE, - created_by, updated_by, deleted_by, timestamps, soft_deletes -); -``` - -### 1.4 item_type 분류 - -| SAM item_type | 설명 | 레거시 소스 | -|---------------|------|-------------| -| **FG** | 완제품 (Finished Goods) | KDunitprice[제품], KDunitprice[상품], models | -| **PT** | 부품 (Parts) | KDunitprice[반제품], parts, parts_sub, category_l4 | -| **SM** | 부자재 (Sub-Materials) | KDunitprice[부재료], price_* 테이블들 | -| **RM** | 원자재 (Raw Materials) | KDunitprice[원재료], price_raw_materials | -| **CS** | 소모품 (Consumables) | KDunitprice[무형상품], 기타 | - -### 1.4.1 KDunitprice.item_div → item_type 매핑 ⭐ - -```sql --- KDunitprice.item_div 값 목록 (603건 중) --- [제품], [상품], [부재료], [원재료], [반제품], [무형상품] - -CASE item_div - WHEN '[제품]' THEN 'FG' -- 완제품 - WHEN '[상품]' THEN 'FG' -- 상품도 완제품으로 분류 - WHEN '[반제품]' THEN 'PT' -- 반제품 = 부품 - WHEN '[부재료]' THEN 'SM' -- 부자재 - WHEN '[원재료]' THEN 'RM' -- 원자재 - WHEN '[무형상품]' THEN 'CS' -- 소모품/무형 - ELSE 'SM' -- 기본값 -END AS item_type -``` - ---- - -## 2. 레거시 DB 구조 분석 - -### 2.1 핵심 테이블 및 레코드 수 (전체 목록) - -#### 📦 품목 마스터 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`KDunitprice`** ⭐ | **603** | **품목 마스터 (핵심!)** | **items (마스터)** | -| `models` | 18 | 모델 마스터 (스크린/철재) | items (FG) | -| `BDmodels` | 59 | 모델별 BOM + 단가 (JSON) | item_bom_items + prices | -| `parts` | 36 | 부품 | item_bom_items | -| `parts_sub` | 134 | 하위 부품 | item_bom_items | -| `category_l1` | 2 | 1단계 카테고리 (스크린/철재) | 참조용 | -| `category_l2` | 14 | 2단계 카테고리 | 참조용 | -| `category_l3` | 24 | 3단계 카테고리 | 참조용 | -| `category_l4` | 37 | 4단계 카테고리 (부품) | items (PT) | -| `item_list` | 5+ | 품목 마스터 | items (PT) | - -#### 🔧 제품 상세 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| `guiderail` | - | 가이드레일 상세 | item_details | -| `bottombar` | - | 하단바 상세 | item_details | -| `shutterbox` | - | 셔터박스 상세 | item_details | -| `bending` | - | 벤딩 상세 | item_details | -| `lift` | - | 리프트 상세 | item_details | - -#### 💰 단가 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| `price_motor` | 2 (JSON) | 모터 단가 | prices | -| `price_shaft` | 2 (JSON) | 감기샤프트 단가 | prices | -| `price_pipe` | 2 (JSON) | 파이프 단가 | prices | -| `price_angle` | 2 (JSON) | 앵글 단가 | prices | -| `price_raw_materials` | 6 (JSON) | 주자재 단가 | prices | -| `price_bend` | 3 (JSON) | 절곡 단가 | prices | -| `price_pole` | 2 (JSON) | 폴 단가 | prices | -| `price_screenplate` | 2 (JSON) | 스크린플레이트 단가 | prices | -| `price_smokeban` | 2 (JSON) | 연기차단 단가 | prices | - -#### 📥 입고/재고 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`instock`** ⭐ | **2,286** | 입고 기록 | item_receipts + stocks | -| `lot` | - | 로트 관리 | lots | -| `lot_sales` | - | 로트 소진 | lot_sales | - -#### 📋 주문/출고 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`output`** ⭐ | **24,564** | 주문/출고 기록 | orders + order_items | -| `estimate` | - | 견적 | orders (type=견적) | - -### 2.2 models 테이블 구조 - -```sql --- models: 제품 모델 마스터 -model_id INT PRIMARY KEY, -model_name VARCHAR(255), -- KSS01, KSE01, KWE01 등 -major_category ENUM('스크린','철재'), -finishing_type ENUM('SUS마감','EGI마감'), -guiderail_type VARCHAR(20), -- 벽면형, 측면형, 혼합형 -description TEXT, -is_deleted, created_at, updated_at -``` - -**샘플 데이터**: -- KSS01/스크린/SUS마감/벽면형 -- KSS01/스크린/SUS마감/측면형 -- KSE01/스크린/EGI마감/벽면형 -- KWE01/스크린/SUS마감/벽면형 - -### 2.3 KDunitprice 테이블 구조 ⭐ (핵심 마스터) - -```sql --- KDunitprice: 품목 마스터 (603건) - 가장 중요한 테이블! -품목코드 VARCHAR(50), -- items.code (유니크 키!) -품목명 VARCHAR(255), -- items.name -규격 VARCHAR(100), -- items.attributes.spec -단위 VARCHAR(20), -- items.unit -item_div VARCHAR(20), -- [제품]/[상품]/[부재료]/[원재료]/[반제품]/[무형상품] → item_type -입고가 DECIMAL, -- prices.purchase_price -출고가 DECIMAL, -- prices.sales_price -비고 TEXT -- items.description -``` - -**item_div 분포 (예상)**: -```sql -SELECT item_div, COUNT(*) FROM KDunitprice GROUP BY item_div; --- [제품] ~100건 → FG --- [상품] ~50건 → FG --- [반제품] ~100건 → PT --- [부재료] ~200건 → SM --- [원재료] ~100건 → RM --- [무형상품] ~53건 → CS -``` - -### 2.3.1 output.iList JSON 파일 구조 ⭐ - -```sql --- output 테이블의 iList 컬럼 --- 값: "../output/i_json/22545.json" (파일 경로!) --- 실제 파일 위치: 5130/output/i_json/{output_id}.json -``` - -**JSON 파일 내용 예시 (5130/output/i_json/22545.json)**: -```json -{ - "inputValue": [ - "2024-12-03", // 날짜 - "명보에스티", // 거래처명 - "KWE01 전체적인 테스트", // 모델/설명 - // ... 추가 입력값들 - ], - "beforeWidth": ["8000", "7000"], // 변경전 폭 - "beforeHeight": ["4000", "3500"], // 변경전 높이 - "afterWidth": ["8000", "7000"], // 변경후 폭 - "afterHeight": ["4000", "3500"], // 변경후 높이 - "pages": [ - { - "page": "1", - "inputItems": { - "openWidth": "8000", - "openHeight": "4000", - // ... 기타 치수 정보 - }, - "checkboxData": [...] - } - ], - "approval": { - "writer": {"name": "개발자", "date": "25/01/02"}, - "approver": {"name": "관리자", "date": "25/01/03"} - } -} -``` - -**SAM 매핑**: -- `inputValue` → `orders.options` (JSON) -- `pages` → `order_items.options` (JSON) -- `approval` → `orders.approved_by`, `orders.approved_at` -- `beforeWidth/Height`, `afterWidth/Height` → `order_items.options.dimensions` - -### 2.4 BDmodels 테이블 구조 (BOM + 단가) - -```sql --- BDmodels: 모델별 BOM 및 단가 정보 -num INT PRIMARY KEY, -major_category VARCHAR(10), -- 스크린/철재 -spec VARCHAR(30), -- 규격 (60*40, 120*70 등) -model_name VARCHAR(255), -- 모델명 -finishing_type ENUM('SUS마감','EGI마감'), -check_type VARCHAR(20), -- 벽면형/측면형/혼합형 -seconditem VARCHAR(30), -- 부품명 (가이드레일, 하단마감재, L-BAR 등) -unitprice TEXT, -- 단가 (문자열) -savejson TEXT, -- BOM 상세 JSON -description TEXT, -is_deleted, priceDate DATE -``` - -**savejson 예시** (가이드레일 BOM): -```json -[ - {"col1":"1번(마감재)","col2":"SUS 1.2T","col3":"-3","col4":"203","col5":"206","col6":"51,000","col7":"10,506","col8":"2","col9":"21,012","col10":"삭제"}, - {"col1":"2번(본체)","col2":"EGI 1.55T","col3":"-5","col4":"294","col5":"299","col6":"27,000","col7":"8,073","col8":"1","col9":"8,073","col10":"삭제"}, - {"col1":"3번(벽면형-C)","col2":"EGI 1.55T","col3":"-1","col4":"104","col5":"105","col6":"27,000","col7":"2,835","col8":"1","col9":"2,835","col10":"삭제"}, - {"col1":"4번(벽면형-D)","col2":"EGI 1.55T","col3":"-3","col4":"105","col5":"108","col6":"27,000","col7":"2,916","col8":"1","col9":"2,916","col10":"삭제"} -] -``` - -### 2.4 카테고리 계층 구조 (4단계) - -``` -category_l1 (2개) -├── 스크린 -│ ├── category_l2 (앵글, 환봉, 각파이프, 감기샤프트, 전동개폐기, 원단, 절곡물) -│ │ ├── category_l3 (받침앵글, 브라켓트, 와이어, 실리카, 마구리, 케이스, 가이드레일, 하단마감재...) -│ │ │ └── category_l4 (점검구양면, 점검구후면, 점검구밑면, 연기차단재, 상부덮개, 마구리, 벽면형, 측면형, 혼합형, L-bar, 하장바, 보강평철, 무게평철...) -│ -└── 철재 - ├── category_l2 (환봉, 앵글, 각파이프, 감기샤프트, 전동개폐기, 슬랫, 절곡물) - │ ├── category_l3 (브라켓트, 받침앵글, 슬랫, 조인트바, 가이드레일, 연동제어기, 모터, 하단마감재, 케이스) - │ │ └── category_l4 (하부베이스, 매립형, 노출형, 유선, 무선, L-bar, 하장바, 보강평철, 점검구양면, 점검구후면) -``` - -### 2.5 price_* 테이블 구조 (단가 정보) - -```sql --- 공통 구조 (price_motor, price_shaft, price_pipe, price_raw_materials 등) -num INT PRIMARY KEY, -registedate DATE, -- 등록일 -itemList TEXT, -- JSON 배열 (단가 정보) -is_deleted TINYINT DEFAULT 0, -update_log TEXT, -created_at TIMESTAMP -``` - -**price_motor itemList 예시**: -```json -[ - {"col1":"220","col2":"150K(S)","col3":"368","col4":"124","col5":"188","col6":"","col7":"680","col8":"6.79","col9":"100.1","col10":"1300","col11":"130,130","col12":"156,156","col13":"285,000","col14":"128,844","col15":"45.2"}, - {"col1":"380","col2":"300K","col3":"420","col4":"180","col5":"188","col6":"","col7":"788","col8":"6.79","col9":"116.1","col10":"1300","col11":"150,930","col12":"181,116","col13":"300,000","col14":"118,884","col15":"39.6"}, - {"col1":"제어기","col2":"노출형","col3":"","col4":"","col5":"300","col6":"","col7":"300","col8":"6.79","col9":"44.2","col10":"1300","col11":"57,460","col12":"68,952","col13":"130000","col14":"61,048","col15":"47"} -] -``` - -### 2.6 단가 시스템 상세 분석 ⭐ - -#### 2.6.1 레거시 단가 테이블 전체 목록 (10개) - -| 테이블명 | 레코드 수 | 최신 날짜 | 용도 | -|---------|----------|----------|------| -| `price_motor` | 2 | 2024-08-25 | 전동개폐기, 제어기 단가 | -| `price_shaft` | 2 | 2024-08-25 | 감기샤프트 단가 | -| `price_pipe` | 2 | 2024-08-26 | 파이프 단가 | -| `price_angle` | 2 | 2024-08-26 | 앵글 단가 | -| `price_raw_materials` | 6 | 2025-06-18 | 슬랫, 스크린 원자재 단가 | -| `price_bend` | 3 | 2025-03-09 | 절곡 단가 | -| `price_pole` | 2 | 2024-08-26 | 폴 단가 | -| `price_screenplate` | 2 | 2024-08-26 | 스크린플레이트 단가 | -| `price_smokeban` | 2 | 2024-08-26 | 연기차단 단가 | -| `price_etc` | 0 | - | 기타 (개별 컬럼 방식, 비활성) | - -#### 2.6.2 공통 테이블 구조 - -```sql --- 9개 테이블 공통 구조 (price_etc 제외) -num INT PRIMARY KEY, -registedate DATE, -- 적용일 (버전 관리 핵심!) -itemList TEXT, -- JSON 배열 (단가 정보) -is_deleted TINYINT DEFAULT 0, -update_log TEXT, -searchtag VARCHAR(255), -created_at TIMESTAMP, -memo TEXT -``` - -#### 2.6.3 각 테이블의 JSON 스키마 분석 - -**price_motor (모터/제어기)**: -``` -col1: 분류 (220/380/제어기/방화/방범) -col2: 용량/타입 (150K, 300K, 노출형, 매립형...) -col3-col10: 치수, 무게, 계산값 -col11: 원가 (VAT 제외) -col12: 원가 (VAT 포함) -col13: 판매단가 ⭐ -col14: 이익금액 -col15: 이익률 (%) -``` - -**price_shaft (감기샤프트)**: -``` -col1: 품목명 (샤프트(BS)) -col2-col5: 규격 (두께, 외경, 두께, 외경) -col6-col10: 길이, 무게, 계산값 -col11-col16: 가공비, 원가 -col17-col20: 단가 옵션들 (길이별) -``` - -**price_raw_materials (원자재)**: -``` -col1: 분류 (슬랫/스크린) -col2: 종류 (방화/방범/실리카/화이바/조인트바) -col3-col12: 규격, 무게, 계산값 -col13: 기준단가 -col14: 품목코드 -col15: 현재단가 ⭐ -``` - -**price_pipe (파이프)**: -``` -col1: 품목 (각파이프) -col2: 길이 (3,000/6,000) -col3: 규격 (50*30, 100*50) -col4: 두께 -col5: 수량 -col6-col7: 원가 -col8: 단가 ⭐ -``` - -#### 2.6.4 SAM prices 테이블 구조 (Target) - -```sql -CREATE TABLE prices ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - - -- 품목 연결 - item_type_code VARCHAR(20), -- FG/PT/SM/RM/CS - item_id BIGINT, -- items.id FK - client_group_id BIGINT NULL, -- NULL = 기본가 - - -- 원가 정보 - purchase_price DECIMAL(15,4), -- 매입단가 (원가) - processing_cost DECIMAL(15,4), -- 가공비 - loss_rate DECIMAL(5,2), -- LOSS율 (%) - - -- 판매가 정보 - margin_rate DECIMAL(5,2), -- 마진율 (%) - sales_price DECIMAL(15,4), -- 판매단가 ⭐ - rounding_rule ENUM('round','ceil','floor'), - rounding_unit INT DEFAULT 1, -- 반올림 단위 - - -- 메타 정보 - supplier VARCHAR(255), -- 공급업체 - effective_from DATE, -- 적용 시작일 ⭐ - effective_to DATE NULL, -- 적용 종료일 - note TEXT, - - -- 상태 관리 - status ENUM('draft','active','inactive','finalized'), - is_final BOOLEAN DEFAULT FALSE, - - -- 감사 컬럼 - created_by, updated_by, deleted_by, timestamps, soft_deletes -); -``` - -#### 2.6.5 Legacy → SAM 단가 매핑 전략 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 단가 마이그레이션 플로우 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ Legacy (chandj) SAM │ -│ ────────────── ─── │ -│ │ -│ 1. price_motor.itemList[i] │ -│ ├── col1,col2 (전압,용량) ───→ items (SM) 생성 │ -│ │ └── code: SM-MOTOR-220-150K │ -│ │ │ -│ └── col11,col13 (원가,판매가) ─→ prices 생성 │ -│ ├── item_id: 위에서 생성된 items.id │ -│ ├── purchase_price: col11 │ -│ ├── sales_price: col13 │ -│ └── effective_from: registedate │ -│ │ -│ 2. 날짜별 버전 관리 │ -│ ├── registedate 2024-08-25 → effective_from │ -│ └── 다음 레코드 존재 시 → effective_to 설정 │ -│ │ -│ 3. 최신 레코드만 active, 나머지는 inactive │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -#### 2.6.6 items와 prices 관계 - -``` -items (품목 마스터) prices (단가 이력) -┌──────────────────────┐ ┌──────────────────────┐ -│ id: 1001 │ │ id: 5001 │ -│ code: SM-MOTOR-220-150K │◄────────────│ item_id: 1001 │ -│ name: 전동개폐기 220V 150K │ │ sales_price: 285000 │ -│ item_type: SM │ │ effective_from: 2024-08-25 │ -│ attributes: {...} │ │ status: active │ -└──────────────────────┘ └──────────────────────┘ - │ - ┌──────────────────────┐ - │ id: 5002 │ - │ item_id: 1001 │ - │ sales_price: 270000 │ - │ effective_from: 2024-01-01 │ - │ effective_to: 2024-08-24 │ - │ status: inactive │ - └──────────────────────┘ -``` - ---- - -## 3. 매핑 설계 - -### 3.1 models → items (FG 완제품) - -| 레거시 (models) | SAM (items) | 비고 | -|----------------|-------------|------| -| model_id | (신규 생성) | | -| model_name | code | KSS01 → FG-KSS01 | -| - | name | 모델명 + 마감타입 + 가이드타입 조합 | -| major_category | attributes.major_category | 스크린/철재 | -| finishing_type | attributes.finishing_type | SUS마감/EGI마감 | -| guiderail_type | attributes.guiderail_type | 벽면형/측면형/혼합형 | -| - | item_type | 'FG' | -| - | tenant_id | 287 | - -**코드 생성 규칙**: -``` -FG-{model_name}-{guiderail_type}-{finishing_type} -예: FG-KSS01-벽면형-SUS -``` - -### 3.2 BDmodels → items (FG 세부 + BOM) - -| 레거시 (BDmodels) | SAM (items) | 비고 | -|------------------|-------------|------| -| seconditem | code (부품) | 가이드레일 → PT-GR-120x70-SUS-벽면형 | -| savejson | bom | JSON 변환 | -| unitprice | attributes.unit_price | | -| spec | attributes.spec | 120*70 | -| priceDate | attributes.price_date | | - -### 3.3 category_l4 → items (PT 부품) - -| 레거시 (category_l4) | SAM (items) | 비고 | -|---------------------|-------------|------| -| name | name | 부품명 | -| - | code | PT-L1-L2-L3-{name} 조합 | -| - | item_type | 'PT' | -| parent_id | attributes.parent_category_id | | - -### 3.4 price_* → prices 테이블 (단가 연동) ⭐ - -> **중요**: 단가 데이터는 items.attributes가 아닌 **prices 테이블**에 별도 관리 - -| 레거시 (price_*) | SAM (prices) | 비고 | -|-----------------|--------------|------| -| registedate | effective_from | 적용 시작일 | -| itemList.col13 (판매가) | sales_price | | -| itemList.col11 (원가) | purchase_price | | -| itemList.col12 (VAT포함) | - | 계산으로 도출 | -| - | item_type_code | FG/PT/SM/RM/CS | -| - | item_id | items.id FK | -| - | client_group_id | NULL (기본가) | -| - | status | 'active' | - ---- - -## 4. 대상 범위 - -### 4.1 Phase 1: 마스터 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| **1.0** | **KDunitprice → items (마스터)** ⭐ | ⏳ | **603건 (최우선!)** | -| 1.1 | models → items (FG) INSERT 쿼리 작성 | ⏳ | 18건 (중복 확인 후) | -| 1.2 | item_list → items (PT) INSERT 쿼리 작성 | ⏳ | 5건+ (중복 확인 후) | -| 1.3 | category_l4 → items (PT) INSERT 쿼리 작성 | ⏳ | 37건 (중복 확인 후) | -| 1.4 | price_motor 파싱 → prices 연결 | ⏳ | code로 items 조회 후 prices 생성 | -| 1.5 | price_shaft 파싱 → prices 연결 | ⏳ | ~15건 | -| 1.6 | price_raw_materials 파싱 → prices 연결 | ⏳ | ~20건 | -| 1.7 | ⚠️ **사용자 승인**: Phase 1 INSERT 실행 | ⏳ | | - -### 4.2 Phase 2: BOM 및 상세 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | BDmodels.savejson → item_bom_items | ⏳ | 59건 | -| 2.2 | parts → item_bom_items | ⏳ | 36건 | -| 2.3 | parts_sub → item_bom_items | ⏳ | 134건 | -| 2.4 | guiderail/bottombar/bending 등 → item_details | ⏳ | 제품 상세 | -| 2.5 | parent_item_id, child_item_id 매핑 | ⏳ | code 기반 조회 | - -### 4.3 Phase 3: 검증 및 배포 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 로컬 테스트 | ⏳ | | -| 3.2 | API 테스트 | ⏳ | | -| 3.3 | 개발서버 배포 | ⏳ | ⚠️ 컨펌 필요 | - -### 4.4 Phase 4: 단가 데이터 이관 ⭐ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | price_motor → items (SM) + prices | ⏳ | ~35건 품목 + 단가 | -| 4.2 | price_shaft → items (SM) + prices | ⏳ | ~15건 | -| 4.3 | price_pipe → items (SM) + prices | ⏳ | ~10건 | -| 4.4 | price_angle → items (SM) + prices | ⏳ | ~10건 | -| 4.5 | price_raw_materials → items (RM) + prices | ⏳ | ~20건 | -| 4.6 | price_bend → items (SM) + prices | ⏳ | ~10건 | -| 4.7 | price_pole → items (SM) + prices | ⏳ | ~5건 | -| 4.8 | price_screenplate → items (SM) + prices | ⏳ | ~5건 | -| 4.9 | price_smokeban → items (SM) + prices | ⏳ | ~5건 | -| 4.10 | 단가 버전 이력 정리 | ⏳ | effective_from/to 설정 | -| 4.11 | ⚠️ **사용자 승인**: 단가 INSERT 실행 | ⏳ | | - -### 4.5 Phase 5: 입고/재고 데이터 이관 ⭐ (신규) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | instock → item_receipts | ⏳ | 2,286건 | -| 5.2 | instock 재고 계산 → stocks | ⏳ | 현재고 집계 | -| 5.3 | lot → lots | ⏳ | 로트 관리 | -| 5.4 | lot_sales → lot_sales | ⏳ | 로트 소진 | -| 5.5 | ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 | ⏳ | | - -### 4.6 Phase 6: 주문/출고 데이터 이관 ⭐ (신규) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 6.1 | output → orders 헤더 | ⏳ | 24,564건 | -| 6.2 | output.iList JSON 파일 파싱 | ⏳ | 파일 경로 → JSON 읽기 | -| 6.3 | JSON → order_items 생성 | ⏳ | pages 배열 처리 | -| 6.4 | JSON.approval → orders 승인 정보 | ⏳ | approved_by, approved_at | -| 6.5 | estimate → orders (type=견적) | ⏳ | 견적 데이터 | -| 6.6 | ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 | ⏳ | | - ---- - -## 5. SQL 쿼리 (예상) - -### 5.0 KDunitprice → items (마스터) ⭐ 최우선! - -```sql --- KDunitprice: 품목 마스터 (603건) → SAM items --- ⚠️ 이 쿼리를 가장 먼저 실행하여 items 마스터 생성 - -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, description, is_active, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - -- item_div → item_type 매핑 - CASE item_div - WHEN '[제품]' THEN 'FG' - WHEN '[상품]' THEN 'FG' - WHEN '[반제품]' THEN 'PT' - WHEN '[부재료]' THEN 'SM' - WHEN '[원재료]' THEN 'RM' - WHEN '[무형상품]' THEN 'CS' - ELSE 'SM' - END AS item_type, - 품목코드 AS code, -- 유니크 키! - 품목명 AS name, - 단위 AS unit, - JSON_OBJECT( - 'spec', 규격, - 'item_div', item_div, - 'legacy_source', 'KDunitprice' - ) AS attributes, - 비고 AS description, - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice -WHERE 품목코드 IS NOT NULL AND 품목코드 != ''; - --- 결과 확인 -SELECT item_type, COUNT(*) -FROM samdb.items -WHERE tenant_id = 287 -GROUP BY item_type; -``` - -### 5.0.1 KDunitprice → prices (기본 단가) - -```sql --- KDunitprice의 입고가/출고가 → prices 테이블 -INSERT INTO samdb.prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, - effective_from, status, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - i.item_type AS item_type_code, - i.id AS item_id, - NULL AS client_group_id, -- 기본가 - COALESCE(k.입고가, 0) AS purchase_price, - COALESCE(k.출고가, 0) AS sales_price, - CURDATE() AS effective_from, -- 적용일 - 'active' AS status, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice k -JOIN samdb.items i ON i.code = k.품목코드 AND i.tenant_id = 287 -WHERE k.품목코드 IS NOT NULL AND k.품목코드 != ''; -``` - -### 5.1 models → items (FG) - -```sql --- 레거시 chandj.models → SAM items (FG) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'FG' AS item_type, - CONCAT('FG-', model_name, '-', - COALESCE(guiderail_type, 'STD'), '-', - CASE finishing_type - WHEN 'SUS마감' THEN 'SUS' - WHEN 'EGI마감' THEN 'EGI' - ELSE 'STD' - END - ) AS code, - CONCAT(model_name, ' ', major_category, ' ', finishing_type, ' ', COALESCE(guiderail_type, '')) AS name, - 'EA' AS unit, - JSON_OBJECT( - 'major_category', major_category, - 'finishing_type', finishing_type, - 'guiderail_type', guiderail_type, - 'legacy_model_id', model_id - ) AS attributes, - CASE WHEN is_deleted = 0 THEN 1 ELSE 0 END AS is_active, - 1 AS created_by, - created_at, - updated_at -FROM chandj.models -WHERE is_deleted = 0; -``` - -### 5.2 category_l4 → items (PT) - -```sql --- 레거시 4단계 카테고리 → SAM items (PT) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'PT' AS item_type, - CONCAT('PT-', l1.name, '-', l2.name, '-', l3.name, '-', l4.name) AS code, - l4.name AS name, - 'EA' AS unit, - JSON_OBJECT( - 'category_l1', l1.name, - 'category_l2', l2.name, - 'category_l3', l3.name, - 'category_l4', l4.name, - 'legacy_l4_id', l4.id - ) AS attributes, - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.category_l4 l4 -JOIN chandj.category_l3 l3 ON l4.parent_id = l3.id -JOIN chandj.category_l2 l2 ON l3.parent_id = l2.id -JOIN chandj.category_l1 l1 ON l2.parent_id = l1.id; -``` - -### 5.3 price_motor → items (SM) + prices [PHP 스크립트] - -```php -query(" - SELECT num, registedate, itemList - FROM price_motor - WHERE is_deleted = 0 - ORDER BY registedate DESC -"); -$priceRecords = $stmt->fetchAll(PDO::FETCH_ASSOC); - -// 최신 단가의 itemList 파싱 → items 생성 -$latestRecord = $priceRecords[0]; -$itemList = json_decode($latestRecord['itemList'], true); - -foreach ($itemList as $idx => $item) { - $voltage = $item['col1']; // 220, 380, 제어기, 방화, 방범 - $capacity = $item['col2']; // 150K(S), 300K, 노출형, 매립형... - $purchasePrice = (float)str_replace(',', '', $item['col11'] ?? '0'); - $salesPrice = (float)str_replace(',', '', $item['col13'] ?? '0'); - - // 품목 코드 생성 - $code = "SM-MOTOR-" . preg_replace('/[^A-Za-z0-9가-힣]/', '', $voltage) - . "-" . preg_replace('/[^A-Za-z0-9가-힣()]/', '', $capacity); - - // 품목명 생성 - if (in_array($voltage, ['220', '380'])) { - $name = "전동개폐기 {$voltage}V {$capacity}"; - $itemType = 'SM'; - } elseif ($voltage === '제어기') { - $name = "연동제어기 {$capacity}"; - $itemType = 'SM'; - } else { - $name = "{$voltage} {$capacity}"; - $itemType = 'SM'; - } - - // 1단계: items INSERT - $itemStmt = $pdo->prepare(" - INSERT INTO items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at - ) VALUES (?, ?, ?, ?, 'EA', ?, 1, ?, NOW(), NOW()) - ON DUPLICATE KEY UPDATE name = VALUES(name) - "); - $attributes = json_encode([ - 'voltage' => $voltage, - 'capacity' => $capacity, - 'legacy_source' => 'price_motor', - 'legacy_col_index' => $idx - ]); - $itemStmt->execute([$tenantId, $itemType, $code, $name, $attributes, $userId]); - $itemId = $pdo->lastInsertId(); - - // 2단계: prices INSERT (모든 버전) - foreach ($priceRecords as $priceIdx => $priceRecord) { - $priceItemList = json_decode($priceRecord['itemList'], true); - if (!isset($priceItemList[$idx])) continue; - - $priceItem = $priceItemList[$idx]; - $pPrice = (float)str_replace(',', '', $priceItem['col11'] ?? '0'); - $sPrice = (float)str_replace(',', '', $priceItem['col13'] ?? '0'); - $effectiveFrom = $priceRecord['registedate']; - - // 다음 레코드가 있으면 effective_to 설정 - $effectiveTo = isset($priceRecords[$priceIdx + 1]) - ? date('Y-m-d', strtotime($effectiveFrom . ' -1 day')) - : null; - - $status = ($priceIdx === 0) ? 'active' : 'inactive'; - - $priceStmt = $pdo->prepare(" - INSERT INTO prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, effective_from, effective_to, - status, created_by, created_at, updated_at - ) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) - "); - $priceStmt->execute([ - $tenantId, $itemType, $itemId, - $pPrice, $sPrice, $effectiveFrom, $effectiveTo, - $status, $userId - ]); - } - - echo "✓ {$code} - items + prices 생성 완료\n"; -} -``` - -### 5.4 단가 마이그레이션 요약 스크립트 - -```php - ['item_type' => 'SM', 'prefix' => 'MOTOR'], - 'price_shaft' => ['item_type' => 'SM', 'prefix' => 'SHAFT'], - 'price_pipe' => ['item_type' => 'SM', 'prefix' => 'PIPE'], - 'price_angle' => ['item_type' => 'SM', 'prefix' => 'ANGLE'], - 'price_raw_materials' => ['item_type' => 'RM', 'prefix' => 'RAW'], - 'price_bend' => ['item_type' => 'SM', 'prefix' => 'BEND'], - 'price_pole' => ['item_type' => 'SM', 'prefix' => 'POLE'], - 'price_screenplate' => ['item_type' => 'SM', 'prefix' => 'SCREEN'], - 'price_smokeban' => ['item_type' => 'SM', 'prefix' => 'SMOKE'], -]; - -$totalItems = 0; -$totalPrices = 0; - -foreach ($priceTables as $table => $config) { - echo "\n📦 Processing: {$table}\n"; - - // 각 테이블별 JSON 스키마에 맞는 파싱 로직 호출 - list($itemCount, $priceCount) = migratePrice($table, $config); - - $totalItems += $itemCount; - $totalPrices += $priceCount; - - echo " → items: {$itemCount}, prices: {$priceCount}\n"; -} - -echo "\n✅ 마이그레이션 완료!\n"; -echo " 총 items: {$totalItems}\n"; -echo " 총 prices: {$totalPrices}\n"; -``` - ---- - -## 6. 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 📦 데이터 전략 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - KDunitprice(603건)가 품목 마스터 → items 최우선 생성 │ -│ - code 필드로 중복 방지 (ON DUPLICATE KEY UPDATE) │ -│ - BOM은 item_bom_items 테이블 사용 (items.bom JSON ❌) │ -│ - 단가 정보는 prices 테이블에 별도 저장 (items.attributes ❌) │ -│ │ -│ ❌ 불필요한 것 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - item_id_mappings 테이블 (양방향 조회 불필요) │ -│ - chandj 수정 (손 안댐, samdb에만 밀어 넣음) │ -│ - 레거시 소스 확인 (마이그레이션 후 검증만) │ -│ │ -│ ✅ 필수 사항 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - 경동기업 기준으로 맞춤 (이미 사용중인 시스템) │ -│ - 전체 이관 (instock 2,286건, output 24,564건 포함) │ -│ - SQL 쿼리 + PHP 스크립트 혼용 (JSON 파싱 필요) │ -│ - 로컬 검증 완료 후 개발서버 배포 │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.1 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | SELECT 쿼리, 분석, 매핑 설계 | 불필요 | -| ⚠️ 컨펌 필요 | INSERT 실행, TRUNCATE, 개발서버 배포 | **필수** | -| 🔴 금지 | 운영서버 직접 작업 | 별도 협의 | - ---- - -## 7. 데이터 규모 예상 (전체 마이그레이션) - -### 7.1 items 테이블 예상 - -| 소스 | 레코드 수 | SAM item_type | 예상 items 건수 | -|------|----------|---------------|----------------| -| **KDunitprice** ⭐ | **603** | FG/PT/SM/RM/CS | **~603 (마스터)** | -| models | 18 | FG | ~0 (중복 제외) | -| category_l4 | 37 | PT | ~20 (일부 신규) | -| item_list | 5 | PT | ~0 (중복 제외) | -| price_* 테이블 | ~130 항목 | SM/RM | ~100 (신규만) | -| **items 합계** | - | - | **~700~800건** | - -**item_type별 분포 예상**: -| item_type | 설명 | 예상 건수 | -|-----------|------|----------| -| FG | 완제품 | ~100건 | -| PT | 부품 | ~250건 | -| SM | 부자재 | ~300건 | -| RM | 원자재 | ~100건 | -| CS | 소모품 | ~50건 | - -### 7.2 prices 테이블 예상 ⭐ - -| 소스 | 버전 수 | 품목당 단가 | 예상 prices 건수 | -|------|--------|------------|-----------------| -| KDunitprice | 1 | 603 | ~603 | -| price_motor | 2 | 35 | ~70 | -| price_shaft | 2 | 15 | ~30 | -| price_pipe | 2 | 10 | ~20 | -| price_angle | 2 | 10 | ~20 | -| price_raw_materials | 6 | 20 | ~120 | -| price_bend | 3 | 10 | ~30 | -| 기타 price_* | 2 | 15 | ~30 | -| **prices 합계** | - | - | **~500건** (중복 제외) - -### 7.3 입고/재고 테이블 예상 ⭐ (신규) - -| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | -|------|----------|------------|----------| -| instock | 2,286 | item_receipts | ~2,286 | -| instock (집계) | - | stocks | ~500 (품목별 현재고) | -| lot | - | lots | ~200 | -| lot_sales | - | lot_sales | ~300 | -| **합계** | - | - | **~3,300건** | - -### 7.4 주문/출고 테이블 예상 ⭐ (신규) - -| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | -|------|----------|------------|----------| -| output | 24,564 | orders | ~24,564 | -| output.iList (JSON) | ~24,564 파일 | order_items | ~50,000 (주문당 2건 평균) | -| estimate | - | orders (type=견적) | ~500 | -| **합계** | - | - | **~75,000건** | - -### 7.5 전체 마이그레이션 요약 - -| SAM 테이블 | 예상 건수 | 비고 | -|------------|----------|------| -| items | ~800 | 품목 마스터 | -| item_bom_items | ~300 | BOM 관계 | -| item_details | ~200 | 제품 상세 | -| prices | ~500 | 단가 정보 | -| item_receipts | ~2,300 | 입고 기록 | -| stocks | ~500 | 현재고 | -| lots | ~200 | 로트 | -| lot_sales | ~300 | 로트 소진 | -| orders | ~25,000 | 주문 헤더 | -| order_items | ~50,000 | 주문 상세 | -| **총계** | **~80,000건** | | - ---- - -## 8. 체크리스트 - -### Phase 1: 마스터 데이터 이관 -- [x] 레거시 DB 구조 분석 완료 -- [x] KDunitprice 테이블 발견 및 분석 (603건, 핵심 마스터) -- [x] 중복 제거 전략 수립 (code 기반, 매핑 테이블 불필요) -- [ ] **KDunitprice → items 마이그레이션 스크립트 작성** ⭐ -- [ ] models → items (FG) INSERT 쿼리 작성 (중복 확인) -- [ ] category_l4 → items (PT) INSERT 쿼리 작성 (중복 확인) -- [ ] ⚠️ **사용자 승인**: 로컬 INSERT 실행 - -### Phase 2: BOM 데이터 이관 -- [ ] BDmodels.savejson 파싱 로직 작성 -- [ ] child_item_id 매핑 테이블 생성 -- [ ] items.bom JSON 생성 -- [ ] ⚠️ **사용자 승인**: BOM 데이터 INSERT 실행 - -### Phase 3: 검증 및 배포 -- [ ] 건수 검증 -- [ ] API 테스트 -- [ ] ⚠️ **사용자 승인**: 개발서버 배포 - -### Phase 4: 단가 데이터 이관 ⭐ -- [x] 레거시 price_* 테이블 구조 분석 (10개) -- [x] 각 테이블별 JSON 스키마 분석 -- [x] SAM prices 테이블 구조 확인 -- [x] Legacy → SAM 단가 매핑 전략 수립 -- [ ] price_motor → prices 연결 스크립트 작성 -- [ ] price_shaft → prices 연결 스크립트 작성 -- [ ] price_pipe → prices 연결 스크립트 작성 -- [ ] price_angle → prices 연결 스크립트 작성 -- [ ] price_raw_materials → prices 연결 스크립트 작성 -- [ ] 기타 price_* 테이블 처리 -- [ ] 단가 버전 이력 정리 (effective_from/to) -- [ ] ⚠️ **사용자 승인**: 단가 INSERT 실행 - -### Phase 5: 입고/재고 데이터 이관 ⭐ (신규) -- [ ] instock 테이블 구조 분석 -- [ ] instock → item_receipts 매핑 설계 -- [ ] 재고 집계 → stocks 매핑 설계 -- [ ] lot/lot_sales 구조 분석 -- [ ] 마이그레이션 스크립트 작성 -- [ ] ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 - -### Phase 6: 주문/출고 데이터 이관 ⭐ (신규) -- [ ] output 테이블 구조 분석 -- [ ] output.iList JSON 파일 구조 분석 (완료) -- [ ] output → orders 매핑 설계 -- [ ] JSON → order_items 매핑 설계 -- [ ] estimate → orders 매핑 설계 -- [ ] 마이그레이션 스크립트 작성 (24,564건) -- [ ] ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 - ---- - -## 9. 참고 문서 - -- **레거시 소스**: `5130/` 폴더 -- **SAM items 마이그레이션**: `api/database/migrations/2025_12_13_152507_create_items_table.php` -- **SAM prices 마이그레이션**: `api/database/migrations/2025_12_08_154633_create_prices_table.php` -- **SAM price_revisions 마이그레이션**: `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php` -- **품목 분석**: `docs/data/analysis/item-db-analysis.md` -- **DummyItemSeeder**: `api/database/seeders/Dummy/DummyItemSeeder.php` -- **DummyDataSeeder**: `api/database/seeders/DummyDataSeeder.php` (TENANT_ID=287, USER_ID=1 상수 참조) -- **prices item_type_code 마이그레이션**: `api/database/migrations/2025_12_21_165524_update_prices_item_type_code_to_actual_item_type.php` - ---- - -## 10. 세션 및 메모리 관리 정책 - -### 10.1 세션 시작 시 (Load Strategy) -```bash -# 1. Docker 확인 -docker ps | grep sam - -# 2. DB 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" - -# 3. 현재 진행 상태 확인 -# → 이 문서의 "📍 현재 진행 상태" 섹션 참조 - -# 4. 마이그레이션 상태 확인 (API 프로젝트) -cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status -``` - -### 10.2 작업 중 관리 - -| 작업 완료 시 | 조치 | -|-------------|------| -| Phase 완료 | "📍 현재 진행 상태" 업데이트 | -| INSERT 실행 | "10. 변경 이력" 추가 | -| 스키마 변경 | 관련 섹션 업데이트 + 변경 이력 추가 | -| 오류 발생 | 체크리스트에 메모 추가 | - -### 10.3 컨텍스트 관리 - -| 컨텍스트 잔량 | 조치 | -|--------------|------| -| **30% 이하** | 현재 작업 중단점 문서에 기록 | -| **20% 이하** | "📍 현재 진행 상태" 최종 업데이트 | -| **10% 이하** | 세션 정리 및 다음 세션 가이드 작성 | - ---- - -## 11. 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 0 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Quick Start 전제 조건 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 SQL 쿼리 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 0 확인 방법 SQL | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 건수, 테이블명, 컬럼 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 문서 헤더 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 → 다음 작업 상세 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 9. 참고 문서 | -| Q4. 작업 완료 확인 방법은? | ✅ | 0. 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서, Quick Start DB 접속 명령어 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - -### 11.3 핵심 정보 요약 (새 세션용) - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 📋 핵심 정보 요약 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 🎯 목표: 경동기업 레거시(chandj) → SAM(samdb) 전체 데이터 이관 │ -│ │ -│ 📊 데이터 규모 (총 ~80,000건): │ -│ - items: ~800건 (KDunitprice 603 + 추가) │ -│ - prices: ~500건 │ -│ - item_receipts: ~2,300건 (입고) │ -│ - orders + order_items: ~75,000건 (주문) │ -│ │ -│ 🔑 핵심 상수: │ -│ - tenant_id = 287 (경동기업) │ -│ - user_id = 1 (생성자) │ -│ - Docker: sam-mysql-1 │ -│ - 레거시 DB: chandj / SAM DB: samdb ⚠️ │ -│ │ -│ ⭐ 마이그레이션 순서: │ -│ 1. KDunitprice → items (마스터, 603건) ← 최우선! │ -│ 2. code 기반 중복 확인 후 추가 items 생성 │ -│ 3. prices 연결 (item_id 참조) │ -│ 4. BOM, 입고, 주문 순서대로 진행 │ -│ │ -│ 📍 현재 상태: Phase 1 대기 (KDunitprice → items 마스터 INSERT) │ -│ │ -│ ❌ 불필요한 것: item_id_mappings (양방향 조회 불필요, chandj 손 안댐) │ -│ │ -│ ⚠️ 주의: 모든 INSERT 실행 전 사용자 승인 필요 │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 12. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-28 | 문서 재작성 | 레거시 5130/ 분석 기반으로 완전 재작성 | - | - | -| 2026-01-28 | 단가 시스템 추가 | price_* 테이블 분석, SAM prices 매핑 전략 | - | - | -| 2026-01-28 | 자기완결성 보완 | Quick Start, 성공 기준, 세션 관리, 자기완결성 점검 섹션 추가 | - | - | -| 2026-01-28 | **전체 범위 확장** | KDunitprice(603건) 발견, Phase 5/6 추가, ~80,000건 전체 이관 | - | - | -| 2026-01-28 | 중복 제거 전략 | code 기반 단순화, item_id_mappings 제거 | - | - | -| 2026-01-28 | DB 이름 수정 | sam → samdb 수정 | - | - | -| 2026-01-28 | output.iList | JSON 파일 구조 분석 및 문서화 | - | - | - ---- - -## 13. 트러블슈팅 가이드 - -### 13.1 일반적인 문제 - -| 문제 | 원인 | 해결책 | -|------|------|--------| -| Docker 컨테이너 없음 | Docker 미실행 | `docker-compose up -d` 실행 | -| DB 접속 실패 | 컨테이너명 변경 | `docker ps`로 정확한 컨테이너명 확인 | -| chandj DB 없음 | 레거시 DB 미설정 | Docker 볼륨 확인 또는 덤프 복원 | -| tenant_id 287 없음 | 경동기업 테넌트 미생성 | SAM에서 테넌트 생성 필요 | -| items 테이블 없음 | 마이그레이션 미실행 | `php artisan migrate` 실행 | -| **SAM DB 이름 오류** | `sam` 대신 `samdb` | 모든 쿼리에서 `samdb` 사용 확인 | -| KDunitprice 테이블 없음 | 레거시 덤프 불완전 | chandj 전체 덤프 확인 | -| output.iList 파일 없음 | JSON 파일 경로 오류 | `5130/output/i_json/` 폴더 확인 | - -### 13.2 JSON 파싱 오류 - -```php -// price_* 테이블의 itemList 파싱 시 주의사항 -$itemList = json_decode($record['itemList'], true); - -// 빈 값 또는 잘못된 JSON 처리 -if (empty($itemList) || !is_array($itemList)) { - // 스킵하고 로그 기록 - error_log("Invalid itemList in {$table} num={$record['num']}"); - continue; -} - -// 숫자 형식 변환 (콤마 제거) -$price = (float)str_replace(',', '', $item['col13'] ?? '0'); -``` - -### 13.3 중복 코드 처리 (code 기반) - -```sql --- 이미 존재하는 품목 확인 (code 유일성 검사) -SELECT code, COUNT(*) AS cnt -FROM samdb.items -WHERE tenant_id=287 -GROUP BY code -HAVING cnt > 1; - --- INSERT 시 ON DUPLICATE KEY UPDATE 사용 --- ⚠️ items 테이블에 (tenant_id, code) UNIQUE 인덱스 필요 -INSERT INTO samdb.items (...) VALUES (...) -ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW(); - --- KDunitprice와 price_* 중복 확인 -SELECT k.품목코드, '모터 150K' AS price_item -FROM chandj.KDunitprice k -WHERE k.품목명 LIKE '%모터%150K%'; --- → KDunitprice가 마스터, price_*는 가격만 추가 -``` - -### 13.4 output.iList JSON 파일 처리 - -```php -// output.iList 값 예시: "../output/i_json/22545.json" -$iListPath = $output['iList']; // "../output/i_json/22545.json" - -// 실제 파일 경로로 변환 -$basePath = '/Users/kent/Works/@KD_SAM/SAM/5130'; -$jsonFile = str_replace('../', '', $iListPath); -$fullPath = $basePath . '/' . $jsonFile; - -// JSON 파일 읽기 -if (file_exists($fullPath)) { - $jsonContent = json_decode(file_get_contents($fullPath), true); - // $jsonContent['inputValue'], $jsonContent['pages'] 등 사용 -} else { - // 파일 없음 - 로그 기록 후 스킵 - error_log("JSON file not found: {$fullPath}"); -} -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/quote-management-url-migration-plan.md b/plans/quote-management-url-migration-plan.md deleted file mode 100644 index 9f2ce61..0000000 --- a/plans/quote-management-url-migration-plan.md +++ /dev/null @@ -1,1282 +0,0 @@ -# 견적관리 URL 구조 마이그레이션 계획 - -> **작성일**: 2026-01-26 -> **목적**: 견적관리 페이지 URL 구조를 Query 기반(?mode=new)에서 RESTful 경로 기반(/test-new, /test/[id])으로 마이그레이션 -> **기준 문서**: docs/standards/api-rules.md, docs/specs/database-schema.md -> **상태**: 📋 계획 수립 완료 (Serena ID: quote-url-migration-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3 코드 작업 완료 - 목록 페이지 링크 V2 적용 | -| **다음 작업** | Step 3.2: 통합 테스트 (사용자 수동 테스트) | -| **진행률** | 11/12 (92%) - Phase 1 ✅, Phase 2 ✅, Phase 3 (테스트 제외) ✅ | -| **마지막 업데이트** | 2026-01-26 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 견적관리 시스템에는 두 가지 URL 패턴이 공존합니다: - -**V1 (기존 - Query 기반):** -- 목록: `/sales/quote-management` -- 등록: `/sales/quote-management?mode=new` -- 상세: `/sales/quote-management/[id]` -- 수정: `/sales/quote-management/[id]?mode=edit` - -**V2 (신규 - RESTful 경로 기반):** -- 목록: `/sales/quote-management` (동일) -- 등록: `/sales/quote-management/test-new` -- 상세: `/sales/quote-management/test/[id]` -- 수정: `/sales/quote-management/test/[id]?mode=edit` - -V2는 `IntegratedDetailTemplate` + `QuoteRegistrationV2` 컴포넌트를 사용하며, 현재 테스트(Mock 데이터) 상태입니다. - -### 1.2 목표 - -1. V2 페이지에 실제 API 연동 완료 -2. V2 URL 패턴을 정식 경로로 채택 (test 접두사 제거) -3. V1 페이지 삭제 또는 V2로 리다이렉트 처리 -4. DB 스키마 변경 없이 기존 API 활용 - -### 1.3 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - DB 스키마 변경 없음 (기존 quotes, quote_items 테이블 활용) │ -│ - 기존 API 엔드포인트 재사용 (POST/PUT /api/v1/quotes) │ -│ - V1 → V2 단계적 마이그레이션 (병행 기간 최소화) │ -│ - IntegratedDetailTemplate 표준 적용 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 컴포넌트 수정, API 연동 코드, 타입 정의 | 불필요 | -| ⚠️ 컨펌 필요 | 라우트 경로 변경, 기존 페이지 삭제/리다이렉트 | **필수** | -| 🔴 금지 | DB 스키마 변경, 기존 API 엔드포인트 삭제 | 별도 협의 | - -### 1.5 준수 규칙 -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/standards/api-rules.md` - API 개발 규칙 - ---- - -## 2. 현재 상태 분석 - -### 2.1 파일 구조 비교 - -#### V1 (기존) -``` -react/src/app/[locale]/(protected)/sales/quote-management/ -├── page.tsx # 목록 + mode=new 감지 → QuoteRegistration -├── new/page.tsx # 리다이렉트용 (거의 미사용) -├── [id]/page.tsx # 상세 + mode=edit 감지 → QuoteRegistration -└── [id]/edit/page.tsx # 리다이렉트용 (거의 미사용) -``` - -#### V2 (신규) -``` -react/src/app/[locale]/(protected)/sales/quote-management/ -├── test-new/page.tsx # 등록 (IntegratedDetailTemplate) -├── test/[id]/page.tsx # 상세/수정 (IntegratedDetailTemplate) -└── test/[id]/edit/page.tsx # 리다이렉트 → test/[id]?mode=edit -``` - -### 2.2 컴포넌트 비교 - -| 항목 | V1 (QuoteRegistration) | V2 (QuoteRegistrationV2) | -|------|------------------------|--------------------------| -| 파일 크기 | ~50KB | ~45KB | -| 레이아웃 | 단일 폼 | 좌우 분할 (개소 목록 \| 상세) | -| 템플릿 | 자체 레이아웃 | IntegratedDetailTemplate | -| 데이터 구조 | `QuoteFormData` | `QuoteFormDataV2` + `LocationItem` | -| API 연동 | ✅ 완료 | ❌ Mock 데이터 | -| 상태 관리 | `status: string` | `status: 'draft' \| 'temporary' \| 'final'` | - -### 2.3 데이터 구조 비교 - -#### V1: QuoteFormData -```typescript -interface QuoteFormData { - id?: string; - quoteNumber?: string; - registrationDate?: string; - clientId?: string | number; - clientName?: string; - siteName?: string; - manager?: string; - contact?: string; - dueDate?: string; - remarks?: string; - status?: string; - items?: QuoteItem[]; // 층별 항목 - bomMaterials?: BomMaterial[]; - calculationInputs?: Record; -} -``` - -#### V2: QuoteFormDataV2 -```typescript -interface QuoteFormDataV2 { - id?: string; - registrationDate: string; - writer: string; - clientId: string; - clientName: string; - siteName: string; - manager: string; - contact: string; - dueDate: string; - remarks: string; - status: 'draft' | 'temporary' | 'final'; - locations: LocationItem[]; // 개소별 항목 (더 상세한 구조) -} - -interface LocationItem { - id: string; - floor: string; - code: string; - openWidth: number; - openHeight: number; - productCode: string; - productName: string; - quantity: number; - guideRailType: string; - motorPower: string; - controller: string; - wingSize: number; - inspectionFee: number; - unitPrice?: number; - totalPrice?: number; - bomResult?: BomCalculationResult; -} -``` - -### 2.4 API 엔드포인트 (변경 없음) - -| HTTP | Endpoint | 설명 | V1 사용 | V2 사용 | -|------|----------|------|:-------:|:-------:| -| GET | `/api/v1/quotes` | 목록 조회 | ✅ | ✅ | -| GET | `/api/v1/quotes/{id}` | 단건 조회 | ✅ | 🔲 (TODO) | -| POST | `/api/v1/quotes` | 생성 | ✅ | 🔲 (TODO) | -| PUT | `/api/v1/quotes/{id}` | 수정 | ✅ | 🔲 (TODO) | -| POST | `/api/v1/quotes/calculate/bom/bulk` | BOM 자동산출 | ✅ | ✅ | - -### 2.5 DB 스키마 (변경 없음) - -**quotes 테이블** - 그대로 사용 -```sql --- 핵심 필드 -id, tenant_id, quote_number -registration_date, author -client_id, client_name, manager, contact -site_name, site_code -product_category, product_id, product_code, product_name -open_size_width, open_size_height, quantity -material_cost, labor_cost, install_cost -subtotal, discount_rate, discount_amount, total_amount -status, is_final -calculation_inputs (JSON) -options (JSON) -``` - -**quote_items 테이블** - 그대로 사용 -```sql -id, quote_id, tenant_id -item_id, item_code, item_name, specification, unit -base_quantity, calculated_quantity -unit_price, total_price -formula, formula_result, formula_source, formula_category -sort_order -``` - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: V2 API 연동 (프론트엔드) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | V2 데이터 변환 함수 구현 | ✅ | `transformV2ToApi`, `transformApiToV2` (2026-01-26) | -| 1.2 | test-new 페이지 API 연동 (createQuote) | ✅ | Mock → 실제 API (2026-01-26) | -| 1.3 | test/[id] 페이지 API 연동 (getQuoteById) | ✅ | Mock → 실제 API (2026-01-26) | -| 1.4 | test/[id] 수정 API 연동 (updateQuote) | ✅ | Mock → 실제 API (2026-01-26) | - -### 3.2 Phase 2: URL 경로 정식화 (라우팅) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | test-new → new 경로 변경 | ✅ | V2 버전으로 교체 완료 (2026-01-26) | -| 2.2 | test/[id] → [id] 경로 통합 | ✅ | V2 버전으로 교체 완료 (2026-01-26) | -| 2.3 | 기존 V1 페이지 처리 결정 | ✅ | V1 백업 보존, test 폴더 삭제 | - -### 3.3 Phase 3: 정리 및 테스트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | V1 컴포넌트/페이지 정리 | ✅ | test 폴더 삭제 완료, V1 백업 보존 | -| 3.2 | 통합 테스트 | ⏳ | CRUD + 문서출력 + 상태전환 (사용자 테스트) | -| 3.3 | 목록 페이지 링크 업데이트 | ✅ | QuoteManagementClient, DevToolbar 완료 | -| 3.4 | 문서 업데이트 | ✅ | 계획 문서 완료 | - ---- - -## 4. 작업 절차 - -### 4.1 단계별 절차 - -``` -Phase 1: V2 API 연동 -├── Step 1.1: 데이터 변환 함수 -│ ├── transformV2ToApi() - V2 → API 요청 형식 -│ ├── transformApiToV2() - API 응답 → V2 형식 -│ └── actions.ts에 추가 -│ -├── Step 1.2: test-new 페이지 연동 -│ ├── handleSave에서 createQuote 호출 -│ ├── 성공 시 /sales/quote-management/test/{id}로 이동 -│ └── 에러 처리 -│ -├── Step 1.3: test/[id] 상세 페이지 연동 -│ ├── useEffect에서 getQuoteById 호출 -│ ├── transformApiToV2로 데이터 변환 -│ └── 로딩/에러 상태 처리 -│ -└── Step 1.4: test/[id] 수정 연동 - ├── handleSave에서 updateQuote 호출 - ├── 성공 시 view 모드로 복귀 - └── 에러 처리 - -Phase 2: URL 경로 정식화 -├── Step 2.1: 새 경로 생성 -│ ├── new/page.tsx → IntegratedDetailTemplate 버전 -│ └── 기존 new/page.tsx 백업 -│ -├── Step 2.2: 상세 경로 통합 -│ ├── [id]/page.tsx를 V2 버전으로 교체 -│ └── 기존 [id]/page.tsx 백업 -│ -└── Step 2.3: V1 처리 - ├── 옵션 A: V1 페이지 삭제 - └── 옵션 B: V1 → V2 리다이렉트 - -Phase 3: 정리 및 테스트 -├── Step 3.1: 파일 정리 -│ ├── test-new, test/[id] 폴더 삭제 -│ ├── V1 백업 파일 삭제 (확인 후) -│ └── 미사용 컴포넌트 정리 -│ -├── Step 3.2: 통합 테스트 -│ ├── 신규 등록 → 저장 → 상세 확인 -│ ├── 상세 → 수정 → 저장 → 상세 확인 -│ ├── 문서 출력 (견적서, 산출내역서, 발주서) -│ ├── 최종확정 → 수주전환 -│ └── 목록 링크 동작 확인 -│ -├── Step 3.3: 목록 페이지 링크 -│ └── QuoteManagementClient의 라우팅 경로 확인 -│ -└── Step 3.4: 문서 업데이트 - ├── 이 계획 문서 완료 처리 - └── 필요시 claudedocs에 작업 기록 -``` - -### 4.2 데이터 변환 상세 - -#### V2 → API (저장 시) -```typescript -function transformV2ToApi(data: QuoteFormDataV2) { - return { - registration_date: data.registrationDate, - author: data.writer, - client_id: data.clientId || null, - client_name: data.clientName, - site_name: data.siteName, - manager: data.manager, - contact: data.contact, - completion_date: data.dueDate, - remarks: data.remarks, - status: data.status === 'final' ? 'finalized' : data.status, - - // locations → items 변환 - items: data.locations.map((loc, index) => ({ - floor: loc.floor, - code: loc.code, - product_code: loc.productCode, - product_name: loc.productName, - open_width: loc.openWidth, - open_height: loc.openHeight, - quantity: loc.quantity, - guide_rail_type: loc.guideRailType, - motor_power: loc.motorPower, - controller: loc.controller, - wing_size: loc.wingSize, - inspection_fee: loc.inspectionFee, - unit_price: loc.unitPrice, - total_price: loc.totalPrice, - sort_order: index, - })), - - // calculation_inputs 생성 (첫 번째 location 기준) - calculation_inputs: data.locations.length > 0 ? { - W0: data.locations[0].openWidth, - H0: data.locations[0].openHeight, - QTY: data.locations[0].quantity, - GT: data.locations[0].guideRailType, - MP: data.locations[0].motorPower, - } : null, - }; -} -``` - -#### API → V2 (조회 시) -```typescript -function transformApiToV2(apiData: QuoteResponse): QuoteFormDataV2 { - return { - id: apiData.id, - registrationDate: apiData.registrationDate, - writer: apiData.author || '', - clientId: String(apiData.clientId || ''), - clientName: apiData.clientName || '', - siteName: apiData.siteName || '', - manager: apiData.manager || '', - contact: apiData.contact || '', - dueDate: apiData.completionDate || '', - remarks: apiData.remarks || '', - status: mapApiStatusToV2(apiData.status), - - // items → locations 변환 - locations: (apiData.items || []).map(item => ({ - id: String(item.id), - floor: item.floor || '', - code: item.code || '', - openWidth: item.openWidth || 0, - openHeight: item.openHeight || 0, - productCode: item.productCode || '', - productName: item.productName || '', - quantity: item.quantity || 1, - guideRailType: item.guideRailType || 'wall', - motorPower: item.motorPower || 'single', - controller: item.controller || 'basic', - wingSize: item.wingSize || 50, - inspectionFee: item.inspectionFee || 0, - unitPrice: item.unitPrice, - totalPrice: item.totalPrice, - })), - }; -} - -function mapApiStatusToV2(apiStatus: string): 'draft' | 'temporary' | 'final' { - switch (apiStatus) { - case 'finalized': - case 'converted': - return 'final'; - case 'draft': - case 'sent': - case 'approved': - return 'draft'; - default: - return 'draft'; - } -} -``` - ---- - -## 5. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| C-1 | URL 경로 정식화 | test-new → new, test/[id] → [id] | 라우팅 전체 | ⏳ 대기 | -| C-2 | V1 페이지 처리 | 삭제 vs 리다이렉트 결정 | 기존 사용자 | ⏳ 대기 | -| C-3 | 컴포넌트 정리 | QuoteRegistration.tsx 삭제 여부 | 코드베이스 | ⏳ 대기 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-26 | Step 3.3, 3.4 | 목록 페이지 V2 URL 적용, 문서 업데이트 | page.tsx, QuoteManagementClient.tsx, DevToolbar.tsx | ✅ | -| 2026-01-26 | Step 3.1 | test 폴더 삭제, V1 백업 보존 | test-new/, test/ 삭제 | ✅ | -| 2026-01-26 | Step 2.1, 2.2 | URL 경로 정식화 (Phase 2 완료) | new/page.tsx, [id]/page.tsx | ✅ | -| 2026-01-26 | Step 1.3, 1.4 | test/[id] 상세/수정 API 연동 (Phase 1 완료) | test/[id]/page.tsx | ✅ | -| 2026-01-26 | Step 1.2 | test-new 페이지 createQuote API 연동 | test-new/page.tsx | ✅ | -| 2026-01-26 | Step 1.1 | V2 데이터 변환 함수 구현 완료 | types.ts | ✅ | -| 2026-01-26 | - | 계획 문서 초안 작성 | - | - | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **DB 스키마**: `docs/specs/database-schema.md` - -### 7.1 핵심 파일 경로 - -#### 프론트엔드 (React) -``` -# V1 (기존) -react/src/app/[locale]/(protected)/sales/quote-management/page.tsx -react/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx -react/src/components/quotes/QuoteRegistration.tsx (50KB) -react/src/components/quotes/actions.ts (28KB) -react/src/components/quotes/types.ts - -# V2 (신규) -react/src/app/[locale]/(protected)/sales/quote-management/test-new/page.tsx -react/src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx -react/src/components/quotes/QuoteRegistrationV2.tsx -react/src/components/quotes/LocationListPanel.tsx -react/src/components/quotes/LocationDetailPanel.tsx -react/src/components/quotes/QuoteSummaryPanel.tsx -react/src/components/quotes/QuoteFooterBar.tsx -react/src/components/quotes/quoteConfig.ts -``` - -#### 백엔드 (Laravel API) - 변경 없음 -``` -api/app/Http/Controllers/Api/V1/QuoteController.php -api/app/Http/Requests/Quote/QuoteStoreRequest.php -api/app/Http/Requests/Quote/QuoteUpdateRequest.php -api/app/Models/Quote/Quote.php -api/app/Models/Quote/QuoteItem.php -api/app/Services/Quote/QuoteService.php -api/app/Services/Quote/QuoteCalculationService.php -``` - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("quote-url-migration-state") // 1. 상태 파악 -read_memory("quote-url-migration-snapshot") // 2. 사고 흐름 복구 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 **Snapshot** | 현재까지의 코드 변경점과 논의 핵심 요약 | -| **20% 이하** | 🧹 **Context Purge** | 수정 중인 핵심 파일 및 함수 목록 | -| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 | - -### 8.3 Serena 메모리 구조 -- `quote-url-migration-state`: { phase, progress, next_step, last_decision } -- `quote-url-migration-snapshot`: 현재까지의 코드 변경 및 논의 요약 -- `quote-url-migration-active-files`: 수정 중인 파일 목록 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---------|------|----------|----------|------| -| 신규 등록 | 견적 정보 입력 후 저장 | DB 저장, 상세 페이지 이동 | - | ⏳ | -| 상세 조회 | /quote-management/[id] 접근 | 저장된 데이터 표시 | - | ⏳ | -| 수정 | mode=edit에서 수정 후 저장 | DB 업데이트, view 모드 복귀 | - | ⏳ | -| 문서 출력 | 견적서 버튼 클릭 | 견적서 모달 표시 | - | ⏳ | -| 최종확정 | 최종확정 버튼 클릭 | status → finalized | - | ⏳ | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|:----:|------| -| V2 API 연동 완료 | ✅ | Phase 1 완료 | -| URL 경로 정식화 | ✅ | Phase 2 완료 | -| V1 정리 완료 | ✅ | test 폴더 삭제, 백업 보존 | -| 통합 테스트 통과 | ⏳ | 사용자 테스트 필요 | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.2 목표 참조 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 3 대상 범위 참조 | -| 4 | 의존성이 명시되어 있는가? | ✅ | DB/API 변경 없음 명시 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7.1 검증 완료 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 상세 절차 참조 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 함수명, 경로 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.2 목표 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 4.1 Step 1.1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 7.1 핵심 파일 경로 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 테스트 케이스 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -## 부록 A: API 스키마 상세 - -> V2 연동 시 참고할 실제 API 요청/응답 스키마 - -### A.1 API 응답 타입 (QuoteApiData) - -```typescript -// react/src/components/quotes/types.ts 에서 발췌 - -interface QuoteApiData { - id: number; - quote_number: string; - registration_date: string; - - // 발주처 정보 - client_id: number | null; - client_name: string; - client?: { id: number; name: string; }; // with('client') 로드 시 - - // 현장 정보 - site_name: string | null; - site_code: string | null; - - // 담당자 정보 (API 실제 필드명) - manager?: string | null; // 담당자명 - contact?: string | null; // 연락처 - manager_name?: string | null; // 레거시 호환 - manager_contact?: string | null; // 레거시 호환 - - // 제품 정보 - product_category: 'screen' | 'steel'; - quantity: number; - unit_symbol?: string | null; // 단위 (개소, set 등) - - // 금액 정보 - supply_amount: string | number; - tax_amount: string | number; - total_amount: string | number; - - // 상태 - status: 'draft' | 'sent' | 'approved' | 'rejected' | 'finalized' | 'converted'; - current_revision: number; - is_final: boolean; - - // 비고/납기 - remarks?: string | null; // API 실제 필드명 - completion_date?: string | null; // API 실제 필드명 - description?: string | null; // 레거시 호환 - delivery_date?: string | null; // 레거시 호환 - - // 자동산출 입력값 (JSON) - calculation_inputs?: { - items?: Array<{ - productCategory?: string; - productName?: string; - openWidth?: string; - openHeight?: string; - guideRailType?: string; - motorPower?: string; - controller?: string; - wingSize?: string; - inspectionFee?: number; - floor?: string; - code?: string; - quantity?: number; - }>; - } | null; - - // 품목 목록 - items?: QuoteItemApiData[]; - bom_materials?: BomMaterialApiData[]; - - // 감사 정보 - created_at: string; - updated_at: string; - created_by: number | null; - updated_by: number | null; - finalized_at: string | null; - finalized_by: number | null; - - // 관계 데이터 (with 로드 시) - creator?: { id: number; name: string; } | null; - updater?: { id: number; name: string; } | null; - finalizer?: { id: number; name: string; } | null; -} -``` - -### A.2 품목 API 타입 (QuoteItemApiData) - -```typescript -interface QuoteItemApiData { - id: number; - quote_id: number; - - // 품목 정보 - item_id?: number | null; - item_code?: string | null; - item_name: string; - product_id?: number | null; // 레거시 호환 - product_name?: string; // 레거시 호환 - specification: string | null; - unit: string | null; - - // 수량 (API는 calculated_quantity 사용) - base_quantity?: number; // 1개당 BOM 수량 - calculated_quantity?: number; // base × 주문 수량 - quantity?: number; // 레거시 호환 - - // 금액 - unit_price: string | number; - total_price?: string | number; // API 실제 필드 - supply_amount?: string | number; // 레거시 호환 - tax_amount?: string | number; - total_amount?: string | number; // 레거시 호환 - - sort_order: number; - note: string | null; -} -``` - -### A.3 API 요청 형식 (POST/PUT /api/v1/quotes) - -```typescript -// transformFormDataToApi() 출력 형식 - -interface QuoteApiRequest { - registration_date: string; // "2026-01-26" - author: string | null; // 작성자명 - client_id: number | null; - client_name: string; - site_name: string | null; - manager: string | null; // 담당자명 - contact: string | null; // 연락처 - completion_date: string | null; // 납기일 "2026-02-01" - remarks: string | null; - product_category: 'screen' | 'steel'; - quantity: number; // 총 수량 (items.quantity 합계) - unit_symbol: string; // "개소" | "SET" - total_amount: number; // 총액 (공급가 + 세액) - - // 자동산출 입력값 저장 (폼 복원용) - calculation_inputs: { - items: Array<{ - productCategory: string; - productName: string; - openWidth: string; - openHeight: string; - guideRailType: string; - motorPower: string; - controller: string; - wingSize: string; - inspectionFee: number; - floor: string; - code: string; - quantity: number; - }>; - }; - - // BOM 자재 기반 items - items: Array<{ - item_name: string; - item_code: string; - specification: string | null; - unit: string; - quantity: number; // 주문 수량 - base_quantity: number; // 1개당 BOM 수량 - calculated_quantity: number; // base × 주문 수량 - unit_price: number; - total_price: number; - sort_order: number; - note: string | null; - item_index?: number; // calculation_inputs.items 인덱스 - finished_goods_code?: string; // 완제품 코드 - formula_category?: string; // 공정 그룹 - }>; -} -``` - ---- - -## 부록 B: 기존 변환 함수 코드 - -> 새 세션에서 바로 사용할 수 있도록 V1 변환 함수 전체 코드 포함 - -### B.1 API → 프론트엔드 변환 (transformApiToFrontend) - -```typescript -// react/src/components/quotes/types.ts - -export function transformApiToFrontend(apiData: QuoteApiData): Quote { - return { - id: String(apiData.id), - quoteNumber: apiData.quote_number, - registrationDate: apiData.registration_date, - clientId: apiData.client_id ? String(apiData.client_id) : '', - clientName: apiData.client?.name || apiData.client_name || '', - siteName: apiData.site_name || undefined, - siteCode: apiData.site_code || undefined, - // API 실제 필드명 우선, 레거시 폴백 - managerName: apiData.manager || apiData.manager_name || undefined, - managerContact: apiData.contact || apiData.manager_contact || undefined, - productCategory: apiData.product_category, - quantity: apiData.quantity || 0, - unitSymbol: apiData.unit_symbol || undefined, - supplyAmount: parseFloat(String(apiData.supply_amount)) || 0, - taxAmount: parseFloat(String(apiData.tax_amount)) || 0, - totalAmount: parseFloat(String(apiData.total_amount)) || 0, - status: apiData.status, - currentRevision: apiData.current_revision || 0, - isFinal: apiData.is_final || false, - description: apiData.remarks || apiData.description || undefined, - validUntil: apiData.valid_until || undefined, - deliveryDate: apiData.completion_date || apiData.delivery_date || undefined, - deliveryLocation: apiData.delivery_location || undefined, - paymentTerms: apiData.payment_terms || undefined, - items: (apiData.items || []).map(transformItemApiToFrontend), - calculationInputs: apiData.calculation_inputs || undefined, - bomMaterials: (apiData.bom_materials || []).map(transformBomMaterialApiToFrontend), - createdAt: apiData.created_at, - updatedAt: apiData.updated_at, - createdBy: apiData.creator?.name || undefined, - updatedBy: apiData.updater?.name || undefined, - finalizedAt: apiData.finalized_at || undefined, - finalizedBy: apiData.finalizer?.name || undefined, - }; -} -``` - -### B.2 프론트엔드 → API 변환 (transformFormDataToApi) - -```typescript -// react/src/components/quotes/types.ts (핵심 부분) - -export function transformFormDataToApi(formData: QuoteFormData): Record { - let itemsData = []; - - // calculationResults가 있으면 BOM 자재 기반으로 items 생성 - if (formData.calculationResults && formData.calculationResults.items.length > 0) { - let sortOrder = 1; - formData.calculationResults.items.forEach((calcItem) => { - const formItem = formData.items[calcItem.index]; - const orderQuantity = formItem?.quantity || 1; - - calcItem.result.items.forEach((bomItem) => { - const baseQuantity = bomItem.quantity; - const calculatedQuantity = bomItem.unit === 'EA' - ? Math.round(baseQuantity * orderQuantity) - : parseFloat((baseQuantity * orderQuantity).toFixed(2)); - const totalPrice = bomItem.unit_price * calculatedQuantity; - - itemsData.push({ - item_name: bomItem.item_name, - item_code: bomItem.item_code, - specification: bomItem.specification || null, - unit: bomItem.unit || 'EA', - quantity: orderQuantity, - base_quantity: baseQuantity, - calculated_quantity: calculatedQuantity, - unit_price: bomItem.unit_price, - total_price: totalPrice, - sort_order: sortOrder++, - note: `${formItem?.floor || ''} ${formItem?.code || ''}`.trim() || null, - item_index: calcItem.index, - finished_goods_code: calcItem.result.finished_goods.code, - formula_category: bomItem.process_group || undefined, - }); - }); - }); - } else { - // 기존 로직: 완제품 기준 items 생성 - itemsData = formData.items.map((item, index) => ({ - item_name: item.productName, - item_code: item.productName, - specification: item.openWidth && item.openHeight - ? `${item.openWidth}x${item.openHeight}mm` : null, - unit: item.unit || '개소', - quantity: item.quantity, - base_quantity: 1, - calculated_quantity: item.quantity, - unit_price: item.unitPrice || item.inspectionFee || 0, - total_price: (item.unitPrice || item.inspectionFee || 0) * item.quantity, - sort_order: index + 1, - note: `${item.floor || ''} ${item.code || ''}`.trim() || null, - })); - } - - // 총액 계산 - const totalSupply = itemsData.reduce((sum, item) => sum + item.total_price, 0); - const totalTax = Math.round(totalSupply * 0.1); - const grandTotal = totalSupply + totalTax; - - // 자동산출 입력값 저장 - const calculationInputs = { - items: formData.items.map(item => ({ - productCategory: item.productCategory, - productName: item.productName, - openWidth: item.openWidth, - openHeight: item.openHeight, - guideRailType: item.guideRailType, - motorPower: item.motorPower, - controller: item.controller, - wingSize: item.wingSize, - inspectionFee: item.inspectionFee, - floor: item.floor, - code: item.code, - quantity: item.quantity, - })), - }; - - return { - registration_date: formData.registrationDate, - author: formData.writer || null, - client_id: formData.clientId ? parseInt(formData.clientId, 10) : null, - client_name: formData.clientName, - site_name: formData.siteName || null, - manager: formData.manager || null, - contact: formData.contact || null, - completion_date: formData.dueDate || null, - remarks: formData.remarks || null, - product_category: formData.items[0]?.productCategory?.toLowerCase() || 'screen', - quantity: formData.items.reduce((sum, item) => sum + item.quantity, 0), - unit_symbol: formData.unitSymbol || '개소', - total_amount: grandTotal, - calculation_inputs: calculationInputs, - items: itemsData, - }; -} -``` - -### B.3 Quote → QuoteFormData 변환 (transformQuoteToFormData) - -```typescript -// react/src/components/quotes/types.ts - -export function transformQuoteToFormData(quote: Quote): QuoteFormData { - const calcInputs = quote.calculationInputs?.items || []; - - // BOM 자재(quote.items)의 총 금액 계산 - const totalBomAmount = quote.items.reduce((sum, item) => sum + (item.totalAmount || 0), 0); - const itemCount = calcInputs.length || 1; - const amountPerItem = Math.round(totalBomAmount / itemCount); - - return { - id: quote.id, - registrationDate: formatDateForInput(quote.registrationDate), - writer: quote.createdBy || '', - clientId: quote.clientId, - clientName: quote.clientName, - siteName: quote.siteName || '', - manager: quote.managerName || '', - contact: quote.managerContact || '', - dueDate: formatDateForInput(quote.deliveryDate), - remarks: quote.description || '', - unitSymbol: quote.unitSymbol, - - // calculation_inputs.items가 있으면 그것으로 items 복원 - items: calcInputs.length > 0 - ? calcInputs.map((calcInput, index) => ({ - id: `temp-${index}`, - floor: calcInput.floor || '', - code: calcInput.code || '', - productCategory: calcInput.productCategory || '', - productName: calcInput.productName || '', - openWidth: calcInput.openWidth || '', - openHeight: calcInput.openHeight || '', - guideRailType: calcInput.guideRailType || '', - motorPower: calcInput.motorPower || '', - controller: calcInput.controller || '', - quantity: calcInput.quantity || 1, - unit: undefined, - wingSize: calcInput.wingSize || '50', - inspectionFee: calcInput.inspectionFee || 50000, - unitPrice: Math.round(amountPerItem / (calcInput.quantity || 1)), - totalAmount: amountPerItem, - })) - : quote.items.map((item) => ({ - id: item.id, - floor: '', - code: '', - productCategory: '', - productName: item.productName, - openWidth: '', - openHeight: '', - guideRailType: '', - motorPower: '', - controller: '', - quantity: item.quantity || 1, - unit: item.unit, - wingSize: '50', - inspectionFee: item.unitPrice || 50000, - unitPrice: item.unitPrice, - totalAmount: item.totalAmount, - })), - - bomMaterials: calcInputs.length > 0 - ? quote.items.map((item, index) => ({ - itemIndex: index, - finishedGoodsCode: '', - itemCode: item.itemCode || '', - itemName: item.productName, - itemType: '', - itemCategory: '', - specification: item.specification || '', - unit: item.unit || '', - quantity: item.quantity, - unitPrice: item.unitPrice, - totalPrice: item.totalAmount, - processType: '', - })) - : quote.bomMaterials, - }; -} - -// 날짜 형식 변환 헬퍼 -function formatDateForInput(dateStr: string | null | undefined): string { - if (!dateStr) return ''; - if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr; - const date = new Date(dateStr); - if (isNaN(date.getTime())) return ''; - return date.toISOString().split('T')[0]; -} -``` - ---- - -## 부록 C: V2 ↔ API 필드 매핑표 - -> 새 변환 함수 작성 시 참고할 필드 매핑 - -### C.1 견적 마스터 필드 매핑 - -| V2 필드 (QuoteFormDataV2) | API 필드 (QuoteApiData) | DB 컬럼 (quotes) | 비고 | -|--------------------------|------------------------|-----------------|------| -| `id` | `id` | `id` | string ↔ number 변환 | -| `registrationDate` | `registration_date` | `registration_date` | | -| `writer` | `author` / `creator.name` | `author` | 저장: author, 조회: creator.name | -| `clientId` | `client_id` | `client_id` | string ↔ number 변환 | -| `clientName` | `client_name` / `client.name` | `client_name` | | -| `siteName` | `site_name` | `site_name` | | -| `manager` | `manager` | `manager` | | -| `contact` | `contact` | `contact` | | -| `dueDate` | `completion_date` | `completion_date` | | -| `remarks` | `remarks` | `remarks` | | -| `status` | `status` | `status` | V2: draft/temporary/final ↔ API: draft/sent/.../finalized | -| `locations` | `items` + `calculation_inputs.items` | - | 복합 변환 필요 | - -### C.2 개소 항목 필드 매핑 - -| V2 필드 (LocationItem) | API calculation_inputs.items | API items | 비고 | -|-----------------------|----------------------------|-----------|------| -| `id` | - | `id` | | -| `floor` | `floor` | `note` (일부) | | -| `code` | `code` | `note` (일부) | | -| `openWidth` | `openWidth` | `specification` (파싱) | "3000x2500mm" 형식 | -| `openHeight` | `openHeight` | `specification` (파싱) | | -| `productCode` | - | `finished_goods_code` | BOM 산출 시 사용 | -| `productName` | `productName` | `item_name` | | -| `quantity` | `quantity` | `quantity` | 주문 수량 | -| `guideRailType` | `guideRailType` | - | calculation_inputs에만 저장 | -| `motorPower` | `motorPower` | - | | -| `controller` | `controller` | - | | -| `wingSize` | `wingSize` | - | | -| `inspectionFee` | `inspectionFee` | - | | -| `unitPrice` | - | `unit_price` | | -| `totalPrice` | - | `total_price` | | - -### C.3 상태값 매핑 - -| V2 status | API status | 설명 | -|-----------|-----------|------| -| `draft` | `draft`, `sent`, `approved`, `rejected` | 작성중/진행중 | -| `temporary` | - | V2 전용 (임시저장) → API에는 `draft`로 저장 | -| `final` | `finalized`, `converted` | 최종확정/수주전환 | - ---- - -## 부록 D: 테스트 명령어 - -> Docker 환경에서 테스트하는 방법 - -### D.1 서비스 확인 - -```bash -# Docker 서비스 상태 확인 -cd /Users/kent/Works/@KD_SAM/SAM -docker compose ps - -# API 서버 로그 확인 -docker compose logs -f api - -# React 개발 서버 로그 확인 -docker compose logs -f react -``` - -### D.2 API 직접 테스트 - -```bash -# 견적 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/quotes" \ - -H "Authorization: Bearer {TOKEN}" \ - -H "Accept: application/json" - -# 견적 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/quotes/{ID}" \ - -H "Authorization: Bearer {TOKEN}" \ - -H "Accept: application/json" - -# 견적 생성 (예시) -curl -X POST "http://api.sam.kr/api/v1/quotes" \ - -H "Authorization: Bearer {TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{ - "registration_date": "2026-01-26", - "client_name": "테스트 발주처", - "site_name": "테스트 현장", - "product_category": "screen", - "quantity": 1, - "total_amount": 1000000, - "items": [] - }' -``` - -### D.3 브라우저 테스트 URL - -``` -# V1 (기존) -http://dev.sam.kr/sales/quote-management # 목록 -http://dev.sam.kr/sales/quote-management?mode=new # 등록 -http://dev.sam.kr/sales/quote-management/1 # 상세 -http://dev.sam.kr/sales/quote-management/1?mode=edit # 수정 - -# V2 (신규 - 테스트) -http://dev.sam.kr/sales/quote-management/test-new # 등록 -http://dev.sam.kr/sales/quote-management/test/1 # 상세 -http://dev.sam.kr/sales/quote-management/test/1?mode=edit # 수정 -``` - -### D.4 디버깅 - -```bash -# React 콘솔 로그 확인 (브라우저 개발자 도구) -# [QuoteActions] 접두사로 API 요청/응답 확인 - -# API 디버그 로그 확인 -docker compose exec api tail -f storage/logs/laravel.log -``` - ---- - -## 부록 E: V2 변환 함수 구현 가이드 - -> Phase 1.1에서 구현할 함수 상세 가이드 - -### E.1 transformV2ToApi 구현 - -```typescript -// react/src/components/quotes/types.ts에 추가 - -import type { QuoteFormDataV2, LocationItem } from './QuoteRegistrationV2'; - -/** - * V2 폼 데이터 → API 요청 형식 변환 - * - * 핵심 차이점: - * - V2는 locations[] 배열, API는 items[] + calculation_inputs.items[] 구조 - * - V2 status는 3가지, API status는 6가지 - * - BOM 산출 결과가 있으면 items에 자재 상세 포함 - */ -export function transformV2ToApi( - data: QuoteFormDataV2, - bomResults?: BomCalculationResult[] -): Record { - - // 1. calculation_inputs 생성 (폼 복원용) - const calculationInputs = { - items: data.locations.map(loc => ({ - productCategory: 'screen', // TODO: 실제 카테고리 - productName: loc.productName, - openWidth: String(loc.openWidth), - openHeight: String(loc.openHeight), - guideRailType: loc.guideRailType, - motorPower: loc.motorPower, - controller: loc.controller, - wingSize: String(loc.wingSize), - inspectionFee: loc.inspectionFee, - floor: loc.floor, - code: loc.code, - quantity: loc.quantity, - })), - }; - - // 2. items 생성 (BOM 결과 있으면 자재 상세, 없으면 완제품 기준) - let items: Array> = []; - - if (bomResults && bomResults.length > 0) { - // BOM 자재 기반 - let sortOrder = 1; - bomResults.forEach((bomResult, locIndex) => { - const loc = data.locations[locIndex]; - const orderQty = loc?.quantity || 1; - - bomResult.items.forEach(bomItem => { - const baseQty = bomItem.quantity; - const calcQty = bomItem.unit === 'EA' - ? Math.round(baseQty * orderQty) - : parseFloat((baseQty * orderQty).toFixed(2)); - - items.push({ - item_name: bomItem.item_name, - item_code: bomItem.item_code, - specification: bomItem.specification || null, - unit: bomItem.unit || 'EA', - quantity: orderQty, - base_quantity: baseQty, - calculated_quantity: calcQty, - unit_price: bomItem.unit_price, - total_price: bomItem.unit_price * calcQty, - sort_order: sortOrder++, - note: `${loc?.floor || ''} ${loc?.code || ''}`.trim() || null, - item_index: locIndex, - finished_goods_code: bomResult.finished_goods.code, - formula_category: bomItem.process_group || undefined, - }); - }); - }); - } else { - // 완제품 기준 (BOM 산출 전) - items = data.locations.map((loc, index) => ({ - item_name: loc.productName, - item_code: loc.productCode, - specification: `${loc.openWidth}x${loc.openHeight}mm`, - unit: '개소', - quantity: loc.quantity, - base_quantity: 1, - calculated_quantity: loc.quantity, - unit_price: loc.unitPrice || loc.inspectionFee || 0, - total_price: loc.totalPrice || (loc.unitPrice || loc.inspectionFee || 0) * loc.quantity, - sort_order: index + 1, - note: `${loc.floor} ${loc.code}`.trim() || null, - })); - } - - // 3. 총액 계산 - const totalSupply = items.reduce((sum, item) => sum + (item.total_price as number), 0); - const totalTax = Math.round(totalSupply * 0.1); - const grandTotal = totalSupply + totalTax; - - // 4. API 요청 객체 반환 - return { - registration_date: data.registrationDate, - author: data.writer || null, - client_id: data.clientId ? parseInt(data.clientId, 10) : null, - client_name: data.clientName, - site_name: data.siteName || null, - manager: data.manager || null, - contact: data.contact || null, - completion_date: data.dueDate || null, - remarks: data.remarks || null, - product_category: 'screen', // TODO: 동적으로 결정 - quantity: data.locations.reduce((sum, loc) => sum + loc.quantity, 0), - unit_symbol: '개소', - total_amount: grandTotal, - status: data.status === 'final' ? 'finalized' : 'draft', - calculation_inputs: calculationInputs, - items: items, - }; -} -``` - -### E.2 transformApiToV2 구현 - -```typescript -/** - * API 응답 → V2 폼 데이터 변환 - * - * 핵심: - * - calculation_inputs.items가 있으면 그것으로 locations 복원 - * - 없으면 items에서 추출 시도 (레거시 호환) - */ -export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 { - const calcInputs = apiData.calculation_inputs?.items || []; - - // calculation_inputs에서 locations 복원 - const locations: LocationItem[] = calcInputs.length > 0 - ? calcInputs.map((ci, index) => { - // 해당 인덱스의 BOM 자재에서 금액 계산 - const relatedItems = (apiData.items || []).filter( - item => item.item_index === index || item.note?.includes(ci.floor || '') - ); - const totalPrice = relatedItems.reduce( - (sum, item) => sum + parseFloat(String(item.total_price || 0)), 0 - ); - const qty = ci.quantity || 1; - - return { - id: `loc-${index}`, - floor: ci.floor || '', - code: ci.code || '', - openWidth: parseInt(ci.openWidth || '0', 10), - openHeight: parseInt(ci.openHeight || '0', 10), - productCode: '', // TODO: finished_goods_code에서 추출 - productName: ci.productName || '', - quantity: qty, - guideRailType: ci.guideRailType || 'wall', - motorPower: ci.motorPower || 'single', - controller: ci.controller || 'basic', - wingSize: parseInt(ci.wingSize || '50', 10), - inspectionFee: ci.inspectionFee || 50000, - unitPrice: Math.round(totalPrice / qty), - totalPrice: totalPrice, - }; - }) - : []; // TODO: items에서 복원 로직 추가 - - // 상태 매핑 - const mapStatus = (s: string): 'draft' | 'temporary' | 'final' => { - if (s === 'finalized' || s === 'converted') return 'final'; - return 'draft'; - }; - - return { - id: String(apiData.id), - registrationDate: formatDateForInput(apiData.registration_date), - writer: apiData.creator?.name || '', - clientId: apiData.client_id ? String(apiData.client_id) : '', - clientName: apiData.client?.name || apiData.client_name || '', - siteName: apiData.site_name || '', - manager: apiData.manager || apiData.manager_name || '', - contact: apiData.contact || apiData.manager_contact || '', - dueDate: formatDateForInput(apiData.completion_date || apiData.delivery_date), - remarks: apiData.remarks || apiData.description || '', - status: mapStatus(apiData.status), - locations: locations, - }; -} -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다. (2026-01-26 보완)* \ No newline at end of file diff --git a/plans/quote-system-development-plan.md b/plans/quote-system-development-plan.md deleted file mode 100644 index 66e8147..0000000 --- a/plans/quote-system-development-plan.md +++ /dev/null @@ -1,319 +0,0 @@ -# 견적 시스템 개발 계획 - -> **작성일**: 2025-12-24 -> **목표**: mng 수식 시뮬레이터를 완전한 견적 시스템으로 확장 후 React API 개발 - ---- - -## 1. 개발 단계 - -### Stage 1: mng 견적 시스템 완성 (현재) -**목표**: 스크린샷과 동일한 견적 시스템을 mng Plain Blade로 구현 - -### Stage 2: API 개발 -**목표**: React 프론트엔드에서 호출할 견적 산출 REST API 개발 - ---- - -## 2. 현재 상태 vs 목표 상태 - -### 2.1 입력 폼 비교 - -| 필드 | 현재 시뮬레이터 | 목표 (스크린샷) | 상태 | -|------|----------------|----------------|------| -| 층수 | ❌ | 예: 1층, B1, 지하1층 | 추가 필요 | -| 부호 | ❌ | 예: A, B, C | 추가 필요 | -| 제품 카테고리 (PC) | ✅ | 스크린, 철재 등 | 완료 | -| 제품명 | ✅ | 방화 스크린 셔터 등 | 완료 | -| 오픈사이즈 W0 | ✅ | 가로 | 완료 | -| 오픈사이즈 H0 | ✅ | 세로 | 완료 | -| 가이드레일 설치유형 (GT) | ✅ | 벽면형, 측면형 | 완료 | -| 모터 전원 (MP) | ✅ | 220V, 380V | 완료 | -| 연동제어기 (CT) | ✅ | 단독, 연동 | 완료 | -| 수량 (QTY) | ✅ | 1, 2, 3... | 완료 | -| 마구리 날개치수 (WS) | ❌ | 50 등 | 추가 필요 | -| 검사비 (INSP) | ❌ | 50000 등 | 추가 필요 | - -### 2.2 출력 결과 비교 - -| 섹션 | 현재 시뮬레이터 | 목표 (스크린샷) | 상태 | -|------|----------------|----------------|------| -| 입력 정보 요약 | ❌ | 제품명, 카테고리, 오픈사이즈, 설치유형 등 요약 | 추가 필요 | -| 기본 산출 공식 | ❌ | 제작폭(W1), 제작높이(H1), 면적(M), 중량(K) 표시 | 추가 필요 | -| BOM 목록 테이블 | ⚠️ 공정별 그룹화 | 순번, 품목코드, 품목명, 품목유형, 규격, 기준수량, 산출수량, 단위, 단가, 금액, 작업 | 구조 변경 필요 | -| 품목 추가/삭제 | ❌ | + 품목 추가 버튼, 휴지통 삭제 버튼 | 추가 필요 | -| 할인율 | ❌ | 할인율(%) 입력 | 추가 필요 | -| 금액 요약 | ⚠️ 합계만 | 합계, 공급가, 최종 금액 | 확장 필요 | - ---- - -## 3. Stage 1: mng 견적 시스템 상세 계획 - -### Phase 1: UI 확장 (1일) -**파일**: `resources/views/quote-formulas/simulator.blade.php` - -#### 1.1 입력 폼 확장 -``` -추가 필드: -- 층수 (floor): text input, placeholder "예: 1층, B1, 지하1층" -- 부호 (code): text input, placeholder "예: A, B, C" -- 마구리 날개치수 (WS): number input, default 50 -- 검사비 (INSP): number input, default 50000 -``` - -#### 1.2 견적 항목 다중 입력 -``` -- 견적 1, 견적 2, ... 탭 형태 -- "+ 견적 추가" 버튼 -- 복사, 삭제 버튼 -``` - -#### 1.3 결과 출력 섹션 -``` -1. 입력 정보 요약 카드 - - 제품명, 제품 카테고리, 오픈사이즈, 가이드레일 설치, 모터 전원, 연동제어기, 수량 - -2. 기본 산출 공식 카드 - - 제작폭 (W1): 값 + 계산식 - - 제작높이 (H1): 값 + 계산식 - - 면적 (M): 값 + 단위 - - 중량 (K): 값 + 단위 - -3. 부품구성표(BOM) 목록 테이블 - - 컬럼: 순번, 품목코드, 품목명, 품목유형, 규격, 기준수량, 산출수량, 단위, 단가, 금액, 작업 - - "+ 품목 추가" 버튼 - - 행별 삭제 버튼 - -4. 금액 요약 - - 할인율(%) 입력 - - 합계, 공급가, 최종 금액 -``` - -### Phase 2: 백엔드 로직 확장 (1일) -**파일**: `app/Services/Quote/FormulaEvaluatorService.php` - -#### 2.1 executeAll() 반환 구조 확장 -```php -return [ - 'input_summary' => [ - 'product_name' => '방화 스크린 셔터 (소형)', - 'product_category' => '스크린', - 'open_size' => 'W2000 × H2500', - 'guide_rail_type' => '벽면형', - 'motor_power' => '220V', - 'controller' => '단독', - 'quantity' => 1, - ], - 'calculation_formula' => [ - 'W1' => ['value' => 2140, 'formula' => 'W0 + 140'], - 'H1' => ['value' => 2850, 'formula' => 'H0 + 350'], - 'M' => ['value' => 6.10, 'unit' => '㎡'], - 'K' => ['value' => 0.00, 'unit' => 'kg'], - ], - 'bom_items' => [ - [ - 'seq' => 1, - 'item_code' => 'SF-SCR-F01', - 'item_name' => '스크린 원단', - 'item_type' => 'SF', - 'spec' => '-', - 'base_quantity' => 1.10, - 'calculated_quantity' => 6.099, - 'unit' => 'M2', - 'unit_price' => 213465, - 'total_price' => 1301923.035, - 'editable' => true, - ], - // ... more items - ], - 'summary' => [ - 'subtotal' => 2806523.035, - 'discount_rate' => 0, - 'discount_amount' => 0, - 'supply_price' => 2806523.035, - 'total_amount' => 2806523.035, - ], -]; -``` - -### Phase 3: 검사비 품목 추가 (0.5일) -**파일**: `database/seeders/DesignItemSeeder.php` - -```php -// 서비스 품목 추가 -$serviceItems = [ - ['code' => 'SVC-INSP', 'name' => '검사비', 'unit' => '식', 'price' => 50000, 'type' => 'CS'], - ['code' => 'SVC-INSTALL', 'name' => '설치비', 'unit' => '식', 'price' => 100000, 'type' => 'CS'], - ['code' => 'SVC-DELIVERY', 'name' => '운송비', 'unit' => '식', 'price' => 80000, 'type' => 'CS'], -]; -``` - -### Phase 4: 테스트 및 검증 (0.5일) -- Playwright로 전체 플로우 테스트 -- Design 시스템 결과와 비교 검증 - ---- - -## 4. Stage 2: API 개발 상세 계획 - -### Phase 1: API 엔드포인트 설계 (0.5일) - -#### 4.1 견적 산출 API -``` -POST /api/v1/quotes/calculate - -Request: -{ - "items": [ - { - "floor": "1층", - "code": "A", - "product_category": "screen", - "product_id": "screen_standard", - "open_width": 2000, - "open_height": 2500, - "guide_rail_type": "wall", - "motor_power": "220V", - "controller": "single", - "quantity": 1, - "wing_size": 50, - "inspection_fee": 50000 - } - ], - "discount_rate": 0 -} - -Response: -{ - "success": true, - "data": { - "quotes": [ - { - "quote_id": "quote-1", - "input_summary": { ... }, - "calculation_formula": { ... }, - "bom_items": [ ... ], - "summary": { ... } - } - ], - "total_summary": { - "total_items": 1, - "total_amount": 2806523.035 - } - } -} -``` - -#### 4.2 제품 목록 API -``` -GET /api/v1/quotes/products?category=screen - -Response: -{ - "success": true, - "data": [ - { - "id": "screen_standard", - "name": "스크린 셔터 (표준형)", - "category": "screen" - } - ] -} -``` - -#### 4.3 옵션 목록 API -``` -GET /api/v1/quotes/options - -Response: -{ - "success": true, - "data": { - "product_categories": [...], - "guide_rail_types": [...], - "motor_powers": [...], - "controllers": [...] - } -} -``` - -### Phase 2: API 컨트롤러 구현 (1일) -**파일**: `api/app/Http/Controllers/Api/V1/QuoteCalculationController.php` - -### Phase 3: API 테스트 (0.5일) -- Postman/Swagger 테스트 -- React 연동 테스트 - ---- - -## 5. 일정 요약 - -| Stage | Phase | 작업 내용 | 예상 일정 | -|-------|-------|----------|----------| -| **Stage 1** | Phase 1 | mng UI 확장 | 1일 | -| | Phase 2 | 백엔드 로직 확장 | 1일 | -| | Phase 3 | 검사비 품목 추가 | 0.5일 | -| | Phase 4 | 테스트 및 검증 | 0.5일 | -| | **소계** | | **3일** | -| **Stage 2** | Phase 1 | API 설계 | 0.5일 | -| | Phase 2 | API 구현 | 1일 | -| | Phase 3 | API 테스트 | 0.5일 | -| | **소계** | | **2일** | -| **합계** | | | **5일** | - ---- - -## 6. 파일 구조 - -### Stage 1 (mng) -``` -/SAM/mng/ -├── app/Services/Quote/ -│ └── FormulaEvaluatorService.php # 로직 확장 -├── database/seeders/ -│ └── DesignItemSeeder.php # 서비스 품목 추가 -└── resources/views/quote-formulas/ - └── simulator.blade.php # UI 확장 -``` - -### Stage 2 (api) -``` -/SAM/api/ -├── app/Http/Controllers/Api/V1/ -│ └── QuoteCalculationController.php # 신규 -├── app/Services/Quote/ -│ └── QuoteCalculationService.php # 신규 (또는 mng 서비스 공유) -└── routes/ - └── api.php # 라우트 추가 -``` - ---- - -## 7. 성공 기준 - -### Stage 1 -1. ✅ 스크린샷과 동일한 입력 폼 (층수, 부호, WS, INSP 포함) -2. ✅ 입력 정보 요약 섹션 표시 -3. ✅ 기본 산출 공식 섹션 표시 -4. ✅ BOM 테이블 (순번~금액 컬럼) -5. ✅ 품목 추가/삭제 기능 -6. ✅ 할인율 + 최종 금액 계산 - -### Stage 2 -1. ✅ POST /api/v1/quotes/calculate 정상 작동 -2. ✅ GET /api/v1/quotes/products 정상 작동 -3. ✅ GET /api/v1/quotes/options 정상 작동 -4. ✅ Swagger 문서화 완료 -5. ✅ React에서 API 호출 테스트 완료 - ---- - -## 8. 참고 문서 - -- `docs/plans/simulator-calculation-logic-mapping.md` - 계산 로직 상세 -- `react/src/components/quotes/QuoteRegistration.tsx` - React UI 참조 -- `design/src/components/AutoCalculationSimulator.tsx` - Design 시뮬레이터 참조 - ---- - -*이 문서는 mng 견적 시스템 완성 및 API 개발 계획을 정의합니다.* \ No newline at end of file diff --git a/plans/react-mock-remaining-tasks.md b/plans/react-mock-remaining-tasks.md deleted file mode 100644 index 2bdfbc5..0000000 --- a/plans/react-mock-remaining-tasks.md +++ /dev/null @@ -1,637 +0,0 @@ -# React Mock → API 마이그레이션 - 잔여 작업 - -> **작성일**: 2025-12-27 -> **목적**: 미완료 Mock → API 연동 작업 추적 -> **원본 문서**: `react-mock-to-api-migration-plan.md` -> **참조 구현**: 단가관리 (`/sales/pricing-management`) - ---- - -## 0. 로컬 개발 환경 - -### 도메인 구성 - -| 서비스 | 도메인 | 설명 | -|--------|--------|------| -| React (프론트엔드) | `http://dev.sam.kr` | 사용자 화면 | -| API (백엔드) | `http://api.sam.kr` | REST API 서버 | -| MNG (운영관리자) | `http://mng.sam.kr` | 관리자 패널 | - -### 테스트 URL 예시 - -``` -# 종합분석 페이지 -http://dev.sam.kr/reports/comprehensive-analysis - -# API 직접 호출 -http://api.sam.kr/api/v1/comprehensive-analysis -``` - -### 테스트 대상 테넌트 - -| 항목 | 값 | 비고 | -|------|-----|------| -| **Tenant ID** | 287 | 프론트_테스트회사 | -| **테스트 User ID** | 33 | 홍킬동 (hhhhhh@example.com) | -| **보조 User ID** | 12 | Ops Admin (결재함/참조함 테스트용 기안자) | - -> ⚠️ **주의**: Seeder 및 테스트 데이터 생성 시 반드시 `tenant_id = 287`, `user_id = 33` 사용 - -### 로그인 정보 - -| 사용자 | Email | 비밀번호 | Tenant | -|--------|-------|---------|--------| -| 홍킬동 | hhhhhh@example.com | (확인 필요) | 287 (기본) | - -### 종합분석 페이지 작업 시 주의사항 - -> ⚠️ **필수**: 종합분석은 여러 모듈의 데이터를 통합 표시하므로, 데이터 수정 시 관련 페이지 점검 필수 - -| 종합분석 섹션 | 원본 데이터 | 관련 페이지 (점검 대상) | -|--------------|------------|----------------------| -| 오늘의 이슈 (결재 대기) | `approvals`, `approval_steps` | `/approval/draft` (기안함), `/approval/pending` (결재함), `/approval/reference` (참조함) | -| 월간 예상 지출 | `expected_expenses` | `/accounting/expected-expenses` | -| 입금 현황 | `deposits` | `/accounting/deposits` | -| 채권추심 | `bad_debts` | `/accounting/bad-debts` | -| 미수금/여신한도 | `clients` | `/sales/clients` | - -**작업 흐름:** -``` -종합분석 데이터 수정 → 종합분석 페이지 확인 → 관련 원본 페이지 점검 -``` - ---- - -## 1. 작업 규칙 - -### 1.0 아키텍처 원칙 (필수) - -> **React는 오직 `api.sam.kr` (api 프로젝트)만 호출한다** - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ react/ │ ───► │ api/ │ │ mng/ │ -│ dev.sam.kr │ │ api.sam.kr │ │ mng.sam.kr │ -│ (프론트엔드) │ │ (REST API) │ │ (관리자패널) │ -└─────────────┘ └─────────────┘ └─────────────┘ - │ │ │ - │ ✅ 호출 허용 │ │ - └────────────────────┘ │ - │ - ❌ 절대 호출 금지 ─────────────────────────┘ -``` - -**규칙:** -- React에서 mng API 직접 호출 **절대 금지** -- 필요한 API가 api 프로젝트에 없으면 **api에 새로 개발** -- mng의 모델/로직은 **참조만** (코드 복사 또는 재구현) - -### 1.1 작업 진행 정책 - -> **단위 작업 → 검수 → 승인 → 문서 업데이트 → 커밋** 순서로 진행 - -### 1.2 세션 규칙 및 Serena 메모리 관리 - -> **세션 간 일관성 보장을 위한 필수 규칙** - -#### 세션 시작 프로토콜 (필수) - -``` -1. Serena 메모리 로드 - read_memory("mock-to-api-state") → 현재 Phase/작업 확인 - read_memory("mock-to-api-snapshot") → 마지막 작업 내용 확인 - -2. 현재 상태 확인 - - 이 문서 읽기 - - 현재 Phase의 기능별 상태 확인 - - "다음 작업은 [Phase]-[번호]의 [기능] 입니다" 명시 - -3. 작업 범위 명확화 - - 사용자에게 작업 범위 확인 - - "[Phase] 전체를 진행할까요, 특정 기능만 진행할까요?" -``` - -#### Serena 메모리 구조 - -```javascript -// mock-to-api-state -{ - "current_phase": "J", - "current_item": "J-1", - "current_feature": "게시판 목록", - "progress": { - "J-1": { "목록": "대기", "상세": "대기" } - }, - "last_update": "2025-12-27" -} - -// mock-to-api-snapshot -"Phase J 게시판 시스템 시작 예정" -``` - -#### 작업 완료 시 (필수) - -``` -1. 문서 업데이트 - - 해당 기능 상태 변경 (🔄 → ✅) - - 변경 이력 추가 - -2. Serena 메모리 저장 - write_memory("mock-to-api-state", 현재 상태) - write_memory("mock-to-api-snapshot", 작업 내용 요약) - -3. 커밋 - feat: [Phase]-[번호] [페이지명] Mock → API 연동 -``` - -### 1.3 작업 템플릿 (표준) - -```markdown -## [Phase-번호] 페이지명 - [기능명] 연동 - -**작업 대상:** -- 컴포넌트: `ComponentName.tsx` -- 액션: `actions.ts` -- API: `GET/POST/PUT/DELETE /api/v1/endpoint` - -**작업 절차:** -1. [ ] API 스펙 확인 (Swagger) -2. [ ] actions.ts 함수 확인/생성 -3. [ ] 타입 정의 확인 (API ↔ Frontend) -4. [ ] 컴포넌트에서 actions 호출 -5. [ ] console.log/MOCK 제거 -6. [ ] 브라우저 테스트 - -**결과:** -- [ ] 검수 요청 -- [ ] [승인] 문서 업데이트 -- [ ] [승인] 커밋 -``` - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 📋 작업 흐름 (페이지 단위) │ -├─────────────────────────────────────────────────────────────────┤ -│ 1️⃣ 작업 시작: 대상 페이지 Mock → API 연동 작업 │ -│ 2️⃣ 작업 완료: 코드 수정 완료 후 사용자에게 검수 요청 │ -│ 3️⃣ 검수: 사용자가 기능 확인 (브라우저 테스트) │ -│ 4️⃣ [승인] 문서 업데이트: 이 문서의 상태 갱신 │ -│ 5️⃣ [승인] 커밋: Git 커밋 생성 │ -│ 6️⃣ 다음 페이지로 이동 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**⚠️ 중요 규칙:** -- 각 단계에서 `[승인]` 표시된 작업은 **사용자 승인 후** 진행 - ---- - -## 2. 잔여 작업 목록 - -### 2.1 Phase J: 게시판 시스템 - -> **상태**: ✅ api 프로젝트에 게시판/게시글 API 완비 → React 연동 작업 가능 - -#### ✅ api 프로젝트 게시판 API 아키텍처 - -> **핵심 설계**: 시스템 게시판과 테넌트 게시판을 **별도 엔드포인트**로 분리하고, **code 기반 URL** 사용 - -``` -시스템 게시판 (본사 운영) 테넌트 게시판 (테넌트 내부) -┌─────────────────────────────┐ ┌─────────────────────────────┐ -│ /api/v1/system-boards/{code}│ │ /api/v1/boards/{code} │ -│ - is_system = true │ │ - is_system = false │ -│ - tenant_id = null │ │ - tenant_id = {current} │ -│ - 메뉴 → global_menus │ │ - 메뉴 → menus │ -└─────────────────────────────┘ └─────────────────────────────┘ -``` - -**장점:** -- 동일한 `board_code`도 시스템/테넌트에서 독립 사용 가능 -- API 호출 시 `is_system` 플래그 불필요 -- URL만으로 게시판 유형 구분 가능 -- RESTful 원칙 준수 - -#### 📌 시스템 게시판 API (System Boards) - -| 기능 | Method | Endpoint (api.sam.kr) | 상태 | -|------|--------|----------------------|------| -| 시스템 게시판 목록 | GET | `/api/v1/system-boards` | ✅ | -| 시스템 게시판 상세 | GET | `/api/v1/system-boards/{code}` | ✅ | -| 시스템 게시판 필드 | GET | `/api/v1/system-boards/{code}/fields` | ✅ | -| 시스템 게시글 목록 | GET | `/api/v1/system-boards/{code}/posts` | ✅ | -| 시스템 게시글 상세 | GET | `/api/v1/system-boards/{code}/posts/{id}` | ✅ | -| 시스템 게시글 등록 | POST | `/api/v1/system-boards/{code}/posts` | ✅ | -| 시스템 게시글 수정 | PUT | `/api/v1/system-boards/{code}/posts/{id}` | ✅ | -| 시스템 게시글 삭제 | DELETE | `/api/v1/system-boards/{code}/posts/{id}` | ✅ | -| 시스템 댓글 CRUD | * | `/api/v1/system-boards/{code}/posts/{id}/comments/*` | ✅ | - -#### 📌 테넌트 게시판 API (Tenant Boards) - -| 기능 | Method | Endpoint (api.sam.kr) | 상태 | -|------|--------|----------------------|------| -| 테넌트 게시판 목록 | GET | `/api/v1/boards` | ✅ | -| 테넌트 게시판 상세 | GET | `/api/v1/boards/{code}` | 🔄 변경 필요 (ID→code) | -| 테넌트 게시판 필드 | GET | `/api/v1/boards/{code}/fields` | ✅ | -| 테넌트 게시글 목록 | GET | `/api/v1/boards/{code}/posts` | ✅ | -| 테넌트 게시글 상세 | GET | `/api/v1/boards/{code}/posts/{id}` | ✅ | -| 테넌트 게시글 등록 | POST | `/api/v1/boards/{code}/posts` | ✅ | -| 테넌트 게시글 수정 | PUT | `/api/v1/boards/{code}/posts/{id}` | ✅ | -| 테넌트 게시글 삭제 | DELETE | `/api/v1/boards/{code}/posts/{id}` | ✅ | -| 테넌트 댓글 CRUD | * | `/api/v1/boards/{code}/posts/{id}/comments/*` | ✅ | - -#### 📌 관리자 게시판 API (Admin - mng.sam.kr) - -| 기능 | Method | Endpoint (mng.sam.kr) | 상태 | -|------|--------|----------------------|------| -| 전체 게시판 목록 | GET | `/boards` (Blade) | ✅ | -| 게시판 등록 | POST | `/boards` | ✅ | -| 게시판 수정 | PUT | `/boards/{id}` | ✅ | -| 게시판 삭제 | DELETE | `/boards/{id}` | ✅ | -| **게시판 CRUD 시 메뉴 자동 연동** | - | mng + api 프로젝트 | ✅ 완료 | - -#### 📌 테넌트 게시판 메뉴 연동 (api 프로젝트) - -> **2025-12-29 추가**: 테넌트 게시판 생성/수정/삭제 시 메뉴 자동 연동 - -| 기능 | 트리거 | 메뉴 처리 | 상태 | -|------|--------|----------|------| -| 게시판 생성 | `BoardService::createTenantBoard()` | `/board` 하위에 메뉴 자동 추가 | ✅ | -| 게시판 수정 | `BoardService::updateTenantBoard()` | 코드/이름 변경 시 메뉴 URL/이름 동기화 | ✅ | -| 게시판 삭제 | `BoardService::deleteTenantBoard()` | 메뉴 Soft Delete | ✅ | - -**구현 파일:** -- `api/app/Services/MenuService.php` - 게시판 메뉴 연동 메서드 추가 -- `api/app/Services/Boards/BoardService.php` - MenuService 호출 로직 추가 - -#### 🏗️ 게시판 시스템 아키텍처 (참조용) - -**EAV (Entity-Attribute-Value) 패턴 기반 통합 게시판:** -``` -boards (게시판 정의) -├── board_settings (EAV 필드 스키마) -├── posts (게시글) -│ └── post_custom_field_values (EAV 값 저장) -└── 첨부파일 (Polymorphic: files → fileable) -``` - -**게시판 모델 주요 필드:** -```typescript -interface Board { - id: number; - tenant_id?: number; // null = 시스템 게시판 - is_system: boolean; // 시스템/테넌트 구분 - board_type: string; // notice, qna, faq, free, gallery, download - board_code: string; // 고유 코드 - name: string; - description?: string; - editor_type: 'wysiwyg' | 'markdown' | 'text'; - allow_files: boolean; - max_file_count: number; - max_file_size: number; // KB - extra_settings: { // JSON - allow_comment?: boolean; - allow_secret?: boolean; - write_roles?: string[]; - read_roles?: string[]; - }; - is_active: boolean; -} - -interface BoardSetting { // EAV 필드 스키마 - id: number; - board_id: number; - name: string; // 필드명 (예: 카테고리) - field_key: string; // 필드 키 (예: category) - field_type: 'text' | 'number' | 'select' | 'date' | 'textarea' | 'checkbox' | 'radio' | 'file'; - field_meta?: { // JSON (select 옵션, 기본값 등) - options?: string[]; - default?: string; - }; - is_required: boolean; - sort_order: number; -} -``` - -**템플릿 시스템 (`config/board_templates.php`):** -- **시스템 템플릿**: notice, qna, faq, popup (본사 ↔ 테넌트 소통용) -- **테넌트 템플릿**: free, gallery, download, notice, qna (테넌트 내부용) - -#### 📋 React 연동 작업 현황 - -| # | 페이지 | React 경로 | 조회 | 등록 | 수정 | 삭제 | API 연동 전략 | -|---|--------|-----------|------|------|------|------|--------------| -| J-1 | 게시판 목록 | `/board` | ✅ | ⏭️ | ⏭️ | ✅ | ✅ 완료 (2025-12-29) - `actions.ts` getPosts/getMyPosts | -| J-2 | 게시글 상세 | `/board/[boardCode]/[postId]` | ✅ | ⏭️ | ⏭️ | ✅ | ✅ 완료 (2025-12-29) - `actions.ts` getPost/deletePost | -| J-3 | 게시글 작성/수정 | `/board/[boardCode]/[postId]/edit` | ✅ | ✅ | ✅ | ⏭️ | ✅ 완료 (2025-12-29) - `actions.ts` createPost/updatePost | -| J-4 | 게시판 관리 | `/board/board-management` | ✅ | ✅ | ✅ | ✅ | ✅ 완료 (2025-12-27) | - -> ✅ **Phase J 완료** (2025-12-29): 모든 게시판 Mock → API 연동 완료 - -**파일 구조 (완료):** -``` -components/board/ -├── types.ts ← ✅ Post, Comment, PostApiData 등 API 타입 정의 -├── actions.ts ← ✅ Server Actions (getPosts, getPost, createPost, updatePost, deletePost) -├── BoardForm/ ← ✅ getBoards + createPost/updatePost API 연동 -├── BoardDetail/ ← ✅ 게시글 상세 + 삭제 API 연동 -├── BoardList/ ← ✅ 게시판별 필터링 + 페이지네이션 + 삭제 -└── BoardManagement/ - ├── types.ts ← ✅ Board 관리 타입 - ├── actions.ts ← ✅ getBoards, createBoard, updateBoard, deleteBoard - └── index.tsx ← ✅ 게시판 CRUD 완료 -``` - -**라우트 변경:** -- 기존: `/board/[id]` → 신규: `/board/[boardCode]/[postId]` -- 삭제된 파일: `board/[id]/page.tsx`, `board/[id]/edit/page.tsx` - ---- - -### 2.2 Phase K: 고객센터 - -> **상태**: ✅ React 연동 완료 (2025-12-29) - `shared/actions.ts` 통해 시스템 게시판 API 호출 - -#### 🎯 통합 전략: 고객센터 = 게시판 템플릿 활용 - -각 고객센터 메뉴를 별도 `board_code`로 생성하여 통합 관리: - -| 메뉴 | board_code | board_type | 템플릿 | 커스텀 필드 | -|------|------------|------------|--------|------------| -| FAQ | `system-faq` | faq | system/faq | category (select) | -| 공지사항 | `system-notice` | notice | system/notice | category (select) | -| 이벤트 | `system-event` | notice | - | start_date, end_date, image_url | -| 1:1 문의 | `system-qna` | qna | system/qna | inquiry_type, answer_status | - -**장점:** -- 코드 중복 제거 (게시판 CRUD 재사용) -- 커스텀 필드로 각 메뉴 특성 반영 -- 관리자 UI 통합 (mng.sam.kr/boards에서 일괄 관리) - -#### 📋 React 연동 작업 현황 - -| # | 페이지 | React 경로 | 조회 | 등록 | 수정 | 삭제 | API 연동 전략 | -|---|--------|-----------|------|------|------|------|--------------| -| K-1 | FAQ 관리 | `/customer-center/faq` | ✅ | ⏭️ | ⏭️ | ⏭️ | ✅ 완료 (2025-12-29) - `shared/actions.ts` | -| K-2 | 이벤트 관리 | `/customer-center/events` | ✅ | ⏭️ | ⏭️ | ⏭️ | ✅ 완료 (2025-12-29) - `shared/actions.ts` | -| K-3 | 공지사항 관리 | `/customer-center/notices` | ✅ | ⏭️ | ⏭️ | ⏭️ | ✅ 완료 (2025-12-29) - `shared/actions.ts` | -| K-4 | 문의 관리 | `/customer-center/inquiries` | ✅ | ✅ | ✅ | ✅ | ✅ 완료 (2025-12-29) - `shared/actions.ts` + 댓글 CRUD | - -**파일 구조 → 연동 계획:** -``` -components/customer-center/ -├── shared/ -│ ├── types.ts ← 🆕 공통 Post, BoardField 타입 -│ ├── actions.ts ← 🆕 게시글 CRUD Server Actions (board_code 파라미터) -│ └── PostForm.tsx ← 🆕 동적 폼 (커스텀필드 기반) -├── FAQManagement/ -│ ├── types.ts ← 🔄 FAQ 특화 타입 (category 필드) -│ ├── actions.ts ← 🆕 board_code='system-faq' 고정 -│ └── FAQList.tsx ← 🔄 카테고리별 그룹핑 UI -├── EventManagement/ -│ ├── types.ts ← 🔄 Event 특화 타입 (start_date, end_date) -│ ├── actions.ts ← 🆕 board_code='system-event' 고정 -│ └── EventList.tsx ← 🔄 진행중/예정/종료 필터 -├── NoticeManagement/ -│ ├── types.ts ← 🔄 Notice 특화 타입 -│ ├── actions.ts ← 🆕 board_code='system-notice' 고정 -│ └── NoticeList.tsx ← 🔄 공지 목록 UI -└── InquiryManagement/ - ├── types.ts ← 🔄 Inquiry 특화 타입 (answer_status) - ├── actions.ts ← 🆕 board_code='system-qna' 고정 - ├── InquiryList.tsx ← 🔄 답변대기/완료 필터 - ├── InquiryDetail.tsx ← 🔄 문의 상세 + 답변 작성 - └── InquiryForm.tsx ← 🔄 문의 등록 폼 -``` - -#### 🛠️ 구현 순서 (권장) - -**Step 1: mng에서 시스템 게시판 생성** -``` -mng.sam.kr/boards → 템플릿으로 생성: -- system-faq (FAQ 템플릿) -- system-notice (공지사항 템플릿) -- system-event (커스텀: 이벤트) -- system-qna (1:1문의 템플릿) -``` - -**Step 2: React 공통 모듈 개발** (✅ 게시판/게시글 API 이미 완비) -``` -react/src/lib/board/ -├── types.ts ← API 타입 정의 -├── actions.ts ← 게시글 CRUD Server Actions -└── utils.ts ← 커스텀필드 렌더링 유틸 -``` - -**Step 3: 각 메뉴별 연동** -``` -K-3 공지사항 (가장 단순) → K-1 FAQ → K-2 이벤트 → K-4 문의 (가장 복잡) -``` - -#### 💡 커스텀 필드 동적 렌더링 전략 - -```typescript -// 게시판 필드 스키마 기반 동적 폼 생성 -function renderCustomField(field: BoardSetting, value: string | null) { - switch (field.field_type) { - case 'text': return ; - case 'select': return