From 925ed82ae16a11c4b52f9de831c04bb4d9b3105a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Mar 2026 11:21:09 +0900 Subject: [PATCH 01/15] =?UTF-8?q?docs:=20=EC=8B=A0=EA=B7=9C=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=9E=90=20=EB=A1=9C=EC=BB=AC=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=85=8B=ED=8C=85=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 - Docker 기반 로컬 개발 환경 전체 셋팅 절차 - api, mng, react, docs, hotfix 5개 저장소 설명 - SSL 인증서, hosts, 환경변수, 트러블슈팅 포함 Co-Authored-By: Claude Opus 4.6 --- dev/guides/LOCAL_SETUP_GUIDE.md | 794 ++++++++++++++++++++++++++++++++ 1 file changed, 794 insertions(+) create mode 100644 dev/guides/LOCAL_SETUP_GUIDE.md diff --git a/dev/guides/LOCAL_SETUP_GUIDE.md b/dev/guides/LOCAL_SETUP_GUIDE.md new file mode 100644 index 0000000..75c4dc9 --- /dev/null +++ b/dev/guides/LOCAL_SETUP_GUIDE.md @@ -0,0 +1,794 @@ +# SAM 로컬 개발 환경 셋팅 가이드 + +> 최종 업데이트: 2026-03-09 + +--- + +## 1. 사전 준비 + +### 필수 소프트웨어 + +| 소프트웨어 | 버전 | 용도 | 설치 | +|-----------|------|------|---------------------------------------------------------------| +| **Docker Desktop** | 최신 | 컨테이너 실행 | [docker.com](https://www.docker.com/products/docker-desktop/) | +| **Git** | 최신 | 버전 관리 | `brew install git` | +| **mkcert** | 최신 | 로컬 SSL 인증서 | `brew install mkcert` | +| **텍스트 에디터** | - | 코드 편집 | JetBrains IDE 권장 | + +> Docker Desktop이 설치되면 PHP, Node.js, MySQL 등은 **별도 설치 불필요** (Docker 컨테이너 내부에서 실행) + +### Git 서버 정보 + +| 항목 | 값 | +|------|-----| +| Git 서버 | Gitea (자체 호스팅) | +| 서버 주소 | `http://114.203.209.83:3000` | +| 조직 | `SamProject` | + +> Gitea 계정이 필요합니다. 팀장에게 계정 생성을 요청하세요. + +--- + +## 2. 저장소 클론 + +### 디렉토리 구조 + +``` +Works/@KD_SAM/SAM/ ← 루트 (원하는 경로에 생성) +├── api/ ← Laravel REST API +├── mng/ ← Laravel 관리자 패널 +├── react/ ← Next.js 프론트엔드 +├── docs/ ← 기술 문서 +├── hotfix/ ← 테스트/QA 문서 +├── docker/ ← Docker 설정 (api 저장소에 포함되지 않음) +├── design/ ← 디자인 시스템 (선택) +└── planning/ ← 기획 문서 (선택) +``` + +### 클론 명령어 + +```bash +# 작업 디렉토리 생성 +mkdir -p ~/Works/@KD_SAM/SAM +cd ~/Works/@KD_SAM/SAM + +# 5개 저장소 클론 +git clone http://114.203.209.83:3000/SamProject/sam-api.git api +git clone http://114.203.209.83:3000/SamProject/sam-manage.git mng +git clone http://114.203.209.83:3000/SamProject/sam-react-prod.git react +git clone http://114.203.209.83:3000/SamProject/sam-docs.git docs +git clone http://114.203.209.83:3000/SamProject/sam-hotfix.git hotfix +``` + +### 기본 브랜치 전환 + +```bash +# api, react는 develop 브랜치에서 작업 +cd api && git checkout develop && cd .. +cd react && git checkout develop && cd .. +``` + +> **중요**: `main` 브랜치에서 직접 작업하지 않습니다. 항상 `develop`에서 작업합니다. + +--- + +## 3. Docker 환경 구성 + +### 3-1. Docker 설정 파일 확인 + +`docker/` 디렉토리에 모든 Docker 설정이 포함되어 있습니다. + +``` +docker/ +├── docker-compose.yml ← 메인 Compose 파일 +├── .env ← Compose 환경변수 +├── api/ +│ ├── Dockerfile ← PHP 8.4 + Nginx +│ ├── nginx.conf +│ ├── supervisord.conf +│ └── uploads.ini +├── mng/ +│ ├── Dockerfile ← PHP 8.4 + Nginx +│ ├── nginx.conf +│ ├── supervisord.conf +│ └── uploads.ini +├── react/ +│ └── Dockerfile ← Node 20 + Chromium (PDF용) +├── mysql/ +│ ├── init.sql ← 초기 DB/유저 생성 +│ └── my.cnf ← MySQL 설정 +├── nginx/ +│ ├── nginx.conf ← 리버스 프록시 설정 +│ └── ssl/ ← SSL 인증서 +└── 5130/ + └── Dockerfile ← 레거시 PHP 7.3 +``` + +### 3-2. 서비스 구성 + +| 서비스 | 이미지 | 내부 포트 | 역할 | +|--------|--------|-----------|------| +| **nginx** | nginx:latest | 80, 443 | 리버스 프록시, SSL 종료 | +| **api** | PHP 8.4-fpm | 9000 | REST API 백엔드 | +| **mng** | PHP 8.4-fpm | 9000 | 관리자 패널 | +| **react** | node:20-alpine | 3000 | Next.js 프론트엔드 | +| **mysql** | mysql:8.0 | 3306 | 데이터베이스 | +| **php73** | PHP 7.3-fpm | 9000 | 레거시 5130 앱 | + +### 3-3. 네트워크 + +모든 컨테이너는 `samnet` 브릿지 네트워크로 연결됩니다. + +``` +[브라우저] → [nginx:443] → api / mng / react (내부 라우팅) + → mysql (컨테이너명: sam-mysql-1) +``` + +--- + +## 4. hosts 파일 설정 + +로컬 도메인을 사용하기 위해 hosts 파일을 수정합니다. + +```bash +sudo nano /etc/hosts +``` + +아래 내용을 추가: + +``` +127.0.0.1 api.sam.kr mng.sam.kr admin.sam.kr dev.sam.kr design.sam.kr plan.sam.kr 5130.sam.kr +127.0.0.1 sam.kr www.sam.kr sales.sam.kr demo.sam.kr +``` + +--- + +## 5. SSL 인증서 설정 + +로컬 HTTPS를 위한 자체 서명 인증서를 생성합니다. + +### 5-1. mkcert 설치 및 초기화 + +```bash +# mkcert 설치 (처음 한 번) +brew install mkcert +mkcert -install # 로컬 CA를 시스템에 등록 +``` + +### 5-2. 와일드카드 인증서 생성 + +```bash +cd docker/nginx/ssl/ + +# *.sam.kr 와일드카드 인증서 생성 +mkcert "*.sam.kr" localhost 127.0.0.1 ::1 + +# 파일명 변경 (nginx.conf에서 참조하는 이름으로) +mv _wildcard.sam.kr+3.pem sam.kr.crt +mv _wildcard.sam.kr+3-key.pem sam.kr.key +``` + +### 5-3. 포트 포워딩 설정 (macOS) + +Docker가 443 포트를 4443으로 매핑하므로, 브라우저에서 표준 443 포트로 접속하려면 포트 포워딩이 필요합니다. + +```bash +# pfctl 규칙 적용 (443 → 4443 포워딩) +sudo pfctl -ef docker/nginx/ssl/pf-sam.conf +``` + +> **참고**: macOS 재부팅 시 이 설정은 초기화되므로 재부팅 후 다시 실행해야 합니다. + +**포트 포워딩 없이 사용하려면** `https://api.sam.kr:4443` 처럼 포트 번호를 직접 지정합니다. + +--- + +## 6. 환경변수 (.env) 설정 + +### 6-1. API (.env) + +```bash +cd api +cp .env.example .env +``` + +`.env` 파일에서 확인/수정할 항목: + +```env +# 앱 키 생성은 Docker 실행 후 컨테이너 내에서 수행 +# docker exec sam-api-1 php artisan key:generate + +APP_NAME="SAM API" +APP_ENV=local +APP_DEBUG=true +APP_URL=https://api.sam.kr/ + +# DB (Docker 환경에서는 docker-compose가 오버라이드) +DB_HOST=127.0.0.1 # 로컬 직접 접속 시 +# DB_HOST=sam-mysql-1 # Docker 환경 (자동 오버라이드) +DB_DATABASE=samdb +DB_USERNAME=samuser +DB_PASSWORD=sampass + +# Swagger +L5_SWAGGER_GENERATE_ALWAYS=true +L5_SWAGGER_CONST_HOST=https://api.sam.kr/ + +# Sanctum 토큰 (분 단위) +SANCTUM_ACCESS_TOKEN_EXPIRATION=120 +SANCTUM_REFRESH_TOKEN_EXPIRATION=10080 + +# 내부 통신 키 (MNG ↔ API) +INTERNAL_EXCHANGE_SECRET= # 팀 내부 문서에서 확인 +``` + +> **API 키, Firebase, AI 서비스 키** 등 민감한 값은 팀 내부 문서(노션)에서 별도 공유합니다. + +### 6-2. MNG (.env) + +```bash +cd mng +cp .env.example .env +``` + +```env +APP_NAME=SAM-MNG +APP_ENV=local +APP_DEBUG=true +APP_URL=https://mng.sam.kr + +DB_HOST=sam-mysql-1 +DB_DATABASE=samdb +DB_USERNAME=samuser +DB_PASSWORD=sampass + +# API 서버 연동 +API_BASE_URL=https://api.sam.kr + +# 내부 통신 키 (API와 동일한 값) +INTERNAL_EXCHANGE_SECRET= # API의 값과 동일하게 설정 +``` + +### 6-3. React (.env.local) + +```bash +cd react +cp .env.example .env.local +``` + +```env +NEXT_PUBLIC_APP_ENV=local +NEXT_PUBLIC_API_URL=https://api.sam.kr +NEXT_PUBLIC_FRONTEND_URL=https://dev.sam.kr +NEXT_PUBLIC_AUTH_MODE=sanctum + +# API Key (서버 사이드 전용 - NEXT_PUBLIC_ 접두사 붙이지 말 것!) +API_KEY= # 팀 내부 문서에서 확인 + +# 개발 도구 +NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=false + +# Puppeteer (PDF 생성 - Docker에서는 자동 설정) +PUPPETEER_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome +``` + +### 6-4. docs / hotfix + +별도 환경변수 설정 불필요 (순수 마크다운 문서 저장소) + +--- + +## 7. Docker 실행 + +### 7-1. 최초 실행 + +```bash +cd docker + +# 이미지 빌드 + 컨테이너 시작 +docker compose up -d --build +``` + +> 첫 실행 시 이미지 빌드에 5~10분 소요될 수 있습니다. + +### 7-2. 초기 설정 (최초 1회) + +```bash +# API: 앱 키 생성 + 의존성 설치 + 마이그레이션 +docker exec sam-api-1 php artisan key:generate +docker exec sam-api-1 composer install +docker exec sam-api-1 php artisan migrate +docker exec sam-api-1 php artisan l5-swagger:generate + +# MNG: 앱 키 생성 + 의존성 설치 +docker exec sam-mng-1 php artisan key:generate +docker exec sam-mng-1 composer install +docker exec sam-mng-1 npm install +docker exec sam-mng-1 npm run build +``` + +> React는 Docker 컨테이너 시작 시 자동으로 `npm install` + `npm run dev`가 실행됩니다. + +### 7-3. 일상적인 시작/종료 + +```bash +cd docker + +# 시작 +docker compose up -d + +# 종료 +docker compose down + +# 로그 확인 +docker compose logs -f # 전체 +docker compose logs -f api # API만 +docker compose logs -f react # React만 + +# 컨테이너 상태 확인 +docker compose ps +``` + +--- + +## 8. 접속 확인 + +### 8-1. 로컬 도메인 + +| 서비스 | URL | 설명 | +|--------|-----|------| +| **프론트엔드** | `https://dev.sam.kr` | Next.js 사용자 화면 | +| **API 문서** | `https://api.sam.kr/api-docs/index.html` | Swagger UI | +| **관리자 패널** | `https://mng.sam.kr` | MNG 관리자 화면 | +| **관리자 (별칭)** | `https://admin.sam.kr` | MNG와 동일 | +| **디자인 시스템** | `https://design.sam.kr` | 컴포넌트 스토리북 | +| **레거시** | `https://5130.sam.kr` | 기존 PHP 시스템 | + +### 8-2. DB 접속 정보 + +| 항목 | 값 | +|------|-----| +| Host | `127.0.0.1` | +| Port | `3306` | +| Database | `samdb` | +| Username | `samuser` | +| Password | `sampass` | +| Root Password | `root` | +| 레거시 DB | `chandj` | + +> DBeaver, DataGrip, MySQL Workbench 등 원하는 DB 클라이언트로 접속 가능 + +### 8-3. 접속 테스트 + +```bash +# API 응답 확인 +curl -k https://api.sam.kr/api-docs/index.html + +# MySQL 접속 확인 +docker exec sam-mysql-1 mysql -u samuser -psampass -e "SHOW DATABASES;" +``` + +--- + +## 9. 저장소별 상세 정보 + +### 9-1. api/ (REST API 서버) + +| 항목 | 값 | +|------|-----| +| 프레임워크 | Laravel 12 (PHP 8.4) | +| 인증 | Sanctum (토큰 기반) | +| API 문서 | Swagger (l5-swagger) | +| DB | MySQL 8.0 (samdb) | +| 역할 | 모든 비즈니스 로직의 중심, 프론트/관리자 모두 이 API 사용 | + +**주요 명령어:** + +```bash +# 컨테이너 진입 +docker exec -it sam-api-1 bash + +# 마이그레이션 +php artisan migrate +php artisan migrate:status + +# Swagger 재생성 +php artisan l5-swagger:generate + +# 코드 포매터 +./vendor/bin/pint + +# 캐시 초기화 +php artisan cache:clear +php artisan config:clear +php artisan route:clear +``` + +**핵심 디렉토리:** + +``` +api/ +├── app/ +│ ├── Http/Controllers/Api/V1/ ← API 컨트롤러 +│ ├── Http/Requests/ ← FormRequest (검증) +│ ├── Models/ ← Eloquent 모델 +│ ├── Services/ ← 비즈니스 로직 (핵심) +│ ├── Swagger/v1/ ← Swagger 문서 클래스 +│ └── Helpers/ApiResponse.php ← 응답 헬퍼 +├── database/migrations/ ← DB 마이그레이션 +├── routes/api.php ← API 라우트 +└── lang/ko/ ← 한국어 메시지 +``` + +### 9-2. mng/ (관리자 패널) + +| 항목 | 값 | +|------|-----| +| 프레임워크 | Laravel 12 (PHP 8.4) | +| 프론트엔드 | Blade + Tailwind CSS + HTMX + DaisyUI | +| 역할 | 시스템 관리, 메뉴/권한 관리, 테넌트 관리 | + +**주요 명령어:** + +```bash +# 컨테이너 진입 +docker exec -it sam-mng-1 bash + +# 프론트 에셋 빌드 +npm run build # 프로덕션 +npm run dev # 개발 (HMR) + +# 코드 포매터 +./vendor/bin/pint +``` + +**핵심 디렉토리:** + +``` +mng/ +├── app/ +│ ├── Http/Controllers/ ← 웹 컨트롤러 +│ ├── Models/ ← 독립 모델 (API와 별개) +│ └── Services/ ← 비즈니스 로직 +├── resources/views/ ← Blade 템플릿 +├── routes/web.php ← 웹 라우트 +└── public/ ← 정적 파일 +``` + +> **주의**: MNG에서는 DB 마이그레이션을 생성/실행하지 않습니다. DB 변경은 반드시 API 프로젝트에서 수행합니다. + +### 9-3. react/ (Next.js 프론트엔드) + +| 항목 | 값 | +|------|-----| +| 프레임워크 | Next.js 15 (React 19, TypeScript) | +| 스타일링 | Tailwind CSS 4 | +| UI 라이브러리 | shadcn/ui (Radix UI 기반) | +| 상태관리 | Zustand | +| 폼 검증 | Zod + React Hook Form | +| 역할 | 사용자용 ERP 프론트엔드 | + +**주요 명령어:** + +```bash +# React는 Docker 컨테이너가 자동으로 dev 서버를 실행합니다. +# 수동 실행이 필요한 경우: +docker exec -it sam-react-1 sh + +npm run dev # 개발 서버 +npm run build # 프로덕션 빌드 +npm run lint # ESLint +``` + +**핵심 디렉토리:** + +``` +react/ +├── src/ +│ ├── app/ ← Next.js App Router 페이지 +│ ├── components/ +│ │ ├── ui/ ← shadcn/ui 원자 컴포넌트 +│ │ ├── molecules/ ← 조합 컴포넌트 +│ │ ├── organisms/ ← 복합 컴포넌트 +│ │ └── [도메인]/ ← 도메인별 컴포넌트 +│ ├── lib/ ← 유틸리티, API 헬퍼 +│ └── stores/ ← Zustand 스토어 +├── public/ ← 정적 파일 +└── next.config.ts ← Next.js 설정 +``` + +**핵심 규칙:** +- 모든 페이지는 `'use client'` 선언 필수 (폐쇄형 ERP, SSR 불필요) +- API 호출은 Server Action을 통해 수행 (HttpOnly 쿠키 프록시) +- `buildApiUrl()` 유틸리티 필수 사용 + +### 9-4. docs/ (기술 문서) + +| 항목 | 값 | +|------|-----| +| 내용 | 개발 가이드, API 스펙, 기획 문서, 표준 | +| 형식 | Markdown | +| 설치 | 없음 (문서만 관리) | + +``` +docs/ +├── INDEX.md ← 문서 인덱스 (여기부터 시작) +├── dev/ +│ ├── dev_plans/ ← 개발 계획 문서 +│ ├── standards/ ← 코드/아키텍처 표준 +│ ├── guides/ ← 셋업/사용 가이드 +│ ├── changes/ ← 변경 로그 +│ └── deploys/ ← 배포 문서 +├── features/ ← 기능 스펙 +├── frontend/ ← 프론트엔드 API 스펙 +├── rules/ ← 비즈니스 규칙 +└── system/ ← 시스템 아키텍처 +``` + +### 9-5. hotfix/ (테스트/QA) + +| 항목 | 값 | +|------|-----| +| 내용 | E2E 테스트 결과, 버그 리포트, 테스트 케이스 | +| 형식 | Markdown + 스크린샷 | +| 설치 | 없음 (문서만 관리) | + +``` +hotfix/ +├── e2e/ ← E2E 테스트 정의 +├── testcase/ ← 테스트 케이스 문서 +├── reports/ ← 테스트 실행 결과 +├── screenshots/ ← 시각적 테스트 증거 +├── research/ ← 조사 자료 +├── sam.code-workspace ← VS Code 워크스페이스 +├── *-Test-Report_*.md ← 개별 테스트 보고서 +└── Fail-*_*.md ← 실패 케이스 보고서 +``` + +--- + +## 10. 주요 아키텍처 개념 + +### 10-1. 멀티테넌트 + +- 모든 데이터에 `tenant_id` 컬럼이 존재 +- `BelongsToTenant` 글로벌 스코프가 자동으로 테넌트 필터링 +- 하나의 DB에서 여러 회사(테넌트)의 데이터를 격리 + +### 10-2. 데이터 흐름 + +``` +[사용자 브라우저] + │ + ▼ +[Next.js (react/)] ──Server Action──▶ [Laravel API (api/)] ──▶ [MySQL] + │ +[관리자 브라우저] │ + │ │ + ▼ │ +[Laravel MNG (mng/)] ──내부 API 호출──────────┘ +``` + +### 10-3. 인증 방식 + +| 클라이언트 | 인증 방식 | 설명 | +|-----------|----------|------| +| React (웹) | Sanctum Cookie | HttpOnly 쿠키, Server Action 프록시 | +| MNG (관리자) | 세션 + 내부 HMAC | Laravel 세션 + API 내부 통신 키 | +| 외부 연동 | API Key + Bearer | Sanctum 토큰 | + +--- + +## 11. 자주 쓰는 명령어 + +### Docker 관련 + +```bash +# 전체 시작/종료 +cd docker && docker compose up -d +cd docker && docker compose down + +# 컨테이너 재시작 +docker restart sam-api-1 +docker restart sam-react-1 + +# 이미지 재빌드 (Dockerfile 수정 후) +docker compose build --no-cache api +docker compose up -d api + +# 볼륨 포함 완전 초기화 (⚠️ DB 데이터 삭제됨) +docker compose down -v +``` + +### API 개발 + +```bash +docker exec sam-api-1 php artisan migrate # 마이그레이션 +docker exec sam-api-1 php artisan migrate:rollback # 롤백 +docker exec sam-api-1 php artisan l5-swagger:generate # Swagger 재생성 +docker exec sam-api-1 ./vendor/bin/pint # 코드 포매팅 +docker exec sam-api-1 php artisan tinker # REPL +docker exec sam-api-1 php artisan route:list # 라우트 목록 +``` + +### MNG 개발 + +```bash +docker exec sam-mng-1 php artisan tinker +docker exec sam-mng-1 npm run dev # Vite HMR +docker exec sam-mng-1 npm run build # 에셋 빌드 +docker exec sam-mng-1 ./vendor/bin/pint +``` + +### MySQL 접속 + +```bash +# CLI 접속 +docker exec -it sam-mysql-1 mysql -u samuser -psampass samdb + +# 특정 쿼리 실행 +docker exec sam-mysql-1 mysql -u samuser -psampass samdb -e "SHOW TABLES;" +``` + +--- + +## 12. 트러블슈팅 + +### SSL 인증서 오류 (브라우저에서 "안전하지 않음") + +```bash +# mkcert CA가 설치되지 않은 경우 +mkcert -install + +# 인증서 재생성 +cd docker/nginx/ssl +mkcert "*.sam.kr" localhost 127.0.0.1 ::1 +mv _wildcard.sam.kr+3.pem sam.kr.crt +mv _wildcard.sam.kr+3-key.pem sam.kr.key + +# Nginx 재시작 +docker restart sam-nginx-1 +``` + +### 도메인 접속이 안 될 때 + +```bash +# hosts 파일 확인 +cat /etc/hosts | grep sam + +# DNS 캐시 초기화 (macOS) +sudo dscacheutil -flushcache +sudo killall -HUP mDNSResponder + +# Nginx 설정 유효성 확인 +docker exec sam-nginx-1 nginx -t +``` + +### 포트 포워딩 확인 (443 → 4443) + +```bash +# 현재 규칙 확인 +sudo pfctl -s rules | grep 4443 + +# 규칙 재적용 +sudo pfctl -ef docker/nginx/ssl/pf-sam.conf + +# 규칙 비활성화 (필요 시) +sudo pfctl -d +``` + +### DB 접속 오류 + +```bash +# MySQL 컨테이너 상태 확인 +docker compose ps mysql + +# MySQL 로그 확인 +docker compose logs mysql + +# 수동 접속 테스트 +docker exec sam-mysql-1 mysql -u root -proot -e "SELECT 1;" +``` + +### React 빌드/HMR 오류 + +```bash +# node_modules 초기화 +docker exec sam-react-1 rm -rf node_modules .next +docker exec sam-react-1 npm install +docker restart sam-react-1 +``` + +### composer/npm 의존성 문제 + +```bash +# API +docker exec sam-api-1 composer install --no-cache +docker exec sam-api-1 composer dump-autoload + +# MNG +docker exec sam-mng-1 composer install --no-cache +docker exec sam-mng-1 npm ci +``` + +--- + +## 13. Git 워크플로우 + +### 브랜치 전략 + +| 브랜치 | 역할 | 규칙 | +|--------|------|------| +| `main` | 배포용 | squash merge로만 올림, 직접 커밋 금지 | +| `develop` | 일상 작업 | 자유롭게 커밋 | +| `feature/*` | 대형 기능 | 선택적 사용 (1주일+ 작업) | + +### 커밋 메시지 형식 + +``` +type: 간결한 설명 + +type 종류: + feat: 새 기능 + fix: 버그 수정 + refactor: 리팩토링 + docs: 문서 + chore: 설정, 빌드 + style: 코드 포매팅 +``` + +### 일상 워크플로우 + +```bash +# 1. 작업 시작 +cd api +git checkout develop +git pull origin develop + +# 2. 작업 수행 + 커밋 +git add <파일들> +git commit -m "feat: 수주관리 삭제 기능 추가" + +# 3. 푸시 +git push origin develop +``` + +--- + +## 14. 셋팅 체크리스트 + +아래 항목을 순서대로 확인하며 셋팅을 완료합니다. + +- [ ] Docker Desktop 설치 및 실행 +- [ ] Git 설치 및 Gitea 계정 발급 +- [ ] 5개 저장소 클론 완료 +- [ ] `/etc/hosts` 파일 수정 +- [ ] mkcert 설치 + SSL 인증서 생성 +- [ ] pfctl 포트 포워딩 설정 +- [ ] api/.env 설정 (`.env.example` → `.env`) +- [ ] mng/.env 설정 +- [ ] react/.env.local 설정 +- [ ] API Key, HMAC Key 등 민감 값 입력 (팀 공유 문서 참조) +- [ ] `docker compose up -d --build` 실행 +- [ ] 초기 설정 실행 (key:generate, composer install, migrate) +- [ ] `https://api.sam.kr/api-docs/index.html` 접속 확인 +- [ ] `https://mng.sam.kr` 접속 확인 +- [ ] `https://dev.sam.kr` 접속 확인 +- [ ] DB 클라이언트로 `127.0.0.1:3306` 접속 확인 + +--- + +## 15. 참고 문서 + +| 문서 | 경로 | 설명 | +|------|------|------| +| 문서 인덱스 | `docs/INDEX.md` | 전체 문서 목록 | +| API 규칙 | `API_RULES.md` | API 개발 규칙 | +| 개발 명령어 | `DEV_COMMANDS.md` | 자주 쓰는 명령어 모음 | +| 품질 체크리스트 | `QUALITY_CHECKLIST.md` | 코드 품질 체크 항목 | +| 빠른 참조 | `SAM_QUICK_REFERENCE.md` | 핵심 규칙 요약 | +| SSL 가이드 | `docker/nginx/ssl/SSL_SETUP_GUIDE.md` | SSL 상세 설정 | + +--- + +> 문의사항은 팀 슬랙 채널 또는 팀장에게 문의하세요. \ No newline at end of file From 5000c67ec1e0fb557836c7a267f806ce1d8707e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Mar 2026 19:57:50 +0900 Subject: [PATCH 02/15] =?UTF-8?q?docs:=20[database]=20codebridge=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=8C=80=EC=83=81=EC=97=90=EC=84=9C=20API?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20=ED=85=8C=EC=9D=B4=EB=B8=94=2022?= =?UTF-8?q?=EA=B0=9C=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Barobill 12개: API 모델/서비스/컨트롤러에서 직접 사용 - ESign 4개: API 전자계약 기능 (EsignService, EsignContractController) - Audit 2개: API 전사 감사 시스템 (AuditLogService, TriggerAuditLogController) - DevTools 1개: api_request_logs (SystemStatService) - System 2개: ai_pricing_configs, ai_token_usages (API 모델) - HR 1개: income_tax_brackets (API Seeder) - codebridge 이동 대상 100개 → 55개로 축소 --- .../system/database/codebridge-separation.md | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 sam/docs/system/database/codebridge-separation.md diff --git a/sam/docs/system/database/codebridge-separation.md b/sam/docs/system/database/codebridge-separation.md new file mode 100644 index 0000000..159e562 --- /dev/null +++ b/sam/docs/system/database/codebridge-separation.md @@ -0,0 +1,332 @@ +# codebridge DB 분리 + +> **작성일**: 2026-03-07 +> **상태**: 로컬/개발 서버 적용 완료, 운영 서버 미적용 +> **최종 수정**: 2026-03-09 — API 사용 테이블 점검 완료, codebridge 이동 대상 100개 → 55개로 축소 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리한다. + +- **samdb**: React 서비스가 사용하는 테이블 (수주, 견적, 생산, 거래처 등) +- **codebridge**: MNG(관리자 패널)에서만 사용하는 코드브릿지엑스 내부 관리 테이블 + +### 1.2 핵심 원칙 + +- 기존 samdb의 테이블은 **삭제하지 않는다** (복사만 수행) +- MNG 모델에 `$connection = 'codebridge'`를 설정하여 읽기 대상 DB만 변경 +- React/API 서비스에는 **영향 없음** + +### 1.3 분리 기준 + +| 분류 | 대상 DB | 기준 | +|------|---------|------| +| React 서비스 테이블 | samdb (유지) | React 프론트엔드 또는 API에서 사용 | +| MNG 전용 테이블 | codebridge (이동) | MNG에서만 사용, React/API 미참조 | +| 공통 테이블 | samdb (유지) | 양쪽 모두 사용 (users, tenants 등) | +| **API 사용 테이블** | **samdb (유지 필수)** | **API에 모델/서비스/컨트롤러 존재 — 이동 시 데이터 불일치 발생** | + +> **경고: API에서 모델/서비스/컨트롤러로 참조하는 테이블을 codebridge로 이동하면, API는 samdb에 쓰고 MNG는 codebridge에서 읽게 되어 데이터 불일치가 발생한다. 절대 이동 금지.** + +--- + +## 2. codebridge 테이블 목록 (55개) + +> 2026-03-09 점검: API 프로젝트 전체 코드 조사를 통해 API에서 사용하는 22개 테이블을 제외함. 제외된 테이블은 [3절](#3-api-사용-테이블--samdb-유지-필수-22개) 참조. + +### Admin (9) + +| 테이블 | 설명 | +|--------|------| +| `admin_api_flows` | API 플로우 정의 | +| `admin_api_flow_runs` | API 플로우 실행 이력 | +| `admin_pm_daily_logs` | PM 일일 로그 | +| `admin_pm_daily_log_entries` | PM 일일 로그 항목 | +| `admin_pm_issues` | PM 이슈 | +| `admin_pm_projects` | PM 프로젝트 | +| `admin_pm_tasks` | PM 태스크 | +| `admin_roadmap_milestones` | 로드맵 마일스톤 | +| `admin_roadmap_plans` | 로드맵 계획 | + +### DevTools (5) + +| 테이블 | 설명 | +|--------|------| +| `api_bookmarks` | API 북마크 | +| `api_deprecations` | API 지원종료 관리 | +| `api_environments` | API 환경 설정 | +| `api_histories` | API 호출 이력 | +| `api_templates` | API 템플릿 | + +### Sales (17) + +| 테이블 | 설명 | +|--------|------| +| `sales_partners` | 영업 파트너 | +| `sales_managers` | 영업 담당자 | +| `sales_manager_documents` | 영업 담당자 문서 | +| `sales_commissions` | 영업 수당 | +| `sales_commission_details` | 영업 수당 상세 | +| `sales_consultations` | 영업 상담 | +| `sales_contract_products` | 계약 제품 | +| `sales_products` | 영업 제품 | +| `sales_product_categories` | 영업 제품 카테고리 | +| `sales_prospects` | 영업 전망 | +| `sales_prospect_consultations` | 전망 상담 | +| `sales_prospect_products` | 전망 제품 | +| `sales_prospect_scenarios` | 전망 시나리오 | +| `sales_records` | 영업 실적 | +| `sales_scenario_checklists` | 시나리오 체크리스트 | +| `sales_tenant_managements` | 테넌트 영업 관리 | +| `tenant_prospects` | 테넌트 전망 | + +### Finance (9) + +| 테이블 | 설명 | +|--------|------| +| `condolence_expenses` | 경조사비 | +| `consulting_fees` | 컨설팅비 | +| `corporate_cards` | 법인카드 | +| `corporate_card_prepayments` | 법인카드 선결제 | +| `customer_settlements` | 고객 정산 | +| `daily_fund_memos` | 일일 자금 메모 | +| `daily_fund_transactions` | 일일 자금 거래 | +| `incomes` | 수입 | +| `vat_records` | 부가세 기록 | + +### ESign (2) + +| 테이블 | 설명 | +|--------|------| +| `esign_field_templates` | 전자서명 필드 템플릿 | +| `esign_field_template_items` | 전자서명 필드 항목 | + +> esign_contracts, esign_audit_logs, esign_sign_fields, esign_signers는 API에서 전자계약 기능으로 사용 중 → samdb 유지 + +### Equipment (2) + +| 테이블 | 설명 | +|--------|------| +| `equipments` | 설비 | +| `equipment_processes` | 설비 공정 | + +### HR (1) + +| 테이블 | 설명 | +|--------|------| +| `business_income_payments` | 사업소득 지급 | + +> income_tax_brackets는 API IncomeTaxBracketSeeder에서 초기 데이터 관리 → samdb 유지 + +### System (1) + +| 테이블 | 설명 | +|--------|------| +| `ai_configs` | AI 설정 | + +> ai_pricing_configs, ai_token_usages는 API 모델에서 직접 사용 → samdb 유지 + +### 기타 (9) + +| 테이블 | 설명 | +|--------|------| +| `biz_certs` | 사업자등록증 | +| `cm_songs` | R&D 곡 관리 | +| `construction_site_photos` | 시공 현장 사진 | +| `construction_site_photo_rows` | 시공 사진 행 | +| `meeting_logs` | 회의 로그 | +| `meeting_minutes` | 회의록 | +| `meeting_minute_segments` | 회의록 세그먼트 | +| `interview_knowledges` | 면접 지식 | +| `sales_records` | 매출 기록 | + +--- + +## 3. API 사용 테이블 — samdb 유지 필수 (22개) + +> **경고: 아래 테이블은 API 프로젝트에서 모델/서비스/컨트롤러/시더로 직접 참조한다. 절대 codebridge로 이동 금지.** +> +> **2026-03-09 점검**: sam/api 프로젝트 전체 코드 (모델, 컨트롤러, 서비스, 라우트, 시더) 조사 완료. + +### Barobill (12) — 전체 samdb 유지 + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `barobill_billing_records` | BarobillBillingService | 과금 기록 CRUD | +| `barobill_members` | BarobillUsageService | 회원사 사용량 집계 | +| `barobill_monthly_summaries` | BarobillBillingService | 월별 집계 갱신 | +| `barobill_pricing_policies` | BarobillUsageService | 과금 계산 | +| `bank_sync_statuses` | BankSyncStatus 모델 | 동기화 상태 추적 | +| `bank_transactions` | BankTransactionController | 은행 거래 조회/분개 | +| `bank_transaction_overrides` | BankTransactionOverride 모델 | 거래 재정의 | +| `bank_transaction_splits` | BankTransactionController | 은행 거래 분개 | +| `card_transaction_amount_logs` | CardTransactionAmountLog 모델 | 금액 수정 이력 + FK → card_transactions | +| `card_transaction_hides` | CardTransactionHide 모델 | 거래 숨김 + FK → card_transactions | +| `hometax_invoices` | BarobillUsageService | 세금계산서 사용량 집계 | +| `hometax_invoice_journals` | HometaxInvoiceJournal 모델 | 세금계산서 분개 + FK → hometax_invoices | + +> **핵심**: API의 BarobillController, BarobillSettingController, BarobillService, EntertainmentService가 바로빌 테이블을 직접 참조. `barobill_card_transactions` (samdb 유지)와 FK로 연결된 자식 테이블도 분리 불가. + +### ESign (4) — API 전자계약 기능 + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `esign_contracts` | EsignContractController, EsignService | 전자계약 CRUD | +| `esign_audit_logs` | EsignService | 감사 추적 기록 | +| `esign_sign_fields` | EsignService | 서명 위치 데이터 | +| `esign_signers` | EsignService | 서명자 정보/인증 | + +### Audit (2) — API 전사 감사 시스템 + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `audit_logs` | AuditLog 모델, AuditLogService, AuditRollbackService | 전사 DML 감사 | +| `trigger_audit_logs` | TriggerAuditLog 모델, TriggerAuditLogController, RegenerateAuditTriggers 명령 | DB 트리거 감사 + 파티셔닝 관리 | + +### DevTools (1) + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `api_request_logs` | ApiRequestLog 모델, SystemStatService | API 통계 집계 | + +### System (2) + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `ai_pricing_configs` | AiPricingConfig 모델 | AI 서비스 비용 계산 (캐시 기반) | +| `ai_token_usages` | AiTokenUsage 모델 | 멀티테넌트 AI 토큰 사용량 추적 | + +### HR (1) + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `income_tax_brackets` | IncomeTaxBracketSeeder | 소득세 구간 초기 데이터 관리 | + +--- + +## 4. 적용 현황 + +### 4.1 환경별 상태 + +| 환경 | codebridge DB | 테이블 복사 | .env 설정 | MNG 모델 적용 | 상태 | +|------|:---:|:---:|:---:|:---:|------| +| **로컬 Docker** | O | 55개 | O | O (develop) | 정상 작동 | +| **개발 서버** | O | 55개 | O | O (develop) | 정상 작동 | +| **운영 서버** | X | X | X | X (revert) | 미적용 (기존 samdb 사용) | + +> **2026-03-09 정리**: +> - `finance_*` 17개 + `barobill_companies` 1개: 마이그레이션 없이 수동 생성된 테이블 → 삭제 완료 +> - API 사용 테이블 22개: codebridge 이동 대상에서 제외 → samdb 유지 (MNG 모델의 `$connection = 'codebridge'` 제거 필요) + +### 4.2 코드 변경 사항 + +**config/database.php** — `codebridge` connection 추가: + +```php +'codebridge' => [ + 'driver' => 'mysql', + 'host' => env('CODEBRIDGE_DB_HOST', env('DB_HOST', '127.0.0.1')), + 'port' => env('CODEBRIDGE_DB_PORT', env('DB_PORT', '3306')), + 'database' => env('CODEBRIDGE_DB_DATABASE', 'codebridge'), + 'username' => env('CODEBRIDGE_DB_USERNAME', env('DB_USERNAME')), + 'password' => env('CODEBRIDGE_DB_PASSWORD', env('DB_PASSWORD')), + // ... (mysql 기본 설정과 동일) +], +``` + +**.env** — 추가 설정: + +``` +CODEBRIDGE_DB_DATABASE=codebridge +``` + +**MNG 모델** — `$connection` 속성 추가 (codebridge 55개만): + +```php +class SalesPartner extends Model +{ + protected $connection = 'codebridge'; // 추가 + protected $table = 'sales_partners'; + // ... +} +``` + +> **주의**: API 사용 테이블 22개에 해당하는 MNG 모델은 `$connection = 'codebridge'`를 설정하지 않는다. 기본 samdb connection을 사용해야 API와 동일한 데이터를 참조한다. + +--- + +## 5. 운영 서버 적용 절차 (미완료) + +> **전제**: 운영 서버 SSH 접근 + DB root 권한 필요 + +### 순서 (반드시 1 → 2 → 3 순서로) + +**1단계: codebridge DB 생성 + 테이블 복사** + +```bash +# DB 생성 +mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + +# DB 계정 권한 부여 +mysql -u root -p -e "GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; FLUSH PRIVILEGES;" + +# sam에서 55개 테이블 구조+데이터 복사 (mysqldump → import) +mysqldump -u codebridge -p sam [테이블목록] | mysql -u codebridge -p codebridge +``` + +**2단계: .env 설정** + +```bash +echo 'CODEBRIDGE_DB_DATABASE=codebridge' >> /home/webservice/mng/.env +cd /home/webservice/mng && php artisan config:clear +``` + +**3단계: 코드 배포 (main push)** + +MNG 코드(codebridge 55개 모델 `$connection` 변경)를 main에 cherry-pick + push. +Jenkins가 자동 배포. + +> **주의**: 3단계 전에 1, 2단계가 완료되어야 한다. DB가 없는 상태에서 코드를 배포하면 MNG 페이지 접속 불가. + +--- + +## 6. 아키텍처 다이어그램 + +``` + React (사용자) + | + API 서버 (Laravel) + | + ┌─────┴─────┐ + | | + samdb sam_stat + (서비스 DB) (통계 DB) + | + | (공통 + API 사용 테이블: users, tenants, barobill_*, esign_*, audit_* 등) + | + MNG (관리자) + | + ┌─────┴─────┐ + | | + samdb codebridge + (공통 참조) (MNG 전용 55개) +``` + +- **React → API → samdb**: 서비스 트래픽 (수주, 견적, 생산, 바로빌, 전자서명 등) +- **MNG → samdb**: 공통 테이블 (users, tenants, menus) + API 사용 테이블 22개 참조 +- **MNG → codebridge**: MNG 전용 데이터 (영업관리, 재무, 설비, PM 도구 등) + +--- + +## 관련 문서 + +- [database/README.md](README.md) — DB 스키마 전체 현황 +- [codebridge-db-separation-plan.md](/home/aweso/sam/docs/plans/codebridge-db-separation-plan.md) — 분리 작업 계획서 (plans/) + +--- + +**최종 업데이트**: 2026-03-09 From ec3abc1a855c379808417fc53a72c86942d24ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Mar 2026 20:34:11 +0900 Subject: [PATCH 03/15] =?UTF-8?q?docs:=20[approvals]=20=EA=B2=B0=EC=9E=AC?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20DB=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=EB=B0=8F=20API=20=EB=AA=A8=EB=8D=B8=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=ED=98=84=ED=99=A9=20=EB=AC=B8=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 - 2026-02-27 ~ 03-05 마이그레이션 15개 변경 타임라인 정리 - API/MNG 모델 $fillable/$casts 동기화 비교표 작성 - API 모델 미반영으로 인한 잠재적 오류 영향 분석 --- sam/docs/INDEX.md | 1 + sam/docs/features/approvals/README.md | 1 + .../approvals/db-changes-and-model-sync.md | 286 ++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 sam/docs/features/approvals/db-changes-and-model-sync.md diff --git a/sam/docs/INDEX.md b/sam/docs/INDEX.md index 3bee9ea..cbc7d50 100644 --- a/sam/docs/INDEX.md +++ b/sam/docs/INDEX.md @@ -289,6 +289,7 @@ docs/ | [approvals/workflows.md](features/approvals/workflows.md) | 결재관리 워크플로우 상세 (승인/반려/회수/보류/전결/복사재기안) | | [approvals/api-reference.md](features/approvals/api-reference.md) | 결재관리 API 명세 (20개 엔드포인트) | | [approvals/ui-screens.md](features/approvals/ui-screens.md) | 결재관리 UI 화면 구성 (Blade + HTMX) | +| [approvals/db-changes-and-model-sync.md](features/approvals/db-changes-and-model-sync.md) | 결재관리 DB 변경사항 및 API 모델 동기화 현황 (2026-02~03) | ### projects/ - 프로젝트별 문서 diff --git a/sam/docs/features/approvals/README.md b/sam/docs/features/approvals/README.md index c7b660a..a43521c 100644 --- a/sam/docs/features/approvals/README.md +++ b/sam/docs/features/approvals/README.md @@ -22,6 +22,7 @@ SAM MNG 전자결재 시스템. 기안부터 최종 승인, 반려, 회수, 보 | [workflows.md](workflows.md) | 상세 워크플로우 (승인/반려/회수/보류/전결/복사재기안) | | [api-reference.md](api-reference.md) | API 엔드포인트 명세 | | [ui-screens.md](ui-screens.md) | 화면별 UI 구성 및 동작 | +| [db-changes-and-model-sync.md](db-changes-and-model-sync.md) | DB 변경사항 및 API/MNG 모델 동기화 현황 | ### 1.3 구현 현황 diff --git a/sam/docs/features/approvals/db-changes-and-model-sync.md b/sam/docs/features/approvals/db-changes-and-model-sync.md new file mode 100644 index 0000000..e59cd2b --- /dev/null +++ b/sam/docs/features/approvals/db-changes-and-model-sync.md @@ -0,0 +1,286 @@ +# 결재관리 DB 변경사항 및 API 모델 동기화 현황 + +> **작성일**: 2026-03-09 +> **상태**: 조사 완료 +> **관련**: [README.md](README.md) | [API 명세](api-reference.md) + +--- + +## 1. 개요 + +### 1.1 목적 + +2026-02-27 ~ 2026-03-05 기간에 결재관리 테이블에 대규모 컬럼 추가가 이루어졌다. 이 문서는 변경된 DB 스키마와 API/MNG 프로젝트 간 모델 동기화 상태를 기록한다. + +### 1.2 핵심 발견 + +- 마이그레이션 **15개** 실행 (API 프로젝트에서 관리) +- MNG 모델: ✅ 모든 신규 컬럼 반영 완료 +- API 모델: ❌ **`$fillable`/`$casts` 미반영** — 오류 원인 가능성 + +--- + +## 2. 마이그레이션 변경 타임라인 + +### 2.1 Phase 2 확장 (2026-02-27) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_columns_to_approvals_table` | `approvals` | `line_id`, `body`, `is_urgent`, `department_id` 추가 | +| `add_columns_to_approval_steps_table` | `approval_steps` | `approver_name`, `approver_department`, `approver_position` 추가 | +| `add_phase2_columns_to_approval_steps_table` | `approval_steps` | `parallel_group`, `acted_by`, `approval_type` 추가 | +| `add_phase2_columns_to_approvals_table` | `approvals` | `recall_reason`, `parent_doc_id` 추가 | +| `create_approval_delegations_table` | `approval_delegations` | 위임 테이블 신규 생성 | +| `add_linkable_to_approvals_table` | `approvals` | `linkable_type`, `linkable_id` 추가 (다형성) | + +### 2.2 도메인 연동 (2026-02-28) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_approval_id_to_leaves_table` | `leaves` | `approval_id` FK 추가 | +| `insert_leave_approval_form` | `approval_forms` | 휴가신청 양식 데이터 등록 | + +### 2.3 양식 확장 (2026-03-03 ~ 03-04) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `insert_attendance_approval_forms` | `approval_forms` | 근태신청, 사유서 양식 등록 | +| `add_body_template_to_approval_forms` | `approval_forms` | `body_template` 컬럼 추가 | +| `insert_expense_approval_form` | `approval_forms` | 지출결의서 양식 + body_template 등록 | +| `update_expense_approval_form_body_template` | `approval_forms` | 지출결의서 body_template 고도화 | + +### 2.4 추적 기능 (2026-03-05) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_drafter_read_at_to_approvals_table` | `approvals` | `drafter_read_at` 추가 | +| `add_resubmit_count_to_approvals_table` | `approvals` | `resubmit_count` 추가 | +| `add_rejection_history_to_approvals_table` | `approvals` | `rejection_history` 추가 | + +--- + +## 3. 추가된 컬럼 상세 + +### 3.1 `approvals` 테이블 (11개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `line_id` | BIGINT FK NULL | NULL | 02-27 | 결재선 템플릿 참조 | +| `body` | LONGTEXT NULL | NULL | 02-27 | 문서 본문 HTML | +| `is_urgent` | BOOLEAN | false | 02-27 | 긴급 여부 | +| `department_id` | BIGINT NULL | NULL | 02-27 | 기안 부서 | +| `recall_reason` | TEXT NULL | NULL | 02-27 | 회수 사유 | +| `parent_doc_id` | BIGINT FK NULL | NULL | 02-27 | 재기안 원본 문서 | +| `linkable_type` | VARCHAR NULL | NULL | 02-27 | 다형성 모델 타입 | +| `linkable_id` | BIGINT NULL | NULL | 02-27 | 다형성 모델 ID | +| `drafter_read_at` | TIMESTAMP NULL | NULL | 03-05 | 기안자 열람 시각 | +| `resubmit_count` | TINYINT UNSIGNED | 0 | 03-05 | 재상신 횟수 | +| `rejection_history` | JSON NULL | NULL | 03-05 | 반려 이력 배열 | + +### 3.2 `approval_steps` 테이블 (6개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `approver_name` | VARCHAR(50) NULL | NULL | 02-27 | 결재자명 스냅샷 | +| `approver_department` | VARCHAR(100) NULL | NULL | 02-27 | 결재자 부서 스냅샷 | +| `approver_position` | VARCHAR(50) NULL | NULL | 02-27 | 결재자 직급 스냅샷 | +| `parallel_group` | INT NULL | NULL | 02-27 | 병렬 결재 그룹 (Phase 3) | +| `acted_by` | BIGINT FK NULL | NULL | 02-27 | 실제 처리자 (대결) | +| `approval_type` | VARCHAR(20) | 'normal' | 02-27 | normal/pre_decided/delegated | + +### 3.3 `approval_forms` 테이블 (1개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `body_template` | TEXT NULL | NULL | 03-04 | HTML 양식 렌더링 템플릿 | + +### 3.4 `approval_delegations` 테이블 (신규 생성) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `delegator_id` | BIGINT FK | 위임자 | +| `delegate_id` | BIGINT FK | 대리인 | +| `start_date` | DATE | 위임 시작일 | +| `end_date` | DATE | 위임 종료일 | +| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) | +| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 | +| `is_active` | BOOLEAN | 활성 여부 | +| `reason` | VARCHAR(200) | 위임 사유 | + +--- + +## 4. API/MNG 모델 동기화 현황 + +### 4.1 Approval 모델 비교 + +| 항목 | MNG (`mng/app/Models/Approvals/Approval.php`) | API (`api/app/Models/Tenants/Approval.php`) | +|------|:---:|:---:| +| `line_id` in $fillable | ✅ | ❌ | +| `body` in $fillable | ✅ | ❌ | +| `is_urgent` in $fillable/$casts | ✅ boolean | ❌ | +| `department_id` in $fillable | ✅ | ❌ | +| `recall_reason` in $fillable | ✅ | ❌ | +| `parent_doc_id` in $fillable | ✅ | ❌ | +| `linkable_type/id` in $fillable | ✅ | ✅ | +| `drafter_read_at` in $fillable/$casts | ✅ datetime | ❌ | +| `resubmit_count` in $fillable/$casts | ✅ integer | ❌ | +| `rejection_history` in $fillable/$casts | ✅ array | ❌ | + +### 4.2 ApprovalStep 모델 비교 + +| 항목 | MNG | API | +|------|:---:|:---:| +| `approver_name` in $fillable | ✅ | ❌ | +| `approver_department` in $fillable | ✅ | ❌ | +| `approver_position` in $fillable | ✅ | ❌ | +| `parallel_group` in $fillable | ✅ | ❌ | +| `acted_by` in $fillable | ✅ | ❌ | +| `approval_type` in $fillable | ✅ | ❌ | + +### 4.3 ApprovalForm 모델 비교 + +| 항목 | MNG | API | +|------|:---:|:---:| +| `body_template` in $fillable | ✅ | ❌ | + +### 4.4 ApprovalDelegation 모델 + +| 항목 | MNG | API | +|------|:---:|:---:| +| 모델 파일 존재 | ✅ | ❌ 미생성 | + +--- + +## 5. 오류 영향 분석 + +### 5.1 API 모델 미반영으로 인한 잠재적 오류 + +API 프로젝트의 모델 `$fillable`에 신규 컬럼이 누락되어, API 엔드포인트를 통한 결재 문서 처리 시 다음 오류가 발생할 수 있다: + +| 증상 | 원인 | 영향 범위 | +|------|------|----------| +| `create()`/`update()` 시 신규 필드 저장 안 됨 | `$fillable` 미포함 → mass assignment 차단 | API v1 결재 CRUD | +| JSON 필드(`rejection_history`) 문자열로 반환 | `$casts` 미정의 → 타입 변환 안 됨 | API 응답 파싱 오류 | +| `drafter_read_at` 날짜 비교 실패 | `$casts` datetime 미정의 → Carbon 미변환 | 열람 추적 기능 | +| `is_urgent` 비교 오류 | `$casts` boolean 미정의 → 문자열 비교 | 긴급 필터링 | +| 위임(delegation) 기능 완전 불가 | 모델 자체 미생성 | Phase 3 기능 전체 | + +### 5.2 MNG는 정상 + +MNG 프로젝트의 모델은 모든 신규 컬럼이 `$fillable`, `$casts`, `$attributes`에 반영되어 있으며, `ApprovalService`에서 정상 사용 중이다. + +``` +MNG 정상 동작 확인 기능: +✅ 반려 이력 저장 (rejection_history) +✅ 재상신 횟수 추적 (resubmit_count) +✅ 기안자 열람 추적 (drafter_read_at) +✅ 결재자 스냅샷 저장 (approver_name/department/position) +✅ 전결 처리 (approval_type = pre_decided) +✅ 회수 사유 기록 (recall_reason) +``` + +--- + +## 6. 수정 필요 파일 목록 + +### 6.1 API 모델 업데이트 필요 + +| 파일 | 수정 내용 | +|------|----------| +| `api/app/Models/Tenants/Approval.php` | `$fillable`에 9개 필드, `$casts`에 4개 필드 추가 | +| `api/app/Models/Tenants/ApprovalStep.php` | `$fillable`에 6개 필드 추가 | +| `api/app/Models/Tenants/ApprovalForm.php` | `$fillable`에 `body_template` 추가 | +| `api/app/Models/Tenants/ApprovalDelegation.php` | 모델 파일 신규 생성 | + +### 6.2 Approval.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'line_id', +'body', +'is_urgent', +'department_id', +'recall_reason', +'parent_doc_id', +'drafter_read_at', +'resubmit_count', +'rejection_history', +``` + +**`$casts` 추가 필요:** + +```php +'drafter_read_at' => 'datetime', +'resubmit_count' => 'integer', +'rejection_history' => 'array', +'is_urgent' => 'boolean', +``` + +### 6.3 ApprovalStep.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'approver_name', +'approver_department', +'approver_position', +'parallel_group', +'acted_by', +'approval_type', +``` + +### 6.4 ApprovalForm.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'body_template', +``` + +--- + +## 7. 연관 테이블 참조 변경 + +결재 시스템과 연동된 다른 테이블의 변경사항: + +| 테이블 | 추가 컬럼 | 추가일 | 용도 | +|--------|----------|--------|------| +| `leaves` | `approval_id` (BIGINT FK) | 02-28 | 휴가 ↔ 결재 연동 | +| `purchases` | `approval_id` (BIGINT FK) | (기존) | 구매 ↔ 결재 연동 | + +--- + +## 8. 등록된 결재 양식 (13종) + +2026-02-28 ~ 03-07 기간에 마이그레이션으로 등록된 양식: + +| 코드 | 양식명 | 카테고리 | 등록일 | +|------|--------|---------|--------| +| `leave` | 휴가신청서 | request | 02-28 | +| `attendance_request` | 근태신청서 | request | 03-03 | +| `reason_report` | 사유서 | request | 03-03 | +| `expense` | 지출결의서 | expense | 03-04 | +| `employment_cert` | 재직증명서 | request | 03-05 | +| `career_cert` | 경력증명서 | request | 03-05 | +| `appointment_cert` | 위촉증명서 | request | 03-05 | +| `resignation` | 사직서 | request | 03-06 | +| `seal_usage` | 사용인감계 | request | 03-06 | +| `delegation` | 위임장 | request | 03-06 | +| `board_minutes` | 이사회의사록 | request | 03-06 | +| `quotation` | 견적서 | request | 03-06 | +| `official_letter` | 공문서 | request | 03-07 | + +--- + +## 관련 문서 + +- [결재관리 시스템 개요](README.md) — 아키텍처, 상태 관리, 권한 +- [API 명세](api-reference.md) — 20개 엔드포인트 상세 +- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름 +- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획 + +--- + +**최종 업데이트**: 2026-03-09 From 03ccd7ba936a61fc9151ab17ab5e3fe5c01ffe18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Mar 2026 21:02:50 +0900 Subject: [PATCH 04/15] =?UTF-8?q?docs:=20[database]=20codebridge=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=AC=B8=EC=84=9C=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Equipment 하위 4개 테이블 추가 (55→59개) - 개발 서버 samdb에서 59개 테이블 삭제 완료 반영 - 테이블명 불일치 수정 (api_bookmarks→admin_api_bookmarks 등) - 운영 서버 적용 절차 4단계로 구체화 --- .../system/database/codebridge-separation.md | 123 ++++++++++++------ 1 file changed, 85 insertions(+), 38 deletions(-) diff --git a/sam/docs/system/database/codebridge-separation.md b/sam/docs/system/database/codebridge-separation.md index 159e562..74a0874 100644 --- a/sam/docs/system/database/codebridge-separation.md +++ b/sam/docs/system/database/codebridge-separation.md @@ -2,7 +2,7 @@ > **작성일**: 2026-03-07 > **상태**: 로컬/개발 서버 적용 완료, 운영 서버 미적용 -> **최종 수정**: 2026-03-09 — API 사용 테이블 점검 완료, codebridge 이동 대상 100개 → 55개로 축소 +> **최종 수정**: 2026-03-09 — API 사용 테이블 점검 완료, codebridge 이동 대상 100개 → 59개로 축소, 개발 서버 samdb에서 59개 테이블 삭제 완료 --- @@ -17,9 +17,10 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 ### 1.2 핵심 원칙 -- 기존 samdb의 테이블은 **삭제하지 않는다** (복사만 수행) +- codebridge DB에 테이블을 복사한 후, samdb에서 해당 테이블을 **삭제**하여 실질적 분리 - MNG 모델에 `$connection = 'codebridge'`를 설정하여 읽기 대상 DB만 변경 - React/API 서비스에는 **영향 없음** +- 개발 서버: samdb에서 59개 테이블 삭제 완료 (백업: `/home/pro/backup/sam_backup_20260309.sql.gz`) ### 1.3 분리 기준 @@ -34,9 +35,10 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 --- -## 2. codebridge 테이블 목록 (55개) +## 2. codebridge 테이블 목록 (59개) > 2026-03-09 점검: API 프로젝트 전체 코드 조사를 통해 API에서 사용하는 22개 테이블을 제외함. 제외된 테이블은 [3절](#3-api-사용-테이블--samdb-유지-필수-22개) 참조. +> Equipment 하위 테이블 4개 추가 (FK 의존성으로 equipments와 동일 DB 필수). ### Admin (9) @@ -54,13 +56,13 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 ### DevTools (5) -| 테이블 | 설명 | -|--------|------| -| `api_bookmarks` | API 북마크 | -| `api_deprecations` | API 지원종료 관리 | -| `api_environments` | API 환경 설정 | -| `api_histories` | API 호출 이력 | -| `api_templates` | API 템플릿 | +| 테이블 | 설명 | 비고 | +|--------|------|------| +| `admin_api_bookmarks` | API 북마크 | 문서 기존명 `api_bookmarks` → 실제 테이블명 | +| `api_deprecations` | API 지원종료 관리 | | +| `api_environments` | API 환경 설정 | | +| `api_histories` | API 호출 이력 | | +| `api_templates` | API 템플릿 | | ### Sales (17) @@ -107,12 +109,18 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 > esign_contracts, esign_audit_logs, esign_sign_fields, esign_signers는 API에서 전자계약 기능으로 사용 중 → samdb 유지 -### Equipment (2) +### Equipment (6) | 테이블 | 설명 | |--------|------| | `equipments` | 설비 | | `equipment_processes` | 설비 공정 | +| `equipment_inspections` | 설비 점검 (FK → equipments) | +| `equipment_inspection_details` | 설비 점검 상세 (FK → equipment_inspections) | +| `equipment_inspection_templates` | 설비 점검 템플릿 (FK → equipments) | +| `equipment_repairs` | 설비 수리 (FK → equipments) | + +> Equipment 하위 4개 테이블은 `equipments`에 FK 의존하므로 반드시 동일 DB에 있어야 한다. ### HR (1) @@ -132,17 +140,17 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 ### 기타 (9) -| 테이블 | 설명 | -|--------|------| -| `biz_certs` | 사업자등록증 | -| `cm_songs` | R&D 곡 관리 | -| `construction_site_photos` | 시공 현장 사진 | -| `construction_site_photo_rows` | 시공 사진 행 | -| `meeting_logs` | 회의 로그 | -| `meeting_minutes` | 회의록 | -| `meeting_minute_segments` | 회의록 세그먼트 | -| `interview_knowledges` | 면접 지식 | -| `sales_records` | 매출 기록 | +| 테이블 | 설명 | 비고 | +|--------|------|------| +| `biz_cert` | 사업자등록증 | 문서 기존명 `biz_certs` → 실제 테이블명 (단수) | +| `cm_songs` | R&D 곡 관리 | | +| `construction_site_photos` | 시공 현장 사진 | | +| `construction_site_photo_rows` | 시공 사진 행 | | +| `admin_meeting_logs` | 회의 로그 | 문서 기존명 `meeting_logs` → 실제 테이블명 | +| `meeting_minutes` | 회의록 | | +| `meeting_minute_segments` | 회의록 세그먼트 | | +| `interview_knowledges` | 면접 지식 | | +| `sales_records` | 매출 기록 | | --- @@ -212,15 +220,24 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 ### 4.1 환경별 상태 -| 환경 | codebridge DB | 테이블 복사 | .env 설정 | MNG 모델 적용 | 상태 | -|------|:---:|:---:|:---:|:---:|------| -| **로컬 Docker** | O | 55개 | O | O (develop) | 정상 작동 | -| **개발 서버** | O | 55개 | O | O (develop) | 정상 작동 | -| **운영 서버** | X | X | X | X (revert) | 미적용 (기존 samdb 사용) | +| 환경 | codebridge DB | 테이블 복사 | samdb 삭제 | .env 설정 | MNG 모델 적용 | 상태 | +|------|:---:|:---:|:---:|:---:|:---:|------| +| **로컬 Docker** | O | 59개 | 미수행 | O | O (develop) | 정상 작동 | +| **개발 서버** | O | 59개 | **59개 삭제 완료** | O | O (develop) | 정상 작동 | +| **운영 서버** | X | X | X | X | X (revert) | 미적용 (기존 samdb 사용) | -> **2026-03-09 정리**: -> - `finance_*` 17개 + `barobill_companies` 1개: 마이그레이션 없이 수동 생성된 테이블 → 삭제 완료 -> - API 사용 테이블 22개: codebridge 이동 대상에서 제외 → samdb 유지 (MNG 모델의 `$connection = 'codebridge'` 제거 필요) +> **2026-03-09 작업 내역**: +> - API 사용 테이블 22개: codebridge 이동 대상에서 제외 → samdb 유지 +> - `finance_*` 17개 + `barobill_companies` 1개: codebridge에 없는 유령 테이블 → samdb에서만 삭제 +> - Equipment 하위 4개 테이블: FK 의존성으로 codebridge 이동 대상에 추가 (55→59개) +> - **개발 서버 samdb에서 59개 테이블 DROP 완료** (FOREIGN_KEY_CHECKS=0 사용) +> - 백업: `/home/pro/backup/sam_backup_20260309.sql.gz` (6.3MB) +> - 개발 서버 상태: samdb 276개 테이블, codebridge 101개 테이블 +> +> **테이블명 불일치 발견 (수정 완료)**: +> - `api_bookmarks` → 실제: `admin_api_bookmarks` +> - `meeting_logs` → 실제: `admin_meeting_logs` +> - `biz_certs` → 실제: `biz_cert` (단수형) ### 4.2 코드 변경 사항 @@ -244,7 +261,7 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 CODEBRIDGE_DB_DATABASE=codebridge ``` -**MNG 모델** — `$connection` 속성 추가 (codebridge 55개만): +**MNG 모델** — `$connection` 속성 추가 (codebridge 59개만): ```php class SalesPartner extends Model @@ -257,13 +274,33 @@ class SalesPartner extends Model > **주의**: API 사용 테이블 22개에 해당하는 MNG 모델은 `$connection = 'codebridge'`를 설정하지 않는다. 기본 samdb connection을 사용해야 API와 동일한 데이터를 참조한다. +### 4.3 samdb 테이블 삭제 절차 (개발 서버 완료) + +> Sales 테이블 그룹은 FK 상호 참조가 있어 `FOREIGN_KEY_CHECKS = 0`으로 일괄 삭제. + +```sql +-- FK 체크 비활성화 (Sales, Equipment 등 FK 체인 테이블) +SET FOREIGN_KEY_CHECKS = 0; + +-- 59개 테이블 DROP (codebridge에 복제 완료 확인 후) +DROP TABLE IF EXISTS admin_api_flows, admin_api_flow_runs, ...; + +SET FOREIGN_KEY_CHECKS = 1; +``` + +> **롤백**: 백업에서 특정 테이블만 복원 가능 +> ```bash +> gunzip < /home/pro/backup/sam_backup_20260309.sql.gz | mysql -u codebridge -p sam +> ``` + --- ## 5. 운영 서버 적용 절차 (미완료) > **전제**: 운영 서버 SSH 접근 + DB root 권한 필요 +> **코드는 이미 main에 배포 완료** — 운영 서버에 codebridge DB만 생성하면 즉시 적용됨 -### 순서 (반드시 1 → 2 → 3 순서로) +### 순서 (반드시 1 → 2 → 3 → 4 순서로) **1단계: codebridge DB 생성 + 테이블 복사** @@ -274,7 +311,7 @@ mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS codebridge CHARACTER SET utf8 # DB 계정 권한 부여 mysql -u root -p -e "GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; FLUSH PRIVILEGES;" -# sam에서 55개 테이블 구조+데이터 복사 (mysqldump → import) +# sam에서 59개 테이블 구조+데이터 복사 (mysqldump → import) mysqldump -u codebridge -p sam [테이블목록] | mysql -u codebridge -p codebridge ``` @@ -285,12 +322,22 @@ echo 'CODEBRIDGE_DB_DATABASE=codebridge' >> /home/webservice/mng/.env cd /home/webservice/mng && php artisan config:clear ``` -**3단계: 코드 배포 (main push)** +**3단계: MNG 웹 동작 확인** -MNG 코드(codebridge 55개 모델 `$connection` 변경)를 main에 cherry-pick + push. -Jenkins가 자동 배포. +codebridge connection이 활성화되므로 MNG 관리자 페이지에서 영업관리, 설비, 재무 등 주요 메뉴 동작 확인. -> **주의**: 3단계 전에 1, 2단계가 완료되어야 한다. DB가 없는 상태에서 코드를 배포하면 MNG 페이지 접속 불가. +**4단계: samdb에서 59개 테이블 삭제 (선택)** + +개발 서버에서 검증 완료 후, 운영 서버에서도 삭제하여 실질적 분리 완성. + +```sql +SET FOREIGN_KEY_CHECKS = 0; +DROP TABLE IF EXISTS [59개 테이블 목록]; +SET FOREIGN_KEY_CHECKS = 1; +``` + +> **주의**: 2단계 전에 1단계가 완료되어야 한다. DB가 없는 상태에서 .env를 설정하면 MNG 페이지 접속 불가. +> **코드 배포**: 이미 main에 59개 모델의 `$connection = 'codebridge'` 코드가 반영됨. .env에 `CODEBRIDGE_DB_DATABASE`가 없으면 기본값 'codebridge'가 사용되므로, DB가 존재하지 않으면 에러 발생. 반드시 1단계 먼저 완료할 것. --- @@ -313,7 +360,7 @@ Jenkins가 자동 배포. ┌─────┴─────┐ | | samdb codebridge - (공통 참조) (MNG 전용 55개) + (공통 참조) (MNG 전용 59개) ``` - **React → API → samdb**: 서비스 트래픽 (수주, 견적, 생산, 바로빌, 전자서명 등) From 1f7bd13816aec7be9ec4a345f75831092c6cf5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Mar 2026 21:32:47 +0900 Subject: [PATCH 05/15] =?UTF-8?q?docs:=20[database]=20codebridge=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=AC=B8=EC=84=9C=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영서버 revert 사유 및 교훈 기록 - 로컬 samdb 58개 삭제, 로컬/개발 265개 동기화 반영 - DevTools 테이블 실제 이름(admin_ prefix) 수정 - 운영서버 적용 절차 5단계로 개정 (DB 선행 필수) --- .../system/database/codebridge-separation.md | 118 ++++++++++++++---- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/sam/docs/system/database/codebridge-separation.md b/sam/docs/system/database/codebridge-separation.md index 74a0874..9022cbe 100644 --- a/sam/docs/system/database/codebridge-separation.md +++ b/sam/docs/system/database/codebridge-separation.md @@ -1,8 +1,8 @@ # codebridge DB 분리 > **작성일**: 2026-03-07 -> **상태**: 로컬/개발 서버 적용 완료, 운영 서버 미적용 -> **최종 수정**: 2026-03-09 — API 사용 테이블 점검 완료, codebridge 이동 대상 100개 → 59개로 축소, 개발 서버 samdb에서 59개 테이블 삭제 완료 +> **상태**: 로컬/개발 서버 적용 완료, **운영 서버 코드 revert 상태 — DB 선행 작업 필요** +> **최종 수정**: 2026-03-09 — API 사용 테이블 점검, 로컬/개발 samdb 삭제 완료, 운영 코드 revert --- @@ -58,11 +58,11 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 | 테이블 | 설명 | 비고 | |--------|------|------| -| `admin_api_bookmarks` | API 북마크 | 문서 기존명 `api_bookmarks` → 실제 테이블명 | -| `api_deprecations` | API 지원종료 관리 | | -| `api_environments` | API 환경 설정 | | -| `api_histories` | API 호출 이력 | | -| `api_templates` | API 템플릿 | | +| `admin_api_bookmarks` | API 북마크 | 기존명 `api_bookmarks` | +| `admin_api_deprecations` | API 지원종료 관리 | 기존명 `api_deprecations` | +| `admin_api_environments` | API 환경 설정 | 기존명 `api_environments` | +| `admin_api_histories` | API 호출 이력 | 기존명 `api_histories` | +| `admin_api_templates` | API 템플릿 | 기존명 `api_templates` | ### Sales (17) @@ -220,24 +220,32 @@ SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리 ### 4.1 환경별 상태 -| 환경 | codebridge DB | 테이블 복사 | samdb 삭제 | .env 설정 | MNG 모델 적용 | 상태 | +| 환경 | codebridge DB | 테이블 복사 | samdb 삭제 | .env 설정 | MNG 코드 | 상태 | |------|:---:|:---:|:---:|:---:|:---:|------| -| **로컬 Docker** | O | 59개 | 미수행 | O | O (develop) | 정상 작동 | -| **개발 서버** | O | 59개 | **59개 삭제 완료** | O | O (develop) | 정상 작동 | -| **운영 서버** | X | X | X | X | X (revert) | 미적용 (기존 samdb 사용) | +| **로컬 Docker** | O | 100개 | **58개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 | +| **개발 서버** | O | 101개 | **63개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 | +| **운영 서버** | **X** | **X** | **X** | **X** | **revert됨** | ⚠️ DB 선행 작업 후 코드 재배포 필요 | > **2026-03-09 작업 내역**: > - API 사용 테이블 22개: codebridge 이동 대상에서 제외 → samdb 유지 > - `finance_*` 17개 + `barobill_companies` 1개: codebridge에 없는 유령 테이블 → samdb에서만 삭제 > - Equipment 하위 4개 테이블: FK 의존성으로 codebridge 이동 대상에 추가 (55→59개) -> - **개발 서버 samdb에서 59개 테이블 DROP 완료** (FOREIGN_KEY_CHECKS=0 사용) +> - **개발 서버 samdb에서 63개 테이블 DROP 완료** (59개 + DevTools 실제 테이블명 4개 추가분) +> - **로컬 samdb에서 58개 테이블 DROP 완료** → 로컬/개발 265개로 동기화 +> - 로컬에 `quality_documents` 등 4개 테이블 구조 동기화 (개발서버에서 복사) > - 백업: `/home/pro/backup/sam_backup_20260309.sql.gz` (6.3MB) -> - 개발 서버 상태: samdb 276개 테이블, codebridge 101개 테이블 > > **테이블명 불일치 발견 (수정 완료)**: > - `api_bookmarks` → 실제: `admin_api_bookmarks` > - `meeting_logs` → 실제: `admin_meeting_logs` > - `biz_certs` → 실제: `biz_cert` (단수형) +> - DevTools 4개: `api_deprecations` → `admin_api_deprecations`, `api_environments` → `admin_api_environments`, `api_histories` → `admin_api_histories`, `api_templates` → `admin_api_templates` +> +> **운영 서버 revert 사유 (2026-03-09)**: +> - MNG main에 codebridge 코드 2건 cherry-pick → Jenkins 배포됨 (빌드 #456, #457) +> - 운영 서버에 codebridge DB가 없는 상태에서 코드 배포 → **59개 모델 사용 페이지 오류 발생 위험** +> - kent가 main에서 revert 2건 push → 운영 서버 정상 복구 +> - **교훈: 운영 서버는 반드시 DB 선행 작업(1~2단계) 완료 후 코드 배포(3단계)** ### 4.2 코드 변경 사항 @@ -298,11 +306,20 @@ SET FOREIGN_KEY_CHECKS = 1; ## 5. 운영 서버 적용 절차 (미완료) > **전제**: 운영 서버 SSH 접근 + DB root 권한 필요 -> **코드는 이미 main에 배포 완료** — 운영 서버에 codebridge DB만 생성하면 즉시 적용됨 +> **현재 상태**: 운영 서버 main 코드는 revert 상태 (codebridge 코드 없음). DB 작업 완료 후 코드 재배포 필요. +> **⚠️ 교훈**: 2026-03-09에 DB 없이 코드만 배포하여 장애 위험 발생 → **반드시 DB 선행 후 코드 배포** -### 순서 (반드시 1 → 2 → 3 → 4 순서로) +### 순서 (반드시 1 → 2 → 3 → 4 → 5 순서로) -**1단계: codebridge DB 생성 + 테이블 복사** +**1단계: 운영 sam DB 백업** + +```bash +# 운영 서버 접속 후 +mysqldump -u codebridge -p'[운영PW]' sam --single-transaction > ~/backup/sam_backup_$(date +%Y%m%d).sql +gzip ~/backup/sam_backup_$(date +%Y%m%d).sql +``` + +**2단계: codebridge DB 생성 + 59개 테이블 복사** ```bash # DB 생성 @@ -311,33 +328,80 @@ mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS codebridge CHARACTER SET utf8 # DB 계정 권한 부여 mysql -u root -p -e "GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; FLUSH PRIVILEGES;" -# sam에서 59개 테이블 구조+데이터 복사 (mysqldump → import) -mysqldump -u codebridge -p sam [테이블목록] | mysql -u codebridge -p codebridge +# sam에서 59개 테이블 구조+데이터 복사 +mysqldump -u codebridge -p sam \ + admin_api_flows admin_api_flow_runs \ + admin_pm_daily_logs admin_pm_daily_log_entries admin_pm_issues admin_pm_projects admin_pm_tasks \ + admin_roadmap_milestones admin_roadmap_plans \ + admin_api_bookmarks admin_api_deprecations admin_api_environments admin_api_histories admin_api_templates \ + sales_partners sales_managers sales_manager_documents sales_commissions sales_commission_details \ + sales_consultations sales_contract_products sales_products sales_product_categories \ + sales_prospects sales_prospect_consultations sales_prospect_products sales_prospect_scenarios \ + sales_records sales_scenario_checklists sales_tenant_managements tenant_prospects \ + condolence_expenses consulting_fees corporate_cards corporate_card_prepayments \ + customer_settlements daily_fund_memos daily_fund_transactions incomes vat_records \ + esign_field_templates esign_field_template_items \ + equipments equipment_process equipment_inspections equipment_inspection_details \ + equipment_inspection_templates equipment_repairs \ + business_income_payments ai_configs \ + biz_cert cm_songs construction_site_photos construction_site_photo_rows \ + admin_meeting_logs meeting_minutes meeting_minute_segments \ + interview_knowledge \ + | mysql -u codebridge -p codebridge ``` -**2단계: .env 설정** +**3단계: .env 설정** ```bash echo 'CODEBRIDGE_DB_DATABASE=codebridge' >> /home/webservice/mng/.env cd /home/webservice/mng && php artisan config:clear ``` -**3단계: MNG 웹 동작 확인** +**4단계: MNG 코드 재배포 (main cherry-pick)** -codebridge connection이 활성화되므로 MNG 관리자 페이지에서 영업관리, 설비, 재무 등 주요 메뉴 동작 확인. +> develop에 codebridge 코드가 있으므로, revert 커밋 이후 develop의 최신 커밋을 cherry-pick. +> 또는 develop의 해당 커밋을 다시 cherry-pick하여 main에 push. -**4단계: samdb에서 59개 테이블 삭제 (선택)** +```bash +# 로컬에서 실행 +cd /home/aweso/sam/mng +git checkout main && git pull origin main +git cherry-pick +git push origin main +git checkout develop +``` -개발 서버에서 검증 완료 후, 운영 서버에서도 삭제하여 실질적 분리 완성. +**5단계: 동작 확인 + samdb 테이블 삭제 (선택)** + +MNG 관리자 페이지에서 영업관리, 설비, 재무 등 주요 메뉴 동작 확인 후, 문제없으면 sam DB에서 59개 테이블 삭제. ```sql SET FOREIGN_KEY_CHECKS = 0; -DROP TABLE IF EXISTS [59개 테이블 목록]; +DROP TABLE IF EXISTS + admin_api_flows, admin_api_flow_runs, + admin_pm_daily_logs, admin_pm_daily_log_entries, admin_pm_issues, admin_pm_projects, admin_pm_tasks, + admin_roadmap_milestones, admin_roadmap_plans, + admin_api_bookmarks, admin_api_deprecations, admin_api_environments, admin_api_histories, admin_api_templates, + sales_partners, sales_managers, sales_manager_documents, sales_commissions, sales_commission_details, + sales_consultations, sales_contract_products, sales_products, sales_product_categories, + sales_prospects, sales_prospect_consultations, sales_prospect_products, sales_prospect_scenarios, + sales_records, sales_scenario_checklists, sales_tenant_managements, tenant_prospects, + condolence_expenses, consulting_fees, corporate_cards, corporate_card_prepayments, + customer_settlements, daily_fund_memos, daily_fund_transactions, incomes, vat_records, + esign_field_templates, esign_field_template_items, + equipments, equipment_process, equipment_inspections, equipment_inspection_details, + equipment_inspection_templates, equipment_repairs, + business_income_payments, ai_configs, + biz_cert, cm_songs, construction_site_photos, construction_site_photo_rows, + admin_meeting_logs, meeting_minutes, meeting_minute_segments, + interview_knowledge; SET FOREIGN_KEY_CHECKS = 1; ``` -> **주의**: 2단계 전에 1단계가 완료되어야 한다. DB가 없는 상태에서 .env를 설정하면 MNG 페이지 접속 불가. -> **코드 배포**: 이미 main에 59개 모델의 `$connection = 'codebridge'` 코드가 반영됨. .env에 `CODEBRIDGE_DB_DATABASE`가 없으면 기본값 'codebridge'가 사용되므로, DB가 존재하지 않으면 에러 발생. 반드시 1단계 먼저 완료할 것. +> **⚠️ 핵심 주의사항**: +> - 반드시 **1→2→3→4** 순서 (DB 먼저, 코드 나중) +> - 4단계(코드 배포) 전에 3단계(.env)까지 완료되어야 함 +> - 5단계(samdb 삭제)는 4단계 동작 확인 후 선택적 수행 --- @@ -376,4 +440,4 @@ SET FOREIGN_KEY_CHECKS = 1; --- -**최종 업데이트**: 2026-03-09 +**최종 업데이트**: 2026-03-09 (운영 revert 반영, 적용 절차 5단계로 개정) From e94123ad4910ea14dc2c77c80e3f7b848c1238ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Mar 2026 22:29:07 +0900 Subject: [PATCH 06/15] =?UTF-8?q?docs:=20[rd,=20approvals]=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=EB=AC=B8=EC=84=9C=202=EA=B1=B4=20=EB=B3=B5?= =?UTF-8?q?=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - features/rd/sound-logo-studio.md (사운드 로고 스튜디오) - dev/changes/20260306_purchase_request_payment_method.md (품의서 지급방법) --- ...0260306_purchase_request_payment_method.md | 69 +++++ sam/docs/features/rd/sound-logo-studio.md | 244 ++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 sam/docs/dev/changes/20260306_purchase_request_payment_method.md create mode 100644 sam/docs/features/rd/sound-logo-studio.md diff --git a/sam/docs/dev/changes/20260306_purchase_request_payment_method.md b/sam/docs/dev/changes/20260306_purchase_request_payment_method.md new file mode 100644 index 0000000..67308f5 --- /dev/null +++ b/sam/docs/dev/changes/20260306_purchase_request_payment_method.md @@ -0,0 +1,69 @@ +# 품의서 지급방법 UI 개선 + +**날짜:** 2026-03-06 +**작업자:** Claude Code + +## 변경 개요 + +품의서 2종(구매품의서, 비용정산품의서)에 지급방법 선택 기능을 추가/개선하였다. + +## 변경 내용 + +### 1. 구매품의서 (pr_purchase) - 지급방법 추가 + +- 납품 정보(납품업체/납품예정일/납품장소) 아래에 **지급방법 radio** 추가 +- 옵션: `법인카드` / `계좌이체` +- **일괄 선택** 방식 (전체 구매건에 하나의 지급방법) + +### 2. 비용정산품의서 (pr_settlement) - 지급방법 행별 변경 + +- 기존: 테이블 아래에 일괄 radio (법인카드/개인선지출) +- 변경: 각 내역행에 **지급방법 select** 컬럼 추가 +- 테이블 하단에 **지급방법별 합계표** 추가 (법인카드 합계 / 개인선지출 합계) +- 이유: 하나의 정산서에 법인카드/개인선지출 내역이 혼재할 수 있음 + +### 3. 지출품의서 (pr_expense) - 라벨 변경 + +- `사용일자` -> `지출일자` 라벨 변경 (폼 + 조회 화면) + +## 수정된 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `mng/resources/views/approvals/partials/_purchase-request-form.blade.php` | 구매품의서 지급방법 radio 추가, 비용정산품의서 행별 select + 합계표, 지출일자 라벨 변경 | +| `mng/resources/views/approvals/partials/_purchase-request-show.blade.php` | 구매품의서/비용정산품의서 조회 화면 동기화 | + +## Alpine.js 데이터 변경 + +### 구매품의서 + +```javascript +// formData에 추가 +payment_method: initialData?.payment_method || '', + +// getFormData()에 포함 +{ ...base, ..., payment_method: this.formData.payment_method } +``` + +### 비용정산품의서 + +```javascript +// makeItem()에 추가 +payment_method: data?.payment_method || '', + +// computed 속성 추가 +get corporateCardTotal() { /* corporate_card 행만 합산 */ }, +get personalAdvanceTotal() { /* personal_advance 행만 합산 */ }, + +// getFormData() 변경 +// 기존: payment_method: this.formData.payment_method (일괄) +// 변경: 각 item.payment_method (행별) + corporate_card_total, personal_advance_total +``` + +## 관련 문서 + +- [결재 양식 기술 명세](../features/approvals/form-types.md) - 섹션 12, 14 업데이트 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/rd/sound-logo-studio.md b/sam/docs/features/rd/sound-logo-studio.md new file mode 100644 index 0000000..d21ed27 --- /dev/null +++ b/sam/docs/features/rd/sound-logo-studio.md @@ -0,0 +1,244 @@ +# 사운드 로고 스튜디오 + +> **작성일**: 2026-03-08 +> **상태**: 운영 중 +> **라우트**: `/rd/sound-logo` +> **뷰**: `resources/views/rd/sound-logo/index.blade.php` + +--- + +## 1. 개요 + +사운드 로고 스튜디오는 기업 시그니처 사운드(사운드 로고)를 제작하는 올인원 도구이다. Web Audio API 기반 시퀀서, Gemini AI 어시스트, TTS 음성 오버레이, Lyria RealTime BGM 생성을 하나의 SPA에서 통합 제공한다. + +**핵심 기능:** +- 시퀀서 기반 사운드 로고 수동/프리셋 제작 +- Gemini AI가 프롬프트 기반으로 음표 시퀀스 자동 설계 +- Gemini TTS로 나레이션 음성 생성 (여성/남성/아이, 30종 음성, 속도 조절) +- Lyria RealTime WebSocket으로 AI 배경음악 실시간 생성 +- 시퀀서/BGM 상호 배타적 재생 + TTS 공통 합성 +- WAV 내보내기 + +--- + +## 2. 아키텍처 + +### 2.1 3레이어 오디오 구조 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Layer 1: 사운드 로고 (시퀀서 또는 AI BGM — 상호 배타적) │ +│ ├── A) 시퀀서 (수동 편집 / 프리셋 / AI 생성) │ +│ └── B) AI 배경음악 (Lyria RealTime) │ +├─────────────────────────────────────────────────────────┤ +│ Layer 2: 음성 TTS (Gemini) ─── 양쪽 모드 공통 │ +├─────────────────────────────────────────────────────────┤ +│ DynamicsCompressor (클리핑 방지) → AudioContext.dest │ +└─────────────────────────────────────────────────────────┘ +``` + +- **시퀀서 모드**: 수동/프리셋/AI 생성 음표 + TTS 합성. BGM 제외. +- **BGM 모드**: AI 배경음악 + TTS 합성. 시퀀서 음표 제외. +- 판단 기준: `bgmBuffer` 존재 여부 + +### 2.2 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 프론트엔드 | Blade + Alpine.js | 단일 SPA | +| 오디오 엔진 | Web Audio API | OscillatorNode, BufferSourceNode, DynamicsCompressorNode | +| AI 시퀀서 | Gemini 2.5 Flash | 프롬프트 → JSON 음표 시퀀스 | +| TTS | `gemini-2.5-flash-preview-tts` | 30종 음성, Director's Note 스타일 제어 | +| BGM | Lyria RealTime (WebSocket) | `models/lyria-realtime-exp`, 48kHz 스테레오 PCM | +| 저장 | 없음 (클라이언트 전용) | WAV 내보내기로 결과물 보존 | + +--- + +## 3. 시퀀서 (Layer 1-A) + +### 3.1 입력 모드 + +| 모드 | 설명 | +|------|------| +| **수동** | 음표 그리드에서 직접 추가/삭제/편집 | +| **프리셋** | 사전 정의된 사운드 패턴 선택 (기업 시그널, 알림음 등) | +| **AI 생성** | 프롬프트 입력 → Gemini가 음표 시퀀스 JSON 반환 | + +### 3.2 음표 데이터 구조 + +```json +{ + "type": "note | chord | rest", + "note": "C5", + "chord": ["C4", "E4", "G4"], + "duration": 0.2, + "velocity": 0.8 +} +``` + +### 3.3 신디사이저 설정 + +| 파라미터 | 범위 | 설명 | +|---------|------|------| +| `synth` | sine, triangle, square, sawtooth | 파형 | +| `volume` | 0~1 | 마스터 볼륨 | +| `adsr.attack` | 1~500ms | 어택 | +| `adsr.decay` | 10~1000ms | 디케이 | +| `adsr.sustain` | 0~1.0 | 서스테인 | +| `adsr.release` | 10~3000ms | 릴리즈 | +| `reverb` | 0~1 | 리버브 양 | + +--- + +## 4. 음성 오버레이 — TTS (Layer 2) + +### 4.1 음성 카테고리 + +| 카테고리 | 음성 수 | 주요 음성 | +|---------|---------|----------| +| **여성** | 9종 | Kore(단정), Aoede(산뜻), Leda(따뜻), Zephyr(차분) 등 | +| **남성** | 9종 | Puck(밝음), Charon(깊음), Orus(안정), Fenrir(무게) 등 | +| **아이** | 5종 | Leda 기반(여아), Puck 기반(남아) 등 | + +### 4.2 속도 조절 + +| 단계 | Director's Note | +|------|----------------| +| 1 (매우 느림) | "아주 천천히 또박또박 말해주세요." | +| 2 (느림) | "조금 느린 속도로 말해주세요." | +| 3 (보통) | (지시문 없음) | +| 4 (빠름) | "조금 빠른 속도로 말해주세요." | +| 5 (매우 빠름) | "아주 빠른 속도로 말해주세요." | + +### 4.3 오디오 형식 + +- 출력: `audio/L16;rate=24000` (16-bit PCM, 24kHz, 모노, little-endian) +- 디코딩: `DataView.getInt16(offset, true)` → Float32 변환 + +--- + +## 5. AI 배경음악 — Lyria (Layer 1-B) + +### 5.1 WebSocket 프로토콜 + +``` +브라우저 ──WebSocket──→ Google Lyria RealTime API + (wss://generativelanguage.googleapis.com/ws/...BidiGenerateMusic) +``` + +서버는 API 키만 전달(`GET /lyria-config`), 실제 WebSocket 통신은 브라우저에서 직접 수행한다. + +### 5.2 메시지 흐름 + +``` +1. setup → { setup: { model: "models/lyria-realtime-exp" } } +2. (수신) ← { setupComplete: {} } +3. 프롬프트 → { clientContent: { weightedPrompts: [...] } } +4. 설정 → { musicGenerationConfig: { bpm, density, brightness, scale, temperature } } +5. 재생 → { playbackControl: "PLAY" } +6. (수신 반복) ← { serverContent: { audioChunks: [{ data: "base64..." }] } } +7. 정지 → { playbackControl: "STOP" } +8. (종료) ← WebSocket close +``` + +### 5.3 BGM 파라미터 + +| 파라미터 | 범위 | 설명 | +|---------|------|------| +| `bgmPrompt` | 텍스트 | 음악 분위기 설명 | +| `bgmBpm` | 60~180 | BPM | +| `bgmDensity` | 0~100 | 밀도 (0~1 변환) | +| `bgmBrightness` | 0~100 | 밝기 (0~1 변환) | +| `bgmScale` | C_MAJOR 등 | 음계 | +| `bgmDuration` | 5~60초 | 생성 길이 | + +### 5.4 오디오 형식 + +- 출력: 16-bit PCM, 48kHz, 스테레오, little-endian +- WAV 헤더 감지 시 `decodeAudioData` fallback + +--- + +## 6. API 엔드포인트 + +### 6.1 사운드 로고 (RdController) + +| HTTP | URI | 메서드 | 설명 | +|------|-----|--------|------| +| GET | `/rd/sound-logo` | `soundLogo()` | 스튜디오 페이지 | +| POST | `/rd/sound-logo/generate` | `soundLogoGenerate()` | AI 음표 시퀀스 생성 (Gemini) | +| POST | `/rd/sound-logo/tts` | `soundLogoTts()` | TTS 음성 생성 (Gemini TTS) | +| GET | `/rd/sound-logo/lyria-config` | `soundLogoLyriaConfig()` | Lyria WebSocket 접속 설정 반환 | + +### 6.2 CM송/나레이션 (CmSongController) + +| HTTP | URI | 메서드 | 설명 | +|------|-----|--------|------| +| GET | `/rd/cm-song` | `index()` | 나레이션 목록 | +| GET | `/rd/cm-song/create` | `create()` | 나레이션 제작 | +| POST | `/rd/cm-song` | `store()` | 나레이션 저장 | +| GET | `/rd/cm-song/{id}` | `show()` | 나레이션 상세 | +| DELETE | `/rd/cm-song/{id}` | `destroy()` | 나레이션 삭제 | +| GET | `/rd/cm-song/{id}/download` | `download()` | 음성 파일 다운로드 | +| POST | `/rd/cm-song/generate-lyrics` | `generateLyrics()` | AI 가사 생성 (Gemini) | +| POST | `/rd/cm-song/generate-audio` | `generateAudio()` | TTS 음성 생성 | + +### 6.3 CM송 데이터 모델 + +| 모델 | 테이블 | 설명 | +|------|--------|------| +| `CmSong` | `cm_songs` | 나레이션 (회사명, 업종, 가사, 음성파일) | + +**저장 경로**: `tenant` 디스크 → `cm-songs/{tenant_id}/{filename}.wav` + +--- + +## 7. 오디오 엔진 상세 + +### 7.1 마스터 출력 체인 + +``` +각 소스 (Oscillator / BufferSource) + → GainNode (개별 볼륨) + → DynamicsCompressorNode (마스터 리미터) + → AudioContext.destination +``` + +**컴프레서 설정:** +- threshold: -6dB +- knee: 10dB +- ratio: 12:1 +- attack: 3ms +- release: 150ms + +### 7.2 WAV 내보내기 + +`OfflineAudioContext`로 오프라인 렌더링 후 `bufferToWav()` 변환. +- 샘플레이트: 44,100Hz +- 채널: 2 (스테레오) +- 비트: 16-bit PCM +- 오프라인 컨텍스트에도 동일한 DynamicsCompressor 적용 + +--- + +## 8. 관련 파일 + +| 파일 | 설명 | +|------|------| +| `resources/views/rd/sound-logo/index.blade.php` | SPA 뷰 (Alpine.js, ~2100줄) | +| `app/Http/Controllers/RdController.php` | 사운드 로고 API (4 메서드) | +| `app/Http/Controllers/Rd/CmSongController.php` | CM송/나레이션 CRUD (8 메서드) | +| `app/Models/Rd/CmSong.php` | CM송 모델 | +| `app/Helpers/AiTokenHelper.php` | Gemini 토큰 사용량 추적 | + +--- + +## 관련 문서 + +- [R&D 메뉴 개요](README.md) — R&D 전체 메뉴 구조 +- [AI 분석 리포트](../ai/README.md) — Gemini API 활용 패턴 참고 +- [사운드 로고 생성기 기획서](../../plans/sound-logo-generator-plan.md) — 초기 기획 + +--- + +**최종 업데이트**: 2026-03-08 From 85dc30bfcd58a285305eb4ef6128ff6620a62dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Mar 2026 13:22:53 +0900 Subject: [PATCH 07/15] =?UTF-8?q?docs:=20[infra]=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(ops-manual=20=EA=B8=B0=EC=A4=80=20=EC=A0=95=EB=A0=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server-access-management.md: sam-cicd IP 정정 (114.203.209.83 → 110.10.147.46), sam-dev 추가, DB 계정/백업 경로 갱신, 리플리케이션 섹션 제거 - CLAUDE.md: dev 서버에서 Jenkins 제거 (Jenkins는 cicd 서버), MySQL 8.0 → 8.4, Next.js 포트 수정 --- CLAUDE.md | 15 ++-- system/server-access-management.md | 110 ++++++++--------------------- 2 files changed, 36 insertions(+), 89 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 123e5e9..603aea0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -301,10 +301,10 @@ Claude가 **절대 직접 실행하지 않으며**, 사용자에게 명령어를 | 서버 | 호스트 | 계정 | SSH 접근 | 역할 | |------|--------|------|---------|------| -| 개발 서버 | `114.203.209.83` | `pro`, `hskwon` | **Claude 가능** | 개발/스테이징 + Jenkins CI/CD + Gitea | +| 개발 서버 (sam-dev) | `114.203.209.83` | `pro`, `hskwon` | **Claude 가능** | 개발/스테이징 + Gitea | | 운영 서버 | (비공개) | 별도 계정 | **Claude 불가** — 개발팀장만 접근 | 정식 서비스 | -> **참고**: Jenkins(`114.203.209.83:8080`)와 Gitea(`114.203.209.83:3000`)는 개발 서버에서 운영한다. +> **참고**: Jenkins는 CI/CD 서버(`110.10.147.46:8080`, ci.sam.it.kr)에서 운영한다. Gitea는 개발 서버(`114.203.209.83:3000`)와 CI/CD 서버(`110.10.147.46:3000`, git.sam.it.kr) 양쪽에 있다. > **운영 서버 정책**: Claude는 운영 서버에 SSH 접속할 수 없다. IP 접근이 제한되어 있으며 개발팀장만 접근 가능하다. 운영 배포는 `git push origin main` → Jenkins 자동 배포로만 이루어지며, 운영 서버 상태 확인이 필요하면 사용자(개발팀장)에게 요청한다. @@ -541,11 +541,11 @@ ssh <운영계정>@<운영서버IP> "cd /home/webservice/mng && php artisan tink |------|-----------|----------|----------| | **구성 방식** | Docker 컨테이너 | Bare-metal (네이티브) | Bare-metal (네이티브) | | **PHP** | 컨테이너 내부 (8.4) | 직접 설치 (8.4) | 직접 설치 (8.4) | -| **MySQL** | 컨테이너 (sam-mysql-1) | 직접 설치 (8.0) | 직접 설치 (8.0) | +| **MySQL** | 컨테이너 (sam-mysql-1) | 직접 설치 (8.4) | 직접 설치 (8.4) | | **Nginx** | 컨테이너 (sam-nginx-1) | 직접 설치 | 직접 설치 | | **명령 실행** | `docker exec` 필요 | 직접 실행 | 직접 실행 | | **서버 IP** | localhost | `114.203.209.83` | (신규, 미확정) | -| **추가 서비스** | — | Jenkins, Gitea | — | +| **추가 서비스** | — | Gitea | — | | **DB** | `samdb` | `samdb` | `sam_prod` | > **배경**: 서버는 Docker가 무거워서 PHP, Nginx, MySQL 등을 네이티브로 설치하여 운영한다. @@ -566,7 +566,7 @@ PHP, Laravel, Node.js 등이 **Docker 컨테이너 안에** 설치되어 있다. ### 서버 환경 (Bare-metal — 개발/운영 동일 구조) -서버에는 Docker가 없다. PHP 8.4, Nginx, MySQL 8.0이 직접 설치되어 있다. +서버에는 Docker가 없다. PHP 8.4, Nginx, MySQL 8.4이 직접 설치되어 있다. ``` 개발 서버 (114.203.209.83) 운영 서버 (신규) @@ -575,13 +575,12 @@ PHP, Laravel, Node.js 등이 **Docker 컨테이너 안에** 설치되어 있다. │ ├── api.sock │ ├── api.sock │ ├── mng.sock │ ├── mng.sock │ └── sales.sock │ └── sales.sock -├── MySQL 8.0 (samdb) ├── MySQL 8.0 (sam_prod) +├── MySQL 8.4 (samdb) ├── MySQL 8.4 (sam_prod) ├── Supervisor ├── Supervisor │ ├── sam-api-worker (x1) │ ├── sam-api-worker (x1) │ ├── sam-mng-worker (x2) │ ├── sam-mng-worker (x2) │ └── sam-api-scheduler │ └── sam-api-scheduler -├── Node.js (React SSR :3000) ├── Node.js (React SSR :3000) -├── Jenkins (:8080) │ +├── Node.js (React SSR :3001) ├── Node.js (React SSR :3000) ├── Gitea (:3000) │ ├── /home/webservice/api ├── /home/webservice/api ├── /home/webservice/mng ├── /home/webservice/mng diff --git a/system/server-access-management.md b/system/server-access-management.md index 67da35a..b32b531 100644 --- a/system/server-access-management.md +++ b/system/server-access-management.md @@ -5,7 +5,7 @@ SAM 시스템의 서버 및 데이터베이스 접근 권한을 관리합니다. 서버 관리자는 1명이며, 외부 인원에게는 임시로 접근을 허용하고 작업 완료 후 차단합니다. -**최종 업데이트:** 2026-03-07 +**최종 업데이트:** 2026-03-09 --- @@ -22,19 +22,21 @@ SAM 시스템의 서버 및 데이터베이스 접근 권한을 관리합니다. | 구분 | 호스트 | SSH alias | 용도 | |------|--------|-----------|------| -| 운영 (prod) | 211.117.60.189 | `sam-prod` | API + React 운영 배포 | -| 개발 (dev) | - | `sam-dev` | 개발 환경 | -| CI/CD | 114.203.209.83 | `sam-cicd` | Jenkins + React 개발 배포 + DB 백업 | +| 운영 (prod) | 211.117.60.189 | `sam-prod` | API + React + MNG 운영 배포 | +| CI/CD | 110.10.147.46 | `sam-cicd` | Jenkins + Gitea + Grafana + Prometheus | +| 개발 (dev) | 114.203.209.83 | `sam-dev` | API + React + MNG 개발 환경 + Gitea + MySQL | + +> **참고:** 상세 서버 구성은 `docs/dev/deploys/ops-manual/01-server-overview.md` 참조 ### 서버 시간대 -| 설정 | prod | cicd | -|------|------|------| +| 설정 | prod | dev | +|------|------|-----| | OS timezone | Asia/Seoul (KST, +0900) | Asia/Seoul (KST, +0900) | | NTP 동기화 | 활성 | 활성 | -| PHP CLI (`/etc/php/8.4/cli/php.ini`) | Asia/Seoul | PHP 미설치 | -| PHP FPM (`/etc/php/8.4/fpm/php.ini`) | Asia/Seoul | - | -| Laravel `app.timezone` | Asia/Seoul | - | +| PHP CLI (`/etc/php/8.4/cli/php.ini`) | Asia/Seoul | Asia/Seoul | +| PHP FPM (`/etc/php/8.4/fpm/php.ini`) | Asia/Seoul | Asia/Seoul | +| Laravel `app.timezone` | Asia/Seoul | Asia/Seoul | > **주의:** PHP timezone이 UTC면 스케줄러 실행 시간이 9시간 어긋남. 반드시 Asia/Seoul로 설정할 것. @@ -54,7 +56,6 @@ SAM 시스템의 서버 및 데이터베이스 접근 권한을 관리합니다. | 계정 | 이름 | 용도 | 상태 | |------|------|------|------| | `hskwon` | 권혁성 | 서버 관리자 | 활성 | -| `pro` | 김보곤 | 서브 관리자 임시 접근용 | 평상시 **잠금** | ### sam-dev (개발서버) @@ -90,39 +91,6 @@ sudo passwd -S pro --- -## 공동 관리 구조 (cicd 서버) - -인력 변동에 대비하여 cicd 서버는 공동 관리 구조로 운영합니다. - -### develop 그룹 - -```bash -# 그룹 구성원 -$ getent group develop -develop:x:1004:hskwon,pro - -# 디렉토리 구조 -/data/ -├── backups/mysql/ # DB 백업 파일 (14일 보관) -└── scripts/ # 운영 스크립트 + .sam_backup.cnf -``` - -### 권한 설정 - -- `/data/` 전체: `develop` 그룹 소유, **setgid** 적용 -- setgid(`chmod g+s`)로 하위에 생성되는 파일/폴더도 자동으로 `develop` 그룹 상속 -- 그룹 권한 = 소유자 권한 (`chmod g=u`) -- 누가 작업하든 다른 관리자가 접근/수정 가능 - -```bash -# 권한 재설정 필요 시 -sudo chown -R hskwon:develop /data -sudo chmod -R g=u /data -sudo chmod g+s /data /data/backups /data/backups/mysql /data/scripts -``` - ---- - ## DB 계정 현황 ### sam-prod (운영서버 MySQL 8.4) @@ -130,17 +98,17 @@ sudo chmod g+s /data /data/backups /data/backups/mysql /data/scripts | 계정 | 인증 플러그인 | 접근 DB | 용도 | |------|-------------|---------|------| | `codebridge@localhost` | caching_sha2_password | sam, sam_stage, sam_stat, codebridge | 애플리케이션 (.env) | -| `hskwon@localhost` | caching_sha2_password | 전역 관리자 + sam | 서버 관리자 | -| `pro@localhost` | caching_sha2_password | sam, sam_stage, sam_stat, codebridge | 서브 관리자용 | +| `hskwon@localhost` | auth_socket | 전역 관리자 (WITH GRANT OPTION) | 서버 관리자 | +| `root@localhost` | auth_socket | 전역 | 시스템 (sudo mysql) | +| `sam_backup@110.10.147.46` | caching_sha2_password | SELECT, LOCK TABLES (sam, sam_stat) | CI/CD 백업 | ### sam-cicd (CI/CD 서버 MySQL 8.4) | 계정 | 인증 플러그인 | 접근 DB | 용도 | |------|-------------|---------|------| | `hskwon@localhost` | caching_sha2_password | 전역 관리자 | 서버 관리자 | -| `pro@localhost` | caching_sha2_password | sam, sam_stage, sam_stat, codebridge, gitea | 서브 관리자용 | -> **참고:** cicd 서버에는 `codebridge` DB 계정이 없음 (애플리케이션이 다른 계정 사용) +> **참고:** cicd 서버의 MySQL은 Gitea DB + 백업 저장 용도 ### sam-dev (개발서버 MySQL 8.4) @@ -184,41 +152,16 @@ FLUSH PRIVILEGES; --- -## DB 리플리케이션 - -운영 DB(prod)에서 cicd 서버로 실시간 리플리케이션이 구성되어 있습니다. - -| 항목 | 값 | -|------|---| -| Source (마스터) | sam-prod (211.117.60.189) | -| Replica (슬레이브) | sam-cicd | -| 리플리케이션 사용자 | `sam_backup` | -| 대상 DB | **sam, sam_stat, codebridge** | -| 지연 | 0초 (실시간) | - -```sql --- cicd에서 리플리케이션 상태 확인 -SHOW REPLICA STATUS\G - --- 확인 항목 --- Replica_IO_Running: Yes --- Replica_SQL_Running: Yes --- Seconds_Behind_Source: 0 -``` - ---- - ## DB 백업 -리플리케이션과 별도로, cicd 서버에서 매일 03:00에 mysqldump 백업을 수행합니다. +sam-cicd 서버에서 prod DB를 원격 mysqldump로 백업합니다. ### 백업 설정 | 항목 | 값 | |------|---| -| 스크립트 | `/data/scripts/backup-db.sh` | -| 인증 파일 | `/data/scripts/.sam_backup.cnf` | -| 백업 경로 | `/data/backups/mysql/` | +| 스크립트 | `/home/hskwon/scripts/backup-db.sh` | +| 백업 경로 | `/home/hskwon/backups/mysql/` | | 실행 시간 | 매일 03:00 (cron, hskwon 사용자) | | 보관 기간 | 14일 | @@ -234,11 +177,14 @@ SHOW REPLICA STATUS\G ### 백업 확인 ```bash +# cicd 서버에서 확인 +ssh sam-cicd + # 최근 백업 파일 확인 -ls -lt /data/backups/mysql/ | head -10 +ls -lt ~/backups/mysql/ | head -10 # 백업 로그 확인 -tail /data/backups/mysql/backup.log +tail ~/backups/mysql/backup.log ``` --- @@ -247,6 +193,8 @@ tail /data/backups/mysql/backup.log ### Jenkins 배포 흐름 +Jenkins는 sam-cicd(110.10.147.46)에서 운영됩니다. + | 프로젝트 | main | develop | |----------|------|---------| | **api** | Jenkins (Stage + Production) | post-update hook (Jenkins 미관여) | @@ -254,6 +202,7 @@ tail /data/backups/mysql/backup.log - 배포 SSH 계정: `hskwon` (deploy-ssh-key 크레덴셜) - Slack 알림: `#deploy_api`, `#deploy_react` +- Jenkins 웹 UI: https://ci.sam.it.kr --- @@ -263,20 +212,19 @@ tail /data/backups/mysql/backup.log 2. **임시 접근만 허용** -- 서브 관리자는 필요 시에만 OS/DB 계정 해제 3. **작업 완료 후 즉시 잠금** -- 해제 상태로 방치하지 않음 4. **DB 접근은 SSH 터널 경유** -- 외부에서 MySQL 직접 접근 불가 (localhost 바인딩) -5. **공동 관리 대비** -- cicd `/data/`는 develop 그룹으로 관리, 인력 변동 대비 -6. **이중 백업** -- 리플리케이션(실시간) + mysqldump(매일 03:00) 병행 +5. **이중 백업** -- cicd에서 mysqldump(매일 03:00) 수행 --- ## 참고 +- 서버 운영 매뉴얼: [../dev/deploys/ops-manual/README.md](../dev/deploys/ops-manual/README.md) - API 보안 가이드: [security-policy.md](./security-policy.md) - Docker 설정: [docker-setup.md](./docker-setup.md) -- 원격 근무 설정: [remote-work-setup.md](./remote-work-setup.md) - Jenkins 설정: [../dev/guides/jenkins-setup-guide.md](../dev/guides/jenkins-setup-guide.md) --- **작성일:** 2026-03-05 -**버전:** 1.1 +**버전:** 1.2 **담당자:** SAM Infrastructure Team \ No newline at end of file From 04e877dea35cf192ca936bd451d1b6b863d22aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Mar 2026 16:16:47 +0900 Subject: [PATCH 08/15] =?UTF-8?q?docs:=20[ops-manual]=20sam-dev=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=9C=A0=EC=A7=80=EB=B3=B4=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EC=B1=85=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 - 01-server-overview: sam-dev 서비스 현황 갱신 (Swap, PHP 5.6/Apache 비활성화, cron 정리) - 02-daily-operations: sam-dev 리소스 관리 섹션 추가 (Swap, Gitea 캐시, 비활성 서비스) - 06-database: sam-dev binlog 7일 보관 정책 추가 --- dev/deploys/ops-manual/01-server-overview.md | 26 ++++++--- dev/deploys/ops-manual/02-daily-operations.md | 53 +++++++++++++++++++ dev/deploys/ops-manual/06-database.md | 27 ++++++++++ 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/dev/deploys/ops-manual/01-server-overview.md b/dev/deploys/ops-manual/01-server-overview.md index 324c64a..eb6baf7 100644 --- a/dev/deploys/ops-manual/01-server-overview.md +++ b/dev/deploys/ops-manual/01-server-overview.md @@ -218,14 +218,24 @@ ### 서비스 현황 -| 서비스 | 포트 | 상태 | -|--------|------|------| -| Nginx | 80/443 | active | -| Apache | 8080 | active (레거시) | -| MySQL 8.4 | 3306 (localhost) | active | -| Gitea | 3000 | active | -| Next.js (PM2) | 3001 | active | -| fail2ban | - | active | +| 서비스 | 포트 | 상태 | 비고 | +|--------|------|------|------| +| Nginx | 80/443 | active | | +| PHP 8.4 FPM | unix socket | active | SAM 앱 (api, mng, sales) | +| PHP 7.3 FPM | unix socket | active | 5130.codebridge-x.com 레거시 전용 | +| MySQL 8.4 | 3306 (localhost) | active | binlog 7일 보관 | +| Gitea | 3000 | active | | +| Next.js (PM2) | 3001 | active | | +| fail2ban | - | active | | +| Swap | - | active | 4G (/swapfile, fstab 등록) | +| ~~Apache~~ | ~~8080~~ | **disabled** | 2026-03-09 비활성화 (front.5130.co.kr 미사용) | +| ~~PHP 5.6 FPM~~ | - | **disabled** | 2026-03-09 비활성화 (미사용) | + +### 자동 정리 (cron, hskwon) + +| 작업 | 주기 | 설명 | +|------|------|------| +| Gitea repo-archive 캐시 정리 | 매주 일요일 04:00 | 7일 이상 된 캐시 파일 삭제 + 빈 디렉토리 정리 | ### 방화벽 (UFW) 규칙 diff --git a/dev/deploys/ops-manual/02-daily-operations.md b/dev/deploys/ops-manual/02-daily-operations.md index 3c24a43..01a886f 100644 --- a/dev/deploys/ops-manual/02-daily-operations.md +++ b/dev/deploys/ops-manual/02-daily-operations.md @@ -48,6 +48,59 @@ chmod 640 /home/webservice/mng/shared/.env --- +## [개발] sam-dev 리소스 관리 + +sam-dev는 2 vCPU / 3.8GB RAM으로 여러 서비스가 공존하므로 리소스 관리가 중요하다. + +### 주요 리소스 소비자 + +| 프로세스 | 메모리 | CPU (상시) | 비고 | +|----------|--------|-----------|------| +| MySQL | ~1.2G (30%) | 2~5% | | +| Gitea | ~400M (10%) | 1.8% | | +| Next.js (PM2) | ~380M (9.5%) | 0.5% | | +| PHP 8.4 FPM | ~250M (변동) | 요청 시 높음 | max_children=5 | +| PHP 7.3 FPM | ~30M | idle | 5130 레거시 전용 | + +### Swap (4G, 2026-03-09 추가) + +Swap 없이 운영하면 메모리 부족 시 OOM Killer가 프로세스를 즉시 종료한다. + +```bash +# Swap 상태 확인 +swapon --show +free -m + +# /etc/fstab에 등록되어 있으므로 재부팅 후에도 유지됨 +# /swapfile none swap sw 0 0 +``` + +### Gitea repo-archive 캐시 + +Gitea는 Web UI에서 ZIP/TAR.GZ 다운로드 시 `/var/lib/gitea/data/repo-archive/`에 캐시를 생성한다. +기본 설정에서는 만료가 없어 무한 증가하므로, cron으로 7일 이상 된 캐시를 정리한다. + +```bash +# 캐시 크기 확인 +sudo du -sh /var/lib/gitea/data/repo-archive/ + +# 수동 정리 (긴급 시) +sudo rm -rf /var/lib/gitea/data/repo-archive/* + +# 자동 정리: hskwon crontab (매주 일요일 04:00) +# 0 4 * * 0 find /var/lib/gitea/data/repo-archive -type f -mtime +7 -delete +# 0 4 * * 0 find /var/lib/gitea/data/repo-archive -type d -empty -delete +``` + +### 비활성화된 서비스 (2026-03-09) + +| 서비스 | 사유 | 복원 명령 | +|--------|------|----------| +| PHP 5.6 FPM | 미사용 (아무 사이트도 참조 안 함) | `sudo systemctl enable --now php5.6-fpm` | +| Apache | front.5130.co.kr 미사용 | `sudo systemctl enable --now apache2` | + +--- + ## 시스템 리소스 모니터링 양쪽 서버 공통 명령어: diff --git a/dev/deploys/ops-manual/06-database.md b/dev/deploys/ops-manual/06-database.md index 148f917..c31a6da 100644 --- a/dev/deploys/ops-manual/06-database.md +++ b/dev/deploys/ops-manual/06-database.md @@ -92,6 +92,33 @@ gunzip -c /home/hskwon/backups/mysql/gitea_YYYYMMDD_HHMMSS.sql.gz | mysql gitea --- +## [개발] MySQL Binlog 관리 + +sam-dev에서는 리플리케이션/PITR을 사용하지 않으므로 binlog 보관을 최소화한다. + +**설정 파일:** `/etc/mysql/mysql.conf.d/mysqld.cnf` + +```ini +binlog_expire_logs_seconds = 604800 # 7일 보관 (2026-03-09 설정) +max_binlog_size = 100M +``` + +```bash +# binlog 상태 확인 +sudo ls -lh /var/lib/mysql/binlog.0* | wc -l # 파일 수 +sudo du -shc /var/lib/mysql/binlog.0* | tail -1 # 총 크기 + +# 수동 퍼지 (필요 시) +mysql -u pro -p -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 3 DAY);" + +# binlog 완전 비활성화가 필요하면 (권장하지 않음) +# mysqld.cnf에 skip-log-bin 추가 후 MySQL 재시작 +``` + +> **참고:** 운영서버(sam-prod)의 binlog는 CI/CD 백업에 활용되므로 별도 정책 적용. + +--- + ## Slow Query 분석 (운영) ```bash From bfcd6178ea28e9455d22f2b51e95c1ab4f17d60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Mar 2026 20:43:56 +0900 Subject: [PATCH 09/15] =?UTF-8?q?docs:=20[quality]=20=ED=92=88=EC=A7=88?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=AC=B8=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 - README.md: 전체 개요, 역할별 프로세스 플로우, 메뉴 구조, 데이터 구조, API, 스토리보드 참조 - inspection-management.md: 제품검사 관리 (15개 검사항목, 상태판정, 캘린더뷰, 요청서/성적서 양식) - performance-reports.md: 생산실적신고 (자동생성, 확정, 누락체크, 건기원 프로세스) - quality-certification-audit.md: 품질인정심사 (기준/매뉴얼 심사 + 로트 추적 심사) - INDEX.md에 품질관리 문서 등록 --- INDEX.md | 1 + features/quality-management/README.md | 362 ++++++++++++++++++ .../inspection-management.md | 317 +++++++++++++++ .../quality-management/performance-reports.md | 268 +++++++++++++ .../quality-certification-audit.md | 169 ++++++++ 5 files changed, 1117 insertions(+) create mode 100644 features/quality-management/README.md create mode 100644 features/quality-management/inspection-management.md create mode 100644 features/quality-management/performance-reports.md create mode 100644 features/quality-management/quality-certification-audit.md diff --git a/INDEX.md b/INDEX.md index 580176b..f3b933c 100644 --- a/INDEX.md +++ b/INDEX.md @@ -141,6 +141,7 @@ DB 도메인별: | [card-vehicle/README.md](features/card-vehicle/README.md) | 법인카드·차량 | | [settlement/README.md](features/settlement/README.md) | 정산 | | [barobill-kakaotalk/README.md](features/barobill-kakaotalk/README.md) | 바로빌 카카오톡 | +| [quality-management/README.md](features/quality-management/README.md) | 품질관리 (제품검사, 실적신고) | --- diff --git a/features/quality-management/README.md b/features/quality-management/README.md new file mode 100644 index 0000000..d2a5391 --- /dev/null +++ b/features/quality-management/README.md @@ -0,0 +1,362 @@ +# 품질관리 시스템 + +> **작성일**: 2026-03-09 +> **상태**: 운영 중 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM 품질관리 시스템은 **방화문 제품검사 → 실적신고 → 건기원 제출**의 전체 흐름을 자동화한다. +건축자재 품질관리 법규(건설기술진흥법)에 따른 제품검사 수행 및 분기별 실적신고를 관리한다. + +### 1.2 역할별 프로세스 플로우 + +> 스토리보드 슬라이드 3 기준 + +``` +매출거래처 견적/수주 담당자 생산 담당자 출고 담당자 품질 담당자 +─────────── ────────────── ────────── ────────── ────────── +주문 견적 작성 생산 부서 할당 출고 대기 제품검사 신청 +(전화/카톡/메일) │ │ │ (거래처 요청) + │ 수주 전환?──No──→ 재고 소요량 출고 완료 │ + │ │ Yes 충분? 검사원 일정 관리 +주문 확인 및 수주서 작성 │ │ +내역 확정 (견적서 선택) 원자재 투입 체크 검사원 현장 방문 + │ │ 및 검사 진행 + 생산지시 생성 공정 작업 진행 │ + │ │ 합격? ──No──→ + 재고 소요량 중간검사 │ Yes + 충분?──No──→ │ 실적 신고 관리 + │ 생산 완료 │ + (자재 담당자 품질 인증 심사 + 입고 등록 + 수입검사) + +* 분할 수주/생산/출고는 1차 이후 반복 가능 +``` + +### 1.3 핵심 흐름 (시스템) + +``` +품질관리서 생성 → 수주 연결 → 개소별 검사 → 검사완료 + ↓ + 실적신고 자동생성 (분기별) + ↓ + 필수정보 확인 → 확정 → 건기원 신고 + ↓ + 품질인정심사 + (기준/매뉴얼 + 로트추적) +``` + +### 1.4 메뉴 구조 + +| 메뉴 | URL | 설명 | 상태 | +|------|-----|------|------| +| 제품검사관리 | `/quality/inspections` | 품질관리서 목록/생성/상세 | 운영 중 | +| 실적신고관리 | `/quality/performance-reports` | 분기별 실적신고 관리 | 운영 중 | +| 품질인정심사 | `/quality/qms` | 기준/매뉴얼 심사 + 로트 추적 심사 | 개발 예정 | + +--- + +## 2. 제품검사 (QualityDocument) + +> 상세 문서: [inspection-management.md](./inspection-management.md) + +### 2.1 품질관리서 생성 + +- **채번**: `KD-QD-YYYYMM-0001` (자동) +- **필수 입력**: 현장명, 접수일, 검사자 +- **선택 입력**: 수주처(client_id), 관련자 정보(options JSON) + +### 2.2 수주 연결 + +- 품질관리서에 수주(Order)를 연결하면 **개소(Location)가 자동 생성** +- 개소 = 수주의 root node (층/부호 단위) +- 시공규격(post_width, post_height)은 발주규격과 다를 수 있음 + +### 2.3 검사 수행 + +각 개소에 대해 **15개 검사항목** + **제품 사진**을 입력: + +| 분류 | 검사항목 | 판정 | +|------|---------|------| +| 외관 | 가공, 봉제, 조립, 차연재, 하부마감 | pass/fail | +| 기능 | 모터, 소재 | pass/fail | +| 치수 | 가로, 세로, 가이드레일 간격, 하부마감 간격 | OK/NG | +| 시험 | 내화, 차연, 개폐, 충격 | pass/fail | + +### 2.4 상태 전이 + +``` +received (접수) → in_progress (검사 중) → completed (검사 완료) +``` + +**개소별 상태 자동 판정**: +- `pending`: 검사 데이터 없음 +- `in_progress`: 일부 항목 입력 또는 사진 미등록 +- `completed`: 15개 항목 전부 + 사진 등록 + +--- + +## 3. 생산실적신고 (PerformanceReport) + +> 상세 문서: [performance-reports.md](./performance-reports.md) + +### 3.1 자동 생성 + +- **트리거**: 품질관리서 검사완료(`complete()`) 시 +- **생성 기준**: 현재 연도 + 분기 (`year`, `quarter`) +- **초기 상태**: `unconfirmed` + +### 3.2 확정 프로세스 + +``` +unconfirmed (미확정) + ↓ confirm() — 필수정보 검증 통과 시 +confirmed (확정) + ↓ distribute() — 건기원 신고 시 (미구현) +reported (신고완료) +``` + +**확정 해제**: `confirmed` → `unconfirmed` (unconfirm) + +### 3.3 필수정보 검증 + +확정 전 4가지 섹션의 필수필드가 모두 입력되어야 함: + +| 섹션 | 필수필드 | +|------|---------| +| 건축공사장 | 현장명, 대지위치, 지번 | +| 자재유통업자 | 업체명, 주소, 대표자, 전화번호 | +| 공사시공자 | 업체명, 주소, 담당자, 연락처 | +| 공사감리자 | 사무소명, 주소, 담당자, 연락처 | + +### 3.4 누락체크 + +- 출고완료(배송 완료)된 수주 중 품질관리서가 미등록된 건 탐지 +- 별도 탭에서 조회 가능 + +### 3.5 건기원 실적신고 비즈니스 컨텍스트 + +**건기원(한국건설기술연구원)** 실적신고는 법적 의무: + +- **주기**: 분기별 (1~3월, 4~6월, 7~9월, 10~12월) +- **대상**: 해당 분기에 검사 완료된 모든 건 +- **내용**: 품질관리서 번호, LOT 번호, 현장 정보, 검사 결과 +- **제출처**: 건기원 온라인 시스템 (수기 입력 또는 엑셀 업로드) + +**SAM 시스템 역할**: +1. 제품검사 완료 시 실적신고 데이터 자동 수집 +2. 필수정보 누락 여부 사전 검증 +3. 확정 후 건기원 제출 양식으로 데이터 정리 +4. (향후) 건기원 시스템 연동 자동 배포 + +--- + +## 4. 데이터 구조 + +### 4.1 테이블 관계 + +``` +quality_documents (품질관리서) +├── quality_document_orders (수주 연결, M:N) +│ └── orders +├── quality_document_locations (개소별 검사) +│ ├── order_items (대표 품목) +│ └── documents (EAV 성적서) +└── performance_reports (실적신고, 1:1) +``` + +### 4.2 주요 테이블 + +| 테이블 | 설명 | 주요 컬럼 | +|--------|------|----------| +| `quality_documents` | 품질관리서 | quality_doc_number, status, client_id, options(JSON) | +| `quality_document_orders` | 품질-수주 연결 | quality_document_id, order_id | +| `quality_document_locations` | 개소별 검사 | inspection_data(JSON), inspection_status, post_width/height | +| `performance_reports` | 실적신고 | year, quarter, confirmation_status, confirmed_date | + +### 4.3 options JSON 구조 (quality_documents) + +```json +{ + "construction_site": { + "name": "현장명", + "land_location": "대지위치", + "lot_no": "지번" + }, + "material_distributor": { + "company": "업체명", + "address": "주소", + "ceo": "대표자", + "tel": "전화번호" + }, + "contractor": { + "company": "업체명", + "address": "주소", + "name": "담당자", + "phone": "연락처" + }, + "supervisor": { + "office": "사무소명", + "address": "주소", + "name": "담당자", + "phone": "연락처" + } +} +``` + +--- + +## 5. API 엔드포인트 + +### 5.1 제품검사 (`/api/v1/quality/documents`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/quality/documents` | 목록 (상태/날짜 필터) | +| GET | `/quality/documents/stats` | 상태별 통계 | +| GET | `/quality/documents/calendar` | 캘린더 스케줄 | +| GET | `/quality/documents/available-orders` | 미등록 수주 조회 | +| POST | `/quality/documents` | 생성 | +| GET | `/quality/documents/{id}` | 상세 | +| PUT | `/quality/documents/{id}` | 수정 | +| DELETE | `/quality/documents/{id}` | 삭제 | +| PATCH | `/quality/documents/{id}/complete` | 검사완료 (→ 실적신고 자동생성) | +| POST | `/quality/documents/{id}/orders` | 수주 연결 | +| DELETE | `/quality/documents/{id}/orders/{orderId}` | 수주 해제 | +| POST | `/quality/documents/{id}/locations/{locId}/inspect` | 개소별 검사 저장 | + +### 5.2 실적신고 (`/api/v1/quality/performance-reports`) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/quality/performance-reports` | 목록 (연도/분기/상태 필터) | +| GET | `/quality/performance-reports/stats` | 확정/미확정 통계 | +| GET | `/quality/performance-reports/missing` | 누락체크 | +| PATCH | `/quality/performance-reports/confirm` | 일괄 확정 | +| PATCH | `/quality/performance-reports/unconfirm` | 확정 해제 | +| PATCH | `/quality/performance-reports/memo` | 메모 일괄 업데이트 | + +--- + +## 6. 프론트엔드 구조 + +### 6.1 페이지 + +``` +react/src/app/[locale]/(protected)/quality/ +├── page.tsx # 대시보드 +├── inspections/ +│ ├── page.tsx # 검사 목록 +│ ├── new/page.tsx # 검사 생성 +│ └── [id]/page.tsx # 검사 상세/수정 +└── performance-reports/ + └── page.tsx # 실적신고 목록 +``` + +### 6.2 컴포넌트 + +``` +react/src/components/quality/ +├── InspectionManagement/ +│ ├── InspectionList.tsx # 검사 목록 +│ ├── InspectionCreate.tsx # 검사 생성 +│ ├── InspectionDetail.tsx # 검사 상세 (수정 포함) +│ ├── OrderSelectModal.tsx # 수주 선택 모달 +│ ├── ProductInspectionInputModal.tsx # 검사 입력 모달 +│ ├── actions.ts # Server Actions +│ ├── types.ts # TypeScript 타입 +│ └── documents/ # 요청서/성적서 문서 +└── PerformanceReportManagement/ + ├── PerformanceReportList.tsx # 실적신고 목록 (2탭) + ├── MemoModal.tsx # 메모 모달 + └── actions.ts # Server Actions +``` + +### 6.3 실적신고 화면 기능 + +**탭 1: 분기별 실적신고** +- 연도/분기 필터 +- 통계: 전체, 확정, 미확정, 총 개소 +- 테이블: 품질관리서번호, 작성일, 현장명, 수주처, 개소수, 필수정보 상태, 확정상태, 확정일, 메모 +- 액션: 선택 확정, 확정 해제, 메모 일괄 작성 + +**탭 2: 누락체크** +- 출고완료 수주 중 품질관리서 미등록 건 조회 +- 빠른 누락 확인으로 법규 준수 지원 + +--- + +## 7. 품질인정심사 (QMS) + +> 상세 문서: [quality-certification-audit.md](./quality-certification-audit.md) + +### 7.1 개요 + +품질 인정 심사 자료를 관리하는 기능. 두 가지 심사 영역으로 구성: + +| 심사 영역 | 설명 | 진행률 추적 | +|----------|------|-----------| +| 기준/매뉴얼 심사 | 품질 기준 문서 및 매뉴얼 점검표 체크 | 완료 항목 / 전체 항목 | +| 로트 추적 심사 | 품질관리서 → 수주코드 → 개소별 제품로트 → 관련 서류 추적 확인 | 확인 개소 / 전체 개소 | + +### 7.2 로트 추적 심사 구조 + +``` +품질관리서 목록 → 수주코드 목록 → 관련 서류 +(1단계 선택) (2단계 선택) (3단계 확인) +``` + +- 해당 분기 실적신고 확정 건 기준 +- 수입검사, 중간검사, 납품확인서, 출고증, 제품검사 성적서, 품질관리서 등 서류 연결 확인 + +### 7.3 구현 상태 + +**개발 예정** — 현재 페이지 구조만 존재 + +--- + +## 8. 미구현 기능 요약 + +| 기능 | 상태 | 설명 | +|------|------|------| +| 배포(distribute) API | 미구현 | 건기원 시스템 연동 자동 배포 | +| 확정건 엑셀 다운로드 | 미구현 | 확정된 실적 건기원 양식 엑셀 내보내기 | +| 품질인정심사 | 미구현 | 기준/매뉴얼 심사 + 로트 추적 심사 | + +--- + +## 9. 스토리보드 참조 + +> **출처**: `SAM_MES_경동기업_품질관리_Storyboard_D1.9_260224` + +| 슬라이드 | 화면 | 기능 영역 | +|---------|------|----------| +| 3 | 프로젝트 진행 플로우차트 | 전체 프로세스 | +| 5~6 | 제품검사 목록 + 캘린더 | 제품검사관리 | +| 7~9 | 제품검사 상세 | 제품검사관리 | +| 10 | 수주 선택 팝업 | 제품검사관리 | +| 11 | 제품검사 팝업 (검사 입력) | 제품검사관리 | +| 12~13 | 제품검사요청서 | 문서 출력 | +| 14~15 | 제품검사성적서 | 문서 출력 | +| 16 | 실적신고 목록 | 실적신고관리 | +| 17 | 메모 팝업 | 실적신고관리 | +| 18 | 누락체크 | 실적신고관리 | +| 19 | 품질인정심사 (기준/매뉴얼) | 품질인정심사 | +| 20 | 품질인정심사 (로트 추적) | 품질인정심사 | + +--- + +## 관련 문서 + +- [DB 스키마 — 생산/품질](../../system/database/production.md) +- [API 규칙](../../dev/standards/api-rules.md) +- [채번 규칙](../../rules/numbering-rules.md) + +--- + +**최종 업데이트**: 2026-03-09 diff --git a/features/quality-management/inspection-management.md b/features/quality-management/inspection-management.md new file mode 100644 index 0000000..f55c560 --- /dev/null +++ b/features/quality-management/inspection-management.md @@ -0,0 +1,317 @@ +# 제품검사 관리 (Inspection Management) + +> **작성일**: 2026-03-09 +> **상태**: 운영 중 +> **URL**: `/quality/inspections` + +--- + +## 1. 개요 + +### 1.1 목적 + +방화문 제품의 현장 출고 전 품질검사를 수행하고 검사성적서를 발행한다. +검사 완료 시 실적신고(PerformanceReport)를 자동 생성한다. + +### 1.2 품질관리서 구조 + +``` +품질관리서 (QualityDocument) +├── 기본정보: 채번, 현장명, 접수일, 검사자, 수주처 +├── 관련자 정보 (options JSON): +│ ├── 건축공사장 정보 +│ ├── 자재유통업자 정보 +│ ├── 공사시공자 정보 +│ └── 공사감리자 정보 +├── 수주 연결 (QualityDocumentOrder, 1:N) +│ └── 개소 (QualityDocumentLocation, 1:N) +│ ├── 시공규격 (post_width, post_height) +│ ├── 검사 데이터 (inspection_data JSON) +│ └── 검사성적서 (Document EAV) +└── 실적신고 (PerformanceReport, 1:1) +``` + +### 1.3 화면 구성 + +**목록 페이지** (슬라이드 5~6): +- 상단: 날짜 필터 (전체/전전월/전월/금월/어제/오늘) + 검색 +- 통계 카드: 접수, 진행중, 완료 건수 +- 테이블: 품질관리서번호, 번호, 수주처, 개소, 실적신고 필수정보, 검사시기, 검사자, 상태, 작성자, 접수일 +- 하단: **캘린더 스케줄** (월간 뷰) + +**캘린더 뷰**: +- 월 단위 표시, 상태 필터(전체/진행중/완료) +- 색상 구분: 완료(초록 배지), 진행중(파란 바) +- 검사 완료 건: `홍길동 - 현장명 / 완료` 형태 +- 검사 진행 건: `홍길동 - 현장명 / 진행중` 형태 (날짜 범위 바) +- 클릭 시 해당 제품검사 상세 화면으로 이동 + +**상세 페이지** (슬라이드 7~9): +- 기본 정보: 품질관리서번호, 현장명, 수주처, 접수일, 담당자, 연락처, 상태, 작성자 +- 관련자 정보 4개 섹션 (실적 신고 시 필수 정보) +- 검사 정보: 검사방문요청일, 검사시작일, 검사종료일, 검사자 +- 현장 주소: 우편번호 찾기 + 상세주소 +- 수주 설정 정보: 수주 선택 버튼 → 수주별 개소 목록 (층수/부호/수주규격/시공규격/변경사유) +- 하단 버튼: 검사제품요청서 보기, 제품검사성적서 보기, **검사 완료**, 수정 + +--- + +## 2. 상태 관리 + +### 2.1 품질관리서 상태 + +| 상태 | 코드 | 조건 | +|------|------|------| +| 접수 | `received` | 생성 직후, 수주 미연결 | +| 진행중 | `in_progress` | 수주 연결됨 또는 일부 검사 진행 | +| 완료 | `completed` | 모든 개소 검사 완료 후 `complete()` 호출 | + +### 2.2 개소별 검사 상태 (자동 판정) + +| 상태 | 코드 | 판정 기준 | +|------|------|----------| +| 대기 | `pending` | 검사 데이터 없음 (15개 항목 0개 + 사진 없음) | +| 진행중 | `in_progress` | 일부 항목 입력 또는 사진 미등록 | +| 완료 | `completed` | 15개 항목 전부 입력 + 사진 1장 이상 | + +### 2.3 상태 자동 재계산 + +개소별 검사 저장 시 → 개소 상태 자동 판정 → 품질관리서 상태 재계산: +- 전부 `pending` → `received` +- 하나라도 `completed` 또는 `in_progress` → `in_progress` +- 전부 `completed` → `in_progress` (수동 `complete()` 필요) + +--- + +## 3. 검사 항목 + +### 3.1 15개 검사 항목 + +| # | 키 | 분류 | 설명 | 판정값 | +|---|-----|------|------|--------| +| 1 | `appearanceProcessing` | 외관 | 가공 상태 | pass/fail | +| 2 | `appearanceSewing` | 외관 | 봉제 상태 | pass/fail | +| 3 | `appearanceAssembly` | 외관 | 조립 상태 | pass/fail | +| 4 | `appearanceSmokeBarrier` | 외관 | 차연재 상태 | pass/fail | +| 5 | `appearanceBottomFinish` | 외관 | 하부마감 상태 | pass/fail | +| 6 | `motor` | 기능 | 모터 작동 | pass/fail | +| 7 | `material` | 기능 | 소재 적합성 | pass/fail | +| 8 | `lengthJudgment` | 치수 | 가로 치수 | OK/NG | +| 9 | `heightJudgment` | 치수 | 세로 치수 | OK/NG | +| 10 | `guideRailGap` | 치수 | 가이드레일 간격 | OK/NG | +| 11 | `bottomFinishGap` | 치수 | 하부마감 간격 | OK/NG | +| 12 | `fireResistanceTest` | 시험 | 내화 시험 | pass/fail | +| 13 | `smokeLeakageTest` | 시험 | 차연 시험 | pass/fail | +| 14 | `openCloseTest` | 시험 | 개폐 시험 | pass/fail | +| 15 | `impactTest` | 시험 | 충격 시험 | pass/fail | + +### 3.2 추가 데이터 + +| 키 | 설명 | 필수 | +|----|------|------| +| `productImages` | 제품 사진 URL 배열 | 완료 판정에 필수 | + +--- + +## 4. 수주 연결 + +### 4.1 수주 선택 + +- `availableOrders()`: 해당 수주처(client_id)의 미등록 수주 조회 +- 모달에서 복수 수주 선택 가능 + +### 4.2 개소 자동생성 + +수주 연결 시 각 수주의 root node(층/부호)마다 개소(Location) 자동생성: + +``` +수주 A (3개 root node) +├── 1F A호 → Location 1 +├── 2F B호 → Location 2 +└── 3F C호 → Location 3 + +수주 B (2개 root node) +├── 지하1F → Location 4 +└── 1F → Location 5 +``` + +### 4.3 개소 데이터 + +| 필드 | 설명 | 출처 | +|------|------|------| +| `order_item_id` | 대표 OrderItem | root node의 첫 번째 품목 | +| `post_width` | 시공 가로 | 발주 규격에서 복사 (수정 가능) | +| `post_height` | 시공 세로 | 발주 규격에서 복사 (수정 가능) | +| `change_reason` | 변경 사유 | 규격 변경 시 입력 | + +--- + +## 5. 문서 자동생성 (EAV) + +### 5.1 제품검사요청서 (슬라이드 12~13) + +- **Template ID**: 66 (제품검사 요청서) +- **트리거**: 품질관리서 생성/수정 시 `syncRequestDocument()` 호출 +- **인쇄용 페이지 형태로 구분**되어 표시 (인쇄, 공유, 닫기 버튼) + +**문서 구성**: + +``` +┌─────────────────────────────────────────┐ +│ 제품검사요청서 │ +│ 문서번호: ABC123 | 작성일자: 2025.11.11 │ +│ │ +│ 승인라인: 작성 → 승인 → 승인 → 승인 │ +│ 홍길동 이름 이름 이름 │ +│ │ +│ ── 기본정보 ── │ +│ 수주처, 수주번호, 담당자, 연락처 │ +│ 현장명, 납품일, 총 개소, 접수일 │ +│ │ +│ ── 입력사항 (실적신고 필수 정보) ── │ +│ 건축공사장: 현장명, 대지위치, 지번 │ +│ 자재유통업자: 회사명, 회사주소, 대표자명, │ +│ 전화번호 │ +│ 공사시공자: 회사명, 회사주소, 성명, 전화번호 │ +│ 공사감리자: 사무소명, 사무소주소, 성명, │ +│ 전화번호 │ +│ │ +│ ── 검사대상 사전 고지 정보 ── │ +│ No. 층수 부호 발주규격(가로/세로) │ +│ 시공후규격(가로/세로) 변경사유 │ +└─────────────────────────────────────────┘ +``` + +**주의 문구** (빨간색): +- 발주 사이즈와 시공 완료된 사이즈가 다를 시 **실질 범위를 넣어야 한다** +- 변경사유를 고지하여야 인정마을을 부착할 수 있다 +- 사전고지를 하지 않음으로 발생하는 문제의 귀책은 신청업체에 있다 + +### 5.2 제품검사성적서 (슬라이드 14~15) + +- 각 개소별 `document_id`로 EAV Document 참조 +- 검사 결과(inspection_data)를 EAV 필드로 저장 +- **개소별 페이지 단위**: 1/50 형태의 페이지 네비게이션 (이전/이동/다음 버튼) + +**문서 구성**: + +``` +┌─────────────────────────────────────────┐ +│ 제품검사성적서 │ +│ 문서번호: ABC123 | 작성일자: 2025.11.11 │ +│ │ +│ 제품명, 제품 LOT NO, 로트크기 │ +│ 제품코드, 검사일자 │ +│ 수주처, 검사자 │ +│ 현장명 │ +│ │ +│ ── 제품 사진 ── │ +│ [IMG] [IMG] │ +│ │ +│ ── 검사 항목 ── │ +│ No. 검사항목 검사기준 검사 특정값 판정│ +│ 1 외모양 │ +│ 가공상태 사용상 해로운 결함이 없을 것 │ +│ 재봉상태 내화심에 의해 견고하게 접합 │ +│ 조립상태 핸드바 견고하게 조립되어야 함 │ +│ 연기차단재 연기차단재 가이드레일 W60, │ +│ 가이드레일 W50 (분체 설치) │ +│ 하단마감재 내부 부재형상 설치 유무 │ +│ 2 모터 인정제품과 동일사양 │ +│ 3 재질 WY-SC780 인쇄상태 확인 │ +│ 4 치수 │ +│ 길이 수주 치수 ± 30mm │ +│ 높이 수주 치수 ± 30mm │ +│ 가이드레일 10 ± 5mm (측정부위 길이 100 이내)│ +│ 간격 가이드레일갑과 하단마감재 25mm 이내│ +│ 5 작동테스트 6mm 관절게이지 관통 여 150mm │ +│ 6 내화시험 25mm 관절게이지 관통 유무 │ +│ 7 차연시험 10초 이상 자속되는 화염 발생 유무 │ +│ 8 개폐시험 전도/개폐 2.5~6.5m/min 등 │ +│ │ +│ 특이사항: │ +│ 종합판정: 합격 │ +│ │ +│ [이전] [1] /50 [이동] [다음] │ +└─────────────────────────────────────────┘ +``` + +--- + +## 6. API + +### 6.1 주요 엔드포인트 + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/quality/documents` | 목록 | +| POST | `/quality/documents` | 생성 | +| GET | `/quality/documents/{id}` | 상세 | +| PUT | `/quality/documents/{id}` | 수정 (개소/규격 포함) | +| PATCH | `/quality/documents/{id}/complete` | 검사완료 | +| POST | `/quality/documents/{id}/orders` | 수주 연결 | +| DELETE | `/quality/documents/{id}/orders/{orderId}` | 수주 해제 | +| POST | `/quality/documents/{id}/locations/{locId}/inspect` | 개소별 검사 저장 | + +### 6.2 검사 저장 요청 예시 + +```json +POST /quality/documents/1/locations/5/inspect +{ + "inspection_data": { + "appearanceProcessing": "pass", + "appearanceSewing": "pass", + "appearanceAssembly": "pass", + "appearanceSmokeBarrier": "pass", + "appearanceBottomFinish": "pass", + "motor": "pass", + "material": "pass", + "lengthJudgment": "OK", + "heightJudgment": "OK", + "guideRailGap": "OK", + "bottomFinishGap": "OK", + "fireResistanceTest": "pass", + "smokeLeakageTest": "pass", + "openCloseTest": "pass", + "impactTest": "pass", + "productImages": ["https://..."] + } +} +``` + +--- + +## 7. 소스 파일 + +### 7.1 Backend + +| 파일 | 역할 | +|------|------| +| `api/app/Models/Qualitys/QualityDocument.php` | 품질관리서 모델 | +| `api/app/Models/Qualitys/QualityDocumentLocation.php` | 개소 모델 | +| `api/app/Models/Qualitys/QualityDocumentOrder.php` | 수주 연결 모델 | +| `api/app/Services/QualityDocumentService.php` | 서비스 (770줄) | +| `api/app/Http/Controllers/Api/V1/QualityDocumentController.php` | 컨트롤러 | + +### 7.2 Frontend + +| 파일 | 역할 | +|------|------| +| `react/src/components/quality/InspectionManagement/InspectionList.tsx` | 목록 | +| `react/src/components/quality/InspectionManagement/InspectionCreate.tsx` | 생성 | +| `react/src/components/quality/InspectionManagement/InspectionDetail.tsx` | 상세/수정 | +| `react/src/components/quality/InspectionManagement/OrderSelectModal.tsx` | 수주 선택 | +| `react/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx` | 검사 입력 | +| `react/src/components/quality/InspectionManagement/actions.ts` | Server Actions | +| `react/src/components/quality/InspectionManagement/types.ts` | 타입 정의 | + +--- + +## 관련 문서 + +- [품질관리 시스템 개요](./README.md) +- [생산실적신고](./performance-reports.md) + +--- + +**최종 업데이트**: 2026-03-09 diff --git a/features/quality-management/performance-reports.md b/features/quality-management/performance-reports.md new file mode 100644 index 0000000..a34f55d --- /dev/null +++ b/features/quality-management/performance-reports.md @@ -0,0 +1,268 @@ +# 생산실적신고 (Performance Reports) + +> **작성일**: 2026-03-09 +> **상태**: 운영 중 +> **URL**: `/quality/performance-reports` + +--- + +## 1. 개요 + +### 1.1 목적 + +건설기술진흥법에 따라 방화문 제품검사 완료 건에 대해 **분기별 실적신고**를 관리한다. +건기원(한국건설기술연구원) 온라인 시스템에 제출할 데이터를 자동 수집하고, 필수정보 검증 후 확정 처리한다. + +### 1.2 비즈니스 배경 + +- **법적 의무**: 방화문은 건축자재 품질관리 대상으로, 제품검사 후 실적을 건기원에 신고해야 함 +- **신고 주기**: 분기별 (Q1: 1~3월, Q2: 4~6월, Q3: 7~9월, Q4: 10~12월) +- **신고 내용**: 품질관리서 번호, 현장정보, LOT, 규격, 검사결과 +- **제출 방식**: 건기원 온라인 시스템 수기 입력 또는 엑셀 업로드 + +### 1.3 SAM에서의 역할 + +``` +제품검사 완료 → 실적신고 자동생성 → 필수정보 검증 → 확정 → 건기원 제출 + (SAM 자동) (SAM 자동) (담당자) (수동/향후 자동) +``` + +--- + +## 2. 상태 흐름 + +### 2.1 상태 정의 + +| 상태 | 코드 | 설명 | +|------|------|------| +| 미확정 | `unconfirmed` | 자동생성 직후, 필수정보 미완료 가능 | +| 확정 | `confirmed` | 필수정보 완료 후 담당자가 확정 | +| 신고완료 | `reported` | 건기원에 신고 완료 (미구현) | + +### 2.2 상태 전이 + +``` + confirm() +unconfirmed ─────────────────→ confirmed + ↑ │ + │ unconfirm() │ + └──────────────────────────────┘ + │ + distribute() + ↓ + reported (미구현) +``` + +### 2.3 확정 조건 + +확정(`confirm`) 시 **필수정보 4개 섹션** 검증: + +``` +✅ 건축공사장: name, land_location, lot_no +✅ 자재유통업자: company, address, ceo, tel +✅ 공사시공자: company, address, name, phone +✅ 공사감리자: office, address, name, phone +``` + +하나라도 누락되면 확정 불가 → `cannotConfirmWithMissingInfo` 에러 반환 + +--- + +## 3. 자동 생성 로직 + +### 3.1 트리거 + +`QualityDocumentService::complete()` 호출 시: + +```php +PerformanceReport::firstOrCreate( + [ + 'tenant_id' => $tenantId, + 'quality_document_id' => $qualityDocument->id, + ], + [ + 'year' => now()->year, // 현재 연도 + 'quarter' => ceil(now()->month / 3), // 현재 분기 + 'confirmation_status' => 'unconfirmed', + 'created_by' => $userId, + ] +); +``` + +### 3.2 특징 + +- `firstOrCreate`: 동일 품질관리서에 대해 중복 생성 방지 +- 검사완료 시점의 연도/분기로 자동 배정 +- Unique 제약: `(tenant_id, quality_document_id)` + +--- + +## 4. 화면 구성 + +### 4.1 탭 구조 + +| 탭 | 내용 | +|----|------| +| **분기별 실적신고** | 기본 탭. 연도/분기별 실적 목록 | +| **누락체크** | 출고완료 수주 중 품질관리서 미등록 건 | + +### 4.2 통계 카드 + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ 전체 │ │ 확정 │ │ 미확정 │ │ 총 개소 │ +│ 12건 │ │ 8건 │ │ 4건 │ │ 48개소 │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ +``` + +### 4.3 테이블 컬럼 (분기별 실적신고) + +| 컬럼 | 설명 | +|------|------| +| 선택 | 체크박스 (일괄 처리용) | +| 번호 | 순번 | +| 품질관리서번호 | KD-QD-YYYYMM-NNNN | +| 작성일 | 품질관리서 접수일 | +| 현장명 | 시공 현장명 | +| 수주처 | 거래처 | +| 개소 | 검사 개소 수 | +| 필수정보 | "완료" 또는 "N건 누락" | +| 확정상태 | 미확정/확정/신고완료 | +| 확정일 | 확정 처리일 | +| 메모 | 메모 내용 | + +### 4.4 액션 버튼 + +| 버튼 | 조건 | 설명 | +|------|------|------| +| 선택 확정 | 미확정 건 선택 시 | 필수정보 완료된 건만 일괄 확정 | +| 확정 해제 | 확정 건 선택 시 | 확정 → 미확정 되돌리기 | +| 배포 | 확정 건 선택 시 | 건기원 신고 (미구현) | +| 메모 | 건 선택 시 | 메모 일괄 작성 | + +--- + +## 5. 누락체크 (슬라이드 18) + +### 5.1 목적 + +실적신고 기간이 지났지만 확정이 안된 목록을 확인한다. +출고(배송)가 완료되었지만 품질관리서가 등록되지 않은 수주를 탐지한다. + +### 5.2 누락 발생 원인 + +| 원인 | 설명 | +|------|------| +| 품질관리서 발행일 기준 분기 불일치 | 품질관리서가 해당 분기에 포함되어야 하나, 공사 미완료로 다음 분기로 이월 | +| 수주 중복 등록 | 수주통 시 다른 현장명으로 등록되어 제품검사 발행 시 누락 | +| 납품 후 미등록 | 납품 후 제품검사 등록이 누락된 경우 | + +### 5.3 누락체크 목록 표시 + +| 컬럼 | 설명 | +|------|------| +| 품질관리서 번호 | 관련 품질관리서 (있는 경우) | +| 현장명 | 수주 현장명 | +| 수주처 | 거래처 | +| 개소 | 수주 개소 수 | +| 제품검사완료일 | 검사 완료 일자 | +| 메모 | 누락 사유 메모 | + +### 5.4 로직 + +```sql +-- 출고완료 수주 중 quality_document_orders에 미등록된 건 +SELECT orders.* +FROM orders +WHERE delivery_status = 'completed' + AND NOT EXISTS ( + SELECT 1 FROM quality_document_orders + WHERE order_id = orders.id + ) +``` + +--- + +## 6. API + +### 6.1 엔드포인트 + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/quality/performance-reports` | 목록 (year, quarter, status 필터) | +| GET | `/quality/performance-reports/stats` | 확정/미확정 통계 | +| GET | `/quality/performance-reports/missing` | 누락체크 | +| PATCH | `/quality/performance-reports/confirm` | 일괄 확정 | +| PATCH | `/quality/performance-reports/unconfirm` | 확정 해제 | +| PATCH | `/quality/performance-reports/memo` | 메모 일괄 업데이트 | + +### 6.2 요청/응답 예시 + +**목록 조회**: +``` +GET /quality/performance-reports?year=2026&quarter=1&status=unconfirmed +``` + +**일괄 확정**: +```json +PATCH /quality/performance-reports/confirm +{ + "ids": [1, 2, 3] +} +``` + +**응답**: +```json +{ + "success": true, + "message": "message.performance_report.confirmed", + "data": { + "confirmed_count": 2, + "skipped_count": 1, + "skipped_ids": [3] + } +} +``` + +--- + +## 7. 소스 파일 + +### 7.1 Backend + +| 파일 | 역할 | +|------|------| +| `api/app/Models/Qualitys/PerformanceReport.php` | 모델 | +| `api/app/Services/PerformanceReportService.php` | 서비스 (280줄) | +| `api/app/Http/Controllers/Api/V1/PerformanceReportController.php` | 컨트롤러 | +| `api/routes/api/v1/quality.php` | 라우트 | + +### 7.2 Frontend + +| 파일 | 역할 | +|------|------| +| `react/src/app/[locale]/(protected)/quality/performance-reports/page.tsx` | 페이지 | +| `react/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx` | 목록 컴포넌트 | +| `react/src/components/quality/PerformanceReportManagement/MemoModal.tsx` | 메모 모달 | +| `react/src/components/quality/PerformanceReportManagement/actions.ts` | Server Actions | + +--- + +## 8. 미구현 기능 + +| 기능 | 설명 | 우선순위 | +|------|------|---------| +| 배포(distribute) API | 건기원 시스템 연동 자동 신고 | 중 | +| 엑셀 다운로드 | 확정건 건기원 양식 엑셀 내보내기 | 높음 | +| 분기 마감 알림 | 분기 종료 전 미확정건 알림 | 낮음 | + +--- + +## 관련 문서 + +- [품질관리 시스템 개요](./README.md) +- [제품검사 관리](./inspection-management.md) + +--- + +**최종 업데이트**: 2026-03-09 diff --git a/features/quality-management/quality-certification-audit.md b/features/quality-management/quality-certification-audit.md new file mode 100644 index 0000000..f64936b --- /dev/null +++ b/features/quality-management/quality-certification-audit.md @@ -0,0 +1,169 @@ +# 품질인정심사 (Quality Certification Audit) + +> **작성일**: 2026-03-09 +> **상태**: 개발 예정 +> **URL**: `/quality/qms` +> **스토리보드**: 슬라이드 19~20 + +--- + +## 1. 개요 + +### 1.1 목적 + +품질인정심사 자료를 조회하고 관리한다. 기준/매뉴얼 심사와 로트 추적 심사 두 가지 영역으로 구성된다. + +### 1.2 탭 구조 + +| 탭 | 설명 | 진행률 예시 | +|----|------|-----------| +| 기준/매뉴얼 심사 | 품질 기준 문서 및 매뉴얼 점검 | 2/15 | +| 로트 추적 심사 | 개소별 제품로트 추적 및 서류 확인 | 7/12 | + +**전체 심사 진행률**: 기준/매뉴얼 + 로트추적 합산 (예: 9/27) + +--- + +## 2. 기준/매뉴얼 심사 + +### 2.1 화면 구성 + +``` +┌─────────────────────────────────────────────────────┐ +│ 필터: 연도, 분기(전체/1~4), 검색 │ +├──────────────────┬──────────────────────────────────┤ +│ 점검표 항목 │ 기준 문서화 │ +│ │ │ +│ ▼ 1. 제목 │ 항목명 ─────────── │ +│ ☑ 1. 항목 [완료] │ 소개 ─────────── │ +│ ☑ 2. 항목 [완료] │ │ +│ □ 3. 항목 │ 관련 기준 문서 │ +│ │ 📄 문서명 R025 2025-01-01 │ +│ ▼ 2. 제목 │ 📄 문서명 R025 2025-01-01 │ +│ ☑ 1. 항목 │ │ +│ ☑ 2. 항목 │ 📎 기준/매뉴얼 확인 │ +│ □ 3. 항목 │ │ +│ │ 문서 미리보기 영역 │ +│ 완료 체크 박스 │ (PDF 등) │ +│ □ 완료 │ 파일명: 파일명.pdf 1/1 페이지 │ +└──────────────────┴──────────────────────────────────┘ +``` + +### 2.2 점검표 항목 + +- 계층 구조: 대분류(제목) → 세부 항목 +- 각 항목에 완료/미완료 체크 +- 항목 클릭 시 우측에 해당 항목의 기준 문서 정보 표시 + +### 2.3 기준 문서화 + +| 영역 | 설명 | +|------|------| +| 항목명 및 소개 | 선택한 점검표 항목의 상세 정보 | +| 관련 기준 문서 | 해당 항목에 연결된 기준 문서 목록 (문서명, 문서번호, 날짜) | +| 문서 미리보기 | PDF 등 첨부 문서 미리보기 | +| 완료 체크 박스 | 기본값: 완료 해제 상태 | + +### 2.4 기준/매뉴얼 확인 버튼 + +- 클릭 시 확인 완료 스티커로 변경 +- 전체 항목 확인 완료 시 탭 진행률 갱신 + +--- + +## 3. 로트 추적 심사 + +### 3.1 화면 구성 + +``` +┌───────────────────────────────────────────────────────────┐ +│ 필터: 연도, 분기(전체/1~4), 검색 │ +├─────────────────┬─────────────────┬───────────────────────┤ +│ 품질관리서 목록 │ 수주코드 목록 │ 관련 서류 │ +│ │ │ │ +│ KD-SS-2024- │ KD-SS-240921-19 │ 수입검사 성적서 │ +│ 2025년 3분기 │ 수주: 2024-09-24│ ┌──────────────────┐ │ +│ 현장명 │ 7개소 / 완료 │ │ 전반 수입검사 성적서│ │ +│ 인정특성, 실리카 │ │ │ 절반 수입검사 성적서│ │ +│ 수주코드 2건 │ 개소별 제품로트 │ └──────────────────┘ │ +│ 14개소 │ KD-SS-240921-19 │ │ +│ │ -01 │ 개소별 제품로트 목록 │ +│ KD-SS-2024- │ 보조 │ 수주코드번호 │ +│ 현장명 │ │ 가로, 세로 │ +│ 수주코드 2건 │ KD-SS-240921-19 │ 3건의 서류 │ +│ 7개소 │ -19 [확인] │ │ +│ │ 보조 │ 확인 서류 목록 │ +│ │ │ 수입검사, 일반전표, │ +│ │ │ 중간검사, 납품확인서, │ +│ │ │ 출고증, 제품검사, │ +│ │ │ 검사 성적서, 품질관리서│ +├─────────────────┴─────────────────┤ │ +│ 문서 정보 영역 │ 확인 버튼 │ +│ 품질: 해당 문서 열람/닫힘 토글 │ [확인] [완료] │ +└────────────────────────────────────┴───────────────────────┘ +``` + +### 3.2 3단 드릴다운 구조 + +| 단계 | 영역 | 표시 내용 | +|------|------|---------| +| 1단계 | 품질관리서 목록 | 해당 분기 확정된 품질관리서, 인정특성, 수주코드 건수, 개소수 | +| 2단계 | 수주코드 목록 | 선택한 품질관리서의 수주코드, 수주일, 개소수, 완료 상태 | +| 3단계 | 관련 서류 | 해당 수주코드의 개소별 제품로트, 확인 서류 목록 | + +### 3.3 품질관리서 목록 (1단계) + +- 해당 분기로 실적신고 확정된 품질관리서 +- 표시: 품질관리서 번호, 해당 분기, 현장명, 인정특성, 수주코드 건수, 개소수 +- 클릭 시 2단계(수주코드 목록) 표시 + +### 3.4 수주코드 목록 (2단계) + +- 해당 품질관리서에 연결된 수주코드 목록 +- 품질관리서는 제품검사 시 수주코드 연결 +- 표시: 수주코드 번호, 개소 수, 수주일, 현장, 개소수, 개소별 제품로트 확인 여부 +- 클릭 시 5단계 영역에 해당 수주코드의 관련 서류 표시 + +### 3.5 관련 서류 (3단계) + +개소별 제품로트에 연결된 서류: + +| 서류 종류 | 설명 | +|---------|------| +| 수입검사 성적서 | 원자재 수입검사 결과 | +| 일반전표 | 회계 전표 | +| 중간검사 성적서 | 공정 중간검사 | +| 납품확인서 | 납품 확인 | +| 출고증 | 출고 기록 | +| 제품검사 성적서 | 완제품 검사 | +| 품질관리서 | 품질관리서 원본 | + +### 3.6 확인 프로세스 + +1. 품질관리서 선택 → 수주코드 선택 → 관련 서류 조회 +2. 각 서류를 열람하고 **확인** 버튼 클릭 +3. 모든 서류 확인 완료 시 해당 로트 **완료** 처리 +4. 전체 로트 완료 시 로트 추적 심사 진행률 갱신 + +--- + +## 4. 구현 상태 + +| 기능 | 상태 | 비고 | +|------|------|------| +| 기준/매뉴얼 심사 점검표 | 미구현 | 페이지만 존재 | +| 기준 문서 관리 | 미구현 | | +| 로트 추적 심사 | 미구현 | | +| 서류 연결/확인 | 미구현 | | + +--- + +## 관련 문서 + +- [품질관리 시스템 개요](./README.md) +- [제품검사 관리](./inspection-management.md) +- [생산실적신고](./performance-reports.md) + +--- + +**최종 업데이트**: 2026-03-09 From cc38b00c11d6a4745fe536fb2b9dda8fd95339b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Mar 2026 22:33:19 +0900 Subject: [PATCH 10/15] =?UTF-8?q?refactor:=20[structure]=20sam/=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EB=AC=B8=EC=84=9C=EB=A5=BC=20docs=20?= =?UTF-8?q?=EB=A3=A8=ED=8A=B8=EB=A1=9C=20=EC=9E=AC=EB=B0=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sam/docs/ 하위 62개 신규 파일을 루트로 이동 (contracts, features, guides, plans 등) - sam/docs/ 하위 52개 변경 파일을 루트에 덮어쓰기 (brochure, rules 등) - sam/ 폴더 전체 삭제 (docker, coocon 포함) --- brochure/v1/sam-brochure-1page.pptx | Bin 113934 -> 115703 bytes brochure/v1/sam-brochure-2page.pptx | Bin 178683 -> 180473 bytes brochure/v1/slides/brochure-1page.html | 6 +- brochure/v1/slides/brochure-2page-back.html | 6 +- brochure/v1/slides/brochure-2page-front.html | 4 +- .../v2/sam-brochure-v2-dashboard-1page.pptx | Bin 140569 -> 142338 bytes .../v2/sam-brochure-v2-dashboard-2page.pptx | Bin 200577 -> 202367 bytes .../v2/slides/brochure-dashboard-1page.html | 6 +- .../v2/slides/brochure-dashboard-back.html | 6 +- .../v2/slides/brochure-dashboard-front.html | 4 +- .../v3/sam-brochure-v3-dashboard-1page.pptx | Bin 165619 -> 167362 bytes .../v3/sam-brochure-v3-dashboard-2page.pptx | Bin 242778 -> 244522 bytes .../v3/slides/brochure-dashboard-1page.html | 6 +- .../v3/slides/brochure-dashboard-back.html | 6 +- .../v3/slides/brochure-dashboard-front.html | 4 +- .../v4/sam-brochure-v4-dashboard-1page.pptx | Bin 164107 -> 165817 bytes .../v4/sam-brochure-v4-dashboard-2page.pptx | Bin 240668 -> 242412 bytes .../v4/slides/brochure-dashboard-1page.html | 6 +- .../v4/slides/brochure-dashboard-back.html | 6 +- .../v4/slides/brochure-dashboard-front.html | 4 +- .../v5/sam-brochure-v5-dashboard-1page.pptx | Bin 204946 -> 206694 bytes .../v5/sam-brochure-v5-dashboard-2page.pptx | Bin 320324 -> 322073 bytes .../v5/slides/brochure-dashboard-1page.html | 6 +- .../v5/slides/brochure-dashboard-back.html | 6 +- .../v5/slides/brochure-dashboard-front.html | 4 +- .../v6/sam-brochure-v6-dashboard-1page.pptx | Bin 165626 -> 167336 bytes .../v6/sam-brochure-v6-dashboard-2page.pptx | Bin 225380 -> 227131 bytes .../v6/slides/brochure-dashboard-1page.html | 6 +- .../v6/slides/brochure-dashboard-back.html | 6 +- .../v6/slides/brochure-dashboard-front.html | 4 +- .../v7/sam-brochure-v7-dashboard-1page.pptx | Bin 163484 -> 165194 bytes .../v7/sam-brochure-v7-dashboard-2page.pptx | Bin 251299 -> 253041 bytes .../v7/slides/brochure-dashboard-1page.html | 6 +- .../v7/slides/brochure-dashboard-back.html | 6 +- .../v7/slides/brochure-dashboard-front.html | 4 +- .../v8/sam-brochure-v8-dashboard-1page.pptx | Bin 143401 -> 145146 bytes .../v8/sam-brochure-v8-dashboard-2page.pptx | Bin 271134 -> 272880 bytes .../v8/slides/brochure-dashboard-1page.html | 6 +- .../v8/slides/brochure-dashboard-back.html | 6 +- .../v8/slides/brochure-dashboard-front.html | 4 +- .../v9/sam-brochure-v9-dashboard-1page.pptx | Bin 132853 -> 134597 bytes .../v9/sam-brochure-v9-dashboard-2page.pptx | Bin 178488 -> 180254 bytes .../v9/slides/brochure-dashboard-1page.html | 6 +- .../v9/slides/brochure-dashboard-back.html | 6 +- .../v9/slides/brochure-dashboard-front.html | 4 +- features/barobill-kakaotalk/README.md | 65 +- features/documents/README.md | 4 +- projects/index_projects.md | 57 +- rules/billing-policy.md | 3 +- rules/customer-pricing.md | 2 +- rules/slides/billing-policy/slide-03.html | 12 +- rules/slides/customer-pricing/slide-06.html | 2 +- .../쿠콘_나이스평가정보_API_개발가이드.md | 685 ------------ sam/docker/api/opcache.ini | 33 - sam/docker/api/supervisord.conf | 36 - sam/docker/docker-compose.yml | 225 ---- sam/docker/mng/Dockerfile | 43 - sam/docker/mng/entrypoint.sh | 19 - sam/docker/mng/nginx.conf | 32 - sam/docker/mng/supervisord.conf | 23 - sam/docs/CLAUDE.md | 876 --------------- sam/docs/INDEX.md | 421 -------- sam/docs/assets/bi/sam_bi_black.png | Bin 2321 -> 0 bytes sam/docs/assets/bi/sam_bi_blue.png | Bin 2277 -> 0 bytes sam/docs/assets/bi/sam_bi_green.png | Bin 1547 -> 0 bytes sam/docs/assets/bi/sam_bi_orange.png | Bin 1547 -> 0 bytes sam/docs/assets/bi/sam_bi_purple.png | Bin 1547 -> 0 bytes sam/docs/assets/bi/sam_bi_red.png | Bin 1547 -> 0 bytes sam/docs/assets/bi/sam_bi_white.png | Bin 2361 -> 0 bytes sam/docs/brochure/README.md | 370 ------- sam/docs/brochure/v1/convert-1page.cjs | 28 - sam/docs/brochure/v1/convert-2page.cjs | 32 - sam/docs/brochure/v1/sam-brochure-1page.pptx | Bin 115703 -> 0 bytes sam/docs/brochure/v1/sam-brochure-2page.pptx | Bin 180473 -> 0 bytes .../brochure/v1/slides/brochure-1page.html | 210 ---- .../v1/slides/brochure-2page-back.html | 227 ---- .../v1/slides/brochure-2page-front.html | 122 --- sam/docs/brochure/v2/convert-1page.cjs | 28 - sam/docs/brochure/v2/convert-2page.cjs | 32 - .../v2/sam-brochure-v2-dashboard-1page.pptx | Bin 142338 -> 0 bytes .../v2/sam-brochure-v2-dashboard-2page.pptx | Bin 202367 -> 0 bytes .../v2/slides/brochure-dashboard-1page.html | 261 ----- .../v2/slides/brochure-dashboard-back.html | 242 ----- .../v2/slides/brochure-dashboard-front.html | 172 --- sam/docs/brochure/v3/convert-1page.cjs | 27 - sam/docs/brochure/v3/convert-2page.cjs | 31 - .../v3/sam-brochure-v3-dashboard-1page.pptx | Bin 167362 -> 0 bytes .../v3/sam-brochure-v3-dashboard-2page.pptx | Bin 244522 -> 0 bytes .../v3/slides/brochure-dashboard-1page.html | 405 ------- .../v3/slides/brochure-dashboard-back.html | 373 ------- .../v3/slides/brochure-dashboard-front.html | 262 ----- sam/docs/brochure/v4/convert-1page.cjs | 27 - sam/docs/brochure/v4/convert-2page.cjs | 31 - .../v4/sam-brochure-v4-dashboard-1page.pptx | Bin 165817 -> 0 bytes .../v4/sam-brochure-v4-dashboard-2page.pptx | Bin 242412 -> 0 bytes .../v4/slides/brochure-dashboard-1page.html | 405 ------- .../v4/slides/brochure-dashboard-back.html | 373 ------- .../v4/slides/brochure-dashboard-front.html | 260 ----- sam/docs/brochure/v5/convert-1page.cjs | 52 - sam/docs/brochure/v5/convert-2page.cjs | 56 - .../v5/sam-brochure-v5-dashboard-1page.pptx | Bin 206694 -> 0 bytes .../v5/sam-brochure-v5-dashboard-2page.pptx | Bin 322073 -> 0 bytes .../v5/slides/brochure-dashboard-1page.html | 319 ------ .../v5/slides/brochure-dashboard-back.html | 311 ------ .../v5/slides/brochure-dashboard-front.html | 216 ---- sam/docs/brochure/v6/convert-1page.cjs | 27 - sam/docs/brochure/v6/convert-2page.cjs | 31 - .../v6/sam-brochure-v6-dashboard-1page.pptx | Bin 167336 -> 0 bytes .../v6/sam-brochure-v6-dashboard-2page.pptx | Bin 227131 -> 0 bytes .../v6/slides/brochure-dashboard-1page.html | 374 ------- .../v6/slides/brochure-dashboard-back.html | 337 ------ .../v6/slides/brochure-dashboard-front.html | 231 ---- sam/docs/brochure/v7/convert-1page.cjs | 27 - sam/docs/brochure/v7/convert-2page.cjs | 31 - .../v7/sam-brochure-v7-dashboard-1page.pptx | Bin 165194 -> 0 bytes .../v7/sam-brochure-v7-dashboard-2page.pptx | Bin 253041 -> 0 bytes .../v7/slides/brochure-dashboard-1page.html | 376 ------- .../v7/slides/brochure-dashboard-back.html | 373 ------- .../v7/slides/brochure-dashboard-front.html | 278 ----- sam/docs/brochure/v8/convert-1page.cjs | 27 - sam/docs/brochure/v8/convert-2page.cjs | 31 - .../v8/sam-brochure-v8-dashboard-1page.pptx | Bin 145146 -> 0 bytes .../v8/sam-brochure-v8-dashboard-2page.pptx | Bin 272880 -> 0 bytes .../v8/slides/brochure-dashboard-1page.html | 238 ----- .../v8/slides/brochure-dashboard-back.html | 320 ------ .../v8/slides/brochure-dashboard-front.html | 281 ----- sam/docs/brochure/v9/convert-1page.cjs | 27 - sam/docs/brochure/v9/convert-2page.cjs | 31 - .../v9/sam-brochure-v9-dashboard-1page.pptx | Bin 134597 -> 0 bytes .../v9/sam-brochure-v9-dashboard-2page.pptx | Bin 180254 -> 0 bytes .../v9/slides/brochure-dashboard-1page.html | 266 ----- .../v9/slides/brochure-dashboard-back.html | 229 ---- .../v9/slides/brochure-dashboard-front.html | 181 ---- .../changes/20260303_gemini_model_upgrade.md | 119 --- .../20260304_eaccount_infinite_loop_fix.md | 165 --- sam/docs/contracts/CHANGELOG.md | 42 - ...고객_서비스이용계약서_v4_0_전자서명용.docx | Bin 39346 -> 0 bytes sam/docs/contracts/docx/비밀유지서약서.docx | Bin 29026 -> 0 bytes .../docx/영업파트너 위촉계약서(단체용).docx | Bin 25013 -> 0 bytes .../contracts/docx/영업파트너 위촉계약서.docx | Bin 33340 -> 0 bytes .../markdown/01-service-agreement.md | 458 -------- sam/docs/contracts/markdown/02-nda.md | 199 ---- .../markdown/03-partner-agreement.md | 276 ----- .../markdown/04-partner-agreement-group.md | 267 ----- sam/docs/contracts/revisions.json | 58 - .../contracts/scripts/extract_to_markdown.py | 334 ------ sam/docs/contracts/scripts/sync_check.py | 263 ----- sam/docs/data/interview-master-questions.sql | 279 ----- .../academy/fire-shutter-image-prompts.md | 369 ------- sam/docs/features/approvals/README.md | 298 ------ sam/docs/features/approvals/api-reference.md | 594 ----------- .../approvals/db-changes-and-model-sync.md | 286 ----- sam/docs/features/approvals/form-types.md | 999 ------------------ sam/docs/features/approvals/ui-screens.md | 381 ------- sam/docs/features/approvals/workflows.md | 565 ---------- .../features/barobill-kakaotalk/README.md | 410 ------- .../esign-notification-guide.md | 250 ----- sam/docs/features/business-card-request.md | 173 --- sam/docs/features/credit-evaluation/README.md | 284 ----- sam/docs/features/documents/README.md | 122 --- .../features/documents/mng-document-system.md | 738 ------------- .../documents/mng-document-template.md | 826 --------------- sam/docs/features/planning/README.md | 129 --- .../features/planning/construction-photos.md | 275 ----- sam/docs/features/planning/meeting-minutes.md | 456 -------- sam/docs/features/planning/planning-views.md | 222 ---- sam/docs/features/rd/README.md | 110 -- sam/docs/features/rd/design-insight.md | 246 ----- sam/docs/features/rd/planning-design.md | 366 ------- sam/docs/guides/ai-config-settings.md | 325 ------ sam/docs/guides/ai-management.md | 291 ----- sam/docs/guides/ai-model-update-workflow.md | 313 ------ sam/docs/guides/pptx-generation-guide.md | 387 ------- sam/docs/guides/server-how-it-works.md | 247 ----- sam/docs/guides/table-design-guide.md | 486 --------- .../plans/SAM_General_Rule_Storyboard_D1.0.md | 737 ------------- sam/docs/plans/ai-quotation-engine-plan.md | 928 ---------------- sam/docs/plans/attendance-management-plan.md | 284 ----- .../plans/block-builder-evolution-plan.md | 706 ------------- sam/docs/plans/design-insight-menu-plan.md | 611 ----------- .../fire-shutter-drawing-generator-plan.md | 753 ------------- sam/docs/plans/sound-logo-generator-plan.md | 637 ----------- .../projects/e-sign/esign-storyboard.pptx | Bin 529331 -> 0 bytes sam/docs/projects/index_projects.md | 304 ------ sam/docs/projects/org-chart/README.md | 317 ------ sam/docs/projects/planning-design/README.md | 157 --- sam/docs/rules/billing-policy.md | 189 ---- sam/docs/rules/billing-policy.pptx | Bin 319165 -> 0 bytes sam/docs/rules/customer-pricing.md | 116 -- sam/docs/rules/customer-pricing.pptx | Bin 294356 -> 0 bytes sam/docs/rules/partner-commission.md | 114 -- sam/docs/rules/partner-commission.pptx | Bin 262731 -> 0 bytes .../rules/slides/billing-policy/slide-01.html | 73 -- .../rules/slides/billing-policy/slide-02.html | 91 -- .../rules/slides/billing-policy/slide-03.html | 103 -- .../rules/slides/billing-policy/slide-04.html | 142 --- .../rules/slides/billing-policy/slide-05.html | 115 -- .../rules/slides/billing-policy/slide-06.html | 116 -- .../rules/slides/billing-policy/slide-07.html | 128 --- .../slides/customer-pricing/slide-01.html | 73 -- .../slides/customer-pricing/slide-02.html | 82 -- .../slides/customer-pricing/slide-03.html | 131 --- .../slides/customer-pricing/slide-04.html | 163 --- .../slides/customer-pricing/slide-05.html | 124 --- .../slides/customer-pricing/slide-06.html | 117 -- .../slides/customer-pricing/slide-07.html | 65 -- .../slides/partner-commission/slide-01.html | 72 -- .../slides/partner-commission/slide-02.html | 91 -- .../slides/partner-commission/slide-03.html | 118 --- .../slides/partner-commission/slide-04.html | 180 ---- .../slides/partner-commission/slide-05.html | 129 --- .../slides/partner-commission/slide-06.html | 73 -- .../rules/slides/usage-plan/SAM_활용방안.pptx | Bin 279940 -> 0 bytes sam/docs/rules/slides/usage-plan/convert.cjs | 31 - .../rules/slides/usage-plan/slide-01.html | 42 - .../rules/slides/usage-plan/slide-02.html | 58 - .../rules/slides/usage-plan/slide-03.html | 108 -- .../rules/slides/usage-plan/slide-04.html | 88 -- .../rules/slides/usage-plan/slide-05.html | 74 -- .../rules/slides/usage-plan/slide-06.html | 68 -- .../rules/slides/usage-plan/slide-07.html | 82 -- sam/docs/system/ai-automation-vision.md | 174 --- .../system/database/codebridge-separation.md | 443 -------- 223 files changed, 178 insertions(+), 32907 deletions(-) delete mode 100644 sam/coocon/쿠콘_나이스평가정보_API_개발가이드.md delete mode 100644 sam/docker/api/opcache.ini delete mode 100755 sam/docker/api/supervisord.conf delete mode 100644 sam/docker/docker-compose.yml delete mode 100755 sam/docker/mng/Dockerfile delete mode 100755 sam/docker/mng/entrypoint.sh delete mode 100755 sam/docker/mng/nginx.conf delete mode 100755 sam/docker/mng/supervisord.conf delete mode 100644 sam/docs/CLAUDE.md delete mode 100644 sam/docs/INDEX.md delete mode 100755 sam/docs/assets/bi/sam_bi_black.png delete mode 100755 sam/docs/assets/bi/sam_bi_blue.png delete mode 100755 sam/docs/assets/bi/sam_bi_green.png delete mode 100755 sam/docs/assets/bi/sam_bi_orange.png delete mode 100755 sam/docs/assets/bi/sam_bi_purple.png delete mode 100755 sam/docs/assets/bi/sam_bi_red.png delete mode 100755 sam/docs/assets/bi/sam_bi_white.png delete mode 100644 sam/docs/brochure/README.md delete mode 100644 sam/docs/brochure/v1/convert-1page.cjs delete mode 100644 sam/docs/brochure/v1/convert-2page.cjs delete mode 100644 sam/docs/brochure/v1/sam-brochure-1page.pptx delete mode 100644 sam/docs/brochure/v1/sam-brochure-2page.pptx delete mode 100644 sam/docs/brochure/v1/slides/brochure-1page.html delete mode 100644 sam/docs/brochure/v1/slides/brochure-2page-back.html delete mode 100644 sam/docs/brochure/v1/slides/brochure-2page-front.html delete mode 100644 sam/docs/brochure/v2/convert-1page.cjs delete mode 100644 sam/docs/brochure/v2/convert-2page.cjs delete mode 100644 sam/docs/brochure/v2/sam-brochure-v2-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v2/sam-brochure-v2-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v2/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v2/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v2/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/brochure/v3/convert-1page.cjs delete mode 100644 sam/docs/brochure/v3/convert-2page.cjs delete mode 100644 sam/docs/brochure/v3/sam-brochure-v3-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v3/sam-brochure-v3-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v3/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v3/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v3/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/brochure/v4/convert-1page.cjs delete mode 100644 sam/docs/brochure/v4/convert-2page.cjs delete mode 100644 sam/docs/brochure/v4/sam-brochure-v4-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v4/sam-brochure-v4-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v4/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v4/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v4/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/brochure/v5/convert-1page.cjs delete mode 100644 sam/docs/brochure/v5/convert-2page.cjs delete mode 100644 sam/docs/brochure/v5/sam-brochure-v5-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v5/sam-brochure-v5-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v5/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v5/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v5/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/brochure/v6/convert-1page.cjs delete mode 100644 sam/docs/brochure/v6/convert-2page.cjs delete mode 100644 sam/docs/brochure/v6/sam-brochure-v6-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v6/sam-brochure-v6-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v6/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v6/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v6/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/brochure/v7/convert-1page.cjs delete mode 100644 sam/docs/brochure/v7/convert-2page.cjs delete mode 100644 sam/docs/brochure/v7/sam-brochure-v7-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v7/sam-brochure-v7-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v7/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v7/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v7/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/brochure/v8/convert-1page.cjs delete mode 100644 sam/docs/brochure/v8/convert-2page.cjs delete mode 100644 sam/docs/brochure/v8/sam-brochure-v8-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v8/sam-brochure-v8-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v8/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v8/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v8/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/brochure/v9/convert-1page.cjs delete mode 100644 sam/docs/brochure/v9/convert-2page.cjs delete mode 100644 sam/docs/brochure/v9/sam-brochure-v9-dashboard-1page.pptx delete mode 100644 sam/docs/brochure/v9/sam-brochure-v9-dashboard-2page.pptx delete mode 100644 sam/docs/brochure/v9/slides/brochure-dashboard-1page.html delete mode 100644 sam/docs/brochure/v9/slides/brochure-dashboard-back.html delete mode 100644 sam/docs/brochure/v9/slides/brochure-dashboard-front.html delete mode 100644 sam/docs/changes/20260303_gemini_model_upgrade.md delete mode 100644 sam/docs/changes/20260304_eaccount_infinite_loop_fix.md delete mode 100644 sam/docs/contracts/CHANGELOG.md delete mode 100644 sam/docs/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx delete mode 100755 sam/docs/contracts/docx/비밀유지서약서.docx delete mode 100755 sam/docs/contracts/docx/영업파트너 위촉계약서(단체용).docx delete mode 100755 sam/docs/contracts/docx/영업파트너 위촉계약서.docx delete mode 100644 sam/docs/contracts/markdown/01-service-agreement.md delete mode 100644 sam/docs/contracts/markdown/02-nda.md delete mode 100644 sam/docs/contracts/markdown/03-partner-agreement.md delete mode 100644 sam/docs/contracts/markdown/04-partner-agreement-group.md delete mode 100644 sam/docs/contracts/revisions.json delete mode 100644 sam/docs/contracts/scripts/extract_to_markdown.py delete mode 100644 sam/docs/contracts/scripts/sync_check.py delete mode 100644 sam/docs/data/interview-master-questions.sql delete mode 100644 sam/docs/features/academy/fire-shutter-image-prompts.md delete mode 100644 sam/docs/features/approvals/README.md delete mode 100644 sam/docs/features/approvals/api-reference.md delete mode 100644 sam/docs/features/approvals/db-changes-and-model-sync.md delete mode 100644 sam/docs/features/approvals/form-types.md delete mode 100644 sam/docs/features/approvals/ui-screens.md delete mode 100644 sam/docs/features/approvals/workflows.md delete mode 100644 sam/docs/features/barobill-kakaotalk/README.md delete mode 100644 sam/docs/features/barobill-kakaotalk/esign-notification-guide.md delete mode 100644 sam/docs/features/business-card-request.md delete mode 100644 sam/docs/features/credit-evaluation/README.md delete mode 100644 sam/docs/features/documents/README.md delete mode 100644 sam/docs/features/documents/mng-document-system.md delete mode 100644 sam/docs/features/documents/mng-document-template.md delete mode 100644 sam/docs/features/planning/README.md delete mode 100644 sam/docs/features/planning/construction-photos.md delete mode 100644 sam/docs/features/planning/meeting-minutes.md delete mode 100644 sam/docs/features/planning/planning-views.md delete mode 100644 sam/docs/features/rd/README.md delete mode 100644 sam/docs/features/rd/design-insight.md delete mode 100644 sam/docs/features/rd/planning-design.md delete mode 100644 sam/docs/guides/ai-config-settings.md delete mode 100644 sam/docs/guides/ai-management.md delete mode 100644 sam/docs/guides/ai-model-update-workflow.md delete mode 100644 sam/docs/guides/pptx-generation-guide.md delete mode 100644 sam/docs/guides/server-how-it-works.md delete mode 100644 sam/docs/guides/table-design-guide.md delete mode 100644 sam/docs/plans/SAM_General_Rule_Storyboard_D1.0.md delete mode 100644 sam/docs/plans/ai-quotation-engine-plan.md delete mode 100644 sam/docs/plans/attendance-management-plan.md delete mode 100644 sam/docs/plans/block-builder-evolution-plan.md delete mode 100644 sam/docs/plans/design-insight-menu-plan.md delete mode 100644 sam/docs/plans/fire-shutter-drawing-generator-plan.md delete mode 100644 sam/docs/plans/sound-logo-generator-plan.md delete mode 100644 sam/docs/projects/e-sign/esign-storyboard.pptx delete mode 100644 sam/docs/projects/index_projects.md delete mode 100644 sam/docs/projects/org-chart/README.md delete mode 100644 sam/docs/projects/planning-design/README.md delete mode 100644 sam/docs/rules/billing-policy.md delete mode 100644 sam/docs/rules/billing-policy.pptx delete mode 100644 sam/docs/rules/customer-pricing.md delete mode 100644 sam/docs/rules/customer-pricing.pptx delete mode 100644 sam/docs/rules/partner-commission.md delete mode 100644 sam/docs/rules/partner-commission.pptx delete mode 100644 sam/docs/rules/slides/billing-policy/slide-01.html delete mode 100644 sam/docs/rules/slides/billing-policy/slide-02.html delete mode 100644 sam/docs/rules/slides/billing-policy/slide-03.html delete mode 100644 sam/docs/rules/slides/billing-policy/slide-04.html delete mode 100644 sam/docs/rules/slides/billing-policy/slide-05.html delete mode 100644 sam/docs/rules/slides/billing-policy/slide-06.html delete mode 100644 sam/docs/rules/slides/billing-policy/slide-07.html delete mode 100644 sam/docs/rules/slides/customer-pricing/slide-01.html delete mode 100644 sam/docs/rules/slides/customer-pricing/slide-02.html delete mode 100644 sam/docs/rules/slides/customer-pricing/slide-03.html delete mode 100644 sam/docs/rules/slides/customer-pricing/slide-04.html delete mode 100644 sam/docs/rules/slides/customer-pricing/slide-05.html delete mode 100644 sam/docs/rules/slides/customer-pricing/slide-06.html delete mode 100644 sam/docs/rules/slides/customer-pricing/slide-07.html delete mode 100644 sam/docs/rules/slides/partner-commission/slide-01.html delete mode 100644 sam/docs/rules/slides/partner-commission/slide-02.html delete mode 100644 sam/docs/rules/slides/partner-commission/slide-03.html delete mode 100644 sam/docs/rules/slides/partner-commission/slide-04.html delete mode 100644 sam/docs/rules/slides/partner-commission/slide-05.html delete mode 100644 sam/docs/rules/slides/partner-commission/slide-06.html delete mode 100644 sam/docs/rules/slides/usage-plan/SAM_활용방안.pptx delete mode 100644 sam/docs/rules/slides/usage-plan/convert.cjs delete mode 100644 sam/docs/rules/slides/usage-plan/slide-01.html delete mode 100644 sam/docs/rules/slides/usage-plan/slide-02.html delete mode 100644 sam/docs/rules/slides/usage-plan/slide-03.html delete mode 100644 sam/docs/rules/slides/usage-plan/slide-04.html delete mode 100644 sam/docs/rules/slides/usage-plan/slide-05.html delete mode 100644 sam/docs/rules/slides/usage-plan/slide-06.html delete mode 100644 sam/docs/rules/slides/usage-plan/slide-07.html delete mode 100644 sam/docs/system/ai-automation-vision.md delete mode 100644 sam/docs/system/database/codebridge-separation.md diff --git a/brochure/v1/sam-brochure-1page.pptx b/brochure/v1/sam-brochure-1page.pptx index af863d1a47198cf8831eb366a2040ba9935e6965..5fe19a426937eeb406a54df3e2cb4392e5268aa1 100644 GIT binary patch delta 1420 zcmZuxZ)_7~7{BM*&F+6End`3NZjA^V8|#&8?^?Fe5eW%H9Ws;!Hm%*P!;DVdhmj}& zM*^7)w!DPb1jPVmpc!M?b-i`1?t^hABJp1$Uo`H6e?DA;m^n?*81J55ff_D(@BQ8H z{k^~ExqF}IPTtP{>(~P&?QIroA!A&JSB1t)YAnViJc`qvYGWc%wqpF09hnP{ArsV@ zrAlOO*JU-N2~b3WEJym_LF5mC$K1h@>vuQ<+vw(5u#sD=4F#2K*1Fy_Tw`vmAd@V{ zB8zOs>~D5GuGLxuV^+E>*9#Cdlnk>7l(N_JZ2Ho^AC@k>twvZw9|3kieTm8e|Q9cG~X;n@bmUIFaX)O<0mAzC-zwo9BkJzAtSpEL8%e4 zgg*1#Xc;i0Wy8xIbh!iGvwHlJte)8oFF}=05dD5vV@p#g&^t89hliS7exE4%eM(y2 zL~ac=(5s!0N3L`?kl_A@=-EB+3%4$PF;$!X=(I-O(dJHQ6AS4}htqUCJ@P);*;h}e zJK>Xj)5U(K6x3sTVHoV;Xix78ulDssdK-o|grohsc0F0Rv9(;5T>ODhe{Zv^!^lAv z$?s!Ntra&)k|?_PgUv44CrKX3o13N}y4`-Sj|3N2$dQ}dbf;2<-^+nRn~Z4}Pw?94 zwD#d;UYkC}r-zSdZ^g3l?6RC2kA7`|_MCXKTM*pt`^Rft#(4Nylq=mfaD<}=21DV& z;C)^YKgK$0e-Vo4{+FSgtl71pv?Zs=)rBq~cxLME=i&2w;}}&}1ne2k+Xvr1f?UyY z$jfl6Mqq3;aub)}6|4l6`vCf}>gdP}U>(wrX8c*IREX2 z($(>MExY5UhQ0Yz5Uxyy-AQU;6M_bZ215kO`6BFU=Y8aq~9malc-98 z)ArjKFaraFjpRJHns%n3xW))k_1aWXonfz!`m8(7Z%zRX#lb<(rJx4Wd3=g}*I&Re V#;DBE{&|)Dn1b5;o`i10{0HQx$&CO2 delta 1154 zcmY*YZAepL6u#$9*Zi1?v1VJ&P5QEQI^A?mw`M4XAK_5RL@RM&7Mi9%w897lqaV8Q zLN8i>ia?_%(I3lwITiZ?si@ykP!R?FQHe~V`<~sUyKwG#?sJ~=ocF%>y}yRHjjT)M zHIc#d7gj#jr^Ct=C32U9~jOXMQ2aE~S`{kPS;+#-v~M3$75TOU=aMr?AV-i<-9wtGX1 zVAc9F5y6*=N??HGSA9l8Rox^(&|mW-JhH8J08-ej)R&iCA^hjXyYoNF=`W#rgOS}bS#dbO>6)(lgjDc@`)r7LShd*djXJe5c6=XcWE z4e(V`TUt#V?O9~wbQw)(glp2U*yCyhHy_v2(xGqf)nB+o4b4zTYn)KeR_B>?0>R~Z zY;EvLid;e`B!@Xo7rfITw>ko`VQ#|(t~BIEA4503v_~=p9k{`?XBKeF>BKDHrqK_x zki+CO*$XBFRzIj|s~6PxblnRj2tr=4M1*RZ>H}4zuBJ6Uz~e`=0z&Vo5B6`?-bdAT z_{EwqrC?L!_KOC>D%A9if2+AHuxWk-Te<^KjNnNCN)fCCz=psW6m2_#Q2hS{(WxL5 zV*Ps%Di9RT!C?gb5#XB>-*kOWG%F5?%;gYNq4;G8P9#Mi5~`mEla6t=GH$4W=QtKx z{4pc9>1Lo$ouH!?3y`Q~TK)LA+a={V7U>Zg=3{5*+j+>waj*btUeQK13t&d#q(Ddx zv;)<5K?WU*xE+6rEr11Q7AQXj>e4ulg)T}Zy-Cv-#dF|T6lRW1 Y(9T8idp~4^`~iKj2$|9=)4~YuANP$_BLDyZ diff --git a/brochure/v1/sam-brochure-2page.pptx b/brochure/v1/sam-brochure-2page.pptx index 26a7357078e4b47b998e57e103103b7ac504d14c..016084a1b90016ebe0dbf9668abfb466e467237f 100644 GIT binary patch delta 1885 zcmaJ?eQZ-z6o2=0Ev#Kf2X2E+yS8D-a2tKE-}`{gFavCKIMJ~I8DmT&0_$X&K>%4G zQTfm{S3Sru&VgbEZ0p+fZ3)vYK~0D#M*IT}IHM3_D_}GjHv+zQ`wGmUO>Xb+{m$=v z-1F|eZ{iQ_-)%bm@^V#zmXm+GXVm-kW|chgv)QyVOP**Gr25}5Fs5q`V@f1uES)jq z^Wqv>M3{({KnC4NrZavQ5fqOMdhRYKv&!#(yfn+s zoF2-Uqh_Eu>z0Xuq7uEHfr@42LkKwe_`%T7 zw7zlO3meyOXv}NPZ)n<#qr=39O-D#w@VncaSiRyBAe0p>{*XY62j>`kF}^7G5KBL_;#<6C7Lwn-gpT@4}oiFsfX~Clz0MuyS%bht=U09RHUtgPuRV8kegf zQS>bZBYE&Ulajgql?(&bq!OR20KM8OxExlxw*O(;S9cA4oy3c#E|Q4D`-vTsy>Qj? z-}}b2!}L_?GE*K-c;QoaLQVwMB%v3GMf|lAHpR9^O%X>EPM<_)_X4}Ja7ix| z+*7~btEidiVqDZFyUgr^NACq#)29TGY>&qiWa5vKGA=5L$92S+O!Notxn<({fD&>% z0EJBC?uSwacJ#vn28R2Ux^MPF;oS=gJwb3W`_>>VWnd%-3mJfr0v3fJR>Pi<66{Qz zQ4K%^6ITsD_2fGU!vjjj>jRKUYEc(f8d@Bd7I!0opkI{w@lqI4ScrWxJ**>mS(IMV zB^}XXei)Ln9&kIb4@T_oJl+n&Y_lwq_tQ$?IC+@vlW<(<89;p)(y>;$R!p+?;)n?C z8jh1k)+FuH8f+IKFV+jGKuBMUmqc)9%WmrOu_QEdoIK2v+~%zV&gW~PDb+??IS5It zBJbxpuYXK9P9C|FYfPI}82-*%%Alp)Fcd}+2GwEMN@<}tpo|t1ooivZ8$($(#KkPr2+{;xluM?W znh0aZ_-;Cd5KYW*5y#-;TB=4#SR7+Y{NapD4L>x~8JA@Pr=D~A6~rtyIX%yL-sipM zobPtKcIgj|uF|iH(MbH~d9NsYqdC2$blLL#%FM+0Dm(5yaEi7_4=_{%Je;>a~ zwF{>tW42m=k_~@m3gGeTbOHp*{e%PhR>e<(Xy0C=5@3GEFOifNcefB7PuW-(`fTcq zBux!eSJ}GNc9+fVqQ3sLfipM##OSoz3+#oaye*#EqJz!B%(~_hlf&gGaNFgs*NBJE zwYB-Q=X5fLku0co`pRinV?9+ZwZ=wN*cyNv*k4AB^wM|@UOz$NWN&chZpTJ1rBQghRYybWiNEl#Gg&HKbWVoI&9Q-*-ig` ze+_NvU5jm#q+FhP5Q0SQU=I0m-&QDBVewWtkZpB2t%X)o=JO>cx5JrV=yKe3B&=CI zS#A_rqjo$#Ll*w8*_(c-#cex5r_Q&ztyaAH8%e{L1Mn-YoO^_}{iw&DtHh4K?}CLj zY`&R4w7r41cf;pm4xj7+Wj0mSFdZxIP;<(hj5Atd#ycU<$omh#oVX0K>k0TOL%8YM zK^+;X;V2wW7u*Lu&=Aqx+Y5)|Ifo2o$$fngh#VKsgSf$H83b_$@!%lj38iNk((v3M z7{zI35S#*dWk%mO1md0I!$VNCqV672)I#)4RMg5{l4W=x8o(m}qss(fvD5H-CW_kA zaQ(27fK}mGp*y3r2zhZ>33+Q6iiOBI0zLtbjKCHF#zvI7Z;U|k@`FUzD7b{ZVHCCr z5E_M24&EG#>{vDikviMQlvtle88Qw5;Z!*eRjZ#Io*!2dUK{5P0m0clkb)gRG-#fH z1mRaa!EBdf2zG{8>%j>~%;GnHtjHI0fk+Zxo5UwE`q!-`cqIf`xTIC$nC|veqH;+( zrz7Iwm@EARlBDGiY)#+C`x5qoB=MEGqGxDmJ9
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

+

무료 데모 및 상담

+

contact@codebridge-x.com

diff --git a/brochure/v1/slides/brochure-2page-back.html b/brochure/v1/slides/brochure-2page-back.html index fbd68ab..a285d16 100644 --- a/brochure/v1/slides/brochure-2page-back.html +++ b/brochure/v1/slides/brochure-2page-back.html @@ -209,17 +209,19 @@
+

무료 데모를 신청하세요

귀사에 최적화된 맞춤 데모를 제공합니다

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

\ No newline at end of file diff --git a/brochure/v1/slides/brochure-2page-front.html b/brochure/v1/slides/brochure-2page-front.html index ae770d9..fe6da03 100644 --- a/brochure/v1/slides/brochure-2page-front.html +++ b/brochure/v1/slides/brochure-2page-front.html @@ -110,8 +110,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능과 가격을 확인하세요

diff --git a/brochure/v2/sam-brochure-v2-dashboard-1page.pptx b/brochure/v2/sam-brochure-v2-dashboard-1page.pptx index 929effcc3a293991d6d93343a896883fd437e85d..d5fb8783d50d8bab565d050fbc1eab4de54e7ac5 100644 GIT binary patch delta 1465 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 delta 1609 zcmZ8hYfKzf6rOtz3uWI=2&_D{sFZGZclO10L1@t`uw76rMvDa-6JMktB%m<}k=Xt) z0vY4Ms%>c0RzzCGIzK82{ox}tt@tR>L@Jo3{ef107#pdn&YYd);U;(QH{UtuJ9EyQ zd;gs<4jeU_8tNcTFQ~tY#{DCvB2YKqZL~d9pl;Fu?iv}g5_7DCm@0)?%qC`~H02}3 z7&%E*JQtrbg5I!BS?XhcEi_j;&+LTzST24z&&|>`t|ua!r}WD-)y-OvOPb)6Q2K%= zg>LXIWV;i5>OR#1-39{Rn_uDt9=46@HYujP(~sUEQoOA)))sxdx4UqEZ$)u1926q~xpfzh zF|@{*4ilG|2j`;f20Tz5$FXy)1KR_<73adtfHN`XOxI#M_c1fWL>te-hFNC9q4#(I zhu`65ybxpo=}gNNQ%_Hir>m{q^JKT@>1UiCz-zD|%J<2&RqQdUL_WEbNmPn_ z_dRSjo!WOMTAhX`PxCU2J3ueTAr^B3==89^(9BK)m}WyrJF`OZwTDqK4u?AIr!7C+Gg%^fc5 zVVmlNc?Jj)s5OBbI}G5ar&kRSBXHgT(UhYbHya^0Wp`tX5$FKpfDv}A(M#)SHx`=U zcCsl6LTgS(Oj@k+0&ZL|t=Dy#*L2<3WYz@w%&?sVJ~KnL3SPHBB@xvYP5OBYY+pTt zIBkJ2*{@rmmOzaa?jrDV3VdbN3eB)-V)xoK=AaGg$$8!e%_emMSC1J+?GU0wl{La3 zPC6iq4dRepYwB4$xRuLh!44i0Fgw7_uh!!h2Skchp|w$Ff{U)h)ZfZ(CP=3;U3l05 zMhbpk35M9a97pVsjhB>EMXJP)Gcq4e7X)<`q@rp6_Bh9%l{Y}I6KsX5Z*TTR`5Y4j zb(L=LJvxbPPKZ#Vx18Y4`1lkTIy$-~TXR1C@`jIbzpbN*eqF4Ou`JF{%XURya0h L!5!&c07~e8>T|?c diff --git a/brochure/v2/slides/brochure-dashboard-1page.html b/brochure/v2/slides/brochure-dashboard-1page.html index 8e1c3cb..eca646a 100644 --- a/brochure/v2/slides/brochure-dashboard-1page.html +++ b/brochure/v2/slides/brochure-dashboard-1page.html @@ -248,10 +248,12 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

+

무료 데모 신청

+

contact@codebridge-x.com

diff --git a/brochure/v2/slides/brochure-dashboard-back.html b/brochure/v2/slides/brochure-dashboard-back.html index 2f446f0..147b8bf 100644 --- a/brochure/v2/slides/brochure-dashboard-back.html +++ b/brochure/v2/slides/brochure-dashboard-back.html @@ -224,17 +224,19 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험해 보세요

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

\ No newline at end of file diff --git a/brochure/v2/slides/brochure-dashboard-front.html b/brochure/v2/slides/brochure-dashboard-front.html index ac67529..0df1434 100644 --- a/brochure/v2/slides/brochure-dashboard-front.html +++ b/brochure/v2/slides/brochure-dashboard-front.html @@ -160,8 +160,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요

diff --git a/brochure/v3/sam-brochure-v3-dashboard-1page.pptx b/brochure/v3/sam-brochure-v3-dashboard-1page.pptx index 307d9331fbd6b77b668f5d8debb8e6d19f5c92c9..cbb60848d6633e033cf2da6d56e1773df2997004 100644 GIT binary patch delta 3400 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 diff --git a/brochure/v3/slides/brochure-dashboard-1page.html b/brochure/v3/slides/brochure-dashboard-1page.html index f6316e2..8547c7d 100644 --- a/brochure/v3/slides/brochure-dashboard-1page.html +++ b/brochure/v3/slides/brochure-dashboard-1page.html @@ -392,10 +392,12 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

+

무료 데모 신청

+

contact@codebridge-x.com

diff --git a/brochure/v3/slides/brochure-dashboard-back.html b/brochure/v3/slides/brochure-dashboard-back.html index 95eb751..f199750 100644 --- a/brochure/v3/slides/brochure-dashboard-back.html +++ b/brochure/v3/slides/brochure-dashboard-back.html @@ -354,18 +354,20 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

\ No newline at end of file diff --git a/brochure/v3/slides/brochure-dashboard-front.html b/brochure/v3/slides/brochure-dashboard-front.html index 5223cb5..b1d8828 100644 --- a/brochure/v3/slides/brochure-dashboard-front.html +++ b/brochure/v3/slides/brochure-dashboard-front.html @@ -250,8 +250,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요 ▶

diff --git a/brochure/v4/sam-brochure-v4-dashboard-1page.pptx b/brochure/v4/sam-brochure-v4-dashboard-1page.pptx index 2ec5fe91c1606a96c977f6009a2fdc9964766ae2..747d4f45fc512f379904382c676cce91bb854a78 100644 GIT binary patch delta 3301 zcmZuz3s6+&6~6z$T@{w_5D*v9m6${dt9$R=-MfpZm}bmOz_===I8qUTp-6cx4*?Oj zG1KX2P{;-U#ym7?s?GRl**G^k_{=0u)6weKRHv3|ns%CGceGA4Monw~$3@(|?##dU z{NMS`cg{U~_P=+_tFcG7#m4&zW5&e_^z+oB^8WZ4G4vwNWS5fZ^rALEYX7!GhU|Wp zAq51vKA9o+e>BQb^ARJd1z=TcP%^{2k)%gh)pL(9YEMwV0?wZdqpG>O)-!rcs(Jue z8ue4)Iu9U+`Lbg~dJX!~$4hPKMXH{oUhBM?5<2i~cJ@0K9Mj-Bx|qJMp~)^w;!?hWeKBik5=tn|`cmGx6|;C=Oe8qL=6i zrLEkF-cMi_I*dLuGwb{jl*o=P?F$WEn#M5gFUZ8PlUGn*Cc~C}hnm>Ir+s=C{g@rH zngxNF#JmN`P{O@$O@c3Z>@U*b&+KH>N~~}h*opWzsn>EQK@iR);EQ)ry0#({F7k@9 z?XZBIuv)Dbig<0u7Ql8hi_5q<|Kc5!AlxzGrK@0ywtf+e%wW5sCdeBL+1sRt5K=ycxBKDwv8XkE9!1a$yweZQF&B|{ zk8v`ab!Ae6YYZOHVHSF+Sr7ehGt6b2xh=4e89gm9pBblGbk%z;F!#}O7U#Eu!u)Nm zz-AAB+zJaAVrtWk7u$I8_?KFDkw2qzszHr(9~Y|3yig&T$u{`(P>gJ*Sug=v}!uUqgm zL7;2Ks2sb#dIB%)fJy2{#fiDTLIf$oqxYoJm(ONzJwe8TuH1;Ik`Wrkf4i+6B;}fG z2>daK2J)fD^ew|3g7D1n_}%Du|q%O zu|++4Q`S)Q;5v$uk*Wl@1B|+0?|T^mN5F0lhQ1Rg1GeCK>69%<66C{NEC3)|3+gl2B^p=*$-jh;wJv;^aH#TGLM(=PPy-0)EC37IXC!7+nJuD; z3m&aK+KoUqnnW`fWN$s`9pF+zjv!?cE5>W~(T3!J0?Yd%HTs8qK1lY|L#^ z*Y-k1?tw4erDTk?+C+(UvPs?Rk4n0cSV$`+*2^aK`%qNi{_;XeMyn*NlZK*R9iA`$!tLTEkRU(r%ORSAe>KAVyyUt|} zWUWkU?F^I=?2=8UPVS$zaFbe5jCQ!WW=rN+RjNRip03#@vWZN5AF6&0sM)O>iB%R& zd?%_i8qtT{6fj$is^2?ktX?lmHuBVM$O}=%PxrywC1g-HVKf5uiz5$2Y(z|A!t9Yb zrA1+LOGib-%H~*ER@)|`c%;;1n{cNIUC~V$ovyU~6-5ZWa&}fSZrOrr+_=j z$KLLvWZxT&JGLPoW#7F4%BY!FaPv0Q!(-{}yb%kEM&vG`26Q|Ux9&vK=)MH@WlXEa=Zs`z(k9Z+bqBD)62VsuP4z zoRI^;YS0{r=92L_P|VTs`S6IF9$pCfoQui_tuKj-VIm*hPzZL8#w~@193>S)E<1FT=8!IX6Dv;o?v~;D7S^odD+T~i>r^_LpiEb^2(F};?kjOxO zx#qa59O7U6(%?VJ!N$f1S3n8_g%yy*fZYKeQ~;ktY^7#aT&Xb!DA- zUiZQS1c634`~7!(oLBMwYKUQ~*H1^TJB9>-Mzlk~u6r)x=hfQEB-D@+2QDLA*?}T( zMU7Uuk0G#Du z3n**ujnCB*ZF4Oc*jx{v&b!+NfPUWp5JrG0iRN%X14pL3$ihr_DF94nj~W zK3hv_71U|2S50fZPWlQ04QJOOq1@!4L;CxS8(X}+1c8RLw7`TI<8_2X89MX_GiRT{Z1%ZaMblf;DZQV=m?T32p-hQS9 z3)A?JUxQXK((Te)f09FhhO^6$=~OzDO?F`e$-SS!bGMRQ#_p$V$287%v><)-q*Mg{ z!i$&5tX`XUwd=`Z&~SEtmdVqGt`^la diff --git a/brochure/v4/sam-brochure-v4-dashboard-2page.pptx b/brochure/v4/sam-brochure-v4-dashboard-2page.pptx index 84ae7aab64a0c3c7916b97c3d6279f175c02fe49..c56e36ebe40aae0300bcf4c0b63d445ade05427b 100644 GIT binary patch delta 5148 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 delta 4859 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 diff --git a/brochure/v4/slides/brochure-dashboard-1page.html b/brochure/v4/slides/brochure-dashboard-1page.html index 7920035..8792a24 100644 --- a/brochure/v4/slides/brochure-dashboard-1page.html +++ b/brochure/v4/slides/brochure-dashboard-1page.html @@ -392,10 +392,12 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

+

무료 데모 신청

+

contact@codebridge-x.com

diff --git a/brochure/v4/slides/brochure-dashboard-back.html b/brochure/v4/slides/brochure-dashboard-back.html index 3de9eb6..6b64b85 100644 --- a/brochure/v4/slides/brochure-dashboard-back.html +++ b/brochure/v4/slides/brochure-dashboard-back.html @@ -354,18 +354,20 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

\ No newline at end of file diff --git a/brochure/v4/slides/brochure-dashboard-front.html b/brochure/v4/slides/brochure-dashboard-front.html index 98d48c7..8f4165f 100644 --- a/brochure/v4/slides/brochure-dashboard-front.html +++ b/brochure/v4/slides/brochure-dashboard-front.html @@ -248,8 +248,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요 ▶

diff --git a/brochure/v5/sam-brochure-v5-dashboard-1page.pptx b/brochure/v5/sam-brochure-v5-dashboard-1page.pptx index f6b930e8c5ab0ad6d8bab01b9768f27de89f4788..60194bf414575bb0fa6214c3b5832ada8e5d10f8 100644 GIT binary patch delta 2921 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
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

+

무료 데모 신청

+

contact@codebridge-x.com

diff --git a/brochure/v5/slides/brochure-dashboard-back.html b/brochure/v5/slides/brochure-dashboard-back.html index 46012b5..aa9f629 100644 --- a/brochure/v5/slides/brochure-dashboard-back.html +++ b/brochure/v5/slides/brochure-dashboard-back.html @@ -292,18 +292,20 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

\ No newline at end of file diff --git a/brochure/v5/slides/brochure-dashboard-front.html b/brochure/v5/slides/brochure-dashboard-front.html index 013a249..ba19d78 100644 --- a/brochure/v5/slides/brochure-dashboard-front.html +++ b/brochure/v5/slides/brochure-dashboard-front.html @@ -204,8 +204,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요 ▶

diff --git a/brochure/v6/sam-brochure-v6-dashboard-1page.pptx b/brochure/v6/sam-brochure-v6-dashboard-1page.pptx index 0fbb21dfb2d7db595eaa28990c48f787e1ce976e..a9fa7c071d0fb8e8e3bef33b99ba86d1f8baa678 100644 GIT binary patch delta 3312 zcmZuz4Nz3q72bQ6r9c3e6$BTBWe1I}EbM!4-|oIu;?I!&2nq=}YNfFR2^zEfSQSuy zMQED_kUZ_}j55{u7h_DoP5PK58Zn7BX`E*IGn%nY(9sNLcl?=Xr;}>i`|f=!?yk&S z-r4Vb=R4<~ci(yM`@V$N_a-Ej73*UXbn2f}Q8k#9qgO9nF1@VERxffrqzw)jDe~HE ziYy|?csfPCw}m+J0z^sjA~55Z+8{1mTW&|Z7HpO;AQQFQk!r`u6Tm{#A(S9blbxl- zq{(jr%_yG(rt=?|M}2ekoO<>DOn)Js8up}apoV`+t09iKDx2JuO@4RUPDtP?IEPmd zydo!ml3x7iPjiNo20fE;Ach+EX56<><8I;2L~2}LT8vaf7D}#Bz`U$dPmPwc083e0 z-j0&hltTaT#-U228>;kwSFv_Rnv>@xx1DpldBMf;f`fB%b_dQGLVNrV8v9I9e%{5~ zIT^{j{mvdGX=-G+z&ri_d$l|{HOe9Jc09-t+4~31CPm3QMYr8aW!Dd1GDf*Y-f0)9 z``fV<`e?T#*j?0pb6vcAnF+44~h~MmCot_f`2~5AyyX z^3HMOmfA^g$#GNO9YQB!47?;cJ$Tttlo{jXco8=)MrARgAo0pb53(V7TruOUL&!Is z^SC9Cwcx1*Ro&bAsq!_;mx42VVHnJSTYYgSo*|y94sR!00Q1BOeBj zb_Vfa@a04DNJAbTKa65<_EB^uRtp^bG#Sb7@0+H?A44U`SY5xa=J`z<*45V7oAayd zeI#fe-ai99Z6deNYN@O8)f8GQ)Z`M68;_#W*`na$oi3}Th1BAvKf(*-c;4l7;Nv#* zFZsYevwUVEU*7feBz(INWy!-IUQP|X`A*>AE0#dtaNv#CEy3UH51u^nFl`FQJHpdq zZUHKtBRrbcBYFf$dR$roqY8AF=3Ufj$6p^tygc52IitG1ZgW-j=0%U$F6)u}=PS9` zcLqIyPrZSbs^dyYIF1faq1MUw&}XsK>KH>tNKr?Zp?EwIXX$k6%8|!Vsytd@R6hO) z9mt~0^;eOBPCKRH8rn6JVtRl^IvbTHJ#3_NSh=1Ef1zVtd2c4HU}Nz?#@?x?L8m(n z_;w;>C}K8TU|I$%{F4q-<@zl69Ow{zE!lY6m8jFX5*hpA9C(_JVkKJ)S+$Fy-9T%Y z_#uoMXxzUG*S@&gpwq23V9rOfoGXD1h9q@w`8O}zvmu?%h8;ebj?20sR$22bI9YX$ z`&mD_q}S;#=~>*n4SyG%w#WS(6T%SHdeRv)xfBQ=vbHz zerqR5GouP@OyVc&V7;D}Bbk2HbDorQo|dCrtc8oLiz6Fg6C=;|ft7WrxE|Ir?}-K& zV^-rPNMPRf&ETULzPSbH8xdQ#0)409)mx!JZ4q{~Kqfx270mS4M_a)~jXTW*PH%!7 z`fGj@cqiF%lg6f`KZQuQnqbLfY-zI=OW;i0-waPu-)Qq>f(0Q-aWbsC<VggPj7c1khXli15G| znwM8rj38C_@Z&1{xuTz3p?|_>_)a?%Skx1`{QTVh)8t5~D=$Q#*Z3PMYbt*ED$2xl z9YCK1+}1&IrUVe~yoRj!R0#f@z;GxI-{=4@)h&7phNO7%nMYQ7m~KV_zS0G$cx5M~ z&~0q&B(c3y8M-fo4y)*zT#9l+enVYt#|^#GaYYVvWPdfEVu2I<#DM#r>GMI(BHXOLEUH z=^-;hT^V8h`1LYI{dfm?($%NtOCo@Za91}>!%uW;JIL-Ps()R@sAP_qXt9$MBvMuQ zDWm?DP^s#1RbcUR4E;w4y`!Q9&ok7zQHIcGKHH^BRP^oO&P9m_omZ4DgJ~*eBFqStl#up6`i&ynRBF&Td>g`<@Ne*>|=P|V~{_lFV WLQkkF+1nZVRWD47?Q11NTK9i3sl3bp delta 3027 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 Hy@LM(I%6?v delta 4423 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
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

+

무료 데모 신청

+

contact@codebridge-x.com

diff --git a/brochure/v6/slides/brochure-dashboard-back.html b/brochure/v6/slides/brochure-dashboard-back.html index 6cd9c34..792d610 100644 --- a/brochure/v6/slides/brochure-dashboard-back.html +++ b/brochure/v6/slides/brochure-dashboard-back.html @@ -316,18 +316,20 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

diff --git a/brochure/v6/slides/brochure-dashboard-front.html b/brochure/v6/slides/brochure-dashboard-front.html index d21776b..d25994a 100644 --- a/brochure/v6/slides/brochure-dashboard-front.html +++ b/brochure/v6/slides/brochure-dashboard-front.html @@ -217,8 +217,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요 ▶

diff --git a/brochure/v7/sam-brochure-v7-dashboard-1page.pptx b/brochure/v7/sam-brochure-v7-dashboard-1page.pptx index 3fb887d47d7f91c03954182e876807077892cadb..6f12fd7682f75f48865c5243687dff8773bfb0d3 100644 GIT binary patch delta 3257 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 diff --git a/brochure/v7/sam-brochure-v7-dashboard-2page.pptx b/brochure/v7/sam-brochure-v7-dashboard-2page.pptx index 9f37b2f3f5abde6eb00990e08ac7b1ace3ac4344..20a6bd175e420d5d08a40147b2b44ba61a39c5ee 100644 GIT binary patch delta 5449 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 IIO!ANe_6_g=l}o! delta 5067 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
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

+

무료 데모 신청

+

contact@codebridge-x.com

diff --git a/brochure/v7/slides/brochure-dashboard-back.html b/brochure/v7/slides/brochure-dashboard-back.html index aab44bf..7298655 100644 --- a/brochure/v7/slides/brochure-dashboard-back.html +++ b/brochure/v7/slides/brochure-dashboard-back.html @@ -354,18 +354,20 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

\ No newline at end of file diff --git a/brochure/v7/slides/brochure-dashboard-front.html b/brochure/v7/slides/brochure-dashboard-front.html index 8d72199..1a4ccdd 100644 --- a/brochure/v7/slides/brochure-dashboard-front.html +++ b/brochure/v7/slides/brochure-dashboard-front.html @@ -266,8 +266,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요 ▶

diff --git a/brochure/v8/sam-brochure-v8-dashboard-1page.pptx b/brochure/v8/sam-brochure-v8-dashboard-1page.pptx index 4f20645919856a1511ae0e3df5509c49cb9584ec..4a2c33d0521b53039bfb34b78c3610b6ee48d6cf 100644 GIT binary patch delta 2464 zcmZ`*dr(wW9KPo)i#%LiUa~GCm*u&z7w&8C-Q^X~G!|&Wh^0j;t&C$Lh|-LhIWpQm zh01!Rlc278b<`d5f zPZ$BePy34|;C}I)i3HrLs00`&!pxrtVVzZLAfUC%uSG7Ivkpwck!HKk_HaI+9?tLo ztp-{XL|zu$3@6LHAaT4XIAwuh(YHImw*K8sCrt67b(wWDXm|(c?Em}SLDM+7EQ<`U z$rrbu9bb2eEF)<3;jR_0jMKYWnGv=6FZZ^ZV?*QJvTq9nX)&QfkyVW)BJ|b91FtzQ zL@9Et%Lg*R^zl+ob}}w3?etxcz-IF}Z(igWx7J{I5qxN#66!6mE`}p-t<(3yeDGdr zxF89<=u*IFQn-^9C0RoEwu4zlPL?>Q()Nz*Thj$f!MIHdveWm?4gi3Wmn4ZpBRj!p zhUjKFx677aQdCz^zq*C?tS+{3JnLqivW@n%stZ97lrJx~q3#9X7;q`o`#7azi_hhfB$JZ@IiOrc;D^Lj{+M90$tmb=2f{see!OwKUR`hp5<+{YUh&F zKLxkTH?j*nZ$O9kfrZ(EV$}_QN_k$t^0g7u;98-80ac3cxFiY9}bO zTSqLOrpA^!PfMw%X>t9VE0-=_QqQc;^E55T`DnNs%$V%9(T#P>>x*qQ_yIYkxwk4f zQ{p){$Ng`n&)Wlb0dr^o0$V_HjK~JN^Qf&1%tjIWfypRv0`ElC9+0HWU2sQP(K!v3 zT0x$2=~^yY6bY`_vIBch+XI8!{K`)MK)e5wF9HYF2aq@L{yuswJ*R9I&7d_0z&U){ zeB}p00y*lw*N%Y;;e^W?0A_LoeFZ1Mchd=Hz6^{SXSo78k_mVCA!sIt*q1U2mT4}V z4DfZ0^M}D)O)+%>JgT9Kk?^rr9g2pH8Y(ftN^p&Z^FcLZhF()sq^C_AGIRs7m9rW8R@Cq!(xm#9 z6X3~&g#_%$gk=QW%7i5ZWM=7(3$tM1lZzPjWI>tKuV%pt0_@rFIRciAfxc`#Cy=e1 zF?OA4u)~?8`IH^L8gkbnItMyweE)7O*OVQKyfXpAmI# z#^9wwNKtqMM-5ta-5+CiQTeJJlT7G%RFto$RAHJ;(diD$Gg2Ch{f&`o0v?v2cm91U b(M?4|`TDb5#dh)Oy4@reCWL3%RUP#&{lK1v delta 2167 zcmZuyeQZ-z6o2=$9oxD}*#{jRb87?4u6^D6e(hLwlPC#;z}!TMgRu}}U@{Ph0fUIa z5dXl8FNF5bAG?y zx$pM&cKo;egO~GtOPXEoLV>@YY+2Ro3%U3p&%)v9Dn3~2pscsYkC_8=Fw@8~mjjp? zubtpn4FpM6BLuD0q!{zth~g-M*15Ymj`OW+VDkeo;Tm(iW;>cP>o8y&>kF`bZbKAn z5tq!z^m5m^0tBLO2OvmK-Rj9_rS_j*G}orhve%$cZIh*X_SRJEbfEeF(_p!VqL%Uj zH-f)r{!@+MX6i-}f*%(&6AmnO;Z;lo7qz<(Y*}*MZuwm6Hu3;(`Ekpuw`y96P}7?J zvkijNWmPg|O_WVj(N)FJ6gim`C97q0Pr85IhrZmprs<-L_1iBGO;XooMO3ifx$E=E z>PbUXv3|C1{nFB${)U-IFCKkpT5e}kQ$?LCGu1A#%bzPv$VTR538|Sxn@H-SiQ1mo zs6TN3!c{4$iZbq8Cb^94Y|PViT{RW!)U^t? zrpktO;rYcLMbeY3&0A(>T_0&Axh}FHi!$ynUAC93@gx*gQR!EE$#|Zj=#pw=A_HWe ziw^E59W|Pg&}CT;S8qv$6Nakkh8(VYxS_ND+0C1(S8Yy(HC>Ss32X6+o0-OgWFINm z*ts^odQ-e>gVj42qq|p<$7t6(WF?A%H8wgE zgte%6x4sf=X{rhi*yu_K8f+Ar1;rk|5^6*Mf2DLy1n_r82O?01rQuqrpf@5A=*BphSU(bn1qjCD@Cbs2ItOg4 zbNUR`Ic9ezGARibB6*htPu+K`&`}BOO^C=)0s4FNaT(%ae)Yur`g(Wg2?CEQyw4{- z7JWbj>#(s4`$EJ|2(36p!n9ohyboy? zPrJS+bo3GtX@}}~8iA3awn{Gd3Re*7Ei&Vp;V5F;2I? z^KO;(Djp|Po&}|JzY2a7--@h!DZ+$20$JkiE_y+Q*&+Uv{Z$2<{$>mw*{mPjz#U8I zB`g3cpvG&L|Xk8HuP z*Aa{{bKK-2o%NJ`PP!*eJX4o66DSq#{wLEGPI<-eP__raBi=`8KRtiw{9F(O9@!m# zEpc>T>Q2Xt+^&5(rb}tv@ku&{3`XfFcPOofvk~2?>)i#c1CR2oZFg5V+_xDQpX&&> Tm{*DE8T6_F{=B7OrW5`HFgHBC diff --git a/brochure/v8/sam-brochure-v8-dashboard-2page.pptx b/brochure/v8/sam-brochure-v8-dashboard-2page.pptx index f149d8357a4a3584e66f92ac125d12f06dcd03bc..ade5f05cf13c04262332a3f57ece415d40ae4ba1 100644 GIT binary patch delta 5153 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 M%8&RehBnafe+fu2F8}}l diff --git a/brochure/v8/slides/brochure-dashboard-1page.html b/brochure/v8/slides/brochure-dashboard-1page.html index bbbec34..fbf62ea 100644 --- a/brochure/v8/slides/brochure-dashboard-1page.html +++ b/brochure/v8/slides/brochure-dashboard-1page.html @@ -218,18 +218,20 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

diff --git a/brochure/v8/slides/brochure-dashboard-back.html b/brochure/v8/slides/brochure-dashboard-back.html index aafa592..28747b2 100644 --- a/brochure/v8/slides/brochure-dashboard-back.html +++ b/brochure/v8/slides/brochure-dashboard-back.html @@ -301,18 +301,20 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM — Smart Automation Management

+

(주)코드브릿지엑스 | SAM - Smart Automation Management

\ No newline at end of file diff --git a/brochure/v8/slides/brochure-dashboard-front.html b/brochure/v8/slides/brochure-dashboard-front.html index ff7f738..10ecdb0 100644 --- a/brochure/v8/slides/brochure-dashboard-front.html +++ b/brochure/v8/slides/brochure-dashboard-front.html @@ -268,8 +268,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요 ▶

diff --git a/brochure/v9/sam-brochure-v9-dashboard-1page.pptx b/brochure/v9/sam-brochure-v9-dashboard-1page.pptx index 1b7de30855676b4163c0fa1796e0768ee5dcc1fe..9a39ae4ce77fefd27677b8933afa0d70a859ebe7 100644 GIT binary patch delta 2154 zcmZuy4Qx|Y6z;kGp==B&Qn$5kv#Ui;qI1_*}0=BQy|B*IMD<{WW9;vyhy z{KyQ#5SPoi1~EEOK{IvG#iFnP3F2QMBnvSa23eGl&@xdsk*##TdtZyJ(B$@h?>p!F z&gpx-=MC>Qe*3O5#aFD=C28pA=`w$Nid{=LekR*i&7d1@K<4&+QyKH&EXFKQm}{ww z8Oe#~-~wPtcmcBDTA0T87I4KB7JT78N3oGaBVfe0k&z{&qPVpqX82@GXvSTL)xzhI zYBq}IvRJm3r+Z+H_QFI4oGGs(1_D#I=#5HfwF6yT7x0>QqKSMN&*i&?d>)@mEqUN7 zJxyeiXVW@$3=BUJu`w`II5>%c>&uFP0_-mNoe`F2R%;p99=Dsr$GS>&yxYqK?FFy{$catxl11cPk|1T} z6&3jNt2Z{;DmNBp@w_OzJy<&M59v;UB}n3e6?HHPGP`<0hudtSz1^YDj)p!yWee~5 zAbkAuTOIF&cb*6zBm=L410>A%E+$KxA=ywy4Q0|<4$Dc}Ymj0QBuS7TRZ9r?Ba;DN z8(2lAXkl*f(q`BUCQp{F-d|T;n6=_Xe`B>x7V-9apGlHMNr`MK%#uWblLd+R-U73Z z_lO*ZIvW{ohI1r)2UN^)E0pNwAIwcC&WXH77Vz22yUDfZU>$L70}Hub3r_NDDd_Q+ zeNIv|6RxCHHq4 z-7ewLwe4qyf=byfFg4Mfn5KXOZ~blYG@g^kSCB}%pxo45J7AKY6D6;R{cEjc%m;&{ zy9$;PTPsY)&E0n`bHayDWrutBhw!1$iMG%ur^83Kg-LsO_eV3xQAH~;Ol2o2Sla=8nJhZL2lOhpQ9oI4Oqo)Pt`3Ys{{Pv7@obhfD-uh(NF~(~m>oALU_hat zVnFe>l3b-+JZ>d5_hMS53pTO{v%DIonPo;aX1OE^^O&|Z3JV$dAqqtdB#p)TUNi>t z?oUbb#u#{6{LC0Eqo5-N&1d`a*xrsK79eER zR+#m0-}|uH+fydxf^sL3%QOq3$QpXz0peFR1ue`avKsU{L;1 z+L%S27Fm(w5Gm0j^+B~Lgch9yvb;w*U$oRL>gh=x=q=08Xy~%X?bx2w3!28praatf z$slRESP$v1e36_(>nqDKF8{rLAhsx^ku^G`KKAW8r3ce5WV-=P=lR^m`Wjy`py`_X zFW1!vzd2fd+Njacl{(I?{5q47J33@y_c2+om=zDIY=J(;`Y0P6R@tq)K<`K_k=uCo zWQWo$UE@on-c@Di<7WNTY+yuXlMS&FGYkrQ>z>N`6xK%X+vW2=7xXC|(>2~U9Hpu3 VRZTvn+MzL3+iyT-LlPiG@gJkAjX3}S delta 1915 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 ige_-C!rtHNC_U+-CV(-{zNIrv
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM

+

(주)코드브릿지엑스

\ No newline at end of file diff --git a/brochure/v9/slides/brochure-dashboard-back.html b/brochure/v9/slides/brochure-dashboard-back.html index 6bd4f11..46e438c 100644 --- a/brochure/v9/slides/brochure-dashboard-back.html +++ b/brochure/v9/slides/brochure-dashboard-back.html @@ -211,17 +211,19 @@
+

무료 데모를 신청하세요

대표님 전용 대시보드를 직접 체험

-

www.sam.it.kr

+

contact@codebridge-x.com

+

www.codebridge-x.com

-

SAM

+

(주)코드브릿지엑스

\ No newline at end of file diff --git a/brochure/v9/slides/brochure-dashboard-front.html b/brochure/v9/slides/brochure-dashboard-front.html index 87a7f93..dc52623 100644 --- a/brochure/v9/slides/brochure-dashboard-front.html +++ b/brochure/v9/slides/brochure-dashboard-front.html @@ -172,8 +172,8 @@
-

SAM

-

www.sam.it.kr

+

(주)코드브릿지엑스

+

www.codebridge-x.com

뒷면에서 상세 기능을 확인하세요

diff --git a/features/barobill-kakaotalk/README.md b/features/barobill-kakaotalk/README.md index 151f1fb..09f1909 100644 --- a/features/barobill-kakaotalk/README.md +++ b/features/barobill-kakaotalk/README.md @@ -1,9 +1,9 @@ # 바로빌 카카오톡 (알림톡/친구톡) 연동 -> **문서 버전**: 1.0 +> **문서 버전**: 1.1 > **작성일**: 2026-02-14 -> **최종 수정**: 2026-02-24 -> **상태**: 운영 중 (전자계약 알림톡 발송 완료) +> **최종 수정**: 2026-02-27 +> **상태**: 운영 중 (알림톡 + SMS + 환경별 분기 완료) > **대상 프로젝트**: MNG --- @@ -25,7 +25,11 @@ | 채널 연동 (바로빌↔카카오) | **완료** (2026-02-20) | 바로빌 관리 URL에서 채널 연동 처리 | | 바로빌 파트너 과금 설정 | **완료** (2026-02-23) | 바로빌 측에서 파트너사 과금 설정 완료 | | 알림톡 템플릿 v1 검수 | **완료** (2026-02-22) | `전자계약_서명요청`, `전자계약_리마인드` 2종 승인 | -| 알림톡 템플릿 v2 검수 | **심사 중** (2026-02-24 접수) | 버튼 URL에 `#{토큰}` 변수 포함 2종 재등록 | +| 알림톡 템플릿 v2 검수 | **완료** (2026-02-25) | 버튼 URL에 `#{토큰}` 변수 포함 3종 승인 | +| 알림톡 `전자계약_완료` | **완료** (2026-02-26) | 서명 완료 알림 발송용 템플릿 승인 | +| 역할 기반 알림 분기 | **완료** (2026-02-26) | 본사=이메일, 상대방=알림톡/SMS | +| 환경별 템플릿 분기 | **완료** (2026-02-27) | `_DEV` 접미사 개발 템플릿 등록 | +| DEV 템플릿 검수 | **심사 중** (2026-02-27 접수) | 개발서버용 3종 (`admin.codebridge-x.com`) | > 상세 등록 가이드: [카카오톡 알림톡 채널 및 템플릿 등록 가이드](../../guides/카카오톡-알림톡-채널-템플릿-등록.md) @@ -310,14 +314,24 @@ $params = [ | 전달 결과 확인 (2단계) | **구현 완료** | 2026-02-24 | | 로그인 페이지 서명 확인 | **성공** | 2026-02-24 | -### 5.3 대기 중인 항목 +### 5.3 완료된 추가 항목 (2026-02-26~27) | 항목 | 상태 | 비고 | |------|------|------| -| 템플릿 v2 승인 | **심사 중** | 버튼 URL에 `#{토큰}` 변수 포함 | -| v2 승인 후 코드 수정 | **대기** | 동적 서명 URL을 버튼에 전달 | -| `전자계약_완료` 템플릿 | **미등록** | 서명 완료 알림 발송용 | -| SMS 대체발송 | **미설정** | 발신번호 등록 필요 | +| 템플릿 v2 승인 | **완료** | 버튼 URL에 `#{토큰}` 변수 포함 3종 승인 | +| `전자계약_완료` 템플릿 | **완료** | 서명 완료 알림 발송 — PDF 다운로드 버튼 | +| 역할 기반 알림 분기 | **완료** | 본사(creator)=이메일, 상대방(counterpart)=알림톡 | +| OTP SMS 발송 | **완료** | 상대방에게 SMS로 인증코드 발송 | +| 환경별 템플릿 분기 | **완료** | `resolveTemplateName()` — `_DEV` 접미사 자동 적용 | +| 서명 PDF 재생성 | **완료** | `downloadDocument()`에서 완료 계약 PDF 자동 재생성 | + +> 상세 가이드: [전자계약 알림톡/SMS 환경별 설정 가이드](./esign-notification-guide.md) + +### 5.4 대기 중인 항목 + +| 항목 | 상태 | 비고 | +|------|------|------| +| DEV 템플릿 검수 | **심사 중** | `admin.codebridge-x.com` 도메인 3종 | | 친구톡 발송 | **대기** | 채널 친구 추가 후 가능 | | 대량 발송 | **대기** | 단건 안정화 후 | @@ -377,37 +391,7 @@ $buttons = [ --- -## 8. API 측 바로빌 연동 (세금계산서) - -MNG의 카카오톡 연동 외에, API(`api/`)에서도 바로빌 서비스를 사용한다: - -### 8.1 바로빌 설정 API - -| HTTP | URI | 설명 | -|------|-----|------| -| GET | `/v1/barobill-settings` | 바로빌 설정 조회 | -| PUT | `/v1/barobill-settings` | 바로빌 설정 저장 (사업자번호, 인증키, 자동발행 등) | -| POST | `/v1/barobill-settings/test-connection` | 연동 테스트 | - -### 8.2 세금계산서 발행 - -`BarobillService`는 세금계산서 발행/취소/국세청 전송 상태 조회도 담당한다: - -| API 메서드 | 설명 | -|-----------|------| -| `issueTaxInvoice()` | 세금계산서 발행 (RegistAndIssueTaxInvoice) | -| `cancelTaxInvoice()` | 세금계산서 취소 | -| `checkNtsSendStatus()` | 국세청 전송 상태 조회 | -| `checkBusinessNumber()` | 사업자번호 휴폐업 조회 | -| `testConnection()` | GetAccessToken으로 연동 테스트 | - -**인증키 보안:** `cert_key`는 Laravel `Crypt` 파사드로 자동 암/복호화 - -→ 상세: [세금계산서 관리](../finance/tax-invoices.md) - ---- - -## 9. 참고 자료 +## 8. 참고 자료 - [바로빌 API 문서](https://dev.barobill.co.kr) - [카카오비즈니스 채널 관리](https://business.kakao.com) @@ -420,6 +404,7 @@ MNG의 카카오톡 연동 외에, API(`api/`)에서도 바로빌 서비스를 | 날짜 | 버전 | 변경 내용 | |------|------|----------| +| 2026-02-27 | 1.1 | 역할 기반 알림, OTP SMS, 환경별 템플릿 분기, 완료 알림톡 추가 | | 2026-02-24 | 1.0 | 전자계약 알림톡 연동 완료, 트러블슈팅 문서화, v2 템플릿 가이드 추가 | | 2026-02-14 | 0.2 | 전자계약(E-Sign) 알림톡 연동 활용 계획 추가 | | 2026-02-14 | 0.1 | 초안 작성 - 코드 구현 완료, 실 서비스 연동 대기 | diff --git a/features/documents/README.md b/features/documents/README.md index 91f3dd9..ab1f4d5 100644 --- a/features/documents/README.md +++ b/features/documents/README.md @@ -111,10 +111,12 @@ DRAFT → PENDING → APPROVED ## 관련 문서 +- [MNG 문서관리 시스템 상세](mng-document-system.md) — MNG 화면 구성, 탭별 기능, 서식 빌더, EAV 저장 패턴 상세 +- [MNG 문서양식관리](mng-document-template.md) — 서식 생성/편집, Legacy/Block Builder, 프리셋, 연결품목 관리 - [DB 스키마 — 문서/전자서명](../../system/database/documents.md) - [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 적용 - Swagger: `/api-docs` → Documents 섹션 --- -**최종 업데이트**: 2026-02-27 +**최종 업데이트**: 2026-03-06 diff --git a/projects/index_projects.md b/projects/index_projects.md index b08bbb9..7ce5106 100644 --- a/projects/index_projects.md +++ b/projects/index_projects.md @@ -1,7 +1,7 @@ # 프로젝트 문서 인덱스 > SAM 시스템 개발 프로젝트별 문서 모음 -> **최종 업데이트**: 2026-02-12 +> **최종 업데이트**: 2026-03-08 --- @@ -18,6 +18,8 @@ | [auto-login](#auto-login---자동-로그인) | ⚪ 대기 | 자동 로그인 기능 | | [migration-5130-mng](#migration-5130-mng---5130--mng-마이그레이션) | 🟡 진행중 | 5130 → mng 통합 마이그레이션 | | [e-sign](#e-sign---전자계약-서명) | 🟢 v1.0 구현 완료 | 전자계약 서명 솔루션 (SAM E-Sign) | +| [org-chart](#org-chart---조직도-관리) | 🟢 v1.0 구현 완료 | 트리형 조직도 관리 (드래그앤드롭, 숨기기) | +| [planning-design](#planning-design---기획디자인-스토리보드-에디터) | 🟢 v1.2 운영 중 | 브라우저 블록 에디터 (Notion/Figma 스타일) | --- @@ -204,6 +206,53 @@ --- +### org-chart - 조직도 관리 + +**경로**: `docs/projects/org-chart/` +**상태**: 🟢 v1.0 구현 완료 (2026-03-06) +**목표**: 테넌트별 조직 구조를 시각적으로 관리하는 트리형 조직도 + +**핵심 문서**: +- [README.md](./org-chart/README.md) - 기술 문서 (아키텍처, API, DB, 프론트엔드 상세) + +**구현 범위**: + +| 영역 | 수량 | +|------|------| +| MNG 컨트롤러 메서드 | 7개 (RdController) | +| API 엔드포인트 | 6개 (조회, 배치, 해제, 순서변경, 숨기기) | +| DB 마이그레이션 | 1개 (departments.options JSON 추가) | +| 뷰 | 1개 (Alpine.js + SortableJS) | + +**기술 스택**: Alpine.js + SortableJS + 수동 DOM 렌더링 (x-for 미사용) + +--- + +### planning-design - 기획디자인 스토리보드 에디터 + +**경로**: `docs/projects/planning-design/` +**상태**: 🟢 v1.2 운영 중 (고도화 진행중) +**목표**: 브라우저 내 Notion/Figma 스타일 블록 에디터로 ERP 화면 기획서 작성 + +**핵심 문서**: +- [README.md](./planning-design/README.md) - 프로젝트 개요 및 구현 이력 + +**구현 범위**: + +| 영역 | 수량 | +|------|------| +| 블록 유형 | 15종 (텍스트/레이아웃/UI모형/미디어/체크리스트) | +| 편집 기능 | 올가미 선택, Undo/Redo, 복사/잘라내기, 서식 | +| 서식 시스템 | 글자색/배경색/크기/굵기/기울임/정렬/z-index | +| 작업 영역 | 사이드바/Description 접기/펼치기, 캔버스 폭 자동 확장 | +| 출력 | HTML 내보내기 + A4 인쇄 (좌표 기반 WYSIWYG) | + +**기술 스택**: Alpine.js + localStorage (서버 API 없음) + +**기술 스펙**: [features/rd/planning-design.md](../features/rd/planning-design.md) + +--- + ## 디렉토리 구조 ``` @@ -232,7 +281,9 @@ docs/projects/ ├── mng-mobile-responsive/ # 모바일 반응형 ├── auto-login/ # 자동 로그인 ├── migration-5130-mng/ # 5130→mng 마이그레이션 -└── e-sign/ # 전자계약 서명 (SAM E-Sign) +├── e-sign/ # 전자계약 서명 (SAM E-Sign) +├── org-chart/ # 조직도 관리 +└── planning-design/ # 기획디자인 스토리보드 에디터 ``` --- @@ -240,7 +291,7 @@ docs/projects/ ## 관련 문서 - [docs/INDEX.md](../INDEX.md) - 전체 문서 인덱스 -- [docs/dev_plans/index_plans.md](../plans/index_plans.md) - 기획 문서 인덱스 +- [docs/plans/index_plans.md](../plans/index_plans.md) - 기획 문서 인덱스 - [docs/guides/PROJECT_DEVELOPMENT_POLICY.md](../guides/PROJECT_DEVELOPMENT_POLICY.md) - 공통 개발 정책 - [CURRENT_WORKS.md](../../CURRENT_WORKS.md) - 현재 작업 diff --git a/rules/billing-policy.md b/rules/billing-policy.md index 5c2dce0..56a5966 100644 --- a/rules/billing-policy.md +++ b/rules/billing-policy.md @@ -54,8 +54,7 @@ SAM 프로젝트의 과금정책 중 **본사 지출 원가**, **마진 구조** |--------|--------|------| | 계좌조회 | 10,000원 | 고객 부담 | | 카드내역 | 10,000원 | 고객 부담 | -| 홈택스 매입 | 33,000원 (VAT 포함) | 코드브릿지엑스 지원 → 본사 부담 (무료) | -| 홈택스 매출 | 33,000원 (VAT 포함) | 코드브릿지엑스 지원 → 본사 부담 (무료) | +| 홈택스 매입/매출 | 33,000원 (VAT 포함) | 코드브릿지엑스 지원 → 본사 부담 (무료) | > **참고**: 계좌조회/카드내역 월정액은 고객에게 전가한다. 홈택스는 본사가 흡수한다. diff --git a/rules/customer-pricing.md b/rules/customer-pricing.md index e233fa6..60a9ff2 100644 --- a/rules/customer-pricing.md +++ b/rules/customer-pricing.md @@ -92,7 +92,7 @@ SAM 서비스 도입 시 고객에게 안내하는 요금 체계를 정리한다 |--------|---------|---------|---------|----------| | 계좌조회 | 월정액 10,000원 | 1계좌 | 추가 1계좌당 10,000원 | 고객 | | 카드내역 | 월정액 10,000원 | 5장 | 추가 1장당 5,000원 | 고객 | -| 홈택스 매입/매출 | 월 33,000원 × 2 | - | - | 본사 (무료) | +| 홈택스 매입/매출 | 월 33,000원 | - | - | 본사 (무료) | | 세금계산서 발행 | 건별 | 100건 | 추가 50건당 5,000원 | 고객 | > **과금 계산 예시**: diff --git a/rules/slides/billing-policy/slide-03.html b/rules/slides/billing-policy/slide-03.html index c32f344..b1e7126 100644 --- a/rules/slides/billing-policy/slide-03.html +++ b/rules/slides/billing-policy/slide-03.html @@ -50,18 +50,12 @@

10,000원

고객 부담 (전가)

- -
-

홈택스 매입

+ +
+

홈택스 매입/매출

33,000원

본사 흡수 (코드브릿지엑스 → 무료)

- -
-

홈택스 매출

-

33,000원

-

본사 흡수 (무료)

-
diff --git a/rules/slides/customer-pricing/slide-06.html b/rules/slides/customer-pricing/slide-06.html index d50a3e8..e3c9461 100644 --- a/rules/slides/customer-pricing/slide-06.html +++ b/rules/slides/customer-pricing/slide-06.html @@ -67,7 +67,7 @@

홈택스 매입/매출

-

월 33,000원 x 2

+

월 33,000원

-

-

diff --git a/sam/coocon/쿠콘_나이스평가정보_API_개발가이드.md b/sam/coocon/쿠콘_나이스평가정보_API_개발가이드.md deleted file mode 100644 index 1c354a6..0000000 --- a/sam/coocon/쿠콘_나이스평가정보_API_개발가이드.md +++ /dev/null @@ -1,685 +0,0 @@ -# 개발가이드 나이스평가정보연계 API - -> Ver 1.0.0 -> 최종수정일: 2024-06-28 - ---- - -## 개정 내역 - -| 개정버전 | 개정일자 | 개정내용 | -|--------|----------|--------| -| 1.0.0 | 2024.06.28 | 초안작성 | - ---- - -## 목차 - -1. [개요](#1-개요) -2. [용어정의](#2-용어정의) -3. [개발절차](#3-개발절차) -4. [API 인증키](#4-api-인증키) -5. [개발가이드](#5-개발가이드) -6. [개발 유의사항](#6-개발-유의사항) -7. [결과코드](#참조-1-결과코드) - ---- - -## 1. 개요 - -### 1.1 설명 - -나이스평가 정보 연동하여 업무를 처리합니다. - -### 1.2 기본정보 - -| 항목 | 값 | -|------|-----| -| Protocol | HTTPS | -| 데이터형식 | JSON | -| Network | 인터넷망 | -| 데이터 제공방식 | 실시간 | -| Content-Type | application/json | - -### 1.3 API 목록 - -| API 명 | 환경 | URL | 설명 | -|--------|------|-----|------| -| [기업]지표-주요경영지표 (OA07) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA07) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]개요-기본정보 (OA08) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA08) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]검색-통합기업검색 (OA09) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA09) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]등급-기업평가등급 (OA10) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA10) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]등급-WATCH 등급 (OA11) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA11) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]신용-신용요약정보 (OA12) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA12) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]신용-단기연체정보 (한국신용정보원) (OA13) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA13) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]신용-신용도 판단정보(공공정보 포함) (한국신용정보원) (OA14) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA14) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]신용-신용도-판단정보 (신용정보사) (OA15) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA15) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]신용-당좌거래정지 정보 (금융결제원) (OA16) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA16) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | -| [기업]신용-법정관리/워크아웃정보 (OA17) | 개발 | https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp | 업무구분 API_ID(OA17) | -| | 운영 | https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp | | - -### 1.4 서버 접속 정보 - -> 전용선 및 VPN 이용고객은 아래 서버정보 참고 - -| 구분 | IP | PORT | -|------|-----|------| -| 개발 | #1 : 183.111.160.137 | 8443 | -| 운영 | #1 : 112.175.51.59 | 443 | - -### 1.5 API 구성/흐름도 - -``` -Application COOCON API Server OpenAPI Provider - │ │ │ - │ 1. INPUT 데이터 조립 │ │ - │ - API 인증키 │ │ - │ - 사업자정보 │ │ - │ │ │ - ├──────── 2. Request ──────────>│ │ - │ │ INPUT 데이터 수신 및 재조립 │ - │ │ [쿠콘 API GW] │ - │ │ │ - │ ├────── 3. Call ──────────────>│ - │ │ [나이스평가정보 API] │ - │ │ │ - │ │<───── 4. OUTPUT 데이터 수신 ──│ - │ │ │ - │<──────── 5. Response ─────────│ │ - │ │ │ - │ OUTPUT 데이터 추출 │ │ - │ - 결과코드/메시지 │ │ - │ - 출력값 │ │ -``` - -**흐름도 설명:** -1. Application에서 INPUT 데이터를 조립하여 [나이스 API]를 호출합니다. -2. INPUT 데이터가 API 서버로 전송되고, 쿠콘 API GW에서 INPUT 데이터를 수신합니다. -3. 쿠콘 API GW에서 수신된 INPUT 데이터를 오픈 API 포맷에 맞게 조립하고, 오픈 API 제공자에게 요청 후에 응답을 수신합니다. -4. Application에 수신받은 응답데이터를 전달합니다. -5. Application에서 필요한 데이터 추출 및 파싱을 합니다. - -### 1.6 API 인터페이스 구조 - -``` -S/W 개발사 Application 서버 COOCON API 서버 -┌─────────────────────────────────┐ ┌─────────────────┐ -│ ┌────────┐ ┌────────┐ │ │ │ -│ │ 공통 │ │ INPUT │ │ Request │ OUTPUT │ -│ │ 입력 │ -> │ │ ────────────────>│ │ -│ │ 항목 │ │ │ <────────────────│ │ -│ └────────┘ └────────┘ │ Response │ │ -└─────────────────────────────────┘ └─────────────────┘ -``` - ---- - -## 2. 용어정의 - -| 용어 | 정의 | -|------|------| -| 이용기업 | API 상품을 구매하여 Application을 개발하는 회사 (S/W 개발사) | -| Application | 이용기업에서 API를 적용하려는 Application | -| COOCON API Server | COOCON에서 제공하는 API가 실행되는 서버 | -| API 인증키 | COOCON에서 제공하는 API를 호출한 이용 기관을 인증하는 고유값. 이 때 COOCON에 등록된 이용 기관의 고유 IP에서 요청(Request)된 API 호출만 정상 처리됩니다. | - ---- - -## 3. 개발절차 - -``` -┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ -│ 1 │ │ 2 │ │ 3 │ │ 4 │ -│ API │ -> │ 개발 │ -> │ 테스트 │ -> │ 운영 │ -│ 인증키 │ │ │ │ │ │ 적용 │ -└─────────┘ └─────────┘ └─────────┘ └─────────┘ -``` - ---- - -## 4. API 인증키 - -### 4.1 발급 - -API 인증키 발급 과정은 다음과 같습니다. - -1. 구매신청 담당자에게 "API 인증키 신청서"가 이메일로 전달됩니다. -2. API 이용기업 담당자는 "API 인증키 신청서" 양식을 작성하여 이메일로 답장을 보냅니다. -3. COOCON 기술지원 담당자는 신청내용 검토 후 인증키를 이메일로 발송합니다. - -### 4.2 개발 - -1. API를 이용하여 Application 개발을 합니다. -2. 자세한 내용은 "5 개발 가이드"를 참조해 주세요. - -### 4.3 테스트 - -개발 혹은 테스트 시스템에서 API 호출 테스트를 진행합니다. - -1. 개발용 API를 이용해서 테스트해야 합니다. - -**[주의사항]** - -1. 개발 혹은 테스트 시스템에서 대량거래 혹은 부하테스트 같이 짧은 시간 내에 너무 많은 거래를 테스트하면 경우에 따라서 정보제공 기관(금융사 등)에서 테스트 거래를 차단할 수 있으니 주의하시기 바랍니다. -2. 테스트 시스템에서 확인한 금융 정보는 정보제공 기관(금융사) 시스템에 따라 정확하지 않을 수 있습니다. -3. 만일 운영 API를 이용하여 테스트를 하게 되면 수수료가 발생하니 유의하시기 바랍니다. - -### 4.4 운영적용 - -개발된 Application이 운영으로 이관되면 반드시 "운영 API 인증키"로 변경해 주어야 정상 거래가 가능합니다. - ---- - -## 5. 개발가이드 - -### 5.1 공통 입력 항목 - -| 공통 입력 ID | 공통 입력 명 | 타입 | 최대길이 | 필수여부 | 비고 | -|-------------|-------------|------|---------|---------|------| -| API_KEY | 인증키 | String | 20 | 필수 | 이용기업 인증키 값 (이용기업 별로 발급됨) | -| API_ID | 업무코드 | String | 4 | 필수 | 나이스 연동: 1.3 API 목록 참조 | -| TR_SEQ | 거래일련번호 | String | 20 | 옵션 | 거래추적 용도 번호 | - ---- - -### 5.2 [기업] 신용-신용요약정보 (OA12) - -#### [INPUT] - -| 개별 입력 ID | 개별 입력 명 | 타입 | 최대길이 | 필수여부 | 비고 | -|-------------|-------------|------|---------|---------|------| -| Companykey | 사업자정보 | String | 13 | 필수 | 조회할 기업의 사업자번호, 법인번호, 업체 코드 중 한 개만 입력 | - -#### [OUTPUT] - -| 출력 항목 | 반복여부 | 출력 항목 명 | 타입 | 최대길이 | 설명 | -|---------|---------|------------|------|---------|------| -| RSLT_CD | - | 결과코드 | String | 8 | 조회 결과 코드값, 정상 00000000 | -| RSLT_MSG | - | 결과메세지 | String | 255 | 조회 결과 메시지 | -| TR_SEQ | - | 거래일련번호 | String | 20 | 거래 추적용도 번호 | -| RSLT_DATA | - | 결과데이터 | Object | - | 나이스 결과데이터 | -| request | | 요청한 값 | Object | - | 단기연체정보 요청항목 | -| requestKey | | 요청 KEY | String | 13 | 사용자가요청한 companyKey | -| requestKeyType | | 요청한 KEY 타입 | String | 6 | BIZNO: 사업자번호, CRPNO: 법인번호, UPCHECD: 업체코드 | -| data | | 기업요약 신용정보 결과 | Object | | | -| listCount | | list 건수 | integer | | | -| creditSummaryList | | 기업신용 요약정보 | Array | | | -| shorttermOverdueCnt | ● | 단기연체정보(한국신용정보원) 건수 | integer | | | -| negativeInfoBbCnt | ● | 신용도판단정보(한국신용정보원) 건수 | integer | | | -| negativeInfoPbCnt | ● | 공공정보 건수 | integer | | | -| negativeInfoCbCnt | ● | 신용도판단정보(신용정보사) 건수 | integer | | | -| suspensionInfoCnt | ● | 당좌거래정지정보 건수 | integer | | | -| workoutCnt | ● | 법정관리/워크아웃정보 건수 | integer | | | - ---- - -### 5.3 [기업]신용-단기연체정보 (한국신용정보원) (OA13) - -#### [INPUT] - -| 개별 입력 ID | 개별 입력 명 | 타입 | 최대길이 | 필수여부 | 비고 | -|-------------|-------------|------|---------|---------|------| -| Companykey | 사업자정보 | String | 13 | 필수 | 조회대상 사업자번호, 법인번호, 업체코드 중 한개만 입력 | -| reqDate | 기준일자 | String | 8 | 옵션 | 기본값(현재날짜(YYYYMMDD) 미입력시 최신일자 기준으로 조회) | - -#### [OUTPUT] - -| 출력 항목 | 반복여부 | 출력 항목 명 | 타입 | 최대길이 | 설명 | -|---------|---------|------------|------|---------|------| -| RSLT_CD | - | 결과코드 | String | 8 | 조회 결과 코드값, 정상 00000000 | -| RSLT_MSG | - | 결과 메시지 | String | 255 | 조회 결과 메시지 | -| TR_SEQ | - | 거래일련번호 | String | 20 | 거래 추적용도 번호 | -| RSLT_DATA | - | 결과데이터 | Object | - | 나이스 결과데이터 | -| request | | 요청한 값 | Object | - | 단기연체정보 요청항목 | -| requestKey | | 요청 KEY | String | 13 | 사용자가요청한 companyKey | -| requestKeyType | | 요청 KEY 타입 | String | 6 | BIZNO: 사업자번호, CRPNO: 법인번호, UPCHECD: 업체코드 | -| reqDate | | 기준일자 | String | 8 | | -| data | | | Object | | 기업신용 단기연체 결과 | -| listCount | | 단기연체정보 건수 | Integer | | | -| creditShorttermOverdueList | | 단기연체정보 | Array | | | -| count | ● | 단기연체정보건수 | Integer | | | -| rcno | ● | 차주대상번호 | String | | | -| rcnoKind | ● | 차주대상구분 | String | | 2: 사업자등록번호, 3: 법인등록번호 | -| name | ● | 기관명 | String | | | -| reqdate | ● | 요청일자 | String | | yyyymmdd | -| basedate | ● | 기준일자 | String | | yyyymmdd | -| organnizationsList | ● | 단기연체정보 기관별 정보 | Array | | | -| orgcd | ● | 한국신용정보원 | String | | | -| orgname | ● | 한국신용정보원기관명 | String | | | -| orgnameEng | ● | 한국신용정보원기관영문명 | String | | | -| principlaOduYn | ● | 연체금액여부(계정과목코드 6909) | String | | | -| accountSubjectsList | ● | 단기연체정보 계정과목별정보 | Array | | | -| acccd | ● | 계정과목코드 | String | | | -| accname | ● | 계정과목명 | String | | | -| kindcode1 | ● | 계정과목대과목코드 | String | | | -| kind1 | ● | 계정과목대과목명 | String | | | -| kindcode2 | ● | 계정과목중과목코드 | String | | | -| kind2 | ● | 계정과목중과목명 | String | | | -| categorycd | ● | 한국신용정보원만기코드 | String | | | -| category | ● | 한국신용정보원만기코드명 | String | | | -| accamt | ● | 거래금액 | String | | | - ---- - -### 5.4 [기업]신용도 판단정보(공공정보 포함)(한국 신용정보원) (OA14) - -#### [INPUT] - -| 개별 입력 ID | 개별 입력 명 | 타입 | 최대길이 | 필수여부 | 비고 | -|-------------|-------------|------|---------|---------|------| -| Companykey | 사업자정보 | String | 13 | 필수 | 조회할 기업의 사업자번호, 법인번호, 업체코드 중 한개만 입력 | - -#### [OUTPUT] - -| 출력 항목 | 반복여부 | 출력 항목 명 | 타입 | 최대길이 | 설명 | -|---------|---------|------------|------|---------|------| -| RSLT_CD | - | 결과코드 | String | 8 | 조회 결과 코드값, 정상 00000000 | -| RSLT_MSG | - | 결과메세지 | String | 255 | 조회 결과 메시지 | -| TR_SEQ | - | 거래일련번호 | String | 20 | 거래 추적용도 번호 | -| RSLT_DATA | - | 결과데이터 | Object | - | 나이스 결과데이터 | -| request | | 요청한 값 | Object | - | 단기연체정보 요청항목 | -| requestKey | | 요청 KEY | String | 13 | 사용자가요청한 companyKey | -| requestKeyType | | 요청한 KEY 타입 | String | 6 | BIZNO: 사업자번호, CRPNO: 법인번호, UPCHECD: 업체코드 | -| data | | | Object | | 신용도판단정보 결과 | -| listCount | | list 건수 | Integer | | | -| creditNegativeInfoList | | 신용도판단정보 | Array | | | -| totaloccCnt | ● | 발생 총 건수 | integer | | | -| bbCnt | ● | 채무불이행건수 | integer | | | -| fdCnt | ● | 금용질서문란 건수 | String | | | -| pbCnt | ● | 공공기록정보건수 | integer | | | -| sbCnt | ● | 특수기록정보건수 | integer | | | -| totalrelCnt | ● | 해제 총건수 | integer | | | -| bbrelCnt | ● | 채무불이행 해제건수 | integer | | | -| fdrelCnt | ● | 금융질서문란 해제건수 | integer | | | -| pbrelCnt | ● | 공공기록정보 해제건수 | integer | | | -| sbrelCnt | ● | 특수기록정보 해제건수 | integer | | | -| negativeInfoDetailList | ● | 신용도판단정보 item | Array | | | -| typecode | ● | 유형구분코드 | String | | BB: 신용도판단정보(채무불이행), FD: 금융질서문란, PB: 공공정보, SB: 특수기록정보 | -| typename | ● | 용도판단정보 | String | | (한국 신용정보원), 신용도판단정보(공공기록정보) 등 | -| gubn | ● | 구분 | String | | | -| delayamt | ● | 연체금액 | String | | | -| causecode | ● | 등록코드 | String | | | -| causename | ● | 등록사유명 | String | | | -| causenameEng | ● | 등록사유영문명 | String | | | -| causecont | ● | 등록사유설명 | String | | | -| causedetail | ● | 등록사유상세설명 | String | | | -| regamt | ● | 등록금액 | String | | | -| regdate | ● | 등록일자 | String | | yyyymmdd | -| relscode | ● | 해제코드 | String | | | -| reldate | ● | 해재일자 | String | | yyyymmdd | -| orgname | ● | 발생기관명 | String | | | -| orgnameEng | ● | 발생기관영문명 | String | | | -| brcname | ● | 발생지점명 | String | | | -| occdate | ● | 발생일자 | String | | yyyymmdd | - ---- - -### 5.5 [기업]신용-신용도 판단정보(신용정보사) (OA15) - -#### [INPUT] - -| 개별 입력 ID | 개별 입력 명 | 타입 | 최대길이 | 필수여부 | 비고 | -|-------------|-------------|------|---------|---------|------| -| Companykey | 사업자정보 | String | 13 | 필수 | 조회할 기업의 사업자번호, 법인번호, 업체코드 중 한개만 입력 | - -#### [OUTPUT] - -| 출력 항목 | 반복여부 | 출력 항목 명 | 타입 | 최대길이 | 설명 | -|---------|---------|------------|------|---------|------| -| RSLT_CD | - | 결과코드 | String | 8 | 조회 결과 코드값, 정상 00000000 | -| RSLT_MSG | - | 결과메세지 | String | 255 | 조회 결과 메시지 | -| TR_SEQ | - | 거래일련번호 | String | 20 | 거래 추적용도 번호 | -| RSLT_DATA | - | 결과데이터 | Object | - | 나이스 결과데이터 | -| request | | 요청한 값 | Object | - | 단기연체정보 요청항목 | -| requestKey | | 요청 KEY | String | 13 | 사용자가요청한 companyKey | -| requestKeyType | | 요청한 KEY 타입 | String | 6 | BIZNO: 사업자번호, CRPNO: 법인번호, UPCHECD: 업체코드 | -| data | | | Object | | 기업 신용 신용도판단 정보(신용정보사) 결과 | -| listCount | | list 건수 | Integer | | | -| creditNegativeInfoCdList | | | Array | | 기업 신용 신용도판단 정보(신용정보사) | -| typecode | ● | 유형구분코드 | String | | CB: 신용도판단정보(신용정보사) | -| typename | ● | 유형구분명 | String | | (예: 신용도판단정보(신용정보사)) | -| causecode | ● | 등록코드 | String | | | -| causename | ● | 등록사유 명 | String | | | -| causenameEng | ● | 등록사유영문명 | String | | | -| causecont | ● | 등록사유설명 | String | | | -| causedetail | ● | 등록사유상세설명 | String | | | -| regamt | ● | 등록금액 | String | | | -| regdate | ● | 등록일자 | String | | yyyymmdd | -| relscode | ● | 해제코드 | String | | | -| relsdate | ● | 해제일자 | String | | yyyymmdd | -| relsname | ● | 해제 명 | String | | | -| orgname | ● | 발생기관명 | String | | | -| orgnameEng | ● | 발생기관영문명 | String | | | -| brcname | ● | 발생지점명 | String | | | -| occdate | ● | 발생일자 | String | | yyyymmdd | - ---- - -### 5.6 [기업]신용-당좌거래정지정보(금융결제원) (OA16) - -#### [INPUT] - -| 개별 입력 ID | 개별 입력 명 | 타입 | 최대길이 | 필수여부 | 비고 | -|-------------|-------------|------|---------|---------|------| -| Companykey | 사업자정보 | String | 13 | 필수 | 조회할 기업의 사업자번호, 법인번호, 업체코드 중 한개만 입력 | - -#### [OUTPUT] - -| 출력 항목 | 반복여부 | 출력 항목 명 | 타입 | 최대길이 | 설명 | -|---------|---------|------------|------|---------|------| -| RSLT_CD | - | 결과코드 | String | 8 | 조회 결과 코드값, 정상 00000000 | -| RSLT_MSG | - | 결과메세지 | String | 255 | 조회 결과 메시지 | -| TR_SEQ | - | 거래일련번호 | String | 20 | 거래 추적용도 번호 | -| RSLT_DATA | - | 결과데이터 | Object | - | 나이스 결과데이터 | -| request | | 요청한 값 | Object | - | 단기연체정보 요청항목 | -| requestKey | | 요청 KEY | String | 13 | 사용자가요청한 companyKey | -| requestKeyType | | 요청한 KEY 타입 | String | 6 | BIZNO: 사업자번호, CRPNO: 법인번호, UPCHECD: 업체코드 | -| data | | | Object | | 기업 신용 당좌거래정지정보 결과 | -| listCount | | list 건수 | Integer | | | -| creditSuspensionInfoList | | | Array | | 기업 신용 당좌거래정 지정보 | -| datSeq | ● | 데이터일련번호 | Integer | | | -| changeHouse | ● | 교환소 | String | 20 | | -| koreantrnm | ● | 업체 명 | String | 70 | | -| korrenprnm | ● | 대표자명 | String | 30 | | -| bizno | ● | 사업자등록번호 | String | 10 | | -| regno | ● | 주민등록번호 | String | 13 | | -| address | ● | 주소 | String | 100 | | -| occdate | ● | 발행일 | String | 8 | yyyymmdd | -| relsdate | ● | 해제일 | String | 8 | yyyymmdd | - ---- - -### 5.7 [기업]신용-법정관리/워크아웃정보 (OA17) - -#### [INPUT] - -| 개별 입력 ID | 개별 입력 명 | 타입 | 최대길이 | 필수여부 | 비고 | -|-------------|-------------|------|---------|---------|------| -| Companykey | 사업자정보 | String | 13 | 필수 | 조회할 기업의 사업자번호, 법인번호, 업체코드 중 한개만 입력 | -| pageNo | 페이지 번호 | String | | | | -| pageSize | 페이지 사이즈 | String | | | | - -#### [OUTPUT] - -| 출력 항목 | 반복여부 | 출력 항목 명 | 타입 | 최대길이 | 설명 | -|---------|---------|------------|------|---------|------| -| RSLT_CD | - | 결과코드 | String | 8 | 조회 결과 코드값, 정상 00000000 | -| RSLT_MSG | - | 결과메세지 | String | 255 | 조회 결과 메시지 | -| TR_SEQ | - | 거래일련번호 | String | 20 | 거래 추적용도 번호 | -| RSLT_DATA | - | 결과데이터 | Object | - | 나이스 결과데이터 | -| request | | 요청한 값 | Object | - | 단기연체정보 요청항목 | -| requestKey | | 요청 KEY | String | 13 | 사용자가요청한 companyKey | -| requestKeyType | | 요청 KEY 타입 | String | 6 | BIZNO: 사업자번호, CRPNO: 법인번호, UPCHECD: 업체코드 | -| pageNo | | | integer | | | -| pageSize | | | integer | | | -| data | | | Object | | 법정관리/워크아웃정보 결과 | -| listCount | | list 건수 | Integer | | | -| totalCount | | 총 건수 | integer | | | -| creditworkoutList | | 기업 신용 법정관리/워크아웃정보 | Array | | | -| upchecd | ● | 업체코드 | String | 6 | | -| lglmgmtRldDate | ● | 법정관리관례일자 | String | 8 | | -| hngno | ● | 사건번호 | String | 14 | | -| lwccd | ● | 법원코드 | String | 3 | | -| lwccd | ● | 법원 명 | String | 100 | | -| crgJudgDeptnm | ● | 담당판사부서명 | String | 30 | | -| crgJudgNam | ● | 담당판사 명 | String | 100 | | -| korentrnm | ● | 업체 명 | String | 100 | | -| lglmgmtdivcd | ● | 법정관리유형코드 | String | 2 | | -| lglmgmtdivnm | ● | 법정관리유형명 | String | 150 | | - ---- - -### 5.8 소스코딩 예제 - -#### 1. [기업]지표-주요경영지표 (OA07) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA07", - "TR_SEQ": "123456789", - "Companykey": "2178149522", - "tpCd": "01", - "fatpCd": "0" -} -``` - -#### 2. [기업]개요-기본정보 (OA08) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA08", - "TR_SEQ": "123456789", - "Companykey": "2178149522", - "idscdcg": "09" -} -``` - -#### 3. [검색]통합기업검색 (OA09) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA09", - "TR_SEQ": "123456789", - "pageNo": "1", - "pageSize": "10", - "keyword": "삼성전자", - "upchecd": "380725" -} -``` - -#### 4. [기업]등급-기업평가등급 (OA10) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA10", - "TR_SEQ": "123456789", - "Companykey": "2178149522", - "pageNo": "1", - "pageSize": "10", - "startDate": "20220101", - "endDate": "20240101" -} -``` - -#### 5. [기업]등급-WATCH 등급 (OA11) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA11", - "TR_SEQ": "123456789", - "Companykey": "2178149522", - "addWatchRsn": "1", - "pageNo": "1", - "pageSize": "10", - "startDate": "20220101", - "endDate": "20240101" -} -``` - -#### 6. [기업]신용-신용요약정보 (OA12) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA12", - "TR_SEQ": "123456789", - "Companykey": "2178149522" -} -``` - -#### 7. [기업]신용-단기연체정보(한국신용정보원) (OA13) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA13", - "TR_SEQ": "123456789", - "Companykey": "2178149522", - "reqDate": "20220101" -} -``` - -#### 8. [기업]신용도-판단정보(공공정보 포함)(한국 신용정보원) (OA14) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA14", - "TR_SEQ": "123456789", - "Companykey": "2178149522" -} -``` - -#### 9. [기업]신용-신용도 판단정보(신용정보사) (OA15) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA15", - "TR_SEQ": "123456789", - "Companykey": "2178149522" -} -``` - -#### 10. [기업]신용-당좌거래정지정보(금융결제원) (OA16) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA16", - "TR_SEQ": "123456789", - "Companykey": "2178149522" -} -``` - -#### 11. [기업]신용-법정관리/워크아웃정보 (OA17) - -**[입력값]** -```json -{ - "API_KEY": "업체 인증키", - "API_ID": "OA17", - "TR_SEQ": "123456789", - "Companykey": "5148145785", - "pageNo": "1", - "pageSize": "10" -} -``` - ---- - -## 6. 개발 유의사항 - -API를 호출하실 경우에는 자바스크립트, VB 스크립트 등의 클라이언트 사이트 스크립트 언어가 아닌 **서버 사이드 스크립트 언어(JSP, PHP, ASP 등)**로 호출하여 주셔야 정상적인 거래가 가능합니다. - -### 서버 사이드 스크립트 사용 JSP 페이지 샘플 - -```jsp -<%@ page contentType="text/html; charset=utf-8"%> -<%@ page import="java.net.*"%> -<%@ page import = "java.io.*" %> -<%@ page import = "java.net.URLConnection" %> -<%@ page import = "javax.net.ssl.HttpsURLConnection" %> -<% -response.setHeader("Cache-Control","no-store"); // HTTP 1.1 -response.setHeader("Pragma","no-cache"); // HTTP 1.0 -response.setDateHeader("Expires", 0); - -String url = "https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp"; -byte[] resMessage = null; -HttpsURLConnection conn; - -try { - conn = (HttpsURLConnection) new URL(url).openConnection(); - conn.setDoInput(true); - conn.setDoOutput(true); - conn.setRequestMethod("POST"); - conn.setRequestProperty("Content-Type","application/json"); - conn.setUseCaches(false); - OutputStreamWriter os = new OutputStreamWriter(conn.getOutputStream()); - - JSONObject inputObj = new JSONObject(); - inputObj.put("API_KEY","발급인증키"); - inputObj.put("API_ID","업무코드"); - inputObj.put("Companykey","사업자정보"); - os.write(inputObj.toString()); - os.flush(); - os.close(); - - DataInputStream in = new DataInputStream(conn.getInputStream()); - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - int bcount = 0; - byte[] buf = new byte[2048]; - while (true) { - int n = in.read(buf); - if (n == -1) break; - bout.write(buf, 0, n); - } - bout.flush(); - resMessage = bout.toByteArray(); - conn.disconnect(); -} -catch (MalformedURLException e) { - System.out.println("MalformedURLException"); -} -catch (IOException e) { - e.printStackTrace(); -} - -//결과처리 -String temp = new String(resMessage, "UTF-8"); -temp = temp.replaceAll("\r\n",""); -temp = temp.replaceAll("\r",""); -temp = temp.replaceAll("\n",""); -out.println(temp.trim()); -%> -``` - ---- - -## [참조 1] 결과코드 - -| Code | 내용 | -|------|------| -| 00000000 | 정상처리되었습니다. | -| GWS06001 | 수신자료 포맷 오류. (REQ_DATA 미입력 오류로 입력값중에 개별입력값 확인 필요) | -| GWS06002 | 수신자료 변환 오류. (입력값 JSON 형식인지 확인필요) | -| GWS06003 | 잘못된 전문번호 입니다. (API ID 오입력으로 API ID 값 확인 필요) | -| GWS06004 | 응답시간을 초과하였습니다. (업무 담당자에게 문의) | -| GWS06005 | 결과처리중 오류가 발생하였습니다. (업무 담당자에게 문의) | -| GWS06006 | 정의되지 않은 응답오류입니다. (업무 담당자에게 문의) | -| GWS06013 | 거래고유번호(TR_SEQ)이 미입력되었습니다. | -| GWS06020 | 서비스가능한 시간대가 아닙니다. | -| GWS09979 | 내부 서버오류 (업무 담당자에게 문의) | -| GWS09984 | 잘못된 인증키 입니다. (API_KEY 값 오입력으로 API KEY 값 확인 필요) | -| GWS09985 | 보안키(API_KEY)값이 미입력되었습니다. | -| GWS09989 | 허용된 아이피가 아닙니다 (접속아이피 확인필요) | -| GWS09990 | 사용가능 아이피가 등록되지 않았습니다. | -| GWS09993 | 서비스 설정값이 잘못되었습니다. (업무 담당자에게 문의) | -| GWS09998 | 인증과정중 알수없는 오류가 발생했습니다. | -| OAPI0005 | Companykey 미입력 되었습니다. | diff --git a/sam/docker/api/opcache.ini b/sam/docker/api/opcache.ini deleted file mode 100644 index 137d2c0..0000000 --- a/sam/docker/api/opcache.ini +++ /dev/null @@ -1,33 +0,0 @@ -; OPcache 설정 (성능 향상) -; PHP OPcode 캐시를 활성화하여 애플리케이션 성능을 크게 향상시킵니다 - -[opcache] -; OPcache 활성화 -opcache.enable=1 - -; CLI 환경에서도 OPcache 활성화 (개발 환경) -opcache.enable_cli=1 - -; OPcache 메모리 사용량 (MB) -; 256MB 권장 (프로젝트 크기에 따라 조정) - 개발 환경 성능 향상 -opcache.memory_consumption=256 - -; 내부 문자열 버퍼 크기 (MB) -opcache.interned_strings_buffer=16 - -; 최대 가속화 파일 수 -opcache.max_accelerated_files=20000 - -; 타임스탬프 검증 활성화 (개발 환경) -; 프로덕션에서는 0으로 설정하여 성능 최적화 -opcache.validate_timestamps=1 - -; 재검증 주기 (초) - 개발 환경 성능 향상을 위해 단축 -; validate_timestamps가 1일 때만 사용됨 -opcache.revalidate_freq=1 - -; 빠른 종료 활성화 -opcache.fast_shutdown=1 - -; 최적화 레벨 (0-7, 높을수록 느리지만 더 최적화됨) -opcache.optimization_level=0x7FFFBFFF diff --git a/sam/docker/api/supervisord.conf b/sam/docker/api/supervisord.conf deleted file mode 100755 index be1dada..0000000 --- a/sam/docker/api/supervisord.conf +++ /dev/null @@ -1,36 +0,0 @@ -[supervisord] -nodaemon=true - -[program:php-fpm] -command=/usr/local/sbin/php-fpm - -[program:nginx] -command=nginx -g "daemon off;" - -[program:queue-worker] -command=php /var/www/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=/var/www/api -autostart=true -autorestart=true -startsecs=5 -startretries=3 -stopwaitsecs=1830 -stdout_logfile=/var/www/api/storage/logs/queue-worker.log -stdout_logfile_maxbytes=5MB -stderr_logfile=/var/www/api/storage/logs/queue-worker-error.log -stderr_logfile_maxbytes=5MB - -[program:scheduler] -command=bash -c "while true; do php /var/www/api/artisan schedule:run --no-interaction; sleep 60; done" -process_name=%(program_name)s -numprocs=1 -directory=/var/www/api -autostart=true -autorestart=true -startsecs=0 -stdout_logfile=/var/www/api/storage/logs/scheduler.log -stdout_logfile_maxbytes=5MB -stderr_logfile=/var/www/api/storage/logs/scheduler-error.log -stderr_logfile_maxbytes=5MB \ No newline at end of file diff --git a/sam/docker/docker-compose.yml b/sam/docker/docker-compose.yml deleted file mode 100644 index e89b1e8..0000000 --- a/sam/docker/docker-compose.yml +++ /dev/null @@ -1,225 +0,0 @@ -services: - nginx: - image: nginx:latest - ports: - - "80:80" - - "443:443" - volumes: - - /home/aweso/sam/api:/var/www/api - - /home/aweso/sam/admin:/var/www/admin - - /home/aweso/sam/mng:/var/www/mng - - /home/aweso/sam/5130:/var/www/5130 - - /home/aweso/sam/docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - /home/aweso/sam/docker/nginx/ssl:/etc/nginx/ssl:ro - command: > - sh -c "rm -f /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" - depends_on: - - api - - admin - - mng - - react - - design - - sales - - php73 - networks: - - samnet - - api: - build: - context: . - dockerfile: /home/aweso/sam/docker/api/Dockerfile - volumes: - - /home/aweso/sam/api:/var/www/api - - api_vendor:/var/www/api/vendor - - api_node_modules:/var/www/api/node_modules - - /home/aweso/sam/docker/api/nginx.conf:/etc/nginx/conf.d/default.conf - - /home/aweso/sam/docker/api/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf - - /home/aweso/sam/docker/api/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini - - /home/aweso/sam/docker/api/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini - - /home/aweso/sam/docker/mysql/client-skip-ssl.cnf:/etc/mysql/conf.d/disable-ssl.cnf:ro - environment: - - DB_HOST=sam-mysql-1 - - DB_PORT=3306 - - DB_DATABASE=samdb - - DB_USERNAME=samuser - - DB_PASSWORD=sampass - networks: - - samnet - working_dir: /var/www/api - - admin: - build: - context: . - dockerfile: /home/aweso/sam/docker/admin/Dockerfile - volumes: - - /home/aweso/sam/admin:/var/www/admin - - admin_vendor:/var/www/admin/vendor - - admin_node_modules:/var/www/admin/node_modules - - /home/aweso/sam/docker/admin/nginx.conf:/etc/nginx/conf.d/default.conf - - /home/aweso/sam/docker/admin/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf - - /home/aweso/sam/docker/admin/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini - environment: - - DB_HOST=sam-mysql-1 - - DB_PORT=3306 - - DB_DATABASE=samdb - - DB_USERNAME=samuser - - DB_PASSWORD=sampass - networks: - - samnet - working_dir: /var/www/admin - - mng: - build: - context: . - dockerfile: mng/Dockerfile - volumes: - - ../mng:/var/www/mng - - mng_vendor:/var/www/mng/vendor - - mng_node_modules:/var/www/mng/node_modules - - ./mng/nginx.conf:/etc/nginx/conf.d/default.conf - - ./mng/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf - - ./mng/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini - - ./mng/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini - # - ./mng/www.conf:/usr/local/etc/php-fpm.d/www.conf - - ../api/storage/logs:/var/www/api/storage/logs:ro - - ./mysql/client-skip-ssl.cnf:/etc/mysql/conf.d/disable-ssl.cnf:ro - - ../sales/apikey:/var/www/sales/apikey:ro # Google 서비스 계정 파일 접근용 - - ../sales:/var/www/sales-docs:ro # 영업 PPTX 문서 접근용 - # shared-storage 제거됨 → mng/storage/app/tenants 로 이동 (2026-02-23) - - ../docs:/var/www/docs:ro # SAM 프로젝트 문서 (RAG 검색용) - environment: - - DB_HOST=sam-mysql-1 - - DB_PORT=3306 - - DB_DATABASE=samdb - - DB_USERNAME=samuser - - DB_PASSWORD=sampass - networks: - - samnet - working_dir: /var/www/mng - - react: - build: - context: /home/aweso/sam - dockerfile: docker/react/Dockerfile - volumes: - - /home/aweso/sam/react:/app - - /app/node_modules - - /app/.next - environment: - - NEXT_PUBLIC_API_URL=https://api.sam.kr - - NEXT_PUBLIC_ADMIN_URL=https://admin.sam.kr - - NEXT_PUBLIC_API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a - - NEXT_PUBLIC_APP_NAME=SAM - - NODE_ENV=development - - NODE_TLS_REJECT_UNAUTHORIZED=0 - extra_hosts: - - "api.sam.kr:host-gateway" - networks: - - samnet - working_dir: /app - - design: - build: - context: /home/aweso/sam - dockerfile: docker/design/Dockerfile - volumes: - - /home/aweso/sam/design:/app - - /app/node_modules - environment: - - NODE_ENV=development - networks: - - samnet - working_dir: /app - - sales: - build: - context: . - dockerfile: /home/aweso/sam/docker/sales/Dockerfile - volumes: - - /home/aweso/sam/sales:/var/www/sales - - sales_vendor:/var/www/sales/vendor - - sales_node_modules:/var/www/sales/node_modules - - /home/aweso/sam/docker/sales/nginx.conf:/etc/nginx/conf.d/default.conf - - /home/aweso/sam/docker/sales/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf - - /home/aweso/sam/docker/sales/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini - environment: - - TZ=Asia/Seoul - networks: - - samnet - working_dir: /var/www/sales - - php73: - build: - context: . - dockerfile: /home/aweso/sam/docker/5130/Dockerfile - volumes: - - /home/aweso/sam/5130:/var/www/5130 - - php73_vendor:/var/www/5130/vendor - - php73_node_modules:/var/www/5130/node_modules - - /home/aweso/sam/docker/5130/nginx.conf:/etc/nginx/conf.d/default.conf - - /home/aweso/sam/docker/5130/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf - - /home/aweso/sam/docker/5130/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini - environment: - - DB_HOST=sam-mysql-1 - - DB_PORT=3306 - - DB_DATABASE=chandj - - DB_USERNAME=root - - DB_PASSWORD=root - - TZ=Asia/Seoul - networks: - - samnet - working_dir: /var/www/5130 - - mysql: - image: mysql:8.0 - restart: always - environment: - MYSQL_DATABASE: samdb - MYSQL_USER: samuser - MYSQL_PASSWORD: sampass - MYSQL_ROOT_PASSWORD: root - TZ: Asia/Seoul - command: --sql-mode="NO_ENGINE_SUBSTITUTION" --default-time-zone="+09:00" --default-authentication-plugin=mysql_native_password - volumes: - - db_data:/var/lib/mysql - - /home/aweso/sam/docker/mysql/init.sql:/docker-entrypoint-initdb.d/01-init.sql - # - /home/aweso/sam/chandj_dump.sql:/docker-entrypoint-initdb.d/02-chandj-dump.sql - ports: - - "3306:3306" - networks: - - samnet - - phpmyadmin: - image: phpmyadmin:latest - restart: always - ports: - - "8080:80" - environment: - - PMA_ARBITRARY=1 - - PMA_HOST=mysql - - PMA_PORT=3306 - - PMA_USER=root - - PMA_PASSWORD=root - - TZ=Asia/Seoul - depends_on: - - mysql - networks: - - samnet - -volumes: - db_data: - # 의존성 디렉토리 분리 (성능 향상) - api_vendor: - api_node_modules: - mng_vendor: - mng_node_modules: - admin_vendor: - admin_node_modules: - sales_vendor: - sales_node_modules: - php73_vendor: - php73_node_modules: - -networks: - samnet: - driver: bridge diff --git a/sam/docker/mng/Dockerfile b/sam/docker/mng/Dockerfile deleted file mode 100755 index f19b269..0000000 --- a/sam/docker/mng/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM php:8.4-fpm - -# 필수 패키지/확장 설치 -RUN apt-get update && apt-get install -y \ - git \ - unzip \ - libzip-dev \ - libicu-dev \ - libxml2-dev \ - libpng-dev \ - libfreetype-dev \ - libjpeg62-turbo-dev \ - nginx \ - supervisor \ - libreoffice-writer-nogui \ - fonts-nanum fonts-nanum-extra \ - ffmpeg \ - wget \ - && docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-install zip mysqli pdo pdo_mysql intl soap gd \ - # Pretendard 폰트 설치 (Word→PDF 변환 시 한글 폰트 지원) - && mkdir -p /usr/share/fonts/truetype/pretendard \ - && wget -q "https://github.com/orioncactus/pretendard/releases/download/v1.3.9/Pretendard-1.3.9.zip" -O /tmp/pretendard.zip \ - && unzip -jo /tmp/pretendard.zip "*/Pretendard-*.otf" -d /usr/share/fonts/truetype/pretendard/ \ - && rm -f /tmp/pretendard.zip \ - && fc-cache -f - -# Composer 설치 -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer - -# 타임존 설정 -RUN echo "date.timezone=Asia/Seoul" > /usr/local/etc/php/conf.d/timezone.ini - -# 포트 개방 -EXPOSE 80 - -# supervisor로 nginx+php-fpm 동시 기동 -CMD ["/usr/bin/supervisord"] - -# entrypoint.sh 복사 및 권한 -COPY ./mng/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/sam/docker/mng/entrypoint.sh b/sam/docker/mng/entrypoint.sh deleted file mode 100755 index f5dcbc9..0000000 --- a/sam/docker/mng/entrypoint.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# 0. Nginx 기본 사이트 설정 비활성화 (충돌 방지) -rm -f /etc/nginx/sites-enabled/default - -# 1. 퍼미션 설정 (mng) -chown -R www-data:www-data /var/www/mng/storage /var/www/mng/bootstrap/cache -chmod -R 775 /var/www/mng/storage /var/www/mng/bootstrap/cache - -# 2. tenant storage 퍼미션 (명함/신분증/통장/게시판 첨부파일 등) -mkdir -p /var/www/mng/storage/app/tenants -chown -R www-data:www-data /var/www/mng/storage/app/tenants -chmod -R 775 /var/www/mng/storage/app/tenants - -# 3. storage:link (실패해도 무시) -cd /var/www/mng && php artisan storage:link || true - -# 4. supervisor 실행(nginx+php-fpm) -exec /usr/bin/supervisord \ No newline at end of file diff --git a/sam/docker/mng/nginx.conf b/sam/docker/mng/nginx.conf deleted file mode 100755 index 32136d4..0000000 --- a/sam/docker/mng/nginx.conf +++ /dev/null @@ -1,32 +0,0 @@ -server { - listen 80; - server_name _; - - root /var/www/mng/public; - index index.php index.html; - - access_log /var/log/nginx/mng_access.log; - error_log /var/log/nginx/mng_error.log; - - # 심볼릭 링크 허용 - disable_symlinks off; - - # tenant-storage 정적 파일 서빙 - location /tenant-storage/ { - alias /var/www/mng/storage/app/tenants/; - expires 7d; - add_header Cache-Control "public, immutable"; - } - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; - fastcgi_read_timeout 300s; - } -} \ No newline at end of file diff --git a/sam/docker/mng/supervisord.conf b/sam/docker/mng/supervisord.conf deleted file mode 100755 index acdf92b..0000000 --- a/sam/docker/mng/supervisord.conf +++ /dev/null @@ -1,23 +0,0 @@ -[supervisord] -nodaemon=true - -[program:php-fpm] -command=/usr/local/sbin/php-fpm - -[program:nginx] -command=nginx -g "daemon off;" - -[program:queue-worker] -command=php /var/www/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=/var/www/mng -autostart=true -autorestart=true -startsecs=5 -startretries=3 -stopwaitsecs=1830 -stdout_logfile=/var/www/mng/storage/logs/queue-worker.log -stdout_logfile_maxbytes=5MB -stderr_logfile=/var/www/mng/storage/logs/queue-worker-error.log -stderr_logfile_maxbytes=5MB \ No newline at end of file diff --git a/sam/docs/CLAUDE.md b/sam/docs/CLAUDE.md deleted file mode 100644 index cce4943..0000000 --- a/sam/docs/CLAUDE.md +++ /dev/null @@ -1,876 +0,0 @@ -# Claude Code 전역 설정 - -> 이 파일은 모든 프로젝트에 적용되는 전역 규칙입니다. - -## 메모리 - -### sam설명 -SAM 프로젝트의 기술적 개요 문서입니다. 이 문서를 참조하면 SAM 프로젝트가 무엇인지 이해할 수 있습니다. - -**파일 경로**: `/home/aweso/sam/docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` - -**핵심 요약**: -- **회사**: 주일/경동 (블라인드/스크린 제조업체) -- **프로젝트**: SAM (Smart Automation Management) - 차세대 ERP/MES 통합 시스템 -- **기술 스택**: Laravel 12 + HTMX + Tailwind CSS + MySQL 8.0 (PHP 8.4) -- **아키텍처**: 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 명령을 실행해야 합니다.** - -> **서버 경로 참고**: -> - 개발/운영 서버 모두 `/home/webservice/` 하위에 동일한 구조로 배치 -> - 서버: `/home/webservice/api`, `/home/webservice/mng`, `/home/webservice/react`, `/home/webservice/sales` - ---- - -## 서버 직접 접근 금지 (최우선 필수 규칙) - -> **경고: 운영/개발 서버에 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 안내 (사용자가 수동으로 실행) -✅ 사용자에게 서버 배포 절차 안내 (사용자가 수동으로 실행) -``` - -### 서버 접속 정보 - -| 서버 | 호스트 | 계정 | 역할 | -|------|--------|------|------| -| 개발 서버 | `114.203.209.83` | `pro`, `hskwon` | 개발/스테이징 + Jenkins CI/CD + Gitea | -| 운영 서버 | (신규, 미확정) | 별도 계정 | 정식 서비스 | - -> **참고**: Jenkins(`114.203.209.83:8080`)와 Gitea(`114.203.209.83:3000`)는 개발 서버에서 운영한다. - -### 배포 흐름 (Jenkins CI/CD) - -``` -Claude 역할 Jenkins (자동) 운영 서버 -┌───────────────────┐ ┌──────────────────┐ ┌──────────────┐ -│ 코드 작성/수정 │ │ │ │ │ -│ git add / commit │ │ │ │ │ -│ │─push──→ │ Gitea Webhook │ │ │ -│ │(사용자) │ → Jenkins 빌드 │ │ │ -│ │ │ → Lint/Test │ │ │ -│ │ │ → SSH Deploy ────│──→ │ git pull │ -│ │ │ │ │ composer │ -│ │ │ │ │ migrate │ -└───────────────────┘ └──────────────────┘ └──────────────┘ -``` - -> **브랜치 전략**: `develop` → 개발 서버 (자동 배포), `main`/`master` → 운영 서버 (PR 머지 + 팀장 승인) - -### 서버 작업이 필요한 경우 - -사용자에게 명령어를 안내만 한다: - -``` -서버에서 다음 명령을 실행해주세요: -cd /home/webservice/api && git pull && composer install && php artisan migrate -``` - -### 체크리스트 (모든 작업 시) - -- [ ] SSH 명령 사용하지 않음 -- [ ] 서버 파일 직접 수정하지 않음 -- [ ] 배포가 필요하면 사용자에게 안내만 제공 -- [ ] git push까지만 Claude 역할 - ---- - -## React 빌드/배포 정책 (필수 규칙) - -> **경고: React(Next.js) 빌드를 운영 서버에서 직접 실행하지 않는다!** - -### 배경 - -개발 서버(2코어, 3.8GB RAM + Swap 4GB)에서 Jenkins가 React 빌드를 수행한다. -Jenkins 빌드 실패 시 로컬(WSL)에서 빌드 후 결과물을 서버에 배포한다(fallback). - -### 금지 사항 - -``` -❌ 운영 서버에서 npm run build 실행 금지 -❌ 서버 SSH 접속 후 빌드 명령 실행 금지 -❌ Claude가 직접 npm run build 실행 금지 (로컬 포함) -``` - -### 빌드/배포 방법 (Jenkins 자동화) - -``` -Claude 역할 Jenkins (자동) 운영 서버 -┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐ -│ 코드 작성/수정 │ │ Checkout │ │ │ -│ git commit │─push──→ │ Install + Lint │ │ │ -│ │(사용자) │ Build (Next.js) │ │ │ -│ │ │ Package (tar.gz) │──scp→ │ 압축 해제 │ -│ │ │ │ │ node 재시작 │ -└─────────────────┘ └──────────────────┘ └──────────────┘ -``` - -> **Fallback**: Jenkins 빌드 실패(OOM) 시 로컬에서 `react/deploy.sh`로 수동 배포 - -### 빌드가 필요한 상황 - -사용자에게 다음과 같이 안내한다: - -``` -React 코드가 변경되었습니다. git push 후 Jenkins가 자동 배포합니다. -(Jenkins 실패 시 로컬에서 deploy.sh로 수동 배포해주세요.) -``` - ---- - -## 데이터베이스 아키텍처 (필수 규칙) - -> **경고: 이 규칙을 반드시 준수하세요!** - -### 핵심 원칙 - -**모든 데이터베이스 관련 파일은 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 - -# 개발 서버: Docker 없음, 직접 실행 -cd /home/webservice/api && php artisan migrate - -# 운영 서버: --force 플래그 필수 (production 환경) -cd /home/webservice/api && php artisan migrate --force - -# MNG에서 마이그레이션 실행 금지 (로컬/서버 모두) -``` - -### DB 환경 분리 - -| 환경 | DB명 | 호스트 | 용도 | -|------|------|--------|------| -| 로컬 (Docker) | `samdb` | `sam-mysql-1:3306` | 개발/테스트 | -| 개발 서버 | `samdb` | `localhost` | 스테이징 | -| 운영 서버 | `sam_prod` | `localhost` | 정식 서비스 | -| 통계 DB | `sam_stat` | 동일 서버 | StatMonitorService 전용 | - -> **참고**: `sam_stat`은 API/MNG 모두 `config/database.php`의 별도 connection으로 접속한다. - -### 이유 - -- 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 pro@114.203.209.83 "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, -]); -\"" - -# 운영 서버 (동일 경로, 서버 주소만 변경) -ssh <운영계정>@<운영서버IP> "cd /home/webservice/mng && php artisan tinker --execute=\"...동일...\"" -``` - -### 체크리스트 (메뉴 변경 요청 시) - -- [ ] 시더 파일 생성하지 않음 -- [ ] 시더 실행하지 않음 -- [ ] tinker 또는 SQL로 개별 레코드만 수정 -- [ ] 변경 후 부서 권한 설정이 유지되는지 확인 - ---- - -## 실행 환경 (필수 인지) - -> **중요: 로컬 / 개발 서버 / 운영 서버의 환경이 다릅니다!** - -### 환경 비교 (3-Tier) - -| 항목 | 로컬 (WSL) | 개발 서버 | 운영 서버 | -|------|-----------|----------|----------| -| **구성 방식** | Docker 컨테이너 | Bare-metal (네이티브) | Bare-metal (네이티브) | -| **PHP** | 컨테이너 내부 (8.4) | 직접 설치 (8.4) | 직접 설치 (8.4) | -| **MySQL** | 컨테이너 (sam-mysql-1) | 직접 설치 (8.0) | 직접 설치 (8.0) | -| **Nginx** | 컨테이너 (sam-nginx-1) | 직접 설치 | 직접 설치 | -| **명령 실행** | `docker exec` 필요 | 직접 실행 | 직접 실행 | -| **서버 IP** | localhost | `114.203.209.83` | (신규, 미확정) | -| **추가 서비스** | — | Jenkins, Gitea | — | -| **DB** | `samdb` | `samdb` | `sam_prod` | - -> **배경**: 서버는 Docker가 무거워서 PHP, Nginx, MySQL 등을 네이티브로 설치하여 운영한다. - -### 로컬 환경 (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 웹서버 -``` - -### 서버 환경 (Bare-metal — 개발/운영 동일 구조) - -서버에는 Docker가 없다. PHP 8.4, Nginx, MySQL 8.0이 직접 설치되어 있다. - -``` -개발 서버 (114.203.209.83) 운영 서버 (신규) -├── Nginx ├── Nginx -├── PHP-FPM (3 pools) ├── PHP-FPM (3 pools) -│ ├── api.sock │ ├── api.sock -│ ├── mng.sock │ ├── mng.sock -│ └── sales.sock │ └── sales.sock -├── MySQL 8.0 (samdb) ├── MySQL 8.0 (sam_prod) -├── Supervisor ├── Supervisor -│ ├── sam-api-worker (x1) │ ├── sam-api-worker (x1) -│ ├── sam-mng-worker (x2) │ ├── sam-mng-worker (x2) -│ └── sam-api-scheduler │ └── sam-api-scheduler -├── Node.js (React SSR :3000) ├── Node.js (React SSR :3000) -├── Jenkins (:8080) │ -├── Gitea (:3000) │ -├── /home/webservice/api ├── /home/webservice/api -├── /home/webservice/mng ├── /home/webservice/mng -├── /home/webservice/react ├── /home/webservice/react -└── /home/webservice/sales └── /home/webservice/sales -``` - -### 도메인 매핑 - -| 서비스 | 로컬 (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` | `admin.codebridge-x.com` | `mng.codebridge-x.com` | -| Sales | `sales.sam.kr` | `sales.dev.codebridge-x.com` | `sales.codebridge-x.com` | -| 5130 (레거시) | `5130.sam.kr` | — | — | - -### 명령어 비교 (로컬 vs 개발 vs 운영) - -| 작업 | 로컬 (Docker) | 개발/운영 서버 (네이티브) | -|------|--------------|-------------------------| -| artisan 실행 | `docker exec sam-api-1 php artisan <명령>` | `cd /home/webservice/api && php artisan <명령>` | -| composer 실행 | `docker exec sam-api-1 composer install` | `cd /home/webservice/api && composer install` | -| 마이그레이션 | `docker exec sam-api-1 php artisan migrate` | 개발: `php artisan migrate` / 운영: `php artisan migrate --force` | -| 캐시 클리어 | `docker exec sam-mng-1 php artisan cache:clear` | `cd /home/webservice/mng && php artisan cache:clear` | -| Queue 재시작 | — | `sudo supervisorctl restart sam-api-worker:*` | - -### 로컬 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` 사용 -- [ ] **서버**: `php artisan`, `composer` 직접 실행 (Docker 없음) -- [ ] **운영 서버 마이그레이션**: `--force` 플래그 필수 -- [ ] **마이그레이션은 반드시 API에서 실행** (로컬: `docker exec sam-api-1`, 서버: 직접) - ---- - -## 공동 개발 워크플로우 (필수) - -> **중요: 코드를 pull 받은 후 반드시 필요한 명령을 실행하세요!** - -### 브랜치 전략 - -| 브랜치 | 배포 대상 | 트리거 | 승인 | -|--------|----------|--------|------| -| `feature/*` | — | — | — | -| `develop` | 개발 서버 (`dev.codebridge-x.com`) | Push 시 자동 배포 | 불필요 | -| `main`/`master` | 운영 서버 (`codebridge-x.com`) | PR 머지 시 Jenkins 배포 | 팀장 승인 필수 | - -``` -feature/* ──→ develop ──→ main/master - (push) (PR merge) - ↓ ↓ - 개발 서버 운영 서버 - (자동 배포) (Jenkins CI/CD) -``` - -### 로컬 환경 (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 -``` - -### 개발 서버 업데이트 (자동) - -> `develop` 브랜치에 Push 시 Gitea Webhook → Jenkins가 자동으로 배포한다. -> 수동 배포가 필요한 경우: - -```bash -# API 프로젝트 -cd /home/webservice/api -git pull origin develop -composer install -php artisan migrate -php artisan config:clear - -# MNG 프로젝트 (마이그레이션 없음) -cd /home/webservice/mng -git pull origin develop -composer install -php artisan config:clear -``` - -### 운영 서버 배포 (Jenkins 자동화) - -> `main`/`master` 브랜치에 PR 머지 시 Jenkins가 자동으로 배포한다. -> 수동 배포는 **비상 절차**로만 사용한다. - -```bash -# 비상 수동 배포 (Jenkins 장애 시에만) -# API 프로젝트 -cd /home/webservice/api -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:* - -# MNG 프로젝트 -cd /home/webservice/mng -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:* -``` - -### 요약 표 - -| 작업 | 로컬 (Docker) | 개발 서버 | 운영 서버 | -|------|--------------|----------|----------| -| 배포 방식 | 수동 | Jenkins 자동 (develop push) | Jenkins 자동 (main/master PR) | -| git pull | WSL에서 직접 | Jenkins 자동 | Jenkins 자동 | -| composer install | `docker exec sam-api-1 composer install` | Jenkins 자동 | `--no-dev --optimize-autoloader` | -| migrate | `docker exec sam-api-1 php artisan migrate` | Jenkins 자동 | `--force` 플래그 포함 | -| config:clear | `docker exec sam-api-1 php artisan config:clear` | Jenkins 자동 | `route:cache` + `view:cache` 포함 | - -### 체크리스트 (pull 후) - -- [ ] API: `git pull` → `composer install` → `php artisan migrate` → `config:clear` -- [ ] MNG: `git pull` → `composer install` → `config:clear` (마이그레이션 없음) -- [ ] 운영 배포: `main`/`master`에 PR 머지 → Jenkins 자동 처리 (수동 금지) - ---- - -## 사용 가능한 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 색상 선택 diff --git a/sam/docs/INDEX.md b/sam/docs/INDEX.md deleted file mode 100644 index cbc7d50..0000000 --- a/sam/docs/INDEX.md +++ /dev/null @@ -1,421 +0,0 @@ -# SAM 프로젝트 문서 인덱스 - -> **Claude Code 작업 전 필수 확인** — 작업 유형에 맞는 문서를 먼저 읽고 시작하세요. -> **최종 갱신**: 2026-03-08 - ---- - -## 🎯 작업별 필수 문서 - -| 작업 유형 | 필수 문서 | 용도 | -|----------|----------|------| -| **API 개발** | `standards/api-rules.md` | Service-First, FormRequest, i18n 규칙 | -| **DB 변경** | `system/database/README.md` | 테이블 구조, 관계, 컬럼 규칙 | -| **새 기능 구현** | `system/overview.md` | 전체 아키텍처 이해 | -| **보안 관련** | `system/security-policy.md` | 인증/인가, 보안 규칙 | -| **Git 커밋** | `standards/git-conventions.md` | 커밋 메시지, 브랜치 전략 | -| **품질 검증** | `standards/quality-checklist.md` | 코드 품질 체크리스트 | -| **Swagger 작성** | `guides/swagger-guide.md` | API 문서 작성 방법 | -| **품목관리** | `rules/item-policy.md` | 품목 정책 (유형, 예약어, API 규칙) | -| **단가관리** | `rules/pricing-policy.md` | 원가/판매가 계산, 리비전 관리 | -| **견적관리** | `features/quotes/README.md` | 견적 시스템, BOM 계산, 10단계 로직 | -| **결재관리** | `features/approvals/README.md` | 결재 시스템 (워크플로우, API, UI) | -| **운영 배포** | `plans/production-deployment-plan.md` | 운영 환경 배포 계획 | -| **서버 운영** | `deploys/ops-manual/README.md` | 서버 운영 매뉴얼 | -| **MES 개발** | `projects/mes/README.md` | MES 프로젝트 개요 | - ---- - -## 📁 폴더 구조 - -``` -docs/ -├── system/ # 시스템 현황 — 아키텍처, DB 스키마, 인프라 (architecture/ + specs/ 통합) -├── standards/ # 개발 표준 — "어떻게 코드를 작성할 것인가" -├── rules/ # 비즈니스 규칙 — "무엇이 유효한 데이터인가" -├── features/ # 기능별 상세 — 도메인별 기능 문서 -├── guides/ # 구현 가이드 — "어떻게 구현할 것인가" -├── quickstart/ # 빠른 시작 — 핵심 요약, 명령어 -├── plans/ # 작업 추적 — 예정 → 진행 → 완료 → archive/ -├── projects/ # 프로젝트 자료 — 프로젝트성 분석, 설계, 참고 -├── deploys/ # 운영 매뉴얼 — 서버 운영, 배포 -├── changes/ # 변경 이력 -├── data/ # 데이터 분석 -├── history/ # 히스토리 기록 -├── api/ # API 통합 문서 -├── requests/ # 요청/기획 문서 -└── assets/ # BI 등 정적 자산 -``` - ---- - -## 📚 폴더별 문서 목록 - -### system/ — 시스템 현황 -> 아키텍처, DB 스키마, 기술 스펙, 인프라 (기존 architecture/ + specs/ 통합) - -| 문서 | 설명 | -|------|------| -| [overview.md](system/overview.md) | 전체 시스템 아키텍처 (api/react/mng 구조, 기술 스택) | -| [api-structure.md](system/api-structure.md) | API 서버 구조 (~1,027 엔드포인트, 18 도메인) | -| [react-structure.md](system/react-structure.md) | React 프론트엔드 구조 (249 페이지, 612 컴포넌트) | -| [mng-structure.md](system/mng-structure.md) | MNG 관리자 패널 구조 (171 컨트롤러, 436 뷰) | -| [docker-setup.md](system/docker-setup.md) | Docker 환경 + CI/CD (7 서비스, Jenkins) | -| [database/README.md](system/database/README.md) | DB 스키마 인덱스 (220 모델, 32 도메인, 459 마이그레이션) | - -**DB 도메인별 스키마:** - -| 문서 | 포함 도메인 | -|------|-----------| -| [database/tenants.md](system/database/tenants.md) | 테넌트, 사용자, 권한 (63 모델) | -| [database/products.md](system/database/products.md) | 제품, 품목, 설계 (21 모델) | -| [database/sales.md](system/database/sales.md) | 영업, 수주, 견적 (18 모델) | -| [database/production.md](system/database/production.md) | 생산, 시공, 자재, 품질 (20 모델) | -| [database/finance.md](system/database/finance.md) | 재무, 회계 | -| [database/hr.md](system/database/hr.md) | 인사, 면접 | -| [database/documents.md](system/database/documents.md) | 문서, 전자서명 (19 모델) | -| [database/commons.md](system/database/commons.md) | 공통, 게시판, 감사 (17 모델) | -| [database/stats.md](system/database/stats.md) | 통계 (21 모델, sam_stat DB) | -| [database/codebridge-separation.md](system/database/codebridge-separation.md) | codebridge DB 분리 (MNG 전용 118 테이블, 78 모델) | - -**이관 완료 (architecture/ + specs/ → system/):** - -| 문서 | 설명 | -|------|------| -| [security-policy.md](system/security-policy.md) | 보안 정책 (다층 방어, Sanctum, RBAC) | -| [scaling-roadmap.md](system/scaling-roadmap.md) | 10K 테넌트 스케일링 로드맵 | -| [ai-automation-vision.md](system/ai-automation-vision.md) | SAM AI 자동화 비전 및 장기 로드맵 | -| [board-system-spec.md](system/board-system-spec.md) | 게시판 시스템 설계 스펙 | -| [item-master-integration.md](system/item-master-integration.md) | 품목 마스터 통합 설계 | -| [remote-work-setup.md](system/remote-work-setup.md) | 원격 개발 설정 (DEPRECATED) | -| [erp-analysis/](system/erp-analysis/) | ERP 스토리보드 분석 (9개 파일) | - ---- - -### standards/ — 개발 표준 -> 코딩 컨벤션, 스타일 가이드, 품질 기준 - -| 문서 | 설명 | 필수 확인 시점 | -|------|------|--------------| -| [api-rules.md](standards/api-rules.md) | API 개발 규칙 (Service-First, FormRequest, i18n) | API 개발 전 | -| [git-conventions.md](standards/git-conventions.md) | Git 커밋 메시지, 브랜치 전략 | 커밋 전 | -| [quality-checklist.md](standards/quality-checklist.md) | 코드 품질 체크리스트 | PR 전 | -| [pagination-policy.md](standards/pagination-policy.md) | 페이지네이션 표준 | 목록 API 구현 시 | -| [options-column-policy.md](standards/options-column-policy.md) | JSON options 컬럼 표준 정책 (마이그레이션, 모델, 쿼리) | 테이블 생성/확장 시 | - ---- - -### rules/ — 비즈니스 규칙 -> 도메인 로직, 검증 규칙, 정책 - -| 문서 | 설명 | 필수 확인 시점 | -|------|------|--------------| -| [README.md](rules/README.md) | 비즈니스 규칙 개요 | 도메인 로직 구현 전 | -| [item-policy.md](rules/item-policy.md) | 품목 정책 (유형, 예약어, API 규칙) | 품목 관련 작업 전 | -| [pricing-policy.md](rules/pricing-policy.md) | 단가 정책 (원가/판매가, 리비전) | 단가 관련 작업 전 | -| [customer-pricing.md](rules/customer-pricing.md) | 고객 안내용 서비스 요금표 | 고객 요금 안내 시 | -| [partner-commission.md](rules/partner-commission.md) | 영업파트너 수당 체계 및 정산 | 수당/정산 관련 작업 전 | -| [billing-policy.md](rules/billing-policy.md) | 내부용 원가/마진/코드참조 (CONFIDENTIAL) | 과금 코드 개발 전 | -| [client-policy.md](rules/client-policy.md) | 고객사 관리 정책 | 고객 관련 작업 전 | -| [attendance-api.md](rules/attendance-api.md) | 근태 API 규칙 | 근태 관련 작업 전 | -| [department-tree-api.md](rules/department-tree-api.md) | 부서 트리 API 규칙 | 부서 관련 작업 전 | -| [employee-api.md](rules/employee-api.md) | 직원 API 규칙 | 직원 관련 작업 전 | -| [numbering-rules.md](rules/numbering-rules.md) | 채번규칙 (패턴 기반 자동 번호 생성) | 채번 로직 수정 전 | - ---- - -### features/ — 기능별 문서 -> 도메인별 기능 상세 (기능 설명 + 엔드포인트 경로 + Swagger 참조) - -| 문서 | 설명 | -|------|------| -| [quotes/README.md](features/quotes/README.md) | 견적 시스템 (BOM 계산, 10단계 로직) | -| [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) | 근태관리 기획서 | -| [hr/hr-api-analysis.md](features/hr/hr-api-analysis.md) | HR API 분석 (근태/직원/부서) | -| [barobill-kakaotalk/README.md](features/barobill-kakaotalk/README.md) | 바로빌 카카오톡 + 세금계산서 연동 | -| ~~business-card-request.md~~ | 명함신청 관리 (DB 마이그레이션만 존재, 문서 미작성) | -| [sales/README.md](features/sales/README.md) | 영업 관리 (면접 시나리오 포함) | -| [crm/README.md](features/crm/README.md) | CRM (거래처, 미수금, 미지급금) | -| [finance/README.md](features/finance/README.md) | 재무 관리 (14개 하위 문서) | -| [card-vehicle/README.md](features/card-vehicle/README.md) | 법인카드·차량 관리 | -| [settlement/README.md](features/settlement/README.md) | 정산 관리 | -| [esign/README.md](features/esign/README.md) | 전자서명 (계약·OTP·PDF 합성) | -| [documents/README.md](features/documents/README.md) | 문서관리 (EAV 기반 서식·결재) | -| [ai/README.md](features/ai/README.md) | AI 분석 리포트 (Gemini 연동) | -| [equipment/README.md](features/equipment/README.md) | 설비관리 (MNG 전용) | -| [approvals/README.md](features/approvals/README.md) | 결재관리 시스템 (순차결재, 보류/전결/참조/복사재기안) | -| [planning/README.md](features/planning/README.md) | 주일기업 기획 (견적/프로젝트/사진대지/회의록/AI) | -| [rd/README.md](features/rd/README.md) | R&D (조직도/AI견적/기획디자인/디자인 인사이트/사운드 로고/CM송) | -| [rd/planning-design.md](features/rd/planning-design.md) | 기획디자인 스토리보드 에디터 기술 명세 | -| [rd/design-insight.md](features/rd/design-insight.md) | 디자인 인사이트 UI/UX 연구 도구 (100종 패턴, AI 프롬프트) | -| [rd/sound-logo-studio.md](features/rd/sound-logo-studio.md) | 사운드 로고 스튜디오 (시퀀서 + Gemini TTS + Lyria BGM 합성) | - ---- - -### guides/ — 구현 가이드 -> 특정 기능 구현을 위한 단계별 매뉴얼 - -| 문서 | 설명 | 필수 확인 시점 | -|------|------|--------------| -| [swagger-guide.md](guides/swagger-guide.md) | Swagger API 문서 작성법 | API 문서 작성 전 | -| [file-storage-guide.md](guides/file-storage-guide.md) | 파일 업로드/다운로드 구현 | 파일 기능 구현 전 | -| [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) | 서버 동작 원리 | 신규 합류 시 | -| [nginx-fastcgi-guide.md](guides/nginx-fastcgi-guide.md) | Nginx & FastCGI 가이드 | 서버 이해 시 | -| [php-fpm-guide.md](guides/php-fpm-guide.md) | PHP-FPM 가이드 | 서버 이해 시 | -| [jenkins-setup-guide.md](guides/jenkins-setup-guide.md) | Jenkins CI/CD 셋업 | Jenkins 설치/설정 시 | -| [auto-login-guide.md](guides/auto-login-guide.md) | MNG→DEV 자동 로그인 | 자동 로그인 구현 시 | -| [erp-api-list.md](guides/erp-api-list.md) | ERP API 목록 (List vs Detail 구분) | 프론트 API 연동 시 | -| [erp-api-detail.md](guides/erp-api-detail.md) | ERP API 상세 스펙 | 프론트 API 연동 시 | -| [table-design-guide.md](guides/table-design-guide.md) | 테이블 설계 가이드 (비전문가용, options JSON 패턴) | 테이블 구조 이해 시 | -| [item-master-guide.md](guides/item-master-guide.md) | 품목기준관리 페이지-섹션-필드 구조 | 품목 UI 구현 시 | -| [item-master-items-api.md](guides/item-master-items-api.md) | ItemMaster & Items API 문서 | 품목 API 연동 시 | -| [ai-management.md](guides/ai-management.md) | AI 관리 종합 가이드 (아키텍처, 버전 이력, 온보딩) | AI 관련 작업 시 | -| [ai-model-update-workflow.md](guides/ai-model-update-workflow.md) | AI 모델 업데이트 표준 절차 (7단계) | AI 모델 변경 시 | -| [ai-config-settings.md](guides/ai-config-settings.md) | AI 설정 기술문서 (DB 구조, 메서드) | AI 설정 구현 시 | - ---- - -### quickstart/ — 빠른 시작 -> 핵심 규칙 요약, 자주 쓰는 명령어 - -| 문서 | 설명 | -|------|------| -| [quick-start.md](quickstart/quick-start.md) | 프로젝트 핵심 규칙 요약 | -| [dev-commands.md](quickstart/dev-commands.md) | 일상 개발 명령어 모음 | - ---- - -### plans/ — 작업 추적 -> 예정 → 진행 → 완료 → archive/ (이미 정리 완료, 현행 유지) - -| 문서 | 설명 | -|------|------| -| [index_plans.md](plans/index_plans.md) | 계획 인덱스 (ACTIVE + PLANNED) | -| [GUIDE.md](plans/GUIDE.md) | 계획 문서 작성 가이드 | -| [fire-shutter-drawing-generator-plan.md](plans/fire-shutter-drawing-generator-plan.md) | 방화셔터 도면생성 기능 기획서 (가이드레일 단면도 + 셔터박스 + 3D 렌더링) | - ---- - -### projects/ — 프로젝트 자료 -> 프로젝트성 분석, 설계, 참고 자료 (지속 보관) - -| 프로젝트 | 문서 | 설명 | -|---------|------|------| -| [index_projects.md](projects/index_projects.md) | 프로젝트 인덱스 | | -| **MES** | [README.md](projects/mes/README.md) | MES 프로젝트 개요 | -| **MES** | [MES_PROJECT_ROADMAP.md](projects/mes/MES_PROJECT_ROADMAP.md) | 개발 로드맵 | -| **5130 이관** | [MASTER_PLAN.md](projects/5130-migration/MASTER_PLAN.md) | 레거시 이관 마스터 플랜 | -| **API 연동** | [MASTER_PLAN.md](projects/api-integration/MASTER_PLAN.md) | React↔API 연동 | -| **Legacy** | [draw-module.md](projects/legacy-5130/draw-module.md) | 레거시 드로우 모듈 | -| **견적** | [quotation/](projects/quotation/) | 견적 프로젝트 자료 | -| **전자서명** | [e-sign/](projects/e-sign/) | 전자서명 프로젝트 자료 | -| **조직도** | [org-chart/README.md](projects/org-chart/README.md) | 조직도 관리 (트리형, 드래그앤드롭, 숨기기) | - ---- - -### deploys/ — 운영 매뉴얼 -> 서버 운영, 배포 (현행 유지) - -| 문서 | 설명 | -|------|------| -| [ops-manual/README.md](deploys/ops-manual/README.md) | 서버 운영 매뉴얼 (11부 구성) | - ---- - -### changes/ — 변경 이력 -> 파일명 형식: `YYYYMMDD_description.md` - -| 문서 | 설명 | -|------|------| -| [20260304_eaccount_infinite_loop_fix.md](changes/20260304_eaccount_infinite_loop_fix.md) | 계좌 입출금내역 부분 월 조회 시 무한루프 크래시 수정 | -| [20260306_purchase_request_payment_method.md](changes/20260306_purchase_request_payment_method.md) | 품의서 지급방법 UI 개선 (구매품의서 추가, 비용정산품의서 행별 변경) | - ---- - -### data/ — 데이터 분석 - -| 문서 | 설명 | -|------|------| -| [analysis/item-db-analysis.md](data/analysis/item-db-analysis.md) | Item DB/API 분석 최종본 | -| [analysis/bom-item-mapping-analysis.md](data/analysis/bom-item-mapping-analysis.md) | BOM-품목 매핑 분석 | - -### 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 동기화 검증 | - -### plans/ - 개발 계획 -> 임시 개발 계획 문서 (작업 완료 후 정리 → 삭제) - -| 문서 | 설명 | -|------|------| -| [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, 서버 아키텍처) | -| [attendance-management-plan.md](plans/attendance-management-plan.md) | 근태현황 개발 계획 (Phase 1~2, HTMX 기반) | -| [leave-management-plan.md](plans/leave-management-plan.md) | 휴가관리 모듈 개발 계획 (연차 발생/신청/승인/정책) | -| [approval-management-system-plan.md](plans/approval-management-system-plan.md) | 결재관리 시스템 기획서 (전자결재 전체 설계: 기안~회수, DB 설계, 17개 양식, 4 Phase) | -| [block-builder-evolution-plan.md](plans/block-builder-evolution-plan.md) | 양식 디자이너(Block Builder) 고도화 계획 (6 Phase: 렌더러→결재→동적테이블→수식→인쇄→Legacy 대체) | -| [design-insight-menu-plan.md](plans/design-insight-menu-plan.md) | UI/UX 디자인 인사이트 연구 메뉴 기획서 (레퍼런스 수집, 화면 분석, 패턴 라이브러리, Before/After) | -| [sound-logo-generator-plan.md](plans/sound-logo-generator-plan.md) | 사운드 로고 생성기 기획서 (Web Audio + Gemini AI 어시스트 + Lyria 자동 생성) | - -### features/ - 기능별 문서 - -| 문서 | 설명 | -|------|------| -| [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용) | -| [approvals/README.md](features/approvals/README.md) | 결재관리 시스템 개요 (아키텍처, DB, 상태관리, 권한) | -| [approvals/workflows.md](features/approvals/workflows.md) | 결재관리 워크플로우 상세 (승인/반려/회수/보류/전결/복사재기안) | -| [approvals/api-reference.md](features/approvals/api-reference.md) | 결재관리 API 명세 (20개 엔드포인트) | -| [approvals/ui-screens.md](features/approvals/ui-screens.md) | 결재관리 UI 화면 구성 (Blade + HTMX) | -| [approvals/db-changes-and-model-sync.md](features/approvals/db-changes-and-model-sync.md) | 결재관리 DB 변경사항 및 API 모델 동기화 현황 (2026-02~03) | - -### projects/ - 프로젝트별 문서 - -| 프로젝트 | 문서 | 설명 | -|---------|------|------| -| **MES** | [README.md](projects/mes/README.md) | MES 프로젝트 개요 | -| **MES** | [MES_PROJECT_ROADMAP.md](projects/mes/MES_PROJECT_ROADMAP.md) | 개발 로드맵 | -| **Legacy** | [draw-module.md](projects/legacy-5130/draw-module.md) | 레거시 드로우 모듈 | -| **조직도** | [README.md](projects/org-chart/README.md) | 조직도 관리 (Alpine.js + SortableJS) | - -### history/ - 히스토리 - -| 기간 | 문서 | -|------|------| -| **2025-11** | [item-master-gap-analysis.md](history/2025-11/item-master-gap-analysis.md), [item-master-spec.md](history/2025-11/item-master-spec.md), [front-requests/](history/2025-11/front-requests/), [item-master-archived/](history/2025-11/item-master-archived/) | -| **2025-09** | [checkpoint.md](history/2025-09/checkpoint.md), [database-schema.md](history/2025-09/database-schema.md) | -| **Roadmaps** | [december-2025.md](history/roadmaps/december-2025.md) | - ---- - -### 서브프로젝트 문서 - -각 서브프로젝트는 독립적인 `docs/` 디렉토리를 가집니다. - -| 프로젝트 | 문서 경로 | 설명 | -|---------|----------|------| -| **API** | [api/docs/](../api/docs/) | REST API 프로젝트 | -| **MNG** | [mng/docs/](../mng/docs/) | Plain Laravel 관리자 | -| **React** | [react/docs/](../react/docs/) | Next.js 프론트엔드 | - ---- - -## 📝 문서 작성 규칙 - -### 폴더 선택 기준 - -| 질문 | 폴더 | -|------|------| -| "시스템이 현재 어떤 상태인가?" | `system/` | -| "어떻게 코드를 작성할 것인가?" | `standards/` | -| "무엇이 유효한 데이터인가?" | `rules/` | -| "이 기능은 어떻게 동작하는가?" | `features/` | -| "어떻게 구현할 것인가?" | `guides/` | -| "무슨 작업을 할 것인가?" | `plans/` | -| "프로젝트 자료를 보관하고 싶다" | `projects/` | -| "무엇이 변경되었는가?" | `changes/` | - -### 파일명 규칙 - -| 유형 | 규칙 | 예시 | -|------|------|------| -| 기술 문서 (코드 참조) | 영문 kebab-case | `api-rules.md`, `database-schema.md` | -| 업무/비즈니스 문서 | 한글 허용 | `영업파트너가이드북.md`, `수당지급.md` | -| 변경 이력 | `YYYYMMDD_description.md` | `20260205_sus_inspection_template.md` | -| 폴더 인덱스 | `README.md` (대문자) | `features/finance/README.md` | -| **혼용 금지** | 한글+영문 섞지 않음 | ❌ `영업partner가이드.md` | - -### 크기 제한 -- **목표**: 10KB 이하 -- 초과 시 도메인별로 분할 - -### 문서 구조 템플릿 - -#### 정책/규칙 문서 (`rules/`, `standards/`) - -```markdown -# 제목 - -> **작성일**: YYYY-MM-DD -> **상태**: 설계 확정 - ---- - -## 1. 개요 -## 2. 핵심 원칙 -## 3. 상세 규칙 -## 4. API 엔드포인트 (해당 시) -## 관련 문서 - ---- - -**최종 업데이트**: YYYY-MM-DD -``` - -#### 변경 이력 문서 (`changes/`) - -```markdown -# 변경 내용 요약 - -**날짜:** YYYY-MM-DD - -## 변경 개요 -## 수정된 파일 -## 상세 변경 사항 -## 테스트 체크리스트 -``` - -### 작성 스타일 - -| 항목 | 규칙 | -|------|------| -| **언어** | 한글 기본, 코드/경로/기술 식별자만 영어 | -| **어조** | 서술형 ("X 한다") | -| **경고** | `> **경고: ...**` 블록인용 | -| **금지/필수** | `❌` 금지, `✅` 필수 | -| **우선순위** | `🔴 필수`, `🟡 중요`, `🟢 권장` | -| **코드 블록** | 반드시 언어 지정 (```php, ```bash 등) | -| **인라인 코드** | 파일 경로, 메서드명, 변수명에 백틱 | -| **구분선** | `---` 주요 섹션 사이 | - -### 새 문서 작성 시 체크리스트 -- [ ] 적절한 폴더에 배치 -- [ ] 파일명 규칙 준수 -- [ ] 문서 구조 템플릿 준수 -- [ ] 이 INDEX.md에 등록 - ---- - -## 🔄 문서 정비 진행 현황 - -> 참조: [docs-comprehensive-update-plan.md](plans/docs-comprehensive-update-plan.md) - -| Phase | 작업 | 상태 | -|-------|------|------| -| **Phase 0** | 문서 정책 재정립 | ✅ 완료 | -| **Phase 1** | 시스템 현황 문서화 (DB, API, React, MNG, Docker) | ✅ 완료 (14개 문서) | -| **Phase 2** | 기존 문서 정비 (architecture/+specs/ → system/ 이관) | ⏳ 대기 | -| **Phase 3** | 신규 도메인 기능 문서 작성 | ⏳ 대기 | -| **Phase 4** | 최종 검증 및 INDEX 갱신 | ⏳ 대기 | \ No newline at end of file diff --git a/sam/docs/assets/bi/sam_bi_black.png b/sam/docs/assets/bi/sam_bi_black.png deleted file mode 100755 index e83d284f8c71a5b87645be6da99e617e11657cb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2321 zcma)8X;f3!7QP7=atI*zMo`cINf=aAMiG3VRLF1aSf>2@nP)O0VnvervrS`|N%8*=wD>_xaY@Urz8B z0Vd=XWC(&xf&$rJ;aZN13>e^Rmo;efMwktPEFmX|@M(AUk3CRin?G1KJvKSHlX?Hsa2h1=WH6^C&;wiIh63===YMS~8p6vx-#vc4 zRz32aCF-^7%0|z^^s@;8Af4;F_4_L+@>?ZZbn(R@`<5a8P2`*q3I%%~}-nRUS3BfN3&g0(aO{)UHZxWeI0wt$`p-}JDsVunI zN}umK)yfV0aIEXEsNyXSsi%M=e(mlZMyhAttomHwILf_gv%y&~*fA3~eNomgH>_YZ zR2iwFvux7vBpMxc&o5r3n1#7QK_99Y&RXiD7=~BSFsA>|R;)<$4hfu7=;WG$eZ3cu zFcFbTlvZ}f^E8c6n8>nyxhlW*h?Qxk`Cf-GQO>E?0v|o;CoXEyTq-H%i3pi|>r&=< z+^n66i@%ge2s9qt;Sbv;I5+v4ISaTpiYE(9H@Zl;-&hgZA=L_#YKA2|0h>VmMst|P zNW|t??O!)S$zm5MHoLoOiYm1?P03LAo8%WwO-}vRcW({QQrZ#`)D5}lmKRH+K8RsCH zczxqILqI=AuFe7NtF+;$%tGBhdHUDgx-|n?CW@p89Xxg+Tr}Zu19s_uK~gX4plsMc z+V?7KpxpjcmbEa}#n^kD)4OLOQxB3{ zvFwO$#|b=v=uZE2H7oZZup@8>@5J#Ne57tOk3Q=6*Z zO=K;W^<>nMHb=MW1{atY1KTKe=~n||iPG8$#o2~RZb(_}VEm?NIBV8mRzn*tk0)p; z*))5r&TZz{aF=lXOo_{&$@^cU!IhZ=N4N5X6sNlP+og%f;VF+!Bd_UWuhI~|+6Y4m zNmH^vs%J3GvL&?7oc=%+JrD9`4H}2J1Mk|7AN3*+w^B&ouH%5}p(XZ}dY9e8WFtER zo)rsL=@yTvuO8X97^PQDQY#u@Y}TOi-pkvxMdlgvID2s2Q)9M2*00@n)krDl?&gx) zzrYnssLqp-&B3kd##bru;L1daO;=yGdt&&Bg{@7G@RwJv{ypVYim{D0A$;7sKa(GA z2&(cXIM+kFlWLF@rw)$POF+^mSKU!56P!@H!Id``na4*`Eb6SXq(^%d>*48sxZ;oc z0{8DnN*iIAmnrW!+RF&+^wf0K-d{)1g1ufvt$wKw)N3b4j$C{5!wjJK5^_P+ooPO| z1r(}HI`5YEQxpe68LDx1lyc^lZ;H(w`0;F%lQn_Rb0@OB-+Z(886Ux(U4R**nP`ha z?S@v)CucnSxqZjosAw>>cBqsyiJa`T+Ui5H)34CG))ODjvsYog`SOmk&k4_C1GjdW zRy|DyMYece{L7nYYn3BMFI9ZAta&!7Qd=hY`q2Gio$*lZ*UxLhMLj8c%iC<9543nC zddAtS4$l2}eZwr}#o?vJs0?1XJ@L&8@4@^JOZJAqhBjx0I8d`)YG1%&jpt0}Wc~~1 zq8+`2$kD!T-JJz5uFHYPKk4$1Y7kH*QpM`8y7l*YT^UW|76cW;)}7@uZAYGvMg;Vz zrOar8UuoKlnn#c_UpKVE!(1pyzHVZOZ&n|? zUlAt0F!a&v7%NYMzeA|4e^W4+#ZKdKQ?E~_cpF7wGh3bB6PEd%Kii6rCFmM zw2R9h=NZ!#G%DT9<%hw$I>zehwf7B9sBU;_48_4R84}eZAeMS?@B(f!R zm;>JcbQ+^60LT%cDGP{!4q+i|RW3rL5i`W9JP<>J1K0?O?nM_x0678e@xzA@&4J~@ zhL55mlp)DSj1eqV9BhfcWCC*(vla&%qAyv%9Kp7TgDudPY=F-ct~eN=Fa3c&B6n9# zob>ag76_324AD}9!(P^g^j;<$i?F;UDkD&B!qyO35nC7*v;)95$%eg*?Gk(qnXr%k zugxmN?eFnc2dMRa7Xg2=HIcBL2``0BgpdP6lq1SwLhgv%7{g&vCgC}}!66GVR?F7F z#MNRzc3mQb$PAGv4?k8&s+#W65vD#n82cFyqcG}g`2Y?^(U6eUFtI)lIF?%y2^KV> z-cmmrWXtC5+XrBt1Q?qb3!ywzcy&3829ek*)ph`@#Gt~*NC;88l2cQ)TTWd3%moS$ n1DPpWmDFj)`akHy!_GxQu9e+~u>#|4oZ~=2oG;joES~5efRpx{ diff --git a/sam/docs/assets/bi/sam_bi_blue.png b/sam/docs/assets/bi/sam_bi_blue.png deleted file mode 100755 index a5cde92f8e59dad76253abd6234784a24bb37e1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2277 zcmai0`Bzid7QQ!JFqbKp075_^2nkWKpdbheNg$d4DiL@LGDVR@MJ7t;ijL+#ej?zB%0WUcYe0d^hB z==-7Gx#)g`V6; z=VW>Q`BZ+teJr}f+UT5%PAHGBqRb|&*w74J8B8oC!{4Jw6RkpQDKQ=GW9s7LT%``4 zn`fqB4t=^nNOL@r110d_Q1skXc-A>PZ|WgEa7(eL+MdJ3L;alVTTlIKQdzL0^z()H ztXPC>y1p!@zE~WTS}Q3B;4LD6)L^DDap=e6esLJK&>M!e{j{E)52ue2ee_~_OgpPD zMq}K@Eeq_N1^|O(;^}1{x+e!dn#A>&g(qP?fDJC^TUcNO){fuZY_MjvMjFZIG(%oQa8$+oqAorLc3X|YLMOaH8{bTgd zrs4d@Q&tMc4a-?aQ<@}+;;1=MrkKPlIMe66uQuBZ;$T9zjTwDq*|x~&K!KH}*PmjK zON8%OL)j~Sgpwxg67V}WxUn!D!jv1jOyQbw-OWTIr|;%Z*g7(+Gf8u0vN zTJ0!>`xev1Ev7axMUSkGJR)Pgt^{(s@Ph%*OMq;_0#T!`Z0K=4UtxmA25r6gp0L%9 zp->6~g|#ovK~uVANlQ1vKR8rfre{2gkMuPaSV=UNZb?nklBy9!MOsZo{Q&_Q*6Zv) zyI}pW680^f=^X?}&n9ob`*(e~#LInFyR?7vL*wtRB-6mG$PKsL+P`xgAU2dL1A|w_ z?mpZV@>3E;hTtHAcDU&5gU{aKqwJ7sF7aL*PPfVZ7oV<;N*i__v1*PaUB*(f&OIHj zyB3tEDU1Sf5^ZdG(24P4h0p8wu-;DS8(kN#Y>!oMZwZV461|K+VHPenm=oRDz-m&^GltC*M%LbI0!%ik?PT*Iu-b1XmLN$pyj(4PV_0}a9`xgv zS`X9H>rV=2SVLEf{;pYvYhL=y=E3!1ifkSS8Efy6UPi0fOAdJvRkU@?kwhSUT|9D% zPMzZ)x8$GDO}~vj2Nt=iRx8vuDhPMFzEJb z-GmElJ_@%VhFPlL>RwZ4kAc`*4^8e9`l|@Vu@!m4`TN8#+vM94VYAG@82w4CB^8~O zqt@lcm~e7}_l22(Z+~&8{gM&i~J?4(RBe*CE9t@=uYj45_dA)UT|$ zg)j3IE^6_v0xt|(noMmHSUGD%clq`MZ(qHMX}U_?yDSVFTsrG4Ui%En?Q$^=JlR+( zYxL1gaR8^9a_!;P=;?$u+wM$H*6kjApN4yf;MqE_V~K&~NS%f|5~ON9o=f5|RBO4B z;(6O|1rzWMT2JlGj2goW&)mLgy&Ljw8E1Z`Mb;V8@Lsx+7T`2RV$ZFeH z9zIDbr}Up*`e27ikTyGgxO_lhWzlrY?062pNcv{9eU<9z%B!N8LtZcM1zc*(T$^;3E&60H@}Z6S%KPE%cKkoiCOgi)V%C|$VMUXDA$uD4hH7iV z7k_HbCuYtql6wNEOOoAu_4~Ulo1eR*RO~_?h3()H_2J^U=_v-zL$F{2z+owh-SICI zy^B-D?4|T`^vrAS*yw4o$Tbeu8zK~^lH{F&l_oL$YGBXL`d24G0c5XeM3$V?Z6Oy?kYh=zqh@Cg=X zpsi$4Dk9y%QCLC^I>c?d;Cm5d%tU2mkt-tg;VAG>GYD6rKss5ZgLJQUaM1(jnK(bB zdliQV;Zz`O$N=KV%mk#{mcuoHlrS+A2dc%8IRlWBnFo+=I){ruc_7gY2mX#ix(qae z%;Y0v8&07Klm`uVxV!6DXnehoMs}Vx^TWwYK3GzGiv|N|C4(Ko7{d4UHb_SwhsWNcz4tBz%fK~gxt*%b6n17N3zKK z7}z&aiYlD}kpFuquD1(;B2ZTtuhmC*c6%Mj>Wxg)+%k}%az%lSn~PAT00(a2M&f!e znLzz43XB(GbnSnjkkuIZ0W%C>fdlmmF+dyw(O6UrZ~&2#H(~&D2HNXXf-1vt;0Biv z3}6B^JYZpf1Vp-yjg`WF<^*G`d(SJbWZEyCxK)bpBH?+QB1W#<7+VCUtN@uFC{0BE b*=cl*B-Hk-`3IQ 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 diff --git a/sam/docs/assets/bi/sam_bi_purple.png b/sam/docs/assets/bi/sam_bi_purple.png deleted file mode 100755 index 7b9dc0c48779ea36bf7153cb6d74d8ee2fac3bb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1547 zcmV+m2K4!fP)AGUffB5(cJ)USe`>eUY?U?Kvwvc=9eAB&)VECMlqdtkRfPQPl~H*yo)?5kt`)ho9RX7cU(IX#!a6l_1Pf1whHf=@ux z+_x{ZW_W^i*ZX19(FiobW;lxS2rR*7d>Yuibio&zgCv*$nx;E&2o%A-4Glgd0z(i6 zum}V}3~-FQzxd|xg7zWNO#%TfIF~~4z!T5{0njwN-{=;x>ApRS3DHFoR?sVj;sGW= z1zCWmd3Zxs7)U`T09+&i1%XN;`cfbPCr|*O1eibtKvoza!M=M}ZUG};1c_288Ylup zPy#p-?jaQhLh$rfc#eV;NdSRZ3dI9KunP=;rg{8fZ{M820zec=RzY?p5eqZHC@=%; z1e*W=Fjp8R!Q&sx)}LV`SOm&aD3QoQFbH6PvP_BrsMIu_z;ybcF;cBoy?81}LjA^qsj*5@$xYRduR>pcd## zq3*EL-2>1Qv;r-l)1JH4Q_o7lT6Nyptbd;WP!Y66l1@9Qye=OEgb)&V0giZPfpU^~bq9Io(l&Ao z1l&?69wtIxPzzu?gZ8!ro#l(dP{s5%)MWuL<|uExznvxvd^L)unjbVi=yNs!^swt7 z@^|9lyEp7|CV{iCESE6)^^ba-MPRK@%)7HN-ham(&LFS=O;$xPIRGd96$#f#Vor#J z=?F6Iu0SBJl|H9W69PkEptnf^86Z>`p)owv*#rT*5Gxo7LU?*E^vWoK{LvGah!VnQ zQPsT)7eI8@D>lCvP7)-*7lpV&xt1!}6Q|U79LJ%j3&jK1jxEvG;(Gx)~g)RaTLAsumt_IrGR<7l)0s>*(i5KMJ z^~}?&T+7`A1j3VeF#>rA{8QjnIrRf9s6apq{0VS@I{`27CJ+S91ctzuKoPhSI08=s zN#ICe3H%5&fg6D*@FEZeP6VdFhd>p$5V!&l0$JcdU<>#Jx`0mL3uf_&-D$xbKCxR6 z%-|EdB>|P-BVZDo1Vn&tCSRatP@4FJOU8fG^-d3+DfQI09h~-$|4t%;S5Z5(EZ-B8U+v0FFTS_`S6)N><&& z|Hh^`X;lyZ8(Sjf)ja%fY>1V3NZF{sAtQ_|T>n@^SzG002ovPDHLkV1iDnjnDu9 diff --git a/sam/docs/assets/bi/sam_bi_red.png b/sam/docs/assets/bi/sam_bi_red.png deleted file mode 100755 index 93d566f85b6c8a0df1dfdd49b5c66ba8f09dbd3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1547 zcmV+m2K4!fP)7c3G zVQ=xk#>o*T^w8A!;N#|yn$$s3?Xb7ZAvNNFj8ctc;Q#;yaY;l$RCt{2-RpAGFc1Y` zJ8f>AGUffB5(cJ)USe`>eUY?U?Kvwvc=9eAB&)VECMlqdtkRfPQPl~H*yo)?5kt`)ho9RX7cU(IX#!a6l_1Pf1whHf=@ux z+_x{ZW_W^i*ZX19(FiobW;lxS2rR*7d>Yuibio&zgCv*$nx;E&2o%A-4Glgd0z(i6 zum}V}3~-FQzxd|xg7zWNO#%TfIF~~4z!T5{0njwN-{=;x>ApRS3DHFoR?sVj;sGW= z1zCWmd3Zxs7)U`T09+&i1%XN;`cfbPCr|*O1eibtKvoza!M=M}ZUG};1c_288Ylup zPy#p-?jaQhLh$rfc#eV;NdSRZ3dI9KunP=;rg{8fZ{M820zec=RzY?p5eqZHC@=%; z1e*W=Fjp8R!Q&sx)}LV`SOm&aD3QoQFbH6PvP_BrsMIu_z;ybcF;cBoy?81}LjA^qsj*5@$xYRduR>pcd## zq3*EL-2>1Qv;r-l)1JH4Q_o7lT6Nyptbd;WP!Y66l1@9Qye=OEgb)&V0giZPfpU^~bq9Io(l&Ao z1l&?69wtIxPzzu?gZ8!ro#l(dP{s5%)MWuL<|uExznvxvd^L)unjbVi=yNs!^swt7 z@^|9lyEp7|CV{iCESE6)^^ba-MPRK@%)7HN-ham(&LFS=O;$xPIRGd96$#f#Vor#J z=?F6Iu0SBJl|H9W69PkEptnf^86Z>`p)owv*#rT*5Gxo7LU?*E^vWoK{LvGah!VnQ zQPsT)7eI8@D>lCvP7)-*7lpV&xt1!}6Q|U79LJ%j3&jK1jxEvG;(Gx)~g)RaTLAsumt_IrGR<7l)0s>*(i5KMJ z^~}?&T+7`A1j3VeF#>rA{8QjnIrRf9s6apq{0VS@I{`27CJ+S91ctzuKoPhSI08=s zN#ICe3H%5&fg6D*@FEZeP6VdFhd>p$5V!&l0$JcdU<>#Jx`0mL3uf_&-D$xbKCxR6 z%-|EdB>|P-BVZDo1Vn&tCSRatP@4FJOU8fG^-d3+DfQI09h~-$|4t%;S5Z5(EZ-B8U+v0FFTS_`S6)N><&& z|Hh^`X;lyZ8(Sjf)ja%fY>1V3NZF{sAtQ_|T>n@^SzG002ovPDHLkV1j|kk3awb diff --git a/sam/docs/assets/bi/sam_bi_white.png b/sam/docs/assets/bi/sam_bi_white.png deleted file mode 100755 index 34a5a8ca9e4f60bdd5ee6d2c052ab2fdf60f6a71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2361 zcmZ{m`#aQ$7so#{X2)l0OrH-bw_z^pQmoCEuS=LQ7^d9{m0PXGqFj>8Bs4B#=A$NZ zNvuMp)TSg#l5&|5DTQj2OBpj+xrCWXzN1jz`Tha>!~49S^Loy6o^zh({BTZs@9@-G zWv~hW04VI;ag7=M~5SfYsXn7wXkW`Z553A6f3MyOYajM`q*S zG1oC=1AD*i3}+u89mf1c4T!}=F-Z=BMC?%vLJ8P_-UZ(*&n(K+%UsrIPxy`S6!jvF z_MmUq_{Nf1N=P2TQ16{M^E>fWoMYIv$ti%4_99ihe0Auh^t(Orj*@vzlfwx?j~k-`-81L zfgg9WO8)SO$S$s^VZGHTxKdK;kK`C=3dRC(!{_e)%yVm0gZ$!FV*=!w_VbqS|4k44 z@==>XEhqH%Zv-M+YvZUpa&J)i$AYkR_GdoR;!Q4Xq*RSD<|DT~0|>w8KsRK0%Ji-e zJS!^|(QHiqmgvV%Yeom``}J$j6gNo#-g{US3*-#^VLO-w-rG?4tsnzfZRVXs;xA;s z=|Kgim`(~>IRW+&`;9kGy>e;~J(eu@9 zz`BEfDCR7%lXJ;^sK5}#oUtE&$O2_`%XFn{iF8@op461}GmbKgD1~j@ZzNYuF)v=Qm76*aG(%2p0fA=^0tbuiV z+Rp!ijr%fNyXKbXo$Yqyu>1;(38NRXCHel=m6g3+V?)!$j%V$DQhK(GdE6|MFC|VWqsQ!Pn!oAOCUySL0$-%2vf=1=o6>sqc@( z36gv$7VlRS4XqI_w%zuzx>0@OAgnu%XX^_Tt>oQBn~{tSPo(Enw&E-0p%8qc(@a_a zu7hKGwj;-H*XhsID%V5OZ}{D{#cbbRz$GGmy+sgkB0J$i@Pps?2i9Vq?d9rgBv+rc z6s4U>*irGI$1spthbf5X%*4rKXrZr+824;emq0fu(hqqpEQg8Q=c;}8gwlN}8jBtd z8UbO%=FKCVriCmU4&6rK=yYj(n`n(oUVf?nKc_<1^{Dn8ZqN?XlKRbSFZX`250VYf z2-jJ<`t-JBmZ+|Cwdm@btr~`P<15V`m?Em)uG=1Gpr{Lkwc~hc{fCDlDkg69O1YhN zb>6MlLAX0}TFZk}*wd$ogKn5^_Q?g!Shbd^#AqK)+0C`AHm*qrwx2O*DE{%!zm1!H zdQ&Pla;;-?N%Im>AXavBd$)f3oO4OzKE3&1&S-W*@Mui>iP+g~bYP|I$CX{`-45Rk z=^M9&!w6D;SY&WcQ|>2kYBuA~$7Y0cu0F0eKi_T89u-Y_vgkGy1N8W$ko6z(mL}C1Wuy+iy)*Ip z(YG5;0QkCM-G8P86{_7h~==FKaDEU}}N2Y(7yU4J#;cw?d7 zD|&@)HNnf4siybTIG1b>PRh~zG^ZsFg{9*XF8snZ;Q)DlT_Ef%xN}g$4 zIGOZU+oF}-RDce@OCsum+S8-mHtqjX(-`e&ibM$Fkh;<9Tj8 zOScU9A(pV{sZe?(ka1u8np-s_c6{kZ;%N!lEmdSl#B{IQeis7Snv_B7Z6Kyj2c8)t zqEWl)n5C2~I#3L`65o&^x`(@W{vHtHtq;HMcf_D0c;IyOS~O}40~7O_kO>%2_(!AE z<+-ryUalJn&~Setc>{qW5FQLLM5FX5=&jd|(}BlA(HjmK;%QLoQ~@AHih!x_)?@-k z%DcGz>kE5LTR1LxmtfTKt|{oaCq~a0W+%JzWNPp)$;uDOUa3|i_?>6ezYxIk*x`PQ H!RG%Db#^7) diff --git a/sam/docs/brochure/README.md b/sam/docs/brochure/README.md deleted file mode 100644 index 9fc78d2..0000000 --- a/sam/docs/brochure/README.md +++ /dev/null @@ -1,370 +0,0 @@ -# SAM 브로셔 버전 관리 - -> **작성일**: 2026-03-01 -> **상태**: 운영 중 - ---- - -## 1. 개요 - -SAM CEO Dashboard 및 ERP/MES 영업 브로셔의 버전별 디자인 변천을 기록한다. -모든 브로셔는 세로형(9:16) HTML로 작성하며, `pptx-skill`(html2pptx.js)로 PPTX 변환한다. - ---- - -## 2. 버전 요약 - -| 버전 | 대상 | 테마 | 배경색 | 주 액센트 | 비고 | -|------|------|------|--------|-----------|------| -| **v1** | 전체 고객 | 다크 | `#0F2439` | `#10B981` (에메랄드) | SAM ERP/MES 범용 | -| **v2** | 경영진 | 다크 | `#0B1929` | `#0EA5E9` (스카이블루) | CEO Dashboard 초판 | -| **v3** | 경영진 | 다크 | `#0B1929` | `#0EA5E9` (스카이블루) | v2 개선, Before/After 추가 | -| **v4** | 경영진 | 라이트 | `#F8FAFC` | `#0EA5E9` (스카이블루) | v3의 밝은 배경 변환 | -| **v5** | 경영진 | 프리미엄 그래디언트 | `#0F172A→#312E81` | `#FBBF24` (골드) | 글래스모피즘 + 골드 | -| **v6** | 경영진 | 코퍼레이트 블루 | `#FFFFFF` | `#2563EB` (블루) | 대기업/공공기관 스타일 | -| **v7** | 경영진 | 웜 그레이 + 틸 | `#FAFAF9` | `#0D9488` (틸) | IT/SaaS 스타일 | -| **v8** | 경영진 | 투톤 스플릿 | `#1E293B` / `#FFFFFF` | `#F97316` (오렌지) | 금융/컨설팅 스타일 | -| **v9** | 경영진 | 미니멀 화이트 | `#FFFFFF` | `#6366F1` (인디고) | Apple/디자인 에이전시 | - ---- - -## 3. 버전별 상세 - -### 3.1 v1 — SAM ERP/MES 범용 브로셔 - -**컨셉**: 중소 제조업 대상 SAM 플랫폼 전체 기능 소개 - -| 항목 | 값 | -|------|------| -| 배경 | `#0F2439` (네이비) | -| 주 액센트 | `#10B981` (에메랄드 그린) | -| 보조 액센트 | `#2E86AB`, `#8B5CF6`, `#E86F2C` | -| 카드 스타일 | 반투명 배경 + 컬러 보더 | -| BI 로고 | `sam_bi_white.png` | - -**앞면 구성**: -- 히어로: "중소 제조업을 위한 ERP/MES 통합 플랫폼" -- 고민 포인트: Excel 과의존, 실시간 가시성, 품질관리, 높은 ERP 비용 -- 효과 지표: 시간 절감 80%, 납기 준수 95%, 추적성 100%, 인사/회계 무료 -- 기술 태그: 클라우드, 모바일 대응, 멀티테넌트 - -**뒷면 구성**: -- 8대 핵심 모듈 (01~08 번호 뱃지): 품목/BOM, 견적/수주, 생산/MES, 출하, 품질, 자재, 인사/회계, 대시보드 -- 확장 기능: 전자서명, 알림톡, AI Lab, QR -- 가격표: 기본 2,000만원 + 월 50만원 -- 도입 프로세스: 인터뷰 → 개발 → 이관 → 교육 - ---- - -### 3.2 v2 — CEO Dashboard 초판 - -**컨셉**: 경영진 타겟, 대시보드 중심 소개. 문제→해결 스토리텔링 - -| 항목 | 값 | -|------|------| -| 배경 | `#0B1929` (짙은 네이비) | -| 주 액센트 | `#0EA5E9` (스카이블루) | -| 보조 액센트 | `#10B981`, `#8B5CF6`, `#F59E0B`, `#EF4444`, `#EC4899` | -| 카드 스타일 | 반투명 다크 카드 | -| BI 로고 | `sam_bi_white.png` | - -**앞면 구성**: -- 히어로: "대표님, 지금 우리 회사 어떻게 돌아가고 있나요?" -- 시간대별 고민: 오전 9시(매출 보고 대기), 오후 2시(수주 취합), 오후 5시(결재 서류) -- 대시보드 Mock UI: KPI 카드 4개 + 매출 추이 차트 + 조직 성과 바 차트 -- 약속 박스: "실시간 KPI 파악" - -**뒷면 구성**: -- 대시보드 7대 기능 (01~07 뱃지) -- 역할별 맞춤 화면: CEO, 관리자, 운영자, 영업자 -- SAM 플랫폼 연동: 견적/수주, 생산, 품질, 재고, 인사/회계 -- 가격표 + 도입 프로세스 - -**v1 대비 차이**: -- 타겟이 전체→경영진으로 좁혀짐 -- 타임라인 기반 문제 제시 (시간대별 고민) -- 대시보드 UI Mock 삽입 -- 역할별 화면 분리 소개 - ---- - -### 3.3 v3 — CEO Dashboard 개선판 (다크) - -**컨셉**: v2 기반 개선. Before/After 인포그래픽 추가, SVG 아이콘 강화 - -| 항목 | 값 | -|------|------| -| 배경 | `#0B1929` (짙은 네이비) | -| 주 액센트 | `#0EA5E9` (스카이블루) | -| 카드 스타일 | 반투명 다크 + 컬러 보더 | -| BI 로고 | `sam_bi_white.png` | - -**v2 대비 개선사항**: -- 1page 통합본 추가 -- Before/After 비교 인포그래픽 도입 -- 핵심 가치 3카드: 즉시 현황 파악, 데이터로 판단, 모바일 승인 -- SVG 인라인 아이콘 전면 적용 (외부 이미지 의존 제거) -- PPTX 텍스트 줄바꿈 방지 패턴 적용 (`white-space: nowrap`, 개별 `

` 분리) - ---- - -### 3.4 v4 — CEO Dashboard 라이트 버전 - -**컨셉**: v3와 동일 콘텐츠, 밝은 배경으로 색상 전환 - -| 항목 | 값 | -|------|------| -| 배경 | `#F8FAFC` (밝은 슬레이트) | -| 주 액센트 | `#0EA5E9` (스카이블루) | -| 제목 텍스트 | `#0F172A` | -| 본문 텍스트 | `#475569` | -| 보조 텍스트 | `#94A3B8` | -| 카드 스타일 | 화이트 + `box-shadow` + `#E2E8F0` 보더 | -| BI 로고 | `sam_bi_black.png` | - -**v3 → v4 색상 변환 규칙**: - -| 요소 | v3 (다크) | v4 (라이트) | -|------|-----------|------------| -| 배경 | `#0B1929` | `#F8FAFC` | -| 카드 | `#111D2E` 반투명 | `#FFFFFF` + shadow | -| 제목 | `#FFFFFF` | `#0F172A` | -| 본문 | `rgba(255,255,255,0.55)` | `#475569` | -| 보조 | `rgba(255,255,255,0.3)` | `#94A3B8` | -| 구분선 | `rgba(14,165,233,0.15)` | `#E2E8F0` | -| 기술 태그 | `rgba(255,255,255,0.03)` | `#F1F5F9` | -| 액센트 | 동일 유지 | 동일 유지 | - ---- - -### 3.5 v5 — Premium Executive Gradient - -**컨셉**: 고급 프리미엄 감성. 네이비→인디고 그래디언트 + 골드 액센트 + 글래스모피즘 - -| 항목 | 값 | -|------|------| -| 배경 | `#0F172A → #1E1B4B → #312E81` (175deg 그래디언트) | -| 주 액센트 | `#FBBF24` (골드/앰버) | -| 보조 액센트 | `#10B981`, `#8B5CF6`, `#EF4444`, `#F59E0B`, `#EC4899` | -| 카드 스타일 | 글래스모피즘 (`rgba(255,255,255,0.05)` + `rgba(255,255,255,0.1)` border) | -| BI 로고 | `sam_bi_white.png` | -| 뱃지 | "EXECUTIVE EDITION" 골드 뱃지 | - -**앞면 구성**: -- 히어로: "대표님의 시간은 보고를 기다리는 데 쓰여선 안 됩니다." -- 잃어버린 시간 카드: 1~2일(매출), 반나절(수주), 30분(결재) — 빨간 톤 -- 골드 전환 구분선: "SAM 도입 후" -- 대시보드 Mock: 골드 차트 라인, 글래스모피즘 카드 -- 약속 박스: 골드 테두리 - -**뒷면 구성**: -- 7대 기능 리스트 (글래스모피즘 카드) -- 역할별 맞춤 화면: CEO(골드), 관리자(그린), 운영자(앰버), 영업자(퍼플) -- 가격표: 골드 강조 -- 도입 프로세스: 골드 화살표 연결 - -**기술 특이사항**: -- body CSS gradient → PPTX 미지원 → Sharp로 PNG 사전 렌더링 -- HTML body는 `#1A1640`(단색 fallback), convert 스크립트에서 `slide.background`로 그래디언트 PNG 덮어쓰기 -- 구분선 gradient도 solid rgba로 변환 (PPTX 호환) - ---- - -### 3.6 v6 — Corporate Blue & White - -**컨셉**: 대기업/공공기관 프레젠테이션 스타일. 정돈된 블루 헤더 바 + 순백색 본문 - -| 항목 | 값 | -|------|------| -| 배경 | `#FFFFFF` (순백) | -| 헤더 바 | `#1E40AF` (로열 블루) | -| 주 액센트 | `#2563EB` (블루) | -| 보조 배경 | `#EFF6FF` (블루-50), `#DBEAFE` (블루-100) | -| 카드 스타일 | 화이트 + `#DBEAFE` 보더 | -| BI 로고 | `sam_bi_white.png` (헤더), `sam_bi_black.png` (본문) | - -**특징**: -- 풀 폭 블루 헤더 바에 흰색 BI 로고 + 뱃지 배치 -- 섹션 라벨은 블루-50 배경 + 로열블루 텍스트 뱃지 -- 대시보드 Mock UI: `#DBEAFE` 보더 카드 -- CTA: 블루 배경 풀 폭 바 - ---- - -### 3.7 v7 — Warm Gray + Teal - -**컨셉**: IT/SaaS 기업 스타일. 따뜻한 그레이 배경 + 틸(Teal) 액센트 - -| 항목 | 값 | -|------|------| -| 배경 | `#FAFAF9` (웜 그레이) | -| 주 액센트 | `#0D9488` (틸) | -| 보조 액센트 | `#10B981`, `#8B5CF6`, `#EF4444`, `#F59E0B`, `#EC4899` | -| 카드 스타일 | 화이트 + `#E7E5E4` 보더 + 미세 그림자 | -| 구분선 | `#D6D3D1` | -| BI 로고 | `sam_bi_black.png` | - -**특징**: -- 부드러운 웜 톤 배경으로 눈의 피로 감소 -- 틸 계열 SVG 아이콘 전면 적용 -- 가벼운 카드 스타일로 정보 구분 -- 타임라인 인포그래픽: 시간대별 고민 (9AM/2PM/5PM) - ---- - -### 3.8 v8 — Two-Tone Navy/White Split - -**컨셉**: 금융/컨설팅 프레젠테이션 스타일. 다크 상단 + 화이트 하단 투톤 분할 - -| 항목 | 값 | -|------|------| -| 상단 배경 | `#1E293B` (슬레이트-800) | -| 하단 배경 | `#FFFFFF` (순백) | -| 주 액센트 | `#F97316` (오렌지) | -| 보조 액센트 | `#FB923C` (오렌지-400) | -| 카드 (다크) | `rgba(255,255,255,0.08)` + `rgba(255,255,255,0.12)` 보더 | -| 카드 (라이트) | 화이트 + `#E2E8F0` 보더 + 컬러 좌측 보더 | -| BI 로고 | `sam_bi_white.png` (다크), `sam_bi_black.png` (라이트) | - -**특징**: -- 앞면 상단: 다크 존에 KPI 카드 + 히어로 메시지 -- 앞면 하단: 화이트 존에 가치 카드 + 기능 그리드 -- 기능 카드에 컬러 좌측 보더 포인트 (오렌지/그린/퍼플/레드) -- 다크 CTA 박스: 하단 풀 폭 다크 배경 + 오렌지 액센트 - ---- - -### 3.9 v9 — Minimal White + Indigo - -**컨셉**: Apple/디자인 에이전시 스타일. 극도의 미니멀리즘 + 인디고 수직 액센트 라인 - -| 항목 | 값 | -|------|------| -| 배경 | `#FFFFFF` (순백) | -| 주 액센트 | `#6366F1` (인디고) | -| 카드 배경 | `#F8FAFC` (거의 투명한 그레이) | -| 카드 보더 | `#F1F5F9` | -| 본문 텍스트 | `#334155` (슬레이트-700) | -| 보조 텍스트 | `#94A3B8` | -| BI 로고 | `sam_bi_black.png` | - -**특징**: -- 좌측 3pt 인디고 수직 액센트 라인 (풀 하이트) -- 타이포그래피 중심 레이아웃 (아이콘 최소화) -- 거의 보이지 않는 카드 구분 (barely-there cards) -- 기능은 텍스트 로우 형태 (인디고 불릿 도트만) -- 가격/프로세스도 텍스트 기반 미니멀 표현 - ---- - -## 4. 폴더 구조 - -``` -docs/brochure/ -├── README.md ← 이 파일 -├── v1/ -│ ├── slides/ -│ │ ├── brochure-2page-front.html -│ │ ├── brochure-2page-back.html -│ │ └── brochure-1page.html -│ ├── convert-1page.cjs -│ ├── convert-2page.cjs -│ ├── sam-brochure-1page.pptx -│ └── sam-brochure-2page.pptx -├── v2/ -│ ├── slides/ -│ │ ├── brochure-dashboard-front.html -│ │ ├── brochure-dashboard-back.html -│ │ └── brochure-dashboard-1page.html -│ ├── convert-1page.cjs -│ ├── convert-2page.cjs -│ ├── sam-brochure-v2-dashboard-1page.pptx -│ └── sam-brochure-v2-dashboard-2page.pptx -├── v3/ -│ ├── slides/ -│ │ ├── brochure-dashboard-front.html -│ │ ├── brochure-dashboard-back.html -│ │ └── brochure-dashboard-1page.html -│ ├── convert-1page.cjs -│ ├── convert-2page.cjs -│ ├── sam-brochure-v3-dashboard-1page.pptx -│ └── sam-brochure-v3-dashboard-2page.pptx -├── v4/ -│ ├── slides/ -│ │ ├── brochure-dashboard-front.html -│ │ ├── brochure-dashboard-back.html -│ │ └── brochure-dashboard-1page.html -│ ├── convert-1page.cjs -│ ├── convert-2page.cjs -│ ├── sam-brochure-v4-dashboard-1page.pptx -│ └── sam-brochure-v4-dashboard-2page.pptx -├── v5/ -│ ├── slides/ -│ │ ├── brochure-dashboard-front.html -│ │ ├── brochure-dashboard-back.html -│ │ └── brochure-dashboard-1page.html -│ ├── convert-1page.cjs ← Sharp 그래디언트 배경 생성 포함 -│ ├── convert-2page.cjs ← Sharp 그래디언트 배경 생성 포함 -│ ├── sam-brochure-v5-dashboard-1page.pptx -│ └── sam-brochure-v5-dashboard-2page.pptx -├── v6/ ← Corporate Blue & White -│ ├── slides/ (front, back, 1page) -│ ├── convert-1page.cjs -│ ├── convert-2page.cjs -│ ├── sam-brochure-v6-dashboard-1page.pptx -│ └── sam-brochure-v6-dashboard-2page.pptx -├── v7/ ← Warm Gray + Teal -│ ├── slides/ (front, back, 1page) -│ ├── convert-1page.cjs -│ ├── convert-2page.cjs -│ ├── sam-brochure-v7-dashboard-1page.pptx -│ └── sam-brochure-v7-dashboard-2page.pptx -├── v8/ ← Two-Tone Navy/White Split -│ ├── slides/ (front, back, 1page) -│ ├── convert-1page.cjs -│ ├── convert-2page.cjs -│ ├── sam-brochure-v8-dashboard-1page.pptx -│ └── sam-brochure-v8-dashboard-2page.pptx -└── v9/ ← Minimal White + Indigo - ├── slides/ (front, back, 1page) - ├── convert-1page.cjs - ├── convert-2page.cjs - ├── sam-brochure-v9-dashboard-1page.pptx - └── sam-brochure-v9-dashboard-2page.pptx -``` - ---- - -## 5. PPTX 변환 방법 - -```bash -# 각 버전 폴더에서 실행 -cd docs/brochure/v5 -node convert-1page.cjs # 1페이지 통합본 -node convert-2page.cjs # 앞면+뒷면 2페이지 -``` - ---- - -## 6. PPTX 변환 시 주의사항 - -### 6.1 텍스트 줄바꿈 방지 - -- 단일행 `

` 태그에 `white-space: nowrap` 필수 -- `
` 멀티라인 `

`는 개별 `

` 태그로 분리 -- `` 포함 텍스트도 개별 `

` 처리 (PPTX 폰트 폭 차이 보정) - -### 6.2 CSS gradient 미지원 - -- html2pptx.js는 CSS `linear-gradient`를 지원하지 않음 -- body gradient → Sharp로 PNG 사전 렌더링 후 `slide.background`에 설정 -- 구분선 gradient → solid `rgba()` 색상으로 변환 - -### 6.3 SVG 처리 - -- 인라인 SVG는 html2pptx가 자동으로 PNG 래스터화 -- SVG 내부 fill 색상은 배경에 맞게 조정 필요 - ---- - -**최종 업데이트**: 2026-03-01 (v6~v9 추가) diff --git a/sam/docs/brochure/v1/convert-1page.cjs b/sam/docs/brochure/v1/convert-1page.cjs deleted file mode 100644 index 5252ffa..0000000 --- a/sam/docs/brochure/v1/convert-1page.cjs +++ /dev/null @@ -1,28 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - // 9:16 세로형 (Portrait) - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-1page.html'); - console.log('Converting 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v1/convert-2page.cjs b/sam/docs/brochure/v1/convert-2page.cjs deleted file mode 100644 index d3590f5..0000000 --- a/sam/docs/brochure/v1/convert-2page.cjs +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - // 9:16 세로형 (Portrait) - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-2page-front.html', 'brochure-2page-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v1/sam-brochure-1page.pptx b/sam/docs/brochure/v1/sam-brochure-1page.pptx deleted file mode 100644 index 5fe19a426937eeb406a54df3e2cb4392e5268aa1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115703 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 bS;J~qamkOcj5*#g-mnPPO8-uv55xZlBjQ$Y diff --git a/sam/docs/brochure/v1/sam-brochure-2page.pptx b/sam/docs/brochure/v1/sam-brochure-2page.pptx deleted file mode 100644 index 016084a1b90016ebe0dbf9668abfb466e467237f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180473 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 diff --git a/sam/docs/brochure/v1/slides/brochure-1page.html b/sam/docs/brochure/v1/slides/brochure-1page.html deleted file mode 100644 index 8eea8ab..0000000 --- a/sam/docs/brochure/v1/slides/brochure-1page.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - -

- -
-

PRODUCT BROCHURE 2026

-
- - -
-

SMART AUTOMATION MANAGEMENT

-

중소 제조업을 위한
ERP/MES 통합 플랫폼

-

품목관리, 견적, 수주, 생산, 출하, 품질, 인사/회계까지
제조업의 모든 업무를 하나의 시스템으로 통합합니다.

-
- - -
- - -
-

현재 업무 과제

-
-
-

Excel 수작업

-

오류 잦음, 시간 낭비

-
-
-

현황 파악 불가

-

생산/재고 실시간 X

-
-
-

ERP 도입비 부담

-

수천만~수억원

-
-
-
- - -
-

SAM 핵심 기능

-
- -
-
-
-
-

01

-
-

견적/수주 자동화

-
-

BOM 전개, 단가 적용, PDF 견적서 자동 생성

-
-
-
-
-

02

-
-

생산관리 (MES)

-
-

바코드/QR 공정추적, 실시간 현황 대시보드

-
-
- -
-
-
-
-

03

-
-

품질/검사 관리

-
-

수입/공정/출하 3단계 검사, 인증 자동 알림

-
-
-
-
-

04

-
-

자재/재고 추적

-
-

안전재고, LOT 추적, 바코드 입출고 관리

-
-
- -
-
-
-
-

05

-
-

인사/회계 (무료)

-
-

근태, 급여, 매입매출, 세금계산서 자동 발행

-
-
-
-
-

06

-
-

경영 대시보드

-
-

수주/생산/매출/품질 KPI 실시간 모니터링

-
-
-
-
- - -
-
-

전자서명

-
-
-

카카오 알림톡

-
-
-

AI 실험실

-
-
-

바로빌 연동

-
-
- - -
- - -
- -
-

도입 기대 효과

-
-
-
-

80%

-

업무 시간 단축

-
-
-

95%

-

납기 준수율

-
-
-
-
-

100%

-

이력 추적성

-
-
-

Free

-

인사/회계 포함

-
-
-
-
- -
-

투자 비용

-
-

제조업 기본 패키지

-

2,000만원

-

+ 월 50만원 (유지보수)

-
-

품목-견적-수주-생산-출하
인사/회계 무료 포함

-
-
-
- - -
-
-

클라우드 기반 (설치 불필요)

-
-
-

모바일 대응

-
-
-

Multi-tenant

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

무료 데모 및 상담

-

contact@codebridge-x.com

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v1/slides/brochure-2page-back.html b/sam/docs/brochure/v1/slides/brochure-2page-back.html deleted file mode 100644 index a285d16..0000000 --- a/sam/docs/brochure/v1/slides/brochure-2page-back.html +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - -
- -
-

FEATURES & PRICING

-
- - -
-

SAM 핵심 모듈

-
- -
-
-

01

-
-

품목/BOM 관리

-

품목 마스터, 다단계 BOM 전개, 단가 관리

-
- -
-
-

02

-
-

견적/수주 자동화

-

견적서 자동 생성, 수주 전환, PDF 출력

-
- -
-
-

03

-
-

생산관리 (MES)

-

작업지시, 바코드/QR 공정추적, 실시간 현황

-
- -
-
-

04

-
-

출하/물류 관리

-

출하 지시, 거래명세서, 배송 추적

-
- -
-
-

05

-
-

품질/검사 관리

-

수입/공정/출하 검사, 인증 만료 자동 알림

-
- -
-
-

06

-
-

자재/재고 관리

-

안전재고, 입출고, LOT 추적, 바코드 관리

-
- -
-
-

07

-
-

인사/회계 (무료)

-

근태, 급여, 매입매출, 세금계산서 자동 발행

-
- -
-
-

08

-
-

경영 대시보드

-

수주/생산/매출/품질 KPI 실시간 모니터링

-
-
-
- - -
- - -
-

확장 기능

-
-
-

전자서명

-

계약/확인서

-
-
-

알림톡

-

카카오 자동발송

-
-
-

AI 실험실

-

음성요약/문서분류

-
-
-

QR 코드

-

설비/장비 점검

-
-
-
- - -
- - -
-

투자 비용

-
- -
-
-

제조업 기본 패키지

-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

품목 - 견적 - 수주 - 생산 - 출하
인사/회계 무료 포함

-
-
- -
-
-

추가 옵션 (선택)

-
-
-

생산공정 추가

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

사진등록/챗봇/녹음

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
-

Step 1

-

1~2주

-

현장 인터뷰

-
-
-

Step 2

-

2~4주

-

맞춤 개발

-
-
-

Step 3

-

1~2주

-

데이터 이관

-
-
-

Step 4

-

1~2주

-

교육/안정화

-
-
-
- - -
-
-

바로빌 API

-

세금계산서 자동

-
-
-

카카오 알림톡

-

점검/납기 알림

-
-
-

이카운트 연동

-

기존 ERP 동기화

-
-
- - -
-
-
-

무료 데모를 신청하세요

-

귀사에 최적화된 맞춤 데모를 제공합니다

-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v1/slides/brochure-2page-front.html b/sam/docs/brochure/v1/slides/brochure-2page-front.html deleted file mode 100644 index fe6da03..0000000 --- a/sam/docs/brochure/v1/slides/brochure-2page-front.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - -
- -
-

PRODUCT BROCHURE 2026

-
- - -
-

SMART AUTOMATION MANAGEMENT

-

중소 제조업을 위한
ERP/MES 통합 플랫폼

-

품목관리, 견적, 수주, 생산, 출하, 품질, 인사/회계까지
제조업의 모든 업무를 하나의 시스템으로 통합합니다.

-
- - -
- - -
-

이런 고민이 있으신가요?

-
-
-
-

Excel 견적서, 수기 전표로 업무 시간 낭비가 심하다

-
-
-
-

생산 현황을 실시간으로 파악할 수 없다

-
-
-
-

품질/검사 기록이 체계적으로 관리되지 않는다

-
-
-
-

ERP 도입비가 수천만원~수억원으로 부담된다

-
-
-
- - -
-

SAM이 해결합니다

-

- SAM은 중소 제조업에 특화된 클라우드 ERP/MES 통합 플랫폼입니다. - 품목/BOM 관리, 견적 자동화, 바코드 생산추적, 품질검사, 인사/회계까지 - 별도 설치 없이 웹 브라우저만으로 모든 업무를 통합 관리합니다. -

-
- - -
- - -
-

도입 기대 효과

-
-
-

80%

-

업무 시간 단축

-
-
-

95%

-

납기 준수율

-
-
-

100%

-

이력 추적성

-
-
-

Free

-

인사/회계 포함

-
-
-
- - -
-
-

클라우드 기반

-

설치 불필요

-
-
-

모바일 대응

-

현장 태블릿/폰

-
-
-

Multi-tenant

-

데이터 완전 격리

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능과 가격을 확인하세요

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v2/convert-1page.cjs b/sam/docs/brochure/v2/convert-1page.cjs deleted file mode 100644 index d11c9ee..0000000 --- a/sam/docs/brochure/v2/convert-1page.cjs +++ /dev/null @@ -1,28 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - // 9:16 세로형 (Portrait) - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-v2-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v2/convert-2page.cjs b/sam/docs/brochure/v2/convert-2page.cjs deleted file mode 100644 index 7d4aed0..0000000 --- a/sam/docs/brochure/v2/convert-2page.cjs +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - // 9:16 세로형 (Portrait) - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-v2-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v2/sam-brochure-v2-dashboard-1page.pptx b/sam/docs/brochure/v2/sam-brochure-v2-dashboard-1page.pptx deleted file mode 100644 index d5fb8783d50d8bab565d050fbc1eab4de54e7ac5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142338 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 T#yr7tg5^@+mC?z7ZkGQKEZ1b* diff --git a/sam/docs/brochure/v2/sam-brochure-v2-dashboard-2page.pptx b/sam/docs/brochure/v2/sam-brochure-v2-dashboard-2page.pptx deleted file mode 100644 index 315707d277bba9b4ccbbf7e63a8466c30345677c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 202367 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

NKXi_~2D zjl`13FOREvNPPcI^Y3=O_{VD=Z4CeE>i>B6iw|vj@r&Ju{G~p8$T`vD_xqk#A9?3; zQ9Vt3vvSdGuYT_F4=lds+MCb*@6C}9KlAPvlMj9{dgO~|z5T(z|Mah+^IrO0ZqtWP zss8*=AHD3Rckf(x;vbJ>@IMulf98FWvp2 z#viOZ;YWu)Tl3t|ML_3KmXJ2+ds3eB6YR!_6v`nd+1AdFMs-@PrhDt)5Bjp z_U$jLf4DO9rmp=2GFCQJ9_tIH&ntt-B zSHBVb-T&U4I{ubTi#}QT%j34(@#o+4-161e5|6H&-%&5D@4WGe?{8XH@y2=Yy!(6c znh(#Owf7?z{p{7}cR#xBsgu6dEh}Z7<$FI8vu08v_%ilcft#8h& zyuLB8PmEU>ifu(ESJMGjzt-tc6cMqI-Pt{G!zua}z zVgK{`kxxYCSN#5{tL6>tIL*KA`FlR`%wwPI`9kB8e}CfxCoarA{La_AzWsy0oEm)X zoTE29^}^#%&42RJHMO4e|9a2EYbrn4)4nm?)Axrv*HwJ~D~J9l^6t~SYc|~)ZBR`(^V#Ckg===9SHyr<`o0t4BcVOKchcvA};mJdPa>ONXE!uR{ zSN>;l*}uP=J^8aMuCA<}^SAjmhi$rGNzKx0<{l7weza-lCu){nIro68r)Sg7#WhQ> znEN}Sr*6~E<7<{~nEN|d&zwy=t7_(6GdDiJ=7PElzBYe@YxWm<=Fea7+=)FGY!1=Tsn8*T-TfRb7#%JY)MVJVU9R|!-=j<8|QDIUwvxNi|gmUyl_sWZvN2=k9BR> zH}{FT3x3ov_s~tk;+po&b2CEE6`O<))U@}^%`EJ}*%NB^;H<0X%1wg1W)IGu*mL-% z7jFwzCVu$V$#dmTxz_%9>yc00zOd)`O)rLOj-9(;(KYiAt)8>;#GYAo&;7?|H@^Rw zTfXwm@Ci?SY)Nc!>!OXlFG{~XtLKyjkN^9o7qjQq?EB#E+PmgIQTdBnVf~StE_ig$ z!ugLk)b%ajJAdv4hq<1s_}tt%eMc|Iy>k5QS*uT*^SAEHPb=%4Uw!Yw!ujhDarM1>&fGaK_RRgt?>}%^*>P{} zop=66uK(#LA5|A$bK8PhS3LFF{3}kMbL=M;T(Guo{YyAe&2DG5wENs@UIr?Y}J3r|FF1_rNHo{gHeA1)pdt(v(T1RrgT0&C%~FBe$J_52;u` z)E#Wfh|I1z;F8DXo_HJh%oX6^zki;Obad+_IrHS`}l;C5$ zJGpT=KsZmuq2A#!}?(j-)b*)$MD7Tg4oRAo-Ba2*%%G zA7e|$W^V2aK*q6rla;e!7!u0L$mbYiBpi7#Hi7)Y+`q!Lc>dnE{`tZYWo6{UkKjIy zjZip|!V%87Wy_N%09nb0UymirjS(nguv(Aj-n;tRhcIFC;lbeQ71}GvEn1OR)oOsc z`G+s|0Ak38e|~l)8=y0$bQOL6(%|2+_hKu_XCdZb{QW09IeFs2QauMZ=qOx`XMb`1 zTS2S@`J8Hug`+3Vv>1-m4!ZfJm_7-L;8zW!yoM{2{iPEJtjaj&!gL2RUTcArunLgJ=fRrZe70#ryKI^0L2xPhNC52xZy-2aF^M?EnA( diff --git a/sam/docs/brochure/v2/slides/brochure-dashboard-1page.html b/sam/docs/brochure/v2/slides/brochure-dashboard-1page.html deleted file mode 100644 index eca646a..0000000 --- a/sam/docs/brochure/v2/slides/brochure-dashboard-1page.html +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - -

- -
-

CEO DASHBOARD EDITION 2026

-
- - -
-

EXECUTIVE DASHBOARD

-

대표님, 지금 우리 회사
어떻게 돌아가고 있나요?

-

보고를 기다리지 마세요. SAM 대시보드 하나면
매출, 수주, 조직 실적, 승인 대기까지 한눈에 파악합니다.

-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
-
-

월 매출

-

5.2억

-

+15.3%

-
-
-

수주 잔량

-

127건

-

+8건

-
-
-

납기 준수율

-

96%

-

목표 초과

-
-
-

승인 대기

-

5건

-

즉시 처리

-
-
- -
- -
-

월별 매출 추이

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-

조직별 실적

-
-
-
-

영업1팀

-
-
-
-
-
-
-

영업2팀

-
-
-
-
-
-
-

생산팀

-
-
-
-
-
-
-

품질팀

-
-
-
-
-
-
-
-
- - -
-

대표님이 얻는 것

-
-
-
-
-

1

-
-

즉시 현황 파악

-
-

보고를 기다릴 필요 없이
로그인만으로 전사 현황 확인

-
-
-
-
-

2

-
-

데이터 기반 의사결정

-
-

감이 아닌 숫자로 판단
매출 추이, KPI, 팀 성과 비교

-
-
-
-
-

3

-
-

빠른 승인/결재

-
-

대기 건수를 실시간 알림
모바일에서도 즉시 승인 처리

-
-
-
- - -
- - -
-

대시보드 핵심 기능

-
-
-
-
-

실시간 매출/수주 KPI

-
-
-
-

조직 계층별 실적 트리

-
-
-
-
-
-

역할별 수당 현황

-
-
-
-

미승인 건수 실시간 알림

-
-
-
-
-
-

기간별 트렌드 분석

-
-
-
-

수익 시뮬레이터

-
-
-
-
- - -
- - -
- -
-

BEFORE

-
-
-

"매출 얼마야?" → 보고 대기 1~2일

-

"수주 현황?" → Excel 취합 반나절

-

"승인할 것 있어?" → 서류 뒤지기

-

"팀별 실적?" → 각 팀장 개별 보고

-
-
-
- -
-

AFTER (SAM)

-
-
-

로그인 → 3초만에 전사 현황

-

클릭 한 번 → 실시간 수주 데이터

-

빨간 뱃지 → 즉시 승인 처리

-

트리 구조 → 전 조직 실적 한눈에

-
-
-
-
- - -
-
-

PC + 모바일

-
-
-

실시간 업데이트

-
-
-

역할별 권한

-
-
-

클라우드 기반

-
-
-

데이터 암호화

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

무료 데모 신청

-

contact@codebridge-x.com

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v2/slides/brochure-dashboard-back.html b/sam/docs/brochure/v2/slides/brochure-dashboard-back.html deleted file mode 100644 index 147b8bf..0000000 --- a/sam/docs/brochure/v2/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,242 +0,0 @@ - - - - - - - - -
- -
-

DASHBOARD FEATURES & PRICING

-
- - -
-

대시보드 핵심 기능

-
- -
-
-

01

-
-

실시간 KPI 카드

-

월 매출, 수주 잔량, 납기 준수율, 승인 대기 한눈에

-
- -
-
-

02

-
-

조직 실적 트리

-

계층 구조로 각 팀/개인 실적 펼쳐보기

-
- -
-
-

03

-
-

역할별 수당 현황

-

판매자/관리자/협업자 수당 배분 실시간 확인

-
- -
-
-

04

-
-

승인 대기 알림

-

가입/지급 승인 미처리 건수 빨간 뱃지로 강조

-
- -
-
-

05

-
-

기간별 트렌드

-

당월/분기/연간 매출 추이 차트, 성장률 비교

-
- -
-
-

06

-
-

수익 시뮬레이터

-

가상 시나리오로 수당/마진 사전 계산

-
- -
-
-

07

-
-

모바일 대응

-

이동중에도 스마트폰으로 KPI 확인 및 승인

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
-
-

CEO

-

전사 KPI

-

매출/수주/조직 총괄

-
-
-

관리자

-

팀 실적 관리

-

하위 조직 성과 추적

-
-
-

운영자

-

인력/승인 관리

-

가입/지급 승인 처리

-
-
-

영업자

-

내 실적 조회

-

계약/수당 현황 확인

-
-
-
- - -
- - -
-

대시보드 + SAM 통합 플랫폼

-
-
-

견적/수주

-
-
-

생산 (MES)

-
-
-

품질/검사

-
-
-

재고/자재

-
-
-

인사/회계

-
-
-

대시보드에 표시되는 모든 데이터는 SAM ERP/MES 실시간 데이터 기반

-
- - -
- - -
-

투자 비용

-
- -
-
-

대시보드 포함 기본 패키지

-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

CEO 대시보드 + 견적/수주 + 생산
인사/회계 무료 포함

-
-
- -
-
-

추가 옵션 (선택)

-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
-

1

-

1~2주

-

현장 인터뷰

-
-
-

-
-
-

2

-

2~4주

-

맞춤 개발

-
-
-

-
-
-

3

-

1~2주

-

데이터 이관

-
-
-

-
-
-

4

-

1~2주

-

교육/안정화

-
-
-
- - -
-
-
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험해 보세요

-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v2/slides/brochure-dashboard-front.html b/sam/docs/brochure/v2/slides/brochure-dashboard-front.html deleted file mode 100644 index 0df1434..0000000 --- a/sam/docs/brochure/v2/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - -
- -
-

CEO DASHBOARD EDITION

-
- - -
-

EXECUTIVE DASHBOARD

-

대표님, 지금 우리 회사
어떻게 돌아가고 있나요?

-

매출이 얼마인지, 수주가 밀려있는지, 승인할 건이 있는지
더 이상 보고를 기다리지 마세요.

-
- - -
- - -
-

대표님의 하루

-
-
-

AM 9:00

-

"어제 매출 얼마야?" → 팀장 보고 대기중...

-
-
-

PM 2:00

-

"수주 밀린 거 없어?" → Excel 취합중...

-
-
-

PM 5:00

-

"결재할 것 좀 정리해줘" → 서류 찾는중...

-
-
-
- - -
-

-

SAM으로 바꾸면

-
- - -
- -
-
-
-
-

SAM CEO Dashboard ― 로그인 후 3초

-
- -
-
-

월 매출

-

5.2억

-

▲ 15.3%

-
-
-

누적 수주

-

127건

-

▲ 8건

-
-
-

납기 준수율

-

96%

-

목표 달성

-
-
-

승인 대기

-

5건

-

즉시 처리

-
-
- -
-
-

월별 매출 추이

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

조직별 실적

-
-
-
-

영업1팀

-
-
-
-
-
-
-

영업2팀

-
-
-
-
-
-
-

생산팀

-
-
-
-
-
-
-
-
- - -
-

SAM 대시보드가 드리는 약속

-

- 로그인 한 번이면 전사 매출, 수주, 조직 실적, 승인 대기 건수를 - 한눈에 파악합니다. 보고를 기다리는 시간을 제로로 만들어 드립니다. -

-
- - -
-
-

클라우드 기반 (설치 불필요)

-
-
-

모바일 대응

-
-
-

역할별 권한 분리

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능을 확인하세요

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v3/convert-1page.cjs b/sam/docs/brochure/v3/convert-1page.cjs deleted file mode 100644 index 38f401a..0000000 --- a/sam/docs/brochure/v3/convert-1page.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard v3 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-v3-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v3/convert-2page.cjs b/sam/docs/brochure/v3/convert-2page.cjs deleted file mode 100644 index ee5132c..0000000 --- a/sam/docs/brochure/v3/convert-2page.cjs +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-v3-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v3/sam-brochure-v3-dashboard-1page.pptx b/sam/docs/brochure/v3/sam-brochure-v3-dashboard-1page.pptx deleted file mode 100644 index cbb60848d6633e033cf2da6d56e1773df2997004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167362 zcmeEP2V4_L7srmC1?*>y;XEslUQke~Vgm(x4M~7Vnn^&godp$pKf7Y@ioF*+%blLR z>)AWSa_VQ-@6GHc*^LQ;1`aiMzu=bH+1V+t{@mW2wJa6O7oZO|(fGI=XWf+jZ0CRZZcKr8;ahST2BTB0||by+o? z_6}q5&%MLg>9}E8)lO@S{_zPe_N|k7g>tMjsYP~lz+F;On zd3x%l(3cXuhej(`!<|TtP9-tGsV>S>E0M-XqU4?&7E9nMCEDR`K)gqfdBBsna(%Qy zt8ZZY4ZXGUD2q2JROr+24ePDyt+n+?l#TDu$60>@u4mCUt=jri=(_cmMJXb!ehj*9 z{RtH>&fw>^-xI^YU2d4)fgISqMWep-7-qupKBAQ?ndfLIk9`2D0OG6O?prK zMDDcXb1gtai^UszkBq6}&$S>$aKc2u$F3*|YQ=*09XQM`C#YPF1DnKx4xEMqengedoUagpe$0Fs{* zQX}mxo>n>!s+Z`Z0d`o z6suDMgf6w-D-I^dm(4!oXLi6V6HW5Q;TnTP8Lm*teORzyO($@tqedswqpQRLT=6w) zFFvgmQk_Pxi8L@mu-M87)%2C?LNyAtp|K~mAUqdJ5gdcCKIIdlcGpXFa=Drj7Om;a zsLK|3iR(4?v{Dj1FVRV&bP{beZVu%P?uB7R(}&&IlRScJZIG6WE)fTCrMW^TldH)R z@JY0DxZk?9zY;Vi^tHxARJT;3l>5Vg`b0{UdO5tpbOF@~vNH(hp%R5ok5^`#!7ENK zHE47Uy`moo%>pJPLZX)w&V}a`+DDO=wg0-+}v@5w0+R0AjCK5ub;<7)@PI zixFr{ye%XW=`GfW1avs_kl@ZR4+zmWzA#KmN`sAEPTcjEC>0Sph0XiqTCnV3dL=~H z5FJWXim^MFT7N$^+L?a{Az*>dr7s~A(}Mq zXqz7rbXX=eRjG~DDY1!2CiRpnVRgYu_GEjoJ(=X|Wl}IJs?1g<>I3)=rKCz>z{Vny z##4*Rq{K~A>3F@u)QG-)`+D@{;%0z_itX8{b*nIJ%yL(#k@-t1C&U_=)XRX@rFkkb zsC2inr>Ov%mvEwwjpz-lF01m!BIFZ5+9f~;%}?M;jaN}27$+szT3dsaE>a&{|_ zvqWAzkr$WK1vQZNJSOSJYbvstUtFoN0=%|AnWGlVqh_5WvM8|lqhC!}9%i#XRqH|P zB>&0Hxi2*95yvO2EM`HFa*;H45G4q;4vbnCZf@$XWRmd2GQ&;HX319 zITwj#C*}%8ChyjAqB54KgMu!Lcj7Pjk5fj zFXZBlC0@8Dl66W<3@!`TjEu09tCWQHge=HRh8o+hsWKV(X~@b0NiK~pNR>LwDCM*# za9LRl83&CMw~?H%>;|QOw%_U6f`CSu`syd%|!AKi-oM`o)RzeA*I2_vy|t8 z@PFiU~$45wEr^51hdKZTHuVy1!#@R@K87n~UH3 zJ>~wUlzTJAKU_7{O6|TnNd&bxGH&P@ZRC9!?KCR*Kr8K+N9c0)M0m7BL4Cp(;0_YC z1ii&d{egyPTIn_k5~ajSIX^kQA3NxP)L1LnEVehT!C}$5ScAe!jV+^9w1TZQ8l|5` zXSEkvqIXfcIXqS!W&P#4SZcX;5=or3cl%4!v{4R>)q>qYVXaF4XgRH!p-M27fIC_( zH!#pWO^jS_Q_j;59k*2lmUm4G$TV6Av-4Sj{6a~KDS+U9U~P{?V+{dsE89w;oeN#+TH?!@Y> zd_^<4hBkl05|oh=xiwPbt5eZPEH*b;snIVsiZ-=d$-%|ZS0aS6Q#MQ8|xCDk>~gsAnep@1_pf(x~5u1Y-(8>Wu0zbphX$&0| zhG_8aBG5S-TQZ~cWYtl6hE@ZfNEW_qwQg*HidBJgjxc$$*+%OoyitOjcI0&ypS0M4 zRTYpQ#8rXNY{oNHk6Rn1M>QK8fS%1sBPcJUH-c-W5klid)Jez-iu%t=Bk-5mr4?c; ztq`H=jYIWGE1+qnQ317^0m`~Jlh1=k;2|&+fQSyDGvo8d(ZyY8&ylv(nM&#m05O~R z66#2cQCn?(UXDhZ)(FaUqc;Ng3>NV8(Vz?)XQA=3oyS^i9#UHKP*>p3hN)$4hEPOa zwc3g=9cy^bk~S#SR7#;#Gx`G7h|7m5Bb5NkAref-aH!J4+|eV=OQMW|uo$U9M=ba+ zp)6XR-Vh+sM-v^6i4GxW0~|rFV<;6WZzlSD(`-=Z0uv4y}o+eI#!V_%4>RtT!mlsA)Lj0Xv7Zxq+%ZO0M>e@+Qit&YP)PXXE z9y|zaqBmsKwj{Y?BdvppYL!bshnxD(3fIjB>!&o${4!dHp%#Bih7rvsuhN@l{J_sN zEjt1Mh3X58)mrqMiAPv{9bEh(&JWL7&RVz>X_PB*Ph;OX)xE93=q)lnQICfboYy)D z2t;96CLzcMERlEvxisL25vv1eS$`Jam*>yn`@6G5{6Kdem&bA!@qM}OzI;BH9mr?1 z0{l1wfw35(Rct=tw+{k0hBATl-s9_^c<@z5^st9iqw*xcH3SR-d+^yDPn#Zmh60FA ze*q^*%oY0y+{IkqAa`DXpU7S8FYt2@5cmrNf&%>cB5}|_CIe#hpfJyu>(3JeirfWk ze}8wL0H9yKP{3Wt<8XX=LSIoJ&vzgx#=w^o2C0It|?fC=P2z8o)a84PskH-1w4rM)p?Zo_6U4WSU-j|9{41i2f`bY zK@z-~Vm8kxQ^|j=5`id?9MEqknLxkp06o_YX1c9Q_lgM4dwR56|2`Y_5dWGyDk-39V z?|{(YAkuM4)GK5Mi76c<;0ql@8qXlqD_9`@RB8k%nW}YIC_J5JPuyR#0K}uf&zPhL z700lCgc!G%wgWOHp|5;7?mPeKJDwK3dGYdu6T0xC4$pTptvc)sq!z(4^q1905^SUeHf0{jB}#X$jVe=b&`>B0U1E+u?eg`Aww z&p2slF&AGmX^4ux`up= zvrq_}9s=#;vQ}~k#m34QU%*?JVWhtR)q}&Ai1(pWX_!(*zFn`D zKwN1v$OwehMB+3A+mx`{j*F9JAy65|NE1pT)QWsqh#>3Z8Av(gf_4qnfQ^mN>huX# zE+ciFcuwo3(J4UlWPqB?VZ*baA#>SAtz2quZd5G9gb`*ec-q`}Qul|O=CW8DN7}Oa z5~U`~78C8Iwbp#edP{*5LpzNfZK@jzTX}U9^lKP|oq~FetpP#-mUWOVfH)-V7jbwd zSWuDX!ncNX5JHQ7wMZ(0$I({Cl`mXwD7q3GL5O<4!zIVSoKF`=c*}|JMYD(u2nq-c z@@?NL951+tC^X?CqT0zL!D0jx6DBiZ5iuFO35P*M6k+5b3*)IsJgnD-L+S%mjXJKG zP8)`KGECbxaj@9YG6P!}5xa!u9lk*p!>4K}M$8J@<59+%Bwq26Iu%-VU|2;qQnbeMw?!WmIC=%aESU22hhTyQrB9d z4T-~YnF^;@1j932(!!J{K7&#ORil&<^UPwlpF1`AsSu*GamV}}3 zWd@=ch%gX;38jT*3#CQIvHaL#j+iuSQi{Q9iHITv`tO&tfL@}I`Y;%nUV>i$eK3_; za*Wh67^Sde0I7m#7b&@YQR;m-Jm=+cmn5z8-`qgHCRcMkC6OFd^;W&FHO^8)!n9 zszekSeFhZA^cvSevIGvFD+acO1Qm`@jKCwF#5+JP!UUhg=d;960PzU2FqA+qBkF=m z5hw&AUsS+6k?&sz&B2y`NgOGOnTrr|m{IqHUPL3P*P_4bxKH^X#KaT)4~c0op!jiO zP>7L7kHDu}_AeScbA9;hjUNyHlAq3F@eyW$5GMH|fe?qwni-==2=~EX|LPMt3ppk}FgCY*-80bm-4>?NkErxhMjVuA(kAQzc6zB^%kGz>` z$XN>!!@(#=EzE6$@jz!hh%g?2C&ynyICCJ|2WSo6Of(|!CZa<8YCYj4!tHXv?oILz&7G9JC*9c`ta(_((PsN$LLq>TLlgmYbRYk~5JrG+SaZJ+-*y3vI4&9} z!ugB~z_*}OSIEisnJy;B1xDIDiQte}U$$2nbPHvbA&vwO*=vnjU?c|T4^Zc0>k+R0AOO|{HFU6Ia!YweHVeX3IJwhXH~KS zeffb_Q+FgY2Q9`(MJ zHHz>X#B6})$@@84ql6RLG_!C^Xh2;nOp|;ewb(bmJk!Sx&YUzSeawu8jEq?+=}T8L zGG~p=7`i$KYt*7EO^Xz!HtN>wh&$BLHql|nZ1|jPWp@cFW7*)eZApxb-#4ZIzL=3Q zY)Iy?IT>@erKJqcm^HPD9p*&N1ST**QwTRV*9bQoR1^*uXST5+F>yqE;eW1}js|%AK4DT$_>AGNw;tq)%B41U+NM=JYX1>0=gqeEqDjM#Yhh2@GD?LS(eInH?;UPmiGFgvk)> zTt<_jL8bMKhV4)|;F6Dbhn;HNxnO%`yR(HtAjv#2ghGmV?yUT1FT|Pz*m`oqUdR;- z`E0Qm8_k4l5m$`V25e9?8Vrp{QLNBo6f-J;1~(53hG4FvaAscaab^*Z%N1F0W*!9J z3B;tukj+8nFM?nZUa~J&sc7adn%Rt0(2+b#F`GdkmRS$6$#QaHdo;3mE0#qfo-iA> z#q66DZCSibeAvVXtgU$4hAx=4l-a@0z1blaal|4Eb25E)h*%teoPTD1XkyheWZZ{_ z!uTRMx2XF$(*3gO(QKe!q zf8$2b?pd3O#K=&JK^wAX-u^zO7~pECb4+00IR-&3Vh->uk0Z;7t~L!g?7P#Do8=Pcdvw`k`it3D|)hTSEKJ5+0u` z6n>ktB#18#WcfK}34#7GIkfRC5!rW^@Od1;H%d#`EI+ZxdgR!TC)BX%PI9q*XNf?} z6A1}%pTmMQ9|Y*q%yA&6Ce|^A?<(b{<<*V z)(q?@hwLK}8gTCCz+6D@<1bVm*cNVbJEKvkq8(sv#9XW#m=_wGg@T%atD}`BWOL0p zFk4G4XVRUJ4Y+EeYjP^u*-%2Wh26l3VI1lO#`}OWOX#jy+pG{|iE6Y0DIxntD-I7# zSozUtg}o@bWV8}-A!daDdU3cAhAQ~PXa(y@fM9TFagoP)XS6~A4HUZsL~Ym=fdfpN zYy5gXkIfZfKnInw5IMbwj~B9ecC%aMe(Lg6wlM91smwa- z{ejjIPJoi5Wt4?7_^r#lw|?KiVDmk=wR5nNa0h<%LZbUoK*h|xg#=DDfpM!E5_1mJ z03m;WM6EI_h6Io@KY1i_0Z_qd&{;-%ndyFxAPzZSa^Ep5G-LKuM*6bF8C&M$`I@79 zr$F7b>zae4SVAQDV&B0xreKmJ-qOoyR|V4+8~HB%S6M5V9Fq@=Kt;f$$-aUKNl4r| z`BA~d>nWEMOtu)dQvSsnAoa=6%SA>~})wIv*U2XRnH z#}wskCD}#AwR|riCNZ&R8UbSK}Bz|MO}lsH6@L z45nrdH8Wc&K_K|aXi7J!nPNdsoG*6LG3jnhXp~(=W7;wjhyVua5RHwQZyY)%q{gv( z5McVs6%fqNNFTF0BXND63%qn+CYVBVZ#4-;96^?-0EeQP?-Wg268U#v+esg~0!L#n z9*k7ZIm`2W>Cs)9U^}(z(&GwQTp@zQeuKIu84hKsYhu^~sT|r4+(a8aW&<;8E9B}2 z)HS&lE=^E9v#)DHJe4~yKkAy;`H)MxCXWpubAJ8>&2s53O)w|hnYI9I*7*6K*EJoe zStDQ1ZC%qAQTqcUQL`e0m6Fhl}r_~rJA{t!>gI?)s%0Lv)}W&8@(MR`XVxb<@vg! zdo?+BhUHL#Gr24-mm{#q?E+p;4$l~zDd4f$M7mW_v}}cb4D#)l1!r1MN|!x7 z6C{BjD|-v%TBHd>b_S(@M~X|PT;M|XFcuMUYsRE-_#%-Q1u;3I=|7hY8z!(h>fuWg z?b1O^82Um@pi9ebWPJ^pww;GX^8F^Khk%ImjfrGT1S4b1R7m2NJJdaEP&(Q%j2iU} zr`fmQ@F8&@KR;SD5l$*fpBKA5P{*E^UteZLT-n_~9=xgA5YA3XhaS^Yjk9ZXI> zB4h=oRt~6xNaQZakJJILh+Lu$To&woW8D1)vLtgpB@d9G5V2XjKFH%dqz)FoBRgY> zxFE5kj3<~numx-p-vR`YtEu!LWsfOSlJ$^p3N+9pT@Z=%vIxHX@seNhOnx04@eghu z`&usr0uft4co?~m%a+H(S;x#0J6B9>YVQ!(%>sLlJOE!WhC{j%vo2uMXZo$D0?3## zBxCw;cIMc@46Z0AkTiM+6E+0c{~~9Jb79(3MK$of*>{;yi7{Y2QN}rFsd<^@TVk9= zl)?{4jOnf<@GRMPmQaZ?gck5Dt#A%v{FNK_(P_n(*hGFLV$4Crn8VL|Vodijfy>Fh z%Zy5lxsc?DWs&AS-xA|2kbFNNF{XP&VDC%&&Jrpy<_b_=ayrZA3fMvbmOD(@5{G1r zrh#$aRl9@BGltF07@C4b7G2LMV{v-RYGAb?RiD8Ea3;i?S(qcyLa@`U6nEzX>rl(8%c(PH+0AQ#j9N#I_x zD=nsMWDO~F#X{J$nzqPzu!B5g=q4l{His)RS$J(>K)#>!pGuoabj{n~Qgap}MUyZ^ zneXSwZ0Oj^nvx31v{x?LUoYeLo#~^eGSW5-%Ym2PF56l2Og=b*%h}nVOId$BmI!v8 zu&@z=Hwyw5NOzS8nTN1P$8H!bk(g!Nuj>n4HWtA?*|J+Z^5bj04Do(B$&b%7c@7C* zFigwr`+@}!>m$sMzF_SC%q3s22!hE__Ir>IAiV@1WsA2&if}{%qbrz09Os=Y7%gfF zPy_;O?aSE390BCS$6wDAiv>c=o6OD!Hf;5X_C3R!!iY>?R6u|5o*bwX{wrzOxc{UwSgl9FYz%|( zKxaINFdkUh2GxN259l*+s!+U5gi>z^GbAYGSV;hnYjz`lO#75m1!jP%kpeUIwv|k6 zG+ZosKSz=^eXpc1@d1GlW)|~(U4Ey{7xtt z)cwyHnKP$Bj7$zly^WcYGxP=oqfq510CU)t)UyF1#WISIwlp)IabN8GrJ0fVjoj4C zY(YeTS$uz6-e!%`<(oX$gk}apclI?iAzSP&%8!~EZ23&Eex*W7;Oq4U$Tud9MzbnX zp;V$Ryh&@9@dhm52q+!|cYPY8ZQnp)$CBtsMgeRDPTv0@$|H_0xR zRFrpwV(c$QyRrcTRK`k^`2Qz%bqPGQ+B;) zwveMgqK;-lQ4l&BXg4`g(nMl+alTa2u+bxz{AZBKL5wXm`PG3&_soNP&#v+$7jgx% zxbP12`L>iahX?H|F=!4YjjE)rT%Mi+sVHC<6`Xb`2khED8kz}XfUzzB@XWmxWlKXd zuPBFx=FrgO2FR}<(aD$$tpaS1LDuk|Mqooe(9l3VOP??^WAUVn=@Zi@Br*t>cn+4a z4bO-}K|_7`efiMRHl$?CTkT0u`Y%ZAi1ePX-4R^?bx=)mxd$%hv-kob1oaWd6mmy& zo`B027+q*=jsQN1@F;vOT7ouHXi2ZEt6l8u>0Q7Lzeo3$9;`oFFLhiP^4Gu-&g+Ro zJOmbSn%o1wLUacgc5Shk9}e)j`cwJZ3@)c)XBkE2W`>loaF%JuvsP$n0)}~yzJn#zSqdjizq@j#IXdSqv;WEhUj*!Gg_O57-Xa%lU{zz*^4sxRBZFECF{v z#JHp{Tb!|FjwiYOGq@cshR0TdOnpDdPfDBngzg8*NY0#$0y?e)LICVa0U>=g4`d30 z1b%BGl654gLquX=g*Q|o{Y2w6d8|nh33y-jMIN?A=9jF?3}S6C1cqTM%u>2jBT4yhIn!QV zkztdEFfvDP%h@hY_K}n5cnJtwurC#)Sds*=gDKl0by|M5Bw6@kj{b5jS6oM|f4_ zl6XY3BIQT<$mBgGtVni7$8xAvBx?eaPyl8mqnpuTMRHh?axM9E`nqi-EYX4s=L7SR zbzby?2uLOuXvVa$=}UK6j85*$NETRwT!f2?SzIA(b4bG)~=BNKD7D;Gwo)Qw0o#Db% z3lb!>g#F0r^uZw@SwRas+-%rWX6H-{+fo3Cls_#<);ZCSAs`{gTJl(g*-qb*n2|U) z&jln4pn=`N+7u|#wM3|ATmA>k%6O zmNl*y+W{NjYmA!haAOhO2MhR^T$>(T*!jb9r)3TsASh_R9|S=W!d^Ro=_T-~wt|^* z7V9YDvG9Zhz-vxJ9j#lvv*&?j^z~)}JQ=y}vWhJtUqO5@B?Rfa40+GmHYu`n--23~ z1Ib$?bY~0lrEL-$Af;MZQIslxjS})X){r?d zSdPs5pyL1|Q|Jspgb4Tqh0lX*Kx}J6pu4OKu1MlAk}lhl>$kqVh>+GMj9VinGav^@ z+QwuCBm{x{9}M;&4y5PH!NQ|=IU!u-IKmZe6+q=(cE%=B7oI@O6sWYS>sO=C zf~@86A9>*sLFB~33yhb_m5hw7%QL5~V5FzaNFO>QBRMJ0mmb}v4Ju=<@jM~y#p>XB z5dA4pMyb7-c1q^pSbEts*W!$`#L~-1oRhv}1|uV758`=T7C2M$eBIGKsRBDg&8RF- z1Psr?@*L~#zuD9y12CP*Lx^>kmIATCka=$25=L4IWS>hUrO(`1e4nv_S$d!0Jo`2< zE}!qt&X+q*voR(kmuz5QJ?4nXTrmQHkSB&+2`xiBg|M-X(FjHy=b;hI!uxDzfF1`h zd0eywFS3GtV-|MnX$kCVVJBbVu&{#()Z_>xu`Mt$vrk=k2-}h`cTMDAJV$O(7ok|l zLKs_YWX^vkcCm%~&fe%Z$gU(1W?^IJ3phe#gLl{mztBF2p)EPGvD+eP-+_(YHz+)? z9iwiTZ|izFkiY2Ypf9xdf$NNF?}Nk)b~QS1sC2|(X$P;k-Q(IER)_3a9nAa9BU`)0 zI?J6=`4akOC9LcF_kwUUHYG7=JH1`hKXWvt`pU}K`sTWr49h^wqU{bCc;hVaFK4=X3WI1y}lA*D1bcIWGopz%sV$f+G~P>7lu?zc2yO5Y)D>h{B{wW zZAl#XY!)B3dUg2WzBG|D-FW&XaA8F5K2Ptyb(}uMI5T}XX_y(kEzemo9aIAjdb_SR zHj5=d(VGORvBHZ5ES^|IhL!Z%5X=7zW!&x=CgXn2PKJWB7coJ11Xm(Fx*Q?lEIF zO90`f#wC*9xG~;6l**3njAe#cQ#OwboD*`a@MEr+58G=y__4~tkGV)^%eB10q5g~p zjP#Yu(^IDY2T^ipYigRJ$9Ql}tWH5Q0OXkP0QJe6BjErehGiPkeoH;e`xiQ@z1nKSU%Z(w= zvIq$@wzx4*%x59x+`)}?2BrVE&W%m(4w4(gz-ONu3z3C7U+!&(6J+L^yO_lmaZF1j z-zmmu-(8A@8geDtl|XRWY_X7Q6xXeAV~z;6gmx&#U!IiQ;W5r$ycvqKn7b=uRtoG9 zlILs~*^Y@-78Whh${C{E18s=}0xp}IRr+fqI>1H(Ql&yI2O>btRBRoHrPc%~luGPN zRjRX@YM6v*5q?ry3PP<;ZwQd+qlp}Z_!gOhS&6_|XlpU##5L_PW;>TO8<`=gG|kQ+ z7z#p8B;=!9A9RGA2$EqKNgA8O6#;u8zSl;F>l_(hEi%5s9;i%4j-oZKL;p~+b>nRR zesfmV%~-UnUdHaJK-ki^!RC;AA%r4h`lO7}i*rEdaF=Hf$B@;^Gr3Oi9w(rN+f|yN zt~@b|53A4i$`kRyQ|DNCKsKd*@<@y%(@H}PdV;N#)IFBy54>)8@HX7Th1j=R-i9G5NiV89S@Ug)CP&v(DW<#ShmGW ztlx#}_DUo+gVH05! zw%U@nWHw7Wiwv82*e_X9T7-7Y!?t8eYmt7Nhi%D{+9H%^9=0V*dJDT?=4D&5q`0sx zT3)s#OPY&9V)Af|vZT6@l{_!ok|o_m@!xsZFJ!oy4Y~_4+JlL`pVar| zYP!}7S4$}9UXtvn(K4N+FR1@2C80s9BnmZ?d>#Uh?VyGmYtv?@Hmy#s2W=kP3205i z78BBPqSq3}6!_Y>W;$&ccA|r|q)i-HnH6A2Fd-ArjKMd^V)#@I#oz!eDzwL=j5V1p z_+VmSfawu92w(xRbIELvgp0`XPhOQqhgc~qjSeKOm(!OTof5G+g*UUmpC4@V>M!zh_ha*d+<5_Fp}TL8fbSl}=kj>|ej;CgZs0&@rIyX} zlIrA`um#IV0t**EA&M%6RHxBvA`Qr^>PZ+mJ++#?a-CMAz;;eH%afqeaS{khg*agz zO9UAMjI$kDiQ8>#EqsEMB%C=mIb&{O!OV7g`4&D}z{v^y>RVxEgyq@H-oD!WT%ve! z8r!#%{TZsI*PAOgXxtC2v337bae}x`@_*vQC{WmJon6ItEN;=-g|AQ@2S7v70AwlO z6lr;*W46XAg-12fnOux09hhxa!5HO8$fLC+mLc7r1@049Ol7#nDHu#997lq;CK0cR6JR0-r< zU@~-GNJhoZF2s4TW6$F8=tu_C0NfV~pV2aq#6g0LG!XyFKxo4p7_lpy{73J@fEi#= z7?g5+?t^o;DdUuEkjuy&NKx(}2yKWAmqdgCM;EgpXbdWa7!F*yT+&MIr-N^WUdIR} z@)U|7X%SR}&{#DgE+SS+hhRw!1O$ta3#fNZ04#|Yyt^H=28k2It7GIktr1jx7>FPQd$&E+wmVbCZ*xFGO&Q$E~Rr;xy= zv|5Eh8XY82eF``yHJgZ7&=zz!&5Aek*5HLiA5zAWi^h+U8)K=qHI`~yW2w%1EEzB@ zU|>0DQ4pgggwaCV$eTus0~kwm;rke^DW8lMicQJ)qlE^@M8de{(E`vP&U{K4E(i)n zAM||;*QbjMWc&O-Txf($!^NWv7Yq$14(8SMT@9C&)rH`Q`JEP`5i$)I-#lC#7GH>` z+jlu!`*(gy3%`ZYLIY$PEdgb;_-q~~ec$D1SrasnV3Y3`7aAeca0w~HB^I(V>vas5 z8Q}eeGQJ(4?4zKRy zgnmYJ0|nOcDSYE=&<&J|zjJ&;L=-|TL`1cdN0M+o77OJ~1s5;fE(Ev$?4KnIW0yJc z@DB?FZ7!D}+$Rd|7zHU~{SM$G z_ynuNfO(+aiJ;vvWwgW?0^{aN=u7En3MHPYRQ(0%*+f%~*a3tpq}BjDmk#dd(hW;GQpP|2U7)UyFf25h<=Q0l~#C3al3iBYyPWd$NWR^ow5 z8IcB<3K-`trUiE!y9V7-AzCG*YQ-5i$uxYgnWkWOGnf3DmYv5(%2H zP!WLqk0IYnDTRm)HLGB*Qb*cGIJlFHgbttN0;L0Zo~^h69_oy*Wqrs(P3L{YG{UnM zFkso|fs|>4aBe7>Dee^O(+E^G9#P^O;)muzMm`wecO)PYNKh7Z$X}_D#xM*T24vwd zFh~%u0d=k6DX(!dIQfQ;*aqySjx%zEn#xV?IL3+<#BRlHfqFVBI_E%qZPXB zSBNJ}))fY6Vs(b-Y&Q|l@6Xhzh|How1Ysm4B{=+Cgbu)7iIozse(h2N z9ILgJhCee$D-B37P+}1*I1We}v_$TTaFNH}Tl*G?D$3vF%A5ZN$uLY=8{>5n*bzFtuE4}_#({{C!te<4eN1XZ!S zm=hpy59ABk{{H;HKwnW%jsmrA4c_i{Yw!*;v37)OQ8I*m!*LNr5E$a^R0@_yZhP$a z{ubQ!1aYT;((}e?|1Ip@Xoyp6DPdxHxZ|_1{7VVP`$jK~y+z-ni<&g$}~5dZL4<09t5z9ysd1g$}}w zdZL4Fji**!^FQm?{!|~_^5|!D%~L$5#`L$8OJALZAa|1B zTIM#$l^g`w@_~Og$Uk6h!MWn%PEP1oiG)K~$)`Iiv`w>8C90q##ifFq2egBKQ{lJI z8FC*v?XFQw4F43@Ht(!=a{8$(`V}~RbwS+3C^s>%zaO6oji%H^)Tx3u*%h4 zUu~agaC^9S^0ikrj$GU$oqG9T*?pH%oAht6BEH(+4>}dksNh|)>Es^4v&K#+GI`HS zpTVi+gD-@aZHtB!)|vfetG65&%yp%;lhMz zduo(+@g3;N`}n^>onNPy^>dq3`rNttPKrJ2v~F$2t#ljOuS?Ms(Uj*+`qW%dpSk?D z-!nyvqfVu}k8xRfbNtgmhwlw;*zjfbUvEFYQoX~_fx8NJ>elkjv4{Qp4s+hJbE?K^ z%!LU4#o^9d>P`MI@i(WRehux*82V!L`C|n-C)9p8cZRUbxY_db@&`-aaCsKgV13y+ z>UQs(7Vk`O>yTK^HC6HJx!Hv_G<9y^lKSk|yCr%(ZFjm3JMwA6wZ&_^^m{h^-h^w# zHk2FI_XK0;?S{|}*Zt*0)%z{q3KbMTd#itCwrlG3xvTa%Rg8*w@~Cdn2an=U6zD8@ z@~B3Kp_l3wXt=ia`@?S;3vcX?-{n;L>4RqL%a%Vj|7;bu)2P%&!O~`-Q>7&rZocVX z`GsM5lGCHAaZl6-rri*_?qmOPyz00uWm_E)7n3QrRgf&6)~jrRIk%SFdpcTD^l?zq zjp5F{*3{&k9JZ%llj>=GO8IQ$F1d-?+#@BTW#WS5B~@C3KmT1v*!4rhW$n**Nt-8I!n_=e&*b+wq;z) zAKN>ZQ&jspb6uIW%{H_a){;#~u6w`Q$(wH<_uTXTeb>XcFQrZMnjt(=E%oy5A&Xvx z^_|_XTKywG`@Qj=zVRA+-c)_bTT^dt3V#1#@9?R8YR@?PvgTyw!<8>mc@yJK+*P`^ zI`Z?r{%hX4i$}TqEfZHi(zxjQ`+sBvir4jTSL&L)Q}LiuO=0Hz7W2V>TJTEm>5H1N zbWUUX&iZXtWXW0w=N+BomU6A?wxtcu4Sd?9ew1^&p$UtYyEfD}Z6d09xMIiN`Z}@2 zZ|aAwyV~V}3o~e9g3H?WPGjzF-23zWar+-_uakP&(LVemB2D?;C+<4CmcLuF z*U!VA?5Pz#m7Dlmy+3cveaTNtfiWGzCOu#E(pP!oL&@-U z(|WZ#?&K3as8Zc7P9sO_=JnXoy=Uijg^zaa|8t@ERWlmw8#qPRCV9uPs-1&U3Qz2# z&(JYi>6NRXaDj( z48BdhNpj*Y-&I!nMn~6uRc01@+~M)|UB3=}ruj{-Uvg+;ow-MD6&u3|>&rWJw+v{|-iAx?_NSyuKL)GpOm)JnY z;JDvYcP91sc--a1%YWG8D;6xUsZyU4cdstn-T&~f3lmvy7;`o+tXwO6`=eF-7MTg- z_EnoWKX}k0?zwBPmcxb3OqapynDY}Bt$h6M+&kIKSm&6RMT4qNQTi|caK3NmgpOT? zKk4vvesQnK!Gc=6?JuW1Rg^2a|6uHjxGO6D&exaHw)?N?ka#`NY0}l@$I`DofArhY zk(zU*Z?mGf_6#Ms4 zsZ^xXb!TneKe^z5lIQz3>#rCz^kAif#cu?i zc%1UrgP`j7XEpgFBd!1ALZO3e?k{?@p66jpOWk?f0ZBSvh%TPCF(lD1f zF1`&DZwz+6RjaTxz@=QR%FLnLoKu}$4+J?EO=9@?MUQpXF%m~4G0ONwCpznDCBoI3 ze#_x1GjVhh!_#j$Ty;wrILE>US7*(Ur<0w>rF1sy^1j}dbDsZoKns;<979e1;U#aew#47snawU-|FXU zxik$72%XU0xp3&uu4m7A6)qS(*f}kxLcsz&zrw1srH43GVGfP!4aLW~`gZN_&u}Uf zaPr!D7w3J>t`|F1Em)v3vtXlTBbquLO4@f`U(021A!f?UPR@mIBsvfHr_2ziYEL&6 z?^$W`@!GrfKI0d-78r5(q05M7g)7%^?G+y|_$FMiaq_-MeDeVfua*p%0=YHpc@OSF4(UGuGgcBiHWn6uzsrqnW zu_xCxBSyxLp1Qp2@5(dNckPVZneKL}^@fh+7+1?TpOjqpG&AsWQTI11_Z56!wM6;A z>*WWAZ5mi%Q{95Uq^dXU>v1V|Vr)kDD?9hdj{dvgO4-;+%zE>l9pz4L*s@D+Q5BzG zMxK3KXn&1%i(Hqy{&(j6SwD|l7v)@e)!0InS5-co6ugfeQC2&4M&W91zZS5=Y4b6z z^-7O@-fmm5fopD*N?urSW4Y5MCbnr;fBcv;n^@zzxZauM;>O(fKr!Hd=?`>ntl9hB zKl*tT5_;_Wqbg%mS_wnbBFhi1+x5JA<9|mk|M%&7Nz&ZmyW1u`Ka)|p;=iHn)er8Z zk010Zt#4qhO)vQM%lDEOcqZ-@=hvoE;mfDiXP#-Zz2Sg=^)u!t9_ci}_4Sn}n`7d-WNlpX9N(RFSpC`Lq1SdwlHu?B^Pd3ROrc>sBK1 zRL#qs_Vg_kF|EVNP6H48`rt#Q^f8swoKD@0nl`Pnr0e5*9Pi2JU+(KYVT+;qso$^e z==gh!MRoWw-G42zx{rKv$n>p`&UBz7fuef8JO( zp=*l*&tIk1Z`u7sbp49VCT#)(@02`O&b7j(g|D{0TUnvxur8_<)7&=PzESeh;EP4x zj31n~vcuT$XpUzaRfd7|T~rY&VPuFvcL8)yC2r)dJs&5=>% z4mZ-*SXLvMnR<4^?KQJMgw$!=|8kp7se4N%jNe{;-sUnRin6A8?wR{%x4+6Kujo9h z?&)#|wHFTkn>l95)}3#gO)Bd5d|4g#t#X+oUvzvvYwCp}&D>7j8azrgtHDn@-W~4Y z^SrMlWZg>TfXV^8_Z)N;o$PmcNx!HmFI&~&MM2k91ylblDB7PZt-3b3r}yO#o4Et87b`bD zV1s7Qj=CidPwx5t-qYt3j?bJmyi)9nj~5?3Yp~?wymfD{HYxddO|O`S{Bf@03UAJ{ zdGBV5&IS*w^~bTKg~wwb?JG03Yt51SqjtWpvv1;+1@B+2>T`Sar3AoxF6mOxv^sKs_I9?r=G5SS|X|#r~8n9 z?_b)*4v&2GdhFp#59@xM{w{9cfydWo^zK^a#mrf!{JIz5WbAqTWTw}X%?U-z^LqQw zdBEded8Ga2emHMfh4DLEZw@cLJ-peJxZ!8F@Y2f6>fEwqLEo*X77uUp(Uq0DKy<~s zP|*f+9&jqSotiZ^XzcO*?4R8#MVvi1XPM;O^R1Vav!>l=)gKqmd%LB=LB*5y%X)87 zvU;3(5T(2yeR|LP%QenMEWN+&=e656d6l}?FluSZJzjfcg7MN3F>US~oA;-3(6K}9 zt_-`pYkTD-`Wb?>b==a2m$s7B?%KS{YStdv%4(ycrUo3U8+7~Lepk=l#Y@d<*IJq= zjI7snPm}3urw0G^{@|EPx9U&jR(pFget4N?16B-p{m0)|whlPdV8EOs%Ub>YQ~x@b zHr080P7u_f^MIE%e*TU5_;`c)Cn~Qk@bKN~yQ4nx=baL68IXBxzh>V_zb@}jTpz*v z!>QYV2MLeWGcPUqDKK+yrPDpHm991UaLBdf_-@KkyRNVoSNGce!nb>|1@+GExH)*j z?>C}^4`0+B5%<5e$rsCXT=wMO#F?3Y|DyT$=H_p69=_d}-2Z;B@j(Y0J*#*2=MeFM2qBnqkoE)YD6tcjvBLcF?u_71wDCp5FEJ2zmc8;9Zp=mwNx{clqh5 zBz|?>wo|Fru&1_Gn%<=9JlHqvwQQq0fz?{Ti<@{=zur37CZ{s zIsW5`lUqIdmQm~+JEhl{GLeV1_k6-Tw?BL~?aa6#5hdSm41TB7xdpdeb-v2}DiuaZ zYbK3sd1}hkKaUNqU8L0Y9Ti;4%wKi!^1{}2{ubWcswsA3dV|hSpVWG__vNC8K4H@@ zB`=D)ab)MoKhyrHI^q&jcC_Q~M~^60ZJ(2vxmdR{zUjesg^P1`&pp)S!i06IgTLIU z%q}hpYE^K&>y*LL18_RccV&xWBa$C4KUMG1)5_O&j#yRv;yO`~{AA2;zEK~yz3aWb z{oU&yDvmiO9^A9T+!J9Nn|Q^YKikivOu+n+??+3++pM~j${gsT+IwqQ6{u3y=*`9F zp~b2x3!PZof21yO#@O3W=J?E#6{_a;_Tf+aYgXs?2(0;+&)K3Y3wCVPy6_so+NzgI zmx`U$oqM_H|Mo4J^uGMDkc(T6j`|h&bn2i{lj8mj%shLv@pZ~D)%2el+D!1wj$o`~t?{fdK z0WUc(f?iybti9T3NQs1QbA~)D)`=I}wM>VeFOu4Jtcv-E?zdd3nYD99(f-R14_I`# zOzMHm;lW35pWUtUbZ_;?hEcz`ram~g#m7KH1>+~!f`?AEozYA5{{pRlWws%zL+U#A}GejpG!;OExsO;XQ zqqV$(zAx3+o6RhEZRX)QE0}TJ*M@q{di7o?-17Q{SGR#>8mKt6I-jLn?h3C{5 z{;+L>>y3mxIJM>V7COJXb3di;jPa`~{Z^p=$iShE*o!Lc>#=S0G3o+J?ow*dt(be4 zf;abVwmBmF?EiF<0UhM$u2nxbdQ#A=pgF&EU7osUdBoM_p$(bk+XwoVFXPi^{rLk! zT9qrarqi&r;Y%v39+s)SwbI6&<1>h{U5IZy&ChCOx}vOXi;VlB+j_9~|FW+2gqHsCh+)H=18% zN?Q2Bs{R?F4fpSQ(SLo{(UX>M9(VOxr~ehX`TXd%}$bB;TCc zWI!Li>!RP97W<#imBe!w7TkaGL_hn)ga18FF1WJ6$pRJcY+U&Bh`Cj#uiyV+(GAU_ zOYGH*Q>z7kK0KtqeyH4iVb4?Q59crMn%lZUc+3dTb}=JsB=beuf{orN)N^kAG-F1w zJ&}v9F83%Al(M<|!zzgtuirb$OQ`fXJiPpngO?`EE??pJ-XR0#PahFD#ZCLVW9U=% z;tiG8?45S|en9b2Z7N-yacxTazsYBcpKJEI@tqE;YpRg#4gcBh?AEbq(cJCFy)zl}lVVzY4AQ^iuK)*XZ=lG0i*d ztlef|y~00nYgb*fSTSRTDmi9hLPWqkr=)p?b5Ddvf@5w~=_6VYlYFORK;nxr?Z#eD zJy#)`c}ySr$NAJ`RJ99?xz;Zw=8RxVlkxJLiSwK}f}@C%qQtV^{}#-HUGv)4VY z*X^J87n?Wt%_z0MSEo_0w%i+`D>7^BnTyOr8$-)X9vRuWLhP%l_ur;UU;lHebn>xsv&g_tS@u5bjFim7Xl}g+q@$F@q`7JM$}F_ zdnf$G7_V+~hgRh-pSE-LA1yk(edXi+;8^>A&#rxcV9BI4k}XYtsyCzce%U`6Tcev! zIa+u0vJ=bJceqgh_Kvo0r#xp>?Cra^YPZsc6?ftuM5RWJnOLrSvm#x(9qcx6X{K&( zTPb7u&`h5t$9_7#Y~b^LyPLI5T`)AHN!J29hfV!Et=Xm}UFJ{U&)(3et#`YA!zy3W z4P6*i_f1u9VB3NV3I)#mCGBkb)3QaT#fNGpE-e@R;hI~8+m$bM-C3#s(0=z%zHxP* zRQpu5F-_+314BO4UN%(aQ?h^Z!69ub6#L`sqb0@JgSL*W>euN|tlR9=|Y2MbM!x2N}gZ!UxWoB$YM0({5UH zqirvw!>`>H?b)?4RD5;OoHXCqHB(zeZvLr^ME!R2>43d)AubojtnbvWOc?*LdV8v- z$wJYnpaB1p&x6Aou(pgnm|5vWhU$a%BMpLCS&$^{=^x}X?;32XFcnzshC;04;N5^jK9+q=C*)uJo@Q1R(o@d6) zA328CK+tLN`r6B@s8?J%kx}Bq;8KqYX_cX6LeqhR#KYvqchs_V(7i%At zKU>y=)W_f${P;0?S9lnFbW!xtg`9>}9~(W&bHHY}V#DZCwr%O%Rp3eVvu}QoPazlS6ddV4K)w$Y$(mjvg*Y@e_ zI;UF+b@#&4+6AmVmnsNkoeZyg=Va|V-F!;a3fOY!r>Q@Wnk*esztDucqh;RR4^O(d zx^=-B$4@=+X|QLWr1Ht5n!&Sug1AeH7jmtZTxnbrt@opn(|#TAG_2|Rca?732|wFS z{xNQduhXD1gE}h4H=0qnMN*N5y@tGNU!dTm4I#|eB4NORTirQPk$flTNplmHcn@}~ z*Z8i}^E#WZ@9%T^QH|e<{JAHiVW=nszUbq}Dr+{6f&;QUk0jI!|L5Pv=irKs?mQ}- z_;?T)qR`KJcjDb|taK;j5huHIG_z-2C#Qm3vO9l2rYd6M$I-A+&+IKvEzW3>X%#-bURUS0q zdZ)5WV~a*6U43zhHD^F$vG}@pWbtu5wpA@Y2 z?LBHZd#;=}cJT7XcRCiC-!XW{^1wxRIA=%IE>yc*!Mz9E{`_@P8^)9aX-mgcSy5T) z+OTRjo=eh;9m+1-2KfCvr}vuqoByrfBDV0zyCV!clhTeSyTyEPAM(*FBOqC|vwx{Y z%&0S04_^pqe`%QOxyfS2)@35s>HjRgFtcq?(AClZ{KZ{3M)L2~3m=zO>^@~@!mo?k zX`a5WcI@{H!6$|;Z`Y#nt+IO)CLJ!ba?p+Rkaw#6RbQ^Ydq?!>?vnEXlHZ%GdjIN0 z*qkF5Lo}6M$x9V_`e&EcL)wOw`=w0Oo#==L=e_428mIqXSf9WKgATL)IoGgj!vPO2 zJ?_~yPF(HqnmKb{-Kuh};l5(?E-cT8|F=htzqOnp%{rdC;@f`DoMo?Ot@0hXMm>M{ z!gm)R3>3~D@O08R!8^80)=3j{;@+LE!JDh}ICh40vue$Os?Dzr9aL>P%yJ&OZqoWn z9h-X2DI(b@{p}Z~^Z1N2D;{o;zv{TA-zA@^t&c2N-*oo58kf1{;&kU!{?|R7W8BYe zY&>`WbN1Mjt!pa&-ttlJD|^BxZ>Zg@Y~`QCADfOm8QpU8U(;ga4uq+$oSRqjv}AYB z?L}Vp_&e@NF*IdM%$~o{JY}DiJLMyv^6OjMQcl#Qot)6m`jo|+BxPgD7W#*r zvK?9~tF|vcQ1jt`7jKJ3HR&^XM{LaIwbh5RPHu=+{Iq~^NkL7kcQyMTs?q<@nb{wdb#C)koL$?m zqUKS5{r$faG>@W|f0*0sOfzm)tz$J}mUq0^AjN}wwez{fLnpVIcCXy6 z;SsTS3#>@E{Gr>&72e1F3;$ZG`s5`K;xlG1=A9WoWK-;;PJ4G8t5~DPtF!VK(pLL6 zEv;9tUd5vyy0#sYQfAhjUZ?gOhIjL9J7e4!*G<9acJ7=|GV{&geU&p-ZU1<}{oRa7 z>zMy+s$^Rx z?_Frz;mnZ>2Os{Qv2P5|Bx<&eZQFJxwrx*r+qP}no){C`PA0Z9v7Ow^hx?u9o-^mU zKdL)Pzwb`>eyY~0wY#d@%%ouJAt=1rIlU95cL+K@x{NZw)9}hMgIR^;xwQN-H6m5P zQ+c7!$)QRNVwt@~Y&jEw9yQsh0eF_%eU)iK0SLpiP*?mHOdGzF@~8C!(=z@tcuRM~ zp-mtbu|e&T3d7;t=YT_^nWNv{3)2G&2Aog(Fw(b@3uG|$F_Xox2Ps?db=CG*JZoQzOHMb{cFgm)pA5_Li-TL{K6_}mjGB%;1CB)Fgrc{_4 z{*D|sBwUJHLNkHr(n!6alPX}ZZj*y=k5Bt}I0Er7c`QekW0cgtOSKYgKQ+=<1x-#$ zQSOP}EGv9`h~~fdr)f>V6w~wST{YkPN}wz_Rd`j>h{L4!t8QaqY4TM(U~KGK zk)>%96#H%W`1l$367B(~okz)n6roAEErCM1xENyrGo{|XF?b;9$0;Sp;tx?Q7KSLa znV8dw&A&DLxg@1AsAJD1!D4*2&F zz%$d^ny6Ic&ttG6v_kl0r&Oy5PuF}~#D!g7B`}|#S)81#|G}2gRj-uNEsxm0W6Lm9 z-$L~l75=gR8C(AGvdRAeTL``e{jb>tkioy{kjo530AtwHWUI#W%sM76PgtUmNP_|* zz>JWnfPfbZKQGMfsVvH%I5=7uAu2B~`r7I&00OO7uvQfWbwN;AAaTv}a_@mUH-*F$ zz#e`(N@5NSIaCh z`|jT2C(v=lv*nHaIMb;zbJFrM^Gr^+HQijBl@&Zwv*0K{TziaFj0ih)8MJWbUgggSj_Gtmrz<>z%V4ZR+_mp1MS4Bj0&3@P=K zM6Urtk>4<+rj^g}e{>_f&0Td65S*G4?gBCtV;eHK8DcedFxzLckA08e~X5b}z70*5@c{Y%K z5|i!Xlh$NX@*4UKe;AwoY_4j0|678y;oF9~?3}2~jId^5075hd9uUwC%D(zz&iv6j z)o(yhRfU#{A3Lw=$=Q=7WuB?0i_baU4=axv$(p)^UuUBS82PpJV<&+5+9CNdd~L-6 z`o6XVfDpg{1Y*nJa-6TS@*++3N6-CurwyOGom*|tN;q?yv4U4L+V8n6CweZXv*9oK_b)-p8pyXAWmCZ0g|>5 zDr8sjXv23YAg`!D#A7+ov*Qxa*DjUkSnmPH->b(?3I>2bWoE_C@hOShNwM6xSl~q8 z>}U7orBNVmM~!y(av;9yA|&P)IS&ovR|MSCfQXDjd|hjgr;%~A<5{DX8_(OGLtow* zf^8SSF}6x=j3l+{QxED5BrUDOD=%slX(hjJo=&X6pclC z_OummI8}p+E>SWfgnYT_wb;-Q4qD9o%;DiXZ-6tg$er|zy(;;V? zn=FoZIT-}tJ84DU498Q{97v%TsA`o$QtC$ z88rLp=U(0lcTe$Sw{3z{RHP976@NTyP82nnv*shv@t6cPSvb%tFVXil+^oAq`#|=7 z65`^=6aCXQYhmkbI9wsJ2Smf;XH*2Rg%H0QWtvWA1{RxXKL_?Hc`enK^Ke(EW%F@i zue`xvPmJ#Lh zslNtfW$YUP&XQrtFT7mif@dNmG9@Z@K9cnRK^patq23X;X<%xePY`K;y&>{CD2kLq zMWZiipNxM)ti%*(@fOkC~Iv zg1LVmZEqD{ny0hY(sebuH0$VL(APvjGqLrpJPpekIR4oa&s!v`s9KV(KA!jD5H|7? zmjH$mSRbJ0@)$*+%Sl-Z$l6{1`tmM}kFfxQ?O>~3CJD~MAu1X+W8L7+--RN}NE&9i zd)R$P<)~>soJ$`6*7nm%`_=b62w2MW#{IT93I&7TMDGLI&o^GBY3jpuKZlLNW=RIa zef)LOWgFkRDalT|)I?pX&xgbCr1v@sMhPC`2!;_1 zz!OBn?D2Mc{mxSXRe*!ltWWd?H+h?;`O4Rcd1vHoGbL7hhCX%jkOWb&SP(j^D&RSR z7LFNV1oKI-=XbfBj^VFtSb7zE3v^u<-xN*2#W4cJ`8q{xGj|-Gc~eWE?#wcD`z*FO z3&dXT@T)GJI(rBhCo4*cn8!VCB6e=Q7W>3S)Hp>0Vg`Z}7y|TcFPtl~aE<1S8_NNR zD1vBxG5iq|mfmjR%lg4N>@(Lt9*rDY4P6kRYNeh33hGS=dV=%a2Wu z60(YRA;jVNOotZCWL!o9*36nLi$x-xM&e$wK8WKST~3z2ON zcEg8`-ROl^sI1Tc6%20wh-606$k-tsM>CVINplAKEsHRw|-RBu=G zMN%T-n2Kc@DeCeL4bb%*KYmou04cSs9KSVW{#(c2KfOmQAtuq+sRYkjuhcH3!lQbT z31GIx2FHv8Av|g?+DJJySKe9iGBlm&bCPLMlSlZIPkk`F?BtN2Dfnw!sxD!eltLK6d;^PTi?rNc7BVG42!)S7RE6W+q8W(xDXEwcQ*9&$#(yAPEE4DPYD0e7cyWHR$831 zeX5+ZV?am4h0*&CYi7FjT{t{lIqm`f2_Y3CV3oEM0$m@T2a14E#?8BY=RKN4R$4E^ zaDy$Kr>llDVB4>cH7X1mKUkJ#zh5#`65Oa{WgqH(*0kP`+e}lVdK-AU9ia2R;kU zUPA~IE+WpDzNPo2Cli%Mo8Db?-|3%@jE(9gj$`yAk85P{-{`W+{`7-?L<0B_pf_xL zXCx5%I+XlEpk1;Uq|;xGSG#{eS|a(sv)iNguJdb zeJ(i-cRUwb$J?NN*bBvQt~-XDhZy7t4C5HNLD}15D`AbTigKf2>+5_{4sJAV7K3i_ z!R;A=hI-41D(8R&has@~5yQD`S;N zK%;ULoP!!e6dy;%7!Fkz3n5ZQ_F*b-^N+V#ZW+fDX;`$ySk-S_!UTo{Fdu?o0aH6l zH#4J81gD*qM*7rwAD@$uJE^G>;K+=h*^v5a-=!1xN0@yIg4lYC zeF6wKAf}Lq(O&wr%eid(b<*v6mzi+zAymdRs@6-ITuf^_U4m@1f&ILN{2%s>k`4OR z*EIXFue|O0qpJPNALByiAS{gdVz~8VTAkHU-tEwF>`(3_q|dN)6(e*TUAmBRWH_5X zyx7Ea`ol|-33&(B4Z0!!*HZ<1-cm_f-*J#EjZ=>PfiolOIH8oARm}I58l%F%kJ8AIuCjdit%WL^XbZ4R@ED}%5@af zVr?(otPe}2SWxg}{I5?Ax5I`n=yZ_0!7UTw*_|6MI0)z}Nv$gG>+37Akf2KV2S@FP z?Q_X{cD2y?3|wlNfB;+p8NulIX-lY!rPw&cbX}f#C!!9VgLisH>%IgYTNICgMcUDw zz3A8DO}`_4IozxLHYh>}4H30vmSJgjY7B#ZaE*aunAibr_3hP4Y`s5o z3B8^wZ~f8vF*=5-oE1rO@2f9AnS$Vboq4Z-6tue1|42{JTI#QLCVn7+z)jzBdy~ySQ6X`Rx7mm%rSb5s4skF!++Tf9!vj z>g;rOHm3hwssk8&jcM=?sZKH+uMPkJgZ^)+?rxWKfII9v?RjQGpCBX7%0|e~u)iIK z14z_V0?w#|vLA>Jhz?440)xPE0@VasI#xs2hnc8KCN|M(r0E9|nr6|%z zrt)8QI4dNl(G;GH5P)Y`#$;y*auWeEqY}Fzg1#a-w$?fA%;QI}dAB)tfo4o>}%>@oSJ5_RQi}ZIFI$S=3NGiqKDkU$fKDk(=8MTh*u+o-17wmz; zfCkcMR(+bO?%kRkZQN^xJ9vJI+Wi=kD!$NeWX@ubLfW($U0kZQWR_^cnYJ{pJU5fQ znN&mHXji*Ti>aCmMP#JILpBgsc{bwRinaw!Akg(!z;=dNDrs5Ex_Y+TrUo3BM)H zhBW4ZQ1a0_i$}9(QOU5WU00ZhNXp3 zZA`PRJyzicjvV9sV!KO^ZI6I^UK}T*KEA8xK$RX{PNKn1w7>BR3-XhDT*sH6hYX9l zvTn0)I$hEPBcP#+`hDsd<;)Y}+LnN$#H}9Xn7W&AuPBkM$WK7yB$gvA2FDK)WIlf= z#r@j+=r0OJ_MG8*w8V;eeqjej#O_F#WP>u+9i-GCK-_lp88+1cw}TWTbFlJZ>iR%p zk0shEBNUS1s-(+l+J|61ABYHCSur?n8>eK2Ws@lEJt>#p4cjN!AoG*b;5=s&C2$pQ z->Ai1SNnGz9I!Czgk7ymrQHggRb52~s5_Zc>^h)+uvz1Z(V~Qy@3tfxd?lhMsFUk? zz$*Mb0#ofK#bv9SmR7#v^zv(K_Xh;1S017@aDdR(?2&r=0tLAC+oT+N*71S>jY5b> z*)>EZoA~qi!g2B@@&TPlw5>(;GnQ4GVgF4{3Z6=#6%NC>n}fik8MCQizC16(Z}xtU zm*QeeKmBr#;&!7X2J>OenSfO}gE`F_-IYpFcbA#gy>P8iY?Gvh;k(xhK{}N{cKv?m zI9EtBfohpw!LAz78_MZ(2bW${NDLNy?i%9ML&YkYSuwDSEEGn$} zhBsUJY}dIg#}OpYv>pfO`SU_4iUPiAIZ>8{*dPO$jPH6TMSj%DfTWW&wC9YrGe}}8 z4rlG)y?5g)7l0_|I~~}_!)}x$L`^OhaUT@8Z;J+;sfE&WY@qjd=>p3*w-LXVQ;n+n zjaTsFI%TRFX}``6ZV$8>T6ez}@IN2IWvzC03k06&E1}Z`1x=)z_QLb)4 zu+m79%nT%-hBTycZY#j;_`<-nDwi@MbD` zEiAa2WLUD$`Z3yyO&uIyT}=#aL^>u*QY4d3Hs%xhG1|Z&rnf2^54xNsh!%vSQ$nJk zO7D@iN~GBU(X}j#m_`A3zg z5&W)O9!02^+h8qAwH zya51FPLEE7Gf#yR)QXTru9vzfijtAF$e^6OE6zNTN;qQ^ti_y)pRzzy=RCnF%a68k zqjx|9b6v27zixCu$z#S%h*gP`R61)Wh)O0Y!)p65O=dqv8W0iKtyfBk-$Ck|xy;pr3;Fq|fd{B9zF4)>!GB@WN0^i~TqPTFw46{DHVe0^*sgxJfGW&qIO@ zboCn}OaVwegC-iG^U~pK!HwoUzl33dQA%~;?BS{MlRSnv0&tRkq0d2dj!Qr`*Nk+a z5k#aDG74z4rO@bQZUJJEWO5EuJZdK1j9Y0E#OOnf7flF8lvLxD)d;!$aIBIYLPAi_ zZGjeHS)+-dNYwshaZO+ux$H$h7Vw@UhY?o-wi&yTsOPm*c~~OkRrhVmN)bx2JBNjs zzfGV3-Ffdt_ZKz3prwg8A39{{ks#X{XPbAfzQY1n&eS-wW$(x~ijJ4tXm+9>z*gHV zU>8ZD2()#&u$BkxROi@K68eQwYORy#$uj^uQSa<8)uLd0M$JNXi*&;39F>ESV^KUt zH~#q{cxn*_EUy#q`l{o=tmu#X+kj6#85V$wjT`^`h3P#Q!SlX;>pLt5L;H8@k4!v( zoG$eg&l7$5=nA7K$WM8nu^S^EW85@gUf=7IM{>(}Q`LuQ>d9vHuav6if%n@l;(qn* zkmOK)xO&L}?J`uk-Y4@YK^gRGm)G z+VAns>_ysd7kFbxxhL>RC>+UkOL>m#!hK~jKWrVQ#j=202$;|);J-kP*27+NvXnpUIkP-lEHpPWvtgCBuXl}w9CdfH!r6xXN{+C%@Tp-!^4_jBIy`KWJURwRidQJg# z=#$Xt=L%{XTZzx!)5(sUn^y(8!TMW2L;;P)C8HVkp!)4Z02vzC( z&|uAlF&bWmbwW4MAW~~%E%_~LRy7ZlEw;#0oYp*at32Gt@^-bT1$H%XrS}LfvU`N+ zNP=LN7+`=wRljGEp_fEJE7>`)OQ@dN<2(B0*b5L8^z&8et{7?l?EPkZed29+cztn( z2LWx&9)W{5bOPS)$%(@$7fZ`|&|)<|4rquh})g^NN-kBYUBWB5NyXsPp(vL@TE_e=D5v z?HbRm5%0M$1rjrx_^AXt6`6--TH^T@fvFNzKJG4-ZMXkOLD|u~z(tWTfq^;J4i=cA zpT;yzZ>Tq6A{R>!-y7alluv|&^+*TJRGcko{DjE}Z79ta$3Vxbx4(`vf>m`mPb{F*;@;UCTD<(dYUN--r8dXga7gHB#VSC@w<{IER;yGLlw^268Yt z+c2nsE5BjuBoil_2rG8Q&5vi{2QDHm9P$@p#neT|Vb-4*GKB8$!YIcRCmRPyBL=iDr&zGjt8E~RGjv16W^!nduVip>J03yB-Jw#FvOsal=cbqu< zkRTb81c4T?Bi^+rmD=^iEF;oSq(WjX|L?mG=s&U~S>JW}Ke8nBhN8voZfxoAvcznA z9A4l{@BFd*D-i~ImL~#Kmyd#gjgb7^Spe~h>w7JKEq__9p%9z6aj0^fyt$GjRt76PTlpUQ z)cj@3+y41kMVF z3lBJ;HiU`+RtX-`(%oNvT2l*`ZnmaP>|E1#-YsiT<(apLH}77JTUo}*&b-Np4z{#uyDZA^$5xaMacJKy^@M)R|1Z!Fg1|JQn7R& zw>bT@B2m!V>5ku#%Seftt5<0WVc-jyL!@L5*WH4;>2>1W_{~oIVr-USX`2f#06$1S z`Q#TWwk>MYBTQokQ;CMQOpI>iZjiazA--qxPYM~WF^=^ykrK@?Z1Nx)8q1lS%!vW| z0W81S4}M7O+c^;ew3+Ff*08r0x7VUGq&y6sQ?%h8d?)E@_w;@-Q%F?$j?Z-}jr0Jt zrR#W8y6&P5otQg4O{QU63g_nym@o^aud;!ppEf$Uv_rZ59$z*JfnF*}rJEs6O-lia z8O4jLfgdHZGW+S&uaQxP+Bh$$-XU}XVisgYPViw@*t{LdLuu>zaikfeOrw%90<>!G zpI&Uvg|sA~nR@pCx}W7w`*8+pkXsZ3KOCg(>-sCa?*t*uTEjekl*l2#3$77ROfJ<` zt&yWFw)+R!3Jv-eCssFRQt8qF86tUIm{AhTELEt2pf=(&W0n_chDPN>ha(YL=5}Z} zxu>hNayDAZLpUyoS)jua)okF*0N*jEU#^eTCKcs;dg1kVFnSq|F{?2pJwi>wl9X&D zw-@Du5mihnpL+qNhaQ9e6`U+=Uk&6fjmR)M@lC)dO~^KJ$`*_|Bz42 z$TrY*fBW^SVP5BuH`!Z^dhDbvDD##mxedpADU?Wq6p>gn)1>Q~Vz#_8P022@)XHCR ztmup|PG$o7D%{hI-l4PYsV_O?}ul4cK_$u{V6)4Z>N@IbG9-N+MJ2>L?N?_>;Ns3r0Iied2nCNzgXb&lPqHD zs{-ZUi(z3-nnIrE`GJ3Dody%YYQitZ`D1^U+3f6`=>MNb8!`NacK+0b{F8Q)%cj@9 zXlLLIX*;-nigjHe{pL1bQ2ekp=au7;^I4}w*92uD)QO;cqC0{Q1vqWQ^lMmxs zZ=z!$dTvwZ|9+gvq9Cs6q!9i*Ut~;Jz6dVxF-mtf<$=oqIXAN%qJixK=kmt z-_uw`-`jKYlVNyP1aC*?Fy;lqzj{Tv#h;<7xN@_Z2t+=N_#`0V*|5#tZ0t>;Y;{ot zoGBc>Y95 zBPc{FW^S6%5o&}mJ6K(XmR>iKK-$ok`f&RQ09qhZ!A?7@(n|W_1k%}e8F>&fKB7$fq zlxkZlgk77_f-;VyRmk#hg!z6yC{*qyS0b7pF+K8r@85=Kj@R5*CmVEA&Tuti&*Y&} zE?(VMaqa*hoConUnhr&w9?V_ocF!<8OZw52`br!a*wK&@-`@%*Cu@oGz$=?PrP65G zcz=A4D6?dZTm>YONrzG?PbWO@w_H8Lm?!h~ApBh2#^|eBXv!ScUkHUXvBB8FL3Fg` zpCIq{n0adLV$;5ZfT*y*UC@-3_UWeZ)hQ9v6c1%}S+CYR-!~`Mef7@n63q%1DrZ?( z`dPl1C&rJ~Z`4SnAH#clj+#rkOQx{f$+~oR|2uCQXpWi^%j~yLh3Ob20QotC_gB!*nFE}~Te<&v9I+<%B_C)g7 zA?>gSC^5g7YW6y`U#&to9e@IbybR1dJLVK6(sN4QtQaXTz_imw-ZF}grOr;NRylcc z!=xINS!ONF^+%^DPnlFtjT0nKIY{ztD_v^&oE<7FG^?34+Z|ZFbmGkV1!mM9Z%s#MHJx}b1B_$Krmpxjk?QrCjbtg`0FGIx2v z{Ij#>dwyqt;-@ryx^}5w7_Q!rs3w=+@V0z{IwELWbXhL1`DS}UXH|I@d33~co81#e zwbTiZIcyz~q#Ye@k3dr~mGRxxBev*k-MN?!-VVSUbUzbVyPu{QW$eI$vWjkI&4AuR zA|SJJSR(;Cc>P0?kF|2;)u5{vOFJJqmuNev8SxP36*QKEQ$d7RkTiK})$LcBQaIEH z?0!uu$z?oHtuI`nCqhz zU{&6k+VEasq&Cn*N@I!xxbw`@6qp6i{<*F$Ksm+Wm#{pP!GR-v8wuOV7`8 zsN~uo5`U)^;8&rE+%HP`V}GF)#{W-R0aO0Bn$fgcQ^w%!8jQ*I%#>PkF<*^w(O!dX zA^@8&BE;iIM(<4n@jOIzO`SjvF;`=;Ex^s!ne}yDzo4)dsC*~$tfcT^r zggI$M#0g&$&k88eO4)Z%pg9och}00F4l)b4t=qsW2_(b~!39Z|gB%ZkBZ?D3^H={C zg(4f86?lFoTTPK12}%hnS58t^4I;F|nsQ?S4YoWYmddnbEMn#Z1sw#WrCVwVq0(%^ z!?}`-X zz@D*J*hiweOH2^)5~DU=ayR;Eup>zjO@Rh=uCHRaE0B;Q!^y9{(323w0q$uf=Nnc3 zD#|{L1*C&|kaIxbs3ILjpJ(=zTQ+NmlQTPBK&sizBif=UJO-Xj1?R#JPM%CuaEJ$W z<9q`;jZ6W8=7DWq%FLzbo8CT4ZAzYdK$bB@YPOSN5D)1(E4D$KAkBgOyZ|WW_zLC% zl4!s*4(_`dh23#64}R-0!Ga%!yg58_eIlJvvOG3+h^fYR>Xe%i&KIv&c=)b%4scPu zG|qU6TXlOG)T!Wj6~QaqD``}N5zfAxI^uqEdn=_U;WMw7sU1nQg~)LCCHggBu@0)0e!ndzfNmcC-10uTu<3O4M{E zH95JwKl0{MY%<7)AouwDTT{vqs8R+jqL((G`(|D-LSR92-b@bn7k;~HhWI7&H9s~c z+AKv_zD+v%c7T%AskSPYlQ2TD$cZZ~O!sYH=2lF)Zr`14+q=y=0%<<}-kjHIR-z`m zt)nO3V&=(-XbUu?t+<`iqspiC2@G+jjQO_B=v5&dRNRi>GiUO3~R0 zb&cuo92uE2G|=k4W#^t9D9#s&0FChtWv5DLx|D%5n^;bss(HUpy_(C}TG!k~s8uK~ z6+W~;g7W!lQTMuuMuSG%p&)`J`#GNsy<0`BiOQ}MRf~c#$P;O;a+=)smj}f|stRbf zdB7b+`k}pG(0;g*6i(tFYD1wGnLb&c%H6Kw+Os!mO2dWaTk^!M>3S-yEP@(F5W2uq zKb!AgSLQurx2Cc(q}~Fs0IM2?HvtUyfm1ju}?vt7YmT3qjuL>S8JCX`z|QHWUN&$E7-m&1o~?=VnB)TiaNWj`~ylXC_Rxc2z! z5xYv^*RX_@?b)OpQ}zB^eQdm8|Gn7D~|d-1l*P7Zfs$p}KbC zLO;2WnNOyGxxtGA$W&QFF5nOtJ9ZxFJ0Es<(5R?Ia<~ZajE&nPd^(}WX846!nF{MN z1}tX{0aBI6h5#GrZ{vVwxx@&{!IPT>FupM45;z$oo>Tzbw6To_rpg>2>wl<9B{(TJ zJN7PLmlPNT=Cc}%S_OW+rSn|{u!3s6e?7#^YwW{$S)!#NZPVY1jgY-`Pn*6^J4HS8 zoB#w2Xd;L;mEfSDeEb(Q0#d`MtEtACJ#Ciqysp)VZ&)g;I|z=q_`NY)#xBxx@HrU? z^={KWwBEs5Yxc{FAR`lYHwd1LZaB}=qcZTiYtY9REq#8{p-~KdQP{tlI$sL=5Yb;x zEB;PugnCaTjB+C!um$*7PSzIvF zJJ(y?dIH`0jL?WjAbZq0*8k zu1t|O2VG{a`XW@(@t}vnea6UX84tw#RWP1bG-e=a0IC}gqz|O1Jmch2Ml;kCI-lbh zr{>oQ*xa7WTrbwDc?07B4X{JLPfL~ShXuA6&HmGqIhO!opMaXbI+%raQjO_K8-W5L z21Q)JC&{X}z3BQ}^6bP{$dtUgp&+vnmlXxfUJgTa*xCetEO%oIRPO8YK0ndiex82) z@UO{D$K_^V`{?*18UIq1u~TMal>iI?@W=i#xqs_G{?ju1&&h36i>W=L@`U*fQ-S7!xCOUUc3FDX z0|!){!vL_YJYb7s{5Q`^dRXybB0V#+>o$kY?E;Es4ZYfhZg3PsPAp~(nYjTh>QGIK zkYZ`N^Am1Oa{CcQ!VG?wu=YqfOb{cxRLgX2Ca!@S;NqUhR6UVpk%LuLUaUXO#gDKS zpM4l_9rng0x5fo#1PL@cVWh)`Hk1fSg&H-35`QSM9YIy(sNVd1#%d*g!;SkdtRA9c zO(Zm|r(laVAO(7()*I7`k&vlDyLCcPyWfn_O|+n44`RhIRq|te4mBhzFLsXEcU-PbVy+ z7%x@JZ5P^EnMlZSV}9RYi}9T$f=per#pam^;mnV3rO`ub`kcb~?3NEVixzPu7Ft!s z5v)paYHe#XK|M81{sfHd#GB9hi~+-A>C@C?fp0hVc3dQDQTZG_(-3bcH>IUu7k?JRUT@0HeHYcE z(1)4Gcs=E*=b0TLng}5=Wyl~pktZ3I-S=3O1#P!VVkL+l4W`P;FM?Ci#HC6aAr(q# z?Qkv@5{-hoL5z{g(#<0+8#9u1c*PKe1h
C_fa+lLDoc07LzKz&!oa+H9Uad-<6R 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 fA}C5PAt}FO7vpFUTn$AH - - - - - - - -
- -
-

CEO DASHBOARD v3

-
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

보고 대기 없이, 로그인 한 번이면
전사 현황이 한눈에 들어옵니다.

-
- -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
- -
- - - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
- -
- - - - -

127건

-

▲ 8건

-

수주 잔량

-
- -
- - - - 96 - -

96%

-

목표 달성

-

납기 준수율

-
- -
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
- -
-

월별 매출 추이

- - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - - - - - -
-
-
-

영업1팀 38%

-
-
-
-

영업2팀 25%

-
-
-
-

생산팀 22%

-
-
-
-

품질팀 15%

-
-
-
-
-
- - -
-

대표님이 얻는 것

-
- -
- - - - - - - -

즉시 현황 파악

-

로그인 3초면
전사 현황 확인

-
- -
- - - - - - - - - - - - -

데이터로 판단

-

감이 아닌 숫자로
KPI/팀 성과 비교

-
- -
- - - - - - - - -

모바일 승인

-

이동중에도 즉시
결재/승인 처리

-
-
-
- - -
- - -
-

대시보드 핵심 기능

-
-
- -
- - - - - -

실시간 매출/수주 KPI

-
- -
- - - - - - - - - - - -

조직 계층별 실적 트리

-
-
-
- -
- - - - -

역할별 수당 현황

-
- -
- - - - 5 - - -

미승인 실시간 알림

-
-
-
- -
- - - - -

기간별 트렌드 분석

-
- -
- - - - - - - - -

수익 시뮬레이터

-
-
-
-
- - -
- - -
- -
-
- - - - - -

BEFORE

-
-

매출? → 보고 대기 1~2일

-

수주? → Excel 취합 반나절

-

승인? → 서류 찾기 30분

-

실적? → 각 팀장 개별 보고

-
- -
- - - - -
- -
-
- - - - -

AFTER (SAM)

-
-

로그인 → 3초 전사 현황

-

클릭 → 실시간 수주 데이터

-

뱃지 → 즉시 승인 처리

-

트리 → 전 조직 한눈에

-
-
- - -
-
- - - - -

실시간 업데이트

-
-
- - - - -

PC + 모바일

-
-
- - - - - -

역할별 권한

-
-
- - - - - -

데이터 암호화

-
-
- - - - - -

클라우드

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

무료 데모 신청

-

contact@codebridge-x.com

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v3/slides/brochure-dashboard-back.html b/sam/docs/brochure/v3/slides/brochure-dashboard-back.html deleted file mode 100644 index f199750..0000000 --- a/sam/docs/brochure/v3/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - - - -
- -
-

FEATURES & PRICING

-
- - -
-

대시보드 핵심 기능

-
- -
- - - - - -
-

실시간 KPI 카드

-
-

매출, 수주, 납기율, 승인 대기

-
- -
- - - - - - - - - - - - - - - -
-

조직 실적 트리

-
-

계층별 팀/개인 실적 펼쳐보기

-
- -
- - - - -
-

역할별 수당 현황

-
-

판매자/관리자/협업자 배분 확인

-
- -
- - - - ! - - -
-

승인 대기 알림

-
-

가입/지급 미처리 빨간 뱃지

-
- -
- - - - -
-

기간별 트렌드

-
-

당월/분기/연간 추이 차트

-
- -
- - - - - - - - -
-

수익 시뮬레이터

-
-

가상 시나리오 수당/마진 계산

-
- -
- - - - - - - - -
-

모바일 대응

-
-

스마트폰으로 KPI 확인/승인

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
- -
- - - - - -

CEO

-

전사 KPI 총괄

-
- -
- - - - - - -

관리자

-

팀 실적 관리

-
- -
- - - - - - - - -

운영자

-

인력/승인 관리

-
- -
- - - - - - - -

영업자

-

내 실적 조회

-
-
-
- - -
- - -
-

대시보드 + SAM ERP/MES 통합

-
-
- - - - - - -

견적/수주

-
-
- - - - - - -

생산 MES

-
-
- - - - - -

품질/검사

-
-
- - - - - -

재고/자재

-
-
- - - - -

인사/회계

-
-
-

대시보드의 모든 데이터는 SAM ERP/MES 실시간 데이터 기반

-
- - -
- - -
-

투자 비용

-
- -
-
-
- - - - -

대시보드 포함 기본 패키지

-
-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

CEO 대시보드 + 견적/수주 + 생산
인사/회계 무료 포함

-
-
- -
-
-
- - - - - -

추가 옵션 (선택)

-
-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
- - - - - -

1~2주

-

현장 인터뷰

-
- - - -
- - - - - - - -

2~4주

-

맞춤 개발

-
- - - -
- - - - - -

1~2주

-

데이터 이관

-
- - - -
- - - - -

1~2주

-

교육/안정화

-
-
-
- - -
-
-
- - - - -
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v3/slides/brochure-dashboard-front.html b/sam/docs/brochure/v3/slides/brochure-dashboard-front.html deleted file mode 100644 index b1d8828..0000000 --- a/sam/docs/brochure/v3/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,262 +0,0 @@ - - - - - - - - -
- -
-

CEO DASHBOARD v3

-
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

매출, 수주, 조직 실적, 승인 대기
더 이상 보고를 기다리지 마세요.

-
- -
- - - - - - - - - - - - - - - - - - - - 5 - - - - - - - - - - - -
-
- - -
- - -
-

대표님의 하루

-
- -
- - - - - 9AM - -
-

"어제 매출 얼마야?" → 팀장 보고 대기중...

-
-
- -
- - - - - 2PM - -
-

"수주 밀린 거 없어?" → Excel 취합중...

-
-
- -
- - - - - 5PM - -
-

"결재할 것 정리해줘" → 서류 찾는중...

-
-
-
-
- - -
- - - -

SAM 도입 후

-
- - -
- -
-
-
-
-

SAM CEO Dashboard ― 로그인 후 3초

-
- -
-
- - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
-
- - - - -

127건

-

▲ 8건

-

누적 수주

-
-
- - - - -

96%

-

목표 달성

-

납기 준수율

-
-
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - - - - - - - - - -
-
- - - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-

품질팀

-
-
-
-
-
- - -
-
- - - - -
-

SAM 대시보드가 드리는 약속

-

로그인 한 번이면 전사 매출, 수주, 승인 대기를 한눈에.
보고를 기다리는 시간을 제로로 만들어 드립니다.

-
-
-
- - -
-
- -

클라우드 기반

-
-
- -

PC + 모바일

-
-
- -

역할별 권한

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능을 확인하세요 ▶

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v4/convert-1page.cjs b/sam/docs/brochure/v4/convert-1page.cjs deleted file mode 100644 index e218f62..0000000 --- a/sam/docs/brochure/v4/convert-1page.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard v4 (Light) 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-v4-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v4/convert-2page.cjs b/sam/docs/brochure/v4/convert-2page.cjs deleted file mode 100644 index f8c691d..0000000 --- a/sam/docs/brochure/v4/convert-2page.cjs +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-v4-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v4/sam-brochure-v4-dashboard-1page.pptx b/sam/docs/brochure/v4/sam-brochure-v4-dashboard-1page.pptx deleted file mode 100644 index 747d4f45fc512f379904382c676cce91bb854a78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165817 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
fp1(TN)SBS-qg|flwaWQQ0F%b!5%)owge; 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 diff --git a/sam/docs/brochure/v4/sam-brochure-v4-dashboard-2page.pptx b/sam/docs/brochure/v4/sam-brochure-v4-dashboard-2page.pptx deleted file mode 100644 index c56e36ebe40aae0300bcf4c0b63d445ade05427b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242412 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 diff --git a/sam/docs/brochure/v4/slides/brochure-dashboard-1page.html b/sam/docs/brochure/v4/slides/brochure-dashboard-1page.html deleted file mode 100644 index 8792a24..0000000 --- a/sam/docs/brochure/v4/slides/brochure-dashboard-1page.html +++ /dev/null @@ -1,405 +0,0 @@ - - - - - - - - -

- -
-

CEO DASHBOARD v4

-
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

보고 대기 없이, 로그인 한 번이면
전사 현황이 한눈에 들어옵니다.

-
- -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
- -
- - - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
- -
- - - - -

127건

-

▲ 8건

-

수주 잔량

-
- -
- - - - 96 - -

96%

-

목표 달성

-

납기 준수율

-
- -
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
- -
-

월별 매출 추이

- - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - - - - - -
-
-
-

영업1팀 38%

-
-
-
-

영업2팀 25%

-
-
-
-

생산팀 22%

-
-
-
-

품질팀 15%

-
-
-
-
-
- - -
-

대표님이 얻는 것

-
- -
- - - - - - - -

즉시 현황 파악

-

로그인 3초면
전사 현황 확인

-
- -
- - - - - - - - - - - - -

데이터로 판단

-

감이 아닌 숫자로
KPI/팀 성과 비교

-
- -
- - - - - - - - -

모바일 승인

-

이동중에도 즉시
결재/승인 처리

-
-
-
- - -
- - -
-

대시보드 핵심 기능

-
-
- -
- - - - - -

실시간 매출/수주 KPI

-
- -
- - - - - - - - - - - -

조직 계층별 실적 트리

-
-
-
- -
- - - - -

역할별 수당 현황

-
- -
- - - - 5 - - -

미승인 실시간 알림

-
-
-
- -
- - - - -

기간별 트렌드 분석

-
- -
- - - - - - - - -

수익 시뮬레이터

-
-
-
-
- - -
- - -
- -
-
- - - - - -

BEFORE

-
-

매출? → 보고 대기 1~2일

-

수주? → Excel 취합 반나절

-

승인? → 서류 찾기 30분

-

실적? → 각 팀장 개별 보고

-
- -
- - - - -
- -
-
- - - - -

AFTER (SAM)

-
-

로그인 → 3초 전사 현황

-

클릭 → 실시간 수주 데이터

-

뱃지 → 즉시 승인 처리

-

트리 → 전 조직 한눈에

-
-
- - -
-
- - - - -

실시간 업데이트

-
-
- - - - -

PC + 모바일

-
-
- - - - - -

역할별 권한

-
-
- - - - - -

데이터 암호화

-
-
- - - - - -

클라우드

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

무료 데모 신청

-

contact@codebridge-x.com

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v4/slides/brochure-dashboard-back.html b/sam/docs/brochure/v4/slides/brochure-dashboard-back.html deleted file mode 100644 index 6b64b85..0000000 --- a/sam/docs/brochure/v4/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - - - -
- -
-

FEATURES & PRICING

-
- - -
-

대시보드 핵심 기능

-
- -
- - - - - -
-

실시간 KPI 카드

-
-

매출, 수주, 납기율, 승인 대기

-
- -
- - - - - - - - - - - - - - - -
-

조직 실적 트리

-
-

계층별 팀/개인 실적 펼쳐보기

-
- -
- - - - -
-

역할별 수당 현황

-
-

판매자/관리자/협업자 배분 확인

-
- -
- - - - ! - - -
-

승인 대기 알림

-
-

가입/지급 미처리 빨간 뱃지

-
- -
- - - - -
-

기간별 트렌드

-
-

당월/분기/연간 추이 차트

-
- -
- - - - - - - - -
-

수익 시뮬레이터

-
-

가상 시나리오 수당/마진 계산

-
- -
- - - - - - - - -
-

모바일 대응

-
-

스마트폰으로 KPI 확인/승인

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
- -
- - - - - -

CEO

-

전사 KPI 총괄

-
- -
- - - - - - -

관리자

-

팀 실적 관리

-
- -
- - - - - - - - -

운영자

-

인력/승인 관리

-
- -
- - - - - - - -

영업자

-

내 실적 조회

-
-
-
- - -
- - -
-

대시보드 + SAM ERP/MES 통합

-
-
- - - - - - -

견적/수주

-
-
- - - - - - -

생산 MES

-
-
- - - - - -

품질/검사

-
-
- - - - - -

재고/자재

-
-
- - - - -

인사/회계

-
-
-

대시보드의 모든 데이터는 SAM ERP/MES 실시간 데이터 기반

-
- - -
- - -
-

투자 비용

-
- -
-
-
- - - - -

대시보드 포함 기본 패키지

-
-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

CEO 대시보드 + 견적/수주 + 생산
인사/회계 무료 포함

-
-
- -
-
-
- - - - - -

추가 옵션 (선택)

-
-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
- - - - - -

1~2주

-

현장 인터뷰

-
- - - -
- - - - - - - -

2~4주

-

맞춤 개발

-
- - - -
- - - - - -

1~2주

-

데이터 이관

-
- - - -
- - - - -

1~2주

-

교육/안정화

-
-
-
- - -
-
-
- - - - -
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v4/slides/brochure-dashboard-front.html b/sam/docs/brochure/v4/slides/brochure-dashboard-front.html deleted file mode 100644 index 8f4165f..0000000 --- a/sam/docs/brochure/v4/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - -
- -
-

CEO DASHBOARD v4

-
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

매출, 수주, 조직 실적, 승인 대기
더 이상 보고를 기다리지 마세요.

-
- -
- - - - - - - - - - - - - - - - - - 5 - - - - - - - - - - - -
-
- - -
- - -
-

대표님의 하루

-
- -
- - - - - 9AM - -
-

"어제 매출 얼마야?" → 팀장 보고 대기중...

-
-
- -
- - - - - 2PM - -
-

"수주 밀린 거 없어?" → Excel 취합중...

-
-
- -
- - - - - 5PM - -
-

"결재할 것 정리해줘" → 서류 찾는중...

-
-
-
-
- - -
- - - -

SAM 도입 후

-
- - -
- -
-
-
-
-

SAM CEO Dashboard ― 로그인 후 3초

-
- -
-
- - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
-
- - - - -

127건

-

▲ 8건

-

누적 수주

-
-
- - - - -

96%

-

목표 달성

-

납기 준수율

-
-
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - - - - - - - - - -
-
- - - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-

품질팀

-
-
-
-
-
- - -
-
- - - - -
-

SAM 대시보드가 드리는 약속

-

로그인 한 번이면 전사 매출, 수주, 승인 대기를 한눈에.
보고를 기다리는 시간을 제로로 만들어 드립니다.

-
-
-
- - -
-
- -

클라우드 기반

-
-
- -

PC + 모바일

-
-
- -

역할별 권한

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능을 확인하세요 ▶

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v5/convert-1page.cjs b/sam/docs/brochure/v5/convert-1page.cjs deleted file mode 100644 index 840ccc9..0000000 --- a/sam/docs/brochure/v5/convert-1page.cjs +++ /dev/null @@ -1,52 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); -const sharp = require('sharp'); - -async function generateGradientBg() { - const svgGradient = ` - - - - - - - - - `; - const buf = await sharp(Buffer.from(svgGradient)).png().toBuffer(); - return buf.toString('base64'); -} - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - // Pre-generate gradient background PNG - console.log('Generating gradient background...'); - const bgBase64 = await generateGradientBg(); - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard v5 (Premium Gradient) 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - // Set gradient background on each slide - for (const slide of pres.slides) { - slide.background = { data: `image/png;base64,${bgBase64}` }; - } - - const outputPath = path.join(__dirname, 'sam-brochure-v5-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v5/convert-2page.cjs b/sam/docs/brochure/v5/convert-2page.cjs deleted file mode 100644 index 7aa1fd0..0000000 --- a/sam/docs/brochure/v5/convert-2page.cjs +++ /dev/null @@ -1,56 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); -const sharp = require('sharp'); - -async function generateGradientBg() { - const svgGradient = ` - - - - - - - - - `; - const buf = await sharp(Buffer.from(svgGradient)).png().toBuffer(); - return buf.toString('base64'); -} - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - // Pre-generate gradient background PNG - console.log('Generating gradient background...'); - const bgBase64 = await generateGradientBg(); - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - // Set gradient background on each slide - for (const slide of pres.slides) { - slide.background = { data: `image/png;base64,${bgBase64}` }; - } - - const outputPath = path.join(__dirname, 'sam-brochure-v5-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v5/sam-brochure-v5-dashboard-1page.pptx b/sam/docs/brochure/v5/sam-brochure-v5-dashboard-1page.pptx deleted file mode 100644 index 60194bf414575bb0fa6214c3b5832ada8e5d10f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 206694 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

CW#D(BxaaK zVUk}p0*B7Mn0g0=S{UjD?A?*SD!OULl#$OPPZ)2sPq0qg**^hONbB5_f>Cs683QXk+m7u95z>Y`Fwa;vZXc%D?^m+$szZ7GSEWf zeo!XhMsh@X@}Y`Vyn{`rZr0ZUZ%oq>3+iT~4C2MDuwCt~14g)T>OLcIflVRw^E3s4 zEd{#_&CPHgL7A7qOn3hwBpwqF0lYC*S#{PSKmKRXMV9cz<$pXsQhFGkUeu)NQq>PR zY*);j4SI}Kfr}Y`EF2q+X!3%;3~Z^HT|3qarQ=9PMy?$Qks>l*R{0;N z)i+l>-&@%**i>{2p4&VW_B$M_WjoDtE&Bm$L9<#d#Xm|-gBQxr9T85%_QBX$z?Y?i zy}36hvl#oFvE6XnGU#*b+i!2$DZG|G({6FE4qXH^itTjZ?#xL9Z~*4;npkqDK^2mw z2KeIXd$2z7wP2oA*{;VB%K&shE7<-S7|^pwu3Ht$79kp3$rIa-Nsc%*kjEB?6Lr4R zb}q?Ekd;=I>teoEd1SzL!}DQ%ix@Z@T`p*z99iaEA!|1~C6mVN<7~;|m+{L-K z{LJvNQj-SRNqHBn;nYsu9NYa~_R}3v!JL%~vGh(DIlO}5p)e8Ngi9Fpu7z#Q^k6e- ziz~fbx|`l@$Ij**0oyzdHa6=4v0mKYvIGXMBn(Ux?gOfWr-t({&r{{VBH4@X!Y~|G zQO23($(n+~i7)pa3%609M!AAHmQwhlZ-9U13C{0-GY+$2Zkc6g{uqz!N-VME$d>tH zFOnT*DQl~pE4%`2D1@-u!)^bXqq6G4{&k%-Rnp^*`0np+C+ZcHmtbUqMnP_5n&Mm? zNsOi_z_Ut)asJXb&mG5WS!nD^lN{30aqfp9W-Q1Fqu(%UiL7;~QOR7bk*slK&Mo8Z zKM4B67X#rFOL8^8ij46Jo@`^FD=kb&GGc@N9uNw2(Ma+P22^Hj#WFo%WW@d~=FH=H z8gYi5;#u{>VqRQqW5a4BDi%aqk~YR)JT2l4O=(Y#^>wS_!^(-QR|$k}6nM72uDP;i ztfSe?I3#X_RNmMx#%N1K?Y2V#6j{Ssc3N-;L)kACYIW{MWr-H6eh%g?;WAgzgl~m* zJ$CE&#bl{cUdC9gUDz{gpYOG4gr|U!X8(Cmvq`aEx z>qh2j!l1eVlucnZ9Bg8|0>Hyy(Zrk_&DPWK9Jc`$Jl?L0{CDzsWn>yR+H zM5smaBc2j9PjrLNY-Es_4txdVnc+o&$2`n|JFeVzVLE2gPAm`rZ>pWce<=r

!GxcFA?_8|^5_6~tBBz_yv` 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 SA2b&K%R^c-{2I2j=>Gw8<36AO diff --git a/sam/docs/brochure/v5/slides/brochure-dashboard-1page.html b/sam/docs/brochure/v5/slides/brochure-dashboard-1page.html deleted file mode 100644 index 6e8d331..0000000 --- a/sam/docs/brochure/v5/slides/brochure-dashboard-1page.html +++ /dev/null @@ -1,319 +0,0 @@ - - - - - - - - -

- - -
-
-

CEO DASHBOARD

-

대표님의 시간은
보고를 기다리는 데
쓰여선 안 됩니다.

-

로그인 3초. 매출, 수주, 승인까지
모든 경영 현황이 한 화면에.

-
- -
- - - - - - - - - - - - - - - - -
-
- - -
-
-
-

SAM 도입 후

-
-
-
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
-
-

5.2억

-

▲ 15.3%

-

월 매출

-
-
-

127건

-

▲ 8건

-

누적 수주

-
-
-

96%

-

목표 달성

-

납기 준수율

-
-
-

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - - - - - - - - - -
-
- - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-
-
- - -
-

대표님이 얻는 것

-
-
- - - - - -

즉시 현황 파악

-

로그인 3초면 전사 확인

-
-
- - - - - - - - - -

데이터로 판단

-

감이 아닌 KPI 비교

-
-
- - - - -

모바일 승인

-

이동중 즉시 결재

-
-
-
- - -
- - -
-

대시보드 핵심 기능

-
-
-
- - - - - -

실시간 매출/수주 KPI

-
-
- - - - - - - - -

조직 계층별 실적 트리

-
-
-
-
- - - - -

역할별 수당 현황

-
-
- - - - 5 - -

미승인 실시간 알림

-
-
-
-
- - - - -

기간별 트렌드 분석

-
-
- - - - - - - -

수익 시뮬레이터

-
-
-
-
- - -
- - -
- -
-
- - - - - -

BEFORE

-
-

매출? → 보고 대기 1~2일

-

수주? → Excel 취합 반나절

-

승인? → 서류 찾기 30분

-

실적? → 각 팀장 개별 보고

-
- -
- - - - -
- -
-
- - - - -

AFTER (SAM)

-
-

로그인 → 3초 전사 현황

-

클릭 → 실시간 수주 데이터

-

뱃지 → 즉시 승인 처리

-

트리 → 전 조직 한눈에

-
-
- - -
-
- -

클라우드

-
-
- -

PC + 모바일

-
-
- -

역할별 권한

-
-
- -

데이터 암호화

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

무료 데모 신청

-

contact@codebridge-x.com

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v5/slides/brochure-dashboard-back.html b/sam/docs/brochure/v5/slides/brochure-dashboard-back.html deleted file mode 100644 index aa9f629..0000000 --- a/sam/docs/brochure/v5/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,311 +0,0 @@ - - - - - - - - -
- -
-

FEATURES & PRICING

-
- - -
-

대시보드 핵심 기능

-
- -
- - - - - -
-

실시간 KPI 카드

-
-

매출, 수주, 납기율, 승인 대기

-
- -
- - - - - - - - - - - -
-

조직 실적 트리

-
-

계층별 팀/개인 실적 펼쳐보기

-
- -
- - - - -
-

역할별 수당 현황

-
-

판매자/관리자/협업자 배분 확인

-
- -
- - - - ! - - -
-

승인 대기 알림

-
-

가입/지급 미처리 빨간 뱃지

-
- -
- - - - -
-

기간별 트렌드

-
-

당월/분기/연간 추이 차트

-
- -
- - - - - - - - -
-

수익 시뮬레이터

-
-

가상 시나리오 수당/마진 계산

-
- -
- - - - - - - - -
-

모바일 대응

-
-

스마트폰으로 KPI 확인/승인

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
-
- - - - - -

CEO

-

전사 KPI 총괄

-
-
- - - - - - -

관리자

-

팀 실적 관리

-
-
- - - - - - - - -

운영자

-

인력/승인 관리

-
-
- - - - - - - -

영업자

-

내 실적 조회

-
-
-
- - -
- - -
-

투자 비용

-
-
-
-
- - - - -

대시보드 포함 기본 패키지

-
-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

CEO 대시보드 + 견적/수주 + 생산
인사/회계 무료 포함

-
-
-
-
-
- - - - - -

추가 옵션 (선택)

-
-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
- - - - - -

1~2주

-

현장 인터뷰

-
- - - -
- - - - - - - -

2~4주

-

맞춤 개발

-
- - - -
- - - - - -

1~2주

-

데이터 이관

-
- - - -
- - - - -

1~2주

-

교육/안정화

-
-
-
- - -
-
-
- - - - -
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v5/slides/brochure-dashboard-front.html b/sam/docs/brochure/v5/slides/brochure-dashboard-front.html deleted file mode 100644 index ba19d78..0000000 --- a/sam/docs/brochure/v5/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - - - -
- -
-
-

EXECUTIVE EDITION

-
-
- - -
-

CEO DASHBOARD

-

대표님의 시간은
보고를 기다리는 데
쓰여선 안 됩니다.

-

로그인 3초. 매출, 수주, 승인 대기까지
모든 경영 현황이 한 화면에.

-
- - -
-
-
- - - - -

1~2일

-

매출 보고 대기

-
-
- - - - - - -

반나절

-

Excel 수주 취합

-
-
- - - - -

30분

-

결재 서류 찾기

-
-
-

매일 반복되는 비효율, SAM이 제로로 만듭니다.

-
- - -
-
-
-

SAM 도입 후

-
-
-
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
-
- - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
-
- - - - -

127건

-

▲ 8건

-

누적 수주

-
-
- - - - -

96%

-

목표 달성

-

납기 준수율

-
-
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - - - - - - - - - -
-
- - - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-

품질팀

-
-
-
-
-
- - -
-
- - - - -
-

대표님께 드리는 약속

-

보고를 기다리는 시간을 제로로.
의사결정에만 집중하실 수 있도록.

-
-
-
- - -
-
- -

클라우드 기반

-
-
- -

PC + 모바일

-
-
- -

역할별 권한

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능을 확인하세요 ▶

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v6/convert-1page.cjs b/sam/docs/brochure/v6/convert-1page.cjs deleted file mode 100644 index 99a9664..0000000 --- a/sam/docs/brochure/v6/convert-1page.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard v6 (Corporate Blue & White) 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-v6-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v6/convert-2page.cjs b/sam/docs/brochure/v6/convert-2page.cjs deleted file mode 100644 index 604c5be..0000000 --- a/sam/docs/brochure/v6/convert-2page.cjs +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-v6-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v6/sam-brochure-v6-dashboard-1page.pptx b/sam/docs/brochure/v6/sam-brochure-v6-dashboard-1page.pptx deleted file mode 100644 index a9fa7c071d0fb8e8e3bef33b99ba86d1f8baa678..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167336 zcmeDk2V4_ZAJHmqQ0pEMcNNKH2L)wCaiZY1YDfY^LK2g(9CcyEz4sPZow)a|b+_8O zR~>aR9JSWEzyEu8N$z5T1Op#x+F$U-yLa#2`|tHa+ZHZT!Ug>&4wv@E2h^XU@NZw8 zLZ$Z(wb`aPw2^CMp*oE=yJkh8nO1AarXw^2)Z$|=EJ?MKMN4&t>|W`?Z1}N$%Tmo1 zu@MTnT&a%Au5aUDV-<3xG@FkyjzO>J&w$;S_c zp#{tCLn~ToZS4)q+RYa)0wh7d^6*TrK#kg@0%vZs+zZeKdg9IHl;@G z>q#_ad$JU2nFax&uVQer8h{`a;ZwAR`_}*DfFH#{7RQ-j{9K2(`vo1XSBhf z_3`%B%U~|0`lcGKLJfB!HM&@-0Zw&M-dd?FMjEB?=CIiUZy7lbF9RBfr(g3RMWi%N zWncv-!tltEz~i1QYP{%GzMfL8R;83l4KPq|bl2PJbBJVQ^I*JMZZSG9YGh5}N!+BdvZ6 zx^Df6SYEuv6Y&7A8t+&w&(;%ghq;y?dm=vD)wi;Un#f3{OaaUwE*1d1DLfyflO_NV z+Za-!N}p&kCOUk5{0(}oP9c@+qZJB6tg0#g7rm+@ppp)zgB1!KqJuOR;MN;)7HF-9 zfnfkcc=t)~rgsJ}r=K>Bl`7Rn0I@X~25F+55RM)ht?!SG2~&3?b)}s+d`z^yb2KKs zH+~{-#__Qcprys)jiXn_)bYng5U*4uDB!9+#KYbB!Hy?eYG(x2aGbE`L zdPmQ@!QA!7MuEZ3D7Vp0K7A7C1OpK-vw--qG127{^RdH6WbT?85X&lIHOc-mjZR_z zN*NhHdfH11N`yjZP%4b9|0AyiZejl$Ex42di92$I{KJU6PcG#-hUnz!SE6|F=Jak^A#z_&U5K3UWRarW$Hhf*rh+Jb zPD+i8w|HBbJm_Aki&7YTJx!2dTqbXugW}|TPz)KCIht67K`NITq#)WdfXT{`Ou%Y; zOx&E0>Ejc{Y+)XdRk4H|lQ(_!^cGM!(QxZkr*B+b&=2V?mUEqZP(*x&#KD}yA@l1< zK(PuMAatqqKJfq?xr09AXLiMyiGaL$xW*t=g)3tfer!Nk(+S+^rqRju=qh;tSNzS| zi(h-COsCOnA`PsLSZrm5Y7!K>P>oV;XzopK2+xJm1V;(hr~Epqz4S7jLZN1bMQajR z4GDpdxKVR&D=pFUQk^tPC)Gyd;n2?DUKmz1{fOq?)Dc|kfV5n6i9CQSt(9`QLQS=R zPhy3p`m7QoVvC7x7Y^QmQs!!cx3BYD!-4L&qwu-k|f7fJUijbXpdYe$8R-({JV+$TCH;k$%CtU+IhkKgl3^d2HYY@r!5wY$ zLy`{5Wu`8*aXJ+?5y@rV3Kd`%V6r#Sl<@YXUN4t{SuxgZWuiZT@6cMtDh=3JMA3M9 zQ@M=1X=4e|VF6 zM5l;XBHJ1w?$F3|O1>6Pqz`_JLe7Gz@gXOYntY#b+`yo4iKicj%@KI9xn67{oM8KK zxjuZNCmJB*c}&uc*J7z*=DJg32N<_MfKdzdsMX+vJPIuS=vNES!)(^4Ydr{03ZC4Y z_d>HCaeUIsVip8x7b#;0*@9H-z^Fx+292`39b&UJ5sfK5;}KX4h+%iJF$jms$yn*M zoEzFjy*KS7+qnTwNeV8xlQfxggTNM1WUy)*wHwnVtby8?8@?k4VKUa32VuID-5@}W zbKFc|+l_eu_|zN!&2}|}4jqHpenT6H`J!<$velqEx?W#(n&skxeibMhW%)I^JU&n0Z|Wa6P#HdK7c2}G1Pe_4;RaHf4CE1VEX+QIoIvdFfHBh= zk%w0L4iW}&394^eBl3{@!I=H|=zm6g+=y%s+6v4oeLNCLpm`2yjmSg(2krR*;(#D# z|F{v^UhqMCA=^KQCuEEVH;P6w?+2~fd;vGm__d}cks4KNtIfrHo;3 z9zMb=D)muFX-}hZ!ASHUTteevfhMlhf>EIj^y*1#a&O0uJ0rH-8L|D&$dPwOY@pta zyJ9@o8ngxaa~UhAWG)!SO5eOVWBLs8)t2pn6PUm4{<<;uckR!vd-!5=^LsPX?r%xE zH+$m4HPfy1?yr+Z(3>OUhMv_yk-+M#iG>feGJZvb&b}wYqoqpv6aEnHAXQ7zTdedS zY=~yG?vNx^Nv*UCP%!7Qs}4wwwT7+Y`ZER`7OjgjD6RC^HaeEku)Rj33ef1R&O%%C zF4{DQ$El;Nzg!nbZ`WBWjkoshK&hIs%E573usbNNbr~3~U<@-<1*Q^kN2?VE7P_a2 zQ7Ei@T@R&_F|THuR1YO9K&rGlCE>~l1~i!;Y7H)Ml{CrP0AalubJM;nD?p>N_SHd( zcxFq$4Jmf?lgMvEws3`tF^#RHaRzCuwJyV@$k%Fy|8`PCw6#HoCFx}JcEMm&LVCPP z!wQCA4865yI_ltiX??c^1BdlxZ?8&Xx^dSPV=!<6c1 zdfO0v41lNh7RtgOR ze#4Sfky3>P?gpS zYk<{8%7j&Pt_B}J3kx?N(#dv8Jp*LJ6j2%*<7uB{L3om+>R74HR?i(^)#-B{3`VwC zYXHzuC5xdq?4X439DoH7^55M+~7S zR;m7|1Y{xm@bgFZv4V1rk$S9m%ph5I$~(45N^1)#$5uN_*(MPDK+1wI+5}1C)S(LC zCLhu*r)1-lY&^es$;K8wJLMT$QioHX@dP=;DbG+Q7RSglW)TLY8)Wt%%{|!o6Km7< z6RA|GaI{`KJvRU7L93BV;39es+dUN~Tc9=?4iTG!7ckmEj0Ap$rPCO?DGkx!-98Y-x^en9gJdtdC*=pa|02LPt&NB zq1lXQ>K>0aN{@Or4gfuC&md?oV-A9AWe`H+MKnps3yS8?${_HUIb;-KE29vh?u|?J z!6;y8rd0v8n+3|cuP2`ekHAAnlT6(xG@KTX9f#+`e;ywjZkR3?BKN)o0pU^JTw*fvjMcs!w`z7t5!#m+qH)0 zENO#EO{WwpH7fyPjktUO8JQHK93sJV42Q8=fE_*3yrilqNQ;pfbR@#(hO%gNdP9&@ zA5Bg;COV{?4dDn19ZRK*_4P!bZ-NGu8q*LwHi82Lp}-DK3}mIC4GtEaxyXvhNC?27 zFFWlDnrZSBC_Kp)tlq_s=e{v|67ug9y0B<@0xLolr|T>Q5aSC8G=XxZ9y|yHIh%4N zxWr6^%nW}PjEN$x}pn}&nWOmjdxqrMp?lr|+4BaDbb z43<8r3~p-T8sr#ka@~(Vgl5jn9>r#!$)GBTYg#9!j5iqBfW=(qj>*$TWt?IQ4o@;^ zOOh)#(mDWCt6Tym+%$hyxNZ(uKW%8{moX*`jrc<{jA%A>l{qxy2YzH|IS~kGR9|51 z)?(gFJi_Yh;9{;gKRjm%wQwoYC|BZ{#=dj9ds~B}x2XI?Jzh$1UhAYF5QSlxgd`h) zB8dhHHQv1EuR3v%?JG0&eH$P)yMyaYsGpchX7pH#0PYrI*Gl!r~(I3WEf6Bv3~nu>w3`8WhtC zPw7LIGD933ka~*NN3W8TaZMp|45N%I7$!IbPskH-1w6?1)%l=>$LSh~Au<}9AE$Q} zA}2fHL%5IWY0{sMpEk9n1yEVPW`3NDVM^;`?GgB%uz@UDBJfFq2f`bYL6Uqu#RShN zQ&Z1Tt&BHtE8`>ZZCW2WnRtwRHYN_QidPZZP#p_gF740^QZgX85utUsG$L%E@m27( z1_)|_CJJdgb$~7guLO=hpibcy3I|4^?r~}vV)9sygpNAH8}!f<0R-BU6{FCh>}w!= zAC)r=iN?SR25Ym7d1xG~_EH&0L6Z^AfNm%#_Yfnf0o{=4DO4s$h_RWP55$P!kYk=2 z9$$zC?3^DwDb)OM&GSQoopXNTRcKPE`Qe%8hu{hcffIlbR-sv;CWud)AQ4A|$pfhc ze9aUH%?dR^0`mlMI3fTbCkX-wLbF0mkdQV(LLS#iqcP(`vqDXf$UH%4c0g!w66tsq zniXn-#Iy+#@P$qyjUfol3Kobzlo~-wrfVG*3QzZWlMmG@1oZ3(;9vnV18}?o*gO%~0s?{p#gZT*kc(AlX0m^fdkH^wVHX$lGXf1G=i*a< zhV1B*zpvl{3sUMJ4hrJhAb1f9m$0HWx`6>wT^N+!0e4=kG*KA~3C40zvrr109un;o z@^%VH#m34QUqE2(%6ukf@Avq)8Hcuj+!kx&NLw~vqV;4sVsgBU z(V8z=A1QER7^iWfO?^XSE3b}%c@2ZKQ&6w5H9#o9vJSEZkcXuGA|6A61r=#6d~MhO zDYWQUo1!9k9c@)y$tAj>>`H6|A^ZITQI3T$>2>o3?iaPBL~$OgChB`UKU`s?4CD4Dlqy4^V>cH5ao>CigLN|@JcT7EbT(JI;@`#ZrNkld\Ux_#ZS+s7 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 OqiBG)#bmIr3iTgjUne*K diff --git a/sam/docs/brochure/v6/sam-brochure-v6-dashboard-2page.pptx b/sam/docs/brochure/v6/sam-brochure-v6-dashboard-2page.pptx deleted file mode 100644 index 8c54f0274c4aa5807403e289f5b8567e7d58de7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 227131 zcmeEP31Ab|x-N(ciVE(#LvV*Qne3@-U7^ZSX~Ff1X)ULSfDecw4}CYec-(3EK{CHK9`WR^2$&N=^ozJD*d;D9cN zWRd@!SLV2!9*F-r2>*@=3Z9UzBlIvXM~aV<`VjKCUBXaDoxeI|myZ+s zptyZc9ap?#m?IPxf~mT$c!zZRp57r1I_+4h))k!z?Qt@taST@pUZEM|=<|n#kkskq zi&FYV#qE3g#vbC}(}zmQgr)k>V-wt+=nbDN_}5oO7(ViM6rQ;(-|q_xzVMi`x_}V+ zSM~(2r{6&QhpWM~x&6NW8a`XE;RK)4PZnW+&8V^>txdy)!VaIy;qm)~{+c==q!~Ei zutWOMHaI-(3-#Ajg~Ndyoi60WSUN)4{(#`al?s2*>j>j#uu>OrILA3E1)V{!x9FU# zAKEbKhh{*(0-?fD?Fn;*6VN@@6Fjcr#Qu_3_18E80gv112&1Dqa#fe`Il?khJXqs% z#q~}r_ADDu(#Ato?m(zd@;8hP2$gYfaC^z8(F=)tH8zm!k;)|B5voc21kR`ETfmq2 zDRMq>&nn#&2|tFMPyB?}R1^0^4c4n)=M9*WJ%KCAx!lteH7RF5!6f=CD%?&1n?bb~ z3p^Xo7X%%(Sj0)XG{F;^5Z5R9`r7jwLV=**aD}P_A?)>J(|^gUMu3!p7!IxkJH&8@ z7wc9>b{5n+2b1>}j^aq9XmyU$b$kW;YWIZ7mb_)_h+ruldsi*(O zI4~g)RO%93O<&$mM^W$qr8KbPvsZunSLduO@<)Uh2F(e+47D6?kCda+7Q$`?!qp5o zq+7Rs@@BbWSAR{g*u`r&hPd~P>e)yYh_H)+d&OIH2?h_%JA#!$xW6X4GNftJCGC`< z`JGaT8p_^yg|Nfr2sB$KE=3n>@;(`x-={qfl=6;PhZ3Sk)+e2O_Cj&1aD{c^ z5buFmS>%t1m+@oW@})>NK1PGhUx&)CV+$qW(f~I0g>q`JI9j&)q|Y2pDU&Sner5i! z!&Bz=3Ip^Y*ys;jxyT=Mg~(ZUfHS!4HCm!M4aH5PLr`Q3`3~{?$f=|2ypQQMW>%)fSdmz}7uayo-+fIi^$j5{ZsBm~f z0$ve4LE40f0~xBs;SPoQCdnoi|$GU zuC$Ohhids;PMzQZ<$=m|d^WGs zh_81!p&9nZ6ov8!=oQ(WUU!)4`$YaPzt!br7o&Gi2)U!3sI9Hdt~JtbK*`AKE*>_t zlS(o?`sxBW3icEFCbc^@|*uDcyGWG;* zR60v;!aRAPE*}O(YlT~IWDb3c;NmbeIcy-s z!OywKm|svZ{ltOa_HsQjQcQoSz1y zP6XeS$eQ*bu0smdS&~kuilI|XgD9WfyhB4?0sV6g(6fSC!C%j-K_=16ZBr!IG)@Qste_Nf?SQm(_aJ2j>}(L z1_@Hbk$n^w`fCUX)9Z;9M(4;8AM%8gJS%&yi2hG}PJF;M&OqN8tSpbd)MPT7EVEi z5#ZhAZoS!J%$L44dP{}hGbG{dcC)F-Y7iTb-Gz~jt}gP53J9Ue)|%Hb)*mEND!Gx^ zgMC~WXtcuNgtRM)M%++$Wfc}4ZEt>P=gYy%#o*Q(O%Wo&z zmk>8xsO3ibz4*Wc{TIrE={-?a<#5ZN$c5XC!{;DxNzi^_xJq&N@H&Ubk>I{OK{<}2 zgJ3m@ZX8@aR?*?os$g~4ouI`Fs=SIDhxz@UJby6ZC|p3^B^&0lYF}mImj|om_l|^VHs3TmJsFS62L8tt_LTJ5+GQs2L3gOffO7zT#Aih`D_65+u zB%bzRo;t7cPMAW*C2Di1!|#{hS>PWBtp-#m38t{r?W>aCTO1mPCF+2>Go0x5VSXir zkl&aGhcD@Lk9G@+>E1Ip7zGY1?^0}mkSo=}1Ya>&@GI7DX`QFSAtYioxk0aj#ZrA( zf)?|tD-}z7r~n&Gt-~b<+^FJ2_xl5iF^_fn{3>vjAwr^sb-qKnR>^N4d__14ZkqDR zq3)1k$(9P0{v`bwRu{*3>Ks0=Bbcn^;h1&#m={6|=uNZ$MtGd#r2PySyZo7yV^ap*yZq+Z zf+=&jxuJ>JEgn%!f9w&mxzfj~6Fyg=B-+?C039hnkJs(nBLa%UKIXQ;J_$%p9n_P! zVFk-d6YnHrDMR8=PO|&ru}wY;EW8O5mhYe;F%bN0lFb-52o&+${%l1-A{$vnB5Z3!^%qBd7hoC3`M2A12cHk*@&2Z1yGcnYNR@CjGp$W?In+*>Y2Mf{qLYc%do?!_q30POCiE z_Slpvx<`hBem0g?tQ%IFcs5}#S~}NwE}l1dd~#0V@o}|qhBBJ5WSkDTT2w%Fj6-h# zYbQh`FNdcR;Z@FXkP&=KI7=WH3Kuv+Rcyd1(_uIpJ`qBY^SHhJHRSW7tHI-=JcRa* zH-I6u+s@g+R@z(VFzL!k7Ah*>1tOnz*%>m@>?b%p+ZGaDrH{A#X7VKBIE7$om8+I3 z_f!W*IFRfoG%Xa>FDRWn{7S7q>BqY*%grqHBnqcX$F-4sl13Gjn!3 zW#!TC&|lFpko^+B83QUC5^*CeuOu6+d{7m;X>`|MeMq|N?)f1ya=#x1%>BzzrCwrJUGO z;PVDuQVTxk2Gf~jF%;R2_B@N$Zp9|ZH61WQ)I#Ypn1Y@Sc2%rjl-n!$w}H9~21 zc}NHsK}Lc&g2)e8!2HCq7Egsr+|F<{4w#%0$O(B|%&)1~Mc1g}3|$ij-efh|j207| zWr8_m;%Phy*$@>s#~znARqSGonBj92eVW?y>BG^>@CJ%rHqtdCUKg8{`Bz1oSDcGI z&Rj17If~1%-jz<|oD;B(@+M$A0#WMvYwWy9g52VB;$6}OdYAMFK0T1*V)5<7q!R@I zo*EAyC<$_~$P6U|EJ8$B8F6=+qr7yY^eTL97(Oq!MQ{xDV7e2lU2VaqNvJ~bXPDB}2- zV&lUbt-K|(fCyEQQ4t5oEE^!3!A98w6B71k4n#&p93V?ft$27gj6EOI;b`X>(MY6(NYluVpyxm$Sq6NhwhqxsS98|OX_r+V^7 zl4p@9B$?O#-i;>&+VZH@Yp(|r3k4FkIp1I~n@qV{Yhj^*`+e1DFVqXU%@e~yk+0PMF3?rS$`G1jh#x|Q|!d(r!>&fwsCWUS65^ zUP5)yyAsloEl8>ZrV9-x$s!X8(OHEkswVe3s|ukfCQ2&#-O!k_7hpw4UrIc*T4;+1V`F9MiHUj^s@l5h(n<`nnfB&dMTx$H*r@9*WyS7DIL7nE4LK zTPOv`cu6Si)q#V@J}x6eB4dH{?chzR;x4O)&ou>EMQe2I)U!oY;4V zC*=8Eb)?O5{EJPc7TJH`EHb_tIZCL;Ki|)k{>WSbbSXKHpe6PIdDIhP_5_R`zKQ;a zY)?p5=EGNELy(LM^gY$Xk-?X+bKFZhfsMxJBB_RmkpR(NaC{Pc+EJwdPwrq?+IYy_ zbS|VG6aG&pJK%N>;5a(#uoOk7Q`{hp(zF+os{xm(9oKB21Qw;9 zJrNI`zQl{e=O_FEYd_5?^?@TJ6G1V_p$-limJWi_LAi7Q6L9>7K$bQZ*Pgb6s8y%= z`vN7+A%smzH5`UG$u zQgQ)XL{u757@M2-q*f&HxFGyyKg`qxjvNdO`Q-3`{K64jL2l`gyb-x03&>P3w-tId z+145z#OUyeJel&#CVOmhI)bW&380i48`Y&F*+yiAHwVK{F)}imEqbdN%$m><59f#| zr<34W%3Y{~5>hDO%&lON0|93V@*L7sCB$!pO>yjcc~dBsoaCr!HZ77fAZQtya-p}}tKnB>_oduroNOB!Y^ z>dQqIuW7h%71y|Uc4YdhG&aY$p^Wa1R1C#`fJ*j+XymgU<-?iPuqR}tIzEc5n7VEK zOfGWkx`tct;UY7pHO^QPS-O7PnyHb+^9QQ1CLm#^ftY|-WQ^O~YK)t=n0T{QN&zKG z?n=mn%NB{{WH@&d&dnQ4(&$8KIepC3Om=Ltn#~rw#Q!@KT54EvPvr3>;Kef+MV4>C zzjv*PteD0{X5AB6HVr>-coO8ufoX4Bza|ZGR0T#s3YSr%rS+)MiV#9bkl09)2{0LI zBw{rgpD4A-aa4vHl!-|=a z`t{fxvs;H6iHaBcWU7~yl-)9uHXtk`Nv5)go>@}ahrIz^6$+gh-~}~7iyCxVlfh`Q zX?d#^|MCW-!D`lP_3cq#Pc@9tlC_4u-e@z~3?`;-wBd){1~#Km*z<(?c06s#{rDDueTT3we@j8w>IZbBhWSZsr};43}I* z2sbSG?Auyz_Kh~)mU{NBW;jhr;&`(8PtRs!)-#X|#cN`%eDY-?nruXwUxE2pxIB%a zPA$eKTWaVIi@cA;LK9?QyUju1D~U5Pfaz z87~QpKIF%AOt$dSYPL1Vj?5T&L~bM^zXGxD^TyiJ++iH`kwDg}azFe!IPR;d3plRf zt|i-^SWi@sjknix#>l#vX{bAtW6OpuiGyMa3R4T8MIEoAZnD@pk}-}r57?x-lJ@VxNRlU} z;oF|vp^9WuP1A|71G{XPvkj43A4_8cX2TaXuC{OoHed>5_CH=_bG(d@jPRl|#Vb}6 zgk-T;n7es~j?zN8>61}%DQ{o*1^(*RM>*b{Z9F3lN}?&n_L`8CCLV-fx7LniuCv=rnKGfJFqWs`M}&=m>;lgN)`Q)`d%qo~LuD?hb)iOiWohHSQcnM_xR2KToh zOGDH;#pFx;Y_@!fpbeYW&>rPWDpa+Se97C9HXDuol?RltgOnQ;E zB$F@^K9Z3zA)o9adO9OvBE@uEbRK-jC5RIZlY@| z+A_wMX!O5o-T)a9Q$|D$58RLZ4P3*lRgwBf(!jf_$eBi^B@>#^)}tomk~5_A3CJjH zB!x}8ku#Ga@(!TdX_$UL0)e^6JyS`j{*skxke(`{rcr6oq%NND(^M-k!DzACt;F9o zg{awH4Vq${Q)`Nv$wI02Ls8QhXVAo&X}+k5gjrfsdlWUPPfja|nkL?4Ly5C?D{2xk zP-f6HsO+C)wCa&EH)GHwiJhCI$Wjd?w?NcP#?(5%Zf#D_2&0m$sWfLARQ5TN(^1Bp zNolD)$eJCPiX!)2ADOj;i#)mr*>BPSyz0oBaW+kr-E0)pl(A`&ovB&W)NW+Wj!btg z%=)H|pc!Y=RM{y<7Ih2coZ=!Wb__{2kfce>NCXp&LFF<6i1d{F-_;DfgdVIT z`nD#I&m|6;GNA!_c2!k@CM&A!u|x?*B;vHwyj@^Yv1MYgneAej-TtKsM82UUbppVf z6QoWirSPI54dV5*tXRtJoB%I{SGXdwxC6(^BbcFKZM_)Jz(pRPk4%BB1By~dEE}Vi zVdhZ|oYZ^;i~%0y=Ju$7p~6yYDPUyG1o2NKE+Z}pAk5pyj|P@TqIQi&J>flVmje)~ zbQQc*Z8sutPZN0u`L;xRE^jp&jONUqOR9Vk_ST+@cA26kI7a>00@I*=t3_*RkJL|v zyH=upquzqlha#3h;W-=4NvOyHisXnizaOQ3cHD;4ue9vI(4#7n0-OeIM?W7GyyB7x z(T^87ANh6>EF^RDNiTz)k^E0=F#$SP(Vl|gSS~O?zB*}QaDeIqC0TH`^chck%iarjkBkwft{6O$Ap=*GfE*9 zrUGTD?7X7jvlb)pWL&1ucFSi|L2hcVju9wdlcfRIFqv`LAr(^b!=%0rDY z+P~Z(NbL{#GEv!tXSC^UVtEmh(P|d8^s!}v0-08l_?Vzs9vb&OE|ZRvbU3*?L|Sl` zR`E$Mk4>RTpIao7qrxL%pU-0xEfNhE2NVZ>y5WZT+_pz&q@l5;!@0Lf97zcbIZ>%> z8I$u~ixElJ;uN+>?rk#KBpK0ag6&XM=4-Pe{{Z9W>GIwVQXNfH)wtYyT^$YS38bA- z^hZMdH0(k9Np-Z;9z*7+?U4BlKyqxp&w$Zn)LPr4&j1x=TFGa?&Kr0Oh6&rL$qHb` z+=1jZ4h)cpvE=BED;~F<%Ya#F%7SfJRZwb#ZzZwy;_J|Eq^c< zw;E9y^eAUQ<*pcn=M_YSB0fyvhCM8qsMp#UJ~Y1C5fn6>r`Q*wHo35L5R?werGsGD zlkfM?yBt1el^+#O!({wG6#mq1b*zeNsyN7mCC{x^X}_0WPLR=E;r=XMPm55M<*ZU` zU^=k-bF3s7DF@bYe_FoJM2pgtKGTYZl}~d?IK*n@W>&!-Efyl1afY~XT|lUCIEDV2 zlAsV4d@j^`({KTI*jZKN@HQ_~aAaA1Wcn&jV$`=SK#jOG7`2j?nCz3bBM(6yaVFakHc5l zUnBUmqe>HTQ)Gh)Psqb=OJNI(+}eX&7`4GOxeyA(=F5d9y4i)EnHAitb>~dhwP-=)^6Y$o~+;Y z*u1@83F$Fx3jms&VXN&M2MlDjKnf)wFqwpyu^bK!g<*t~+2Aq~qK4=>o23>_lHwtO z=CU;LFE?F38yt zqo^eEq0AnikqoCV+6*F!#2|JvT)b&TE)k&oOI%8dRFZY?KiX_c`%! z+oNkDcdXJeKHY-MqKOU3X-bDm?1A9o@Ot?Bu@=*HE8P!W`cqrTFn8k4F3LZUhnjC9Gb7tQuWy$E5Ot+)B>@OQ zVU%GL_dzRSZ>gt~BHxn5NSv*yjffq#o$Dk~+sLt+%0A^LlZj+3rLL2R)8ONY-;6+H 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 diff --git a/sam/docs/brochure/v6/slides/brochure-dashboard-1page.html b/sam/docs/brochure/v6/slides/brochure-dashboard-1page.html deleted file mode 100644 index 20ee484..0000000 --- a/sam/docs/brochure/v6/slides/brochure-dashboard-1page.html +++ /dev/null @@ -1,374 +0,0 @@ - - - - - - - - -
- -
-
-

CEO DASHBOARD

-
-
- - -
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

보고 대기 없이, 로그인 한 번이면

-

전사 현황이 한눈에 들어옵니다.

-
-
- - - - - - - - - - - - - - - - - - - -
-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
-
- - - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
-
- - - - -

127건

-

▲ 8건

-

수주 잔량

-
-
- - - - 96 - -

96%

-

목표 달성

-

납기 준수율

-
-
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - - - - - -
-
- - - - - - - -
-
-
-

영업1팀 38%

-
-
-
-

영업2팀 25%

-
-
-
-

생산팀 22%

-
-
-
-

품질팀 15%

-
-
-
-
-
- - -
-
-

대표님이 얻는 것

-
-
-
- - - - - - -

즉시 현황 파악

-

로그인 3초면
전사 현황 확인

-
-
- - - - - - - - - - -

데이터로 판단

-

감이 아닌 숫자로
KPI/팀 성과 비교

-
-
- - - - - - -

모바일 승인

-

이동중에도 즉시
결재/승인 처리

-
-
-
- - -
- - -
-
-

대시보드 핵심 기능

-
-
-
-
- - - - - -

실시간 매출/수주 KPI

-
-
- - - - - - - - - - - -

조직 계층별 실적 트리

-
-
-
-
- - - - -

역할별 수당 현황

-
-
- - - - 5 - - -

미승인 실시간 알림

-
-
-
-
- - - - -

기간별 트렌드 분석

-
-
- - - - - - - - -

수익 시뮬레이터

-
-
-
-
- - -
- - -
-
-
- - - - - -

BEFORE

-
-

매출? → 보고 대기 1~2일

-

수주? → Excel 취합 반나절

-

승인? → 서류 찾기 30분

-

실적? → 각 팀장 개별 보고

-
-
- - - - -
-
-
- - - - -

AFTER (SAM)

-
-

로그인 → 3초 전사 현황

-

클릭 → 실시간 수주 데이터

-

뱃지 → 즉시 승인 처리

-

트리 → 전 조직 한눈에

-
-
- - -
-
- - - - -

실시간 업데이트

-
-
- - - - -

PC + 모바일

-
-
- - - - - -

역할별 권한

-
-
- - - - - -

데이터 암호화

-
-
- - - - - -

클라우드

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

무료 데모 신청

-

contact@codebridge-x.com

-
-
-
- -
- - \ No newline at end of file diff --git a/sam/docs/brochure/v6/slides/brochure-dashboard-back.html b/sam/docs/brochure/v6/slides/brochure-dashboard-back.html deleted file mode 100644 index 792d610..0000000 --- a/sam/docs/brochure/v6/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,337 +0,0 @@ - - - - - - - - -
- -
-
-

FEATURES & PRICING

-
-
- - -
- - -
-
-

대시보드 핵심 기능

-
-
- -
- - - - - -
-

실시간 KPI 카드

-
-

매출, 수주, 납기율, 승인 대기

-
- -
- - - - - - - - - - - - - - - -
-

조직 실적 트리

-
-

계층별 팀/개인 실적 펼쳐보기

-
- -
- - - - -
-

역할별 수당 현황

-
-

판매자/관리자/협업자 배분 확인

-
- -
- - - - ! - - -
-

승인 대기 알림

-
-

가입/지급 미처리 빨간 뱃지

-
- -
- - - - -
-

기간별 트렌드

-
-

당월/분기/연간 추이 차트

-
- -
- - - - - - - - -
-

수익 시뮬레이터

-
-

가상 시나리오 수당/마진 계산

-
- -
- - - - - - - - -
-

모바일 대응

-
-

스마트폰으로 KPI 확인/승인

-
-
-
- - -
- - -
-
-

역할별 맞춤 화면

-
-
- -
- - - - - -

CEO

-

전사 KPI 총괄

-
- -
- - - - - - -

관리자

-

팀 실적 관리

-
- -
- - - - - - - - -

운영자

-

인력/승인 관리

-
- -
- - - - - - - -

영업자

-

내 실적 조회

-
-
-
- - -
- - -
-
-

투자 비용

-
-
- -
-
-
- - - - -

대시보드 포함 기본 패키지

-
-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

CEO 대시보드 + 견적/수주 + 생산

-

인사/회계 무료 포함

-
-
- -
-
-
- - - - - -

추가 옵션 (선택)

-
-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-
-

도입 프로세스

-
-
-
- - - - - -

1~2주

-

현장 인터뷰

-
- - - -
- - - - - - - -

2~4주

-

맞춤 개발

-
- - - -
- - - - - -

1~2주

-

데이터 이관

-
- - - -
- - - - -

1~2주

-

교육/안정화

-
-
-
- - -
-
-
- - - - -
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- -
- - \ No newline at end of file diff --git a/sam/docs/brochure/v6/slides/brochure-dashboard-front.html b/sam/docs/brochure/v6/slides/brochure-dashboard-front.html deleted file mode 100644 index d25994a..0000000 --- a/sam/docs/brochure/v6/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - - -
- -
-
-

CEO DASHBOARD

-
-
- - -
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

매출, 수주, 조직 실적, 승인 대기

-

더 이상 보고를 기다리지 마세요.

-
- -
- - - - - - - - - - - - - 5 - - - - -
-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard ― 로그인 후 3초

-
- -
-
- - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
-
- - - - -

127건

-

▲ 8건

-

누적 수주

-
-
- - - - -

96%

-

목표 달성

-

납기 준수율

-
-
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - - - -
-
- - - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-

품질팀

-
-
-
-
-
- - -
-
-

대표님이 얻는 것

-
-
- -
- - - - - - -

즉시 현황 파악

-

로그인 3초면
전사 현황 확인

-
- -
- - - - - - - - - - -

데이터로 판단

-

감이 아닌 숫자로
KPI/팀 성과 비교

-
- -
- - - - - - -

모바일 승인

-

이동중에도 즉시
결재/승인 처리

-
-
-
- - -
-
- -

클라우드 기반

-
-
- -

PC + 모바일

-
-
- -

역할별 권한

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능을 확인하세요 ▶

-
-
-
- -
- - \ No newline at end of file diff --git a/sam/docs/brochure/v7/convert-1page.cjs b/sam/docs/brochure/v7/convert-1page.cjs deleted file mode 100644 index 723bfce..0000000 --- a/sam/docs/brochure/v7/convert-1page.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard v7 (Warm Gray + Teal) 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-v7-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v7/convert-2page.cjs b/sam/docs/brochure/v7/convert-2page.cjs deleted file mode 100644 index bf9d100..0000000 --- a/sam/docs/brochure/v7/convert-2page.cjs +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-v7-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v7/sam-brochure-v7-dashboard-1page.pptx b/sam/docs/brochure/v7/sam-brochure-v7-dashboard-1page.pptx deleted file mode 100644 index 6f12fd7682f75f48865c5243687dff8773bfb0d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165194 zcmeD^2V4`^|EQ?A!CmKxqb?wOpeS3Zs3B4OIp?<4S z&1K4PSww_f6_r=thQTUj5pqc$A7vPWP9cww1xpguv3U-;7BvP&^A8Q2@s7?ConEHR zYv_!37@B|R9R{G|f#uaZV=%f8PNo5l`e>O_mJc|p)OwlDFlg(G((sLp<{$b-XA1cE zp-?nod3|V23#~1^VQKIJ@ghJH^s5HXeD9}L>18VYz_3J(OxNE%L8)ll9{wTXVA|wr zm6t2qo#jf%R8losgkG*a!UEkyu7pl6QAJ1;YL(2(HBqK>ZQrg^`L?(Z0-jdsyj-L8 zdX1-thfWHxl<3^m8kq|2M5?t)i5^b1Q63tJG)597^Wd;p0uL!U4mUj-hikjG{<26( ztU^x&B*5^?=@FebB z7cJN5T3LU?V2v!w>IsMdKDUquT3&h%B7^I1I!nXCuXnlWZOqjYGs4MNn=3}Duov$(J zJn$2RGma0908K3xZ)`m?#*RNUf;hP>zEDPB?D#_?Ao(-?_tPfrGzz@z400Ro zXUf3%(bH}kP$FbnyW5V$Vm)a}3eRwO#pg<=WD_C4wTnT(vnd3d$}4{hGCjKtPYBk|3NWiSmtPyGQA{1qL+YZ%UDbnhGbl< z*2l!n|Cl}uC}s=OfGmn7`V61h6JsV?nxXQ)jD;gp6H6jRw6_lFVlvogK!=qk!y7rGvoB0 zaWbi1ttE8wp&&F1T#0arPDYZ8IEhv+QRy*ZDc2S?B`>r?#|n*3uWc6y8l{ftp_4_l z^)OztzJcb9tJU5 zx*ldT(3X5#*GQzdSe_El;Y?G4fnk~u5;i_CO$yq84In4)`biY>aIM_xeF_)M0nF@# z1PuvLvRlM#5O~SugTKxETiW`ml^TgEv0aEppU^?3`YyDshv5>yB3Q19(e==Tsr@B- znPIsyFQNqvHa}Sea3n(u(*?Ak!9<}|ppN{aC8{V{grU{@H=s4(cu&&g(vB@~XTg6M z?+w?W1z@v*7MhXYE<;b>cnAUj`XP&#fi7XrgG7>#q`&Y|sm2pAC)2A*GVCeUuWuiZT@6cK*<$7!^ zqG&w5X@r!#X>6UKlN$#SA0O`?&&9(43l-a=cjw?xY|L_#tC0CiDkH_32&tzY;iYLQ z5p=rS*2CBUtxGu3#fA?Ctjnvtp$YW_kah_WLemqtQ^Qr%2K0vIqh|<4FRN0={Q2uAhHcpldr0iz9Gjaot$# zFgDAREA-^C`l10cp2sBJa7{@K^P@X8bbxXDy)|lv9u2=e77+y&fAnhz=wUMJ)3qLi zC&f>0E_|U`hd4fIWibhYw2PFngKR;nbzsz@OM?bkz5rsgB@vA&J>wCW4TxcPu`&po z%E?&iG=~lCqRxYMk|%6{Q<8$q$4Qz@*q~*LC^A^J4cd+I64pSiz=m(jK^ToSra>4l z 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 V5yv`I5Gn{d;BIk00l4i${2ww0(3SuI diff --git a/sam/docs/brochure/v7/sam-brochure-v7-dashboard-2page.pptx b/sam/docs/brochure/v7/sam-brochure-v7-dashboard-2page.pptx deleted file mode 100644 index 20a6bd175e420d5d08a40147b2b44ba61a39c5ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253041 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 - - - - - - - -

- -
-

CEO DASHBOARD v7

-
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

보고 대기 없이, 로그인 한 번이면
전사 현황이 한눈에 들어옵니다.

-
- -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
- -
- - - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
- -
- - - - -

127건

-

▲ 8건

-

수주 잔량

-
- -
- - - - 96 - -

96%

-

목표 달성

-

납기 준수율

-
- -
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
- -
-

월별 매출 추이

- - - - - - - - - -
- -
- - - - - - - -
-
-
-

영업1팀 38%

-
-
-
-

영업2팀 25%

-
-
-
-

생산팀 22%

-
-
-
-

품질팀 15%

-
-
-
-
-
- - -
-

대표님이 얻는 것

-
-
- - - - - - -

즉시 현황 파악

-

로그인 3초면
전사 현황 확인

-
-
- - - - - - - - - - -

데이터로 판단

-

감이 아닌 숫자로
KPI/팀 성과 비교

-
-
- - - - - - -

모바일 승인

-

이동중에도 즉시
결재/승인 처리

-
-
-
- - -
- - -
-

대시보드 핵심 기능

-
-
-
- - - - - -

실시간 매출/수주 KPI

-
-
- - - - - - - - - - - -

조직 계층별 실적 트리

-
-
-
-
- - - - -

역할별 수당 현황

-
-
- - - - 5 - - -

미승인 실시간 알림

-
-
-
-
- - - - -

기간별 트렌드 분석

-
-
- - - - - - - - -

수익 시뮬레이터

-
-
-
-
- - -
- - -
- -
-
- - - - - -

BEFORE

-
-

매출? 보고 대기 1~2일

-

수주? Excel 취합 반나절

-

승인? 서류 찾기 30분

-

실적? 각 팀장 개별 보고

-
- -
- - - - -
- -
-
- - - - -

AFTER (SAM)

-
-

로그인 3초 전사 현황

-

클릭 실시간 수주 데이터

-

뱃지 즉시 승인 처리

-

트리 전 조직 한눈에

-
-
- - -
-
- - - - -

실시간 업데이트

-
-
- - - - -

PC + 모바일

-
-
- - - - - -

역할별 권한

-
-
- - - - - -

데이터 암호화

-
-
- - - - - -

클라우드

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

무료 데모 신청

-

contact@codebridge-x.com

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v7/slides/brochure-dashboard-back.html b/sam/docs/brochure/v7/slides/brochure-dashboard-back.html deleted file mode 100644 index 7298655..0000000 --- a/sam/docs/brochure/v7/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - - - -
- -
-

FEATURES & PRICING

-
- - -
-

대시보드 핵심 기능

-
- -
- - - - - -
-

실시간 KPI 카드

-
-

매출, 수주, 납기율, 승인 대기

-
- -
- - - - - - - - - - - - - - - -
-

조직 실적 트리

-
-

계층별 팀/개인 실적 펼쳐보기

-
- -
- - - - -
-

역할별 수당 현황

-
-

판매자/관리자/협업자 배분 확인

-
- -
- - - - ! - - -
-

승인 대기 알림

-
-

가입/지급 미처리 빨간 뱃지

-
- -
- - - - -
-

기간별 트렌드

-
-

당월/분기/연간 추이 차트

-
- -
- - - - - - - - -
-

수익 시뮬레이터

-
-

가상 시나리오 수당/마진 계산

-
- -
- - - - - - - - -
-

모바일 대응

-
-

스마트폰으로 KPI 확인/승인

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
- -
- - - - - -

CEO

-

전사 KPI 총괄

-
- -
- - - - - - -

관리자

-

팀 실적 관리

-
- -
- - - - - - - - -

운영자

-

인력/승인 관리

-
- -
- - - - - - - -

영업자

-

내 실적 조회

-
-
-
- - -
- - -
-

대시보드 + SAM ERP/MES 통합

-
-
- - - - - - -

견적/수주

-
-
- - - - - - -

생산 MES

-
-
- - - - - -

품질/검사

-
-
- - - - - -

재고/자재

-
-
- - - - -

인사/회계

-
-
-

대시보드의 모든 데이터는 SAM ERP/MES 실시간 데이터 기반

-
- - -
- - -
-

투자 비용

-
- -
-
-
- - - - -

대시보드 포함 기본 패키지

-
-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

CEO 대시보드 + 견적/수주 + 생산
인사/회계 무료 포함

-
-
- -
-
-
- - - - - -

추가 옵션 (선택)

-
-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
- - - - - -

1~2주

-

현장 인터뷰

-
- - - -
- - - - - - - -

2~4주

-

맞춤 개발

-
- - - -
- - - - - -

1~2주

-

데이터 이관

-
- - - -
- - - - -

1~2주

-

교육/안정화

-
-
-
- - -
-
-
- - - - -
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v7/slides/brochure-dashboard-front.html b/sam/docs/brochure/v7/slides/brochure-dashboard-front.html deleted file mode 100644 index 1a4ccdd..0000000 --- a/sam/docs/brochure/v7/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - - - -
- -
-

CEO DASHBOARD v7

-
- - -
-
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사
지금 어떤 상태인가요?

-

매출, 수주, 조직 실적, 승인 대기
더 이상 보고를 기다리지 마세요.

-
- -
- - - - - - - - - - - - - - - - - - 5 - - - - - -
-
- - -
- - -
-

대표님의 하루

-
- -
- - - - - 9AM - -
-

"어제 매출 얼마야?" 팀장 보고 대기중...

-
-
- -
- - - - - 2PM - -
-

"수주 밀린 거 없어?" Excel 취합중...

-
-
- -
- - - - - 5PM - -
-

"결재할 것 정리해줘" 서류 찾는중...

-
-
-
-
- - -
- - - -

SAM 도입 후

-
- - -
- -
-
-
-
-

SAM CEO Dashboard --- 로그인 후 3초

-
- -
-
- - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
-
- - - - -

127건

-

▲ 8건

-

누적 수주

-
-
- - - - -

96%

-

목표 달성

-

납기 준수율

-
-
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - - - -
-
- - - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-

품질팀

-
-
-
-
-
- - -
-

대표님이 얻는 것

-
- -
- - - - - - -

즉시 현황 파악

-

로그인 3초면
전사 현황 확인

-
- -
- - - - - - - - - - -

데이터로 판단

-

감이 아닌 숫자로
KPI/팀 성과 비교

-
- -
- - - - - - -

모바일 승인

-

이동중에도 즉시
결재/승인 처리

-
-
-
- - -
-
- -

클라우드 기반

-
-
- -

PC + 모바일

-
-
- -

역할별 권한

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능을 확인하세요 ▶

-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v8/convert-1page.cjs b/sam/docs/brochure/v8/convert-1page.cjs deleted file mode 100644 index aa5af35..0000000 --- a/sam/docs/brochure/v8/convert-1page.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard v8 (Two-Tone Split) 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-v8-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v8/convert-2page.cjs b/sam/docs/brochure/v8/convert-2page.cjs deleted file mode 100644 index 1b887e1..0000000 --- a/sam/docs/brochure/v8/convert-2page.cjs +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-v8-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v8/sam-brochure-v8-dashboard-1page.pptx b/sam/docs/brochure/v8/sam-brochure-v8-dashboard-1page.pptx deleted file mode 100644 index 4a2c33d0521b53039bfb34b78c3610b6ee48d6cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145146 zcmeEP2V7HE7Y9W}>%i5zjj?Wl?16$Z6-7nCZPkzjh-8_B$Yl@;Hb4~UEjI)y_dWf6CfC@BkeEp@^asM_nmvrx##@Px#vc=FYZ;w1N|2g zE9-*~$bU-0-+l(A+UOtcuuW-bqtMBt4LW^Z&AgzQUT@0dMyLy@MSc(bgls2Ilo?EU zJ+dORVSe|mKsHxu;*<)7N}G_^y{XP>lnRwBkC&qQU{tFV$|za7E;-LGe@6C!(LBG7 zGoH~wW;7`cd3Bxf461p4&!C8oJC@ho8J#iaJD3z5O^Hg4vH*0{>P$)_)oI5}NxdVZ zd4BKcLlPff6oUpVuNVE)PHTHlSQfQF`ZWj%`c#H%dWY(?CZ*QYFE(ATG=AZirct+S z1^=*8VAxbTZ6KTL$6>RSTDcA}VIaF}Y=p0b%`%!~T7^ul(<%em=}IHJRZzJyE%7~A zaJAML$WAnw^a1|e;$V;@|P3+@HL@+ zu!CBLDdT0yY7;9w4Z2741TJT@$o`^71+ry&y;>!gnV_To=vRNc*CCaW$Au|cg-!2# z$)5SamH6hyM3vsy%<&Tj=#>dJPf%<KlE)_iBLN(Ip8E-ouz;{|Y#tN8j{X`&Xg! z_Is9~inseR=)C^Ee}YkOP|6g>M5WTCQTyS)=uw?Olng))Ry52I9c3Ee zTYogOKx-p(3^N##yEl3^{bulR`d2@VOr@n5#8GD$s)_c6>*}7-?wwzsuy&`kEA5NR z`$W5Uf%;_h$5#|hKl1AV8d_}LxO!wPH_opIDJo@Zk@UcFn-hVqFp%=H8i@JziEf`*k6qp)>#fNSvFs{V zi|sGh8I;bC6gBaqt9|w0L?{g=m6DqJ^LZr97S2D>W|mSSb4Q_6y#DZ({@A<e?l=^SUY6bEFt>jPai#_4Hr%}UH!Ln2@enSL4J!ZtqaeJXr3Wt zu;6^id^>_s?6L+JU0P#63NT0ExQ}|xZdfuAledi3nPlo%l|~uF0fDuAfnU1o3<@JU zOB}$NV5|2M)IlXT=#08}6RQ(8TUpV%RHY$Wr_!2Q`qLZ2bgaDB1L9U$+RY{SW34<HSLuG1JoI&OAcTyH? z3Cz3+K^lUf#BI@JgUCy;4}Le>cWD`_)97W|^q^?HDXooC+b*W1KXnRd5v9^58N2FZ zbzw4-l44ipNyN~A`6(4JBT+4^ClEseL}5^)8-*syvKwEHX{L0cjr#p*&4I`F!2m!Xgm_VU3C}#pBq^l+g@?-Z0cdivKAKR&0djpo zlqO?#bUYstdRQU1+)|%xP-BQlA@^6RLApSa{keWze>VAeg&bf-jTL30Ux0t1wbZCg z7%U=nJiVzxPW)(Ton};7I+2>1>X*vL-2jA&>))e8R15~Qd{tTmf60}ES)-5#n2=mr zS&2p0yDj}K4G>?#7h`hV0Fb)8+EY!)D?qi&KoMH6z&EANqDC-IYQS1M07@6HQYu0i zjQ#qL-q$Ki>5IecFZ zH5zY7n%3?JHX(vgrgJ?mxbpUG7twG9`cfxG8XQDB!XWRpu4l!^S2c2-K zos6AMTiVc08vSWs@}v#Sl!W0@I7^dB8^pFqBLk{Uc{i3**aLMy8=)&ZVF7Ebov@tB zs}o?xxsDPTyRmivfAz=z<~bT-m-fMm-_UwuJxLKGPYLgTppnr1%Z@n)P(nh5M(w>i?M6EO$Koq^5)^>ER9ac)doCKDj8Sc zwu&S!YGF(_hN=y$6q!1Z4T!u#ml_KW8k>ctF!&*mjqD!|2chTwvH~%>HUd_2({9X)`&P1zSdqC zA`J;+-XAw2+KXOmFXjY?3B-*4;6{na=6$U-M=0WlQg3T%60cLYwcA`O6hw%5xbQW-5%c9+Wh9M z^|!Lu-<&h~_Ntk7?jCHA#nGE1aKp&zs7z&b(P`iX?et$6XK?O{*hHC%enl{(JIJ&$ z^b|Yy4>u(;T6avBsbzNBg(#VH>}CK}W3OSGbY_c}N{=*H)^mbijvJ`vI4wY#cqa2>B2i!qr@0Ov7N=7%M)c}=1I9jVT zvCwb2B&E{M+x1kb7<9E_Qax3y5Shv@Nn%xT3~sVs)D}YEYFWCy4#IddXw#t^D@3QZ z_ts&`6lP114H*vf6T)v1wpgW_LB=++WRpx|@0PJL6l=Bef0WFWXs?qo=>|EyT{wVB z$d6a+SmBV2VYGKmCj-1M?cVJH;IKdJ9n|R>W=n`PB-y)XluW0iw+z!I0iXcrg`FXc zQE3zDZ6l3Iz@joh6HWG-chE5<1bv5bkZBzbcQ=)iG2HoUgRY=q>0Poi2>K=2VCOB` zD0K|}jY(I>%arz1O|U`3P_YH@+$hSg&02z8#k}($tabP!Ll*R1f7HabV#>RJn1s6MrLqy=Z-Mybee|) z$fmK!0G-tGBznV+DoD=(ez8lGPDZ!~jkz7(H9Aqon846?qkVo&H!#MpATkDWIe^iP z0>~v?qjdTjQ0(+;@}x~HK-=`jR)&d{saR3=)Gb`X7=G&|XmZ7uCEL9&o+-4}VF2rh zVf1KJ+I%e_8{0?tc5ELzBIg>b$Nr5OD$7ZG$B{~DYh&d&YG-TPghCuh_3oQCVX|ax zv=U~M*Y%d$vT<8B><`|uaU{=fd&ZH~;kIYkU}w1P84_Y~jXh&EVZgdUum=J6VDLxd z5c`Q&sns}JFUpAFA0v1*iU>G~uEV&e(t-u*6X6g|bMOF0JIIm1*RTvaQ+Jgq5u&>& z>70v^%mgEO>jWc9uY*V=2cNckZwx>sYalpBK%QJKg>J$VWhiJzp63Wj#12qZ$owGA zio{kJ&vJX*+XN%JGt~iftuvjVJ&f52zMW2psgsCEC<=<`XQvbR&0Nw8shwU((Cw+A zdaW1GHOr`g+sy)JJ&-LFz$I`I00m&8L(&;_JvF+RgsvQEN5oXqIRMOT;!WryEk%8G z_6${VWu-!{5nl)_BbPyzLp(soaH!D(?--HiB~vHBvKYC^KnQ$cG>hI~G=<5G zi3Gv1(jn|@NJmf_SZb9fkd0p7!VPLI)*-lWTplQdceZmUN)ul}!xPhj-CyzLg>Q_mg!OkyLrkI~l@+H>Hgu5ziwVVCL_meg2oVG>K~04U zLShz0X5~MdiHTIZ^cWE4A>zDDtxuGZGYVaq6V0(Iw{!=8v(SL{jeKVyD2*iKL>QNV zCRjRA8MCQn)*$-eaMsQLB1Ads^eDB?nGCIhW=;Fbl<@>=8nB^d;U;-nuP74R%<#k{ zZL4ynl&%9twL41y!7cQ&GwT*$>Zf(h`Y;B;P>)|%!$@b7XPI52UNE1o6~rK*X?+oO zTN}Dr<_NowgOi1t`QbWSu7y+alwFA_jbrEZ@U{+TZ;|U0jd&;_cx{k@K@@{+5<+c& zh@_cF(SRdXvH?=d26KeLf>53?)R!X>hWiTm0*?a3Ply7Q!0}6N@QP z!xa*7`v?eQsME>lJwE?h1YbjC5Bte=8h;|WhDd|J5qvJs-(duwr2?fhSj3Bv@}(gn zUnxI0!dDO$BJq`mib8zDM4{rah_FzhL>e)a&4M+0&{z=64;6^QCB7nVXsEA11gT%a z(7;zL;PHY5;$TU*Ab2Qg#=x5s>Vu>OkQWowCPiyX@XK4f%X)aA50(C4o19h1^aog% zst=&w>y7guh;&6z$u7v{p%NMkmQG1a{AQ+Ru#B=4WlVCMQE7?*9|_zMSXcoOFdfQi zg{zFwD!C~c4hTO*A7E4~h`gpq1?Ex48O#$ru0Sl1@I?Yx?P~}?1CN($pb3%D*m^nr zR*?$gCPGN}v0P2W^YPV|wzLE)`{%5e^D$591MFP_?-MhKB~OESk}Ck?4cQ>+fov&P zK-tvfb!02*2i%If1l~;_pdc0=qnM3lhF7Pkx%y}W3qmeYh=QaI2w_BM9V?5A8ALq_ z-qr+3Ezm?Mi_(S|lJH32>;v*E{DjhhQL1~gR*oilY)8V4y1)~Ra3?M(Xf`WJX+W#5 zLGk5lXPVNe#0tjLW}EZSOr!Nxn+QXbkE2)1+6Cq<2jLefWIr4N@c=8D`5gro{lg(Q%WMj#1Kg4F}z z1$@jJ2vLP3kjP3P9!~-cZ!gq6$eMiIqTzI$$)om2`?4 zQH3Osltv(tQ0!LHn1c{iutEH~)d*HH-RrP%czVpAy05M$tVe+lRip?t$7%g2W878V z4%n20zf$M=ek6mcyV*fCoGC`c;9v|OK$%|09gq5f>3<{AMVxaU6g!44AG?fMx7%RZdLQCL`u+UDa zh*H8*Z0w8)MO^G%S$~Vw>G4lefrv{WjLZk1`{3nE#PiTsd5l^?KHaF5!Mf5mP!R~b ziNx1V7*oP-J8n+4g@wv^jWppT!d+1e3n^q{8VfmxeDJQ}Hh{4aUY#-B&S~VX6W8ej zbOsf8o-A;ad0e;_JY+tX^2+7b?j|I|nlJ*!f~&2aCw+hT(b^V!Cc4!nO{=0*FHbzevHHV53Fail3R* z!4g{ZsYhxNJdTbkt`t(-(CSJIf)Mxp07Z_4XFfv;5iKX)7jY4}ASf(6BDiZ*EEc%9 z1jO)h30;)&fEWQ|0y7hchz0N_0tRsj1js=)#;izOY}Cg>=>v2dZAu%1J_gUpz_uMz zK-iI(0TxEAT|#_^e;}LTuUcqEa0TP?1nN$*fV6mn21y+Nt4MTc42eNr%xG{DF&Qa{ z@`#`7#S>45S!{e+j3B zfrZ+vj6=%=Nm!HC45TG4fi&npT-Aa!3!;-Y<#kQTqckO&&{+t1M%o7{QcU)a_!5iO z#J?i-Ngh!5Gm>s&LG&hty<_dk6Y>GOr@(CLa+ECrOF}d-tDvB`0YdlCF<4`nf@2S$ zKdnbpTWlmTQZwjbac($I$`5&+0l=q&L<_hf{(}fbytpYXM5jnc_l$$TFsr0O`5Z_G z8JG+OKrrMVsv}T;C<_7RC6yimcOou8m#U3KKmo`n3xE-Tf6)8{)xNawRxl$_(FOdR zPG+s!Y^DC0tpm(x~${$~68^TEyrtGeVrn=pUy- zr~1XI`lTj9-ijZpC<@Pjr&yQ9F{PHV4xcN+p*gED#N|Q*9&`b{Iu}CQVrreRBbnAEYZYiP=zr=ZmEhDbgP_9^~-& zVjfIrhzBJQm!y`qJ7Pf_?t`(i&stg?QaAWnPR$ltQ0P#qAQBbF1_f)P4h+;m9CZL8 zWZU@!2?q2izuCkR6)O0&g5}#_JE_X*pBDhv;<~BjJQ&WDwhK|9f_!t$It@l7d%lSn6~H;Rq%M} zHiFp2dPOXhNQjgSu9U}-5|bPq&Jv3xLec>TcvS$i*nevyygfV%t+jO`P?CqYj5CAX z-ZBAc3H^BHS(}`gozYspg2tO1!&oqX(d9@|WO9V0h)**pZPo@BSLnxY;DjJy0=_B6#I*!%ad<^qbaDiN7MPHkJ}Z|) zn~y{y(SPo;z8ljKz_=mGcw0c2OwdFow&i!{Bn~NF=jJU3A;=FP6l0!0la;f4nR)X9 zXGG2=QJjoV zCC*8bYaC2wP!#;lq7x*jbcSL-0L3t%8ml0=T}*05vbvCPWJzTB4_wR-7}Uwh%rGw+ z$ueioF)!WW#0-Y(2D~w_!wg_@U7kK9C5rSQOW2W@=?-Cnu4AOSeuQ2o58MHI2orz@ zC%jCtz?b(fdYM?9ipb04b2%bd8S&0~nXqbt9(&=sI?+kQm2xG-x{v}yG2ewur*{K3 zc6*rxS`PqO{Z!Q3l9(?QITLwtjf;tvmC(`!@p2+p<`~h$h{q8@4$x}$QH~U>_M%l0 zcd%CMo$)gzA;QoIY7^cMD|sMi)JmLOyKDq_kI~h%31|Xe7Tnd8KoxcVyXb0S_o|3oO-LDr>fG;cK$B+^ z&~$Nr60G=!3?u+35Q^m>plP*{-3ha}Zu#2?Xog1!;6E}qnWoe}fPkjZxgd;-d`)IJ z6C7ZdMq%i37DU4KV-QZzYQ(hB-V2}BX=%wG?_6?08m{jceEAOJTa^g&a-?8m&Zrp z&5vc-2ka5y7~!!~lLT#^L%O7et4V^lCv++ajuQdp!h)%%TI*$f05+qgBnes*ZJxC~ zXZ%dooeiU%kUvb$H_a*FTSB=+EiM=5^3df^B9%a34-Ft4zZb& zSVRq&pXsNy7AW@_0gk!2)m7UQLHM&yj5 z;s8MHB%;i35}bl>1l$@2vdG*_7=E&B{RqkbgsR0`KN7oq!nj+ve#B2^^c_D7JOb1u zT7d$%k41*b{T=ZtSxc{Ra)b4a&;t1nsMe+9wTFC5{2OoP421-XZXS|CVMM8I;EpZ_ z)DSr+@&Q{IE1`Gvb^i8Qn^BOifE5bEdEt@>ssjw!Kntkt>`iNnNn(!4y96cQ5+x;x z?c!_$E}+X$4go_zJbPLPgbNk_D~P^%|A}o0qLIWdy1Ey9K9qkG<)z^%#`<76+Y&Y* ziS6=`CF}@7tONV+7@OF97%3ozeqhW$WHD9JT%;RrE>t6y&;Vh&xH?7!1=NHDjLrjX z!WUxfl4iQVc3C1ZxG%VtEo?4nnOYricY*6uGxRo|kqcHM`Id&bKO0^IOdbfCB~U}S zwgBQ3ogjSl5E;~oC-W~!YP%Sh=Y#Jm1@qgX+WD9uGJy^F3+lMYSlf+}32e|`zN^t+ z2@k*;&{mFS)VIRg1p&z%uakBiNG7nrzZfKgjbUBP+ER$=k$S|9k|9gox`@J!lCfRB z-A>M=^$;-6%~%iaIDlkRb0#>mHo_$!86>gN*G7O_USuNWM#-!jBIdV+RY4tbH%bN} z3u+@Rm&@m!^;2;|OIAkCgo#imW0m=$JyrgJl3^7Hr-t zsEoS7vb^`dv$x*8eRQOM?!*x+Sg3nv;|^BNwv0QQroMH#V+N4;t`8td#iBd{;BK@` zouCb5yYtE3%HGM2(K3N0Rzu)7fakc>NQ7XGRS?{0nFXUKsqJEr9(EsrJuZprJ$6C* zw*n~?V1MDak-Y~S8!@0sz;lX>wcThL%8(%z;*+fHav)eN<)O{U-+8pmakU56p=D^> zXj<6=@bq2F+OR!2?3h3h+l`hL)N63c8*t~8G0OL>$XP$ryl@B0j2C%1;SRV2Dnt2` zba`?y)X%5MlN%~?LuK#R&hdbg7C_2#cu`x8^5^YsQ? zBX-On65s_cwKQq#F8Pp?DX^{=T2r_vkqySO-r3xe>F(4Y$@Vl421Nk9e-LkA5jx4#nNZbk8 zu>S6pM}o5v-OJt)g-t=Q>>Y}{z!u^;Ga!DSwaGkTH47>o=8XTw3B!lV?=%5uK@E=6 z>GVQor~+WEA`?N_>s!nb6LXkF1nC->2me{v%#jLqGOri5YQm3%g&6ji2Rl~Pc6{YaKFBQ4oFe{uA3KPyt|+ONmXhu>~9+2XQUat?3cT9mA-6};OTVx4U z5IcQ>Ls9U;xKJY1SMV+t#>MDU5f#Se3#5=9f`c!7SXvBAAV6M$z}C5l3yUI9z>Rc@ z(J{0@hAEzqP*W6ZYNOO?Sm+>-&0Nz@LU3dgtZaztIpEf!(S!kXoHBq)_R@ejSbphJ zZ6)Nx>k#D(_!1G9FTiDytnV%qi($bv`R*J6s?O~iMBwA8W&jqZb9-@r$khxcECWC9 z&iV`%&iKLa1=ty91p-Oo0xa05s-O z0ir=C0&wyqL|n}}4-0t$p%gSGhK=0N2~uCchshmvS|#aVAj=8TQt>~4U#ewVYhfDW zAOJJn&`E$LJ;}Uyy?Ht8F%Jj|Y&D-deyelap|}J(LDiFKU?gCZE|n-Qm&1d)Rv0=F zaJhVut&1n(abasNH*}(r849;S7t_y2g6BBg{{cfMcQ$62w{FVWm;tpSM)==ZzX0uY zLP(YqXe5)Eg#=B+OnhJD>HrzcQXC?>A%J?tVlFnDt&^jpq3=Ii$fp`(8QkB`ItZxw4X5@p#sFprA8B- zvxN}JhNR(-n6qu*K#{AlR~l&YpD}hpKoi$3i_IG}5nCWZ0yKfI1qMw79N36i^e%!X z7({*xpb0F4;zOascM3GIz>c^wAne=XQf);CO<=7F-&R|RI06ymM7u$gLc4fw(1fs|Byu8Q!ux>s zyL6^v*ws)`z*eT*$jMu+Ddt8_93UsKhL_=A0zO`Zav+rgQuFcpmnd(O?#u&>PB$sEL^1vRj}|t`S+~`^VGiIWP=0*{ zTKtyt{b=*@5l)B`laPf4OqkFD|r_I z6G+Gfws{MPi9`UkO;G8#cV~f))Pgx;{|>-)mr4*FF_8#i_fMPLXIRZCaOax15ffL{ z58k>Sd^^0T02b#gn)n}-Hj!E|WkR4_954aYnF}?Ni1j%VAtajE!~)&xbKVewWcv9Z zFJLlZn|c0B|D4Sk06zMgXCuL4nK#3h71?haG?6kCizFyISX?0!?h-KnOayl-P96<_X0js5R+W z!++?U%|&{ep+{T|zFJX}|BkZ@gqlDi9TzLPqD?aan2w@20h<8e-~?{M6L5W{?<8)5 z;kKg3O@tf?6gEL?>fRmP1j-~(V@mjWE(X%gtEtgrnz@WD_pOf-hsCg2Xq3rl$=tfLeuQ~6DipLJXB0|0G0$1XLIqDoIU`a`AgYi~ zBbUPj_#r;&Qjubq))FXgVk1!ur_l|bxWSWrREtuph?W%rc*5maaCyR^huCq^34#== zo8+~t6s0ge)WAHKV}Wc*YP&cL!hlbBcJ$821fOtG1eNa5Lr}NKcw5MU3MV$s2t&z# z_`Gcc4MN1vwbZ8JC;u61zgzf;1s^2w6Tnjno_7Knq&qqt^ z-XQ=5fnbEvB78j;1MGAF1-8@y0@z@_JRaX&8WzSNAA`(Y8deQ^nL=4*mON2rFe)Lu z>C5F&lbprl%lU3a&Uy%r??B6S%nPTzb?5|YBQV6t#UT`k8VI=*bix-xc@vwIYN1#n z6@#8erIy|GO%QMedfk~y%S?Cwqs==pk6)IvJQHdw=T2FVcBmpZgLgunn1n1WZUReZ z81m%mAQ{Y0WKmcs!6ibafgFbVXIRWEDLL zY{`d#9Y`>CU^=848eQvC5qP^T%?@k6V3CTsqReB z(4vnB4_1#SAyZn2PLYle3HvM$D~-ntDxgGzBAdv-AeKUX0UIBLCQa_Bf;+09G(m!x z%%EVi^eU4)F+!$M6(~H{j0KBXSwd2fSdGmK7QyV4voRYIpPM)Va=9k90Fy3rl5%-) zK`MrDonvbpIX^eqhum`!o&wt4pn4yC(#=c=r z#TAQ)1;lViXP3cn!+BDEh}~o_Ef7f-phkbF0Hr>FN_c|xxRCe+Rhd%_GJPQ1I51ge zP_kJDlR8wV#?`E4T6v<*5XhFB42ay+1?Y{`MpKL_U9H4af z;uu;WzQDPhEW`u`qp6Kjr(vOkKsGa(lLSx5CN!P|=0qckgXlzwL}VhS$@8`*NWQX3TjMGmAKAys0O4<$tM$&O&YJeD8( zqOH|MsMKmKNmx`1DzRFf7UWeOsWoD0HBko!>L89fup`q*F%>|fWq5-?HQNc%c3dGt z;&dWw1pd#iZhPNT!*f<==PaGb%9*qt)ynv02UJH~WZtkQH*1bLW2<@IOf))>eGmU) zf;uV$XB-6lis)*w{Yf#y!a?bJtp4*8Fv}7oa*lOzssLTUV5Mn30>*)gaqd)c8Kf1- zLzMAof~7CrDlou|w&L6C)G7s1MEHv#Aud#HV1aEC$R>plotLTgi8A6W zfa%sw5X~{Sepu<|P`xI1+Jc-3b6Dm@XaP7bX9HzSH)g|LN?2-_k2Eiu#xifn&YeBS z37KUMK%r&UCfp7AgIMleH7PT&GaR8B~e{rEi*_T&H>af@Kr>hLnz?6D-)HObUsR zAUM|bxNgC+U+<(SCPOBMUYN;{|4~easi~f;+H$SuucLn^?igCY=TxUY(ZJL8g@@C^fKQy%TdH zliqnq9`+Ms4N4_3*~O?87+FypI>_{$Qm`Yckr_=&12_k8O0Uu;pt}M1Vr_!%0oO&h zFYZ;w1N|2gE9)~o=Kzqln*>Hkli&l!WTr;Xp+I|H#NW@f$Vgpk=-h&T$z?#lqgY2r!^+3 z^hOrk1;HbUCX+tE-`^-tR6_s#bb6&0eu>u^fU5A-kl?SE$&-Mv{;*zI+SV~pI1*HUhwvMBFL*J;5@y(o(S{m2>_^X zuP36sdICt(+v|xqubzMwd3!w}c6)aqJK%7Tck^#nieg&dW(I@Q`e#zg|6!hwhE$T>AGYS+}1iF($16C0?`QD49Iu3H#MTF zajxr{R%cQgW02Lt=$ZAj(cgM(>6bsfV@4ah$Zy`fX;Vvjc%V;R(k`;zI{)EaUP7>Q1k%?gdQ=@j|NL zP@0J)hb6yo5`{>~v+~4F#7baBTX|x*oz~#t5V$Gy!17QOxkr7~;&sSmugRz=Xe4r@ z6U3D)BHSIVmdTZgI<-P+U}03r5*x*ymOe@+PcpK!I(T!itl>&>!OLfQ8WK8`ZQ)0d1TFLq4HxW3z*5r+D&a9S z1#zvuGdu(}bP!%eOyyu0keh4KHp(V@JvbypD&mDoLVQEGf(Tzh zm{jZ=93c|=MhN)=L1>61IFuhg6nd%W3IgN?C01;a3R1zs$=8&kMkP1sjJkLeLd5+E z?dq@Br78`2oeKMvT#i4XrE%O2_GA@sBv3|(;&$j8R&G>p;T7bh;-vZSvVelP_;@z4 znas8a1bhAH>EVGsP3=r(gr{$v-rm^VFH^cSW7>C-zYn)%-nU46eoTiB7)^QIoFLAV z^FQ&0GAJDO&ZXulHn$l4!e{6}4d{F{0D0OsCE8ypJG+jD2l~{b<37|2;=9sb@J%cn zP)o#}+*S^07Q8=J4o#iDrj}T73EV)o+5{GgDGiPkNpKXGz(ePN=}l>Hq)37znOqJg zBD5e6s59_8amLbyPn`jygZK&bEqOql5mINsi6DLwQD*>3Nra-qNuZEAMC&shjB+S0p&P-H* zB@TvVyova$0HY1hz-W@i37SU8(1R~3lUj+t6DfV_6g3wmY!c~R7}r-Q<6~uUF)*V` zxv+>GZVFjr_~uGkls3cw?+R4Mi$KY*&;(hF;37mPYYB6aFfi#ED{>Q&E<)G^^n4ae zzKSdWp4|m{gUkuy(Md`JLVZy&QhZBBZVCc%m6q%j;0SDd<`W!frWxR?Hlda<+0!e7 zq92qp;s?f-0q6(0k-Rt2Lu?I@v_qMXOu_=4k%?)C%tR78U%-gB?Jv=W64M3 z$0~}k)H)hVt)sEj<~^1uc@+khhlGL{EisH1qc*lnejgV!y1hnCTp~1?*q6>K+)Nt96 zF3g?p_;8^SvJ98dI$S(Re8(oi2RU5(w!f|m--ppc17sO35pA@DTme>nALMA+Gc=H4 z^UfO@XoM`oC8iCRRLsRwuY0(x`R9eS@m**aq7kwTm&7_;gwg&{SsL$ybfFQl440HP zToQ@sL$);R7>4h|aG?<*JZPHB3=Xu;l&Cb&JZSJHqoE~uXQW?ggXRM|Pd33z6)9YU zTJ>E7&Kk|XCDisiZqp&0$08Us|5gBtHPEc-djIHsqmUz?lET5<6msasL4Bkma8&gs$& z2kdx5O9vWg1w%gKmKH;XaH+ z(6&k$%KN0J{9zisY04jD8Zh(yhy(=(X@1CIrz!wIq8i!=2kbj*QY^}nL#eVn3j0I} zvjU++40~4)@oDPn!c`P{J;xBOC4-nd)Uz5y)y%>E9i)%oDmmC4hMWidh5Uu6pF{m( zsfz_2v?hY$V@5WG8mBDK{TJkC6K_T34xp?;s)QE01FoRud=6q>slk?P+Ax_($>0Z< zB-1J&JHH^Qw01awFOdjD*Ii)Cd|&#v*8>A1%31agh0+M5-EEE2M1lsR8IZ?Z~26 z4P>ZF{G|vq9i-XdPsDFbn28G9i&HW9C2) zg{oEZB$i3Xf>qBZSmTbCn6pesx{RpGq|vIN;AM!6d~(vXJOx`hc|Rpa3H4R2d3>L(DLFcQHbjY%nDjxQm#-^SPrcK93s$hfth>nFXl`1gTJ>26?PZE0a*z_%)r9G{V}HfZpbTrNI!g*KJPmyDwuF ztr|zX4BdF7f?gExRY-HgwdSX#m|zTEnPz7t60RinrVK;mLfhjRq7WIS&N1^4a{#3i zU-MAtcIn5+Id5*LhK38nrJy&JPwXrNQ1(*dE8d--ob0L7KN7w0|GygM;=2eNc3o zOxk}tA^Z1%K8PsD5CGAl=Uu;@fc^VGARMqK2t>;1BmvVS~F>-BB@9y+f95iIHG8Lo#M@D$=-9{(U<$l^=>EJ8(pn-#V6aQn(E))J^j*?I!Dj$lFvMU zu+pA$ds_`|wlb~OPk;6(ZLS(vw)OPBk@F@_DKUN5|R>G`wnrC!yZs1Hyq z|MRv-`JulI+57yPbH84BC8@pPLB#S>tjJaaf|P@P{UhzUIja&}yJ z;`ppZYa)LyK6TmbdEGN6)F?IjdXLiQ=Vtql2;BlFrey7^Q{E?dsK4OVKf`)H&8ZYp ze?j@vr<;1HcCFLb?>OnZ`lAN*D!E=Vn?7}UU5C-p{m`F9_7E7;Pc(p$q$Ad zzB!_K^T)LtUcdKy?QWxn?kLuyPy2t5-5#7e+I#c%nL3XNzr_j9j`7~yWctgg!#zG} z7@f)*^=SN=W1cuVa)Vao<&DS<~arhZ) z$(4O+J3Pui__NLTm8u;3`cw_B$GE*MBIRwOXUfYi*>p9u+9T77Opm)YQ|@aIWL*(g z+{4}fbInPcD@7fZmQtv;R+TNw>R-uo!L@I0J{T`6c`qXK${6qdYw8Mq8NF*ztJ-%4 zmOpaBXLM@dIxnyFmw9QwJt{wcXGCj{M+;P!WE&-Q+wcP;^oyTX?&=lYeM{vPJHt7D z)tWlSFZ9nQ#fq&TxaQ|W%?FKJ)HuOo%RF62<;l}KI#;Mz<=F0pypsB-x$7#dZL^_+ zxSnFlqQ!y@IJJ$8V@BMjfrbXYiV5zS40%KPjZO zkG3rN{jL4xaOt|iUCLcj_9z`ut~GGZ@T8ZaS&`ob&R*JvWAK=eI&b)@__Fm5e)Z$D z`s**%-1=>^(?cJ$YMS8PWmNjo6&0HsTep(bJ^X3+0meqjrLP)Cue;dmPak%~)O4S< zT|Fk;*tom;tx5atZmX4hadG=&xAvSXw)OP$+pB6Wy?1z1m+)!bMlIKdvSl$uEZeWQHid~fa5p3D^|Htwtw}}_jlEcoypG_-sH%Y zg^z`Mmo1R{RQ&Q_!v`BL956PRb6`TZm}!5ndK|32^0I8~x~%?Pe)b4T9QIk`ULIeL zH+&aPY|qdchK`GVI99V~#Cor(1C3_=Jxwe5iCQ6>xu4AT zS8(jWJ?^tP^Gcue;!nb*#d|1f@8DlFZhJI_Yi!NEAJB5~jdpt~1WleY zulpYp<)0pV+()~qlA_#|ZVxLx|M{t;*VXgp%ukyIcm8?j$k~rq4ldUkcy`74id6EoV}$_wTFF;m&Y6WUe*lzQkUDVwa1J3pTCHKchhFej^7YgRFuEc zz2cr4b4%Upc5mB`hNB+phAWNV9NO4u;n5oV1|+eMJPEz6@%g#ep+=Y13_JE)-^z;8 zSG##d|JdlpT=Dt8=jm$C&urNs;6$yd-~9Po#{A*8H9I@`B!{y`q|Dm8J#(<%yclVy3ey*6C?49(uWJJvw>d+N0&!px~>E3J1{caDwE*&sEQdCc{ z?eUBUs>)^e9ZX)C^1DX3{pqy@pzB~NKFLkHOUv2lW(V||> zN?u-?;M@1{$`ii@eD`Q$+oTt*o8KBS{_5j9UDpK8?$9@NLaD#*mfI`$@w-rdLh0Ov zt%ZXxuBmfuSA&e+k*)rBuR{HL<}HuLrGC5nNptb-9+gKOzO?7C&%R+vp`3nC*KA1& zXpoUOTA#80^2)(}+lQ1qAb)&xc}V64@%^D0(QSUcUT^DA@1sed{3fp#R<3`rcXE}$ zvU?xJjQHwVwYp=cDt#vV+-W>-%f9KwhLk-sxXobIuu%s;J6QTk#PNIU|MzD^?OXF& z?Kj^Ue6M))h`Req{#bVCgO-`)hx`@f@xRBW#w|Kdf`UuWd_qcP_HPx^cCz;!R>pzM zMRh{jj`zO9&dA7I6co~StoM0VMp)*e+97R6d7o!zcx5gM4Dp`qt@a7&AJ+d%pHb{$ zy)%4#DxR&M(LZxfsd`=^Uct~e7ys}svpxOVRvA3)dJMGVzIeM$y}fM1-yHknQ8$zZCndUpsHF^_44{uh%Z-8Qa?HS^AjP9$7xYwa?V^X&oLGJ*BI+ zS9JA?r%nfW6-yl9eJ81EF;78=m*!OYksdYJqf!Py^GOwhdk+p}c@z)(<*iX z&-SQU%(EK1Sc~OjTYDVJ+;hfQ&u2t&_WH*?yuGeucn`TyVWdZ`2ic|jeK!5)20M*G zlNVR?9DDe-&)7Cz)#_C2pB6UaDx7fek&9BRkf`V{V2&AS-*j^~7=KH$D5|>!;_w4NRSPA(LOV->}b)JYCl> z{?|RzCVTT6jjs?^Y)9s-Die?SwyXazQ}TTWZARsze^%c%N_%DL(K8E6Hrzk2S(ysE z|7kXBouR&>-p_aTe)e={n<|~p`S^uD^Z7*8WUB<0-ZS^9m2+Bfv+>9? z;s2}Ozvr2Ikd*oA)xk;^GT}{2qBkw>@k|h~Gq%pRKfxIX)cvY%Y131nEc)1|F0O@I z0P1?KC8+yTO#Al5I#=7dZtlQ{<=on%7qJGl4gceZ-M<7S{@QTB#oYrsSE=%MqXCt( z{~S5ZC$`-fRluF09lC#hs>Z^4;)Mq<4tcy_@UD9oru=c{%-%#+f5r1=rN_yPXJjkq zOq6+F^(nTjSa;Rx=myDW8vWWwzapu6Ct(+D@a4Mfz1M3jY?rob?a-ISlvBLg&mHu= z_S5fJr&7jTE4l6#Z{LZwYcyWTWp+*OS}v^k)5MiAHMahqd3j*aso@^G8jWRN>RvzX z+!xx8jfTx;k2$K@xpn8~wJugT+H}vD@n`P+FY8>dfeCY4MBKc8X~^m^?e%SjU-~g? z^M*Uiv!6yTUG_xY>!w6^G0QXaO3014OEY^L|F>mQ`mD)Wn`fVTcK3gw&jaRED3)-w z&W(QJjgt<}oZTTLCI1eo-ykIj>LV){;wtHEre}RCEvV9Nr~B_5Hy5f6p*Z$c|UD8od18 z9KY+@nq{Y7j`SW8HD`LHvMS7aQgat@ z#H&}A%@;3QxuxsyCAYSOR;urqTN>Tav4(*_p^trT!L`8)z%9l2lq|MSs@%EC;d@c> zywK?#2h~ zN7n9ec)|}SvwS;m?w#~+n~WWaUSt0J-%o31>=!p4U#7zg&aPb-()v&Bo7l2j11vSdvd354M%i;SB zr?>rMF8Q)bpV^s8|EhQ6pAP(8_*EwVY;O7cOBzP~cB1>$J)yHBu!f=z`{9Yzx!0y0XtF4yVzHP%$`Ms6RbEu-<)|gAHA6~{srj^ii{LTM&(1saDDl8Q^U9`vKGCB> zu(VfZ*qmjT|J=>{qQS&J=ah@itUkSdy;I{i-#^*opJy#}zw*1co-i)*$l4d4P5$Am z2+!t(Z0_EmPeS6|eMf!@dM2+jJHGwd+m#ge%Ky9a^ogL@G0pb(k=FK~aDH&ck*{71 znY_y9`0Y9E_YV3!eCt=+*7{Fwc>LzmJ3R)Jsr0{wXW2(LuToDxc5ADAa>CwbX-7&g zS^3N1!_m*TUC3R!@8IPxR{SkKQGed3XM;BE{Pw&i`*F(NcHzA*wOI0U^U9xkdl^k- z2E6M1s^{gN_j<1SwDYg5r7vgSdyE+F)vHWC2kCWE!A)**<8;p>|2%WdvZtDtUHlU$ zH}tX3vbczR0a(^^B*n7Fnq_pZa_IQBe%F&0&s?&8eWeZk<;vjk(?3@({e0T8jI358 zn{4jUrTLhw;d{n@v!UKMpEnEtd)MP#x6-&LCq5|uP*(5wUl#@s54?5ztEJ}yo-LL% zY20gg@p`+YtBRfXf3#icQKEi)Tx^s6XSVXLg!uT58dT+ApvvP=@>DT6Hz`j#NqH zB~>~4d(}&8F0TH`+_3fD8!;`e{k3Y%!BN*9Rq~6tH810uV4=9|7yfC>2N@-|=X#zV z>(RK?g7q(_3~E*7%ML+_eJ1u_y0X@TjH{8I15Lv26L%Dk%((Ta!kzYu{{4GoVkfVZ z3!k>`s{1*uBtW zzf4&^9NziWtLeu%D(PVrzQ=qT-xt!<8kB0J!mo2r}DI| z17%UMXP@?%lUeQaiyn*1bWLpH)i^hMOv<+lJ1+mNe&y0DXNod1%855_z4gewSv8ZZ%=fR; zEbaNqHJk3l_=t;C!#ZB%oH+ zbvGQpvtrkP&sOyHc^dpGqu<0~-s%3ma@PGmul4D~UmmLVZ!dn#ci5AyS+^5e`xdaX z*EDL%;!n%gpI5}(Tm03A4ZQ|*J>RbBU(#3X)Hw+H#uq>!)qtw2| zr!_R)&ro)Lbfj_3?w>~_dDVKgu2SpqA?wnslUQ#+VbsJ4LS*Uu>?s)OmdE8?EkC8&uZU@EjD-t5+=>UjU4k zlBT#VT?5meAPCrzG&SD9{#yc)gg*AA32(De5NXOKr0GD{7BL`AL66lb;&Wq~r^ls* z$4AeZR{fKjHESH+x9`%vU%&rr_}SmjKWVn$YTf7nc?ZvckP&TAqxkW#$1Qg~Tj*VJ@9xds&r8j(GV}B7FQ(UN+;qjfC(WWuemDC_(DKbE z`&IGg##Eb?x!miSxWmNnn)r90Fuh{^reBu|Y-oyEQ9~lyRO64c{eO$>93EeB(i6p^ zzBR{R-Z86Ow~{lABdSebm(yC+X|sCX+^Q?3wTd6T(c;2`YN`MDMJzuwb6raFRc&U@ zJ3LoCJfUreW^IPFYQDPZ51w*g?u5R*yOmd-IgC)SOs?MePbKraxHIU>Ncoj7?l-yUeZ9$&%WZzE?lGwc`(%luH^aj! zpWAV7e!5`ONX?pt)lUZgay8-TPq!lc*UA(Nc73sBW0|Z^47WWu{JW>aR~?s+Xs_?` zWYWN21}-+RmdCJLm-SQzxAXVD`DwrMGpZ+5-4c|(yh2rW8D>!wsumH+3-cEvaSJ!2pFT$xbg`^+abXK|O5f8Jmiw?hlfcU5oi zXg45x)$@|6mv7bor2Z>InQ;qRb=KCYB5z|_UhY=aYkPWD?OA@B$j;i3_+(u}dR+k@2J(cckzp*HvIF|l9`vz&Hdqr zV!w?U>G^5-rJB(*?_3M{>Z{FO^T%au+i=3O*S6iW+q7Y2%$dKm*zLt%HTin@;weqa z9XRms*lFhs|L9(n__0R2Z_aD_UA%B)e`t`j$)}(8DBk_cpjO?-eg4@OpMBH2pT5?R zL2>){?~QJrwPQx*pg>J?zhQlE$mM&gJ?U-Y4qkb;M)dyvvu>2~3i+S^AANrMx#ZeW zs_}z(qr;om;SN}L@OjIO5T8GrSU$E8N7qt78 zd+KOL>rUL+Ic3hT{-H)xzn$NFlG$bauUAuiSC?GcMYA|?)$m1YN`)Jbd>t{bT&RD1uMUa{7x>}-}2#A$NaYA{<8FAFfE`@HE4#m ziL;3$a?o~81!1^R6vK6D?A5v2p52cxnR(|X9o#%-dadZqnk4@&=SNRqcPmqB!LmM8 zKHH|=mN;QSpQ(-8&HtqQvOhwmXV2~+JrXJU=FHdUCk?$fSGBooQv8-nlV`lB{q56B z;)nC^J&W&Kf8fu1)t^5p{&HKD(M^W5EdG3l{z1ER^Q^laiY@PzF;u=))2IEo?&F*0 zME0L+@^7u$R;ga^-E!;QqS*t3A5#wGk*=-mT>gkxk?l6u3ElfwCULB z@Nch{UcY_W;^fBPf7143#D(*p7)orv)aAF#uF|%nS4E%pKGgQmq3~~F%;o#P%KrC$ zfPY|_kiW7_Ki?45=sHgmH6!!kmPUUJ7&+w6w&$LfnP84ft^3dM5k7-*<)Z?N9}V*C z9R1*M_nGs46k`@xacMu_$^xln$9bMQQ+>N*7P!)Jl1vBc27Mf`0N$mk04z}KbAko_ z$ojEUys@D%2=nr>!JSFzEiZgRl0^s8)B} zgSeJy+yU2uqcXcym^WupV!O7J>IIEz%<|2ic_RIOxtiX-7r$w)XFcCA-~7|hFK_;y zK18lsBis3X|Kev4beZyEhh$ zHA@{9&~r_n@l`@3-n;ZuV$1mc zbz@Ly_UNE*2F16VQXy;hnN6cwHR*P5*(WWdyEM4`=kUvkfA;)?l~J?XtU*iL?4HnO z_K5DXq0P>PU#{#^V%Xo$Yb=Y4X!+;0+}Y>GO_-3(8`gc-_TwGD@YM3_c6|D>Uc|gw zOaFZy)u8W*;*UJ94tn(9`{QjN-SgkN`(U+Jfmsh)j9Ad6Mw0Y!jVUYpy)dUexYQ7R)0Lvw|v zPR*L!Bq>1k*O8E21NVP7uF4O`H}v>2a?#AFFDrldYTN1b855Q4FG}2&{rdZwN`cwE z(jQhI_V0J{KJ!m4^c{BhI5tAJ1|+PvGO2d);e=~Zs=TteEwVQr0_3&=KR01GmnR=>jU_VB}G%#M~N(1TSQZq3L%7O z#8Zk&b`r)?DN5P((1du)Glr+MiAvdeX_1$rMMFi5HPn#E`q<{V=Z@Yxb3ZrU_k2FT znLB^{zUOy-=ghhHp7Xo+*T|Y`xa{g7p>VEMi~ovKtKm?TVyBbG<4O^EPX%qMX!>-v z4&z+f!*ra(BXTG|@1wOTk-{qJ!c3=Cfu0ZduD9*8koMaaBpc)5mPN7poZ~)Sw<$Kt zYD3?$ioC-o9_Jo(%_AL_Z23JqEI-^(H)r$Ov5~-i0Rflrb~@vZfl8Twk7m>4MsVe7 z3+xDs#R{~Rl3Xd~u|w2=?L@NAE$e8h`wICCZ&kU zD|YPV$ltGNzO26 zNy(;0WER{{?ax)|uy;>%$0;c!5HF?@ct%b*HTmEkoQ-BfD}9F8w(_B3Z-SmVX^nc;h}3v%Ao@jnM3EgnF_rdD#*tDdu&TfSAF#Q?p2bZSt)&LQ%~u| zBBdF8SK|3z@jIW_(ULW6HEIN(6-#do?RsRGnqxtHx~Oi{{>r)Mb^I>&@yfZUrmB72 z3(TE86dmn%s}+CVk@}{$CFsMJe`n=xIY6bklC6hZ*Dq^pTF|BbzTRMksKr^ExW{@v zRxk7)zTI1&{(5Jh##Rl-_;0l)tj@o(;(ZvRj~TT^jw0C+%iDTQ7x3^k6Qvm0p|8d0 zq7kk7s#+U0BR#1(U-Ua|XsS)unC(}k#{bOQ=Ui7+7N=WbEt!6G`5DGmfo%)X|&^nqArW8>$iK2O3XF^_C0joPKfAuX)Nd zU}j-K%N0i&aS-_mAbP z4f$wY;AYE1_kJEfwRd;vi4JomLE}d8_M299{FN`;U6@Oiyn6LM(>bX2=H~JC zDa{H#Gd1RQVwz_3?&n@dofmz7Lhtsb-!`z`+(9vUo+y+5B!jV1c>43Ru>|>0N#{?@ zR66y7fzKKbt^5$t)A|Lc$n9^^>I%$~3waeJgKY}^b~jzrRpeKS`rsI<9hHjvexQN= zW^cYrl0nX?s-=yzl+iYw5^qY|{RcAc8Fghc>u<}B(Hi)=n7pjP35;V z<+R%tiIIn1zgJhk^Sal%N~2_xM_ucUBdcbb5(17I4cgF(ySP&cen2)<;R0ZTXygS35PRhK9F21{YJD1_2!N@#-k18s#;#9f05I; zTf*y%6DiG4+p*X_?-t&;PKsK0V7Qf{GIL8rNXlSOS2$TO_HZz%ENigebI5b_WdtEW zByz<~a;=`W6m>uGVBhb0cxPME)T(_;KP)mz?wrWkOTtT@Clq(vNw+&HnHmLOD(cmG zwsp-5Lz_Xvf)p#Gef#^xLWZa|_G@o9jK9)LsEn)6s|e3G+4jTgR*T%CL(=|b!bf%W zcFMl3{f`mv58h95L))&=)#=4!3^kSU8`0}1H){)dPQMoSh1SqC9=;BLJAfY-6W3W- z*JHPN_`92jmxI;gtpRH5yPc|EW(i0p^DRr6bTcb)cO|X&4+|5ijdW7(HRR`2S}o+i zYcVxEVuGMu6~C_8hcc`!)8BX_mL6-LS^ioF3O<}IKt~Z0S3eh0fS-Th`nl9#n_xwtW(f(n z>6lA{Z8A^+8b@a}<~L9XsK5p2f%|Y!lM9qXBWP|DMc7)*j8(6m+!&l-c)Wb2*9IW}{g>j<-S0T`gE_U>9IB8B2ZshI89+ zofE>29sYl8c#ow%zYW}-E(l#O0{_8&n7ejv^EHf@BJ1(tH%*8 zJ-5}|{Z@jdKELgt%h?4S^>aetr-|n_0qlmd3(%H^mWhGK*#YF78RyYnxVH08T^clr z01^9%fWJu)!@(Fzk^`mfIRw(l1|9&3>d}CK<`?4!AY*|b-3|zH1RRdtZ1@#$XvM^! z_6ZwkFAoUPcA$72`tSaC7CY?kk5%of+={UQs7fHnpaI%S5**qtegSS+>ar@;t{%`N z2n1xHMap*Q(iFIkZ8>NDRlvb)$u1r!08b8Dzkx&?Awfr9p!*&GV~;@i6Arvb`{IJ( zOwW_WI2;geLXRGsfHnk8uurFy%R+xvAl&|Fus{g#i2=(W4bur13L%4ma1R6JowH3i z>^z%HD-a8?U06_G0~#?vxa%iau>r@t+>ZaU{vFzrL3g391R~GnLLU-aoof>1_<`xq z_wSMhwINRcS<8jnZ;OFr_ca=d+1J8qMGE8l@HimQK;tM)b{u5RjDhH`O46t4e_Dvc z0g>YXK-)og@thMShgrzu8fY3imofreSF{cb~q+}zILfS-oKZRcR3I|uz- z4ubPzVc^(?qM?|EqMtrN$elPIxTjDn$Y22(Z5}$P5`ox2=WNj3+z@zm2Ms~T4kD1d zLRb(YmL=$rG6cdPU_syjGqfe>ARh!m5ypbRAwOscI#2|GTolEEz+oe32s-!yf$)f9 zLEumbGz7ipi$F+{SP*yz7!5)1Wg(CkO<|yJSGKm(4yC(5Q?}WCS`Wi^27IN8*v+PWzf})iW+pf!ulIU_}} zr~R2oqgn3Ud(XM&eCIpocAT-_l!NNX|5_KzU!)(@|Ll){&rd1Qw7bLdoC9%AI1%bd zC6dKXn}XAl$xM+OktUEc#(Lm~)$>BVaw=2ok<-m5j&O1&_z?4zYIBXGl9-vu3s`-EvV4%Oj=#iWAPs8{=!$_c#*axQ;WytZuSH!&ic~|sfz9!tUq}S>S z{9+!?fp?DMb#a zdle-Ui_W0`l1D9sD5cOHTnAQ&1#%4Y)=gFxoSjC)=maBjw_DFP-V6^new`7MBXMnl zST)9^nhalfyIW?s_gHPB+FjGGhA+0a$#CzI+LU(FD=O2Dl3GxpR|LC9YMi}NnRblT zD`dh`qtEVM&~H3e3;H9@#w8eG^_-?}>T zN1PXCniKmnJ$`e0k5VeE8s^r-l#CpfGcr67(_|{pBz>}4w@EC$ zO=EYI`HWnH3R;h>O>X1prSoRtHtFKiVjrrjBY(ub%*PerouF&Qj<9-7~dO_YLD z1GaHI-O!K8F}~TSz2;d|GRY*Lxj2!Lql+UkWtIm5tAD{SXD3qOG&#yX;7C)>$(%Jm z5=te~iS7)yka`zfM`A!pbtEG3%uKiOL|oTlu`e@CC-MG< z;AdvK3!F)=ms4_2N>29D<`}-=w@!)&%o1k0)lWFKfPfL?5c_~5r$xeHC9a-!Aoqle10SkGj-=95GW#Hd&_MpWC)G6~Be3{Rnea$V7WMC1M`6$cRVXsHKm`q)F-U!TOPg~ec z+{?m4;xi5j?Tp-ju)b-+Fhvaw*1DYi+9F3IU8#uW@2V{1cd+>;tZP_@vfJk70Q-&g z0Q-$BL}Yl`)S~a5|DH2j60xKlUpA{FnOQnViJ#s%)2$ssKh2B8m!!{3E>5({8AY4Q z=7VIi1GZJdSgo`(atFv{2g;O+k{h-3%JCj0terLb2b>N0KZ~Jevv~gu5B^7Wf_4mN zK+-imnWvf)4}INEDoRnlaLBDhA$gE;H$Q`SyVS>rLqLXOIiS#Z0sX~rW-OASc%Q2Oji-h~ z>_`3VrRj*?h=GBD83Q7120%vMea`%OofMC(kHiUr8B&VCxO_f<*6xTBZZ=Kq(Lbt}q?G;$ zfk$USvT&0Al|7brWCMMQ66Vk~4Xh*8&TlwdY-wHG?wTcd1b@9ptoQJXc~67rZSVxn zB@HxPPu0D4ET%Sd+&9&3KxKQ>9L*DtCDM96i=YkpI|kyBi_92ZG$JRpXH!?+piUEO zfI&hzBhheB#jDsE%m)Q7X|xnIJHHH*x{wybFj@)~4<&MX>Xs|{VG5zP6uXawMC zX2Oim<}~*N^@{Tc$Z#<2Hhd{MKv>G{YZj3mvMeAvqPbuCA?hMq^bJfr`x>FcTe(K) zhl({qva=Bs?aDQPes$CT799-)!tCn^1L9?9Ne%|nI-{tIw{YJVw?D_#)ser$RYjTq zHsdrUX37opPU`wC>#J;$5MP)@Tw+n?-jPUzi;L43h|y?7yidJ(n`$bM1EFY&uGWg_ z3VLpM2~W;QFH5&XQ(V6sZFC{B6HW{)h6m~5$U2H28ePN)^LR)U#w|Cf7ezA`*BY*C zr~gy0Q!fZF5$H2hJze@kyCC_ z&B5kY^Zn^b?0o;I^CeGHt5-6$ho00+4DV5AdwhPeMSENQlND(iUoJiuqaV`UEv|A3r+f`j7ZeFCP7ZVTuNFt5H9>LS@ z5%FL0e=q$+M^tW5WW&{@U1&*q)FILy4p2cP3Do0s+|^|`Vo8HQF-Dz-HjMoisyC%DN1-I*gEyL?Kut(>gkChAImeb`1S#<5h3nxxT>Nn^JO@@pOW|q`3vk0JkU+!wVK@ztWW|?~28}a>RH=6KFGX zTqaK`aR0VUuj%Xs%jBqB;JjwV+>d9aplS-8IH&JIQ-eEuQ+=68fjgej8#A3aKaq$w zCsGA_;SBODLpLw(i}w_Mc&g8M-XdA9Jwze#|qtYu}ot2x%od&&h!>)WaqL}$ar2G zP%i=|L=#*aNImI7*DOroeGT_M0|-pvVV@se7BioTP{@)(_napu62>!I6H9<<075A+ zgq@LiukqZu=_Qz=GT_ckq0{Fl%o0MrV;tnT<#3-BQB1=#ruFqJb)twh$)Qx@K z1{5$>XaX#ZhL#vlTo3`x3-hI5rz}k49)`(ndDo6!*|dO>@9Dzxby>b!j0rUikjNbQ?8t#V(y zL&0h?T5ma)jbqukCcI^1CC`pMV`X(X_KXX5hGWmD5Q{zbOwNRXbwgke2KP|#CuWKL zbVs65%5R*PrtnW1UQM_i2g!95_f&K&FxiWbWSPSQOy_~bL9gLbiOkuNOfN#rBzb|S zNM=u3y>(BTOC}Hw_0Yow?@a-yz8E5e4D#f84c){OWfD15k9&M7Vh2sd=O;=xbwY<(eS{)T0GkG2XGpb@{p>8aHZ+ zXq+1W8A|kF%7kQ4ExG|6<6|s|xsxWImmKXudQ~WsVgf%N&5}%|Gp%yEmvuN*I?T=n zCqhYa(MYV(MP6T@4beE&A+&A00EJMqjSE7p)LP>(=;lFIy1T&wF%G-o23o4zc5MG}$YwD~eH>v(u?6*PBDX>3v}MroJ;e)X*jBLfF+q7Fc7anpRVN)nIL~ zTy@93i1b`;c?{;(Op{h2t7c(kYI=gU4CM9Y_!fCXtF$i8TjALvov(6(ny$k{Ex1ac z!}b0vu-cC>onVRRCrbCI~bxMXo)Jbg;rZylVOEaoy;1k!E0>5p! z$JgX-5qvH6o`A2d-Ya@N^#Na#Sl{IHiF})n_p~+(mtbMZ^u~A}i`%y&j1gU?M(^qI zQ4#!@MsJ=GO2piZ;lpSUG=k3yZp#Qh7lG1Q?ibpFVzAj?9~7I~>%Fbbf%;&Jzq!8E z-y*fPx3>5K!S+jB9CAHzvbRZW@%q~W^?tskrQYiY|Evio)JtALX!1%;fi`c`C8`;N zH)rZY?F)1-?a(})*o*hR_wSbuuB#(|8vP-kAvR9>1M-`b4aR%zHV=YvCZUpTpcrYa z%F;NU24=dMn!%;zexNo=~(V^M!iTjn10T+uJd9pv=qxI?}cqgOq6Aft|0H#Om6i$^(^fN zdX{zxUOm|mX8G+Tq@x=E(f%l(>_~Bl$jl=hteS|3GUDvTa#!cY+N1Ec8E{^3iXzX8 zH>Z}+fuiID^(%fNlx0HI?~8|s4M5FI+-MP=kj9;O=x-OdL`jh(f9S779gQ7-{Oq>VuLny6EI%^>ncC|mqI*pG)u4}wLi8R>hAKEEu z|A@K%Vbjj(pZ+N66t#c6x&GlriT67bh)5Ob6}5wWh7Jk{0jeIDlQ5Yz5a|`QgZ#M; z5`+LIkfVYygGjHa9V8h#Nb-t~j;0G2=@qqu0=W(%y#u4cQPTZU(kp5Q1q~hK_eqYD zrZb523bmIz+PlALPv7PpgGRPN%GYLMXS(8G=p8+?nyy!fVeR za_9BtZ~uAa)enJGPyR^pERuy}VD0Z-f5M^d(owJ7?o3P+c-X!cLGXFKP4!Y+o1dTr zLVdHx8vs<$+}aXsZ{=G=>S3FU6|_z}XqIQcx;pZwO&U{5#_miScB9>XzuKED#Hj;K z3bZ$H8&L^{>rJFCZkAJ>sPKenUrb&ai6KEc3^$9U>7|hjsD$S!NF}Gvn9t8s?<)7( z;zW}E6!iLe20)r0K<-1co7nTn*HC9PtUf&*my!EA2Zg7oo5;Q{q>vN!>gnm~xk$98 zIo`}k#9c{BAjpB1`r(c>e9;!946j8yDr&~NJZdza&VIc9@oM{7I{X( zM;mj^?de0_8a!);1>_p9LIe0QcP{nDiMO0PWVlm)i&=Y3t<4=O+)}K>Xs6cE`n_>- zfk*eCUptW+3-_9$0g|8EIs^+~AK5GQ)0vQ`MNX6cp1BukzU1#%Rg2JZv?^gUPH`g{ zpcDjQ_nm+u$I+E9)z5R}*`OVggbb&jw=3 z9)KCGKa_}q`dau%`i>^~;$t*5UrsLUr*>J4R`CQbjYCOHdG;EHQ^;+iVK&e5X8Q}j zxb{5>)(81JrO<3=rEm-aKr`-~G+wnH&WstX*c?My3up2c%?Mnx_@Mi*Idz{p^7k?0 z02oto$Eji{CXSJxfVzcBa88$Wk`!x>Xbhql*ngSRV!}f666Z)3p$cmnPJ^^`^{58@ z#8oXsvygs@_m9?;f~G0Sg3b}-nP?xPNNKk(pqJ=clm1H7r}{y=Us`n=OVOJe_D;2@ z2QDkNr}LS!EeLkea;1^=NxbDy+xsgY!2ZWyEHe47vyn@3Lo z_;lc5;D+=c(oyWiGfSHj;br8WUHBKP$^gp$fGu)SABvPv$iF4QP=A!JfO)C0M{p;0 z0l74qW&s6spWJ{Ez<R&ZdK*wU{gbbhML^&@7Il!;VmqKA!3dDQe?BmsH2az$37MEJqYX z?MTMK?>L68F-35Y3MgLgV;mS zzDQ_ZBD5sU#S^S{OH(?D5hbg%x_n`}(UaAS4HFE_W_QOi?IpTUDM=&#kX5HG+p5@2=_ zlZZ#;Thh{qtzz{IEzdb04@O{Wz z85lF*fvPdv?HJqLZJIVjjX z2PF>%4bvP`%!`j=fJ_L(9OQWt-ez+!fViZV`EE4@^JX8$s_@i`Cy2VC&nj!Rh(n>H zlEGY3gB@BSqkTweAG)*;2qEXMCqyvNqOn#JJt|cCvO?uMmA|ME=!0UsUDSQj*BH{n z<tTxUf8s#l?(_YztD56)@Qvd^-g4Z!D6Ad7AF8U z%#EcgU9?x646B(G02!RB*PJwY13aoFK;sjQE{c*rsOjH5B~F_0wPytiw9_P3VhBYt z)PZt|wBm{2_iOXV_+uAvp# z8`saSy2OG6X^>cA;M?dg85URGk99Pl)f`eiMU%wxJ_(dtby;v$*HNo0FL<@y(IvX9 zHIwqEbPYd-MavdTa+1MYBn%_14c6Y`-fr>^Z`+!EXcLyw)f=<7K8k;D8Om`;6K!PMG{MZdAG3Std`gE;K+c_8&ucp!jO^4%Q7Q{ z1=km-$g!7~lEFv|e-E!e@xVxeivxtGsJ0d%Fc1NBCIE|b;Hwtr$i$NFUThC1fIGId z9y|BdlX>E+=OsxH{ZL{)(d(`E)MAi>0;|9VD-q<7BtK7JAD}*h;1Bu%`Xh!x;vqTV z@*<^=$rg{?8#;5V73BbV$*5NX_UaXp_abSIy}Z}w5BMmAYk-`1W~G|~J%-qe%9>_)uKIgfBIhM_JjxO| zm3zP@*TfRJ>`m+8UlPEm44I=nxZ5&krWt^(Zz~uClQsZ83$p^Pyh~6G9PiS^4491G zr5>PX-X(mK(!EP>P^jl?(YvHPg9>?(fJxR<6=%=RA+egAczMl z9ifaB%oZOSRnY~SjM9*nt1_DyV*E^b%LPSP&EZU7a+Q-KC!C2Tnv}{i?2Bbbd6V*e%?T_;+1tOKU9*YH-oFvKVr3Y- z_V}9Sm?rSh%1M(G(=1tmfczzRF&(mu>p z4@8kv)0-@D2W;G8v4%nF3KR+g7N%Km`tM~St5Vrybx%7l2~FBlb4BbX00~a76NG$e zR3Q28@D=O1UH7jlW8I_Oxx37DQ4i(Pie7+RD<*$_q>kjvAF><4ZHKc;(ia1MDd^Ym zJgfA@^3tMdokb?l<4lV#lqiua&# zZ!f_i{KD+XE{LLe|8M-H@74szTPxd`7sH<0t(896#8sfrf@4jZlvguE6DD;_ zR+=TNw;E$_d?QlOVgl8lwaz^h_3$^{f1RV%HSLUl8|t(o4$k{&u6}MAGKqnvWi2EC zyN)XmQe$bz!c*H+B(lV+p>0B2pj~T#$w?<=Vhe01ou6YtXH4<3u%aF$RXf|}(kZ;; z4+JH~V-gD~9SBpY>SpinY(0jicDCIi>D->CpknBh`CrF*-o?VTGatVo>xGGn`Bz zl%c^sd@u-1#WV!e)h1`Kf)ey|^-6||dBFj@wbpuGf z8r3n?>k5=5r9PtJb?l({V(tZA%^VgO`zD`YuP@Rue9NX?5AP%;B*pBG)x&p`lay%g zLN*#@*Hb5@oeV`JAsiJGP5~<$Qby2FgWu^0XQ7>MjMAr9X)Yj~j>r|?I|212H9e)% zva?$TQ4O8bmc$!2K>Es95^a$>6z?@joz1~MJ|K9#SO=+*T?l|6aF35*PNl*SDF^jb zO(6&MEO*x}Ys=89WB&}U05E6k>XRo2g<(%!4m3?+VHfW8NSrqhW5zBsw)VD&@Cr6` zw$?hTPhU!uW$*9oJd9biIak&=wJmYa$uqU{KxV*lwoYw6;1yB(b`MYOQh-nE)OUM< zPp^(NvkY87R%HO6$iVg0BJfFL6BPnJflUNZM!#l}Pp=MTv#qskj?)-l-xB&1G@I0c zKF3p>h7e>N=u_RSZurU}Fk40jhk&sF`CK==rko@u+Lby!HBNBFYYrx0cSd_xC4)w> zgg$kVa-dJG;8UsSN@4D&1AV5s%(A4iyL(^l8d^u0XB!5G*Q`ZBn>({NuY;&o9g4|| zPS_mqGc9{o2&f~Yl#6EE`@2*bbr8_<5zsR_=DJ7LuHcYSzw3cVx#33!cRjeS>T+ie zJOB~3b!`+yKNdWgv{qCV6XUHENWy_YHJiwRK<$G-Nf?yH-xvf6NVJl2P#6N$3D6TA z0wu-eOucP$;Kdv85u4mv>r5sH)R%`hdu>MdjVj&}0-ZclI}dP1rnAk8BoGLoESu$m zT9v6?3Iu9hmBn5Nl!Q8r+)s#o=@2M*wDta4gg_}qQ6UIaL{QqJ8D%v{{zRQ$V^|&- zx4j}{czsI<)M0;;v?QY|SeA?DK%l1LY1@Z~)@L_As_s)3yY3&#-h7uk%c@S4U+u-j zTpwp-5P3f)OVj4aprPu7*AL#XB${d0USGf~1%be_G$WFb#a_gM`&M#Jgo&MR%E+@$ z?4?-~6S0Q9HSy7x2Y%ZebTSS;LiiTVat{dd?N|4UaCdlo7!L% z0=~U`AgFF^7^8hNE8szMA^@<+&&A2xL8@6gIpAQBhG$NoxynP_Z zY8vF$VbSjikTTok-GkU|Zw!T1vNJN?1nqbN1&+8qo1R_345PLj(iqJ~So-2d7cHn! zR>=cvRk7EnR5!P^FI?1C298F1Z+EG_B&x-1S<0SAB*|70GAIxqFM1@X3Tn+OOEX-E z6nYB=B_W{d4@ZWGBLP+3w*ZpN99z^*CWowL$N|UDJ(*d<+qPyO+BBPc^{dx%!w>A( z^}rSmg(AsTApE5QB1vV)mAOaNVW3ixS=tQelzwCmOA)ulj87w?R57^{1Rq(X9l2sP zA+6&_j$D<`xh1>k-1;b!tG1;fCCX(V-afM74sLj88iAmSNtP&} z4ud0ETJ?TMvZ#&?*^@`Y;y6sQvV)t3Z`;UahaM*@mgpINXt3&HWrp?<=&6`kd3nFr zS+Mj?F&we7Rjk%*#YW>ytac4uh78IT*_&_Ub`5PBeqc~F4J#vC#la_3C{rCRmUs?v zD+7E&;(4GJ!6%-EJ}QJh3Elvx7w9+OD?a4Ckk9$>lY}U-20yXS)q|f%sbr(?Y&<0aOD z)q+};sa;A$)V|&x!zk6!e`b`zoGaZb^?SU0eXu62QmPXxWt9ee9z-g%uwcyvMI|%x zjG(uz>w^F@yl7O^=NBXaZE&KZsqt+QTYgm3T3zh*QPHOM#chkYlRBH`f2Itt-Ima( zWCof+Hn3Na40^a$F((pDi0TfoUy;1Sk;`@r-+nhYvSAZ9eE(`ki^*dK^J#L7++bI{ z5`mR9n|2K$=U=)v0 zbIJ-~*xk&LfF6jZ!w-H_#fCp91$ZHE$60TH_j<@KfHd<=6h$Zf3duk_KlW>q~0!HB)unBhze*+sdY4D?^?2RFd- zHKDNb_GK-p@(Wr+O21U2m{274?;wcRYJ#sOyd_&l?mC``Q%(N2&JH{(DZZl$hjz* zV)V2@Qnb}P@%elx9HPFx-vcg;SvLl}e!m|9WNMx8UQw4mi#jb%a=5WVkDeT^uaL!6 z^#tB&JFCO)dDeyPai;QlC+$H=L}_gp1z0;wwXybk-U-VmcC%0ihn+|Adf&>c8a;Da z@04AGmQ>?6Acdg)+N!?-60Hx~G4UAM$c z(VV>I9Ru-qJusNv`5?(q&9gIVz{;D*%siSb%&}lrUe8zD!V8)SE%2r7weWKB6TcUB zx=8V#T=g;sz3Wy&nR)39fG`6N#-&MFDxhTqcuPb7YoC|kI1VkrA^gL5%3Yw#rV^R6 zBbi>BvP6ICiuR=z$>BzqgWgd#B!x^e(7#DpUgxfvM6BJON2eyik({9yMwz zWZ)UmJD(_Gd!w2{?}FyovSc%B*@)ixyr@K*S78~U4uRytIq03dJ4s9qdY8Ta?vV{! zYSXT>-fwQNNACdJ4X=5G8`(HGa>E@}x9jqV5q1-mjmVv2*g44Egbm9Eb$Wwp*x`HW zh8?QS*7IUb8g|svtB_%5LhfqOveRSjjI&}p1KNPx6*BE4KcG7;DD4Dh$5CCoy`+>oN?J;9a)%eVX5sVhCoqd=M~ zj**0j$&?Ptd-MptYQ6xgD&2fRT4X)%uSxTT2HPrRzKDQlNSOfeBP9V9q6Lx+&Jk{< zhR^sxGgBjP>|^z^L3!k78q=vDinfZs7dER}6I>ERuPEoW0obGiR+M6?6%Evj|#4(e&m~r&RR-q zF%TUQO=mhY%c5k*U=Xh9e^7%q5)Uh-;lWnGnl5ccCrWX0HV4vw zC)MOyN&~+YT1$E4-XXeEvx})7yJJ9r&FpMm;8ENQ^M;xd1y)lmwqj5#{p75s0v1xoEgiFlW)a4OX(5=j3u!LTY1cz*QIWRlJ}I9U zo3fjh>Gh%b9(+!#r4;L{gYT;K0c(CK<@HzqTS~D6m9dmc1mCU6eGie>Kmq%%Xe7y? z*K`I!vQRG>k#5vCS_D{;8LRpa0QSl0tol(B1!6&hBA_jeOHlYW(cR4<_gz3TiJ*x< z4KAfRW>0Nt6j!hn;1&D=kBtwlmr@7eEe&jPmeMiEhr?G6u@EVj-LQUSnTu z)uSbBCtj3TuveOL(Nd5;{MhE+94$4~cGtr97Ln3uB(9VnD%s5%z`$8k=}fDf?&bPr z)V`(Yi5h=3g-c7=Kb_~zcsgnyRIIZl9tMA28RtOI8$P5orU1_c7VXw@#&e=lo;0&{BLq;dvUy|HgAp&A&cME5EYmExUpl@a$X zVu}+Yj~@P=wBj^;?Zd-&?7-Ga*+Fa=x9YO&;7)a?kL>0_Zsdjy+08pgHf+SpITZ zZ^}V+lF~98yP;;`pUIk&4L~d`31FjcQ5ta_bAO8|>huO+P9ibYG{Q&z7fEH~JqCGfYZ*AkYjQV}1CQ%Pq^@P(&<{aZ~`{$&Rog{!w9OMQ4gai-A5MskPR%FJ|AJo(6*EmkY z2YAS)%X_ptsgZEtOKKb(8UZ8_EhR)PJ%fWJwMRV~>RrfU=B9H)y=?`~MurLtY@r4E zLrEN-v^Cy8`gI_A>~-j@L##K@f7-K27m?N^dujd)qFvGO+*pr#BKwv0pLRhSW5Djh zUc>rj^q~C$B4?cOHE7-p9WKk)7`vr}+X+Z4Olta%+1}ARUE%?S(kluBXF6U+ufit0 z7+UsUdj9TZSy*>rE>ugj6t|!+)`dx6>`9s!y1PAzF~_X|@Lb&JA{&1KaMC)KVfSRV zl6g{SCY0Q>-O(_W+2!q$hhzZ&W)TBYy|<;wUmxK8?e+K;M3!{3ucf)=5|=(7@#b-~ zjtm#7_;69fW`gQy@VVi@LnvNccuxW{sc0GvUPRmZ6d%a|(0?>-gRDlfc?9Hw>36fb)%jJ$Vn>Htk>9 zS{w?ginDA8k*+3ri`jP@nx_0@pSn8o_t^rA8AB3t%iC_li3c5cplR8+2~Wgr&G)Sc zrbzo?K1FLsn-lD~y8g2-nn7XNI{VEqQ@f}EW(0yUtH=NpwQpW%K5^0iF5jn){C&)f zE7SW41X11{s|KoQ0^2mE)tyj5K+x-9|0U|RSf)-p{U4Ql(`gX;t{zo4vwKZUqp5f* zqHZ`blL@w&mOjy&h$(J)KuIUuX*uQ&CqikroK7p5w7V-p4xQf>Ill`g){>gcM8q7y zid7lXXO5@^{$~*B`*}J2*alA`8}Jb8S0EL9h%rDbt1tp7<||=J%~29Dj(k9z#+2Hw zuD0qa>JqC?RWh8I{nnB*lP;a`udMED{2C9(6B>3_n70BGMP| zq3p&OGteY@RBIHTS(B@(L>G00b@y$IHFo0edEMUDwD`MmbnY& zz7dPLyQv`&k|g=Jh= zaQ2$I;pq|}Db$fkv(qEve=m!(S3Yaj)6PpmleU5-^a`R_ao|et)pcEl*RCKH z(jM4B>bi_E|qFjhWy#Wnl*D~D@d6|%_JW+a18Q?96Cv>>>vbuerj-%9S5A`MxJqTq|s;5#T zm20exI-SAF(qC+5unIY+D&P$Y`Hsb;N`BERV6dLHxs;SQ;0ppVrV*P!0ez!-Z>4CO zy*|wdR*f1r>XnlUXLHRS!<;6?i-bTPDSJ{)>Red|Hmhx}>mmdLUSeqzr*BV9YB4~H ze2>>vDmQtqIIx$?{b$G?;3y5*%fyoI-b^eifZV*aKHc9_?-c?b>>VSaOrxI{uz#k{ zSI=k{$e2dHGYOC^Ab~9UjeUuP81jl{Xn|DAplJs^S-sd05u*C_PKhnUM@YKl|>>}gElJY zH8nB~_tF${R6)aB$ecu>$pqC*M>T#AzfV$2bp}O`N1*m#&IA<&(Ip0 zO(0zPOFzmf`y?N5QYukSxxR5E7@Nz@6<>8HaJjzWZCkSsZ90V`_4{vq^b~G*hpu=8$ygj<6dWl>zU+W3`Me;2&5F@@1! zdo#up@X%CG7t-!nz!U6MCDqv*9-x7h91cW`7iNo0@K9K;js)@% zBhW$)v?0>0zq}~)UhmH5Y8$AXZF8i=2%Vs(M{AwQgq`G4+SSgk94rox!_PDN-ejHG zd6*zpS9{e+4nEm)6T1}rg!eQDu?ZoIqnZK}HsdEcG|2E15T#1TPke%3O@*#8{Dg{V zE;B28%TRXPWgJ0Lwp_-MU1D#)s~i{4RLo?YtQ1i3Od@4-_ykl#zn}0ZfyhFp zeB`sqwk6f2JR%}l4u~>tnKHThlNLnT^}t|u=YwQ(>yfo9+`EQ05!^(Rt*ai*KClif zlsy2Vkd#v6qGfl`EEja=z1Ts>SIl{IK$J+Pm%2KX!O<1%OD&QSmJ)n4@EDE|A~!Ff zBsMJ*)#xI}2`oZO2b*X!^tk$ifs5>bC^UM-QXAFa7R`JUbgW)=HErG#zzCvvQM3@Q zt!be#fr%3xL=kl0R_$z?Ln9^-rI6N{Odtx_=_UpW+xWx2z^-v-OJ2!?a-WlUX6rOg zHnUl2cpE$00HW-%iCqea5}DVe)OZ;nN|3K-wS{U7M4`}I z6+jfANIsFKju|(M-LoKy4ntBF3e3}rfhay7kNxh=+DhcT*iys+QFdo<9^&{rAc_j2 zTz~h-hAqVHL-c%j%_H2%#=(&r?x;GBqGQcWs%#FVh<@G`4ZpTp=*tm_UuJT}SvRH?MQ=+OYzNB>6P5@tW-Qm*W#RJTx@?kX9qO z93eA-RLp8AdxvJMq(&$OUXHCxm&wpZEpA^i7t(=J94KWBRa2?TNTde5G35acXyxP} zfS`R|9BfiSDWVSFst%{k5fT%WLUy&x*E*94N)huwLO7q5n%RQKhm6c(V7az3PE@9q zmRKIvB~EKATZ?BOfK;7bIv$6Ywh{*C{ADP>KVkq-nv(Zq!O`bah~7R9K#J zD(vEt5p0s$F2^EK@GpqI5B16^)cVkJ1X#VpNt*WH)j?aevuzHI7y*=mT4yo=C?s9k2o1vKtJusI{eeKR7(_nN0Tgc@ zGzkCR-pQ4}qtvtA^J)x0p@4Gb0TgUI z5D=jcY6(CA>SprxY{u3N0E)m%q8|d92cRIj5pVz$2SBk0KtXl^d^?(gfMlL6Yxh7& z6jD^#h@aRTKoJoP^r-j=L@FTW8vrl~2=&^*PsVU`m+4Ae%s{ z+}Z1iWO3Pt)@L{G;Ic#8ku6tk@Wg9I`%$ypUTZIx$RmisK+vB@oHRw<9q_~fPsY$N zm13U|lA+1Pv^nZ!1QAiR9A>-OXfw&dgm*D@z?Ok008?yLIwtU>kk*-0;7J}V2r$Lg z&Nj|$zl8l|ioxR(eP)wjp;3(hOtH7K4dBTho7g3RCzcy;*jo(+HR;t3XW$8}nKHnW zpr_tfi838c24<)GtaHqsp6E{qnpg;33s! ziK`%ap?_&F+qR2*z^5zpa@BOrY$DH~0?vj?-q@Q^Uw54C(Aeg|w30X30mX=11IezO z7z923{AI`_2HFtWUZkhGm{S_H@W`&eqwA*o^Pp=$*|zpJ;26Qr!?2(gA_{076}m1( z-|Y39&-wG9Ye3nycDBL7@(P}yH-EE+>P+n*04KaxUfF2V?grLoce$jrA~QP=y7t)} zCkS}?$u+TyZzu21L$1;Nr6bp1ztu~%h+NZnM}?4UNkobhx{0Vp9})%WN@PN=1&_!R z)QnhU>gVe1Cokn8*EWZKP^mizw3@zsKmq|WFChi{U;6|4j`EXh|LagDWzU}S)GsNk zmUp`)UpOjfBJr{-K~W0|(NnqXD^WwrUN^6$ZDF-tI>`)IBN$}sf^U?eU=WZ>9*Nc@ zLu6eZ!=5b}QHv+qBhe`J!OH^5;)%H8;-YioX-aL)XdhDAhc4|yDiduKE^KFTLTVCOOuY_XjG-8MQo8ia3UD&c)!2Y|7V%pgq zn|JLPEJO5YM}{M*43<4@#+F^pTQY;$2priedf0F{0F2t&u_4Abf=LdX!le?KvkMae z9XLf-E0rPwN;;^|xM_&-R8S5q@DyDRG-=)=c#3@+LtA^DMBt6+6=Ev3b~m;X(+dQe5*#iq;;E~Mii53;Du!<_kcl1WwXNrw_?01hwqPp(b7;VCcWBKQlsu%M z1X;!kS%MC>LLknrsLR1tC_=o4k}Lx{y02m@HiyUrlN{7Ewqjq6UTbV6pd-4hyAhzY z)p{oDxtEY&t<=uMb3lu9b{=ksNS3XgZJgT@473@w^>0%%Fu6U1ub9IGh;!Pyl8K-l(Vac5932N=aqtx`5)T(jNG!0LSBkqr zg2Bf4J|fT+l!KbaR|3txmUdDgmRJHNrl5Tp6G0u-Rq+*@Ln_8HRs~_*$%LisN)@c1;IOWZ`+L2qVTFmnq0K3;@p*>4j-!c3Z{F>d7U{6Vt>a{yel&cW$^#(lBsNoOr*mn%b z2Cc|BxhS|GELosRPMNF9BikdZ?}DYn4L>rNz44ARl&&oirJxzo2H~M%O4lDm*@Qf$ zD+qp%AG@Kj;#NEWZdz^tV46iLS}M^O4=>8AFeo?lgjL-khc9$--La_L=tA}|$9p7R zgXf&;RxYQyl|{WJG6jvWA9yGglLHh~M+ptH@nMpKJ(`VaU1-8SM4P1ao4ecQ2m(rL z`vej!2V~(bq<1t(y9AB1*O#{j7=DD+9LVdtk@#H#Tu2SKl?VVLP{L=g{y2JGqgy+ecPz&2C#! zhU-WBc6TfB@Fd}w8NF1-z%#IQp)#tNpdWTY-X16d02d!gS)*n2gaDiql}KUb`{lWP zj*RhCLzh)Z&eINU-=uc5iLtWR3z={ViZ>5$TbsS99AYl(!`&AqGkOgkNu`9z#|Kfo zx0oEOsXX^R-HJae^5ePEpJAXrW4Ufm{ioGUsySCR+KL2Z4#TQ4-X|Y zv83o^E_>5@&Zi0zcT#rK7A#I!y0BIAXUbWlOx>xD0hNi&(q_~Oq#v0DSj-MIM+HC_ zvQ?~%t}8a_f@l?ST?MkECf!(8GlC1)fo0;8h&gD2S~flzXm4t7DX7UO-zF)j>Ss1) zYz`lZa2M3ncOR6ifAmCwF+**=2+MqR5Hq(#~xOWECP>#Ib*EApbMKLi?-LW#^!J+tLp}9X4eCQ z-0)pHhHqV~`uSy;O!i10$(c5~^>&A{{2tLuupO(|iV5zQ0-m7FFIaE?fKCn5R@U@>PP$+53aK zBp<=ZSOAg7*4_p&M_2Pcuix*T+)-U@ifZm|n*&nl?tq}?m`sw%Jw{j;+wQ16u7-6n z;Y$ze3O)%&UQLE|sW()iur7h9ytN$H1sl(3oDwFGy=p5Ufe2D0vI|8PAR8*1!rH_} zcX0Ajaq{v5yO!q1ULSMHKD2(<5XydTd@y_YMu6pmTSiuGE(<0ny{ShX#*4UMhat0A z*&Kc{=@o>Z#C-hI=K<(~Mo_VG!cRo2LB*F-zPg<7(?s5&h(eaT9(XXjWkdO8Ud)|1 zZfP>d5(uTw)>>CF!!X{BN zhXM#$(^gS3E=)dNKmgZkj^O%(B4OJ*qGSvOI15uM?BbGqJ? zkkaVtNGTcQ*vsXwP)&1H0tknE;^C`rp!AQy`^pGF+aytXAV`%cn*(pedlFQP&sRvI zya9yc9Emd1Kb+-hymCeOu)OC|p?q$+P4c9N`c!$cIk0LZ!3I#GNX*+(%c+dAXDg$i z>|UO|Z+EOWR~rQdh$Wv$NzOS!9Wn%++9;#=bWUxQLVmNoTdECTJ(#`ua&Bb(;P5S* zvV%LbS8OfAoUx@s3P7bi>9afZpOZd656dNxCw=fpfIve95HuKmxgVwvL$<9#mSyH3 z@Dc!ScE`PzP=^h;8MHzYk$C}IM$xDm=`cpvqg1}MF^7j92eeE$esVO~E3?!da=zLd z>ghNG@nM|?2=CBH5uAy9W|r8TY|t`W5c)xe3-+`bNXf}Ym&V0UWuh&KC?g5RL%oSq zqbrn25qDF&fcV4Fbfz=2EUI8c4EuRl6lf}7)m5rM7cc0sdq${3wH~`&EUE8Sq52vMZ9ta$1K=Fp971Zr{{2a>|b z;Lz~HLuH6m8Hn{*)`pH+tJVf!lrpT1pr@Yq)}*yTqgfTQHh9qfu*Ncv4Ev4Hwr_phClxZlJfW7lD};mE%2)E+t-nW~U7?G_MW} zt7H!F6>T-U1(PUPz+pN|a*x7c*84Ks2#0y|@kg*!ZS8O*P4n8O=LL1oq=Lh|`RF6K zkhXTWX?|P5VU?fXwE~BE^WjHuA?@z(U{K@r6k=UZyy-m#90oR`*+9kjG7MA_ua#=j zK&8NSg$z{D6O?#u8r38mrh}ZSiE4M$1Q-~~*%XJv9DdlS-+*8&(l9Bx+Pp6f6+2$`fcxA&6O*RM${AYw%}y7uRx%t*knRi+HZM*^Y{5RgPi3k)esGh7J!7n+LvVJBmfg=lM) zDFR%ncPyOnZ^|4pRomg)grL~0UTiuoI#GAJ9wS)m&%;hh5#usU(;#0jcxuu#r9OM5 zOjDnP^ihf(20*CvVcE*dtra{XPr^N@9_9&NKvn6}~-6I<|l1h#@mb1pve%)?TmLoTz1~9Mn;5R=R1DPcqbzNwYGHWM@F8 zd~&~+C5n_cu1}fbIYg=ul9LRTAhByst`V$qBuuir;yio(4tB$9SBzY>F1uwg zd*dA(4rJGC8h&8M@HaM5soi#2cFQ$f_Wm6s*KaJxF1DpQ4?$9liz4#-Y_*F;pErO~ ze414d5RkfOGK@j*!j_UiyC+|PtCXe8mM@9rEs8-&L~RjO{u9wixIGe$67k&Y#VwSG zXXH@k)KDU4o*Eq3^1 z<6=nc(%o7NVXI7n#c*I?0Kzza$uxB{Q(s9cRa#}}QfkZi^-!NGnKlQqdB0a8wLszI z27&=pw9G3=XDatk;-8u&Hu0@gCqQ9u{r;1Xzy6u*W9x}2xpVFC?N4O4t;k+~4a)d) zU*uPM#H4i zQz)96FDDoF(~^@hIh|2bE%Z<_67L~*Go3>2f$KWX*l)@~b>x4oi{&r!*L?BBA0ARy zNB%e*OC~e!G%Ob-?LI%HMAPhxFk@*fI)i+lIUAq2ekGO0OC#Dm0}`pkp?vVfM%S5( z+v^diOlM?l1r-Gh(&$>Iq+PRT9dgjjMGCQk67h6zB$?)Lmw39-)tkvA8{F=6s8>Pz zXC#tJ9KUoYQs`BDP4&2wa%c%U){XLUem7c>y9XA2<2|{#h<>N$h5?l=2KrFSy%XKt zk&x1w2=$S1EAR++B!*|>hH|P$LEvJ>40lWkM`U)qUa03O9Eq+j)DnPJ)oY1RtR+AY zs@D=$ZP}tv;IdS&C9D#oMN42vR<9+jv4uy9tdB`h7^q9ve?RIep0@5-Vj&?41q z2_s}%v;a~P1Z!B7ZjVr3y6rP1ztlCmtHCt5AlD#0RH$;3+t{v?GdS&)!5dtfV+vWSPDvyBM*^^Nt%l%@a8}faEHXA1la?Rd}`@Ml^>-k zodi*X9AdqJ{?ndKx`?zU*-PmH*s3lXo*U~?Ph`K+{?jf%WK;(=w zz6QmZioeF#EhRhbAwCtxxM?qD-QLk#^Wg!7w%RMaNsEr0)2rC-Bqi)oV!RBKp==}= z>drVH7WBosFbRx3NlARW+mje`+!|mLwp6*O4``f{(mIwQzht(Oc~WR5l-#r3(J)P` zSl*89CQ<#F$8J&qsovYtLZiNVi@phIc)<@oe1e)ucd2f5Yw>2o$ zH?{kH_3gmVy)DgwrWUd760|bOdmBP2g(|kWVO7E6;3%aSi-b~%bfP;$qJM6tUERsV zfRaikA{0a8J#MC@`(^*Mu$XeEnXp{D$cUuOuCxnEoMtU@xsGD z*{80K{C&2-V#ZL--14^D(B?r09%x$jZNfHjTl0M@f^X7(m`{=8(dGm@uCD*=i)QFp zw$4_QkeXYjcF`k7P#Uv}41l|seeU5M^;<|eHMprZg3njA@pTbMx42 zY(7kHuE_J^%_+PqdY!Uq)US9hu@>PXbo9lUxk#2$Ga(_AVeC9+7Z`ul89{Obo=qxD z5_5t*dWn)E5iYWC5xu36&_e@O3W9Cy#+VUwc^iXecXI5)`e=rnnMZXcKPWQ$A(S?7 zV95`mw0dvWLTU}DeMdHIG(v$CVtr^BOOft7 zRf(~TTa9JhYAoZ$$CB*1iGdY}P_WUGFj|Cz5H3oMj}{pq zU4@Cc(L$;a;Tsu;O9;?>wTUrYqlF8$eeDky86kbRyoTYz&`{-&ldcIhTm__y&dwT_ zE;2&;aQSk>C3t)ioo*9yxSsy*XkEBBj20OneYE_B(eh!xjLD@7_lDsjBcuPgm!l^DC#Wk1<2O+iJ`ZxHu zOl{Y=O-K4e7Yu`ci|q)h1@82al~1YRU``KNx^be!SIEiU+r-Sub>aiq{F0bE&MdO@ zSE%dnbP@1JOCLep$3b!+f2S0RY8&#m46*IWn8iio`?k?JK}fJIzYLy6%uM8@N8I$k8rh;a zyTSj7R~}n@R9zkU)7T9LTE}Sm#_q5i3^$(O{D!U`!dmF+S)_EUbUlxU)EYpDmn19Z zV!z-8(Db=iV5;2Z^05`%^K#ZNLox&Is#N8!~ho#Mk#gldr zfB{im zi-PFM@n6Fj?o1@6v`2dSQp!SPQIH|9Ai_#+L3Hy$8|bjbreYo|c{EGC1X8j+EOjGZNh3r6J)(E72vyY!I-#Ju*gX+&r3c&8EJxsehSMWzv4HE(&9RB$swS67$JYK^$X zAY-a=SnpIqB2-WjJ=7A7gqCob1cwR_8PxM24YhM!}dg9D$`qX6Y2aO%6t`c5dFG&8M0CK9dIx4RRDaiI&%8CHq7oH=8( zODE&Z*?mK`rOnqCY-!=^TO^O47^=bgpwQ~CZ}UlfON+0qttrr6R-)FcD;_!S)fL~# zDQhQP%TRr4FV2ggV3zLwY8tFcVS5_)-W$U9Ou1J*Xumh~4-MM0{xQ(Ns}r){8~TTa z>{D0VKf4tAnFPZ(FpLM?e+3QDK@>Pd^>WDuyedUAqhtJ;U zxW9kxgMI2=T3z?OZzWYt|#BM`tokyv2XRfv+9VWrvLSkb(!Pddg9ub{(AJY zzkNKk{`WsP?8)CfHGBEVcP#z%Pv1P}!0g8w4?gwUFU`Gi?bRQ;_VEv9t$6D2xxZQX zkH4RNz`u`Ln`{Vgd-JWjLofOHm!JB_ZNK})%l{4>b^m+q+YaF7&c1M#a`7+zU+=rp zT~kKpf9G@0ePMXv-0QpAdau~93SoPXD2mbzs9qtt^597eq z8y-LU&}mJVxV``W-%G#nx8cK@kK1(U3om@OF7o)j$>SDW`R(IYUVQHULxFGn&+Lni zx#hF2?XNZeC35;t>JI(SHPgQR%2n@O`plnJoN~$sN1gE6JO6#uSt~Djbl-Enc*g%d z_tx@(RZ}1O?)pUCn%{K!etY@Uhdy)dzt(-F?!zZ^3~(#ozv9==?em3Y$G^3Cqjc_- zHz~u1|KQ-4r~Re<5m4Ex_JA;xWR$HdUg2` z{Pd^(xcSZ}>OS7n_3qmz?f>T6OMkY{7vy)}KKiVczdLE4Q|>wbpU?cA`_{`(FMYJ` z(D&Y)bKhZyKljaF9m&^y{i)B*4bACTA3FG355CfJ#QT};tLxtWRR6p2?{9cnn*JpJ zz2`r5fyR5MOd0y4u=F?YA9~Yc?WfkgzbW#P{6OHCIbvgb@|M4S^vo$8XFvSW z?T@v2{`~26m(OT<^E3PIJ9N?A&;Rh0i@(0*q@KEmZ%iyu{_BNDKmU>Ghd=kkW?}#2 z-$w5J$USrJpD!I7zIw|^Z+!aaul)U;^B@1`KhJySwcqX9(6CW@_R~-O{;|t${cGpI zO&5Rqv(J9A`TrWPf8Ztl=Jn}=UtRyoj=BH**Athozv%dlzxv>qYh7=B`~9c9>-vB8 zdUX1{XFvJm^1J_DAN=~XpN4}+Jv(#%``-9owk>$?@edY0_?0`m4?gw>H~-|CY|m)Zrwipl=P{y z1IIk`@v|>XpV)WcE9q7DzIg7N(_HQAmQB0o%(^wNKk&pS-?;MWw;%cR$cwj}@!T6v z{%+r$FZ|=JJ3n>nJI_40sO_4wR(|W8<@gJ1c~k6zyVf$yoUn?lp3f9(e+ zy!XH#zMnpRh>D8+_x+pMLPghgQG*#bx{bwJo^cIpL!o6<to#O{Tc>bCD+P*RE!kZ8M*y{(s{J@{S`{Z#`l7HN~Nt$uzRrmbr+yn1_ z`;dS9_PFVDKEFP_?}NJ@{rr_vPndh}$zMKd$>$Gv<*tv-ePa1B-{1d*=HGtmssFg{ zzm9(N^Up84^|Q-oB(CKC^3s!E*#DKg=0E$%r{8Y8{^2hk@s}^9|9C~}OMi@A`nAN! z>8IBH^QQm!XD8k*zC-@m>%QsXLtj37`jbcAaKJlfz4OSUC#?KS;wwt}wjVxl;^t?M zeEPyAt{?ri<*nGX=g2tsN(nrGwzH-)-j-QA}aaP3_GvED~M_{MvYj~@K= z5Bl!t|L>UZyMO!Lu18w#K5OugZFSeYxc#}|m;UGNul(d|i5Cui<@CRtxaHiF_y6Oq zJ@sGu;ErGZrs3P~KXBTTf1Y~E8!N7O<%3;k-racp{4WixIpEK4AM#Xa+KfLOy5_)< z&8Pa7zj*i2&pm$p;CXXr|M;Dc9CvK?;rG8jaQhQ~JwV-F(_rQ+IKL-(S7u=;qU|n7Yd~IJkPttmf0cI`#M5VC(8FM>U_ea_aA0 zgHu*-X>6W))zs*;<_lUc_}a9Uu6@rNoHlLxZ;u~>#~$HYxpV4MQ>TBwed_+JxmnG< zYp15T!LP37KGNJfI5lyd6hfgIBEP+|Ao@^ti!;R=<3U)DZjLdnZkmPjoGP zbNwOD+Dkwg+IOGDr%w6%vdd4c+c2%^s9zsD?bNo`j;qg{I;G>2(|`3s!<2n{ zS4`ct)q}HT1zbQ>VNx)o0$C_SHF4jyQVy1xs63yn+K3U;g=_rKf%QlotaJ2K@7DW>{r6eD|QIJ%^u~IRBw< z^?&x9H(&kJpF;ohi9h_)HTwro&3^a9Dbk#47M%W}&!2bU`E%Fq*t%-ln-d2Cbf2_Q%{mEtDz4{-# z^vf6TIc(8W++Fwn-|?ScaiSz{zJ0J$I`X|gU$F1Os~$Y*jW_SSZ*k`2Q+#j!X!SwI zUU0y;D!Ofo2cL%;m{-$(p)?LBzI{mC2dS9jsy?3>TH>pW7F z%%WF^+{eB57Oo_Jg?p9OdoH0@_qmz%>aW&+{*0$P4u9rnZ$I|D|GKV?&7VJ~bz#%A z+HO$Wv&L=0IlSXz+>}F%9sdv-G`2m%%*9x&a5`w5 z|As4Ub2MRs%XyhwmboQQE+yaTh7FiykGfhA_b}lGAh^&@oy|~ZsN*M;bZk|P{dQ#9 zRvI{T24^m?OXbz}0EX7Z66FpMwH!-+g1G<7%jdMV^MAZU=e}%WUjO_zBL3hV#Zz;> zXOJs@%fEZaUlq9j7v2F=^tX4crcBsii4%`ln3H{-D)i(k=~J<8VrXVap#%m2DZS1O z(Si_OplhDt46ew|D=g4+!RQ4N;^tlP#X>N8lg zY-VHbq!E0-j~yx|-sbBpq^&=<`zMUjgnzcHEg!Aogw-O~shOU#7iB(tXETlFs>nyq zK-fn1%~n=6WN0hEF^I!rS^q~wG>|GS=@%RAr^R2+Lo$<1ag!v_?uE}3qjW}!So4GC zPlaQROEmLtN2b)T*_ItYD0jrGYWcDV&yl21`Os#so!O4$Bn(rqe5x@-T61dt#QLe` zSJ>#zIv?}|1v?@lGw6q;lnI8h3f~yT94wxLQf#~1nD%!BEe@^p3E;33!$}})bfeO# z5w9zz^U2Aukl8}sqUKC~MVa;yh@Tm72kggSq~_bICTnH@?K;p z7#$Q$nIJI86s{m>cKzg-10lc7rey<%`pIJGcyd)#@S$UMEVS4RKZiO-{bdEs`&dt_ z@sq-GbFLC^xSI?Xf^wKac7T!62nhm(FaKr}8K#@fX#tLurwbm20c5q^q?1jzH-;@Z zU3#sYm%~6b4?#qGu?}NPIvBokMC9IR*!5E@&WJT1F*vIUr`~0M;$azq{XE;+JmR$b zhMj(+I1!R;X~ZW2mj;fEi^{m{UCbM^7Dg+=Ek;}o;QSqW?~$zI*uLGWX+S&Wb^^4u)cu?> z*QLPWMgkPV_u&$MQ=cMdUZ9u)D$}LkGd{Oc@euDEj?3z zvAx-MopJjp2pgIq=x!b`=vT|p<~%JN-*GB(aNKp%1j2aSsO^0lposZ;G#AVTZWyTL zLr4UH2gnPGzRg-A*r2t_v-M!t%ivO5UJ5AQ1UszeS+(_s2npsjlIFlAu(Kt97Uz_7 zYzW5ny=X5mHc0ew{RFGd1P;PRH0Bcv%n&pYN|C=48Fg|}ZS*f#AVHX!R{vSr5Hbt? zh-Yp;$dGmn|0~Wk%j9^RiJMm*OSqN|@pG`T7s#C?tWEr4c{xF^sEF3cI#O1}xt~}ekEvbLJAZj_z)n5r zTumYpo+`A)OKFUEm*(Z6m{$+<12lOv*x0DVyLu!Y0})Jiu(Wk8zR1}4D!V*YKHbHN zXIZ~-Bg4V+4<3qZE9P29{hE{~!g@z5R!5H-KPGIsDknxyj{MB9bCc@~O7Cws&kYY_ znSbOn6*qQE$J1zDBp!3b!dIPbM7HStu1V%avWC;ESFJ4C^M*&^)};#p`B**e50juZmh@e=p2=c=3-{fW2RR0%Fi@(p>99J$qS{J^HU! zU~%;XFk8ItqE-ltSalITJh5J~8X&#`i~7UokUnDw#wUFnoc?4@$@qjqRleaxo|A$* z+%@Bru|JX|SV_yyJB~LwCYgoBgOm?9cPEU!OwS$vBOV{x8PmmE^O+yXXop;&+hn<% z)CVW~zR^ca!jBV^LxA(REdlW{+3Sj0H;NfPf;nnpqn@Dl5>$puaDu-j<%}urIc58* z@tx>AEFLDpmqdS*2U&RAFj&^13ViRb2nNLMScbFm1jI)yJq!9Yy88wdWz9Y7_!JWLFCtY*K;c)SJc(~PO z{Gk2Mc|_=}K5J{lRki-DcS@$aYLodqUFN`@hk05!UjkI1s&dn{?gD zrAyXJ1}jeEGM6WYW~CVwzTfL@R3N;K{Y1)T%d4U}dKYB9H>EIX@-$1iovEcXanBg4 z#+BsGtmsY(^Zl%Peu2Caq4IN);f-9MzAL!~t4aPu2XxEggxvj#_;QwwF?kWX8FV1h zlCW(-(qs~u=2IPT3^{~(q8`S=WUVLhojf%|x^N1u@8{iu&R3SZQOtWd7l`0#i}|3R zS9SJ;N5-VgbSd8odrwEOzhfC^e7^SF!LgudpJ0C>b;O~&F`+vm!phcHKpz|7B)*eh zqcWTcb{VB-vUoKZ6=;85MQG&;>d=aw9N&AR<>j>=W)@q1+1!*g+h>4Yg0Rb?bBAb1 z!>4LUby31n9cCL~bLWMJLn|C?FB^tCK%ny}+fEMwv<>QqOqgj@9A zt@Xu6D9AE$|I^=5k6@P7Cn2i&Ghb)_)XJlU47~rAf9LDJ)`d?wa11o*=;%j& zcu8mc^E_xIZ zeR)R?H6)?dDSkEi(hOr53XWrUo_bs>~)#vGJTjC6D&Gl{e z<5k4SKryOA<;uJK1t;z;)5n9aeX@*o25;I&4BzrcuzV~OWsBxEGE;j$=lpU@?0}Xt z^rJ13jJ~=|gTVeiXU&h!JS8#d;FEjzq9ard=(zjw2cdNS)Pj0}ibXc%$P#=KS3O23 z$-O)p^zmNzjlblnB+b#fj=!Ux6Wj)WUs1Q~s>g=g#m{o32DJy>HG`lvyjGBt*P`Hc zNO`1oAYFUs&2!#FnF<+JXFN`D;SF-Lv?W*a);7l>J?dP9Uk+=03{GBw5VUT571P~#8J$1zO_5Azl7ax1leadQT)<%G%mmqYc| zASUN~c*X(r(9bO$>za?xE_<3G7Osi8Hr%5c9@`%n@BwA!b>W7JXK~pNrrWGR#r=gP z(qOaTk|Oc9#+pB*2Ttc^gGe;ZJO97HZaQn$vb_9GtvSW(fPw z!|TEQW0$y!EYEl?>%Gh#G_j{1SU&+vNYlTD3L^w#8DW95hq9JvU=AYWAgrvW5<;95 zCwc$}MMM>hSnqg>KtQd`)6!lcdg#7LPOF=Fds;NndZpAoUR6hK)wk(nKp$yUTm(8? z?(}B}r6@{w;;ovzS9@RY!&!r3@WbY1-4hwELBhMXk#q|&g7@jOMCI;PO(?@aY( zs1|qQf+0g>KMRpBfH!8Z>x!i;Fmwp)VO1zp^Z~Dp(bY7zgYn2+cFK2YW?FMd2%W;Q zkiSB=iPha>PtTAFOoB zzRck7Uw&n`ZQMI0Og<&EOiRO#P(BZbaUT~3 z-?^?Y{rXhld58N;$lH}8S!#6Rli)5v?;Bo=X*+2gi3B`J^~?TI#Hg(7yjD43`k>@V z_>HgBUmHiy5CxT66|rM0GJycA-4C5HY&0UF_sHhrNyt8CsXHqvH25@Y!BMQ_Wc0Ot zkC5gI_r+Sk&bHVqfe|{tY=I-xEXg*^RE=gfm1W5(RaG-G_wF0_t3;#aL< z@Iy>KYYuk2MQa`mbgdrTHL-)MYJKVOeO}1KZN6GeBw&iSmYL^;LjBJl7alpN3J~> zXr%EuG9&pXM)pD^jCY?hKV4$KS8_XePIkivX&xAeBwJeP)>uZV!g& zk>7mn?psLGjfmCEBEP*}?haFN>u!O5<~~bUE6p>1_f!AlnR~gBovMCY2^}8&<<>BV zOh`xfHop#ao<+)20DbQqZVr#TmIR-X!?4zA2+IKQudEzwI_q!S{LO zR3tg1d?8sY6fGh%GzZZRQhJtNRkt1NN;1-}m1q|Uk>0qv8P61O{M(0blt1wiw#1oI z?|%9v%Cp@i^%jBnJcdat7KwP2ep8J7{a#tbJeVORFioy)&d0RzI%QpIkkSCug=6X~ z_am~|!*A{Zno7x9H`j@w)CHx+jhI1JjO3E3r9;rSJ$GwN$y%|WjbqSOq`*|;=xbkaTK7}B7vw6((*)lWS z+9C=c9tbyNQqa=gJ8nXJf&_D51C(j9P~W5UvnH8?>k%tV!PI@PLYV~ zN7P@cu61f}c5C5;o8O}&v&&h7Jih#3Ru)QlswAbZvZ(McD~V-%f7e@^grBCDmGQu% zX5Eox@BQmBlM|Xf*!e2UeNuOlA0CZ$i*M*L6gpvTUhsI?saGR-1JI zV}-(x*7rJF$Ab|HqK!|?u)l7k6%F=H9^%%!3P1L3se+csoWUHKxBbbIoYEvxagj~z zJ$7kl$pEN6XeSD;X5NtCz|Km*>?*cyQ-s22SJ|fnrY-5H=v(YTY_Yem7oEtiD_=R5 zz({$D(C5pcwi>^FdBUHmJ1*Ygj2}Y! zzvbVl`>$0}|Ao5oAveYUYZS);rvlRNfI0n}x+hcQWUW>4N6vQNe3X=$+f`BhltB3n zd{U^0)o5!m4>dY2q183;aQf!B z;+hnF`vQ$}-MQz?z5A!=%b6@ND8OfkJb{()1!a(}a(UwJN!w4vh$BBE!yh$@l$ec) zX>~Eh1#@aTm6wbD+X@CHH-;jf9x(K5JK>Vw|CFtGL=(z^{9 zkqJ*vKI92?wE<=7b^Hp1AYq}H(-Ve{4)z=t%Q}^v9a%0$RG@?1lcmj$y}f#HumsyJ z4`;GR3cz@2DIyBV(}W6=At5d8;xB62M5~>LCM+0T%Uq#p5*jKWO;g{&)Y)AUsEvEW z3b_S$Y`GmzL4A_22pTFX#g?OoZ;o4=N|ol0qahp|h|<9tGr4nJ9dz)Mf*8RsQgmYP zMw8SDEUOklB?gu_giXH8l%brQ;-D|l$QS&)zq+I|P_%1BUe2McGRNd54HM*{ zKMmI>iIF4u{EUyESMjon?CHQd8L9s;5IEa-_3Gs@#+s`f zp`{p305%(5X4=*^udOZf`}fvIzG^}O3>`mFZ?C9xp|19*v=0GqjoZei_xb8e5zHHv z=#GBv2#{nWClw))2@Ajw<1==4RpoqC<`Pqr+OF4R^7npya~*h7j?GS-tfA4Rs2Dhu zV&2dgD{|-u3aVc*3LcL`JM?2_WzA*;~ZLnB#w9k z|Lqr744(x>kc*mlLeg@^EQ)w$`UJ^`e&2z;u(OHy-P0sN35HNFK%hWq2o1B5QDC5` zh(Um`htcbm#h;w!Mu?M0Zzx!8Mc;Lt47920L%)tU7)4GV5~5Zwm7LI0;9-pkvq*`R zB$r$EYiC26z_5c(Mvk3kI6}yM+Z|&WoxWHHS&77Mg=gS0mbaQd*GgaBEl_P5bJ#w3;CYFm-g!s!M=rUnc zIIUPv@(87NMN99)6eeLN+BnXy~RLy3FYe|56n)p%XTk1Bvri1(hgbiBp4bSo0h z!X@KJf{>t`c0Z;)-z|hFUtZ9uif#X~eT7lRjN9Fs^`C7I#nLq*0AbnR@(0`hDQOZH za@W79)@e197(%sY|DjrSJ=tU<{AYI;JA0+lHxqWdMgrqj#r)VDa~zoY15)Ygcnut# zhy|C^5>YTU%!88JZ^8NW@U#+m6imuL6Fz_GM{%WVNxE^EeiD4{vOC(Scm+iCxxKsX zPCN7MK6qL`cqr$Kqtb)L`O5K37rJ(oD}K$Gr8>Gfz8xy(vLvd~$zgN)Dwg?+!dBk` zzS~LlBN0I{7v^2dQ_=YmSXjHz{5DivV`!#)+v2tHov~&?pOwbSq`dk@Ge_#!u5eQB z`h-%Pg3*OxxU}HXTGp$i9d5y&&+5DmdL8H8?l=usr!}v3ie>T=KHbAB5=vc|@exGA zOdgbLiYyu(g5_Yt39*3i$V43e$L}5-c}p)@@6bMu9IqaJ$i3fp)7L`BxFDCBu#l4L zlx3lhNJTeGjdMMIBNVW-im7#lf6&uZgl=XpBG(+Y?Dj;Z2k_rU3_20}?}Awn!^So2W!Foia1A>iEz zZMzn6>eAjEOBF}rG%-EjNHGV{%0`K(?y)a(LP`A54&9@K;S=!>uU4Vd8p}|cWC9~t1qlbch#l&r`amm`+I{>uUnBR4tGlc6N&sTCTHiAARW2!+z zT_`tx)&g^EbIroQ$$xnqFp>s%I6Xek0~2sZqG1yA1jRkSdZeL`yu%w4$G{}vOD4JGthAdX9R#@tn?oHdLW2-dASiuLH-ACylM(&i}izU zf5zL2#selfglB)tAMp04v>C|KWl*Glac!d-|L=b9;(xd{l^wlL3_q;LMp-pAOq$YY zPJ7V!2K@(X`XTqeP40b#sD0)Po&~(*gg#BYWcl2l z>R*qOy$~qLog6^7pbLq4m4|7h8%^zcxNe1X{GxBNk?e@dJAZ>_?{{zt2tUtg;mVfU=8em#vzE^OfAUMzYQR|Jn_ zSD*t{QO5=2-l~YJM6pUOycVC8^cMr~zm_!&Gz!xMb+?(e#^~B7PB05_@;m43O|Q0b zlH??D- z+Es4E#GiS->m?I=AT73O;MI(|*Mqz@&)7V8J*p0OTYIJdm_U-03~$#*(~nrr<*o!n z6@$u@au=>DO%0|)wF2ge`-((|vr#G^?j(j8czgpy^=i-$j)=mikk>aZc2S(}lg-{) zAv*TO4?0~;$57QNj%kL|^0XjvkzE`X)=VMQedzM8Tom?lc-YExb(6(>syNZ8uj$jG zl547&Q->!Jg15A=I!Mm?O4Fo=Hd^<=oR5YMTl{iSns4`048t~M`B?&YL;{GGwBu>d z?#Tr`szBnMEJGR#^XYh7^E~iO-q(^B%^a5+uOE6f3Cxeh8UQ%!y;@HuQ(3a zOB)xIUV|bN49uaXqU}od8nGOjkbTSxgq;(PY(BeMiB7lSja@?H)-P4Z^X=vKyhIhV zdMyl1rZSVm%Pa|| z0pP6GnbuHAP?Uh0=aj0U`)(S9~j4&IPG}t7$ z7uCyuJB=n0wjW%f#%HBkBuOgg&5S{Y*h+y0C!M}Ww7KHrH{0_sG3*(_k}oe= zhHt+A!v-b2<%RysN{~UlKe54HNKjENgb9AjAK2heX;S|`HZc2}4F)ya-eZY_PIA9W zT8bx`A$!|}pzamqAyHytB7g*`VKEWFMLtk%Z!nE9M;@WUk5H{qqp9%{;fc0K+E(si zyqL|J;b-sX`cmE8_NR`HnwYD&-^S~>*tzjEsx1gCsD>9LXj@L0)5$dtXa!X-U-jkM zAKO%IPtXR~Cpnlga5HFUdt{Fc3s@eiiSK-;9-W8?k!hYtMco!SR{5kf1?ybgNH55w zmh5A2_*Bp6U`1i|UTJka_t2w5t~khgkvp&SG$vmZB^HRyi_JI9f;5r;8C9n4?mYg6 zL&(_y-+Qx*2mx;7fHXVS#)Dmv705buT~YnKL?`i!o?MReS7@iK;{IvoQwqr<;+2PG zNb6dGOI)IFdjvFI-7vbTrI1TiNR0E6sKMM2YtwHB9E7) z^?YAIJ}X6U6hs;|*H^w5a=faPf$DgWMXSY~U#%(l!XHl7aAK=k(Gmhx%n(__Zh z)SpwoLbYxtmgl2WMCqfoKSr3iPqwfb2p>;P4AFS5lo)Cu4BpY)Im%H-%6EX}E^f;w zXONk~kgY;k&?Gx}CmYfxTXnh|2j*jn+Pc$^lf?3zgZa@j!&041Lb$&HR7EP|%JS$g zf4aryJP;U3XLQgIUW`%>13Pdm9B+@fTCCNAd z^<`!NXs=hpsP9C+v!aF6b{0cH>lcxFA=k`RlM_jW5IFE>PdImen?_!1sExg=Qf$IB;Hp!k7Z{?N zmhS#^o5~9ZKDmW$q`qyW0(MKO;J&;IPX1UjAmGc|*ER(FKn-mZuC|U_I3p15|$s*ZhQcy2jz*LTQC|c9(;IlT)7Z1nD+&1yMdWC8gWDwnU|$wy9ri zi^>t*8&x85ax}QizOoB4#l{Lp>Taxjro`~`_AZrSaB4%rZqTu_iQZUEM~^UmLoU|Q z&Rllwp^qARj(+U{u1RZMlX#5`lwil2m}nc4f{OHeuJ(BngNm7Lx$38_kAQ_8fAkqz zk~EkMAVh;LuHRt2`IY7NbxxHZu4t&Qrdn+Jux_W`ojI`#aX?`*y}etB80}#J+sfY0 zGMy&5Sc7|etKUg2t-JH(D)ITgkE@qCpFQXzg*;u!T2N4$ktRM)WIQ^-r+dBvNLrI; zF8;|D@#N%Jr@3{m($O5f^=vL_8SL2Eh#%Bw?XH7BL*Asvml;J(4nrccnx#sH!tSmr zd)v|EWMkhS#-@p~ZuV%_NAWTr#Pas#nhZba$~$)CQxqj)Sg@s|!#7!ec=s;a*LTpK z^_RD#wGM8!QZ0;=z=opoP7QN~jm`ZjOEc8XVwaL#^&5uQX=E)6-*m4~yz6q8^Nzqt zn*<9ydUFjD`6AcR+Z#pI9}>6D)iuZgxya2u?_RBUaSVUA@-r*emX@MWwiMx1Uuepgp|`!GUPmoyx1v{B@_J)q;QV;h?RF`*&cTSji#xKj`Pt{WBvOJ{irMq$Qf|L4*hvz&9t!N_8Lv@# zGfs@B;WAoyZB-I(5n{fW(95P=kX>CCq+{aTMNS|p<-q#Yhs+;IDrdTLQ-H#wbF z_F2-$I!y=?`MkWO4TcH)<2)31JARm82+7f&eV5r!r;E8!AY6DQ;VJ@FHpo5Cs4NpN0&K%9hcga)@lO2 z1p{sZUloDNfq2IAt!&6lBecVf7$d3&JHD&aC*KD-r7fhXADulr-DiAz+^5?P*4=4_ z@tNbPwRx&sxQGa5g@i%GaEhVWrmmac6t!KTq{afw12L-3;Cf!qNJOJ1VSmO3-hOGZ z$DUGAHwVX~f7YPqk2-P}x6UNh>L9i32^;jZj8OUv=gccPUN6v_i;!5ZJwmKoV-MDj zx;;83yW*f{$z;HgHOwzCg~v_;d#`~|`wAFWV{vWXTN(|JYG*#nRC6n{jyhpdW;_oU zl}|3m^U#^@(?tz;CV700@VEXDi^iyI-;i&wVzzGMiUe1zt)C0^EgS8k_*zI)*FG}5 z23Y!IRtLC6PV^ule0(D8bRZ`Tv)~W+O^lAnkCn6yzC`1GVi&ti6_#}>PKU*w@HB;p zvABZPNUzu^08=%SrQWLNyBCp05onp6k>Pb%{`Yqp{g(sQ`N#b}EXsjW0xP4#-4!{g zSJ-&58J>sQ7M5IE*c+wGo4;gnn(7iSpFU@HxZd>rz(#+o*^x;Rx?Pyv{{yC*Ad%Lu zY%TtaJMYG(Bvya-&s91thQ{SL??X)IKpAsu2N)g?jeYLq;Tit2PqXqXJTexQTg{G+ zXm;{Dq>6WVG$zlu`wwMr=+{a<&CG3Kq8o5%v>aw3wtTBEpP~hv@T=47zA#O)6yVf> z^w8c=e3mt`30c-C4+|z++8bkL2MoEC@7VgJM8edKh!r zn7TwQvy0s@h5>}@HBO%9LRyJGl5}K1;N1V7uWOntYQ=42&a%;P%+JnLJ<7T9dsyP- zMHtoi{}`Xhg`?>)AQJz-AZtH(pAj8G=)dI;$of+*0vhBGL4S?! zem|RoiV&uX?X$J4FW*cgbq$YqYW|8uEak-}Hll};Ki`|CH^HnDpjE8Wb|dLe^J@I> znB$7OwypJulZj*Ssvq*b66@>tCcfsBxC=tQtpXA^*yUNvybFCItF;X>o z1!dwuo#KZ{SBCa<*hVq#&EKI!lRM5L7yIpncXa=yL_IVA6UfGYK8-qvc+IlbGOz?W z@`pj5{$;P6Uyxsa%O8~Zr&&P|161KFwkTZy|#g2<3B&ZLvGzA)ZOOECon z7c?ZltSe>{4;F9Eii5%8bLm`*gYx3)YVrtlGdZU_D0m`X#v<~tQVe&vODhB8E3%%^ zx+t4m6$1uYQ1~GeDas|$e#_+_F6Y2&*Ynp=q57p7oQMiv+{}dtJJCZ~-5&~dW(l`y zMf&iWijDnccUj>g^d|RDGcEvumyX^50dc#0+?E53GD4kNfhn&mOx5 z+4zsL;!_3U3oPX#AxHk8tfK7adu52X{g!`M7RR6FbpBge(15>{)tU6#re6#+@|k*w zI!pfhkQ&X9?k9=YR{67jM+Ohj-$=o#*jIVvBCtnHB5qhtS_s%=k$G)=rJ|FM`=XpN zwu&Tu%A-Lud~~y1-d~M}2oqtYlCZfYJ~~7SlWSF$7$~et79XAA%hL2O}Jb1EfG0f z)lRqN*dH=~g@h7}q%f~CA!8iGIo04)BDh9iXPJ2S3hf~GXYfy7#|stq^zR`V<=j{m z-}k3{n?w`T6%VvEW2BJuz7~hJu|q-9m3XIFqY&BKGv$Y+4CZ3e+>_?|{7htPs@B-kT}7X%l-X~-)L5BcIo}rTr%N@( zR_K!+7nWq5nc5L77IRS_-(JUT17-|bhf5v8r*eo*yz9DF0Ya5Gn{f7+uAl`O8VBr1 z6eTUVd{6)aJ@Ke@FbWb8<+;f4GHZmto$Ye^q&qisqsqpB^?`XEcatVn8a8fnXM8b_ z`I1YD_YLRT^C!#g(^$mAb9g*)T|N|FbC@%BWm0AazB4%fDqLaQr{k78&^4yeDgGMnoEf;TQhR(9TTXjn|;R zB5A5asRyiyml=K!P!T^y0^$s+*S5m8Cb$r3ZFZe2sC23cksWlBIp=QBP)~#Blc{3Y zGk*TKxKI_Fc;51b3gD#_`xyZ>MFqnZ51bYH(@u7kRu=Ek;y%;ajwJyGpKc8gZ<1fC ziww}ufL-3M#=Ea4?dq)X#fR0ZL-bhWo>-$JqE{QQH>apZ`SA=vIP^UHyEu1Ti)5Gq zP%Me%>T2n*>8p4ku+oL~STF}YPX-A2M!7cv%F7qHp&>}`BLPSufpcWPY%!3%ckTM7 zpCUr*69;63{H>lM^5z#oiLz)Q<9CBrGA`DD^^R-npC!FBeTdnVRe8vpZ$rs^ArDc_ z__$gEDYl`>4kdS=|By-mhaoNwd4sASaI;h){T_nqvcLKt`_AJ{CksHDK`_KH3Ax?Q zhUe8#nn48Zc$=|B4kYNoI7YOZL6LF$0Yb0|&juLy4e1dnhC$H5j-=^gIgrhwYis9n z4`^AGbwz0al?!XFC^l<=0a^djgBS-zP@p+VzfMQORiGy%?l24hLlc{v+mlmzi2ssk ze^U+R6~YE(5#F5#sl`D9f7Tt4vKa#hN6zkWB?Wfu_iDP6n?QtU#^T)Vkf4;G{B@69 zyYb|<1K6=B5Xx(kh=c;hmoV7l6A#QGhD7NdFlZWDr!DPj?YZ{h^MjZWH;MQT^i?KV zr_IeV2-;`Y;64*|61*ybj=&eKSO2RSuhEx2KKkw)? z^d)1^zZ60EZOh~OgII}d8z!}1ovZ~+LeXh|YE`a*dPdQ@GyhuvWTNWkocw#1GxQZ^ zI5`bT=W87Ct**dV*J*6)sCPqefi>4Ij2u}m-Ylhe3JD${`%CUlWWJxSd%hsaB5g!W zg!43W{lRr#f+me=9Ez#Ux3|EU8~ExigZZO2cPBoitrkoKGiE7Z{stONE^GydX77%w z%$Jk^y2lsSvpR>@FZ!_g2xt9O0P!36-0|H3@)xlWIMy`4M&5I5a|_9KQM#RUDM%{7 zKqVj>jhOX9r$ME~52X$huukL=LDn@L&gr2Q_8?!o0I~cU{h0TjHwxL6i`KE=Ot9ly zL||UbsZ^szw`U;;QqVyvfiD074k>SBbIpYXgBns8u{Q+l~(N3&)y`>ix6H;=*kYu7!Z-ee9ViZxG#j`F|;HR~*%U3*2DZNSC?*MRk(M3n5U$Wy97FI%^ryY>H3D_H|_Ze&Ad2 z`5s;Lyk#$7G8N>x94q+Anbvlk%Ck*y8*cP-uQCGO*iNK8qHGksSuNBG8uW(6Yol77 zZ{qXdwWis$`Q_Hgb%VKWkClSa3wRhfHQ5$ikG)4TQU@TCeSlQlXrT6)mU$`Uy$-&J z-DXWzoP^f-KHdE|^1x@nX0`b7G1loE1k4NCw1A`QImvmUU z1W@Q>iM@rEO!wU`T!31OecT}A7DX^+7@gEkB<)lYgz;WzQJ{c!x^+;*Gxv5BRzM|BM+P`uSJ2L7zV=i!~1ZNX*K ztSATois;*X+jNm;-5{AFK_1Y{c-oeI)-5@L)XmL75OT!2N!#ejA$+!2DvMkNOij*v z$1v-st7GN`A)s;#5%Lzu+c#ZMTb9M{)&I!-^^W%0t-=3xvYIX(Kj?rMM1eiKHe+?1 z9{Qb|yaaDC28E)ZoR|We?fv@bBPIP$rV044&1T+n#1NI>cX8UQKNuuI3)3ffSSxriK7a3TA2KUnCWw zEz-#1fS;g#7*L;2NCGj&2XSB|hB$vn0yf46DIg+-xMxTL3dREDC`ep5Bmsc2067Y& zE)@bI#`r=W(i_o>MgfolGy^HfDQjhbeV{;e31A5D5hMWuZ$^V)$YK0LHe-OsuW+3HAMtc$DxzM^RGaCuVYxVEDKEdldDjkbt~Faxn@NBk}&pz4V#z`tU=BrBg}jebE?JX=Bc0;dmC@ zSe7wt=nWL3=YEChQdmAR(~qfDi9Sy$pB?(+1dHpd0QPwI80owe$G?n>J9xCP5=zMl z3yN_M8P1s%LrL88CiNbz`JX_z!2W0Kk{g12dk?~TR9v^{u(xjG#u(lju-{=k$w}_m zlGtno370EK_)bXf2=)$|rgE*+iB*N=lK5AKUkLiU;oT5^$FL|N3ULrk{3nKePbesO zq)#>QXYIQs5^qvMG6a9izia*guOBP3=|aX@_)3i`C0)>75l_RKtWHp zge=6k4vwM^oiQQ`topT!ZW~|ZLJ2erF)(?N8RO0Em4=D87Gh<(g`N-D z${gj|D0z|7esnHrJ^)G3Hoki8erV^*=SQfONq(#4q8Pr6vw%Y~0uuR@ecmzt?TkXG zsN?aySCoe*x{ty9yd^=%we@X zXLZz9z0eAQk$FPUVR3UElF-_p*_|ZNiF@6O%=*6BMk{JH*a4ObWPM&xj)R)j1I}wu zxS(gWQ31Mz4!$NN-=xZZjWr<(JK8*XId2p{!}kVshi`!RcM2J*WN*BFWtf)5I_BKZ z0v87yyw}X>HL7I|swb-$%03h@jD4Z>cOMesyf!#?aMWDiG;PX+uM5t6Q2Mrg?e&oa zbDm!iqbKy-qqrV5+Us?~@X^4Tb!?7oc5ahI!fnnwy;t5dC;Bmnh?K$JyIVIE5?1Ch z-OIvjkn&@x?=T$H98OQ%vRVUmf=xRH^5{;EE*gdEe7r;EDz91#9W7VG>0fgG z%uAmF90Fp8qf6X9HhiVYq9N!F(UlxgNSGX?p$MycBSO9{bz)Es^$rb1&E2Nxm2xFt zISXzfKC#CczD?tyQdzVF%2WTUyMhJ~TD)(Q#g}6d&Pn}%%u8U=1kq#KMt-rM%aKEb z_rdUybI@I>!D9U<nH?;(K@l?J2cm{haQwu=35ON9{=F313Sl`te6M5c zidk5=L?}nR6ilEALP}eE#HYPpTX9TH0@@hOaW;%1U=q~`c+Sggx1Z~l_=5IWPH(n_ z7+=p3Z!cbRTFqHr0iahs3)4ll{hCR0FiVVozsFpzRx!yHpUjtk4P7!7Z%gr$ugm-y zBkh7<5o7SoU7wL5lj3}z)@NA0ceTl*L+rntB)pPWC6*-_ z;LI8Mkq?{E%9U#|8-5PwxI;bB@Ir0l@Wfra2zvv`)_-}yYf*xfzWg6r5hU9#<8YVf z{WGn!_oqxoLn!69{5!4uDcj<|X$2tmA6n7su!nqSl_S{U^U6+bd!oQLGEov%Az?c9 z5`jexz?MJ@JugTGB6rOs_Z1=c?WQnWZAA+XiTplVFqj5T)E9AcAGS#4YSU*SqeU4A zWd&deHaO3$F1NY6GSGNy6*RchpYeVfHC>VwX#Nn?I?a_MWn2{ABGB>bOD&zCBL5*r zZ>K>Pf*^6RM^Y01{L+uYSVRue@uk;R>t)Lkob<8y3g{Ti>2~ZRN#WtK3MuK9B(7#;N`YhymA9ju_{Z9C8&o;#a?S?7wE~BEU!EodKL?376p9{F;^(ME~3&ScdMg7jt}O}%*?aHx}TIj63dfZuBSNrG9(l5Oq4@S zw7>e^Ej$5?BD(}fu)kT#{NbsN^=iUZOb6}4B&xk5m*paZ3RDG7CCBRLj2~W zL|PV~k~XR9pN=UgYC~V%yG2#ut$}6s#;B?IlLb{pvS|`VP@SG>Y!owMFq+MTw-55P zYvx(AaYv^2tFP8nLq4fTECp8($y9GuQ0QJb-FVZ8O|um0#4%3LF5{|VB5`<~OwCLN zT24VR^Rct+IbCr`g}k_wF#2qnbtpJj^X;%;yi_~vU20|Oxx3OO#nq3{M2-o?x?D$c z+0$pO>#CoR0Mle&T+*EU2>vW#?`!Hgw+=h*c*+j3`W)k#DJAM@?2|fC5v#SAwJSpO zCX5BtHqu6lhe|xuM?dr{?B;Tzxk|whKRn|adZsssHLsyTqbue%7N=&MXPs@oZG{h#B}4ZPuB;F5;E!0f|# zp-48OSUw*E@v?E?9gWQMc-lkqFcVYXLiMN1^6yKr%8nWt?E9{q-5oU4o|^b@W%lKa zr4B-F(!|IUR5_J5xS9yiLrO5k6qrH1?uaz~9%6O87G;{Gm~1krx7oOGx`bWr(;h?3 zAv4poU=&cMf>q)>i1W@A8u#dQiBIf=>2>CCbs6-yWEp8RUXt?J{*Q!;6aoH0oLdBG z?9Bkv))M3VcZD}MnyNxDgd>Yn#s^%oJG^^csm2EiymGN@57l6!Vd`*!7BF@Am{~Ar0>yup*Z{(N{If zti1c!FhxHI#STejcM?9MWJp&+vg>`Ba*PP+i1M<~<@p%AUtLdH0SHpJw;4ZGTkhgl ziu-pztEX?49*$obMH~YXfFVzyfR0}e<&gm6*FPJj5dYUG<#|a1Xq1{5s}VxPK>3SC zc8mGu1$`1mj12s$HrLtvsW-NJY>5A^7-+$`wA%#>EuQ7pupG=6fiMvfdTLIJrG?d~2j68xNS z*HJ&%|HbnxC7PBRrO;ei@^{h|rJTmbe8KPI9yM0`lMRe5+Q36a=jA@PYl@j2#(++u_ST3(GkC{a!J{k$B9XCMS zl%_;Rj=}GTeKf5qg3)i>6&dO7FDaSs$a_Zjwh;i1p9Xc&CFzX=o<09 zQ!*L-8a5=A_E0GvE{}HQ3ilapXq?yVQmN*P0x#i&j`JyL{H5<1v|&K%d! zBvc!{u6vovKPWzU$^Ff{p=>5I{C;vS#MskSUc4P&}Rsjm= z+V@bx%Kg>@1nGZX2_?i|0jB!3v8WcopN)mkQJJcX7@3v@m}U-JsJ4m}P39BZJyuG| z3`7>Ktze(*^rVHg<=1Keyp$}J^2(!6Y((&#B6+K1K)(H%j%MgI865%s{KMs^Pu7dtrihGo+ZT%{hrEMF^o;+*-x%ml^}!+goS< zcm3j*)X9oy^U}!0>Drlkk zcTS;Ky!Ax-1X7Q#VtTYglkMzF$r&-jX5j8A+UJ)PrkgC14+ILc^-7eLyUu<8Nwxy?>K59-VI3)CthX0r~Vz@TTrhsw$ z7MIM|Awxc@w(p8a>r0XcmsXDQbPwlddrinW&T7XQvgNPI1uH2bw|5?zEi+tY@3#2) zVGrH(2_Vx0JR5)l+Cv}8in;Hhkl6oe-~P&q;oE3&i(;3#HA_4b%EwBA7u8k!?l*=_ zPos_-;Hy@GEtn!$9Qiq)e-rkrAw8*hW#NRv7@XAQ=!Ed_5mC-Nc?ZjdIS=j;oE#RB zRaSqrGrQ$9kA)AO=L-%aA2!rO;EWNllLUqsnDb~Enw$oyRd z9n>^z8WMvMWRFidcHW*JIc4oJDAg0JVOrAkTpw4xPivx4M`FmUhbR8vleNOz4^zOy*hh7#s#e1B47lYo7Ya{(2tLQ#Fp)VFcfs*%EXVxC&^4n2%g)0nD*cG3spnl)pE1o`kP zZQ9AGOo-XC&zVrNUKEyzwz>(BZY+w^l8B}tb9&4k!roF!)X+u`)_lz&`_m2+-=JxU@iUX9PdlolN0 z$sCCiT0mYEOfcH50{#fPRcUQl{}S1Z#h_^@Cd>*pS> zm6D+xE$;x&44{Cnl@Ddq{8y_@_CK|91h5n;rnikV$BZFIlbNy^y}mxew!jIRyf7+Z zi!0j6N>BK0@(m;*ZTg7q>>L8T?p{j6w1+uL)xIeA@1(ZdLWKsIl!oPmP=x zypMayNT;7ARlea|F|2uykcRbjE5DnVx$3!dHVl`Ep|r980qV%s&J(!+vuY_9Ik_kS z(Zz+M^W)9orLB*0bVBM>!KeFIA8Co1t@-?#X)h?F5pE92IkB2A!QO$tgEOZqJPho+ zI<|1WQ63vnurHeOO|vRhN=S0{T~pJga8k})m`Ff&AFbE(#kLpOwAqU5A$AT7wnqg| z=IZx@dF?E>ux!RERL3@!e4X~?%8X>d(+6^j>l0j>QErjldWjE{9r^-W{DHlxwTI5=9hB%HS za!xbOhS*AH6(^HMm9bf#GYhApo3k>U^j`uRm!?O{ggF}dmpyXiS}sOt1{mV%6hghy zdzXh@Uz|z9e;Cpv!o2&eHjekDBH}=uT{;@6)GzF-Y{+hg-NArs|J(F10spj?AQQZ8 zw=K!)CNbPaYIC3L+I-Gg*W+Bs2*YA12n{MQgzHxdnBz9pQk*(v-brL$|5D=!~OUObL=cU`)oPeU)SP z;^IKS=u2?MlP;HI;yvslobbZN@2f?KmU}P}oQ?-S*4;Myy#Za@x3@*tv4E$=fBI1H z!5bfO{M?Iu*qiH`yU{1108{z^0@|A&N^N2QKpB2@%}&oY>wg)1Qvud!+z-wzJVAKUfG$aZ!KW9Q&vY9Jj_@EtR;EG^`GWljQ3HJmf$vvH* zkd!IR${hP)UGI06kl0$7V7=wBRX3mZq0hkr61?vFkTu7$SevEu`QCGfjai{^5;wEe+(->P!iI?tey9Qkz{sxFN}RN>YSB}G!uYAHt-+95hsl}W-S6N$3h z6Zb`q-E>o5sJ6#ecaY`3`mERYf=_Hv(T&UbHQTqu1z!|BT3W41cdkGRWDq<1b*U=xlwI6r<#;6Mxa zbC#G`X@I6Xi*RcBbg99)ddD2{d3WA;=?#z6azXY_QX`BgWTq~fg>PYnHwv`#h>n?T zNfoYPO{s1r;#IC+S{x1h`lEornyAOlB)V#AegPBiB z`*h{ma7yRdq!J5pd=FiR&yK)ml^L=!2hq~ZGd~H>hHwksf($OI@O@-VTAi2@zGFfx zO-9t?Q^0pi5L|SO_luQ03l9CzecA% zL0&(*<*~;zDSsaLNIg61_>a7&OgHT?tH*|p$_J8V-Zw&{w#qkPEV8z_RqnyD^;6=$ zOqnbA6NBgB$%=}@n}+1{I@U~j@9F3g%i(gu1x*?kf^)7JxnDnhzPZ`6=PvI8qbchq z{`tB24WIf&YY~$3hc{Mz7G=z=Op5dF(diw;jZ)KtHAnSElJe;4#}HU4kN0+rmF&2* z_Fq7Bsce;19U`v{%;#I972v*ak_vnOox`m94C}*0fnI*rse@Q9tbzHciR2!k4R6Q+ z=f`s94@C0Rmlr?io%##I_P&%vb$nA<-b(4f!3@8=LLa2(+75ivQ5t4i2xWy2Avh8F zv)#p%!&TjSD!bgz9&V`U>J>v1(O>~R%sNE6HkdhaUZrXI5kBL5%=C5ru67;aI)W5h z(~FJXeq*KHpsjZ@+Djkw%~>1!j=O7KskznQ&OM^@<{BaxEH+X5rk~g_Xz(oMHh2v3 zm?k|*<*>UsjPm#UprwW8`CTM5%iO3uHdiGEeDlEjpc`dGGdo~%A5cK$%mEUu9!gvU zuHgGS6wWbNY5_vwpADOKl`W(-jBxo10P`8;sG!yNpoQ53mM552lkn!n>*z2ZHKWO# z5Sd@)sAY%V9Q0k~46J+)=8mI=L1tnSU0v7A=S(YahiM?0Di6eVZ_|-A^ZL5FrCRZs zD2m8y%=6+hZ6o#AbKh-o!-MO+YDB~Q5*iPf2s%<2DK4f6BUPAgv`)9$&ec>tb(%#F z_L-Ap$vN$I(DqsA^qgZp*SE(Go2TBvPQ*iFJ`e(s5NBkh~$sDPgYDs=PjyD3IU6${tU(o$U9ki{BaI7Q7yznef|}F z^jJ8e_v}^zJPsDVlFRp)qR?Gkqo&tCvzGYWhTm#IN(TE<~n;(wTx6svnbix~NhJgm5bJCZl)5Pt{BEZW#1*+X`*Fl!Now zakR*uD5VAa9-{59gn71z+VcyG_&$s?KUmA682mSix}$d zZcIZ!gAU<0eq|MAg}@5&SX6`a#@&Z3LjKbuV?)w3rc3rtVQ|vDgegs@l+B9tD0mg# z=JS3w+?_`rY-HK7C_GtmlFnT z9iI*ei+GwhSR%NYLc|b!@YJ7c;pSwj!aH)5EQu=GE5#Z#JMX^Hww}HfJr||lkq_grU^4z8!j@fl=dldOTSF`=zYt2y)3eS%#h{Pc6xGqRlzW^|YShxa8`;Ob zYCHIIq1wJpC$DNcw?aLM>&L9RlD*$Qn}_2(l*6hZ<9N&(AWX{;?2DlR`5{NcyPHVa z6sf_MJy~esG@1h~kH(dpaos*0Zj~Wal9op5NNBV_dlALOPEH^KCEY{1BTVmV>P~JeOuH3kb^o)+Y#%uLxci z^StkQkrp)PNv?Bc`vE&nErZP;PN|!%V$D{hgxiEZ?=@ehognE%(v`W+Cl9FtGV5B~ z>f!KP@_vPO+3pc<^!EhBk&L92o5C7Lv&Rd9v1Hs%Vw}+e0c@k`?SUcRUq?X+_hY0X zYE8S57}_bSe@e5Oy`}uW*dmA7kxAv((FO>8x3?15MZn~XKM4V;ntZ&HKLRxY-#id& zFQ0#JE(gR}pga(3AIeX@A8Vlv{xmyPrKB~DF!?sxx{xq~RD$~$p~n)jFRSRq!$ONB zCh>A4v@W(~&-WTxMgz@QnXF|v7$Ol3^jEU$VwDgRVzV=`qc<3vgO-`v+mVnQZdWfJ z2lWm41pAur`aO#?`>@e=dwt{3dfZ^}l=MfNo__%tEExtTaZEdSDAbv(pR{_iZ$EFh za&8-JbJ@zuY;+o`S}@}X^bB;R%Whz|dC1i~CIVwjByB4@-hRB1No6L)iV89hwu+tv zJ>1jdNS3E0GRfQdGkPH}DBAE-*EahTs{L8Nk`zB%Ue3XU7o_5s0#7E)T%uA=?Yctf zM|vC`QWkrzqOKNDYALS{8<_CAK`)GGYW;Pp;1G4dN)B%LApbKcvHo*7N%P8GR>Rlx z;E`c*!+nKU5dkbz$p=WDFTCo<^yBu0^lgc1$qM-7 z$4l7(BBo7b zelp$>tUST^*o1X1)^olxJpWwex+yuBPF8&J>6d4XBfcn&{-W6J5&FyM$dF*ipD4e1 z6cV~!K*~=R%D&*Ryg5TYy(ZIW>(<2iqAktli$w3{Fs(6|C$WUW zEk|m<=s@hPk&nZ6WYTvfr0h%l(E$IeH)gq7L06Iqsm2A`fz4ycv#BcIRge9QDN#PZ z-!Pi0$2_sJTC*q^Xq&iAGzooTb7AHzVN7<@TH@+TM3%@oG5UBt`H~Ud3Y+mra^k%4 z=_(`9oZnXK@Qb{SW^uc$^nk)P2Fj+&Beidz0$>UjgF5>Cvdt$`*P9!*Qoiq+1Y`De z_0>7IopF`%yjkxv^N!Gh=H9IG*U;BXpyEm3&a#!rTngF!@pY*gY)(%DH^4A!M25*PHhcz z?1m-&tqI3uVsSqD^5pJxqP_{xc;?Fs0*4FBe8H14mkD#9JzhO(k9$U)XCfMp&!J)+2j*68&`>tp^qIp zoMxk5*r`U**ra7K8`T@}#UGAP7fqNb;tpOY=Pzx3D(R*dMOjcee*JUz48lBcUevpb zqB!N7Fit%=-Ve2&5c2$;qn$Tzue|dqxXzG?UY8_RV>0u@JbMw+>EL@b&&a}$xVqg{ zkp^x3O7M#ZR$zVuO$o&Zd^vfctRSi>ctS=J7Q;)1Fnkr0$3gysyZLh^-1`W8Oa=Lu}v1eq;AsV^i3 z{1u=_;r7-z3LiMk`6tGI_mYrUT7x=xIL6QSWp=XwN&@A982?bt-Tkfwh|T|WE&SOt z=xlUieLjh`Y+9-8ORPKn2*u5e=o{gc(D`X?fN{o5a84LIe8CE`K z?@JRNg7z|$VxaJ;Uk-JLb;NM>Ht)rc2r8+$s^{qxvu9G-L*8L!+X$t7J3kxqGt<3PERI%yBoKuMN<9ZRZ`Hzu3>f8?o6r zF!&aqx?P(RMTsCh){SgsspQ3Tlae2uj%{G9lAAiK6kQlU&b$j5R zBF(+YKorJ6p4@z88pz1)tiJG;&5T%yh383{U={nOM0CCF_3?&1p}|m{RVWf4jDXiS zs!*f6 zfV-;>w+8HhyS;q_r5%1((Tq)gW_D8thCN)-X!75-C;>X7f$~5_dnmo~{)TuE@V_fs zX~Hm|M;f;`!^Jt4RU(Bwjj#UHem)_v6{MyX@~e57 z#8iY>@&~I+S+1XzYjBD%$62l7Sm%eE)2Jmhk}cz8j`5vM-li>n>O1Cp`tI>*oUqm_ zKZ8K&SO+tuB7WQsDtLLAzEEnm8$4(1^lzx)IXI?$-t)P&wW}9C+Yb1G3OghLIg?dm z)x0e52{`lD6U`n3Z*Px~!r|@AUklP7bUf%5T2= zbPeqQ9-Qaqa2hNZNA=m%dPd2Pc6+t8cGT_r`9{g*S@Ve(J-hO1MXIax%YL5&R??=r zK*hyH&O?&w?Rl>h)pWb7)w1K0{4-xpJ2mYO8kfEwzpjmabZOjOCbTSMAFdMn8ENoF zCw6MzS9p?>F%kR629HNB?8nE)S3CaKu4sek1UN?f*Hv^bnwg_GPlSZx76d$g;Js|F z7qf032z|O?v#aXwFF z6yC4NfnhvSKV>hi+mzyn%2=3Gy_FP}rmyQO2 zF9Ft=douay&J%;4eENybMkM{PCw>rFd@~Iw36uvs@u7^o|MJ8?tD$u(eY75zLrZ%< z&b!M$oBNUwIsrnE9_|nWk!&-F;ED1Tl!bEX4@f>=RNZlcC;Skh(0PEBNBA*wq0o~# zCS*;`QCXa|Tba!LX-@lNzkhIv>T~&&3WwftLjv(L3^HpK(v0mJA(2@3ZElWnS{PAB z`D-ocshH^exEL6gvSE6ms{>nvpnAWY9@KapSo^hvaBf!G<;54*%%A00v zwEB2`gkQ%l@Mfj+m3UgWcxhYXaWD-RP3bWtnTf3#mce@4R><5Co~n`fGjq!FE>(QN zQR~LV1%5WzbxelEt;xb7pNrM!4$DJoR$I!iOVr+$1iue(c2B4u2{atQY(!*s;f5@ve_+2HhVdDx9{Jdx-rVGM#yO+gfZ!p{y&gY31-H&8 zKB%=`GOz!JtZq%5PWMyJSY*KOmt0RmUB0{8P%1?e+YFeI{%~#32xLx~0MrDaJWv}R z%I_isQ1O3_tz0B0-2hfN4gDRL=U!QfE6U^Y?Q^-n7;{%y4a>)|i`a?63DJ_%^#eAx z77&rY-c%um8h+u$?<)@_@NEPmtQ8J99?-|L*)>G^J|I$JN33u*9{H{8+oto>Po_i4 zP3P9V-ru^`I-igHG@fqu(PLzL_p0<;HI5aRiX}$6iemIP~Z5G8mME`h+@N& z=W2~?9vEQ4OF+78ep7KWV^r=P_bjiWBXGG!2q~F9kBb!hjL=fqf}CQxJyzHcx2BPM zqhVI~fstUFq!t!uyMJn>IDb({iSFrct2ivt5#^dvL>Q{<>rY`8XAJo3YpW9yu#>}m zC6Ah9SBeSdsO3yE!BJh%1~tmLJPFEJX~_gdrPy{K={Pdlyg>BwuVrhne^T@!s4|RF zH+0XUm38x{$He}63e+%ERzrNsqc4`kDN)%*&}pzah>`mpmK+bL(*B;#7E~d4+pr?B%%G@OWexQ@njKuM_z6xI8 z^S4a(?~wGT@HhKvdM^;buZ9QJgU;WcV@?qY9-*m!y~xnb$r!6T8uLZ3w7jWCEm~sv z^o*c9J&pC)gWp8J9LxY3(REC*s^#p=HwzzYGn#b8BgT|pCo%A(?P;*}6<`+f?X8{N zYl6F)ko5?5gYI&R=i!>*XW2Wu4)`@t9;gWqNgfNYq#4#)Z+J&S+ogQ%PEqpgSU1tEaj zXQJa+>#RjPKP(&?p*@r-ga#!N&ru{aWqtZMyW}(d<`?-*M8|TWnGOwASa^@OD0oKO zdXxO`=dD=bG^|Wi;xccoZilXt+_cx#6cUXNW=RELPf#;2&h49%u*u;I!$_!~bvao=5HbU>^LrHJT2zYVfbM{dmE4(@IVO?VYN67h5;jt;(~DXuIf$H+UC#n~s> zb+u9T#;vZeWj$38BTEx70IC?jRwAhWJ=aFR$2!;77+DTm?xQbWjr@hA#Bwy}s0gmN z`kNrhDts&jgt!7+qON+B9wy5HcBGTdH{1x?x2$jFl6Xa4GGCVoEuWf{GOV#9smx?= zT~^}w5@ux`bhI!SV^T;d$g<3GW9m-AQMJH`5n)13<66PG=Thf)o=52;Q+%etOq>Wj zI5~wEdjFYYf&o(j+lnw#OeT{N8sjsQ{@PdlYz8M8^CXtBn7N!nH^<=WN$*uEfX?97 ztnm?-BZ4XEZiv2AKR2>lB!Q3efKBoWc zx}qpvFCxGlyuB@@^biGn_~SBO^qzY8X<3DUZys=&g@~`=tAO7E?3>c+1rW$540vIM2EQ}i-T|~eJM*>$LHq-Jc57ahT z^hFLCCA*cgtGln@mNS~ZJKnb&E~oCBsQN|5v5?uRC+EwfSV`akbDf8wZ(r^=_|*a{ z%sFV#@|Mflce>rs0|tD z3$3N{Di=;}(IfRTwgtSMB7g4h9{hqxa>gpuc7cCn9kGeFF(T~8^4I6HF6zkJ^u#Ff zC=!S5@bX{1AtiN<6cb+&QRBSgqbQ@898Zc{M|8DNL1tyhi2j(vsL+Rjx2F4?is?|X z8^f*u8J<9Z2!q-X4^5C;GXj-5s&uK^oGrf&L zIS>81#S4dURfVOOV|sd?`L}?m3fRU&GrIiF5x!L=2n~u}AUx~{P^Q+l|Ng*va1ojZ zA2@Nt2!3*Cfg31qvt6j*0OJJ$DeUN0fEpr`0;NLQfF^)`rFH?vfN4Atm~AUiAvJQu zNR;~clejUAmqbz?xdA7)K#WLMQKUeOgKqTsj>js=;g4MAjcHN}-nL0q0(fSf0SEnY zbN^si(~R7*_#_1e`?geWeUF}xVgw2uKW>9&<{Nt`^aRWGeEP;MbQyj?(f|x_d#hvE zE_G*{i3@gDdw-*Vhix;x%keT4&?E&4=t{!;(Adj=HB0$^SCWY@!%~37*1!%N_M4>9 z^8v~8A|g7UVLya)lEP`i#6%v$PLK-P+RN8a!y%X(3CqmT!1)oUWMyN}+M?5kiHDNX z7hwl3hLPs1Hl4ecv!Jl-ABPQfR^ul9+`G10_^>~B4n-qE_PL|Yp;H|nCLAT6hu!h<9VG*Bi)LVoV@MqLdw%`i?}n|Y{Db*yna(RQMyokDrm z*-_9d%6OMYEo;wAniE*dWe^pi((5L(eHh@BJ^9(_|KhtLKCS_edD+_9hrtGWm(h0; zX*QK9++yV+3qe(q#Yk@-Pmb<>3E$d+=b40x6C!oy*oeC6Szff99o==p-%K13)zwUG zhPJ3PhP_zezMSMPrYj^4HJ?JL1%E%}=9Yyte zQZzM^Y!}_h{Lke1HjeYdA=|5vLO4U!pP)|%=_^mxe!wM zVqyiQ02(L=UqSM)OH|kOK$RPqw%4%QXb@yngOEx!dE(%;$gH7XDX3@gXVrMMjoZ2o zBOd8{@Ul4LQE#84&AaXmUGKG+Tztx^e)KkU96XtmjT0LSMK7v~b4;JAG)bII0_$lP zX=wUqHZ0%Hjn1#8svm=ph)4PKbSPCycFhyiDwvY^g>^HR>LAD%GKyqFXas$+qh%aj z#C|k&YjnUt=P{=h%8(mgE~TU}UpEX{Qb+iq(-rB_2;rwZt6DycsBFY2o4@3`znR4H>G{6 zR94>1q{s&p5k#-1iP*cvuw)T+lZ^b;sgAh<3BMuxRxy)2m0nGtnSF}2WcQplRM!)V zsfpmmi?VcOuQ?}I5fKB&WC$u)?}fyt*J)_;3VLWauDNr4OUV-6)eL-PcET^gDE-#- zIBXf=vE%yH-_9@;5_cv`s02ZG)qJks8EqbPr8}!C^k&r5F0WGGV401799E;?2hLuf zBj1!Pg=Y7LHqJ|uJ-2IVLzA=OvOE15#c>5CCUQ>2ls@739;VQBDi_|^B0;Hje}rY- zk@E~}WGP210una)o?XwaHiq4ro-yYNc$+t&^m2;fLS7ch}PBwh2z zc$o@D3_rJ6p>ydfOxau%A;S72C64+6DyqAqm|&B51=+q42P=42@^$0b-z0XC%zGu~WmkWmeRqKU$YrV?TTtp>q$N%$ z7L_Xq6KUJ7g1^YC`6&<~5N`x&4mu&#>+2ND*lt3>l&B<`-bHKnY^vD=Yc(}26PY)8 ze&$F{U2M3-i)YIgNB56%~XL2(#1A2P$5{4Q#qR$o@| z4gn~CONF*?PZzC1n6NW&COIuyIF!768WiNV)=(gz^O27zIQtYXZo389eQL5&Mt)Gn zE0+%JZkz}6U0I#<4TrQOovlrLQ5m-=O#wfx4yv9TCDO14=dwnZV)Y&`Zya1v;rP=|Flg!f3)fpGj z;?Y^m96iGKpiRO){A-FaSBh6C7|;EQ2k7S@8;f^UiXw~sBHWWBgp(xh z3VNBAq0F40l1+s00`TXzlZRDRTap~?9gF^t|93C1_}|# zpOT@lOds+)`gmkQfWH-l+FdT7w|l>v_ZE_kO*?YH=9Gg5r++`(sPN5nEy@N`TJSNt zm*zwj>`YE+&OS#DPx2dPq^~-mEWLTTNFD13IGe6kw3)rsg;lJNkjDLtaW_X#VsarJ zZsu0AmOewW63M9$ND@1eiN0gRqv{OSvP`R5HjfVtNAjB#S(fK|H~0%gqnr2 zcK>d^fJ0z{{c7!j2`lOSN-F{W9(cg~{txgk@XZEz=-3-sIy{q;dbkXr3**lLECWo+ zH?%R3v$wJR_hNtq=|2as7_i9&+^*{1PV`%4U?E@$t3M_Fw}ObkBEJq`xdea!&tzb# zXYcs$Dgx)f{XT#NTLJQqg8%!xsDBFo)))Am1uSl4rEg?tXl8Bl@8|yeV(|dac3|P( zyLtVak^a_M@J~4hOEW_wX+1X^r~h%uz#+5W2k?^ZKo$Sr7yh>n`TvB-=s7qV+5h{c z|EZWmC1yG-64asY;gkd3vYk+q|alAEoOgBF9UmF0c+KbY(Pp#@MDD0jCVi0MQ5 zPXJ>8hOzqv3pm`xtpM#_pxj+Mu%Ha!cKlxy)3db&by}Oc;JpLDDS>i#_kg800jBuB z(grs6MxcsYaKtn10)ik=fc;i?2P|Ol4rsdH=CZXnasXVvo}-zKHIM`HN?tP9i{M1u}v4(OlPdjh*l+#r zAweoRDM+bs<^c`{l)tT}UH;c4|GEdvpd5@TMw7A^U@@TlZM9$;$Oo+U3j|W3nf7`J z&{hlQ{7t*NEQf!DQIM*+;!M_qwwmgT|AyVS+QUjhOD!S21$0vZh2*chVIlC3Oa9Lj z>X+?){{Zsl%Jxx5wE}J|Q2wqI0=gh@|M0$lKtWoQ9cQ^k2{4ETl)n{~(Eo=4{-fwG z)Bb~bKbAdqC`Y6LC<&Cm6`k_|f%}I+{{aQ5sBk$%8W*6+43xWu4$!o6zQ4)8+jwSH zdL~BnOuttnzOP;<_KFn=KsBKJ4QUMn1^Ly3b01Rq?CIea005M~A(#=MAS?{F)+YBM zdBdY#`+$fEl)oX&QJ^5d@>T9b$R-p;2LM_C%$D1SqAGC)Co4KCe>+}}C- zuJYmKfr9)Rh`A5Bzaj7)B%}}&^Q=+}|+sj&f24k^<6c z?{5%!N0F=mNdf7#_cxZiqo`Jcq=0nW`x{r?QTXaXQb79c{hfa9D2k0BDIgv9{+2Iy zl;~!V6p)^Ke|w8N%61z_3P{(zzrVvBbpLY=3&!8Y6-T(eVe|M0+ z2~ZG_-hY3!tvg8Y3@8Xl=fA&b&>aMN9ux$m@84h7`x#hw6k7-=P`ZG0 z-21soca#W7kQ9)fdp~{Uj?xAVk^<6o@8^2lQKn%)Qb79d{X~H~$}$`%3h1DHe@Oj~ pat#lX0@8Qyk8Ivinvp - - - - - - - -
- -
- -
-
-

EXECUTIVE DASHBOARD

-
-
- - -
-

대표님, 우리 회사

-

지금 어떤 상태인가요?

-

보고 대기 없이, 로그인 한 번이면 전사 현황이 한눈에.

-
- - -
-
- - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
-
- - - - -

127건

-

▲ 8건

-

누적 수주

-
-
- - - - -

96%

-

목표 달성

-

납기 준수율

-
-
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
-
- - -
- - -
-

대표님이 얻는 것

-
-
-

즉시 현황 파악

-

로그인 3초면

-

전사 현황 확인

-
-
-

데이터로 판단

-

감이 아닌 숫자로

-

KPI/팀 성과 비교

-
-
-

모바일 승인

-

이동중에도 즉시

-

결재/승인 처리

-
-
-
- - -
- - -
-

대시보드 핵심 기능

-
-
- - - - - -

실시간 KPI 카드

-

매출, 수주, 납기율, 승인

-
-
- - - - - - - - -

조직 실적 트리

-

계층별 팀/개인 실적

-
-
- - - - -

역할별 수당 현황

-

판매자/관리자/협업자

-
-
- - - - ! - -

승인 대기 알림

-

미처리 빨간 뱃지

-
-
- - - - -

기간별 트렌드

-

당월/분기/연간 추이

-
-
- - - - - - - -

수익 시뮬레이터

-

가상 시나리오 계산

-
-
- - - - - - - - -

모바일 대응

-

스마트폰 KPI/승인

-
-
-
- - -
- - -
-

투자 비용

-
-
-

기본 패키지

-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

추가 옵션 (선택)

-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
- - -
-
-
- - - - -
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v8/slides/brochure-dashboard-back.html b/sam/docs/brochure/v8/slides/brochure-dashboard-back.html deleted file mode 100644 index 28747b2..0000000 --- a/sam/docs/brochure/v8/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,320 +0,0 @@ - - - - - - - - -
- -
-
-

FEATURES & PRICING

-
-
- - -
-

대시보드 핵심 기능

-
- -
- - - - - -
-

실시간 KPI 카드

-
-

매출, 수주, 납기율, 승인 대기

-
- -
- - - - - - - - - - - -
-

조직 실적 트리

-
-

계층별 팀/개인 실적 펼쳐보기

-
- -
- - - - -
-

역할별 수당 현황

-
-

판매자/관리자/협업자 배분 확인

-
- -
- - - - ! - - -
-

승인 대기 알림

-
-

가입/지급 미처리 빨간 뱃지

-
- -
- - - - -
-

기간별 트렌드

-
-

당월/분기/연간 추이 차트

-
- -
- - - - - - - - -
-

수익 시뮬레이터

-
-

가상 시나리오 수당/마진 계산

-
- -
- - - - - - - - -
-

모바일 대응

-
-

스마트폰으로 KPI 확인/승인

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
- -
- - - - - -

CEO

-

전사 KPI 총괄

-
- -
- - - - - - -

관리자

-

팀 실적 관리

-
- -
- - - - - - - - -

운영자

-

인력/승인 관리

-
- -
- - - - - - - -

영업자

-

내 실적 조회

-
-
-
- - -
- - -
-

투자 비용

-
- -
-
-
- - - - -

대시보드 포함 기본 패키지

-
-

2,000만원

-

+ 월 50만원 (유지보수)

-
-
-

CEO 대시보드 + 견적/수주 + 생산

-

인사/회계 무료 포함

-
-
- -
-
-
- - - - - -

추가 옵션 (선택)

-
-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
- - - - - -

1~2주

-

현장 인터뷰

-
- - - -
- - - - - - - -

2~4주

-

맞춤 개발

-
- - - -
- - - - - -

1~2주

-

데이터 이관

-
- - - -
- - - - -

1~2주

-

교육/안정화

-
-
-
- - -
-
-
- - - - -
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스 | SAM - Smart Automation Management

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v8/slides/brochure-dashboard-front.html b/sam/docs/brochure/v8/slides/brochure-dashboard-front.html deleted file mode 100644 index 10ecdb0..0000000 --- a/sam/docs/brochure/v8/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,281 +0,0 @@ - - - - - - - - -
- -
- -
-
-

EXECUTIVE DASHBOARD

-
-
- - -
-

대표님, 우리 회사

-

지금 어떤 상태인가요?

-

매출, 수주, 조직 실적, 승인 대기

-

더 이상 보고를 기다리지 마세요.

-
- - -
- -
- - - - - -

5.2억

-

▲ 15.3%

-

월 매출

-
- -
- - - - -

127건

-

▲ 8건

-

누적 수주

-
- -
- - - - -

96%

-

목표 달성

-

납기 준수율

-
- -
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
-
- - -
- - -
-

대표님이 얻는 것

-
- -
- - - - - - -

즉시 현황 파악

-

로그인 3초면

-

전사 현황 확인

-
- -
- - - - - - - - - -

데이터로 판단

-

감이 아닌 숫자로

-

KPI/팀 성과 비교

-
- -
- - - - - - -

모바일 승인

-

이동중에도 즉시

-

결재/승인 처리

-
-
-
- - -
- - -
- -
-
- - - - - -

BEFORE

-
-

매출? → 보고 대기 1~2일

-

수주? → Excel 취합 반나절

-

승인? → 서류 찾기 30분

-

실적? → 각 팀장 개별 보고

-
- -
- - - - -
- -
-
- - - - -

AFTER (SAM)

-
-

로그인 3초 → 전사 현황

-

클릭 한 번 → 실시간 수주

-

뱃지 터치 → 즉시 승인

-

트리 펼침 → 전 조직 한눈에

-
-
- - -
- - -
-

대시보드 핵심 기능

-
-
-
- - - - - -

실시간 매출/수주 KPI

-
-
- - - - - - - - - - - -

조직 계층별 실적 트리

-
-
-
-
- - - - -

역할별 수당 현황

-
-
- - - - 5 - - -

미승인 실시간 알림

-
-
-
-
- - - - -

기간별 트렌드 분석

-
-
- - - - - - - - -

수익 시뮬레이터

-
-
-
-
- - -
-
- -

클라우드 기반

-
-
- -

PC + 모바일

-
-
- -

역할별 권한

-
-
- -

데이터 암호화

-
-
- - -
-
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-
-

뒷면에서 상세 기능을 확인하세요 ▶

-
-
-
-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v9/convert-1page.cjs b/sam/docs/brochure/v9/convert-1page.cjs deleted file mode 100644 index 9846c6f..0000000 --- a/sam/docs/brochure/v9/convert-1page.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const htmlFile = path.join(__dirname, 'slides', 'brochure-dashboard-1page.html'); - console.log('Converting CEO Dashboard v9 (Minimal White + Indigo) 1-page brochure...'); - - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - - const outputPath = path.join(__dirname, 'sam-brochure-v9-dashboard-1page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v9/convert-2page.cjs b/sam/docs/brochure/v9/convert-2page.cjs deleted file mode 100644 index a635175..0000000 --- a/sam/docs/brochure/v9/convert-2page.cjs +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - - pres.defineLayout({ name: 'PORTRAIT_9x16', width: 5.625, height: 10 }); - pres.layout = 'PORTRAIT_9x16'; - - const slidesDir = path.join(__dirname, 'slides'); - const slides = ['brochure-dashboard-front.html', 'brochure-dashboard-back.html']; - - for (const file of slides) { - const htmlFile = path.join(slidesDir, file); - console.log(`Converting ${file} ...`); - try { - await html2pptx(htmlFile, pres); - } catch (err) { - console.error(`Error on ${file}: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'sam-brochure-v9-dashboard-2page.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); diff --git a/sam/docs/brochure/v9/sam-brochure-v9-dashboard-1page.pptx b/sam/docs/brochure/v9/sam-brochure-v9-dashboard-1page.pptx deleted file mode 100644 index 9a39ae4ce77fefd27677b8933afa0d70a859ebe7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134597 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

&aP4Z(j-?>| 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 h)@1(-lK&1I$hFDB0H1^az!Ts - - - - - - - -

- - -
- -
-

v9

-
- - -
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사

-

지금 어떤 상태인가요?

-

보고 대기 없이, 로그인 한 번이면 전사 현황이 한눈에.

-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
-
-

5.2억

-

+15.3%

-

월 매출

-
-
-

127건

-

+8건

-

누적 수주

-
-
-

96%

-

목표 달성

-

납기 준수율

-
-
-

5건

-

즉시 처리

-

승인 대기

-
-
- -
-
-

월별 매출 추이

- - - - - -
-
- - - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-

품질팀

-
-
-
-
-
- - -
-
- - - - -

즉시 현황 파악

-

3초면 전사 현황

-
-
- - - - - -

데이터로 판단

-

KPI/팀 성과 비교

-
-
- - - - -

모바일 승인

-

즉시 결재 처리

-
-
- - -
- - -
-

핵심 기능

-
-
-
-

실시간 KPI 카드

-
-
-
-

조직 실적 트리

-
-
-
-

역할별 수당 현황

-
-
-
-

승인 대기 알림

-
-
-
-

기간별 트렌드

-
-
-
-

수익 시뮬레이터

-
-
-
-

모바일 대응

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
-
-

CEO

-

전사 KPI

-
-
-

관리자

-

팀 실적

-
-
-

운영자

-

인력/승인

-
-
-

영업자

-

내 실적

-
-
-
- - -
- - -
-

투자 비용

-
-

2,000만원

-

+ 월 50만원 (유지보수)

-
-

CEO 대시보드 + 견적/수주 + 생산 + 인사/회계 포함

-
- - -
-

도입 프로세스

-
-
-

01

-

현장 인터뷰

-
- - - -
-

02

-

맞춤 개발

-
- - - -
-

03

-

데이터 이관

-
- - - -
-

04

-

교육/안정화

-
-
-
- - -
-
-
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
-
-

(주)코드브릿지엑스

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v9/slides/brochure-dashboard-back.html b/sam/docs/brochure/v9/slides/brochure-dashboard-back.html deleted file mode 100644 index 46e438c..0000000 --- a/sam/docs/brochure/v9/slides/brochure-dashboard-back.html +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - -
- - -
- -
-

FEATURES & PRICING

-
- - -
-

대시보드 핵심 기능

-
- -
-
-

실시간 KPI 카드

-

매출, 수주, 납기율, 승인 대기

-
- -
-
-

조직 실적 트리

-

계층별 팀/개인 실적 펼쳐보기

-
- -
-
-

역할별 수당 현황

-

판매자/관리자/협업자 배분 확인

-
- -
-
-

승인 대기 알림

-

가입/지급 미처리 빨간 뱃지

-
- -
-
-

기간별 트렌드

-

당월/분기/연간 추이 차트

-
- -
-
-

수익 시뮬레이터

-

가상 시나리오 수당/마진 계산

-
- -
-
-

모바일 대응

-

스마트폰으로 KPI 확인/승인

-
-
-
- - -
- - -
-

역할별 맞춤 화면

-
- -
- - - - - -

CEO

-

전사 KPI 총괄

-
- -
- - - - - - -

관리자

-

팀 실적 관리

-
- -
- - - - - - - - -

운영자

-

인력/승인 관리

-
- -
- - - - - - - -

영업자

-

내 실적 조회

-
-
-
- - -
- - -
-

투자 비용

-
- -
-

대시보드 포함 기본 패키지

-

2,000만원

-

+ 월 50만원 (유지보수)

-
-

CEO 대시보드 + 견적/수주 + 생산

-

인사/회계 무료 포함

-
-
- -
-

추가 옵션 (선택)

-
-
-

생산공정 관리

-

+500만원

-
-
-

품질관리(인정검사)

-

+2,000만원

-
-
-

AI 견적 자동 생성

-

월 10~20만원

-
-
-
-
-
- - -
- - -
-

도입 프로세스

-
-
-

01

-

현장 인터뷰

-

1~2주

-
- - - -
-

02

-

맞춤 개발

-

2~4주

-
- - - -
-

03

-

데이터 이관

-

1~2주

-
- - - -
-

04

-

교육/안정화

-

1~2주

-
-
-
- - -
-
-
-

무료 데모를 신청하세요

-

대표님 전용 대시보드를 직접 체험

-
-
-

contact@codebridge-x.com

-

www.codebridge-x.com

-
-
-
- - -
-

(주)코드브릿지엑스

-
- - \ No newline at end of file diff --git a/sam/docs/brochure/v9/slides/brochure-dashboard-front.html b/sam/docs/brochure/v9/slides/brochure-dashboard-front.html deleted file mode 100644 index dc52623..0000000 --- a/sam/docs/brochure/v9/slides/brochure-dashboard-front.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - - - -
- - -
- -
-

v9

-
- - -
-

EXECUTIVE DASHBOARD

-

대표님, 우리 회사

-

지금 어떤 상태인가요?

-

매출, 수주, 조직 실적, 승인 대기

-

더 이상 보고를 기다리지 마세요.

-
- - -
- - -
- -
-
-
-
-

SAM CEO Dashboard

-
- -
- -
- - - - - -

5.2억

-

+15.3%

-

월 매출

-
- -
- - - - -

127건

-

+8건

-

누적 수주

-
- -
- - - - -

96%

-

목표 달성

-

납기 준수율

-
- -
- - - - - -

5건

-

즉시 처리

-

승인 대기

-
-
- -
- -
-

월별 매출 추이

- - - - - - -
- -
- - - - - - - -
-
-
-

영업1팀

-
-
-
-

영업2팀

-
-
-
-

생산팀

-
-
-
-

품질팀

-
-
-
-
-
- - -
- -
- - - - -

즉시 현황 파악

-

로그인 3초면

-

전사 현황 확인

-
- -
- - - - - -

데이터로 판단

-

감이 아닌 숫자로

-

KPI/팀 성과 비교

-
- -
- - - - -

모바일 승인

-

이동중에도 즉시

-

결재/승인 처리

-
-
- - -
-
-

(주)코드브릿지엑스

-

www.codebridge-x.com

-
-

뒷면에서 상세 기능을 확인하세요

-
- - \ No newline at end of file diff --git a/sam/docs/changes/20260303_gemini_model_upgrade.md b/sam/docs/changes/20260303_gemini_model_upgrade.md deleted file mode 100644 index e3806fc..0000000 --- a/sam/docs/changes/20260303_gemini_model_upgrade.md +++ /dev/null @@ -1,119 +0,0 @@ -# Gemini 모델 업그레이드: 2.0-flash → 2.5-flash - -**날짜:** 2026-03-03 -**작업자:** Claude Code - ---- - -## 변경 개요 - -Google이 2026년 6월 1일부로 Gemini 2.0 Flash 모델 서비스를 종료한다는 통보를 받아, SAM 시스템 전체의 Gemini 모델을 `gemini-2.0-flash` → `gemini-2.5-flash`로 마이그레이션했다. - ---- - -## 변경 사유 - -- Google의 공식 메일 통보: Gemini 2.0 Flash / 2.0 Flash-Lite → 2026-06-01 강제 종료 -- 마이그레이션 경로: `gemini-2.0-flash` → `gemini-2.5-flash` -- API 키, Base URL 변경 없음 (모델명만 변경) - ---- - -## 수정된 파일 - -### API 프로젝트 (`/home/aweso/sam/api`) - -| 파일 | 변경 내용 | -|------|----------| -| `.env` | `GEMINI_MODEL=gemini-2.0-flash` → `gemini-2.5-flash` | -| `config/services.php` | fallback 기본값 `gemini-2.0-flash` → `gemini-2.5-flash` | -| `app/Services/AiReportService.php` | fallback 기본값 변경 | - -### MNG 프로젝트 (`/home/aweso/sam/mng`) - -| 파일 | 변경 내용 | -|------|----------| -| `.env` | `GEMINI_MODEL=gemini-2.0-flash` → `gemini-2.5-flash` | -| `config/services.php` | fallback 기본값 변경 | -| `app/Models/System/AiConfig.php` | `DEFAULT_MODELS['gemini']` 상수 + `getActiveGemini()` fallback 변경 | -| `app/Services/NotionService.php` | fallback 기본값 변경 | -| `resources/views/system/ai-config/index.blade.php` | UI placeholder, 기본값, JS defaultModels 변경 | -| `resources/views/google-cloud/ai-guide/index.blade.php` | 서비스 현황 테이블 모델명 7곳 변경 | -| `resources/views/academy/env-management.blade.php` | 환경변수 예시 테이블 변경 | - -### 문서 (`/home/aweso/sam/docs`) - -| 파일 | 변경 내용 | -|------|----------| -| `guides/ai-config-settings.md` | 기본 모델명 업데이트, 최종 업데이트 날짜 변경 | -| `guides/ai-management.md` | **신규** — AI 관리 종합 가이드 (아키텍처, 버전 이력, 온보딩) | -| `guides/ai-model-update-workflow.md` | **신규** — 모델 업데이트 표준 절차 (7단계 워크플로우) | -| `changes/20260303_gemini_model_upgrade.md` | **신규** — 이 변경 이력 문서 | - -### 수정하지 않은 파일 (의도적) - -| 파일 | 이유 | -|------|------| -| `api/database/migrations/2026_01_27_*.php` | 이미 실행된 마이그레이션 — 변경 시 DB 무결성 문제 | -| `api/database/migrations/2026_02_07_*.php` | 동일 | -| `api/database/migrations/2026_02_09_*.php` | 동일 | -| `mng/views/google-cloud/cloud-api-pricing/index.blade.php` | `2.0 → 2.5` 마이그레이션 안내 UI — 이전 모델명이 의도적 잔존 | - ---- - -## 서버 .env 수정 필요 (배포 후) - -| 환경 | 파일 | 변수 | 담당 | -|------|------|------|------| -| 개발서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 | -| 개발서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 | -| 운영서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 | -| 운영서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 | - -수정 후 반드시 실행: -```bash -php artisan config:clear -``` - ---- - -## DB 단가 설정 필요 - -MNG `/system/ai-token-usage` → 단가 설정에서: -- 기존 `gemini-2.0-flash` 단가 → 비활성화 -- 신규 `gemini-2.5-flash` 단가 추가: - - `input_price_per_million`: 0.15 - - `output_price_per_million`: 0.60 - - `exchange_rate`: 현재 환율 - ---- - -## 테스트 체크리스트 - -- [x] 로컬 .env 수정 완료 -- [x] 코드 fallback 전체 변경 완료 -- [ ] 로컬 연결 테스트 (MNG `/system/ai-config`) -- [ ] 개발서버 .env 수정 + config:clear -- [ ] 개발서버 연결 테스트 -- [ ] 운영서버 .env 수정 + config:clear -- [ ] DB 단가 설정 (gemini-2.5-flash) -- [ ] 토큰 사용량 로그 확인 (새 모델명) - ---- - -## 롤백 절차 - -문제 발생 시 `.env`만 되돌리면 즉시 복구: -```bash -# 모든 환경의 .env에서 -GEMINI_MODEL=gemini-2.0-flash -php artisan config:clear -``` - ---- - -## 관련 문서 - -- [AI 관리 종합 가이드](../guides/ai-management.md) -- [모델 업데이트 워크플로우](../guides/ai-model-update-workflow.md) -- [AI 설정 기술문서](../guides/ai-config-settings.md) diff --git a/sam/docs/changes/20260304_eaccount_infinite_loop_fix.md b/sam/docs/changes/20260304_eaccount_infinite_loop_fix.md deleted file mode 100644 index e754f9f..0000000 --- a/sam/docs/changes/20260304_eaccount_infinite_loop_fix.md +++ /dev/null @@ -1,165 +0,0 @@ -# 계좌 입출금내역 부분 월 조회 시 무한루프 크래시 수정 - -**날짜:** 2026-03-04 -**작업자:** Claude Code - ---- - -## 변경 개요 - -계좌 입출금내역 페이지에서 **날짜를 수동 입력**하여 조회 시 500 에러가 발생하는 문제를 수정했다. -편의 버튼(이번달, 지난달 등)은 항상 전체 월(1일~말일)을 사용하여 문제가 없었으나, -수동으로 날짜를 입력하면 **부분 월**(예: 12/01~12/18)이 되어 무한루프가 발생했다. - ---- - -## 근본 원인 - -### `splitDateRangeMonthly()` 함수의 cursor 이동 버그 - -긴 기간 조회 시 바로빌 SOAP API의 한계로 인해 기간을 **월별 청크**로 분할하는 함수에서, -endDate가 **월 중간**일 때 cursor가 **같은 달 1일로 되돌아가** 무한루프가 발생했다. - -```php -// ❌ 버그 코드 — endDate가 월 중간이면 무한루프 -$cursor = $chunkEnd->copy()->addDay()->startOfMonth(); - -// 예시: endDate = 20251218 -// chunkEnd = 20251218 -// → addDay() = 20251219 -// → startOfMonth() = 20251201 ← 같은 달 1일로 되돌아감! -// → while($cursor <= $end) 조건 여전히 true → 무한 반복 -``` - -```php -// ✅ 수정 코드 — chunkStart 기준으로 다음 월로 이동 -$cursor = $chunkStart->copy()->addMonth()->startOfMonth(); - -// 예시: startDate = 20251201 -// chunkStart = 20251201 -// → addMonth() = 20260101 -// → startOfMonth() = 20260101 ← 다음 달로 정상 이동 -// → while($cursor <= $end) 조건 false → 루프 종료 -``` - -### 재현 조건 - -| 조건 | 결과 | -|------|------| -| 전체 월 (12/01~12/31) | 정상 — `addDay()` = 01/01 → `startOfMonth()` = 01/01 | -| 부분 월 (12/01~12/18) | **무한루프** — `addDay()` = 12/19 → `startOfMonth()` = 12/01 | -| 다중 월 (12/01~02/18) | **무한루프** — 마지막 월이 부분 월이면 동일 증상 | - -### 증상 - -- PHP 프로세스가 메모리 한도(256M/512M)에 도달하여 **Fatal Error로 크래시** -- Laravel 로그에 에러 기록 없음 (try-catch 밖에서 프로세스가 종료) -- 프론트엔드에 `서버 응답 오류 (500):` (빈 응답 본문) - ---- - -## 수정된 파일 - -| 파일 | 변경 내용 | -|------|----------| -| `app/Http/Controllers/Barobill/EaccountController.php` | `splitDateRangeMonthly()` cursor 이동 로직 수정 | - ---- - -## 검증 결과 - -tinker에서 수정 전후 비교 테스트: - -``` -=== 수정 전 (버그): 20251201~20251218 === -→ 같은 청크 무한 반복 (10회 제한으로 강제 중단) - -=== 수정 후: 20251201~20251218 === -→ [{start: 20251201, end: 20251218}] ← 1개 청크, 정상 - -=== 수정 후: 20251201~20260218 (다중 월) === -→ [{20251201~20251231}, {20260101~20260131}, {20260201~20260218}] ← 3개 청크, 정상 - -=== 수정 후: 20251215~20251231 === -→ [{start: 20251215, end: 20251231}] ← 1개 청크, 정상 -``` - ---- - -## 동일 패턴 코드베이스 점검 결과 - -`sam/mng` 전체를 검색하여 유사 패턴을 점검했다: - -| 파일 | 함수 | 패턴 | 위험도 | -|------|------|------|--------| -| `EaccountController.php` | `splitDateRangeMonthly()` | 월별 청크 분할 | ✅ 수정 완료 | -| `DashboardStatService.php` | `generateDateRange()` | `addDay()` 단순 증가 | 안전 | -| `InspectionCycle.php` | `getHolidayDates()` | `addDay()` 단순 증가 | 안전 | -| `CorporateCardController.php` | `getNextBusinessDay()` | `addDay()` 단순 증가 | 안전 | -| `PartitionManagementService.php` | `addPartitions()` | `for` 루프 (고정 횟수) | 안전 | - -> **결론**: `EaccountController` 외에 동일 버그 패턴 없음. -> 다른 코드들은 모두 `addDay()` 단순 증가 패턴을 사용하여 무한루프 위험 없음. - ---- - -## 교훈 및 방지 규칙 - -### R1. 날짜 cursor 이동 시 `chunkEnd` 기반 이동 금지 - -```php -// ❌ 위험: chunkEnd가 월 중간이면 startOfMonth()가 같은 달로 되돌림 -$cursor = $chunkEnd->copy()->addDay()->startOfMonth(); - -// ✅ 안전: chunkStart 기준으로 항상 다음 월로 이동 -$cursor = $chunkStart->copy()->addMonth()->startOfMonth(); -``` - -### R2. 날짜 루프에 안전장치(max iterations) 추가 권장 - -```php -$maxIterations = 120; // 10년 = 120개월 -$iterations = 0; - -while ($cursor->lte($end) && $iterations < $maxIterations) { - // ... 청크 처리 ... - $iterations++; -} - -if ($iterations >= $maxIterations) { - Log::error('날짜 분할 루프 안전장치 작동', compact('startDate', 'endDate')); -} -``` - -### R3. 부분 월 테스트 필수 - -날짜 범위를 분할하는 코드 작성/수정 시 반드시 다음 케이스를 테스트: - -- [ ] 전체 월 (01일~말일) -- [ ] 부분 월 — 시작 (01일~중간) -- [ ] 부분 월 — 끝 (중간~말일) -- [ ] 다중 월 (마지막 월이 부분 월) -- [ ] 같은 날 (시작일 = 종료일) - ---- - -## 부수 개선 사항 - -이 문제 조사 과정에서 추가로 발견/수정된 항목: - -| 항목 | 내용 | -|------|------| -| WSDL 캐싱 | `WSDL_CACHE_NONE` → `WSDL_CACHE_BOTH` (4개 바로빌 컨트롤러 전체) | -| 소켓 타임아웃 | `default_socket_timeout` 60→120초 연장 | -| Shutdown handler | PHP Fatal Error 감지 시 Laravel 로그에 기록 | -| SOAP 호출 로깅 | 호출 시작/완료 시간 + 소요시간(ms) 기록 | - ---- - -## 관련 문서 - -- `app/Http/Controllers/Barobill/EaccountController.php` — 바로빌 계좌 입출금내역 - ---- - -**최종 업데이트**: 2026-03-04 diff --git a/sam/docs/contracts/CHANGELOG.md b/sam/docs/contracts/CHANGELOG.md deleted file mode 100644 index cfd3346..0000000 --- a/sam/docs/contracts/CHANGELOG.md +++ /dev/null @@ -1,42 +0,0 @@ -# 계약서 개정이력 - -> **작성일**: 2026-02-22 -> **관리 대상**: 전자계약 DOCX 4종 - ---- - -## v4.1 (2026-02-22) - -**작성자**: 개발팀 -**대상**: 고객사 서비스 이용계약서 - -- 제4조에 사용량 기반 추가 과금 조항(4.5) 추가 - - 파일 저장 공간: 기본 100GB 초과 시 100GB당 100,000원/월 - - AI 토큰: 월 100만 토큰 기본, 초과 시 1,000토큰 단위 실비 과금 -- 제4조에 바로빌 부가 서비스 요금 조항(4.6) 추가 - - 계좌조회, 카드내역, 세금계산서 발행 요금 명시 - - 홈택스 매입/매출 조회는 회사 부담 명시 - ---- - -## v4.0 (2026-02-22) - -**작성자**: 개발팀 - -- 계약서 버전 관리 시스템 도입 -- DOCX → Markdown 미러링 체계 구축 -- 4개 전자계약 문서에 개정이력 테이블 삽입 -- 동기화 검증 스크립트 구축 - -### 대상 문서 - -| 파일 | 문서명 | -|------|--------| -| `01_고객_서비스이용계약서_v4_0_전자서명용.docx` | 고객사 서비스 이용계약서 | -| `비밀유지서약서.docx` | 비밀유지서약서 (NDA) | -| `영업파트너 위촉계약서.docx` | 영업파트너 위촉계약서 | -| `영업파트너 위촉계약서(단체용).docx` | 영업파트너 위촉계약서 (단체용) | - ---- - -**최종 업데이트**: 2026-02-22 (v4.1) diff --git a/sam/docs/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx b/sam/docs/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx deleted file mode 100644 index de1e3ed35ccfea3c3b4b0b6e91664c1f4240caa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39346 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

J( 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 diff --git a/sam/docs/contracts/docx/영업파트너 위촉계약서(단체용).docx b/sam/docs/contracts/docx/영업파트너 위촉계약서(단체용).docx deleted file mode 100755 index 1332c74876f5e975e58e9542a227fda12bcfcd05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25013 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 T2?hay0&W3db$b$nU%meUdV(v2 diff --git a/sam/docs/contracts/docx/영업파트너 위촉계약서.docx b/sam/docs/contracts/docx/영업파트너 위촉계약서.docx deleted file mode 100755 index 9e849b513362dd7b7d2e788da6d693dc289c2de0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33340 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

@zWj)5wQoxb|&()EWYj>^EmkuAe~yIqwH97YC{ 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 g;V|z1fdBQfmz4km^eDeZ&nQ4wfKfDq*RR ⚠️ 중요: - 월 구독료는 원이며, 영업 협상 및 개발 범위에 따라 증액될 수 있습니다. - -- 계약 시 확정된 구독료: [ ]원/월 - -### 4.3 납부 방법 - -- **개발비**: - - 계좌이체 (세금계산서 발행) - - 입금 계좌: 기업은행 170-175519-04-011  (주)코드브릿지엑스 -- **구독료**: - - CMS 자동이체 (권장) - - 또는 세금계산서 발행 후 계좌이체 - -### 4.4 잔금 지급 기한 [법률 검토 반영] - -- **지급 기한**: 서비스 게시일로부터 **3일 이내** -- **사전 준비**: 회사는 영업 단계부터 납품 일정을 공유하여 고객이 미리 준비할 수 있도록 합니다. -- **미납 시 조치**: 제13조 참조 - -### 4.5 사용량 기반 추가 과금 - -기본 제공 한도 초과 시 다음과 같이 실비 과금됩니다. - -| 항목 | 기본 제공 | 추가 과금 기준 | -| --- | --- | --- | -| 파일 저장 공간 | 100GB | 100GB당 100,000원/월 (부가세 별도) | -| AI 토큰 | 월 100만 토큰 | 1,000토큰 단위 실비 과금 | - -- **파일 저장 공간: **기본 100GB를 초과하는 경우 100GB 단위로 월 100,000원(부가세 별도)이 추가 과금됩니다. -- **AI 토큰: **월 100만 토큰 기본 제공되며, 초과 사용 시 1,000토큰 단위로 실비 과금됩니다. - - 미사용 잔여 토큰은 이월되지 않습니다. (매월 1일 갱신) - - 기본 제공량 80%, 100% 소진 시 자동 알림이 발송됩니다. - -### 4.6 바로빌 부가 서비스 요금 - -고객이 선택적으로 이용하는 바로빌 연동 서비스의 요금은 다음과 같습니다. - -| 서비스 | 과금 방식 | 기본 제공 | 추가 과금 | -| --- | --- | --- | --- | -| 계좌조회 | 월정액 10,000원 | 1계좌 | 추가 1계좌당 10,000원 | -| 카드내역 | 월정액 10,000원 | 5장 | 추가 1장당 5,000원 | -| 세금계산서 발행 | 건별 | 100건 | 추가 50건당 5,000원 | - -- **바로빌 서비스 요금은 고객이 부담하며, 월 구독료와 별도로 청구됩니다.** - - 홈택스 매입/매출 조회 서비스(월 30,000원)는 회사가 부담합니다. - - 상기 금액은 부가세 별도입니다. - -## 제5조 (마일스톤 및 진행 일정) - -### 5.1 개발 단계 (5단계 통일) - -| 단계 | 주요 활동 | 진행률 | 기간 | 납부 | -| --- | --- | --- | --- | --- | -| M1 | 요구사항 분석 및 기획 | 20% | [ 2 ]주 | 1차 개발비 (착수금 50%) | -| M2 | 설계 및 개발 착수 | 50% | [ 2 ]주 | - | -| M3 | 개발 진행 (50% 완료) | 60% | [ 2 ]주 | - | -| M4 | 개발 완료 및 테스트 | 80% | [ 2 ]주 | - | -| M5 | 검수 및 서비스 게시 | 100% | 최대 2주 | 2차 개발비 (잔금 50%) | - -> ⚠️ 중요: - 5단계 마일스톤으로 통일 관리 - M5 검수 완료 후 서비스 게시 - 서비스 게시일로부터 3일 이내 잔금 납부 - -### 5.2 일정 조정 - - - 개발 일정은 고객의 협조에 따라 변동될 수 있습니다. - - 고객 귀책 사유로 인한 지연은 회사의 책임이 아닙니다. - - 불가항력으로 인한 지연 시 양측 협의하여 일정을 조정합니다. - -## 제6조 (서비스 게시 및 검수) - -### 6.1 서비스 게시 - -- 회사는 개발 완료 후 고객에게 **서비스 게시**를 통지합니다. -- **서비스 게시일**은 고객이 서비스에 접근 가능한 날짜를 의미합니다. - - 서비스 게시일부터 구독료가 발생합니다. - -### 6.2 검수 기간 - -- 고객은 개발 완료 후 **최대 2주간 검수 기간**을 가집니다. -- 검수 기간은 서비스 게시 **전**에 이루어집니다. - - 검수 기간 중 발견된 하자는 회사가 무상으로 수정합니다. - -### 6.3 검수 완료 - - - 고객이 서면으로 검수 완료를 통지하거나, - - 검수 기간 2주 종료 시점에 특별한 이의가 없으면 자동 승인으로 간주합니다. - - 검수 완료 후 서비스 게시일이 확정되고, 하자담보 책임 정책이 적용됩니다. - -## 제7조 (하자담보 책임) - -### 7.1 책임 기간 - -서비스 게시일로부터 1년 (소프트웨어산업진흥법 제16조, 민법 제667조) - -### 7.2 하자담보 범위 (무상 처리) - -| 항목 | 내용 | 예시 | -| --- | --- | --- | -| 버그 수정 | 소프트웨어 오류 | 계산 오류, 기능 미작동 | -| 미구현 기능 | 계약서에 명시된 기능 누락 | 약속된 기능 미구현 | -| 성능 개선 | 명시된 성능 기준 미달 | 속도 저하, 응답 지연 | -| UI/UX 수정 | 사용성 문제 | 버튼 미작동, 화면 깨짐 | -| 데이터 오류 | 데이터 손실 또는 오류 | 데이터 삭제, 중복 생성 | -| 보안 패치 | 보안 취약점 수정 | 해킹 방지, 암호화 | - -### 7.3 제외 사항 (별도 비용) - -| 항목 | 내용 | 예시 | -| --- | --- | --- | -| 신규 기능 개발 | 계약서에 없던 새 기능 | 새로운 모듈, 기능 확장 | -| 구조 변경 | 시스템 아키텍처 변경 | DB 구조, 프레임워크 교체 | -| 추가 모듈 | 새로운 모듈 개발 | 회계 모듈, 재고 모듈 | -| 기획 변경 | 초기 기획과 다른 요구사항 | 화면 구성, 프로세스 변경 | -| 교육/컨설팅 | 사용자 교육, 업무 컨설팅 | 직원 교육, 프로세스 개선 | - -### 7.4 하자 처리 절차 - -| 단계 | 내용 | 기간 | -| --- | --- | --- | -| 1. 하자 신고 | 고객이 이메일로 하자 신고 | - | -| 2. 하자 확인 | 회사가 하자 여부 판정 | 3영업일 | -| 3. 수정 작업 | 하자 인정 시 무상 수정 | 7영업일 | -| 4. 검수 완료 | 고객이 수정 사항 확인 | - | - -> ⚠️ 긴급 하자 (서비스 중단)는 24시간 이내 조치합니다. - -### 7.5 책임 면제 사유 - -다음의 경우 하자담보 책임이 면제됩니다: -- **고객 귀책 사유**: - - 고객의 임의 수정 또는 변경 - - 승인되지 않은 제3자 개입 - - 사용 환경 미준수 -- **불가항력**: - - 천재지변 (지진, 태풍 등) - - 전쟁, 테러, 전염병 - - 정부 규제 또는 법령 변경 -- **기간 만료**: - - 서비스 게시일로부터 1년 경과 - -## 제8조 (계약 해제 및 환불) - -### 8.1 환불 정책 개요 - -고객의 임의 해제 권리와 회사의 투입 비용 보전의 균형을 고려하여 수립되었습니다. - -### 8.2 단계별 환불 - -### Phase 1: 상담(인터뷰) 시작 전 - -- **환불율**: 100% (전액 환불) -- **조건**: 계약 후 상담(인터뷰) 배정 전 -- **위약금**: 없음 -- **임의 해제 가능** - -### Phase 2: 상담(인터뷰) 시작 후, 개발 착수 전 - -| 진행 상황 | 환불율 | 공제 내역 | -| --- | --- | --- | -| M1: 기획안 작성 중 (50% 미만) | 80% | 상담매니저 및 기획/개발자 투입 비용 20% 공제 | -| M2: 기획안 완료 (50% 이상) | 50% | 상담매니저 및 기획/개발자 투입 비용 50% 공제 | - -### Phase 3: 개발 진행 중 (5단계 마일스톤 기준) - -| 마일스톤 | 진행률 | 청구 금액(개발비 대비) | 비고 | -| --- | --- | --- | --- | -| M3: 개발 진행 중 (50%) | 70% | 70% | 30% 환불 | -| M4: 개발 완료 및 테스트 | 90% | 90% | 10% 환불 | -| M5: 서비스 개시 완료 | 100% | 100% | 환불 불가 | - -> ⚠️ 중요: 5단계 마일스톤으로 통일 관리 - -### Phase 4: 서비스 게시 후 - -- **환불율**: 0% (환불 불가) -- **개발비**: 전액 확정, 환불 불가 -- **구독료**: 매월 말일 후불제이므로 사용한 만큼만 청구 (환불 개념 없음) -- **대신 제공**: 하자담보 책임 (1년) + 유지보수 (구독 기간 전체) - -### 8.3 환불 불가 사유 - -- **고객 귀책 사유**: - - 협조 지연으로 인한 개발 지연 - - 요구사항 변경으로 인한 추가 개발 - - 승인 거부 또는 회피 -- **약관 위반**: - - 허위 정보 제공 - - 부정 사용 또는 재판매 - - 회사 명예 훼손 - -### 8.4 할인 계약 해지 시 추가 조건 - -본 계약이 정상가 대비 할인 조건으로 체결된 경우, 다음 조건이 추가 적용된다. - -- 발주자 귀책 해지 시 정상가(할인 전 금액) 기준으로 정산한다. - -## 제9조 (구독 및 해지) - -### 9.1 구독 시작 - -- **시작일**: 서비스 게시일 (검수 완료 후) -- **결제일**: 매월 말일 -- **청구 방식**: 후불제 (해당 월 사용량 기준) -- **일할 계산**: (사용 일수 / 해당 월 일수) × 구독료 - -> ⚠️ 중요: - 계약 시 확정된 구독료 금액은 [ ]원/월입니다. - -- 매월 말일에 해당 월 사용일수만큼만 후불 청구됩니다. - -### 9.2 구독 해지 - - - 고객은 언제든지 구독을 해지할 수 있습니다. (위약금 없음) - - 해지 신청 후 30일간 데이터 백업 기간 제공 - - 해지일로부터 30일 후 모든 데이터 완전 삭제 - -## 제10조 (유지보수 정책) - -### 10.1 유지보수 개요 - -- **적용 대상**: 구독료를 정상 납부하는 고객 -- **적용 기간**: 구독 기간 전체 (하자담보 책임 1년 이후에도 구독 중이면 계속 제공) -- **비용**: 월 구독료(500,000원)에 포함 - -### 10.2 하자담보 책임과의 차이 - -| 구분 | 하자담보 책임 (제7조) | 유지보수 (제9조의2) | -| --- | --- | --- | -| 기간 | 서비스 게시일로부터 1년 | 구독 기간 전체 | -| 근거 | 법적 의무 (소프트웨어산업진흥법) | 계약 조건 | -| 비용 | 무상 | 구독료에 포함 | -| 범위 | 하자(버그, 미구현 등) | 하자 + 일반 유지보수 | - -### 10.3 유지보수 범위 (구독료에 포함) - -> ✅ 무상 제공: - 모든 버그 수정 및 오류 처리 - 보안 패치 및 업데이트 - 성능 최적화 - 긴급 장애 대응 (24시간 이내) - 데이터 백업 및 복구 - 기술 지원 (고객센터, 이메일) - 플랫폼 업데이트 (프레임워크, 브라우저 호환성) - -> ❌ 별도 비용: - 신규 기능 개발 - 커스터마이징 및 추가 개발 - 기획 변경 (화면 구성, 프로세스 변경) - 외부 시스템 연동 - 추가 교육 및 컨설팅 - -### 10.4 서비스 레벨 (SLA) - -| 심각도 | 상황 | 응답 시간 | 해결 목표 | -| --- | --- | --- | --- | -| 긴급 (P0) | 서비스 완전 중단 | 1시간 | 24시간 | -| 높음 (P1) | 주요 기능 장애 | 4시간 | 3영업일 | -| 보통 (P2) | 일반 버그 | 1영업일 | 7영업일 | -| 낮음 (P3) | 문의/안내 | 1영업일 | 3영업일 | - -### 10.5 정기 유지보수 - -- **월간**: 보안 패치, 백업 점검 (매월 첫째 주 일요일 새벽) -- **분기**: 성능 최적화 (분기 말 일요일 새벽) -- **반기**: 시스템 점검 (6월/12월 일요일 새벽) - -> ⚠️ 모든 정기 점검은 최소 7일 전 사전 공지됩니다. - -### 10.6 유지보수 신청 - -- **고객센터**: 02-6347-0005 (평일 09:00~18:00 ) -- **이메일**: support@codebridge-x.com (24시간) -- **시스템 내**: SAM 시스템 내 고객지원 메뉴 - -### 10.7 유지보수 종료 - -다음의 경우 유지보수 서비스가 종료됩니다: 1. 구독 해지 시 2. 구독료 3개월 연속 미납 시 3. 중대한 약관 위반 시 - -## 제11조 (고객의 의무) - -고객은 다음 사항을 준수해야 합니다: -- **정확한 정보 제공**: 허위 정보 제공 금지 -- **협조 의무**: 개발에 필요한 자료 및 정보 제공 -- **부정 사용 금지**: 서비스의 재판매, 재배포 금지 -- **지적재산권 존중**: 무단 복제, 역설계 금지 - -## 제12조 (회사의 의무) - -회사는 다음 사항을 준수합니다: -- **서비스 제공**: 계약서에 명시된 서비스 제공 -- **하자담보 책임**: 1년간 하자 무상 수정 -- **개인정보 보호**: 개인정보보호법 준수 -- **기술 지원**: 고객센터 운영 및 기술 지원 - -## 제13조 (미입금 시 법적 조치) - -### 13.1 개발비 미입금 절차 - -| 단계 | 시점 | 조치 내용 | -| --- | --- | --- | -| 1차 독촉 | 기한 경과 후 3일 | 이메일 및 SMS 발송 | -| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 | -| 추심등 | 기한 경과 후 14일 | 신용정보사 연체 등록, 법률대리인 위임 | -| 법적 조치 | 기한 경과 후 30일 | 지급명령 신청 또는 소송 제기 | - -### 13.2 구독료 미입금 절차 - -| 단계 | 시점 | 조치 내용 | -| --- | --- | --- | -| 1차 실패 | 익일 | 재출금 | -| 2차 실패 | 기한 경과 후 3일 | 재출금 | -| 3차 실패 | 미수금 처리 | 서비스 접근 제한, 1차 독촉 | -| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 | -| 서비스 중단 | 기한 경과 후 14일 | 로그인 불가, 데이터 격리 | -| 강제 해지 | 기한 경과 후 30일 | 계약 해지, 법적 조치 검토 | - -## 제14조 (개인정보 보호) - - - 회사는 「개인정보 보호법」을 준수합니다. - - 고객의 개인정보는 서비스 제공 목적으로만 사용됩니다. - - 제3자에게 제공하지 않습니다. (법령 제외) - - 계약 종료 시 개인정보는 즉시 삭제됩니다. (법정 보관 의무 제외) - -## 제15조 (지적재산권) - -- **소유권**: 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. -- **사용 권한**: 고객은 서비스 사용 권한만을 부여받습니다. -- **금지 사항**: 복제, 배포, 역설계, 재판매 금지 - -## 제16조 (손해배상) - - - 회사 또는 고객이 본 계약을 위반하여 상대방에게 손해를 입힌 경우 배상 책임이 있습니다. - - 다만, 불가항력으로 인한 손해는 배상 책임에서 제외됩니다. - -## 제17조 (불가항력) - -다음의 사유로 서비스 제공이 불가능한 경우 회사는 책임을 지지 않습니다: - - 천재지변 (지진, 태풍, 홍수 등) - - 전쟁, 테러, 전염병 - - 정부 규제 또는 법령 변경 - - 제3자의 불법 행위 (해킹 등) - -## 제18조 (분쟁 해결) - - - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. -- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. - -## 제19조 (계약의 효력) - - - 본 계약은 계약일로부터 효력이 발생합니다. - - 본 계약은 구독 해지 시까지 유효합니다. - -## 제20조 (기타) - - - 본 계약서는 2부 작성하여 회사와 고객이 각 1부씩 보관합니다. - - 본 계약의 해석 및 적용은 대한민국 법률을 준거법으로 합니다. - -## 계약 당사자 - -### [회사] - -상호: 주식회사 코드브릿지엑스 -대표자: 이의찬 -사업자등록번호: 664-86-03713 -주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 -이메일: contact@codebridge-x.com -전화: 02-6347-0005 -서명: -날짜: - -### [고객] - -상호: -대표자: -사업자등록번호: -주소: -이메일: -전화: -서명: -날짜: - -## 별첨 - -### 별첨 1: 기획서 - -[별도 첨부] - -### 별첨 2: 개발 일정표 - -[별도 첨부] - -### 별첨 3: 기능 명세서 - -[별도 첨부] - -주식회사 코드브릿지엑스 -이메일: contact@codebridge-x.com -전화: 02-6347-0005 -주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 - diff --git a/sam/docs/contracts/markdown/02-nda.md b/sam/docs/contracts/markdown/02-nda.md deleted file mode 100644 index cb6ff9c..0000000 --- a/sam/docs/contracts/markdown/02-nda.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: "비밀유지서약서 (NDA)" -version: "v4.0" -date: "2026-02-22" -docx_file: "비밀유지서약서.docx" ---- - -# 비밀유지서약서 (NDA) - -- **작성일**: - -- **서약인 정보** -- **구분**: - -- **인적 사항:** -상호(성명): _______________ -대표자(본인): _______________ -사업자등록번호(주민등록번호): ____________________ -주소: ______________________________________________________________________ -연락처: _______________ -이메일: _______________ - -## 제1조 (목적) - - - 본 서약서는 주식회사 코드브릿지(이하 “회사”)와의 업무 관계에서 알게 된 기밀 정보를 - - 보호하기 위해 작성되었습니다. - -## 제2조 (기밀 정보의 정의) - - - 다음 각 호의 정보는 회사의 기밀 정보로 간주됩니다: - -### 2.1 고객 정보 - - - 고객사 명단 (법인명, 대표자명, 연락처) - - 고객사 담당자 정보 (성명, 부서, 연락처, 이메일) - - 계약 내역 (가입비, 할인율, 구독료, 특약 사항) - - 고객사의 사업 정보 (매출, 직원 수, 거래처 등) - - 고객사가 회사에 요구한 개발 내역 및 기획 문서 - -### 2.2 영업 정보 - - - 가격 정책 (정가, 할인 정책, 최소 가입비) - - 수수료 정책 (비율, 지급 기준, 상계 방식) - - 영업 전략 및 마케팅 계획 - - 잠재 고객 리스트 - - 계약 체결 노하우 및 제안서 템플릿 - -### 2.3 기술 정보 - - - SAM 시스템의 소스코드 - - 데이터베이스 구조 및 설계 문서 - - 개발 프로세스 및 방법론 - - 서버 인프라 구성 및 보안 정책 - - API 키, 접속 정보, 관리자 권한 - -### 2.4 경영 정보 - - - 회사의 재무 정보 (매출, 이익, 원가) - - 조직도 및 인사 정보 - - 사업 계획 및 전략 - - 투자 유치 및 M&A 관련 정보 - -### 2.5 기타 - -- 회사가 **“기밀(Confidential)”** 또는 **“대외비”**로 표시한 모든 문서 및 정보 - -## 제3조 (기밀 유지 의무) - -### 3.1 기본 의무 - - - 본인은 업무 수행 중 알게 된 모든 기밀 정보를: -- **외부에 누설하지 않습니다** -- **업무 목적 외에 사용하지 않습니다** -- **무단으로 복사, 복제, 전송하지 않습니다** -- **제3자에게 제공하거나 공개하지 않습니다** - -### 3.2 정보 관리 - - - 기밀 문서는 안전한 장소에 보관 - - 이메일, 메신저 등 전송 시 암호화 - - 업무 종료 시 모든 기밀 자료 반환 또는 파기 - - 개인 디바이스에 기밀 정보 저장 금지 - -### 3.3 제3자 접근 차단 - - - 가족, 친구 등 타인이 기밀 정보에 접근하지 못하도록 조치 - - 공공장소(카페, 도서관 등)에서 기밀 정보 취급 금지 - - 비밀번호 및 접속 정보 타인 공유 금지 - -## 제4조 (예외 사항) - - - 다음의 정보는 기밀 정보에서 제외됩니다: - - 본인이 알기 전에 이미 공개된 정보 - - 본인의 귀책사유 없이 공개된 정보 - - 제3자로부터 적법하게 취득한 정보 - - 본인이 독자적으로 개발한 정보 - - 법원, 정부기관의 법적 요구에 따라 공개해야 하는 정보 (단, 회사에 사전 통지 필수) - -## 제5조 (의무 기간) - -### 5.1 기간 - - - 본 서약서의 기밀 유지 의무는: -- **계약 체결일로부터 효력 발생** -- **계약 종료 후 2년간 유지** - -### 5.2 영구 보호 - -- 단, 다음 정보는 **영구적으로** 보호됩니다: - - 고객사 개인정보 - - 회사의 영업 비밀 (부정경쟁방지법상 영업 비밀) - - 기술 정보 (특허, 저작권 대상) - -## 제6조 (위반 시 책임) - -### 6.1 민사 책임 - - - 본인이 본 서약을 위반하여 회사 또는 고객에게 손해를 입힌 경우: -- **실손해**** 배상**: 실제 발생한 손해 전액 -- **징벌적 손해배상**: 실손해의 최대 3배 (악의적 유출 시) -- **법률 비용**: 소송 비용, 변호사 비용 등 - -### 6.2 형사 책임 - - - 다음의 경우 형사 고발 대상이 됩니다: -- **부정경쟁방지법** 위반 (영업 비밀 침해) -- **개인정보보호법** 위반 (고객 정보 유출) -- **정보통신망법** 위반 (기술 정보 침해) -- **형법** 위반 (업무상 배임) -- **※ 형사 처벌: 5년 이하 징역 또는 5천만원 이하 벌금** - -### 6.3 계약 해지 - - - 회사는 본 서약 위반 시 즉시 계약을 해지할 수 있으며, 이미 지급한 수수료 또는 - - 대금을 환수할 수 있습니다. - -## 제7조 (자료 반환) - -### 7.1 반환 대상 - - - 계약 종료 또는 요청 시 다음을 즉시 반환해야 합니다: - - 회사로부터 제공받은 모든 문서 (종이, 파일) - - 고객사 계약서 및 개인정보 - - 가격표, 제안서, 템플릿 등 영업 자료 - - USB, 하드디스크 등 저장 매체 - -### 7.2 파기 확인 - -- 반환 불가능한 파일(이메일, 클라우드 등)은 즉시 삭제하고, **삭제 확인서**를 회사에 - - 제출해야 합니다. - -## 제8조 (경업 금지) - -### 8.1 경업 금지 기간 - -- 계약 종료 후 **6개월간** 다음 행위를 금지합니다: - - 회사의 고객에게 경쟁 제품 판매 - - 회사의 기밀 정보를 이용한 유사 사업 - - 회사 직원 또는 영업파트너를 스카우트 - -### 8.2 예외 - - - 단순히 경쟁사 제품을 판매하는 것은 허용되나, 회사의 기밀 정보를 활용해서는 - - 안 됩니다. - -## 제9조 (분쟁 해결) - -### 9.1 관할 법원 - - - 본 서약과 관련된 분쟁은 회사 본사 소재지 관할 법원으로 합니다. - -### 9.2 준거법 - - - 본 서약은 대한민국 법률에 따라 해석됩니다. - -- **서약 확인** - - 본인은 위 내용을 충분히 이해하였으며, 이를 성실히 준수할 것을 서약합니다. -- **서약일**: ___________________ -- **서약인** -상호(성명): _______________ -대표자(본인): _______________ -주민등록번호(또는 사업자등록번호): _______________ -- **서명 또는 인**: _______________ - -- **수령인 (주식회사 ****코드브릿지엑스****)** - - 대표이사: 이의찬 -- **확인****일**: ___________________ -- **서명 또는 인**: _______________ - -- **참고: 관련 법률** -- **부정경쟁방지법 제2조 (영업비밀)** - - “영업비밀”이란 공공연히 알려져 있지 아니하고 독립된 경제적 가치를 가지는 것으로서, - - 비밀로 관리된 생산방법, 판매방법, 그 밖에 영업활동에 유용한 기술상 또는 경영상의 - - 정보를 말한다. -- **부정경쟁방지법 제18조 (벌칙)** -- 영업비밀을 외국에서 사용하거나 외국에서 사용되게 할 목적으로 취득·사용 또는 제3자에게 누설한 자는 **15년 이하의 징역** 또는 **15억원 이하의 벌금**에 처한다. - -- **※ 본 서약서는 2부를 작성하여 회사와 서약인이 각 1부씩 보관합니다.** -- **※ 서약 위반 시 민·형사상 책임을 질 수 있습니다.** \ No newline at end of file diff --git a/sam/docs/contracts/markdown/03-partner-agreement.md b/sam/docs/contracts/markdown/03-partner-agreement.md deleted file mode 100644 index 81e6af5..0000000 --- a/sam/docs/contracts/markdown/03-partner-agreement.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -title: "영업파트너 위촉계약서" -version: "v4.0" -date: "2026-02-22" -docx_file: "영업파트너 위촉계약서.docx" ---- - -# < 영업파트너 위촉계약서 > - -# Sales Partner Engagement Agreement - - - 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다. - -## 제1조 (계약의 목적) - - - 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를 - - 명확히 함을 목적으로 합니다. - -## 제2조 (용어의 정의) - -- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너 -- **관리자**: 판매자를 관리하고 지원하는 상급 영업파트너 -- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용 -- **수수료**: 파트너가 영업 활동의 대가로 받는 보상 -- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것 - -## 제3조 (파트너의 역할 및 업무) - -### 3.1 판매자의 역할 - - - 잠재 고객 발굴 및 초기 접촉 - - SAM 서비스 소개 및 제안 - - 고객과의 계약 체결 지원 - - 계약 후 고객 관리 및 사후 지원 - -### 3.2 관리자의 역할 - - - 판매자 모집 및 관리 - - 판매자 교육 및 지원 - - 영업 전략 수립 및 실행 - - 회사와 판매자 간 소통 중재 - -### 3.3 공통 의무 - - - 회사의 브랜드 이미지 유지 - - 정확한 정보 제공 - - 윤리적 영업 활동 준수 - - 비밀 유지 의무 - -## 제4조 (수수료 정책) - -### 4.1 수수료 비율 - -| 역할 | 수수료 비율 | 산정 기준 | -| --- | --- | --- | -| 판매자 | 개발비의 20% | 1차,2차 입금액 기준 | -| 관리자 | 개발비의 5% | 1차,2차 입금액 기준 | - -### 4.2 수수료 산정 예시 - -- **총 개발비 80,000,000원 계약 시** - -| 단계 | 고객 입금 | 판매자 수수료 (20%) | 관리자 수수료 (5%) | -| --- | --- | --- | --- | -| 1차 착수금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 | -| 2차 잔금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 | -| 총계 | 80,000,000원 | 16,000,000원 | 4,000,000원 | - -- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다. - -### 4.3 지급 시기 - -- **지급일**: 고객 입금일 **익월 10일** -- **지급 방식**: 계좌 이체 -- **세금**: 3.3% 원천징수 (사업소득) - -### 4.4 수수료 지급 조건 - - - 고객이 개발비를 실제로 입금한 경우에만 지급 - - 계약 해지 또는 환불 시 수수료 미지급 또는 환수 - - 파트너가 계약 위반 시 수수료 지급 보류 - -## 제5조 (수수료 정책 변경) - -### 5.1 사전 고지 의무 - -- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다. - - 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다. - - 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다. - -### 5.2 변경 효력 - -- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다. - - 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다. - - 진행 중인 계약은 최초 약정 조건을 유지합니다. - -### 5.3 변경 예시 - -#### 예시 1: 수수료율 변경 - - - 고지일: 2026년 2월 1일 - - 변경 내용: 판매자 수수료 20% → 18% - - 적용일: 2026년 3월 1일 이후 체결 계약부터 - -#### 예시 2: 수수료 정책 폐지 - - - 고지일: 2026년 2월 1일 - - 변경 내용: 수수료 정책 완전 폐지 - - 적용일: 2026년 3월 1일 이후 체결 계약부터 - -## 제6조 (계약 기간) - -- 본 계약은 계약일로부터 **1년간** 유효합니다. -- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다. - - 자동 연장은 동일한 조건으로 반복됩니다. - -## 제7조 (계약 해지) - -### 7.1 일반 해지 (양방향) - -- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다. -- **통지 방법**: 이메일 또는 등기우편 -- **효력 발생**: 통지일로부터 30일 후 -- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급 -- **예시**: - - 통지일: 2026년 2월 1일 - - 해지일: 2026년 3월 1일 - - 2월 중 발생한 수수료는 3월 10일 정상 지급 - -### 7.2 즉시 해지 사유 - -- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다: -- **(1) 품위 유지 결격사유 발생 [신설]** - - 음주운전으로 적발된 경우 - - 형사 범죄로 기소 또는 구속된 경우 - - 사회적 물의를 일으킨 경우 - - 기타 파트너로서의 품위를 훼손한 경우 -- **(2) 계약 위반** - - 허위 정보 제공 또는 사기 행위 - - 회사 명예 훼손 또는 영업 방해 - - 비밀 유지 의무 위반 - - 중대한 업무 태만 -- **(3) 부정 행위** - - 고객으로부터 금품 수수 - - 계약서 위조 또는 변조 - - 회사 자산 횡령 또는 유용 - -### 7.3 즉시 해지 시 조치 - - - 미지급 수수료는 지급하지 않습니다. - - 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외) - - 진행 중인 계약은 회사가 직접 관리합니다. - -## 제8조 (비밀 유지) - -### 8.1 비밀 정보 - - - 다음 정보는 비밀로 유지되어야 합니다: - - 회사의 영업 전략 및 계획 - - 고객 정보 (회사명, 담당자, 연락처 등) - - 수수료 정책 및 계약 조건 - - 기술 정보 및 노하우 - - 회사 내부 자료 - -### 8.2 비밀 유지 의무 - - - 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다. -- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다. - - 위반 시 손해배상 책임이 있습니다. - -## 제9조 (지적재산권) - - - SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. - - 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다. - - 영업 활동에 필요한 자료는 회사가 제공합니다. - -## 제10조 (세금 및 원천징수) - -### 10.1 사업소득 - -- 파트너 수수료는 **사업소득**으로 간주됩니다. - -### 10.2 원천징수 - -| 항목 | 비율 | 비고 | -| --- | --- | --- | -| 소득세 | 3.0% | | -| 지방소득세 | 0.3% | 소득세의 10% | -| 합계 | 3.3% | | - -### 10.3 지급명세서 - -- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다. - -## 제11조 (손해배상) - -### 11.1 파트너의 귀책 사유 - - - 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다: - - 허위 정보 제공으로 계약 취소 - - 고객과의 분쟁으로 회사 명예 훼손 - - 비밀 유지 의무 위반 - - 부정 행위 - -### 11.2 회사의 귀책 사유 - - - 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다. - -## 제12조 (분쟁 해결) - - - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. -- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. - -## 제13조 (기타 사항) - -### 13.1 계약서 교부 - - - 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다. - -### 13.2 통지 - - - 모든 통지는 다음의 연락처로 발송됩니다: -- **회사**: -- 이메일: admin@codebridge-x.com -- 전화: 02-6347-0005 -- **파트너**: -- 이메일: -- 전화: - -### 13.3 준거법 - - - 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다. - -- **계약 당사자** -- **[회사]** -- **상호**: 주식회사 코드브릿지엑스 -- **대표자**: 이의찬 (인) -- **사업자등록번호**: 664-86-03713 -- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 -- **이메일**: admin@codebridge-x.com -- **전화**: 02-6347-0005 -- **날짜**: - -- **[파트너]** -- **상호/성명**: -- **대표자/본인**: (서명) -- **사업자등록번호**: -- **주소**: -- **이메일**: -- **전화**: -- **날짜**: - -- **별첨** - -#### 별첨 1: 수수료 정산표 - -| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 | -| --- | --- | --- | --- | --- | --- | --- | -| | | | | | | | - -#### 별첨 2: 영업 활동 보고서 - -| 날짜 | 활동내용 | 고객사 | 진행 상황 | -| --- | --- | --- | --- | -| | | | | - - - 첨부 서류 - - 사업자등록증 사본 (사업자인 경우) - - 주민등록등본 사본 (개인인 경우) - - 통장 사본 (수수료 입금용) - - 비밀유지서약서 - -- **주식회사 코드브릿지엑스** -- 이메일: admin@codebridge-x.com -- 전화: 02-6347-0005 -- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 diff --git a/sam/docs/contracts/markdown/04-partner-agreement-group.md b/sam/docs/contracts/markdown/04-partner-agreement-group.md deleted file mode 100644 index b3251c4..0000000 --- a/sam/docs/contracts/markdown/04-partner-agreement-group.md +++ /dev/null @@ -1,267 +0,0 @@ ---- -title: "영업파트너 위촉계약서 (단체용)" -version: "v4.0" -date: "2026-02-22" -docx_file: "영업파트너 위촉계약서(단체용).docx" ---- - -# < 영업파트너 위촉계약서 > - -# Sales Partner Engagement Agreement - - - 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다. - -## 제1조 (계약의 목적) - - - 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를 - - 명확히 함을 목적으로 합니다. - -## 제2조 (용어의 정의) - -- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너 -- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용 -- **수수료**: 파트너가 영업 활동의 대가로 받는 보상 -- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것 - -## 제3조 (파트너의 역할 및 업무) - -### 3.1 판매자의 역할 - - - 잠재 고객 발굴 및 초기 접촉 - - SAM 서비스 소개 및 제안 - - 고객과의 계약 체결 지원 - - 계약 후 고객 관리 및 사후 지원 - -### 3.2 공통 의무 - - - 회사의 브랜드 이미지 유지 - - 정확한 정보 제공 - - 윤리적 영업 활동 준수 - - 비밀 유지 의무 - -## 제4조 (수수료 정책) - -### 4.1 수수료 비율 - -| 역할 | 수수료 비율 | 산정 기준 | -| --- | --- | --- | -| 판매자 | 개발비의 30% | 1차,2차 입금액 기준 | - -### 4.2 수수료 산정 예시 - -- **총 개발비 80,000,000원 계약 시** - -| 단계 | 고객 입금 | 판매자 수수료 (30%) | -| --- | --- | --- | -| 1차 착수금 (50%) | 40,000,000원 | 12,000,000원 | -| 2차 잔금 (50%) | 40,000,000원 | 12,000,000원 | -| 총계 | 80,000,000원 | 24,000,000원 | - -- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다. - -### 4.3 지급 시기 - -- **지급일**: 고객 입금일 **익월 10일** -- **지급 방식**: 계좌 이체 -- **세금**: 사업소득일 경우 3.3% 원천징수 - -### 4.4 수수료 지급 조건 - - - 고객이 개발비를 실제로 입금한 경우에만 지급 - - 계약 해지 또는 환불 시 수수료 미지급 또는 환수 - - 파트너가 계약 위반 시 수수료 지급 보류 - -## 제5조 (수수료 정책 변경) - -### 5.1 사전 고지 의무 - -- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다. - - 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다. - - 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다. - -### 5.2 변경 효력 - -- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다. - - 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다. - - 진행 중인 계약은 최초 약정 조건을 유지합니다. - -### 5.3 변경 예시 - -#### 예시 1: 수수료율 변경 - - - 고지일: 2026년 2월 1일 - - 변경 내용: 판매자 수수료 20% → 18% - - 적용일: 2026년 3월 1일 이후 체결 계약부터 - -#### 예시 2: 수수료 정책 폐지 - - - 고지일: 2026년 2월 1일 - - 변경 내용: 수수료 정책 완전 폐지 - - 적용일: 2026년 3월 1일 이후 체결 계약부터 - -## 제6조 (계약 기간) - -- 본 계약은 계약일로부터 **1년간** 유효합니다. -- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다. - - 자동 연장은 동일한 조건으로 반복됩니다. - -## 제7조 (계약 해지) - -### 7.1 일반 해지 (양방향) - -- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다. -- **통지 방법**: 이메일 또는 등기우편 -- **효력 발생**: 통지일로부터 30일 후 -- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급 -- **예시**: - - 통지일: 2026년 2월 1일 - - 해지일: 2026년 3월 1일 - - 2월 중 발생한 수수료는 3월 10일 정상 지급 - -### 7.2 즉시 해지 사유 - -- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다: -- **(1) 품위 유지 결격사유 발생 [신설]** - - 음주운전으로 적발된 경우 - - 형사 범죄로 기소 또는 구속된 경우 - - 사회적 물의를 일으킨 경우 - - 기타 파트너로서의 품위를 훼손한 경우 -- **(2) 계약 위반** - - 허위 정보 제공 또는 사기 행위 - - 회사 명예 훼손 또는 영업 방해 - - 비밀 유지 의무 위반 - - 중대한 업무 태만 -- **(3) 부정 행위** - - 고객으로부터 금품 수수 - - 계약서 위조 또는 변조 - - 회사 자산 횡령 또는 유용 - -### 7.3 즉시 해지 시 조치 - - - 미지급 수수료는 지급하지 않습니다. - - 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외) - - 진행 중인 계약은 회사가 직접 관리합니다. - -## 제8조 (비밀 유지) - -### 8.1 비밀 정보 - - - 다음 정보는 비밀로 유지되어야 합니다: - - 회사의 영업 전략 및 계획 - - 고객 정보 (회사명, 담당자, 연락처 등) - - 수수료 정책 및 계약 조건 - - 기술 정보 및 노하우 - - 회사 내부 자료 - -### 8.2 비밀 유지 의무 - - - 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다. -- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다. - - 위반 시 손해배상 책임이 있습니다. - -## 제9조 (지적재산권) - - - SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. - - 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다. - - 영업 활동에 필요한 자료는 회사가 제공합니다. - -## 제10조 (세금 및 원천징수) - -### 10.1 사업소득 - -- 파트너 수수료는 **사업소득**으로 간주됩니다. - -### 10.2 원천징수 - -| 항목 | 비율 | 비고 | -| --- | --- | --- | -| 소득세 | 3.0% | | -| 지방소득세 | 0.3% | 소득세의 10% | -| 합계 | 3.3% | | - -### 10.3 지급명세서 - -- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다. - -## 제11조 (손해배상) - -### 11.1 파트너의 귀책 사유 - - - 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다: - - 허위 정보 제공으로 계약 취소 - - 고객과의 분쟁으로 회사 명예 훼손 - - 비밀 유지 의무 위반 - - 부정 행위 - -### 11.2 회사의 귀책 사유 - - - 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다. - -## 제12조 (분쟁 해결) - - - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. -- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. - -## 제13조 (기타 사항) - -### 13.1 계약서 교부 - - - 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다. - -### 13.2 통지 - - - 모든 통지는 다음의 연락처로 발송됩니다: -- **회사**: -- 이메일: admin@codebridge-x.com -- 전화: 02-6347-0005 -- **파트너**: -- 이메일: -- 전화: - -### 13.3 준거법 - - - 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다. - -- **계약 당사자** -- **[회사]** -- **상호**: 주식회사 코드브릿지엑스 -- **대표자**: 이의찬 (인) -- **사업자등록번호**: 664-86-03713 -- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 -- **이메일**: admin@codebridge-x.com -- **전화**: 02-6347-0005 -- **날짜**: - -- **[파트너]** -- **상호/성명**: -- **대표자/본인**: (서명) -- **사업자등록번호**: -- **주소**: -- **이메일**: -- **전화**: -- **날짜**: - -- **별첨** - -#### 별첨 1: 수수료 정산표 - -| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 | -| --- | --- | --- | --- | --- | --- | --- | -| | | | | | | | - -#### 별첨 2: 영업 활동 보고서 - -| 날짜 | 활동내용 | 고객사 | 진행 상황 | -| --- | --- | --- | --- | -| | | | | - - - 첨부 서류 - - 사업자등록증 사본 (사업자인 경우) - - 주민등록등본 사본 (개인인 경우) - - 통장 사본 (수수료 입금용) - - 비밀유지서약서 - -- **주식회사 코드브릿지엑스** -- 이메일: admin@codebridge-x.com -- 전화: 02-6347-0005 -- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 diff --git a/sam/docs/contracts/revisions.json b/sam/docs/contracts/revisions.json deleted file mode 100644 index 1bcd843..0000000 --- a/sam/docs/contracts/revisions.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "documents": { - "01-service-agreement": { - "title": "고객사 서비스 이용계약서", - "docx_file": "01_고객_서비스이용계약서_v4_0_전자서명용.docx", - "revisions": [ - { - "version": "v4.0", - "date": "2026-02-22", - "author": "개발팀", - "description": "버전 관리 시스템 도입, 개정이력 추적 시작" - }, - { - "version": "v4.1", - "date": "2026-02-22", - "author": "개발팀", - "description": "제4조에 사용량 기반 추가 과금(4.5) 및 바로빌 부가 서비스 요금(4.6) 조항 추가" - } - ] - }, - "02-nda": { - "title": "비밀유지서약서 (NDA)", - "docx_file": "비밀유지서약서.docx", - "revisions": [ - { - "version": "v4.0", - "date": "2026-02-22", - "author": "개발팀", - "description": "버전 관리 시스템 도입, 개정이력 추적 시작" - } - ] - }, - "03-partner-agreement": { - "title": "영업파트너 위촉계약서", - "docx_file": "영업파트너 위촉계약서.docx", - "revisions": [ - { - "version": "v4.0", - "date": "2026-02-22", - "author": "개발팀", - "description": "버전 관리 시스템 도입, 개정이력 추적 시작" - } - ] - }, - "04-partner-agreement-group": { - "title": "영업파트너 위촉계약서 (단체용)", - "docx_file": "영업파트너 위촉계약서(단체용).docx", - "revisions": [ - { - "version": "v4.0", - "date": "2026-02-22", - "author": "개발팀", - "description": "버전 관리 시스템 도입, 개정이력 추적 시작" - } - ] - } - } -} diff --git a/sam/docs/contracts/scripts/extract_to_markdown.py b/sam/docs/contracts/scripts/extract_to_markdown.py deleted file mode 100644 index ea44889..0000000 --- a/sam/docs/contracts/scripts/extract_to_markdown.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python3 -""" -DOCX → Markdown 추출 스크립트 - -4개 전자계약 DOCX 파일을 Markdown으로 변환한다. -- 서비스이용계약서: Heading 스타일 기반 매핑 -- 나머지 3개: Bold 런 + 패턴 매칭으로 구조 유추 -""" - -import re -import sys -from datetime import date -from pathlib import Path - -from docx import Document - -# 경로 설정 -BASE_DIR = Path(__file__).resolve().parent.parent -DOCX_DIR = BASE_DIR / "docx" -MD_DIR = BASE_DIR / "markdown" - -# DOCX → Markdown 매핑 -FILE_MAP = { - "01_고객_서비스이용계약서_v4_0_전자서명용.docx": { - "output": "01-service-agreement.md", - "title": "고객사 서비스 이용계약서", - "type": "styled", - }, - "비밀유지서약서.docx": { - "output": "02-nda.md", - "title": "비밀유지서약서 (NDA)", - "type": "pattern", - }, - "영업파트너 위촉계약서.docx": { - "output": "03-partner-agreement.md", - "title": "영업파트너 위촉계약서", - "type": "pattern", - }, - "영업파트너 위촉계약서(단체용).docx": { - "output": "04-partner-agreement-group.md", - "title": "영업파트너 위촉계약서 (단체용)", - "type": "pattern", - }, -} - - -def table_to_markdown(table): - """DOCX 테이블을 Markdown 테이블로 변환""" - rows = [] - for row in table.rows: - cells = [cell.text.strip().replace("\n", " ") for cell in row.cells] - rows.append(cells) - - if not rows: - return "" - - lines = [] - # 헤더 - lines.append("| " + " | ".join(rows[0]) + " |") - lines.append("| " + " | ".join(["---"] * len(rows[0])) + " |") - # 본문 - for row in rows[1:]: - # 셀 개수 맞추기 - while len(row) < len(rows[0]): - row.append("") - lines.append("| " + " | ".join(row[: len(rows[0])]) + " |") - - return "\n".join(lines) - - -def get_paragraph_heading_level_styled(para): - """스타일 기반 문서의 헤딩 레벨 판별 (서비스이용계약서)""" - style = para.style.name if para.style else "" - - if style == "Heading 1": - return 1 - elif style == "Heading 2": - return 2 - elif style == "Heading 3": - return 3 - - return 0 - - -def get_paragraph_heading_level_pattern(para): - """패턴 매칭 기반 문서의 헤딩 레벨 판별 (비밀유지서약서, 영업파트너 위촉계약서)""" - text = para.text.strip() - has_bold = any(r.bold for r in para.runs if r.bold) - - if not text or not has_bold: - return 0 - - # "제X조" 패턴 → ## (h2) - if re.match(r"^ 0: - lines.append("") - lines.append(f"{'#' * level} {text}") - lines.append("") - elif style == "Compact": - # Bold 런이 있으면 강조 리스트 - has_bold = any(r.bold for r in para.runs if r.bold) - if has_bold: - # Bold 부분과 일반 부분 분리 - parts = [] - for run in para.runs: - if run.bold: - parts.append(f"**{run.text}**") - else: - parts.append(run.text) - combined = "".join(parts) - lines.append(f"- {combined}") - else: - # 들여쓰기된 하위 항목 - lines.append(f" - {text}") - elif style in ("Body Text", "First Paragraph"): - # 본문 텍스트 - if text.startswith("⚠️") or text.startswith("✅") or text.startswith("❌"): - lines.append("") - lines.append(f"> {text}") - lines.append("") - else: - lines.append(text) - else: - lines.append(text) - - elif tag == "tbl": - if table_idx <= len(doc.tables): - current_table_idx = sum( - 1 - for c in list(body)[: list(body).index(child)] - if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl" - ) - if current_table_idx < len(doc.tables): - lines.append("") - lines.append(table_to_markdown(doc.tables[current_table_idx])) - lines.append("") - - return "\n".join(lines) - - -def extract_pattern_doc(doc, file_info): - """패턴 매칭 기반 문서 추출 (비밀유지서약서, 영업파트너 위촉계약서)""" - lines = [] - - body = doc.element.body - para_idx = 0 - - for child in body: - tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag - - if tag == "p": - para = doc.paragraphs[para_idx] - para_idx += 1 - text = para.text.strip() - - if not text: - lines.append("") - continue - - level = get_paragraph_heading_level_pattern(para) - has_bold = any(r.bold for r in para.runs if r.bold) - - if level > 0: - lines.append("") - # 제목에서 < > 제거 - clean_text = re.sub(r"^<\s*|\s*>$", "", text).strip() - lines.append(f"{'#' * level} {clean_text}") - lines.append("") - elif has_bold: - # Bold 텍스트는 강조 처리 - parts = [] - for run in para.runs: - if run.bold: - parts.append(f"**{run.text}**") - else: - parts.append(run.text) - combined = "".join(parts) - - # (1), (2) 같은 번호 패턴 - if re.match(r"^\*\*\(\d+\)", combined): - lines.append(f"- {combined}") - # "예시 N:", "Phase N:" 같은 패턴 - elif re.match(r"^\*\*(예시|Phase|별첨)\s", combined): - lines.append("") - lines.append(f"#### {text}") - lines.append("") - else: - lines.append(f"- {combined}") - else: - # 일반 텍스트 - # 빈칸 양식 (___) 유지 - if "___" in text: - lines.append(text) - elif re.match(r"^(이메일|전화|주소|상호|대표|사업자|주민|연락처|날짜):", text): - lines.append(f"- {text}") - else: - lines.append(f" - {text}") - - elif tag == "tbl": - current_table_idx = sum( - 1 - for c in list(body)[: list(body).index(child)] - if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl" - ) - if current_table_idx < len(doc.tables): - lines.append("") - lines.append(table_to_markdown(doc.tables[current_table_idx])) - lines.append("") - - return "\n".join(lines) - - -def add_frontmatter(content, file_info, docx_name): - """YAML 프론트매터 추가""" - frontmatter = f"""--- -title: "{file_info['title']}" -version: "v4.0" -date: "{date.today().isoformat()}" -docx_file: "{docx_name}" ---- -""" - return frontmatter + "\n" + content - - -def extract_file(docx_name, file_info): - """단일 DOCX 파일 추출""" - docx_path = DOCX_DIR / docx_name - if not docx_path.exists(): - print(f" [SKIP] {docx_name} - 파일 없음") - return False - - doc = Document(str(docx_path)) - - if file_info["type"] == "styled": - content = extract_styled_doc(doc, file_info) - else: - content = extract_pattern_doc(doc, file_info) - - # 프론트매터 추가 - content = add_frontmatter(content, file_info, docx_name) - - # 연속 빈 줄 정리 (3줄 이상 → 2줄로) - content = re.sub(r"\n{3,}", "\n\n", content) - - # 파일 저장 - output_path = MD_DIR / file_info["output"] - output_path.write_text(content, encoding="utf-8") - print(f" [OK] {docx_name} → {file_info['output']}") - return True - - -def main(): - print("DOCX → Markdown 추출 시작") - print(f" DOCX 디렉토리: {DOCX_DIR}") - print(f" 출력 디렉토리: {MD_DIR}") - print() - - MD_DIR.mkdir(parents=True, exist_ok=True) - - success = 0 - for docx_name, file_info in FILE_MAP.items(): - if extract_file(docx_name, file_info): - success += 1 - - print(f"\n완료: {success}/{len(FILE_MAP)} 파일 변환됨") - return 0 if success == len(FILE_MAP) else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/sam/docs/contracts/scripts/sync_check.py b/sam/docs/contracts/scripts/sync_check.py deleted file mode 100644 index 09d55d9..0000000 --- a/sam/docs/contracts/scripts/sync_check.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env python3 -""" -DOCX ↔ Markdown 동기화 검증 스크립트 - -DOCX에서 텍스트를 추출하고 Markdown 파일의 텍스트와 비교하여 -불일치 항목을 리포트한다. -""" - -import difflib -import re -import sys -from pathlib import Path - -from docx import Document - -BASE_DIR = Path(__file__).resolve().parent.parent -DOCX_DIR = BASE_DIR / "docx" -MD_DIR = BASE_DIR / "markdown" - -# DOCX → Markdown 파일 매핑 -FILE_MAP = { - "01_고객_서비스이용계약서_v4_0_전자서명용.docx": "01-service-agreement.md", - "비밀유지서약서.docx": "02-nda.md", - "영업파트너 위촉계약서.docx": "03-partner-agreement.md", - "영업파트너 위촉계약서(단체용).docx": "04-partner-agreement-group.md", -} - - -def extract_text_from_docx(docx_path): - """DOCX에서 순수 텍스트만 추출 (개정이력 테이블 제외, 인터리빙 방식)""" - doc = Document(str(docx_path)) - lines = [] - - from docx.oxml.ns import qn as _qn - - body = doc.element.body - para_idx = 0 - table_idx = 0 - skip_revision = False - - for child in body: - tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag - - if tag == "p": - if para_idx < len(doc.paragraphs): - text = doc.paragraphs[para_idx].text.strip() - para_idx += 1 - - if "개정이력" in text: - skip_revision = True - continue - if text: - skip_revision = False - lines.append(text) - - elif tag == "tbl": - if table_idx < len(doc.tables): - table = doc.tables[table_idx] - table_idx += 1 - - # 개정이력 테이블 건너뛰기 - if len(table.rows) > 0: - first_row_text = [cell.text.strip() for cell in table.rows[0].cells] - if "버전" in first_row_text and "날짜" in first_row_text: - skip_revision = False - continue - - if skip_revision: - skip_revision = False - continue - - for row in table.rows: - cells = [cell.text.strip() for cell in row.cells] - # 빈 셀만 있는 행 건너뛰기 - if not any(cells): - continue - row_text = " | ".join(cells) - if row_text.strip(): - lines.append(row_text) - - return lines - - -def extract_text_from_markdown(md_path): - """Markdown에서 순수 텍스트만 추출 (프론트매터, 마크업 제거)""" - content = md_path.read_text(encoding="utf-8") - lines = [] - - in_frontmatter = False - in_table = False - - for line in content.split("\n"): - stripped = line.strip() - - # YAML 프론트매터 건너뛰기 - if stripped == "---": - in_frontmatter = not in_frontmatter - continue - if in_frontmatter: - continue - - # 빈 줄 건너뛰기 - if not stripped: - in_table = False - continue - - # Markdown 마크업 제거 - text = stripped - - # 헤딩 마크업 제거 - text = re.sub(r"^#{1,6}\s+", "", text) - - # 리스트 마크업 제거 - text = re.sub(r"^\s*[-*+]\s+", "", text) - - # Bold/Italic 마크업 제거 - text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) - text = re.sub(r"\*(.+?)\*", r"\1", text) - - # 블록인용 제거 - text = re.sub(r"^>\s*", "", text) - - # 테이블 구분선 건너뛰기 - if re.match(r"^\|[\s\-|]+\|$", text): - continue - - # 테이블 행 - if text.startswith("|") and text.endswith("|"): - # 파이프 제거하고 셀 텍스트 추출 - cells = [c.strip() for c in text.strip("|").split("|")] - text = " | ".join(cells) - - text = text.strip() - if text: - lines.append(text) - - return lines - - -def normalize_text(text): - """비교를 위한 텍스트 정규화""" - # 공백 정규화 - text = re.sub(r"\s+", " ", text).strip() - # 특수문자 정규화 - text = text.replace("\u00a0", " ") # non-breaking space - text = text.replace("\u3000", " ") # ideographic space - # 언더스코어 빈칸 정규화 - text = re.sub(r"_{3,}", "___", text) - # Bold 마크업(**) 제거 (DOCX 텍스트에 리터럴 ** 포함되는 경우) - text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) - # 선행 리스트 마커 제거 (DOCX 텍스트가 "- "로 시작하는 경우) - text = re.sub(r"^-\s+", "", text) - return text - - -def compare_documents(docx_name, md_name): - """두 문서의 텍스트를 비교""" - docx_path = DOCX_DIR / docx_name - md_path = MD_DIR / md_name - - if not docx_path.exists(): - return {"status": "error", "message": f"DOCX 파일 없음: {docx_name}"} - if not md_path.exists(): - return {"status": "error", "message": f"Markdown 파일 없음: {md_name}"} - - docx_lines = [normalize_text(l) for l in extract_text_from_docx(docx_path) if l.strip()] - md_lines = [normalize_text(l) for l in extract_text_from_markdown(md_path) if l.strip()] - - # difflib로 비교 - matcher = difflib.SequenceMatcher(None, docx_lines, md_lines) - ratio = matcher.ratio() - - # 차이점 추출 - diffs = [] - for tag, i1, i2, j1, j2 in matcher.get_opcodes(): - if tag == "equal": - continue - elif tag == "replace": - for idx in range(max(i2 - i1, j2 - j1)): - docx_text = docx_lines[i1 + idx] if i1 + idx < i2 else "(없음)" - md_text = md_lines[j1 + idx] if j1 + idx < j2 else "(없음)" - diffs.append({ - "type": "변경", - "docx": docx_text[:80], - "markdown": md_text[:80], - }) - elif tag == "delete": - for idx in range(i1, i2): - diffs.append({ - "type": "DOCX에만 존재", - "docx": docx_lines[idx][:80], - "markdown": "-", - }) - elif tag == "insert": - for idx in range(j1, j2): - diffs.append({ - "type": "Markdown에만 존재", - "docx": "-", - "markdown": md_lines[idx][:80], - }) - - return { - "status": "ok", - "similarity": round(ratio * 100, 1), - "docx_lines": len(docx_lines), - "md_lines": len(md_lines), - "diff_count": len(diffs), - "diffs": diffs[:20], # 상위 20개만 - } - - -def main(): - print("=" * 70) - print("DOCX ↔ Markdown 동기화 검증") - print("=" * 70) - - all_ok = True - - for docx_name, md_name in FILE_MAP.items(): - print(f"\n{'─' * 50}") - print(f"문서: {docx_name}") - print(f" ↔ {md_name}") - print(f"{'─' * 50}") - - result = compare_documents(docx_name, md_name) - - if result["status"] == "error": - print(f" [ERROR] {result['message']}") - all_ok = False - continue - - similarity = result["similarity"] - status_icon = "OK" if similarity >= 80 else "WARN" if similarity >= 60 else "FAIL" - - print(f" 유사도: {similarity}% [{status_icon}]") - print(f" DOCX 라인: {result['docx_lines']}") - print(f" Markdown 라인: {result['md_lines']}") - print(f" 차이점: {result['diff_count']}개") - - if result["diffs"]: - print(f"\n 주요 차이점 (상위 {min(len(result['diffs']), 10)}개):") - for i, diff in enumerate(result["diffs"][:10]): - print(f" [{diff['type']}]") - if diff["docx"] != "-": - print(f" DOCX: {diff['docx']}") - if diff["markdown"] != "-": - print(f" MD: {diff['markdown']}") - - if similarity < 80: - all_ok = False - - print(f"\n{'=' * 70}") - if all_ok: - print("결과: 모든 문서 동기화 상태 양호") - else: - print("결과: 일부 문서에서 불일치 발견 - 확인 필요") - print(f"{'=' * 70}") - - return 0 if all_ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/sam/docs/data/interview-master-questions.sql b/sam/docs/data/interview-master-questions.sql deleted file mode 100644 index 58b4899..0000000 --- a/sam/docs/data/interview-master-questions.sql +++ /dev/null @@ -1,279 +0,0 @@ --- ============================================================ --- 인터뷰 질문 마스터 데이터 SQL --- 8개 도메인 × 16개 템플릿 × 80개 질문 --- --- 실행 방법: --- 로컬: docker exec -i sam-mysql-1 mysql -u root -p samdb < docs/data/interview-master-questions.sql --- 개발서버: mysql -u -p samdb < interview-master-questions.sql --- phpMyAdmin: SQL 탭에서 전체 복사 후 실행 --- --- 주의: 한 번만 실행할 것. 중복 실행 시 데이터가 중복됨. --- ============================================================ - -SET NAMES utf8mb4; -SET @tenant_id = 1; -SET @user_id = 1; -SET @now = NOW(); - --- ============================================================ --- 대분류: 제조업-방화셔터 (parent_id=null, 루트 카테고리) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, NULL, '제조업-방화셔터', '방화셔터 제조업 인터뷰', NULL, 1, 1, @user_id, @user_id, @now, @now); -SET @root_manufacturing = LAST_INSERT_ID(); - --- ============================================================ --- Domain 1: 제품 분류 체계 (product_classification) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, '제품 분류 체계', '제품 카테고리, 모델 코드, 분류 기준 파악', 'product_classification', 3, 1, @user_id, @user_id, @now, @now); -SET @cat_1 = LAST_INSERT_ID(); - --- 템플릿 1.1: 제품 카테고리 구조 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_1, '제품 카테고리 구조', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_1_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_1_1, '귀사의 주요 제품군을 모두 나열해주세요', 'text', NULL, '쉼표 구분으로 제품군 나열', NULL, NULL, 'product_classification', 1, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_1, '각 제품군의 하위 모델명과 코드 체계를 알려주세요', 'table_input', '{"columns":["모델코드","모델명","비고"]}', '코드-이름 매핑 테이블', NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_1, '제품을 분류하는 기준은 무엇인가요? (소재, 용도, 크기 등)', 'multi_select', '{"choices":["소재별","용도별","크기별","설치방식별","인증여부별"]}', NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_1, '인증(인정) 제품과 비인증 제품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'product_classification', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_1, '인증 제품의 경우 구성이 고정되나요?', 'checkbox', NULL, NULL, NULL, '{"question_index":3,"value":"있음"}', 'product_classification', 0, 5, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_1, '카테고리별 제품 수는 대략 몇 개인가요?', 'number', NULL, NULL, '개', NULL, 'product_classification', 0, 6, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_1, '제품 코드 명명 규칙을 설명해주세요 (예: KSS01의 의미)', 'text', NULL, '코드 체계의 각 자릿수 의미', NULL, NULL, 'product_classification', 0, 7, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_1, '기존 시스템(ERP/엑셀)에서 사용하는 제품 분류 방식을 캡처하여 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'product_classification', 0, 8, 1, @user_id, @user_id, @now, @now); - --- 템플릿 1.2: 설치 유형별 분류 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_1, '설치 유형별 분류', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_1_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_1_2, '설치 유형(벽면형, 측면형, 혼합형 등)에 따라 견적이 달라지나요?', 'select', '{"choices":["예, 크게 달라짐","약간 달라짐","달라지지 않음"]}', NULL, NULL, NULL, 'product_classification', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_2, '각 설치 유형별로 어떤 부품이 달라지나요?', 'table_input', '{"columns":["설치유형","추가부품","제외부품","비고"]}', NULL, NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_1_2, '설치 유형에 따른 추가 비용 항목이 있나요?', 'text', NULL, NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- Domain 2: BOM 구조 (bom_structure) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, 'BOM 구조', '완제품-부품 관계, 부품 카테고리, BOM 레벨', 'bom_structure', 4, 1, @user_id, @user_id, @now, @now); -SET @cat_2 = LAST_INSERT_ID(); - --- 템플릿 2.1: 완제품-부품 관계 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_2, '완제품-부품 관계', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_2_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_2_1, '대표 제품 1개의 완제품→부품 구성을 트리로 그려주세요', 'bom_tree', NULL, '최상위 제품부터 하위 부품까지 트리 구조', NULL, NULL, 'bom_structure', 1, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_1, '모든 제품에 공통으로 들어가는 부품은 무엇인가요?', 'multi_select', '{"choices":["가이드레일","케이스","모터","제어기","브라켓","볼트/너트"]}', '직접 입력 가능', NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_1, '제품별로 선택적(옵션)인 부품은 무엇인가요?', 'table_input', '{"columns":["제품명","옵션부품","적용조건"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_1, 'BOM이 현재 엑셀로 관리되고 있나요? 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_1, '하위 부품의 단계(레벨)는 최대 몇 단계인가요?', 'number', NULL, NULL, '단계', NULL, 'bom_structure', 0, 5, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_1, '부품 수량이 고정인 것과 계산이 필요한 것을 구분해주세요', 'table_input', '{"columns":["부품명","고정/계산","고정수량 또는 계산식"]}', NULL, NULL, NULL, 'bom_structure', 0, 6, 1, @user_id, @user_id, @now, @now); - --- 템플릿 2.2: 부품 카테고리 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_2, '부품 카테고리', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_2_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_2_2, '부품을 카테고리로 분류하면 어떻게 나눠지나요? (본체, 절곡품, 전동부, 부자재 등)', 'text', NULL, '부품 분류 체계', NULL, NULL, 'bom_structure', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_2, '각 카테고리에 속하는 부품 목록을 정리해주세요', 'table_input', '{"columns":["카테고리","부품명","규격"]}', NULL, NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_2, '외주 구매 부품과 자체 제작 부품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_2_2, '부자재(볼트, 너트, 패킹 등)는 별도 관리하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- Domain 3: 치수/변수 계산 (dimension_formula) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, '치수/변수 계산', '오픈 사이즈→제작 사이즈 변환, 파생 변수 계산', 'dimension_formula', 5, 1, @user_id, @user_id, @now, @now); -SET @cat_3 = LAST_INSERT_ID(); - --- 템플릿 3.1: 오픈 사이즈 → 제작 사이즈 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_3, '오픈 사이즈 → 제작 사이즈', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_3_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_3_1, '고객이 입력하는 기본 치수 항목은 무엇인가요? (폭, 높이, 깊이 등)', 'multi_select', '{"choices":["폭(W)","높이(H)","깊이(D)","두께(T)","지름(Ø)"]}', NULL, NULL, NULL, 'dimension_formula', 1, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_1, '오픈 사이즈에서 제작 사이즈로 변환할 때 더하는 마진값은?', 'formula_input', NULL, '예: W1 = W0 + 120, H1 = H0 + 50', 'mm', NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_1, '제품 카테고리별로 마진값이 다른가요?', 'table_input', '{"columns":["제품카테고리","W 마진(mm)","H 마진(mm)","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_1, '면적(㎡) 계산 공식을 알려주세요', 'formula_input', NULL, '예: area = W1 * H1 / 1000000', '㎡', NULL, 'dimension_formula', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_1, '중량(kg) 계산 공식을 알려주세요', 'formula_input', NULL, '예: weight = area * 단위중량(kg/㎡)', 'kg', NULL, 'dimension_formula', 0, 5, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_1, '기타 파생 변수가 있나요? (예: 분할 개수, 절곡 길이 등)', 'table_input', '{"columns":["변수명","계산식","단위","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 6, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_1, '치수 계산에 사용하는 엑셀 수식을 캡처해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 7, 1, @user_id, @user_id, @now, @now); - --- 템플릿 3.2: 변수 의존 관계 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_3, '변수 의존 관계', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_3_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_3_2, '변수 간 의존 관계를 설명해주세요 (A는 B와 C로 계산)', 'text', NULL, '계산 순서와 변수 의존성', NULL, NULL, 'dimension_formula', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_2, '계산 순서가 중요한 변수가 있나요?', 'text', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_3_2, '단위는 mm, m, kg 중 어떤 것을 기본으로 사용하나요?', 'select', '{"choices":["mm","m","cm","혼용"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- Domain 4: 부품 구성 상세 (component_config) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, '부품 구성 상세', '주요 부품별 규격, 선택 기준, 특수 구성', 'component_config', 6, 1, @user_id, @user_id, @now, @now); -SET @cat_4 = LAST_INSERT_ID(); - --- 템플릿 4.1: 주요 부품별 상세 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_4, '주요 부품별 상세', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_4_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_4_1, '가이드레일의 표준 길이 규격은? (예: 1219, 2438, 3305mm)', 'table_input', '{"columns":["규격코드","길이(mm)","비고"]}', NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_1, '가이드레일 길이 조합 규칙은? (어떤 길이를 몇 개 사용?)', 'text', NULL, '높이에 따른 가이드레일 조합 로직', NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_1, '케이스(하우징) 크기별 규격과 부속품 차이를 설명해주세요', 'table_input', '{"columns":["케이스규격","적용조건","부속품"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_1, '모터 용량 종류와 선택 기준은? (무게별? 면적별?)', 'table_input', '{"columns":["모터용량","적용범위(최소)","적용범위(최대)","단위"]}', '무게/면적 범위별 모터 매핑', NULL, NULL, 'component_config', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_1, '모터 전압 옵션은? (380V, 220V 등)', 'multi_select', '{"choices":["380V","220V","110V","DC 24V"]}', NULL, NULL, NULL, 'component_config', 0, 5, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_1, '제어기 종류와 선택 기준은? (노출형/매립형 등)', 'table_input', '{"columns":["제어기유형","적용조건","비고"]}', NULL, NULL, NULL, 'component_config', 0, 6, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_1, '절곡품(판재 가공) 목록과 각각의 치수 결정 방식은?', 'table_input', '{"columns":["절곡품명","치수결정방식","재질","두께(mm)"]}', NULL, NULL, NULL, 'component_config', 0, 7, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_1, '부자재(볼트, 너트, 패킹 등) 목록과 수량 결정 방식은?', 'table_input', '{"columns":["부자재명","규격","수량결정방식","기본수량"]}', NULL, NULL, NULL, 'component_config', 0, 8, 1, @user_id, @user_id, @now, @now); - --- 템플릿 4.2: 특수 구성 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_4, '특수 구성', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_4_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_4_2, '연기차단재 등 특수 부품이 있나요? 적용 조건은?', 'text', NULL, NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_2, '보강재(샤프트, 파이프, 앵글 등) 사용 조건은?', 'table_input', '{"columns":["보강재명","규격","적용조건","수량"]}', NULL, NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_4_2, '고객 요청에 따라 추가/제외되는 옵션 부품은?', 'table_input', '{"columns":["옵션부품","추가/제외","추가비용","비고"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- Domain 5: 단가 체계 (pricing_structure) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, '단가 체계', '단가 관리 방식, 계산 방식, 마진/LOSS율', 'pricing_structure', 7, 1, @user_id, @user_id, @now, @now); -SET @cat_5 = LAST_INSERT_ID(); - --- 템플릿 5.1: 단가 관리 방식 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_5, '단가 관리 방식', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_5_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_5_1, '부품별 단가를 어디서 관리하나요? (엑셀, ERP, 구두 등)', 'select', '{"choices":["엑셀","ERP 시스템","구두/경험","기타"]}', NULL, NULL, NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_1, '단가표 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_1, '단가 변경 주기는? (월/분기/연 등)', 'select', '{"choices":["수시","월 단위","분기 단위","반기 단위","연 단위"]}', NULL, NULL, NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_1, '단가에 포함되는 항목은? (재료비만? 가공비 포함?)', 'multi_select', '{"choices":["재료비","가공비","운송비","설치비","마진"]}', NULL, NULL, NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_1, '고객별/거래처별 차등 단가가 있나요?', 'select', '{"choices":["있음 (등급별)","있음 (거래처별)","없음 (일괄 동일)"]}', NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_1, 'LOSS율(손실률)을 적용하나요? 적용 방식은?', 'formula_input', NULL, '예: 실제수량 = 계산수량 × (1 + LOSS율)', '%', NULL, 'pricing_structure', 0, 6, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_1, '마진율 설정 방식은? (일괄? 품목별?)', 'select', '{"choices":["일괄 마진율","품목별 마진율","카테고리별 마진율","고객별 마진율"]}', NULL, NULL, NULL, 'pricing_structure', 0, 7, 1, @user_id, @user_id, @now, @now); - --- 템플릿 5.2: 단가 계산 방식 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_5, '단가 계산 방식', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_5_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_5_2, '면적 기반 단가 품목은? (원/㎡)', 'table_input', '{"columns":["품목명","단가(원/㎡)","비고"]}', NULL, '원/㎡', NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_2, '중량 기반 단가 품목은? (원/kg)', 'table_input', '{"columns":["품목명","단가(원/kg)","비고"]}', NULL, '원/kg', NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_2, '수량 기반 단가 품목은? (원/EA)', 'table_input', '{"columns":["품목명","단가(원/EA)","비고"]}', NULL, '원/EA', NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_2, '길이 기반 단가 품목은? (원/m)', 'table_input', '{"columns":["품목명","단가(원/m)","비고"]}', NULL, '원/m', NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_5_2, '기타 특수 단가 계산 방식이 있나요?', 'text', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- Domain 6: 수량 수식 (quantity_formula) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, '수량 수식', '부품별 수량 결정 규칙, 계산식, 검증', 'quantity_formula', 8, 1, @user_id, @user_id, @now, @now); -SET @cat_6 = LAST_INSERT_ID(); - --- 템플릿 6.1: 수량 결정 규칙 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_6, '수량 결정 규칙', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_6_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_6_1, '고정 수량 부품 목록 (항상 1개, 2개 등)', 'table_input', '{"columns":["부품명","고정수량","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_6_1, '치수 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 슬랫수량 = CEIL(H1 / 슬랫피치)', NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_6_1, '면적 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 스크린수량 = area / 기준면적', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_6_1, '중량 기반 수량 계산 부품과 수식', 'formula_input', NULL, NULL, NULL, NULL, 'quantity_formula', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_6_1, '올림/내림/반올림 규칙이 있는 계산은?', 'table_input', '{"columns":["계산항목","올림/내림/반올림","소수점자릿수"]}', NULL, NULL, NULL, 'quantity_formula', 0, 5, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_6_1, '여유 수량(LOSS) 적용 품목과 비율은?', 'table_input', '{"columns":["품목명","LOSS율(%)","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 6, 1, @user_id, @user_id, @now, @now); - --- 템플릿 6.2: 수식 검증 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_6, '수식 검증', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_6_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_6_2, '실제 견적서에서 수량 계산 예시를 보여주세요 (W=3000, H=2500일 때)', 'table_input', '{"columns":["부품명","수식","계산결과","단위"]}', NULL, NULL, NULL, 'quantity_formula', 1, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_6_2, '수식에 사용하는 함수가 있나요? (SUM, CEIL, ROUND 등)', 'multi_select', '{"choices":["CEIL (올림)","FLOOR (내림)","ROUND (반올림)","MAX","MIN","IF 조건문","SUM"]}', NULL, NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_6_2, '조건에 따라 수식이 달라지는 경우가 있나요?', 'text', NULL, '예: 폭이 3000 초과이면 분할 계산', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- Domain 7: 조건부 로직 (conditional_logic) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, '조건부 로직', '범위/매핑 기반 부품 자동 선택 규칙', 'conditional_logic', 9, 1, @user_id, @user_id, @now, @now); -SET @cat_7 = LAST_INSERT_ID(); - --- 템플릿 7.1: 범위 기반 선택 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_7, '범위 기반 선택', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_7_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_7_1, '무게 범위별 모터 용량 선택표를 작성해주세요', 'price_table', '{"columns":["범위 시작(kg)","범위 끝(kg)","모터용량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 1, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_7_1, '크기 범위별 부품 자동 선택 규칙이 있나요?', 'table_input', '{"columns":["조건(변수)","범위","선택부품","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_7_1, '브라켓 크기 결정 기준은?', 'table_input', '{"columns":["조건","범위","브라켓 규격"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now); - --- 템플릿 7.2: 매핑 기반 선택 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_7, '매핑 기반 선택', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_7_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_7_2, '제품 모델 → 기본 부품 세트 매핑표', 'table_input', '{"columns":["제품모델","기본부품1","기본부품2","기본부품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_7_2, '설치 유형 → 추가 부품 매핑표', 'table_input', '{"columns":["설치유형","추가부품","수량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_7_2, '제어기 유형 → 부속품 매핑표', 'table_input', '{"columns":["제어기유형","부속품1","부속품2","부속품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_7_2, '기타 조건부 자동 선택 규칙', 'text', NULL, '위 항목에 해당하지 않는 조건-결과 매핑', NULL, NULL, 'conditional_logic', 0, 4, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- Domain 8: 견적서 양식 (quote_format) --- ============================================================ -INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, NULL, @root_manufacturing, '견적서 양식', '출력 양식, 항목 그룹, 소계/합계 구조', 'quote_format', 10, 1, @user_id, @user_id, @now, @now); -SET @cat_8 = LAST_INSERT_ID(); - --- 템플릿 8.1: 출력 양식 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_8, '출력 양식', 1, 1, @user_id, @user_id, @now, @now); -SET @tpl_8_1 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_8_1, '현재 사용 중인 견적서 양식을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'quote_format', 1, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_1, '견적서에 표시되는 항목 그룹은? (재료비, 노무비, 설치비 등)', 'multi_select', '{"choices":["재료비","노무비","경비","설치비","운반비","이윤","부가세"]}', NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_1, '소계/합계 계산 구조를 설명해주세요', 'text', NULL, '항목 그룹별 소계와 최종 합계의 관계', NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_1, '할인 적용 방식은? (일괄? 항목별?)', 'select', '{"choices":["일괄 할인","항목별 할인","할인 없음","협의 할인"]}', NULL, NULL, NULL, 'quote_format', 0, 4, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_1, '부가세 표시 방식은? (별도? 포함?)', 'select', '{"choices":["별도 표시","포함 표시","선택 가능"]}', NULL, NULL, NULL, 'quote_format', 0, 5, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_1, '견적서에 표시하지 않는 내부 관리 항목은?', 'text', NULL, NULL, NULL, NULL, 'quote_format', 0, 6, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_1, '견적 번호 체계를 알려주세요', 'text', NULL, '예: Q-2026-001 형식', NULL, NULL, 'quote_format', 0, 7, 1, @user_id, @user_id, @now, @now); - --- 템플릿 8.2: 특수 요구사항 -INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) -VALUES (@tenant_id, @cat_8, '특수 요구사항', 2, 1, @user_id, @user_id, @now, @now); -SET @tpl_8_2 = LAST_INSERT_ID(); - -INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES -(@tenant_id, @tpl_8_2, '산출내역서(세부 내역)를 별도로 제공하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 1, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_2, '위치별(층/부호) 개별 산출이 필요한가요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now), -(@tenant_id, @tpl_8_2, '일괄 산출(여러 위치 합산)을 사용하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now); - --- ============================================================ --- 완료 확인 --- ============================================================ -SELECT - (SELECT COUNT(*) FROM interview_categories WHERE interview_project_id IS NULL AND domain IS NOT NULL) AS master_categories, - (SELECT COUNT(*) FROM interview_templates t JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_templates, - (SELECT COUNT(*) FROM interview_questions q JOIN interview_templates t ON q.interview_template_id = t.id JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_questions; diff --git a/sam/docs/features/academy/fire-shutter-image-prompts.md b/sam/docs/features/academy/fire-shutter-image-prompts.md deleted file mode 100644 index 1614eda..0000000 --- a/sam/docs/features/academy/fire-shutter-image-prompts.md +++ /dev/null @@ -1,369 +0,0 @@ -# 방화셔터 백과사전 이미지 생성 프롬프트 - -> **작성일**: 2026-02-22 -> **상태**: 확정 -> **용도**: Google Gemini (Nano Banana Pro) 이미지 생성용 - ---- - -## 1. 개요 - -### 1.1 목적 - -MNG 아카데미 > 방화셔터 백과사전 페이지에 삽입할 기술 일러스트레이션을 AI 이미지 생성 도구(Google Gemini)로 제작하기 위한 프롬프트 모음이다. - -### 1.2 사용 방법 - -1. Google Gemini (Nano Banana Pro 모델)에서 프롬프트를 입력한다 -2. 생성된 이미지를 `mng/public/images/academy/fire-shutter/` 경로에 저장한다 -3. Blade 뷰에서 `` 태그로 참조한다 - -### 1.3 주의사항 - -- **화면 내 모든 라벨은 영어**로 작성되어 있다 (한글 텍스트는 AI 이미지 생성 시 깨짐 현상 발생) -- 전체 구성도, 설치 장면 등 넓은 이미지는 **16:9** 비율 권장 -- 단면도, 부품 상세 등은 **1:1** 또는 **4:3** 비율 권장 -- 생성 실패 시 프롬프트 앞에 `Detailed technical engineering illustration, clean white background, ` 를 추가한다 - ---- - -## 2. 프롬프트 목록 - -### 2.1 방화셔터 전체 구성도 (Full Component Diagram) - -``` -Technical illustration of a fire shutter (automatic fire-rated rolling shutter) installed in a building opening, cutaway side view showing all components with English labels. - -Show these parts clearly labeled: -- Top: "CEILING SLAB" with "HEAD BOX / CASE" mounted below -- Inside head box: "SHAFT" with coiled steel slats, "BALANCE SPRING", "GEAR BOX", "MOTOR", "ELECTROMAGNETIC BRAKE", "BRACKET" on both sides -- Both sides: vertical "GUIDE RAIL" mounted on fireproof walls with "ANCHOR BOLTS" -- Center: multiple horizontal "STEEL SLATS" hanging down in interlocking pattern -- Bottom: "BOTTOM BAR" touching the floor with rubber seal -- Nearby wall: "MANUAL CONTROL BOX" with UP/STOP/DOWN buttons -- Ceiling: "SMOKE DETECTOR" and "HEAT DETECTOR" -- Wall-mounted: "FIRE SHUTTER CONTROLLER" - -Style: Clean technical cutaway diagram, white background, professional engineering illustration, labeled with arrows pointing to each component. Color-coded: structural parts in gray/silver, electrical parts in blue, safety parts in red. Isometric or 3/4 perspective view. -``` - ---- - -### 2.2 슬랫 인터록킹 구조 (Slat Interlocking) - -``` -Technical cross-section illustration showing how fire shutter steel slats interlock with each other. - -Show 3-4 slats connected in interlocking pattern: -- Each slat is a C-shaped or S-shaped profile made from 1.6mm EGI steel -- The curved edges of adjacent slats hook into each other, allowing flexibility while maintaining a continuous curtain surface -- One slat highlighted with dimension labels: "THICKNESS 1.6mm", "PITCH 75-100mm" -- Show the slight curved profile that allows the slat to wrap around the shaft when rolled up -- Arrow labeled "ROLLING DIRECTION" - -Label each part: "SLAT", "INTERLOCKING JOINT", "EGI STEEL 1.6mm" - -Style: Clean engineering cross-section diagram, white background, metallic silver color for steel. Include dimension lines. Zoomed-in detail view with magnified interlocking joint area in a callout circle. -``` - ---- - -### 2.3 가이드레일 단면도 (Guide Rail Cross-Section) - -``` -Technical cross-section illustration of a fire shutter guide rail mounted on a fireproof wall, viewed from top-down. - -Show the C-channel shaped guide rail: -- C-channel profile, steel thickness 2.3mm+ -- Inside the channel: slat edge sitting in the groove -- Smoke seal material strips on both sides of the channel, pressing against the slat -- Anchor bolts securing the guide rail to the concrete wall -- Wall shown as hatched concrete pattern - -Labels with arrows: -- "GUIDE RAIL BODY (C-CHANNEL)" -- "SLAT EDGE" -- "SMOKE SEAL PACKING" -- "ANCHOR BOLT" -- "FIREPROOF WALL" -- "STEEL 2.3mm+" - -Style: Clean technical cross-section, white background, steel parts in metallic gray, seal material in orange/red, wall in light brown hatched pattern. Include dimension annotations. -``` - ---- - -### 2.4 샤프트 어셈블리 (Shaft Assembly) - -``` -Technical illustration showing the inside of a fire shutter head box, exploded or cutaway view. - -Show these components assembled on or around the shaft: -- Central pipe labeled "SHAFT" with slats attached, partially wound -- Left side: "BRACKET" steel plate bolted to wall, with "BEARING" supporting shaft end -- Right side: "GEAR BOX" and "MOTOR" mounted on bracket -- "ELECTROMAGNETIC BRAKE" attached to motor assembly -- "BALANCE SPRING" torsion spring visible inside the shaft -- "AUTO CLOSER" device mounted near the brake -- "LIMIT SWITCH" small switches with actuator arms -- "HEAD BOX CASE" shown as transparent or partially removed to reveal internals -- Wiring connections going down labeled "TO CONTROLLER" - -Style: Exploded technical diagram or cutaway 3D illustration, white background, professional engineering style. Color-coded: mechanical parts in silver/gray, motor in dark blue, brake in red, spring in green. All labels in English with leader lines. -``` - ---- - -### 2.5 감속기+모터+브레이크 (Gear Box + Motor + Brake Assembly) - -``` -Technical illustration of a fire shutter drive unit assembly, showing three main components connected together. - -Show them assembled in sequence with labels: -1. "MOTOR (220V)" - cylindrical body with power cables -2. "ELECTROMAGNETIC BRAKE" - disc-type brake between motor and gearbox, showing brake disc, coil, and spring -3. "WORM GEAR BOX" - rectangular housing with cutaway revealing the worm gear and worm wheel inside - -Assembly order shown with arrows: MOTOR → BRAKE → GEAR BOX → "OUTPUT TO SHAFT" -Include rotation direction arrows - -Small inset callout showing worm gear mechanism detail labeled: "WORM", "WORM WHEEL", "SELF-LOCKING" - -Style: Technical exploded/assembly diagram, white background, metallic rendering, engineering illustration style. -``` - ---- - -### 2.6 연동제어기 시스템 (Controller System) - -``` -Technical schematic diagram showing the fire shutter interlock control system wiring and signal flow. - -Layout (block diagram style): -- Top center: "FIRE ALARM PANEL" - rectangular box -- Left: "SMOKE DETECTOR (PHOTOELECTRIC)" - circular device on ceiling -- Right: "HEAT DETECTOR (FIXED TEMP.)" - circular device on ceiling -- Center: "FIRE SHUTTER CONTROLLER" - panel with LED indicators labeled "POWER", "PARTIAL CLOSE", "FULL CLOSE" -- Below controller: "AUTO CLOSER" connected to shutter mechanism -- Bottom left: "MANUAL CONTROL BOX" with "UP / STOP / DOWN" buttons -- Bottom: "FIRE SHUTTER" shown schematically - -Signal flow arrows with labels: -- Smoke detector → Controller: "STAGE 1: PARTIAL CLOSE (1m gap)" -- Heat detector → Controller: "STAGE 2: FULL CLOSE (floor sealed)" -- Controller → Auto closer: "CLOSE COMMAND" -- Controller → Speaker icon: "ALARM OUTPUT" -- Controller ↔ Fire alarm panel: "STATUS SIGNAL" - -Style: Clean schematic/block diagram, white background, professional electrical diagram style. Color coding: red for fire signals, blue for power, green for status. All labels in English. -``` - ---- - -### 2.7 2단계 폐쇄 시퀀스 (2-Stage Closure Sequence) - -``` -Technical illustration showing the two-stage closing sequence of an automatic fire shutter, presented as 3 side-by-side panels: - -Panel 1 - Title: "NORMAL (OPEN)": -- Fire shutter fully open, rolled up inside head box -- People walking through the opening freely -- Smoke and heat detectors on ceiling shown in standby (green LED) -- Caption: "Shutter open, passage clear" - -Panel 2 - Title: "STAGE 1: PARTIAL CLOSE": -- Smoke detector activated (red LED, smoke wisps shown) -- Shutter descended leaving about 1 meter gap from floor -- A person crouching to pass under the gap -- Alarm buzzer icon showing sound waves -- Caption: "Smoke detected → Partial close, 1m gap for evacuation" - -Panel 3 - Title: "STAGE 2: FULL CLOSE": -- Heat detector activated (red LED, flames shown) -- Shutter fully closed to floor, bottom bar sealed against floor -- Fire and smoke on one side, clean air on other side -- Caption: "Heat detected → Full close, fire/smoke blocked" - -Arrow at bottom labeled "TIME SEQUENCE →" - -Style: Clean technical illustration with slight architectural rendering, sequential format left to right, white background. People as simple silhouettes. Fire/smoke rendered subtly. -``` - ---- - -### 2.8 롤포밍 공정 (Roll Forming Process) - -``` -Technical illustration showing the roll forming manufacturing process for fire shutter steel slats, production line viewed from the side. - -Show the line from left to right with labels: -1. "UNCOILER" - Steel coil (EGI 1.6mm) being unrolled -2. "LEVELER" - Flattening rollers correcting coil curvature -3. "ROLL FORMING STATION" - 6-8 pairs of forming rollers progressively shaping the flat strip into C/S-shaped slat profile -4. "CUTTING STATION" - Flying shear cutting the formed strip to length -5. "FINISHED SLATS" - Slats stacked neatly on output table - -Detail callout at top showing progressive cross-section shape changes: "FLAT → STAGE 1 → STAGE 2 → STAGE 3 → FINAL PROFILE" - -Arrow at bottom: "MATERIAL FLOW →" -Label on coil: "EGI STEEL COIL 1.6mm" - -Style: Technical factory/manufacturing illustration, clean white background, machinery in industrial gray/green, steel in silver. Side view. Directional arrows showing material flow. -``` - ---- - -### 2.9 현장 설치 (Field Installation) - -``` -Technical illustration showing fire shutter installation at a construction site, depicting key installation steps in a single scene. - -Scene showing a large building opening (about 5m wide, 4m tall) with: -- Two workers on scaffolding installing the head box assembly at the top -- Brackets already bolted to both side walls near the ceiling -- Guide rails mounted vertically on walls with anchor bolts -- Shaft with wound slat curtain being lifted up to place on brackets -- Manual control box being mounted on adjacent wall -- Wiring conduits visible running from controller to head box -- Construction tools: level tool, drill, anchor bolts, wrenches - -Labels with arrows pointing to activities: -- "BRACKET MOUNTING" -- "GUIDE RAIL ANCHORING" -- "SHAFT PLACEMENT" -- "ELECTRICAL WIRING" -- "LEVEL CHECK" -- "ANCHOR BOLT FIXING" - -Style: Technical construction illustration, slightly warm tone, realistic building interior with exposed concrete. Workers wearing safety helmets and vests. Clean architectural illustration style. All text in English. -``` - ---- - -### 2.10 유지보수 점검 (Maintenance Inspection) - -``` -Technical illustration showing fire shutter maintenance inspection scene. - -Show a maintenance technician inspecting a fire shutter: -- Technician with safety vest and hard hat, holding a tablet -- Fire shutter partially lowered (halfway) for testing -- Close-up callout bubbles showing key inspection points: - 1. "SLAT CONDITION" - checking for deformation, rust - 2. "SMOKE SEAL CHECK" - checking guide rail seal condition - 3. "BOTTOM BAR PACKING" - checking floor seal - 4. "MOTOR / BRAKE CHECK" - head box open, listening for sounds - 5. "MANUAL BOX TEST" - pressing UP/STOP/DOWN buttons - 6. "CONTROLLER STATUS" - checking LED indicators - -Checklist overlay in corner: -☑ MOTOR OPEN/CLOSE TEST -☑ DETECTOR INTERLOCK TEST -☑ ALARM SOUND CHECK -☑ MANUAL OPERATION CHECK -☑ BOTTOM BAR SEAL CHECK - -Style: Clean technical illustration, bright well-lit building interior, professional maintenance scene. Color callout bubbles with icons. All text in English. -``` - ---- - -### 2.11 강판형 vs 스크린형 (Steel Plate vs Screen Type) - -``` -Technical side-by-side comparison illustration of two types of fire shutters in similar building openings: - -Left side - Title "STEEL PLATE TYPE": -- Steel slat fire shutter in partially closed position -- Opaque metallic surface of interlocking steel slats visible -- Heavier, industrial appearance with thick guide rails -- Bottom bar with rubber seal -- Callout: "EGI STEEL 1.6mm / HEAVY / OPAQUE / HIGH SEALING" - -Right side - Title "SCREEN / FABRIC TYPE": -- Fabric fire shutter in partially closed position -- Semi-transparent woven silica fiber screen, you can faintly see light through it -- Lighter, sleeker with thin guide rails (11mm) -- Fabric gathered at top -- Callout: "SILICA FIBER / LIGHTWEIGHT / SEMI-TRANSPARENT / RAIL 11mm" - -Center dividing line with "VS" label -Bottom comparison bar: "WEIGHT: Heavy vs Light | VISIBILITY: Opaque vs See-through | RAIL WIDTH: Wide vs 11mm" - -Style: Clean technical comparison, white background, same scale, professional product comparison layout. All text in English. -``` - ---- - -### 2.12 주요 고장 유형 (Major Fault Types) - -``` -Technical illustration showing 6 common fire shutter failure types in a 2x3 grid layout, each in its own panel with a red problem highlight: - -Panel 1 - "SLAT DERAILMENT": -- A slat edge coming out of the guide rail groove, curtain jammed -- Red circle on problem area - -Panel 2 - "MOTOR BURNOUT": -- Motor with smoke marks, burnt wiring -- Overheat warning symbol - -Panel 3 - "BRAKE PAD WEAR": -- Electromagnetic brake with worn disc pad -- Side comparison: "NEW" thick pad vs "WORN" thin pad - -Panel 4 - "CONTROLLER MALFUNCTION": -- Controller panel with error LED, disconnected wires -- Broken signal path indicator - -Panel 5 - "CLOSER SPEED FAULT": -- Shutter dropping fast, speedometer showing "0.15 m/s LIMIT EXCEEDED" -- Governor mechanism detail - -Panel 6 - "SMOKE SEAL FAILURE": -- Smoke wisps leaking through guide rail gaps -- Comparison: "NEW SEAL" vs "DEGRADED SEAL" - -Style: Technical diagnostic illustration, white background, bordered panels. Problem areas in red/orange highlight. Clean maintenance manual style. All titles and labels in English. -``` - ---- - -## 3. 이미지 파일 관리 - -### 3.1 저장 경로 - -``` -mng/public/images/academy/fire-shutter/ -├── 01-full-component-diagram.png -├── 02-slat-interlocking.png -├── 03-guide-rail-cross-section.png -├── 04-shaft-assembly.png -├── 05-gearbox-motor-brake.png -├── 06-controller-system.png -├── 07-two-stage-closure.png -├── 08-roll-forming-process.png -├── 09-field-installation.png -├── 10-maintenance-inspection.png -├── 11-steel-vs-screen-type.png -└── 12-major-fault-types.png -``` - -### 3.2 Blade 참조 예시 - -```html -방화셔터 전체 구성도 -``` - ---- - -## 관련 문서 - -- `mng/resources/views/academy/fire-shutter.blade.php` - 방화셔터 백과사전 Blade 뷰 -- `mng/app/Http/Controllers/AcademyController.php` - 아카데미 컨트롤러 - ---- - -**최종 업데이트**: 2026-02-22 diff --git a/sam/docs/features/approvals/README.md b/sam/docs/features/approvals/README.md deleted file mode 100644 index a43521c..0000000 --- a/sam/docs/features/approvals/README.md +++ /dev/null @@ -1,298 +0,0 @@ -# 결재관리 시스템 - -> **작성일**: 2026-02-28 -> **상태**: Phase 2 구현 완료 -> **프로젝트**: SAM MNG (관리자 웹) -> **우선순위**: 🔴 필수 - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM MNG 전자결재 시스템. 기안부터 최종 승인, 반려, 회수, 보류, 전결, 참조까지 기업 결재 프로세스를 디지털화한다. - -### 1.2 문서 구조 - -| 문서 | 설명 | -|------|------| -| **README.md** (이 문서) | 시스템 전체 개요, 아키텍처, 상태 관리 | -| [form-types.md](form-types.md) | 양식별 필드/JSON 구조/인터랙션 기술 명세 | -| [workflows.md](workflows.md) | 상세 워크플로우 (승인/반려/회수/보류/전결/복사재기안) | -| [api-reference.md](api-reference.md) | API 엔드포인트 명세 | -| [ui-screens.md](ui-screens.md) | 화면별 UI 구성 및 동작 | -| [db-changes-and-model-sync.md](db-changes-and-model-sync.md) | DB 변경사항 및 API/MNG 모델 동기화 현황 | - -### 1.3 구현 현황 - -| Phase | 범위 | 상태 | -|-------|------|------| -| **Phase 1** | 순차결재, 기안/상신/승인/반려/회수 | ✅ 완료 | -| **Phase 2** | 보류/해제, 전결, 참조 열람 추적, 복사 재기안 | ✅ 완료 | -| **Phase 3** | 병렬결재, 위임(대결), 알림 | 미착수 | -| **Phase 4** | ERP 연동, 결재 통계, 관리자 설정 | 미착수 | - ---- - -## 2. 아키텍처 - -### 2.1 기술 스택 - -| 계층 | 기술 | 설명 | -|------|------|------| -| 뷰 | Blade + HTMX + Alpine.js | 동적 UI, 부분 렌더링 | -| API | Laravel Controller + Service | JSON API (내부용) | -| 모델 | Eloquent ORM | Multi-tenant 스코프 | -| DB | MySQL 8.0 | API 프로젝트에서 마이그레이션 관리 | - -### 2.2 프로젝트 분리 - -``` -API (/home/aweso/sam/api) -├── database/migrations/ ← 모든 결재 테이블 마이그레이션 - -MNG (/home/aweso/sam/mng) -├── app/Models/Approvals/ ← 모델 (Approval, ApprovalStep, ApprovalForm, ApprovalLine, ApprovalDelegation) -├── app/Services/ ← ApprovalService (비즈니스 로직) -├── app/Http/Controllers/ ← ApprovalController (웹), ApprovalApiController (API) -├── resources/views/approvals/ ← Blade 뷰 -└── routes/ ← 웹 라우트 + API 라우트 -``` - -### 2.3 핵심 클래스 - -``` -ApprovalService -├── 목록 조회: getMyDrafts(), getPendingForMe(), getCompletedByMe(), getReferencesForMe() -├── CRUD: createApproval(), updateApproval(), deleteApproval(), getApproval() -├── 워크플로우: submit(), approve(), reject(), cancel(), hold(), releaseHold(), preDecide(), copyForRedraft() -├── 참조: markAsRead() -└── 유틸: getBadgeCounts(), getApprovalLines(), getApprovalForms(), saveApprovalSteps() -``` - ---- - -## 3. 데이터베이스 - -### 3.1 테이블 관계 - -``` -approval_forms (결재 양식) - │ 1:N - ▼ -approvals (결재 문서) - │ 1:N │ N:1 (self) - ▼ ▼ -approval_steps (결재 단계) approvals (parent_doc_id → 원본 문서) - -approval_lines (결재선 템플릿) ← approvals.line_id 참조 - -approval_delegations (위임 설정) ← Phase 3 준비 -``` - -### 3.2 approvals (결재 문서) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `tenant_id` | BIGINT | 테넌트 격리 | -| `document_number` | VARCHAR | `APR-YYMMDD-001` 형식 | -| `form_id` | BIGINT FK | 양식 | -| `line_id` | BIGINT FK NULL | 결재선 템플릿 | -| `title` | VARCHAR(200) | 제목 | -| `content` | JSON | 양식 필드 데이터 | -| `body` | TEXT NULL | 본문 | -| `status` | VARCHAR(20) | 문서 상태 (6가지) | -| `is_urgent` | BOOLEAN | 긴급 여부 | -| `drafter_id` | BIGINT FK | 기안자 | -| `department_id` | BIGINT FK NULL | 기안 부서 | -| `current_step` | INT | 현재 결재 단계 번호 | -| `drafted_at` | TIMESTAMP NULL | 상신 일시 | -| `completed_at` | TIMESTAMP NULL | 완료 일시 | -| `recall_reason` | TEXT NULL | 회수 사유 | -| `parent_doc_id` | BIGINT FK NULL | 재기안 원본 문서 | -| `attachments` | JSON NULL | 첨부파일 | - -### 3.3 approval_steps (결재 단계) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `approval_id` | BIGINT FK | 결재 문서 | -| `step_order` | INT | 순서 (1, 2, 3...) | -| `step_type` | VARCHAR | `approval`, `agreement`, `reference` | -| `parallel_group` | INT NULL | 병렬 그룹 (Phase 3) | -| `approver_id` | BIGINT FK | 결재자 | -| `acted_by` | BIGINT FK NULL | 실제 처리자 (대결 시) | -| `approver_name` | VARCHAR | 결재자명 스냅샷 | -| `approver_department` | VARCHAR | 부서 스냅샷 | -| `approver_position` | VARCHAR | 직급 스냅샷 | -| `status` | VARCHAR(20) | 단계 상태 (5가지) | -| `approval_type` | VARCHAR(20) | `normal`, `pre_decided`, `delegated` | -| `comment` | TEXT NULL | 결재 의견 | -| `acted_at` | TIMESTAMP NULL | 처리 일시 | -| `is_read` | BOOLEAN | 참조 열람 여부 | -| `read_at` | TIMESTAMP NULL | 열람 일시 | - -### 3.4 approval_delegations (위임 설정, Phase 3) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `tenant_id` | BIGINT FK | | -| `delegator_id` | BIGINT FK | 위임자 | -| `delegate_id` | BIGINT FK | 대리인 | -| `start_date` | DATE | 위임 시작일 | -| `end_date` | DATE | 위임 종료일 | -| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) | -| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 | -| `is_active` | BOOLEAN | 활성 여부 | -| `reason` | VARCHAR(200) | 위임 사유 | - ---- - -## 4. 상태 관리 - -### 4.1 문서 상태 (6가지) - -| 상태 | 코드 | 라벨 | 색상 | 설명 | -|------|------|------|------|------| -| 임시저장 | `draft` | 임시저장 | gray | 작성 중, 미상신 | -| 진행 | `pending` | 진행 | blue | 결재선 순환 중 | -| 완료 | `approved` | 완료 | green | 최종 승인 | -| 반려 | `rejected` | 반려 | red | 결재자가 반려 | -| 회수 | `cancelled` | 회수 | yellow | 기안자가 회수 | -| 보류 | `on_hold` | 보류 | amber | 결재자가 보류 | - -### 4.2 단계 상태 (5가지) - -| 상태 | 코드 | 라벨 | 아이콘 | 설명 | -|------|------|------|--------|------| -| 대기 | `pending` | 대기 | 숫자 | 차례 아직 아님 | -| 승인 | `approved` | 승인 | ✓ (녹색) | 승인 완료 | -| 반려 | `rejected` | 반려 | ✗ (적색) | 반려 | -| 건너뜀 | `skipped` | 건너뜀 | — (회색) | 전결/회수로 소멸 | -| 보류 | `on_hold` | 보류 | ⏸ (노란) | 보류 중 | - -### 4.3 결재 유형 (approval_type) - -| 유형 | 코드 | 아이콘 | 설명 | -|------|------|--------|------| -| 일반결재 | `normal` | ✓ | 기본 승인 | -| 전결 | `pre_decided` | ⚡ (남색) | 이후 단계 모두 건너뛰고 즉시 완료 | -| 대결 | `delegated` | — | 대리인이 처리 (Phase 3) | - -### 4.4 참여자 역할 (step_type) - -| 역할 | 코드 | 의사결정 | 설명 | -|------|------|---------|------| -| 결재 | `approval` | ✅ 있음 | 승인/반려/보류/전결 가능 | -| 합의 | `agreement` | ✅ 있음 | 타부서 동의 (승인/반려 가능) | -| 참조 | `reference` | ❌ 없음 | 열람만 가능, 열람 추적 | - -### 4.5 상태 전이 다이어그램 - -``` - ┌─────────────────────────────┐ - │ │ - ┌────────┐ submit() │ ┌─────────┐ │ - │ draft │────────────→│ │ pending │ │ - └────────┘ │ └────┬────┘ │ - ▲ │ │ │ - │ │ ┌────┼─────────┬───────┐ │ - │ (수정 후 재상신) │ │ │ │ │ │ - │ │ │ approve() reject() hold()│ - │ │ │ │ │ │ │ - │ │ │ ▼ ▼ ▼ │ - │ │ │ 다음 step rejected on_hold│ - │ │ │ 또는 │ │ │ - │ │ │ approved │ releaseHold() - │ │ │ │ │ │ │ - │ │ │ │ │ │ │ - │ │ └────┼────────┼───────┘ │ - │ │ │ │ │ - │ │ preDecide() │ │ - │ │ → approved │ │ - │ │ │ │ cancel() │ - │ │ │ │ │ │ - │ │ ▼ │ ▼ │ - │ │ ┌─────────┐ │ ┌──────────┐ - │ │ │approved │ │ │cancelled │ - │ │ └─────────┘ │ └──────────┘ - │ │ │ │ │ - │ │ │ │ │ - │ │ copyForRedraft() │ - │ │ │ │ │ - └───────────────────┼───────┴────────┘ │ - (새 draft 생성) │ │ - │ copyForRedraft() │ - │◀──────────────────────┘ - └─────────────────────────────┘ -``` - ---- - -## 5. 권한 매트릭스 - -### 5.1 누가 무엇을 할 수 있는가 - -| 액션 | 대상자 | 조건 | -|------|--------|------| -| **기안 작성** | 모든 사용자 | — | -| **수정** | 기안자 | `draft` 또는 `rejected` | -| **삭제** | 기안자 | `draft`만 | -| **상신** | 기안자 | `draft` 또는 `rejected`, 결재선 1명 이상 | -| **승인** | 현재 결재자 | `pending`, 자신이 현재 차례 | -| **반려** | 현재 결재자 | `pending`, 사유 필수 | -| **보류** | 현재 결재자 | `pending`, 사유 필수 | -| **보류 해제** | 보류한 결재자 | `on_hold`, 자신이 보류한 건 | -| **전결** | 현재 결재자 | `pending`, 이후 모든 단계 건너뜀 | -| **회수** | 기안자 | `pending` 또는 `on_hold`, 첫 결재자 미처리 | -| **복사 재기안** | 기안자 | `approved`, `rejected`, `cancelled` | -| **참조 열람** | 참조자 | `reference` step 보유 | - -### 5.2 회수 가능 조건 상세 - -``` -회수(cancel) 가능 여부 판단: - -1. 문서 상태가 pending 또는 on_hold인가? → 아니면 불가 -2. 요청자가 기안자(drafter_id)인가? → 아니면 불가 -3. 첫 번째 결재자(approval/agreement)의 상태가 pending 또는 on_hold인가? - → 이미 approved/rejected이면 불가 (첫 결재자가 이미 처리) -``` - ---- - -## 6. 메뉴 구조 - -``` -결재관리 -├── 기안함 /approval-mgmt/drafts ← 내가 기안한 문서 -├── 결재 대기함 /approval-mgmt/pending ← 내가 결재해야 할 문서 -├── 처리 완료함 /approval-mgmt/completed ← 내가 결재한 문서 -└── 참조함 /approval-mgmt/references ← 참조 문서 (열람 추적) -``` - -### 추가 페이지 - -| URL | 설명 | -|-----|------| -| `/approval-mgmt/create` | 기안 작성 | -| `/approval-mgmt/{id}` | 상세 조회 | -| `/approval-mgmt/{id}/edit` | 기안 수정 | - ---- - -## 7. 관련 문서 - -- [결재 양식 기술 명세](form-types.md) — 양식별 필드, JSON 구조, 인터랙션 -- [결재관리 워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 -- [API 명세](api-reference.md) — 엔드포인트 목록 및 요청/응답 예시 -- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작 -- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/approvals/api-reference.md b/sam/docs/features/approvals/api-reference.md deleted file mode 100644 index b63e31b..0000000 --- a/sam/docs/features/approvals/api-reference.md +++ /dev/null @@ -1,594 +0,0 @@ -# 결재관리 API 명세 - -> **작성일**: 2026-02-28 -> **상태**: Phase 2 구현 완료 -> **Base URL**: `/api/admin/approvals` -> **미들웨어**: `web`, `auth`, `hq.member` -> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [UI 화면](ui-screens.md) - ---- - -## 1. 개요 - -모든 API는 JSON 응답을 반환한다. 인증은 세션 기반이며, CSRF 토큰이 필요하다. - -### 1.1 공통 응답 형식 - -**성공:** - -```json -{ - "success": true, - "message": "처리 메시지", - "data": { ... } -} -``` - -**실패 (400):** - -```json -{ - "success": false, - "message": "에러 메시지" -} -``` - -### 1.2 공통 헤더 - -``` -Content-Type: application/json -Accept: application/json -X-CSRF-TOKEN: {csrf_token} -``` - ---- - -## 2. 목록 조회 API - -### 2.1 기안함 - -내가 기안한 문서 목록을 조회한다. - -``` -GET /api/admin/approvals/drafts -``` - -**Query Parameters:** - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `search` | string | 제목/문서번호 검색 | -| `status` | string | 상태 필터 (`draft`, `pending`, `approved`, `rejected`, `cancelled`, `on_hold`) | -| `is_urgent` | boolean | 긴급 문서만 | -| `date_from` | date | 시작일 (YYYY-MM-DD) | -| `date_to` | date | 종료일 (YYYY-MM-DD) | -| `per_page` | int | 페이지당 건수 (기본 15) | -| `page` | int | 페이지 번호 | - -**응답:** Laravel 페이지네이션 형식 - -```json -{ - "data": [ - { - "id": 1, - "document_number": "APR-260228-001", - "title": "휴가 신청", - "status": "pending", - "is_urgent": false, - "form": { "id": 1, "name": "휴가신청서" }, - "steps": [...], - "created_at": "2026-02-28T10:00:00", - "drafted_at": "2026-02-28T10:05:00" - } - ], - "current_page": 1, - "last_page": 3, - "per_page": 15, - "total": 42 -} -``` - ---- - -### 2.2 결재 대기함 - -내가 현재 결재해야 할 문서 목록을 조회한다. - -``` -GET /api/admin/approvals/pending -``` - -**Query Parameters:** - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `search` | string | 제목/문서번호 검색 | -| `is_urgent` | boolean | 긴급 문서만 | -| `date_from` | date | 시작일 | -| `date_to` | date | 종료일 | -| `per_page` | int | 페이지당 건수 | - -> 현재 사용자가 결재 차례인 문서만 표시된다. 이미 승인/반려한 문서는 표시되지 않는다. - ---- - -### 2.3 처리 완료함 - -내가 승인 또는 반려한 문서 목록을 조회한다. - -``` -GET /api/admin/approvals/completed -``` - -**Query Parameters:** - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `search` | string | 제목/문서번호 검색 | -| `status` | string | 상태 필터 | -| `date_from` | date | 시작일 | -| `date_to` | date | 종료일 | -| `per_page` | int | 페이지당 건수 | - ---- - -### 2.4 참조함 - -내가 참조자로 지정된 문서 목록을 조회한다. - -``` -GET /api/admin/approvals/references -``` - -**Query Parameters:** - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `search` | string | 제목/문서번호 검색 | -| `is_read` | string | 열람 상태 필터 (`true`=열람완료, `false`=미열람) | -| `date_from` | date | 시작일 | -| `date_to` | date | 종료일 | -| `per_page` | int | 페이지당 건수 | - ---- - -## 3. CRUD API - -### 3.1 상세 조회 - -``` -GET /api/admin/approvals/{id} -``` - -**응답:** - -```json -{ - "success": true, - "data": { - "id": 1, - "tenant_id": 1, - "document_number": "APR-260228-001", - "form_id": 1, - "line_id": null, - "title": "휴가 신청", - "content": {}, - "body": "2월 27일~28일 연차 사용 신청합니다.", - "status": "pending", - "is_urgent": false, - "drafter_id": 10, - "department_id": 3, - "current_step": 2, - "drafted_at": "2026-02-28T10:05:00", - "completed_at": null, - "recall_reason": null, - "parent_doc_id": null, - "form": { "id": 1, "name": "휴가신청서" }, - "drafter": { "id": 10, "name": "홍길동" }, - "line": null, - "steps": [ - { - "id": 1, - "step_order": 1, - "step_type": "approval", - "approver_id": 20, - "approver_name": "김과장", - "approver_department": "경영지원팀", - "approver_position": "과장", - "status": "approved", - "approval_type": "normal", - "comment": "승인합니다.", - "acted_at": "2026-02-28T11:00:00", - "is_read": false, - "read_at": null - }, - { - "id": 2, - "step_order": 2, - "step_type": "approval", - "approver_id": 30, - "approver_name": "박부장", - "approver_department": "경영지원팀", - "approver_position": "부장", - "status": "pending", - "approval_type": "normal", - "comment": null, - "acted_at": null, - "is_read": false, - "read_at": null - } - ] - } -} -``` - ---- - -### 3.2 생성 (임시저장) - -``` -POST /api/admin/approvals -``` - -**Request Body:** - -```json -{ - "form_id": 1, - "title": "휴가 신청", - "body": "2월 27일~28일 연차 사용", - "is_urgent": false, - "steps": [ - { "user_id": 20, "step_type": "approval" }, - { "user_id": 30, "step_type": "approval" }, - { "user_id": 40, "step_type": "reference" } - ] -} -``` - -**Validation:** - -| 필드 | 규칙 | -|------|------| -| `form_id` | required, exists:approval_forms,id | -| `title` | required, string, max:200 | -| `body` | nullable, string | -| `is_urgent` | boolean | -| `steps` | nullable, array | -| `steps.*.user_id` | required_with:steps, exists:users,id | -| `steps.*.step_type` | required_with:steps, in:approval,agreement,reference | - -**응답 (201):** - -```json -{ - "success": true, - "message": "결재 문서가 저장되었습니다.", - "data": { ... } -} -``` - ---- - -### 3.3 수정 - -``` -PUT /api/admin/approvals/{id} -``` - -> `draft` 또는 `rejected` 상태에서만 수정 가능 - -**Request Body:** (생성과 동일, 모든 필드 선택) - -**Validation:** - -| 필드 | 규칙 | -|------|------| -| `title` | sometimes, string, max:200 | -| `body` | nullable, string | -| `is_urgent` | boolean | -| `steps` | nullable, array | - ---- - -### 3.4 삭제 - -``` -DELETE /api/admin/approvals/{id} -``` - -> `draft` 상태에서만 삭제 가능 - -**응답:** - -```json -{ - "success": true, - "message": "결재 문서가 삭제되었습니다." -} -``` - ---- - -## 4. 워크플로우 API - -### 4.1 상신 - -``` -POST /api/admin/approvals/{id}/submit -``` - -> 기안자가 `draft`/`rejected` 문서를 결재 요청한다. - -**Request Body:** 없음 - -**응답:** `{ "success": true, "message": "결재가 상신되었습니다.", "data": {...} }` - ---- - -### 4.2 승인 - -``` -POST /api/admin/approvals/{id}/approve -``` - -> 현재 결재자가 승인한다. - -**Request Body:** - -```json -{ - "comment": "승인합니다." // 선택 -} -``` - -**응답:** `{ "success": true, "message": "승인되었습니다.", "data": {...} }` - ---- - -### 4.3 반려 - -``` -POST /api/admin/approvals/{id}/reject -``` - -> 현재 결재자가 반려한다. 사유 필수. - -**Request Body:** - -```json -{ - "comment": "예산 초과로 반려합니다." // 필수 -} -``` - -**Validation:** `comment` — required, string, max:1000 - -**응답:** `{ "success": true, "message": "반려되었습니다.", "data": {...} }` - ---- - -### 4.4 회수 - -``` -POST /api/admin/approvals/{id}/cancel -``` - -> 기안자가 `pending`/`on_hold` 문서를 회수한다. 첫 결재자 미처리 시에만 가능. - -**Request Body:** - -```json -{ - "recall_reason": "내용 수정 필요" // 선택 -} -``` - -**응답:** `{ "success": true, "message": "결재가 회수되었습니다.", "data": {...} }` - ---- - -### 4.5 보류 - -``` -POST /api/admin/approvals/{id}/hold -``` - -> 현재 결재자가 결재를 보류한다. 사유 필수. - -**Request Body:** - -```json -{ - "comment": "추가 자료 검토 필요" // 필수 -} -``` - -**Validation:** `comment` — required, string, max:1000 - -**응답:** `{ "success": true, "message": "보류되었습니다.", "data": {...} }` - ---- - -### 4.6 보류 해제 - -``` -POST /api/admin/approvals/{id}/release-hold -``` - -> 보류한 결재자가 보류를 해제한다. - -**Request Body:** 없음 - -**응답:** `{ "success": true, "message": "보류가 해제되었습니다.", "data": {...} }` - ---- - -### 4.7 전결 - -``` -POST /api/admin/approvals/{id}/pre-decide -``` - -> 현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인한다. - -**Request Body:** - -```json -{ - "comment": "전결 처리합니다." // 선택 -} -``` - -**응답:** `{ "success": true, "message": "전결 처리되었습니다.", "data": {...} }` - ---- - -### 4.8 복사 재기안 - -``` -POST /api/admin/approvals/{id}/copy -``` - -> 기안자가 `approved`/`rejected`/`cancelled` 문서를 복사하여 새 draft를 생성한다. - -**Request Body:** 없음 - -**응답:** - -```json -{ - "success": true, - "message": "문서가 복사되었습니다.", - "data": { - "id": 15, - "document_number": "APR-260228-003", - "parent_doc_id": 1, - "status": "draft", - ... - } -} -``` - -> 응답의 `data.id`를 사용하여 `/approval-mgmt/{id}/edit`로 이동한다. - ---- - -### 4.9 참조 열람 추적 - -``` -POST /api/admin/approvals/{id}/mark-read -``` - -> 참조자가 문서를 열람했음을 기록한다. - -**Request Body:** 없음 - -**응답:** `{ "success": true, "message": "열람 처리되었습니다." }` - ---- - -## 5. 유틸리티 API - -### 5.1 결재선 템플릿 목록 - -``` -GET /api/admin/approvals/lines -``` - -**응답:** - -```json -{ - "success": true, - "data": [ - { "id": 1, "name": "일반 결재선", "steps": [...] } - ] -} -``` - ---- - -### 5.2 양식 목록 - -``` -GET /api/admin/approvals/forms -``` - -**응답:** - -```json -{ - "success": true, - "data": [ - { "id": 1, "name": "휴가신청서", "is_active": true } - ] -} -``` - ---- - -### 5.3 미처리 건수 (뱃지) - -``` -GET /api/admin/approvals/badge-counts -``` - -**응답:** - -```json -{ - "success": true, - "data": { - "pending": 3, - "draft": 1, - "reference_unread": 5 - } -} -``` - -| 필드 | 설명 | -|------|------| -| `pending` | 내가 결재해야 할 문서 수 | -| `draft` | 내 임시저장 문서 수 | -| `reference_unread` | 미열람 참조 문서 수 | - ---- - -## 6. 라우트 전체 목록 - -| Method | Path | 컨트롤러 메서드 | 이름 | 설명 | -|--------|------|---------------|------|------| -| GET | `/drafts` | `drafts` | `drafts` | 기안함 | -| GET | `/pending` | `pending` | `pending` | 결재 대기함 | -| GET | `/completed` | `completed` | `completed` | 처리 완료함 | -| GET | `/references` | `references` | `references` | 참조함 | -| GET | `/lines` | `lines` | `lines` | 결재선 템플릿 | -| GET | `/forms` | `forms` | `forms` | 양식 목록 | -| GET | `/badge-counts` | `badgeCounts` | `badge-counts` | 뱃지 건수 | -| POST | `/` | `store` | `store` | 생성 | -| GET | `/{id}` | `show` | `show` | 상세 | -| PUT | `/{id}` | `update` | `update` | 수정 | -| DELETE | `/{id}` | `destroy` | `destroy` | 삭제 | -| POST | `/{id}/submit` | `submit` | `submit` | 상신 | -| POST | `/{id}/approve` | `approve` | `approve` | 승인 | -| POST | `/{id}/reject` | `reject` | `reject` | 반려 | -| POST | `/{id}/cancel` | `cancel` | `cancel` | 회수 | -| POST | `/{id}/hold` | `hold` | `hold` | 보류 | -| POST | `/{id}/release-hold` | `releaseHold` | `release-hold` | 보류 해제 | -| POST | `/{id}/pre-decide` | `preDecide` | `pre-decide` | 전결 | -| POST | `/{id}/copy` | `copyForRedraft` | `copy` | 복사 재기안 | -| POST | `/{id}/mark-read` | `markAsRead` | `mark-read` | 열람 추적 | - ---- - -## 관련 문서 - -- [README.md](README.md) — 시스템 전체 개요 -- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 -- [UI 화면 구성](ui-screens.md) — 화면별 동작 - ---- - -**최종 업데이트**: 2026-02-28 diff --git a/sam/docs/features/approvals/db-changes-and-model-sync.md b/sam/docs/features/approvals/db-changes-and-model-sync.md deleted file mode 100644 index e59cd2b..0000000 --- a/sam/docs/features/approvals/db-changes-and-model-sync.md +++ /dev/null @@ -1,286 +0,0 @@ -# 결재관리 DB 변경사항 및 API 모델 동기화 현황 - -> **작성일**: 2026-03-09 -> **상태**: 조사 완료 -> **관련**: [README.md](README.md) | [API 명세](api-reference.md) - ---- - -## 1. 개요 - -### 1.1 목적 - -2026-02-27 ~ 2026-03-05 기간에 결재관리 테이블에 대규모 컬럼 추가가 이루어졌다. 이 문서는 변경된 DB 스키마와 API/MNG 프로젝트 간 모델 동기화 상태를 기록한다. - -### 1.2 핵심 발견 - -- 마이그레이션 **15개** 실행 (API 프로젝트에서 관리) -- MNG 모델: ✅ 모든 신규 컬럼 반영 완료 -- API 모델: ❌ **`$fillable`/`$casts` 미반영** — 오류 원인 가능성 - ---- - -## 2. 마이그레이션 변경 타임라인 - -### 2.1 Phase 2 확장 (2026-02-27) - -| 마이그레이션 파일 | 대상 테이블 | 작업 | -|------------------|-----------|------| -| `add_columns_to_approvals_table` | `approvals` | `line_id`, `body`, `is_urgent`, `department_id` 추가 | -| `add_columns_to_approval_steps_table` | `approval_steps` | `approver_name`, `approver_department`, `approver_position` 추가 | -| `add_phase2_columns_to_approval_steps_table` | `approval_steps` | `parallel_group`, `acted_by`, `approval_type` 추가 | -| `add_phase2_columns_to_approvals_table` | `approvals` | `recall_reason`, `parent_doc_id` 추가 | -| `create_approval_delegations_table` | `approval_delegations` | 위임 테이블 신규 생성 | -| `add_linkable_to_approvals_table` | `approvals` | `linkable_type`, `linkable_id` 추가 (다형성) | - -### 2.2 도메인 연동 (2026-02-28) - -| 마이그레이션 파일 | 대상 테이블 | 작업 | -|------------------|-----------|------| -| `add_approval_id_to_leaves_table` | `leaves` | `approval_id` FK 추가 | -| `insert_leave_approval_form` | `approval_forms` | 휴가신청 양식 데이터 등록 | - -### 2.3 양식 확장 (2026-03-03 ~ 03-04) - -| 마이그레이션 파일 | 대상 테이블 | 작업 | -|------------------|-----------|------| -| `insert_attendance_approval_forms` | `approval_forms` | 근태신청, 사유서 양식 등록 | -| `add_body_template_to_approval_forms` | `approval_forms` | `body_template` 컬럼 추가 | -| `insert_expense_approval_form` | `approval_forms` | 지출결의서 양식 + body_template 등록 | -| `update_expense_approval_form_body_template` | `approval_forms` | 지출결의서 body_template 고도화 | - -### 2.4 추적 기능 (2026-03-05) - -| 마이그레이션 파일 | 대상 테이블 | 작업 | -|------------------|-----------|------| -| `add_drafter_read_at_to_approvals_table` | `approvals` | `drafter_read_at` 추가 | -| `add_resubmit_count_to_approvals_table` | `approvals` | `resubmit_count` 추가 | -| `add_rejection_history_to_approvals_table` | `approvals` | `rejection_history` 추가 | - ---- - -## 3. 추가된 컬럼 상세 - -### 3.1 `approvals` 테이블 (11개 컬럼 추가) - -| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | -|------|------|--------|--------|------| -| `line_id` | BIGINT FK NULL | NULL | 02-27 | 결재선 템플릿 참조 | -| `body` | LONGTEXT NULL | NULL | 02-27 | 문서 본문 HTML | -| `is_urgent` | BOOLEAN | false | 02-27 | 긴급 여부 | -| `department_id` | BIGINT NULL | NULL | 02-27 | 기안 부서 | -| `recall_reason` | TEXT NULL | NULL | 02-27 | 회수 사유 | -| `parent_doc_id` | BIGINT FK NULL | NULL | 02-27 | 재기안 원본 문서 | -| `linkable_type` | VARCHAR NULL | NULL | 02-27 | 다형성 모델 타입 | -| `linkable_id` | BIGINT NULL | NULL | 02-27 | 다형성 모델 ID | -| `drafter_read_at` | TIMESTAMP NULL | NULL | 03-05 | 기안자 열람 시각 | -| `resubmit_count` | TINYINT UNSIGNED | 0 | 03-05 | 재상신 횟수 | -| `rejection_history` | JSON NULL | NULL | 03-05 | 반려 이력 배열 | - -### 3.2 `approval_steps` 테이블 (6개 컬럼 추가) - -| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | -|------|------|--------|--------|------| -| `approver_name` | VARCHAR(50) NULL | NULL | 02-27 | 결재자명 스냅샷 | -| `approver_department` | VARCHAR(100) NULL | NULL | 02-27 | 결재자 부서 스냅샷 | -| `approver_position` | VARCHAR(50) NULL | NULL | 02-27 | 결재자 직급 스냅샷 | -| `parallel_group` | INT NULL | NULL | 02-27 | 병렬 결재 그룹 (Phase 3) | -| `acted_by` | BIGINT FK NULL | NULL | 02-27 | 실제 처리자 (대결) | -| `approval_type` | VARCHAR(20) | 'normal' | 02-27 | normal/pre_decided/delegated | - -### 3.3 `approval_forms` 테이블 (1개 컬럼 추가) - -| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | -|------|------|--------|--------|------| -| `body_template` | TEXT NULL | NULL | 03-04 | HTML 양식 렌더링 템플릿 | - -### 3.4 `approval_delegations` 테이블 (신규 생성) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `tenant_id` | BIGINT FK | 테넌트 격리 | -| `delegator_id` | BIGINT FK | 위임자 | -| `delegate_id` | BIGINT FK | 대리인 | -| `start_date` | DATE | 위임 시작일 | -| `end_date` | DATE | 위임 종료일 | -| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) | -| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 | -| `is_active` | BOOLEAN | 활성 여부 | -| `reason` | VARCHAR(200) | 위임 사유 | - ---- - -## 4. API/MNG 모델 동기화 현황 - -### 4.1 Approval 모델 비교 - -| 항목 | MNG (`mng/app/Models/Approvals/Approval.php`) | API (`api/app/Models/Tenants/Approval.php`) | -|------|:---:|:---:| -| `line_id` in $fillable | ✅ | ❌ | -| `body` in $fillable | ✅ | ❌ | -| `is_urgent` in $fillable/$casts | ✅ boolean | ❌ | -| `department_id` in $fillable | ✅ | ❌ | -| `recall_reason` in $fillable | ✅ | ❌ | -| `parent_doc_id` in $fillable | ✅ | ❌ | -| `linkable_type/id` in $fillable | ✅ | ✅ | -| `drafter_read_at` in $fillable/$casts | ✅ datetime | ❌ | -| `resubmit_count` in $fillable/$casts | ✅ integer | ❌ | -| `rejection_history` in $fillable/$casts | ✅ array | ❌ | - -### 4.2 ApprovalStep 모델 비교 - -| 항목 | MNG | API | -|------|:---:|:---:| -| `approver_name` in $fillable | ✅ | ❌ | -| `approver_department` in $fillable | ✅ | ❌ | -| `approver_position` in $fillable | ✅ | ❌ | -| `parallel_group` in $fillable | ✅ | ❌ | -| `acted_by` in $fillable | ✅ | ❌ | -| `approval_type` in $fillable | ✅ | ❌ | - -### 4.3 ApprovalForm 모델 비교 - -| 항목 | MNG | API | -|------|:---:|:---:| -| `body_template` in $fillable | ✅ | ❌ | - -### 4.4 ApprovalDelegation 모델 - -| 항목 | MNG | API | -|------|:---:|:---:| -| 모델 파일 존재 | ✅ | ❌ 미생성 | - ---- - -## 5. 오류 영향 분석 - -### 5.1 API 모델 미반영으로 인한 잠재적 오류 - -API 프로젝트의 모델 `$fillable`에 신규 컬럼이 누락되어, API 엔드포인트를 통한 결재 문서 처리 시 다음 오류가 발생할 수 있다: - -| 증상 | 원인 | 영향 범위 | -|------|------|----------| -| `create()`/`update()` 시 신규 필드 저장 안 됨 | `$fillable` 미포함 → mass assignment 차단 | API v1 결재 CRUD | -| JSON 필드(`rejection_history`) 문자열로 반환 | `$casts` 미정의 → 타입 변환 안 됨 | API 응답 파싱 오류 | -| `drafter_read_at` 날짜 비교 실패 | `$casts` datetime 미정의 → Carbon 미변환 | 열람 추적 기능 | -| `is_urgent` 비교 오류 | `$casts` boolean 미정의 → 문자열 비교 | 긴급 필터링 | -| 위임(delegation) 기능 완전 불가 | 모델 자체 미생성 | Phase 3 기능 전체 | - -### 5.2 MNG는 정상 - -MNG 프로젝트의 모델은 모든 신규 컬럼이 `$fillable`, `$casts`, `$attributes`에 반영되어 있으며, `ApprovalService`에서 정상 사용 중이다. - -``` -MNG 정상 동작 확인 기능: -✅ 반려 이력 저장 (rejection_history) -✅ 재상신 횟수 추적 (resubmit_count) -✅ 기안자 열람 추적 (drafter_read_at) -✅ 결재자 스냅샷 저장 (approver_name/department/position) -✅ 전결 처리 (approval_type = pre_decided) -✅ 회수 사유 기록 (recall_reason) -``` - ---- - -## 6. 수정 필요 파일 목록 - -### 6.1 API 모델 업데이트 필요 - -| 파일 | 수정 내용 | -|------|----------| -| `api/app/Models/Tenants/Approval.php` | `$fillable`에 9개 필드, `$casts`에 4개 필드 추가 | -| `api/app/Models/Tenants/ApprovalStep.php` | `$fillable`에 6개 필드 추가 | -| `api/app/Models/Tenants/ApprovalForm.php` | `$fillable`에 `body_template` 추가 | -| `api/app/Models/Tenants/ApprovalDelegation.php` | 모델 파일 신규 생성 | - -### 6.2 Approval.php 수정 상세 - -**`$fillable` 추가 필요:** - -```php -'line_id', -'body', -'is_urgent', -'department_id', -'recall_reason', -'parent_doc_id', -'drafter_read_at', -'resubmit_count', -'rejection_history', -``` - -**`$casts` 추가 필요:** - -```php -'drafter_read_at' => 'datetime', -'resubmit_count' => 'integer', -'rejection_history' => 'array', -'is_urgent' => 'boolean', -``` - -### 6.3 ApprovalStep.php 수정 상세 - -**`$fillable` 추가 필요:** - -```php -'approver_name', -'approver_department', -'approver_position', -'parallel_group', -'acted_by', -'approval_type', -``` - -### 6.4 ApprovalForm.php 수정 상세 - -**`$fillable` 추가 필요:** - -```php -'body_template', -``` - ---- - -## 7. 연관 테이블 참조 변경 - -결재 시스템과 연동된 다른 테이블의 변경사항: - -| 테이블 | 추가 컬럼 | 추가일 | 용도 | -|--------|----------|--------|------| -| `leaves` | `approval_id` (BIGINT FK) | 02-28 | 휴가 ↔ 결재 연동 | -| `purchases` | `approval_id` (BIGINT FK) | (기존) | 구매 ↔ 결재 연동 | - ---- - -## 8. 등록된 결재 양식 (13종) - -2026-02-28 ~ 03-07 기간에 마이그레이션으로 등록된 양식: - -| 코드 | 양식명 | 카테고리 | 등록일 | -|------|--------|---------|--------| -| `leave` | 휴가신청서 | request | 02-28 | -| `attendance_request` | 근태신청서 | request | 03-03 | -| `reason_report` | 사유서 | request | 03-03 | -| `expense` | 지출결의서 | expense | 03-04 | -| `employment_cert` | 재직증명서 | request | 03-05 | -| `career_cert` | 경력증명서 | request | 03-05 | -| `appointment_cert` | 위촉증명서 | request | 03-05 | -| `resignation` | 사직서 | request | 03-06 | -| `seal_usage` | 사용인감계 | request | 03-06 | -| `delegation` | 위임장 | request | 03-06 | -| `board_minutes` | 이사회의사록 | request | 03-06 | -| `quotation` | 견적서 | request | 03-06 | -| `official_letter` | 공문서 | request | 03-07 | - ---- - -## 관련 문서 - -- [결재관리 시스템 개요](README.md) — 아키텍처, 상태 관리, 권한 -- [API 명세](api-reference.md) — 20개 엔드포인트 상세 -- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름 -- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획 - ---- - -**최종 업데이트**: 2026-03-09 diff --git a/sam/docs/features/approvals/form-types.md b/sam/docs/features/approvals/form-types.md deleted file mode 100644 index 3242b78..0000000 --- a/sam/docs/features/approvals/form-types.md +++ /dev/null @@ -1,999 +0,0 @@ -# 결재 양식 기술 명세 - -> **작성일**: 2026-03-06 -> **상태**: Phase 2 구현 완료 -> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md) - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM MNG 결재관리의 **기안함 양식** 기술 명세. 각 양식의 필드 구조, JSON Content 데이터 형식, UI 인터랙션, 특수 로직을 정의한다. - -### 1.2 양식 목록 - -| 코드 | 양식명 | 분류 | Blade 파일 | 설명 | -|------|--------|------|------------|------| -| `BUSINESS_DRAFT` | 업무기안서 | 일반 | (body 편집기) | 일반 업무 보고·요청 | -| `leave` | 휴가신청 | 인사/근태 | `_leave-form.blade.php` | 연차, 휴가, 근태 신청 | -| `attendance_request` | 근태신청 | 인사/근태 | `_leave-form.blade.php` | 외근, 출장, 조퇴 등 | -| `reason_report` | 사유서 | 인사/근태 | `_leave-form.blade.php` | 지각, 결근 등 사유 소명 | -| `resignation` | 사직서 | 인사/근태 | `_resignation-form.blade.php` | 퇴직 서류 | -| `employment_cert` | 재직증명서 | 증명서 | `_certificate-form.blade.php` | 재직 증명 발급 (PDF) | -| `career_cert` | 경력증명서 | 증명서 | `_career-cert-form.blade.php` | 경력 증명 발급 (PDF) | -| `appointment_cert` | 위촉증명서 | 증명서 | `_appointment-cert-form.blade.php` | 위촉/임명 증명 발급 (PDF) | -| `pr_expense` | 지출품의서 | 품의 | `_purchase-request-form.blade.php` | 지출 전 사전 승인 | -| `pr_contract` | 계약체결품의서 | 품의 | `_purchase-request-form.blade.php` | 계약 체결 전 승인 | -| `pr_purchase` | 구매품의서 | 품의 | `_purchase-request-form.blade.php` | 물품 구매 전 승인 | -| `pr_trip` | 출장품의서 | 품의 | `_purchase-request-form.blade.php` | 출장 계획 승인 | -| `pr_settlement` | 비용정산품의서 | 품의 | `_purchase-request-form.blade.php` | 비용 정산 승인 | -| `expense` | 지출결의서 | 재무 | `_expense-form.blade.php` | 법인카드/송금/자동이체 지출 | - -### 1.3 공통 구조 - -모든 양식은 동일한 패턴으로 동작한다: - -``` -양식 선택 (form_id) - ↓ -양식별 Blade 파셜 렌더링 (create.blade.php 내 조건부 display) - ↓ -사용자 입력 → Alpine.js / JavaScript 인터랙션 - ↓ -getFormData() → JSON content 생성 - ↓ -ApprovalService::createApproval() → Approval.content (JSON 컬럼) 저장 -``` - -### 1.4 양식 선택 UI (2단계 분류 + 설명 카드) - -양식 선택은 **2단계 드롭다운 + 설명 카드** 레이아웃으로 구성된다. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 양식 * │ -│ ┌──── 30% ────────┐ ┌─────────────── 70% ───────────────────────────┐ │ -│ │ 📋 품의 ▼ │ │ ┌─────────────────────────────────────────┐ │ │ -│ │ │ │ │ 📋 지출품의서 │ │ │ -│ │ 지출품의서 ▼ │ │ │ 지출이 발생하기 전 사전 승인을 받는 │ │ │ -│ │ │ │ │ 문서입니다. 예산 범위 내에서 지출 항목과 │ │ │ -│ │ │ │ │ 금액을 기재하여 사전에 승락을 받습니다. │ │ │ -│ └──────────────────┘ │ └─────────────────────────────────────────┘ │ │ -│ └─────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -#### 1단계: 분류 선택 (`form_category`) - -| 분류 | 아이콘 | 포함 양식 | -|------|--------|----------| -| 일반 | 📄 | 업무기안서 | -| 인사/근태 | 👤 | 휴가신청, 근태신청, 사유서, 사직서 | -| 증명서 | 📜 | 재직증명서, 경력증명서, 위촉증명서 | -| 품의 | 📋 | 지출품의서, 계약체결품의서, 구매품의서, 출장품의서, 비용정산품의서 | -| 재무 | 💰 | 지출결의서 | - -#### 2단계: 양식 선택 (`form_id`) - -- 1단계 분류 선택 시 해당 분류에 속하지 않는 양식은 `display:none` + `disabled` -- 분류 내 첫 번째 양식 자동 선택 - -#### 설명 카드 (`formDescriptions`) - -- 양식 선택 시 우측에 해당 양식의 아이콘/제목/설명 텍스트 표시 -- 14종 전체 양식에 대한 설명 정의 (create/edit 공통) -- 색상: 양식별 Tailwind 테마 색상 (`border-*-200 bg-*-50`) - -#### 핵심 JavaScript 함수 - -| 함수 | 설명 | -|------|------| -| `buildCategoryOptions()` | 사용 가능한 카테고리만 `form_category` 옵션으로 생성 | -| `filterFormsByCategory(cat)` | 선택된 분류 외 양식 옵션 숨김/비활성화 | -| `selectCategoryByFormId(formId)` | formId로 카테고리 역산하여 자동 선택 | -| `updateFormDescription(formId)` | 설명 카드 DOM 업데이트 | - -### 1.5 파일 구조 - -``` -resources/views/approvals/ -├── create.blade.php ← 기안 작성 (2단계 양식 선택 + 설명 카드 + 동적 폼) -├── edit.blade.php ← 기안 수정 (create와 동일한 2단계 선택 구조) -├── show.blade.php ← 상세 조회 (양식별 조회 컴포넌트) -└── partials/ - ├── _leave-form.blade.php ← 휴가신청 폼 - ├── _expense-form.blade.php ← 지출결의서 폼 - ├── _expense-show.blade.php ← 지출결의서 조회 - ├── _purchase-request-form.blade.php ← 품의서 5종 통합 폼 (Alpine.js) - ├── _purchase-request-show.blade.php ← 품의서 5종 통합 조회 - ├── _certificate-form.blade.php ← 재직증명서 폼 - ├── _certificate-show.blade.php ← 재직증명서 조회 - ├── _career-cert-form.blade.php ← 경력증명서 폼 - ├── _career-cert-show.blade.php ← 경력증명서 조회 - ├── _appointment-cert-form.blade.php ← 위촉증명서 폼 - ├── _appointment-cert-show.blade.php ← 위촉증명서 조회 - ├── _resignation-form.blade.php ← 사직서 폼 - ├── _resignation-show.blade.php ← 사직서 조회 - ├── _approval-stamp-table.blade.php ← 결재 도장 테이블 - └── _approval-line-editor.blade.php ← 결재선 편집기 -``` - ---- - -## 2. 휴가신청 (leave) - -### 2.1 폼 필드 - -| 필드 ID | 라벨 | 타입 | 필수 | 기본값 | 설명 | -|---------|------|------|------|--------|------| -| `leave-user-id` | 신청자 | select | 필수 | `auth()->id()` | 활성 사원 목록 | -| `leave-type` | 유형 | select | 필수 | - | 휴가/근태신청/사유서 | -| `leave-start-date` | 시작일 | date | 필수 | - | `YYYY-MM-DD` | -| `leave-end-date` | 종료일 | date | 필수 | - | `YYYY-MM-DD` | -| `leave-reason` | 사유 | textarea | 선택 | - | 자유 텍스트 | - -### 2.2 Content JSON - -```json -{ - "user_id": "10", - "leave_type": "연차", - "start_date": "2026-03-06", - "end_date": "2026-03-07", - "reason": "개인 사유" -} -``` - -### 2.3 특수 로직 - -- **자동 선택**: 로그인 사용자가 기본 선택 (`auth()->id()`) -- **직원 목록**: `$employees` Props로 전달 (활성 사원만) -- **단순 구조**: Alpine.js 없이 Blade 폼으로 구현 - ---- - -## 3. 지출결의서 (expense) - -### 3.1 폼 구조 (Alpine.js 기반) - -```javascript -x-data="expenseForm(initialData, authUserName, initialFiles, cardsData, accountsData)" -``` - -### 3.2 기본 정보 필드 - -| 필드 | 라벨 | 타입 | 필수 | 기본값 | -|------|------|------|------|--------| -| `expense_type` | 지출형식 | radio | 필수 | `corporate_card` | -| `tax_invoice` | 세금계산서 | radio | 필수 | `normal` | -| `write_date` | 작성일자 | date | 선택 | 오늘 | -| `approval_date` | 결재일자 | date | 선택 | 오늘 | -| `department` | 부서 | text | 선택 | `경리부` | -| `writer_name` | 이름 | text | 선택 | 인증 사용자명 | - -### 3.3 지출형식별 선택 - -| 지출형식 | 코드 | 연결 데이터 | -|---------|------|------------| -| 법인카드 | `corporate_card` | `$cards` → `selected_card` | -| 송금 | `transfer` | `$accounts` → `selected_account` | -| 자동이체 출금 | `auto_transfer` | `$accounts` → `selected_account` | -| 현금/가지급정산 | `cash_advance` | 없음 | - -**법인카드 선택 시 저장 구조:** - -```json -{ - "selected_card": { - "id": 1, - "card_name": "삼성카드", - "card_company": "삼성", - "card_number_last4": "1234", - "card_holder_name": "홍길동" - } -} -``` - -**계좌 선택 시 저장 구조:** - -```json -{ - "selected_account": { - "id": 1, - "bank_name": "국민은행", - "account_number": "123-456-789012", - "account_holder": "주일기업" - } -} -``` - -### 3.4 세금계산서 옵션 - -| 옵션 | 코드 | -|------|------| -| 일반 | `normal` | -| 이월발행 | `deferred` | -| 없음 | `none` | - -### 3.5 내역 테이블 - -**동적 rows** (`.items` 배열): - -| 필드 | 라벨 | 타입 | 설명 | -|------|------|------|------| -| `date` | 일자 | date | `YYYY-MM-DD` | -| `description` | 적요 | text | 지출 설명 | -| `amount` | 금액 | number | 콤마 제거 정수 | -| `vendor` | 거래처 | text | Autocomplete 검색 | -| `vendor_id` | 거래처 ID | hidden | API 연결 ID | -| `vendor_biz_no` | 사업자번호 | hidden | 자동 채움 | -| `bank` | 은행명 | text | 수동 입력 | -| `account_no` | 계좌번호 | text | 수동 입력 | -| `depositor` | 예금주 | text | 수동 입력 | -| `remark` | 비고 | text | 메모 | - -### 3.6 Content JSON (전체) - -```json -{ - "expense_type": "corporate_card", - "tax_invoice": "normal", - "write_date": "2026-03-06", - "approval_date": "2026-03-06", - "department": "경리부", - "writer_name": "홍길동", - "items": [ - { - "date": "2026-03-05", - "description": "사무용품 구매", - "amount": 150000, - "vendor": "오피스디포", - "vendor_id": 123, - "vendor_biz_no": "123-45-67890", - "bank": "", - "account_no": "", - "depositor": "", - "remark": "" - } - ], - "total_amount": 150000, - "attachment_memo": "영수증 첨부", - "selected_card": { ... }, - "selected_account": null -} -``` - -### 3.7 특수 기능 - -#### 거래처 검색 (Autocomplete) - -``` -입력 → 250ms 디바운싱 → API 호출 → 드롭다운 렌더링 - -API: /barobill/tax-invoice/search-partners?keyword=... -키보드: ↑↓(네비게이션), Enter(선택), Esc(닫기) -마우스: 항목 클릭(선택) -``` - -#### 금액 입력 포맷팅 - -``` -입력 시: 콤마 제거 → 정수 저장 (parseMoney) -표시 시: 콤마 포맷 (formatMoney) -합계: totalAmount getter → footer 실시간 업데이트 -``` - -#### 파일 업로드 - -``` -드래그 앤 드롭 + 파일 입력 -최대: 20MB -형식: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, jpeg, png, gif, zip, rar -API: POST /api/admin/approvals/upload-file -진행률: XHR 업로드 이벤트 -``` - -#### 카드/계좌 연동 - -``` -카드 선택 → 모든 내역 행에 "결제카드" 자동 표시 -계좌 선택 → 모든 내역 행에 "은행/계좌/예금주" 자동 채움 -``` - -### 3.8 조회 화면 (_expense-show.blade.php) - -| 섹션 | 내용 | -|------|------| -| 기본 정보 | 지출형식, 세금계산서, 작성일, 결재일, 부서, 이름 | -| 선택 카드/계좌 | 유색 박스로 표시 | -| 내역 테이블 | 읽기 전용, `number_format()` 금액 | -| 첨부서류 메모 | `whitespace-pre-wrap` | -| 첨부파일 목록 | 다운로드 링크 + 파일 크기 | - ---- - -## 4. 증명서 양식 공통 - -### 4.1 공통 패턴 - -모든 증명서 양식은 동일한 패턴을 따른다: - -``` -사원 선택 → loadXxxInfo(userId) → API 호출 → 읽기 전용 필드 자동 채움 - ↓ - 일부 필드만 수정 가능 - ↓ - 미리보기 모달 (인쇄 가능) -``` - -### 4.2 공통 함수 - -| 함수 | 설명 | -|------|------| -| `loadXxxInfo(userId)` | 사원 선택 시 인적/재직 정보 로드 | -| `openXxxPreview()` | 미리보기 모달 열기 | -| `printXxxPreview()` | 미리보기 인쇄 (`window.print()`) | -| `closeXxxPreview()` | 미리보기 닫기 | -| `onXxxPurposeChange()` | 용도 선택 시 직접입력 필드 표시 | - -### 4.3 조회 화면 공통 - -- 읽기 전용 필드 표시 -- PDF 다운로드: `route('api.admin.approvals.cert-pdf', $approval->id)` - ---- - -## 5. 재직증명서 (employment_cert) - -### 5.1 폼 필드 - -| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | -|------|---------|------|------|------|------| -| 인적사항 | `cert-name` | 성명 | text | readonly | DB 자동 채움 | -| | `cert-resident` | 주민등록번호 | text | readonly | DB 자동 채움 | -| | `cert-address` | 주소 | text | editable | 직접 입력 | -| 재직사항 | `cert-company` | 회사명 | text | readonly | DB 자동 채움 | -| | `cert-business-num` | 사업자번호 | text | readonly | DB 자동 채움 | -| | `cert-department` | 근무부서 | text | readonly | DB 자동 채움 | -| | `cert-position` | 직급 | text | readonly | DB 자동 채움 | -| | `cert-hire-date` | 재직기간 | text | readonly | DB 자동 채움 | -| 발급정보 | `cert-purpose-select` | 사용용도 | select | editable | 드롭다운 선택 | -| | (custom) | 기타 용도 | text | editable | "기타" 선택 시 표시 | -| | `cert-issue-date` | 발급일 | text | readonly | `now()->format('Y-m-d')` | - -### 5.2 Content JSON - -```json -{ - "name": "홍길동", - "resident_number": "900101-1XXXXXX", - "address": "서울특별시 강남구", - "company_name": "(주)코드브릿지엑스", - "business_num": "123-45-67890", - "department": "개발팀", - "position": "과장", - "hire_date": "2020-03-01", - "purpose": "은행 제출용", - "issue_date": "2026-03-06" -} -``` - ---- - -## 6. 경력증명서 (career_cert) - -### 6.1 폼 필드 (재직증명서 대비 추가/변경) - -| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | -|------|---------|------|------|------|------| -| 인적사항 | `cc-birth-date` | 생년월일 | text | readonly | DB 자동 채움 | -| 경력사항 | `cc-ceo-name` | 대표자 | text | readonly | DB 자동 채움 | -| | `cc-phone` | 대표전화 | text | readonly | DB 자동 채움 | -| | `cc-company-address` | 소재지 | text | readonly | DB 자동 채움 | -| | `cc-department` | 소속부서 | text | readonly | DB 자동 채움 | -| | `cc-position` | 직위/직급 | text | readonly | DB 자동 채움 | -| | `cc-hire-date` | 근무기간 시작 | text | readonly | DB 자동 채움 | -| | `cc-resign-date` | 근무기간 종료 | date | editable | 직접 입력 | -| | `cc-job-description` | 담당업무 | text | editable | 직접 입력 | -| 발급정보 | 용도 | select | editable | + "이직 제출용" 옵션 | - -### 6.2 Content JSON - -```json -{ - "name": "홍길동", - "birth_date": "1990-01-01", - "address": "서울특별시 강남구", - "company_name": "(주)코드브릿지엑스", - "business_num": "123-45-67890", - "ceo_name": "김대표", - "phone": "02-1234-5678", - "company_address": "서울특별시 강남구 테헤란로", - "department": "개발팀", - "position": "과장", - "hire_date": "2020-03-01", - "resign_date": "2026-02-28", - "job_description": "웹 애플리케이션 개발", - "purpose": "이직 제출용", - "issue_date": "2026-03-06" -} -``` - ---- - -## 7. 위촉증명서 (appointment_cert) - -### 7.1 폼 필드 - -| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | -|------|---------|------|------|------|------| -| 인적사항 | `ac-name` | 성명 | text | readonly | DB 자동 채움 | -| | `ac-resident` | 주민등록번호 | text | readonly | DB 자동 채움 | -| | `ac-department` | 소속 | text | readonly | DB 자동 채움 | -| | `ac-phone` | 연락처 | text | editable | 직접 입력 | -| 위촉정보 | `ac-hire-date` | 위촉기간 시작 | text | readonly | DB 자동 채움 | -| | `ac-resign-date` | 위촉기간 종료 | date | editable | 직접 입력 | -| | `ac-contract-type` | 계약자격 | text | editable | 직접 입력 | -| 발급정보 | `ac-purpose-select` | 용도 | select | editable | 드롭다운 선택 | -| | `ac-issue-date` | 발급일 | text | readonly | 자동 설정 | -| (숨김) | `ac-company-name` | 회사명 | hidden | - | 미리보기용 | -| | `ac-ceo-name` | 대표자명 | hidden | - | 미리보기용 | - -### 7.2 Content JSON - -```json -{ - "name": "홍길동", - "resident_number": "900101-1XXXXXX", - "department": "기술자문팀", - "phone": "010-1234-5678", - "hire_date": "2024-01-01", - "resign_date": "2026-12-31", - "contract_type": "기술자문위원", - "purpose": "관공서 제출용", - "issue_date": "2026-03-06" -} -``` - ---- - -## 8. 사직서 (resignation) - -### 8.1 폼 필드 - -| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 필수 | -|------|---------|------|------|------|------| -| 인적사항 | `rg-department` | 소속 | text | readonly | - | -| | `rg-position` | 직위 | text | readonly | - | -| | `rg-name` | 성명 | text | readonly | - | -| | `rg-resident` | 주민등록번호 | text | readonly | - | -| | `rg-hire-date` | 입사일 | text | readonly | - | -| | `rg-resign-date` | 퇴사(예정)일 | date | editable | 필수 | -| | `rg-address` | 주소 | text | editable | - | -| 사직사유 | `rg-reason-select` | 사유 | select | editable | 필수 | -| | (custom) | 기타 사유 | text | editable | - | -| 제출일 | `rg-issue-date` | 제출일 | text | readonly | - | - -### 8.2 사직사유 옵션 - -| 옵션 | -|------| -| 일신상의 사유 | -| 가사 사정 | -| 건강상의 이유 | -| 진학/학업 | -| 이직 | -| 기타 (직접입력) | - -### 8.3 Content JSON - -```json -{ - "department": "개발팀", - "position": "대리", - "name": "홍길동", - "resident_number": "900101-1XXXXXX", - "hire_date": "2020-03-01", - "resign_date": "2026-04-01", - "address": "서울특별시 강남구", - "reason": "이직", - "issue_date": "2026-03-06" -} -``` - ---- - -## 9. 품의서 5종 공통 (_purchase-request-form/show) - -### 9.1 통합 Alpine.js 컴포넌트 - -품의서 5종은 **단일 Blade 파일**(`_purchase-request-form.blade.php`)에서 `prType` 프로퍼티로 동적 전환된다. - -```javascript -x-data="purchaseRequestForm(initialData, authUserName, initialFiles)" -``` - -#### 타입 전환 메커니즘 - -``` -create.blade.php → switchFormMode() - ↓ -code.startsWith('pr_') 감지 - ↓ -#purchase-request-form-container display: block - ↓ -setTimeout(50ms) → _x_dataStack[0].setPrType(code) - ↓ -Alpine.js x-if 분기 → 해당 폼 렌더링 -``` - -#### prType 코드 및 라벨 - -| prType | 라벨 | 색상 | -|--------|------|------| -| `pr_expense` | 지출품의서 | `bg-orange-50 text-orange-700` | -| `pr_contract` | 계약체결품의서 | `bg-purple-50 text-purple-700` | -| `pr_purchase` | 구매품의서 | `bg-blue-50 text-blue-700` | -| `pr_trip` | 출장품의서 | `bg-green-50 text-green-700` | -| `pr_settlement` | 비용정산품의서 | `bg-teal-50 text-teal-700` | - -### 9.2 공통 필드 (모든 품의서) - -| 필드 | 라벨 | 타입 | 기본값 | -|------|------|------|--------| -| `write_date` | 작성일자 | date | 오늘 | -| `department` | 요청부서 | text | - | -| `writer_name` | 요청자 | text | 인증 사용자명 | -| `attachment_memo` | 첨부서류 메모 | textarea | - | -| `files` | 파일 업로드 | file[] | - | - -### 9.3 공통 함수 - -| 함수 | 설명 | -|------|------| -| `setPrType(type)` | 외부에서 prType 설정 (switchFormMode에서 호출) | -| `getFormData()` | prType별 다른 JSON 구조 반환 (base에 `pr_type` 포함) | -| `addItem()` | 내역 행 추가 | -| `removeItem(index)` | 내역 행 삭제 | -| `formatMoney(val)` | 숫자 → 콤마 포맷 | -| `parseMoney(str)` | 콤마 문자열 → 정수 | -| `prVendorSearch(target, fieldName)` | 범용 거래처 Autocomplete 검색 | - -### 9.4 조회 화면 분기 (show.blade.php) - -```php -// show.blade.php에서 pr_ prefix로 분기 -@if(str_starts_with($approval->form?->code ?? '', 'pr_')) - @include('approvals.partials._purchase-request-show', ['content' => $content]) -@endif -``` - -`_purchase-request-show.blade.php`에서 `$content['pr_type']`으로 5종 분기 렌더링. - ---- - -## 10. 지출품의서 (pr_expense) - -### 10.1 추가 필드 - -| 필드 | 라벨 | 타입 | 필수 | -|------|------|------|------| -| `expense_category` | 지출항목 | text | 선택 | -| `usage_date` | 사용일자 | date | 선택 | -| `purpose` | 사용목적 | textarea | 필수 | - -### 10.2 내역 테이블 - -| 컬럼 | 라벨 | 타입 | -|------|------|------| -| `description` | 항목 | text | -| `amount` | 금액 | number (콤마 포맷) | -| `remark` | 비고 | text | - -### 10.3 Content JSON - -```json -{ - "pr_type": "pr_expense", - "write_date": "2026-03-06", - "department": "개발팀", - "writer_name": "홍길동", - "expense_category": "사무용품", - "usage_date": "2026-03-05", - "purpose": "업무용 모니터 구매", - "items": [ - { "description": "27인치 모니터", "amount": 350000, "remark": "LG전자" } - ], - "total_amount": 350000, - "attachment_memo": "견적서 첨부" -} -``` - ---- - -## 11. 계약체결품의서 (pr_contract) - -### 11.1 추가 필드 - -| 필드 | 라벨 | 타입 | 필수 | -|------|------|------|------| -| `contract_party` | 계약상대방 | text + Autocomplete | 필수 | -| `contract_party_biz_no` | 사업자번호 | text (자동) | - | -| `contract_content` | 계약내용 | textarea | 필수 | -| `contract_period_start` | 계약기간 시작 | date | 선택 | -| `contract_period_end` | 계약기간 종료 | date | 선택 | -| `contract_amount` | 계약금액 | number (콤마) | 필수 | -| `contract_conditions` | 주요조건 | textarea | 선택 | - -### 11.2 Content JSON - -```json -{ - "pr_type": "pr_contract", - "write_date": "2026-03-06", - "department": "경영지원팀", - "writer_name": "홍길동", - "contract_party": "(주)에이비씨", - "contract_party_biz_no": "123-45-67890", - "contract_content": "연간 IT 유지보수 계약", - "contract_period_start": "2026-04-01", - "contract_period_end": "2027-03-31", - "contract_amount": 12000000, - "contract_conditions": "월 1회 정기점검, 장애 발생 시 4시간 내 대응", - "attachment_memo": "계약서 초안 첨부" -} -``` - -### 11.3 특수 로직 - -- **거래처 검색**: `prVendorSearch(formData, 'contract_party')` — 계약상대방 필드에 Autocomplete 적용 -- 선택 시 `contract_party_biz_no` 자동 채움 - ---- - -## 12. 구매품의서 (pr_purchase) - -### 12.1 추가 필드 - -| 필드 | 라벨 | 타입 | 필수 | -|------|------|------|------| -| `vendor` | 납품업체 | text + Autocomplete | 선택 | -| `vendor_biz_no` | 사업자번호 | text (자동) | - | -| `delivery_date` | 납품예정일 | date | 선택 | -| `delivery_location` | 납품장소 | text | 선택 | - -### 12.2 내역 테이블 - -| 컬럼 | 라벨 | 타입 | -|------|------|------| -| `name` | 품목 | text | -| `spec` | 규격 | text | -| `quantity` | 수량 | number | -| `unit_price` | 단가 | number (콤마) | -| `amount` | 금액 | number (자동: 수량×단가) | -| `remark` | 비고 | text | - -### 12.3 Content JSON - -```json -{ - "pr_type": "pr_purchase", - "write_date": "2026-03-06", - "department": "생산팀", - "writer_name": "홍길동", - "vendor": "(주)공급사", - "vendor_biz_no": "987-65-43210", - "delivery_date": "2026-03-20", - "delivery_location": "본사 1층 창고", - "items": [ - { "name": "A4용지", "spec": "80g 500매", "quantity": 10, "unit_price": 25000, "amount": 250000, "remark": "" } - ], - "total_amount": 250000, - "attachment_memo": "" -} -``` - -### 12.4 특수 로직 - -- **금액 자동 계산**: `quantity × unit_price → amount` (x-effect 반응) -- **거래처 검색**: `prVendorSearch(formData, 'vendor')` — 납품업체 필드에 Autocomplete 적용 - ---- - -## 13. 출장품의서 (pr_trip) - -### 13.1 추가 필드 - -| 필드 | 라벨 | 타입 | 필수 | -|------|------|------|------| -| `destination` | 출장지 | text | 필수 | -| `trip_period_start` | 출장기간 시작 | date | 필수 | -| `trip_period_end` | 출장기간 종료 | date | 필수 | -| `trip_purpose` | 출장목적 | textarea | 필수 | - -### 13.2 일정표 (items) - -| 컬럼 | 라벨 | 타입 | -|------|------|------| -| `date` | 일자 | date | -| `schedule` | 일정 | text | -| `remark` | 비고 | text | - -### 13.3 경비 내역 (expenses) - -| 필드 | 라벨 | 타입 | -|------|------|------| -| `transport` | 교통비 | number (콤마) | -| `accommodation` | 숙박비 | number (콤마) | -| `meals` | 식비 | number (콤마) | -| `others` | 기타 | number (콤마) | -| (자동) | 합계 | number (합산) | - -### 13.4 Content JSON - -```json -{ - "pr_type": "pr_trip", - "write_date": "2026-03-06", - "department": "영업팀", - "writer_name": "홍길동", - "destination": "부산 해운대", - "trip_period_start": "2026-03-10", - "trip_period_end": "2026-03-11", - "trip_purpose": "거래처 방문 및 현장 점검", - "items": [ - { "date": "2026-03-10", "schedule": "거래처 미팅", "remark": "오전 10시" }, - { "date": "2026-03-11", "schedule": "현장 점검 및 복귀", "remark": "" } - ], - "expenses": { - "transport": 120000, - "accommodation": 80000, - "meals": 40000, - "others": 0 - }, - "total_amount": 240000, - "attachment_memo": "" -} -``` - -### 13.5 조회 화면 특수 구조 - -- **일정표**: 테이블 형태로 일자/일정/비고 렌더링 -- **경비 카드**: 교통비/숙박비/식비/기타 4개 항목 + 합계를 카드 그리드로 표시 - ---- - -## 14. 비용정산품의서 (pr_settlement) - -### 14.1 추가 필드 - -| 필드 | 라벨 | 타입 | 필수 | -|------|------|------|------| -| `settlement_period_start` | 정산기간 시작 | date | 선택 | -| `settlement_period_end` | 정산기간 종료 | date | 선택 | -| `payment_method` | 지급방법 | radio | 필수 | - -### 14.2 지급방법 옵션 - -| 값 | 라벨 | -|----|------| -| `corporate_card` | 법인카드 사용 | -| `personal_advance` | 개인 선지출 (환급 요청) | - -### 14.3 내역 테이블 - -| 컬럼 | 라벨 | 타입 | -|------|------|------| -| `date` | 사용일자 | date | -| `description` | 항목 | text | -| `amount` | 금액 | number (콤마) | -| `remark` | 비고 | text | - -### 14.4 Content JSON - -```json -{ - "pr_type": "pr_settlement", - "write_date": "2026-03-06", - "department": "개발팀", - "writer_name": "홍길동", - "settlement_period_start": "2026-02-01", - "settlement_period_end": "2026-02-28", - "payment_method": "personal_advance", - "items": [ - { "date": "2026-02-15", "description": "택시비", "amount": 25000, "remark": "야근 귀가" }, - { "date": "2026-02-20", "description": "회의 다과", "amount": 15000, "remark": "팀 미팅" } - ], - "total_amount": 40000, - "attachment_memo": "영수증 첨부" -} -``` - -### 14.5 조회 화면 특수 구조 - -- **지급방법 표시**: `corporate_card` → "법인카드 사용", `personal_advance` → "개인 선지출 (환급 요청)" -- 해당 라벨을 뱃지 형태로 표시 - ---- - -## 15. 결재 도장 테이블 (_approval-stamp-table.blade.php) - -### 15.1 구조 - -전통 한글 결재 양식의 도장 테이블을 구현한다. - -``` -┌──────┬────────┬────────┬────────┐ -│ │ 과장 │ 부장 │ 이사 │ ← 1행: 직급 헤더 -│ 결재 ├────────┼────────┼────────┤ -│ │ [승인] │ [대기] │ [대기] │ ← 2행: 서명/도장 영역 -│ ├────────┼────────┼────────┤ -│ │ 김과장 │ 박부장 │ 이이사 │ ← 3행: 이름 + 처리일 -│ │ 03/06 │ │ │ -└──────┴────────┴────────┴────────┘ -``` - -### 15.2 상태별 표시 - -| 상태 | approval_type | 표시 | 색상 | -|------|---------------|------|------| -| 승인 | `normal` | 빨간 원형 "승인" | `bg-red-500` | -| 전결 | `pre_decided` | 파란 원형 "전결" | `bg-blue-500` | -| 반려 | - | 빨간 원형 "반려" | `bg-red-500` | -| 보류 | - | 주황 원형 "보류" | `bg-amber-500` | -| 건너뜀 | - | 회색 "-" | `bg-gray-300` | - ---- - -## 16. 결재선 편집기 (_approval-line-editor.blade.php) - -### 16.1 2패널 구조 - -``` -┌─────────────────────┬─────────────────────┐ -│ 인원 목록 │ 결재선 │ -│ │ │ -│ [검색 input] │ [템플릿 선택 ▼] │ -│ │ │ -│ ▼ 개발팀 │ ① 김과장 (결재) [✗] │ -│ 홍길동 과장 [+] │ ② 박부장 (합의) [✗] │ -│ 김영희 대리 [+] │ ③ 이대리 (참조) [✗] │ -│ │ │ -│ ▼ 경영지원팀 │ (드래그로 순서 변경) │ -│ 박부장 부장 [+] │ │ -│ │ │ -├─────────────────────┴─────────────────────┤ -│ 결재: 1명 합의: 1명 참조: 1명 합계: 3명 │ -└───────────────────────────────────────────┘ -``` - -### 16.2 기능 - -| 기능 | 설명 | -|------|------| -| **인원 검색** | 이름/부서 실시간 검색 | -| **부서별 접기** | 부서 헤더 클릭으로 인원 접기/펼치기 | -| **드래그 정렬** | SortableJS로 결재선 순서 변경 | -| **유형 선택** | 각 단계별 approval/agreement/reference 선택 | -| **템플릿 로드** | 저장된 결재선 템플릿 드롭다운 | - -### 16.3 데이터 소스 - -``` -API: /api/admin/tenant-users/list - -응답: -[ - { - "department_id": 1, - "department_name": "개발팀", - "users": [ - { "id": 10, "name": "홍길동", "position": "과장", "job_title": "팀장" } - ] - } -] -``` - -### 16.4 Hidden Inputs (form 전송) - -```html - - - - -``` - ---- - -## 17. ApprovalForm 모델 - -### 17.1 테이블 스키마 - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `tenant_id` | BIGINT FK | 테넌트 격리 | -| `name` | VARCHAR | 양식명 (예: "휴가신청서") | -| `code` | VARCHAR UNIQUE | 양식 코드 (예: `leave`) | -| `category` | ENUM | `request`, `expense`, `certificate`, `expense_estimate` | -| `template` | JSON | 필드 정의 메타데이터 | -| `body_template` | LONGTEXT NULL | HTML 본문 템플릿 | -| `is_active` | BOOLEAN | 활성 여부 | - -### 17.2 카테고리 - -#### DB 카테고리 (ApprovalForm.category) - -| 카테고리 | 설명 | 양식 코드 | -|---------|------|----------| -| `request` | 신청서 | `leave`, `attendance_request`, `reason_report` | -| `expense` | 지출결의서 | `expense` | -| `certificate` | 증명서/서류 | `employment_cert`, `career_cert`, `appointment_cert`, `resignation` | -| `expense_estimate` | 품의서 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` | - -#### UI 분류 (formCategoryMap — 2단계 선택용) - -| UI 분류 | 양식 코드 | -|---------|----------| -| 일반 | `BUSINESS_DRAFT` | -| 인사/근태 | `leave`, `attendance_request`, `reason_report`, `resignation` | -| 증명서 | `employment_cert`, `career_cert`, `appointment_cert` | -| 품의 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` | -| 재무 | `expense` | - -> **참고**: DB 카테고리와 UI 분류는 별도 매핑이다. DB는 `approval_forms.category` ENUM이고, UI 분류는 JavaScript `formCategoryMap` 객체로 정의된다. - ---- - -## 18. 양식별 저장/조회 흐름 - -### 18.1 저장 (create/update) - -``` -사용자 입력 - ↓ -getFormData() (JavaScript) - ↓ -POST /api/admin/approvals - body: { form_id, title, content: {...}, body, steps: [...] } - ↓ -ApprovalService::createApproval() - ↓ -Approval.content = JSON encode → DB 저장 -``` - -### 18.2 조회 (show) - -``` -GET /approval-mgmt/{id} - ↓ -ApprovalController::show() - ↓ -Blade: show.blade.php - ↓ -양식 코드별 분기: - leave → (본문에 인라인 표시) - expense → @include('_expense-show') - pr_* → @include('_purchase-request-show') ← str_starts_with 매칭 - employment_cert → @include('_certificate-show') - career_cert → @include('_career-cert-show') - appointment_cert → @include('_appointment-cert-show') - resignation → @include('_resignation-show') -``` - -> **품의서 분기**: `str_starts_with($approval->form?->code ?? '', 'pr_')` 조건으로 5종 모두 단일 include로 처리. `_purchase-request-show.blade.php` 내부에서 `$content['pr_type']`으로 세부 분기. - ---- - -## 관련 문서 - -- [README.md](README.md) — 결재관리 시스템 전체 개요 -- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름 -- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 -- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/approvals/ui-screens.md b/sam/docs/features/approvals/ui-screens.md deleted file mode 100644 index f81b9ae..0000000 --- a/sam/docs/features/approvals/ui-screens.md +++ /dev/null @@ -1,381 +0,0 @@ -# 결재관리 UI 화면 구성 - -> **작성일**: 2026-02-28 -> **상태**: Phase 2 구현 완료 -> **기술**: Blade + HTMX + Alpine.js + Tailwind CSS -> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) - ---- - -## 1. 개요 - -결재관리 화면은 MNG(관리자 웹)에서 Blade 템플릿으로 구현되며, API 호출은 `fetch()`를 사용한다. - -### 1.1 파일 구조 - -``` -resources/views/approvals/ -├── drafts.blade.php ← 기안함 (목록) -├── pending.blade.php ← 결재 대기함 (목록) -├── completed.blade.php ← 처리 완료함 (목록) -├── references.blade.php ← 참조함 (목록) -├── create.blade.php ← 기안 작성 -├── edit.blade.php ← 기안 수정 -├── show.blade.php ← 상세 조회 + 결재 처리 -└── partials/ - ├── _status-badge.blade.php ← 상태 뱃지 컴포넌트 - └── _step-progress.blade.php ← 결재 단계 진행 표시 -``` - ---- - -## 2. 목록 화면 - -### 2.1 기안함 (`/approval-mgmt/drafts`) - -내가 기안한 모든 문서를 표시한다. - -**UI 구성:** - -``` -┌──────────────────────────────────────────────────────────┐ -│ 기안함 [+ 새 기안] │ -├──────────────────────────────────────────────────────────┤ -│ [검색] [상태 필터 ▼] [긴급만 □] [날짜 범위] │ -├──────────────────────────────────────────────────────────┤ -│ 문서번호 │ 제목 │ 양식 │ 상태 │ 기안일 │ -│ APR-260228-001│ 휴가 신청 │ 휴가서 │ 🟢완료 │ 02-28 │ -│ APR-260228-002│ 출장 보고 │ 출장서 │ 🔵진행 │ 02-28 │ -│ APR-260227-001│ 경비 청구 │ 경비서 │ ⬜임시 │ 02-27 │ -├──────────────────────────────────────────────────────────┤ -│ [◀ 이전] 1 / 3 [다음 ▶] │ -└──────────────────────────────────────────────────────────┘ -``` - -**상태 필터:** 전체, 임시저장, 진행, 완료, 반려, 회수, 보류 - ---- - -### 2.2 결재 대기함 (`/approval-mgmt/pending`) - -내가 현재 결재해야 할 문서를 표시한다. - -**UI 구성:** - -``` -┌──────────────────────────────────────────────────────────┐ -│ 결재 대기함 [뱃지: 3건] │ -├──────────────────────────────────────────────────────────┤ -│ 문서번호 │ 제목 │ 기안자 │ 양식 │ 상신일 │ -│ 🔴 APR-260..│ 긴급 승인 │ 홍길동 │ 구매서 │ 02-28 │ -│ APR-260..│ 휴가 신청 │ 김영희 │ 휴가서 │ 02-27 │ -└──────────────────────────────────────────────────────────┘ -``` - -> 긴급 문서는 🔴 아이콘과 함께 상단에 표시 - ---- - -### 2.3 참조함 (`/approval-mgmt/references`) - -내가 참조자로 지정된 문서를 표시한다. - -**UI 구성:** - -``` -┌──────────────────────────────────────────────────────────┐ -│ 참조함 │ -├──────────────────────────────────────────────────────────┤ -│ [전체] [미열람 (5)] [열람완료] │ -├──────────────────────────────────────────────────────────┤ -│ 문서번호 │ 제목 │ 기안자 │ 상태 │ 열람 │ -│ APR-260228-001│ 회의록 │ 박부장 │ 🟢완료 │ ❌미열람│ -│ APR-260227-003│ 인사발령 │ 이팀장 │ 🔵진행 │ ✅열람 │ -└──────────────────────────────────────────────────────────┘ -``` - -**열람 추적:** -- 문서 클릭 시 `mark-read` API가 자동 호출된다 -- 미열람/열람완료 탭으로 필터링 가능 -- 미열람 건수가 뱃지로 표시된다 - ---- - -## 3. 상세 화면 (`/approval-mgmt/{id}`) - -### 3.1 전체 레이아웃 - -``` -┌──────────────────────────────────────────────────────────┐ -│ 결재 상세 [수정] [목록으로] │ -│ APR-260228-001 │ -├──────────────────────────────────────────────────────────┤ -│ │ -│ 상태: [🔵 진행] [🔴 긴급] │ -│ 양식: 휴가신청서 기안자: 홍길동 │ -│ 기안일: 2026-02-28 10:05 완료일: - │ -│ 원본 문서: APR-260225-003 (재기안 시 표시) │ -│ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ 회수 사유 (cancelled 상태에서만) │ │ -│ │ 내용 수정이 필요하여 회수합니다. │ │ -│ └──────────────────────────────────────────────────┘ │ -│ │ -│ 제목: 2월 연차 사용 신청 │ -│ 본문: 2월 27일~28일 연차 사용합니다... │ -│ │ -├──────────────────────────────────────────────────────────┤ -│ │ -│ 결재 진행 │ -│ ┌────────────────────────────────────────────────┐ │ -│ │ [결재 단계 프로그레스 바] │ │ -│ │ ✓김과장(승인) → ●박부장(대기) → ③이사(대기) │ │ -│ └────────────────────────────────────────────────┘ │ -│ │ -│ 결재 의견 │ -│ ┌────────────────────────────────────────────────┐ │ -│ │ ✓ 김과장 2026-02-28 11:00 │ │ -│ │ 승인합니다. │ │ -│ └────────────────────────────────────────────────┘ │ -│ │ -├──────────────────────────────────────────────────────────┤ -│ │ -│ 결재 처리 (현재 결재자에게만 표시) │ -│ [결재 의견 textarea] │ -│ [승인] [반려] [보류] [전결] │ -│ │ -├──────────────────────────────────────────────────────────┤ -│ 보류 해제 (on_hold + 보류한 본인에게만) │ -│ [보류 해제] │ -├──────────────────────────────────────────────────────────┤ -│ 회수 (기안자 + pending/on_hold) │ -│ [회수 사유 textarea] │ -│ [결재 회수] │ -├──────────────────────────────────────────────────────────┤ -│ 복사 재기안 (기안자 + approved/rejected/cancelled) │ -│ [복사하여 재기안] │ -└──────────────────────────────────────────────────────────┘ -``` - -### 3.2 조건부 섹션 표시 - -| 섹션 | 표시 조건 | -|------|----------| -| **수정 버튼** | 기안자 + `draft`/`rejected` | -| **회수 사유** | `cancelled` + `recall_reason` 존재 | -| **원본 문서 링크** | `parent_doc_id` 존재 (재기안 문서) | -| **결재 처리** | `pending` + 현재 결재자 | -| **보류 해제** | `on_hold` + 보류한 본인 | -| **회수** | 기안자 + `pending`/`on_hold` | -| **복사 재기안** | 기안자 + `approved`/`rejected`/`cancelled` | - ---- - -## 4. 파셜 컴포넌트 - -### 4.1 상태 뱃지 (`_status-badge.blade.php`) - -문서 상태를 색상 뱃지로 표시한다. - -| 상태 | 라벨 | 스타일 | -|------|------|--------| -| `draft` | 임시저장 | `bg-gray-100 text-gray-700` | -| `pending` | 진행 | `bg-blue-100 text-blue-700` | -| `approved` | 완료 | `bg-green-100 text-green-700` | -| `rejected` | 반려 | `bg-red-100 text-red-700` | -| `cancelled` | 회수 | `bg-yellow-100 text-yellow-700` | -| `on_hold` | 보류 | `bg-amber-100 text-amber-700` | - ---- - -### 4.2 결재 단계 프로그레스 (`_step-progress.blade.php`) - -결재선의 각 단계를 가로 프로그레스 바로 표시한다. - -**단계 아이콘:** - -| 상태 | 아이콘 | 배경색 | 텍스트색 | -|------|--------|--------|---------| -| `approved` (normal) | ✓ | `bg-green-500` | white | -| `approved` (pre_decided) | ⚡ | `bg-indigo-500` | white | -| `rejected` | ✗ | `bg-red-500` | white | -| `on_hold` | ⏸ | `bg-amber-400` | white | -| `skipped` | — | `bg-gray-300` | gray | -| `pending` (현재 차례) | 번호 | `bg-blue-500` | white | -| `pending` (대기) | 번호 | `bg-gray-200` | gray | - -**레이아웃:** - -``` -┌──────────────────────────────────────────────────────┐ -│ │ -│ ✓ ──── ⚡ ──── — ──── — ──── ● ──── 3 │ -│ 김과장 박부장 이사장 팀장 최대리 참조자 │ -│ 경영팀 경영팀 대표실 개발팀 개발팀 인사팀 │ -│ (승인) (전결) (건너뜀)(건너뜀)(대기) (참조) │ -│ │ -└──────────────────────────────────────────────────────┘ -``` - -**특수 표시:** -- **전결** step: ⚡ 아이콘 + "전결" 라벨 (남색) -- **보류** step: ⏸ 아이콘 + "보류" 라벨 (노란색) -- **건너뜀** step: 이름에 취소선 (line-through) -- **참조** step: 별도 구분 없이 동일 프로그레스 바에 표시 -- **연결선**: 단계 사이 가로선 (`border-t-2`) - ---- - -## 5. 결재 처리 인터랙션 - -### 5.1 승인 - -``` -[승인 버튼 클릭] - → confirm("승인하시겠습니까?") - → POST /api/admin/approvals/{id}/approve - body: { comment: "의견 텍스트" } - → 성공 시: 토스트("승인되었습니다") + 페이지 리로드 -``` - -### 5.2 반려 - -``` -[반려 버튼 클릭] - → comment 빈 값 체크 → 경고 토스트("반려 시 사유를 입력해주세요") - → confirm("반려하시겠습니까?") - → POST /api/admin/approvals/{id}/reject - body: { comment: "사유" } - → 성공 시: 토스트("반려되었습니다") + 페이지 리로드 -``` - -### 5.3 보류 - -``` -[보류 버튼 클릭] - → comment 빈 값 체크 → 경고 토스트("보류 사유를 입력해주세요") - → confirm("이 결재를 보류하시겠습니까?") - → POST /api/admin/approvals/{id}/hold - body: { comment: "사유" } - → 성공 시: 토스트("보류되었습니다") + 페이지 리로드 -``` - -### 5.4 전결 - -``` -[전결 버튼 클릭] - → confirm("전결 처리하시겠습니까?\n이후 모든 결재를 건너뛰고 문서를 최종 승인합니다.") - → POST /api/admin/approvals/{id}/pre-decide - body: { comment: "의견(선택)" } - → 성공 시: 토스트("전결 처리되었습니다") + 페이지 리로드 -``` - -### 5.5 보류 해제 - -``` -[보류 해제 버튼 클릭] - → confirm("보류를 해제하시겠습니까?") - → POST /api/admin/approvals/{id}/release-hold - → 성공 시: 토스트("보류가 해제되었습니다") + 페이지 리로드 -``` - -### 5.6 회수 - -``` -[결재 회수 버튼 클릭] - → confirm("결재를 회수하시겠습니까? 이 작업은 되돌릴 수 없습니다.") - → POST /api/admin/approvals/{id}/cancel - body: { recall_reason: "사유(선택)" } - → 성공 시: 토스트("결재가 회수되었습니다") + 페이지 리로드 -``` - -### 5.7 복사 재기안 - -``` -[복사하여 재기안 버튼 클릭] - → confirm("이 문서를 복사하여 새 결재를 작성하시겠습니까?") - → POST /api/admin/approvals/{id}/copy - → 성공 시: 토스트("문서가 복사되었습니다") - → /approval-mgmt/{newId}/edit로 이동 -``` - ---- - -## 6. 결재 의견 표시 - -상세 페이지에서 결재 의견이 있는 step을 카드 형태로 표시한다. - -``` -┌──────────────────────────────────────┐ -│ ✓ 김과장 2026-02-28 11:00 │ -│ 승인합니다. │ -├──────────────────────────────────────┤ -│ ⚡ 박부장 (전결) 2026-02-28 14:00 │ -│ 전결 처리합니다. │ -├──────────────────────────────────────┤ -│ ⏸ 이사장 (보류) 2026-02-28 15:00 │ -│ 추가 자료 검토 필요 │ -├──────────────────────────────────────┤ -│ ✗ 팀장 2026-02-28 16:00 │ -│ 예산 초과로 반려합니다. │ -└──────────────────────────────────────┘ -``` - -**아이콘 색상:** -- ✓ 승인: 녹색 (`bg-green-100 text-green-600`) -- ⚡ 전결: 남색 (`bg-indigo-100 text-indigo-600`) -- ⏸ 보류: 노란색 (`bg-amber-100 text-amber-600`) -- ✗ 반려: 적색 (`bg-red-100 text-red-600`) - ---- - -## 7. 참조함 열람 추적 UI - -### 7.1 탭 필터 - -``` -[전체] [미열람 (5)] [열람완료] -``` - -- 탭 클릭 시 `is_read` 파라미터로 API 재호출 -- 미열람 탭에 건수 뱃지 표시 - -### 7.2 열람 상태 표시 - -| 상태 | 표시 | -|------|------| -| 미열람 | `bg-red-100 text-red-700` "미열람" | -| 열람완료 | `bg-green-100 text-green-700` "열람완료" | - -### 7.3 자동 열람 처리 - -문서 행 클릭 시: -1. `mark-read` API 호출 (비동기) -2. 상세 페이지로 이동 - ---- - -## 8. 버튼 스타일 가이드 - -| 버튼 | 색상 | Tailwind 클래스 | -|------|------|----------------| -| 승인 | 녹색 | `bg-green-600 hover:bg-green-700` | -| 반려 | 적색 | `bg-red-600 hover:bg-red-700` | -| 보류 | 노란색 | `bg-amber-500 hover:bg-amber-600` | -| 전결 | 남색 | `bg-indigo-600 hover:bg-indigo-700` | -| 보류 해제 | 노란색 | `bg-amber-500 hover:bg-amber-600` | -| 회수 | 노란색 | `bg-yellow-500 hover:bg-yellow-600` | -| 복사 재기안 | 회색 | `bg-gray-600 hover:bg-gray-700` | -| 수정 | 회색 | `bg-gray-600 hover:bg-gray-700` | - ---- - -## 관련 문서 - -- [README.md](README.md) — 시스템 전체 개요 -- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 -- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 - ---- - -**최종 업데이트**: 2026-02-28 diff --git a/sam/docs/features/approvals/workflows.md b/sam/docs/features/approvals/workflows.md deleted file mode 100644 index 202d525..0000000 --- a/sam/docs/features/approvals/workflows.md +++ /dev/null @@ -1,565 +0,0 @@ -# 결재관리 워크플로우 상세 - -> **작성일**: 2026-02-28 -> **상태**: Phase 2 구현 완료 -> **관련**: [README.md](README.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md) - ---- - -## 1. 개요 - -이 문서는 결재관리 시스템의 각 동작(Action)에 대한 상세 워크플로우를 정의한다. -모든 워크플로우는 `ApprovalService`에서 트랜잭션으로 처리된다. - -### 1.1 용어 정의 - -| 용어 | 설명 | -|------|------| -| **기안자** | 결재 문서를 작성한 사람 (`drafter_id`) | -| **현재 결재자** | 결재선에서 현재 차례인 사람 (가장 작은 `step_order`의 `pending` step) | -| **결재자** | `step_type`이 `approval` 또는 `agreement`인 참여자 | -| **참조자** | `step_type`이 `reference`인 참여자 (의사결정 권한 없음) | -| **전결** | 현재 결재자가 이후 모든 결재를 건너뛰고 즉시 최종 승인 | - ---- - -## 2. 기안 작성 (createApproval) - -### 2.1 흐름 - -``` -사용자 → [양식 선택] → [제목/본문 입력] → [결재선 설정] → [임시저장] - │ - ▼ - 새 Approval 생성 - status = 'draft' - current_step = 0 -``` - -### 2.2 조건 - -- 모든 로그인 사용자가 작성 가능 -- `form_id` 필수 (양식 선택) -- 결재선(steps)은 저장 시 선택사항 (상신 시 필수) - -### 2.3 처리 로직 - -1. 문서번호 자동 채번 (`APR-YYMMDD-001` 형식) -2. `numbering_sequences` 테이블로 일일 순번 관리 -3. 결재선 설정 시 `approval_steps` 저장 + 사용자 정보 스냅샷 (이름, 부서, 직급) -4. `status = 'draft'`, `current_step = 0` - ---- - -## 3. 상신 (submit) - -### 3.1 흐름 - -``` -기안자 → [상신 버튼] → 유효성 검사 → 결재선 검사 → 상신 완료 - │ - ▼ - status = 'pending' - current_step = 1 - drafted_at = now() -``` - -### 3.2 조건 - -| 조건 | 설명 | -|------|------| -| 문서 상태 | `draft` 또는 `rejected` | -| 결재선 | 결재/합의 step 1명 이상 필수 | -| 요청자 | 기안자만 | - -### 3.3 처리 로직 - -1. `isSubmittable()` 검증 → `draft` 또는 `rejected`인지 확인 -2. 결재/합의 step 존재 확인 -3. **반려 후 재상신인 경우**: 모든 step을 `pending`으로 초기화 (comment, acted_at도 초기화) -4. `status → pending`, `drafted_at → now()`, `current_step → 1` - -### 3.4 반려 후 재상신 - -``` -rejected 문서 - │ - ├── 기안자가 내용 수정 (updateApproval) - │ - └── 상신 (submit) - ├── 모든 steps → pending (초기화) - ├── status → pending - └── current_step → 1 (처음부터 다시) -``` - -> 반려 후 재상신 시 결재선이 초기화되므로, 이전 결재 의견(comment)은 사라진다. - ---- - -## 4. 승인 (approve) - -### 4.1 흐름 - -``` -현재 결재자 → [의견 입력(선택)] → [승인 버튼] - │ - ┌──────────┴──────────┐ - │ 현재 step │ - │ status → 'approved' │ - │ comment → (입력값) │ - │ acted_at → now() │ - └──────────┬──────────┘ - │ - ┌─────────────────┴─────────────────┐ - │ │ - 다음 pending step 있음 마지막 결재자 - │ │ - current_step 갱신 status → 'approved' - (다음 순서 결재자 대기) completed_at → now() -``` - -### 4.2 조건 - -| 조건 | 설명 | -|------|------| -| 문서 상태 | `pending` | -| 요청자 | 현재 차례 결재자 (`approver_id === auth()->id()`) | - -### 4.3 처리 로직 - -1. `isActionable()` 검증 → `pending` 상태인지 확인 -2. `getCurrentApproverStep()` → 현재 차례 step 조회 -3. 현재 step → `approved` + comment + acted_at -4. 다음 pending 결재/합의 step 조회 - - **있으면**: `current_step` 갱신 - - **없으면**: 문서 `approved` + `completed_at` - -### 4.4 순차결재 순서 결정 - -``` -step_order = 1 (결재) → step_order = 2 (합의) → step_order = 3 (결재) - │ │ │ - 1번째 승인 → 2번째 승인 → 3번째 승인 → 문서 완료 -``` - -> 결재와 합의는 동일한 순차 흐름을 따른다. `step_order` 순서대로 처리된다. - ---- - -## 5. 반려 (reject) - -### 5.1 흐름 - -``` -현재 결재자 → [반려 사유 입력(필수)] → [반려 버튼] - │ - ┌──────────┴──────────┐ - │ 현재 step │ - │ status → 'rejected' │ - │ comment → (사유) │ - │ acted_at → now() │ - └──────────┬──────────┘ - │ - ▼ - 문서 status → 'rejected' - completed_at → now() -``` - -### 5.2 조건 - -| 조건 | 설명 | -|------|------| -| 문서 상태 | `pending` | -| 요청자 | 현재 차례 결재자 | -| 반려 사유 | **필수** (빈 값 불가) | - -### 5.3 처리 로직 - -1. `isActionable()` 검증 -2. 현재 결재자 확인 -3. 반려 사유 빈 값 체크 -4. 현재 step → `rejected` + comment + acted_at -5. 문서 → `rejected` + completed_at - -### 5.4 반려 후 가능한 동작 - -``` -rejected 문서 - │ - ├── 기안자가 수정 → 재상신 (submit) - │ └── 결재선 초기화, 처음부터 다시 진행 - │ - └── 기안자가 복사 재기안 (copyForRedraft) - └── 새 문서 생성 (draft), 원본은 그대로 유지 -``` - ---- - -## 6. 회수 (cancel) - -### 6.1 흐름 - -``` -기안자 → [회수 사유 입력(선택)] → [회수 버튼] - │ - ┌──────────┴──────────┐ - │ 회수 가능 여부 판단 │ - │ (첫 결재자 미처리?) │ - └──────────┬──────────┘ - │ - ┌───────────┴───────────┐ - │ │ - 첫 결재자 첫 결재자 이미 - pending/on_hold 승인/반려 - │ │ - 회수 진행 회수 불가 - │ (에러 반환) - ▼ - 모든 pending/on_hold steps → 'skipped' - 문서 status → 'cancelled' - recall_reason → (입력값) - completed_at → now() -``` - -### 6.2 조건 - -| 조건 | 설명 | -|------|------| -| 문서 상태 | `pending` 또는 `on_hold` | -| 요청자 | 기안자만 (`drafter_id === auth()->id()`) | -| 첫 결재자 상태 | `pending` 또는 `on_hold` (이미 처리했으면 불가) | - -### 6.3 회수 가능 판단 로직 - -```php -// 1단계: 문서 상태 확인 -$approval->isCancellable() // pending 또는 on_hold - -// 2단계: 기안자 확인 -$approval->drafter_id === auth()->id() - -// 3단계: 첫 결재자 상태 확인 -$firstStep = steps.approvalOnly().orderBy('step_order').first() -$firstStep->status === 'pending' || 'on_hold' // 미처리 상태여야 함 -``` - -### 6.4 처리 로직 - -1. `isCancellable()` 검증 → `pending` 또는 `on_hold` -2. 기안자 확인 -3. 첫 번째 결재/합의 step의 상태 확인 → `pending`/`on_hold`이 아니면 거부 -4. 모든 `pending`/`on_hold` steps → `skipped` -5. 문서 → `cancelled` + `recall_reason` + `completed_at` - ---- - -## 7. 보류 (hold) - -### 7.1 흐름 - -``` -현재 결재자 → [보류 사유 입력(필수)] → [보류 버튼] - │ - ┌──────────┴──────────┐ - │ 현재 step │ - │ status → 'on_hold' │ - │ comment → (사유) │ - │ acted_at → now() │ - └──────────┬──────────┘ - │ - ▼ - 문서 status → 'on_hold' -``` - -### 7.2 조건 - -| 조건 | 설명 | -|------|------| -| 문서 상태 | `pending` (`isHoldable()`) | -| 요청자 | 현재 차례 결재자 | -| 보류 사유 | **필수** (빈 값 불가) | - -### 7.3 처리 로직 - -1. `isHoldable()` 검증 → `pending` 상태인지 확인 -2. `getCurrentApproverStep()` → 현재 차례 step 조회 -3. 현재 결재자 확인 (`approver_id === auth()->id()`) -4. 보류 사유 빈 값 체크 -5. 현재 step → `on_hold` + comment + acted_at -6. 문서 → `on_hold` - -### 7.4 보류 상태의 영향 - -``` -on_hold 상태에서: -├── 다른 결재자는 아무 동작 불가 (결재 흐름 중단) -├── 기안자는 회수 가능 (첫 결재자가 미처리 상태이면) -└── 보류한 결재자만 보류 해제 가능 -``` - ---- - -## 8. 보류 해제 (releaseHold) - -### 8.1 흐름 - -``` -보류한 결재자 → [보류 해제 버튼] - │ - ┌──────────┴──────────┐ - │ on_hold step │ - │ status → 'pending' │ - │ comment → null │ - │ acted_at → null │ - └──────────┬──────────┘ - │ - ▼ - 문서 status → 'pending' - (결재 흐름 재개) -``` - -### 8.2 조건 - -| 조건 | 설명 | -|------|------| -| 문서 상태 | `on_hold` (`isHoldReleasable()`) | -| 요청자 | 보류한 본인만 (`on_hold` step의 `approver_id === auth()->id()`) | - -### 8.3 처리 로직 - -1. `isHoldReleasable()` 검증 → `on_hold` 상태인지 확인 -2. `on_hold` 상태인 step 조회 -3. 해당 step의 `approver_id`가 현재 사용자인지 확인 -4. step → `pending` + comment/acted_at 초기화 -5. 문서 → `pending` - ---- - -## 9. 전결 (preDecide) - -### 9.1 흐름 - -``` -현재 결재자 → [의견 입력(선택)] → [전결 버튼] → 확인 팝업 - │ - ┌──────────┴──────────┐ - │ 현재 step │ - │ status → 'approved' │ - │ approval_type → │ - │ 'pre_decided' │ - │ comment → (입력값) │ - │ acted_at → now() │ - └──────────┬──────────┘ - │ - ▼ - 이후 모든 pending - approval/agreement steps - → status = 'skipped' - │ - ▼ - 문서 status → 'approved' - completed_at → now() -``` - -### 9.2 조건 - -| 조건 | 설명 | -|------|------| -| 문서 상태 | `pending` (`isActionable()`) | -| 요청자 | 현재 차례 결재자 | - -### 9.3 처리 로직 - -1. `isActionable()` 검증 -2. `getCurrentApproverStep()` → 현재 차례 step 조회 -3. 현재 결재자 확인 -4. 현재 step → `approved` + `approval_type = 'pre_decided'` + comment + acted_at -5. 이후 모든 pending 결재/합의 steps → `skipped` -6. 문서 → `approved` + `completed_at` - -### 9.4 전결 예시 - -``` -step_order=1 (이사장, 결재) → approved (normal) -step_order=2 (부장, 결재) → approved (pre_decided) ← 여기서 전결 -step_order=3 (과장, 합의) → skipped (전결로 건너뜀) -step_order=4 (팀장, 결재) → skipped (전결로 건너뜀) -step_order=5 (참조자, 참조) → (참조는 영향 없음, 그대로 유지) - -문서 → approved, completed_at = now() -``` - -> 전결은 결재/합의 step만 건너뛴다. 참조 step은 영향받지 않는다. - ---- - -## 10. 복사 재기안 (copyForRedraft) - -### 10.1 흐름 - -``` -기안자 → [복사하여 재기안 버튼] - │ - ▼ - ┌─────────────────────────────┐ - │ 원본 문서에서 복사 │ - │ ├── form_id │ - │ ├── title │ - │ ├── content (양식 데이터) │ - │ ├── body │ - │ ├── is_urgent │ - │ ├── department_id │ - │ └── 결재선 (모두 pending) │ - └─────────────┬───────────────┘ - │ - ▼ - 새 문서 생성 (status = 'draft') - parent_doc_id = 원본.id - 새 문서번호 채번 - │ - ▼ - 수정 페이지로 이동 - (/approval-mgmt/{newId}/edit) -``` - -### 10.2 조건 - -| 조건 | 설명 | -|------|------| -| 원본 문서 상태 | `approved`, `rejected`, `cancelled` (`isCopyable()`) | -| 요청자 | 기안자만 (`drafter_id === auth()->id()`) | - -### 10.3 처리 로직 - -1. `isCopyable()` 검증 → `approved`/`rejected`/`cancelled` 중 하나 -2. 기안자 확인 -3. 새 문서 생성: - - 새 문서번호 채번 - - 원본의 양식, 제목, 내용, 본문, 긴급 여부, 부서 복사 - - `parent_doc_id = 원본.id` - - `status = 'draft'`, `current_step = 0` -4. 결재선 복사: 원본의 모든 steps를 새 문서에 복사 (모두 `pending` 상태) -5. 새 문서의 edit 페이지로 리다이렉트 - -### 10.4 원본과의 관계 - -``` -원본 문서 (approved/rejected/cancelled) - │ - └── parent_doc_id로 연결 - │ - ▼ - 새 문서 (draft) - ├── 상세 페이지에서 "원본 문서" 링크 표시 - └── 기안자가 내용 수정 후 상신 가능 -``` - ---- - -## 11. 참조 열람 추적 (markAsRead) - -### 11.1 흐름 - -``` -참조자 → [참조함 목록에서 문서 클릭] - │ - ├── markAsRead API 호출 - │ ├── is_read → true - │ └── read_at → now() - │ - └── 상세 페이지로 이동 -``` - -### 11.2 조건 - -| 조건 | 설명 | -|------|------| -| 요청자 | 해당 문서의 참조자 (`step_type = 'reference'`) | - -### 11.3 처리 로직 - -1. 현재 사용자의 참조 step 조회 -2. `is_read = false`인 step → `is_read = true`, `read_at = now()` -3. 이미 열람한 경우 중복 업데이트 없음 (`where('is_read', false)`) - ---- - -## 12. 전체 상태 전이 요약 - -``` -┌───────────────────────────────────────────────────────────────────┐ -│ │ -│ draft ──submit()──→ pending ──approve()──→ (다음 step 또는) │ -│ ▲ │ │ approved │ -│ │ │ │ │ -│ │ │ ├──reject()──→ rejected │ -│ │ │ │ │ │ -│ │ │ │ ├── 수정 → submit() │ -│ │ │ │ │ (재상신, draft X) │ -│ │ │ │ │ │ -│ │ │ │ └── copyForRedraft() │ -│ │ │ │ → 새 draft 생성 │ -│ │ │ │ │ -│ │ │ ├──hold()──→ on_hold │ -│ │ │ │ │ │ -│ │ │ │ ├── releaseHold() │ -│ │ │ │ │ → pending 복원 │ -│ │ │ │ │ │ -│ │ │ │ └── cancel() (기안자) │ -│ │ │ │ → cancelled │ -│ │ │ │ │ -│ │ │ ├──preDecide()──→ approved │ -│ │ │ │ (이후 steps → skipped) │ -│ │ │ │ │ -│ │ │ └──cancel()──→ cancelled │ -│ │ │ (기안자, 첫결재자 미처리 시) │ -│ │ │ │ │ -│ │ │ └── copyForRedraft() │ -│ │ │ → 새 draft 생성 │ -│ │ │ │ -│ │ └── approved ──copyForRedraft() │ -│ │ → 새 draft 생성 │ -│ │ │ -│ └── updateApproval() (draft/rejected 상태에서 수정) │ -│ │ -└───────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 13. 에러 케이스 정리 - -| 동작 | 에러 조건 | 에러 메시지 | -|------|----------|------------| -| submit | 상태가 draft/rejected 아님 | "상신할 수 없는 상태입니다." | -| submit | 결재선 없음 | "결재선을 설정해주세요." | -| approve | 상태가 pending 아님 | "승인할 수 없는 상태입니다." | -| approve | 현재 결재자 아님 | "현재 결재자가 아닙니다." | -| reject | 상태가 pending 아님 | "반려할 수 없는 상태입니다." | -| reject | 사유 미입력 | "반려 사유를 입력해주세요." | -| cancel | 상태가 pending/on_hold 아님 | "회수할 수 없는 상태입니다." | -| cancel | 기안자 아님 | "기안자만 회수할 수 있습니다." | -| cancel | 첫 결재자 이미 처리 | "첫 번째 결재자가 이미 처리하여 회수할 수 없습니다." | -| hold | 상태가 pending 아님 | "보류할 수 없는 상태입니다." | -| hold | 현재 결재자 아님 | "현재 결재자가 아닙니다." | -| hold | 사유 미입력 | "보류 사유를 입력해주세요." | -| releaseHold | 상태가 on_hold 아님 | "보류 해제할 수 없는 상태입니다." | -| releaseHold | 보류한 본인 아님 | "보류한 결재자만 해제할 수 있습니다." | -| preDecide | 상태가 pending 아님 | "전결할 수 없는 상태입니다." | -| preDecide | 현재 결재자 아님 | "현재 결재자가 아닙니다." | -| copyForRedraft | 상태가 approved/rejected/cancelled 아님 | "복사할 수 없는 상태입니다." | -| copyForRedraft | 기안자 아님 | "기안자만 복사할 수 있습니다." | -| update | 상태가 draft/rejected 아님 | "수정할 수 없는 상태입니다." | -| delete | 상태가 draft 아님 | "삭제할 수 없는 상태입니다." | - ---- - -## 관련 문서 - -- [README.md](README.md) — 시스템 전체 개요 -- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 -- [UI 화면 구성](ui-screens.md) — 화면별 동작 - ---- - -**최종 업데이트**: 2026-02-28 diff --git a/sam/docs/features/barobill-kakaotalk/README.md b/sam/docs/features/barobill-kakaotalk/README.md deleted file mode 100644 index 09f1909..0000000 --- a/sam/docs/features/barobill-kakaotalk/README.md +++ /dev/null @@ -1,410 +0,0 @@ -# 바로빌 카카오톡 (알림톡/친구톡) 연동 - -> **문서 버전**: 1.1 -> **작성일**: 2026-02-14 -> **최종 수정**: 2026-02-27 -> **상태**: 운영 중 (알림톡 + SMS + 환경별 분기 완료) -> **대상 프로젝트**: MNG - ---- - -## 1. 개요 - -### 1.1 목적 - -바로빌(Barobill) 플랫폼의 카카오톡 알림톡/친구톡 API를 SAM에 연동하여, -고객사에 카카오톡 메시지를 자동 또는 수동으로 발송하는 기능을 제공한다. - -### 1.2 사전 요구사항 - -| 항목 | 상태 | 설명 | -|------|------|------| -| 법인 명의 휴대폰 준비 | **완료** | 카카오톡 채널 가입에 법인 명의 번호 사용 | -| 카카오톡 채널 개설 | **완료** (2026-02-20) | 채널 ID: `@codebridge`, 채널명: (주)코드브릿지엑스 | -| 바로빌 카카오톡 서비스 신청 | **완료** (2026-02-20) | 바로빌 관리자 페이지에서 카카오톡 서비스 활성화 | -| 채널 연동 (바로빌↔카카오) | **완료** (2026-02-20) | 바로빌 관리 URL에서 채널 연동 처리 | -| 바로빌 파트너 과금 설정 | **완료** (2026-02-23) | 바로빌 측에서 파트너사 과금 설정 완료 | -| 알림톡 템플릿 v1 검수 | **완료** (2026-02-22) | `전자계약_서명요청`, `전자계약_리마인드` 2종 승인 | -| 알림톡 템플릿 v2 검수 | **완료** (2026-02-25) | 버튼 URL에 `#{토큰}` 변수 포함 3종 승인 | -| 알림톡 `전자계약_완료` | **완료** (2026-02-26) | 서명 완료 알림 발송용 템플릿 승인 | -| 역할 기반 알림 분기 | **완료** (2026-02-26) | 본사=이메일, 상대방=알림톡/SMS | -| 환경별 템플릿 분기 | **완료** (2026-02-27) | `_DEV` 접미사 개발 템플릿 등록 | -| DEV 템플릿 검수 | **심사 중** (2026-02-27 접수) | 개발서버용 3종 (`admin.codebridge-x.com`) | - -> 상세 등록 가이드: [카카오톡 알림톡 채널 및 템플릿 등록 가이드](../../guides/카카오톡-알림톡-채널-템플릿-등록.md) - -### 1.3 알림톡 vs 친구톡 - -| 구분 | 알림톡 | 친구톡 | -|------|--------|--------| -| **용도** | 정보성 메시지 (주문확인, 배송안내 등) | 광고성 메시지 (프로모션, 이벤트 등) | -| **수신 대상** | 모든 카카오톡 사용자 | 채널 친구 추가한 사용자만 | -| **템플릿** | 필수 (카카오 사전 검수) | 불필요 (자유 형식) | -| **광고 표시** | 불가 | 필수 (`(광고)` 표기) | -| **이미지 첨부** | 불가 | 가능 (이미지/와이드 이미지) | -| **비용** | 건당 약 8~9원 | 건당 약 15~20원 | -| **SMS 대체발송** | 설정 가능 | 설정 가능 | - ---- - -## 2. 아키텍처 - -### 2.1 시스템 구조 - -``` -SAM MNG (브라우저) - │ - ├─ [페이지] /barobill/kakaotalk/* ← Blade 뷰 - │ KakaotalkController (페이지 렌더링) - │ - ├─ [API] /api/admin/barobill/kakaotalk/* ← AJAX 호출 - │ BarobillKakaotalkController - │ - └─ [전자계약] /esign/* ← 자동 발송 - EsignApiController::sendAlimtalk() - │ - └─ BarobillService (SOAP 클라이언트) - │ - └─ 바로빌 KAKAOTALK.asmx (WSDL) - │ - └─ 카카오톡 서버 -``` - -### 2.2 바로빌 SOAP API 엔드포인트 - -| 환경 | WSDL URL | -|------|----------| -| **테스트** | `https://testws.baroservice.com/KAKAOTALK.asmx?WSDL` | -| **운영** | `https://ws.baroservice.com/KAKAOTALK.asmx?WSDL` | - ---- - -## 3. 전자계약 알림톡 연동 (핵심) - -### 3.1 발송 흐름 - -``` -전자계약 생성 (E-Sign) - │ - ├─ [1단계] EsignApiController::sendAlimtalk() - │ │ - │ ├─ 채널 ID 조회 (getKakaotalkChannelId) - │ ├─ 템플릿 본문 + 버튼 조회 (getTemplateData) - │ ├─ 변수 치환 (#{이름}, #{계약명}, #{기한}) - │ └─ SendATKakaotalkEx 호출 - │ - ├─ [2단계] 바로빌 접수 → SendKey 반환 - │ - ├─ [3단계] 3초 대기 후 GetSendKakaotalk으로 전달 결과 확인 - │ │ - │ ├─ ResultCode = 1 → 성공 - │ └─ ResultCode != 1 → 실패 (에러 반환) - │ - └─ [이메일 폴백] 알림톡 실패 시 이메일로 자동 전환 -``` - -### 3.2 등록된 템플릿 (v1 — 현재 운영) - -**`전자계약_서명요청`** - -``` - 안녕하세요, #{이름}님. - 전자계약 서명 요청이 도착했습니다. - - ■ 계약명: #{계약명} - ■ 서명 기한: #{기한} - - 아래 버튼을 눌러 계약서를 확인하고 서명해 주세요. -``` - -- 버튼: `계약서 확인하기` (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. 트러블슈팅 (실전 경험) - -> **경고: 아래 내용은 실제 연동 과정에서 발견한 핵심 이슈다. 반드시 숙지할 것.** - -### 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 완료된 추가 항목 (2026-02-26~27) - -| 항목 | 상태 | 비고 | -|------|------|------| -| 템플릿 v2 승인 | **완료** | 버튼 URL에 `#{토큰}` 변수 포함 3종 승인 | -| `전자계약_완료` 템플릿 | **완료** | 서명 완료 알림 발송 — PDF 다운로드 버튼 | -| 역할 기반 알림 분기 | **완료** | 본사(creator)=이메일, 상대방(counterpart)=알림톡 | -| OTP SMS 발송 | **완료** | 상대방에게 SMS로 인증코드 발송 | -| 환경별 템플릿 분기 | **완료** | `resolveTemplateName()` — `_DEV` 접미사 자동 적용 | -| 서명 PDF 재생성 | **완료** | `downloadDocument()`에서 완료 계약 PDF 자동 재생성 | - -> 상세 가이드: [전자계약 알림톡/SMS 환경별 설정 가이드](./esign-notification-guide.md) - -### 5.4 대기 중인 항목 - -| 항목 | 상태 | 비고 | -|------|------|------| -| DEV 템플릿 검수 | **심사 중** | `admin.codebridge-x.com` 도메인 3종 | -| 친구톡 발송 | **대기** | 채널 친구 추가 후 가능 | -| 대량 발송 | **대기** | 단건 안정화 후 | - ---- - -## 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 | -| `getKakaotalkTemplates` | `GetKakaotalkTemplates` | 템플릿 목록 조회 | -| `getKakaotalkTemplateManagementUrl` | `GetKakaotalkTemplateManagementURL` | 템플릿 관리 URL | -| `sendATKakaotalk` | `SendATKakaotalk` | 알림톡 단건 발송 | -| `sendATKakaotalkEx` | `SendATKakaotalkEx` | 알림톡 단건 발송 (버튼 포함) | -| `sendATKakaotalks` | `SendATKakaotalks` | 알림톡 대량 발송 | -| `sendFTKakaotalk` | `SendFTKakaotalk` | 친구톡 텍스트 단건 | -| `sendFTKakaotalks` | `SendFTKakaotalks` | 친구톡 텍스트 대량 | -| `sendFIKakaotalk` | `SendFIKakaotalk` | 친구톡 이미지 | -| `sendFWKakaotalk` | `SendFWKakaotalk` | 친구톡 와이드 이미지 | -| `getSendKakaotalk` | `GetSendKakaotalk` | 전송 결과 단건 조회 | -| `getSendKakaotalks` | `GetSendKakaotalks` | 전송 결과 다건 조회 | -| `cancelReservedKakaotalk` | `CancelReservedKakaotalk` | 예약 전송 취소 | - ---- - -## 8. 참고 자료 - -- [바로빌 API 문서](https://dev.barobill.co.kr) -- [카카오비즈니스 채널 관리](https://business.kakao.com) -- [카카오 알림톡 가이드](https://kakaobusiness.gitbook.io) -- 바로빌 템플릿 관리: 로그인 후 `https://www.barobill.co.kr` → 카카오톡 템플릿 관리 - ---- - -## 변경 이력 - -| 날짜 | 버전 | 변경 내용 | -|------|------|----------| -| 2026-02-27 | 1.1 | 역할 기반 알림, OTP SMS, 환경별 템플릿 분기, 완료 알림톡 추가 | -| 2026-02-24 | 1.0 | 전자계약 알림톡 연동 완료, 트러블슈팅 문서화, v2 템플릿 가이드 추가 | -| 2026-02-14 | 0.2 | 전자계약(E-Sign) 알림톡 연동 활용 계획 추가 | -| 2026-02-14 | 0.1 | 초안 작성 - 코드 구현 완료, 실 서비스 연동 대기 | diff --git a/sam/docs/features/barobill-kakaotalk/esign-notification-guide.md b/sam/docs/features/barobill-kakaotalk/esign-notification-guide.md deleted file mode 100644 index dce2345..0000000 --- a/sam/docs/features/barobill-kakaotalk/esign-notification-guide.md +++ /dev/null @@ -1,250 +0,0 @@ -# 전자계약 알림톡/SMS 환경별 설정 가이드 - -> **작성일**: 2026-02-27 -> **상태**: 운영 중 -> **대상 프로젝트**: MNG - ---- - -## 1. 개요 - -### 1.1 목적 - -전자계약(E-Sign) 시스템의 카카오톡 알림톡, SMS, 이메일 발송을 **3개 환경(로컬/개발/운영)**에서 올바르게 설정하고 테스트하기 위한 가이드이다. - -### 1.2 핵심 원칙 - -- **역할 기반 알림**: 본사(creator)는 이메일, 상대방(counterpart)은 카카오톡/SMS -- **환경별 템플릿 분리**: 운영은 원본 템플릿, 개발은 `_DEV` 접미사 템플릿 사용 -- **URL 자동 분기**: `config('app.url')`로 환경별 도메인 자동 적용 - ---- - -## 2. 환경별 설정 - -### 2.1 도메인 및 APP_URL - -| 환경 | `APP_ENV` | `APP_URL` | 알림톡 버튼 URL 도메인 | -|------|-----------|-----------|----------------------| -| 로컬 (Docker) | `local` | `https://mng.sam.kr` | 로컬 — 알림톡 미사용 | -| 개발 서버 | `local` | `https://admin.codebridge-x.com` | `admin.codebridge-x.com` | -| 운영 서버 | `production` | `https://mng.codebridge-x.com` | `mng.codebridge-x.com` | - -### 2.2 바로빌 서버 모드 - -`barobill_members.server_mode` 컬럼으로 바로빌 API 엔드포인트를 결정한다: - -| server_mode | WSDL (카카오톡) | WSDL (SMS) | 용도 | -|-------------|----------------|------------|------| -| `test` | `testws.baroservice.com/KAKAOTALK.asmx` | `testws.baroservice.com/SMS.asmx` | 테스트 | -| `production` | `ws.baroservice.com/KAKAOTALK.asmx` | `ws.baroservice.com/SMS.asmx` | 실제 발송 | - -> `server_mode`는 환경(로컬/개발/운영)과 독립적이다. 개발서버에서도 `production` 모드로 실제 발송 가능. - -### 2.3 알림톡 템플릿 환경별 분기 - -코드에서 `resolveTemplateName()` 메서드가 `APP_ENV`에 따라 템플릿명을 자동 결정한다: - -```php -private function resolveTemplateName(string $baseName): string -{ - return $baseName . (app()->environment('production') ? '' : '_DEV'); -} -``` - -| 기본 템플릿명 | 운영 (`production`) | 개발/로컬 (기타) | -|-------------|--------------------|--------------------| -| `전자계약_서명요청` | `전자계약_서명요청` | `전자계약_서명요청_DEV` | -| `전자계약_완료` | `전자계약_완료` | `전자계약_완료_DEV` | -| `전자계약_리마인드` | `전자계약_리마인드` | `전자계약_리마인드_DEV` | - ---- - -## 3. 등록된 알림톡 템플릿 - -### 3.1 운영 템플릿 (mng.codebridge-x.com) - -| 템플릿명 | 용도 | 상태 | 버튼 URL | -|---------|------|------|---------| -| `전자계약_서명요청` | 서명 요청 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | -| `전자계약_완료` | 서명 완료 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | -| `전자계약_리마인드` | 서명 독촉 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | - -### 3.2 개발 템플릿 (admin.codebridge-x.com) - -| 템플릿명 | 용도 | 상태 | 버튼 URL | -|---------|------|------|---------| -| `전자계약_서명요청_DEV` | 서명 요청 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | -| `전자계약_완료_DEV` | 서명 완료 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | -| `전자계약_리마인드_DEV` | 서명 독촉 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | - -> 개발 템플릿 본문은 운영 템플릿과 동일하며, 버튼 URL 도메인만 다르다. - -### 3.3 템플릿 변수 - -| 변수 | 용도 | 사용 템플릿 | -|------|------|-----------| -| `#{이름}` | 서명자 이름 | 서명요청, 완료, 리마인드 | -| `#{계약명}` | 계약 제목 | 서명요청, 완료, 리마인드 | -| `#{기한}` | 서명 기한 | 서명요청, 리마인드 | -| `#{완료일}` | 계약 완료일 | 완료 | -| `#{토큰}` | 서명자 액세스 토큰 | 버튼 URL | - ---- - -## 4. 역할 기반 알림 흐름 - -### 4.1 전체 흐름 - -``` -① 계약 발송 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡 -② OTP 인증 ─→ 본사: 이메일 / 상대방: SMS -③ 다음 서명자 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡 -④ 서명 완료 ─→ 본사: 이메일(PDF) / 상대방: 카카오톡(PDF 다운로드) -``` - -### 4.2 역할 판별 - -```php -$isCounterpart = $signer->role === EsignSigner::ROLE_COUNTERPART; -``` - -| 역할 | 상수 | 알림톡 | SMS(OTP) | 이메일 | -|------|------|--------|----------|--------| -| 본사 (creator) | `ROLE_CREATOR` | ❌ | ❌ | ✅ 항상 | -| 상대방 (counterpart) | `ROLE_COUNTERPART` | ✅ 우선 | ✅ OTP만 | ✅ 폴백 | - -### 4.3 이메일 폴백 조건 - -상대방(counterpart)에게도 이메일을 보내는 경우: -- 전화번호가 없을 때 (`$signer->phone` 없음) -- 알림톡 발송 실패 시 (`$alimtalkFailed = true`) -- 발송 방식이 `email` 또는 `both`일 때 - -### 4.4 완료 알림 특수 처리 - -완료 알림톡 버튼은 **서명 페이지가 아닌 문서 다운로드 URL**로 강제 변경된다: - -```php -// sendCompletionAlimtalk() 내부 -$documentUrl = config('app.url') . '/esign/sign/' . $signer->access_token . '/api/document'; - -// 버튼 URL 강제 변경 (서명페이지 → 문서 다운로드) -if (str_contains($btn[$urlKey], '/esign/sign/') && !str_contains($btn[$urlKey], '/api/document')) { - $btn[$urlKey] = $documentUrl; -} -``` - ---- - -## 5. SMS (OTP 인증) - -### 5.1 발송 조건 - -상대방(counterpart)이 `alimtalk` 또는 `both` 발송 방식이고 전화번호가 있을 때 SMS로 OTP 발송: - -```php -if (in_array($sendMethod, ['alimtalk', 'both']) - && $signer->phone - && $signer->role === EsignSigner::ROLE_COUNTERPART) { - $this->sendOtpViaSms($contract, $signer, $otpCode); -} -``` - -### 5.2 SMS 발송 파라미터 - -| 항목 | 값 | -|------|-----| -| API | `BarobillService::sendSMSMessage()` | -| 발신번호 | `barobill_members.manager_hp` | -| 수신번호 | `esign_signers.phone` | -| 메시지 | `[SAM] 전자계약 인증코드: {코드} (5분 이내 입력)` | -| OTP 유효시간 | 5분 | -| 최대 시도 | 5회 | - -### 5.3 SMS 실패 시 이메일 폴백 - -SMS 발송 실패 → 이메일 OTP 폴백 → 이메일도 없으면 500 에러 반환. - ---- - -## 6. 바로빌 템플릿 등록 절차 - -### 6.1 관리자 페이지 - -``` -https://www.barobill.co.kr 로그인 → 카카오톡 → 템플릿관리 -``` - -### 6.2 DEV 템플릿 등록 시 주의사항 - -1. **본문**: 운영 템플릿과 **완전히 동일** (1글자도 다르면 안 됨) -2. **버튼 URL**: 도메인만 `admin.codebridge-x.com`으로 변경 -3. **템플릿명**: 운영 이름 + `_DEV` 접미사 (예: `전자계약_서명요청_DEV`) -4. **검수 기간**: 영업일 기준 2~3일 - -### 6.3 새 템플릿 추가 시 체크리스트 - -- [ ] 바로빌에서 운영용 + 개발용 2개 등록 -- [ ] 코드에서 `resolveTemplateName('기본명')`으로 호출 -- [ ] 본문의 변수 치환 로직 추가 (str_replace) -- [ ] 버튼 URL의 `#{토큰}` 치환 확인 -- [ ] 2단계 검증 (SendKey → GetSendKakaotalk) 포함 - ---- - -## 7. 관련 파일 - -| 파일 | 역할 | -|------|------| -| `app/Http/Controllers/ESign/EsignApiController.php` | 계약 발송, `sendAlimtalk()`, `resolveTemplateName()` | -| `app/Http/Controllers/ESign/EsignPublicController.php` | OTP SMS, 완료 알림톡, `sendCompletionAlimtalk()` | -| `app/Services/Barobill/BarobillService.php` | SOAP 클라이언트 (`sendATKakaotalkEx`, `sendSMSMessage`) | -| `app/Models/ESign/EsignSigner.php` | `ROLE_CREATOR`, `ROLE_COUNTERPART` 상수 | -| `app/Mail/EsignCompletedMail.php` | 완료 이메일 (PDF 다운로드 링크) | -| `app/Services/ESign/PdfSignatureService.php` | 서명 PDF 합성 (`mergeSignatures`) | - ---- - -## 8. 트러블슈팅 - -### 8.1 환경별 템플릿 미스매치 - -**증상**: `ResultCode=4` (템플릿 데이터 일치 오류) -**원인**: 개발서버에서 운영용 템플릿(`전자계약_서명요청`)으로 발송 시 버튼 URL 도메인 불일치 -**해결**: DEV 템플릿 등록 후 `APP_ENV`가 `production`이 아닌지 확인 - -### 8.2 서명 PDF 누락 (이메일) - -**증상**: 완료 이메일의 다운로드 링크가 서명 없는 초안 PDF 반환 -**원인**: `mergeSignatures()` 실패 → `signed_file_path` 미설정 → preview PDF 폴백 -**해결**: `downloadDocument()`가 완료 상태에서 자동 재생성 시도. 로그에서 trace 확인: - -```bash -# 개발서버 로그 확인 -ssh pro@114.203.209.83 "tail -100 /home/webservice/mng/storage/logs/laravel.log | grep 'PDF 서명'" -``` - -**주요 실패 원인**: -- `storage/fonts/Pretendard-Regular.ttf` 폰트 파일 누락 -- FPDI/TCPDF 패키지 미설치 → `composer install` 필요 -- `storage/app/esign/{tenant_id}/signed/` 디렉토리 권한 문제 - -### 8.3 MNG 모델 상수 누락 - -**증상**: `Undefined constant App\Models\ESign\EsignSigner::ROLE_COUNTERPART` -**원인**: API 프로젝트와 MNG 프로젝트의 모델이 독립적 — API에만 상수 정의됨 -**해결**: MNG `EsignSigner.php`에도 동일한 상수 추가 (2026-02-26 핫픽스 완료) - ---- - -## 관련 문서 - -- [바로빌 카카오톡 연동 README](./README.md) — SOAP API 전체 연동 가이드 -- [E-Sign 기술 설계](../../projects/e-sign/technical-design.md) — 전자계약 아키텍처 -- [E-Sign API 명세](../../projects/e-sign/api-specification.md) — API 엔드포인트 -- [알림톡 연동 계획](../../plans/esign-alimtalk-integration.md) — 초기 계획 (구현 완료) - ---- - -**최종 업데이트**: 2026-02-27 diff --git a/sam/docs/features/business-card-request.md b/sam/docs/features/business-card-request.md deleted file mode 100644 index b574f00..0000000 --- a/sam/docs/features/business-card-request.md +++ /dev/null @@ -1,173 +0,0 @@ -# 명함신청 관리 - -> **작성일**: 2026-02-25 -> **상태**: 구현 완료 - ---- - -## 1. 개요 - -### 1.1 목적 - -영업파트너가 명함을 신청하면 본사에서 제작소에 의뢰하고, 완료 후 처리하는 3단계 워크플로우를 제공한다. - -### 1.2 워크플로우 - -``` -요청(pending) ──제작의뢰──→ 제작중(ordered) ──처리완료──→ 완료(processed) - 노랑 파랑 초록 -``` - -### 1.3 메뉴 구조 - -| 메뉴 | URL | 대상 | 설명 | -|------|-----|------|------| -| 파트너 명함신청 | `/sales/business-cards` | 모든 사용자 | 신청폼 + 내 이력 | -| 명함신청 처리 | `/sales/business-cards/manage` | 관리자 전용 | 3단계 처리 + 뱃지 | - ---- - -## 2. 테이블 구조 - -### 2.1 `business_card_requests` - -| 필드 | 타입 | 설명 | -|------|------|------| -| `id` | bigint | PK | -| `tenant_id` | bigint | 테넌트 ID | -| `user_id` | bigint | 신청자 ID | -| `name` | varchar(50) | 성함 | -| `phone` | varchar(20) | 전화번호 | -| `title` | varchar(50) | 직함 (nullable) | -| `email` | varchar(100) | 이메일 (nullable) | -| `quantity` | int | 수량 (기본 100) | -| `memo` | text | 비고 (nullable) | -| `status` | varchar(20) | 상태: `pending`, `ordered`, `processed` | -| `ordered_by` | bigint | 제작의뢰 처리자 ID (nullable) | -| `ordered_at` | timestamp | 제작의뢰 일시 (nullable) | -| `processed_by` | bigint | 처리완료 처리자 ID (nullable) | -| `processed_at` | timestamp | 처리완료 일시 (nullable) | -| `process_memo` | text | 처리 메모 (nullable) | -| `created_at` | timestamp | 생성일 | -| `updated_at` | timestamp | 수정일 | - -**인덱스**: `(tenant_id, status)`, `user_id` - ---- - -## 3. 상태 전이 - -``` -pending ──→ ordered ──→ processed - │ ▲ - └── (역방향 전이 없음) ──┘ -``` - -| 상태 | 라벨 | 색상 | 설명 | -|------|------|------|------| -| `pending` | 요청 | 노랑 | 파트너가 신청, 관리자 확인 대기 | -| `ordered` | 제작의뢰 | 파랑 | 관리자가 제작소에 의뢰 | -| `processed` | 처리완료 | 초록 | 제작 완료, 전달 완료 | - ---- - -## 4. API 엔드포인트 - -| Method | Path | 이름 | 설명 | -|--------|------|------|------| -| GET | `/sales/business-cards` | `sales.business-cards.index` | 파트너 명함신청 (신청폼 + 이력) | -| POST | `/sales/business-cards` | `sales.business-cards.store` | 신청 등록 | -| GET | `/sales/business-cards/manage` | `sales.business-cards.manage` | 관리자 처리 화면 | -| POST | `/sales/business-cards/{id}/order` | `sales.business-cards.order` | 제작의뢰 (관리자) | -| POST | `/sales/business-cards/{id}/process` | `sales.business-cards.process` | 처리완료 (관리자) | - ---- - -## 5. 파일 구조 - -### 5.1 API 프로젝트 - -| 파일 | 설명 | -|------|------| -| `database/migrations/2026_02_24_100000_create_business_card_requests_table.php` | 테이블 생성 | -| `database/migrations/2026_02_25_100000_add_ordered_columns_to_business_card_requests_table.php` | ordered 컬럼 추가 | - -### 5.2 MNG 프로젝트 - -| 파일 | 설명 | -|------|------| -| `app/Models/Sales/BusinessCardRequest.php` | 모델 (상태 상수, 스코프, 헬퍼) | -| `app/Services/Sales/BusinessCardRequestService.php` | 서비스 (CRUD, 통계, 뱃지) | -| `app/Http/Controllers/Sales/BusinessCardRequestController.php` | 컨트롤러 | -| `app/Providers/ViewServiceProvider.php` | 사이드바 뱃지 연동 | -| `routes/web.php` | 라우트 5개 | -| `resources/views/sales/business-cards/admin-index.blade.php` | 관리자 뷰 | -| `resources/views/sales/business-cards/partner-index.blade.php` | 파트너 뷰 | - ---- - -## 6. 화면 구성 - -### 6.1 파트너 명함신청 (`partner-index`) - -``` -┌─ 회사 정보 안내 (코드브릿지엑스) ──────────────┐ -├─ 신청 폼 ─────────────────────────────────────┤ -│ 성함* │ 직함 │ 전화번호* │ 이메일 │ -│ 수량 │ 메모 │ [명함 신청하기] │ -├─ 내 신청 이력 ────────────────────────────────┤ -│ 신청일 │ 성함 │ 직함 │ 전화번호 │ 수량 │ 상태 │ -│ (요청=노랑, 제작중=파랑, 처리완료=초록) │ -└───────────────────────────────────────────────┘ -``` - -- 로그인 사용자 정보(name, phone, email)로 자동 채움 -- 관리자도 동일한 화면 접근 가능 - -### 6.2 명함신청 처리 (`admin-index`) - -``` -┌─ 통계 ──────────────────────────────────────┐ -│ 신규요청(노랑) │ 제작의뢰(파랑) │ 오늘처리(초록) │ 전체 │ -├─────────────────┬───────────────────────────┤ -│ 신규 요청 │ 제작 중 │ -│ [제작의뢰] 버튼 │ 의뢰일 + [처리완료] 버튼 │ -├─────────────────┴───────────────────────────┤ -│ 처리 완료 이력 (하단 스크롤 테이블) │ -└─────────────────────────────────────────────┘ -``` - -- 사이드바 뱃지: 요청 + 제작의뢰 합산 건수 표시 -- 처리 버튼 클릭 시 `showConfirm()` 확인 다이얼로그 - ---- - -## 7. 뱃지 연동 - -`ViewServiceProvider`에서 `BusinessCardRequestService::getPendingCount()`를 호출하여 사이드바 메뉴 뱃지에 대기 건수를 표시한다. - -- **카운트 기준**: `pending` + `ordered` 합산 -- **표시 위치**: "명함신청 처리" 메뉴 (`sales.business-cards.manage`) -- **0건일 때**: 뱃지 미표시 - ---- - -## 8. 메뉴 등록 정보 - -| ID | parent_id | 이름 | URL | sort_order | -|----|-----------|------|-----|------------| -| 15507 | 15456 | 파트너 명함신청 | `/sales/business-cards` | 5 | -| 15508 | 15456 | 명함신청 처리 | `/sales/business-cards/manage` | 6 | - -> 영업파트너에게는 "파트너 명함신청"만 보이도록 메뉴 권한 설정 필요 - ---- - -## 관련 문서 - -- 참고 패턴: `api/app/Models/CompanyRequest.php` (상태 관리 모델) -- 참고 뷰: `mng/resources/views/sales/managers/approvals.blade.php` (2분할 레이아웃) - ---- - -**최종 업데이트**: 2026-02-25 diff --git a/sam/docs/features/credit-evaluation/README.md b/sam/docs/features/credit-evaluation/README.md deleted file mode 100644 index d4d38b3..0000000 --- a/sam/docs/features/credit-evaluation/README.md +++ /dev/null @@ -1,284 +0,0 @@ -# 신용평가 시스템 (쿠콘 연동) - -> **작성일**: 2026-03-02 -> **상태**: 운영중 - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM에서 거래처/협력업체의 **기업 신용정보를 조회**하여, 거래 안전성을 사전 판단하는 시스템이다. - -### 1.2 핵심 원칙 - -- **쿠콘(KooCon/나이스평가정보)** API로 기업 신용정보 7개 항목 조회 -- **국세청 공공데이터포털** API로 사업자등록 상태(영업/휴업/폐업) 확인 -- 모든 조회 결과는 DB에 원본 저장 (감사 추적용) -- 테넌트별 월 5건 무료, 초과 시 건당 2,000원 과금 - ---- - -## 2. 시스템 구조 - -### 2.1 전체 흐름 - -``` -사용자 (SAM MNG) - │ - ▼ -CreditController::search() - │ - ├──▶ CooconService::getAllCreditInfo() - │ ├── OA08: 기업 기본정보 - │ ├── OA12: 신용요약정보 - │ ├── OA13: 단기연체정보 - │ ├── OA14: 신용도판단정보 (KCI) - │ ├── OA15: 신용도판단정보 (CB) - │ ├── OA16: 당좌거래정지정보 - │ └── OA17: 법정관리/워크아웃 - │ - ├──▶ NtsBusinessService::getBusinessStatus() - │ └── 국세청 사업자등록 상태 조회 - │ - └──▶ CreditInquiry::createFromApiResponse() - └── DB에 조회 이력 저장 -``` - -### 2.2 파트너 구조 - -| 역할 | 대상 | 설명 | -|------|------|------| -| **API 제공사** | 쿠콘(KooCon) / 나이스평가정보 | 기업 신용정보 API 플랫폼 | -| **파트너사** | (주)코드브릿지엑스 | API 키 보유, 쿠콘과 직접 계약 | -| **이용사** | 각 테넌트 (주일, 경동 등) | SAM을 통해 신용조회 실행 | - ---- - -## 3. 쿠콘(KooCon) API - -### 3.1 API 엔드포인트 - -| 환경 | URL | -|------|-----| -| 테스트 | `https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp` | -| 운영 | `https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp` | - -### 3.2 인증 방식 - -- **API_KEY**: 쿠콘에서 발급받은 인증키 (DB `coocon_configs` 테이블에서 관리) -- **API_ID**: 조회할 API 식별자 (OA08~OA17) -- **TR_SEQ**: 거래일련번호 (중복 방지용, `YmdHis` + 마이크로초 6자리) - -### 3.3 요청 형식 - -```json -{ - "API_KEY": "발급받은_API_키", - "API_ID": "OA12", - "TR_SEQ": "20260302173000123456", - "COMPANY_KEY": "1234567890" -} -``` - -- **Method**: POST -- **Content-Type**: application/json -- **Timeout**: 30초 - -### 3.4 API 목록 - -| API ID | 상수명 | 설명 | 데이터 출처 | -|--------|--------|------|------------| -| `OA08` | `API_COMPANY_INFO` | 기업 기본정보 | 나이스평가정보 | -| `OA12` | `API_CREDIT_SUMMARY` | 신용요약정보 (이슈 건수 요약) | 나이스평가정보 | -| `OA13` | `API_SHORT_TERM_OVERDUE` | 단기연체정보 | 한국신용정보원 | -| `OA14` | `API_NEGATIVE_INFO_KCI` | 신용도판단정보 (KCI) | 한국신용정보원 + 공공정보 | -| `OA15` | `API_NEGATIVE_INFO_CB` | 신용도판단정보 (CB) | 신용정보사 | -| `OA16` | `API_SUSPENSION_INFO` | 당좌거래정지정보 | 금융결제원 | -| `OA17` | `API_WORKOUT_INFO` | 법정관리/워크아웃정보 | 법원 | - -### 3.5 응답 형식 - -```json -{ - "RSLT_CD": "00000000", - "RSLT_MSG": "정상처리되었습니다.", - "RSLT_DATA": { ... } -} -``` - -- `RSLT_CD === '00000000'`: 성공 -- 기타 값: 에러 (에러 메시지는 `RSLT_MSG`에 포함) - ---- - -## 4. 국세청 사업자등록 조회 API - -### 4.1 API 정보 - -| 항목 | 값 | -|------|------| -| URL | `https://api.odcloud.kr/api/nts-businessman/v1/status` | -| 인증 | serviceKey (쿼리 파라미터) | -| 출처 | 공공데이터포털 | - -### 4.2 상태 코드 - -| 코드 | 상태 | 설명 | -|------|------|------| -| `01` | 계속사업자 | 정상 영업 중 | -| `02` | 휴업자 | 영업 중지 | -| `03` | 폐업자 | 사업 종료 | - ---- - -## 5. 데이터베이스 - -### 5.1 `coocon_configs` — API 설정 - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `name` | VARCHAR(100) | 설정 이름 | -| `environment` | ENUM('test', 'production') | 환경 | -| `api_key` | VARCHAR(100) | 쿠콘 API 키 | -| `base_url` | VARCHAR(255) | API 기본 URL | -| `description` | TEXT | 설명 | -| `is_active` | BOOLEAN | 활성화 여부 | - -> **규칙**: 환경당 1개만 활성화 가능. 새 설정 활성화 시 기존 설정은 자동 비활성화. - -### 5.2 `credit_inquiries` — 조회 이력 - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `tenant_id` | BIGINT FK | 테넌트 | -| `inquiry_key` | VARCHAR(32) UNIQUE | 조회 고유키 | -| `company_key` | VARCHAR(20) | 사업자번호/법인번호 | -| `company_name` | VARCHAR | 업체명 | -| `user_id` | BIGINT FK | 조회자 | -| `inquired_at` | TIMESTAMP | 조회 일시 | -| `nts_status` | VARCHAR(20) | 국세청 상태 | -| `nts_status_code` | VARCHAR(2) | 국세청 상태코드 | -| `short_term_overdue_cnt` | UINT | 단기연체 건수 | -| `negative_info_kci_cnt` | UINT | KCI 건수 | -| `negative_info_pb_cnt` | UINT | 공공정보 건수 | -| `negative_info_cb_cnt` | UINT | CB 건수 | -| `suspension_info_cnt` | UINT | 당좌거래정지 건수 | -| `workout_cnt` | UINT | 법정관리/워크아웃 건수 | -| `raw_*` | JSON | 각 API 원본 응답 (7개 + NTS) | -| `status` | ENUM | success / partial / failed | - ---- - -## 6. 과금 정책 - -| 항목 | 값 | -|------|------| -| 월 무료 할당량 | **5건** | -| 초과 건당 요금 | **2,000원** | -| 계산식 | `max(0, (조회건수 - 5)) × 2,000` | - -### 요금 예시 - -| 월 조회 건수 | 무료 | 유료 | 요금 | -|-------------|------|------|------| -| 3건 | 3 | 0 | 0원 | -| 5건 | 5 | 0 | 0원 | -| 10건 | 5 | 5 | 10,000원 | -| 20건 | 5 | 15 | 30,000원 | - ---- - -## 7. 환경 설정 - -### 7.1 테스트/운영 분리 - -| 환경 | API URL | 설명 | -|------|---------|------| -| 테스트 | `dev2.coocon.co.kr:8443` | 개발/검증용 (과금 없음) | -| 운영 | `sgw.coocon.co.kr` | 실 서비스 (과금 발생) | - -- `coocon_configs` 테이블에서 환경별로 별도 설정 관리 -- 각 환경에서 `is_active=true`인 설정 1개만 사용 - -### 7.2 필요한 설정 - -| 항목 | 관리 위치 | 설명 | -|------|----------|------| -| 쿠콘 API 키 | DB (`coocon_configs`) | 쿠콘에서 발급 | -| 쿠콘 API URL | DB (`coocon_configs`) | 환경별 URL | -| 국세청 API 키 | 코드 내 하드코딩 | 공공데이터포털 발급 | - ---- - -## 8. MNG 라우트 - -| Method | Path | 설명 | -|--------|------|------| -| GET | `/credit/inquiry` | 조회 이력 목록 | -| POST | `/credit/inquiry/search` | 신용정보 조회 실행 | -| POST | `/credit/inquiry/test` | API 연결 테스트 | -| GET | `/credit/inquiry/{key}/raw` | 원본 데이터 조회 | -| GET | `/credit/inquiry/{key}/report` | 리포트 조회 | -| DELETE | `/credit/inquiry/{id}` | 이력 삭제 | -| GET | `/credit/usage` | 조회회수 집계 | -| GET | `/credit/settings` | 설정 관리 | -| POST | `/credit/settings` | 설정 생성 | -| PUT | `/credit/settings/{id}` | 설정 수정 | -| DELETE | `/credit/settings/{id}` | 설정 삭제 | -| POST | `/credit/settings/{id}/toggle` | 활성화 토글 | - ---- - -## 9. 에러 코드 - -### 9.1 쿠콘 API - -| 코드 | 설명 | -|------|------| -| `NO_CONFIG` | API 설정 없음 | -| `HTTP_ERROR` | HTTP 통신 오류 | -| `EXCEPTION` | 예외 발생 | -| `RSLT_CD ≠ 00000000` | 쿠콘 API 에러 (RSLT_MSG 참조) | - -### 9.2 국세청 API - -| 코드 | 설명 | -|------|------| -| `INVALID_FORMAT` | 사업자번호 형식 오류 | -| `NOT_FOUND` | 조회 결과 없음 | -| `HTTP_ERROR` | HTTP 통신 오류 | - ---- - -## 10. 관련 파일 - -### MNG 프로젝트 - -| 구분 | 경로 | -|------|------| -| 컨트롤러 | `app/Http/Controllers/Credit/CreditController.php` | -| 컨트롤러 | `app/Http/Controllers/Credit/CreditUsageController.php` | -| 서비스 | `app/Services/Coocon/CooconService.php` | -| 서비스 | `app/Services/Nts/NtsBusinessService.php` | -| 모델 | `app/Models/Coocon/CooconConfig.php` | -| 모델 | `app/Models/Credit/CreditInquiry.php` | -| 뷰 | `resources/views/credit/inquiry/index.blade.php` | -| 뷰 | `resources/views/credit/usage/index.blade.php` | -| 뷰 | `resources/views/credit/settings/index.blade.php` | - -### API 프로젝트 (마이그레이션) - -| 경로 | -|------| -| `database/migrations/2026_01_22_192637_create_coocon_configs_table.php` | -| `database/migrations/2026_01_22_201143_create_credit_inquiries_table.php` | -| `database/migrations/2026_01_22_203001_add_company_info_to_credit_inquiries_table.php` | -| `database/migrations/2026_01_28_163000_add_tenant_id_to_credit_inquiries_table.php` | - ---- - -**최종 업데이트**: 2026-03-02 diff --git a/sam/docs/features/documents/README.md b/sam/docs/features/documents/README.md deleted file mode 100644 index ab1f4d5..0000000 --- a/sam/docs/features/documents/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# 문서관리 시스템 (Document Management) - -> **상태**: API 완전 구현 -> **최종 갱신**: 2026-02-27 - ---- - -## 1. 개요 - -EAV(Entity-Attribute-Value) 패턴 기반의 동적 문서 관리 시스템. 문서 서식(Template)을 정의하면 해당 서식에 따라 문서를 생성·결재·관리할 수 있다. 제품 검사(FQC), 공정 검사 등 다양한 문서 유형을 하나의 시스템으로 처리한다. - -**핵심 기능:** -- 문서 서식(Template) 관리: 결재선, 기본필드, 섹션, 컬럼 정의 -- EAV 기반 동적 데이터 저장 (section_id + column_id + row_index + field_key) -- 결재 워크플로우: 작성 → 검토 → 승인 (다단계) -- FQC(제품검사) 일괄 생성 및 진행 현황 -- 첨부파일 관리 (서명, 이미지, 참조 문서) - ---- - -## 2. 모델 - -### 서식 (Template) 계층 - -| 모델 | 설명 | -|------|------| -| `DocumentTemplate` | 서식 마스터 (이름, 카테고리, 회사 정보, 활성 여부) | -| `DocumentTemplateApprovalLine` | 결재선 (이름, 부서, 역할, 순서) | -| `DocumentTemplateBasicField` | 기본 필드 (라벨, 유형, 기본값) | -| `DocumentTemplateSection` | 섹션 (제목, 이미지, 순서) | -| `DocumentTemplateSectionField` | 섹션 필드 (field_key, 유형, 옵션, 필수 여부) | -| `DocumentTemplateColumn` | 컬럼 (라벨, 너비, 유형, 하위 라벨) | -| `DocumentTemplateLink` | 서식 간 연결 | - -### 문서 (Document) 계층 - -| 모델 | 설명 | Traits | -|------|------|--------| -| `Document` | 문서 인스턴스 (서식 기반, 상태, 연결 대상) | BelongsToTenant, Auditable, SoftDeletes | -| `DocumentApproval` | 결재 기록 (단계, 역할, 상태, 코멘트) | BelongsToTenant | -| `DocumentData` | EAV 데이터 (section + column + row + field_key → value) | BelongsToTenant | -| `DocumentAttachment` | 첨부파일 (유형: general, signature, image, reference) | BelongsToTenant | - -**문서 상태 흐름:** -``` -DRAFT → PENDING → APPROVED - → REJECTED → DRAFT (재작성) - → CANCELLED -``` - -**컬럼 유형:** text, check, complex, select, measurement - ---- - -## 3. 서비스 - -| 서비스 | 주요 메서드 | -|--------|-----------| -| `DocumentService` | list, show, create, update, destroy, submit, approve, reject, cancel, bulkCreateFqc, fqcStatus, resolve, upsert, formatTemplateForReact | -| `DocumentTemplateService` | list, show | - ---- - -## 4. API 엔드포인트 - -### 서식 조회 (읽기 전용) - -| HTTP | URI | 설명 | -|------|-----|------| -| GET | `/v1/document-templates` | 서식 목록 | -| GET | `/v1/document-templates/{id}` | 서식 상세 (필드·컬럼·섹션 포함) | - -### 문서 CRUD + 워크플로우 - -| HTTP | URI | 설명 | -|------|-----|------| -| GET | `/v1/documents` | 문서 목록 (필터: status, template_id, 날짜, 검색) | -| POST | `/v1/documents` | 문서 생성 | -| GET | `/v1/documents/{id}` | 문서 상세 | -| PATCH | `/v1/documents/{id}` | 문서 수정 | -| DELETE | `/v1/documents/{id}` | 문서 삭제 | -| POST | `/v1/documents/{id}/submit` | 결재 요청 | -| POST | `/v1/documents/{id}/approve` | 승인 | -| POST | `/v1/documents/{id}/reject` | 반려 | -| POST | `/v1/documents/{id}/cancel` | 취소/회수 | - -### 특수 기능 - -| HTTP | URI | 설명 | -|------|-----|------| -| POST | `/v1/documents/bulk-create-fqc` | FQC 일괄 생성 | -| GET | `/v1/documents/fqc-status` | FQC 진행 현황 | -| GET | `/v1/documents/resolve` | 카테고리+item_id로 문서 조회 | -| POST | `/v1/documents/upsert` | 생성 또는 업데이트 | - ---- - -## 5. FormRequest - -| Request | 주요 검증 | -|---------|----------| -| `StoreRequest` | template_id (필수, exists), title, approvers[], data[] (EAV), attachments[] | -| `UpdateRequest` | title, data[] (EAV), attachments[] | -| `IndexRequest` | status, template_id, search, 날짜 범위, 정렬 | -| `BulkCreateFqcRequest` | order_id, template_id, item_count | -| `ResolveRequest` | category, item_id | -| `ApproveRequest` | comment (선택) | -| `RejectRequest` | comment (필수) | - ---- - -## 관련 문서 - -- [MNG 문서관리 시스템 상세](mng-document-system.md) — MNG 화면 구성, 탭별 기능, 서식 빌더, EAV 저장 패턴 상세 -- [MNG 문서양식관리](mng-document-template.md) — 서식 생성/편집, Legacy/Block Builder, 프리셋, 연결품목 관리 -- [DB 스키마 — 문서/전자서명](../../system/database/documents.md) -- [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 적용 -- Swagger: `/api-docs` → Documents 섹션 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/documents/mng-document-system.md b/sam/docs/features/documents/mng-document-system.md deleted file mode 100644 index eae95a6..0000000 --- a/sam/docs/features/documents/mng-document-system.md +++ /dev/null @@ -1,738 +0,0 @@ -# MNG 문서관리 시스템 상세 기술 명세 - -> **작성일**: 2026-03-06 -> **상태**: 운영 중 -> **프로젝트**: SAM MNG (관리자 웹) -> **관련**: [README.md](README.md) (API 명세) - ---- - -## 1. 개요 - -### 1.1 목적 - -블라인드/스크린 제조 현장의 **검사 성적서, 작업일지, 수입검사 기록** 등 품질/생산 문서를 전자화하여 관리하는 시스템. 문서 양식(Template)을 정의하면 EAV 패턴으로 데이터를 동적 저장하며, 다단계 결재 워크플로우를 지원한다. - -### 1.2 핵심 특징 - -| 특징 | 설명 | -|------|------| -| **EAV 패턴** | 양식별로 다른 필드를 하나의 `document_data` 테이블에 저장 | -| **2가지 양식 빌더** | 레거시 빌더 (DB 정규화) + 블록 빌더 (A4 JSON 스키마) | -| **결재 워크플로우** | 작성 → 검토 → 승인 (다단계 순차 결재) | -| **자동 데이터 매핑** | 작업지시서/수주 데이터에서 기본필드 자동 채움 | -| **다형성 연결** | work_order, sales_order 등 다양한 모델과 연결 | -| **자재 LOT 추적** | 검사 문서에서 투입 자재의 LOT 이력 조회 | - -### 1.3 문서 구조 - -| 문서 | 설명 | -|------|------| -| [README.md](README.md) | API 엔드포인트, 모델 요약, FormRequest | -| **이 문서** | MNG 화면별 상세, 동작원리, 데이터 흐름 | - ---- - -## 2. 메뉴/탭 구조 - -``` -생산 관리 -└── 문서관리 - ├── 문서 목록 /documents ← 문서 검색/필터/관리 - ├── 새 문서 작성 /documents/create ← 템플릿 선택 → 폼 입력 - ├── 문서 상세 /documents/{id} ← 읽기 전용 + 결재 현황 - ├── 문서 수정 /documents/{id}/edit ← DRAFT/REJECTED만 - ├── 인쇄 /documents/{id}/print ← 성적서 인쇄용 - │ - └── 문서양식 관리 - ├── 양식 목록 /document-templates ← 양식 검색/관리 - ├── 새 양식 (레거시) /document-templates/create ← 레거시 빌더 - ├── 양식 수정 /document-templates/{id}/edit ← 자동 빌더 판별 - ├── 양식 디자이너 /document-templates/block-create ← 블록 빌더 - └── 블록 수정 /document-templates/{id}/block-edit ← 블록 빌더 수정 -``` - ---- - -## 3. 파일 구조 - -``` -mng/ -├── app/Http/Controllers/ -│ ├── DocumentController.php ← 문서 CRUD 화면 -│ └── DocumentTemplateController.php ← 양식 관리 화면 -├── app/Models/Documents/ -│ ├── Document.php ← 문서 모델 -│ ├── DocumentApproval.php ← 결재 단계 -│ ├── DocumentData.php ← EAV 데이터 -│ ├── DocumentTemplate.php ← 양식 마스터 -│ └── ... (기타 템플릿 관련 모델) -└── resources/views/ - ├── documents/ - │ ├── index.blade.php ← 문서 목록 - │ ├── edit.blade.php ← 문서 작성/수정 - │ ├── show.blade.php ← 문서 상세 - │ └── print.blade.php ← 인쇄 전용 - └── document-templates/ - ├── index.blade.php ← 양식 목록 - ├── edit.blade.php ← 레거시 빌더 - ├── block-editor.blade.php ← 블록 빌더 - └── partials/ - ├── block-palette.blade.php ← 블록 타입 목록 - ├── block-canvas.blade.php ← 편집 캔버스 - └── block-properties.blade.php ← 속성 패널 -``` - ---- - -## 4. 데이터베이스 아키텍처 - -### 4.1 테이블 관계도 - -``` -document_templates (양식 마스터) -├── 1:N → document_template_approval_lines (결재선 정의) -├── 1:N → document_template_basic_fields (기본필드 정의) -├── 1:N → document_template_sections (섹션 정의) -│ └── 1:N → document_template_section_items (검사항목) -├── 1:N → document_template_columns (테이블 컬럼 정의) -├── 1:N → document_template_section_fields (섹션 필드) -├── 1:N → document_template_links (외부 연결 정의) -│ └── 1:N → document_template_link_values (템플릿 레벨 연결값) -│ -└── 1:N → documents (문서 인스턴스) - ├── 1:N → document_approvals (결재 진행) - ├── 1:N → document_data (EAV 필드값) - ├── 1:N → document_attachments (첨부파일) - └── 1:N → document_links (문서 레벨 연결) -``` - -### 4.2 documents (문서) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `tenant_id` | BIGINT FK | 테넌트 격리 | -| `template_id` | BIGINT FK | 사용 양식 | -| `document_no` | VARCHAR UNIQUE | 문서번호 (자동 채번) | -| `title` | VARCHAR | 문서 제목 | -| `status` | VARCHAR(20) | 상태 (5가지) | -| `linkable_type` | VARCHAR NULL | 다형성 모델 타입 | -| `linkable_id` | BIGINT NULL | 다형성 모델 ID | -| `submitted_at` | TIMESTAMP NULL | 결재 요청 일시 | -| `completed_at` | TIMESTAMP NULL | 결재 완료 일시 | -| `created_by` | BIGINT FK | 작성자 | -| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | - -**인덱스**: `(tenant_id, status)`, `document_no`, `(linkable_type, linkable_id)` - -### 4.3 document_data (EAV 필드값) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `document_id` | BIGINT FK | 소속 문서 | -| `section_id` | BIGINT FK NULL | 소속 섹션 (NULL=기본필드) | -| `column_id` | BIGINT FK NULL | 소속 컬럼 (테이블 데이터용) | -| `row_index` | INT | 테이블 행 번호 (기본: 0) | -| `field_key` | VARCHAR | 필드 식별자 (`bf_1`, `cf_2`, `col_3`) | -| `field_value` | TEXT NULL | 실제 값 | - -**인덱스**: `(document_id, section_id)`, `(document_id, field_key)` - -### 4.4 document_approvals (결재) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `document_id` | BIGINT FK | 소속 문서 | -| `user_id` | BIGINT FK | 결재자 | -| `step` | INT | 결재 순서 (1, 2, 3...) | -| `role` | VARCHAR | 역할 (작성, 검토, 승인) | -| `status` | VARCHAR(20) | PENDING / APPROVED / REJECTED | -| `comment` | TEXT NULL | 결재 의견 | -| `acted_at` | TIMESTAMP NULL | 처리 일시 | - -**인덱스**: `(document_id, step)`, `(user_id, status)` - -### 4.5 document_attachments (첨부파일) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `document_id` | BIGINT FK | 소속 문서 | -| `file_id` | BIGINT FK | File 모델 연결 | -| `attachment_type` | VARCHAR | `general`, `signature`, `image`, `reference` | -| `description` | VARCHAR NULL | 설명 | -| `created_by` | BIGINT FK | 업로드자 | - ---- - -## 5. 양식(Template) 시스템 - -### 5.1 두 가지 빌더 방식 - -| 방식 | 필드명 | 저장 구조 | UI | 상태 | -|------|--------|----------|-----|------| -| **레거시 빌더** | `builder_type = null` | 정규화 테이블들 | `edit.blade.php` | 기존 양식용 | -| **블록 빌더** | `builder_type = 'block'` | `schema` JSON | `block-editor.blade.php` | 신규 양식용 | - -**자동 판별 로직:** - -```php -// DocumentTemplateController::edit() -if ($template->isBlockBuilder()) { - return $this->blockEdit($id); // block-editor.blade.php -} else { - return view('document-templates.edit'); // 레거시 -} -``` - -### 5.2 양식 마스터 (document_templates) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `name` | VARCHAR | 양식명 (예: "제품검사 성적서") | -| `category` | VARCHAR | 분류 (common_codes 기반) | -| `title` | VARCHAR NULL | 문서 제목 템플릿 | -| `company_name` | VARCHAR NULL | 회사명 | -| `company_address` | VARCHAR NULL | 회사 주소 | -| `company_contact` | VARCHAR NULL | 연락처 | -| `footer_remark_label` | VARCHAR NULL | 비고란 라벨 | -| `footer_judgement_label` | VARCHAR NULL | 판정란 라벨 | -| `footer_judgement_options` | JSON NULL | 판정 선택지 (적합/부적합) | -| `builder_type` | VARCHAR NULL | `block` 또는 NULL | -| `schema` | JSON NULL | 블록 빌더 JSON 스키마 | -| `page_config` | JSON NULL | 페이지 설정 (A4, 여백 등) | -| `is_active` | BOOLEAN | 활성 여부 | - -### 5.3 레거시 빌더 구성 요소 - -#### 결재선 (document_template_approval_lines) - -``` -step 1: 작성 (작성자 본인) -step 2: 검토 (팀장) -step 3: 승인 (부장) -``` - -| 컬럼 | 설명 | -|------|------| -| `name` | 라벨 (작성, 검토, 승인) | -| `dept` | 부서 | -| `role` | 역할 | -| `sort_order` | 순서 | - -#### 기본필드 (document_template_basic_fields) - -문서 상단의 고정 필드 영역. - -| 컬럼 | 설명 | -|------|------| -| `label` | 필드 라벨 (품명, LOT NO, 납기일 등) | -| `field_key` | 식별자 (EAV 저장 시 사용) | -| `field_type` | 입력 타입 (text, date, number, item_search) | -| `default_value` | 기본값 | -| `sort_order` | 순서 | - -**EAV 저장 시 field_key 패턴:** - -``` -bf_1 → 기본필드 ID 1 (예: 품명) -bf_2 → 기본필드 ID 2 (예: LOT NO) -bf_3 → 기본필드 ID 3 (예: 납기일) -``` - -#### 섹션 (document_template_sections) - -검사 기준서의 섹션 단위. - -| 컬럼 | 설명 | -|------|------| -| `title` | 섹션 제목 (예: "겉모양 검사", "치수 검사") | -| `image_path` | 도해 이미지 경로 (검사 부위 도면) | -| `sort_order` | 순서 | - -#### 검사항목 (document_template_section_items) - -각 섹션 내의 개별 검사항목. - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `category` | VARCHAR | 구분 (겉모양, 치수, 재질) | -| `item` | VARCHAR | 검사항목명 | -| `standard` | VARCHAR | 검사기준 (100mm ±5mm) | -| `tolerance` | JSON NULL | 허용오차 (min/max) | -| `standard_criteria` | VARCHAR NULL | 판정기준 | -| `method` | VARCHAR | 검사방법 (육안, 측정) | -| `measurement_type` | VARCHAR NULL | 측정 유형 | -| `frequency_n` | INT NULL | 검사건수 N | -| `frequency_c` | INT NULL | 합격건수 C | -| `frequency` | VARCHAR NULL | 검사빈도 텍스트 | -| `field_values` | JSON NULL | 확장 필드 (마이그레이션 없이 추가) | - -#### 테이블 컬럼 (document_template_columns) - -검사 데이터 테이블의 컬럼 정의. - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `label` | VARCHAR | 컬럼 라벨 | -| `width` | INT NULL | 너비 (px) | -| `column_type` | VARCHAR | `text`, `check`, `complex`, `measurement`, `select` | -| `group_name` | VARCHAR NULL | 상단 병합 헤더명 | -| `sub_labels` | JSON NULL | complex 타입 하위 라벨 | -| `sort_order` | INT | 순서 | - -**컬럼 타입 상세:** - -| 타입 | 설명 | 예시 | -|------|------|------| -| `text` | 단순 텍스트 입력 | 비고, 메모 | -| `check` | 체크박스 (합격/부적합) | 외관 검사 합격 여부 | -| `complex` | 여러 서브필드 조합 | 측정값 + 단위 + 판정 | -| `measurement` | 수치 입력 | 길이: 100.5mm | -| `select` | 드롭다운 선택 | 판정: 합격/불합격/보류 | - -#### 외부 연결 (document_template_links) - -템플릿에서 외부 테이블 데이터를 참조하기 위한 정의. - -| 컬럼 | 설명 | -|------|------| -| `link_key` | 연결 식별자 | -| `label` | 화면 라벨 | -| `link_type` | `single` (1개 선택) / `multiple` (다중 선택) | -| `source_table` | 소스 테이블 (`items`, `processes`, `users`) | -| `search_params` | API 검색 추가 조건 (JSON) | -| `display_fields` | 표시 필드 (title, subtitle) | -| `is_required` | 필수 여부 | - -### 5.4 블록 빌더 구조 - -**페이지 설정 (page_config):** - -```json -{ - "size": "A4", - "orientation": "portrait", - "margin": { - "top": 20, - "right": 15, - "bottom": 20, - "left": 15 - } -} -``` - -**스키마 (schema):** - -블록 배열로 레이아웃 정의. 드래그앤드롭으로 편집. - -```json -{ - "blocks": [ - { "type": "text", "x": 0, "y": 0, "width": 100, "content": "검사 성적서" }, - { "type": "table", "x": 0, "y": 50, "columns": [...], "rows": [...] }, - { "type": "image", "x": 200, "y": 100, "src": "..." } - ] -} -``` - -**블록 빌더 UI (3패널):** - -``` -┌──────────┬────────────────────┬──────────┐ -│ 블록 │ │ 속성 │ -│ 팔레트 │ A4 캔버스 │ 패널 │ -│ │ │ │ -│ [텍스트] │ ┌──────────────┐ │ 너비: _ │ -│ [이미지] │ │ 드래그앤드롭 │ │ 높이: _ │ -│ [표] │ │ 블록 배치 │ │ 색상: _ │ -│ [선] │ │ │ │ 폰트: _ │ -│ [도형] │ └──────────────┘ │ │ -└──────────┴────────────────────┴──────────┘ -``` - ---- - -## 6. EAV 데이터 저장 패턴 - -### 6.1 핵심 개념 - -하나의 `document_data` 테이블에 **모든 양식의 모든 필드값**을 저장. 양식이 다르면 field_key가 다르고, 같은 양식이라도 섹션/행이 다르면 section_id/row_index로 구분. - -### 6.2 저장 구조 - -``` -document_data 레코드 예시: - -기본필드 (상단 고정 영역): -┌─────────────┬────────────┬───────────┬───────────┬───────────┬─────────────┐ -│ document_id │ section_id │ column_id │ row_index │ field_key │ field_value │ -├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤ -│ 42 │ NULL │ NULL │ 0 │ bf_1 │ 블라인드A │ ← 품명 -│ 42 │ NULL │ NULL │ 0 │ bf_2 │ LOT-2026-001│ ← LOT NO -│ 42 │ NULL │ NULL │ 0 │ bf_3 │ 2026-03-15 │ ← 납기일 -├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤ - -테이블 데이터 (섹션별 검사 결과): -│ 42 │ 10 │ 20 │ 0 │ col_20 │ 합격 │ ← 섹션10, 컬럼20, 1행 -│ 42 │ 10 │ 20 │ 1 │ col_20 │ 부적합 │ ← 섹션10, 컬럼20, 2행 -│ 42 │ 10 │ 21 │ 0 │ col_21 │ 100.5 │ ← 섹션10, 컬럼21, 1행 -└─────────────┴────────────┴───────────┴───────────┴───────────┴─────────────┘ -``` - -### 6.3 field_key 네이밍 규칙 - -| 접두사 | 의미 | 예시 | -|--------|------|------| -| `bf_` | 기본필드 (BasicField) | `bf_1`, `bf_2` | -| `cf_` | 섹션필드 (SectionField) | `cf_5`, `cf_6` | -| `col_` | 컬럼 데이터 | `col_20`, `col_21` | - -### 6.4 데이터 조회 패턴 - -```php -// 기본필드 값 조회 -$data = DocumentData::where('document_id', $id) - ->whereNull('section_id') - ->get() - ->keyBy('field_key'); - -$productName = $data['bf_1']->field_value; - -// 섹션별 테이블 데이터 조회 -$rows = DocumentData::where('document_id', $id) - ->where('section_id', $sectionId) - ->get() - ->groupBy('row_index'); -``` - ---- - -## 7. 결재 워크플로우 - -### 7.1 상태 전이 - -``` -DRAFT (작성중) - │ - ├── submit() → PENDING (결재중) - │ │ - │ ├── approve() [step 1] → 다음 step 대기 - │ ├── approve() [step 2] → 다음 step 대기 - │ ├── approve() [마지막] → APPROVED (승인) - │ │ - │ └── reject() → REJECTED (반려) - │ │ - │ └── edit → submit() → PENDING (재요청) - │ - └── cancel() → CANCELLED (취소) -``` - -### 7.2 상태값 및 라벨 - -| 코드 | 라벨 | 색상 | 편집 가능 | -|------|------|------|----------| -| `DRAFT` | 작성중 | gray | 예 | -| `PENDING` | 결재중 | yellow | 아니오 | -| `APPROVED` | 승인 | green | 아니오 | -| `REJECTED` | 반려 | red | 예 (수정 후 재요청) | -| `CANCELLED` | 취소 | gray | 아니오 | - -### 7.3 결재 단계 (Approval) - -``` -DocumentTemplateApprovalLine (양식 정의) - ↓ (문서 생성 시 복사) -DocumentApproval (문서별 결재 레코드) - -step 1: 작성 → PENDING → 결재자 승인 → APPROVED -step 2: 검토 → PENDING → 결재자 승인 → APPROVED -step 3: 승인 → PENDING → 결재자 승인 → APPROVED → 문서 전체 APPROVED -``` - -### 7.4 결재 판단 메서드 - -```php -// Document 모델 -canEdit() // DRAFT 또는 REJECTED -canSubmit() // DRAFT 또는 REJECTED -canApprove() // PENDING (현재 결재자만) -canCancel() // DRAFT 또는 PENDING (작성자만) -``` - ---- - -## 8. 자동 데이터 매핑 - -### 8.1 개요 - -문서 작성/수정 시, 연결된 작업지시서(work_order)/수주(order) 데이터에서 기본필드를 **자동으로 채움**. 사용자 입력 부담을 줄이고 데이터 정확성을 보장. - -### 8.2 검사 성적서 매핑 (field_key 기반) - -| field_key | 라벨 | 소스 | -|-----------|------|------| -| `product_name` | 품명 | `workOrderItem.item_name` | -| `specification` | 규격 | `workOrderItem.specification` | -| `lot_no` | LOT NO | `order.order_no` | -| `lot_size` | LOT 크기 | `"N 개소"` (개소 수 기반) | -| `client` | 발주처 | `order.client_name` | -| `site_name` | 현장명 | `workOrder.project_name` | -| `inspection_date` | 검사일 | `workOrderItem.options.inspection_data.inspected_at` | -| `inspector` | 검사자 | 검사자 이름 | - -### 8.3 작업일지 매핑 (label 기반) - -| label 포함 문자열 | 소스 | -|------------------|------| -| `발주처` | `order.client_name` | -| `현장명` | `workOrder.project_name` | -| `작업일자` | `now()` | -| `LOT NO`, `LOT` | `order.order_no` | -| `납기일`, `납기` | `order.delivery_date` | -| `작업지시번호` | `workOrder.work_order_no` | -| `수주일` | `order.received_at` 또는 `order.created_at` | - -### 8.4 자동 매핑 흐름 - -``` -문서 작성/수정 페이지 로드 - ↓ -DocumentController::edit() - ↓ -resolveAndBackfillBasicFields($template, $document) - ↓ -linkable_type 확인 (work_order? order?) - ↓ -field_key 또는 label 매칭 - ↓ -DB에 값이 없으면 → 소스 데이터에서 resolve - ↓ -뷰에 자동 채움된 값 전달 -``` - ---- - -## 9. 자재 LOT 추적 - -### 9.1 개요 - -검사 성적서에서 해당 작업지시의 **투입 자재 LOT 이력**을 조회. `stock_transactions` 테이블의 OUT(투입)/IN(취소) 트랜잭션을 상쇄하여 순수 투입량을 계산. - -### 9.2 추적 구조 - -``` -work_orders (작업지시) - │ - ├── stock_transactions (재고 트랜잭션) - │ ├── OUT (투입): qty < 0 - │ └── IN (취소/반납): qty > 0 - │ → 순수 투입량 = ABS(SUM(qty)) where qty < 0 - │ - └── work_order_material_inputs (개소별 투입자재) - └── stock_lots (LOT 정보) JOIN -``` - -### 9.3 표시 내용 - -| 항목 | 설명 | -|------|------| -| 자재명 | 투입된 원자재/부자재 이름 | -| LOT 번호 | 자재의 LOT 식별 번호 | -| 투입 수량 | OUT 트랜잭션 합계 (절대값) | -| 투입일 | 트랜잭션 일시 | - ---- - -## 10. 화면별 상세 - -### 10.1 문서 목록 (/documents) - -**필터 항목:** - -| 필터 | 타입 | 설명 | -|------|------|------| -| 검색 | text | 문서번호 또는 제목 | -| 상태 | dropdown | DRAFT, PENDING, APPROVED, REJECTED, CANCELLED, 휴지통(admin) | -| 양식분류 | dropdown | category | -| 템플릿 | dropdown | template_id | -| 날짜 범위 | date | created_at (from ~ to) | - -**목록 테이블 컬럼:** - -``` -문서번호 | 제목 | 양식 | 상태 | 작성자 | 작성일 | 결재현황 -``` - -### 10.2 문서 작성/수정 (/documents/create, /documents/{id}/edit) - -**폼 구성:** - -``` -┌──────────────────────────────────────────────┐ -│ 템플릿 선택 (읽기전용) │ -│ 제목 (필수) │ -├──────────────────────────────────────────────┤ -│ 기본 필드 (template.basicFields) │ -│ ┌─────────────────┬─────────────────┐ │ -│ │ 품명: [자동채움] │ LOT NO: [자동] │ │ -│ │ 납기일: [날짜] │ 발주처: [자동] │ │ -│ └─────────────────┴─────────────────┘ │ -├──────────────────────────────────────────────┤ -│ 섹션 1: 겉모양 검사 │ -│ ┌──────────────────────────────────────┐ │ -│ │ 도해 이미지 (있으면) │ │ -│ ├──────┬──────┬──────┬──────┬──────┤ │ -│ │ 구분 │ 항목 │ 기준 │ 결과1│ 결과2│ │ -│ ├──────┼──────┼──────┼──────┼──────┤ │ -│ │ 치수 │ 길이 │±5mm │ [ ] │ [ ] │ │ -│ │ 외관 │ 흠집 │ 없음 │ [✓] │ [✓] │ │ -│ ├──────┴──────┴──────┴──────┴──────┤ │ -│ │ [+ 행 추가] [행 삭제] │ │ -│ └──────────────────────────────────────┘ │ -├──────────────────────────────────────────────┤ -│ 외부 연결 (template.links) │ -│ 품목 선택: [검색 드롭다운] │ -├──────────────────────────────────────────────┤ -│ 첨부파일 │ -│ [일반 문서] [서명 이미지] [검사 사진] [참고 자료] │ -├──────────────────────────────────────────────┤ -│ [임시저장] [결재 요청] │ -└──────────────────────────────────────────────┘ -``` - -### 10.3 문서 상세 (/documents/{id}) - -**읽기 전용 표시:** - -``` -┌──────────────────────────────────────────────┐ -│ 문서번호: DOC-260306-001 상태: [🟢 승인] │ -│ 제목: 블라인드A 검사 성적서 │ -├──────────────────────────────────────────────┤ -│ 기본 필드 (읽기 전용) │ -├──────────────────────────────────────────────┤ -│ 검사 데이터 테이블 (읽기 전용) │ -├──────────────────────────────────────────────┤ -│ 결재 현황 │ -│ ┌────────┬────────┬────────┐ │ -│ │ 작성 │ 검토 │ 승인 │ │ -│ │ 홍길동 │ 김과장 │ 박부장 │ │ -│ │ ✓승인 │ ✓승인 │ ●대기 │ │ -│ └────────┴────────┴────────┘ │ -├──────────────────────────────────────────────┤ -│ 자재 투입 LOT (작업지시 연결 시) │ -│ ┌────────┬──────────┬──────┬──────┐ │ -│ │ 자재명 │ LOT 번호 │ 수량 │ 투입일│ │ -│ └────────┴──────────┴──────┴──────┘ │ -├──────────────────────────────────────────────┤ -│ 첨부파일 목록 │ -├──────────────────────────────────────────────┤ -│ [수정] [인쇄] [결재 승인] [결재 반려] │ -└──────────────────────────────────────────────┘ -``` - -### 10.4 인쇄 (/documents/{id}/print) - -성적서 형식의 인쇄 전용 화면. `window.print()` 호출. 작업지시 관련 자재(work_order_items) 데이터 포함. - -### 10.5 양식 목록 (/document-templates) - -**필터:** -- 검색: 양식명, 제목, 분류 -- 카테고리: common_codes 기반 + 기존 데이터 폴백 -- 활성 상태: 활성 / 비활성 / 휴지통(admin) - -**HTMX**: 필터 변경 시 테이블 영역만 부분 로드 - ---- - -## 11. 첨부파일 유형 - -| 유형 | 코드 | 용도 | 예시 | -|------|------|------|------| -| 일반 문서 | `general` | PDF, 엑셀 등 | 규격서, 보고서 | -| 서명 이미지 | `signature` | 검사 완료 서명 | 검사자 서명 사진 | -| 검사 사진 | `image` | 검사 증빙 사진 | 불량 부위 촬영 | -| 참고 자료 | `reference` | 참고용 문서 | KS 규격, 작업 지침 | - ---- - -## 12. API 연동 (MNG → API) - -MNG 뷰에서 데이터 저장/삭제는 **API 서버를 호출**하여 처리. GET 요청(뷰 렌더링)은 MNG 컨트롤러가 직접 처리. - -| 작업 | MNG (GET 요청) | API (POST/PUT/DELETE) | -|------|---------------|----------------------| -| 목록 조회 | `DocumentController::index()` | `GET /v1/documents` | -| 상세 조회 | `DocumentController::show()` | `GET /v1/documents/{id}` | -| 생성 | 폼 표시만 | `POST /v1/documents` | -| 수정 | 폼 표시만 | `PATCH /v1/documents/{id}` | -| 삭제 | - | `DELETE /v1/documents/{id}` | -| 결재 요청 | - | `POST /v1/documents/{id}/submit` | -| 승인 | - | `POST /v1/documents/{id}/approve` | -| 반려 | - | `POST /v1/documents/{id}/reject` | - ---- - -## 13. 카테고리 해결 로직 - -양식 카테고리는 **common_codes 테이블**에서 조회하되, 없으면 **기존 데이터에서 추출**하여 폴백. - -```php -// DocumentTemplateController::getCategories() -$categories = CommonCode::where('group', 'document_category') - ->orderBy('sort_order') - ->get(); - -if ($categories->isEmpty()) { - // 폴백: 기존 템플릿의 category 값에서 중복 제거 - $categories = DocumentTemplate::distinct('category') - ->pluck('category') - ->filter(); -} -``` - ---- - -## 14. 검사항목 확장 (field_values JSON) - -`document_template_section_items.field_values` JSON 컬럼으로 마이그레이션 없이 새 필드를 추가할 수 있다. - -```json -{ - "custom_field_1": "추가 기준값", - "min_value": 95.0, - "max_value": 105.0, - "unit": "mm" -} -``` - -> options JSON 컬럼 정책(`docs/standards/options-column-policy.md`) 준용 - ---- - -## 15. HTMX 전체 페이지 로드 규칙 - -문서관리 페이지들은 JavaScript를 사용하므로 HTMX 부분 로드 시 스크립트 미실행 문제가 있다. 컨트롤러에서 HX-Request 감지 시 **HX-Redirect로 전체 페이지 리로드 강제**. - -```php -if ($request->header('HX-Request')) { - return response('', 200)->header('HX-Redirect', route('documents.index')); -} -``` - ---- - -## 관련 문서 - -- [README.md](README.md) — API 엔드포인트, 모델 요약, FormRequest -- [DB 스키마 — 문서/전자서명](../../system/database/documents.md) — 테이블 상세 -- [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 참고 -- [결재관리 시스템](../approvals/README.md) — 별도 결재 시스템 (문서관리와 독립) - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/documents/mng-document-template.md b/sam/docs/features/documents/mng-document-template.md deleted file mode 100644 index 5570865..0000000 --- a/sam/docs/features/documents/mng-document-template.md +++ /dev/null @@ -1,826 +0,0 @@ -# MNG 문서양식관리 (Document Template Management) - -> **작성일**: 2026-03-06 -> **상태**: 운영 중 -> **라우트**: `/document-templates` -> **관련**: [README.md](README.md) | [MNG 문서관리](mng-document-system.md) - ---- - -## 1. 개요 - -문서관리 시스템에서 사용하는 **서식(Template)**을 생성, 편집, 복제, 관리하는 기능. 검사 성적서, 작업지시서 등 다양한 문서 양식을 정의하며, 2가지 빌더 타입을 지원한다. - -| 빌더 | builder_type | UI 명칭 | 설명 | -|------|-------------|---------|------| -| **Legacy Builder** | `legacy` 또는 null | 새 양식 | 탭 기반 폼 UI (순수 JavaScript) | -| **Block Builder** | `block` | 양식 디자이너 | WYSIWYG 캔버스 편집기 (Alpine.js + SortableJS) | - -> **명칭 변경 이력**: Block Builder의 UI 표시 명칭이 '블록 빌더' → '양식 디자이너'로 변경됨 (2026-02-28) - -**핵심 기능:** -- 결재선, 기본필드, 검사 기준서, 테이블 컬럼 정의 -- EAV 데이터 구조의 서식 스키마 관리 -- 양식 복제 (연결품목 제외) -- 프리셋 자동 제안 (카테고리별) -- 소프트 삭제 + 휴지통 관리 (슈퍼어드민) - ---- - -## 2. 라우트 - -### 2.1 웹 라우트 (페이지) - -``` -GET /document-templates → index (목록) -GET /document-templates/create → create (Legacy 신규 생성) -GET /document-templates/block-create → blockCreate (양식 디자이너 신규 생성) -GET /document-templates/{id}/edit → edit (Legacy 편집) -GET /document-templates/{id}/block-edit → blockEdit (양식 디자이너 편집) -``` - -### 2.2 API 라우트 (CRUD + 기능) - -``` -Prefix: /api/admin/document-templates (HQ 관리자 전용) - -GET / → index (HTMX 테이블) -POST / → store (생성) -GET /{id} → show (상세 조회) -PUT /{id} → update (수정) -DELETE /{id} → destroy (소프트 삭제) -DELETE /{id}/force → forceDestroy (영구삭제, 슈퍼어드민) -POST /{id}/restore → restore (복원, 슈퍼어드민) -POST /{id}/toggle-active → toggleActive (활성 토글) -POST /{id}/duplicate → duplicate (복제) -POST /upload-image → uploadImage (이미지 업로드) -GET /admin/common-codes/{group} → getCommonCodes (공통코드 조회) -``` - ---- - -## 3. 모델 구조 - -### 3.1 모델 관계도 - -``` -DocumentTemplate (서식 마스터) -├── 1:N DocumentTemplateApprovalLine (결재선) -├── 1:N DocumentTemplateBasicField (기본필드) -├── 1:N DocumentTemplateSection (섹션/기준서) -│ └── 1:N DocumentTemplateSectionItem (섹션 항목) -├── 1:N DocumentTemplateSectionField (섹션 필드) -├── 1:N DocumentTemplateColumn (테이블 컬럼) -└── 1:N DocumentTemplateLink (연결 설정) - └── 1:N DocumentTemplateLinkValue (연결 값) -``` - -### 3.2 DocumentTemplate 핵심 필드 - -```php -// 기본 정보 -builder_type // 'legacy' | 'block' -name // 양식명 -category // 분류명 -title // 문서 제목 - -// 회사 정보 -company_name // 회사명 -company_address // 회사 주소 -company_contact // 회사 연락처 - -// 하단 설정 -footer_remark_label // 비고 라벨 -footer_judgement_label // 판정 라벨 -footer_judgement_options // array - 판정 선택지 - -// Block Builder 전용 -schema // array - 블록 스키마 (JSON) -page_config // array - 페이지 설정 (A4/A3, 여백 등) - -// 연결 (레거시) -linked_item_ids // array - 연결 품목 ID 목록 -linked_process_id // int - 연결 공정 ID - -// 상태 -is_active // boolean - 활성 여부 -deleted_at // timestamp - 소프트 삭제 -deleted_by // int - 삭제자 -``` - -**Helper 메서드:** - -```php -isBlockBuilder(): bool // builder_type === 'block' -isLegacyBuilder(): bool // builder_type !== 'block' -``` - -### 3.3 DocumentTemplateApprovalLine (결재선) - -| 필드 | 타입 | 설명 | -|------|------|------| -| `template_id` | FK | 서식 ID | -| `name` | string | 결재자 이름/직책 | -| `department` | string | 부서 | -| `role` | string | 역할 (작성/검토/승인) | -| `sort_order` | int | 순서 | - -### 3.4 DocumentTemplateBasicField (기본필드) - -| 필드 | 타입 | 설명 | -|------|------|------| -| `template_id` | FK | 서식 ID | -| `field_key` | string | 필드 키 (bf_ 접두사) | -| `label` | string | 라벨 | -| `field_type` | string | text, date, select 등 | -| `default_value` | string | 기본값 | -| `is_required` | boolean | 필수 여부 | -| `sort_order` | int | 순서 | -| `options` | array | 선택지 (select 타입) | - -### 3.5 DocumentTemplateSection (섹션/검사 기준서) - -| 필드 | 타입 | 설명 | -|------|------|------| -| `template_id` | FK | 서식 ID | -| `title` | string | 섹션 제목 | -| `image_path` | string | 섹션 이미지 경로 | -| `sort_order` | int | 순서 | - -**하위 관계:** - -``` -Section 1:N SectionItem - ├── category // 카테고리 (그룹핑) - ├── name // 항목명 - ├── standard // 기준 - ├── tolerance_type // 공차 유형 (symmetric/asymmetric/range/limit) - ├── tolerance_plus // +공차 - ├── tolerance_minus // -공차 - ├── reference_value // 기준값 - ├── method // 검사방법 - ├── measurement_type // 측정유형 - └── frequency // 검사주기 -``` - -### 3.6 DocumentTemplateColumn (테이블 컬럼) - -| 필드 | 타입 | 설명 | -|------|------|------| -| `template_id` | FK | 서식 ID | -| `label` | string | 컬럼 라벨 | -| `group_name` | string | 그룹명 (다단계 "/" 구분) | -| `width` | int | 컬럼 너비 | -| `column_type` | string | text, check, complex, select, measurement | -| `sub_labels` | array | complex 타입 하위 라벨 | -| `sort_order` | int | 순서 | - -### 3.7 DocumentTemplateLink (연결 설정) - -| 필드 | 타입 | 설명 | -|------|------|------| -| `template_id` | FK | 서식 ID | -| `link_key` | string | 연결 키 | -| `label` | string | 라벨 | -| `link_type` | string | `single` / `multiple` | -| `source_table` | string | `items` / `processes` / `users` | -| `search_params` | array | 검색 파라미터 | -| `display_fields` | array | 표시 필드 | -| `is_required` | boolean | 필수 여부 | -| `sort_order` | int | 순서 | - -**하위 관계:** - -``` -Link 1:N LinkValue - ├── link_id // FK → Link - ├── linkable_id // 연결 엔티티 ID - └── (source_table에 따라 items/processes/users 참조) -``` - -**레거시 호환 처리:** - -```php -// 신규 links가 있으면 사용 -if ($template->links->isNotEmpty()) { - // template_links + link_values 사용 -} - -// 레거시만 있으면 가상 엔트리 생성 -if (!empty($template->linked_item_ids)) { - return [['link_key' => 'items', 'values' => [...]]] -} -``` - ---- - -## 4. 컨트롤러 상세 - -### 4.1 DocumentTemplateController (웹) - -| 메서드 | 동작 | -|--------|------| -| `index()` | HTMX 요청 → HX-Redirect 반환 (전체 페이지 로드 강제) | -| `create()` | Legacy 신규 생성 폼 렌더링 | -| `edit($id)` | Legacy 편집. 양식 디자이너 타입이면 `blockEdit`으로 자동 리다이렉트 | -| `blockCreate()` | 양식 디자이너 신규 생성 (빈 캔버스) | -| `blockEdit($id)` | 양식 디자이너 편집 (스키마 로드) | - -**공통 데이터 준비:** - -```php -// 현재 테넌트 조회 -$tenantId = getCurrentTenant(); // 세션의 selected_tenant_id - -// 카테고리 목록 = common_codes + 기존 템플릿 카테고리 -$categories = getCategories(); - -// 기본필드 키 옵션 -$basicFieldKeys = getBasicFieldKeys(); // common_codes 'doc_template_basic_field' -``` - -### 4.2 DocumentTemplateApiController (API) - -#### `index()` — HTMX 테이블 조회 - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `search` | string | 양식명/분류 검색 | -| `category` | string | 분류 필터 | -| `is_active` | string | `1` / `0` / `TRASHED` (휴지통) | - -```php -// 휴지통 모드 (슈퍼어드민 전용) -if ($isActive === 'TRASHED') { - $query->onlyTrashed(); -} -``` - -#### `store()` / `update()` — 생성/수정 - -``` -요청 데이터 - ↓ -검증 (직접 validate, FormRequest 미사용) - ↓ -연결품목 중복 검증 (checkLinkedItemDuplicates) - ↓ -DB::transaction 시작 - ↓ -Template 생성/수정 - ↓ -saveRelations() — 관계 데이터 upsert - ↓ -DB::transaction 완료 - ↓ -JSON 응답 -``` - -#### `duplicate()` — 양식 복제 - -```php -$source = DocumentTemplate::with([...all relationships...]); - -$newTemplate = DocumentTemplate::create([ - ...원본 데이터, - 'name' => request('name', '원본 (복사)'), - 'is_active' => false, // 비활성으로 생성 - 'linked_item_ids' => null, // 연결품목 제외 - 'linked_process_id' => null, // 연결공정 제외 -]); - -// 각 관계 데이터 복사 (approvalLines, basicFields, sections, columns...) -// linkValues는 복사 안 함 (동일 분류 내 중복 방지) -``` - -#### `forceDestroy()` — 영구삭제 - -```php -// 사전 검사: 참조하는 문서 존재 여부 -$documentCount = Document::withTrashed() - ->where('template_id', $id) - ->count(); - -if ($documentCount > 0) { - return 422; // "이 양식을 사용한 문서 {count}건이 있어 삭제 불가" -} -``` - -#### `uploadImage()` — 이미지 업로드 - -``` -요청 (multipart) - ↓ -ApiTokenService::exchangeToken($userId, $tenantId) - ↓ -API /files/upload 호출 (Bearer 토큰) - ↓ -응답: file_path (1/temp/2026/02/xxx.jpg) - ↓ -최종 URL: http://api.sam.kr/storage/tenants/{file_path} -``` - ---- - -## 5. 저장 메커니즘 (saveRelations) - -### 5.1 upsert 전략 - -| 관계 | 방식 | 이유 | -|------|------|------| -| approvalLines | 전체 삭제 → 재생성 | ID 참조 없음 | -| basicFields | 전체 삭제 → 재생성 | ID 참조 없음 | -| **sections** | **ID 보존 upsert** | document_data가 section_id 참조 | -| **sectionItems** | **ID 보존 upsert** | section 하위 항목 | -| **columns** | **ID 보존 upsert** | document_data가 column_id 참조 | -| sectionFields | 전체 삭제 → 재생성 | ID 참조 없음 | -| links + linkValues | 전체 삭제 → 재생성 | ID 참조 없음 | - -### 5.2 ID 보존 upsert 로직 - -```php -// 1. 요청 ID 수집 -$incomingIds = collect($data['sections'])->pluck('id')->filter(); - -// 2. 요청에 없는 항목 삭제 -$template->sections() - ->whereNotIn('id', $incomingIds) - ->each(function($s) { - $s->items()->delete(); - $s->delete(); - }); - -// 3. 각 항목 upsert -foreach ($data['sections'] as $section) { - if (!empty($section['id']) && $existing = $template->sections()->find($section['id'])) { - $existing->update($sectionData); // 기존: update - } else { - DocumentTemplateSection::create([...]); // 신규: create - } -} -``` - -> **ID 보존이 필수인 이유**: `document_data` 테이블이 `section_id`, `column_id`를 FK로 참조한다. 양식 수정 시 ID가 변경되면 기존 문서 데이터와의 매핑이 깨진다. - ---- - -## 6. 화면 구성 - -### 6.1 목록 화면 (`index.blade.php`) - -``` -┌─────────────────────────────────────────────────┐ -│ 문서양식관리 │ -│ [+ 새 양식] [+ 양식 디자이너] │ -├─────────────────────────────────────────────────┤ -│ 필터: [검색어] [분류 ▼] [활성/비활성/휴지통 ▼] │ -├─────────────────────────────────────────────────┤ -│ # │ 양식명 │ 분류 │ 활성 │ 수정일 │ 액션 │ -│ 1 │ FQC... │ 검사 │ ✅ │ 03-06 │ 편집 복제 삭제 │ -│ 2 │ 수입... │ 검사 │ ✅ │ 03-05 │ 편집 복제 삭제 │ -│ ...│ │ │ │ │ │ -└─────────────────────────────────────────────────┘ -``` - -**HTMX 테이블 로드:** - -```html -

-
-``` - -**액션 버튼:** -- **편집**: 새 양식 → `/document-templates/{id}/edit`, 양식 디자이너 → `/document-templates/{id}/block-edit` -- **복제**: `duplicateTemplate(id)` — 이름 입력 모달 후 POST -- **삭제**: `confirmDelete(id)` — 확인 후 DELETE -- **미리보기**: `previewTemplate(id)` — 모달 표시 -- **활성 토글**: `toggleActive(id)` — POST toggle-active -- **복원/영구삭제**: 휴지통 모드에서만 표시 (슈퍼어드민) - -### 6.2 Legacy Builder 편집 화면 (`edit.blade.php`) - -**4개 탭 구조:** - -``` -┌─────────────────────────────────────────────────────┐ -│ [기본정보] [기본필드] [검사 기준서] [테이블 컬럼] │ -├─────────────────────────────────────────────────────┤ -│ │ -│ (각 탭 콘텐츠) │ -│ │ -├─────────────────────────────────────────────────────┤ -│ [미리보기] [저장] [취소] │ -└─────────────────────────────────────────────────────┘ -``` - -#### 탭 1: 기본정보 - -| 필드 | 설명 | -|------|------| -| 양식명 | 서식 이름 (필수) | -| 제목 | 문서 제목 | -| 분류 | 카테고리 (common_codes + 기존값) | -| 회사명 | 문서 헤더 회사명 | -| 회사 주소/연락처 | 문서 헤더 | -| 활성 | 체크박스 | -| 결재선 | 동적 행 추가/삭제 (이름, 부서, 역할) | - -#### 탭 2: 기본필드 - -| 항목 | 설명 | -|------|------| -| 필드 키 | `bf_` 접두사 (common_codes에서 선택) | -| 라벨 | 표시 라벨 | -| 필드 타입 | text, date, select 등 | -| 기본값 | 문서 생성 시 자동 입력 | -| 필수 여부 | 체크박스 | - -#### 탭 3: 검사 기준서 - -``` -┌──────────────────────────────────────────────────┐ -│ 섹션 1: [제목 입력] [이미지 업로드] [+ 항목 추가] │ -│ ┌──────────────────────────────────────────────┐ │ -│ │ 카테고리 │ 항목 │ 기준 │ 공차 │ 기준값 │ ... │ │ -│ │ 외관 │ 색상 │ 기준 │ ±0.5 │ 5.0 │ ... │ │ -│ │ 외관 │ 흠집 │ 무 │ │ │ ... │ │ -│ └──────────────────────────────────────────────┘ │ -│ │ -│ 섹션 2: [제목 입력] [이미지 업로드] [+ 항목 추가] │ -│ ... │ -│ [+ 섹션 추가] │ -└──────────────────────────────────────────────────┘ -``` - -**공차 유형:** - -| 유형 | 입력 | 표시 예 | -|------|------|--------| -| `symmetric` | ± 값 | ±0.5 | -| `asymmetric` | +값, -값 | +0.3 / -0.2 | -| `range` | 최소~최대 | 4.5 ~ 5.5 | -| `limit` | 상한 또는 하한 | ≤ 10 | - -#### 탭 4: 테이블 컬럼 - -| 항목 | 설명 | -|------|------| -| 라벨 | 컬럼 헤더 | -| 그룹명 | 다단계 그룹 ("/" 구분) | -| 너비 | 컬럼 너비 (px 또는 %) | -| 컬럼 타입 | text, check, complex, select, measurement | -| 하위 라벨 | complex 타입 시 sub_labels | - -**자동 컬럼 생성:** - -``` -[기준서에서 자동 생성] 버튼 클릭 - ↓ -검사 기준서 섹션의 항목들을 분석 - ↓ -카테고리 그룹별 컬럼 자동 생성 - ↓ -measurement_type에 따라 컬럼 타입 결정 -``` - -### 6.3 양식 디자이너 편집 화면 (`block-editor.blade.php`) - -**3패널 레이아웃:** - -``` -┌──────────┬──────────────────────────┬───────────┐ -│ 팔레트 │ 캔버스 │ 속성 패널 │ -│ (220px) │ (flex: 1) │ (300px) │ -│ │ │ │ -│ 기본: │ ┌──────────────────────┐ │ 선택 블록: │ -│ □ 제목 │ │ [제목 블록] │ │ │ -│ □ 문단 │ │ [문단 블록] │ │ 제목: ... │ -│ □ 테이블 │ │ [테이블 블록] │ │ 크기: ... │ -│ □ 컬럼 │ │ [입력 필드 블록] │ │ 정렬: ... │ -│ □ 구분선 │ │ │ │ │ -│ □ 여백 │ └──────────────────────┘ │ │ -│ │ │ │ -│ 폼: │ │ │ -│ □ 텍스트 │ │ │ -│ □ 숫자 │ │ │ -│ □ 날짜 │ │ │ -│ □ 선택 │ │ │ -│ □ 체크 │ │ │ -│ □ 텍스트영역│ │ │ -│ □ 서명 │ │ │ -└──────────┴──────────────────────────┴───────────┘ -``` - -**블록 타입 (15개):** - -| 분류 | 타입 | 설명 | -|------|------|------| -| 기본 | `heading` | 제목 (h1~h6) | -| 기본 | `paragraph` | 문단 텍스트 | -| 기본 | `table` | 테이블 (행/열 편집) | -| 기본 | `columns` | 다단 컬럼 레이아웃 | -| 기본 | `divider` | 구분선 | -| 기본 | `spacer` | 여백 | -| 폼 | `text_field` | 텍스트 입력 | -| 폼 | `number_field` | 숫자 입력 | -| 폼 | `date_field` | 날짜 입력 | -| 폼 | `select_field` | 선택 드롭다운 | -| 폼 | `checkbox_field` | 체크박스 | -| 폼 | `textarea_field` | 긴 텍스트 입력 | -| 폼 | `signature_field` | 서명 영역 | - -**Alpine.js 상태 관리:** - -```javascript -blockEditor(initialSchema, templateId) { - blocks: [], // 블록 배열 - selectedBlockId: null, // 현재 선택 블록 - history: [], // Undo/Redo 스택 (최대 50) - historyIndex: -1, - pageConfig: { // 페이지 설정 - size: 'A4', // A4 / A3 - orientation: 'portrait', // portrait / landscape - margins: { top, right, bottom, left } - }, - templateName: '', - category: '' -} -``` - -**키보드 단축키:** - -| 단축키 | 기능 | -|--------|------| -| `Ctrl+Z` / `Cmd+Z` | Undo | -| `Ctrl+Shift+Z` / `Cmd+Shift+Z` | Redo | -| `Ctrl+S` / `Cmd+S` | 저장 | - -**SortableJS:** -- 캔버스 내 블록 드래그-앤-드롭 정렬 -- 팔레트에서 캔버스로 블록 추가 - ---- - -## 7. 미리보기 시스템 - -### 7.1 Legacy Builder 미리보기 - -```javascript -buildDocumentPreviewHtml(data) -├── 결재란 테이블 (역할별 칸) -├── 기본필드 (2열 15:35:15:35 비율) -├── 섹션별 이미지 (title + image 또는 placeholder) -├── 검사 데이터 테이블 -│ ├── 다단계 그룹 헤더 (group_name "/" 구분) -│ ├── sub_labels (complex 컬럼) -│ ├── 항목 행 (카테고리 그룹핑) -│ └── 측정치 셀 (measurement_type별 렌더) -└── 비고/종합판정 섹션 -``` - -### 7.2 양식 디자이너 미리보기 - -```javascript -buildBlockPreviewHtml(data) -├── 블록 타입별 HTML 렌더링 -├── 폼 필드 placeholder 표시 -└── A4/A3 레이아웃 시뮬레이션 -``` - -### 7.3 이미지 URL 처리 - -```javascript -_previewImageUrl(imagePath) -├── http(s):// 시작 → 그대로 사용 -├── /^\d+\// 패턴 → API tenant storage URL 생성 -│ → http://api.sam.kr/storage/tenants/{imagePath} -└── 기타 → MNG local storage (/storage/{imagePath}) -``` - ---- - -## 8. 분류(Category) 관리 - -### 8.1 소스 (우선순위) - -1. **common_codes** (code_group = `document_category`, is_active = true) - - tenant_id가 있는 것 우선 (테넌트 전용) - - tenant_id가 null인 것도 포함 (공통) - - code 기준 중복 제거 (테넌트 우선) -2. **기존 템플릿의 category** (common_codes에 없는 값) - - 기존 이름 그대로 추가 - -### 8.2 연동 공통코드 그룹 - -| 그룹 | 용도 | -|------|------| -| `document_category` | 문서 분류 | -| `doc_template_basic_field` | 기본필드 키 옵션 | -| `doc_inspection_method` | 검사방법 | -| `doc_measurement_type` | 측정유형 | - ---- - -## 9. 프리셋 시스템 - -### 9.1 테이블 - -``` -document_template_field_presets -├── name // 프리셋 이름 -├── category // 대상 카테고리 -├── description // 설명 -└── field_definitions // array - 필드 정의 목록 - [{ field_key, label, field_type, options, ... }] -``` - -### 9.2 동작 - -``` -분류(Category) 변경 - ↓ -매칭 프리셋 검색 - ↓ -기존 section_fields가 비어있으면 - ↓ -"'{category}' 카테고리에 맞는 프리셋을 적용할까요?" 확인 - ↓ -승인 시 field_definitions 자동 적용 -``` - -> **주의**: 초기 로드 시에는 제안하지 않음. 분류 변경 시에만 제안. - ---- - -## 10. 연결품목 중복 검증 - -### 10.1 규칙 - -같은 category 내 서로 다른 템플릿이 동일한 items를 연결할 수 없다. - -### 10.2 검증 로직 - -```php -checkLinkedItemDuplicates($templateId, $category, $itemIds) - -// 1. 같은 category의 다른 템플릿 조회 -$otherTemplates = DocumentTemplate::where('category', $category) - ->where('id', '!=', $templateId) - ->get(); - -// 2. 각 템플릿의 연결품목 수집 -foreach ($otherTemplates as $other) { - // 레거시: linked_item_ids (JSON 배열) - // 신규: template_links → linkValues (source_table = 'items') - $existingItemIds = ...; -} - -// 3. 교집합 검사 -$duplicates = array_intersect($itemIds, $existingItemIds); -if (!empty($duplicates)) { - return 422; // 중복 항목 목록과 함께 오류 반환 -} -``` - ---- - -## 11. JavaScript 상태 관리 (Legacy Builder) - -### 11.1 templateState 객체 - -```javascript -const templateState = { - // 기본정보 - id, name, category, title, - company_name, company_address, company_contact, - footer_remark_label, footer_judgement_label, - footer_judgement_options, - is_active, - - // 관계 데이터 - approval_lines: [], // 결재선 - basic_fields: [], // 기본필드 - sections: [], // 섹션 + items - columns: [], // 테이블 컬럼 - section_fields: [], // 섹션 필드 - template_links: [], // 연결 설정 + values -}; -``` - -### 11.2 저장 흐름 - -``` -사용자 입력 (Blade 폼) - ↓ -templateState 객체 갱신 - ↓ -saveTemplate() 호출 - ↓ -fetch POST/PUT /api/admin/document-templates - ↓ -DocumentTemplateApiController::store/update() - ↓ -검증 → 중복 검사 → DB 트랜잭션 → saveRelations() - ↓ -JSON 응답 - ↓ -showToast() 메시지 - ↓ -htmx.trigger('#template-table', 'filterSubmit') → 테이블 새로고침 -``` - ---- - -## 12. 양식 디자이너(Block Builder) vs 새 양식(Legacy Builder) 비교 - -| 항목 | 양식 디자이너 | 새 양식 | -|------|:------------:|:-------------:| -| builder_type | `block` | `legacy` 또는 null | -| 편집 UI | WYSIWYG 캔버스 (Alpine.js) | 탭 폼 (순수 JavaScript) | -| 데이터 저장 | `schema` JSON 컬럼 | 관계 테이블 (7개) | -| Undo/Redo | 히스토리 스택 (최대 50) | 불가 | -| 블록 타입 | 15개 (기본 6 + 폼 7 + 기타 2) | N/A | -| 드래그-앤-드롭 | SortableJS | 불가 | -| 페이지 설정 | A4/A3, 여백, 방향 | 없음 | -| 복제 | 스키마 JSON 복사 | 각 관계 데이터 개별 복사 | -| 미리보기 함수 | `buildBlockPreviewHtml()` | `buildDocumentPreviewHtml()` | -| 적합 용도 | 자유 레이아웃 문서 | 정형화된 검사 성적서 | - ---- - -## 13. 권한 및 보안 - -### 13.1 미들웨어 - -- **웹 라우트**: 일반 인증 (auth) -- **API 라우트**: HQ 관리자 미들웨어 (`admin` prefix) - -### 13.2 슈퍼어드민 전용 기능 - -| 기능 | 엔드포인트 | -|------|-----------| -| 영구삭제 | `DELETE /{id}/force` | -| 복원 | `POST /{id}/restore` | -| 휴지통 조회 | `GET /?is_active=TRASHED` | - -### 13.3 삭제 보호 - -- 소프트 삭제: `deleted_at` + `deleted_by` 기록 -- 영구삭제 전 참조 문서 검사 (Document 테이블) -- 참조 문서가 있으면 영구삭제 불가 (422 응답) - ---- - -## 14. API 프로젝트 연동 - -### 14.1 API 서비스 - -```php -// DocumentTemplateService (API) -list(array $params): LengthAwarePaginator - // 필터: is_active, category, search - -show(int $id): DocumentTemplate - // 전체 관계 로드 (approvalLines, basicFields, sections, columns...) -``` - -### 14.2 API 엔드포인트 - -``` -GET /v1/document-templates → index (목록) -GET /v1/document-templates/{id} → show (상세) -``` - -> API는 **읽기 전용**. 서식 생성/수정은 MNG에서만 수행. - ---- - -## 15. 주요 파일 경로 - -| 기능 | 경로 | -|------|------| -| 웹 컨트롤러 | `mng/app/Http/Controllers/DocumentTemplateController.php` | -| API 컨트롤러 | `mng/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php` | -| 모델 (8개) | `mng/app/Models/DocumentTemplate*.php` | -| 뷰 - 목록 | `mng/resources/views/document-templates/index.blade.php` | -| 뷰 - Legacy 편집 | `mng/resources/views/document-templates/edit.blade.php` | -| 뷰 - 양식 디자이너 | `mng/resources/views/document-templates/block-editor.blade.php` | -| 뷰 - 테이블 | `mng/resources/views/document-templates/partials/table.blade.php` | -| 뷰 - 미리보기 | `mng/resources/views/document-templates/partials/preview-modal.blade.php` | -| API 서비스 | `api/app/Services/DocumentTemplateService.php` | -| API 모델 | `api/app/Models/Documents/DocumentTemplate*.php` | - ---- - -## 관련 문서 - -- [README.md](README.md) — 문서관리 시스템 개요 (API 중심) -- [MNG 문서관리](mng-document-system.md) — 문서 생성/편집/결재 (서식을 사용하는 측) -- [DB 스키마 — 문서](../../system/database/documents.md) - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/planning/README.md b/sam/docs/features/planning/README.md deleted file mode 100644 index ef681d0..0000000 --- a/sam/docs/features/planning/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# 주일기업 기획 메뉴 - -> **작성일**: 2026-03-06 -> **상태**: 운영 중 -> **프로젝트**: SAM MNG (관리자 웹) -> **라우트 접두사**: `/juil` - ---- - -## 1. 개요 - -### 1.1 목적 - -블라인드/스크린 제조업체의 현장 관리를 위한 기획 도구 모음. 견적부터 공사, 준공까지의 업무 흐름과 현장 기록(사진대지), 회의 기록(STT/AI 요약)을 제공한다. - -### 1.2 문서 구조 - -| 문서 | 설명 | -|------|------| -| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 아키텍처 | -| [construction-photos.md](construction-photos.md) | 공사현장 사진대지 기술 명세 | -| [meeting-minutes.md](meeting-minutes.md) | 회의록 작성 기술 명세 (STT/AI 통합) | -| [planning-views.md](planning-views.md) | 견적/프로젝트/워크플로우 화면 명세 | - -### 1.3 하위 메뉴 구조 - -``` -주일기업 기획 -├── 견적/입찰/공사관리 /juil/estimate -├── 프로젝트관리/기성청구 /juil/project -├── 업무 Workflow /juil/workflow -├── 공사현장 사진대지 /juil/construction-photos -└── 회의록 작성 /juil/meeting-minutes -``` - ---- - -## 2. 아키텍처 - -### 2.1 기술 스택 - -| 계층 | 기술 | 설명 | -|------|------|------| -| 뷰 | Blade + React (인라인) + Babel | 브라우저 트랜스파일 React 컴포넌트 | -| API | Laravel Controller + Service | JSON API (AJAX) | -| 모델 | Eloquent ORM | Multi-tenant (BelongsToTenant) | -| 파일 저장 | Google Cloud Storage | 사진, 오디오 파일 | -| AI | Gemini API (Vertex AI) | 요약, 화자 분리 | -| STT | Google Speech-to-Text V1/V2 + Web Speech API | 음성 인식 | - -### 2.2 프로젝트 파일 구조 - -``` -mng/ -├── app/Http/Controllers/ -│ ├── PlanningController.php ← 견적/프로젝트/워크플로우 -│ ├── ConstructionSitePhotoController.php ← 사진대지 CRUD + 파일 관리 -│ └── MeetingMinuteController.php ← 회의록 CRUD + AI 기능 -├── app/Services/ -│ ├── ConstructionSitePhotoService.php ← 사진대지 비즈니스 로직 -│ └── MeetingMinuteService.php ← 회의록 + AI 통합 로직 -├── app/Models/ -│ ├── ConstructionSitePhoto.php ← 사진대지 모델 -│ ├── ConstructionSitePhotoRow.php ← 사진 행 모델 -│ ├── MeetingMinute.php ← 회의록 모델 -│ └── MeetingMinuteSegment.php ← 회의 세그먼트 모델 -└── resources/views/juil/ - ├── estimate.blade.php ← 견적/입찰/공사관리 - ├── project.blade.php ← 프로젝트관리/기성청구 - ├── workflow.blade.php ← 업무 Workflow - ├── construction-photos.blade.php ← 사진대지 SPA - └── meeting-minutes.blade.php ← 회의록 SPA -``` - -### 2.3 기능별 구현 현황 - -| 기능 | 구현 방식 | 백엔드 | DB | -|------|----------|--------|-----| -| 견적/입찰/공사관리 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 | -| 프로젝트관리/기성청구 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 | -| 업무 Workflow | React 뷰 (정적 데이터) | PlanningController (뷰 반환만) | 없음 | -| 공사현장 사진대지 | React SPA + API | Controller + Service | 2 테이블 | -| 회의록 작성 | React SPA + API | Controller + Service + AI | 2 테이블 | - ---- - -## 3. 외부 서비스 의존성 - -| 서비스 | 용도 | 추적 | -|--------|------|------| -| **Google Cloud Storage** | 사진/오디오 파일 저장 | `AiTokenHelper::saveGcsStorageUsage()` | -| **Google Speech-to-Text V2 (Chirp2)** | 자동 화자 분리 (최우선) | `AiTokenHelper::saveSttUsage()` | -| **Google Speech-to-Text V1** | 화자 분리 (V2 실패 시 폴백) | `AiTokenHelper::saveSttUsage()` | -| **Gemini API (Vertex AI)** | 요약 생성 + 화자 재분배 | `AiTokenHelper::saveGeminiUsage()` | -| **Web Speech API** | 브라우저 음성 입력 (현장명/설명) | `logSttUsage()` | - -### 3.1 도메인 용어 힌트 (STT 정확도 향상) - -``` -블라인드, 스크린, 롤스크린, 허니콤, 버티컬, -원단, 바텀레일, 헤드레일, 브라켓, -주일, 경동, 주일블라인드, 경동블라인드, -수주, 발주, 납기, 출하, 재고, 원가, 단가, -SAM, ERP, MES -``` - ---- - -## 4. HTMX 전체 페이지 로드 규칙 - -모든 `/juil/*` 페이지는 React 인라인 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 컨트롤러 메서드에서 HTMX 요청 감지 시 **HX-Redirect로 전체 페이지 리로드를 강제**한다. - -```php -if ($request->header('HX-Request')) { - return response('', 200)->header('HX-Redirect', route('juil.estimate')); -} -``` - ---- - -## 5. 관련 문서 - -- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 행 구조, 음성 입력 -- [회의록 작성](meeting-minutes.md) — STT/화자분리/AI 요약, 오디오 녹음 -- [견적/프로젝트/워크플로우](planning-views.md) — React 뷰 구성, 업무 프로세스 정의 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/planning/construction-photos.md b/sam/docs/features/planning/construction-photos.md deleted file mode 100644 index 0a85ffb..0000000 --- a/sam/docs/features/planning/construction-photos.md +++ /dev/null @@ -1,275 +0,0 @@ -# 공사현장 사진대지 - -> **작성일**: 2026-03-06 -> **상태**: 운영 중 -> **라우트**: `/juil/construction-photos` -> **관련**: [README.md](README.md) | [회의록](meeting-minutes.md) | [뷰 화면](planning-views.md) - ---- - -## 1. 개요 - -건설/시공 현장의 작업 과정을 **작업전/작업중/작업후** 3단계 사진으로 기록하고 관리하는 기능. Google Cloud Storage에 사진을 저장하며, 음성 입력(Web Speech API)으로 현장명과 설명을 입력할 수 있다. - ---- - -## 2. 라우트 - -``` -/juil/construction-photos -├── GET / → index (목록 페이지) -├── GET /list → list (JSON 목록) -├── POST / → store (새 사진대지 등록) -├── POST /log-stt-usage → logSttUsage (STT 시간 기록) -├── GET /{id} → show (상세 조회) -├── PUT /{id} → update (메타데이터 수정) -├── DELETE /{id} → destroy (삭제) -├── POST /{id}/rows → addRow (행 추가) -├── DELETE /{id}/rows/{rowId} → deleteRow (행 삭제) -├── POST /{id}/rows/{rowId}/upload → uploadPhoto (사진 업로드) -├── DELETE /{id}/rows/{rowId}/photo/{type} → deletePhoto (사진 삭제) -└── GET /{id}/rows/{rowId}/download/{type} → downloadPhoto (다운로드) -``` - ---- - -## 3. 데이터베이스 - -### 3.1 construction_site_photos (사진대지) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `tenant_id` | BIGINT FK | 테넌트 격리 | -| `user_id` | BIGINT FK | 등록자 | -| `site_name` | VARCHAR(200) | 현장명 (필수) | -| `work_date` | DATE | 작업일자 (필수) | -| `description` | TEXT NULL | 설명 | -| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | - -**인덱스**: `tenant_id`, `user_id`, `(tenant_id, work_date)` - -### 3.2 construction_site_photo_rows (사진 행) - -각 사진대지는 1개 이상의 행을 가지며, 각 행에 3개 타입(before/during/after) 사진 저장. - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `construction_site_photo_id` | BIGINT FK | 부모 (cascade delete) | -| `sort_order` | INT | 정렬 순서 (0부터) | -| `before_photo_path` | VARCHAR(500) NULL | 작업전 GCS 경로 | -| `before_photo_gcs_uri` | VARCHAR(500) NULL | 작업전 GCS URI | -| `before_photo_size` | INT UNSIGNED NULL | 작업전 파일크기 (bytes) | -| `during_photo_path` | VARCHAR(500) NULL | 작업중 GCS 경로 | -| `during_photo_gcs_uri` | VARCHAR(500) NULL | 작업중 GCS URI | -| `during_photo_size` | INT UNSIGNED NULL | 작업중 파일크기 (bytes) | -| `after_photo_path` | VARCHAR(500) NULL | 작업후 GCS 경로 | -| `after_photo_gcs_uri` | VARCHAR(500) NULL | 작업후 GCS URI | -| `after_photo_size` | INT UNSIGNED NULL | 작업후 파일크기 (bytes) | - -### 3.3 테이블 관계 - -``` -construction_site_photos - │ 1:N - ▼ -construction_site_photo_rows (sort_order ASC) - ├── before_photo_* (작업전) - ├── during_photo_* (작업중) - └── after_photo_* (작업후) -``` - ---- - -## 4. API 명세 - -### 4.1 목록 조회 - -``` -GET /juil/construction-photos/list -``` - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `search` | string | 현장명 검색 | -| `date_from` | date | 시작일 | -| `date_to` | date | 종료일 | -| `per_page` | int | 페이지당 건수 | - -### 4.2 생성 - -``` -POST /juil/construction-photos -``` - -| 필드 | 규칙 | 설명 | -|------|------|------| -| `site_name` | required, max:200 | 현장명 | -| `work_date` | required, date | 작업일자 | -| `description` | nullable, max:2000 | 설명 | - -> 생성 시 빈 행 1개 자동 추가 - -### 4.3 사진 업로드 - -``` -POST /juil/construction-photos/{id}/rows/{rowId}/upload -``` - -| 필드 | 규칙 | 설명 | -|------|------|------| -| `type` | required, in:before,during,after | 사진 타입 | -| `photo` | required, image, mimes:jpeg,jpg,png,webp, max:10240 | 최대 10MB | - -### 4.4 사진 다운로드 - -``` -GET /juil/construction-photos/{id}/rows/{rowId}/download/{type}?inline=1 -``` - -| 파라미터 | 설명 | -|---------|------| -| `inline=1` | 브라우저 표시 (미지정 시 다운로드) | - ---- - -## 5. GCS 저장 구조 - -### 5.1 경로 패턴 - -``` -construction-site-photos/{tenant_id}/{photo_id}/{row_id}_{timestamp}_{type}.{ext} -``` - -**예시:** - -``` -construction-site-photos/1/42/15_1709723456_before.jpg -construction-site-photos/1/42/15_1709723456_during.jpg -construction-site-photos/1/42/15_1709723456_after.png -``` - -### 5.2 업로드 흐름 - -``` -클라이언트 (Canvas 이미지 압축: 1920px, quality 80%) - ↓ -FormData (multipart) 전송 - ↓ -컨트롤러: uploadPhoto() - ↓ -서비스: uploadPhoto() - ├── 기존 사진 있으면 GCS에서 삭제 - ├── GCS에 업로드 - ├── DB에 path + uri + size 저장 - └── AiTokenHelper::saveGcsStorageUsage() 호출 - ↓ -응답: { success, data: Photo with rows } -``` - -### 5.3 삭제 흐름 - -``` -사진 삭제: GCS 파일 삭제 → DB 필드 null -행 삭제: 행 내 모든 사진 GCS 삭제 → 행 삭제 → sort_order 재정렬 -사진대지 삭제: 모든 행의 모든 사진 GCS 삭제 → soft delete -``` - ---- - -## 6. 음성 입력 (Web Speech API) - -### 6.1 VoiceInputButton 컴포넌트 - -현장명, 설명 필드에 음성으로 텍스트 입력 가능. - -```javascript -// Web Speech Recognition 설정 -recognition.lang = 'ko-KR'; -recognition.continuous = true; -recognition.interimResults = true; -recognition.maxAlternatives = 1; -``` - -### 6.2 인식 상태 - -| 상태 | 표시 | 설명 | -|------|------|------| -| interim (미확정) | 이탤릭 + 회색 | 인식 중간 결과, 2초 후 소실 | -| final (확정) | 일반체 + 진한색 | 확정 텍스트, 영구 저장 | - -### 6.3 사용량 추적 - -``` -STT 사용 종료 시: -duration = Math.max(1, (Date.now() - startTime) / 1000) - ↓ -POST /juil/construction-photos/log-stt-usage - body: { duration_seconds } - ↓ -AiTokenHelper::saveSttUsage('공사현장사진대지-음성입력', seconds) -``` - ---- - -## 7. UI 구성 (React) - -### 7.1 사진 타입별 색상 - -| 타입 | 라벨 | 배경색 | 뱃지색 | -|------|------|--------|--------| -| `before` | 작업전 | `bg-blue-50` | `bg-blue-100 text-blue-800` | -| `during` | 작업중 | `bg-yellow-50` | `bg-yellow-100 text-yellow-800` | -| `after` | 작업후 | `bg-green-50` | `bg-green-100 text-green-800` | - -### 7.2 행 관리 - -- **행 추가**: sort_order 자동 계산 (마지막 + 1) -- **행 삭제**: 최소 1개 행 유지 필수 -- **행별 사진**: 각 행에 3개 타입 사진 독립 업로드/삭제 - ---- - -## 8. 모델 메서드 - -### 8.1 ConstructionSitePhoto - -```php -user() # BelongsTo User (등록자) -rows() # HasMany Row (sort_order ASC) -getPhotoCount(): int # 전체 사진 개수 (모든 행의 사진 합계) -``` - -### 8.2 ConstructionSitePhotoRow - -```php -constructionSitePhoto() # BelongsTo 부모 -hasPhoto(string $type): bool # 특정 타입 사진 존재 여부 -getPhotoCount(): int # 이 행의 사진 개수 (0~3) -``` - -### 8.3 ConstructionSitePhotoService - -```php -getList(array $filters) # 검색/필터 목록 (페이지네이션) -create(array $data) # 생성 + 빈 행 1개 자동 추가 -update(ConstructionSitePhoto, array $data) # 메타데이터만 수정 -delete(ConstructionSitePhoto) # GCS 전체 삭제 → soft delete -uploadPhoto(Row, UploadedFile, string $type) # GCS 업로드 + DB 기록 -deletePhotoByType(Row, string $type) # 특정 타입 GCS 삭제 -addRow(ConstructionSitePhoto) # 행 추가 (sort_order 자동) -deleteRow(Row) # 행 내 GCS 삭제 → 행 삭제 → 재정렬 -``` - ---- - -## 관련 문서 - -- [README.md](README.md) — 기획 메뉴 전체 개요 -- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록 -- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/planning/meeting-minutes.md b/sam/docs/features/planning/meeting-minutes.md deleted file mode 100644 index 09d089a..0000000 --- a/sam/docs/features/planning/meeting-minutes.md +++ /dev/null @@ -1,456 +0,0 @@ -# 회의록 작성 - -> **작성일**: 2026-03-06 -> **상태**: 운영 중 -> **라우트**: `/juil/meeting-minutes` -> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [뷰 화면](planning-views.md) - ---- - -## 1. 개요 - -음성으로 회의 내용을 기록하고, **Google STT(화자 분리)** + **Gemini AI(요약/결정사항/액션아이템)** 로 자동 정리하는 회의록 시스템. 브라우저 MediaRecorder로 녹음하고, GCS에 오디오를 저장하며, 세그먼트(화자별 발화)를 관리한다. - ---- - -## 2. 라우트 - -``` -/juil/meeting-minutes -├── GET / → index (목록 페이지) -├── GET /list → list (JSON 목록) -├── POST / → store (새 회의록 생성) -├── POST /log-stt-usage → logSttUsage (STT 시간 기록) -├── GET /{id} → show (상세 조회 + segments) -├── PUT /{id} → update (메타데이터 수정) -├── DELETE /{id} → destroy (삭제) -├── POST /{id}/segments → saveSegments (세그먼트 저장) -├── POST /{id}/upload-audio → uploadAudio (오디오 업로드) -├── POST /{id}/summarize → summarize (AI 요약 생성) -├── POST /{id}/diarize → diarize (자동 화자 분리) -└── GET /{id}/download-audio → downloadAudio (오디오 다운로드) -``` - ---- - -## 3. 데이터베이스 - -### 3.1 meeting_minutes (회의록) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `tenant_id` | BIGINT FK | 테넌트 격리 | -| `user_id` | BIGINT FK | 작성자 | -| `title` | VARCHAR(300) | 제목 (기본: "무제 회의록") | -| `folder` | VARCHAR(100) NULL | 폴더 분류 | -| `participants` | JSON NULL | 참여자 목록 배열 | -| `meeting_date` | DATE | 회의 날짜 | -| `meeting_time` | TIME NULL | 회의 시작 시간 | -| `duration_seconds` | INT UNSIGNED | 녹음 총 시간(초) | -| `audio_file_path` | VARCHAR(500) NULL | 오디오 GCS 경로 | -| `audio_gcs_uri` | VARCHAR(500) NULL | 오디오 GCS URI | -| `audio_file_size` | BIGINT UNSIGNED NULL | 오디오 파일 크기 (bytes) | -| `full_transcript` | LONGTEXT NULL | 전체 트랜스크립트 | -| `summary` | LONGTEXT NULL | AI 요약 | -| `decisions` | JSON NULL | 결정사항 배열 | -| `action_items` | JSON NULL | 액션아이템 배열 | -| `status` | VARCHAR(20) | 상태 (5가지) | -| `stt_language` | VARCHAR(10) | STT 언어 (기본: ko-KR) | -| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | - -**인덱스**: `tenant_id`, `user_id`, `(tenant_id, meeting_date)`, `status` - -### 3.2 meeting_minute_segments (세그먼트) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | BIGINT PK | | -| `meeting_minute_id` | BIGINT FK | 회의록 (cascade delete) | -| `segment_order` | INT UNSIGNED | 순서 | -| `speaker_name` | VARCHAR(100) | 화자 이름 (기본: "화자 1") | -| `speaker_label` | VARCHAR(20) NULL | 화자 라벨/번호 | -| `text` | TEXT | 발화 텍스트 | -| `start_time_ms` | INT UNSIGNED | 시작 시간 (ms, 기본: 0) | -| `end_time_ms` | INT UNSIGNED NULL | 종료 시간 (ms) | -| `is_manual_speaker` | BOOLEAN | 수동 화자 전환 여부 (기본: true) | - -**인덱스**: `meeting_minute_id`, `(meeting_minute_id, segment_order)` - -### 3.3 테이블 관계 - -``` -meeting_minutes - │ 1:N - ▼ -meeting_minute_segments (segment_order ASC) - ├── speaker_name (화자명) - ├── text (발화 내용) - └── start_time_ms / end_time_ms (타임스탬프) -``` - ---- - -## 4. 상태 관리 - -### 4.1 상태값 - -| 상태 | 코드 | 색상 | 설명 | -|------|------|------|------| -| 초안 | `DRAFT` | 회색 | 생성 직후, 편집 가능 | -| 녹음중 | `RECORDING` | 빨강 | (클라이언트 상태) | -| 처리중 | `PROCESSING` | 노랑 | AI 요약/화자분리 처리 중 | -| 완료 | `COMPLETED` | 초록 | AI 처리 완료 | -| 실패 | `FAILED` | 빨강 | AI 처리 실패 | - -### 4.2 상태 전이 - -``` -DRAFT - ↓ [오디오 업로드, 세그먼트 추가] -DRAFT (계속 편집) - ↓ [summarize() 호출] -PROCESSING - ↓ -COMPLETED (성공) 또는 FAILED (실패) - -DRAFT - ↓ [diarize() 호출 → 화자 분리] -DRAFT (세그먼트 갱신, 상태 유지) -``` - ---- - -## 5. API 명세 - -### 5.1 목록 조회 - -``` -GET /juil/meeting-minutes/list -``` - -| 파라미터 | 타입 | 설명 | -|---------|------|------| -| `search` | string | 제목 검색 | -| `date_from` | date | 시작일 | -| `date_to` | date | 종료일 | -| `status` | string | 상태 필터 | -| `per_page` | int | 페이지당 건수 | - -### 5.2 생성 - -``` -POST /juil/meeting-minutes -``` - -| 필드 | 규칙 | 설명 | -|------|------|------| -| `title` | nullable, max:300 | 제목 (미입력 시 "무제 회의록") | -| `folder` | nullable, max:100 | 폴더 분류 | -| `participants` | nullable, array | 참여자 목록 | -| `meeting_date` | required, date | 회의 날짜 | -| `meeting_time` | nullable | 회의 시간 | -| `stt_language` | nullable, max:10 | STT 언어 (기본: ko-KR) | - -### 5.3 세그먼트 저장 - -``` -POST /juil/meeting-minutes/{id}/segments -``` - -```json -{ - "segments": [ - { - "speaker_name": "김과장", - "speaker_label": "1", - "text": "블라인드 납기일 확인 필요합니다.", - "start_time_ms": 0, - "end_time_ms": 5000, - "is_manual_speaker": true - } - ] -} -``` - -> **전처리**: 빈 텍스트 필터링, 언더스코어 노이즈 제거, 다중 공백 정규화 -> **자동 생성**: `full_transcript` = `[화자명] 발화텍스트\n...` 형식 - -### 5.4 오디오 업로드 - -``` -POST /juil/meeting-minutes/{id}/upload-audio -``` - -| 필드 | 규칙 | 설명 | -|------|------|------| -| `audio` | required, file | webm/mp3 등 | -| `duration_seconds` | required, integer, min:1 | 녹음 시간(초) | - -### 5.5 AI 요약 생성 - -``` -POST /juil/meeting-minutes/{id}/summarize -``` - -**요청**: 없음 (서버에서 `full_transcript` 사용) - -**응답 예시:** - -```json -{ - "success": true, - "message": "AI 요약이 완료되었습니다.", - "data": { - "summary": "블라인드 납품 일정과 현장 설치 계획을 논의했습니다...", - "decisions": [ - "납품일을 3월 15일로 확정", - "현장 실측은 3월 10일 진행" - ], - "action_items": [ - { - "assignee": "김과장", - "task": "거래처에 납기 확인 연락", - "deadline": "2026-03-08" - } - ], - "status": "COMPLETED" - } -} -``` - -### 5.6 자동 화자 분리 - -``` -POST /juil/meeting-minutes/{id}/diarize -``` - -| 필드 | 설명 | 기본값 | -|------|------|--------| -| `min_speakers` | 최소 화자 수 | 2 | -| `max_speakers` | 최대 화자 수 | 6 | - -**응답:** - -```json -{ - "success": true, - "message": "자동 화자 분리가 완료되었습니다. (3명 감지)", - "data": { /* Meeting with segments */ }, - "speaker_count": 3 -} -``` - ---- - -## 6. AI 통합 상세 - -### 6.1 화자 분리 (Diarization) 3단계 폴백 - -``` -[1단계] Google STT V2 (Chirp2) ← 최우선 - │ speechToTextWithDiarizationAuto() - │ 최신 모델, 높은 정확도 - │ 도메인 용어 힌트 포함 - │ - ↓ (실패 시) -[2단계] Google STT V1 (latest_long) ← 폴백 - │ 안정적이지만 약간 덜 정확 - │ - ↓ (1명만 인식 시) -[3단계] Gemini AI 화자 재분배 - splitSpeakersWithGemini() - 대화 맥락/호칭/질답 패턴/어투 변화 분석 - 2명 이상으로 재분배 -``` - -### 6.2 요약 생성 (Gemini API) - -``` -입력: full_transcript (전체 트랜스크립트) - ↓ -Gemini API 호출 - ├── 모드 1: Vertex AI (projectId, region, JWT) - └── 모드 2: Google AI Studio (API key) ← 폴백 - │ - │ Temperature: 0.3 (결정적) - │ Max tokens: 4096 - ↓ -출력 JSON: -{ - "summary": "3-5문장 요약", - "decisions": ["결정사항 1", "..."], - "action_items": [ - { "assignee": "담당자", "task": "할일", "deadline": "기한" } - ], - "keywords": ["키워드1", "..."] -} -``` - -### 6.3 Gemini 화자 재분배 - -Google STT가 1명만 인식할 때 Gemini로 대화 맥락 분석: - -``` -입력: 단일 화자 트랜스크립트 + 예상 화자 수 - ↓ -Gemini 프롬프트: - - 대화 맥락 분석 (호칭, 질답, 어투 변화) - - 지정된 수의 화자로 분리 - ↓ -출력: 화자별 세그먼트 배열 - → DB 세그먼트 교체 -``` - ---- - -## 7. 오디오 관리 (GCS) - -### 7.1 GCS 경로 패턴 - -``` -meeting-minutes/{tenant_id}/{meeting_id}/{timestamp}.webm -``` - -### 7.2 녹음 흐름 - -``` -브라우저 MediaRecorder API - ├── navigator.mediaDevices.getUserMedia({ audio: true }) - ├── new MediaRecorder(stream) - ├── recorder.ondataavailable → webm 블롭 수집 - └── 녹음 종료 → FormData로 업로드 - ↓ -POST /{id}/upload-audio - ├── GCS 업로드 - ├── DB: audio_file_path, audio_gcs_uri, audio_file_size, duration_seconds - └── AiTokenHelper::saveGcsStorageUsage() -``` - -### 7.3 다운로드 - -``` -GET /{id}/download-audio - → GCS에서 파일 콘텐츠 다운로드 - → Content-Disposition: attachment; filename="{title}.webm" -``` - ---- - -## 8. 세그먼트 처리 로직 - -### 8.1 저장 시 전처리 - -```php -// 1. 빈 텍스트 필터링 -trim($segment['text']) !== '' - -// 2. 언더스코어 노이즈 제거 -str_replace('_', '', $text) - -// 3. 다중 공백 정규화 -preg_replace('/\s{2,}/', ' ', $text) -``` - -### 8.2 전체 트랜스크립트 자동 생성 - -``` -[김과장] 블라인드 납기일 확인 필요합니다. -[박부장] 3월 15일로 확정합시다. -[김과장] 네, 거래처에 연락하겠습니다. -``` - -### 8.3 화자 분리 결과 세그먼트 변환 - -``` -Google STT 결과 → MeetingMinuteSegment 변환: -{ - segment_order: 순서, - speaker_name: "화자 N", - speaker_label: "N", - text: 발화 텍스트, - start_time_ms: 시작시간, - end_time_ms: 종료시간, - is_manual_speaker: false // 자동 분리 -} -``` - ---- - -## 9. UI 구성 (React) - -### 9.1 화자 색상 - -| 화자 | 배경색 | 뱃지색 | -|------|--------|--------| -| 화자 1 | `bg-blue-50` | `bg-blue-100 text-blue-800` | -| 화자 2 | `bg-green-50` | `bg-green-100 text-green-800` | -| 화자 3 | `bg-purple-50` | `bg-purple-100 text-purple-800` | -| 화자 4 | `bg-orange-50` | `bg-orange-100 text-orange-800` | - -### 9.2 지원 언어 - -| 코드 | 라벨 | -|------|------| -| `ko-KR` | 한국어 | -| `en-US` | English | -| `ja-JP` | 日本語 | -| `zh-CN` | 中文 | - ---- - -## 10. 사용량 추적 - -| 추적 항목 | 레이블 | Helper | -|----------|--------|--------| -| Web Speech API 사용 | `회의록-음성인식` | `AiTokenHelper::saveSttUsage()` | -| Google STT V1 화자 분리 | `회의록-화자분리` | `AiTokenHelper::saveSttUsage()` | -| Google STT V2 화자 분리 | `회의록-화자분리(Chirp2)` | `AiTokenHelper::saveSttUsage()` | -| GCS 오디오 저장 | `회의록-GCS저장` | `AiTokenHelper::saveGcsStorageUsage()` | -| Gemini 요약/분리 | `회의록-AI요약` | `AiTokenHelper::saveGeminiUsage()` | - ---- - -## 11. 모델 메서드 - -### 11.1 MeetingMinute - -```php -user() # BelongsTo User -segments() # HasMany Segment (segment_order ASC) -getFormattedDurationAttribute() # "H:MM:SS" 또는 "MM:SS" -``` - -**Cast**: `participants`, `decisions`, `action_items` → array, `meeting_date` → date - -### 11.2 MeetingMinuteService - -```php -# CRUD -getList(array $filters) # 검색/필터 목록 -create(array $data) # 생성 (DRAFT) -update(MeetingMinute, array $data) # 수정 -delete(MeetingMinute) # GCS 삭제 → soft delete - -# 세그먼트 -saveSegments(MeetingMinute, array $segments) # 전처리 + 저장 + 트랜스크립트 생성 -uploadAudio(MeetingMinute, UploadedFile, int $seconds) # GCS 업로드 -logSttUsage(int $seconds) # STT 사용량 기록 - -# AI -generateSummary(MeetingMinute) # Gemini 요약 생성 -processDiarization(MeetingMinute, int $min, int $max) # 3단계 화자 분리 -splitSpeakersWithGemini(string $text, int $expected) # Gemini 화자 재분배 -``` - ---- - -## 관련 문서 - -- [README.md](README.md) — 기획 메뉴 전체 개요 -- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력 -- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/planning/planning-views.md b/sam/docs/features/planning/planning-views.md deleted file mode 100644 index 4b087ac..0000000 --- a/sam/docs/features/planning/planning-views.md +++ /dev/null @@ -1,222 +0,0 @@ -# 견적/프로젝트/워크플로우 화면 명세 - -> **작성일**: 2026-03-06 -> **상태**: 뷰 구현 완료 (목데이터 기반, API 미연동) -> **라우트**: `/juil/estimate`, `/juil/project`, `/juil/workflow` -> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [회의록](meeting-minutes.md) - ---- - -## 1. 개요 - -3개 화면 모두 **React 인라인 컴포넌트**(Babel 브라우저 트랜스파일)로 구현. 현재는 정적/목데이터 기반이며, 향후 API 연동 예정. PlanningController에서 뷰만 반환한다. - ---- - -## 2. 견적/입찰/공사관리 (/juil/estimate) - -### 2.1 개요 - -블라인드/스크린 설치 프로젝트의 견적서 작성, 입찰 관리, 공사 진행 현황을 한 화면에서 관리. - -### 2.2 데이터 구조 (initialEstimates) - -```javascript -{ - id: "string", - name: "프로젝트명", - client: "고객사명", - status: "견적중|입찰|계약|공사중|준공", - amount: number, // 금액 - startDate: "YYYY-MM-DD", - endDate: "YYYY-MM-DD", - manager: "담당자명", - items: [ // 품목 내역 - { name: "품목명", quantity: number, unitPrice: number } - ] -} -``` - -### 2.3 공사관리 정보 (initialConstructionData) - -```javascript -{ - id: "string", - estimateId: "string", // 연결 견적 - siteName: "현장명", - address: "현장 주소", - progress: number, // 진행률 (0~100) - workers: number, // 투입 인원 - safetyChecks: [ // 안전점검 - { date: "YYYY-MM-DD", result: "합격|불합격", inspector: "점검자" } - ] -} -``` - -### 2.4 상태별 배지 색상 - -| 상태 | 색상 | -|------|------| -| 견적중 | 파랑 | -| 입찰 | 보라 | -| 계약 | 초록 | -| 공사중 | 주황 | -| 준공 | 회색 | - -### 2.5 SAM 연계 - -- 견적서 작성 시 SAM 견적 시스템(`features/quotes/`) 데이터 활용 가능 -- 향후 `/juil/estimate` ↔ SAM 견적 API 연동 계획 - ---- - -## 3. 프로젝트관리/기성청구 (/juil/project) - -### 3.1 개요 - -계약된 프로젝트의 현장 관리, 발주/청구/인건비 상태 추적, 기성 청구 관리. - -### 3.2 데이터 구조 (initialProjects) - -```javascript -{ - id: "string", - name: "프로젝트명", - client: "발주처", - contractAmount: number, // 계약금액 - status: "진행중|완료|보류", - sites: [ // 현장 목록 - { - name: "현장명", - address: "주소", - progress: number // 진행률 - } - ], - orders: [ // 발주 내역 - { - vendor: "거래처", - amount: number, - status: "발주|납품|정산" - } - ], - claims: [ // 기성 청구 - { - round: number, // 차수 - amount: number, // 청구금액 - claimDate: "YYYY-MM-DD", - status: "청구|승인|입금" - } - ], - laborCosts: [ // 인건비 - { - month: "YYYY-MM", - amount: number, - workers: number - } - ] -} -``` - -### 3.3 금액 포맷 함수 - -```javascript -fmt(amount) // 1,234,567 (쉼표 포맷) -fmtBillion(amount) // 12.3억 (억 단위 축약) -``` - ---- - -## 4. 업무 Workflow (/juil/workflow) - -### 4.1 개요 - -블라인드/스크린 사업의 전체 업무 프로세스를 단계별로 시각화. 각 프로세스에 담당 부서, 산출물, 서브스텝을 정의. - -### 4.2 프로세스 데이터 구조 - -```javascript -{ - id: "S1-1", // 프로세스 ID - phase: "영업", // Phase 명 - name: "정보 수집", // 프로세스 이름 - icon: "icon-name", // 아이콘 - dept: "영업팀", // 담당 부서 - color: "#3B82F6", // 테마 색상 - description: "프로세스 설명", - documents: [ // 산출물 목록 - "현장조사서", "고객요구사항서" - ], - subSteps: [ // 상세 서브스텝 - { - name: "서브스텝명", - description: "상세 설명", - responsible: "담당자/팀", - output: "산출물" - } - ] -} -``` - -### 4.3 업무 Phase 목록 - -| Phase | ID 범위 | 설명 | -|-------|---------|------| -| **영업** | S1-1 ~ S1-4 | 정보 수집 → 현장 실측 → 고객 미팅 → 프로젝트 검토 | -| **견적서 작성** | S2-1 ~ S2-4 | 물량 산출 → 단가 산정 → 견적가 산출 → 견적서 작성/검토 | -| **입찰** | S3-* | 입찰 준비 → 제출 → 결과 확인 | -| **계약** | S4-* | 계약 협상 → 계약 체결 | -| **공사** | S5-* | 자재 발주 → 시공 → 현장 관리 | -| **준공** | S6-* | 검수 → 하자보수 → 준공 정산 | - -### 4.4 SAM 연계 포인트 - -```javascript -// 견적서 작성 Phase에서 SAM 견적 화면으로 연결 -{ samLink: '/juil/estimate', label: '견적서 작성 바로가기' } -``` - ---- - -## 5. 공통 특징 - -### 5.1 HTMX 전체 페이지 로드 - -3개 화면 모두 React 컴포넌트 사용하므로 HTMX 부분 로드 불가: - -```php -// PlanningController의 모든 메서드 -if ($request->header('HX-Request')) { - return response('', 200)->header('HX-Redirect', route('juil.xxx')); -} -return view('juil.xxx'); -``` - -### 5.2 현재 구현 상태 - -| 항목 | 상태 | -|------|------| -| UI 화면 | 구현 완료 (React 인라인) | -| 목데이터 | 블레이드에 하드코딩 | -| API 연동 | 미연동 (향후 계획) | -| DB 테이블 | 미생성 (향후 계획) | -| CRUD 기능 | 뷰 조회만 (생성/수정/삭제 미구현) | - -### 5.3 향후 개발 방향 - -1. 견적/프로젝트 DB 테이블 설계 (API 프로젝트) -2. API 엔드포인트 구현 -3. React 컴포넌트 API 연동 -4. SAM 견적 시스템과 데이터 동기화 - ---- - -## 관련 문서 - -- [README.md](README.md) — 기획 메뉴 전체 개요 -- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력 -- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록 -- [견적 시스템](../quotes/README.md) — SAM 견적 관리 (BOM, 10단계 로직) - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/features/rd/README.md b/sam/docs/features/rd/README.md deleted file mode 100644 index 1483160..0000000 --- a/sam/docs/features/rd/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# R&D 메뉴 - -> **작성일**: 2026-03-08 -> **상태**: 운영 중 -> **프로젝트**: SAM MNG (관리자 웹) -> **라우트 접두사**: `/rd` - ---- - -## 1. 개요 - -### 1.1 목적 - -R&D 메뉴는 SAM 플랫폼의 **연구개발 및 내부 도구** 모음이다. AI 견적, 조직도 관리, 기획디자인(스토리보드 에디터), 안전점검 등 실험적이거나 내부 운영 목적의 기능을 제공한다. - -### 1.2 문서 구조 - -| 문서 | 설명 | -|------|------| -| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 컨트롤러 매핑 | -| [planning-design.md](planning-design.md) | 기획디자인 스토리보드 에디터 기술 명세 | -| [design-insight.md](design-insight.md) | 디자인 인사이트 UI/UX 연구 도구 (100종 패턴, AI 프롬프트) | - -### 1.3 하위 메뉴 구조 - -``` -R&D -├── 대시보드 /rd -├── 조직도 관리 /rd/org-chart -├── 중대재해처벌법 점검 /rd/safety-audit -├── AI 견적 /rd/ai-quotation -│ ├── 목록 /rd/ai-quotation -│ ├── 생성 /rd/ai-quotation/create -│ ├── 상세 /rd/ai-quotation/{id} -│ ├── 편집 /rd/ai-quotation/{id}/edit -│ └── 문서 /rd/ai-quotation/{id}/document -├── 기획디자인 /rd/planning-design -└── 디자인 인사이트 /rd/design-insight -``` - ---- - -## 2. 아키텍처 - -### 2.1 기술 스택 - -| 계층 | 기술 | 설명 | -|------|------|------| -| 뷰 | Blade + Alpine.js | 반응형 SPA (서버 렌더링 없음) | -| 컨트롤러 | `RdController` | 모든 R&D 라우트 처리 | -| 서비스 | `AiQuotationService` | AI 견적 비즈니스 로직 | -| 모델 | `AiQuotation`, `Employee`, `Department`, `Tenant` | Multi-tenant | -| 저장 | localStorage (기획디자인), DB (견적/조직도) | 용도별 분리 | - -### 2.2 컨트롤러 구조 - -**파일**: `app/Http/Controllers/RdController.php` - -| 메서드 | 라우트 | 설명 | -|--------|--------|------| -| `index()` | `GET /rd` | R&D 대시보드 | -| `orgChart()` | `GET /rd/org-chart` | 조직도 관리 | -| `orgChartAssign()` | `POST /rd/org-chart/assign` | 직원 부서 배치 | -| `orgChartUnassign()` | `POST /rd/org-chart/unassign` | 직원 부서 해제 | -| `orgChartReorder()` | `POST /rd/org-chart/reorder` | 직원 순서/이동 | -| `orgChartReorderDepts()` | `POST /rd/org-chart/reorder-depts` | 부서 순서 변경 | -| `orgChartToggleHide()` | `POST /rd/org-chart/toggle-hide` | 부서 숨기기/표시 | -| `safetyAudit()` | `GET /rd/safety-audit` | 중대재해처벌법 점검 | -| `quotations()` | `GET /rd/ai-quotation` | AI 견적 목록 | -| `createQuotation()` | `GET /rd/ai-quotation/create` | AI 견적 생성 폼 | -| `showQuotation()` | `GET /rd/ai-quotation/{id}` | AI 견적 상세 | -| `editQuotation()` | `GET /rd/ai-quotation/{id}/edit` | AI 견적 편집 | -| `documentQuotation()` | `GET /rd/ai-quotation/{id}/document` | AI 견적 문서 | -| `planningDesign()` | `GET /rd/planning-design` | 기획디자인 | -| `designInsight()` | `GET /rd/design-insight` | 디자인 인사이트 | - -### 2.3 HTMX 전체 페이지 로드 규칙 - -모든 `/rd/*` 페이지는 Alpine.js 또는 React 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 메서드에서 `HX-Request` 감지 시 `HX-Redirect`로 전체 페이지 로드를 강제한다. - -```php -if ($request->header('HX-Request')) { - return response('', 200)->header('HX-Redirect', route('rd.planning-design')); -} -``` - ---- - -## 3. 기능별 구현 현황 - -| 기능 | 구현 방식 | 백엔드 | DB | 상태 | -|------|----------|--------|-----|------| -| R&D 대시보드 | Blade | AiQuotationService | ai_quotations | 운영 중 | -| 조직도 관리 | Blade + Alpine.js | RdController (직접 쿼리) | employees, departments | 운영 중 | -| 중대재해처벌법 점검 | Blade (정적) | 없음 | 없음 | 운영 중 | -| AI 견적 | Blade + Alpine.js | AiQuotationService | ai_quotations | 운영 중 | -| **기획디자인** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** | -| **디자인 인사이트** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** | - ---- - -## 4. 관련 문서 - -- [기획디자인 스토리보드 에디터](planning-design.md) — 블록 에디터, 서식, 인쇄, 내보내기 -- [디자인 인사이트](design-insight.md) — UI/UX 연구 도구 (100종 패턴, AI 프롬프트 복사) -- [조직도 관리 기술문서](../../projects/org-chart/) — 조직도 시스템 상세 (별도 프로젝트 문서) - ---- - -**최종 업데이트**: 2026-03-08 diff --git a/sam/docs/features/rd/design-insight.md b/sam/docs/features/rd/design-insight.md deleted file mode 100644 index 683d02f..0000000 --- a/sam/docs/features/rd/design-insight.md +++ /dev/null @@ -1,246 +0,0 @@ -# 디자인 인사이트 — UI/UX 연구 도구 - -> **작성일**: 2026-03-08 -> **상태**: 운영 중 -> **라우트**: `GET /rd/design-insight` -> **뷰**: `resources/views/rd/design-insight/index.blade.php` - ---- - -## 1. 개요 - -### 1.1 목적 - -디자인 인사이트는 UI/UX 패턴을 **수집·분석·비교**하는 연구 도구이다. 외부 서비스의 UI 레퍼런스를 카드 형태로 정리하고, CSS 와이어프레임 미리보기와 AI 프롬프트 생성 기능으로 디자인 의사결정을 지원한다. - -### 1.2 핵심 기능 - -| 기능 | 설명 | -|------|------| -| 카드 관리 | 레퍼런스/분석/패턴/Before-After 4종 카드 CRUD | -| 프로젝트 관리 | 다중 프로젝트, localStorage 저장 | -| CSS 와이어프레임 | 100종 UI 패턴의 순수 CSS 미니 와이어프레임 | -| 프리셋 템플릿 | 인기 UI 패턴 100종 원클릭 불러오기 | -| AI 프롬프트 복사 | 카드 정보를 AI용 구조화 프롬프트로 변환·복사 | -| 3종 뷰 | 보드(카테고리별)/갤러리(그리드)/리스트 뷰 | -| JSON 내보내기/가져오기 | 프로젝트 데이터 백업/복원 | - ---- - -## 2. 아키텍처 - -### 2.1 기술 스택 - -| 계층 | 기술 | 설명 | -|------|------|------| -| 뷰 | Blade + Alpine.js | 단일 파일 SPA | -| 컨트롤러 | `RdController::designInsight()` | HX-Redirect 패턴 | -| 저장 | localStorage | `di_projects`, `di_current` 키 사용 | -| 백엔드 | 없음 | 서버 API 호출 없이 클라이언트 단독 동작 | -| 스타일 | 커스텀 CSS 변수 | `--di-*` 접두사 (Tailwind 미사용) | - -### 2.2 데이터 구조 - -```json -{ - "id": "di_1709000000_abc123", - "title": "프로젝트명", - "cards": [ - { - "id": "di_1709000001_def456", - "type": "pattern", - "title": "KPI 대시보드", - "category": "dashboard", - "rating": 5, - "tags": ["대시보드", "KPI", "통계"], - "memo": "레퍼런스 설명", - "guidelines": "디자인 가이드라인", - "usedIn": ["Stripe", "Shopify"], - "components": [ - { "name": "KPI 요약 카드", "required": true }, - { "name": "필터 영역", "required": false } - ], - "image": null, - "createdAt": "2026-03-08T00:00:00.000Z" - } - ], - "createdAt": "2026-03-08T00:00:00.000Z", - "updatedAt": "2026-03-08T00:00:00.000Z" -} -``` - ---- - -## 3. 카드 유형 (4종) - -| 코드 | 라벨 | 용도 | -|------|------|------| -| `reference` | 레퍼런스 | 외부 서비스 UI 스크린샷 수집 | -| `analysis` | 분석 | CRAP 원칙 등 UX 분석 (8가지 디자인 원칙 평가) | -| `pattern` | 패턴 | 재사용 가능한 UI 패턴 정의 | -| `comparison` | Before/After | 개선 전후 비교 (이미지 2장) | - ---- - -## 4. 카테고리 (8종) - -| 코드 | 라벨 | 아이콘 | -|------|------|--------| -| `dashboard` | 대시보드 | 📊 | -| `list` | 목록 | 📋 | -| `form` | 상세/폼 | 📝 | -| `modal` | 모달/팝업 | 💬 | -| `navigation` | 네비게이션 | 🧭 | -| `auth` | 로그인 | 🔐 | -| `report` | 보고서 | 📄 | -| `etc` | 기타 | 📎 | - ---- - -## 5. CSS 와이어프레임 시스템 - -### 5.1 동작 원리 - -`getWireframe(card)` 함수가 카드의 `title`과 `tags`를 키워드 매칭하여 해당 패턴에 맞는 순수 CSS/HTML 미니 와이어프레임을 반환한다. - -```javascript -getWireframe(card) { - const t = (card.title || '').toLowerCase(); - const tags = (card.tags || []).join(' ').toLowerCase(); - const key = t + ' ' + tags; - - if (key.includes('kpi') || key.includes('대시보드') && key.includes('통계')) return `...`; - // ... 100종 패턴 매칭 - return `기본 와이어프레임 (매칭 안 됨)`; -} -``` - -### 5.2 프리셋 100종 분포 - -| 카테고리 | 패턴 수 | 대표 패턴 | -|---------|---------|----------| -| 대시보드 | 10 | KPI, 실시간 모니터링, 게이지/미터, 히트맵, 퍼널 | -| 목록 | 10 | 데이터 테이블, 칸반 보드, 마스터-디테일, 피벗 테이블 | -| 상세/폼 | 16 | 프로필, 설정, 위지윅 에디터, 멀티스텝 폼, 태그 입력 | -| 모달/팝업 | 10 | 확인 다이얼로그, 라이트박스, 바텀시트, 컨텍스트 메뉴 | -| 네비게이션 | 10 | 사이드바, 탭, 브레드크럼, FAB, 앵커 스크롤 | -| 로그인 | 8 | 로그인 폼, 회원가입, 비밀번호 재설정, RBAC, API 키 | -| 보고서 | 9 | 인쇄용 보고서, 간트 차트, 조직도, 워터폴, 리포트 빌더 | -| 기타 | 27 | 댓글, 에러 페이지, FAQ, 캐러셀, 파일 매니저 등 | - ---- - -## 6. AI 프롬프트 복사 기능 - -### 6.1 목적 - -카드에 정리된 UI 패턴 정보를 **AI가 이해할 수 있는 구조화된 마크다운 프롬프트**로 변환하여 클립보드에 복사한다. 복사한 프롬프트를 AI(Claude, ChatGPT 등)에 붙여넣으면 해당 스타일로 코드를 생성할 수 있다. - -### 6.2 UI 위치 - -카드 상세 모달 상단, **편집** 버튼 왼쪽에 보라색 `✨ AI 프롬프트` 버튼으로 배치. - -### 6.3 프롬프트 구조 - -`copyAiPrompt(card)` 함수가 카드 데이터를 다음 구조로 변환한다: - -```markdown -## UI 패턴 구현 요청 - -아래 UI/UX 패턴 레퍼런스를 참고하여, 동일한 스타일과 구조로 코드를 작성해 주세요. - ---- - -### 패턴 정보 -- **패턴명**: {title} -- **카테고리**: {category label} -- **완성도 평점**: ★★★☆☆ ({rating}/5) -- **키워드**: {tags} - -### 레퍼런스 설명 -{memo} - -### 실제 사용처 (벤치마킹 대상) -- {usedIn[0]} -- {usedIn[1]} - -### 필수 구성 요소 - -**필수 (반드시 포함)**: -- ✅ {required component} - -**선택 (권장)**: -- ○ {optional component} - -### 디자인 가이드라인 -{guidelines} - -### 개선 제안 -{suggestion} - -### 기대 효과 -{effect} - ---- - -### 구현 요구사항 - -1. **기술 스택**: HTML + Tailwind CSS (또는 프로젝트에 맞는 프레임워크) -2. **반응형**: 모바일/태블릿/데스크톱 대응 -3. **접근성**: 시맨틱 태그, ARIA 라벨, 키보드 네비게이션 -4. **인터랙션**: hover, focus, active 상태 포함 -5. **위 구성 요소를 모두 포함**하되, 실제 서비스처럼 자연스러운 더미 데이터 사용 -6. **위 가이드라인을 충실히 반영**하여 UX 완성도를 높일 것 -``` - -### 6.4 포함 필드 매핑 - -| 카드 필드 | 프롬프트 섹션 | 조건 | -|----------|-------------|------| -| `title` | 패턴 정보 > 패턴명 | 항상 | -| `category` | 패턴 정보 > 카테고리 | 항상 (라벨로 변환) | -| `rating` | 패턴 정보 > 평점 | 항상 (별점으로 변환) | -| `tags` | 패턴 정보 > 키워드 | 태그가 있을 때만 | -| `memo` | 레퍼런스 설명 | 값이 있을 때만 | -| `usedIn` | 실제 사용처 | 배열이 비어있지 않을 때만 | -| `components` | 필수 구성 요소 | 배열이 비어있지 않을 때만 | -| `guidelines` | 디자인 가이드라인 | 값이 있을 때만 | -| `suggestion` | 개선 제안 | 값이 있을 때만 | -| `effect` | 기대 효과 | 값이 있을 때만 | -| `principles` | UX 원칙 | `type === 'analysis'`일 때만 | - ---- - -## 7. 뷰 모드 (3종) - -| 모드 | 설명 | 정렬 | -|------|------|------| -| `board` | 카테고리별 컬럼 그룹핑 | 카테고리 → 생성순 | -| `gallery` | 그리드 갤러리 (와이어프레임 강조) | 필터 순 | -| `list` | 테이블형 리스트 | 필터 순 | - ---- - -## 8. 파일 구조 - -``` -resources/views/rd/design-insight/ -└── index.blade.php # 단일 파일 SPA (~6,200줄) - ├── - - - -

제목

-

본문 내용

- - -``` - -### 3.2 슬라이드 크기 (body width/height) - -| 용도 | body 크기 | 변환 스크립트 layout | -|------|----------|---------------------| -| **16:9 가로형** (발표용) | `width: 720pt; height: 405pt;` | `width: 10, height: 5.625` | -| **9:16 세로형** (브로셔) | `width: 405pt; height: 720pt;` | `width: 5.625, height: 10` | -| **4:3 가로형** (구형) | `width: 720pt; height: 540pt;` | `width: 10, height: 7.5` | - -> **중요**: HTML의 body 크기와 변환 스크립트의 layout 크기가 일치해야 한다. 불일치 시 에러 발생. - -### 3.3 필수 규칙 - -#### 텍스트 줄바꿈 방지 (가장 중요) - -브라우저와 PowerPoint의 폰트 렌더링 차이로, HTML에서 한 줄인 텍스트가 PPTX에서 두 줄로 넘어가는 문제가 발생한다. - -```html - -

이 텍스트는 한 줄입니다

- - -

이 텍스트는 한 줄입니다

- - -

- 여러 줄로 의도된 텍스트입니다.
- 이 경우 nowrap을 넣지 않는다. -

-``` - -#### 적용 대상 - -- 뱃지 텍스트 (01, UC-01 등) -- 카드 제목, 태그, 짧은 라벨 -- 푸터 텍스트 -- 단일행 `

` 태그 전부 - -#### 이미지 경로 - -```html - - - - - -``` - -#### 스타일 작성 - -```html - -

- - -``` - -### 3.4 사용 가능한 CSS 속성 - -| 속성 | 지원 | 비고 | -|------|:----:|------| -| background (색상) | ✅ | 단색, rgba 모두 지원 | -| background (그라데이션) | ✅ | linear-gradient 지원 | -| border | ✅ | 색상, 두께, radius | -| border-radius | ✅ | px, pt 단위 | -| font-size, font-weight | ✅ | pt 단위 권장 | -| color | ✅ | hex, rgba | -| padding, margin | ✅ | pt 단위 권장 | -| display: flex | ✅ | gap, align-items 등 | -| white-space: nowrap | ✅ | 줄바꿈 방지 (필수) | -| opacity | ✅ | | -| box-shadow | ⚠️ | 부분 지원 | -| transform | ❌ | 미지원 | -| animation | ❌ | 미지원 | - ---- - -## 4. 변환 스크립트 작성법 - -### 4.1 기본 구조 (복사해서 사용) - -```javascript -const path = require('path'); - -// ① 패키지 경로 설정 (이 두 줄은 항상 동일) -module.paths.unshift( - path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules') -); -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require( - path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js') -); - -async function main() { - const pres = new PptxGenJS(); - - // ② 레이아웃 설정 (HTML body 크기와 일치해야 함) - pres.defineLayout({ name: 'CUSTOM', width: 10, height: 5.625 }); - pres.layout = 'CUSTOM'; - - // ③ HTML 파일 변환 - await html2pptx('/절대경로/slides/slide-01.html', pres); - - // ④ PPTX 출력 - await pres.writeFile({ fileName: '/절대경로/output.pptx' }); - console.log('완료!'); -} - -main().catch(console.error); -``` - -### 4.2 실전 예시: 여러 슬라이드 변환 - -```javascript -const path = require('path'); -module.paths.unshift( - path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules') -); -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require( - path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js') -); - -async function main() { - const pres = new PptxGenJS(); - - // 16:9 가로형 - pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); - pres.layout = 'CUSTOM_16x9'; - - const slidesDir = path.join(__dirname, 'slides'); - - // 변환할 HTML 파일 목록 - const slides = [ - 'slide-01.html', - 'slide-02.html', - 'slide-03.html', - ]; - - for (const file of slides) { - console.log(`Converting ${file} ...`); - try { - await html2pptx(path.join(slidesDir, file), pres); - } catch (err) { - console.error(`Error: ${err.message}`); - } - } - - const outputPath = path.join(__dirname, 'my-presentation.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX created: ${outputPath}`); -} - -main().catch(console.error); -``` - -### 4.3 실전 예시: 번호 기반 자동 변환 (18슬라이드) - -```javascript -// slide-01.html ~ slide-18.html 자동 변환 -const totalSlides = 18; -for (let i = 1; i <= totalSlides; i++) { - const num = String(i).padStart(2, '0'); - const htmlFile = path.join(slidesDir, `slide-${num}.html`); - await html2pptx(htmlFile, pres); -} -``` - ---- - -## 5. 실행 방법 - -### 5.1 터미널에서 직접 실행 - -```bash -# 해당 폴더로 이동 후 실행 -cd /home/aweso/sam/docs/brochure -node convert-2page.cjs - -# 또는 절대 경로로 실행 -node /home/aweso/sam/docs/brochure/convert-2page.cjs -``` - -### 5.2 실행 결과 - -``` -Converting brochure-2page-front.html ... -Converting brochure-2page-back.html ... - -PPTX created: /home/aweso/sam/docs/brochure/sam-brochure-2page.pptx -``` - -> 에러가 나면 HTML body 크기와 layout 설정 불일치가 가장 흔한 원인이다. - ---- - -## 6. 프로젝트 내 기존 사용 사례 - -| 경로 | 슬라이드 수 | 레이아웃 | 용도 | -|------|:-----------:|:--------:|------| -| `docs/usecase/` | 18장 | 16:9 가로 | 방화셔터 제안서 | -| `docs/usecase/brochure/` | 1장 / 2장 | 9:16 세로 | 방화셔터 브로셔 | -| `docs/brochure/` | 1장 / 2장 | 9:16 세로 | SAM 범용 브로셔 | -| `docs/plans/slides/` | N장 | 16:9 가로 | 배포 계획 발표 | -| `docs/rules/slides/` | N장 | 16:9 가로 | 정책 규칙 문서 | - ---- - -## 7. 폴더 구조 권장 패턴 - -새 PPTX를 만들 때 아래 구조를 따른다: - -``` -docs/my-document/ -├── slides/ ← HTML 슬라이드 파일들 -│ ├── slide-01.html -│ ├── slide-02.html -│ └── slide-03.html -├── convert.cjs ← 변환 스크립트 -└── my-document.pptx ← 생성된 PPTX (node convert.cjs 실행 후) -``` - ---- - -## 8. 문제 해결 - -| 증상 | 원인 | 해결 | -|------|------|------| -| `Error: dimensions don't match` | HTML body 크기 ≠ layout 설정 | body의 width/height와 `defineLayout` 값 맞추기 | -| 텍스트가 PPTX에서 줄바꿈됨 | `white-space: nowrap` 미적용 | 단일행 `

` 태그에 nowrap 추가 | -| 이미지 안 보임 | 상대 경로 사용 | 절대 경로(`/home/aweso/...`)로 변경 | -| `Cannot find module 'pptxgenjs'` | module.paths 설정 누락 | 스크립트 상단 2줄(module.paths.unshift) 확인 | -| `Cannot find module 'playwright'` | Playwright 미설치 | `cd ~/.claude/skills/pptx-skill/scripts && npx playwright install chromium` | -| PPTX는 생성되지만 빈 슬라이드 | HTML 내용 없음 | HTML 파일을 브라우저로 열어 확인 | - ---- - -## 9. 빠른 시작 (3분 가이드) - -### Step 1: 폴더 만들기 - -```bash -mkdir -p /home/aweso/sam/docs/my-pptx/slides -``` - -### Step 2: HTML 슬라이드 만들기 - -`slides/slide-01.html` 파일 생성: - -```html - - - - - - - -

MY COMPANY

-

발표 제목을 여기에

-

부제목 또는 설명

- - -``` - -### Step 3: 변환 스크립트 만들기 - -`convert.cjs` 파일 생성: - -```javascript -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -async function main() { - const pres = new PptxGenJS(); - pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); - pres.layout = 'CUSTOM_16x9'; - - await html2pptx(path.join(__dirname, 'slides', 'slide-01.html'), pres); - - await pres.writeFile({ fileName: path.join(__dirname, 'my-presentation.pptx') }); - console.log('완료!'); -} -main().catch(console.error); -``` - -### Step 4: 실행 - -```bash -cd /home/aweso/sam/docs/my-pptx -node convert.cjs -# → my-presentation.pptx 생성됨 -``` - ---- - -**최종 업데이트**: 2026-03-01 diff --git a/sam/docs/guides/server-how-it-works.md b/sam/docs/guides/server-how-it-works.md deleted file mode 100644 index 974693b..0000000 --- a/sam/docs/guides/server-how-it-works.md +++ /dev/null @@ -1,247 +0,0 @@ -# SAM 서버 동작 원리 초보자 가이드 - -> **작성일**: 2026-02-22 -> **대상**: SAM 프로젝트에 새로 합류한 개발자 - ---- - -## 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 -``` - ---- - -## 관련 문서 - -| 문서 | 설명 | -|------|------| -| [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 diff --git a/sam/docs/guides/table-design-guide.md b/sam/docs/guides/table-design-guide.md deleted file mode 100644 index a920f08..0000000 --- a/sam/docs/guides/table-design-guide.md +++ /dev/null @@ -1,486 +0,0 @@ -# SAM 테이블 설계 가이드 — 비전문가용 - -> **작성일**: 2026-03-02 -> **대상 독자**: 개발자, 기획자, 관리자 — 데이터베이스를 잘 모르는 분도 읽을 수 있습니다 -> **관련 정책**: `standards/options-column-policy.md` (개발자 전용 상세 규칙) - ---- - -## 1. 이 문서는 왜 필요한가? - -SAM은 **여러 회사가 하나의 시스템을 공유**하는 구조입니다. -A회사, B회사, C회사가 모두 같은 프로그램을 쓰지만, 각 회사가 필요한 정보는 다릅니다. - -이 문서는 SAM에서 **데이터를 어떻게 저장하는지**, 그 설계 철학을 누구나 이해할 수 있도록 설명합니다. - ---- - -## 2. 기본 개념: 데이터베이스 테이블이란? - -데이터베이스 테이블은 **엑셀 시트**와 같습니다. - -``` -"주문" 테이블 (= 엑셀 시트) - - 열(컬럼) → 주문번호 │ 고객명 │ 금액 │ 상태 - ───────────────────────────────────────────────────────── - 행(레코드) 1 → ORD-001 │ 김철수 │ 500,000 │ 완료 - 행(레코드) 2 → ORD-002 │ 이영희 │ 300,000 │ 진행중 - 행(레코드) 3 → ORD-003 │ 박민수 │ 800,000 │ 대기 -``` - -- **열(컬럼)** = 정보의 종류 (주문번호, 고객명, 금액...) -- **행(레코드)** = 실제 데이터 한 건 (주문 1건) - ---- - -## 3. 문제: 회사마다 필요한 정보가 다르다 - -SAM은 여러 회사가 같은 테이블을 공유합니다. - -``` -같은 "주문" 테이블을 쓰는데... - - 🏭 A회사 (블라인드 제조) - → "절곡 각도", "날개 수" 정보가 필요해요 - - 🏭 B회사 (스크린 제조) - → "메시 밀도", "소재 종류" 정보가 필요해요 - - 🏭 C회사 (셔터 제조) - → "날개 간격", "색상 코드" 정보가 필요해요 -``` - ---- - -### 3.1 전통적인 해결 방법 (SAM은 이렇게 안 합니다) - -필요할 때마다 엑셀에 열을 추가하는 것처럼, 테이블에 컬럼을 추가합니다. - -``` -"주문" 테이블 — 전통적 방식 - - 주문번호 │ 고객명 │ 금액 │ 절곡각도 │ 날개수 │ 메시밀도 │ 소재 │ 날개간격 │ 색상코드 - ───────────────────────────────────────────────────────────────────────────────── - ORD-001 │ 김철수 │ 50만 │ 45도 │ 12개 │ (빈칸) │ (빈칸) │ (빈칸) │ (빈칸) ← A회사 - ORD-002 │ 이영희 │ 30만 │ (빈칸) │ (빈칸)│ 18 │ 폴리 │ (빈칸) │ (빈칸) ← B회사 - ORD-003 │ 박민수 │ 80만 │ (빈칸) │ (빈칸)│ (빈칸) │ (빈칸) │ 25mm │ #FF0000 ← C회사 -``` - -**문제점:** - -- 회사가 100개면? 열이 수백 개로 늘어남 -- 각 회사는 자기 것 빼고 전부 빈칸 -- 새 회사가 들어올 때마다 시스템 전체를 수정해야 함 -- 열 추가 = 시스템 중단 위험이 있는 작업 - ---- - -### 3.2 SAM의 해결 방법: "메모칸(options)" 하나로 통합 - -**핵심 열만 남기고**, 나머지는 **메모칸 하나**에 자유롭게 적습니다. - -``` -"주문" 테이블 — SAM 방식 - - 주문번호 │ 고객명 │ 금액 │ 상태 │ options (메모칸) - ──────────────────────────────────────────────────────────────────────── - ORD-001 │ 김철수 │ 50만 │ 완료 │ {"절곡각도": 45, "날개수": 12} ← A회사 - ORD-002 │ 이영희 │ 30만 │ 진행 │ {"메시밀도": 18, "소재": "폴리에스터"} ← B회사 - ORD-003 │ 박민수 │ 80만 │ 대기 │ {"날개간격": 25, "색상코드": "#FF0000"} ← C회사 - ORD-004 │ 최지은 │ 40만 │ 대기 │ null ← 메모 없음 -``` - -**`options`** = JSON이라는 형식의 메모칸. `{ }` 안에 자유롭게 정보를 넣을 수 있습니다. - ---- - -## 4. 어떤 정보를 열(컬럼)로 만들고, 어떤 정보를 메모칸(options)에 넣나? - -이것이 SAM 테이블 설계의 **가장 중요한 판단 기준**입니다. - -### 4.1 판단 흐름 (5가지 질문) - -새로운 정보를 저장해야 할 때, 아래 질문에 답합니다. - -``` -질문 1. 이 정보로 다른 테이블의 데이터를 연결(참조)하나? - 예: 고객ID로 고객 테이블을 찾는다 - → YES: 일반 컬럼 - -질문 2. 이 정보로 자주 검색(필터)하나? - 예: "완료" 상태인 주문만 보여줘 - → YES: 일반 컬럼 - -질문 3. 이 정보로 정렬하나? - 예: 최신 주문부터 보여줘 - → YES: 일반 컬럼 - -질문 4. 이 정보가 절대 중복되면 안 되나? - 예: 주문번호는 세상에 하나뿐이어야 한다 - → YES: 일반 컬럼 - -질문 5. 이 정보로 합계/평균을 계산하나? - 예: 이번 달 매출 합계 - → YES: 일반 컬럼 - -질문 1~5 전부 NO → options 메모칸에 저장 -``` - -### 4.2 실생활 예시로 비교 - -#### 예시 1: "주문" 테이블 - -| 정보 | 어디에 저장? | 이유 | -|------|:-----------:|------| -| 주문번호 | **일반 컬럼** | 중복 불가 + 검색 필수 | -| 고객 ID | **일반 컬럼** | 고객 테이블과 연결 | -| 금액 | **일반 컬럼** | 합계 계산 필요 | -| 상태 (진행/완료) | **일반 컬럼** | 필터(검색) 필수 | -| 생성일 | **일반 컬럼** | 정렬 필요 | -| 배송지 주소 | **options** | 부가 정보, 검색 안 함 | -| 수신자 이름 | **options** | 부가 정보 | -| 수신자 연락처 | **options** | 부가 정보 | -| 특이사항 메모 | **options** | 있어도 되고 없어도 됨 | - -**실제 SAM 코드에서 주문(Order) 테이블:** - -``` -일반 컬럼: id, tenant_id, order_number, client_id, total_amount, status, created_at -options: {"shipping_cost_code":"착불", "receiver":"홍길동", - "receiver_contact":"010-1234-5678", - "shipping_address":"서울 강남구 역삼동 123"} -``` - -#### 예시 2: "입고검사" 테이블 - -| 정보 | 어디에 저장? | 이유 | -|------|:-----------:|------| -| 품목 ID | **일반 컬럼** | 품목 테이블과 연결 | -| 수량 | **일반 컬럼** | 합계 계산 | -| 입고일 | **일반 컬럼** | 정렬 + 검색 | -| 제조사 | **options** | 모든 입고에 있지는 않음 | -| 검사 결과 (합격/불합격) | **options** | 검사를 안 하는 회사도 있음 | -| 검사일 | **options** | 선택적 정보 | - -**실제 SAM 코드에서 입고(Receiving) 테이블:** - -``` -일반 컬럼: id, tenant_id, item_id, quantity, received_at, status -options: {"manufacturer":"삼성전자", - "inspection_status":"합격", - "inspection_date":"2026-03-01"} -``` - -> 검사 결과가 options에 있는 이유: **모든 회사가 입고검사를 하는 것은 아닙니다.** -> A회사는 검사를 하고, B회사는 안 합니다. 이걸 일반 컬럼으로 만들면 B회사에겐 항상 빈칸입니다. - -#### 예시 3: "공정" 테이블 - -| 정보 | 어디에 저장? | 이유 | -|------|:-----------:|------| -| 공정 코드 | **일반 컬럼** | 중복 불가 + 검색 | -| 공정명 | **일반 컬럼** | 검색 + 표시 | -| 담당 부서 | **일반 컬럼** | 필터 | -| 작업일지 필요 여부 | **options** | 회사별로 다름 | -| 검사 필요 여부 | **options** | 회사별로 다름 | - -``` -일반 컬럼: id, tenant_id, process_code, process_name, department -options: {"needs_work_log": true, "needs_inspection": false} -``` - ---- - -## 5. 메모칸(options)의 실제 모습: JSON이란? - -`options`에 저장되는 데이터 형식은 **JSON**입니다. -JSON은 프로그래밍 세계의 "구조화된 메모장"이라고 생각하면 됩니다. - -### 5.1 JSON 기본 문법 - -``` -{ ← 시작 - "키": "값", ← 문자(텍스트) - "이름": "홍길동", - "나이": 30, ← 숫자 (따옴표 없음) - "합격": true, ← 참/거짓 (따옴표 없음) - "메모": null ← 값 없음 -} ← 끝 -``` - -### 5.2 중첩(nested) — 메모 안의 메모 - -``` -{ - "배송": { ← 배송 관련 정보를 묶음 - "주소": "서울 강남구 역삼동", - "수신자": "홍길동", - "연락처": "010-1234-5678" - }, - "검사": { ← 검사 관련 정보를 묶음 - "결과": "합격", - "검사일": "2026-03-01", - "검사자ID": 5 - } -} -``` - -### 5.3 목록(배열) — 여러 개를 나열 - -``` -{ - "선택지": [ ← 대괄호 [ ] = 목록 - {"label": "블라인드", "value": "blind"}, - {"label": "스크린", "value": "screen"}, - {"label": "셔터", "value": "shutter"} - ] -} -``` - -> 이 형태는 드롭다운 메뉴의 선택지 목록을 저장할 때 사용합니다. - ---- - -## 6. 멀티테넌시란? — 여러 회사가 하나의 시스템을 쓰는 구조 - -### 6.1 개념 - -``` -┌──────────────────────────────────────────────┐ -│ SAM 시스템 (하나의 프로그램) │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ A회사 │ │ B회사 │ │ C회사 │ │ -│ │ tenant=1 │ │ tenant=2 │ │ tenant=3 │ │ -│ │ │ │ │ │ │ │ -│ │ 블라인드 │ │ 스크린 │ │ 셔터 │ │ -│ │ 제조 │ │ 제조 │ │ 제조 │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -│ 같은 테이블을 쓰지만, │ -│ tenant_id로 데이터가 완전히 분리됨 │ -└──────────────────────────────────────────────┘ -``` - -### 6.2 tenant_id = 회사 식별 번호 - -모든 테이블의 모든 행에 `tenant_id`(회사 번호)가 붙어 있습니다. - -``` -"주문" 테이블 - - id │ tenant_id │ 주문번호 │ 금액 │ options - ─────────────────────────────────────────────────────────── - 1 │ 1 │ ORD-001 │ 50만 │ {"절곡각도": 45} ← A회사 데이터 - 2 │ 1 │ ORD-002 │ 30만 │ {"절곡각도": 90} ← A회사 데이터 - 3 │ 2 │ ORD-001 │ 80만 │ {"메시밀도": 18} ← B회사 데이터 - 4 │ 3 │ ORD-001 │ 40만 │ {"날개간격": 25} ← C회사 데이터 -``` - -**A회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 1`인 데이터만 보여줌 -**B회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 2`인 데이터만 보여줌 - -> A회사는 B회사의 데이터를 절대 볼 수 없습니다. 시스템이 자동으로 차단합니다. - -### 6.3 options + tenant_id = 강력한 조합 - -이 두 가지가 합쳐지면: - -``` -같은 테이블, 같은 컬럼 구조인데 - ✅ 회사마다 다른 데이터 (tenant_id로 분리) - ✅ 회사마다 다른 속성 (options로 유연하게) - ✅ 시스템 수정 없이 확장 가능 -``` - ---- - -## 7. SAM 테이블의 표준 구조 - -SAM에서 새 테이블을 만들면 항상 이 구조를 따릅니다. - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SAM 표준 테이블 구조 │ -│ │ -│ ① 식별자 │ -│ id — 자동 생성 번호 (1, 2, 3...) │ -│ tenant_id — 어느 회사의 데이터인지 │ -│ │ -│ ② 핵심 정보 (검색/정렬/연결에 쓰는 것만) │ -│ code — 코드 (중복 불가) │ -│ status — 상태 (검색용) │ -│ is_active — 사용 여부 │ -│ sort_order — 표시 순서 │ -│ (+ FK 컬럼들) — 다른 테이블 연결 │ -│ │ -│ ③ 메모칸 │ -│ options — 나머지 전부 (JSON) │ -│ │ -│ ④ 감사 기록 (자동) │ -│ created_by — 누가 만들었나 │ -│ updated_by — 누가 수정했나 │ -│ deleted_by — 누가 삭제했나 │ -│ created_at — 언제 만들었나 │ -│ updated_at — 언제 수정했나 │ -│ deleted_at — 언제 삭제했나 (휴지통 개념) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 영역별 설명 - -| 영역 | 역할 | 비유 | -|------|------|------| -| ① 식별자 | "이 데이터가 누구 것인지" 구분 | 우편물의 받는 사람 + 주소 | -| ② 핵심 정보 | 검색, 정렬, 집계에 꼭 필요한 정보 | 엑셀의 고정 열 | -| ③ options | 회사마다 다른 부가 정보 | 엑셀의 "비고" 칸 (자유 서식) | -| ④ 감사 기록 | 언제 누가 뭘 했는지 자동 추적 | CCTV 기록 | - ---- - -## 8. 실제 SAM에서 options를 쓰는 테이블들 (22개) - -현재 SAM에서 options 메모칸을 사용하는 주요 테이블입니다. - -| 테이블 | 한글명 | options에 저장하는 정보 예시 | -|--------|--------|--------------------------| -| `orders` | 주문 | 배송지, 수신자, 연락처, 담당자 | -| `quotes` | 견적 | 견적 요약, 비용 항목, 가격 조정 | -| `receivings` | 입고 | 제조사, 검사 결과, 검사일 | -| `work_orders` | 작업지시 | 절곡 정보 (bending_info) | -| `work_order_items` | 작업지시 항목 | 작업 결과, 양품/불량 수량, LOT번호 | -| `processes` | 공정 | 작업일지 필요 여부, 검사 필요 여부 | -| `order_nodes` | 주문 노드 | 위치, 구역, 층, 실 (트리 구조) | -| `products` | 제품 | 동적 옵션 (라벨, 값, 단위) | -| `items` | 품목 | 품목별 동적 속성 | -| `materials` | 자재 | 자재 추가 속성 | -| `menus` | 메뉴 | 섹션, 메뉴 타입, 필요 권한 | -| `users` | 사용자 | 개인 설정/환경설정 | -| `tenants` | 회사(테넌트) | 회사 규모, 업종 | -| `document_template_section_fields` | 문서 양식 필드 | 선택지 목록, API 경로 | -| `item_fields` | 품목 필드 정의 | 필드별 세부 설정 | - ---- - -## 9. 자주 묻는 질문 (FAQ) - -### Q1. options에 넣으면 검색이 안 되나요? - -**아닙니다.** MySQL 8.0은 JSON 내부도 검색할 수 있습니다. - -``` -일반 컬럼 검색: "상태가 '완료'인 주문 찾아줘" → 매우 빠름 -options 검색: "제조사가 '삼성'인 입고 찾아줘" → 가능하지만 조금 느림 -``` - -다만, **매일 수천 번 검색하는 정보**라면 일반 컬럼으로 승격하는 것이 맞습니다. -가끔 검색하는 정보라면 options로 충분합니다. - -### Q2. options에 아무 정보나 마음대로 넣을 수 있나요? - -기술적으로는 가능하지만, 개발팀 내부에서 **어떤 키를 쓸지 미리 약속**합니다. - -``` -✅ 약속된 키: {"manufacturer": "삼성", "inspection_status": "합격"} -❌ 멋대로: {"asdf": 123, "temp_data": "???"} -``` - -코드에서 상수로 정의하여 일관성을 유지합니다. - -### Q3. 전통적 방식보다 뭐가 좋은 건가요? - -| 비교 항목 | 전통적 방식 (열 추가) | SAM 방식 (options JSON) | -|----------|:------------------:|:---------------------:| -| 새 정보 추가 시 | 시스템 수정 필요 | 코드만 변경 | -| 다른 회사에 영향 | 있음 (전체 구조 변경) | 없음 | -| 빈칸(null) 낭비 | 많음 | 없음 | -| 검색 속도 | 빠름 | 조금 느림 (충분히 실용적) | -| 유연성 | 낮음 | 높음 | -| 시스템 중단 위험 | 있음 (대형 테이블 수정 시) | 없음 | - -### Q4. 그럼 모든 정보를 options에 넣으면 되지 않나요? - -**아닙니다.** 핵심 정보는 반드시 일반 컬럼으로 만들어야 합니다. - -``` -❌ 나쁜 예: 모든 것을 options에 - - id │ tenant_id │ options - ────────────────────────────────────────────────────────────── - 1 │ 1 │ {"주문번호":"ORD-001", "금액":500000, "상태":"완료", ...} - - → 주문번호 검색 느림, 금액 합계 계산 불가, 중복 방지 불가 -``` - -``` -✅ 좋은 예: 핵심은 컬럼, 부가는 options - - id │ tenant_id │ order_number │ amount │ status │ options - ────────────────────────────────────────────────────────────── - 1 │ 1 │ ORD-001 │ 500000 │ 완료 │ {"배송지":"서울..."} - - → 검색 빠름, 합계 가능, 중복 방지 가능, 부가 정보도 유연 -``` - -### Q5. options 데이터는 화면에서 어떻게 보이나요? - -사용자 화면에서는 options 안에 있는지, 일반 컬럼인지 **구분할 수 없습니다**. -프로그램이 자동으로 꺼내서 보여줍니다. - -``` -화면에 보이는 모습: - - ┌─────────────────────────────────┐ - │ 입고 상세 정보 │ - │ │ - │ 품목: SUS304 스틸 │ ← 일반 컬럼 - │ 수량: 100개 │ ← 일반 컬럼 - │ 입고일: 2026-03-01 │ ← 일반 컬럼 - │ 제조사: 삼성전자 │ ← options에서 꺼냄 - │ 검사결과: 합격 │ ← options에서 꺼냄 - │ 검사일: 2026-03-01 │ ← options에서 꺼냄 - └─────────────────────────────────┘ - - 사용자는 어디에 저장되어 있는지 알 필요 없음! -``` - ---- - -## 10. 한 장 요약 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ │ -│ SAM 테이블 설계 = "핵심만 컬럼, 나머진 메모칸(options)" │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ tenant_id → 어느 회사 것인지 (자동 격리) │ │ -│ │ 핵심 컬럼들 → 검색/정렬/연결/집계에 쓰는 필수 정보 │ │ -│ │ options → 나머지 전부 (회사마다 다른 부가 정보) │ │ -│ │ 감사 컬럼들 → 누가/언제 만들고/수정하고/삭제했는지 │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ 이렇게 하면: │ -│ ✅ 회사 추가해도 테이블 구조 안 바꿈 │ -│ ✅ 새 정보 추가해도 시스템 수정 최소화 │ -│ ✅ 회사마다 다른 정보를 유연하게 저장 │ -│ ✅ 데이터 보안 (회사 간 완전 분리) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 관련 문서 - -| 문서 | 설명 | 대상 | -|------|------|------| -| [options-column-policy.md](../standards/options-column-policy.md) | 개발자용 상세 정책 (코드 규칙, 마이그레이션 패턴) | 개발자 | -| [database/README.md](../system/database/README.md) | DB 스키마 전체 현황 (220개 모델) | 개발자 | -| [PROJECT_DEVELOPMENT_POLICY.md](PROJECT_DEVELOPMENT_POLICY.md) | 개발 공통 정책 (테이블 생성 절차) | 개발자 | -| [system/overview.md](../system/overview.md) | SAM 시스템 전체 아키텍처 | 전체 | - ---- - -**최종 업데이트**: 2026-03-02 diff --git a/sam/docs/plans/SAM_General_Rule_Storyboard_D1.0.md b/sam/docs/plans/SAM_General_Rule_Storyboard_D1.0.md deleted file mode 100644 index ae23c7b..0000000 --- a/sam/docs/plans/SAM_General_Rule_Storyboard_D1.0.md +++ /dev/null @@ -1,737 +0,0 @@ -# SAM General Rule Storyboard D1.0 - -> **작성일**: 2026-01-16 -> **버전**: D1.0 -> **원본**: `SAM_General_Rule_Storyboard_D1.0_260116.pdf` (43페이지) -> **상태**: PC 섹션 정리 완료 - ---- - -## 1. 문서 이력 - -| 날짜 | 버전 | 주요 내용 | 세부 내용 | -|------|------|----------|----------| -| 2026-01-15 | D0.9 | 초안 | General Rule - PC, 태블릿, 모바일 UIUX 공통 작성 | -| 2026-01-16 | D1.0 | 작성 | PC 섹션 정리 33p | - ---- - -## 2. 인터랙션 (Interaction) - -> **페이지**: 4 - -사용자 입력 제스처 및 적용 여부를 정의한다. - -| Type | 제스처/마크 | 설명 | 적용 | -|------|-----------|------|------| -| 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 | - ---- - -## 3. 반응형 웹 (Responsive Web) - -> **페이지**: 5 - -### 3.1 브레이크 포인트 - -| 디바이스 | 브레이크 포인트 | Tailwind 접두사 | -|---------|---------------|----------------| -| 모바일 | < 640px | 기본 | -| 태블릿 | 768px ~ 1280px | `md` | -| 데스크탑 | 1280px+ | `lg` | -| 대형 모니터 | 1920px+ | `xl` | - -### 3.2 레이아웃 구성 - -**PC Web 레이아웃**: -1. Contents 영역 -2. Footer 영역 - -**Mobile Web 레이아웃**: -1. Contents 영역 -2. Footer 영역 - ---- - -## 4. 화면 템플릿 (Screen Template) - -> **페이지**: 6 - -모바일 웹 화면 구조를 정의한다. - -| 영역 | 코드 | 설명 | -|------|------|------| -| Status bar | A | 안테나, 통화, 배터리 등 시스템 OS 관리 영역. 모든 페이지 상단에 존재 | -| Browser 영역 | B | 브라우저 기능 영역 | -| Title 영역 | C | 텍스트 또는 기능 버튼으로 구현됨. 텍스트는 기본 가운데 정렬 | -| Content 영역 | D | 컨텐츠 내용 표시. 컨텐츠 길이가 길어질 경우 스크롤 제공 | -| Browser bar 영역 | E | 브라우저 유틸 바 영역 | -| Keypad 영역 | F | 키보드 입력할 때 활성화. 모든 페이지 위에 덮어쓰기 구현 | - ---- - -## 5. 메시지 (Notifications) - -> **페이지**: 7 - -| 유형 | 설명 | -|------|------| -| 알림 Alert | 사용자에게 상황을 알려주기 위한 팝업. `[확인]` 버튼 제공 | -| 확인 Alert | 사용자에게 확인이 필요할 경우 제공되는 팝업. `[취소]` `[확인]` 버튼 제공 | -| 토스트 메시지 | 단순 Notify. 2~3초 후 페이지 내에서 Fade out | - ---- - -## 6. GNB, LNB, 푸터 - -> **페이지**: 8 - -PC 화면의 전체 레이아웃 구조를 정의한다. - -### 6.1 구성 요소 - -| 번호 | 영역 | 설명 | -|------|------|------| -| 01 | 메뉴 버튼 | 클릭: 메뉴 영역(06) 축소/확장 토글. 디폴트: 메뉴 영역 확장 상태 | -| 02 | SAM 로고 버튼 | 클릭: 대시보드 화면으로 이동 | -| 03 | 알림 버튼 | 클릭: 알림 팝업 표시 | -| 04 | 개인 정보 버튼 | 항목: 디폴트 이미지, 이름, 직급. 클릭: 마이페이지 팝업 표시 | -| 05 | 회사 로고 | 회사정보 화면에서 등록한 로고 표시. 회사 변경 선택 시 해당 로고 변경 | -| 06 | 메뉴 영역 | 메뉴 클릭: 하위 메뉴 있을 경우 하단에 표시, 없을 경우 해당 메뉴 화면으로 이동. 목록 길 경우 해당 영역 내 스크롤 처리 | -| 07 | MES 메뉴 영역 | 영업관리, 판매관리, 구매관리 등 해당하는 MES 메뉴 영역 표시 | -| 08 | 푸터 영역 | 모든 화면 하단 공통 표시 | -| 09 | SAM AI 채팅 버튼 | 클릭: SAM AI 채팅 팝업 표시 | - -### 6.2 메뉴 목록 - -- 대시보드 -- MES 메뉴 -- 인사관리 -- 전자결재 -- 게시판 -- 회계관리 -- 기준정보 -- 보고서 및 분석 -- 계정정보 -- 회사정보 -- 구독관리 -- 결제내역 -- 고객센터 - -### 6.3 푸터 내용 - -``` -(C) 2025 SAM. All right reserved. -Codebridge X -상호: 코드 브릿지 엑스 대표: 이경호 사업자등록번호: 123-45-12345 -주소: 서울특별시 강서구 양천로 583 우림블루나인 B동 1602호 (우: 07547) -팩스: 02-123-1234 통신판매업신고번호: 제 2019-서울강서-0001호 -서비스이용문의: 02-1234-1234 이메일: cs@a.com -서비스 이용약관 | 개인정보 취급방침 -``` - ---- - -## 7. 메뉴, 페이지, 섹션, 항목 영역 - -> **페이지**: 9 - -### 7.1 영역 구분 - -| 번호 | 영역 | 설명 | -|------|------|------| -| 01 | 메뉴 영역 | 축소 상태 | -| 02 | 페이지 영역 | - | -| 03 | 섹션 영역 | - | -| 04 | 항목 영역 | - | - -### 7.2 텍스트 오버플로우 처리 - -텍스트가 영역보다 길 경우 "텍스트+..." 형태로 표시한다. - ---- - -## 8. 메뉴 목록 (3Depth) - -> **페이지**: 10 - -### 8.1 메뉴 계층 구조 - -| 번호 | 레벨 | 설명 | -|------|------|------| -| 01 | 대메뉴 | 1Depth 메뉴 | -| 02 | 중메뉴 | 2Depth 메뉴 (대메뉴 클릭 시 하단에 표시) | -| 03 | 소메뉴 | 3Depth 메뉴 (중메뉴 클릭 시 하단에 표시) | - -**메뉴 확장 예시**: -``` -대시보드 -MES 메뉴 -인사관리 -전자결재 - - 중메뉴명 - - 중메뉴명 - · 소메뉴명 - · 소메뉴명 - - 중메뉴명 - - 중메뉴명 - - 중메뉴명 -게시판 -... -``` - ---- - -## 9. 알림 팝업 - -> **페이지**: 11 -> **경로**: 메인 > 알림 팝업 - -### 9.1 구성 요소 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 알림 목록 | 각 디폴트 썸네일, 종류(공지사항, 안내), 제목/내용, 전송일시 표시. 클릭: 해당 상세 화면으로 이동. 최신순 10개까지 표시 | -| 02 | New 아이콘 | 새 알림일 경우 New 아이콘 표시. 해당 알림 클릭 시 사라짐 | -| 02-1 | 붉은 점 아이콘 | 새 알림이 있을 경우 표시. 해당 알림 모두 클릭 시 사라짐 | - ---- - -## 10. 마이페이지 팝업 - -> **페이지**: 12 -> **경로**: 메인 > 마이페이지 팝업 - -### 10.1 구성 요소 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 계정 아이디 (이메일) | 이메일 주소 표시 (예: `name@company.com`) | -| 02 | 회사 셀렉트 박스 | 종류: 회사명 목록 (해당 계정이 생성한 회사(테넌트) 목록 표시). 정렬: 등록순. 한 회사만 소유중일 경우에는 해당 영역 숨김 | -| 03 | 로그아웃 버튼 | 클릭: "정말 로그아웃하시겠습니까?" 로그아웃 확인 Alert 표시. 확인 버튼 클릭시 로그아웃 처리 | - ---- - -## 11. 셀렉트 박스 - -> **페이지**: 13 - -### 11.1 기본 셀렉트 박스 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 셀렉트 박스 | 클릭: 하단에 종류 목록 표시 | -| 02 | 종류 목록 | 목록 중 하나만 선택 가능 | - -### 11.2 다중 선택 셀렉트 박스 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 03 | 다중 선택 셀렉트 박스 | 선택된 첫번째 항목명 + 추가 수 표시. 텍스트 영역 부족할 경우 `항목..+3` 형태로 표시 | -| 04 | 다중 선택 종류 목록 | 목록 중 복수 선택 가능. 전체 선택 시 전체 선택/해제 토글 | - -### 11.3 검색 셀렉트 박스 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 05 | 검색 영역 | 검색어 입력 후 엔터 또는 검색 아이콘 클릭 시 검색 상태로 전환되며 검색 결과 표시 | -| 05-3 | 삭제 버튼 | 클릭: 검색어 삭제 처리, 전체 종류 목록 표시 | - -### 11.4 셀렉트 박스 유형 정리 - -| 유형 | 단일 선택 | 다중 선택 | 검색 | 검색+다중 선택 | -|------|----------|----------|------|--------------| -| 선택 방식 | 하나만 | 복수 | 하나만 | 복수 | -| 검색 기능 | X | X | O | O | -| 전체 선택 | X | O | X | O | - ---- - -## 12. 가이드 메시지 - -> **페이지**: 14 - -상황에 따라 입력 필드 하단 또는 Alert에 가이드 메시지를 표시한다. - -### 12.1 표시 규칙 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 가이드 메시지 표시 위치 | 입력 필드 하단에 표시 | -| - | 긍정 메시지 | 녹색으로 표시 | -| 01-1 | 부정 메시지 | 붉은색으로 표시 | - ---- - -## 13. 태블릿/모바일 헤더 - -> **페이지**: 15 - -### 13.1 동작 규칙 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 태블릿/모바일 헤더 | 하단으로 스크롤 시 숨김. 역스크롤 시 표시 | - -### 13.2 적용 화면 - -- TABLET 가로 목록 -- TABLET 세로 목록 -- MOBILE 가로 목록 -- MOBILE 세로 목록 - ---- - -## 14. 태블릿/모바일 바텀 버튼 영역 - -> **페이지**: 16 - -### 14.1 동작 규칙 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 태블릿/모바일 바텀 버튼 영역 | 최하단 바텀에 플로팅 표시. 하단으로 스크롤 시 숨김. 역스크롤 시 표시 | - -### 14.2 버튼 예시 - -- `[수정]` `[삭제]` - ---- - -## 15. 공지 팝업 - -> **페이지**: 17 - -### 15.1 구성 - -| 항목 | 설명 | -|------|------| -| 대상 | 전체, 설정 부서 | -| 내용 | 설정 기간동안 대상에게 팝업 표시 | - -### 15.2 구성 요소 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 팝업 내용 영역 | 이미지, 텍스트 | -| 02 | 1일간 이 창을 열지 않음 체크박스 | 클릭: 체크 설정/해제 토글. 디폴트: 체크 해제 상태. 체크 설정 시 1일 동안 팝업 미표시 (자정 기준) | - ---- - -## 16. 목록 화면 - 4단계 반응형 - -> **페이지**: 18~25 - -PC, TABLET, MOBILE 환경에서 목록 화면의 4단계 반응형 표시를 정의한다. - -### 16.1 반응형 단계 개요 - -| 단계 | 디바이스 | 화면명 | -|------|---------|--------| -| 1단계 | PC | PC_목록 | -| 2단계 | TABLET 가로 | TABLET_가로_목록 | -| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_목록, MOBILE_가로_목록 | -| 4단계 | MOBILE 세로 | MOBILE_세로_목록 | - -### 16.2 PC_목록 (1단계) - -> **페이지**: 19 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 헤더 영역 | 항목 클릭: 값이 국문/영문/숫자일 경우 오름/내림차순으로 토글 | -| 02 | 정렬 아이콘 | 현재 칼럼으로 정렬 상태일 경우에만 표시 | - -**목록 테이블 예시 칼럼**: - -| 칼럼 | 설명 | -|------|------| -| 시공번호 | 고유 식별 번호 | -| 거래처 | 회사명 | -| 현장명 | 현장 이름 | -| 공사PM | 담당 PM | -| 작업반장 | 작업반장 이름 | -| 작업자 | 작업자 수 | -| 시공투입일 | 시공 투입 날짜 | -| 시공완료일 | 시공 완료 날짜 | -| 상태 | 시공대기, 시공진행, 시공완료 | - -### 16.3 TABLET_가로_목록 (2단계) - -> **페이지**: 20 - -- PC와 동일한 테이블 구조 -- 사이드 메뉴가 아이콘 축소 상태로 변경 - -### 16.4 TABLET_세로_목록 (3단계) - -> **페이지**: 21~22 - -- 테이블 대신 카드형 목록으로 전환 -- 각 카드에 시공번호와 상태 표시 -- 카드 클릭 시 확장되어 상세 정보 표시 - -**확장 시 표시 항목**: - -| 필드 | 예시 값 | -|------|---------| -| 거래처 | 회사명 | -| 현장명 | 현장명 | -| 공사PM | 홍길동 | -| 작업반장 | 홍길동 | -| 작업자 | 3 | -| 시공투입일 | 2026-01-01 | -| 시공완료일 | 2026-01-01 | - -### 16.5 MOBILE_가로_목록 (3단계) - -> **페이지**: 23~24 - -- 카드형 목록 -- 시공번호와 상태 표시 -- 클릭 시 확장하여 상세 항목 표시 - -### 16.6 MOBILE_세로_목록 (4단계) - -> **페이지**: 25 - -- 카드형 목록 (세로 스크롤) -- 클릭 시 확장하여 상세 항목 표시 -- 확장 시 거래처, 현장명, 공사PM, 작업반장, 작업자, 시공투입일, 시공완료일 표시 - ---- - -## 17. 상세 화면 - 4단계 반응형 - -> **페이지**: 26~31 - -PC, TABLET, MOBILE 환경에서 상세 화면의 4단계 반응형 표시를 정의한다. - -### 17.1 반응형 단계 개요 - -| 단계 | 디바이스 | 화면명 | -|------|---------|--------| -| 1단계 | PC | PC_상세 | -| 2단계 | TABLET 가로 | TABLET_가로_상세 | -| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_상세, MOBILE_가로_상세 | -| 4단계 | MOBILE 세로 | MOBILE_세로_상세 | - -### 17.2 PC_상세 (1단계) - -> **페이지**: 27 - -- 페이지 제목: "메뉴 상세" + 설명: "메뉴 상세를 관리합니다" -- 섹션명: "시공 정보" -- 버튼: `[수정]` `[삭제]` - -**표시 항목 예시**: - -| 필드 | 예시 값 | -|------|---------| -| 시공번호 | 123123 | -| 상태 | 시공진행 | -| 현장 | 현장명 | -| 작업반장 | 홍길동 (셀렉트 박스) | -| 시공투입일 | 2025-12-15 | -| 시공완료일 | 2025-12-15 | -| 항목명 | 항목 (다수) | - -### 17.3 TABLET_가로_상세 (2단계) - -> **페이지**: 28 - -- PC와 동일한 상세 정보 표시 -- 사이드 메뉴 아이콘 축소 상태 - -### 17.4 TABLET_세로_상세 (3단계) - -> **페이지**: 29 - -- 항목 수가 줄어들며 스크롤로 나머지 확인 - -### 17.5 MOBILE_가로_상세 (3단계) - -> **페이지**: 30 - -- 상세 항목을 세로 배치 -- 바텀에 `[수정]` `[삭제]` 버튼 플로팅 - -### 17.6 MOBILE_세로_상세 (4단계) - -> **페이지**: 31 - -- 모든 항목 세로 배치 -- 바텀에 `[수정]` `[삭제]` 버튼 플로팅 - ---- - -## 18. PC 섹션 정리 - -> **페이지**: 32~33 - -PC 화면의 섹션 레이아웃 및 필터/정렬 구성을 정의한다. - -### 18.1 필터 규칙 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 라디오 버튼형 필터 | 선택 값이 2개일 경우 사용 (예: 수취/발행) | -| 02 | 필터 셀렉트 박스 | 최소로만 활용 | -| 03 | 표 헤더 정렬 | 표 헤더 정렬로 정렬 셀렉트 박스는 삭제 | - -### 18.2 PC 섹션 구성 요소 - -**상단 영역**: -- 페이지 제목 + 설명 -- 집계 카드 (예: `수취어음 55건`, `발행어음 1건`, `만기임박 5건`, `결제완료 15건`) -- 기간 선택 (날짜 범위 + 단축 버튼: 전전월, 어제, 오늘, 전월, 당월, 당해년도) -- 버튼: `[버튼명]` `[버튼명]` `[버튼명]` -- 탭: 탭1, 탭2, 탭3 - -**필터 영역**: -- 셀렉트 박스 필터 (전체) -- 라디오 버튼형 필터 (수취/발행) -- 상태 셀렉트 박스 (보관중) -- `[저장]` 버튼 - -**목록 테이블 예시**: - -| No. | 어음번호 | 구분 | 거래처 | 금액 | 발행일 | 만기일 | 차수 | 상태 | -|-----|---------|------|--------|------|--------|--------|------|------| -| 7 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 1 | 보관중 | -| 6 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 2 | 만기임박 | - -**하단 정보**: `총 7건` / `1건 선택` - ---- - -## 19. TBD (미정) - -> **페이지**: 34 - -추후 결정 예정 영역이다. - ---- - -## 20. 나의 메뉴 - -> **페이지**: 35~38 - -### 20.1 나의 메뉴 - 없음 - -> **페이지**: 35 - -- 나의 메뉴가 설정되지 않은 상태 -- 콘텐츠 상단에 `[...]` 아이콘만 표시 - -### 20.2 나의 메뉴 - 있음 - -> **페이지**: 36 - -- 나의 메뉴가 1개 설정된 상태 -- 콘텐츠 상단에 나의 메뉴명 탭 표시 (예: `메뉴관리`) - -### 20.3 나의 메뉴 - 여러 줄 - -> **페이지**: 37 - -- 나의 메뉴가 여러 개 설정된 상태 -- 콘텐츠 상단에 여러 메뉴명이 나열됨 -- 줄바꿈되어 여러 줄로 표시 가능 (예: `메뉴관리 메뉴명 메뉴명 메뉴명 ...`) - -### 20.4 나의 메뉴 - 메뉴 영역에 통합 - -> **페이지**: 38 - -- 좌측 메뉴 영역에 "메뉴" / "나의 메뉴" 탭으로 통합 -- 메뉴 탭: 일반 메뉴 목록 표시 -- 나의 메뉴 탭: 사용자 즐겨찾기 메뉴 표시 - ---- - -## 21. 검색, 필터, 정렬 모음 - -> **페이지**: 39 - -### 21.1 구성 요소 - -| 영역 | 구성 | -|------|------| -| 기간 선택 | 날짜 범위 (`2025-09-01 ~ 2025-09-03`) + 단축 버튼 (전전월, 어제, 오늘, 전월, 당월, 당해년도) | -| 검색바 | 검색 입력 필드 | -| 필터 셀렉트 박스 | 복수의 전체 셀렉트 박스 | -| 정렬 | 최신순 셀렉트 박스 | -| 항목 필터 | 항목명 태그 형태로 나열 | - ---- - -## 22. 페이지 설정 버튼 - -> **페이지**: 40 - -### 22.1 기능 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 섹션 표시 및 순서 변경 | 페이지 내 섹션 ON/OFF 토글 및 순서 변경 | -| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 | - -### 22.2 설정 패널 구성 - -- 버전기록 -- 가져오기 -- 내보내기 -- 섹션 목록: 각 섹션별 ON/OFF 토글 - -**예시**: -``` -섹션명 [ON] -섹션명 [ON] -섹션명 [ON] -섹션명 [ON] -섹션명 [ON] -``` - ---- - -## 23. 섹션 설정 버튼 - -> **페이지**: 41 - -### 23.1 기능 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 항목 표시 및 순서 변경 | 섹션 내 항목 ON/OFF 토글 및 순서 변경 | -| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 | - -### 23.2 설정 패널 구성 - -- 가져오기 -- 내보내기 -- 항목 목록: 각 항목별 ON/OFF 토글 - -**예시**: -``` -기간 [ON] -기간단축버튼 [ON] -검색바 [ON] -필터명 [ON] -필터명 [ON] -필터명 [ON] -필터명 [ON] -정렬 [ON] -``` - ---- - -## 24. 태스크 알림 아이콘 - -> **페이지**: 42~43 - -### 24.1 동작 규칙 - -| 번호 | 항목 | 설명 | -|------|------|------| -| 01 | 태스크 알림 아이콘 | 태스크가 추가될 경우 카운트하여 표시 | -| - | 메뉴 확장 시 표시 | 대/중/소메뉴로 확장될 경우 해당 메뉴에 아이콘 표시 | -| - | 카운트 범위 | 최소 1 ~ 최대 99 | - -### 24.2 표시 예시 - -**축소 상태**: 대메뉴 옆에 카운트 배지 표시 (예: `전자결재 [3]`) - -**확장 상태 (2Depth)**: -``` -전자결재 [3] - - 중메뉴명 [2] - - 중메뉴명 [1] - - 중메뉴명 - - 중메뉴명 - - 중메뉴명 -``` - -**확장 상태 (3Depth)**: -``` -전자결재 [3] - - 중메뉴명 - - 중메뉴명 [1] - · 소메뉴명 [1] - · 소메뉴명 - - 중메뉴명 - - 중메뉴명 - - 중메뉴명 -``` - -### 24.3 페이지 내 표시 - -- 메뉴 축소 상태에서도 대메뉴 아이콘 옆에 카운트 배지 표시 - ---- - -## 부록: 페이지 맵 - -| 페이지 | 섹션 | 화면명 | -|--------|------|--------| -| 1 | 표지 | SAM_General Rule | -| 2 | 문서 이력 | Document History | -| 3 | 공통 | - | -| 4 | 인터랙션 | Interaction | -| 5 | 반응형 웹 | Responsive Web | -| 6 | 화면 템플릿 | Screen Template | -| 7 | 메시지 | Notifications | -| 8 | GNB, LNB, 푸터 | GNB, LNB, 푸터 | -| 9 | 영역 구분 | 메뉴, 페이지, 섹션, 항목 영역 | -| 10 | 메뉴 목록 | 메뉴 목록 3Depth | -| 11 | 알림 팝업 | 알림 팝업 | -| 12 | 마이페이지 | 마이페이지 팝업 | -| 13 | 셀렉트 박스 | 셀렉트 박스 (기본/다중/검색) | -| 14 | 가이드 메시지 | 가이드 메시지 | -| 15 | 태블릿/모바일 헤더 | 태블릿/모바일 헤더 | -| 16 | 태블릿/모바일 바텀 버튼 | 태블릿/모바일 바텀 버튼 영역 | -| 17 | 공지 팝업 | 공지 팝업 | -| 18 | (구분) | PC, TABLET, MOBILE - 목록 4단계 | -| 19 | 목록 1단계 | PC_목록 | -| 20 | 목록 2단계 | TABLET_가로_목록 | -| 21 | 목록 3단계 | TABLET_세로_목록 | -| 22 | 목록 3단계 확장 | TABLET_세로_목록_확장 | -| 23 | 목록 3단계 | MOBILE_가로_목록 | -| 24 | 목록 3단계 확장 | MOBILE_가로_목록_확장 | -| 25 | 목록 4단계 | MOBILE_세로_목록, MOBILE_세로_목록_확장 | -| 26 | (구분) | PC, TABLET, MOBILE - 상세 4단계 | -| 27 | 상세 1단계 | PC_상세 | -| 28 | 상세 2단계 | TABLET_가로_상세 | -| 29 | 상세 3단계 | TABLET_세로_상세 | -| 30 | 상세 3단계 | MOBILE_가로_상세 | -| 31 | 상세 4단계 | MOBILE_세로_상세 | -| 32 | (구분) | 섹션 정리 | -| 33 | 섹션 정리 | PC 섹션 정리 | -| 34 | TBD | 미정 | -| 35 | 나의 메뉴 | 나의 메뉴_없음 | -| 36 | 나의 메뉴 | 나의 메뉴_있음 | -| 37 | 나의 메뉴 | 나의 메뉴_여러 줄 | -| 38 | 나의 메뉴 | 나의 메뉴_메뉴 영역에 통합 | -| 39 | 검색/필터/정렬 | 검색, 필터, 정렬 모음 | -| 40 | 페이지 설정 | 페이지 설정 버튼 | -| 41 | 섹션 설정 | 섹션 설정 버튼 | -| 42~43 | 태스크 알림 | 태스크 알림 아이콘 | - ---- - -## 관련 문서 - -- [SAM ERP 회계관리 스토리보드 D1.6](SAM_ERP_회계관리_Storyboard_D1.6.md) -- 원본 PDF: `SAM_General_Rule_Storyboard_D1.0_260116.pdf` - ---- - -**최종 업데이트**: 2026-02-23 diff --git a/sam/docs/plans/ai-quotation-engine-plan.md b/sam/docs/plans/ai-quotation-engine-plan.md deleted file mode 100644 index c1b5098..0000000 --- a/sam/docs/plans/ai-quotation-engine-plan.md +++ /dev/null @@ -1,928 +0,0 @@ -# AI 견적서 자동생성 엔진 개발 계획 - -> **작성일**: 2026-03-02 -> **상태**: 기획 초안 -> **프로젝트**: SAM API + MNG -> **우선순위**: 🔴 필수 -> **참조**: `docs/features/ai/README.md`, `docs/features/quotes/README.md`, `docs/rules/customer-pricing.md` - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM 계약 완료 후 매니저가 고객사 직원과 인터뷰를 진행할 때, **인터뷰 내용을 AI가 분석하여 SAM 표준 견적서 형태로 자동 변환**하는 엔진을 구축한다. - -현재 매니저가 수동으로 수행하는 프로세스: - -``` -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ SAM 계약 │ → │ 현장 인터뷰 │ → │ 업무 파악 │ → │ 견적서 작성 │ -│ 완료 │ │ (매니저+직원)│ │ (수동 정리) │ │ (수동 작성) │ -└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ -``` - -AI 엔진 도입 후: - -``` -┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐ -│ SAM 계약 │ → │ 현장 인터뷰 │ → │ AI 엔진 │ → │ 견적서 초안 │ -│ 완료 │ │ (매니저+직원)│ │ (음성/텍스트 분석) │ │ (자동 생성) │ -│ │ │ │ │ (업무 매핑) │ │ (매니저 확인)│ -└──────────────┘ └──────────────┘ └──────────────────────┘ └──────────────┘ -``` - -### 1.2 핵심 원칙 - -| 원칙 | 설명 | -|------|------| -| **자체 AI 엔진** | Claude API 기반으로 SAM 전용 AI 서비스 구축 | -| **기존 인프라 활용** | `ai_configs`, `ai_token_usages`, `ai_pricing_configs` 테이블 재사용 | -| **견적 시스템 연동** | 기존 `quotes`, `quote_items` 테이블과 직접 연동 | -| **Multi-tenant** | 모든 데이터에 `tenant_id` 격리 적용 | -| **점진적 확장** | Phase 1 텍스트 → Phase 2 음성 (STT 구현 완료, 통합만 필요) → Phase 3 학습 고도화 | - -### 1.3 기존 인프라 현황 - -#### AI 인프라 (구축 완료) - -| 구성요소 | 상태 | 비고 | -|---------|------|------| -| `ai_configs` 테이블 | ✅ 완료 | Claude provider 설정 가능 | -| `ai_token_usages` 테이블 | ✅ 완료 | 토큰/비용 자동 추적 | -| `ai_pricing_configs` 테이블 | ✅ 완료 | Claude 모델 단가 등록 | -| `AiTokenHelper` | ✅ 완료 | `saveClaudeUsage()` 메서드 존재 | -| MNG AI 설정 UI | ✅ 완료 | `/system/ai-config` | - -#### 견적 시스템 (구축 완료) - -| 구성요소 | 상태 | 비고 | -|---------|------|------| -| `quotes` 테이블 | ✅ 완료 | 견적 마스터 | -| `quote_items` 테이블 | ✅ 완료 | 견적 품목 상세 | -| `QuoteService` | ✅ 완료 | 견적 CRUD, 상태 관리 | -| `QuoteCalculationService` | ✅ 완료 | BOM 10단계 계산 | -| 견적 API 엔드포인트 | ✅ 완료 | REST API 전체 | - -#### 음성 녹음/STT (구축 완료) - -| 구성요소 | 상태 | 비고 | -|---------|------|------| -| `ai_voice_recordings` 테이블 | ✅ 완료 | DB 스키마 + CRUD | -| GCS 업로드 | ✅ 완료 | `GoogleCloudService` — GCS 저장/조회/삭제 | -| Google Cloud STT 변환 | ✅ 완료 | `GoogleCloudService::speechToText()` — LongRunningRecognize, ko-KR | -| Web Speech API (브라우저 STT) | ✅ 완료 | `voice-recorder.blade.php` — 실시간 음성→텍스트 (무료) | -| STT + Gemini AI 분석 | ✅ 완료 | `AiVoiceRecordingService` — 음성→STT→AI 분석 파이프라인 | -| 화자 분리 (Diarization) | ✅ 완료 | `MeetingMinuteService` — Speaker Diarization | -| 영업 상담 음성 녹음 | ✅ 완료 | `ConsultationController` — MediaRecorder + STT + GCS 백업 | - -> **참조 구현 파일:** -> - `mng/app/Services/GoogleCloudService.php` — Google Cloud STT/GCS 통합 서비스 -> - `mng/app/Services/AiVoiceRecordingService.php` — STT + Gemini 분석 -> - `mng/app/Services/MeetingMinuteService.php` — 회의록 STT + 화자분리 -> - `mng/app/Http/Controllers/Sales/ConsultationController.php` — 영업 상담 음성 -> - `mng/resources/views/sales/modals/voice-recorder.blade.php` — 브라우저 음성 녹음 UI -> - `docs/features/voice-input-stt-guide.md` — STT 기술 가이드 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 기획 초안 작성 | -| **다음 작업** | Phase 1 상세 설계 → 구현 | -| **진행률** | 0/4 Phase (0%) | -| **마지막 업데이트** | 2026-03-02 | - ---- - -## 2. 시스템 아키텍처 - -### 2.1 전체 구조 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SAM AI 견적 엔진 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌───────────────┐ ┌───────────────────────┐ │ -│ │ 입력 채널 │ │ AI 분석 파이프라인 │ │ -│ │ │ │ │ │ -│ │ 📝 텍스트 │───→│ 1. 인터뷰 전처리 │ │ -│ │ 🎤 음성(P2) │ │ 2. 업무 도메인 분류 │ │ -│ │ 📄 문서(P3) │ │ 3. SAM 모듈 매핑 │ │ -│ └───────────────┘ │ 4. 견적 항목 추출 │ │ -│ │ 5. 금액 산출 │ │ -│ └──────────┬────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────┐ │ -│ │ 견적서 생성기 │ │ -│ │ │ │ -│ │ SAM 표준 모듈 카탈로그 ←──→ Claude API │ │ -│ │ 고객 요금 정책 ←──→ 프롬프트 엔진 │ │ -│ │ 기존 견적 템플릿 ←──→ 결과 파서 │ │ -│ └──────────────────────┬────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────┐ │ -│ │ 출력 │ │ -│ │ │ │ -│ │ 📊 견적서 초안 (quotes 테이블) │ │ -│ │ 📋 업무 분석 리포트 │ │ -│ │ 💡 추천 모듈 목록 │ │ -│ └───────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 Claude API 연동 구조 - -``` -┌──────────────────┐ ┌──────────────────┐ -│ SAM API Server │ │ Claude API │ -│ (Laravel) │ │ (Anthropic) │ -│ │ │ │ -│ AiQuotation │ ──HTTP──→ Messages API │ -│ Service │ ←─JSON── (claude-sonnet) │ -│ │ │ │ -│ ┌────────────┐ │ │ System Prompt: │ -│ │ Prompt │ │ │ - SAM 모듈 목록 │ -│ │ Engine │ │ │ - 요금 정책 │ -│ │ │ │ │ - 견적 구조 │ -│ │ - 모듈목록 │ │ │ - 출력 형식 │ -│ │ - 요금표 │ │ │ │ -│ │ - 템플릿 │ │ │ │ -│ └────────────┘ │ │ │ -└──────────────────┘ └──────────────────┘ -``` - -### 2.3 데이터 흐름 - -``` -매니저 인터뷰 입력 - │ - ▼ -┌──────────────────────────────────────────────────┐ -│ Step 1: 인터뷰 전처리 │ -│ - 텍스트 정규화 (불필요한 표현 제거) │ -│ - 핵심 키워드 추출 │ -│ - 업무 도메인 태깅 │ -└──────────────────────┬───────────────────────────┘ - ▼ -┌──────────────────────────────────────────────────┐ -│ Step 2: Claude API 1차 호출 — 업무 분석 │ -│ - 고객사 업종/규모 파악 │ -│ - 현재 업무 프로세스 분석 │ -│ - 디지털화 필요 영역 식별 │ -│ - Pain Point 도출 │ -│ 출력: 구조화된 업무 분석 JSON │ -└──────────────────────┬───────────────────────────┘ - ▼ -┌──────────────────────────────────────────────────┐ -│ Step 3: SAM 모듈 매핑 │ -│ - 업무 분석 결과 ↔ SAM 모듈 카탈로그 대조 │ -│ - 필수/선택 모듈 분류 │ -│ - 사용자 수, 데이터량 추정 │ -│ 출력: 추천 모듈 목록 + 근거 │ -└──────────────────────┬───────────────────────────┘ - ▼ -┌──────────────────────────────────────────────────┐ -│ Step 4: Claude API 2차 호출 — 견적 생성 │ -│ - SAM 요금 정책 적용 │ -│ - 모듈별 개발비 + 월 구독료 계산 │ -│ - 추가 옵션 (AI 토큰, 저장공간) 산출 │ -│ - 할인 정책 적용 (통합 패키지 등) │ -│ 출력: 견적서 JSON (SAM 표준 형식) │ -└──────────────────────┬───────────────────────────┘ - ▼ -┌──────────────────────────────────────────────────┐ -│ Step 5: 견적서 초안 저장 │ -│ - quotes 테이블에 저장 (status: draft) │ -│ - quote_items에 모듈별 항목 저장 │ -│ - 업무 분석 리포트 첨부 │ -│ - 매니저에게 알림 → 검토/수정 → 확정 │ -└──────────────────────────────────────────────────┘ -``` - ---- - -## 3. SAM 모듈 카탈로그 (AI 프롬프트용) - -> **참조**: `docs/rules/customer-pricing.md` - -AI가 인터뷰 내용을 SAM 견적으로 변환하려면, SAM이 제공하는 모듈과 요금을 정확히 알아야 한다. 이 데이터는 **DB 테이블로 관리**하여 프롬프트에 동적 주입한다. - -### 3.1 모듈 카탈로그 테이블 설계 - -```sql -CREATE TABLE ai_quotation_modules ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - module_code VARCHAR(50) NOT NULL, -- 'HR', 'SALES', 'FINANCE' 등 - module_name VARCHAR(100) NOT NULL, -- '인사관리', '영업관리', '재무관리' - category ENUM('basic', 'individual', 'addon') NOT NULL, - description TEXT, -- 모듈 기능 설명 - keywords JSON, -- AI 매핑용 키워드 목록 - dev_cost DECIMAL(12,0) DEFAULT 0, -- 개발비 (원) - monthly_fee DECIMAL(10,0) DEFAULT 0, -- 월 구독료 (원) - is_active BOOLEAN DEFAULT TRUE, - sort_order INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_tenant (tenant_id), - INDEX idx_category (category), - UNIQUE KEY uk_tenant_module (tenant_id, module_code) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -### 3.2 초기 데이터 (customer-pricing.md 기준) - -| module_code | module_name | category | dev_cost | monthly_fee | -|-------------|-------------|----------|----------|-------------| -| `BASIC_PKG` | 기본 패키지 (인사+근태+급여+게시판) | basic | 5,000,000 | 200,000 | -| `HR` | 인사관리 | individual | 2,000,000 | 80,000 | -| `ATTENDANCE` | 근태관리 | individual | 1,500,000 | 60,000 | -| `PAYROLL` | 급여관리 | individual | 2,500,000 | 100,000 | -| `BOARD` | 게시판/공지사항 | individual | 500,000 | 20,000 | -| `SALES` | 영업관리 (CRM+견적+수주) | individual | 5,000,000 | 150,000 | -| `PURCHASE` | 구매/자재관리 | individual | 3,000,000 | 100,000 | -| `PRODUCTION` | 생산관리 (MES) | individual | 8,000,000 | 250,000 | -| `QUALITY` | 품질관리 | individual | 4,000,000 | 120,000 | -| `FINANCE` | 재무/회계관리 | individual | 5,000,000 | 150,000 | -| `LOGISTICS` | 물류/출하관리 | individual | 3,000,000 | 100,000 | -| `APPROVAL` | 전자결재 | individual | 3,000,000 | 80,000 | -| `DOCUMENT` | 문서관리 (전자서명) | individual | 2,000,000 | 60,000 | -| `EQUIPMENT` | 설비관리 | individual | 3,000,000 | 100,000 | -| `INTEGRATED` | 통합 패키지 | basic | 30,000,000 | 800,000 | -| `AI_TOKEN` | AI 토큰 추가 | addon | 0 | 별도 | -| `STORAGE` | 파일 저장공간 추가 | addon | 0 | 별도 | -| `CUSTOM_DEV` | 커스텀 개발 | addon | 별도 협의 | 0 | - -### 3.3 keywords 필드 예시 - -```json -// HR 모듈 -{ - "keywords": ["직원", "사원", "인사", "조직도", "부서", "입퇴사", "인력"], - "pain_points": ["엑셀로 직원 관리", "입퇴사 관리가 번거로움", "조직도 없음"], - "business_needs": ["직원 정보 통합", "조직 구조 관리", "인력 현황 파악"] -} - -// PRODUCTION 모듈 -{ - "keywords": ["생산", "제조", "작업지시", "공정", "LOT", "불량", "MES"], - "pain_points": ["생산 현황을 수기로 기록", "불량 추적 불가", "납기 관리 어려움"], - "business_needs": ["실시간 생산현황", "불량률 관리", "작업지시 자동화"] -} -``` - ---- - -## 4. AI 프롬프트 엔진 - -### 4.1 시스템 프롬프트 구조 - -``` -┌─────────────────────────────────────────────────────┐ -│ System Prompt │ -├─────────────────────────────────────────────────────┤ -│ │ -│ [역할 정의] │ -│ 너는 SAM ERP/MES 솔루션의 컨설팅 AI이다. │ -│ 고객 인터뷰를 분석하여 맞춤형 견적서를 작성한다. │ -│ │ -│ [SAM 모듈 카탈로그] ← DB에서 동적 로드 │ -│ 각 모듈의 기능, 키워드, 가격 정보 │ -│ │ -│ [요금 정책] ← customer-pricing 기반 │ -│ 기본패키지, 개별모듈, 추가옵션 요금 체계 │ -│ 할인 정책 (통합 패키지 할인 등) │ -│ │ -│ [출력 형식] │ -│ JSON Schema 명시 (견적서 구조) │ -│ │ -│ [분석 지침] │ -│ - 고객 업종/규모별 권장 모듈 기준 │ -│ - 우선순위 결정 기준 (필수 vs 선택) │ -│ - 비용 최적화 원칙 (패키지 vs 개별) │ -│ │ -└─────────────────────────────────────────────────────┘ -``` - -### 4.2 프롬프트 템플릿 (1차: 업무 분석) - -``` -당신은 SAM(Smart Automation Management) ERP/MES 솔루션의 전문 컨설턴트입니다. - -아래는 고객사 직원과의 인터뷰 내용입니다. 이를 분석하여 구조화된 업무 분석 보고서를 작성하세요. - -## 고객 정보 -- 회사명: {company_name} -- 업종: {industry} -- 직원 수: {employee_count} -- 인터뷰 대상: {interviewee_role} - -## 인터뷰 내용 -{interview_content} - -## 분석 기준 -다음 SAM 모듈 영역에 맞춰 분석하세요: -{module_catalog_json} - -## 출력 형식 (JSON) -{ - "company_analysis": { - "industry": "업종 분류", - "scale": "소규모/중소/중견", - "current_systems": ["현재 사용 중인 시스템"], - "digitalization_level": "상/중/하" - }, - "business_domains": [ - { - "domain": "인사/급여", - "current_process": "현재 처리 방식 설명", - "pain_points": ["문제점 1", "문제점 2"], - "improvement_needs": ["개선 필요사항"], - "priority": "필수/높음/보통/낮음", - "matched_modules": ["HR", "PAYROLL"] - } - ], - "recommendations": { - "essential_modules": ["반드시 필요한 모듈 코드"], - "recommended_modules": ["권장 모듈 코드"], - "optional_modules": ["선택 모듈 코드"], - "package_suggestion": "BASIC_PKG 또는 INTEGRATED 또는 individual", - "reasoning": "패키지 추천 근거" - } -} -``` - -### 4.3 프롬프트 템플릿 (2차: 견적 생성) - -``` -아래 업무 분석 결과를 바탕으로 SAM 견적서를 생성하세요. - -## 업무 분석 결과 -{analysis_result_json} - -## SAM 요금 정책 -{pricing_policy_json} - -## 견적 생성 규칙 -1. 필수 모듈은 반드시 포함 -2. 통합 패키지가 개별 합산보다 저렴하면 패키지 추천 -3. 직원 수 기반 사용자 라이선스 산출 -4. AI 토큰은 월 기본 100만 토큰 포함, 초과분 별도 -5. 파일 저장공간은 기본 10GB, 초과분 별도 - -## 출력 형식 (JSON) -{ - "quotation": { - "title": "견적서 제목", - "client_name": "고객사명", - "valid_until": "견적 유효기간", - "items": [ - { - "category": "기본서비스/추가모듈/추가옵션", - "module_code": "모듈코드", - "module_name": "모듈명", - "description": "포함 기능 설명", - "dev_cost": 0, - "monthly_fee": 0, - "quantity": 1, - "note": "비고" - } - ], - "summary": { - "total_dev_cost": 0, - "total_monthly_fee": 0, - "discount_type": "패키지할인/볼륨할인/없음", - "discount_rate": 0, - "final_dev_cost": 0, - "final_monthly_fee": 0 - }, - "implementation_plan": { - "estimated_months": 0, - "phases": [ - { - "phase": 1, - "name": "단계명", - "modules": ["모듈코드"], - "duration_weeks": 0 - } - ] - }, - "analysis_summary": "업무 분석 요약 (고객 설명용)" - } -} -``` - ---- - -## 5. API 설계 - -### 5.1 엔드포인트 - -| Method | Path | 설명 | -|--------|------|------| -| `POST` | `/api/v1/ai/quotation/analyze` | 인터뷰 분석 (1차) | -| `POST` | `/api/v1/ai/quotation/generate` | 견적서 생성 (2차) | -| `POST` | `/api/v1/ai/quotation/generate-full` | 분석+생성 통합 (원스텝) | -| `GET` | `/api/v1/ai/quotation/{id}` | AI 견적 상세 조회 | -| `GET` | `/api/v1/ai/quotation` | AI 견적 목록 | -| `PUT` | `/api/v1/ai/quotation/{id}` | AI 견적 수정 (매니저) | -| `POST` | `/api/v1/ai/quotation/{id}/confirm` | 정식 견적으로 전환 | -| `DELETE` | `/api/v1/ai/quotation/{id}` | AI 견적 삭제 | - -### 5.2 요청/응답 예시 - -#### POST `/api/v1/ai/quotation/analyze` - -**Request:** -```json -{ - "client_id": 15, - "client_name": "(주)대한기계", - "industry": "기계제조업", - "employee_count": 45, - "interviewee_role": "관리부 팀장", - "interview_content": "현재 직원 관리는 엑셀로 하고 있어요. 출퇴근도 수기로 기록하고... 영업팀에서는 견적서를 한글 프로그램으로 만들어서 이메일로 보내는데, 이력 관리가 안 돼요. 생산 현장에서는 작업일보를 종이에 쓰고 있고, 불량이 나면 어디서 발생했는지 추적이 안 됩니다. 재고도 실사를 해봐야 알 수 있어요...", - "interview_type": "text" -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "id": 1, - "analysis": { - "company_analysis": { - "industry": "기계제조업", - "scale": "중소기업 (45명)", - "current_systems": ["엑셀", "한글 프로그램", "종이 문서"], - "digitalization_level": "하" - }, - "business_domains": [ - { - "domain": "인사/급여", - "current_process": "엑셀로 직원 관리, 수기 출퇴근 기록", - "pain_points": ["인사정보 분산", "출퇴근 수기 기록"], - "priority": "필수", - "matched_modules": ["HR", "ATTENDANCE", "PAYROLL"] - }, - { - "domain": "영업관리", - "current_process": "한글 프로그램 견적서, 이메일 발송", - "pain_points": ["견적 이력 관리 불가", "영업 현황 파악 어려움"], - "priority": "높음", - "matched_modules": ["SALES"] - }, - { - "domain": "생산관리", - "current_process": "종이 작업일보, 수동 불량 관리", - "pain_points": ["불량 추적 불가", "생산현황 실시간 파악 불가"], - "priority": "필수", - "matched_modules": ["PRODUCTION", "QUALITY"] - }, - { - "domain": "재고/물류", - "current_process": "실사로만 재고 파악", - "pain_points": ["실시간 재고 파악 불가"], - "priority": "높음", - "matched_modules": ["PURCHASE", "LOGISTICS"] - } - ], - "recommendations": { - "essential_modules": ["HR", "ATTENDANCE", "PAYROLL", "PRODUCTION", "QUALITY"], - "recommended_modules": ["SALES", "PURCHASE", "LOGISTICS"], - "optional_modules": ["APPROVAL", "DOCUMENT"], - "package_suggestion": "INTEGRATED", - "reasoning": "8개 이상 모듈이 필요하므로 통합 패키지(30,000,000원)가 개별 합산(34,500,000원)보다 경제적" - } - }, - "token_usage": { - "prompt_tokens": 1250, - "completion_tokens": 890, - "cost_krw": 45 - } - } -} -``` - -### 5.3 컨트롤러 / 서비스 구조 - -``` -app/Http/Controllers/Api/V1/ -└── AiQuotationController.php - ├── analyze() ← 인터뷰 분석 - ├── generate() ← 견적 생성 - ├── generateFull() ← 통합 (분석+생성) - ├── index() ← 목록 - ├── show() ← 상세 - ├── update() ← 수정 - ├── confirm() ← 정식 견적 전환 - └── destroy() ← 삭제 - -app/Services/ -└── AiQuotationService.php - ├── analyzeInterview() ← 1차: 인터뷰 분석 - ├── generateQuotation() ← 2차: 견적 생성 - ├── generateFull() ← 통합 처리 - ├── confirmToQuote() ← quotes 테이블로 전환 - │ - ├── buildAnalysisPrompt() ← 분석 프롬프트 조립 - ├── buildQuotationPrompt() ← 견적 프롬프트 조립 - ├── loadModuleCatalog() ← DB에서 모듈 카탈로그 로드 - ├── loadPricingPolicy() ← DB에서 요금 정책 로드 - ├── callClaudeApi() ← Claude API 호출 - ├── parseResponse() ← 응답 JSON 파싱 - └── saveTokenUsage() ← 토큰 사용량 기록 - -app/Http/Requests/V1/AiQuotation/ -├── AiQuotationAnalyzeRequest.php -├── AiQuotationGenerateRequest.php -├── AiQuotationUpdateRequest.php -└── AiQuotationConfirmRequest.php -``` - ---- - -## 6. 데이터베이스 설계 - -### 6.1 신규 테이블 - -#### ai_quotations (AI 견적 마스터) - -```sql -CREATE TABLE ai_quotations ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - - -- 고객 정보 - client_id BIGINT UNSIGNED NULL, - client_name VARCHAR(200) NOT NULL, - industry VARCHAR(100) NULL, - employee_count INT NULL, - interviewee_role VARCHAR(100) NULL, - - -- 인터뷰 데이터 - interview_content TEXT NOT NULL, - interview_type ENUM('text', 'voice', 'document') DEFAULT 'text', - voice_recording_id BIGINT UNSIGNED NULL, -- ai_voice_recordings 참조 - - -- AI 분석 결과 - analysis_result JSON NULL, -- 1차 분석 결과 - quotation_result JSON NULL, -- 2차 견적 결과 - - -- 상태 관리 - status ENUM('analyzing', 'analyzed', 'generating', 'generated', - 'confirmed', 'failed') DEFAULT 'analyzing', - error_message TEXT NULL, - - -- 연결 - quote_id BIGINT UNSIGNED NULL, -- 정식 견적 전환 시 quotes.id - - -- 금액 요약 - total_dev_cost DECIMAL(12,0) DEFAULT 0, - total_monthly_fee DECIMAL(10,0) DEFAULT 0, - discount_rate DECIMAL(5,2) DEFAULT 0, - final_dev_cost DECIMAL(12,0) DEFAULT 0, - final_monthly_fee DECIMAL(10,0) DEFAULT 0, - - -- 토큰 사용 - total_tokens INT DEFAULT 0, - total_cost_krw DECIMAL(12,2) DEFAULT 0, - - -- 감사 - created_by BIGINT UNSIGNED NULL, - updated_by BIGINT UNSIGNED NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL, - - INDEX idx_tenant_status (tenant_id, status), - INDEX idx_tenant_client (tenant_id, client_id), - INDEX idx_created (created_at) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### ai_quotation_items (AI 견적 항목) - -```sql -CREATE TABLE ai_quotation_items ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - ai_quotation_id BIGINT UNSIGNED NOT NULL, - - category ENUM('basic', 'module', 'addon') NOT NULL, - module_code VARCHAR(50) NOT NULL, - module_name VARCHAR(100) NOT NULL, - description TEXT NULL, - - dev_cost DECIMAL(12,0) DEFAULT 0, - monthly_fee DECIMAL(10,0) DEFAULT 0, - quantity INT DEFAULT 1, - priority ENUM('essential', 'recommended', 'optional') DEFAULT 'recommended', - - ai_reasoning TEXT NULL, -- AI가 이 모듈을 추천한 근거 - matched_pain_points JSON NULL, -- 매칭된 고객 Pain Point - - sort_order INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - INDEX idx_quotation (ai_quotation_id), - FOREIGN KEY (ai_quotation_id) REFERENCES ai_quotations(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -### 6.2 기존 테이블 활용 - -| 테이블 | 용도 | 연동 방식 | -|--------|------|----------| -| `ai_configs` | Claude API 키/모델 설정 | provider='claude' 조회 | -| `ai_token_usages` | 토큰 비용 추적 | menu_name='AI견적' | -| `ai_pricing_configs` | Claude 모델 단가 | provider='claude' | -| `quotes` | 정식 견적 전환 대상 | `confirm` 시 생성 | -| `clients` | 고객사 정보 | client_id 참조 | - ---- - -## 7. Phase별 개발 계획 - -### Phase 1: 텍스트 인터뷰 → AI 견적 (MVP) - -> **목표**: 텍스트 인터뷰 입력 → Claude 분석 → 견적서 자동생성 -> **기간**: 2~3주 -> **우선순위**: 🔴 필수 - -| 단계 | 작업 | 프로젝트 | 상태 | -|------|------|---------|------| -| 1-1 | `ai_quotation_modules` 마이그레이션 + 시더 | API | ⏳ | -| 1-2 | `ai_quotations`, `ai_quotation_items` 마이그레이션 | API | ⏳ | -| 1-3 | 모델 생성 (`AiQuotation`, `AiQuotationItem`, `AiQuotationModule`) | API | ⏳ | -| 1-4 | `AiQuotationService` — Claude API 연동 | API | ⏳ | -| 1-5 | 프롬프트 엔진 구현 (분석 + 견적 템플릿) | API | ⏳ | -| 1-6 | `AiQuotationController` + FormRequest | API | ⏳ | -| 1-7 | API 라우트 등록 (`routes/api/v1/common.php`) | API | ⏳ | -| 1-8 | 정식 견적 전환 로직 (`confirmToQuote`) | API | ⏳ | -| 1-9 | MNG AI 견적 관리 화면 (목록/상세/수정) | MNG | ⏳ | -| 1-10 | 테스트 (인터뷰 샘플 3건 이상) | API | ⏳ | - -### Phase 2: 음성 인터뷰 연동 - -> **목표**: 현장에서 녹음한 음성 → STT 변환 → AI 분석 → 견적 생성 -> **기간**: 1주 (기존 STT 인프라 재사용으로 단축) -> **선행 조건**: Phase 1 완료 - -| 단계 | 작업 | 프로젝트 | 상태 | 비고 | -|------|------|---------|------|------| -| 2-1 | Google STT 서비스 구현 | MNG | ✅ 완료 | `GoogleCloudService::speechToText()` 재사용 | -| 2-2 | 음성 업로드 API (GCS 저장) | MNG | ✅ 완료 | `AiVoiceRecordingService`, `ConsultationController` 재사용 | -| 2-3 | STT → 텍스트 변환 파이프라인 | MNG | ✅ 완료 | LongRunningRecognize + 폴링 패턴 구현됨 | -| 2-4 | 음성 인터뷰 → AI 견적 통합 플로우 | API | ⏳ | 기존 STT 결과를 Claude 분석에 연결 | -| 2-5 | MNG 음성 녹음 업로드 UI | MNG | ✅ 완료 | `voice-recorder.blade.php` 재사용 가능 | - -> **핵심**: Google Cloud STT, GCS, 브라우저 음성 녹음 UI가 모두 구현 완료 상태. -> Phase 2에서는 기존 STT 결과를 AI 견적 파이프라인(Claude API)에 연결하는 **통합 플로우(2-4)만 신규 개발**하면 된다. - -### Phase 3: 학습 데이터 고도화 - -> **목표**: 과거 견적 데이터를 활용하여 AI 정확도 향상 -> **기간**: 2주 -> **선행 조건**: Phase 1 + 실제 사용 데이터 축적 - -| 단계 | 작업 | 프로젝트 | 상태 | -|------|------|---------|------| -| 3-1 | 과거 견적 데이터 → 프롬프트 Few-shot 예시 구성 | API | ⏳ | -| 3-2 | 확정된 AI 견적 → 학습 데이터 피드백 루프 | API | ⏳ | -| 3-3 | 업종별 견적 패턴 분석 → 추천 정확도 향상 | API | ⏳ | -| 3-4 | 프롬프트 A/B 테스트 프레임워크 | API | ⏳ | - -### Phase 4: 고객 셀프서비스 (확장) - -> **목표**: 고객이 직접 간단한 질문에 답변하면 견적 자동 생성 -> **기간**: 3주 -> **선행 조건**: Phase 1~3 안정화 - -| 단계 | 작업 | 프로젝트 | 상태 | -|------|------|---------|------| -| 4-1 | 고객용 인터뷰 설문 폼 설계 | React | ⏳ | -| 4-2 | 단계별 질문 → AI 분석 통합 | API + React | ⏳ | -| 4-3 | 견적서 미리보기 + PDF 다운로드 | React | ⏳ | -| 4-4 | 매니저 알림 → 후속 상담 연결 | API + MNG | ⏳ | - ---- - -## 8. Claude API 연동 상세 - -### 8.1 SDK 설치 - -```bash -# API 프로젝트에 Anthropic SDK 설치 -docker exec sam-api-1 composer require anthropic-ai/laravel -``` - -> **참고**: `anthropic-ai/laravel` 패키지는 Laravel용 공식 래퍼로, HTTP Client 기반으로 Claude API를 호출한다. 미출시/미지원 시 `GuzzleHttp`로 직접 HTTP 호출한다. - -### 8.2 Claude API 호출 패턴 - -```php -// app/Services/AiQuotationService.php - -class AiQuotationService -{ - private function callClaudeApi(string $systemPrompt, string $userMessage): array - { - // 1. ai_configs에서 Claude 설정 로드 - $config = AiConfig::where('provider', 'claude') - ->where('is_active', true) - ->first(); - - // 2. HTTP 호출 - $response = Http::withHeaders([ - 'x-api-key' => $config->api_key, - 'anthropic-version' => '2023-06-01', - 'content-type' => 'application/json', - ])->post($config->base_url . '/messages', [ - 'model' => $config->model, // claude-sonnet-4-20250514 - 'max_tokens' => 4096, - 'temperature' => 0.3, // 견적은 일관성 중요 - 'system' => $systemPrompt, - 'messages' => [ - ['role' => 'user', 'content' => $userMessage] - ], - ]); - - // 3. 토큰 사용량 기록 - $usage = $response->json('usage'); - AiTokenHelper::saveClaudeUsage( - tenantId: auth()->user()->tenant_id, - menuName: 'AI견적', - promptTokens: $usage['input_tokens'], - completionTokens: $usage['output_tokens'], - model: $config->model, - ); - - // 4. 응답 파싱 - $content = $response->json('content.0.text'); - return json_decode($content, true); - } -} -``` - -### 8.3 비용 예측 - -| 항목 | 토큰 | 비용 (USD) | 비용 (KRW) | -|------|------|-----------|------------| -| 1차 분석 프롬프트 (시스템+사용자) | ~2,000 입력 | $0.0005 | ~1원 | -| 1차 분석 응답 | ~1,500 출력 | $0.0019 | ~3원 | -| 2차 견적 프롬프트 | ~3,000 입력 | $0.0008 | ~1원 | -| 2차 견적 응답 | ~2,000 출력 | $0.0025 | ~4원 | -| **견적 1건 합계** | **~8,500** | **~$0.006** | **~9원** | - -> Claude Sonnet 기준. 1건당 약 **9원**으로 매우 경제적이다. - ---- - -## 9. MNG 관리 화면 - -### 9.1 화면 목록 - -| 화면 | URL | 설명 | -|------|-----|------| -| AI 견적 목록 | `/ai-quotation` | 생성된 AI 견적 목록 | -| AI 견적 생성 | `/ai-quotation/create` | 인터뷰 입력 폼 | -| AI 견적 상세 | `/ai-quotation/{id}` | 분석 결과 + 견적서 조회 | -| AI 견적 수정 | `/ai-quotation/{id}/edit` | 매니저가 수정 | -| 모듈 카탈로그 관리 | `/ai-quotation/modules` | SAM 모듈 목록 관리 | - -### 9.2 AI 견적 생성 화면 구성 - -``` -┌─────────────────────────────────────────────────────┐ -│ AI 견적서 생성 │ -├─────────────────────────────────────────────────────┤ -│ │ -│ [고객 정보] │ -│ ┌─────────────┐ ┌─────────────┐ │ -│ │ 고객사 선택 ▼│ │ 업종 │ │ -│ └─────────────┘ └─────────────┘ │ -│ ┌─────────────┐ ┌─────────────┐ │ -│ │ 직원 수 │ │ 인터뷰 대상 │ │ -│ └─────────────┘ └─────────────┘ │ -│ │ -│ [인터뷰 내용] │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ (텍스트 입력 또는 음성 녹음 업로드) │ │ -│ │ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────┘ │ -│ │ -│ [🔄 분석 시작] [📊 분석+견적 한번에] │ -│ │ -├─────────────────────────────────────────────────────┤ -│ [분석 결과] (Ajax 응답) │ -│ │ -│ 📊 업무 도메인 분석 │ -│ ┌──────────┬──────────┬──────────┬──────────┐ │ -│ │ 도메인 │ 현재상태 │ 문제점 │ 추천모듈 │ │ -│ ├──────────┼──────────┼──────────┼──────────┤ │ -│ │ 인사/급여 │ 엑셀관리 │ 분산관리 │ HR,PAYROLL│ │ -│ │ 생산관리 │ 종이기록 │ 추적불가 │PRODUCTION │ │ -│ └──────────┴──────────┴──────────┴──────────┘ │ -│ │ -│ 💰 견적서 초안 │ -│ ┌──────────┬──────────┬──────────┬──────────┐ │ -│ │ 모듈 │ 개발비 │ 월구독료 │ 우선순위 │ │ -│ ├──────────┼──────────┼──────────┼──────────┤ │ -│ │ 통합패키지 │30,000,000│ 800,000 │ 필수 │ │ -│ │ AI토큰 │ 0│ 50,000 │ 선택 │ │ -│ └──────────┴──────────┴──────────┴──────────┘ │ -│ 합계: 개발비 30,000,000원 / 월 850,000원 │ -│ │ -│ [✅ 정식 견적으로 전환] [✏️ 수정] [🗑️ 삭제] │ -│ │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## 10. 보안 및 운영 - -### 10.1 보안 고려사항 - -| 항목 | 대책 | -|------|------| -| API 키 노출 | `ai_configs` 테이블에 암호화 저장, .env 폴백 | -| 인터뷰 데이터 보호 | tenant_id 격리, 접근 권한 제어 | -| Claude API 비용 제어 | 일일/월별 토큰 한도 설정 (ai_configs.options) | -| 프롬프트 인젝션 | 사용자 입력 sanitize, 시스템 프롬프트 분리 | - -### 10.2 모니터링 - -| 항목 | 방법 | -|------|------| -| API 호출 성공/실패 | `ai_quotations.status` + `error_message` | -| 토큰 사용량 | `ai_token_usages` 테이블 (기존 인프라) | -| 비용 추적 | MNG `/system/ai-token-usage` (기존 UI) | -| 견적 전환율 | `ai_quotations` → `quotes` 전환 비율 통계 | - -### 10.3 에러 처리 - -```php -try { - $result = $this->callClaudeApi($systemPrompt, $userMessage); -} catch (ConnectionException $e) { - // Claude API 연결 실패 - $aiQuotation->update(['status' => 'failed', 'error_message' => 'Claude API 연결 실패']); -} catch (JsonException $e) { - // 응답 JSON 파싱 실패 - $aiQuotation->update(['status' => 'failed', 'error_message' => 'AI 응답 파싱 실패']); -} -``` - ---- - -## 11. 기대 효과 - -| 항목 | Before (현재) | After (AI 엔진) | -|------|--------------|-----------------| -| 견적 작성 시간 | 2~4시간 (수동) | 5~10분 (AI 초안 + 검토) | -| 모듈 누락 위험 | 매니저 경험 의존 | AI가 체계적으로 분석 | -| 고객 맞춤화 | 표준 템플릿 복사 | 인터뷰 기반 맞춤 견적 | -| 비용 최적화 | 수동 비교 | AI가 패키지 vs 개별 자동 비교 | -| 견적 1건 AI 비용 | — | ~9원 (Claude Sonnet) | - ---- - -## 변경 이력 - -| 날짜 | 내용 | -|------|------| -| 2026-03-02 | 기획 초안 작성 | - ---- - -## 관련 문서 - -| 문서 | 경로 | -|------|------| -| SAM 프로젝트 개요 | `docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` | -| 견적 기능 상세 | `docs/features/quotes/README.md` | -| 견적 시스템 분석 | `docs/data/견적/견적시스템_분석문서.md` | -| AI 기능 현황 | `docs/features/ai/README.md` | -| AI 설정 가이드 | `docs/guides/ai-config-settings.md` | -| 고객 요금 안내 | `docs/rules/customer-pricing.md` | -| 내부 과금 정책 | `docs/rules/billing-policy.md` | -| 단가 정책 | `docs/rules/pricing-policy.md` | -| Plans 가이드 | `docs/plans/GUIDE.md` | - ---- - -**최종 업데이트**: 2026-03-02 diff --git a/sam/docs/plans/attendance-management-plan.md b/sam/docs/plans/attendance-management-plan.md deleted file mode 100644 index c5f8889..0000000 --- a/sam/docs/plans/attendance-management-plan.md +++ /dev/null @@ -1,284 +0,0 @@ -# MNG 근태현황 개발 계획서 - -> **작성일**: 2026-02-26 -> **상태**: 계획 수립 - ---- - -## 1. 개요 - -### 1.1 목적 - -MNG 인사관리 > 근태현황 기능을 완성한다. 현재 기본 CRUD가 구현되어 있으나, 미완성 기능과 알려진 버그를 해결하고 실무에 필요한 추가 기능을 구현한다. - -### 1.2 현재 상태 분석 - -#### 구현 완료 - -| 항목 | 상태 | 파일 | -|------|------|------| -| 근태 목록 조회 (HTMX 테이블) | ✅ | `index.blade.php`, `table.blade.php` | -| 월간 통계 카드 (5종) | ✅ | `index.blade.php` | -| 필터 (이름, 부서, 상태, 날짜) | ✅ | `index.blade.php` | -| 등록/수정 모달 | ✅ | `index.blade.php` | -| CRUD API (목록/등록/수정/삭제) | ✅ | `AttendanceController.php` (API) | -| AttendanceService | ✅ | `AttendanceService.php` | -| Attendance 모델 (8개 상태) | ✅ | `Attendance.php` | -| Soft Delete | ✅ | 모델 + 서비스 | - -#### 알려진 문제 (E2E 테스트 결과) - -| 문제 | 심각도 | 설명 | -|------|--------|------| -| 엑셀 다운로드 미구현 | 🟡 중요 | 버튼 없음, API 미연결 | -| 근태 등록 서버 에러 | 🔴 필수 | 모달 submit 시 500 에러 발생 가능 | - -#### 미구현 기능 (API 대비) - -| 기능 | API 지원 | MNG 상태 | -|------|---------|---------| -| 엑셀 내보내기 | ✅ `/v1/attendances/export` | ❌ 미구현 | -| 일괄 삭제 | ✅ `/v1/attendances/bulk-delete` | ❌ 미구현 | -| 개인별 근태 상세 | ✅ `/v1/attendances/{id}` | ❌ 미구현 | -| 월간 요약 통계 | ✅ `/v1/attendances/monthly-stats` | ⚠️ 기본만 구현 | -| 출퇴근 설정 관리 | ✅ `attendance_settings` 테이블 | ❌ 미구현 | - ---- - -## 2. 구현 범위 - -### 2.1 Phase 1: 버그 수정 + 핵심 기능 (우선) - -| # | 작업 | 난이도 | 설명 | -|---|------|--------|------| -| 1-1 | 근태 등록/수정 버그 수정 | 🟢 낮음 | store/update API 요청 오류 점검 및 수정 | -| 1-2 | 엑셀 다운로드 | 🟢 낮음 | API `/v1/attendances/export` 연동, 다운로드 버튼 추가 | -| 1-3 | 일괄 삭제 | 🟡 보통 | 체크박스 선택 → 일괄 삭제 버튼 | -| 1-4 | 월간 통계 기간 선택 | 🟢 낮음 | 현재 당월 고정 → 연/월 선택 가능하게 | - -### 2.2 Phase 2: 확장 기능 - -| # | 작업 | 난이도 | 설명 | -|---|------|--------|------| -| 2-1 | 개인별 근태 상세 페이지 | 🟡 보통 | 사원 클릭 → 월간 달력 + 출퇴근 이력 | -| 2-2 | 출퇴근 설정 관리 | 🟡 보통 | 표준 출근시간, GPS 사용여부, 허용반경 설정 | -| 2-3 | 월간/주간 요약 뷰 | 🟡 보통 | 부서별/사원별 근태 요약 테이블 | -| 2-4 | 근태 일괄 등록 | 🔴 높음 | 날짜 범위 + 대상 사원 → 일괄 근태 등록 | - ---- - -## 3. 상세 설계 - -### 3.1 Phase 1-1: 근태 등록/수정 버그 수정 - -**점검 항목**: -- MNG `AttendanceController::store()` validation 규칙과 실제 폼 데이터 일치 여부 -- `check_in`, `check_out` 포맷 (HH:MM vs HH:MM:SS) 불일치 가능성 -- `user_id` 전달 누락 여부 -- HTMX `hx-headers` CSRF 토큰 전달 확인 - -**수정 대상 파일**: -- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` -- `mng/resources/views/hr/attendances/index.blade.php` (JS `submitAttendance()`) - ---- - -### 3.2 Phase 1-2: 엑셀 다운로드 - -**방식**: MNG에서 직접 엑셀 생성 (API 서버 미경유) - -**구현**: -1. `AttendanceService::getExportData()` 메서드 추가 -2. `AttendanceController::export()` 메서드 추가 -3. 라우트: `GET /api/admin/hr/attendances/export` -4. 인덱스 페이지에 다운로드 버튼 추가 - -**엑셀 컬럼**: - -| 컬럼 | 값 | -|------|-----| -| 날짜 | `base_date` | -| 사원명 | `user.name` | -| 부서 | `department.name` | -| 상태 | `status_label` | -| 출근 | `check_in` | -| 퇴근 | `check_out` | -| 비고 | `remarks` | - -**수정 대상 파일**: -- `mng/app/Services/HR/AttendanceService.php` -- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` -- `mng/routes/api.php` -- `mng/resources/views/hr/attendances/index.blade.php` - ---- - -### 3.3 Phase 1-3: 일괄 삭제 - -**UI**: 테이블 각 행에 체크박스 추가, 헤더에 전체선택, 상단에 "선택 삭제" 버튼 - -**구현**: -1. `table.blade.php`에 체크박스 컬럼 추가 -2. Alpine.js 컴포넌트로 선택 상태 관리 -3. `AttendanceController::bulkDestroy()` 메서드 추가 -4. 라우트: `POST /api/admin/hr/attendances/bulk-delete` - -**수정 대상 파일**: -- `mng/resources/views/hr/attendances/partials/table.blade.php` -- `mng/resources/views/hr/attendances/index.blade.php` -- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` -- `mng/app/Services/HR/AttendanceService.php` -- `mng/routes/api.php` - ---- - -### 3.4 Phase 1-4: 월간 통계 기간 선택 - -**현재**: 당월 통계만 표시 (하드코딩) -**변경**: 연/월 드롭다운 추가 → 선택 시 통계 카드 HTMX 갱신 - -**구현**: -1. 통계 카드 영역을 별도 partial로 분리 (`partials/stats.blade.php`) -2. 연/월 선택 UI 추가 -3. `hx-get` + `hx-vals`로 선택된 연/월 전달 -4. `stats()` API가 `year`, `month` 파라미터 이미 지원 - -**수정 대상 파일**: -- `mng/resources/views/hr/attendances/index.blade.php` -- `mng/resources/views/hr/attendances/partials/stats.blade.php` (신규) - ---- - -### 3.5 Phase 2-1: 개인별 근태 상세 페이지 - -**URL**: `/hr/attendances/{userId}` - -**페이지 구성**: -1. **사원 프로필 카드**: 이름, 부서, 직급, 재직상태 -2. **월간 달력**: 날짜별 근태 상태를 색상 도트로 표시 -3. **월간 통계**: 정시출근 N일, 지각 N일, 결근 N일 등 -4. **출퇴근 이력 테이블**: 해당 월의 상세 출퇴근 기록 - -**수정 대상 파일**: -- `mng/routes/web.php` (라우트 추가) -- `mng/app/Http/Controllers/HR/AttendanceController.php` (`show()` 추가) -- `mng/app/Services/HR/AttendanceService.php` (`getUserMonthlyAttendances()` 추가) -- `mng/resources/views/hr/attendances/show.blade.php` (신규) - ---- - -### 3.6 Phase 2-2: 출퇴근 설정 관리 - -**URL**: `/hr/attendance-settings` - -**설정 항목** (`attendance_settings` 테이블 기반): - -| 항목 | 필드 | 설명 | -|------|------|------| -| GPS 출퇴근 사용 | `use_gps` | on/off 토글 | -| 자동 출퇴근 | `use_auto` | on/off 토글 | -| 허용 반경 | `allowed_radius` | 미터 단위 입력 | -| 본사 주소 | `hq_address` | 주소 입력 | -| 본사 위도/경도 | `hq_latitude`, `hq_longitude` | 좌표 입력 | - -**수정 대상 파일**: -- `mng/routes/web.php`, `mng/routes/api.php` -- `mng/app/Http/Controllers/HR/AttendanceSettingController.php` (신규) -- `mng/app/Models/HR/AttendanceSetting.php` (신규 — API 모델 미러링) -- `mng/resources/views/hr/attendance-settings/index.blade.php` (신규) - ---- - -## 4. 데이터 흐름 - -### 4.1 MNG 자체 CRUD 패턴 (현재) - -``` -브라우저 ──HTMX──→ MNG API Controller ──→ MNG Service ──→ DB (직접) - (api/admin/hr/attendances) -``` - -> MNG는 API 서버를 경유하지 않고 DB에 직접 접근한다. - -### 4.2 엑셀 다운로드 흐름 - -``` -브라우저 ──GET──→ MNG AttendanceController::export() - → AttendanceService::getExportData() - → ExportService::download() (라라벨 엑셀) - ← BinaryFileResponse (.xlsx) -``` - ---- - -## 5. 구현 순서 및 의존 관계 - -``` -Phase 1 (버그 수정 + 핵심) - 1-1 버그 수정 ─────────────────────────────┐ - 1-2 엑셀 다운로드 ─────────────────────────┤ 독립적, 병렬 가능 - 1-3 일괄 삭제 ─────────────────────────────┤ - 1-4 통계 기간 선택 ────────────────────────┘ - -Phase 2 (확장) - 2-1 개인별 상세 ───→ 2-3 월간/주간 요약 (데이터 재사용) - 2-2 출퇴근 설정 ─── 독립적 - 2-4 일괄 등록 ───── 독립적 -``` - ---- - -## 6. 관련 파일 목록 - -### MNG 프로젝트 (`/home/aweso/sam/mng`) - -| 파일 | 역할 | -|------|------| -| `app/Models/HR/Attendance.php` | 모델 (8개 상태, json_details) | -| `app/Services/HR/AttendanceService.php` | 비즈니스 로직 | -| `app/Http/Controllers/HR/AttendanceController.php` | 뷰 컨트롤러 | -| `app/Http/Controllers/Api/Admin/HR/AttendanceController.php` | API 컨트롤러 | -| `resources/views/hr/attendances/index.blade.php` | 메인 페이지 | -| `resources/views/hr/attendances/partials/table.blade.php` | 테이블 partial | -| `routes/web.php` | 웹 라우트 | -| `routes/api.php` | API 라우트 | - -### API 프로젝트 (`/home/aweso/sam/api`) - -| 파일 | 역할 | -|------|------| -| `database/migrations/2025_12_09_*_attendances*` | 마이그레이션 (2개) | -| `database/migrations/2025_12_17_*_attendance_settings*` | 설정 테이블 | -| `app/Models/Tenants/Attendance.php` | API 모델 (참조용) | -| `app/Models/Tenants/AttendanceSetting.php` | 설정 모델 (참조용) | - -### 참조 문서 - -| 문서 | 경로 | -|------|------| -| 근태 API 규칙 | `docs/rules/attendance-api.md` | -| GPS 출퇴근 스펙 | `docs/specs/erp-analysis/03-gps-attendance.md` | - ---- - -## 7. 검증 방법 - -### Phase 1 체크리스트 - -- [ ] 근태 등록 모달 → 사원 선택 + 날짜 + 상태 입력 → 저장 성공 -- [ ] 근태 수정 모달 → 기존 데이터 로드 → 수정 → 저장 성공 -- [ ] 동일 사원/날짜 중복 등록 시 기존 데이터 업데이트 (Upsert) -- [ ] 엑셀 다운로드 버튼 클릭 → .xlsx 파일 다운로드 -- [ ] 체크박스 선택 → 일괄 삭제 → 테이블 갱신 -- [ ] 연/월 선택 → 통계 카드 갱신 - -### Phase 2 체크리스트 - -- [ ] 사원 이름 클릭 → 개인별 상세 페이지 이동 -- [ ] 달력에 근태 상태 색상 표시 -- [ ] 출퇴근 설정 페이지 → GPS/자동 토글 → 저장 -- [ ] 허용 반경 변경 → 저장 → DB 반영 - ---- - -**최종 업데이트**: 2026-02-26 diff --git a/sam/docs/plans/block-builder-evolution-plan.md b/sam/docs/plans/block-builder-evolution-plan.md deleted file mode 100644 index 57385f3..0000000 --- a/sam/docs/plans/block-builder-evolution-plan.md +++ /dev/null @@ -1,706 +0,0 @@ -# 양식 디자이너(Block Builder) 고도화 계획 - -> **작성일**: 2026-03-06 -> **상태**: 계획 수립 -> **담당**: Claude Code + 개발팀 -> **관련**: [문서양식관리](../features/documents/mng-document-template.md) | [문서관리](../features/documents/mng-document-system.md) - ---- - -## 1. 현재 상태 진단 - -### 1.1 구현 완료 (Phase 1 — 2026-02-28) - -- 13개 블록 타입 (기본 6 + 폼 7) -- 3패널 UI (팔레트 / 캔버스 / 속성) -- SortableJS 드래그-앤-드롭 정렬 -- Undo/Redo (최대 50단계) -- JSON 스키마 저장 (`document_templates.schema`) -- 페이지 설정 (A4/A3/B5, 세로/가로, 여백) - -### 1.2 핵심 미구현 사항 - -| 기능 | 상태 | 영향도 | -|------|------|--------| -| 문서 생성 시 블록 렌더링 | 미구현 | 블록 서식으로 문서 작성 불가 | -| 결재선 블록 | 미구현 | 결재 워크플로우 연동 불가 | -| 데이터 바인딩 (EAV 연동) | 미구현 | 입력값 저장/로드 불가 | -| 동적 행 추가 | 미구현 | 검사 데이터 행 추가 불가 | -| 변수/매크로 시스템 | 미구현 | 자동 값 주입 불가 | -| 인쇄/PDF 출력 | 미구현 | 블록 문서 인쇄 불가 | -| Columns 내부 블록 | 미구현 | 다단 레이아웃 활용 불가 | - -> **결론**: 양식 디자이너는 **레이아웃 편집기**로만 동작. 실제 문서 생성/결재/인쇄에서는 Legacy Builder만 사용 가능. - ---- - -## 2. 목표 - -Legacy Builder의 모든 기능을 양식 디자이너에서 지원하면서, 더 유연하고 확장 가능한 문서 시스템 구축. - -**최종 목표:** -``` -양식 디자이너로 서식 설계 - ↓ -블록 스키마 기반 문서 생성 (데이터 입력) - ↓ -결재 워크플로우 (작성 → 검토 → 승인) - ↓ -인쇄/PDF 출력 - ↓ -Legacy Builder 완전 대체 -``` - ---- - -## 3. 개발 로드맵 (6단계) - -### Phase 2: 블록 런타임 렌더러 (기반 인프라) - -> **목표**: 저장된 블록 스키마를 문서 생성/조회/인쇄에서 렌더링 - -#### 2-1. 블록 렌더러 엔진 - -**위치**: `mng/resources/views/documents/partials/block-renderer.blade.php` - -``` -schema JSON 입력 - ↓ -블록 타입별 Blade 컴포넌트 렌더링 - ↓ -모드별 출력: - - view 모드: 읽기 전용 HTML - - edit 모드: 입력 폼 HTML - - print 모드: 인쇄 최적화 HTML -``` - -**핵심 구현:** - -```php -// BlockRendererService -class BlockRendererService -{ - public function render(array $schema, string $mode, array $data = []): string - { - $html = ''; - foreach ($schema['blocks'] as $block) { - $html .= $this->renderBlock($block, $mode, $data); - } - return $html; - } - - private function renderBlock(array $block, string $mode, array $data): string - { - return match($block['type']) { - 'heading' => $this->renderHeading($block, $mode), - 'paragraph' => $this->renderParagraph($block, $mode), - 'table' => $this->renderTable($block, $mode, $data), - 'text_field' => $this->renderTextField($block, $mode, $data), - 'number_field' => $this->renderNumberField($block, $mode, $data), - 'date_field' => $this->renderDateField($block, $mode, $data), - 'select_field' => $this->renderSelectField($block, $mode, $data), - 'checkbox_field' => $this->renderCheckboxField($block, $mode, $data), - 'textarea_field' => $this->renderTextareaField($block, $mode, $data), - 'signature_field'=> $this->renderSignatureField($block, $mode, $data), - 'divider' => $this->renderDivider($block), - 'spacer' => $this->renderSpacer($block), - 'columns' => $this->renderColumns($block, $mode, $data), - 'approval_line' => $this->renderApprovalLine($block, $mode, $data), - 'dynamic_table' => $this->renderDynamicTable($block, $mode, $data), - default => '', - }; - } -} -``` - -#### 2-2. 문서 편집 화면 통합 - -**수정 대상**: `mng/resources/views/documents/edit.blade.php` - -``` -Template 로드 - ↓ -isBlockBuilder() 체크 - ├── true → BlockRendererService::render(schema, 'edit', data) - └── false → 기존 Legacy 렌더링 (변경 없음) -``` - -#### 2-3. 데이터 바인딩 (EAV 매핑) - -블록의 `binding` 속성으로 EAV 데이터와 연결: - -```javascript -// 블록 스키마 예시 -{ - "type": "text_field", - "props": { - "label": "제품명", - "binding": "bf_product_name", // ← EAV field_key - "required": true - } -} -``` - -``` -저장 시: - block.binding → document_data.field_key - input.value → document_data.field_value - block.id → document_data.section_id (블록 ID를 섹션으로 활용) - -로드 시: - document_data 조회 → field_key로 블록 매칭 → 값 주입 -``` - -**산출물:** - -| 파일 | 작업 | -|------|------| -| `mng/app/Services/BlockRendererService.php` | 신규 생성 | -| `mng/resources/views/documents/partials/block-renderer.blade.php` | 신규 생성 | -| `mng/resources/views/documents/edit.blade.php` | 블록 빌더 분기 추가 | -| `mng/resources/views/documents/show.blade.php` | 블록 빌더 분기 추가 | -| `api/app/Services/DocumentService.php` | 블록 데이터 저장/로드 로직 | - ---- - -### Phase 3: 결재선 블록 - -> **목표**: 블록 스키마 내에서 결재 워크플로우 정의 - -#### 3-1. approval_line 블록 타입 추가 - -**스키마:** - -```json -{ - "type": "approval_line", - "props": { - "steps": [ - { "role": "작성", "department": "", "name": "" }, - { "role": "검토", "department": "", "name": "" }, - { "role": "승인", "department": "", "name": "" } - ], - "style": "horizontal", - "showStamp": true - } -} -``` - -#### 3-2. 팔레트에 결재선 블록 추가 - -```javascript -// 블록 팔레트 추가 -{ type: 'approval_line', icon: '✓', label: '결재선', category: '워크플로우' } -``` - -#### 3-3. 속성 패널 결재선 편집기 - -``` -┌─────────────────────────────┐ -│ 결재선 설정 │ -│ │ -│ [+ 단계 추가] │ -│ │ -│ 1. 역할: [작성 ▼] │ -│ 부서: [___________] │ -│ 이름: [___________] │ -│ │ -│ 2. 역할: [검토 ▼] │ -│ 부서: [___________] │ -│ 이름: [___________] │ -│ │ -│ 3. 역할: [승인 ▼] │ -│ 부서: [___________] │ -│ 이름: [___________] │ -│ │ -│ ☐ 직인 표시 │ -│ 스타일: [가로형 ▼] │ -└─────────────────────────────┘ -``` - -#### 3-4. 문서 생성 시 결재 연동 - -``` -블록 스키마 → approval_line 블록 추출 - ↓ -DocumentApproval 레코드 자동 생성 - ↓ -기존 결재 워크플로우 (submit → approve → reject) 그대로 활용 -``` - -**산출물:** - -| 파일 | 작업 | -|------|------| -| `block-editor.blade.php` | approval_line 블록 추가 | -| `block-canvas.blade.php` | 결재선 렌더링 | -| `BlockRendererService.php` | 결재선 view/edit/print 렌더 | -| `DocumentService.php` | 블록 결재선 → DocumentApproval 변환 | - ---- - -### Phase 4: 동적 테이블 블록 + 변수 시스템 - -> **목표**: 문서 작성 시 행 추가/삭제 가능한 테이블 + 자동 값 주입 - -#### 4-1. dynamic_table 블록 타입 - -기존 `table` 블록은 정적 (양식 설계 시 행 고정). `dynamic_table`은 문서 작성 시 행 동적 추가. - -**스키마:** - -```json -{ - "type": "dynamic_table", - "props": { - "label": "검사 데이터", - "columns": [ - { "key": "col_item", "label": "항목", "type": "text", "width": 120 }, - { "key": "col_standard", "label": "기준값", "type": "text", "width": 100 }, - { "key": "col_measured", "label": "측정값", "type": "number", "width": 100 }, - { "key": "col_result", "label": "판정", "type": "select", - "options": ["합격", "불합격", "보류"], "width": 80 } - ], - "minRows": 1, - "maxRows": 50, - "initialRows": 3, - "showRowNumber": true, - "binding": "inspection_data" - } -} -``` - -#### 4-2. EAV 데이터 매핑 - -``` -dynamic_table 블록 데이터 저장: - -document_data 레코드: - section_id = (dynamic_table 블록 ID → section 매핑) - column_id = (columns[].key → column 매핑) - row_index = 0, 1, 2, ... - field_key = "col_item", "col_standard", ... - field_value = 입력값 -``` - -#### 4-3. 변수/매크로 시스템 - -**내장 변수:** - -| 변수 | 값 | 설명 | -|------|-----|------| -| `{{today}}` | 현재 날짜 | YYYY-MM-DD | -| `{{now}}` | 현재 시각 | YYYY-MM-DD HH:mm | -| `{{user.name}}` | 로그인 사용자명 | | -| `{{user.department}}` | 로그인 사용자 부서 | | -| `{{doc.number}}` | 문서 번호 | 자동채번 | -| `{{doc.title}}` | 문서 제목 | | -| `{{template.company}}` | 서식 회사명 | | - -**연결 데이터 변수 (linked data):** - -| 변수 | 설명 | -|------|------| -| `{{item.name}}` | 연결 품목명 | -| `{{item.code}}` | 연결 품목 코드 | -| `{{order.number}}` | 연결 작업지시서 번호 | -| `{{order.quantity}}` | 연결 수량 | - -**변수 사용 예시 (블록 속성):** - -```json -{ - "type": "text_field", - "props": { - "label": "검사일자", - "binding": "bf_inspection_date", - "default": "{{today}}" - } -} -``` - -```json -{ - "type": "paragraph", - "props": { - "text": "작성자: {{user.name}} ({{user.department}})" - } -} -``` - -#### 4-4. 변수 해석 엔진 - -```php -// VariableResolver -class VariableResolver -{ - public function resolve(string $text, array $context): string - { - return preg_replace_callback('/\{\{(\w+(?:\.\w+)*)\}\}/', function ($m) use ($context) { - return data_get($context, $m[1], $m[0]); - }, $text); - } - - public function buildContext(Document $document, ?User $user = null): array - { - return [ - 'today' => now()->format('Y-m-d'), - 'now' => now()->format('Y-m-d H:i'), - 'user' => [ - 'name' => $user?->name, - 'department' => $user?->department?->name, - ], - 'doc' => [ - 'number' => $document->document_number, - 'title' => $document->title, - ], - 'item' => $this->resolveLinkedItem($document), - 'order' => $this->resolveLinkedOrder($document), - ]; - } -} -``` - -**산출물:** - -| 파일 | 작업 | -|------|------| -| `block-editor.blade.php` | dynamic_table 블록 추가 | -| `BlockRendererService.php` | 동적 테이블 렌더링 (edit: 행 추가/삭제 UI) | -| `mng/app/Services/VariableResolver.php` | 신규 생성 | -| `DocumentService.php` | 동적 테이블 EAV 저장/로드 | - ---- - -### Phase 5: 고급 블록 + 조건부 로직 - -> **목표**: 수식 계산, 조건부 표시, 이미지 블록 등 고급 기능 - -#### 5-1. 수식 블록 (formula) - -```json -{ - "type": "formula_field", - "props": { - "label": "합계", - "expression": "SUM(inspection_data.col_measured)", - "format": "number", - "decimal": 2 - } -} -``` - -**지원 함수:** - -| 함수 | 설명 | 예시 | -|------|------|------| -| `SUM()` | 합계 | `SUM(table.col_amount)` | -| `AVG()` | 평균 | `AVG(table.col_measured)` | -| `COUNT()` | 개수 | `COUNT(table.col_item)` | -| `MIN()` / `MAX()` | 최솟값/최댓값 | `MIN(table.col_value)` | -| `IF()` | 조건 | `IF(AVG(table.col_measured) > 5, "합격", "불합격")` | -| `ROUND()` | 반올림 | `ROUND(AVG(table.col_measured), 2)` | - -#### 5-2. 조건부 표시 (conditional visibility) - -모든 블록에 `visibility` 속성 추가: - -```json -{ - "type": "paragraph", - "props": { - "text": "불합격 사유를 기재해 주세요.", - "visibility": { - "condition": "field", - "field": "bf_judgement", - "operator": "equals", - "value": "불합격" - } - } -} -``` - -**연산자:** - -| 연산자 | 설명 | -|--------|------| -| `equals` | 같으면 표시 | -| `not_equals` | 다르면 표시 | -| `contains` | 포함하면 표시 | -| `greater_than` | 크면 표시 | -| `less_than` | 작으면 표시 | -| `is_empty` | 비어있으면 표시 | -| `is_not_empty` | 비어있지 않으면 표시 | - -#### 5-3. 이미지 블록 - -```json -{ - "type": "image", - "props": { - "label": "검사 사진", - "source": "upload", - "maxSize": 10, - "accept": ["jpeg", "png", "webp"], - "width": "100%", - "align": "center" - } -} -``` - -#### 5-4. Columns 내부 블록 (중첩 렌더링) - -```json -{ - "type": "columns", - "props": { - "count": 2, - "ratio": "1:1", - "children": [ - [ - { "type": "text_field", "props": { "label": "품명" } }, - { "type": "date_field", "props": { "label": "검사일" } } - ], - [ - { "type": "text_field", "props": { "label": "LOT NO" } }, - { "type": "select_field", "props": { "label": "판정", "options": ["합격","불합격"] } } - ] - ] - } -} -``` - -**산출물:** - -| 파일 | 작업 | -|------|------| -| `block-editor.blade.php` | formula, image, conditional 블록 추가 | -| `mng/app/Services/FormulaEngine.php` | 수식 해석 엔진 | -| `BlockRendererService.php` | 조건부 표시, 수식 계산 렌더링 | - ---- - -### Phase 6: 인쇄/PDF + Legacy 대체 - -> **목표**: 블록 문서 인쇄 완성, Legacy Builder 완전 대체 - -#### 6-1. 인쇄 레이아웃 - -``` -print 모드 렌더링: - - 페이지 설정 (A4/A3) 적용 - - 여백 적용 - - 폼 필드 → 값 표시 (입력란 제거) - - 서명 → 서명 이미지 표시 - - 결재선 → 직인 표시 - - 페이지 넘김 (page-break) 자동 계산 -``` - -#### 6-2. PDF 내보내기 - -``` -블록 렌더러 (print 모드 HTML) - ↓ -Puppeteer / wkhtmltopdf - ↓ -PDF 파일 생성 - ↓ -다운로드 또는 첨부 -``` - -#### 6-3. Legacy → Block 마이그레이션 도구 - -기존 Legacy 서식을 Block 스키마로 자동 변환: - -```php -// LegacyToBlockMigrator -class LegacyToBlockMigrator -{ - public function convert(DocumentTemplate $legacy): array - { - $blocks = []; - - // 1. 결재선 → approval_line 블록 - if ($legacy->approvalLines->isNotEmpty()) { - $blocks[] = $this->convertApprovalLines($legacy->approvalLines); - } - - // 2. 기본필드 → text_field / date_field 블록 - foreach ($legacy->basicFields as $field) { - $blocks[] = $this->convertBasicField($field); - } - - // 3. 섹션 → heading + image 블록 - foreach ($legacy->sections as $section) { - $blocks[] = ['type' => 'heading', 'props' => ['text' => $section->title]]; - if ($section->image_path) { - $blocks[] = ['type' => 'image', 'props' => ['source' => $section->image_path]]; - } - } - - // 4. 컬럼 → dynamic_table 블록 - if ($legacy->columns->isNotEmpty()) { - $blocks[] = $this->convertColumns($legacy->columns); - } - - return [ - 'version' => '1.0', - 'page' => ['size' => 'A4', 'orientation' => 'portrait'], - 'blocks' => $blocks, - ]; - } -} -``` - -#### 6-4. Legacy Builder 비활성화 - -``` -Phase 6 완료 후: - - 새 양식 생성: 양식 디자이너만 허용 - - 기존 Legacy 서식: 조회/편집 가능 (변환 유도) - - Legacy Builder "새 양식" 버튼: "양식 디자이너" 사용 안내 -``` - -**산출물:** - -| 파일 | 작업 | -|------|------| -| `mng/resources/views/documents/print-block.blade.php` | 인쇄 전용 뷰 | -| `mng/app/Services/LegacyToBlockMigrator.php` | 변환 도구 | -| `mng/app/Services/PdfExportService.php` | PDF 생성 | - ---- - -## 4. Phase별 우선순위 및 의존관계 - -``` -Phase 2: 블록 런타임 렌더러 ──────────────────────┐ - (렌더러 엔진, 데이터 바인딩, 문서 편집 통합) │ - │ -Phase 3: 결재선 블록 ─────────────────────┐ │ - (approval_line 블록, 결재 워크플로우) │ │ - ↓ ↓ -Phase 4: 동적 테이블 + 변수 ──────→ Phase 5: 고급 블록 - (dynamic_table, 매크로) (수식, 조건부, 이미지) - │ - ↓ - Phase 6: 인쇄/PDF + Legacy 대체 - (마이그레이션 도구) -``` - -| Phase | 의존 | 난이도 | 예상 범위 | -|-------|------|--------|----------| -| **Phase 2** | 없음 (기반) | 높음 | 렌더러 엔진 + EAV 매핑 | -| **Phase 3** | Phase 2 | 중간 | 결재선 블록 + 워크플로우 연동 | -| **Phase 4** | Phase 2 | 높음 | 동적 테이블 + 변수 해석 | -| **Phase 5** | Phase 4 | 높음 | 수식 엔진 + 조건부 로직 | -| **Phase 6** | Phase 3~5 | 중간 | 인쇄 + 마이그레이션 | - ---- - -## 5. 스키마 버전 관리 - -### 5.1 버전 규칙 - -| 버전 | Phase | 변경 내용 | -|------|-------|----------| -| `1.0` | Phase 1 (현재) | 기본 13개 블록 | -| `2.0` | Phase 2~3 | 데이터 바인딩, approval_line 추가 | -| `3.0` | Phase 4 | dynamic_table, 변수 시스템 | -| `4.0` | Phase 5 | formula, conditional, image | - -### 5.2 하위 호환 - -```json -{ - "version": "3.0", - "page": { ... }, - "blocks": [ ... ], - "variables": { ... }, - "migrations": { - "from_1.0": "auto" - } -} -``` - -- 이전 버전 스키마 자동 인식 및 업그레이드 -- 신규 블록 타입은 이전 버전에서 무시 (graceful degradation) - ---- - -## 6. 신규 블록 타입 전체 목록 - -### Phase별 블록 추가 계획 - -| Phase | 블록 타입 | 카테고리 | 설명 | -|-------|----------|---------|------| -| 1 (완료) | `heading` | 기본 | 제목 | -| 1 (완료) | `paragraph` | 기본 | 문단 | -| 1 (완료) | `table` | 기본 | 정적 테이블 | -| 1 (완료) | `columns` | 기본 | 다단 레이아웃 | -| 1 (완료) | `divider` | 기본 | 구분선 | -| 1 (완료) | `spacer` | 기본 | 여백 | -| 1 (완료) | `text_field` | 폼 | 텍스트 입력 | -| 1 (완료) | `number_field` | 폼 | 숫자 입력 | -| 1 (완료) | `date_field` | 폼 | 날짜 입력 | -| 1 (완료) | `select_field` | 폼 | 드롭다운 | -| 1 (완료) | `checkbox_field` | 폼 | 체크박스 | -| 1 (완료) | `textarea_field` | 폼 | 장문 텍스트 | -| 1 (완료) | `signature_field` | 폼 | 서명 | -| **3** | `approval_line` | 워크플로우 | 결재선 | -| **4** | `dynamic_table` | 데이터 | 동적 행 테이블 | -| **5** | `formula_field` | 데이터 | 수식 계산 | -| **5** | `image` | 미디어 | 이미지 업로드/표시 | - ---- - -## 7. 기술 스택 정리 - -| 구성 요소 | 기술 | 비고 | -|----------|------|------| -| 블록 에디터 UI | Alpine.js + Blade | 기존 유지 | -| 드래그-앤-드롭 | SortableJS | 기존 유지 | -| 블록 렌더러 | PHP (BlockRendererService) | 신규 | -| 변수 해석 | PHP (VariableResolver) | 신규 | -| 수식 엔진 | PHP (FormulaEngine) | 신규 | -| 데이터 저장 | EAV (document_data) | 기존 테이블 활용 | -| 결재 워크플로우 | DocumentApproval | 기존 로직 활용 | -| 인쇄 | CSS @media print | 신규 | -| PDF | Puppeteer 또는 wkhtmltopdf | 신규 | - ---- - -## 8. 위험 요소 및 대응 - -| 위험 | 영향 | 대응 | -|------|------|------| -| EAV 매핑 복잡도 | 블록 ID ↔ section_id 매핑 불일치 | 블록 ID를 section 대용으로 사용, 매핑 테이블 추가 검토 | -| Legacy 데이터 호환 | 기존 문서 데이터 접근 불가 | Legacy 서식 문서는 기존 방식 유지, 신규 서식만 블록 적용 | -| 수식 엔진 보안 | 임의 코드 실행 위험 | 화이트리스트 함수만 허용, eval 사용 금지 | -| 인쇄 레이아웃 | 브라우저별 차이 | CSS @page 규격 준수, PDF 변환 권장 | -| 스키마 마이그레이션 | 버전 업그레이드 시 데이터 손실 | 하위 호환 보장, 자동 업그레이드 로직 | - ---- - -## 9. 성공 기준 - -| 기준 | 측정 방법 | -|------|----------| -| 블록 서식으로 문서 생성 가능 | Phase 2 완료 후 테스트 | -| 결재 워크플로우 정상 동작 | Phase 3 완료 후 테스트 | -| 동적 행 추가/삭제 | Phase 4 완료 후 테스트 | -| 변수 자동 주입 | Phase 4 완료 후 테스트 | -| Legacy 서식 자동 변환 | Phase 6 완료 후 테스트 | -| 인쇄 품질 A4 기준 정상 | Phase 6 완료 후 테스트 | - ---- - -## 관련 문서 - -- [문서양식관리](../features/documents/mng-document-template.md) — 현재 양식관리 기술문서 -- [문서관리 시스템](../features/documents/mng-document-system.md) — 문서 생성/결재 기술문서 -- [문서관리 API](../features/documents/README.md) — API 엔드포인트 목록 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/plans/design-insight-menu-plan.md b/sam/docs/plans/design-insight-menu-plan.md deleted file mode 100644 index 28122f5..0000000 --- a/sam/docs/plans/design-insight-menu-plan.md +++ /dev/null @@ -1,611 +0,0 @@ -# UI/UX 디자인 인사이트 연구 메뉴 기획서 - -> **작성일**: 2026-03-08 -> **상태**: 기획 중 -> **라우트**: `/rd/design-insight` -> **모티브**: 기획디자인 스토리보드 에디터 (`/rd/planning-design`) - ---- - -## 1. 개요 - -### 1.1 배경 - -기획디자인 메뉴는 ERP 화면을 **설계(Output)**하는 도구다. -그런데 좋은 설계를 하려면 **연구(Input)**가 먼저 필요하다. - -``` -연구 (이 메뉴) 설계 (기획디자인) -┌─────────────────┐ ┌─────────────────┐ -│ 레퍼런스 수집 │ │ 스토리보드 작성 │ -│ 패턴 분석 │ ──→ │ 와이어프레임 설계 │ -│ 인사이트 정리 │ │ HTML 내보내기 │ -│ 디자인 원칙 학습 │ │ 인쇄 │ -└─────────────────┘ └─────────────────┘ -``` - -현재 SAM ERP 화면을 만들 때 참고할 디자인 패턴이나 인사이트를 체계적으로 관리하는 도구가 없다. 외부 서비스(Dribbble, Mobbin 등)를 참고하지만 **우리 ERP에 맞는 패턴**을 축적하는 곳이 없다. - -### 1.2 목적 - -SAM ERP 화면 개발에 필요한 **UI/UX 디자인 인사이트를 수집·분석·축적**하는 연구 도구 - -### 1.3 핵심 가치 - -| 가치 | 설명 | -|------|------| -| **패턴 축적** | "이 화면은 왜 좋은가?" — 반복 사용할 패턴을 라이브러리화 | -| **Before/After** | 개선 전후를 비교하여 디자인 결정의 근거를 기록 | -| **팀 학습** | 디자인 인사이트를 팀원과 공유, 일관된 UI 품질 유지 | -| **빠른 참조** | 새 화면 설계 시 기존 패턴을 즉시 찾아 재사용 | - ---- - -## 2. 기술 아키텍처 - -### 2.1 기획디자인과 동일한 패턴 - -기획디자인 메뉴의 성공 패턴을 그대로 적용한다. - -| 항목 | 선택 | 이유 | -|------|------|------| -| 프레임워크 | Alpine.js 단일 파일 SPA | 서버 API 없이 즉시 사용, MNG 기존 스택 | -| 저장 | localStorage | 서버 의존성 제거, 즉시 사용 가능 | -| 뷰 파일 | `resources/views/rd/design-insight/index.blade.php` | 단일 파일 구조 | -| 컨트롤러 | `RdController@designInsight()` | 기존 R&D 컨트롤러 확장 | -| 이미지 | Base64 Data URL (localStorage) | 서버 업로드 불필요 | - -### 2.2 라우트 - -```php -// routes/web.php — R&D 그룹 내 추가 -Route::get('/rd/design-insight', [RdController::class, 'designInsight']) - ->name('rd.design-insight'); -``` - -### 2.3 localStorage 키 - -| 키 | 용도 | -|----|------| -| `di_projects` | 연구 프로젝트 목록 (메인 저장소) | -| `di_current` | 현재 프로젝트 ID | -| `di_patterns` | 디자인 패턴 라이브러리 (프로젝트 간 공유) | - ---- - -## 3. 화면 구조 - -### 3.1 전체 레이아웃 - -``` -┌──────────────────────────────────────────────────────────────┐ -│ 툴바: [프로젝트명] [저장] [내보내기] [뷰: 보드│리스트│갤러리] │ -├──────────────────────────────────────────────────────────────┤ -│ 카테고리 탭: 전체 │ 레퍼런스 │ 분석 │ 패턴 │ Before/After │ -├────────┬─────────────────────────────────────────────────────┤ -│ │ │ -│ 사이드 │ 메인 콘텐츠 영역 │ -│ 바 │ │ -│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ ◆ 프로 │ │ 인사이트 │ │ 인사이트 │ │ 인사이트 │ │ -│ 젝트 │ │ 카드 1 │ │ 카드 2 │ │ 카드 3 │ │ -│ 목록 │ │ │ │ │ │ │ │ -│ │ │ 🏷️태그 │ │ 🏷️태그 │ │ 🏷️태그 │ │ -│ ◆ 태그 │ └─────────┘ └─────────┘ └─────────┘ │ -│ 필터 │ │ -│ │ ┌─────────┐ ┌─────────┐ │ -│ ◆ 검색 │ │ 인사이트 │ │ + 새 카드 │ │ -│ │ │ 카드 4 │ │ 추가 │ │ -│ │ └─────────┘ └─────────┘ │ -│ │ │ -├────────┴─────────────────────────────────────────────────────┤ -│ 상태바: 카드 12개 │ 패턴 5개 │ 태그 8개 │ -└──────────────────────────────────────────────────────────────┘ -``` - -### 3.2 뷰 모드 (3종) - -| 뷰 | 설명 | 용도 | -|----|------|------| -| **보드 (Board)** | 칸반 스타일 카드 격자 배열 | 전체 현황 파악, 기본 뷰 | -| **리스트 (List)** | 테이블형 목록 (정렬/필터) | 대량 데이터 관리, 검색 | -| **갤러리 (Gallery)** | 이미지 중심 큰 썸네일 격자 | 시각적 비교, 레퍼런스 브라우징 | - ---- - -## 4. 인사이트 카드 (핵심 데이터 단위) - -### 4.1 카드 유형 (4종) - -#### A. 레퍼런스 카드 (Reference) - -외부/내부 화면 스크린샷을 수집하고 메모를 남긴다. - -``` -┌──────────────────────────────┐ -│ 📷 [스크린샷 이미지] │ -│ │ -├──────────────────────────────┤ -│ 📌 Notion 대시보드 │ -│ "카드형 레이아웃이 정보 밀도를 │ -│ 유지하면서도 시각적으로 깔끔" │ -├──────────────────────────────┤ -│ 출처: notion.so │ -│ 🏷️ 대시보드 카드 레이아웃 │ -│ ⭐⭐⭐⭐☆ │ -└──────────────────────────────┘ -``` - -| 필드 | 타입 | 설명 | -|------|------|------| -| `image` | string (Base64) | 스크린샷 이미지 | -| `title` | string | 제목 | -| `memo` | string | 인사이트 메모 (왜 좋은가/나쁜가) | -| `source` | string | 출처 (URL, 앱 이름 등) | -| `tags` | string[] | 태그 배열 | -| `rating` | number (1-5) | 평점 | -| `category` | string | 화면 카테고리 | - -#### B. 분석 카드 (Analysis) - -화면을 분석하고 디자인 원칙을 체크한다. - -``` -┌──────────────────────────────┐ -│ 🔍 SAM 수주 목록 화면 분석 │ -├──────────────────────────────┤ -│ [스크린샷 + 어노테이션 오버레이]│ -│ ①→ 검색 영역 너무 넓음 │ -│ ②→ 버튼 정렬 불일치 │ -│ ③→ 여백 불균형 │ -├──────────────────────────────┤ -│ ✅ 정렬 (Alignment) │ -│ ❌ 대비 (Contrast) │ -│ ✅ 반복 (Repetition) │ -│ ⚠️ 근접성 (Proximity) │ -├──────────────────────────────┤ -│ 개선 제안: │ -│ "검색 영역을 접을 수 있게 하고 │ -│ 버튼 그룹을 우측 정렬" │ -│ 🏷️ 목록화면 개선필요 │ -└──────────────────────────────┘ -``` - -| 필드 | 타입 | 설명 | -|------|------|------| -| `image` | string (Base64) | 분석 대상 스크린샷 | -| `annotations` | Annotation[] | 어노테이션 배열 (마커 번호, 좌표, 텍스트) | -| `principles` | object | CRAP 원칙 체크 (contrast, repetition, alignment, proximity) | -| `suggestion` | string | 개선 제안 | -| `severity` | string | 심각도 (info, warning, critical) | - -#### C. 패턴 카드 (Pattern) - -반복 사용할 UI 패턴을 템플릿으로 등록한다. - -``` -┌──────────────────────────────┐ -│ 📐 검색 + 필터 + 목록 패턴 │ -├──────────────────────────────┤ -│ [패턴 와이어프레임 이미지] │ -├──────────────────────────────┤ -│ 사용처: │ -│ • 수주 목록 │ -│ • 거래처 목록 │ -│ • 품목 목록 │ -├──────────────────────────────┤ -│ 구성 요소: │ -│ ☑ 검색바 (상단 고정) │ -│ ☑ 필터 칩 (접기/펼치기) │ -│ ☑ 테이블 (정렬 가능) │ -│ ☑ 페이지네이션 (하단) │ -│ ☑ 액션 버튼 (우상단) │ -├──────────────────────────────┤ -│ 🏷️ 목록 CRUD 테이블 │ -│ 📊 사용빈도: ★★★★★ (12회) │ -└──────────────────────────────┘ -``` - -| 필드 | 타입 | 설명 | -|------|------|------| -| `image` | string (Base64) | 패턴 와이어프레임 | -| `usedIn` | string[] | 사용처 목록 | -| `components` | Component[] | 구성 요소 체크리스트 | -| `guidelines` | string | 사용 가이드라인 | -| `frequency` | number | 사용 빈도 | - -#### D. Before/After 카드 (Comparison) - -디자인 개선 전후를 비교한다. - -``` -┌──────────────────────────────────────────┐ -│ 🔄 거래처 상세 화면 리뉴얼 │ -├───────────────────┬──────────────────────┤ -│ ❌ Before │ ✅ After │ -│ [이전 스크린샷] │ [개선 스크린샷] │ -│ │ │ -├───────────────────┴──────────────────────┤ -│ 변경 포인트: │ -│ 1. 탭 구조 → 섹션 접기/펼치기 변경 │ -│ 2. 좌우 2컬럼 → 단일 컬럼 (모바일 대응) │ -│ 3. 저장 버튼 하단 고정 → 상단 sticky │ -├──────────────────────────────────────────┤ -│ 효과: 스크롤 40% 감소, 작업 완료 시간 단축 │ -│ 🏷️ 상세화면 폼 리뉴얼 │ -└──────────────────────────────────────────┘ -``` - -| 필드 | 타입 | 설명 | -|------|------|------| -| `beforeImage` | string (Base64) | 개선 전 스크린샷 | -| `afterImage` | string (Base64) | 개선 후 스크린샷 | -| `changes` | string[] | 변경 포인트 목록 | -| `effect` | string | 개선 효과 | - -### 4.2 공통 필드 - -모든 카드 유형이 공유하는 기본 필드: - -```json -{ - "id": "di_1709856000000_abc", - "type": "reference", - "title": "카드 제목", - "createdAt": "2026-03-08T10:00:00", - "updatedAt": "2026-03-08T15:30:00", - "tags": ["대시보드", "카드", "레이아웃"], - "category": "dashboard", - "pinned": false, - "archived": false -} -``` - -### 4.3 카테고리 (화면 유형별) - -| 카테고리 | 코드 | 설명 | -|---------|------|------| -| 대시보드 | `dashboard` | 통계, KPI, 차트 화면 | -| 목록 | `list` | 테이블, 검색, 필터 화면 | -| 상세/폼 | `form` | 입력, 편집, 상세 보기 | -| 모달/팝업 | `modal` | 모달 다이얼로그, 확인창 | -| 네비게이션 | `navigation` | 사이드바, 탭, 메뉴 | -| 로그인/온보딩 | `auth` | 인증, 초기 설정 | -| 보고서/인쇄 | `report` | 인쇄용, PDF 출력 화면 | -| 기타 | `etc` | 분류 불가 | - ---- - -## 5. 기능 상세 - -### 5.1 이미지 수집 - -| 기능 | 설명 | -|------|------| -| 파일 업로드 | 이미지 파일 선택 (PNG, JPG, GIF) | -| 클립보드 붙여넣기 | `Ctrl+V`로 스크린샷 즉시 붙여넣기 | -| 드래그 앤 드롭 | 이미지 파일을 카드 영역에 드롭 | - -> **Ctrl+V 붙여넣기가 핵심** — 스크린샷 캡처 후 즉시 카드 생성이 워크플로우의 핵심 - -### 5.2 어노테이션 (분석 카드) - -분석 카드에서 이미지 위에 마커를 추가하여 문제점이나 인사이트를 표시한다. - -| 어노테이션 유형 | 설명 | -|---------------|------| -| 번호 마커 (①②③) | 이미지 위 클릭 → 번호 자동 증가, 하단 설명과 연동 | -| 영역 하이라이트 | 드래그로 사각형 영역 표시 (반투명 컬러 오버레이) | -| 텍스트 메모 | 이미지 위 임의 위치에 짧은 메모 | - -> 기획디자인의 번호 마커(marker 블록) + Description 패널 패턴을 재활용 - -### 5.3 태그 시스템 - -| 기능 | 설명 | -|------|------| -| 자유 태그 | 카드에 자유 태그 추가 (콤마 구분 입력) | -| 태그 자동 완성 | 기존 태그 목록에서 자동 완성 | -| 태그 필터 | 사이드바에서 태그 클릭 → 해당 태그 카드만 표시 | -| 태그 색상 | 카테고리별 자동 색상 배정 | - -### 5.4 CRAP 디자인 원칙 체크리스트 - -분석 카드에서 사용하는 디자인 원칙 평가: - -| 원칙 | 체크 항목 | -|------|----------| -| **C**ontrast (대비) | 중요 요소가 시각적으로 구분되는가? | -| **R**epetition (반복) | 일관된 스타일이 반복 적용되는가? | -| **A**lignment (정렬) | 요소들이 논리적으로 정렬되어 있는가? | -| **P**roximity (근접성) | 관련 요소가 가까이 그룹핑되어 있는가? | - -추가 체크: - -| 원칙 | 체크 항목 | -|------|----------| -| 여백 (Whitespace) | 적절한 여백이 확보되어 있는가? | -| 계층 (Hierarchy) | 정보의 우선순위가 시각적으로 드러나는가? | -| 일관성 (Consistency) | 다른 화면과 일관된 패턴을 따르는가? | -| 접근성 (Accessibility) | 색상 대비, 폰트 크기가 충분한가? | - -### 5.5 검색 & 필터 - -| 기능 | 설명 | -|------|------| -| 텍스트 검색 | 제목, 메모, 태그에서 전문 검색 | -| 카테고리 필터 | 화면 유형별 필터 (탭) | -| 카드 유형 필터 | 레퍼런스 / 분석 / 패턴 / Before/After | -| 평점 필터 | ⭐ 3점 이상만 표시 등 | -| 정렬 | 최신순, 평점순, 이름순 | - -### 5.6 내보내기 - -| 형식 | 설명 | -|------|------| -| JSON | 전체 프로젝트 데이터 백업/복원 | -| HTML | 인사이트 카드를 HTML 보고서로 출력 (인쇄 가능) | -| 패턴 → 기획디자인 | 패턴 카드의 와이어프레임을 기획디자인 템플릿으로 전송 | - -### 5.7 키보드 단축키 - -| 단축키 | 기능 | -|--------|------| -| `Ctrl+V` | 클립보드 이미지로 새 카드 생성 | -| `Ctrl+S` | 프로젝트 저장 | -| `Ctrl+F` | 검색 포커스 | -| `Ctrl+N` | 새 카드 추가 | -| `Delete` | 선택 카드 삭제 | -| `Ctrl+Z` | 실행 취소 | -| `Ctrl+Y` | 다시 실행 | - ---- - -## 6. 데이터 구조 - -### 6.1 프로젝트 (localStorage: `di_projects`) - -```json -[ - { - "id": "diproj_1709856000000", - "title": "SAM ERP v2 디자인 연구", - "description": "SAM ERP 화면 개선을 위한 UI/UX 인사이트 수집", - "cards": [], - "createdAt": "2026-03-08T10:00:00", - "updatedAt": "2026-03-08T15:30:00" - } -] -``` - -### 6.2 인사이트 카드 (cards 배열 내) - -```json -{ - "id": "di_1709856000000_abc", - "type": "reference", - "title": "Notion 대시보드 카드 레이아웃", - "image": "data:image/png;base64,...", - "memo": "카드형 레이아웃이 정보 밀도를 유지하면서도 시각적으로 깔끔", - "source": "notion.so", - "tags": ["대시보드", "카드", "레이아웃"], - "category": "dashboard", - "rating": 4, - "pinned": false, - "archived": false, - "createdAt": "2026-03-08T10:00:00", - "updatedAt": "2026-03-08T15:30:00" -} -``` - -### 6.3 분석 어노테이션 - -```json -{ - "annotations": [ - { - "id": "ann_001", - "type": "marker", - "num": 1, - "x": 150, - "y": 80, - "text": "검색 영역 너무 넓음 — 접기 기능 필요" - }, - { - "id": "ann_002", - "type": "highlight", - "x": 200, - "y": 300, - "w": 150, - "h": 40, - "color": "rgba(239,68,68,0.3)", - "text": "버튼 정렬 불일치" - } - ] -} -``` - -### 6.4 디자인 패턴 라이브러리 (localStorage: `di_patterns`) - -프로젝트 간 공유되는 패턴 라이브러리: - -```json -[ - { - "id": "pat_001", - "name": "검색 + 필터 + 목록", - "image": "data:image/png;base64,...", - "components": [ - { "name": "검색바", "required": true }, - { "name": "필터 칩", "required": false }, - { "name": "데이터 테이블", "required": true }, - { "name": "페이지네이션", "required": true }, - { "name": "액션 버튼", "required": true } - ], - "guidelines": "검색바는 상단 고정, 필터는 접기/펼치기 지원", - "usedIn": ["수주 목록", "거래처 목록", "품목 목록"], - "tags": ["목록", "CRUD", "테이블"], - "frequency": 12, - "createdAt": "2026-03-08T10:00:00" - } -] -``` - ---- - -## 7. 프리셋 데이터 - -### 7.1 기본 카테고리 (하드코딩) - -```javascript -categories: [ - { code: 'dashboard', label: '대시보드', icon: '📊', color: '#6366f1' }, - { code: 'list', label: '목록', icon: '📋', color: '#3b82f6' }, - { code: 'form', label: '상세/폼', icon: '📝', color: '#10b981' }, - { code: 'modal', label: '모달/팝업', icon: '💬', color: '#f59e0b' }, - { code: 'navigation',label: '네비게이션',icon: '🧭', color: '#8b5cf6' }, - { code: 'auth', label: '로그인', icon: '🔐', color: '#ec4899' }, - { code: 'report', label: '보고서', icon: '📄', color: '#0ea5e9' }, - { code: 'etc', label: '기타', icon: '📎', color: '#64748b' }, -] -``` - -### 7.2 CRAP 원칙 체크리스트 (하드코딩) - -```javascript -designPrinciples: [ - { key: 'contrast', label: '대비 (Contrast)', icon: '🔲', desc: '중요 요소가 시각적으로 구분' }, - { key: 'repetition', label: '반복 (Repetition)', icon: '🔁', desc: '일관된 스타일 반복 적용' }, - { key: 'alignment', label: '정렬 (Alignment)', icon: '📏', desc: '논리적 정렬' }, - { key: 'proximity', label: '근접성 (Proximity)', icon: '🧲', desc: '관련 요소 그룹핑' }, - { key: 'whitespace', label: '여백 (Whitespace)', icon: '⬜', desc: '적절한 여백 확보' }, - { key: 'hierarchy', label: '계층 (Hierarchy)', icon: '🔺', desc: '정보 우선순위 시각화' }, - { key: 'consistency', label: '일관성 (Consistency)',icon: '🔗', desc: '다른 화면과의 일관성' }, - { key: 'a11y', label: '접근성 (A11y)', icon: '♿', desc: '색상 대비, 폰트 크기' }, -] -``` - -### 7.3 샘플 패턴 템플릿 (프리셋) - -| 패턴명 | 구성 요소 | SAM 내 사용처 | -|--------|----------|--------------| -| 검색 + 목록 | 검색바, 필터, 테이블, 페이지네이션, 액션버튼 | 수주/거래처/품목 목록 | -| 상세 폼 | 섹션 헤더, 라벨+입력, 저장/취소 버튼 | 수주 상세, 거래처 상세 | -| 대시보드 | 통계 카드 4개, 차트 2개, 요약 테이블 | 메인 대시보드 | -| 탭 레이아웃 | 탭 메뉴, 탭 콘텐츠, 액션 버튼 | 설정, 품목기준관리 | -| 트리 + 상세 | 좌측 트리, 우측 상세 패널 | 메뉴 관리, 조직도 | -| 모달 폼 | 모달 헤더, 입력 필드, 확인/취소 | 등록/수정 팝업 | -| 칸반 보드 | 컬럼 헤더, 드래그 카드, 필터 | 업무 관리 | -| 캘린더 | 월/주/일 뷰, 이벤트 카드, 필터 | 일정 관리, 근태 | - ---- - -## 8. 워크플로우 - -### 8.1 일반 사용 흐름 - -``` -1. 새 연구 프로젝트 생성 ("SAM ERP v2 디자인 연구") - ↓ -2. 레퍼런스 수집 - • 외부 서비스 스크린샷 → Ctrl+V 붙여넣기 - • SAM 기존 화면 스크린샷 → 파일 업로드 - • 태그 + 카테고리 분류 - ↓ -3. 화면 분석 - • 분석 카드 생성 → 어노테이션 추가 - • CRAP 원칙 체크 - • 개선 제안 작성 - ↓ -4. 패턴 추출 - • 반복되는 좋은 패턴 → 패턴 카드로 등록 - • 구성 요소 정리, 사용 가이드라인 작성 - ↓ -5. Before/After 기록 - • 개선 전후 비교 카드 생성 - • 변경 포인트 + 효과 기록 - ↓ -6. 기획디자인 연계 - • 패턴 라이브러리에서 참고하며 스토리보드 작성 -``` - -### 8.2 기획디자인 연계 - -``` -디자인 인사이트 기획디자인 -┌──────────────┐ ┌──────────────┐ -│ 패턴 카드: │ │ 스토리보드: │ -│ "검색+목록" │──참조──→│ 새 페이지에 │ -│ 구성요소 체크 │ │ 패턴 적용 │ -│ 가이드라인 │ │ │ -└──────────────┘ └──────────────┘ -``` - -> 향후 패턴 카드의 구성 요소를 기획디자인 블록 템플릿으로 자동 변환하는 연계 기능을 검토한다. - ---- - -## 9. 개발 로드맵 - -### Phase 1 — 기본 구조 (MVP) - -| 항목 | 내용 | -|------|------| -| 라우트 + 컨트롤러 | `GET /rd/design-insight` → 뷰 반환 | -| 프로젝트 CRUD | 생성/저장/로드/삭제 (localStorage) | -| 레퍼런스 카드 | 이미지 업로드 + 메모 + 태그 + 카테고리 | -| 보드 뷰 | 카드 격자 배열 기본 화면 | -| 검색/필터 | 텍스트 검색, 카테고리 탭 필터 | -| Ctrl+V 붙여넣기 | 클립보드 이미지 → 새 카드 자동 생성 | - -### Phase 2 — 분석 도구 - -| 항목 | 내용 | -|------|------| -| 분석 카드 | 어노테이션 시스템 (마커, 하이라이트) | -| CRAP 체크리스트 | 디자인 원칙 체크 UI | -| Before/After 카드 | 전후 비교 카드 유형 | -| 갤러리 뷰 | 이미지 중심 큰 썸네일 | -| 리스트 뷰 | 테이블형 정렬/필터 | - -### Phase 3 — 패턴 라이브러리 - -| 항목 | 내용 | -|------|------| -| 패턴 카드 | 구성 요소 체크리스트, 가이드라인 | -| 패턴 프리셋 | SAM ERP 기본 패턴 8종 | -| 패턴 공유 | 프로젝트 간 패턴 공유 (di_patterns) | -| 내보내기 | JSON 백업, HTML 보고서 | - -### Phase 4 — 연계 & 고도화 - -| 항목 | 내용 | -|------|------| -| 기획디자인 연계 | 패턴 → 블록 템플릿 변환 | -| DB 저장 전환 | localStorage → DB (협업 지원) | -| 팀 공유 | 다른 사용자와 인사이트 공유 | - ---- - -## 10. 파일 구조 (예상) - -``` -mng/ -├── app/Http/Controllers/ -│ └── RdController.php ← designInsight() 메서드 추가 -├── resources/views/rd/design-insight/ -│ └── index.blade.php ← 전체 CSS + HTML + Alpine.js -└── routes/web.php ← Route 추가 -``` - ---- - -## 11. 관련 문서 - -- [기획디자인 기술 스펙](../features/rd/planning-design.md) — 모티브가 된 스토리보드 에디터 -- [기획디자인 프로젝트](../projects/planning-design/README.md) — 프로젝트 이력 -- [R&D 메뉴 개요](../features/rd/README.md) — R&D 전체 메뉴 구조 - ---- - -**최종 업데이트**: 2026-03-08 diff --git a/sam/docs/plans/fire-shutter-drawing-generator-plan.md b/sam/docs/plans/fire-shutter-drawing-generator-plan.md deleted file mode 100644 index 1b9a680..0000000 --- a/sam/docs/plans/fire-shutter-drawing-generator-plan.md +++ /dev/null @@ -1,753 +0,0 @@ -# 방화셔터 도면생성 기능 기획서 - -> **작성일**: 2026-03-08 -> **상태**: 기획 초안 -> **위치**: MNG > R&D > 방화셔터 도면생성 -> **라우트**: `GET /rd/fire-shutter-drawing` -> **참고**: 기존 `자동도면 생성` (`/rd/auto-drawing`) 구조를 확장 - ---- - -## 1. 개요 - -### 1.1 목적 - -방화셔터의 **가이드레일 단면**과 **셔터박스(케이스) 형태**를 파라미터로 입력하면, **2D 단면도(SVG)**와 **3D 렌더링(Three.js)**을 실시간으로 생성하는 도구를 제공한다. - -### 1.2 핵심 가치 - -| 기존 (수동) | 개선 (SAM 도면생성) | -|-------------|-------------------| -| CAD 프로그램에서 수동 작도 | 파라미터 입력 → 자동 도면 생성 | -| 도면 수정 시 전체 재작업 | 치수 변경 → 실시간 미리보기 | -| 제품별 도면 관리 어려움 | 프리셋 저장/불러오기로 재활용 | -| 영업/설치팀 도면 요청 대기 | 현장에서 즉시 단면도 확인 가능 | - -### 1.3 대상 사용자 - -- 설계팀: 방화셔터 단면 설계 및 검토 -- 영업팀: 고객 제안 시 단면도/3D 이미지 첨부 -- 설치팀: 현장 설치 전 가이드레일/케이스 형태 확인 -- 생산팀: 절곡/제작 사양 확인 - ---- - -## 2. 방화셔터 핵심 구조 - -### 2.1 전체 구성도 - -``` -┌─────────────────── 천장 슬래브 ───────────────────┐ -│ │ -│ ┌──────────── 셔터박스 (HEAD BOX / CASE) ──────┐ │ -│ │ ┌─────┐ ┌─────┐ │ │ -│ │ │브래킷│ [샤프트+슬랫 감김] │브래킷│ │ │ -│ │ └──┬──┘ [모터+감속기+브레이크] └──┬──┘ │ │ -│ │ │ [밸런스 스프링] │ │ │ -│ └─────┼─────────────────────────────────┼──────┘ │ -│ │ │ │ -│ ┌─────┴─────┐ ┌──────┴─────┐ │ -│ │ 가이드레일 │ ← 슬랫 커튼 → │ 가이드레일 │ │ -│ │ (좌) │ (강판/스크린) │ (우) │ │ -│ │ │ │ │ │ -│ │ 연기차단재│ │연기차단재 │ │ -│ │ │ │ │ │ -│ └─────┬─────┘ └──────┬─────┘ │ -│ │ │ │ -│ ══════╧═══ 하장바 (BOTTOM BAR) ═════════╧═══════ │ -│ [고무 실링] │ -└────────────────── 바닥 ──────────────────────────┘ -``` - -### 2.2 주요 구성요소 상세 - -#### A. 가이드레일 (Guide Rail) - -- **형태**: C-채널 단면 (ㄷ자 형태) -- **재질**: 강판 2.3mm 이상 -- **기능**: 슬랫 커튼의 좌우 안내 + 연기 차단 -- **표준 길이**: 2,438mm / 3,305mm / 4,430mm (조합 사용) -- **수량**: 항상 **2개** (좌우 1쌍) -- **부속**: 연기차단재(Smoke Seal Packing), 앵커볼트 - -``` -가이드레일 단면 (상단에서 본 모습) - - ┌────────────┐ - │ │ ← 가이드레일 본체 (C-채널) - │ ┌──────┐ │ - │ │ 연기 │ │ - │ │ 차단 │ │ - │ │ 재 │ │ - │ │ │ │ - │ │슬랫 │ │ - │ │엣지→ ● │ - │ │ │ │ - │ │ 연기 │ │ - │ │ 차단 │ │ - │ │ 재 │ │ - │ └──────┘ │ - │ │ - └────────────┘ - ■■■■■■■■■■■■■ ← 방화벽 -``` - -**파라미터**: - -| 파라미터 | 설명 | 단위 | 기본값 | -|---------|------|------|--------| -| `rail_width` | 레일 전체 폭 | mm | 65 | -| `rail_depth` | 레일 깊이 (채널 깊이) | mm | 50 | -| `rail_thickness` | 강판 두께 | mm | 2.3 | -| `rail_lip` | 립(입구) 높이 | mm | 15 | -| `seal_thickness` | 연기차단재 두께 | mm | 5 | -| `seal_depth` | 연기차단재 깊이 | mm | 40 | -| `slat_thickness` | 슬랫 두께 (끼워지는 부분) | mm | 1.6 | -| `rail_height` | 레일 전체 높이 | mm | 3305 | -| `anchor_spacing` | 앵커볼트 간격 | mm | 500 | - -#### B. 셔터박스 / 케이스 (Head Box / Case) - -- **형태**: 직사각형 박스 (상부 천장 부착) -- **재질**: 강판 1.6mm 이상 -- **기능**: 샤프트/모터/슬랫 감김 수납 -- **표준 규격**: 1500×380mm / 500×380mm (개구부 크기에 따라) - -``` -셔터박스 단면 (정면에서 본 모습) - - ┌─────────────────────────────────────────┐ ← 상판 - │ │ - │ [브래킷] ┌──── 샤프트 ────┐ [브래킷] │ - │ │ │ (슬랫 감김) │ │ │ - │ ├──────┤ ├──────┤ │ - │ │ │ ◎ 중심축 │ │ │ - │ │ └───────────────┘ │ │ - │ │ │ │ - │ │ [모터+감속기] [브레이크] │ │ - │ │ [밸런스 스프링] │ │ - │ │ - └───┬─────────────────────────────────┬───┘ ← 하판 (슬랫 출구) - │ ↓ 슬랫 하강 ↓ │ - └─────────────────────────────────┘ -``` - -**파라미터**: - -| 파라미터 | 설명 | 단위 | 기본값 | -|---------|------|------|--------| -| `box_width` | 케이스 전체 폭 (= 개구부 폭 + 마진) | mm | 1500 | -| `box_height` | 케이스 높이 | mm | 380 | -| `box_depth` | 케이스 깊이 (전후) | mm | 380 | -| `box_thickness` | 케이스 강판 두께 | mm | 1.6 | -| `shaft_diameter` | 샤프트 직경 | mm | 120 | -| `shaft_offset_x` | 샤프트 중심 수평 오프셋 | mm | 0 | -| `shaft_offset_y` | 샤프트 중심 수직 오프셋 | mm | 0 | -| `motor_side` | 모터 위치 (좌/우) | - | 우 | -| `slat_exit_width` | 슬랫 출구 폭 | mm | 1400 | -| `bracket_width` | 브래킷 폭 | mm | 80 | - -#### C. 슬랫 (Steel Slat / Screen) - -- **강판형**: EGI 강판 1.6mm, C/S형 인터록킹 프로파일 -- **스크린형**: 실리카/와이어 원단, 가이드레일 11mm 홈 -- **피치**: 75~100mm - -**파라미터**: - -| 파라미터 | 설명 | 단위 | 기본값 | -|---------|------|------|--------| -| `slat_type` | 슬랫 유형 (강판/스크린) | - | 강판 | -| `slat_pitch` | 슬랫 피치 | mm | 80 | -| `slat_thickness` | 슬랫 두께 | mm | 1.6 | -| `slat_profile` | 단면 형태 (C형/S형) | - | C형 | - -#### D. 하장바 (Bottom Bar) - -- **기능**: 슬랫 커튼 하단 마감 + 바닥 밀착 -- **부속**: 고무 실링 - -**파라미터**: - -| 파라미터 | 설명 | 단위 | 기본값 | -|---------|------|------|--------| -| `bar_width` | 하장바 폭 | mm | 60 | -| `bar_height` | 하장바 높이 | mm | 40 | -| `bar_seal_height` | 고무 실링 높이 | mm | 15 | - ---- - -## 3. 기능 설계 - -### 3.1 탭 구성 - -기존 자동도면 생성의 탭 구조를 참고하여 4개 탭으로 구성한다. - -``` -┌──────────┬──────────┬──────────┬──────────┐ -│ 설정 │ 가이드 │ 셔터박스 │ 3D │ -│ Settings │ 레일 │ (케이스) │ 렌더링 │ -└──────────┴──────────┴──────────┴──────────┘ -``` - -| 탭 | ID | 기능 | -|----|----|------| -| **설정** | `Settings` | 제품 유형 선택, 개구부 크기, 전역 설정 | -| **가이드레일** | `GuideRail` | 가이드레일 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 | -| **셔터박스** | `ShutterBox` | 셔터박스 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 | -| **3D 렌더링** | `3D` | 전체 방화셔터 조립체 3D 렌더링 (Three.js) | - -### 3.2 설정 탭 (Settings) - -#### 입력 항목 - -| 항목 | 타입 | 설명 | -|------|------|------| -| 제품 유형 | 드롭다운 | 강판형 (KFS) / 스크린형 (KSS) | -| 제품 모델 | 드롭다운 | KSS01, KSS02, KFS01 등 (유형 선택 시 필터링) | -| 개구부 폭 (W0) | 숫자 입력 | mm | -| 개구부 높이 (H0) | 숫자 입력 | mm | -| 수량 | 숫자 입력 | 기본값 1 | - -#### 자동 계산 (표시 전용) - -| 항목 | 수식 | 설명 | -|------|------|------| -| 제작 폭 (W1) | 스크린: W0+140 / 강판: W0+110 | 마진 포함 | -| 제작 높이 (H1) | H0+350 | 마진 포함 | -| 면적 (M) | W1 × H1 / 1,000,000 | m² | -| 중량 (K) | 스크린: M×2 / 강판: M×25 | kg | -| 권장 모터 | K 기준 자동 선택 | 150K~1500K | - -#### 프리셋 관리 - -- **프리셋 저장**: 현재 파라미터를 이름 지정하여 localStorage에 저장 -- **프리셋 불러오기**: 저장된 프리셋 목록에서 선택하여 파라미터 복원 -- **기본 프리셋**: 강판형 기본, 스크린형 기본 (제품 유형 선택 시 자동 적용) - -### 3.3 가이드레일 탭 - -#### UI 구성 (2컬럼 레이아웃) - -``` -┌──────────────────────┬──────────────────────────────────┐ -│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │ -│ │ │ -│ ■ 레일 전체 폭: [65] │ │ -│ ■ 레일 깊이: [50] │ ┌────────┐ │ -│ ■ 강판 두께: [2.3] │ │ │ │ -│ ■ 립 높이: [15] │ │ ┌────┐ │ ← SVG 실시간 │ -│ ■ 연기차단재: [5/40] │ │ │ ● │ │ 렌더링 │ -│ ■ 슬랫 두께: [1.6] │ │ └────┘ │ │ -│ │ │ │ │ -│ [치수 표시 ON/OFF] │ └────────┘ │ -│ [연기차단재 ON/OFF] │ ← 치수 라벨 (mm) │ -│ │ │ -│ ■ 레일 높이: [3305] │ [줌 +] [줌 -] [리셋] [DXF 저장] │ -│ ■ 앵커 간격: [500] │ │ -└──────────────────────┴──────────────────────────────────┘ -``` - -#### SVG 단면도 렌더링 상세 - -**뷰 모드 3가지**: - -1. **횡단면도 (Cross-Section)**: 가이드레일을 위에서 본 단면 — 슬랫이 레일에 끼워진 형태 -2. **종단면도 (Longitudinal)**: 가이드레일을 측면에서 본 단면 — 앵커볼트 배치 -3. **정면도 (Front View)**: 가이드레일을 정면에서 본 모습 — 레일 전체 높이 + 앵커 위치 - -**렌더링 요소**: - -| 요소 | 색상 | 설명 | -|------|------|------| -| 레일 본체 | `#94a3b8` (은회색) | 강판 단면 | -| 연기차단재 | `#f97316` (주황) | 실링 재질 | -| 슬랫 엣지 | `#60a5fa` (파랑) | 레일 안의 슬랫 | -| 방화벽 | `#a1887f` (갈색 해칭) | 콘크리트 벽 | -| 앵커볼트 | `#ef4444` (빨강) | 고정 부속 | -| 치수선 | `#3b82f6` (파랑) | mm 단위 치수 | - -### 3.4 셔터박스 탭 - -#### UI 구성 (2컬럼 레이아웃) - -``` -┌──────────────────────┬──────────────────────────────────┐ -│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │ -│ │ │ -│ ■ 케이스 폭: [1500] │ ┌────────────────────────────┐ │ -│ ■ 케이스 높이: [380] │ │ [브래킷] ◎샤프트 [브래킷]│ │ -│ ■ 케이스 깊이: [380] │ │ 감김 슬랫 │ │ -│ ■ 강판 두께: [1.6] │ │ [모터+감속기] [브레이크] │ │ -│ │ └────────────────────────────┘ │ -│ ■ 샤프트 직경: [120] │ │ -│ ■ 샤프트 오프셋 │ ← SVG 실시간 렌더링 │ -│ X: [0] Y: [0] │ ← 치수 라벨 (mm) │ -│ ■ 모터 위치: [좌/우] │ │ -│ │ │ -│ ■ 내부 부품 표시 │ [줌 +] [줌 -] [리셋] [DXF 저장] │ -│ □ 샤프트 │ │ -│ □ 모터/감속기 │ │ -│ □ 브레이크 │ │ -│ □ 밸런스 스프링 │ │ -└──────────────────────┴──────────────────────────────────┘ -``` - -#### SVG 단면도 렌더링 상세 - -**뷰 모드 3가지**: - -1. **정면 단면도**: 케이스를 정면에서 본 내부 구조 (샤프트, 모터, 브래킷 위치) -2. **측면 단면도**: 케이스를 측면에서 본 단면 (깊이 방향, 슬랫 감김 단면) -3. **하부 상세도**: 슬랫 출구 부분 확대 - -**렌더링 요소**: - -| 요소 | 색상 | 설명 | -|------|------|------| -| 케이스 외곽 | `#94a3b8` (은회색) | 강판 박스 | -| 샤프트 | `#64748b` (짙은 회색) | 중심축 + 감김 슬랫 | -| 모터 | `#3b82f6` (파랑) | 전동 개폐기 | -| 브레이크 | `#ef4444` (빨강) | 전자 브레이크 | -| 스프링 | `#22c55e` (녹색) | 밸런스 스프링 | -| 브래킷 | `#8b5cf6` (보라) | 벽 고정 브래킷 | -| 슬랫 | `#f59e0b` (주황) | 감긴 슬랫 단면 | - -### 3.5 3D 렌더링 탭 - -#### 렌더링 대상 - -Three.js를 사용하여 방화셔터 전체 조립체를 3D로 시각화한다. - -``` -3D 렌더링 요소: -├── 셔터박스 (반투명 상자) -│ ├── 샤프트 (원통) -│ ├── 감긴 슬랫 (원통 표면) -│ ├── 모터+감속기 (박스) -│ ├── 브레이크 (디스크) -│ └── 브래킷 (L형 판) -├── 가이드레일 좌 (C-채널 압출) -├── 가이드레일 우 (C-채널 압출) -├── 슬랫 커튼 (평면 텍스처) -│ ├── 강판형: 줄무늬 텍스처 (인터록킹 표현) -│ └── 스크린형: 반투명 메쉬 -├── 하장바 (직사각형 바) -└── 방화벽 (반투명 콘크리트 텍스처) -``` - -#### 3D 인터랙션 - -| 기능 | 조작 | 설명 | -|------|------|------| -| 회전 | 마우스 드래그 | OrbitControls | -| 줌 | 마우스 휠 | 확대/축소 | -| 팬 | 우클릭 드래그 | 시점 이동 | -| 부품 하이라이트 | 마우스 호버 | 해당 부품 강조 + 이름 표시 | -| 부품 ON/OFF | 체크박스 | 개별 부품 표시/숨김 | -| 투명도 | 슬라이더 | 케이스 투명도 조절 (내부 구조 확인) | -| 셔터 개폐 | 슬라이더 | 0%(전개)~100%(전폐) 애니메이션 | -| 조명 | 프리셋 | 기본/스튜디오/야외/드라마틱 | - -#### 3D 모델링 방식 - -DB나 외부 3D 파일 없이, **파라미터 기반 절차적 모델링(Procedural Modeling)**으로 구현한다. - -```javascript -// 가이드레일 C-채널 3D 생성 예시 (Three.js ExtrudeGeometry) -function createGuideRailMesh(params) { - const shape = new THREE.Shape(); - // C-채널 프로파일 경로 정의 - shape.moveTo(0, 0); - shape.lineTo(params.rail_width, 0); - shape.lineTo(params.rail_width, params.rail_lip); - shape.lineTo(params.rail_width - params.rail_thickness, params.rail_lip); - shape.lineTo(params.rail_width - params.rail_thickness, params.rail_thickness); - shape.lineTo(params.rail_thickness, params.rail_thickness); - shape.lineTo(params.rail_thickness, params.rail_lip); - shape.lineTo(0, params.rail_lip); - shape.lineTo(0, 0); - - // 높이 방향으로 압출 - const extrudeSettings = { - depth: params.rail_height, - bevelEnabled: false - }; - - return new THREE.Mesh( - new THREE.ExtrudeGeometry(shape, extrudeSettings), - new THREE.MeshStandardMaterial({ color: 0x94a3b8 }) - ); -} -``` - -### 3.6 출력 기능 - -| 기능 | 형식 | 설명 | -|------|------|------| -| **DXF 다운로드** | `.dxf` | 가이드레일/셔터박스 단면도를 CAD 호환 파일로 저장 | -| **PNG 다운로드** | `.png` | SVG 단면도를 이미지로 저장 | -| **3D 스크린샷** | `.png` | 3D 렌더링 현재 뷰를 이미지로 저장 | -| **파라미터 JSON** | `.json` | 현재 설정값을 파일로 내보내기/가져오기 | - ---- - -## 4. 기술 설계 - -### 4.1 아키텍처 - -기존 자동도면 생성과 동일한 **순수 클라이언트 측** 아키텍처를 사용한다. - -``` -┌─────────────────────────────────────────────┐ -│ Browser (Client-Side Only) │ -│ │ -│ ┌─────────────────────────────────────┐ │ -│ │ Blade Template │ │ -│ │ (fire-shutter-drawing/index.blade) │ │ -│ ├─────────────────────────────────────┤ │ -│ │ JavaScript State Management │ │ -│ │ (fireShutterState 객체) │ │ -│ ├──────────┬──────────────────────────┤ │ -│ │ SVG 엔진 │ Three.js 3D 엔진 │ │ -│ │ (단면도) │ (조립체 렌더링) │ │ -│ ├──────────┴──────────────────────────┤ │ -│ │ DXF 생성기 │ PNG 내보내기 │ │ -│ └─────────────────────────────────────┘ │ -│ │ -│ DB 연동: 없음 (localStorage 프리셋만 사용) │ -│ API 호출: 없음 │ -└─────────────────────────────────────────────┘ -``` - -### 4.2 파일 구조 - -``` -mng/ -├── routes/web.php ← 라우트 추가 -├── app/Http/Controllers/RdController.php ← 메서드 추가 -└── resources/views/rd/fire-shutter-drawing/ - ├── index.blade.php ← 메인 레이아웃 + 탭 UI - ├── partials/ - │ ├── _settings.blade.php ← 설정 탭 HTML - │ ├── _guide-rail.blade.php ← 가이드레일 탭 HTML - │ ├── _shutter-box.blade.php ← 셔터박스 탭 HTML - │ └── _3d-viewer.blade.php ← 3D 렌더링 탭 HTML - └── js/ - (인라인 또는 @push('scripts')에 포함) -``` - -> **참고**: 기존 `auto-drawing/index.blade.php`는 단일 파일 4,884줄이다. 유지보수성을 위해 **Blade partial로 분리**하되, JavaScript는 상태 공유가 필요하므로 메인 파일의 `@push('scripts')`에 통합한다. - -### 4.3 상태 관리 객체 - -```javascript -const fireShutterState = { - // 활성 탭 - activeTab: 'Settings', - - // 설정 탭 - settings: { - productType: 'steel', // 'steel' | 'screen' - productModel: 'KFS01', // 제품 모델 코드 - openWidth: 2000, // 개구부 폭 W0 (mm) - openHeight: 3000, // 개구부 높이 H0 (mm) - quantity: 1, - // 자동 계산 - mfgWidth: 0, // 제작 폭 W1 - mfgHeight: 0, // 제작 높이 H1 - area: 0, // 면적 M (m²) - weight: 0, // 중량 K (kg) - motorSpec: '', // 권장 모터 - }, - - // 가이드레일 파라미터 - guideRail: { - width: 65, - depth: 50, - thickness: 2.3, - lip: 15, - sealThickness: 5, - sealDepth: 40, - slatThickness: 1.6, - height: 3305, - anchorSpacing: 500, - // 뷰 옵션 - showDimensions: true, - showSeal: true, - viewMode: 'cross', // 'cross' | 'longitudinal' | 'front' - }, - - // 셔터박스 파라미터 - shutterBox: { - width: 1500, - height: 380, - depth: 380, - thickness: 1.6, - shaftDiameter: 120, - shaftOffsetX: 0, - shaftOffsetY: 0, - motorSide: 'right', // 'left' | 'right' - slatExitWidth: 1400, - bracketWidth: 80, - // 내부 부품 표시 - showShaft: true, - showMotor: true, - showBrake: true, - showSpring: true, - viewMode: 'front', // 'front' | 'side' | 'bottom' - }, - - // 슬랫 파라미터 - slat: { - type: 'steel', // 'steel' | 'screen' - pitch: 80, - thickness: 1.6, - profile: 'C', // 'C' | 'S' - }, - - // 하장바 파라미터 - bottomBar: { - width: 60, - height: 40, - sealHeight: 15, - }, - - // 3D 뷰 설정 - threeD: { - caseOpacity: 0.3, // 케이스 투명도 - shutterPosition: 100, // 0=전개, 100=전폐 - showComponents: { - case: true, - shaft: true, - motor: true, - brake: true, - spring: true, - guideRailL: true, - guideRailR: true, - slats: true, - bottomBar: true, - wall: true, - }, - lightPreset: 'default', - }, - - // 프리셋 관리 - presets: [], // localStorage에서 로드 - - // 뷰 컨트롤 (줌/팬) - view: { - scale: 1, - offset: { x: 0, y: 0 }, - isDragging: false, - }, -}; -``` - -### 4.4 제품 유형별 기본값 매핑 - -```javascript -const PRODUCT_DEFAULTS = { - steel: { - label: '강판형', - marginW: 110, // W1 = W0 + 110 - marginH: 350, // H1 = H0 + 350 - weightFactor: 25, // K = M × 25 - guideRail: { width: 65, depth: 50, thickness: 2.3, lip: 15 }, - slat: { type: 'steel', pitch: 80, thickness: 1.6, profile: 'C' }, - }, - screen: { - label: '스크린형', - marginW: 140, // W1 = W0 + 140 - marginH: 350, // H1 = H0 + 350 - weightFactor: 2, // K = M × 2 - guideRail: { width: 30, depth: 25, thickness: 1.5, lip: 11 }, - slat: { type: 'screen', pitch: 100, thickness: 0.8, profile: 'flat' }, - }, -}; - -const MOTOR_TABLE = [ - { maxWeight: 150, spec: '150K', inch: 4 }, - { maxWeight: 300, spec: '300K', inch: 4 }, - { maxWeight: 500, spec: '500K', inch: 5 }, - { maxWeight: 750, spec: '750K', inch: 5 }, - { maxWeight: 1000, spec: '1000K', inch: 6 }, - { maxWeight: 1500, spec: '1500K', inch: 6 }, -]; -``` - ---- - -## 5. 개발 단계 - -### Phase 1: 기본 구조 + 가이드레일 단면도 (1단계) - -> **목표**: 라우트/컨트롤러/뷰 생성, 설정 탭, 가이드레일 SVG 단면도 - -| 작업 | 상세 | 예상 | -|------|------|------| -| 라우트 등록 | `GET /rd/fire-shutter-drawing` | 10분 | -| 컨트롤러 메서드 | `RdController@fireShutterDrawing` | 10분 | -| 레이아웃 + 탭 UI | 4탭 구조, 다크 테마 | 1시간 | -| 설정 탭 | 제품 유형/개구부 크기 입력 + 자동 계산 | 1시간 | -| 가이드레일 SVG 엔진 | C-채널 단면도 + 치수선 + 연기차단재 | 3시간 | -| 줌/팬 컨트롤 | 기존 auto-drawing 코드 재사용 | 30분 | - -**산출물**: 가이드레일 파라미터 입력 → SVG 횡단면도 실시간 렌더링 - -### Phase 2: 셔터박스 단면도 (2단계) - -> **목표**: 셔터박스(케이스) SVG 단면도 + 내부 부품 표시 - -| 작업 | 상세 | 예상 | -|------|------|------| -| 셔터박스 SVG 엔진 | 케이스 외곽 + 내부 구조 | 3시간 | -| 내부 부품 렌더링 | 샤프트, 모터, 브레이크, 스프링 | 2시간 | -| 뷰 모드 전환 | 정면/측면/하부 | 1시간 | -| 부품 ON/OFF 토글 | 체크박스 → SVG 요소 표시/숨김 | 30분 | - -**산출물**: 셔터박스 파라미터 입력 → SVG 단면도 (정면/측면/하부) - -### Phase 3: 3D 렌더링 (3단계) - -> **목표**: Three.js 기반 방화셔터 전체 조립체 3D 렌더링 - -| 작업 | 상세 | 예상 | -|------|------|------| -| Three.js 씬 구축 | 카메라, 조명, OrbitControls | 1시간 | -| 가이드레일 3D 모델 | ExtrudeGeometry (C-채널 압출) | 2시간 | -| 셔터박스 3D 모델 | BoxGeometry + 내부 부품 | 2시간 | -| 슬랫 커튼 3D 모델 | 평면 메쉬 + 텍스처 | 1시간 | -| 셔터 개폐 애니메이션 | 슬라이더 → 슬랫 위치 변경 | 1시간 | -| 조명/투명도 패널 | 기존 auto-drawing 패널 재사용 | 30분 | - -**산출물**: 파라미터 연동 3D 방화셔터 조립체 + 인터랙션 - -### Phase 4: 출력 + 프리셋 (4단계) - -> **목표**: DXF/PNG 저장, 프리셋 관리, 완성도 향상 - -| 작업 | 상세 | 예상 | -|------|------|------| -| DXF 내보내기 | 기존 DXF 생성기 확장 | 1시간 | -| PNG 내보내기 | SVG → Canvas → PNG | 30분 | -| 3D 스크린샷 | Three.js renderer.domElement.toDataURL | 15분 | -| 프리셋 저장/불러오기 | localStorage CRUD | 1시간 | -| JSON 가져오기/내보내기 | 파일 업로드/다운로드 | 30분 | -| UI 다듬기 | 반응형, 툴팁, 키보드 단축키 | 1시간 | - -**산출물**: 완성된 방화셔터 도면생성 도구 - ---- - -## 6. UI/UX 설계 - -### 6.1 디자인 시스템 - -기존 자동도면 생성의 **다크 테마 (Space Theme)**를 그대로 이어간다. - -| 요소 | 값 | -|------|------| -| 배경 | `#020617` (slate-950) | -| 패널 | `rgba(15, 23, 42, 0.7)` + backdrop-blur | -| 테두리 | `rgba(255, 255, 255, 0.1)` | -| 강조색 | `#3b82f6` (blue-500) | -| 텍스트 | `#f8fafc` (white) / `#94a3b8` (slate-400) | -| 입력 필드 | `bg-slate-950/80` + `border-slate-800` | - -### 6.2 반응형 레이아웃 - -``` -Desktop (1200px+): 2컬럼 (4:8 비율) -Tablet (768-1199px): 1컬럼 (상: 파라미터, 하: 미리보기) -Mobile: 지원 안 함 (최소 768px) -``` - -### 6.3 인터랙션 흐름 - -``` -사용자 → 제품 유형 선택 (강판/스크린) - → 기본값 자동 적용 - → 개구부 크기 입력 - → 제작 치수/중량/모터 자동 계산 - - → 가이드레일 탭 이동 - → 파라미터 조정 (폭, 깊이, 두께 등) - → SVG 실시간 업데이트 (입력 즉시) - → 뷰 모드 전환 (횡단면/종단면/정면) - → DXF 다운로드 가능 - - → 셔터박스 탭 이동 - → 파라미터 조정 - → SVG 실시간 업데이트 - → 내부 부품 ON/OFF - - → 3D 탭 이동 - → 전체 조립체 3D 뷰 - → 셔터 개폐 애니메이션 - → 스크린샷 저장 -``` - ---- - -## 7. 메뉴 등록 - -### 7.1 메뉴 위치 - -``` -R&D -├── 대시보드 -├── 조직도 관리 -├── 중대재해처벌법 점검 -├── AI 견적 -├── 기획디자인 -├── 디자인 인사이트 -├── 사운드 로고 스튜디오 -├── CM송 제작 -├── 자동도면 생성 ← 기존 -└── 방화셔터 도면생성 ← 신규 (자동도면 하위에 배치) -``` - -### 7.2 메뉴 등록 (tinker) - -```php -// 개발 서버 -App\Models\Commons\Menu::create([ - 'tenant_id' => 1, - 'parent_id' => , - 'name' => '방화셔터 도면생성', - 'url' => '/rd/fire-shutter-drawing', - 'icon' => 'shield', - 'sort_order' => <자동도면 다음 순서>, - 'is_active' => true, -]); -``` - -> **주의**: 메뉴 시더 실행 금지 — tinker로 수동 등록 - ---- - -## 8. 향후 확장 가능성 - -| 확장 | 설명 | 우선순위 | -|------|------|---------| -| **STL/OBJ 내보내기** | 3D 프린팅/CAD 호환 | 🟡 중요 | -| **견적 연동** | 도면 파라미터 → 견적 자동 산출 | 🔴 필수 | -| **제품 카탈로그 연동** | DB에서 제품별 기본 파라미터 로드 | 🟡 중요 | -| **비교 모드** | 2개 설정을 나란히 비교 | 🟢 권장 | -| **PDF 도면 출력** | A3/A4 도면 양식 포함 출력 | 🟡 중요 | -| **설치 시뮬레이션** | 현장 사진 위에 3D 오버레이 | 🟢 권장 | - ---- - -## 관련 문서 - -- `/home/aweso/sam/docs/features/academy/fire-shutter-image-prompts.md` — 방화셔터 이미지 프롬프트 -- `/home/aweso/sam/docs/samples/방화셔터_견적구조_인터뷰.md` — 견적 구조 인터뷰 -- `/home/aweso/sam/docs/features/quotes/README.md` — 견적 시스템 분석 -- `/home/aweso/sam/docs/projects/quotation/phase-1-5130-analysis/js-formulas.md` — 견적 수식 상세 -- `/home/aweso/sam/mng/resources/views/rd/auto-drawing/index.blade.php` — 기존 자동도면 생성 - ---- - -**최종 업데이트**: 2026-03-08 diff --git a/sam/docs/plans/sound-logo-generator-plan.md b/sam/docs/plans/sound-logo-generator-plan.md deleted file mode 100644 index a3ecf48..0000000 --- a/sam/docs/plans/sound-logo-generator-plan.md +++ /dev/null @@ -1,637 +0,0 @@ -# 사운드 로고 생성기 — 기획서 - -> **작성일**: 2026-03-08 -> **상태**: 기획 확정 -> **메뉴명**: 사운드 로고 생성기 -> **라우트**: `GET /rd/sound-logo` -> **담당**: R&D실 - ---- - -## 1. 개요 - -### 1.1 목적 - -1~5초의 **짧고 강렬한 시그니처 사운드(Sound Logo)**를 생성하는 도구. 브라우저 내 Web Audio API 신디사이저와 Google Gemini AI를 결합하여, 누구나 전문적인 사운드 로고를 만들 수 있도록 한다. - -### 1.2 벤치마킹 - -| 브랜드 | 사운드 | 길이 | 특징 | -|--------|--------|------|------| -| Intel | 봉-봉봉봉-봉 | 1.5초 | 5음, 밝고 미래적 | -| Netflix | 타-둠 | 3초 | 2음, 깊은 울림 + 리버브 | -| Samsung | 오버더호라이즌 | 2초 | 5음, 따뜻한 멜로디 | -| McDonald's | 바다바바~ | 2초 | 5음, 경쾌한 리듬 | -| Windows | 시작 사운드 | 3초 | 4음, 화음 진행 | -| 카카오톡 | 카톡~ | 0.5초 | 2음, 귀여운 효과음 | -| T-Mobile | 띠-띠띠-띠-띠 | 1초 | 5음, 단순 반복 | - -### 1.3 핵심 차별점 — AI 어드바이저 엔진 - -단순 신디사이저 도구가 아니라, **Google Gemini AI가 음악 이론 기반으로 조언하고 생성을 도와주는** 지능형 도구. - -``` -┌─────────────────────────────────────────────────────┐ -│ 사운드 로고 생성기 │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ 수동 모드 │ │ AI 어시스트│ │ AI 자동 │ │ -│ │ (신디사이저)│ ←→ │ (Gemini) │ ←→ │ (Lyria) │ │ -│ │ │ │ │ │ │ │ -│ │ 음표 직접 │ │ 브랜드 분석│ │ 프롬프트→ │ │ -│ │ 배치/편집 │ │ 음악 추천 │ │ AI 음악 │ │ -│ │ │ │ 코드 제안 │ │ 직접 생성 │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ ↕ ↕ ↕ │ -│ ┌──────────────────────────────────────────┐ │ -│ │ Web Audio API 재생 엔진 │ │ -│ │ (실시간 미리듣기 + WAV 내보내기) │ │ -│ └──────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## 2. 3가지 모드 설계 - -### 2.1 모드 A — 수동 모드 (신디사이저) - -> 음표를 직접 배치하고 파라미터를 조절하여 사운드를 만드는 전통적 방식 - -| 기능 | 설명 | -|------|------| -| 음표 시퀀서 | 음표(C4, E4 등) + 길이(0.05~2초) + 쉼표를 시각적 배열로 편집 | -| 신디사이저 4종 | Sine(부드러움), Square(8bit), Triangle(따뜻함), Sawtooth(날카로움) | -| ADSR 엔벨로프 | Attack(0~500ms), Decay(0~1s), Sustain(0~1), Release(0~3s) | -| 화음(Chord) | 동시에 여러 음 재생 (C+E+G = C Major 등) | -| 이펙트 | 리버브, 딜레이, 로우패스/하이패스 필터 | -| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 표시 | - -**기술 기반**: Web Audio API (`OscillatorNode`, `GainNode`, `BiquadFilterNode`, `ConvolverNode`) - -### 2.2 모드 B — AI 어시스트 (Gemini 텍스트) - -> 브랜드 정보를 입력하면 Gemini가 **음악 이론 기반으로 사운드 로고를 설계**해 주고, 사용자가 미세 조정 - -**입력 → AI 분석 → 음표 데이터 출력 → Web Audio 재생** - -``` -사용자 입력 Gemini 분석·추천 출력 -┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐ -│ 브랜드명 │ │ │ │ │ -│ 업종/분위기 │──요청──→ │ 1. 브랜드 성격 분석 │──JSON──→ │ 음표 시퀀스 │ -│ 키워드 │ │ 2. 조성/스케일 추천 │ │ BPM, 조성 │ -│ 참고 브랜드 │ │ 3. 음표 시퀀스 생성 │ │ 신디 파라미터 │ -│ 원하는 느낌 │ │ 4. ADSR 파라미터 제안 │ │ ADSR 값 │ -└───────────────┘ │ 5. 이펙트 추천 │ │ 이펙트 설정 │ - │ 6. 근거 설명 │ │ │ - └──────────────────────┘ └──────────────┘ - ↓ - Web Audio 재생 - ↓ - 사용자 미세 조정 -``` - -**Gemini 프롬프트 설계**: - -``` -당신은 사운드 브랜딩 전문가이자 음악 이론가입니다. -다음 브랜드 정보를 분석하여 1~5초 사운드 로고를 설계해 주세요. - -[브랜드 정보] -- 브랜드명: {name} -- 업종: {industry} -- 브랜드 성격: {personality} (예: 혁신적, 신뢰, 친근함) -- 참고 사운드: {reference} (예: 인텔처럼 밝은 느낌) -- 원하는 길이: {duration}초 - -[응답 형식 - 반드시 JSON으로] -{ - "analysis": "브랜드 분석 설명 (한글)", - "reasoning": "이 사운드를 추천하는 음악 이론적 근거 (한글)", - "key": "C", - "scale": "major", - "bpm": 120, - "synth": "sine", - "adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 }, - "effects": { "reverb": 0.3, "delay": 0 }, - "notes": [ - { "note": "C5", "duration": 0.2, "velocity": 0.8 }, - { "note": "E5", "duration": 0.2, "velocity": 0.9 }, - { "note": "G5", "duration": 0.15, "velocity": 0.7 }, - { "rest": 0.05 }, - { "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 } - ], - "variations": [ - { "name": "밝은 버전", "notes": [...] }, - { "name": "차분한 버전", "notes": [...] } - ] -} -``` - -**AI 어시스트 기능 상세**: - -| 기능 | 설명 | -|------|------| -| 브랜드 분석 | 업종·성격 기반 적합한 조성/템포/음색 추천 | -| 음표 시퀀스 생성 | 음악 이론(화성학, 리듬 패턴) 기반 멜로디 제안 | -| 변형 3종 제공 | 밝은/차분한/임팩트 버전 동시 생성 | -| 근거 설명 | "C Major → 신뢰감, 5도 상행 → 상승 에너지" 등 이론 설명 | -| 반복 개선 | "좀 더 밝게" "더 짧게" 등 자연어로 수정 요청 | - -### 2.3 모드 C — AI 자동 생성 (Google Lyria) - -> Google Lyria AI가 프롬프트 기반으로 **실제 음악을 직접 생성** - -**2가지 Lyria 엔진 지원** (기존 API 키로 사용 가능, 별도 발급 불필요): - -| 엔진 | 인증 | 방식 | 특징 | -|------|------|------|------| -| **Lyria RealTime** (권장) | 기존 Gemini API 키 | WebSocket (브라우저 직접) | 실시간 스트리밍, BPM/스케일 실시간 조절 | -| Lyria 2 (폴백) | Vertex AI 서비스 계정 | REST API (서버 경유) | 30초 단위 파일 생성, $0.06/30초 | - -#### Lyria RealTime — 브라우저에서 직접 음악 생성 - -``` -브라우저 (Alpine.js) Google API -┌───────────────────┐ ┌──────────────────┐ -│ BPM: 130 │ │ │ -│ Scale: C Major │──WebSocket 연결──→ │ Lyria RealTime │ -│ 프롬프트 입력 │ │ (lyria-realtime- │ -│ │←─2초 단위 오디오── │ exp) │ -│ 🔊 실시간 재생 │ │ │ -│ BPM 슬라이더 조절 │──실시간 파라미터──→ │ 즉시 반영 │ -└───────────────────┘ └──────────────────┘ -``` - -- **모델**: `lyria-realtime-exp` (experimental, Gemini API v1alpha) -- **인증**: 기존 `.env`의 `GEMINI_API_KEY` 그대로 사용 -- **출력**: 48kHz 스테레오, 2초 청크 단위 스트리밍 -- **제어 파라미터**: BPM(60~200), Scale(Key + Mode), 텍스트 프롬프트 -- **지연**: 파라미터 변경 후 최대 2초 이내 반영 - -**Lyria RealTime 프롬프트 예시**: - -``` -Short sonic logo. Bright, futuristic, memorable melody. -Clean synthesizer with light reverb. -Ascending progression, major chord resolution. -``` - -#### Lyria 2 — 서버 경유 파일 생성 (폴백) - -기존 `BgmService::generateWithLyria()` 패턴 재활용. Vertex AI 서비스 계정(`google_service_account.json`) 이미 보유. - -``` -사용자 입력 서버 (Laravel) 결과 -┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐ -│ 분위기 선택 │ │ SoundLogoService │ │ │ -│ 길이 (1~5초) │──POST──→ │ → Lyria 2 API (REST) │──→ │ WAV/MP3 파일 │ -│ 프롬프트 입력 │ │ (Vertex AI) │ │ (다운로드) │ -└───────────────┘ └──────────────────────┘ └──────────────┘ -``` - -> **우선순위**: Lyria RealTime(브라우저) 먼저 시도 → 실패 시 Lyria 2(서버) 폴백. -> 두 엔진 모두 기존 인증 정보로 사용 가능하며 별도 API 키 발급 불필요. - ---- - -## 3. UI 레이아웃 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎵 사운드 로고 생성기 [내 프로젝트 ▾] [저장] │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 모드: [🎹 수동] [🤖 AI 어시스트] [✨ AI 자동] │ -│ │ -│ ┌─ 모드 B: AI 어시스트 ──────────────────────────────────────┐ │ -│ │ │ │ -│ │ 브랜드명 [SAM ] │ │ -│ │ 업종 [ERP/MES 통합 솔루션 ] │ │ -│ │ 브랜드 성격 [○혁신적 ●신뢰 ○친근 ○고급 ○에너지] │ │ -│ │ 참고 사운드 [인텔처럼 짧고 밝은 ▾ ] │ │ -│ │ 길이 [━━━●━━━ 2초 ] │ │ -│ │ │ │ -│ │ [🤖 AI에게 사운드 설계 요청] │ │ -│ │ │ │ -│ │ ┌─ AI 분석 결과 ─────────────────────────────────────┐ │ │ -│ │ │ 💡 "SAM은 ERP/MES 통합 솔루션으로, 신뢰와 기술력을 │ │ │ -│ │ │ 전달해야 합니다. C Major 조성으로 안정감을, │ │ │ -│ │ │ 5도 상행 진행으로 성장과 발전을 표현합니다." │ │ │ -│ │ │ │ │ │ -│ │ │ 추천: C Major | BPM 130 | Sine + 리버브 30% │ │ │ -│ │ │ │ │ │ -│ │ │ 변형 3종: │ │ │ -│ │ │ [▶ 밝은 버전] [▶ 차분한 버전] [▶ 임팩트 버전] │ │ │ -│ │ └─────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ 💬 추가 요청: [좀 더 짧고 강렬하게 해줘 ] [전송] │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ 음표 에디터 (AI 결과 또는 수동 편집) ──────────────────────┐ │ -│ │ C5(0.2s) E5(0.2s) G5(0.15s) .(0.05s) [CEG](0.8s) │ │ -│ │ ████ ████ ███ · ████████████ │ │ -│ │ [+음표] [+쉼표] [+화음] [삭제] │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ 파라미터 ────────────────────────────────────────────────┐ │ -│ │ 음색: ●Sine ○Square ○Triangle ○Sawtooth │ │ -│ │ BPM: ━━━━━━●━━━━━ 130 │ │ -│ │ 리버브: ━━━●━━━━━━━ 30% │ │ -│ │ Attack: ━●━━━━━━━━ 10ms Release: ━━━━━●━━━ 500ms │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ 파형 시각화 ─────────────────────────────────────────────┐ │ -│ │ ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿ │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ [▶ 재생] [⏹ 정지] [💾 WAV 저장] [📋 JSON 내보내기] │ -│ │ -├─ 내 사운드 라이브러리 ──────────────────────────────────────────┤ -│ 🎵 SAM 시그널 v1 | 🎵 알림음 v2 | 🎵 전환 효과 | [+ 새로] │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. 프리셋 템플릿 (10종) - -| 프리셋 | 스타일 | 음표 수 | 길이 | 용도 | -|--------|--------|---------|------|------| -| 기업 시그널 (밝음) | Intel 스타일 | 5 | 1.5초 | 브랜드 인트로 | -| 기업 시그널 (무게감) | Netflix 스타일 | 2 | 3초 | 프리미엄 브랜드 | -| 알림음 (경쾌) | 카카오톡 스타일 | 2~3 | 0.5초 | 푸시 알림 | -| 알림음 (정보) | Slack 스타일 | 3 | 1초 | 시스템 알림 | -| 성공 사운드 | 게임 레벨업 | 4 | 1초 | 작업 완료 | -| 에러 사운드 | 경고음 | 2 | 0.5초 | 오류 알림 | -| 전환 효과 (업) | 상승 스윕 | 연속 | 0.5초 | 화면 전환 | -| 전환 효과 (다운) | 하강 스윕 | 연속 | 0.5초 | 메뉴 닫기 | -| 팡파레 | 축하 | 6 | 2초 | 이벤트/달성 | -| 로딩 루프 | 반복 패턴 | 4 | 2초 | 대기 상태 | - ---- - -## 5. 기술 아키텍처 - -### 5.1 기술 스택 - -| 계층 | 기술 | 설명 | -|------|------|------| -| 프론트엔드 | Blade + Alpine.js | 단일 파일 SPA (디자인 인사이트 패턴) | -| 오디오 엔진 | Web Audio API | `OscillatorNode`, `GainNode`, `ConvolverNode` | -| 시각화 | Canvas API | `AnalyserNode` → 파형/스펙트럼 렌더링 | -| AI 어시스트 | Gemini 2.5 Flash | 텍스트 기반 음악 이론 분석·추천 (모드 B) | -| AI 자동 생성 (1차) | Lyria RealTime | WebSocket 실시간 음악 스트리밍 (모드 C, 브라우저 직접) | -| AI 자동 생성 (폴백) | Lyria 2 (Vertex AI) | REST API 파일 생성 (모드 C, 서버 경유) | -| 저장 | localStorage + DB | 프로젝트 데이터(localStorage), 음원 파일(DB+Storage) | - -### 5.2 기존 인프라 재활용 - -| 기존 코드 | 재활용 내용 | -|----------|-----------| -| `BgmService::generateWithLyria()` | Lyria API 호출 패턴, Vertex AI 인증 흐름 | -| `BgmService::getMoodChord()` | 분위기별 화음 주파수 매핑 | -| `BgmService::generateAmbient()` | FFmpeg 기반 오디오 합성 (서버사이드 폴백) | -| `CmSongController::generateLyrics()` | Gemini API 호출 패턴 (프롬프트 → JSON 응답) | -| `CmSongController::pcmToWav()` | PCM → WAV 변환 유틸리티 | -| `AiConfig::getActiveGemini()` | AI 설정 조회 (API 키, 모델, 리전) | -| `GoogleCloudService::getAccessToken()` | Vertex AI 인증 토큰 | - -### 5.3 파일 구조 - -``` -app/Http/Controllers/Rd/ -└── SoundLogoController.php # 컨트롤러 (AI API 프록시) - -app/Services/Rd/ -└── SoundLogoService.php # Gemini 프롬프트 + Lyria 호출 - -resources/views/rd/sound-logo/ -└── index.blade.php # 단일 파일 SPA - -routes/web.php # 라우트 추가 -``` - -### 5.4 라우트 설계 - -| Method | Path | 컨트롤러 | 설명 | -|--------|------|---------|------| -| `GET` | `/rd/sound-logo` | `soundLogo.index` | 메인 페이지 | -| `POST` | `/rd/sound-logo/ai-assist` | `soundLogo.aiAssist` | Gemini 음악 설계 요청 (모드 B) | -| `POST` | `/rd/sound-logo/ai-refine` | `soundLogo.aiRefine` | Gemini 추가 수정 요청 | -| `POST` | `/rd/sound-logo/ai-generate` | `soundLogo.aiGenerate` | Lyria 음악 생성 (모드 C) | -| `POST` | `/rd/sound-logo/save` | `soundLogo.save` | 사운드 저장 (DB + Storage) | -| `GET` | `/rd/sound-logo/{id}/download` | `soundLogo.download` | WAV 다운로드 | - -### 5.5 Web Audio API 핵심 구조 - -```javascript -// 노드 그래프 -const ctx = new AudioContext(); - -// Oscillator → Gain(ADSR) → Filter → Reverb → Analyser → Destination -function createSynthChain(type, freq, adsr, effects) { - const osc = ctx.createOscillator(); // 음원 - const gain = ctx.createGain(); // ADSR 엔벨로프 - const filter = ctx.createBiquadFilter(); // LP/HP 필터 - const analyser = ctx.createAnalyser(); // 시각화 - - osc.type = type; // sine | square | triangle | sawtooth - osc.frequency.value = freq; // Hz - - // ADSR - const now = ctx.currentTime; - gain.gain.setValueAtTime(0, now); - gain.gain.linearRampToValueAtTime(1, now + adsr.attack); - gain.gain.linearRampToValueAtTime(adsr.sustain, now + adsr.attack + adsr.decay); - - osc.connect(gain).connect(filter).connect(analyser).connect(ctx.destination); - return { osc, gain, filter, analyser }; -} - -// WAV 내보내기 (OfflineAudioContext) -async function exportWav(notes, params) { - const offline = new OfflineAudioContext(2, 44100 * duration, 44100); - // ... 노트 렌더링 - const buffer = await offline.startRendering(); - const wav = audioBufferToWav(buffer); - // Blob → 다운로드 -} -``` - -### 5.6 음표 ↔ 주파수 매핑 - -```javascript -const NOTE_FREQ = { - 'C3': 130.81, 'D3': 146.83, 'E3': 164.81, 'F3': 174.61, - 'G3': 196.00, 'A3': 220.00, 'B3': 246.94, - 'C4': 261.63, 'D4': 293.66, 'E4': 329.63, 'F4': 349.23, - 'G4': 392.00, 'A4': 440.00, 'B4': 493.88, - 'C5': 523.25, 'D5': 587.33, 'E5': 659.25, 'F5': 698.46, - 'G5': 783.99, 'A5': 880.00, 'B5': 987.77, - 'C6': 1046.50 - // 반음(#/b)도 포함 -}; -``` - ---- - -## 6. Phase별 개발 계획 - -### Phase 1 — MVP (수동 모드 + 프리셋) - -| 항목 | 내용 | -|------|------| -| 음표 시퀀서 UI | 음표 추가/삭제/편집, 드래그 순서 변경 | -| 신디사이저 4종 | Sine, Square, Triangle, Sawtooth | -| ADSR 슬라이더 | Attack, Decay, Sustain, Release | -| 실시간 재생 | Web Audio API 즉시 재생 | -| WAV 내보내기 | OfflineAudioContext → WAV 다운로드 | -| 프리셋 10종 | 즉시 로드 가능한 사운드 패턴 | -| 프로젝트 저장 | localStorage (디자인 인사이트 패턴) | - -### Phase 2 — AI 어시스트 (Gemini) - -| 항목 | 내용 | -|------|------| -| 브랜드 입력 폼 | 브랜드명, 업종, 성격, 참고 사운드, 길이 | -| Gemini 분석 API | 브랜드 → 음악 이론 기반 사운드 설계 JSON | -| 변형 3종 생성 | 밝은/차분한/임팩트 버전 동시 제공 | -| 대화형 개선 | "좀 더 밝게" 등 자연어 추가 수정 | -| 근거 표시 | AI가 이 사운드를 추천하는 이유 설명 | -| 라우트 | `POST /rd/sound-logo/ai-assist`, `ai-refine` | - -### Phase 3 — AI 자동 생성 (Lyria RealTime + Lyria 2) + 고도화 - -| 항목 | 내용 | -|------|------| -| Lyria RealTime 연동 | WebSocket으로 브라우저에서 직접 실시간 음악 생성 (기존 Gemini API 키) | -| Lyria 2 폴백 | Vertex AI REST API로 서버 경유 파일 생성 (기존 서비스 계정) | -| 실시간 BPM/스케일 조절 | Lyria RealTime의 파라미터 실시간 변경 | -| 화음(Chord) 편집 | 동시에 여러 음 배치 | -| 이펙트 체인 | 리버브, 딜레이, 필터 | -| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 | -| DB 저장 | 사운드 로고를 DB + Storage에 영구 저장 | -| 공유/내보내기 | JSON 설정 공유, MP3 변환 | - -### Phase 4 — 프로급 확장 (선택) - -| 항목 | 내용 | -|------|------| -| 타임라인 UI | 드래그로 음표 배치하는 DAW 스타일 | -| 샘플 기반 음색 | 피아노, 벨, 마림바 등 실제 악기 | -| 드럼/퍼커션 | 노이즈 기반 킥/스네어/하이햇 | -| MIDI 내보내기 | 전문 DAW에서 추가 편집 가능 | -| A/B 비교 | 두 사운드를 나란히 비교 재생 | - ---- - -## 7. Gemini AI 연동 상세 - -### 7.1 API 호출 흐름 - -``` -Frontend (Alpine.js) - │ - │ POST /rd/sound-logo/ai-assist - │ { brand_name, industry, personality, reference, duration } - │ - ▼ -SoundLogoController::aiAssist() - │ - │ 프롬프트 구성 - │ - ▼ -Gemini 2.5 Flash API - │ - │ JSON 응답 (notes, adsr, effects, analysis) - │ - ▼ -SoundLogoController → JsonResponse - │ - │ { success: true, data: { notes, params, analysis, variations } } - │ - ▼ -Frontend: 음표 에디터에 자동 로드 → 즉시 재생 -``` - -### 7.2 대화형 개선 흐름 - -``` -사용자: "좀 더 짧고 강렬하게" - │ - ▼ -POST /rd/sound-logo/ai-refine -{ previous_notes: [...], feedback: "좀 더 짧고 강렬하게" } - │ - ▼ -Gemini: 기존 노트를 분석하고 피드백 반영하여 수정된 JSON 반환 - │ - ▼ -수정된 음표가 에디터에 반영 -``` - -### 7.3 Lyria 음악 생성 흐름 - -#### 7.3.1 Lyria RealTime (브라우저 직접, 권장) - -``` -사용자: "AI 자동 생성" 탭 → Lyria RealTime 선택 - │ - ▼ -브라우저 JavaScript (Alpine.js) - │ - │ WebSocket 연결 (Gemini API v1alpha) - │ wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent - │ API Key: .env GEMINI_API_KEY (서버 경유 프록시) - │ Model: lyria-realtime-exp - │ - ▼ -Lyria RealTime 스트리밍 - │ - │ 2초 청크 단위 48kHz 스테레오 오디오 - │ ← BPM/Scale 실시간 조절 가능 - │ - ▼ -Web Audio API로 실시간 재생 + 녹음(MediaRecorder) → WAV 저장 -``` - -> **API 키 보안**: 브라우저에서 직접 Gemini API 키를 노출하지 않기 위해, -> 서버를 WebSocket 프록시로 사용하거나 `/rd/sound-logo/ws-token` 엔드포인트에서 -> 임시 토큰을 발급하는 방식을 검토한다. - -#### 7.3.2 Lyria 2 (서버 경유, 폴백) - -``` -사용자: Lyria RealTime 실패 시 자동 전환 - │ - ▼ -POST /rd/sound-logo/ai-generate -{ mood: "bright_futuristic", duration: 3, prompt: "..." } - │ - ▼ -SoundLogoService::generateWithLyria() - ├── AiConfig::getActiveGemini() → Vertex AI 설정 확인 - ├── GoogleCloudService::getAccessToken() → OAuth 토큰 - └── Lyria API 호출 → audioContent (base64) - │ - ▼ -WAV 파일 저장 → 다운로드 URL 반환 -``` - ---- - -## 8. 데이터 모델 - -### 8.1 localStorage 구조 (Phase 1~2) - -```json -{ - "sl_projects": [ - { - "id": "sl_1709000000_abc", - "title": "SAM 사운드 로고", - "sounds": [ - { - "id": "snd_001", - "name": "SAM 시그널 v1", - "notes": [ - { "note": "C5", "duration": 0.2, "velocity": 0.8 }, - { "note": "E5", "duration": 0.2, "velocity": 0.9 }, - { "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 } - ], - "params": { - "synth": "sine", - "bpm": 130, - "adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 }, - "effects": { "reverb": 0.3, "delay": 0, "filterFreq": 2000 } - }, - "aiAnalysis": "C Major 조성, 5도 상행으로 신뢰감과 성장 표현", - "createdAt": "2026-03-08T00:00:00.000Z" - } - ], - "createdAt": "2026-03-08T00:00:00.000Z" - } - ], - "sl_current": "sl_1709000000_abc" -} -``` - -### 8.2 DB 테이블 (Phase 3, API 프로젝트에서 마이그레이션) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | bigint | PK | -| `tenant_id` | bigint | FK → tenants | -| `user_id` | bigint | FK → users | -| `name` | varchar(200) | 사운드 이름 | -| `audio_path` | varchar(500) | WAV/MP3 파일 경로 | -| `options` | json | notes, params, aiAnalysis 등 | -| `created_at` | timestamp | | -| `updated_at` | timestamp | | - ---- - -## 9. API 인증 및 키 현황 - -> **별도 API 키 발급 불필요** — 기존 인증 정보로 모든 엔진 사용 가능 - -### 9.1 사용 가능한 인증 정보 - -| 엔진 | 인증 방식 | 설정 위치 | 상태 | -|------|----------|----------|------| -| Gemini 2.5 Flash (모드 B) | API 키 | `.env` `GEMINI_API_KEY` | ✅ 운영 중 | -| Lyria RealTime (모드 C) | 동일 API 키 | `.env` `GEMINI_API_KEY` | ✅ 사용 가능 (experimental) | -| Lyria 2 (모드 C 폴백) | 서비스 계정 | `GOOGLE_APPLICATION_CREDENTIALS` | ✅ 파일 존재 | -| Vertex AI | 프로젝트 ID | `.env` `VERTEX_AI_PROJECT_ID=codebridge-chatbot` | ✅ 설정됨 | - -### 9.2 현재 .env 설정 (관련 항목) - -```env -GEMINI_API_KEY=AIzaSy... # Gemini + Lyria RealTime 공용 -GEMINI_MODEL=gemini-2.5-flash # 텍스트 AI (모드 B) -GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta -VERTEX_AI_PROJECT_ID=codebridge-chatbot # Lyria 2 (폴백) -VERTEX_AI_LOCATION=us-central1 -GOOGLE_APPLICATION_CREDENTIALS=/var/www/sales/apikey/google_service_account.json -``` - -### 9.3 Lyria RealTime API 사양 - -| 항목 | 값 | -|------|------| -| 모델 | `lyria-realtime-exp` | -| API 버전 | `v1alpha` (experimental) | -| 프로토콜 | WebSocket (양방향 스트리밍) | -| 출력 포맷 | 48kHz 스테레오 PCM | -| 청크 크기 | 2초 단위 | -| 제어 파라미터 | BPM (60~200), Scale (Key + Mode) | -| 비용 | 무료 (experimental 기간) | -| 참고 | [공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) | - -### 9.4 Lyria 2 API 사양 (폴백) - -| 항목 | 값 | -|------|------| -| 모델 | `lyria` | -| API | Vertex AI REST (`/publishers/google/models/lyria:predict`) | -| 인증 | 서비스 계정 OAuth 토큰 | -| 출력 포맷 | WAV (base64) | -| 비용 | $0.06 / 30초 | -| 기존 코드 | `BgmService::generateWithLyria()` | - ---- - -## 10. 관련 문서 - -- [AI 관리 종합 가이드](../guides/ai-management.md) — Gemini API 설정, 호출 흐름 -- [R&D 메뉴 개요](../features/rd/README.md) — R&D 메뉴 구조 -- [디자인 인사이트](../features/rd/design-insight.md) — 유사 SPA 패턴 참고 -- [Lyria RealTime 공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) — Gemini API 음악 생성 -- [Lyria 2 Vertex AI 문서](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/lyria-music-generation) — REST API 레퍼런스 -- [Lyria RealTime 개발자 가이드](https://dev.to/googleai/lyria-realtime-the-developers-guide-to-infinite-music-streaming-4m1h) — 구현 튜토리얼 - ---- - -**최종 업데이트**: 2026-03-08 diff --git a/sam/docs/projects/e-sign/esign-storyboard.pptx b/sam/docs/projects/e-sign/esign-storyboard.pptx deleted file mode 100644 index 07c3b5d25d2d371ff044debec6f0e6f4e0ab569c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 529331 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
-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�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 JpML SAM 시스템 개발 프로젝트별 문서 모음 -> **최종 업데이트**: 2026-03-08 - ---- - -## 프로젝트 현황 요약 - -| 프로젝트 | 상태 | 설명 | -|---------|------|------| -| [mes](#mes---meserp-프로젝트) | 🟡 진행중 | 차세대 MES/ERP 기능 개발 | -| [quotation](#quotation---견적-기능) | 🟢 Phase 3 완료 | 5130 견적 → SAM 이관 | -| [api-integration](#api-integration---react--api-연동) | 🟡 진행중 | React ↔ API 연동 | -| [5130-migration](#5130-migration---품목-마이그레이션) | 🟡 Phase 1 진행중 | 5130 품목 데이터 마이그레이션 | -| [legacy-5130](#legacy-5130---레거시-분석) | 📚 참조용 | 5130 레거시 모듈 분석 | -| [mng-mobile-responsive](#mng-mobile-responsive---모바일-반응형) | 🟡 진행중 | mng 모바일 반응형 개선 | -| [auto-login](#auto-login---자동-로그인) | ⚪ 대기 | 자동 로그인 기능 | -| [migration-5130-mng](#migration-5130-mng---5130--mng-마이그레이션) | 🟡 진행중 | 5130 → mng 통합 마이그레이션 | -| [e-sign](#e-sign---전자계약-서명) | 🟢 v1.0 구현 완료 | 전자계약 서명 솔루션 (SAM E-Sign) | -| [org-chart](#org-chart---조직도-관리) | 🟢 v1.0 구현 완료 | 트리형 조직도 관리 (드래그앤드롭, 숨기기) | -| [planning-design](#planning-design---기획디자인-스토리보드-에디터) | 🟢 v1.2 운영 중 | 브라우저 블록 에디터 (Notion/Figma 스타일) | - ---- - -## 프로젝트 상세 - -### mes - MES/ERP 프로젝트 - -**경로**: `docs/projects/mes/` -**상태**: 🟡 Phase 0 (베이스라인 분석) 30% 완료 -**목표**: SAM 시스템의 차세대 MES/ERP 기능 개발 - -**핵심 문서**: -- [README.md](./mes/README.md) - 프로젝트 개요 및 문서 안내 -- [MES_PROGRESS_TRACKER.md](./mes/MES_PROGRESS_TRACKER.md) - 진행 상황 추적 -- [MES_PROJECT_ROADMAP.md](./mes/MES_PROJECT_ROADMAP.md) - 전체 로드맵 - -**분석 결과**: -- `00_baseline/` - Phase 0 분석 결과 - - [PHASE_0_FINAL_REPORT.md](./mes/00_baseline/PHASE_0_FINAL_REPORT.md) - - [BACKEND_DEVELOPMENT_ROADMAP_V2.md](./mes/00_baseline/BACKEND_DEVELOPMENT_ROADMAP_V2.md) - - `docs_breakdown/` - 문서 분석 (7개) - -**v2 분석**: -- `v2-analysis/` - MES v2 화면 분석 - - `quote-analysis/` - 견적 분석 - - `order-analysis/` - 주문 분석 - - `production-analysis/` - 생산 분석 - - `customer-analysis/` - 거래처 분석 - - `site-analysis/` - 현장 분석 - - `price-analysis/` - 단가 분석 - - `master-data-analysis/` - 기준정보 분석 - - `production-userflow/` - 생산 유저플로우 - ---- - -### quotation - 견적 기능 - -**경로**: `docs/projects/quotation/` -**상태**: 🟢 Phase 3 완료 (2025-12-19) -**목표**: 5130 레거시 견적 기능을 SAM 시스템으로 이관 - -**핵심 문서**: -- [MASTER_PLAN.md](./quotation/MASTER_PLAN.md) - 마스터 플랜 -- [PROGRESS.md](./quotation/PROGRESS.md) - 진행 현황 - -**Phase 문서**: -| Phase | 상태 | 경로 | -|-------|------|------| -| 1. 5130 분석 | ✅ 완료 | `phase-1-5130-analysis/` | -| 2. mng 분석 | ✅ 완료 | `phase-2-mng-analysis/` | -| 3. 구현 | ✅ 완료 | `phase-3-implementation/` | -| 4. API 개발 | ⚪ 대기 | `phase-4-api/` | - -**참조 자료**: -- `screenshots/` - MES 프로토타입 화면 캡쳐 (7개) - ---- - -### api-integration - React ↔ API 연동 - -**경로**: `docs/projects/api-integration/` -**상태**: 🟡 Phase 4 진행중 -**목표**: React(dev.sam.kr)와 API(api.sam.kr) 완벽 연동 - -**핵심 문서**: -- [MASTER_PLAN.md](./api-integration/MASTER_PLAN.md) - 마스터 플랜 -- [PROGRESS.md](./api-integration/PROGRESS.md) - 진행 현황 -- [WORKFLOW.md](./api-integration/WORKFLOW.md) - 작업 프로세스 - -**Phase 문서**: -| Phase | 상태 | 경로 | -|-------|------|------| -| 1. 테이블 통합 | 🟢 완료(스킵) | `phase-1-table-migration/` | -| 2. 메뉴 추출 | 🟡 진행중 | `phase-2-menu-extraction/` | -| 3. API 매핑 | 🟡 진행중 | `phase-3-api-mapping/` | -| 4. 연동+검증 | 🟡 진행중 | `phase-4-integration/` | - -**TC 파일**: `phase-4-integration/tc/` - 기능별 테스트 케이스 JSON (17개) - ---- - -### 5130-migration - 품목 마이그레이션 - -**경로**: `docs/projects/5130-migration/` -**상태**: 🟡 Phase 1 진행중 -**목표**: 5130 품목(부품, 자재, BOM) 데이터를 SAM DB로 이전 - -**핵심 문서**: -- [MASTER_PLAN.md](./5130-migration/MASTER_PLAN.md) - 마스터 플랜 -- [PROGRESS.md](./5130-migration/PROGRESS.md) - 진행 현황 - -**Phase 문서**: -| Phase | 상태 | 경로 | -|-------|------|------| -| 1. 소스 분석 | 🟡 진행중 | `phase-1-source-analysis/` | -| 2. 타겟 분석 | ⚪ 대기 | `phase-2-target-analysis/` | -| 3. 매핑 설계 | ⚪ 대기 | `phase-3-mapping/` | - ---- - -### legacy-5130 - 레거시 분석 - -**경로**: `docs/projects/legacy-5130/` -**상태**: 📚 참조용 문서 -**용도**: 5130 레거시 시스템 모듈별 분석 - -**모듈별 분석 문서**: -| 문서 | 내용 | -|------|------| -| [00_OVERVIEW.md](./legacy-5130/00_OVERVIEW.md) | 시스템 개요 | -| [01_MATERIAL.md](./legacy-5130/01_MATERIAL.md) | 자재 관리 | -| [02_PRODUCT.md](./legacy-5130/02_PRODUCT.md) | 제품 관리 | -| [03_ESTIMATE.md](./legacy-5130/03_ESTIMATE.md) | 견적 관리 | -| [04_PRODUCTION.md](./legacy-5130/04_PRODUCTION.md) | 생산 관리 | -| [05_SHIPPING.md](./legacy-5130/05_SHIPPING.md) | 출하 관리 | -| [06_QUALITY.md](./legacy-5130/06_QUALITY.md) | 품질 관리 | -| [07_ACCOUNTING.md](./legacy-5130/07_ACCOUNTING.md) | 회계 관리 | -| [08_SAM_COMPARISON.md](./legacy-5130/08_SAM_COMPARISON.md) | SAM 비교 분석 | -| [draw-module.md](./legacy-5130/draw-module.md) | 도면 모듈 | - ---- - -### mng-mobile-responsive - 모바일 반응형 - -**경로**: `docs/projects/mng-mobile-responsive/` -**상태**: 🟡 진행중 -**목표**: mng 관리자 패널 모바일 반응형 개선 - -**문서**: -- [01-analysis.md](./mng-mobile-responsive/01-analysis.md) - 분석 -- [02-implementation-plan.md](./mng-mobile-responsive/02-implementation-plan.md) - 구현 계획 -- [06-excluded-menus.md](./mng-mobile-responsive/06-excluded-menus.md) - 제외 메뉴 -- [PROGRESS.md](./mng-mobile-responsive/PROGRESS.md) - 진행 현황 - ---- - -### auto-login - 자동 로그인 - -**경로**: `docs/projects/auto-login/` -**상태**: ⚪ 대기 -**목표**: 자동 로그인 기능 구현 - -**문서**: -- [PROGRESS.md](./auto-login/PROGRESS.md) - 진행 현황 - ---- - -### migration-5130-mng - 5130 → mng 마이그레이션 - -**경로**: `docs/projects/migration-5130-mng/` -**상태**: 🟡 진행중 -**목표**: 5130 기능을 mng로 통합 마이그레이션 - -**문서**: -- [MIGRATION_TRACKER.md](./migration-5130-mng/MIGRATION_TRACKER.md) - 마이그레이션 추적 - ---- - -### e-sign - 전자계약 서명 - -**경로**: `docs/projects/e-sign/` -**상태**: 🟢 v1.0 구현 완료 (2026-02-12) -**목표**: 모두싸인과 유사한 간편 전자계약 서명 솔루션 자체 구축 - -**핵심 문서**: -- [technical-design.md](./e-sign/technical-design.md) - 기술 설계 문서 -- [implementation-guide.md](./e-sign/implementation-guide.md) - 구현 가이드 - -**구현 범위**: -| 영역 | 수량 | -|------|------| -| DB 마이그레이션 | 4개 (esign_contracts, esign_signers, esign_sign_fields, esign_audit_logs) | -| API 모델 | 4개 | -| API 서비스 | 4개 | -| API 컨트롤러 | 2개 (16 엔드포인트) | -| MNG 컨트롤러 | 2개 (8 화면) | -| MNG 뷰 | 8개 (React 하이브리드) | - -**기술 스택**: Laravel 11 + React 18 + SignaturePad + PDF.js - -**참고 자료**: -- `esign-storyboard.pptx` - 화면 스토리보드 -- `storyboard-config.json` - 스토리보드 설정 - ---- - -### org-chart - 조직도 관리 - -**경로**: `docs/projects/org-chart/` -**상태**: 🟢 v1.0 구현 완료 (2026-03-06) -**목표**: 테넌트별 조직 구조를 시각적으로 관리하는 트리형 조직도 - -**핵심 문서**: -- [README.md](./org-chart/README.md) - 기술 문서 (아키텍처, API, DB, 프론트엔드 상세) - -**구현 범위**: - -| 영역 | 수량 | -|------|------| -| MNG 컨트롤러 메서드 | 7개 (RdController) | -| API 엔드포인트 | 6개 (조회, 배치, 해제, 순서변경, 숨기기) | -| DB 마이그레이션 | 1개 (departments.options JSON 추가) | -| 뷰 | 1개 (Alpine.js + SortableJS) | - -**기술 스택**: Alpine.js + SortableJS + 수동 DOM 렌더링 (x-for 미사용) - ---- - -### planning-design - 기획디자인 스토리보드 에디터 - -**경로**: `docs/projects/planning-design/` -**상태**: 🟢 v1.2 운영 중 (고도화 진행중) -**목표**: 브라우저 내 Notion/Figma 스타일 블록 에디터로 ERP 화면 기획서 작성 - -**핵심 문서**: -- [README.md](./planning-design/README.md) - 프로젝트 개요 및 구현 이력 - -**구현 범위**: - -| 영역 | 수량 | -|------|------| -| 블록 유형 | 15종 (텍스트/레이아웃/UI모형/미디어/체크리스트) | -| 편집 기능 | 올가미 선택, Undo/Redo, 복사/잘라내기, 서식 | -| 서식 시스템 | 글자색/배경색/크기/굵기/기울임/정렬/z-index | -| 작업 영역 | 사이드바/Description 접기/펼치기, 캔버스 폭 자동 확장 | -| 출력 | HTML 내보내기 + A4 인쇄 (좌표 기반 WYSIWYG) | - -**기술 스택**: Alpine.js + localStorage (서버 API 없음) - -**기술 스펙**: [features/rd/planning-design.md](../features/rd/planning-design.md) - ---- - -## 디렉토리 구조 - -``` -docs/projects/ -├── INDEX.md # 이 파일 -├── mes/ # MES/ERP 프로젝트 (핵심) -│ ├── 00_baseline/ # Phase 0 분석 -│ ├── v1-analysis/ # v1 분석 -│ ├── v2-analysis/ # v2 화면 분석 -│ └── phases/ # Phase별 진행 -├── quotation/ # 견적 기능 -│ ├── phase-1-5130-analysis/ -│ ├── phase-2-mng-analysis/ -│ ├── phase-3-implementation/ -│ └── screenshots/ -├── api-integration/ # React ↔ API 연동 -│ ├── phase-1-table-migration/ -│ ├── phase-2-menu-extraction/ -│ ├── phase-3-api-mapping/ -│ └── phase-4-integration/tc/ -├── 5130-migration/ # 품목 마이그레이션 -│ ├── phase-1-source-analysis/ -│ ├── phase-2-target-analysis/ -│ └── phase-3-mapping/ -├── legacy-5130/ # 레거시 분석 (참조용) -├── mng-mobile-responsive/ # 모바일 반응형 -├── auto-login/ # 자동 로그인 -├── migration-5130-mng/ # 5130→mng 마이그레이션 -├── e-sign/ # 전자계약 서명 (SAM E-Sign) -├── org-chart/ # 조직도 관리 -└── planning-design/ # 기획디자인 스토리보드 에디터 -``` - ---- - -## 관련 문서 - -- [docs/INDEX.md](../INDEX.md) - 전체 문서 인덱스 -- [docs/plans/index_plans.md](../plans/index_plans.md) - 기획 문서 인덱스 -- [docs/guides/PROJECT_DEVELOPMENT_POLICY.md](../guides/PROJECT_DEVELOPMENT_POLICY.md) - 공통 개발 정책 -- [CURRENT_WORKS.md](../../CURRENT_WORKS.md) - 현재 작업 - ---- - -**범례**: -- 🟢 완료 -- 🟡 진행중 -- ⚪ 대기 -- 📚 참조용 \ No newline at end of file diff --git a/sam/docs/projects/org-chart/README.md b/sam/docs/projects/org-chart/README.md deleted file mode 100644 index 238ce52..0000000 --- a/sam/docs/projects/org-chart/README.md +++ /dev/null @@ -1,317 +0,0 @@ -# 조직도 관리 시스템 - -> **작성일**: 2026-03-06 -> **상태**: 🟢 v1.0 구현 완료 -> **프로젝트**: MNG 전용 (Blade + Alpine.js + SortableJS) - ---- - -## 1. 개요 - -### 1.1 목적 - -테넌트별 조직 구조를 시각적으로 관리하는 트리형 조직도 시스템. -부서 계층 구조와 직원 배치를 드래그 앤 드롭으로 관리한다. - -### 1.2 주요 기능 - -| 기능 | 설명 | -|------|------| -| 트리형 조직도 | 회사 → 부서 → 하위부서 (무한 depth) 계층 표시 | -| 직원 배치 | 드래그 앤 드롭으로 직원을 부서에 배치/해제 | -| 부서 순서 변경 | 같은 레벨 내 부서 순서 드래그로 변경 | -| 부서 계층 이동 | 부서를 다른 부서 아래로 드래그하여 parent 변경 | -| 부서 숨기기 | 더블클릭 → 숨기기 버튼 → DB 저장 (영구) | -| 임원 필터링 | 대표이사/사장 등은 미배치 목록에서 제외 | - ---- - -## 2. 기술 스택 - -| 구분 | 기술 | -|------|------| -| 백엔드 | Laravel (MNG 프로젝트) | -| 프론트엔드 | Alpine.js + 수동 DOM 렌더링 | -| 드래그 앤 드롭 | SortableJS | -| 스타일 | Tailwind CSS + inline style | -| 데이터 저장 | MySQL `departments`, `employees` 테이블 | - ---- - -## 3. 아키텍처 - -### 3.1 렌더링 방식 - -> **핵심**: Alpine.js `x-for` 대신 수동 `innerHTML` 렌더링을 사용한다. - -SortableJS와 Alpine.js `x-for` 템플릿이 동시에 DOM을 조작하면 **이중 업데이트 버그**가 발생한다. -이를 해결하기 위해 부서 트리는 JavaScript로 HTML 문자열을 생성하고 `innerHTML`로 삽입한다. - -``` -Alpine.js 데이터 변경 - ↓ -renderTree() 호출 - ↓ -기존 SortableJS 인스턴스 destroy - ↓ -buildChildrenHtml(null, 0) → 재귀적 HTML 생성 - ↓ -$refs.deptTree.innerHTML = html - ↓ -$nextTick → initDeptSortables() + initEmpSortables() -``` - -### 3.2 이벤트 처리 - -수동 렌더링된 HTML에는 Alpine 디렉티브가 없으므로 **이벤트 위임(Event Delegation)** 패턴을 사용한다. - -``` -루트 div @click="handleClick($event)" - @dblclick="handleDblClick($event)" - ↓ -e.target.closest('[data-action]') 으로 액션 식별 - ↓ -data-action 값에 따라 분기: - - "unassign" → 직원 미배치 - - "hide-dept" → 부서 숨기기 - - "restore-dept" → 부서 복원 - - "dept-dblclick" → 더블클릭 시 숨기기 버튼 토글 -``` - -### 3.3 순환 참조 방지 - -부서를 자신의 하위로 드래그하면 무한 루프가 발생한다. -`isDescendant(ancestorId, targetId)` 재귀 함수로 이를 차단한다. - -```javascript -// 드래그 대상(dragId)의 자손인 곳으로는 이동 불가 -onMove: (evt) => { - const dragId = parseInt(evt.dragged.dataset.deptId); - const toPid = evt.to.dataset.parentId ? parseInt(evt.to.dataset.parentId) : null; - if (toPid === dragId || this.isDescendant(dragId, toPid)) return false; -} -``` - ---- - -## 4. 파일 구조 - -### 4.1 MNG 프로젝트 - -| 파일 | 역할 | -|------|------| -| `app/Http/Controllers/RdController.php` | 컨트롤러 (7개 메서드) | -| `app/Models/Tenants/Department.php` | 부서 모델 (`options` JSON cast) | -| `resources/views/rd/org-chart.blade.php` | 뷰 (Alpine.js + SortableJS) | -| `routes/web.php` | 라우트 (6개 엔드포인트) | - -### 4.2 API 프로젝트 - -| 파일 | 역할 | -|------|------| -| `database/migrations/2026_03_06_201500_add_options_to_departments_table.php` | `options` JSON 컬럼 추가 | - ---- - -## 5. API 엔드포인트 - -> 모든 엔드포인트는 `rd.` 네임 프리픽스 하위에 위치한다. - -| Method | Route | 컨트롤러 메서드 | 설명 | -|--------|-------|---------------|------| -| GET | `/rd/org-chart` | `orgChart` | 조직도 페이지 | -| POST | `/rd/org-chart/assign` | `orgChartAssign` | 직원 부서 배치 | -| POST | `/rd/org-chart/unassign` | `orgChartUnassign` | 직원 부서 해제 | -| POST | `/rd/org-chart/reorder` | `orgChartReorder` | 직원 일괄 이동 | -| POST | `/rd/org-chart/reorder-depts` | `orgChartReorderDepts` | 부서 순서/계층 변경 | -| POST | `/rd/org-chart/toggle-hide` | `orgChartToggleHide` | 부서 숨기기/표시 토글 | - -### 5.1 요청/응답 형식 - -**부서 배치** (`POST /rd/org-chart/assign`): -```json -{ "employee_id": 1, "department_id": 5 } -→ { "success": true } -``` - -**부서 순서 변경** (`POST /rd/org-chart/reorder-depts`): -```json -{ - "orders": [ - { "id": 1, "parent_id": null, "sort_order": 1 }, - { "id": 2, "parent_id": null, "sort_order": 2 }, - { "id": 3, "parent_id": 1, "sort_order": 1 } - ] -} -→ { "success": true } -``` - -**부서 숨기기** (`POST /rd/org-chart/toggle-hide`): -```json -{ "department_id": 5, "hidden": true } -→ { "success": true } -``` - ---- - -## 6. DB 구조 - -### 6.1 departments 테이블 (관련 컬럼) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | int | PK | -| `tenant_id` | int | 테넌트 FK | -| `parent_id` | int (nullable) | 상위 부서 (null = 최상위) | -| `name` | varchar | 부서명 | -| `code` | varchar | 부서 코드 | -| `is_active` | bool | 활성 여부 | -| `sort_order` | int | 정렬 순서 | -| `options` | json (nullable) | 확장 속성 | - -**`options` 키**: - -| 키 | 타입 | 설명 | -|----|------|------| -| `orgchart_hidden` | boolean | 조직도에서 숨김 여부 | - -### 6.2 employees 테이블 (관련 컬럼) - -| 컬럼 | 타입 | 설명 | -|------|------|------| -| `id` | int | PK | -| `tenant_id` | int | 테넌트 FK | -| `department_id` | int (nullable) | 소속 부서 (null = 미배치) | -| `display_name` | varchar | 표시 이름 | -| `position_label` | varchar | 직책/직급 | -| `employee_status` | enum | `active`, `leave`, `resigned` | - ---- - -## 7. 프론트엔드 구현 상세 - -### 7.1 Alpine.js 컴포넌트 (`orgChart()`) - -**데이터**: - -| 속성 | 타입 | 설명 | -|------|------|------| -| `departments` | Array | 전체 부서 목록 (서버에서 전달) | -| `employees` | Array | 전체 직원 목록 (서버에서 전달) | -| `hiddenDepts` | Set | 숨긴 부서 ID (DB에서 초기화) | -| `dblClickDept` | int/null | 더블클릭된 부서 ID (숨기기 버튼 표시용) | -| `execTitles` | Array | 임원 직책 목록 (`['대표이사', '사장', '부사장', '회장', '부회장']`) | - -**핵심 메서드**: - -| 메서드 | 설명 | -|--------|------| -| `renderTree()` | SortableJS 파괴 → HTML 재생성 → SortableJS 재초기화 | -| `buildChildrenHtml(parentId, level)` | 재귀적 자식 부서 HTML 생성 | -| `buildNodeHtml(dept, level)` | 단일 부서 카드 HTML (level별 스타일 차등) | -| `buildEmpHtml(emp, isLarge)` | 직원 카드 HTML | -| `isDeptHidden(deptId)` | 부서 또는 상위 부서가 숨김인지 재귀 체크 | -| `isDescendant(ancestorId, targetId)` | 순환 참조 방지 | -| `isExecutive(emp)` | 임원 여부 판별 | - -### 7.2 SortableJS 그룹 - -| 그룹 | 대상 | 핸들 | 기능 | -|------|------|------|------| -| `departments` | `.org-children`, `.org-drop-target` | `.dept-drag-handle` | 부서 순서/계층 변경 | -| `employees` | `.emp-zone`, `#unassigned-zone` | (전체) | 직원 배치/해제 | - -### 7.3 CSS 연결선 - -부서 간 연결선은 CSS `::before`/`::after` 의사 요소로 구현한다. - -``` - 부모 노드 - │ (vertical: div 1px × 24px) - ┌───────┼───────┐ (horizontal: ::before + ::after) - │ │ │ (vertical: div 1px × 24px) - 자식1 자식2 자식3 -``` - -| 선택자 | 역할 | -|--------|------| -| `.org-node-wrap` 내부 div (1px × 24px) | 세로 연결선 | -| `.org-node-wrap:not(:first-child)::before` | 왼쪽 가로선 (left:0 ~ right:50%) | -| `.org-node-wrap:not(:last-child)::after` | 오른쪽 가로선 (left:50% ~ right:0) | -| `:only-child` | 단일 자식이면 가로선 숨김 | - -### 7.4 부서 숨기기 UX 흐름 - -``` -① 부서 헤더 더블클릭 - ↓ -② dblClickDept = dept.id → renderTree() - ↓ -③ 헤더에 빨간 "숨기기" 버튼 표시 - ↓ -④ "숨기기" 클릭 - ↓ -⑤ hiddenDepts.add(id) → renderTree() → POST /toggle-hide (DB 저장) - ↓ -⑥ 해당 부서 + 하위 부서가 트리에서 제거 -⑦ "숨겨진 부서" 패널에 표시 - ↓ -⑧ 패널에서 👁 아이콘 클릭 → hiddenDepts.delete(id) → POST /toggle-hide -``` - -### 7.5 부서 레벨별 스타일 - -| Level | 색상 테마 | 너비 | 아이콘 | -|-------|---------|------|--------| -| 0 (최상위) | 보라 (`#7C3AED`) | 200px | `ri-building-2-line` | -| 1 (중간) | 인디고 (`#6366F1`) | 180px | `ri-git-branch-line` | -| 2+ (하위) | 회색 (`#6B7280`) | 160px | `ri-subtract-line` | - ---- - -## 8. 비즈니스 규칙 - -### 8.1 임원 필터링 - -미배치 직원 목록에서 다음 조건에 해당하면 제외: - -- `position_label`이 `['대표이사', '사장', '부사장', '회장', '부회장']` 중 하나 -- `display_name`이 테넌트의 `ceo_name`과 일치 - -> 이유: 조직도 최상단에 "대표이사 OOO"이 이미 표시되므로 중복 방지 - -### 8.2 부서 숨기기 - -- `departments.options` JSON의 `orgchart_hidden` 키로 저장 -- 숨긴 부서의 **하위 부서도 자동으로 숨겨짐** (`isDeptHidden` 재귀 체크) -- 숨겨진 부서 패널에는 **직접 숨긴 부서만** 표시 (자식은 부모 복원 시 같이 복원) -- 숨기기는 **조직도 표시 전용** — `is_active`와 무관하며, 부서 데이터에 영향 없음 - -### 8.3 직원 표시 형식 - -- 직책이 있으면: `{직책} {이름}` (예: "과장 전진선") -- 직책이 없으면: `{이름}` (예: "김보곤") - ---- - -## 9. 개발 이력 - -| 날짜 | 커밋 | 내용 | -|------|------|------| -| 2026-03-06 | `a12ee886` | CSS 연결선 수정 + 빈 드롭 타겟 숨김 | -| 2026-03-06 | `9fd72e49` | 부서 숨기기 기능 추가 (프론트 전용) | -| 2026-03-06 | `8c8fd5f6` | 대표이사 미배치 제외 + 숨긴 부서 연결선 제거 | -| 2026-03-06 | `81157a15` | 부서 숨기기 상태 DB 저장 (`options.orgchart_hidden`) | - ---- - -## 관련 문서 - -- [rules/department-tree-api.md](../../rules/department-tree-api.md) — 부서 트리 API 규칙 -- [rules/employee-api.md](../../rules/employee-api.md) — 직원 API 규칙 -- [system/database/hr.md](../../system/database/hr.md) — HR 테이블 스키마 -- [standards/options-column-policy.md](../../standards/options-column-policy.md) — options JSON 컬럼 정책 - ---- - -**최종 업데이트**: 2026-03-06 diff --git a/sam/docs/projects/planning-design/README.md b/sam/docs/projects/planning-design/README.md deleted file mode 100644 index 12dba75..0000000 --- a/sam/docs/projects/planning-design/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# 기획디자인 스토리보드 에디터 - -> **시작일**: 2026-03-07 -> **상태**: 🟢 v1.2 운영 중 (고도화 진행중) -> **경로**: MNG `/rd/planning-design` -> **담당**: Claude Code + 개발팀 - ---- - -## 1. 프로젝트 개요 - -### 1.1 배경 - -ERP 화면 기획서(스토리보드)를 PowerPoint나 Figma 없이 **SAM 관리자 웹 내에서 직접 설계**할 수 있는 도구가 필요했다. 기획자와 개발자가 같은 플랫폼에서 화면을 설계하고, 즉시 HTML/인쇄 출력까지 가능한 올인원 솔루션을 목표로 했다. - -### 1.2 목표 - -- 브라우저 내 Notion/Figma 스타일 블록 에디터 구현 -- ERP 스토리보드 표준 양식 (메뉴트리 + 와이어프레임 + Description) 지원 -- 서버 API 없이 localStorage 기반 즉시 사용 가능 -- HTML 내보내기 및 좌표 기반 WYSIWYG 인쇄 - -### 1.3 기술 스택 - -| 항목 | 선택 | 이유 | -|------|------|------| -| 프레임워크 | Alpine.js | 서버 없이 반응형 SPA, 기존 MNG 스택과 일치 | -| 캔버스 | DOM absolute positioning | Canvas API보다 접근성 좋고 텍스트 편집 용이 | -| 저장 | localStorage | 서버 API 불필요, 즉시 사용 가능 | -| 내보내기 | HTML 생성 + window.print() | 별도 라이브러리 없이 브라우저 내장 기능 활용 | - ---- - -## 2. 구현 이력 - -### v1.0 — 기본 블록 에디터 (2026-03-07) - -| 커밋 | 내용 | -|------|------| -| `063d8c61` | 스토리보드 블록 Undo/Redo 기능 (Ctrl+Z/Y, 50단계 히스토리) | -| `78c8f3f8` | 페이지 복사 기능 (블록 ID 재생성) | -| `a27d9921` | placeholder 색상 옅게 + italic 스타일 | -| `08cc866a` | 블록 툴바를 단위업무 상단으로 이동 (기획서 보기 방해 제거) | -| `20e5ab78` | 메뉴/캔버스 경계 드래그 리사이즈 (80~400px) | -| `7785dfed` | 올가미(마퀴) 다중 선택 + 그룹 이동/복사/삭제 | -| `ff373c71` | 올가미 선택 동작 수정 (캔버스 빈 영역 판별 개선) | -| `95cd217c` | Ctrl+X 잘라내기 기능 (단일/다중) | -| `f4131df0` | Ctrl+X 후 Ctrl+Z 복구 수정 (히스토리 인덱스 보정) | -| `8ff84e7f` | Description 패널 리사이즈 + 번호 마커 블록 (D&D/툴바) | -| `ac5ae6eb` | 좌표 기반 인쇄 + HTML 내보내기 블록 좌표 배치 | - -### v1.1 — 서식 시스템 (2026-03-08) - -| 커밋 | 내용 | -|------|------| -| `dfbbd3a1` | 플로팅 서식 툴바 + 우클릭 컨텍스트 메뉴 추가 | -| `280bfddb` | 블록 서식 CSS 상속 수정 (자식 요소 color inherit) | - -### v1.2 — 작업 영역 극대화 (2026-03-08) - -| 커밋 | 내용 | -|------|------| -| `5e0f1a63` | 좌측 사이드바 접기/펼치기 버튼 추가 | -| `f1202731` | 메뉴트리/Description 패널 접기/펼치기 + 캔버스 폭 자동 확장 (1100→1400px) | -| `a38c017c` | 이미지 블록 업로드를 더블클릭으로 변경 (드래그 중 파일 창 오픈 방지) | - ---- - -## 3. 현재 기능 목록 - -### 3.1 블록 유형 (15종) - -| 분류 | 유형 | -|------|------| -| 텍스트 | Heading (H1), Heading2 (H2), Text, Code | -| 레이아웃 | Divider, Callout | -| 데이터 | Table | -| UI 모형 | Button, Input, Select, Card, Badges | -| 미디어 | Image, Marker (번호 뱃지) | -| 체크 | Todo (체크리스트) | - -### 3.2 편집 기능 - -- 자유 배치 캔버스 (드래그 이동, 핸들 리사이즈) -- 올가미 다중 선택 + 그룹 이동/복사/삭제 -- Undo/Redo (50단계) -- 복사/붙여넣기/잘라내기 (Ctrl+C/V/X) -- 전체 선택 (Ctrl+A) -- 더블클릭 인라인 편집 (contenteditable) -- 이미지 블록 더블클릭 업로드 (드래그 충돌 방지) - -### 3.3 서식 시스템 - -- 플로팅 서식 툴바: 글자색, 배경색, 크기, 굵기, 기울임, 정렬, z-index -- 우클릭 컨텍스트 메뉴: 복제/잘라내기/삭제/색상/정렬/레이어/서식 초기화 - -### 3.4 문서 관리 - -- 멀티 페이지 (추가/복사/삭제/이동) -- ERP 메뉴 트리 편집 (드래그 순서 변경) -- Description 패널 (기능 설명 + 번호 마커 D&D) -- 프리셋/커스텀 템플릿 - -### 3.6 작업 영역 극대화 - -- 좌측 사이드바(메뉴트리) 접기/펼치기 토글 -- Description 패널 접기/펼치기 토글 바 -- 패널 접힘 시 캔버스 폭 자동 확장 (1100px → 1400px) -- sb-editor 패딩 축소 (24px → 12px) - -### 3.5 출력 - -- HTML 파일 내보내기 (좌표 기반 WYSIWYG) -- 인쇄 미리보기 (A4 Landscape, 페이지 분할) - ---- - -## 4. 향후 로드맵 - -| 우선순위 | 기능 | 설명 | 상태 | -|---------|------|------|------| -| 🔴 필수 | DB 저장 | localStorage → DB 전환 (협업, 용량 해결) | ⚪ 대기 | -| 🔴 필수 | 스냅/그리드 정렬 | 블록 간 자석 가이드라인 | ⚪ 대기 | -| 🟡 중요 | 그룹핑 | 여러 블록을 하나의 그룹으로 묶기/풀기 | ⚪ 대기 | -| 🟡 중요 | 레이어 패널 | z-index 순서를 시각적으로 관리 | ⚪ 대기 | -| 🟡 중요 | 리치 텍스트 | 블록 내 부분 텍스트 서식 (인라인 B/I/색상) | ⚪ 대기 | -| 🟢 권장 | PDF 내보내기 | 서버사이드 PDF 생성 | ⚪ 대기 | -| 🟢 권장 | 버전 관리 | 명시적 스냅샷 저장 및 비교 | ⚪ 대기 | -| 🟢 권장 | 공유 링크 | 읽기 전용 공유 URL 생성 | ⚪ 대기 | - ---- - -## 5. 기술적 특이사항 - -### 5.1 단일 파일 아키텍처 - -모든 CSS + HTML + JavaScript가 `index.blade.php` 하나에 포함 (~4,430줄). 서버 API가 없고 localStorage만 사용하므로, 컨트롤러는 뷰만 반환한다. - -### 5.2 CSS 스타일 상속 문제 - -블록 자식 요소에 하드코딩된 `color`가 있어 부모의 인라인 스타일이 무시되는 문제를 CSS attribute selector(`[style*="color"]`)로 해결했다. 향후 블록 유형 추가 시 inherit 규칙도 함께 추가해야 한다. - -### 5.3 localStorage 용량 한계 - -이미지를 base64 Data URL로 저장하므로 대량 사용 시 5~10MB 한계에 도달할 수 있다. DB 저장 전환이 중장기 과제. - ---- - -## 6. 관련 문서 - -- [기술 스펙](../../features/rd/planning-design.md) — 데이터 구조, 블록 유형, CSS 상속 상세 -- [R&D 메뉴 개요](../../features/rd/README.md) — R&D 전체 메뉴 구조 -- [프로젝트 인덱스](../index_projects.md) — 전체 프로젝트 목록 - ---- - -**최종 업데이트**: 2026-03-08 diff --git a/sam/docs/rules/billing-policy.md b/sam/docs/rules/billing-policy.md deleted file mode 100644 index 56a5966..0000000 --- a/sam/docs/rules/billing-policy.md +++ /dev/null @@ -1,189 +0,0 @@ -# SAM 과금정책 — 내부용 (CONFIDENTIAL) - -> **작성일**: 2026-02-21 -> **상태**: 설계 확정 -> **열람 범위**: 내부 개발팀/관리층 전용 — 대외 공유 금지 - ---- - -## 문서 분리 안내 - -과금정책이 대상 독자별 3개 문서로 분리되었다. - -| 문서 | 대상 | 내용 | -|------|------|------| -| [고객 요금 안내](customer-pricing.md) | 고객 | 서비스 가격표, 과금 기준 | -| [영업파트너 수당 체계](partner-commission.md) | 영업파트너 | 수당률, 정산 프로세스 | -| **본 문서** (billing-policy.md) | 내부 개발팀/관리층 | 본사 원가, 마진율, 코드 참조 | - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM 프로젝트의 과금정책 중 **본사 지출 원가**, **마진 구조**, **코드 참조**를 정리한다. -고객 공개용 요금과 영업파트너 수당 체계는 별도 문서로 분리되었다. - -### 1.2 적용범위 - -- 외부 서비스 이용 비용 (바로빌, Google, Anthropic) -- 마진 구조 및 가격 책정 배경 -- 코드 참조 (모델, 서비스, DB 테이블) - -### 1.3 용어 정의 - -| 용어 | 설명 | -|------|------| -| **개발비** | 서비스 도입 시 1회 납부하는 초기 비용 | -| **구독료** | 서비스 유지를 위한 월 정기 비용 | -| **토큰** | AI가 언어를 처리하는 최소 단위 (한글 ~1.5자 = 1토큰) | -| **MRR** | Monthly Recurring Revenue (월간 반복 매출) | - ---- - -## 2. 본사 지출 과금정책 (Company → Service Provider) - -본사가 외부 서비스 제공자에게 납입하는 비용이다. - -### 2.1 바로빌 API 비용 - -#### 월정액 서비스 - -| 서비스 | 월정액 | 비고 | -|--------|--------|------| -| 계좌조회 | 10,000원 | 고객 부담 | -| 카드내역 | 10,000원 | 고객 부담 | -| 홈택스 매입/매출 | 33,000원 (VAT 포함) | 코드브릿지엑스 지원 → 본사 부담 (무료) | - -> **참고**: 계좌조회/카드내역 월정액은 고객에게 전가한다. 홈택스는 본사가 흡수한다. - -#### 건별 과금 - -| 서비스 | 단가 | 비고 | -|--------|------|------| -| 전자세금계산서 발행 | 100원/건 | 원가 기준 | - -### 2.2 AI/클라우드 서비스 비용 - -#### 토큰 기반 과금 (LLM) - -| 제공자 | 모델 | 입력 단가 (USD/1M) | 출력 단가 (USD/1M) | 입력 (KRW/1M) | 출력 (KRW/1M) | -|--------|------|------------------:|------------------:|--------------:|--------------:| -| Google | Gemini 2.0 Flash | $0.10 | $0.40 | 140원 | 560원 | -| Anthropic | Claude 3 Haiku | $0.25 | $1.25 | 350원 | 1,750원 | - -#### 시간/작업 기반 과금 - -| 서비스 | 단가 (USD) | 단위 | KRW 환산 | -|--------|----------:|------|----------:| -| Google STT | $0.009 | 15초당 | 12.6원/15초 (약 50원/분) | -| Google GCS | $0.005 | 1,000건당 | 7원/1,000건 | -| Google FCM | 무료 | - | - | - -#### 환율 기준 - -- 기본 환율: **1,400원/USD** -- `ai_pricing_configs` 테이블에서 동적 관리 -- 캐시 TTL: 1시간 - -### 2.3 본사 지출 월간 예상 (테넌트 1개 기준) - -| 항목 | 최소 | 최대 | 비고 | -|------|-----:|-----:|------| -| 바로빌 홈택스 | 0원 | 0원 | 코드브릿지엑스 지원 | -| 세금계산서 원가 | ~5,000원 | ~10,000원 | 50~100건 기준 | -| AI 토큰 (Gemini) | ~100원 | ~5,000원 | 사용량 비례 | -| GCS/STT | ~50원 | ~2,000원 | 사용량 비례 | - ---- - -## 3. 고객 안내용 과금정책 - -> 고객 요금 상세는 **[고객 요금 안내](customer-pricing.md)** 문서를 참조한다. - ---- - -## 4. 내부 정산 정책 - -### 4.1 영업 수당 체계 - -> 수당률 및 정산 프로세스는 **[영업파트너 수당 체계](partner-commission.md)** 문서를 참조한다. - -### 4.2 마진 구조 - -- **회사 마진**: 개발비의 약 **67~75%** (가입 유형에 따라 변동) - - 개인 가입: 개발비의 약 72~75% (수당 25~28% 차감 후) - - 단체 가입: 개발비의 약 67% (수당 33% 차감 후) - -### 4.3 가격 책정 배경 - -- 내부 총 개발비 산정: **약 7,600만원** -- 구성: 개발비 2,000만원 + 장기 구독(약 7년) 및 금융 비용(4.6%) -- 중소기업 초기 투자 부담 분산 구조 - ---- - -## 5. 코드 참조 (개발자용) - -### 5.1 바로빌 과금 - -| 파일 | 내용 | -|------|------| -| `mng/app/Models/Barobill/BarobillSubscription.php` | `DEFAULT_MONTHLY_FEES` 월정액 상수 | -| `mng/app/Models/Barobill/BarobillBillingRecord.php` | `USAGE_UNIT_PRICES` 건별 단가 상수 | -| `mng/app/Models/Barobill/BarobillPricingPolicy.php` | 과금 정책 모델 + `calculateBilling()` | -| `mng/app/Services/Barobill/BarobillBillingService.php` | 월정액/건별 과금 처리 서비스 | -| `mng/app/Services/Barobill/BarobillUsageService.php` | 사용량 집계 및 과금 계산 | -| `mng/database/seeders/BarobillPricingPolicySeeder.php` | 과금 정책 시더 (5개 정책) | - -### 5.2 AI 가격/토큰 - -| 파일 | 내용 | -|------|------| -| `api/app/Models/Tenants/AiPricingConfig.php` | AI 단가 모델 + `getActivePricing()` | -| `api/app/Models/Tenants/AiTokenUsage.php` | 토큰 사용량 기록 모델 | -| `api/app/Services/AiReportService.php` | 토큰 비용 계산 로직 (`saveTokenUsage()`) | -| `api/database/migrations/2026_02_09_*_ai_pricing_configs.php` | AI 단가 테이블 + 시드 데이터 | -| `api/database/migrations/2026_02_07_*_ai_token_usages.php` | 토큰 사용량 테이블 | - -### 5.3 정산 관련 - -| 파일 | 내용 | -|------|------| -| `mng` 또는 `api` 정산 컨트롤러/서비스 | 영업수수료, 구독료, 컨설팅비 정산 | - -### 5.4 DB 테이블 참조 - -| 테이블 | 설명 | -|--------|------| -| `barobill_subscriptions` | 바로빌 월정액 구독 현황 | -| `barobill_billing_records` | 바로빌 월별 과금 내역 | -| `barobill_pricing_policies` | 바로빌 과금 정책 (무료 제공량, 추가 단가) | -| `ai_pricing_configs` | AI 제공자별 단가 설정 | -| `ai_token_usages` | AI 토큰 사용량 기록 | -| `ai_voice_recordings` | AI 음성 녹음 (STT 비용 발생) | -| `sales_commissions` | 영업수수료 정산 | - ---- - -## 6. 관련 문서 - -- [고객 요금 안내](customer-pricing.md) - 서비스 가격표 (고객 공개용) -- [영업파트너 수당 체계](partner-commission.md) - 수당률 및 정산 (파트너용) -- [단가 정책 (품목)](pricing-policy.md) - 품목 단가/원가 계산 -- [영업수수료정산](../features/settlement/sales-commissions.md) -- [구독료정산](../features/settlement/subscriptions.md) -- [컨설팅비용정산](../features/settlement/consulting-fees.md) -- [고객사정산](../features/settlement/customer-settlements.md) -- `sales/price/수당지급체계.md` - 수당 지급 체계 상세 -- `sales/policy/SAM_영업정책문서.md` - 영업 정책 상세 -- `sales/price/ref/토큰정책.md` - AI 토큰 고객 안내 가이드 - ---- - -> **공통 안내**: 본 문서에 명시된 모든 개발비 및 구독료는 **부가가치세(VAT) 별도** 금액이다. - ---- - -**최종 업데이트**: 2026-02-21 diff --git a/sam/docs/rules/billing-policy.pptx b/sam/docs/rules/billing-policy.pptx deleted file mode 100644 index 6a3f307a35fff7b563bf2341764496e0554b0959..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 319165 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 diff --git a/sam/docs/rules/customer-pricing.md b/sam/docs/rules/customer-pricing.md deleted file mode 100644 index 60a9ff2..0000000 --- a/sam/docs/rules/customer-pricing.md +++ /dev/null @@ -1,116 +0,0 @@ -# SAM 서비스 요금 안내 - -> **작성일**: 2026-02-21 -> **상태**: 설계 확정 - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM 서비스 도입 시 고객에게 안내하는 요금 체계를 정리한다. - -### 1.2 용어 정의 - -| 용어 | 설명 | -|------|------| -| **개발비** | 서비스 도입 시 1회 납부하는 초기 비용 | -| **구독료** | 서비스 유지를 위한 월 정기 비용 | -| **토큰** | AI가 언어를 처리하는 최소 단위 (한글 ~1.5자 = 1토큰) | - ---- - -## 2. 기본 서비스 요금 - -### 2.1 제조업 기본 패키지 - -- **포함**: 품목관리 → 견적 → 수주 → 생산 → 출하 (ERP 인사/회계 무료 포함) -- **개발비**: 2,000만원 (VAT 별도) -- **구독료**: 50만원/월 (VAT 별도) - -### 2.2 개별 모듈 - -| 모듈명 | 개발비 (VAT 별도) | 구독료/월 (VAT 별도) | -|--------|------------------:|-----------------:| -| QR코드 관리 | 1,020만원 | 5만원 | -| 사진/출하 관리 | 1,920만원 | 10만원 | -| 검사/토큰 적용 | 1,020만원 | 5만원 | -| 이카운트 연동 | 1,920만원 | 10만원 | - -### 2.3 통합 패키지 - -| 패키지명 | 개발비 (VAT 별도) | 구독료/월 (VAT 별도) | -|---------|------------------:|-----------------:| -| 공사관리 패키지 | 4,000만원 | 20만원 | -| 공정/정부지원사업 | 8,000만원 | 40만원 | - ---- - -## 3. 추가 옵션 요금 - -| 옵션명 | 개발비 추가 (VAT 별도) | 구독료 추가/월 (VAT 별도) | -|--------|---------------------:|----------------------:| -| 생산공정 1개 추가 | 500만원 | 10만원 | -| 품질관리 (인정검사) | 2,000만원 | 50만원 | -| 사진 등록 | - | 10만원 | -| 챗봇/녹음/업무일지 | - | 각 20만원 | -| 연구소 연구노트 | - | 5만원 | -| 장비점검, 사무소 정비 | - | 5만원 | - -> **참고**: 품질관리(인정검사)에는 '장비점검, 사무소 정비' 기능이 기본 포함된다. - ---- - -## 4. 사용량 기반 추가 과금 - -기본 제공 한도 초과 시 실비 과금한다. - -| 항목 | 기본 제공 | 추가 과금 기준 | -|------|----------|--------------| -| 파일 저장 공간 | 100GB | 100GB당 **10만원/월** | -| AI 토큰 | 월 100만 토큰 | 1,000토큰 단위 실비 과금 | - -### AI 토큰 사용량 체감 (100만 토큰 기준) - -| 활용 시나리오 | 예상 처리량 | -|-------------|-----------| -| 음성 회의 요약 | 약 520분 (8.6시간) | -| 문서 자료 정리 (A4) | 약 300~400매 | -| 이메일/노트 분류 | 약 1,500~2,000건 | - -- 미사용 잔여 토큰은 이월되지 않는다 (매월 1일 갱신) -- 기본 제공량 80%, 100% 소진 시 자동 알림 발송 - ---- - -## 5. 바로빌 부가 서비스 요금 - -고객이 선택적으로 이용하는 바로빌 연동 서비스이다. - -| 서비스 | 과금 방식 | 기본 제공 | 추가 과금 | 부담 주체 | -|--------|---------|---------|---------|----------| -| 계좌조회 | 월정액 10,000원 | 1계좌 | 추가 1계좌당 10,000원 | 고객 | -| 카드내역 | 월정액 10,000원 | 5장 | 추가 1장당 5,000원 | 고객 | -| 홈택스 매입/매출 | 월 33,000원 | - | - | 본사 (무료) | -| 세금계산서 발행 | 건별 | 100건 | 추가 50건당 5,000원 | 고객 | - -> **과금 계산 예시**: -> - 법인카드 8장 등록 → (8-5) × 5,000 = 15,000원 추가 -> - 세금계산서 151건 → ceil((151-100)/50) × 5,000 = 10,000원 추가 - ---- - -## 6. 관련 문서 - -- [내부 과금정책](billing-policy.md) - 본사 지출 원가 및 코드 참조 (내부용) -- [영업파트너 수당 체계](partner-commission.md) - 수당률 및 정산 프로세스 (파트너용) -- [단가 정책 (품목)](pricing-policy.md) - 품목 단가/원가 계산 - ---- - -> **공통 안내**: 본 문서에 명시된 모든 개발비 및 구독료는 **부가가치세(VAT) 별도** 금액이다. - ---- - -**최종 업데이트**: 2026-02-21 diff --git a/sam/docs/rules/customer-pricing.pptx b/sam/docs/rules/customer-pricing.pptx deleted file mode 100644 index f96693b7870866e564e9400ce95fa43d0687fe5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294356 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%0Dc zR}K3R!(h=y&7oDR$p|$r8EO~enB&P&Tl6N?Kwm?INKsoV-PTKvTG|?%ng%IiUn6C$ zcGhZha@4%n>l4#O%`lVP`>dSm*DwqgZPdd5uGM6OngSp?r8Ja_VsOeN{yVzt878tL bL7#}y)FlcxMVmc*jE|WCFP|f6a6{&Qy{$&W diff --git a/sam/docs/rules/partner-commission.md b/sam/docs/rules/partner-commission.md deleted file mode 100644 index 7917fe1..0000000 --- a/sam/docs/rules/partner-commission.md +++ /dev/null @@ -1,114 +0,0 @@ -# SAM 영업파트너 수당 체계 - -> **작성일**: 2026-02-21 -> **상태**: 설계 확정 - ---- - -## 1. 개요 - -### 1.1 목적 - -영업파트너가 알아야 할 수당 체계 및 정산 프로세스를 안내한다. - -### 1.2 기본 원칙 - -- 수당은 **개발비에 대해서만** 지급한다 -- 구독료는 플랫폼 유지보수 및 클라우드 인프라 비용으로 활용되며 수당 대상이 아니다 -- 기준금액은 개발비의 50%이며, 2단계(1차/2차) 분할 지급한다 - ---- - -## 2. 가입 유형별 수당률 - -| 가입 유형 | 파트너/단체 수당 | 매니저 수당 | 협업지원금(유치자) | **본사 총 지급률** | -|-----------|:--------------:|:----------:|:-----------------:|:----------------:| -| **개인 가입** | 20% | 5% | 3% | **28%** | -| **단체 가입** | 30% | 0% | 3% | **33%** | - -- **협업지원금**: 유치자(parent)가 자신이 유치한 하위 파트너의 매출에서 3%를 수령하는 내부 제도 -- 1단계까지만 적용 (유치자 → 하위 파트너, 그 이상 상위로는 올라가지 않음) -- 단체 가입 시 매니저 수당은 0% -- 단체 가입 시 수수료율은 개발비의 33% (VAT 별도) - ---- - -## 3. 수당 산정 예시 - -### 3.1 기준금액 100만원 기준 - -| 가입 유형 | 파트너/단체 | 매니저 | 협업지원금 | 본사 총 지급 | -|-----------|----------:|-------:|----------:|------------:| -| 개인 (유치자 있음) | 200,000원 | 50,000원 | 30,000원 | **280,000원 (28%)** | -| 개인 (유치자 없음) | 200,000원 | 50,000원 | 0원 | 250,000원 (25%) | -| 단체 | 300,000원 | 0원 | 30,000원 | **330,000원 (33%)** | - -### 3.2 제조업 기본 패키지 (개발비 2,000만원) 기준 - -**개인 가입:** - -| 항목 | 금액 | -|------|-----:| -| 판매자(파트너) 수당 | 400만원 (20%) | -| 매니저 수당 | 100만원 (5%) | -| 협업지원금(유치자) | 60만원 (3%) | - -**단체 가입:** - -| 항목 | 금액 | -|------|-----:| -| 단체 수당 | 600만원 (30%) | -| 매니저 수당 | 0원 (0%) | -| 협업지원금(유치자) | 60만원 (3%) | - ---- - -## 4. 수당 지급 프로세스 - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ 1. 계약 및 입금 │ → │ 2. 수당 확정 │ → │ 3. 지급 │ -│ │ │ │ │ │ -│ 개발비 전액 │ │ 매월 정산일에 │ │ 파트너 등록 │ -│ 완납 확인 │ │ 건별 수당 확정 │ │ 계좌로 지급 │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - -- **지급 시점**: 개발비 입금 완료 확인 후 익월 정산일 - ---- - -## 5. 추가 옵션별 수당 참고 - -개발비가 있는 옵션만 수당 대상이다. 서비스 요금 상세는 [고객 요금 안내](customer-pricing.md)를 참조한다. - -| 옵션명 | 개발비 (VAT 별도) | 판매자 수당 (20%) | -|--------|------------------:|------------------:| -| 생산공정 1개 추가 | 500만원 | 100만원 | -| 품질관리 (인정검사) | 2,000만원 | 400만원 | - -> **참고**: 구독료만 있는 옵션(사진 등록, 챗봇/녹음/업무일지 등)은 수당 대상이 아니다. - ---- - -## 6. 기타 정책 - -- **계약 유지**: 장기 구독을 전제로 한 초기 비용 할인 정책이므로, 중도 해지 시 별도의 위약금 규정이 적용될 수 있다 -- **구독료**: 플랫폼 유지보수 및 클라우드 인프라 비용으로 활용되며, 수당 지급 대상이 아니다 - ---- - -## 7. 관련 문서 - -- [고객 요금 안내](customer-pricing.md) - 서비스 요금 상세 (고객용) -- [내부 과금정책](billing-policy.md) - 본사 지출 원가 및 코드 참조 (내부용) -- `sales/price/수당지급체계.md` - 수당 지급 체계 원본 -- `sales/policy/SAM_영업정책문서.md` - 영업 정책 상세 - ---- - -> **공통 안내**: 본 문서에 명시된 모든 개발비 및 구독료는 **부가가치세(VAT) 별도** 금액이다. - ---- - -**최종 업데이트**: 2026-02-21 diff --git a/sam/docs/rules/partner-commission.pptx b/sam/docs/rules/partner-commission.pptx deleted file mode 100644 index ff69d4acc8afb5c4bc596be85ddc2811fccc5789..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262731 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 - - - - - - - -
-
- -

SAM

-
-
-

CONFIDENTIAL

-
-
- - -
-

INTERNAL USE ONLY

-

SAM 과금정책

-

본사 지출 원가 · 마진 구조 · 코드 참조

-
- - -
-
-

COST PER TENANT

-

~1만원/월

-

테넌트당 월 원가

-
-
-

OPERATING MARGIN

-

67~75%

-

영업이익률

-
-
-

SCALE ADVANTAGE

-

MC → 0

-

스케일 시 한계비용

-
-
- - -
-
-

SCOPE

-

내부 개발팀 / 관리층

-
-
-

DATE

-

2026. 02

-
-
-

WARNING

-

대외 공유 금지

-
-
- - diff --git a/sam/docs/rules/slides/billing-policy/slide-02.html b/sam/docs/rules/slides/billing-policy/slide-02.html deleted file mode 100644 index 863fc46..0000000 --- a/sam/docs/rules/slides/billing-policy/slide-02.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - -
-
-
-

OVERVIEW

-
-

과금정책 3분할 체계

-
-
-

CONFIDENTIAL

-
-
- - -
- -
-
-

FOR CUSTOMER

-
-

고객용

-
-

customer-pricing.md

-
-

서비스 가격표, 과금 기준

-
-

고객에게 직접 전달 가능

-
-
- - -
-
-

FOR PARTNER

-
-

파트너용

-
-

partner-commission.md

-
-

수당률, 정산 프로세스

-
-

영업파트너 제공용

-
-
- - -
-
-

INTERNAL ONLY

-
-

내부용 (본 문서)

-
-

billing-policy.md

-
-

원가, 마진, 코드참조

-
-

대외 공유 금지

-
-
-
- - -
-

본 문서는 내부 개발팀/관리층 전용입니다. 고객 및 파트너에게 공유하지 마세요.

-
- - -
-

SAM 내부 과금정책 | CONFIDENTIAL

-

02

-
- - diff --git a/sam/docs/rules/slides/billing-policy/slide-03.html b/sam/docs/rules/slides/billing-policy/slide-03.html deleted file mode 100644 index b1e7126..0000000 --- a/sam/docs/rules/slides/billing-policy/slide-03.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - -
-
-
-

COST

-
-

바로빌 API 원가 분석

-
-

03

-
- - -
- -
-

월정액 서비스

- -
-

서비스

-

월정액

-

처리 방식

-
- -
-

계좌조회

-

10,000원

-

고객 부담 (전가)

-
- -
-

카드내역

-

10,000원

-

고객 부담 (전가)

-
- -
-

홈택스 매입/매출

-

33,000원

-

본사 흡수 (코드브릿지엑스 → 무료)

-
-
- - -
- -
-

건별 과금

- -
-

서비스

-

원가

-
-
-

세금계산서 발행

-

100원/건

-
- -
-

고객 과금: 5,000원/50건

-

마진: 4,900원 (98%)

-
-
- - -
-

핵심 요약

-
-
-

계좌/카드 → 고객 전가, 홈택스 → 코드브릿지엑스 지원으로 본사 원가 0원

-
-
-
-

본사 실질 부담: 세금계산서 원가만 (5,000~10,000원/월)

-
-
-
-
- - -
-

SAM 내부 과금정책 | CONFIDENTIAL

-

03

-
- - diff --git a/sam/docs/rules/slides/billing-policy/slide-04.html b/sam/docs/rules/slides/billing-policy/slide-04.html deleted file mode 100644 index 89ea645..0000000 --- a/sam/docs/rules/slides/billing-policy/slide-04.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - -
-
-
-

COST

-
-

AI / 클라우드 원가 분석

-
-

04

-
- - -
- -
-
-

토큰 기반 과금 (LLM)

-
-

환율 1,400원/USD

-
-
- - -
-
-

Google Gemini 2.0 Flash

-
-
-
-

입력 /1M 토큰

-

$0.10 → 140원

-
-
-

출력 /1M 토큰

-

$0.40 → 560원

-
-
-
-

초저비용 AI — 100만 토큰 처리해도 700원

-
-
- - -
-
-

Anthropic Claude 3 Haiku

-
-
-
-

입력 /1M 토큰

-

$0.25 → 350원

-
-
-

출력 /1M 토큰

-

$1.25 → 1,750원

-
-
-
-

고품질 AI — 100만 토큰에도 약 2,100원

-
-
-
- - -
-

시간/작업 기반 과금

- - -
-
-

Google STT

-

$0.009 / 15초

-
-
-

~50원/분

-

1시간 녹음 처리 = 3,000원

-
-
- - -
-
-

Google GCS

-

$0.005 / 1,000건

-
-
-

7원/1,000건

-

사실상 무료 수준

-
-
- - -
-
-

Google FCM (푸시알림)

-

무제한

-
-
-

무료

-

완전 무료

-
-
- - -
-

핵심 인사이트

-

LLM 토큰 비용은 건당 1원 미만 수준. 클라우드 인프라 비용도 무시 가능한 수준으로, AI 기능이 수익 마진에 미치는 영향이 극히 작음.

-
-
-
- - -
-

ai_pricing_configs 테이블에서 동적 관리

-

캐시 TTL 1시간

-
- - -
-

SAM 내부 과금정책 | CONFIDENTIAL

-

04

-
- - diff --git a/sam/docs/rules/slides/billing-policy/slide-05.html b/sam/docs/rules/slides/billing-policy/slide-05.html deleted file mode 100644 index 377697e..0000000 --- a/sam/docs/rules/slides/billing-policy/slide-05.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - -
-
-
-

MARGIN

-
-

마진 구조 + 가격 책정 배경

-
-

05

-
- - -
- -
-
-

MARGIN STRUCTURE

-

회사 영업이익률

-

67~75%

-
-
-
-

개인 가입

-

72~75% (수당 25~28%)

-
-
-

단체 가입

-

~67% (수당 33%)

-
-
-
-

SaaS 업계 평균 마진 60% 대비 높은 수익성

-
-
- - -
-
-

PRICE BACKGROUND

-

내부 총 개발비 산정

-

7,600만원

-
-
-
-
-

개발비 2,000만원 + 장기 구독 약 7년

-
-
-
-

금융 비용 4.6% 포함

-
-
-
-

중소기업 초기 투자 부담 분산 구조

-
-
-
-
- - -
-

본사 지출 월간 예상 (테넌트 1개)

-
-
-

바로빌 홈택스

-

0원

-
-
-

세금계산서

-

5,000~10,000원

-
-
-

AI 토큰

-

100~5,000원

-
-
-

GCS/STT

-

50~2,000원

-
-
-
-

합계 최대

-

~17,000원/월

-
-
-
-

구독료 50만원 대비 원가율 3.4% — 스케일 시 한계비용 0에 수렴

-
-
- - -
-

SAM 내부 과금정책 | CONFIDENTIAL

-

05

-
- - diff --git a/sam/docs/rules/slides/billing-policy/slide-06.html b/sam/docs/rules/slides/billing-policy/slide-06.html deleted file mode 100644 index 923547d..0000000 --- a/sam/docs/rules/slides/billing-policy/slide-06.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - -
-
-
-

CODE REF

-
-

코드 참조 — 소스 파일

-
-

06

-
- - -
- -
-
-
-

바로빌 과금

-
-

6개 파일

-
-
- -
-
-

BarobillSubscription.php

-

DEFAULT_MONTHLY_FEES 상수

-
-
-

BarobillBillingRecord.php

-

USAGE_UNIT_PRICES 상수

-
-
-

BarobillPricingPolicy.php

-

calculateBilling() 메서드

-
-
-

BarobillBillingService.php

-

월정액/건별 과금 처리

-
-
-

BarobillUsageService.php

-

사용량 집계

-
-
-

BarobillPricingPolicySeeder.php

-

5개 정책 시더

-
-
-
- - -
-
-
-

AI 가격/토큰

-
-

5개 파일

-
-
- -
-
-

AiPricingConfig.php

-

getActivePricing()

-
-
-

AiTokenUsage.php

-

토큰 사용량 기록

-
-
-

AiReportService.php

-

saveTokenUsage()

-
-
-

ai_pricing_configs 마이그레이션

-

AI 단가 테이블

-
-
-

ai_token_usages 마이그레이션

-

토큰 사용량 테이블

-
-
- - -
-

모든 과금 상수와 정책은 소스 코드에 정의되며, DB 시더/마이그레이션으로 초기 데이터가 관리됩니다.

-
-
-
- - -
-

SAM 내부 과금정책 | CONFIDENTIAL

-

06

-
- - diff --git a/sam/docs/rules/slides/billing-policy/slide-07.html b/sam/docs/rules/slides/billing-policy/slide-07.html deleted file mode 100644 index 7a5a772..0000000 --- a/sam/docs/rules/slides/billing-policy/slide-07.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - -
-
-
-

CODE REF

-
-

코드 참조 — DB 테이블

-
-

07

-
- - -
- -
-

테이블

-

카테고리

-

설명

-
- - -
-

barobill_subscriptions

-
-
-

바로빌

-
-
-

바로빌 월정액 구독 현황

-
- - -
-

barobill_billing_records

-
-
-

바로빌

-
-
-

바로빌 월별 과금 내역

-
- - -
-

barobill_pricing_policies

-
-
-

바로빌

-
-
-

과금 정책 (무료 제공량, 추가 단가)

-
- - -
-

ai_pricing_configs

-
-
-

AI

-
-
-

AI 제공자별 단가 설정

-
- - -
-

ai_token_usages

-
-
-

AI

-
-
-

AI 토큰 사용량 기록

-
- - -
-

ai_voice_recordings

-
-
-

AI

-
-
-

AI 음성 녹음 (STT 비용 발생)

-
- - -
-

sales_commissions

-
-
-

영업

-
-
-

영업수수료 정산

-
-
- - -
-

모든 테이블은 tenant_id 기반 Multi-tenant 구조이며, API 프로젝트(/home/aweso/sam/api)에서 마이그레이션을 관리합니다.

-
- - -
-

SAM 내부 과금정책 | CONFIDENTIAL

-

07

-
- - diff --git a/sam/docs/rules/slides/customer-pricing/slide-01.html b/sam/docs/rules/slides/customer-pricing/slide-01.html deleted file mode 100644 index e20a544..0000000 --- a/sam/docs/rules/slides/customer-pricing/slide-01.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - -
-
- -

SAM

-
-
-

SERVICE PRICING 2026

-
-
- - -
-

SMART AUTOMATION MANAGEMENT

-

SAM 서비스 요금 안내

-

제조 현장의 디지털 전환, SAM 하나로 완성합니다

-
- - -
-
-

80% UP

-

업무 자동화율 향상

-
-
-

30% DOWN

-

인건비 절감 효과

-
-
-

100%

-

실시간 현황 파악

-
-
- - -
-
-

COMPANY

-

(주)코드브릿지엑스

-
-
-

DATE

-

2026. 02

-
-
-

NOTE

-

VAT 별도

-
-
-

01 / 07

-
-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/customer-pricing/slide-02.html b/sam/docs/rules/slides/customer-pricing/slide-02.html deleted file mode 100644 index 7a6d142..0000000 --- a/sam/docs/rules/slides/customer-pricing/slide-02.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - -
- -
-
-

CONTENTS

-
-

목차

-

SAM 서비스의 투명한 요금 체계를 한눈에 확인하세요

-
- - -
- -
-
-

01

-
-
-

기본 서비스 요금

-

제조업 패키지, 개별 모듈, 통합 패키지

-
-
- -
-
-

02

-
-
-

추가 옵션 요금

-

생산공정 추가, 품질관리, 챗봇, 연구노트

-
-
- -
-
-

03

-
-
-

사용량 기반 과금

-

파일 저장 공간, AI 토큰 사용량

-
-
- -
-
-

04

-
-
-

바로빌 부가서비스

-

계좌조회, 카드내역, 세금계산서 발행

-
-
-
-
- - -
-

SAM 서비스 요금 안내 | (주)코드브릿지엑스

-

02 / 07

-

모든 금액 VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/customer-pricing/slide-03.html b/sam/docs/rules/slides/customer-pricing/slide-03.html deleted file mode 100644 index 925e39a..0000000 --- a/sam/docs/rules/slides/customer-pricing/slide-03.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 01

-
-

기본 서비스 요금

-
-

03 / 07

-
- - -
- - -
-
-
-
-

BEST SELLER

-
-
-

BASIC

-
-
-

제조업 기본 패키지

-

품목관리 → 견적 → 수주 → 생산 → 출하

-
-

ERP 인사/회계 무료 포함

-
-
-
-
-
-

개발비 (1회)

-

2,000만원

-
-
-

구독료 (월)

-

50만원

-
-
-
-

하루 약 1.6만원으로 제조 ERP 완성

-
-
-
- - -
- -

개별 모듈

- -
-

모듈명

-

개발비

-

구독/월

-
- -
-

QR코드 관리

-

1,020만원

-

5만원

-
- -
-

사진/출하 관리

-

1,920만원

-

10만원

-
- -
-

검사/토큰 적용

-

1,020만원

-

5만원

-
- -
-

이카운트 연동

-

1,920만원

-

10만원

-
- - -

통합 패키지

- -
-

패키지명

-

개발비

-

구독/월

-
- -
-

공사관리 패키지

-

4,000만원

-

20만원

-
- -
-

공정/정부지원사업

-

8,000만원

-

40만원

-
-
-
- - -
-

SAM 서비스 요금 안내 | (주)코드브릿지엑스

-

모든 금액 VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/customer-pricing/slide-04.html b/sam/docs/rules/slides/customer-pricing/slide-04.html deleted file mode 100644 index 154d62c..0000000 --- a/sam/docs/rules/slides/customer-pricing/slide-04.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 02

-
-

추가 옵션 요금

-
-

04 / 07

-
- - -
- -
- -
-
-

생산공정 1개 추가

-

생산 라인별 개별 공정 확장

-
-
-
-

개발비

-

500만원

-
-
-

구독/월

-

10만원

-
-
-
- - -
-
-
-

품질관리 (인정검사)

-
-
-

품질 인증 필수!

-
-

ISO 인증 대응 필수 모듈

-
-
-
-

개발비

-

2,000만원

-
-
-

구독/월

-

50만원

-
-
-
- - -
-
-

사진 등록

-

제품/공정 사진 관리 기능

-
-
-
-

개발비

-

-

-
-
-

구독/월

-

10만원

-
-
-
-
- - -
- -
-
-

챗봇 / 녹음 / 업무일지

-

AI 기반 업무 보조 도구 3종

-
-
-
-

개발비

-

-

-
-
-

구독/월

-

각 20만원

-
-
-
- - -
-
-

연구소 연구노트

-

R&D 기록 및 지식재산 관리

-
-
-
-

개발비

-

-

-
-
-

구독/월

-

5만원

-
-
-
- - -
-
-

장비점검 / 사무소 정비

-

설비 유지보수 이력 관리

-
-
-
-

개발비

-

-

-
-
-

구독/월

-

5만원

-
-
-
-
-
- - -
-

참고 품질관리(인정검사)에는 장비점검/사무소 정비 기능이 기본 포함됩니다.

-
- - -
-

SAM 서비스 요금 안내 | (주)코드브릿지엑스

-

모든 금액 VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/customer-pricing/slide-05.html b/sam/docs/rules/slides/customer-pricing/slide-05.html deleted file mode 100644 index 03b55af..0000000 --- a/sam/docs/rules/slides/customer-pricing/slide-05.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 03

-
-

사용량 기반 추가 과금

-
-

05 / 07

-
- - -
- - -
- -
-
-
-

F

-
-

파일 저장 공간

-
-
-

기본 제공

-

100GB

-
-
-
-

초과 시

-

100GB당 10만원/월

-
-
- - -
-
-
-

AI

-
-

AI 토큰

-
-
-

기본 제공

-

100만 토큰/월

-
-
-
-

초과 시

-

실비 정산

-
-
-
- - -
-

AI 100만 토큰, 이만큼 쓸 수 있습니다

-

대부분의 중소 제조업에서 충분한 사용량

- - -
-
-
-

음성 회의 요약

-

하루 30분씩 매일 회의해도 한 달 충분

-
-

520

-
-
- - -
-
-
-

문서 자동 정리

-

매일 15장씩 처리해도 여유

-
-

300~400매(A4)

-
-
- - -
-
-
-

이메일/노트 자동 분류

-

하루 75건 자동 분류 가능

-
-

2,000

-
-
- -
-

미사용 토큰 이월 불가 | 80% / 100% 소진 시 자동 알림

-
-
-
- - -
-

SAM 서비스 요금 안내 | (주)코드브릿지엑스

-

모든 금액 VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/customer-pricing/slide-06.html b/sam/docs/rules/slides/customer-pricing/slide-06.html deleted file mode 100644 index e3c9461..0000000 --- a/sam/docs/rules/slides/customer-pricing/slide-06.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 04

-
-

바로빌 부가서비스

-
-

06 / 07

-
- - -
- -
-

서비스

-

과금 방식

-

기본 제공

-

추가 과금

-

부담

-
- - -
-

계좌조회

-

월정액 10,000원

-

1계좌

-

추가 1계좌당 10,000원

-
-
-

고객

-
-
-
- - -
-

카드내역

-

월정액 10,000원

-

5장

-

추가 1장당 5,000원

-
-
-

고객

-
-
-
- - -
-

홈택스 매입/매출

-

월 33,000원

-

-

-

-

-
-
-

SAM 무료!

-
-
-
- - -
-

세금계산서 발행

-

건별

-

100건

-

추가 50건당 5,000원

-
-
-

고객

-
-
-
-
- - -
-

과금 계산 예시

-
- -
-

법인카드 8장 등록 시

-

(8 - 5) x 5,000 = 15,000원 추가

-
- -
-

세금계산서 151건 발행 시

-

ceil((151-100)/50) x 5,000 = 10,000원 추가

-
-
-
- - -
-

SAM 서비스 요금 안내 | (주)코드브릿지엑스

-

모든 금액 VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/customer-pricing/slide-07.html b/sam/docs/rules/slides/customer-pricing/slide-07.html deleted file mode 100644 index b7ebd6e..0000000 --- a/sam/docs/rules/slides/customer-pricing/slide-07.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - -
- -

SAM

-
- - -
-

지금 SAM과 함께 시작하세요

-

제조 현장의 모든 흐름을 하나의 플랫폼에서 관리합니다

-
-
-

ERP + MES 통합

-
-
-

AI 기반 업무 자동화

-
-
-

실시간 현황 대시보드

-
-
-
- - -
-
-

COMPANY

-

(주)코드브릿지엑스

-
-
-

SYSTEM

-

Smart Automation Management

-
-
-

CONTACT

-

담당 영업파트너 또는 본사

-
-
-
-

모든 금액 VAT 별도

-
-

07 / 07

-
-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/partner-commission/slide-01.html b/sam/docs/rules/slides/partner-commission/slide-01.html deleted file mode 100644 index 1a7624d..0000000 --- a/sam/docs/rules/slides/partner-commission/slide-01.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - -
-
- -

SAM

-
-
-

PARTNER COMMISSION GUIDE

-
-
- - -
-

SALES PARTNER COMMISSION

-

SAM 영업파트너
수당 체계

-

Smart Automation Management

-

업계 최고 수준 수당률, 함께 성장하는 파트너십

-
- - -
-
-

33%

-

최대 수당률

-
-
-

660만원

-

개발비 2,000만원 기준

-
-
-

무제한

-

누적 파트너 수익

-
-
- - -
-
-

COMPANY

-

(주)코드브릿지엑스

-
-
-

DATE

-

2026. 02

-
-
-

NOTE

-

VAT 별도

-
-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/partner-commission/slide-02.html b/sam/docs/rules/slides/partner-commission/slide-02.html deleted file mode 100644 index b7ce596..0000000 --- a/sam/docs/rules/slides/partner-commission/slide-02.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 01

-
-

수당 지급 3대 원칙

-
-

02

-
- - -
- -
-
-

1

-
-

개발비 기반

-

수당은 개발비에 대해서만 지급합니다.

-

구독료는 인프라 비용으로 활용되며 수당 대상이 아닙니다.

-
-
-

개발비 = 수당 대상

-
-
-
- - -
-
-

2

-
-

기준금액 50% 적용

-

개발비의 50%를 기준금액으로 산정합니다.

-

기준금액에 수당률을 적용하여 2단계 분할 지급합니다.

-
-
-

2단계 분할 지급

-
-
-
- - -
-
-

3

-
-

투명한 정산

-

매월 정산일에 건별 수당 확정합니다.

-

익월 파트너 등록 계좌로 입금합니다.

-
-
-

매월 정산 + 익월 지급

-
-
-
-
- - -
-

명확한 기준, 신뢰할 수 있는 파트너십

-
- - -
-

SAM 영업파트너 수당 체계 | (주)코드브릿지엑스

-

02

-

VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/partner-commission/slide-03.html b/sam/docs/rules/slides/partner-commission/slide-03.html deleted file mode 100644 index ce86831..0000000 --- a/sam/docs/rules/slides/partner-commission/slide-03.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 02

-
-

가입 유형별 수당률

-
-

03

-
- - -
- -
-
-

개인 가입

-
-

총 28%

-
-
- - -
-

28%

-
- - -
-
-

파트너 수당

-

20%

-
-
-

매니저 수당

-

5%

-
-
-

협업지원금(유치자)

-

3%

-
-
- -
-

매니저와 유치자를 통한 추가 수익 구조

-
-
- - -
-
-

단체 가입

-
-

총 33%

-
-
- - -
-

33%

-
- - -
-
-

단체 수당

-

30%

-
-
-

매니저

-

0%

-
-
-

협업지원금(유치자)

-

3%

-
-
- -
-

단체 가입 시 최고 수당률!

-
-
-
- - -
-

유치자 수당은 하위 파트너 1단계까지 적용

-

|

-

단체 가입 시 수수료율 개발비의 33% (VAT 별도)

-
- - -
-

SAM 영업파트너 수당 체계 | (주)코드브릿지엑스

-

03

-

VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/partner-commission/slide-04.html b/sam/docs/rules/slides/partner-commission/slide-04.html deleted file mode 100644 index 72c5b0f..0000000 --- a/sam/docs/rules/slides/partner-commission/slide-04.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 03

-
-

수당 산정 시뮬레이션

-
-

04

-
- - -
-

기준금액 100만원 기준

- - -
-
-

가입 유형

-
-
-

파트너/단체

-
-
-

매니저

-
-
-

협업지원금

-
-
-

총 지급

-
-
- - -
-
-

개인 (유치자O)

-
-
-

200,000원

-
-
-

50,000원

-
-
-

30,000원

-
-
-

280,000원 (28%)

-
-
- - -
-
-

개인 (유치자X)

-
-
-

200,000원

-
-
-

50,000원

-
-
-

0원

-
-
-

250,000원 (25%)

-
-
- - -
-
-

단체

-
-
-

300,000원

-
-
-

0원

-
-
-

30,000원

-
-
-

330,000원 (33%)

-
-
-
- - -
-
-

제조업 기본 패키지 (개발비 2,000만원) 실제 수당

-
-

실전 시뮬레이션

-
-
- -
- -
-

개인 가입

-
-

판매자(파트너)

-

400만원

-
-
-

매니저

-

100만원

-
-
-

협업지원금

-

60만원

-
-
-
-

총 560만원 수익!

-
-
-
- - -
-
-

단체 가입

-
-

BEST

-
-
-
-

단체

-

600만원

-
-
-

매니저

-

0원

-
-
-

협업지원금

-

60만원

-
-
-
-

총 660만원 수익!

-
-
-
-
-
- - -
-

SAM 영업파트너 수당 체계 | (주)코드브릿지엑스

-

04

-

VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/partner-commission/slide-05.html b/sam/docs/rules/slides/partner-commission/slide-05.html deleted file mode 100644 index de4844e..0000000 --- a/sam/docs/rules/slides/partner-commission/slide-05.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - -
-
-
-

SECTION 04

-
-

수당 지급 프로세스

-
-

05

-
- - -
- -
-
-

1

-
-

계약 및 입금

-

개발비 전액 완납 확인

-
- - -
-

-
- - -
-
-

2

-
-

수당 확정

-

매월 정산일 건별 수당 확정

-
- - -
-

-
- - -
-
-

3

-
-

지급

-

파트너 등록 계좌로 입금

-
-
- - -
-
-

추가 옵션 수당 참고

-
-

개발비가 있는 옵션만 수당 대상

-
-
- -
- -
-

생산공정 추가

-
-

개발비

-

500만원

-
-
-

판매자 수당

-

100만원

-
-
- - -
-

품질관리 인정검사

-
-

개발비

-

2,000만원

-
-
-

판매자 수당

-

400만원

-
-
- - -
-

구독료만 있는 옵션

-
-

개발비

-

없음

-
-
-

판매자 수당

-

비대상

-
-
-
-
- - -
-

SAM 영업파트너 수당 체계 | (주)코드브릿지엑스

-

05

-

VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/partner-commission/slide-06.html b/sam/docs/rules/slides/partner-commission/slide-06.html deleted file mode 100644 index 4c69874..0000000 --- a/sam/docs/rules/slides/partner-commission/slide-06.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - -
-
- -

SAM

-
-

06

-
- - -
-

PARTNERSHIP

-

지금, SAM 파트너로
함께하세요

-

제조업 디지털 전환 시장, 폭발적 성장 중

-

대한민국 제조업 디지털 전환, 당신이 이끄는 비즈니스 기회

-
- - -
-
-

33%

-

최대 수당률

-
-
-

660만원

-

건당 최대 수익

-
-
-

무제한

-

누적 수익 상한

-
-
- - -
-
-
-

COMPANY

-

(주)코드브릿지엑스

-
-
-

SYSTEM

-

Smart Automation Management

-
-
-

CONTACT

-

본사 정산팀

-
-
-

모든 금액 VAT 별도

-
- - \ No newline at end of file diff --git a/sam/docs/rules/slides/usage-plan/SAM_활용방안.pptx b/sam/docs/rules/slides/usage-plan/SAM_활용방안.pptx deleted file mode 100644 index ed996ef54512b2edd90d38b08859909c8ff895fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 279940 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 mFnP)ZVa~x(AXk>6K!_PR7e{Gg#>fVih}UV|*?E diff --git a/sam/docs/rules/slides/usage-plan/convert.cjs b/sam/docs/rules/slides/usage-plan/convert.cjs deleted file mode 100644 index b35464a..0000000 --- a/sam/docs/rules/slides/usage-plan/convert.cjs +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); -const PptxGenJS = require('pptxgenjs'); - -async function main() { - const pres = new PptxGenJS(); - pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); - pres.layout = 'CUSTOM_16x9'; - pres.author = '(주)코드브릿지엑스'; - pres.subject = 'SAM 활용방안 - AI 자동화로 중소 제조업을 혁신하다'; - - const slideDir = __dirname; - const slideFiles = [ - 'slide-01.html', 'slide-02.html', 'slide-03.html', - 'slide-04.html', 'slide-05.html', 'slide-06.html', 'slide-07.html' - ]; - - for (const file of slideFiles) { - const htmlPath = path.join(slideDir, file); - console.log(`Converting: ${file}`); - await html2pptx(htmlPath, pres); - } - - const outputPath = path.join(slideDir, 'SAM_활용방안.pptx'); - await pres.writeFile({ fileName: outputPath }); - console.log(`\nPPTX saved: ${outputPath}`); -} - -main().catch(err => { console.error(err); process.exit(1); }); diff --git a/sam/docs/rules/slides/usage-plan/slide-01.html b/sam/docs/rules/slides/usage-plan/slide-01.html deleted file mode 100644 index bfa9d9f..0000000 --- a/sam/docs/rules/slides/usage-plan/slide-01.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - -
-
-
- -
-

SAM PROJECT

-
- -

SAM 활용방안

-

AI 자동화로 중소 제조업을 혁신하다

- -

방화셔터 제조업 실증 | 80% 공통화 전략 | Multi-tenant SaaS 플랫폼

- -
-
-

코어 모델 실증

-
-
-

AI 자동화

-
-
-

다산업군 확장

-
-
-
- -
-

SAM 활용방안 | (주)코드브릿지엑스 | 2026.03

-
- - diff --git a/sam/docs/rules/slides/usage-plan/slide-02.html b/sam/docs/rules/slides/usage-plan/slide-02.html deleted file mode 100644 index c16747f..0000000 --- a/sam/docs/rules/slides/usage-plan/slide-02.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - -
-

왜 SAM인가? — Before / After

-

중소 제조업의 현실과 SAM이 제시하는 변화

-
- -
-
-
-
- -
-

Before — 기존 방식

-
-

Excel 수기 관리

-

데이터 유실, 버전 혼란, 실시간 공유 불가

-

ERP 도입비 수천만원

-

중소기업에 과도한 초기 투자 부담

-

업체별 커스텀 6개월+

-

도입까지 긴 시간, 업데이트 어려움

-

부서간 정보 단절

-

영업/생산/경영 각자 관리, 의사결정 지연

-
- -
-
-
- -
-

After — SAM 도입 후

-
-

시스템 기반 통합 관리

-

실시간 데이터 공유, 단일 진실 공급원(SSOT)

-

월 구독 SaaS

-

초기 비용 최소화, 사용한 만큼 지불

-

멀티테넌시 즉시 입주

-

설정만으로 바로 사용, 지속적 업데이트

-

영업~출고 원스톱 자동화

-

AI가 연결하는 End-to-End 프로세스

-
-
- -
-

SAM 활용방안 | (주)코드브릿지엑스

-

2 / 7

-
- - diff --git a/sam/docs/rules/slides/usage-plan/slide-03.html b/sam/docs/rules/slides/usage-plan/slide-03.html deleted file mode 100644 index 07e9995..0000000 --- a/sam/docs/rules/slides/usage-plan/slide-03.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - -
-

전체 프로세스 — 영업에서 출고까지

-

6단계 비즈니스 플로우와 AI 자동화 포인트

-
- -
-
-

01

-

영업

-

고객 DB 자동분류

-
-

-
-

02

-

상담

-

STT 음성 기록

-
-

-
-

03

-

견적서

-

AI 자동 산출

-
-

-
-

04

-

수주서

-

자동 전환

-
-

-
-

05

-

작업공정

-

AI 공정 최적화

-
-

-
-

06

-

출고

-

배송 자동화

-
-
- -
-

경동/주일 실증 현황

-
-
-

단계

-

구현 기능

-

상태

-

AI 적용

-
-
-

영업관리

-

고객/거래처 CRM

-

운영중

-

고객 분류 자동화

-
-
-

상담/문의

-

상담 이력, 음성 입력

-

운영중

-

STT 음성→텍스트

-
-
-

견적서

-

견적 작성/승인/발송

-

운영중

-

AI 견적 산출 (개발중)

-
-
-

수주서

-

견적→수주 연동

-

운영중

-

자동 전환 프로세스

-
-
-

작업공정

-

BOM, 공정 관리

-

개발중

-

AI 공정 최적화 (계획)

-
-
-

출고/배송

-

출고 지시, 배송 추적

-

계획

-

물류 자동화 (계획)

-
-
-
- -
-

SAM 활용방안 | (주)코드브릿지엑스

-

3 / 7

-
- - diff --git a/sam/docs/rules/slides/usage-plan/slide-04.html b/sam/docs/rules/slides/usage-plan/slide-04.html deleted file mode 100644 index d6fb551..0000000 --- a/sam/docs/rules/slides/usage-plan/slide-04.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - -
-

80% 공통화론 — 핵심 설득 논거

-

중소 제조업 업무의 80%는 업종과 무관하게 동일하다

-
- -
-
-

공통 업무

-
-
-

80% — 영업, 회계, 인사, 재고, 문서, 품질

-
-
-
-
-

커스텀

-
-
-

20%

-
-
-
-

커스텀 20% = 상품 마스터, 견적 계산식, 공정 시퀀스

-
- -
-

업종별 확장 시나리오

-
-
-

업종

-

공통 (80%)

-

커스텀 (20%)

-

난이도

-
-
-

방화셔터

-

영업, 견적, 수주, 회계, 인사

-

셔터 규격 계산, 설치 공정

-

실증완료

-
-
-

블라인드

-

영업, 견적, 수주, 회계, 인사

-

원단/슬랫 규격, 재단 공정

-

즉시가능

-
-
-

금속가공

-

영업, 견적, 수주, 회계, 인사

-

소재/두께 단가표, CNC 공정

-

단기적용

-
-
-

식품제조

-

영업, 견적, 수주, 회계, 인사

-

레시피 관리, HACCP, 유통기한

-

중기적용

-
-
-

전자부품

-

영업, 견적, 수주, 회계, 인사

-

PCB BOM, SMT 공정, 검사

-

중기적용

-
-
- -
-

"상품만 바꾸면 새로운 제조업이 된다. 영업, 회계, 인사, 재고 — 이 80%는 이미 완성되어 있다."

-
-
- -
-

SAM 활용방안 | (주)코드브릿지엑스

-

4 / 7

-
- - diff --git a/sam/docs/rules/slides/usage-plan/slide-05.html b/sam/docs/rules/slides/usage-plan/slide-05.html deleted file mode 100644 index 87cf510..0000000 --- a/sam/docs/rules/slides/usage-plan/slide-05.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - -
-

멀티테넌시 — 하나의 플랫폼, 다수의 기업

-

tenant_id 기반 데이터 격리로 안전하게 다수 기업을 서비스

-
- -
-
-
-
-

A 기업 (경동)

-
-
-

B 기업 (주일)

-
-
-

C 기업 (금속)

-
-
-

D 기업 (식품)

-
-
-

▼ ▼ ▼ ▼

-
-

SAM 플랫폼

-
-

공유: 코드 100%

-

격리: 데이터 100%

-

기반: tenant_id

-
-
-
- -
-
-
-
-

비용 절감

-
-

하나의 코드베이스로 N개 기업 서비스. 기업이 늘어도 개발비 동일.

-
-
-
-
-

즉시 입주

-
-

tenant_id 발급 + 기본 설정. 별도 개발 없이 수일 내 사용.

-
-
-
-
-

데이터 격리

-
-

모든 쿼리에 tenant_id 자동 적용. A기업과 B기업 데이터 완전 분리.

-
-
-
- -
-

SAM 활용방안 | (주)코드브릿지엑스

-

5 / 7

-
- - diff --git a/sam/docs/rules/slides/usage-plan/slide-06.html b/sam/docs/rules/slides/usage-plan/slide-06.html deleted file mode 100644 index ea16e8a..0000000 --- a/sam/docs/rules/slides/usage-plan/slide-06.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - -
-

AI 자동화 현황 & 로드맵

-

구현 완료된 AI 기능과 향후 계획

-
- -
-
-
-
-

구현 완료

-
-
-
-

AI 재무 분석

-

CEO 대시보드에서 매출/비용/손익 AI 분석. Claude API로 자연어 인사이트 제공.

-
-
-

STT 음성 입력

-

상담 메모, 현장 보고를 음성 입력. 자동 텍스트 변환 후 시스템 기록.

-
-
-

Claude Code 개발 자동화

-

SAM 시스템을 Claude Code로 개발. 코드 생성, 리뷰, 배포 자동화.

-
-
- -
-
-
-

향후 계획

-
-
-
-

AI 견적 자동 생성

-

고객 요구사항 입력 시 과거 데이터 기반 최적 견적 자동 산출.

-
-
-

AI 공정 최적화

-

생산 데이터 분석으로 최적 공정 순서, 자재 배치 제안.

-
-
-

AI 고객 상담

-

FAQ 자동 응답, 견적 문의 자동 접수. 필요 시 담당자 연결.

-
-
-
- -
-

"공정의 다양성은 천차만별. 이를 AI와 데이터로 정복하는 것이 SAM의 연구 과제다."

-
- -
-

SAM 활용방안 | (주)코드브릿지엑스

-

6 / 7

-
- - diff --git a/sam/docs/rules/slides/usage-plan/slide-07.html b/sam/docs/rules/slides/usage-plan/slide-07.html deleted file mode 100644 index ed9da70..0000000 --- a/sam/docs/rules/slides/usage-plan/slide-07.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - -
-

로드맵 & 비전

-

방화셔터에서 시작하여 모든 중소 제조업으로

-
- -
-
- -
-
-
-

Phase 1

-

코어 실증

-

2025~2026 상반기

-
-

진행중

-
-
-

경동/주일 방화셔터 제조업에서 전 프로세스 실증. 영업→출고 파이프라인 완성.

-
- -
-
-
-

Phase 2

-

3~5사 확장

-

2026 하반기

-
-

계획

-
-
-

블라인드, 금속가공 등 유사 제조업 3~5사에 멀티테넌시 확장.

-
- -
-
-
-

Phase 3

-

AI 고도화

-

2027

-
-

계획

-
-
-

AI 견적 자동 산출, AI 공정 최적화, AI 고객 상담 순차 적용.

-
- -
-
-
-

Phase 4

-

다산업군 플랫폼

-

2028~

-
-

비전

-
-
-

식품, 전자부품 등 다양한 제조업종. 중소 제조업 표준 SaaS.

-
-
- -
-

"방화셔터에서 시작하여, 모든 중소 제조업의 디지털 전환을 이끄는 SAM"

-

AI 자동화 + 멀티테넌시 + 80% 공통화 = 중소 제조업 혁신 플랫폼 | (주)코드브릿지엑스

-
- -
-

7 / 7

-
- - diff --git a/sam/docs/system/ai-automation-vision.md b/sam/docs/system/ai-automation-vision.md deleted file mode 100644 index 18534a5..0000000 --- a/sam/docs/system/ai-automation-vision.md +++ /dev/null @@ -1,174 +0,0 @@ -# SAM 활용방안 — AI 자동화 비전 - -> **작성일**: 2026-03-02 -> **상태**: 설계 확정 -> **대상**: CEO, 경영진, 전 직원 -> **관련 페이지**: MNG 관리자 → Claude Code → 활용방안 - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM(Smart Automation Management)은 방화셔터 제조업(경동기업, 주일기업)을 코어 모델로 실증한 차세대 ERP/MES 통합 시스템이다. 이 문서는 SAM의 장기 비전과 AI 자동화 전략을 기술한다. - -### 1.2 핵심 논지 - -> "중소 제조업 업무의 80%는 업종과 무관하게 동일하다. 상품만 바꾸면 새로운 제조업이 된다." - -| 항목 | 내용 | -|------|------| -| **코어 모델** | 방화셔터 제조업 (경동/주일 실증 완료) | -| **확장 전략** | 80% 공통 프로세스 + 20% 상품 커스텀 | -| **최종 목표** | Multi-tenant SaaS 플랫폼 (다산업군) | - ---- - -## 2. Before / After — 왜 SAM인가 - -### 2.1 기존 방식의 문제 - -| 문제 | 상세 | -|------|------| -| Excel 수기 관리 | 데이터 유실, 버전 혼란, 실시간 공유 불가 | -| ERP 도입비 수천만원 | 중소기업에 과도한 초기 투자 부담 | -| 업체별 커스텀 6개월+ | 도입까지 긴 시간, 업데이트 어려움 | -| 부서간 정보 단절 | 영업/생산/경영 각자 관리, 의사결정 지연 | - -### 2.2 SAM 도입 후 - -| 개선 | 상세 | -|------|------| -| 시스템 기반 통합 관리 | 실시간 데이터 공유, 단일 진실 공급원(SSOT) | -| 월 구독 SaaS | 초기 비용 최소화, 사용한 만큼 지불 | -| 멀티테넌시 즉시 입주 | 설정만으로 바로 사용, 지속적 업데이트 | -| 영업~출고 원스톱 자동화 | AI가 연결하는 End-to-End 프로세스 | - ---- - -## 3. 전체 프로세스 — 영업에서 출고까지 - -``` -영업 → 상담 → 견적서 → 수주서 → 작업공정 → 출고 - (01) (02) (03) (04) (05) (06) -``` - -### 3.1 각 단계별 AI 자동화 포인트 - -| 단계 | 구현 기능 | AI 적용 | 상태 | -|------|----------|---------|------| -| 영업관리 | 고객/거래처 CRM | 고객 분류 자동화 | 운영중 | -| 상담/문의 | 상담 이력, 음성 입력 | STT 음성→텍스트 변환 | 운영중 | -| 견적서 | 견적 작성/승인/발송 | AI 견적 자동 산출 | 운영중 (AI 개발중) | -| 수주서 | 견적→수주 연동 | 자동 전환 프로세스 | 운영중 | -| 작업공정 | BOM, 공정 관리 | AI 공정 최적화 | 개발중 | -| 출고/배송 | 출고 지시, 배송 추적 | 물류 자동화 | 계획 | - ---- - -## 4. 80% 공통화론 - -### 4.1 업무 구성 비율 - -``` -공통 업무 ██████████████████████████████████████████ 80% -커스텀 ██████████ 20% -``` - -- **공통 80%**: 영업/CRM, 회계/재무, 인사/근태, 재고관리, 문서/전자결재, 품질관리 -- **커스텀 20%**: 상품 마스터, 견적 계산식, 공정 시퀀스 (업종마다 다른 부분) - -### 4.2 업종별 확장 시나리오 - -| 업종 | 공통 (80%) | 커스텀 (20%) | 난이도 | -|------|-----------|-------------|--------| -| 방화셔터 | 영업, 견적, 수주, 회계, 인사 | 셔터 규격 계산, 설치 공정 | 실증완료 | -| 블라인드 | 영업, 견적, 수주, 회계, 인사 | 원단/슬랫 규격, 재단 공정 | 즉시가능 | -| 금속가공 | 영업, 견적, 수주, 회계, 인사 | 소재/두께 단가표, CNC 공정 | 단기적용 | -| 식품제조 | 영업, 견적, 수주, 회계, 인사 | 레시피 관리, HACCP, 유통기한 | 중기적용 | -| 전자부품 | 영업, 견적, 수주, 회계, 인사 | PCB BOM, SMT 공정, 검사 | 중기적용 | - ---- - -## 5. 멀티테넌시 구조 - -``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ A 기업 │ │ B 기업 │ │ C 기업 │ │ D 기업 │ -│ (경동기업) │ │ (주일기업) │ │ (금속가공) │ │ (식품제조) │ -└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - │ │ │ │ - └─────────────┴──────┬──────┴─────────────┘ - │ - ┌─────────▼─────────┐ - │ SAM 플랫폼 │ - │ │ - │ 공유: 코드 100% │ - │ 격리: 데이터 100% │ - │ (tenant_id 기반) │ - └───────────────────┘ -``` - -### 5.1 핵심 이점 - -| 이점 | 상세 | -|------|------| -| **비용 절감** | 하나의 코드베이스로 N개 기업 서비스. 기업이 늘어도 개발비 동일 | -| **즉시 입주** | 새 기업 추가 = tenant_id 발급 + 기본 설정. 별도 개발 없이 수일 내 사용 | -| **데이터 격리** | 모든 쿼리에 tenant_id 자동 적용. A기업이 B기업 데이터에 접근 불가 | - ---- - -## 6. AI 자동화 현황 & 로드맵 - -### 6.1 구현 완료 - -| 기능 | 상세 | -|------|------| -| **AI 재무 분석** | CEO 대시보드에서 매출/비용/손익 AI 분석. Claude API로 자연어 인사이트 제공 | -| **STT 음성 입력** | 상담 메모, 현장 보고를 음성으로 입력. 자동 텍스트 변환 후 시스템 기록 | -| **Claude Code 개발 자동화** | SAM 시스템 자체를 Claude Code로 개발. 코드 생성, 리뷰, 테스트, 배포 자동화 | - -### 6.2 향후 계획 - -| 기능 | 상세 | -|------|------| -| **AI 견적 자동 생성** | 고객 요구사항 입력 시 과거 데이터 기반으로 최적 견적 자동 산출 | -| **AI 공정 최적화** | 생산 데이터 분석으로 최적 공정 순서, 자재 배치 제안. 불량률 예측 및 사전 경고 | -| **AI 고객 상담** | FAQ 자동 응답, 견적 문의 자동 접수. 사람의 개입이 필요한 경우만 담당자 연결 | - -> "공정의 다양성은 천차만별. 이를 AI와 데이터로 정복하는 것이 SAM의 연구 과제다." - ---- - -## 7. 로드맵 — 4단계 비전 - -| Phase | 제목 | 기간 | 상태 | 핵심 목표 | -|-------|------|------|------|----------| -| **Phase 1** | 코어 실증 | 2025~2026 상반기 | 진행중 | 경동/주일 방화셔터에서 영업→출고 전 프로세스 실증 | -| **Phase 2** | 3~5사 확장 | 2026 하반기 | 계획 | 블라인드, 금속가공 등 유사 제조업 멀티테넌시 확장 | -| **Phase 3** | AI 고도화 | 2027 | 계획 | AI 견적 자동 산출, 공정 최적화, 고객 상담 순차 적용 | -| **Phase 4** | 다산업군 플랫폼 | 2028~ | 비전 | 식품, 전자부품 등 다양한 업종. 중소 제조업 표준 SaaS | - ---- - -## 결론 - -> "방화셔터에서 시작하여, 모든 중소 제조업의 디지털 전환을 이끄는 SAM" - -**AI 자동화 + 멀티테넌시 + 80% 공통화 = 중소 제조업 혁신 플랫폼** - ---- - -## 관련 문서 - -| 문서 | 설명 | -|------|------| -| [SAM 프로젝트 개요](../SAM_PROJECT_OVERVIEW_FOR_AI.md) | 기술적 개요 | -| [스케일링 로드맵](scaling-roadmap.md) | 10,000 테넌트 기술 스케일링 | -| [보안 정책](security-policy.md) | 보안 아키텍처 | - ---- - -**최종 업데이트**: 2026-03-02 diff --git a/sam/docs/system/database/codebridge-separation.md b/sam/docs/system/database/codebridge-separation.md deleted file mode 100644 index 9022cbe..0000000 --- a/sam/docs/system/database/codebridge-separation.md +++ /dev/null @@ -1,443 +0,0 @@ -# codebridge DB 분리 - -> **작성일**: 2026-03-07 -> **상태**: 로컬/개발 서버 적용 완료, **운영 서버 코드 revert 상태 — DB 선행 작업 필요** -> **최종 수정**: 2026-03-09 — API 사용 테이블 점검, 로컬/개발 samdb 삭제 완료, 운영 코드 revert - ---- - -## 1. 개요 - -### 1.1 목적 - -SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리한다. - -- **samdb**: React 서비스가 사용하는 테이블 (수주, 견적, 생산, 거래처 등) -- **codebridge**: MNG(관리자 패널)에서만 사용하는 코드브릿지엑스 내부 관리 테이블 - -### 1.2 핵심 원칙 - -- codebridge DB에 테이블을 복사한 후, samdb에서 해당 테이블을 **삭제**하여 실질적 분리 -- MNG 모델에 `$connection = 'codebridge'`를 설정하여 읽기 대상 DB만 변경 -- React/API 서비스에는 **영향 없음** -- 개발 서버: samdb에서 59개 테이블 삭제 완료 (백업: `/home/pro/backup/sam_backup_20260309.sql.gz`) - -### 1.3 분리 기준 - -| 분류 | 대상 DB | 기준 | -|------|---------|------| -| React 서비스 테이블 | samdb (유지) | React 프론트엔드 또는 API에서 사용 | -| MNG 전용 테이블 | codebridge (이동) | MNG에서만 사용, React/API 미참조 | -| 공통 테이블 | samdb (유지) | 양쪽 모두 사용 (users, tenants 등) | -| **API 사용 테이블** | **samdb (유지 필수)** | **API에 모델/서비스/컨트롤러 존재 — 이동 시 데이터 불일치 발생** | - -> **경고: API에서 모델/서비스/컨트롤러로 참조하는 테이블을 codebridge로 이동하면, API는 samdb에 쓰고 MNG는 codebridge에서 읽게 되어 데이터 불일치가 발생한다. 절대 이동 금지.** - ---- - -## 2. codebridge 테이블 목록 (59개) - -> 2026-03-09 점검: API 프로젝트 전체 코드 조사를 통해 API에서 사용하는 22개 테이블을 제외함. 제외된 테이블은 [3절](#3-api-사용-테이블--samdb-유지-필수-22개) 참조. -> Equipment 하위 테이블 4개 추가 (FK 의존성으로 equipments와 동일 DB 필수). - -### Admin (9) - -| 테이블 | 설명 | -|--------|------| -| `admin_api_flows` | API 플로우 정의 | -| `admin_api_flow_runs` | API 플로우 실행 이력 | -| `admin_pm_daily_logs` | PM 일일 로그 | -| `admin_pm_daily_log_entries` | PM 일일 로그 항목 | -| `admin_pm_issues` | PM 이슈 | -| `admin_pm_projects` | PM 프로젝트 | -| `admin_pm_tasks` | PM 태스크 | -| `admin_roadmap_milestones` | 로드맵 마일스톤 | -| `admin_roadmap_plans` | 로드맵 계획 | - -### DevTools (5) - -| 테이블 | 설명 | 비고 | -|--------|------|------| -| `admin_api_bookmarks` | API 북마크 | 기존명 `api_bookmarks` | -| `admin_api_deprecations` | API 지원종료 관리 | 기존명 `api_deprecations` | -| `admin_api_environments` | API 환경 설정 | 기존명 `api_environments` | -| `admin_api_histories` | API 호출 이력 | 기존명 `api_histories` | -| `admin_api_templates` | API 템플릿 | 기존명 `api_templates` | - -### Sales (17) - -| 테이블 | 설명 | -|--------|------| -| `sales_partners` | 영업 파트너 | -| `sales_managers` | 영업 담당자 | -| `sales_manager_documents` | 영업 담당자 문서 | -| `sales_commissions` | 영업 수당 | -| `sales_commission_details` | 영업 수당 상세 | -| `sales_consultations` | 영업 상담 | -| `sales_contract_products` | 계약 제품 | -| `sales_products` | 영업 제품 | -| `sales_product_categories` | 영업 제품 카테고리 | -| `sales_prospects` | 영업 전망 | -| `sales_prospect_consultations` | 전망 상담 | -| `sales_prospect_products` | 전망 제품 | -| `sales_prospect_scenarios` | 전망 시나리오 | -| `sales_records` | 영업 실적 | -| `sales_scenario_checklists` | 시나리오 체크리스트 | -| `sales_tenant_managements` | 테넌트 영업 관리 | -| `tenant_prospects` | 테넌트 전망 | - -### Finance (9) - -| 테이블 | 설명 | -|--------|------| -| `condolence_expenses` | 경조사비 | -| `consulting_fees` | 컨설팅비 | -| `corporate_cards` | 법인카드 | -| `corporate_card_prepayments` | 법인카드 선결제 | -| `customer_settlements` | 고객 정산 | -| `daily_fund_memos` | 일일 자금 메모 | -| `daily_fund_transactions` | 일일 자금 거래 | -| `incomes` | 수입 | -| `vat_records` | 부가세 기록 | - -### ESign (2) - -| 테이블 | 설명 | -|--------|------| -| `esign_field_templates` | 전자서명 필드 템플릿 | -| `esign_field_template_items` | 전자서명 필드 항목 | - -> esign_contracts, esign_audit_logs, esign_sign_fields, esign_signers는 API에서 전자계약 기능으로 사용 중 → samdb 유지 - -### Equipment (6) - -| 테이블 | 설명 | -|--------|------| -| `equipments` | 설비 | -| `equipment_processes` | 설비 공정 | -| `equipment_inspections` | 설비 점검 (FK → equipments) | -| `equipment_inspection_details` | 설비 점검 상세 (FK → equipment_inspections) | -| `equipment_inspection_templates` | 설비 점검 템플릿 (FK → equipments) | -| `equipment_repairs` | 설비 수리 (FK → equipments) | - -> Equipment 하위 4개 테이블은 `equipments`에 FK 의존하므로 반드시 동일 DB에 있어야 한다. - -### HR (1) - -| 테이블 | 설명 | -|--------|------| -| `business_income_payments` | 사업소득 지급 | - -> income_tax_brackets는 API IncomeTaxBracketSeeder에서 초기 데이터 관리 → samdb 유지 - -### System (1) - -| 테이블 | 설명 | -|--------|------| -| `ai_configs` | AI 설정 | - -> ai_pricing_configs, ai_token_usages는 API 모델에서 직접 사용 → samdb 유지 - -### 기타 (9) - -| 테이블 | 설명 | 비고 | -|--------|------|------| -| `biz_cert` | 사업자등록증 | 문서 기존명 `biz_certs` → 실제 테이블명 (단수) | -| `cm_songs` | R&D 곡 관리 | | -| `construction_site_photos` | 시공 현장 사진 | | -| `construction_site_photo_rows` | 시공 사진 행 | | -| `admin_meeting_logs` | 회의 로그 | 문서 기존명 `meeting_logs` → 실제 테이블명 | -| `meeting_minutes` | 회의록 | | -| `meeting_minute_segments` | 회의록 세그먼트 | | -| `interview_knowledges` | 면접 지식 | | -| `sales_records` | 매출 기록 | | - ---- - -## 3. API 사용 테이블 — samdb 유지 필수 (22개) - -> **경고: 아래 테이블은 API 프로젝트에서 모델/서비스/컨트롤러/시더로 직접 참조한다. 절대 codebridge로 이동 금지.** -> -> **2026-03-09 점검**: sam/api 프로젝트 전체 코드 (모델, 컨트롤러, 서비스, 라우트, 시더) 조사 완료. - -### Barobill (12) — 전체 samdb 유지 - -| 테이블 | API 사용처 | 사유 | -|--------|-----------|------| -| `barobill_billing_records` | BarobillBillingService | 과금 기록 CRUD | -| `barobill_members` | BarobillUsageService | 회원사 사용량 집계 | -| `barobill_monthly_summaries` | BarobillBillingService | 월별 집계 갱신 | -| `barobill_pricing_policies` | BarobillUsageService | 과금 계산 | -| `bank_sync_statuses` | BankSyncStatus 모델 | 동기화 상태 추적 | -| `bank_transactions` | BankTransactionController | 은행 거래 조회/분개 | -| `bank_transaction_overrides` | BankTransactionOverride 모델 | 거래 재정의 | -| `bank_transaction_splits` | BankTransactionController | 은행 거래 분개 | -| `card_transaction_amount_logs` | CardTransactionAmountLog 모델 | 금액 수정 이력 + FK → card_transactions | -| `card_transaction_hides` | CardTransactionHide 모델 | 거래 숨김 + FK → card_transactions | -| `hometax_invoices` | BarobillUsageService | 세금계산서 사용량 집계 | -| `hometax_invoice_journals` | HometaxInvoiceJournal 모델 | 세금계산서 분개 + FK → hometax_invoices | - -> **핵심**: API의 BarobillController, BarobillSettingController, BarobillService, EntertainmentService가 바로빌 테이블을 직접 참조. `barobill_card_transactions` (samdb 유지)와 FK로 연결된 자식 테이블도 분리 불가. - -### ESign (4) — API 전자계약 기능 - -| 테이블 | API 사용처 | 사유 | -|--------|-----------|------| -| `esign_contracts` | EsignContractController, EsignService | 전자계약 CRUD | -| `esign_audit_logs` | EsignService | 감사 추적 기록 | -| `esign_sign_fields` | EsignService | 서명 위치 데이터 | -| `esign_signers` | EsignService | 서명자 정보/인증 | - -### Audit (2) — API 전사 감사 시스템 - -| 테이블 | API 사용처 | 사유 | -|--------|-----------|------| -| `audit_logs` | AuditLog 모델, AuditLogService, AuditRollbackService | 전사 DML 감사 | -| `trigger_audit_logs` | TriggerAuditLog 모델, TriggerAuditLogController, RegenerateAuditTriggers 명령 | DB 트리거 감사 + 파티셔닝 관리 | - -### DevTools (1) - -| 테이블 | API 사용처 | 사유 | -|--------|-----------|------| -| `api_request_logs` | ApiRequestLog 모델, SystemStatService | API 통계 집계 | - -### System (2) - -| 테이블 | API 사용처 | 사유 | -|--------|-----------|------| -| `ai_pricing_configs` | AiPricingConfig 모델 | AI 서비스 비용 계산 (캐시 기반) | -| `ai_token_usages` | AiTokenUsage 모델 | 멀티테넌트 AI 토큰 사용량 추적 | - -### HR (1) - -| 테이블 | API 사용처 | 사유 | -|--------|-----------|------| -| `income_tax_brackets` | IncomeTaxBracketSeeder | 소득세 구간 초기 데이터 관리 | - ---- - -## 4. 적용 현황 - -### 4.1 환경별 상태 - -| 환경 | codebridge DB | 테이블 복사 | samdb 삭제 | .env 설정 | MNG 코드 | 상태 | -|------|:---:|:---:|:---:|:---:|:---:|------| -| **로컬 Docker** | O | 100개 | **58개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 | -| **개발 서버** | O | 101개 | **63개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 | -| **운영 서버** | **X** | **X** | **X** | **X** | **revert됨** | ⚠️ DB 선행 작업 후 코드 재배포 필요 | - -> **2026-03-09 작업 내역**: -> - API 사용 테이블 22개: codebridge 이동 대상에서 제외 → samdb 유지 -> - `finance_*` 17개 + `barobill_companies` 1개: codebridge에 없는 유령 테이블 → samdb에서만 삭제 -> - Equipment 하위 4개 테이블: FK 의존성으로 codebridge 이동 대상에 추가 (55→59개) -> - **개발 서버 samdb에서 63개 테이블 DROP 완료** (59개 + DevTools 실제 테이블명 4개 추가분) -> - **로컬 samdb에서 58개 테이블 DROP 완료** → 로컬/개발 265개로 동기화 -> - 로컬에 `quality_documents` 등 4개 테이블 구조 동기화 (개발서버에서 복사) -> - 백업: `/home/pro/backup/sam_backup_20260309.sql.gz` (6.3MB) -> -> **테이블명 불일치 발견 (수정 완료)**: -> - `api_bookmarks` → 실제: `admin_api_bookmarks` -> - `meeting_logs` → 실제: `admin_meeting_logs` -> - `biz_certs` → 실제: `biz_cert` (단수형) -> - DevTools 4개: `api_deprecations` → `admin_api_deprecations`, `api_environments` → `admin_api_environments`, `api_histories` → `admin_api_histories`, `api_templates` → `admin_api_templates` -> -> **운영 서버 revert 사유 (2026-03-09)**: -> - MNG main에 codebridge 코드 2건 cherry-pick → Jenkins 배포됨 (빌드 #456, #457) -> - 운영 서버에 codebridge DB가 없는 상태에서 코드 배포 → **59개 모델 사용 페이지 오류 발생 위험** -> - kent가 main에서 revert 2건 push → 운영 서버 정상 복구 -> - **교훈: 운영 서버는 반드시 DB 선행 작업(1~2단계) 완료 후 코드 배포(3단계)** - -### 4.2 코드 변경 사항 - -**config/database.php** — `codebridge` connection 추가: - -```php -'codebridge' => [ - 'driver' => 'mysql', - 'host' => env('CODEBRIDGE_DB_HOST', env('DB_HOST', '127.0.0.1')), - 'port' => env('CODEBRIDGE_DB_PORT', env('DB_PORT', '3306')), - 'database' => env('CODEBRIDGE_DB_DATABASE', 'codebridge'), - 'username' => env('CODEBRIDGE_DB_USERNAME', env('DB_USERNAME')), - 'password' => env('CODEBRIDGE_DB_PASSWORD', env('DB_PASSWORD')), - // ... (mysql 기본 설정과 동일) -], -``` - -**.env** — 추가 설정: - -``` -CODEBRIDGE_DB_DATABASE=codebridge -``` - -**MNG 모델** — `$connection` 속성 추가 (codebridge 59개만): - -```php -class SalesPartner extends Model -{ - protected $connection = 'codebridge'; // 추가 - protected $table = 'sales_partners'; - // ... -} -``` - -> **주의**: API 사용 테이블 22개에 해당하는 MNG 모델은 `$connection = 'codebridge'`를 설정하지 않는다. 기본 samdb connection을 사용해야 API와 동일한 데이터를 참조한다. - -### 4.3 samdb 테이블 삭제 절차 (개발 서버 완료) - -> Sales 테이블 그룹은 FK 상호 참조가 있어 `FOREIGN_KEY_CHECKS = 0`으로 일괄 삭제. - -```sql --- FK 체크 비활성화 (Sales, Equipment 등 FK 체인 테이블) -SET FOREIGN_KEY_CHECKS = 0; - --- 59개 테이블 DROP (codebridge에 복제 완료 확인 후) -DROP TABLE IF EXISTS admin_api_flows, admin_api_flow_runs, ...; - -SET FOREIGN_KEY_CHECKS = 1; -``` - -> **롤백**: 백업에서 특정 테이블만 복원 가능 -> ```bash -> gunzip < /home/pro/backup/sam_backup_20260309.sql.gz | mysql -u codebridge -p sam -> ``` - ---- - -## 5. 운영 서버 적용 절차 (미완료) - -> **전제**: 운영 서버 SSH 접근 + DB root 권한 필요 -> **현재 상태**: 운영 서버 main 코드는 revert 상태 (codebridge 코드 없음). DB 작업 완료 후 코드 재배포 필요. -> **⚠️ 교훈**: 2026-03-09에 DB 없이 코드만 배포하여 장애 위험 발생 → **반드시 DB 선행 후 코드 배포** - -### 순서 (반드시 1 → 2 → 3 → 4 → 5 순서로) - -**1단계: 운영 sam DB 백업** - -```bash -# 운영 서버 접속 후 -mysqldump -u codebridge -p'[운영PW]' sam --single-transaction > ~/backup/sam_backup_$(date +%Y%m%d).sql -gzip ~/backup/sam_backup_$(date +%Y%m%d).sql -``` - -**2단계: codebridge DB 생성 + 59개 테이블 복사** - -```bash -# DB 생성 -mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - -# DB 계정 권한 부여 -mysql -u root -p -e "GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; FLUSH PRIVILEGES;" - -# sam에서 59개 테이블 구조+데이터 복사 -mysqldump -u codebridge -p sam \ - admin_api_flows admin_api_flow_runs \ - admin_pm_daily_logs admin_pm_daily_log_entries admin_pm_issues admin_pm_projects admin_pm_tasks \ - admin_roadmap_milestones admin_roadmap_plans \ - admin_api_bookmarks admin_api_deprecations admin_api_environments admin_api_histories admin_api_templates \ - sales_partners sales_managers sales_manager_documents sales_commissions sales_commission_details \ - sales_consultations sales_contract_products sales_products sales_product_categories \ - sales_prospects sales_prospect_consultations sales_prospect_products sales_prospect_scenarios \ - sales_records sales_scenario_checklists sales_tenant_managements tenant_prospects \ - condolence_expenses consulting_fees corporate_cards corporate_card_prepayments \ - customer_settlements daily_fund_memos daily_fund_transactions incomes vat_records \ - esign_field_templates esign_field_template_items \ - equipments equipment_process equipment_inspections equipment_inspection_details \ - equipment_inspection_templates equipment_repairs \ - business_income_payments ai_configs \ - biz_cert cm_songs construction_site_photos construction_site_photo_rows \ - admin_meeting_logs meeting_minutes meeting_minute_segments \ - interview_knowledge \ - | mysql -u codebridge -p codebridge -``` - -**3단계: .env 설정** - -```bash -echo 'CODEBRIDGE_DB_DATABASE=codebridge' >> /home/webservice/mng/.env -cd /home/webservice/mng && php artisan config:clear -``` - -**4단계: MNG 코드 재배포 (main cherry-pick)** - -> develop에 codebridge 코드가 있으므로, revert 커밋 이후 develop의 최신 커밋을 cherry-pick. -> 또는 develop의 해당 커밋을 다시 cherry-pick하여 main에 push. - -```bash -# 로컬에서 실행 -cd /home/aweso/sam/mng -git checkout main && git pull origin main -git cherry-pick -git push origin main -git checkout develop -``` - -**5단계: 동작 확인 + samdb 테이블 삭제 (선택)** - -MNG 관리자 페이지에서 영업관리, 설비, 재무 등 주요 메뉴 동작 확인 후, 문제없으면 sam DB에서 59개 테이블 삭제. - -```sql -SET FOREIGN_KEY_CHECKS = 0; -DROP TABLE IF EXISTS - admin_api_flows, admin_api_flow_runs, - admin_pm_daily_logs, admin_pm_daily_log_entries, admin_pm_issues, admin_pm_projects, admin_pm_tasks, - admin_roadmap_milestones, admin_roadmap_plans, - admin_api_bookmarks, admin_api_deprecations, admin_api_environments, admin_api_histories, admin_api_templates, - sales_partners, sales_managers, sales_manager_documents, sales_commissions, sales_commission_details, - sales_consultations, sales_contract_products, sales_products, sales_product_categories, - sales_prospects, sales_prospect_consultations, sales_prospect_products, sales_prospect_scenarios, - sales_records, sales_scenario_checklists, sales_tenant_managements, tenant_prospects, - condolence_expenses, consulting_fees, corporate_cards, corporate_card_prepayments, - customer_settlements, daily_fund_memos, daily_fund_transactions, incomes, vat_records, - esign_field_templates, esign_field_template_items, - equipments, equipment_process, equipment_inspections, equipment_inspection_details, - equipment_inspection_templates, equipment_repairs, - business_income_payments, ai_configs, - biz_cert, cm_songs, construction_site_photos, construction_site_photo_rows, - admin_meeting_logs, meeting_minutes, meeting_minute_segments, - interview_knowledge; -SET FOREIGN_KEY_CHECKS = 1; -``` - -> **⚠️ 핵심 주의사항**: -> - 반드시 **1→2→3→4** 순서 (DB 먼저, 코드 나중) -> - 4단계(코드 배포) 전에 3단계(.env)까지 완료되어야 함 -> - 5단계(samdb 삭제)는 4단계 동작 확인 후 선택적 수행 - ---- - -## 6. 아키텍처 다이어그램 - -``` - React (사용자) - | - API 서버 (Laravel) - | - ┌─────┴─────┐ - | | - samdb sam_stat - (서비스 DB) (통계 DB) - | - | (공통 + API 사용 테이블: users, tenants, barobill_*, esign_*, audit_* 등) - | - MNG (관리자) - | - ┌─────┴─────┐ - | | - samdb codebridge - (공통 참조) (MNG 전용 59개) -``` - -- **React → API → samdb**: 서비스 트래픽 (수주, 견적, 생산, 바로빌, 전자서명 등) -- **MNG → samdb**: 공통 테이블 (users, tenants, menus) + API 사용 테이블 22개 참조 -- **MNG → codebridge**: MNG 전용 데이터 (영업관리, 재무, 설비, PM 도구 등) - ---- - -## 관련 문서 - -- [database/README.md](README.md) — DB 스키마 전체 현황 -- [codebridge-db-separation-plan.md](/home/aweso/sam/docs/plans/codebridge-db-separation-plan.md) — 분리 작업 계획서 (plans/) - ---- - -**최종 업데이트**: 2026-03-09 (운영 revert 반영, 적용 절차 5단계로 개정) From 7a969b9d57c38e7e94fdb08eafc16706d48fb85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Mar 2026 22:53:07 +0900 Subject: [PATCH 11/15] =?UTF-8?q?refactor:=20[structure]=20sam/=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EB=AC=B8=EC=84=9C=EB=A5=BC=20docs=20?= =?UTF-8?q?=EB=A3=A8=ED=8A=B8=EB=A1=9C=20=EC=9E=AC=EB=B0=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .gitignore를 sam/ 기반에서 루트 기반으로 변경 - sam/docs/ 하위 문서를 루트로 이동 (contracts, features, guides, plans 등) - sam/ 폴더 삭제 (docker, coocon 포함) --- .gitignore | 48 +- changes/20260303_gemini_model_upgrade.md | 119 +++ .../20260304_eaccount_infinite_loop_fix.md | 165 +++ ...0260306_purchase_request_payment_method.md | 0 contracts/CHANGELOG.md | 42 + ...고객_서비스이용계약서_v4_0_전자서명용.docx | Bin 0 -> 39346 bytes contracts/docx/비밀유지서약서.docx | Bin 0 -> 29026 bytes .../docx/영업파트너 위촉계약서(단체용).docx | Bin 0 -> 25013 bytes contracts/docx/영업파트너 위촉계약서.docx | Bin 0 -> 33340 bytes contracts/markdown/01-service-agreement.md | 458 ++++++++ contracts/markdown/02-nda.md | 199 ++++ contracts/markdown/03-partner-agreement.md | 276 +++++ .../markdown/04-partner-agreement-group.md | 267 +++++ contracts/revisions.json | 58 + contracts/scripts/extract_to_markdown.py | 334 ++++++ contracts/scripts/sync_check.py | 263 +++++ data/interview-master-questions.sql | 279 +++++ dev/dev_plans/qms-api-integration-plan.md | 316 ++++++ .../academy/fire-shutter-image-prompts.md | 369 +++++++ features/approvals/README.md | 298 ++++++ features/approvals/api-reference.md | 594 +++++++++++ .../approvals/db-changes-and-model-sync.md | 286 +++++ features/approvals/form-types.md | 999 ++++++++++++++++++ features/approvals/ui-screens.md | 381 +++++++ features/approvals/workflows.md | 565 ++++++++++ .../esign-notification-guide.md | 250 +++++ features/business-card-request.md | 173 +++ features/credit-evaluation/README.md | 284 +++++ features/documents/mng-document-system.md | 738 +++++++++++++ features/documents/mng-document-template.md | 826 +++++++++++++++ features/planning/README.md | 129 +++ features/planning/construction-photos.md | 275 +++++ features/planning/meeting-minutes.md | 456 ++++++++ features/planning/planning-views.md | 222 ++++ features/rd/README.md | 110 ++ features/rd/design-insight.md | 246 +++++ features/rd/planning-design.md | 366 +++++++ .../rd/sound-logo-studio.md | 0 guides/ai-config-settings.md | 325 ++++++ guides/ai-management.md | 291 +++++ guides/ai-model-update-workflow.md | 313 ++++++ guides/pptx-generation-guide.md | 387 +++++++ guides/server-how-it-works.md | 247 +++++ guides/table-design-guide.md | 486 +++++++++ plans/SAM_General_Rule_Storyboard_D1.0.md | 737 +++++++++++++ plans/ai-quotation-engine-plan.md | 928 ++++++++++++++++ plans/attendance-management-plan.md | 284 +++++ plans/block-builder-evolution-plan.md | 706 +++++++++++++ plans/design-insight-menu-plan.md | 611 +++++++++++ plans/fire-shutter-drawing-generator-plan.md | 753 +++++++++++++ plans/sound-logo-generator-plan.md | 637 +++++++++++ projects/org-chart/README.md | 317 ++++++ projects/planning-design/README.md | 157 +++ rules/slides/usage-plan/SAM_활용방안.pptx | Bin 0 -> 279940 bytes rules/slides/usage-plan/convert.cjs | 31 + rules/slides/usage-plan/slide-01.html | 42 + rules/slides/usage-plan/slide-02.html | 58 + rules/slides/usage-plan/slide-03.html | 108 ++ rules/slides/usage-plan/slide-04.html | 88 ++ rules/slides/usage-plan/slide-05.html | 74 ++ rules/slides/usage-plan/slide-06.html | 68 ++ rules/slides/usage-plan/slide-07.html | 82 ++ system/ai-automation-vision.md | 174 +++ system/database/codebridge-separation.md | 443 ++++++++ 64 files changed, 18723 insertions(+), 15 deletions(-) create mode 100644 changes/20260303_gemini_model_upgrade.md create mode 100644 changes/20260304_eaccount_infinite_loop_fix.md rename {sam/docs/dev/changes => changes}/20260306_purchase_request_payment_method.md (100%) create mode 100644 contracts/CHANGELOG.md create mode 100644 contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx create mode 100755 contracts/docx/비밀유지서약서.docx create mode 100755 contracts/docx/영업파트너 위촉계약서(단체용).docx create mode 100755 contracts/docx/영업파트너 위촉계약서.docx create mode 100644 contracts/markdown/01-service-agreement.md create mode 100644 contracts/markdown/02-nda.md create mode 100644 contracts/markdown/03-partner-agreement.md create mode 100644 contracts/markdown/04-partner-agreement-group.md create mode 100644 contracts/revisions.json create mode 100644 contracts/scripts/extract_to_markdown.py create mode 100644 contracts/scripts/sync_check.py create mode 100644 data/interview-master-questions.sql create mode 100644 dev/dev_plans/qms-api-integration-plan.md create mode 100644 features/academy/fire-shutter-image-prompts.md create mode 100644 features/approvals/README.md create mode 100644 features/approvals/api-reference.md create mode 100644 features/approvals/db-changes-and-model-sync.md create mode 100644 features/approvals/form-types.md create mode 100644 features/approvals/ui-screens.md create mode 100644 features/approvals/workflows.md create mode 100644 features/barobill-kakaotalk/esign-notification-guide.md create mode 100644 features/business-card-request.md create mode 100644 features/credit-evaluation/README.md create mode 100644 features/documents/mng-document-system.md create mode 100644 features/documents/mng-document-template.md create mode 100644 features/planning/README.md create mode 100644 features/planning/construction-photos.md create mode 100644 features/planning/meeting-minutes.md create mode 100644 features/planning/planning-views.md create mode 100644 features/rd/README.md create mode 100644 features/rd/design-insight.md create mode 100644 features/rd/planning-design.md rename {sam/docs/features => features}/rd/sound-logo-studio.md (100%) create mode 100644 guides/ai-config-settings.md create mode 100644 guides/ai-management.md create mode 100644 guides/ai-model-update-workflow.md create mode 100644 guides/pptx-generation-guide.md create mode 100644 guides/server-how-it-works.md create mode 100644 guides/table-design-guide.md create mode 100644 plans/SAM_General_Rule_Storyboard_D1.0.md create mode 100644 plans/ai-quotation-engine-plan.md create mode 100644 plans/attendance-management-plan.md create mode 100644 plans/block-builder-evolution-plan.md create mode 100644 plans/design-insight-menu-plan.md create mode 100644 plans/fire-shutter-drawing-generator-plan.md create mode 100644 plans/sound-logo-generator-plan.md create mode 100644 projects/org-chart/README.md create mode 100644 projects/planning-design/README.md create mode 100644 rules/slides/usage-plan/SAM_활용방안.pptx create mode 100644 rules/slides/usage-plan/convert.cjs create mode 100644 rules/slides/usage-plan/slide-01.html create mode 100644 rules/slides/usage-plan/slide-02.html create mode 100644 rules/slides/usage-plan/slide-03.html create mode 100644 rules/slides/usage-plan/slide-04.html create mode 100644 rules/slides/usage-plan/slide-05.html create mode 100644 rules/slides/usage-plan/slide-06.html create mode 100644 rules/slides/usage-plan/slide-07.html create mode 100644 system/ai-automation-vision.md create mode 100644 system/database/codebridge-separation.md diff --git a/.gitignore b/.gitignore index 9109c08..bbbc6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # 추적할 파일만 허용 !.gitignore !CLAUDE.md +!INDEX.md +!README.md +!resources.md # .claude 폴더 - 스킬/에이전트는 추적 !.claude/ @@ -14,22 +17,37 @@ !.claude/agents/ !.claude/agents/** -# sam 문서 -!sam/ -sam/* -!sam/docs/ -!sam/docs/** -sam/docs/contracts/docx/backup/ - -# sam 배포/운영 문서 -!sam/deploys/ -!sam/deploys/** -!sam/front/ -!sam/front/** -!sam/projects/ -!sam/projects/** +# 문서 폴더 (루트 기준) +!assets/ +!assets/** +!brochure/ +!brochure/** +!changes/ +!changes/** +!contracts/ +!contracts/** +contracts/docx/backup/ +!data/ +!data/** +!dev/ +!dev/** +!features/ +!features/** +!frontend/ +!frontend/** +!guides/ +!guides/** +!plans/ +!plans/** +!projects/ +!projects/** +!requests/ +!requests/** +!rules/ +!rules/** +!system/ +!system/** # 기타 -sam/sales .DS_Store _to_notion/ diff --git a/changes/20260303_gemini_model_upgrade.md b/changes/20260303_gemini_model_upgrade.md new file mode 100644 index 0000000..e3806fc --- /dev/null +++ b/changes/20260303_gemini_model_upgrade.md @@ -0,0 +1,119 @@ +# Gemini 모델 업그레이드: 2.0-flash → 2.5-flash + +**날짜:** 2026-03-03 +**작업자:** Claude Code + +--- + +## 변경 개요 + +Google이 2026년 6월 1일부로 Gemini 2.0 Flash 모델 서비스를 종료한다는 통보를 받아, SAM 시스템 전체의 Gemini 모델을 `gemini-2.0-flash` → `gemini-2.5-flash`로 마이그레이션했다. + +--- + +## 변경 사유 + +- Google의 공식 메일 통보: Gemini 2.0 Flash / 2.0 Flash-Lite → 2026-06-01 강제 종료 +- 마이그레이션 경로: `gemini-2.0-flash` → `gemini-2.5-flash` +- API 키, Base URL 변경 없음 (모델명만 변경) + +--- + +## 수정된 파일 + +### API 프로젝트 (`/home/aweso/sam/api`) + +| 파일 | 변경 내용 | +|------|----------| +| `.env` | `GEMINI_MODEL=gemini-2.0-flash` → `gemini-2.5-flash` | +| `config/services.php` | fallback 기본값 `gemini-2.0-flash` → `gemini-2.5-flash` | +| `app/Services/AiReportService.php` | fallback 기본값 변경 | + +### MNG 프로젝트 (`/home/aweso/sam/mng`) + +| 파일 | 변경 내용 | +|------|----------| +| `.env` | `GEMINI_MODEL=gemini-2.0-flash` → `gemini-2.5-flash` | +| `config/services.php` | fallback 기본값 변경 | +| `app/Models/System/AiConfig.php` | `DEFAULT_MODELS['gemini']` 상수 + `getActiveGemini()` fallback 변경 | +| `app/Services/NotionService.php` | fallback 기본값 변경 | +| `resources/views/system/ai-config/index.blade.php` | UI placeholder, 기본값, JS defaultModels 변경 | +| `resources/views/google-cloud/ai-guide/index.blade.php` | 서비스 현황 테이블 모델명 7곳 변경 | +| `resources/views/academy/env-management.blade.php` | 환경변수 예시 테이블 변경 | + +### 문서 (`/home/aweso/sam/docs`) + +| 파일 | 변경 내용 | +|------|----------| +| `guides/ai-config-settings.md` | 기본 모델명 업데이트, 최종 업데이트 날짜 변경 | +| `guides/ai-management.md` | **신규** — AI 관리 종합 가이드 (아키텍처, 버전 이력, 온보딩) | +| `guides/ai-model-update-workflow.md` | **신규** — 모델 업데이트 표준 절차 (7단계 워크플로우) | +| `changes/20260303_gemini_model_upgrade.md` | **신규** — 이 변경 이력 문서 | + +### 수정하지 않은 파일 (의도적) + +| 파일 | 이유 | +|------|------| +| `api/database/migrations/2026_01_27_*.php` | 이미 실행된 마이그레이션 — 변경 시 DB 무결성 문제 | +| `api/database/migrations/2026_02_07_*.php` | 동일 | +| `api/database/migrations/2026_02_09_*.php` | 동일 | +| `mng/views/google-cloud/cloud-api-pricing/index.blade.php` | `2.0 → 2.5` 마이그레이션 안내 UI — 이전 모델명이 의도적 잔존 | + +--- + +## 서버 .env 수정 필요 (배포 후) + +| 환경 | 파일 | 변수 | 담당 | +|------|------|------|------| +| 개발서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 | +| 개발서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 | +| 운영서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 | +| 운영서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 | + +수정 후 반드시 실행: +```bash +php artisan config:clear +``` + +--- + +## DB 단가 설정 필요 + +MNG `/system/ai-token-usage` → 단가 설정에서: +- 기존 `gemini-2.0-flash` 단가 → 비활성화 +- 신규 `gemini-2.5-flash` 단가 추가: + - `input_price_per_million`: 0.15 + - `output_price_per_million`: 0.60 + - `exchange_rate`: 현재 환율 + +--- + +## 테스트 체크리스트 + +- [x] 로컬 .env 수정 완료 +- [x] 코드 fallback 전체 변경 완료 +- [ ] 로컬 연결 테스트 (MNG `/system/ai-config`) +- [ ] 개발서버 .env 수정 + config:clear +- [ ] 개발서버 연결 테스트 +- [ ] 운영서버 .env 수정 + config:clear +- [ ] DB 단가 설정 (gemini-2.5-flash) +- [ ] 토큰 사용량 로그 확인 (새 모델명) + +--- + +## 롤백 절차 + +문제 발생 시 `.env`만 되돌리면 즉시 복구: +```bash +# 모든 환경의 .env에서 +GEMINI_MODEL=gemini-2.0-flash +php artisan config:clear +``` + +--- + +## 관련 문서 + +- [AI 관리 종합 가이드](../guides/ai-management.md) +- [모델 업데이트 워크플로우](../guides/ai-model-update-workflow.md) +- [AI 설정 기술문서](../guides/ai-config-settings.md) diff --git a/changes/20260304_eaccount_infinite_loop_fix.md b/changes/20260304_eaccount_infinite_loop_fix.md new file mode 100644 index 0000000..e754f9f --- /dev/null +++ b/changes/20260304_eaccount_infinite_loop_fix.md @@ -0,0 +1,165 @@ +# 계좌 입출금내역 부분 월 조회 시 무한루프 크래시 수정 + +**날짜:** 2026-03-04 +**작업자:** Claude Code + +--- + +## 변경 개요 + +계좌 입출금내역 페이지에서 **날짜를 수동 입력**하여 조회 시 500 에러가 발생하는 문제를 수정했다. +편의 버튼(이번달, 지난달 등)은 항상 전체 월(1일~말일)을 사용하여 문제가 없었으나, +수동으로 날짜를 입력하면 **부분 월**(예: 12/01~12/18)이 되어 무한루프가 발생했다. + +--- + +## 근본 원인 + +### `splitDateRangeMonthly()` 함수의 cursor 이동 버그 + +긴 기간 조회 시 바로빌 SOAP API의 한계로 인해 기간을 **월별 청크**로 분할하는 함수에서, +endDate가 **월 중간**일 때 cursor가 **같은 달 1일로 되돌아가** 무한루프가 발생했다. + +```php +// ❌ 버그 코드 — endDate가 월 중간이면 무한루프 +$cursor = $chunkEnd->copy()->addDay()->startOfMonth(); + +// 예시: endDate = 20251218 +// chunkEnd = 20251218 +// → addDay() = 20251219 +// → startOfMonth() = 20251201 ← 같은 달 1일로 되돌아감! +// → while($cursor <= $end) 조건 여전히 true → 무한 반복 +``` + +```php +// ✅ 수정 코드 — chunkStart 기준으로 다음 월로 이동 +$cursor = $chunkStart->copy()->addMonth()->startOfMonth(); + +// 예시: startDate = 20251201 +// chunkStart = 20251201 +// → addMonth() = 20260101 +// → startOfMonth() = 20260101 ← 다음 달로 정상 이동 +// → while($cursor <= $end) 조건 false → 루프 종료 +``` + +### 재현 조건 + +| 조건 | 결과 | +|------|------| +| 전체 월 (12/01~12/31) | 정상 — `addDay()` = 01/01 → `startOfMonth()` = 01/01 | +| 부분 월 (12/01~12/18) | **무한루프** — `addDay()` = 12/19 → `startOfMonth()` = 12/01 | +| 다중 월 (12/01~02/18) | **무한루프** — 마지막 월이 부분 월이면 동일 증상 | + +### 증상 + +- PHP 프로세스가 메모리 한도(256M/512M)에 도달하여 **Fatal Error로 크래시** +- Laravel 로그에 에러 기록 없음 (try-catch 밖에서 프로세스가 종료) +- 프론트엔드에 `서버 응답 오류 (500):` (빈 응답 본문) + +--- + +## 수정된 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `app/Http/Controllers/Barobill/EaccountController.php` | `splitDateRangeMonthly()` cursor 이동 로직 수정 | + +--- + +## 검증 결과 + +tinker에서 수정 전후 비교 테스트: + +``` +=== 수정 전 (버그): 20251201~20251218 === +→ 같은 청크 무한 반복 (10회 제한으로 강제 중단) + +=== 수정 후: 20251201~20251218 === +→ [{start: 20251201, end: 20251218}] ← 1개 청크, 정상 + +=== 수정 후: 20251201~20260218 (다중 월) === +→ [{20251201~20251231}, {20260101~20260131}, {20260201~20260218}] ← 3개 청크, 정상 + +=== 수정 후: 20251215~20251231 === +→ [{start: 20251215, end: 20251231}] ← 1개 청크, 정상 +``` + +--- + +## 동일 패턴 코드베이스 점검 결과 + +`sam/mng` 전체를 검색하여 유사 패턴을 점검했다: + +| 파일 | 함수 | 패턴 | 위험도 | +|------|------|------|--------| +| `EaccountController.php` | `splitDateRangeMonthly()` | 월별 청크 분할 | ✅ 수정 완료 | +| `DashboardStatService.php` | `generateDateRange()` | `addDay()` 단순 증가 | 안전 | +| `InspectionCycle.php` | `getHolidayDates()` | `addDay()` 단순 증가 | 안전 | +| `CorporateCardController.php` | `getNextBusinessDay()` | `addDay()` 단순 증가 | 안전 | +| `PartitionManagementService.php` | `addPartitions()` | `for` 루프 (고정 횟수) | 안전 | + +> **결론**: `EaccountController` 외에 동일 버그 패턴 없음. +> 다른 코드들은 모두 `addDay()` 단순 증가 패턴을 사용하여 무한루프 위험 없음. + +--- + +## 교훈 및 방지 규칙 + +### R1. 날짜 cursor 이동 시 `chunkEnd` 기반 이동 금지 + +```php +// ❌ 위험: chunkEnd가 월 중간이면 startOfMonth()가 같은 달로 되돌림 +$cursor = $chunkEnd->copy()->addDay()->startOfMonth(); + +// ✅ 안전: chunkStart 기준으로 항상 다음 월로 이동 +$cursor = $chunkStart->copy()->addMonth()->startOfMonth(); +``` + +### R2. 날짜 루프에 안전장치(max iterations) 추가 권장 + +```php +$maxIterations = 120; // 10년 = 120개월 +$iterations = 0; + +while ($cursor->lte($end) && $iterations < $maxIterations) { + // ... 청크 처리 ... + $iterations++; +} + +if ($iterations >= $maxIterations) { + Log::error('날짜 분할 루프 안전장치 작동', compact('startDate', 'endDate')); +} +``` + +### R3. 부분 월 테스트 필수 + +날짜 범위를 분할하는 코드 작성/수정 시 반드시 다음 케이스를 테스트: + +- [ ] 전체 월 (01일~말일) +- [ ] 부분 월 — 시작 (01일~중간) +- [ ] 부분 월 — 끝 (중간~말일) +- [ ] 다중 월 (마지막 월이 부분 월) +- [ ] 같은 날 (시작일 = 종료일) + +--- + +## 부수 개선 사항 + +이 문제 조사 과정에서 추가로 발견/수정된 항목: + +| 항목 | 내용 | +|------|------| +| WSDL 캐싱 | `WSDL_CACHE_NONE` → `WSDL_CACHE_BOTH` (4개 바로빌 컨트롤러 전체) | +| 소켓 타임아웃 | `default_socket_timeout` 60→120초 연장 | +| Shutdown handler | PHP Fatal Error 감지 시 Laravel 로그에 기록 | +| SOAP 호출 로깅 | 호출 시작/완료 시간 + 소요시간(ms) 기록 | + +--- + +## 관련 문서 + +- `app/Http/Controllers/Barobill/EaccountController.php` — 바로빌 계좌 입출금내역 + +--- + +**최종 업데이트**: 2026-03-04 diff --git a/sam/docs/dev/changes/20260306_purchase_request_payment_method.md b/changes/20260306_purchase_request_payment_method.md similarity index 100% rename from sam/docs/dev/changes/20260306_purchase_request_payment_method.md rename to changes/20260306_purchase_request_payment_method.md diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md new file mode 100644 index 0000000..cfd3346 --- /dev/null +++ b/contracts/CHANGELOG.md @@ -0,0 +1,42 @@ +# 계약서 개정이력 + +> **작성일**: 2026-02-22 +> **관리 대상**: 전자계약 DOCX 4종 + +--- + +## v4.1 (2026-02-22) + +**작성자**: 개발팀 +**대상**: 고객사 서비스 이용계약서 + +- 제4조에 사용량 기반 추가 과금 조항(4.5) 추가 + - 파일 저장 공간: 기본 100GB 초과 시 100GB당 100,000원/월 + - AI 토큰: 월 100만 토큰 기본, 초과 시 1,000토큰 단위 실비 과금 +- 제4조에 바로빌 부가 서비스 요금 조항(4.6) 추가 + - 계좌조회, 카드내역, 세금계산서 발행 요금 명시 + - 홈택스 매입/매출 조회는 회사 부담 명시 + +--- + +## v4.0 (2026-02-22) + +**작성자**: 개발팀 + +- 계약서 버전 관리 시스템 도입 +- DOCX → Markdown 미러링 체계 구축 +- 4개 전자계약 문서에 개정이력 테이블 삽입 +- 동기화 검증 스크립트 구축 + +### 대상 문서 + +| 파일 | 문서명 | +|------|--------| +| `01_고객_서비스이용계약서_v4_0_전자서명용.docx` | 고객사 서비스 이용계약서 | +| `비밀유지서약서.docx` | 비밀유지서약서 (NDA) | +| `영업파트너 위촉계약서.docx` | 영업파트너 위촉계약서 | +| `영업파트너 위촉계약서(단체용).docx` | 영업파트너 위촉계약서 (단체용) | + +--- + +**최종 업데이트**: 2026-02-22 (v4.1) diff --git a/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx b/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx new file mode 100644 index 0000000000000000000000000000000000000000..de1e3ed35ccfea3c3b4b0b6e91664c1f4240caa4 GIT binary patch literal 39346 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

J( zeXu-s(gpZ?%_lJcjX#e}S-A0wTx$ePp``2E;38N{`%vYdb6Pn^GZT0T_A z5m7pGpYfUggc0;nvSXjk1-Y_h&Yu@HmdnH{5nxIMHahzrV6OfoIN}^tx#rGOBYR_j+8&9&8R-lzdMid(sbuI1)Z{4QZrPa4ip&EgHgCY*J90zrjb(gZ47vt4co3})`)nhayq za9-)46)9H1Uth|behq>A`+yhU=U2T^{g0rKIxpMCwy&UW-;m4JS*Sd8eLEQRCs2Fi zs5u|tr>${A1I(zKaaFxw#&@Cjb!E4(SnE)~ly|ql(sKSWWhMbqq&K3%8Qu z?-RvpFwbb1ZK=!mzP{V@^*r7q02KY2J^u1|%MN4I%zA$qZmF|mp4^bEmgB#llD%xEb88<;^XVvl8~XIVn3zI-s@v5;Dct{Xzq5TLcU zy+X|{exB1-|18IPZ|Ju79=P1gX4UERdbiz%;`vkse0}N#uJ>O4>ioPOS*po{W8ZpR zzk+-7TzyCQj+{7~lST$h!578}KP=+P0}+{_SS)wKgkTJJp=Kr{Iuh znCZv@5!r`)NlR~NzsAby+O#0MY3MA?Ji5h>{*~`zs%cp_t8*2TuQ>bhIx#65mO1V8 zbk!A}Ik`NOs5&Ep-%=qpyIY%asWJ`X9Ug_7{GCo36E!M}$Sf_w&%z_%(w*BkM4ugh zT4;|-rqBl7>3|Qsq6U*X{t0Ol0tEmCa*95RqESr(Y*F!I~;?G1#|= zGL7Vy8Ogn7s~nh79kWx^%qBxcOvDT-{)-0ms2OHgJ@qY6K*W>r5&4co&q7;^nVdgO zU}7;|^suCbg7ETk3z{W9N}Zo5 z@)+XmxroV&FmrV}V4d!AK9Wy01p?!4=IFt&=@`t0Wo7}i^q}G}l58B%i`?d<1!Pig zR4HmASEMLP_#1$3-^NIV8n;Blc^>am_dxim5ebMOJ4O2v=eapSqi2kFzeB}M^)Brb z@IWzjl$u>cgJWK4Bm+#;+zDUY!(AFQRxdt78=oIRAl4O1<(U$#1*9mA-XB_Qs7(oN zuzVN4zT-dlCA4PL&tx8ngZ$*J{4eP?J=3oyy*m^~T$DN#(Up}96e)xmO<@j~A&bi1 z1FJ#IsKH5OEKhqL=KXxzI^DPr!PD84hX zTDrN2qy~hCst|gl>l;}GN~TEpgxP|?)5#N#?Dd`WLCh_y3U+;gO9=#5SJZNYLo2ltxpjAUzLb*M7ApE^|ZnbK|Koa~xi&mEh;K#699S7s+}npVk9 z%pk9V7hVbeOC_~S)REAdd)FFszrnT1FNz=;>xoHw%f`{wq`YOm(`%PGBOfX+0lrJp z0ht8TginA&>|EH4N@;+E7uy@@m^yX{M-Vg$WSSjWM;ncw?KG03t^iF zyIk1_u}8X6s=NXMnQGqI(+e8ZQs}RA_8g3H+sCyb$BWp-ASoh(evSIdi)|YX-gL@b z=ht9c64bI)7L_SGyVhNbqPFEXVo}Ksiq`~tCgGOzlLodhBvlYtsu&6lq!28?mJMY0 zTN9IF?=0|$PG=w4P4Pxa5Ts>r#k=;x@oi%Ts3-LGh&-`d|HgO?%xWCSv-_VU5 z7qZ!DF>n7`ED~Wo@g5i6z*m#hjV0_~GV*S~BDTPDq*zvW(*!=PQ-dhSrKa_MWq=cOporcP8$F@KLo0x8HgG0>YWzg`vVG zEqsG@l-I@pQ}5{hGHAZg5uZMSvyw@Jr>R~Ut$jC4O7~ar56y2Xri}Wp+1SVD_)YC9 zy{jH)8S?-KPg{5#LcGU?pa9LsON*27eC?lb__Ai}NX4-cjAgE1djP2%sWyWSP|9QJ zVeekFX1}Y%GM676q;6fr(8?e3F-9q+Dd&z`G9a-?eVJ5R^PLzi)&nRXs^vI8I$8_Y@^Tu(ssA2Q>Ck^jG_o=!Q~5Wr!VN||@;J723Ga^f zBan1gm}z5RaLcTbbE}WACsIMQ#4DeU0j1c!ga) zsZ#)?fnoO^H9tl%N7Lk8+KrS0G=BMM{L0Oqw}m%G_COV3J25{JMyzgdkZJJn38!f! zmjUmX?^dD2R;s44WMAH7UzY97wLeA&!%0bjO4aS;-QY`_{?*@r`Gg%UllAB{cq;at zJKGVjVDdNiY2UM<=QHN>eiDRQfV%*7_%PoWML#s$T05yAYDD${@tzp;QlB`DaZUE; zPa#QV)Z+aTzX37=ommr#Hbi_da;m5JuVhmF5^2c9Wx_>5VRZwT$kIpua&ZG zWgG4y`?v$O$kA#w)*p}-saG#^%(TZ!*RS4QtJD~{$}xYX zlqh2qwqUGzIN29S=ADqTC0^n;Bg;uhG2WSAqf}+^;aMF?g`l4o`ATuo?#%=U7cavH z&(?YQE|qvcmVh;0j&Eg?_*J{kd3CtImV$A4?%`WiQF+b@01?!IDyd*!NC!h4?8+LW z&62Bd_**F3ClvwBX=W#CqQ0FOu0XDemM;3Y7PEKe! zQzV`IXvIs(?`OV&9P69B6gW3@)_fY>uE1kI&)37qnHI~umNX3RPU4@z{9p0p(LxCj zfiL~Vk=f>~3JrR_-N&hp92VouC z4EHwYznbdLbhM-;yho%)uRR@rFj)}11OX7!#+0A@31a(`J%f;HVcX=3`xsm882)k-hQd(6Z;z@33Glbqr zld87ZP<>%@MC=v3m&piakmfSe4c!n5j}&-mIik^rT__@QRFoJxY7xc z8Hgxhfqr8WhghK+=&2igiNtvy$HUYiA~8?W0db7!eCv||KCmuj(PjjV9RfLJf{F5Q zt;Y`>#OG})48gV9EvC?yIgrv$Od(Kfek8^Kii;&$4nZ4Fp|Di>c^Dp7nwSe?p90nz ztt8jP+}|V>Se*2Y-BDRaX*uR9XdidzsCoHFnup>l>$rAHnJZc4&zzaf;|eNbdN@U% zhNDETp9k@KQDm%HVed$C{pqcUJ*gnnE1 zzq>I(&O}rETP~_>pu{S{(G*mn!s2K`s3Er0U|7mzJmu*mUJ_9-i$OJ9mVo}W@i8KIWG*FKBa%2tj8H1c;W)`uJsgYw+W}caAsD-)DGzj@p+D?GBb|sf35w_< zJwJxkVaqb(daZmMI})vtJltx7^?o{+xkgtnJdA-fEr0 z00-FA?mgYX*x^ct{?t{eH6bOev}YxfxalvSYbxV8^@uc+DTF1JSKv1GfMIr*pC1o2 z6Xx)MQIo3#C6~Ni750eCY{wTt4(%{GqA}Fq)jL!*+w_mGbsh$;6Q>u`QlXBGo!mtj zL4?Do6js3MR)44%1ykYh+cVLTyL;uE;ui2wv)TytKB-u@5=yxY>Np2kNi^O}udaUM7 zO5I>5gmIDITv4YN=&QVM@N+lJtb!N_e4*vKIl#%+E5ytiAu)lAaKQ8AllZFU=?(w) zzlQ?a({8Deqq?2P&c#aA)2HNG;F0_bG#-hmOX65}^JTGgp z+EZdr)uVl$ppH91$31CRx-%Bo`x3ro@5w-jBi7}kU!GnWj9FGZ|`nkOTo`40_n+$Np=p4uYuTblIzuZd z{Zs1{6c+#O44I$&w==Z!hhozAT7cVgf9)`>N_A)3(EWH-z8`LHZ-U#{`0Q-{VWWli zK0hZLkZTCgzccv1_7L~1gHhmG5=MbB|N4D~O9-#^g3=-CA;Oh>i{8y**YPhH%=)i-P?&ZWSHLD`0`}y`S_jb&-Y7O1K3(} zV*Ro9`#E?iY)?_v1HaoU(H1J$Z$f~%3a`SK`1!XDHrqZU7)^dv7krHEreMz=7rh#$ z_O)hU0FXyDJBPi?2mC3D#Pk?dHEQm*XOlS`oJuMhY~5_-2Uz=43`JVZuB|DhG&+w8 z+(oAqQ6C;3w>Sy&N{OrH8?w43gxnLRjs2c(oa>C%jQjkk6OHOh{wls1RJUKwa6|FwSx9Aud=+xLA?& zm(txqQGdd&ERvs;hr^Zef=iRJqL4Qb5 zPU8uoPEMd^FNg$r%@f$r%p!w~LAyo(8JxMTrWw-~XUUj0Z7$L!ujo5IaZ4oH%x2u1MuUId8J`D! zvM$E)F8)?l>+_Vr#!<>K(JWaG)^y`#JZJBEP6~etz@Vapz)KkB|(;>Q`%RaJ;UJl4AwRPw#e0PN6obVE!EkD7F&%+h|Azqdpo*bAR{l>$zq zuHVXwu^x_{k0z}^4x6>ydTSlrDcw4a5mRQe4ztLVY+V~&l$+bqg8-VsS-9_gRv9a2 z4>BWmP?}0aj3=W*qpFIsAWc;&x&|sw{bbzmRC^WyTtM~seUKz3Ax~@Qb*gWWv2(%K zl20Bq2a8VRrx0zdEah5q*9D9efSkEM3G}+>U0e z)T~A?$woMo8&I>ng}3M#{bgV_Q&1R55BXQEFx5J5;?gkJWW2`l`xS8kjn93RE7w-s z=Xzxdh(QD?OEINx$yqkBdA(*m$Hh?LM1%g@Tqz3^)|^fujHO>rI|mP4rtBs9TBt_F zC0aThWXM$2Mh7Q(NY5R;jjKsG#%9>NV*;t9+dq*z#3Z-?@BRxisb{^Ti*t5Q~FIlvQ!Y)4f5+;W~Q=Vx3bw7;DQS$oHtxqnGn0l&=#d{Yk! z?^eh1DT&UJ1u7!yu5GL5bd%=k%3gR6yNS>j%Zg26+?e2^TsiQZD?<8=)VdpMyIM4yO-B#N>`uInhzXaU2B)9>$bEmwozcyUd>G_NOmv z-W1X*=7W6410aO8iGHB{2v%d-7s;m*0WzHBq+Re;Vs^C)LOS-i2Vq5A^uSkf3SGJ; zdZU11cBw-I{Q4W(7P~{AV)6>2tc1%p1C-nD!bHw#2K2xvqpIl(3H_Bbq(g0Jad|ic z6LXs+Qz4UDXgUi{M;AUoD2Hl&B5gBClm| z8s4i|<$$OtHYL>*&PT6;1L2_8#)YuQTQY}7AJ`QZr@@X+Lu4C~(vz_4*7I4BQ}mu5 zv>xxe0WU{Wg*biaD{%+mb;+C=?0oX%4R-ahk8E~Gxc03Vemf$Ez&D|*( z{CN-e{@!BI4eBVgb}gFnbBabqeo#l}HOXrVmRz4xE}1k7$w)(B=SezDbaV+Zg>2E( zrGi9ysM&=bA(GQ)!$+rAbuZEWQTL+lI%hDKmC|RE*U< za6Tw_O2!r5SZl~csZ+o=Ew3`D$dRi6fD!GrLh6ET0$DeYvPpG5tW z8IXHJ*8b5%U18uC1f=vVdnuxY;MA^dNQ|m#*S^XT(ii`;wFK3kCN^E2}Vp6F9vk|IBL*u(n;qoA)!uxh*Z^7C)M?@Cd9pdTT9ku z*w&%3XF^6bJ0(n|ik0KG+blYW-fi$U8ajCY(Gx|ZP_hag-GE0siB79pNSdcpB#TLo znm;3t8Fh^mTA08}gt589QyRRZAJJ>B&APkybf%4xh1 zB&^rlF|7|xVYC@r9#f1SuAj3GvncqBH_LI=D>IR{7KPFjCRaI;_3U`6!_0hvOB_!D z5l<XeUPTZrzLnxz!t}CL+O7mMKyx2{NKF_zCJ5=9l2EP&5=y{dZhHYGba2a7t3h zLI1=sm02)OOLgi5LIe>SJW0xibSX;i_;RA-^UWn29U37KNwZsdgliCDeHP-D(MalL zgK(EM)6-G>y|21c>vjA6@C5kqO<;#QC-3w9e3*P>V!Eb^bG_=Yx-)d!E)2nFUee@xnI`H3dSNRTv$O%>y1A>jjlL09XHD zXJ7AT8Av>Tl$$Qgk`H>s-h_F!UYlW~d4-i}Jn-P0Sq6KkMA*73Gb$Lj)eVETH4XD` zU06U|TNGLeCYgrQoQO?}q3XWx@>})ej%x0N-&|>^{F=ZjP~-~e%HOHM8g42;Kf#&O zNQZrz-&g(h(vF{NXO%hT|y~TNb zTG&5qr8Rl6?gsIX>Ms;AjU!bU6UW!zu6rMO`nTZ zeAwN>qb?N(l@3x8e6&Vt)i&H5i&^7|!%HFVPI!?#M{-dqm1{aMJw{%0uDYpQPpFg8 zi{VA2d7{Y9*#6Q~_22M!42o zib#cXr@~M5K&^vgKRmepqTTWbPm*eFl<0Ex6_;>!pJgb30xsD|G$R3z!BLA#cU^R3 z;_?7^2T^y1fNkCJdF($ot4J&sBFAYN2Owfw+_^Me^h(TWs~jf-D-~?F<*wIM^$6Eh zP{)~vbuQ>Txn%pPoLtIRH}i;Y*JK8|8Eg`~md0o2S|5OGAEPhqOY`&-06S-uEK$OD z;e7ds%PH-0KIXj&9X6h}DO#@bLmR5V8es=v5$!iF1iU;%D8kVp?QNUAR1#DYeshz@CNCqI(gn8~{s<(O#!MzmD5y6wmqXY9W}Do|Dar#uAxr zSamwG=y<;b-`V%AU^ZHmo%@vxeNFlV2~WYfa2b1XT|1UF#9N02amX!%%W4Yqz4_1Iq)lJ4)Hn>G%4fnbt1s`b&H`G72XTqQC zR#FkEIiHF;lZaF^Q_^nXEw~CH`m-5J@f6w04S7SsEbxu!a*a~7=pRhlKA`{FJPKz! zKK6AJH$l zn|63>j)a0CYKHii%M2;4p;$(z7QR|hTK36H*|aU-YzB%m^O}5sH<~=4J{lcfsg|iI z4P}f_*?5*7r#-kw=4pfZFaZ>%luDA1N$9K3?GQ;^NP|kjHeG}6RTLyVIlY*NT=bPx z;fKBba2l#4j~u)pmf{n#axqkB`=0QYNOJp)a4BpOl&kp%neUC)yks)n;7~|-|B=+& z%jX}0dFeg2EHfFk6zE+ZB!C1iF1#U{Zi2V`=HySakCvjP ztJV#PtcsW=HbA)yk7rx~)?? z=3_yf#v5qDBa6#edhA2Dt2-iIynBieg_$^{eSTEjd&PXN9;}5*6U_WNL>*Q6Jf~S< zCD_pQ>l`wUJ>yIOenSctZyAaByItA6Apnx<72vDSaAwT&oe4Nrx3t`sC-F4upWftQ zv++RiY2=(Tixa}WWu0WqvDz*ppI_y@EM;$!s<=oA3B-Xau^4t>^y~C-Op^2NR=mvC zeC@0t{jJJSy9;tXfxN>SZdy8p#N{Waa^}+SPW#3WE*&S8=(HfZYk$IgiOR91wV}k= z-aQDM(5VsgUh4OBU%NuQIu~UoZrTcezNc42hI2U_#NeT{`5m<-$gN779A9Eux+csk zOUbDZ7O(vgPAy9O6XWn1=*H)!`}?VKz!=|;Z}ja-!jBdny zvPrn$cVf(;2uF?)Qx(}|8=l%_E6S>nPf121V(i>&2}2tf9$Lv>qXaRsx;gx{3*)1B zcRnJ%>CJLzfBYox&tC^s>8!i8glltqQ|_VFz$yFn5p*BSnN)B~-4h?MaF`t{xJo(b zd{v0IwUu3)`j5KS*K?meuq)HtN1?!ZHeh%oHz0g(d5$|a%I{J)#t|TaB~+3FjA2JC z%@2zYmml6U^;iG-Q(jAW3|iP>28WQGtds#m0iSR1;8=Jn-vRJ_W+2%O)Rj^r`;~kX z<+Q$mo_|NMqo@K)gH^DDHmge|qXtO?M|FyBSZeP#r(n;T{!bPfy8_t%AkFCpS-fa& zo2A=tXqE~>leUP5_}LbRY`$U=4u?qVK*XH5!T)=d3I$vlH3*PrecPYw$Vg+WX~1*t zV=ab&+iw&^4J7sX?CxuuYzqI!lB~B4*T}LcFR{du>3@`&9Y4hbu=jPW+Gqx^^qlTo zWHN6)Gix8QGB7cFm@;(G4l*&kH<#?ATE4fKu|iUm8HTFk)LffI-lg5I_f2=d1E?S@ z)Sa^fa4Y6yK-~Q2DNE4Y*Jpc z`=1f_4F2y%ZV+znV*vkle$Hriid+=eyQeS6_Y53l72n9ec41uY^na9$7I>#OMC;|9Uz19P@Q>ZxQvT-XMR!ceRbrbM{QGOnq-U5 zq{-&)Z@mfu1{Ubi9z(m*-8_LE@j1qX4XuUEYu|N>==zXMn#RJs&`pA_u*=ZL8;p>+ zE9w9O{WtGL_5x4 zWtoI~1j=aEN-S7~O5ypKgFLC%NyISJidG=F>4zC1A8ba78`NcUhK|Acx!GmGYRm(C z0BgeMHKcaKKJAQI=_e;e;M9SA6DX+CMg8py`ifz<^$yGqD$rK(az7J9$gra*fM!z1 zo}(?A!k?k1EO{_;KvdzLzD%UH+&lT8akuxK$f%c2+{YS|PlF}zkAc_Sv37N5g00=3 znLe|tl&Y#bet(cpk~0-BoNUSl&sBb0N=ME5Co3>Dq=IIa0qO}G5*cu8YGLq@1tx<( z$vh&oLfVu0Voy6v@3iaFie>&5$keL&vy!cG!sFz(Bd4K^gft2Rn6Nl9Pf&$xZ$2&t z>B~~Pm8s9Bs#Bq~(f4n}DKGv5C-V_5jn3%#l~Jv3_f8n3Nr#T{q`N{EpDBlibTn6O7c5DG41j4#K1P(_Wum`WXIU>C~9ep3A*!2$;GHssP~!@2-wm; zIKc(A^X=?9y1>f)k7y>xqQQC5ZED#z06o6sS@|f*d+|l; z&PLrxM#8GQMM#-=xCK?@n{?ben9|jd0BAC(?WVoWJ+K^tZZT&3K37Gw*RKAFcCt%W%;7*M4--{`~#_`UUq}J zfmFHST5#|BWwICePyFhF`#Og!dui@*ri`9SG7F9b!20?H>!J%>ff22|z6)#JWgHnF*mSX)p<3%ed8yLy8eghN1;#haDtP~()FoS1ym|G@!iu^QjvPG;iM&z=c~CbaG%e(9d35%*66A-)duxs zD0;Gz6hxVy2z!4urcV8|1DL$XyvT5c2bIwN4`c5DWl6Jji=8LKR_w^kopVRVin-RzWWZjZoakA!k%&TofxutF z;6IneRJ1p-XP@5R!e{skb`9dNAmt8583}ta)N|G7GTFR~L?@OMho8NP{@fZl;=hzo zFoFd0u?2Tsq!`7)Eb{uL_vdzXZOdY?xl_;dSOE}0La{8#8l_@K5&keasR)Sa z#wg(EmV)E*a`TXs&|lp6_6SHm9xhs-tgXGyb2)VgqWt8TY`9DuC zL5zT9!X;!FnEjvhEdm;d$oc6*PvxFACh{nq zzu+l5&g?-&f}}~{$e#)1i;XD(BZQ7K4G>@v01W#R-_on|&Ij*%HbCH!t zg~cFtWR%;SR(4TXZ&QhnnDIzr*d`H%W!`Az0*8S{M9z)rXNMnO{$*rIj*}Wgn}%Z; z+JPN#h%_rQ&S@tm+UYaEqCl-DSfx>69)lYf&?wV>5ftbExZc0W1h{pc?t*6Mp&MJ2 z18YG*8v9!a!)ZLYZ=~vjyeSn8tgeYG5Dm3`$HG@nIk+BfKA68toFN`6O2c4RCYYc| zcJan{0-oo;GDMy`Zkwn5s_HZYC9unPSM+lDrrlPJIDgo})Ysm4wTNb0L7iKC=j09r z7bkx)H`uv9-4|P?{+dMV?xt_B>r&ppHeZL1H-4(mFo1_iko4UU$#Wek+-@fZ}BiHq91V-A$;Et+TH zOIru!^JbAD+!v}b3_z!5@BH}fr@DSXt$0230TVhF84A@kW>NH9eD+BbcY7Bbn^p?F zH*l9?g&tpoGlgL>;8@Kli_h1|8SZIR{s1hIwb)Qj^bM4!i=|v5cCG{FqQ~jS=86%4eSX>Lu~D zTY~Hu@co!jHd*i50|C4J#7kYtG&3El7|(nlAr-x5C%jD0J^3J^htPI(`*$cJL6HOp z%z9z>pP1En;f**@zHZ)gO$t}JH|!Uryw-I$$%3qM0_{ZoS{kM9tDOJ@8z@h)Lh25+ z@c3b+{(AdB(Zb;&87O*D4Y=rz0c_FW+|L!=g~Jn-t`+9=+8Y(y1y6g;bo7O3)ENZ; zJO{!r65EX~{FzEdMpV``>>Xm$(=sc*@IA7l-4swj896Uj^kx}YtSnTEi%O!F7C=Vi zXg?dOO~uz1FEe06+RDwpn$9nG^m9b~&Bp@3oiuj;EZqoMvP$$z=y@wneSSb7uX)-} ziQ&bfk0?sP@~fv#%TwEo)ei=wRNjpRi8u_IA;rR*sb^Ow=8|vYks03< zb`y7FyJe(UgYVfEmDhR?29kEeN;gP#h*dqWU_Xb)GRT2ncplma6;R{{i7z@Xj?zVw z>#=)UAI?bqSAJ}ICqTW43-?Lez_a(S)xy`K2u3@RrIIe3*I+pKtT*_=3AV|k`j*KG zgN7Y=uIqfpi}vw#vH)s2MWrEkB)@)XKBgact~?Al_2EOy#q;z-?wc3=s2%5BSu(=7_;wnHqZC9m!l#Ce}1rN)^h|ohl^_P%OU||yb-QF*K&%#V5SC5{3PAc?n ztnMV?Pp-ihd~Edjn`WZ8atk|Wb^Q|R;ta+ySK`FN=Ubw`vnkqxgwKHQxEh7%1iS#7 z9w)MfZf5qM-6MA<*df;O!-=5W(gUr?!gmw{NQ%trM*MSzZW4!YjJ-7I4@BSwOk?I2 z2R${s1xI4hMtZy*ikyXb2_#Pu0h4X!gnX%Xdl%GB+p-@IR(b(VA5C0o56Cyn_O`^ zehmr_yAvwl^23lNC`qNla3B%*o=Y1MMXL?}xw{%L2{yDxWL5?#Px?`vNKb_mn;Irs zD+pdJ$rn~d@1GJz9>)Ag<_r(6s6M~|hCj+LWF<661^h~J~T^sg7Od*)a zGJBw>G-0Cjtz1KN89{{ZO0Su1mzxMY2J4Tu>4;2Wu!caLjlJ9)5GmjW+i9)~ZH_qNYhwO}_|^a9bD~TA0+dvO4Afdj zZCh2r?|i%h+X&KcVTFNd0FBr(X>Zg`!=%CKw*fi6-B3Q+^VfFYTs+6r97Qk0QGg^$ z))*cY?D}yL_cU;Pt0(4-GFTO33p}_K2>2P-t3{V%t4|>oYG>RGHCU3SL7`bT2EsxK z6d)u+#%utgT0oF@V5Lb;lqOAd`n_Ik$+)#uWs6@&e7@O@X+h^K%WUnHx+Z0<8)HB% zV8&RT!9a=xMQu;L-ELaMtl@$M6$`#n0Xr!dGj|?iw%2_$0PH`P@jRid!-^wki=2ad zFKpuNTgad=yn%+_pYZGK@>f&VJUML)rJy2{n96nh*Ooz$A=!vK-^P$qsJ72>uv{9j zy7LH_TV|iX3)kqEU1B7H;bV=h>p^pCCnl$Hf zlO50FQt-A-tKdECf`-%fRBin(a{e{pdgg%Wc+{Jb_&g_Sq(u&D`ybh{oSKjOG&7Ok z(HmC^AGtk-eE8lYC%J1oJrS&1sghyGj!-7(&K=^zm@TL8M`WJ+T-Z1YMOCx_g>>l~vvi9DBm@$q>6$TeFHl5Jj6T7R81>6M`buV(b`@e~!7t z*JbC!X~Hy{)Vil`%L&`CyA^J!|ISL`l*4D=h;4;bbRSC$qtILFc=j5sdamECKIKZT zgNrR<%%_x0Uoi}a0!d5w?LlqdSdWc0xHy|P9gH}7UE^LE!DhP@xp#?j-a+@;=2s@> zMfF*xVd_mG>VxL9u6fKjDu^>4nw)DH@Sfb(Kfwi z1JNp2qfx%21B+^Xg}em@GikSCC^(}{pHN0D94|teI$XhMvrLuJ>KffTaCOON7x4=# zrCb&B)~tzU?`P#E3W1!XZP$Zk!9FIw8sZDVT!S|Rl0R3?CDrxIL6Gg8i4gDk2918* zmxq?;ePnFVsoY^@SB0Gry+0~BQeLEqhd>=YAjO#)Gx@S0==Yxrkb)wD%+MJ`MbG0U zpm|SZTY_Ts;)cWY+BO7gAt*2+qncofN|-~6tSBMPJ_evvYN1)5Hz!R>@)w_n?w4Xx zSa>;5k+Bv?wg4?@Jp837XUL((`e^Wl0@gqFE!bY#`qxI;T z20qcLQmBuUhFF9tGtDy105jxr(vs&TcZRM;nc0GfS>Hx%0M3%Evj0u^f)70!!cx}p zet6xi+4LP`tnVyPa5d26H0-V*3B^bQKrMhHP(dtAn0y4EIw`%pjoVJmrIgT7H~$?f z%G$uSX%lVzq;NGQKKKd`pEZEQlzs49!(xUuj1f|LX$s zNP3MV!LE(EZ7YD479R*FMLvpY3LGA2K<4$2Udcv;E9sFS5?=zulWf)Z`Y}M2U-C=W zqe*Sq$-vf&8e}6>!;3(`CRNX59@<}N8lSl9Rl;(Ke8|RqOE8pp1qdn$fV>(^#5qxP zr5oTmdmbpNmDCpyb1?Ig#0?Oy=>h&ib>Z<44Dnh zb1bgQ5Kc$RY^Ms!Jurr_65};LyTAw>hEg*M154QJPe&L{PG$mFrjbE;LsCtQbBEyu z+B?Js*ggoE$~>C*O!Tf!neTj&z)^_;Vb?^JI&4;kB*yDzi7{84<3nb_pQY3XvC4Xc zl)mg67oiQC(!N=P9YJBE*b}@Mft zH7lcF`Lek<7O8J`u-XDZ5~y{&MQw|3WteGFum;pj*!K;2fV}ThmDhR?J2O5E9n4&O zOdH%PUJ7p<`eURZOqw67%B!Vxp0UeMpt%*g)YMhNYC30MXWK`Fr@lhhfBrnp41B7d zzHnQ3`{Z~J6@I*@uK4nGT)ufPwcezXz8TA+;QI1(thIqr(_*+IPJF7w_IR9H~;|7 zw_M;ql6C*m)%@?mb^mB=HYHEV2GOGgKl6g>FC?R2;EFNPV;eGR4uO-~y@byuJjIM} zY|vxsB{9>E-{H+W_T*@;f9!QwXUWA?pm7uVpCE&~k=UgeRmsE5)h;hqCr(S;(^3~5gyy;6JXlg!J%u`SQ? zY`sk`b(|zt#k#g**8qa zPsmP2ZLMd3L5du#3;`PE(P?4ON=_CO`UsAQ0gkwi8`R6u+qRtsQ1zs`%0{2t`xE=w zwt9$^DPiQ%gI9s?sDw(1JRL4ioRB-}7=(xOvuIy{|GWuBZ3&@z-+GO?Q2$c# z{O2Z^8S5JvJ23v04J=OCNXVea8NPW#fnn1!2S`>H-HKdy>9p>EkCO>7A_=JLhbu=`B@)PCI|0#zlB54+%PJsjyMqL=@7x@!oP#4CU?>H}j8^F>0+fq1rq!4?pDf}JN5a)f{(-4Y;uS<4~;W?scHg|9>Xhp zD*l?1*H*Cf80tO`>jCPz+<6gs9NP`_6`8-!Q%+#?~V5` zCcuI7N;;(AuGUmy5*KkIG_&kYGyuG?g2GIv#oChY(4-6l@AcVJiGNz;`*SS5h#4$9 zBCzjwic~>WA)Kz7B>0rzx^X;+!%C#}wsa!w`wOz~ESYN=|Bu&-s$SJ75lDH3IC zOIM;7bfP=b_4`|mw{wQW^U-ZXNBPI#ARn6#2Cd2-W6p{uS3;knzJ_bB3+(ky>IUC` zqH)L*L$l@^jUwM@MEb8G!Q@*qSxMi(>K_TwrIZ<)b$X26o5UwLm22xy1c#|JHuF$DrZqilr7Vn<|K2+q^}yh*8=WGk(V1A$m%m@BBGL? zUV9E<&g1}o*!blEVj#TEMl)>EYn`f})V5-q)*jWi4!?BtEdt!uBNLq}AJhh4=@_1^ z4w|!RHHp?-MZ(I~$-0J1EJtH3hZzSLCu}aIH)b||qO~5oz;a;rs6&+h<_1PGA`KW6 z1jR_EvV>M5gRW@IvtTWYyUwz8q1A{6JqGaTId)7(y~TkpFNart#xS{g+&l1OEhM%D zFy4v8Sz#riP|3)!9>|0Fk=9_PEs}hwmW0Z|L{h!~tj53>Uq-M6m;Luvs>KLXIFAZW3yH|bHX3!Ip zSP{dKK6@%BT1$Wg>xm9qRMeMILF<+D#En?hp&Ubbc?=I55d#BNr7F5M5ti1lV%>@4 z(tmiQ1H$vGe@M)YYqh0dyFn34EHpbHRrFH~`>47gq@NE66`j`3#D+j8`t|JXM0bZb zKbH?Lx7*7)nf^gnxPNh)qwFNm*Bw{pZ6hoh0WI61zB$o4d;F@{Eb%gX#1YXi} zb?0eouEy}@dX>&dX}gS!1VdYFZmH_F_IF^QVBQ=cVJ5QoC-#4$+shVfJPQl}pb_<7 z`ndm$wT{M4PUbeIj(-Q+CN(L?4R)mNn_oZA_9rNbAi9epm_!r~*_rGUBC@uagay&Y zN&0{YsK%E+pUvZf$cOC6a+D;&<3F5zKll%ZR(ib4E60dY6K;=Rp}HJu$Qmi!#xmY6 zDra0@sM|BdMKThUI`dal#VCLIc;5e72`w&?^iRnG5F$=>w&&TRSfV{~RKGNnIi%X2 z2kWIlXOaI+d40ly2><4y^1Gch!dh);(f*oKmLAva&!ZDv#t}b5r8;5jp=Pm6idTm* zDDG!e@E^pUuCWO8w+z;bdv^5301P)`*Q(@@>d#UufNPksgXaXdDyl+P{%QCk&)ouU z^>=Z%^*q$hJcWLMrDVuBmO{fD$hom+s^2_CtMdLF4(+A)qaUe=t#S|n0Y2`SV$61v zeRo-*$#e&hvfNeN2QQ|5S%^t*=<=LFPN#+%59#Q*_efG&7={RVVl~FHkGLi z9;Q*=fy$k3>?cKZR2jZR+{*_TD1oJmTI+QxPVR~DE?@#IlY54ED3F)#N<6sfdMvDbU%XkF-` zKuLI7<@$nmljkU%@k?uMp;uKrBDE=&x#s7Ub>Uu)S#%|7FwTj8#7ez_9wXAEXUG~& zjA!&_+;IAwINB7lQ@7AdV5R-Q5B3R6c@y3PAxa7lN-$^%>AMM}U-vur)(fdJIJim3 zWIakuwRNK{4ms|x<(oV$?J9DAioto&nG!SF!cTg5@TU!c^!EAFc0{^#)r@{Qk~&{b z0eV-uV?U^$q+ffsW$tr6WI5G6-%9c2XY3DX=nC>04$+McVBL5EUz@UntC^u#@HETQ zGpfFl=RUNq2^LxHwvkjj{K05y)lm$DL$5|?lkb?m8(K5{0r$J|T`=?mJxQNx$dz6b zAE`#qzK5fktL$+my6EhxE_+Ymr{w)_NR#Q-rJzUzV*&hf(HU046^Vw)7y>@IDhQWC zBYm*|)P5I|$@#^IAa)2Is)Qg}vsi)wcfH5J0s_g)QJ`;SGi(h|ooluDNg*36A+4bP z&5YPc`<;MUj2Jl)^_ zZ~(&5ybq^;;<2=n2NSY*Z1_FE&d*9BiZ@-QjoQKP2^OBc*kpn=LN0m5T1gOD^+JLH z2ee!xsSs7$#3>P?67PJda4FzlC5~X!?VM5sJ)*AOL8%=?^=2ha2sAR@Ea`1@lTF=E zUQ2ABw!RWb{ip(M$;;8tptFWJ<9fxKAIWW5;vR6{U2wk;p-cC;0^{9&mithrukegU zQO*_+KUV#@&9H)neH&Lhyo(K$%IXnQ^^6TXZNfuf*;;&9>yO!ccJw+3CDwP{)=s+b zbBN0vqznZ<%_pn%9fmn{W4CAF`2e6A`N^)Q6n6rfQYC+*W_{?;qmEq(R+Dauu({34 z9Z@4|=)NlnxMAs9+rY;M%jHSB?4erHQe^!$;OJ?N!u{of~Ih)&*R~aVq6)WeyEM#lRw5xXB+n-z86#1 zGZJuR3p&X1gT4UHVwx;Wv2|Cz1*WkTeGfNl9sish(x0tqPkail8E$m&9LwVi^>b(Q zKp)m#nXt7bAf>@@I(=2M9fqE*gMo^!(`>N@%Now_p9axeI#*845Kc z^I8n^@?-U8@2f5T!ejN;R6c7rJ@pWhyZDS+t4{&{gyAx8>?^d*yt}3Q1^Pc9WjmcG zGY0>Aw}Ih*=Qe;QBf73TuirLr@$$1m_KU+M4lZo4g$Xn_a`|lw_j~ntz01}5w9U?Q zAeKW-E6}CA<_KZzx9YtuzI{A-*FWGC6AsiKMip%*liwKM8+*$-P~#GtP;x*gwYM!3 z0rKZ({VXeKW)8d1bk?L|lI z3Z+?QExm07lbe&RML#p8+Q3o92jrz;)cCbK(T($eg`AC`0YkfB?2VRT0cG#?1%c+_ zBszr91eadR*B{p8GQW;{C|uk~6%rWt7*2h=4Ych_>lVhjt4nt@Vq%hM-6F8!3K{GU zC3k|%3;r?1>6vZ8S-jA_;VCvuYXLdcPd#(Z)eFA?p6YyOaA_=0a*=_1#8|0>3r`LA z4$_w)Sy12E-IUBU`d~fa@S!2N{4nBk=%6RmwlM*C+B^19cs9lz_CpkR(M+?_(E#wl z3H#F@ThZNKfyfCJZA(S-A^x>30Ajnrlfz}$+faUwn( zqhE#Y3f*QSPRu$+MtK|b;ZH}t{f3|a_zn7G93o>jGp#{4#r8()D-5`&n~b}QG(Tx% zx0GY5@kdIw+LTKbqlH}!yT5|g(tZF*wlZ}gcZ=Tt+A)du++g`7t9?a#&DW5bCMW&Q zaA<@`9Iv)H39rG-T#S46I)v+V+W>x;c+aX;IA8d zkAi|Oi`AQr^p>}zsAKBx=LC_EK%ESd+L8+ZcBWqfJu5}B_&hP<8+f%5eLM{+CBh!r zrFZTLXA<@!DBIxX`PcK zAWJcbM2%ZTX_#W~LJ|Q?jTRZf18~pl4-zpo3dec8dT6C<;82?nv+y8QVp@MkKSpwv zZXV1&y-^^m-gamxF~3RV5CL=+>?nQcWwDe4hR1^zG3OPSPciO>0KhVX)z(e63*ack;7?Mlw{&;y75KbvDWEH{!L}76oCM%IaqZ8&3#o z2i+sp(AB2r+NK5v0-DYIT-V3**}Im@c1Y<46Ge3jadU~^#zT1Wdfq!IgFL1tXJDY1 z#wa3Uehibok46(ObjO_WmyY`>w(z&HJF9Xu``~1{lvc==;)GvMdwoGRFvNl+{WkM3 zeZ0PTetQ|OcA2VFRPBh(U5lRlVRA?8;_X{^yRz50#<`1v*7gm?4w|xVI@kZXrUFer zAe#XX0ANrG0Pwx}*TR;gle?AiKePNRZ7$b?R@CczDz{B(7OHrUw<@-gSo>kqlf5V& zE*4<{G{XobaWgC1CYg#x3WXx^h(i(%3WcM~F}d-}SO@@KVJP@Lg``Q$8)^OU-VXq; zouLCDlE+87o0INGM6o(_FtM4-U+Xi}IIVkw&^9qST|K^L8_ywKVwIN%^!Y(-W}!}JkmIz_Vbx6<>rg6E77 zgK1+7*N@wBcc^mW0N^=j~5`ko!#!&#sq!5A@#?Jdb>FarXIx<dE#_NPT z!pfRKjH+QEhtAej^_(%g&M&e~T@fdZDk&H^l~g30Dq2i-HEk@rS`E7gskqKfD^wb<(I!K>uS^cnc4u1!XwXDP%sA$k} zcHd%&UTA^O)^TTJGSHqv*ihL>i;YIdW~v*LN<3r9ahq^gDJrhq)KD>XTz0BqnlSZC zt(lv=OW${9ewqI1)DeEn&A3XfnX`oA5a>qgS{I{@4(`#d>SLIbnP?Q}xs^z{$_|h@ z*pdephlFGsh~+1Bnuc*0W0v5)MJ&L&D=7UTbY9giOUZe0yg4+E0Rs@w0Az*ZC$pw5 z14}@fl)bbFpd~mm$Zw~jZIax8wdM!S@CaYtaBc!Mr_Odlt0J1Jc~k{}4X&r?qM`^a z4ry%4!UB*w%fNt;rJSmMR3-Xf$Iy;zu$k7bilS{9R|jN;>n*vgD)~OA@o5VS0NQ** z0|NF++J*z4H;Pc zlH^>K<$$e0G2#BZRqd1C%8vDb=mzKb+`9GHLu}ir+vltS*Q1lQrVK>O)t=T&7d}4H zlmw?l13>-!(RrFR=m=I+XUguv?bAVo!7*6+dLgsxbNO2mE-O8vdT%0m9ADniPM z>uVYoh=7z$VF1Y*!et~)0E*fch!ypVL`p2>T$)leMEu2QhOo7b&w)cy3A2 z5cZS&SGSZBG`&ix-`!C+K@{)Mm&(gXSj6>%I<68`=|Ox8X@}JIwdtu5YEY`FMx{VO zpcazKS6a*|$T$8iy8af!7IO)CrcGuNlvdwGh23{S!$Pf*Afeq=s1liCq%crGTD(|m zCPDRGJ1+?vnX>q|D*bO2ne$^`n?_?%5*8Ybd2xt{lt#J!T!My$S|dr~chy+YcNOE` zYpeXXN#)yAl=A!}0I?S(f$dAA3*8q%I0yhNR8HHhLboglO zcU`+K;G~_xw^4bSxCA}$L^z?JV%&8uGP9L5ZRT6ac*n-Yw<|ca??Dc}PPP1q?cuWq z!vthwOugoa+y|f{g75xXDl}ByP8{xVY~>%eiKT9d{jF0W3JzMDz1e&7S6ha{ns=x;6vd{bGx?mW^L=(Ld4L_UvmGg6XA5)?J z--+Cy`mR^^HPV@rTzBV;TpPtlIaxnQt8;{Inj5GpPi8yR>(&kq3Hoq~^`=<^zVl71 zIr$TQ9bR&!EV3poZnqWTiX}0(sVL9KD9lf@Cb)DXNMvVAox_MoBw8wyUQv=>*$|tz z>?OcOMz|zLxD-WraJRT-_}Nz4Dj#`B9(in#ue>oPU0fH@sVr6eTCC`aR{03AT%xfk zPO1REGU`Hn>!HQTZ_*fZxvF~}zlLEuAy1Sg>=1uh7LzbqLndV9_)$P7O5f+2Q!YXiGvF#AkK&3&Pr zZG8G$;O)~xMz((}&{?J<(B_@|$!f3I)jdj8Lv?-N(RDX{aZ!6%iND*ud3wJMeTd|S zTae4`uEKFlb3p|@bk~|iMpv zm@RWP!Nn$vItY_+L%?oLO8a^Y=|xon&F7KLCS({05l6x_d?|(h7o82{15B!7@fiOJ zQdEL-f$!^mF7L-Z{&GZ(bGvoC=Hk|1RmPNePr4(v0qQno$mL}*vqk7thNqRLM3r`* z@!4Qp4VJh42%`*&H{OnOTbBu@)WHD520ytD5t3yoz}(IqN%NUvwJ-3)i2 z-Iu`|^7P&~MwY8%tyZfHZC6Ll7PIcZS)r13c1~-YG*&pP#51V`i_8Wi#d1#0a!&ek zPC3;I@2dI^&hmEp@+3lirsDPIyhy_4cz9XYLU+t~v~-vfrXjtBR6IvbL$EW3VBR53@> zeAYdA`L9;Cj8-wPv$nuL?!3xwOi^squmK_Hq&0~|U{ z!z*?!sVZ?7!^WDnf++**Bv;}@S0y@&+~)TS?y%-#9Pga+!3*xYxR}S860U1ECDmY2 z8^2##ZX&w=OmOa=;I~4h%G)p-i;ds(cLa7eNjPpU_VYj%LdNVX46#RFrV=dZO26?| zli$;3eLZN@7w9C=S1Kc39Ms)x9pdE@oV?K9dz3C!7y9Pxm7}{|x~EyBUzG6WcyYH`BIZXF?sQ3t>rH-M z;bDtQi6B--SY{!qc}Ao{&PU+q!58jj=dzibVMHdR5Ne)6LVQ zv)?ZVT`jLKWUxD(5}%OBecNj@Q=*&T&G>Y*mGHP@l~2_X?5RYEwhRkb*(ZyE>6Xxh z1;O^KQ4-i89`I>k9pB8kJ}N{%_YC2g#z&7-E3`$B3QD40G>m0ijt?WUD}M{GQI+Ai z#w3tQc$TFp&R$vC5=osjLo})A>Ds*i+I2KFA*qx(rNHJb?RCC}f-|4Sm)MY8&TStg z`;Cx@G2kxfhwQ95#vll&LQI5tbg?5rv%lK8mr7vLflR+kR59xAcM51tyl%>X<|iAI^0~E%f)f&%4_u&yz$H z?Ty>vJL6`kHAG0Pb>&(7@F6MJ?YuChvI@E1%I~TZ&R>hMvTc2UCgnD?aIFm6b z?que}#ktw`j)4sCyiE`{ZBsBX49CifUY|Bw5$+mbYsBvoovZe;Z_C*xX?JNILy~@x z?=*Nz!s z>)n$mAC{L|3*gJI9nSSJA4y%e^-^Rv^H!uSTVYHzTfy0qQ$bGo;24e4v)zQv*qWq z+C;Qahky$s;gYb?J&E@%(W{DKuE{&tXejT31vnXP5b^#H|N1BQ^ziB}l=-%#p8SBj zgFWzMcQR2u34Z_bY5(%ZR@HVeT~$S&6(W&45#2Plp4;>5I<@8b!zy8WRPga0MOfwO zW@JvpR!RAw`zCcxyhPtUXIMTy^`(|G_HJ<|bbvkK(_06|3xpu;gO&D9Jf*n*1;b>a z_L63|Aar0BOvLRMm?i(dSYd71;Q$mcD_FkIZlpDV5nz!?*e)xI4a|_&lm&E{8X$#= zruZV<7(ItcXi#%}D=yNuD3Xh~rh{Y)Xiltgi4EL65iHeXUF+jQZEI%K4*9OV+7O#P zl4>jzaxE5FR09{ZsKp`@;ZWc(#HQT6zx3}s(J?xYM4}72;U4vvmBqLF+sFxrAZa+y zDi5vQiBL+4N)Idh(e(l5EjDqA?)aC31Y4nY4vLbbELb5?N4wcEmy4Pwj#Wkzh?Jc z%vPupy>nSI3H0QY8E7}+UpS#g#uG~Azg}49xkE=RA5GC)=L@|L!}Gh%&Es2=hVQnJ~MVR}sS#|g0jMVlR(+&#C{mXDFds3ww>|Cf@%nmR-L^b|dN1_2nAd1y{fcJgd7F}*I^LcY(kT1j zz<=3yus!v`p7`K+erq_|o}JSi{oqjjxhVEnz6_|Qx^`rej9G?GwdBwEXp zn@O;mNiTQ%#7%jrHmlIhI-h*%F;+^>V1f2SAcw6jP~Ja|+tT2rVDiU9qA(Tdae#F& z%sC)_W{nqwfzKaKE?-xxZ0uk(LxbXhg5rXL;)4d`fCl4%2IGPTS@U-wF3aEq zORDGLHODWb4{(^oEI_fvo#VXUogA=af%lgf7XZLSLl7)ADm|TNIjwBotmfW;2$H@VooDnUw^$sr$vH7Icldi0el?1{-3sk*X`n$(4 zcK{HwD2)QFM5iMG&;Sqw*MhSuS<@;wMtpNY!LXWKjT~Mg$8Hq8d;@71EBWYns?sF= z5Sm!#c!q#HKcav(yT00u@vXJ-VE}{Ub3_M87s>0GgeiF{5DW$*Y&`?R#RvidAaJ7E z2mDp9BFx_a0>e+%<$;n3q4eza%~3fFN-(G;BE7z6}Q-XC)kYZ5QX z5^HJs&>)2UVeK~~oD{BNk96}qdctuXA z5%>|V|NUb|?q(s2BII40EladIMh4dJHH^ft11UrgCXo5czB^MeCw=2#jfm??` zlsZRQ)wg}Do>HVi9~e0X_1?bTZ;qd?rI}8|izGQVfNq9C;q=qQD7dWpa(7}I0N5po z(ek61>Os*9dbh6MS~e8L)dpM1#u*?=`V)IK)EtHky#qOdxnmTi zcf%(jrnP5ia)J)i$c?8v#4f{7%IyAdN1;hLG4`Xi2C)k$Bsxy~78O#l0}kXh;{`OL z&hqqLG+-VNRjI@?kfb-!L}_8vzDLJTc$xt7iQtn5=L@u10*_1NhmMUj9DL+B38UP%mQjBK&!qsA(!P& z<*_#DSBPkU(LfB#h&2Jnh4h(*3f!$WMfVANY0z1}<d{DVEk~4f|A2tE!8pA{l!k})x$Mk%%z>UBkZ6ya< zwtDyFfcJR4BSyQ%@=O5{fcBbODT#2@l^F+YSF#v z>{__QX2$WNkP2g-2HdrXi-X3YYGi7|x0oWI+j*K&!t za5j?$XulzzW(j&Df|Z5wBbn3sr`I1b-$!`&L(mF!JQ=gKR@TCv8YxxH(Z>X zicEoW$fCn<<{m0!cU*GGI zsN;!X(K!2tTi@a{PCvp3CYMd;^Nwxe>Nkl9Gmynpe|s(LPPbuuVVPj?mqZ*>&Wp~p zpg{sak%SDkkYplh+Xv?n$pZ3WHwyEkO2r8B*-iZlIt(K>mh}j}1WJDE&DZ3^O8IRu z2x(GCVAL@Q5g)+1YhK;-eMSM7V`)$iG8j@q!>s zm&NK(zC&U4qZjnqGl_qi<%duxotf_E|C>Za{U#C1r%1j@#4`VXNJQS1GCmT%$dD8s zu?@C=k%;&o+Ngp&rpKyK!Rd!6q;JFrNIr+d@#eZCPI-zZz2zEtYFQQkPhV#p6;<1{ zaTm1m zi?hz}K4&e4J$v8#y7noc(h?4ruTxgdQ4W=Ge*U|qeZPU*Bc!RbZZu14D?u%X%_$b> zBFcYYynR+<3v>~;7i@H`H@lEi?5Ar{E1y4he`fUqp`|Ue2eWAgg0Bz|eE-nGT08s^ zeJ8CF&!aEnCiTBxmBqnKqVg<7N;xx6*Y}Z%Igv?ej1sA4+jJ<3n~^D^M~a_MFJw#!v#@>6)WSJK|xS}HvkTY$p>u|;@ch%G!s zc== zbsvwhfG3o~FD^iV;DP!#eAz4kSm&H}=-W4v6ieWxuR-I@QnP^}_+`?ikqcF9QK9w| z@6b~qjQ$1(6=J8%t_*C@`RC9a*Z+ij?!cKYZ5#HeSK zoAk>t5j=so)Mr%X&B}JCz5c=4#;%1dT4JBtTW9dDG+FvTDAJiVXeK=)gRHstvFU2S z8xv9Sxbug!!KZ{n<;&s2PW$`c%hQ*mcup%Y8k^kIG3MSSPekHA&Dtt?IGLwMdH3e& zaN3~IDLjb9OnzD8vF9cu3Z z)LuGu;k(RDT^ zGGy-uxZl~#g~&+L0TBn33>`&{dcclqmFE79a5{9I0dp#|8HS?H2!W;cT68x0B^yqn zV0~2KVGw6wpI;J+x^-i<;!SGpRv_3wZ2JGUZ~ z#2>{QfTEHjzPQYb%^grtiJl02@=|z%oMYu>XS-HxXK~8MyQ%OrS%IIJ@uka;sM4}P zdeuX$hK0LrpK#iM;fsEca+safUISJ~crf`7PvIB6VO99oVIy9*tGndau_vsR_S|a_ zGheoY$2IzZH;En%35n#Fgu~g`K=}`~cJd(1y7d+b68t3cl)$&{NixNtdIPH#pr9S^ zr1sMM2TsvEe0Gpgv}s;Hmdup)H5T)0!|-x0=`qoWGzTaplrGd)zd381;c$INAKKpK zYR>mmPr_Sv4sPQ1vYcgJ%tF}rU`{c>+d#We!<+l-P8?w;S6UvWIGY@wT#NZy+sIa6 zwv$KKBU}fChA9~aH|cC8XftDnZiYyL%|>;qY!lU6fM2h z8XKA-3Poiz0TZq|PpHv#s!Vq(Hv693Y_UW7{3hrz>T;XsDtf8jJl_ZP6kOtePQn-4 zk|bBaGxle(as;yhqV2oIunf(7c*;`s$~Z@H5mBB&UJkkAcuics{(K=LFJ89i=1Df$ zK4b@<)$yq~Y(6I$S+ZW}eZs`6t2lk8k`PAJns=?)*Qe(oc!krR4AriI*Y}A2Nr8LI;)Il9!MNXt9hd zW#;$yuf-q4&$^l&zdFB;aUMAFZipH z@0TJkwXWjFHTUqR1Nw7I?YDmV9&0krQpFz4PEJTMe(XWXtgHW4ZYlchty|o!e zExPR$T5!`$Fl_gA08UPXw~!fk7<3~l9rzmL?|1jI_lmEHx06oejon6Lie3qcqb&6l z@c1ZQOyj@mcOBKfOy%M$+ia9{Ik)edY_!x@}fTtdtkXbF8Y9Ki)a0nis@5vYO-x19{yWrC^lNGoJR>PWyn zm4s+ak;?4o*qM?9c%QTgB>R!@KD~`SBSQsE>^^dE+S8Po8d6AV6|qDfiTrMIXluep zWn1!@FDTd6x8@dWabn-SNgkVxa^xVNTGyu0yDIYHU=jjCW6t74W!4Xrld0}@)Rx_Z zkH(OW)Gg9#$z}Lw-m%V!-zOd&BkSbgHF_vUoba#|gNoW`CQ!53GPVdSVvNUt#A~9i zjrLnvzpP5aB+e2w_74N*Nc_}oRN-XKI}rw$bc{#D%Khn6d}2_m)`zH0Q*Dd`JWRyI z7;D+dXYnKZj?bATjKeLYiWARZOUo!e5s5wD-ZqgyA6Qi^vhLM!pC(o-+&g03%R*W_ zDScv!y6$3Jx7Jri{%J!gqfXT}m_;a~sXDJo)vA^O{h9WhY~u5c5dllZmRMC6Yt+xB z(wA|ZEvl~RMvgZT$a)zTa_FX zeP)DVssp+!ewY+$JYA1|48vXAD^SZ()J+jO+T2|o9As=FtE0!tlCr{lFRHq=`TE(9 z)9*M7CC@xuPBth7b&bARyPs{tzIxQlHmE?{PUc~~H%gjL);^}Dn9bsaA8WY0mQW{i zhL_pCmMj&4p;dqU$k>lV)Gfm^FY=a*fs4Z7kw5lvABj?C%e^x5PE4wI{`fBTcDaN? zSVt1XcyK2&=4QbTNR;hnf)%5^aS=xl>gnRE$oJFYU%>aBpTl~SyUAKpzo=A-6@Hq^ znYAOCC6lEiH>3-_XFvc_+Ngc>st=7)W`UM7ArK-zKrB<6{Gb`a zm9PA{;}Hw-K1RC0n>ZGe`k>pylA0Y%=^Be{k!j(JEzu{zEq-WbW*HxX}#!D?4f-4RENSvM>km#1VSl;J z)%&6>DnN_fRx^~KYpKL$0z&-NkWZx;JE;I=<%jz0;^s=hyXE}C8kXm0HhArNh zX|PdTAy?L^EYQ#`a?npv1sk=>Gem&e2=QO-t2t^pNNcdD+9ot!bIp}6JWHX`+vcZrIFj;>c*go}^-dp`+ ztyjk#t8lF{cJ`VCw)`m}lAZxz4yyzE z{Lra$T195Oy{C#@`B>+JMef4j7pf_|qr>_8b&5d*!h~Ey`7g{R1uN=-$G_lse{fi$ zs;jA)`QALJCum+IrxRbvtLs#LMl0b?nLiAAW8ft1gtFxP{9A%u3}|3R(uqH!!#Aq- zsB51hD`vd{+i7^6MCN^@PVS2^gS0tU$(#j#r@B=kjEpW0yZZ4dox?K4oE_&QvmYHN z-yuro&vrD7%Aoj_Pv&;r8W)zEKHkStUyhixm9c8GB-OOc(PpaIv`f=u=Ec5SHRVM4 zuW!$Ehv6WfOp8MK^p=xg)nhBq5gPQ`$PbM+o56M%y-XFBqGnZ_h zOO}yQNKnsnTP!5>0Cnx5mCxI8K*n1LLPEL*9R2n31dbsgX^8_STw^PTXDY5X#`Zer z%}>My_{U!9t^%jZ1A7)Yzi8my6S*5$;-|0w!pn~c9`H)%Edr;^qW-j?|6)CnCxNAa zQ2>watZnSs^=)ka(`C`#gzw4&bUbh_(J%DkvYSBe{+l+mw!82TBVOF)^AV}90uqud z9x4KE27J3;I5htpMu3PT&M^3O1D?oKPyhEs1H_dOjrdn9rKw)7^oK1Ukw!F5U(xIu zm-JsYYD6BRHhe+Ab&U&4QN{1JJ?4#^eY=zhun*)KuF5%-+0@B*(({O2G2Vh0+L zM%*F1q96EP(&zQj@BKnV9&yLzioX-^2Y)#U0+B{6k+10Lpi3H|U`AX4v7);K134PpjmR=J67~w>xL>?R$4Jtzx;Sj>#?&4${@{Z=T z6Qp|;AXS9O9IEbFH+Kz+R6ok#uCuTgIVf zwYM<&%0;RFt}p1}$gN-YmOs}fEkKV5w=E)|;%$=sXM9vdVEnjg3Q#tQI3rk7uU*58 z*j5l5+ddou60$9f0ddG#GV~m#gU6 zWD6n@BWO(@-L9y`HW;9q({P20jj$LWrS0>3{a)%zP}M;iT~*R2)%S0l`)$S^tc8!1 zqiq4-vlpe@^n+Q@zUg*EAPXG-Q_s(|wq_1gi4kvvUReRs zIWZ8Mm|sYm^>oS>b=gVQxyDLWfsO2fTzb(R(0c212S^F&EMah8vi}2eAL|u?Mw8yI-H&V?fxCY>AJ0Hieq&ft8GNy#^ zyQakCabjge8QSM_h_D4_gyU_zxSSH=F#HgrkiOk|kcDX}EGf0QYikjMJ+GQ9Sf5+`q$iSjlot4rCm}P8G;f?Fd3redA??`T^Od#q@qCKB z_3>HYB1YZ!sP3N4i|dsvW$ZNN#YDa0(W=#LSWq5gW(e8KkTNMdvOse|kDQF9oo$Qc zVUGp?Z;CWE!y^qHUAQFEtg*$6jcDxQ<9BI_w3U@7qp*W%iY(D|sqYaoZN|E7495O^ z+!v5<0j#F4%qs)*2NKAVdDxsflUa~A=H@uTnJ9<7NS)=xy)n@Fk615MH0Sm6L=$3q z=Qc8=f@L>2?)4+9=IYqSzO&tarW14Qlc;0DiFgYMGwu+DsK{(Fd8@)b26aMY%RT0N z^aEJtJmIr@sSVu16D?`^SE|IO9G*gyUi8`pQOXBGP8qKG3($5=a09wmhFuwv#q2)Q zT*UfQVukd+EzlOkU>Y#w{xQr>Pw%LEe$O!O+}$YOC!G@HyU8tY7^TBL8he17Qc~Pa zdMNa*FlS=K1{fxiyU>x!7H)xAU-04e&%>|YRY-ewvM*<+;3FC>MVxMz@kkkwOEO3I zq5YD&7~GBNS(EY9_{5Gp4Pfj{CR{(^T9>KMP-F#jZG$ zFGJI;BV*i|n760cR@``t?$sRWbt#>3Azayg+h5?It*K-Ol&hmjf*B}$eLWk;e(Jlm z*G?Q)3tP}RgBr@hn}{kQfJd^-A9LCW@9s$HCWe%`V~3wSxzQOrw17!aNOBS_)4*tf z;$mC8dTvVk5#XjR!Y}(mgR?iQgjRIo22sjzT!*2%^pU1-ICmyMKdwPMGF-$K8`EQFk(tOLhz)F3~jN8_Z?#5XB}<5QBa|YaTRM3 za)+g4&)YI1f1XO6ko#dtU}Fs}4LOk|2Cqo8`laPeocM=(r2R4?$&luh2|@inq=HAj z;_XQvd|-Il4}>D<+*y4ah%gEIp!9fs%OK=__{ymAjK!nxAHm5!I7^u15iJ(JIfMn| z6~_~q8-N~liE*cg;kvUwY{Fma256kFfQX~mXwkbDxly^`0z3P5;(&5?8`Tn=bRGx4 z;*a=b!t78g<{XSB!N>feqvo$lH4Vn0ah6F;?d02AuNn*zRyulpgs9mRB|1DY&~tlc zs|zQ{zj1C+Si=MD*y+xDvkZKzxeF7yrq+cHc{o!P{s(uK<&%E zimXq1CzT_4?o52*k(Zw&c{uBp6-#D-UoXyzK0Qn{lFbgYLE3O4j)I-*Oqv>Ff;%W` z>ySoof&OWL$f3e!tf3FN+uf_-r{P4EH|k0e&)DEB0FxzTIYr5J*?}5vABbN>QAnKm zQFjMOge1ab(QwL3Ud3$HbzFIiu%{oK~xPf`pwvIQ512kq{{2n$r=HhF5>%u54X>decy+P+Gz zvLtpJ*<~0`O?OhQA9Z}%M0z3NP|52f(h+UiuUFrx>Vpsd1s|`K2tjn!){;8PA8MD% z*`Vy5JZ%~PKrrD%2ZBXy10I-oZoh5}JRgEVUqL03iWl&%%;>kvBif9h zIoDUw{syDwLUu$MNv@qZxay!qsh~Yaz@fBUB*a!vF4|EqrIb>Go6SIKdJa`iZS%BA z1(vuS36cc7FD_HL)-LN-2M1N8(r;^gpGKxDO%6O)HxMu$+w}H0C)h%0dmXXtQZ<;_ zvIx}A+_}(3#-G%!KpCWZ=;a4K>&|mRXZtlb!Q6DoI=aCt-_4BdWfg68z~g_3u?)qo zPOJsJS~|84jHj>dOo{HcF$By%YUVgSQvm4d)rPhdiuJx0EtQ@+L@hMSe192+)C33)5EGhuj3~4oM7E%GI6;#1-jSmp*pie6$ovABd;ah5KT!9RSLc?@|L6kTzi@M zm=3UF5mmoU6p_YbwK^qh@?m}^; zP)o#VW|wR~EMb>c>BG6*`xN}BV98hu&ZTAtbw0q5QNsd|ub@A61mNZJN&Kw04Fi7MfYE~sS!%x)>e!KvCJeJ;A=K-Rn9KmkNgKgzOfJ9xzBwXaYy2dyi(fbIi zS|x|zkCP37N~f|Mj>n)*?$wWKh$EtEb2R; z|HHCRokvOqQo~^LSIuI|r{DqCR!mA@hc0P|&tPO`{Ti2B$8n7sCcqyRVDG6aI^lLC zfBR| zP2zMT{-rzf- zJK~8XCm}>-0xnCIFIuP<3Yx|`1Z4>8MAbk*ivmWzIfME*l0qj9eO(WgztW_ls6X(s z4wy;{XeO9q(}KOw^msrcip(GY5Fn`88U+!}VUI-$mP0K!jZ^4Jp8@w(CdIo^IEFeI z>+23!Y^&3pPW~=s@`jTol@nPb#eFl`!5jEO*1W3&`A7nx{Je{s_UacjsG@sNgiLb& zpGH+2Y+&C={V?MA5 z2Tcx)(#-L@7ShP&V2d8uvK477_ZNCFevkowF?HY`kLq8LUEri#PJr8uExBfL&<9b_ z_*KXENzJWolL4g%dvH6W0EN~o2=(LCT7ccm2UR5QtlIcM_o{X5 zAB#{?CG@bcMG3ng6HoV_@DuCg%p)5e%_f~YOOi2aC8pSG>Y9NQFtaz_D-buQ^iWhf z#QlFT-Asn6tKFe3$%Aox8L)5h^XwgWpG%Wy6D_LRi_Qjq>C$Y*&$1U*3_ira+J#|P z*aR~56NVJxGO}3gsHEaXr`#I*KzG&#Ir=;StEk45d0<6fI5VQScu_ca^X+zj{dhm* zrE5Dwg-hB2CqZL{j_M>21^1=E$H?5V3*eHP`a{mKV)5<SnfMX&Y`)n=L^i)+H z`#JXq`asp-qTfVPHOFLRfGjE9QN`0Nmv)1pB<1EbU$p%TTzQAC>(gxvP{Z=i4xK?~ z*h5?2pyM*Z0n5WRKnYeBM4BOcsiHUR^-};%-_u#D#4|`r5f+mv1+c9Mc!)*UgihUI zY)eFSXl8WxEb>%$+@vLEJ@~1GpTEtgvh&b42!@27+acd+g{ zj_=qaK^uCTUKChS78`PS_h|!Pv;s}SNTA<)Xg|?v^57DCN>)M(*@ZM$05j!p-f7w` zBeo>EhMP{rTtH-+g4`r=%+*osIMDO;1q78~r0Q$9`TSzXc`YmFw+Qon7IBkd7b2@^ zD@pmD*JVHo++uv-@SLqh2~e9nx<_W!H=V2O2qu`C`xlMUw4rN^KYKTW=gqB;hfucn?f}h!M{gp z(5tUe-gWuZ=vJb5KX!53{@nT${&Yig1G3wnyYu5*f8i)i{n0{x7a9wUBie1b-7p%% znv27%ORK+Ls;u0jh6QN%fnWie z%B)$0%o<`q!yPCg?o@==1Ys;u4XKVrY-vRkm%P**^3mRQP?6x(!H=i7!;GAe+H}^j zj^G90LHlY{3nxSf)tSp-*x}~>0GzLHp4#gAB1dnKrDubD{xizhrl!6{*sZL3DZRad zBbIV(xcBNErQNmq7e8>_9Wi{TK%_Nz>}KxhEYA7)k&DxVB?~?Mi0&@6ZRXDPP*qzQ zkP&a@eepnk6!MS@SdBb!2%C-1POGBJQRGfiw}NwPnmOF>q)Pqk1+R+j=BN&RcF1FY zD7xuB4oPgn@h(P6r_v?G7JSao2T&=CotIvobGxeJxWq-bE}4OMq|{jtH^W7HTrQOF$GO*LvzN>uAS#KhLMl9dkBP6ooqJy7yS?Nov!t2WJ2G>!jw0?rzKkXBo>Q8NzXUamHx(vrcHCmM|1osr61$)t}@&`dVWATqm*jBx4`S84%^T zKLhAREY0EWa#tc)H*qtHa20!~8GwEG07a{&p=!4|TTU^!Xay%RXoS*c6Aa6nd(~a7s7tr* ze#(kdB4|d{NHQ-C13$5!Kq~1cRqMUkPUyB-X!8L3qZ)hRWBt?p2ZF3R5;3rt!Jg!43uWYvip#`fWKhb*fei)m`mMb$AV5Vb3*Gv9cSeK-jwpi17zC`@IuI;V*Jn2^ z)+7T!VP-on>rIClxLBBTD#r6`{Lh;B3%yQQ8((}ab4qQy+3NNV+(NY>RU6$O@2=v4 z`MNIMU6xeQLq3zX7c4Eo$JR7VQ074jSOeAm(RxJqtARxp$|-OJt~OQ&xg1Y#SNH9X zxJamoYP0${DpchO#Zg)6bmPcnc^ABEPN-9%@g(NB}&K2;feh3I}cdcNq7IeF6 zY1KdV_0~1)yz+Z$QL+GSnI5{}E!rz51~BQ((;Z8^68mBX?eYu~m{)(RB%OGv*Iuq) zds}t30wn(7^@_^vF4Mnx+t18e`_af{dl3I~$)BQgDte`Ugvn-qj1FI;Ep6Ax<@}bi znp_2MeeWV5DEjWgK+A?}z&OB)QqZ1_-*!Z!3roKB0%!5z?Q;k=)S*&TopI3D8(yuB zUUZK}VV>EgSPO#)RviPFHZ(u$3;wSy?F~&Expj54Pb$?%DY44P?F(ac;t>bcI|Gv6 zFMukNFv3Zs344Mac|gG`XoNEDBqY)}teXasmqpo?-nqqwr+V4~Hj(PDK$?PW-ZlKeC z9bEJboJ~Y8*X&%(y9p0Jl6)-B0xAGNQBa8hid!ZQldaexp*_HjWD9|GYtPVx^@bRk zBpm!s*qisNVZA#RU{$3qE}gs)?_-QAWib;a?F?e9=`p(YOP!-rDZT5Q%+A_mnN8wZ z?weWW9k%jkU^I7uW;1Y}`HaY6e0?BRkCpWSuU6eLr5=nNl~hL7ka!l%%=CKh{Yn^{ zHW6^9O#$tL7=pX>$>X)zGoEL6T_TFcT}RHzt8#ZhXZsQDHha54s$1*vrc&AUrK{iD z&Gl7DpJNGYV;9mJiO#IM`0~sar?M_$3%OHK6eLur*5(oJw{`Lx2aJmb!7zO1)e&P9 zDRdkP=!-^Zc;^E2y>mEU6t?;&Lt8z-igV@SwB(NzDwLiS8gCec_E{7lW`V_|hG9R7 z+0Y9fcmMK0wtoD+%FaRvNep77P<*loBg_^n{;FF0p%ZIWAglr`{tOVd3BU~JQ`kbN ztxqJ|)bwdj&`Iz`Ec1GYtP>-%9N4h6RRUVBVn2+}B^3ph`-D3;zQ*1Ms36M#*RIodSU(tY z#!Sm@SGl!6D25XK_WM*n#gIYDfF%im&*Sn&Z+?=B^&}f%(Hi*Mh*+S+ZY4)7gWX*} zZqh5Tmsu4SW9U(~VA4`6Acf!w)-4;(`r^FHVKm4)>qtWh)iw5w0pT=Z=5vL&bOj+Z z68-iJv4q>JWi650o(RM*S@ToAIi%fmah}2DB0CNl$R6r8Fb_l5;K+!M!;VEf0e@bt zPHa5_;x}MtuNj8L@5-w9Aq+2%y0k9Pljkm0J62hwr-d-XWIiP_g)9i&w$YimZt#n} zw!HK6rHng$JpqJftTG$x%bL+1Fiu4&1NmG)(s(X(Sb?2oRUmLSLuO(e4+Uq z98As{oXZ4^CQ%0)Qt>7WCtnSzzkeDI2E1QogB%fR)4?u@wcUc^YIl)_vPq>s1#DLn zYc>@)s}pH?+#y1ZgVn=n2eSFI&BM{w(dVYyy9ePcgOl)sZ>>eUA^eHfSCG5TDpZv* zuQ3XrDQgVS(6rLrYDk)r{%!&CX_O&ET~lHgR<$hZ*@eaiqcIc%DbPxyL&7vB4UIGA zk%nIWe*M;sp)}9+zD`u<5IL0b7FI~F0qE&-urI4wi`NF0nL$?qi#jXW_P67QBai^J zXM30~^PF!8^`=E-cqb1tybiMg?AlF}xT!Ae<#m z5H4eDukMW{(~664aVjWPULGqoBjuX|V6HrM`sz8n>v+S*&)Xfa8(UZ&7x|7z-A{Pe zR`(A`T@JnE;|EuwwC=+Wp%i9R0mQ zC&~#&j?8S_6k=K|)3e`nf}J2!cYjc(F5~N@e!LEbbg3L9e$2Wge^Tx~h-~%reiV?c zu!Q#k>**#O1W%K8BgtZSEugB)MpOMk2s{G3d(F(ZLYffq$bp^ z2^_NV0i+rSGj@_Y{HB;hoXsi~?Pd@1b<-~8B>O;VX|t=E)t7Z2jaZX5 zXIY|T=RZ$>TxN2Ai$j5oP_7AQOGBTl)jtC32?OQ}XfFVmiU#KYVjEWI%-SawV~in* zr_-15N4WCGpt9~|qQzbbsjjnt5I>JCxX950)s;<_z$H`S5BO<9Yxd(ZSkH^5z#weh z^`R6rT}}1}Fc9&0T}}Ly69n-H2i-jlqG-#Q(U%0pT?-N%n`QB8d}ADOWF$lFH&%-l zvLgG>RnaEBp^+a3qrr=d!Q>Tewvb9kx|bVRQF9UBU*Z=)X|n4KwCdS#U|uSq9?TO%YT!0`M|AM{UjnQ zIB3YLwL?H2T6gLE+f^%I1Q#0 z*6NPKCX0+;6v+GUAQ2p9hlJ6nM6Q6OMb8{zy7Lxzc{OmA+XkF@twP+qYkIGL_I~Yz zo7V!$nDwG{9Q^vUYr~&;yA8>}`-Bs`6lT!)$;lY>ZEA7Xh76$L%PWt(`H1ppJ`kdG4 zVEONAasV6v02=@T@NWg=-&^|sT0{P$u?PGKj}r1k+6{c73l$y9 zhnXJ3kWu{y7^Ur9*j&O(?BK#C9mYACnRfgZciyQdS8Hwav%{)OF1`|xo3sEA0p6W9 zGE|MhLr%{YH@qzVCa^hBNE%GuI8R&eLbt%A9KE9bB&AHJEm9(8g-@aK7K^jgdk%P6 zPm+Kuv^^R1LgTZKGG76~x`LTU+;R5=Co+9hsNk(k2>$B^&17}|n)=oV92;G87E2va zk&96S&~m0e*4sw-*zwDL4=Eo7TPd}fo=G|}rk^4hNSH^Dg((~LNB_rnxmFz%z5yqg z4{2Y?#tKmOfeJ^Hj*MY-_NmiyG(rnpA;ya=od|1qh6Lo^=W;3vWj2%ACpGe(6Vfo36@W`Zx0hTn52Rjv@nZs^lflb60EkC(J+Yf z#3e9iRH=w0P2!WNh%gbvV-O-G@(h_?-D(fTNWqq{KX~=KN7iiY_vvHNsI&v?Kxq*q z>KX6t4WH44Qjfu^!}4E15Y*Y9!pe6He?w_hgXIqqBbc+`)08DlW3*<9X2MG>FNpWW zH|2_AVUw|P?+7ZwB}K@?{CHO88m#lA+K1^y$~tG6U~&Q}ZM9@z-?Kwz2C9hd4SY82 zXE7u!^+0Gvsds=L8W8ne8q-=F(CoJ0gv^XvM{DU2xYu{I(bQlWc58Mg7VjfxOIeOD z<=l2)Qh6cMXGpDhgJ%{xY$Yl#~csKuY^Q`~h$R^yDAr?AB z&uJE7emMZfxe{W94cmBKS;w<{_~ONaQ^J+ZEnD52h^TyO`**w2B*swmkO$<_~vf-q4RCjD>~4YybzMsaUrYX zm@mbqX%Iv@q?w%or3QOOWL@j`)N+(2>=}g$UHMiuix~}3U{*Bx;&%erlDvvefVOkk z^NYIt3QI6c`54{V3fubpm+arye*7B!W*_OB{r@};Vq$CjU(naGTgON7-zK}@Q|~N$ zPOMriR!p-wU2n4G^EbrSVnGLtP4eTr_Hj{Ny&SmJhp^|J4sqJ~+!bUxk}j6ySO>nE z28A<77o*_1=)2O}M$_6Mk5Lb1oF2v1jsSuWo5{P3imwZIkV!T|kPZpy^@=yA5rjaM zg%CN7Eme0MGV%%I2;q)ZlGg7t0r6ROn#hgEMlPB3)kEMS0=2~MjxG{GLKR&BN<$zM zw{$$g*fJi4y4-PvxHa>S4(VnS22E11GH=H6Pi^EEtpc=QiE|!kd*DvwBepqA2qduJ zieqM%b$0Llg+D1htgTo)rRNcdGPbNu4Wo{q(_O4lSsQv%^8-e5^$aE^T3ISzfH|Ws zAX$tUx-hi=2|tbc=K*?krwY%{{j<^({M9y_DOWCWT27_*gcKuhwFo?pX$ph(d#q$) zCf|K&k_z)G;2bHYBne=So+E#zsZ5uUYthoR9?A83C?Rm{B3~S?OqI+Yx%&;oxu$Sc z!)PeKt5tKsDacFK+=I+qr>=ydGQelxUaX{qG?9UoC7AGz^OkI9V@;N9O3KtE*VqTq zrQX)@djJJq$X)Ol+?M)3JgJe+$7RmUFq%tDWVrYS(ZRdzpz^M)!OH32Q z3)wA}0cvYe1?)c53O!P}QL(AM(T~4V&ii?DN$XC7uh0n|7))}JiKR)(*u6O!Xy>F% znHf*cz71y_Vx^X2w1uK$Uom$5PRZ@hq-)_uJqaRqyI8(b@GB*ZJYoRxyfzS%@q)!} z%2_a?cBt#+7>wuH=Grzjo&bksjk=8(+rvy`Cb-4D6TJ4*&9N^J*u`wZAo+~5U1{-G ze2W5*fl-ADBZ3IG7%AChlwVqk3I$nclo7pH33t|vJ!P=bJ3SyQg0*Ln%u)*@8+O1cjCdf}G+mRc>Th+9|B!T054d$ai=a=kMc zJ(aQ%a^r#mCXFQGOesPqPLi5QT~F-Y>!}OhZ<*G+Kq_uiD6;Z^_@DA@({lP<@nuCNzJ+ zJ?|*KHc13KF9CzBZ9!_lq#~*h&Nqb!X}tyI1F9dR^OKfgqmGgB`%$gBD=k;~@nKezp7&Cn-8O1v$l(l2i)q*`nq2FyW#ie@*1292ImgOi_1AkZL*(qbP?j* zefW}LbTHqHbuz^0&gG6q%?Sq^?y5IduOWasGV86$96c)wa6(Nx4;h|@Dr{L~j*?n|URHO{n z<0ubu@Pi)|%mn0}jkp1CoT4N7jMwi}V*Dc*Ex;~f7x&T+3r#g*7>rNoVuP0lS9SeD zNWG@NY9XcS0bCWi&1fF4gFX_sr+OJ)p~G39g0}7KBL4%2@+$3Z`(F~uV8}`QZwO`l z2ccR2K`8!~mQdn62unqaYR+r>1h~Bovb;}{P2&CfRr=eoD&UmR`N}t;9Bkfheu&)c z3`Wo-PX^vNA%IE3bAq@}yR~_8YU}iMa8i-K60PyN8GdZx@%puMm2t2Ds!L_%vaE*v za^e&jP0WV0t`!l8ZlH8p7KSWt_=yjblTu)gI;6m|Ac|;8gZ=nljvHM$3V=P(f+Lz|K5l$#ByNE9`SD)3v74n|B zATv*4V>;pP43icK5Mvvkdx90%bPO459$Fn1AW}bRH?`()ivj+;TI&%C^2A?JL&{FBBMApd}R&~o$iI*1Lgj@pGeFU-m)xRb_}eWZmdGc#Sr8idAy34aN481>{e z!>qwWw&TR#&JaP!=Uu(Pm#OkMPxUj##V@;6Ge5h`K1(RymEiM{!S~VnP+3~>SMTKU z9dx+Jd~e|L-uy`auTbGfAr_M~%grM>rm?oXS;lJ`2JYSuxq*Jt#g!ei@*J1gzNSb7L6?WJARF2O$J!oY@Z3?FdQ zx(Wt%WIJS(02QH~33ujQI0sCnmT0TUjkjND3MPD3=Q;P~O|_xJfei*D{SHszx-?8J zEsH|lYMFQ>V;rb0*I4t(0eB_!7Nwb&y=C+IOw|NOaoF~UKR>rcLV%?%{Hib1LkmFR z67+^*8x>zh2I(;nWWvm;L&cx(CN&|$U~J)Y_cLRi1_Gwaz^Fa_VGDru&{&ML7!WL~ z)*TuGOjMl4%h_d@I9ah*fWOcd00&>l2MsmO?1PxD>ZGmroUv`jWOs)k-Jw`>e-;N)(B`>|#{MqqdK?>=V#Qvl$bq{ar7;MOOL>D1 zzUNg?mqc!pc)nz7>f`!($~4`p~Xlf&9ekFRfzjrWW?wc%uOKG#@AjkL3Z@ln}QB?N(x;&}b9Jk&CL28N~~tU zynYy+-nEy6kYXuSS4)%~t3eRnd;xrl%zp>+$N;YyBSw>`tP)&P7AT++*McSHJ}yj4 zj%ezw_|2kNB(Et$^t1%|imXjpx!07CWdzOl6#0*O&w(YFeu&LuX8mM0Fq8T+Ms`Fi z|JoYTPQV+}mg^ku4j%ZZ8 z8yRB2gqI32qX`kedze-S3xTzQ%Q?!7{Mzk3R02Na>CK!Cpo_|^ufPazTD)Li?QN*! zFj2-Pv%>iScOKvubpr6m8^y4@+@-BA!%dyE#eow}Z3Kct%i1AQMv0k(0doby@=t>h zHFC7X{5{i1@!of;^}p&w7IJB?mhZ91s7g)U6!o&udK}u!60NHYykaGA%1X!kJD|F< z{|A6R{SDCQZ$LpU&0gLQED(di{|2Z_)zc!lDp8x!@V1*T0(S25Sy{dY=RC;Kq^s+! zZ&c*l!RNo0&{`R-*ncAQ?=h6=?h0k?|xPf5p(i z?-+XcR}3}L5cn&GDj@xPlNbGOCT|P~kxL&jQod;Y8_Yw6Xx0-ELODj9Z9@FZy={t7 zk&$$hmZ*BT_=0hsJZF%-FU1szmB|Fe^?Ct2q4%)lw;I6G3hE3f-3AqXk<4Evukm-2 zpZgD!7x}+4dAoma@^=4QlaG%E`0;;W^1=<4F3){g@an0YnKJ{;41!x|T-iqJs4_B> zq^yCdZMX<$AbXL{ACpY0++~~2y=n~M1-#w0@;#U;r@5+~agV=S&l`K#ru11r`K$#W zeGGl~)rH8=iG4UFO|F^1MdZ5ymG$LCdi{h5-wHAtq?#}9$})^M8TKDcoOsG`^s?NX1bPkhlfmIJXHc ze%xOF_*XAVcFuM)|HkD11)**rx^)AC)5VWXN$a%m`49%_-zL8@z5W#5RWb!|6G9@z zo98NP?SWn?Lp!tue+9H%6gWzZe$@4KcCH<>0@#u^tPN1{b0a%7DnnK@>#0Edz$k|hkb%iy2D z7`?qw8*G}nXQ*MgHc{e%wRx=(2r?noW890-`~#Tp7|MuUsG6jJ=vWDs|3Lj6L*rTR z{vy;I*MfuC<|si`Xxw$^jSOt{g9W;v)oYRik%wBsAtB)%m* zJ9;hCx5-QGR2efGM72qiYmvZ;Tz&{fwYBIqeQT)p>aviPz-rsKhI$FxrDCqHoHBG* zP0A&rd~2xZe`qMRnvL6z)3=7Q#7}s^wb?JdUsd)e{7pkE`)2`1Mr(~mG)mrm3^Ag= zHM9xM=r0W|`qof4@EJ#$nP0o#+e!eb!2H6;qkruTEW!x4pSosy8*BWvGZ35P77Hu( z*xNI74`q5IqYZ?E&`np#mm#L#@<&;86_byVHXY^i_f?5!|8(lb3KjwH7K)=_myE2o@_ zRtJg_!Vf@zAU42bt+Vr0Plf$yFx7n?$FM@Y^#RYJvT5leQNkWOqTe5_ubg(3{d$_M zR2Hk*?r=s}FpYT_Sl4v+y3D=Ycaj`J9MgUowBS&3!6{hCESy}@c2-koZ@2kA0M0h+ zg1HC6g`XFBR(>|DNL!;;We7DOu5ARJ-MwUSUZH1@soF-WSR+^kgzPW-hw_<-;#a535Jf6cO+(Ao?(r~Bb zO`sD-NMKNS=J`5Pz168e6I{MQq?P#NtVf#`9pWe;b#Ja1;a*gr;}ZRq#eZC9qr6%- zgcp{7f#RHbPjUD)rgRz3(Hb(Dg{Ga0rpNS;)T>p;E7z2-Jli!^GRs+q7q*+|Kxn{n z{?FBwzNgOo?FvkH*zwFM9B-fBR-i<{wE_$zC*J6r*3Iiv*gE{C(OWJ3TvMgXScliJ zm70+?(!42ehBN|k7_IlpnFom?9Z2-uUpBsK!}u?rz@*m+%P+7p7) z7yx^>e;LL1M!G8@M%&qBXYdb;p5xtH;@MQON~x}sCVKnhtuc&4Vco7`I-hxZv=gz9eRE(c>Gum;Nh7Xj_V1%liYDsoI)$5og>IZLyzqBqGEoS zm<2xm3?fJ5gDW1_39vJ0p;K(dSA+2?wnkYtm-tMoDttpgTxFtALK& z^mfjJqe=5amV{NeFM{ZhhEY%EomFT` z@tuJH60_~;7GZ>C%9s{SEgklm7wA3_eq6iA@Q-yvM$8+MMr4KvTk$+&A$gjm^%oi$ z%&DF}HO$)mQrmHKjQ5n?DmRP9-@q>xIw+I&z1Wm`y5}{@T!*7xKO?V6+p`D~rr3~1 zDxr_wyx7!V3n zN+m+fd<|F1uM$5gOAUL1F!4dm@2|kX#KQ+S3Wx*r>D~fLNgQ$5nXjmR@pJ6@5S49@ z>kc7$(3Tpz1xly!ZpiD>OXl>E&ah4~xB2)cQyT;c+WDsbLW#Qd^@nD&ix zC05f#=qI$)=K4!ZmJF`IK74_;&b)PS>Fh!>x|p_8UY&gkTg9lXL`#_qR{jY?E(dX^ zf3gLW?5B%k!eDG8=zlY|v*qD0C!GlkzTT1D(`$l+h@|1DHIH zpQBj6N|b9>>j}Ld_sr`)8`z}EJe_k2>53coi_bpCtnN>P@)^qAFNVt%YZtpDJLalX zJYz(s@L0UKJD3Y*#ae< z0NUD@t&}}K*7E1A<6iq#@D{^x{hh?L_c_>Ghd3$iKEJD=f8H^7?j2q*1_b~J`7ZVU zvnX;hadx(_HTzpf+N8Gaw8oC$+mrEzhmjknZ?uO{+B7gjYU6k@IQF2?Vlqo0g)0#{ zJN2G!eaX5T-Xf3W(wv zWx_UKu$_{Nf=;qnT##I$!*=Gb&{bwY>_W26KxU3YoU(2I*F33E?h*0O1D_dzZeuM= z#!V9?y^`rjO)rY~j&zMU1&-EDqcuPc_Sm6V*b2(;6C0l^)R9fcgbNzoyho|6gM=aNp}xl+7d2o=*I1eR(O*+4+K_!zsX9g=4d}a| zN0p%kXdbv!#l3k2)C?u1U8N{Q_I_O2P|Ey9&PYZ{yVxqqJo&_`oggK!f#k5+LYN68 zA{|z=qiWMAg-ii0y|23Q)^H!*N7S(oUwwiJ_a+y)i>B{RLy9UHkI#tEVC`VyYTp(* zjhaRdx{LCNmd$x54XzK>rNe(#ugnn~AVAC;H9$u{E3$qQmtt9e^XsjP#&oqY?#ey3 zB=&H0wwv3NWEaA{Z)mT&OGp?|$^oXV(7i!vcB^WzraNA`+8W~l(2ikk2U2o5Q7|Ot zVZgjMk{3d_94-bGlHa@K^f?RAyIz{G%PoB8|Z_R?>mwW*vA=cdFQ*p?9rO^`FgI&LYddF(*?q zoTI$RPr54E9s+N?ZFv^WgPwuy7;0whqk{!dbYx`-KIl(7KHa0WX**Zh*+aBF1dem= z^%<{8qpFfdC$nZ`8@jx6PlzLj$$YY8-*%a~LnaRen%FIvdhSO)%43~zwhhb4d=&wi39VOa&i?qG>H(;APwN;=((Go5hAJv19DubOhTO+ zOWnk&e{5+AWheJTKN-xFY->?(4NPcgI#bD8M9ow@8>nj`f#o#XJ8F>BHyYAoVe?7X%u!%ABKR6wn$Uq*4F@fhmFstDmL7U z*;Hn=)=r^#n_Qm_dlOIS#yr3*HKxDtSMXAsFGqD! ziD-t zkc>gy7n_ymj{Vlm%5=LBS2NQ5#ceuEWW71kE;dYh)^jHUFHI2u6XPZ_4?~Q;#M*HjjpPH(_681d1GYi2p*JwtRL;`E<|Q?;I)=bx&g?}9dQYb zpD3CaQ&3E)sj3!gpC^-=1OP>#O;B1E6TXpuued?188UqFq=G9Vx%588FyJKQVI=?P z;mzvp^Jm%iu1EQr(OsNQpl3tozWbC-B&|(J5BHY0!^tC+ixF{;?>Jbpc6o z&TYX7UIxdBk^Bu_6mRFVRSx(-P^dAPT6?_^$m3P0dno}aS<^Cn9u6h1AVm`*Wkb@( zad*Y7x9Z!tV)|Iu10rdis7*`2mEmbnFW=xPt=)WZ6IoN7dwyS(Q-3n>hIIR7XqAZl zr`I7C)CBT;DkPV*P>0#&jfZOhUe_9FkrZW>PF912^#m8(V6pxw0G>;METrQ0j#AK> zml|cLJCbkf35k_nbT}`H0Jh&UghBd>LuCQ;Ck&CZbg#P42t2OEi1`KB{+N82;k=Rl zWk_dG25!4BnQ=u#>m-$JpTD?XAC97y7Lm(-b#b~}HFL?QLAxY|3qTxm_t#)6XFl0I zkukWrlgMUYRsNoZ66LnRvm0gU$Rm&qvoR_lfI5dN%zyrwmKQW2L`|rkxPo#R6ZJKw z?#dmPs?dw@6yxNOcZ1*lh4m}YCbA~c*S7m?KBjs3>k$F0hkZHLmlVT-_SieiW*)nq z*G%~qt`~dcM^ZYnj>lrVP6$b1alr;UhfadJxvjUFI%Ep#GK;PbN2g}NeLd3^#)IrJ z7-=1RS95Kbo$~Up+Zx5wk-O*Ci}-lnsm{BaGI82e(JOU$#h@LFxG-q~ca3y(x@-11 zisYBmJ4Q$LKp(F})hO@IHji#V@_H^D-!C^ed-atypUH`Od@5b5TeYE&L?{zie!cE^ zaNR+IbAgmf_Tho6{v<7aEl5>8VaL*A_Ju<_!Za(>D#LkQ{tgF%=`f{ zjnWqyB}Lk}vN$Erl6V49Zr1*m2&!q zwH2w-IuWLn+h7AOR5Wf0lRd8E^rL)_Wq0bhjx~l;Z()OFFmPPQ>qlydKbgd!%AMG# zlABDmCid1jt`Nr@Nh9r-w|e{}0XGClWbmSB01`AIl7Lsx{_wOz2Q&d)qx`R+OA?;) zno{Jq!8>wH_c%^_4r%G~mlGimj4c%eZSK6}K>=oKQwMKaXhc4v^EBkXA|U-9hw#N*yqxG;DwDv`ag+EdRb@^{K-UF34p{|SsE@YGk}adCQEv)T zj0T{qwIn%3&BCW7Mq{kOE1X2v959E&Fu2DQpcY?JGK3*I3f&U`4}}i>tHn}IUST9- z^S9v{=RhTMQ>-6%cSE4b@zV5qq!>l=Z z`-f!#wHzGkJT&`zWdSm2czZPn`V9UWU@T+|u|fmTJsmGhkMn~H*4W2Z{42@zQW(~! zwyPSsS7W1nMQ%~U-8l9G+iEkiA1;(0ygMne1q>5(b9A!OvG8XkNbo7zkCSVCPtw3v zLX-JnwbwVRN3aYtjkY96u0b*e^!xDs#H4m~W#Gq1OuF&oP!85Z*<7ra+I!nfP66Dd zVpK)#{XB@|8oos1Vkgozi(@pWl8;kGL1->qXx2@Agj1LCWvHf1kzwH#ENiF_z6z`p zlR=nduD|8pJAVX@q^t}?8GLtJ4ZDR}F`GzF{7Dr*DdKAa70$xp*Vt-R>xD)qUYlt&vb*ZVg7H#6n*Kr^)qje-EhgE(#P)ShG=Ecf; z|ME#vUY4Sv>KRk*TlN&}k5mlH)mQ~ZwOaJsYzj%F%Uk069c7jws*a=cWq=`><);O{ z_^NZTTD(Bn7+Xc+LU(@avhIpwTtN`hzrEs!A&nl9fEBc0vmhYA6VJcBRXI6(*!-=R zyRSRsxY2{&nooP=w@$QsA1>TasI~ z;YEI`l1JXGN66rF3BL%)u|Q2rhO)XZWJ2FVlJ0NzlAeKl?l%wkP?jgyjQO|^&jl9_ zVAv2mx?+nrmKO%js6tvu1|%_J5~;q|yS=VjPS6M4yM0_CGQHN7sd@!LF*nA?cQ|zF z?ND767tG2-HD~TEgn6GfYi{S}Hp$9tNr)wo zRv9g@z2+zvr!#ky!-fOk>o>EB!M5OhGivKjz@YSOoE|eRy_djFPNOp58!F^zYB0^n z72AW1b5FH|5W?Ag=?z=d48$#$A3p>$;x`7AjZuSr_yHD_aC@$7Y?N>*F53tiNKt95 zuH_7i5D@v0w1#H8#S@i9_4-d`NQ$Pma0MVog)cPpv&; zlFPPIZk&eW_>gQPC2&|=7n>w3<;0#zh+!=fDaLM6;ghEF%#dFR9@F(P?mkv zC?i*~Q&ZYlASe&b*;YDRc}&wYg-Md$ZTY3%=v(PlO&ee35}oMiLh%@I^@w{>uE=5Y zA`iNA2M`b8lav^98r#YzbZM;^F(TOxjJ{ll2VV{W5qA1|Iq&ny%%5GR3$PRRIuGlW zQ)fw9_L}sN-Pnr9)!|1>FSK#18(MM%c_#rz0s~{hLb<|+ZABWRHB~^#(G6VR2`$kE zS`uu0>`m_zpg!5T7gl{#BnyenXhXV(%~fCWQnwLCEJ@63(#9mn%2j+0;N(#}+0Mr$QJHzFlKksg zwIn7l574tjcLmb7E1y2SVoR)~)0}X&<&s^VNMy2jl_jz20gaWr!cybjMVY6}lS?Bj zyvtgC=ifu0DO8y%&)2mTpySbru~}z+i4D+;?V!!*XZa`@!;C7#Pi6Q8tUH2fcfE!^ z(0D5q06hY9kl{CMIl(kSEcSYnVbGWWiPwr5yHd=*#dpjTbum${e`WZ^t9x zb3Fu3NC78s22L;8CD2EyhB1xs+#Jjt${90`0Gw@m6Ll%A=4x1DHCd4vC8SD~DidKG zBO0I8E}HK$U>YH}O^nY)R|T@yVZRt$AR~B9=9E>4jTX zre?GpE04TSh-+4>MlAeb(MzGXi}1#+CZ^dds07A-cziJB>8>Ha=rJL8+6U*GKJPs> zSG!q);-*g=Rk1T$_!6ik@%i$1IcvCXNZ|2wzmavEJIdHsg#0aFgKv{(`b+GJa(s0x zXwLg3Ilgj@5=_>eg1(M4dh?6$Vd6#rf;8yZ^}<9B>?I;asJY(z4!oF!q-iGAdj;v; zbCx>NVwn8lvtV;}oZyY+YAAI;9It!}|4B7p&S$x#5Fv5$lRnJYM6=2u4}1~qW%cj2 z9YeWjOJj^mMP-crg;ZTHlbFDE{eiWjdd=w9y`44^Yj;UGGNnk@7*n~p<>QrxwW;2( zI}ZyFkx`pJJ@siBoVv+Q-lesEYrm9_fL(v64BefF_sj(3xnl!U>Sa!6-^$}f5Jzu3 ztz0nd-rA_WdVOejxjwRbl9)eXx)<3B#EHF9ZLGQUDcZ6z?PTLL1Qs?8#p1hOZl_OAPd|yQ z{G``ZJGAd8zP&WC6P_64+_M450}9N&5o_U_zSOnh&}Yw{s@$e?WgiGUv8kEYe>Zk% z&!&X~;#+p#T|l5fkR zY10#^Q^99pi4f&6JFfJt+Gs8(WSgR;6gem`?@DvZ9k|MC5tjAA;lMBj%F7q4Wvnan%VxvL6>n|Z$V#VKujEQZcXVvnk>{Uu+`krq?bb0rFm)!ljNva z(z$r>dMI|!<@O6g{spH?3l?TRMYa&baK>F|u@tc4w!)>cLZo__*A+00(1$Wci>W!J zNYfuV&ciuJszpV^3(ce5jt-enlEKfYE##G81EZ)@sT!i`N1S!;{29d`hc@UHQcb5! zJBmLBEwdX2t*8(Zws1x;BI&4bFeY=VLMFdl!RWGB0a`csABI5%{%_=;GA^kL+CynU zh>GCiXcn2m@YM=a-W7rWKP(uHIupu?DpMG%T7eW=s{jp(LB*#yv*~}sxm1?_&+E&g z5g4M8S}?uv0Ttz+huvM|m&#jeMe9oLaSxkYxS0?gLSwpaGnqqu4vf;uH(ha z4Xe<3N3E9k#f+h^(yl-^)z>H=Sv-3~7?Zq6C%Kg&1Z=Dkrd8g_lIqt&i!GPaj1K?} zrnzmtw3T#CpBi-qqLhE+tsIfh1EO33KoTq<+k0L&?fo-`}? zO&Rte$7})=Q;i~0D4-R6j9RsxQ?;XshN~uFV(74jwlB17WAk}yZm!M|6TS*Q7%srFG`;po?DT_loB~u zPQM&jL{T<@I>VIWKXTOm$^5Glll(t2UH-ELo(wXHA+KLWY*P$9c{>vBB~+;?*C?2J z{3dq%eeaPa*44o-oeGniEp1s_?CfrLfYF1_m5cyni*u7w&PNDqh^a^>7z{Guz@P&C(B)1q#h)Z39 zhtquK3DL|*sONd>7ooNKDd}BdniCh}uWvCJ66&bvuAg6it@;+>HO-VrIN5905+|YSRhCM*E7_M1Kp#4Wt@Y?;6k>*Y4)Cyp-Oxy#tzvCi#1v4IgL93C;4B zSDGVAP+g!rgYgPv@j49;=EvSf?O4wIz|T71V03h>Sk6gbMb)M;tL$)3vd>>rX`Z^@ zwLI~uKUKv=>(#N+iHEV?WeFkPQnCeh+sAP(U)k@=I*ef&t{*^q`nOHHF@c0& z1EYzvl8Ztuvl^&<6~gQ`4!A(ykLeBfD_d3?qqtE!hdY}oT1+~ZD^E=?BgL>Q}llxHdLF-iabT5<9p*VTFi>`?82TeajRDLvQ15^k$$NaQM~hi79RKrf;8} z3*JE~zsU%Srr*AK627JiUHn|T`z>YNzTk(4T_@>^lUT}YhohWhpAQYKNyIC5lEuW$ z{fi?H_d*3QG}zYMs>k0I!4Bf_^>Gy2t5$B>rR|BAp)q`WoW(|ZLC*^qW4E(!ctIQEPYP1f_5@m z^&~D_!J%1Th~oL(Br`uPS-oj|2`|D(ejpqtyO=82@s{{!xTw6Pv$b{$Vy+j-fJT64 ziw7S$`T>!`@xI&n6a@0gBw3&q?i3tj+*x;(#NCv6)1+?_Jo}lz+|i~~E{KHbN)=Q7 zo5uk!ToiCP+-H?sGA%K^{iTsYgQ$Y&)i&U*9nd0l>>hw zRB;kAMb1^c&&j_VeKn&HX}g0p#d)D2Ah7;fOmH(bQvPihJ9VNT>>C^nV$>`1!N6Po zYmOu}DN=l_(hjWdVHjVe7)3-(JkQ~b+gEy*KFY)HaZK#g?G!ik-!tn6t$kolJjA9sls{s=Z=KZbrjv@XEqRMzZeu3Y-b9%T_$rEm4%@ zCPo|s3uckxhWyFW#4Xnr8XhQ@_7)u}N}uibA+sX|(a;V7&tx!wEbD#|L$(z_DHf(ElFJ0Bs* zAs1YG4wem6=A6N+SNO!lNgSyUvU!j>DQcn*Tg*|TL8tFFl|F7A)OX@nC=BqAGz`dh z%hX`PAv?IWV}-d)1i7Yg@CAH8F)!%8axfh|%_R%9H5J9BD_T3wJwuQs?x;OPU8qE5 z!G@Zz!Ee#0Z=O7b!?8G`2cpm;Eqwq9x% zv_uAMp7)zYwym>@p^=U0kDaYaV7u)+GoIg3+y#o_;hS|d_zYo6t!USzypoCG9tXhG zY^06yYOQ)=H8IseE!?bji5K8b4hk9ZX{Br&4zz>sg8u$U8)j+g44>1pf^nI!m~cj) znaqH`#L(_OA1}_Pou)_*jV`XZfXGGUsl)TD>#E`@=9mr(xEmz?uO?sfdcKN`Oi<(! z?MWvoLw*QVAtJ0-L5ebVX&Yn$eC6h_8D=r#&|AIbuj?pqqGplA%LFSuaAW7~9ML30 zDk7#;>eyKpJ6X^buIoq&*SzCC`}EaeunQ%y0~WxVl>-cI2Jg5Wfjgg>(#`zP2%YX zKkS_4E<+`!Bk`sIrU9n0i!({pY1M})^=q!k-1w~;@P)^GPcTe!pM-8=8Qdc68Ow{s zl!n(&DPhMB;(VU3pKlnpqGpC0O+o%R@CmT3At8&d!f%k+vJ#AX`bPY=Y5 zWX!9m$T`tMZ@}l^}A)}FQkSjHOnnZYO6fDEU`pI6%^N%aMD{Vd=psR0yNXg93FpIK{ofq7U zCJM_1FDUR9#m9y|oeaKemn0qC35qVt=oS1|%M?TTDSU5m&K9_jql0~5fE`~bINI4e zF&W!A{?)y~p(_5*@dbE@ejlqX-@y#FK9>cdO0T?batLKK?vjQVSHxr{Sh4c*`!db` z#_;&)fCF#%cy*z{`_9pG-o1K2$qs2Apo5|~O4|3N-;wdFBVc*U_yN(QN2%X3SsE{k zACFCnk;y2J3~22>)d2Md25%%OCp3{D>q8A~$80&a$)qz8sjgGZ-g~WtT}trw&o>Nv zi9QH(1Dbr`y3$T@4**4Km>_q4ry9fl9cl2>9Cof9=<R(IjrKj9m~JtKlW$a8|FG z$kBU?&so$wVs*kxAjtbffuW+}8)0CRkzZ_LRUcxCFvf=R4q4Up4XoLoV_bcrWd_Gg z|Nh$b=2*(2Odhr)eFBnxXKNoj=b=5F6CPY=wZb&NafvN9QJRu*{-G`|)YXAN=F*;? zhUGn*lv<}+{ByEz>Y{NyAqUeUke7>US)L)i!(Nw%z5?Qs5*_~X!h$=)l1#fXfanp& z(&_6sX5OdAsK$7Mfy}#?^F7%ada}3XF9gBQ{Xdlre4}*ukHB5_>ysZA)4z3DLwoxl z1M>g%Sny*5|D`IoM@}%KwUD2nDjsT6Y1Sfr*u&mMJb;{)8ahmmV6|i*TBtNHW?1R) zrvnjqL&pWd;&R1Iq;SS5`++&V-8q{j88T` zdC3&|k*pP{4KNy>gTD$ylEq3e?Vf|bwxSADkzl}@@^I+a6h59=CM=;)a_iuorA#fYfIP=zdnVehNpM;>9~*cY@H8@d5T*fn{0Jb>w!<&?a)sK`Kui{` zgbkhRR`!@M`p$1g>GIiS{cNBxZe@Ys7@(N%mW2F!iT0Kkesy(Xl&LYZ!3H_($;QC; z(Mqc$2l^A4J*7|j07q&Pdo3>LK!lF`3#$m&^*7Q9op0+Il&&D=wNsHNTw}aUJ z9d5ME6=|qq>qxXs{cH%g@9N9fAfBeX7m_*9N6U-dJFq6VO}2Nf4XJ*1w%l1lmEp38z)VV zB|Ubh^Gg=Wi=UGIaIW*1f9#Uv7mrBulmFd8$z%MnYlmMrB>hkPH%AYT>BnaGzvyhn zKj^ zVfYh%G|hZ0;qip$mxK)CpAyW#AqxL%`tz9o^M&#k w4FTb21_ALOua}SUKY!eR$5}0Z!+-wND@enDN0c9PXe@~D;JLJh&5x`92Y)*bzW@LL literal 0 HcmV?d00001 diff --git a/contracts/docx/영업파트너 위촉계약서(단체용).docx b/contracts/docx/영업파트너 위촉계약서(단체용).docx new file mode 100755 index 0000000000000000000000000000000000000000..1332c74876f5e975e58e9542a227fda12bcfcd05 GIT binary patch literal 25013 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 T2?hay0&W3db$b$nU%meUdV(v2 literal 0 HcmV?d00001 diff --git a/contracts/docx/영업파트너 위촉계약서.docx b/contracts/docx/영업파트너 위촉계약서.docx new file mode 100755 index 0000000000000000000000000000000000000000..9e849b513362dd7b7d2e788da6d693dc289c2de0 GIT binary patch literal 33340 zcmeFYV|Qc=yS81iZQFJ_NyoNr+eXJWI<~EjI<{@wPRD+-*4pyc7s1DgYb+2><|y0HKmUSn`1YfGQ{e00jUEtR-Y;>uh4{tf%Z@ zZ{nmw?`~sFm=6j}nF|2^I{yE?{uiHt{^SkIKz_vFD;OV$m9_kgLV3yX5fNYdHU2q> zg!{EA`xMs*lW8 zuNb;Js%H4QdDsd#4^FJDUC3NC7(txoYid#0L%0)z5@k?q6qpP}xI-|1tBaFu@Ef|* zcA)N+zf=JdYlymM?aVb8a@{bKyUyHRM8CkL#p(nLQax>3SzvY z*52Is3pcg?o4%ljBaeQ`Ywk>ov;ZR#{FaD-inmGT?&z?H!02(q1dwbZXAqVG+&?b?zWh_u;<=? z9&HKyID1ygPCb|g>zQmt0=B^OJN5iXX=(h1Ch-%+tg__3_r2ia{X@Q7$Z|QL@BA*O zSeEqc*K437n?f$U@$E%D+sjvgKR-bL^8YQ``0?1yS6{r!ekB*?E82RFCe}_2^nbno zzexWt&hdZw>6HmRmVJz{{8xeRfioQnt38+nvJ6JkYnUq#kXjPbC~M1>OCRq%%gewz zCkA3;vvY~lo=%yfF55{u*VxG_a1k9)i_f}!TCeTyfS^G9#em?697u&m8uhXx5n%Ke|ZAo=ci*3!wK$aPASP6}N-9B^bS4*b4C&EsuUshl7?24a-s-gI z4Z>1f$-@5m7sh-e>?WK|rgUWY+j9vA)fJ3Wl`nUsD3?+mx`8~x8PCFQLd9Cc-< z=n{>XWGs2h*60;Ej~}WFuOAy*zIBWY8>1L0N6?W3vHlJo(xVKrXvIh9X1w^XDZZ|J zvx9{Pinb119D3^GzB~^mKRnK_YCi7u-Zw5rPI~7jg5L}!@`Y&W zMiwSy33k}zX}>t>hPV#CtON%#&$onhTT*97Qo0Rsie-x((m;&lKaqERXTiII)xulo zX5{FO12`Nhy!jvu2a--phlfX!db3O>WZ2{!&F&-vQOC{LB(%9F{UoI*Fu^BpJn1o_ zRNg$Uzgjr4etZ@TKCaAD1YU(QMBD8j4g5&{$mEUx%xCS=j_A}v?V*2a#iK@#SrumD zlXIY@rJtIlV{7OWT%9=%{WU|_>Lv3{8Fd0V> zCCXh#9!Z#FHyH^8JcB`+?G6ivB?p#u7V~;67#*8_B2z}*SB-XE+cJjTt!akWqWpYq z+(VL4mGZhu()`&v@!6ZbS(xp)cYdFOB{JCXa+N0w2~96tGPj?q`$GyVt>c>SR`2>O z%D0!Vdg>Qnv7MweS&UAR>~>@CUCOyj_zypd6vu&e3gr;Sp7dbYQ|bDO`Jm)w6}Gof z-K;|RqdPN#*pYgeFl(!5sT}kb5gi4#b5{D*=d<4#w`31c)BaVzXDD)-w0D0!H4N*1 zOrgHNsmA;{Mpb!x-dzT5(}d-1AKcRZmHd46$5*787<0YpO>fb0FTJ-~CTJ_m{Effv z%vtM4;W0Y8ly&OLZxOC5Z`$UU&feSkvW8@fD(e48}&v?};`{){4!b}2p}bd`6|yyA2{9Z0Uf)dT0> zHM10LHOV9>ypg(r6e8&~R(&|RyrEQGhJ<>wn9<=ipHKPv-}1g~a{_IFP`PdUbX z=E%E-1vz}2^ANbZ1^4@ju249LbfIHh9oZ?Etc;lFx1zhA$ree~iUtI$PTDr^U<9P{ zQjL}?zD?OcyqC<)Q_;Q&rAmA6+QsFn(AorLohmo@b_<($QjL9Rc6K?)Bfbx+fABl* z+!mh>PSJ~W+RPf1lHUURRs|Y6Z&tnT{5E+u*<-`2i_ym-EH%~p=gC>Ud-&=?V&zP1 z%lmyfnmO6DDE~ZjE(`RB+!bpj4)DBZip+gJgW;U<9r?jRjgk6}6vX^KnLRIjTU&Ws zAGfwZj!G<=O;t|Kr28^~DLPIJTvvm3eqRv5?H9WfQ*vmBFn1eU`^FCpI4$Ze@g*F( zvF$`$J)2Kd5e97`gK^mH7)t?q`y~TjM%JBX&WCRu(APO?MKnl}HjDx#VNLOQPKz&XY|z>MG2+-r_HYLQyH;}^d37444;(-w$^fI zHNBc2^Y*vel(q_mKE*p&c?t3Qyu5~OQT^l=*kD%GvDl676;(Gg7G4%beIEUlx0cPI zDYn789M8Ykh=itcIA4*U0hVNs#-d8^lQ7Z{b-rJ4Q z{h7r~1>c#E;VXOZ2VO?I+HsQ>$4}`v;=Xe%dbsqVWNi%e0#3@m#8yD_0DzNaK}Z;j z0rk%e?ed7TY2g=I>x(f-?`3lOY~z#e;u{Oe*;D*k$vMUGQl{W3_N|^(m+sF|wgCfx zWvArf(#o3SvVM;y7Y42cXf%hxaj8nzmryN1N+teCk?ZTaAw)=o3PFCShDT$GMB zU`D@z$rHC42pF4gj!6b9ie3Ksz-|NvlJr9J73zl)DGqdAFs8XFfRO9{-sm^AyS~6F`A

@zWj)5wQoxb|&()EWYj>^EmkuAe~yIqwH97YC{ z?#0W*?y;laaO%nzYTM(k{v$d}Whl9vX5K!UkmvUI!K~Tyz=<~#-@An^xgGlLZyayq zQ6)Ml61dCqi*b0{q1$@^5jFX?^xSQr(AkF98>-9G=EG^QGrsbu7zB*J<`bvGHNyh* zMf1fm+|m8gv~2q6i2x=T2d>Kh`CHh?nNw07AI-Wc4M6d@y3TC^p{n<70?ZYf5&G^k(q>PV9Q_^c}{| zjQ_z($V+zm)KSEFvV4aC`^%-P3j5tH>@5c&w`;qX_6G#Lk$>Qjp}bRH^i z-%Rn?t|KY#xh|;LxVJ5+Te!h+vAn&r#Ip)&|YxFew=4oD1Sy$umG$^;cBr4c7fx22hi3&gC z-!>LjXFz=Sp1T`aU2^?wxJsVJdo{5uRtXze1IU^3ye%%2DE+FrEg|xXRLToo2H>&I z6T~eVS+N2VnzZ|zv!oQ^R3v_-5bi9*les8|V#b?ZN<`PP`%|*5+R5d>z-AXaZORu* zVznH(coyVtl?pU5rw(X)~DvLc9Y4VD)OVm*cT7oBe?ZO zk0`@Pq`&3S-hRGrRdYNT4e&ghQ#cTjZqzsOlNhWdKD0?Zj)k$l<+rTc+VPsp_;7+V z<$6l8qPAGmRU(;jF5j{ZGwsvDGa?Dk+Z=bLHf-@?a?eB1?R{S@nf=+j>GZ+PP+!(P zkEes8AxIPGLAk}tczr1P=P2GL5;tSN{Q$wF@yk3O|Qy|oTEoE)BwkP zM@=!$Tkp%_HErK*L|PE0?!cmDJ+w}$w(M%N3X4F7?sDu(nmp-vE`l4 z%4ZIn>yI@)L@sMf%ygsjeYG%$=TaxUdsh1y&X;K6i{HNTH+nYm6(l+a6h}E#FON&@ zHEzN&`qb=oHpdYy5^43Um}S#(b>fO=`5NiEXl&PV^hp1PzDoPjB zaY1Md7?rw^i1N#FZ}gp1sKmF`=X=En>8OzpRRNy2{be6DE9t)95+YeSdVJP5EO^5+ zp@hAEQ%w>L+%>gm1z|;`MS3bCh+nS!c)9g%1UASTO2<$`p+f)wsb-c=#`xM*z{FF@;Bj zM`aWh2-?Yeiz36LT!&a|?uQM*-Ae}qPZA6m`Lv+6X|DuC{Z19d%ptu4(+(YkSPU9h z5?r(!Nd2#mknan!bEkY&o?A5$1!ekbD$X3Y_h3qSCtcJ_UC$8=6c-Kmt3~+ZL~uzb z+^+mFM@9kg4B;|4uyE1clY_k^1!W3_Wx;YtqcP=e=-939#Xp9(M@R{)&#PtGUF!8# z5QvbwMHgtrocIsNcEnVy6iewX6UfOZg$A_fq~aKs!m1Ye(OMBtj(!s1)3WCwdblB! zHCmm#FBq=nz2TfNJbqFFRjnz!poq#VFxhLIj3Wut*h8;!LWyMd2MBF)5SlkkpGre^ zhiI<_2%Fp46epobmt^MeDspweUyvfe+{sBPj&L~j>-#`hq z;DLI%?hWqZFOG7XxIq2Ukyb@>htW-{ZvRX{z(kHVA;~N0ZA9IjqFLg$pPs#NY=!%g zv<(M8t=G5ij#8I5Ch%)y0qf|y3Qj4mWA@vC$6Ei9e5kZnUL0EPO0n+p<*#lkrHreYT-d@C4efWp<9>ba{V>{GS6dVRNhXwSmCC4t zT73-;x-V2!ZIzwGNf!%dw^1Ply;42fbVtinKpTvkg-AF$D30Y;{cdM70X99p0Ta$>n70 zUCisp^MNm9Zc%wU{Qlf2yBR-J@fn|#$Ll~{*Tj1bp}^7AhC%4eGCB9;4e^WYAXm5R z14yj%MHWZd?X&71`0N*kQ6BMPrj_?bO7V&g-?ObxA?9{mrA0iF+qMCvKl2;S(!Q4) z@$Jn+=hx8O^DR!&D-}Q7^l(Wve#ODy#S3a;Ad-#>-FsC|c)K?R{`sj_&B%>n&d~XB zb5ZB(+u`w{B#jLBLJPv_l>)W_&u7k8N(q2e^fNssebr9d_2yNPnF5wtT2<_D2sysg z8Z}mY&!{=3Bd#RorhYa#m~VnzruEksCk{zQ~Od^48joYKFx z6-c}}4D8nxOEIVRRt2G``n~__szGNOn#a>#Amb|`AdA|HdU5@Ht$_vAHFpwGiObrA zLlf4cS#T@d9vyDU8n&LGu83w05tG^1ZKF%7>h;0N?&>InsDR7swGk4+VE6R$w}(jJ zf+#hO*J5_A;XQ4(+Ix;jAq*kfbQs_3?9WqLyA%xuE*pM#9;endW8-9t^?y(Pi)^2?ucDMdrE~Lt*&@}M%Rp&M9g?(&?=7zc3>I&GO z3wXX);iwIf60W!eMged?zZ>MlOEnuwtI7<`96ZtOGiq@U5_U(B-EI~Zm=Tz#R^H$l z*~G_8$1sV!F;{q`PtUy3`(Ax1@CMndT<$FZjYRX(349+F+xQ zf+)*Ru}RmegRFw=RQZzH#X>g=a>}GWh}J&OFWzjH5x)>gI(<728~%0B`-!m2QNH&2nAC`C{{Fexzzm%j(98j3M@5U4%% z#KwF#bzmlIYqE|qy&;T7LpV(+KC;k_$Y={&m#Rq<^e&k!W#0R_MbDXa@vvr2lz z;k3l$X`TDGiS1BoDR8e@CCOTG)Jw52-erDs?`K;vwLq_SdFz33}>24sH-&A zMI`DW=RG2$RJ(uNvGUltX%s%J+MZ6u;%aqKg4T&erBEb-mR=wK zjJD>i-}DWPPQX^)fTO(it8-@~r!HC8mK>KlxbG>@u(!U~OmD|_;#YaJp$&QOxHqXFO48u`K12;3a#4Np#s*WPfWav|!a@{;t(E56%90*+O;_0=NWiCW;+ zR|9Jq2%18Ap5dG?RCE{ty6ro;Z%c;hz4Z_l+neUP9XxB-ti^}7zlqz(8f-J!aSuFR z$_naypCNxe2e*$g=#I&3q*9^V(#wj8>9jqrfMN=$)OtD!GFh}`MM`YFT0cllda~|B zVf4NX=CpHcJYYD~dLd=j9eW^YJJ2po53CaEAh$mk@C(@g9lTAE%2`92NzY z5OYR5??FxK258}Q}2r)=j@aT*FlL86Z~>Lip$EC1od4hZP$PEa`m=8V<2hFGSCIyP~g zELQ>QUv|D%nym<&DX-rfPN#t>a%B){j21Y>sj<3h#RED2#jIk}NB#hIEy{dM2QQE| z3c%s-!RXGw$Q9=37}O2ObP7fDr>>)=%;yRt0?Cd>^}2pon>*SKEIG;KXdsN2@nH3W z0HkHX?jN5-kh>^8&4OcbT=yNst205*x_rzGXBWk{j>?( zg=KscY&lZWIas4EN8_C!ef^_cdDhD2$K`U8{25*WsHA*QU;o25viPUV@(e!L;awd1 zyVgEL53%o9wP+pto;<}gZ%x%b$YQWQZi6{Mb?dSOZSz39AvMPu1tm>3dGR?z>cyHw z;g_?X`&06Ls`+^m%x{0H!HtENOHJjinxks9cqhYoB%)$dgbnQ=T%yi28kLMtTeQ%8 zI+&fwtDFZ5cS-0&oL?syFYj)1x>9v) za|!NhX3E>lTE2#LJz;$z-Y!#ll`3>Fl7R0X0p3I4e#7Nb_H)NVYvnGeDhp@^fY0B& z=^Vob++<139(mKoy(l^Ou)tL>G+9G`x{YUrIj^w27>%}8hNmpTW0k+0Zjrj;7O%)E zqwgbJ;7C6Ia(hbV_4@WEx(!-;dG>fY&kMXRk_{j?@lDLyfnJe3075eWCW<{DbX!FO$e_eYhh z#|`KGGGTgk2FRY@baIwR$;q!gzK&C+(o`C4YHNay}PVj(& z76rTbd&KV?T`@iYHiW`N88e*DR*e#cmN2Z%&k*cB20Lm~yJd%_P&qP8R4Mn}6yLBY zsciq}-}`Qh}v(U)tf zq`Upd?RN9!^{T2AkpfAX>G)r}TfUo|mc;Xd0f;(E_j%k+Wo1~9{A-E#;Buf~B20M~ z*eg@Rg(tBnbNZZ?3LGmZ1`|JiCoxP;(QU>>B#xs|{$QRUO3l)uM~l#_H=-9rrEMZu z4qr^cNg9`77^nN_Q#Ro8t>WbPp7?njc~jOaNYRM>E4(19oVc$p>)b3#@t*z9ULbR#)n2P23+3XU1UcxN8Y?RsdVU(F-Wh zQ$YHqy7qCk35~NFECi61aOBU>$l^+GQn+bX5p-apy12c78SA5uEB_Mu*)*9M>6fc( zn8uPHO@tR~55^s$dvtkvxUaFU`RSisJSL51Uzeq?K2t)RKYmes^rZ+MiZ8G^JsncA z$x9RPl{riHm8oCg&Mq(342F1|b9cCEHH}veC9fuj4<;qI?Os7HxT)c3)g{r|(DFq4 z2PUAmooz1hxlg&Z2SP|ZYbaccrT>YKQuY((R8xgZ%Y`jckrfIXwHi81QCdl^VVo!U zI9E{W<_b{dI4)ZCqQ2VVxj~?&6%k^+%iFh8>$r;TC@9&C^trkuuSmD^jBO^(9j;A; z-`EkxEbkt~2;0fe6~>*pWTr}}-kzH>Zw*LoH7GX}p{W=6hEGR^927JHonKMB$82_= zuin_^^-*!Rv7vQ|=Qn{2&^BE&7$JlPD~>cGtXHo^?!pnCQdfc-0#D9|#-nKS`{9)o zf#R@HXH6ZFU8JL4QpR%GD))3i(9Th@OH6AV)hZMO3&L5>;%qBXvq@ay)9d9XoR!F1 zFwYG9$~&JY-HY`o@b2<4cUJO>EvVhmi*B=32|WPAyokmc_MLDg3S*M+`?9?BDT5B` zQLfp~(IBD$Ecrlw1syT0mjF{wa2$DWM3C633hW*sx%Q$^iaeAb=+>+dh#UAXx^eT% zr1yhUe$n{~zC!{a;>yMAZAYnq<1iJ;<5Nzf%PrF(2k?jk&SaMKBP{jBqg<`<4iw5% zXh&-q<0+Y5l7ln>0pP@m0{C{L`3>rOs8`uoo!oSouN&8OBBN_{jg-st*sOT2Dhh=e%hS6_FP{*9?NOcMSjgds-PKgCGgQq7 zb<`}DZdOFkONJ?544K<7JfN>xRV`t@gv%oz@LLw}0}sc9&5{a7C__sei06I~T)pr) zo4We=FX`G!!wt9H9!`vBxJ}^%stC6k7nkfY;hG_H_LMWx0%TSYDQj`%-25V#3R0Zl z&xt?Pk%J5OmTKH;~3CjX>*g zGtw*i1*E2l&yiVe0hEezUj2~b=kw}VeB!q$`J_zDwPz^f4}HA1-!-c3rw^s!S;-dR zMv#=-b<1*;4+@FjCMh$&l%sw)l=8ebAo{-39S2Xq>zWa?lX)&%?~p*mCV)wsMKJTE zKf6b*S!!Ba?gs*dY*vHSaa^(#1<7R(Ed}eA5|s}nyo)P+l+GBIq@rj#@5D*0ivo`1 zCloQ7EtKt|!y1;y8J@Qt#uWI%Wr2v#s))lv86}X}e~aydKeRMvis=2sNRh_f>L7{4 zDuNaeWVk)p_qe2MpbEnKm-SzmA z&Z}(=lge=OMd#}Id*yEh7vHV!^$vvS4m!Kv5u``4CybPcWbUB1q)yFQ!I}PGWi{Ck zKf$^|ocLe%*965<_#YpW+19Xw=b(bvjaJFbRs%$X8srf)vt(CBf8GXHf1m^MP%E!U zJK}7cfBexB9s_PG{a9q5yJiE)){p6S*6_cFItPQ{nIn*X1WE)QaY47Pn#<5fP))hq zLzto=?SmWHc?0E6MXf^LP2c`l-EG-U#2sFuirzl>)m&`lo8Xkv)ZY9K>u63JknW`c zT(S>3CMlc*vZ}t_0sQDw*LK&l=IzE@H08e*go!T)Vq{T##XCZu=LWCYjDBSwC| zt}LfevU%P60rB=n+uAy&{@L|Tta+kCrEq`}SvpCP_|H}pa^$>K0cdIo3(X|1F)}ML z(C?fg+i)g`V-n0$JD)7zcyJ1Nu=yJPs_NdLhNEdrr8RsA2(6WpRM2klVhdxY^4m|_ z1$tQiKaojCZWV~-uJXK&2j6qDqusJ*e6UifAgU@b6g9>~J7tNXbQh2T7P1X+({{ke zSst8Ac!}K>{1#Bzse5%1X|DCeA1aHCEE-~@w#*_FAsU8@i%zD+(=qap%8uQv0vw+H z3fz$UIVYTN!6<8sl|YDQz$X5n4%S9@{Z|TjV}n^%mPX*kJwEJGdS^+Qupt>IN_Un}6Smfd1H_CAwh-d%tqoU#jqV ztntawxJF?tAE+6WMYBJ3A2$ROF~K5=FvAltWxMjd(`~<`R6er;R=?iJop4id7nU!F zHSEpvBIY7e$|M#z7QY>SZ#ciC^<1F4x*)!m*jyD*ce#_d(A%+yE-Wy1!9Y$o4=_SY zOEEOa3vbuV3-7atR!O2kO$Xnn_dmM9UrNuQ>CR-l(qHh=bUCBB%<#?IqNjf5L16=TBC9pFvMyVjuwQw7^HVfQ_Y17@qRvLU`Pb~Um32KN1Fj>R#f(_q zo{W+qmCCwb;QZ0C-OAbDZ(J)b$EXLd=Tk5t$1gK{nmbQBS6x#U4O)`7Cs2EKjX!;b zImf|6$d#iNNPO7BOw(d$@J2%bc;Ebx?eml_vJf(0u|o1n;Jj8-nXC)q0@5#XE~CB` zAf92uAkiX*WIvpo;QLp%Q13>Wd{aQ@Uud_#W1waD*y#=De1gwfx6qVV4hev82?@j)fDq5H$sqFOl8|@zVqt7hVca9a z<-A-neN3_A7;)}I;&}nJNln0#+yfOMgb_)rWWKBO##%2JRwt&~krWJR(x3Kr^gdHi;(}YHXYUHBy=<3=3BQnf0OiiQeBTuhv2|5TWcBD#1M1ngcTNV?PR= zRmf97=l!6`|8rl}WCYq>aX{0rfkJY7)nqnv@J?^{HS2pChs;#0myNM-pdzCwrKds)|@hs(KhH>t-HNL(sJ&w$}H1zNc{@!-J z$S!rgV%O_h$Ilmr>mS7f~5h%r! z`MyQtr3fY17y?GXxMg&)$mxhH8i~g1R}TgrR;Z5U$;0wiLFCjRIL2$v1q_NW`_60W z%4X?bsq$T(5gJtWUi{Xf5N48YFuvGFku!$%EHQacykoEb1wvbV)PNBw!T==4G89wQ zE;tjQZkX6%>R~F|eTNPQZuVvqB$B&;yiD7fjz~;ym_D@|LcD03=dOGvr|DzmseG~| zQz39`FxKlxBOFV*k471-0@f6fQpoqaIT z9(V^)yxJwXX4D90wvBju)?wsf@;oHGg;Se>K4S^Xv~$IVBZZl{ecM|gI0seDSNp{3 zXMs>l=)7$*!8S)@t_;9YDfkJW`mYDkiw3iUF?|KLSQkW)~ zdr{p&olh0*hs0iv)zaBV9+!Z$E7_RUh>7P2DnqbY=>|b-9d}cmk4X2-{ zGx)w3|DR8kBtOct&VQwV^D71axiN2IYy4kAFk-vLh{%5h_JR;_2~Xw`jmYh{*Dy?H zK8v7FIf_h7Pe{e!yOpQP;Rqt}dmbVl-x1#|B$MdsQjXRMUin1%y76qwy;|rSlEUbt z)#Kw??8xX}8R$*>D+6V&R~%bi8+byaB=oQg@PsY!u?dwM{g_z9g%W~TiyX9MlIBPm zIK-I94w=XiF@hqL*WNY95@e7I*o$7>Z+?whJHurJbc#=qqv)JMq&=h4N@Ld$LWjrb zRjs-DppZPUZ}gU2n2r34pu}d|B8U` z0zOmarqef|+J15$sbBM%{wH*nu(HpukdgcqM70*IskOq=!mJshY$BB#+BdbcIjk>> zceV31>7s$A@5#Xjwn*R8q%$xlfeTe;J$gI z`+hCc~G5Oi)8eog%z zCOJkr3AS&+E2xs5owKJ^B0T}~krtHA!Bv13L7iM&X^J)x9%i*2D0^i9fAoC``Mw1C zx3$H|lDVAC2=D6996h$@4HPDiIc6}ThCF7Sa5J8x_}AfuhG&Uv zIM-hs0HI1?lD9^aS|43_^>-9a0B(8{$a3WPjzaM9YXTt5Os(_wd86zK81)kRg_^0swS7o zl>W-eXhC_A%&1lQoaLsgl)aU~1)cgHgY4G!`&`kYaGid25Ya+NeU)e`=Yd< z327{Uyf;<~(pnF?dpJKf*8!;>m`Ct&^EE?zB0UUB;EEVRWUdVR(4hP%iiV<6A9d_B z_6Lrvj+E@hhlyzoM&1i`&P)8dioM0gCew&3%i99tBK=tvU7-+%>w*M!2InQbeno8S zL%RL9Y>>%-=Fhfccf+#xnnt`c#=CB+KN-tVx-`Bym|`gTCrDWXs8GJyE7VAFJ?}k{ z+AQ|AJax|vehZMbWY)W5`6lPs;KV*IUb0Mem7216Xj4?4LxU(@+gpa<0L@9k$^_L= zg7#-Wld^(m9&H?weVe+zZ%sNg^ZnL_BVnt4T$ym+d33=LCdZexM|3c^L=C!0 z6qWRPO=nVd@_CUIUKH_ zTJ$%c>4$&wNte$_ZT2^xRq*QBb#PX=zhep%0vQM&y3vTS*kg3KMR~C5MIX(%LFr8W zw&5^t5F0b2RtfDE01fG2z!l(y_dK(Fy0$E_4VVd+hw;fmv#$6Sak7S~+mb9$97-mj zuJ#tP;`+C$$2b71^FK?ON0)f1GsQjI+MN@%kjiudv?s>Bc*qgVorQ2B7rnj1g^ODa zo_3y!CueyojJ&p5%m47nrpYqs_VcY&oG8_~XerJB_pP*W!g0eDf++=@;}^K&$1spt zPsR~F7rut&rOL zbX${sZjOqfDqFW-WrlxDrV?N+PLF^|=*lW$y^Zz|Q*HsM87WD&KZ)Ys1m&Xea zmwV-W1aZ%r6*mCt23qatm9i8JhEmeO<`k6^_(_Tj#nv~q|xs( zW>S!d#rsZASjB0aBL-snPhW-UVbqdVH!ZWiNiKA~e8$esngdwMQzXji`P0Bnq zZ@#TMZPhbeZn6e0puW{YrA>uDrx}4|WhU-FlNJuvz@iE~d&>Lkq5(a^g`bC5O4cm2 zj^&spFmXsoO*IOCehCx|rb5^t`b(hsdq${ywpVX|33Rc_HUpuO+u+ z<47z|u04kRUGTwVpr0juE7N;(Su9tQjUz>zPdh@}Udo7R`@4?UzVAE?uAOGI=uoDL z(}AC|(Vxz5F9$d1C8}Pz=;WPLj@K+!Dp7GnSO?2I*O}%P#+C$O`{KnGvXre7S-B>N z;0Mwc5~WPF3oPhr)^FHimwV>ngB*z5+tBf_R#I1ZB$==p1>aZU1jeuk-TO>9g+&q- zaP}*){ZxPP>o52d}9-L7Vr`TgWw%j}X+MXvRFQ|$)+ zOqqJ`@j4-_T|sxzx}!Jmp@(q<bg9DwVZ^4D#GP5A7Q{YVmr1`LlJw<~?u$b24@0%b#J{OC|an^jYQ+ zFs5E^(>F+8{@nZb{`8VlR{tOV{AU1V`KRCi>t8S^GLLYQe)tGXI>(DYK(G=Tf-U}q z)<;*|^PA;xqyrF%*pB#Ozrqn>)p`P1SeoP=2?m=LFc~FK>GmxMK3<;h|Gx%MoB!Vd zluQ9vg~bNdV6j(0?yrK!!Tl}!SHb6=`&$V+TI04oM7gsY2VTzD!b+ll6?~$5l;(t} zO?>m0KN*1iRY+z{u}%mWnsr1a2MgOw3Kvovv_!Fs5-}-SW~F-#i=(>X@h705taYV) zK=h+^ZWOP3JAhbLn=rM9m%z~dRmBi#{$l{GF_%(!tz~7lxwGT)vGdeC;}9z8d1^4) zY4Pf>j?r2=O{eOtScr{C3Xo9qfWsQ>iIrS!pVo0hl(MKzZH~M3g6D+Uy!`J2=pjcP zN_*3rk0Gv1Sh1KhwvzPI?f4=x5Y{?D`!E~G(I85O9E`ZAADsaTA3B}U5+mYC#hOz@ zxXFU!X{o#H3^#lBGRP;pDIoWNR=D2s;yO@`pqAE!H7C@{D5#y@m2;?xCOth>#u}8) z;XC0X)F9f)OPX1|hwPejHwRN#959=X|}3&)Wdr7d1M#IggzS>pS9qF(cs%~ zU5G5b_=|VK=r#sCy1+BIyuSb%&?iFpd8p-R_0RPqIhK)@oN4B3Iwqd(cez0QEKBQa zmU-C*8CSilF6&-N=p5OjqOS*^3aQq=3Vs>0Dw}<1*sfAUEO{rPtp}QmC-?M+#}nMY zyGQ`5eyRUI2GD=}`CkG3?*(u4Rq$gVjHo71JKzcRS^egE=qW_4v9rFTAJJX%)u{bG<&Zus=_(pXc_-Mk*h=HBP)wG`H5AbH>0VILEp zNo~*(jTkE2V4kJSdgAOU$_Hj=Df`xRt(1c}^irkl22fo_uK}Yu=VlC)iR=k>*UE*g zD0!-8=aW=) zq_#d_E2IM9lQj4v=(6F@P}F0(DL6^+ZF^kfwJQ>uB6;;wbFB_u2qBZWD;&?P@lR~H z0V7d5{EUWl{Pj^<6*Bz4_zd}r&oEIxxI7fS|K!u*KltSQi_ibl-dlL(wF7&@P#lU= z+@-ifad&rjcX#*VuEpKmo#HOVonpnUxPMQN+@8ZdcYXiB%UZKZCizXWcjn37k(s0e z^b?g4C?BAoC{66E-^U=R;+Ox_&vfcW*1^L6(NE@I`biAXPlIVUX$_aJF5DqG>}$%u zI#4AUO6$CiE9%FsQz=GOsc!hRm=>wAXl%;;opfSaQUYdgqio2Lg#hTQQpjc-Q9~9* z1^0@1trw>Fh!!z*_ezonh~`&vRaFvbi>G%DbxP@Xy+|{XHaTuRF zZv*hXwebR+njG^nXujYaXZI47o+bxx{O9|9YtQt888EgdCIkZd@b?Jb(bUMwi1v@` zAH(=#b&YT=5mayZV;|E8EoGM@Ybq?Q?M22LI>wv0!MN60EoG9}d$MilWq8?T!d@ zCNO^ZSJxjsEO0J#A0z}3N4A~@%-9v2u=D3K2__UYA5~V{+N?eGLaGLpe4*hWKzQhTZ3NuU)>NT8Q{ z^;1wYWk%h#Qu7hWwam zTYlgnvTy;;-V!Q-nYxvOy32S=;=!Wrfn(fTmgO8Xf%&M-1KUNo_p{$ZcKOm`_icOj zMhS*1>`3Z3j;GhG1sD-X6(2pxfhXFUWy8uiwid5(#Cl^7$9Ula=8tpOQuVM(N$$i) zeQLgFbUYAV4m7bicKe2}j7(swh=WMIYMVJnRI($W5pI5G0<&`+*{)^|c}M6=Lf1Qr z2_NeSghmk+q)}^+)`6d`p?+>H)$K)Rar-=-?PYmCJYDc)fFSix6S`m($f=#q#aF%F zK4)cQy<9h`YhBLhE@^rf1XT83}xtmQroCYV$__$sqS9w1@vY@&~=ZRr>iz88` zd)%$E8ADl4)dFRnv&6GTB9cp7O^arukYm8O6B~ed*#wc*et%f3-xh#V>ji(Zy&uB& zM7k^>M*Y6VO79yS@r`?Xo@-6XBC(=clIU^EQ*8i;$kABCqk4Dun?3NRj3osG{U=jg zIa{_c+pvPXAB)%q;fsTT=+0ck3KO7dVtWQ#SBN#KI|-5zrpu6PAEEAyjEEp#K3O{4 zO&u_rEAaH0d_mxz;=QbUmMm%23T8NNTHT#v=uVb9il8u_+3d4GNWHxzki2DqV z@if2?v*^=cC5_08x(GT(PB`I5z2+aX2{4ytmsYnlOHBI`?7aQ?Q28W2=E{pYFniVY zV&RN>ag^Nq_-hln$Bj|aiM|P*bz2_>9d6uhR}V(6k$d2#d<}0Ebb8slJr|wt#~hO? zM&n9q4odTjLhF2LbtQ&Z207Qgcoif|kHn_JWL&Y7Veo;KexT{kw(kR4^Mb-4WWv!? z@9n{9wnUq7CU)>bBV_dJeT_Uj_=)Z3Vr@|+d7%n|D_ZA3wLS(@jb$B`s7vr1fdUh= zY-{JC2d7FI=Z(+*=r+yKz9oDLlF-9ye?S5o&d(Cg2o4|a#ihsCVPa5OmGMcPv zh1AteF}k)gTv;dW{8$;<)P*QM`zah_7R3OjVZ2!E1c}mr+Fr=?w2heNk$o{r-ASMv zMq+K{zA;S-mv09?M^kIsGO%!JCIL-E(;=(cHj$-dNLsA1$O$w1fIgFrxZO9wj8Xc< z$v(C(st#;+R|!(R!W{aCYtSg1B|$pa9W68(lDkoH2aKLp(hAB@xc=_@EVl9?%$5Sh z%B31Y&zo)2>X%v;i6VE$j9l9Mx}E%^mtl*m!+>o1V%L*_V)?4M4)L~cWlHXm!sB?% zo;Hi{+`#S((oe!azS4_BYVGobPHl7Wgp1DOsa8s<$s0Y)2r9uGWeMH+otPjNnXiMD z0afVkECguN6~F)NT_+F!d0f0r=|~Bj&jI}kKb~cB?GT`~emKB%9U)%wxxpl!p z(fxTjd)hMkp?eW;E*RI>K}2(hjkT$toxj z=xAnb^2ds4z3Phf3M+z_j_zys`6YXjp_XNy^rpmYzEq=7x^-d4eRO z5e(c07uCiM?U(z(v=dqV0$!;`U;kmf*vMszf)TwY$~tD6nD*}^#y~VJWHJ6SHHWC8 zoXSR|+d`ozY6#-9l~}IYtCePV#ad9G%E{TmhC!1(OX`PXXp@P1ucPY_s+JZ~$6S>^ zq>$GcsOUm-+kRZgPl&3xQ~Lz85M_9oCtwMEc-7c>8@6rTGIWbtWhaA`;BTTT-kj~v z4gtksiQS36eC7r0u^_^vsC(*R>~!qrh|72Gk3Vrfm1CWLrK?)N} zuzRkd6s5RR`jxIwSe6Ls&1XXngc4!S3z{)i>EwbY@2m*yHlX5k_^j61!WEu+!QDQy9PG0VC<7YU`a;?^U5Q3^TQtmp=GFaNh53QB@qnctL6^frac$fa~f>& z;6di-RL~gviH0}nDqRruqi^aHX-6CaM!NmQ{k%3~Zita9Tz9T(t-=&=X^@r|L5upz z$~U{Ve$xO_GLHn5INNfMB4Ek~e1shKEoI&Ms!Z|y>Pu#|QjhX%Jaow2S7rG%q|a6K zY87*^9#gcdKSk5E*K%>H5;`Qdf37+kvSJB<<@bmscWkrOOVWMVxn?Va9kVzm!s`4H z$=c50>b=Oys=@)yXJ9VfxkW#^(Mt|VmkMzQgZnfDJa#pKmt)F#ES|0pDiz~GP84&v^?6s8%6rAwu%++aCw$RO4p9ukyZOGQ`K*lw+V*PN?Dx!V5Hg=U(rpV| z?K&tI5RP=(#F8C@IRh>jSqgY*PYDWcOSKP0*2`_U=JtUv9<|vkC(qww+|_w6M_f&J z=Oca@u#00N=>8fFZpZj8vEp0PI#a5f(?~j@-uFkoDyYyNR(tAA42I8Nc1tnDa$m}! z#EiW$nU{%GI#^9P5AdZQ^zz2>FCfM{rE*9p!h&yoOjdk<&00CyqIF{yYnlZ({}?yZEEbr z165wskZBa8Uf_?cK?mbgD`Tna1U2mE^O(7E&^TAmnG*1Ls_g;vx%Kpp3(Vk#u(KfhHV^F^z?q8-(#M!f169do9wc5&4)B{=z3B z>-V-c+gWn-3rkUFvTDn@HLCY{pXRJa7L1`g?e7#OJ*kxWBEi66sEoN~3*ljwueb1F z(vL-}_6$1$s2pq0@x#-p*oX8h0Y|viI)Y^N%vo(^Ou^(75c3LACciRjA;Q*MgkVk)Y-}12<*tHmked z{|KgwF=TPqhO6L|EFAutwj}>U#Mo4^kl<^_#`Z3#So%T=V`TY36Ws-AydlHBVBMDm zsCd`q1Jb%?M#!gb$K%OqKF<59360wY6^YbLyq6g&rV?B#lLUM`}NBq&7-0*%~-viyK4;p3>>PZAEVr{_= zj2=)PY925WhqTg`$R9~a*S7kBwiS7*?mM0?F6EG}XC`&(cIO1tEm7|ejd|BS0?Hhj zNB6?#_R_k~vI@_3zMI!cDPJ0XshUpfZgOP2yYJO`{W-IJZJxYV7{q=qZdU$jA^K=( z{JVFXmJWPy+LBi)N!sm_zVc4mll7x_l1cB=sWrVsaI_meo_w_HuFer*RzX_ibZGpX zKrP0seO=XF$$0pVTt+$AlC5(V%F5MIJa`T2X0I(7d{A*fOjt~u0QrWx_YSRBo_sCq;y+_++cCPv^bw);u&n%8-NHUAof*8hOfD$1I#{k2O&%HaY-rIbU-?GhprpWY zh7}7D4-CIl%XVrfHF0cS^tsVa30B5`52dTJ%~CCKwOq6vFcgreH%V6r+}l`6AXcEh zI|$DxUt&y7XS-v1cg4#)HqGcmg$b)>@!mwHL*s~Q;#fIST8UEK?rV*~v~SJLaK7yp?^H<+8K_`FaK$0#lf!OpUqjsONl?i8^Bg zV`P*6Mr{YXrqAiT+^hCZ+jabMZrbu-5y7L}OneZ)kJT+ij#N|6lXF)2oM`5FC6BlE zu8@73B8mLkt@!oVvRd$~S0#aYyxpvW1^YH@o@+c8iRVF2mxAEFZU`a2;S*yG;<^*s zw&TO4F7DFGd{g;Q9kdeuA%2^3U;5US!v12>c-jYqirVGII7544pvqm2O1#fg_3*6Z^n57(?PDY$z(&0(}z z=dyMEQQDHe9pl}JhI1P2r~;AEejRN1oH9#Yn;?ctqT^*%p`n$Y_?kE3sIJj^KrGJv zVSb`rCnc(x-L=F+^WNTQg_e{P7@NW4lM8&K^GBvSN>N}T_QHH-5V*_ zr|UQ#AqyfC-^qw|6^^jTk%IL4g!oYks3~9(JbZ=EmSf21;K8-I87JQZZ~<)!&`kRA z09Ju_?ToP2Qb90uOSZetRQc;^qF%ZZd(OAcE5qXteNqKHa!3TwzgoblISCcmIMrTp} z!1IrG9er^ENDLXmo}85?_P1$3#`;zP{gPD1`m_=YgT4KdmbbR_$l+fWN2fKrvaju(tV2Il9_%a@HWi$Dg6q8Mx;!6$u2wW;{< z5+bbRd4sYe^b#l{U*Fhp4FX*Yi;4mY{qApoEKVgg7+Xhu;3I(%Ri({QO;I-MPcELm zBMUyh`Az@dQfKkN4r*31{aKY>5%-!t#<0f73%c@`2kY|vUG6WbbDPWZF*CI%ngg*S zgEJJ|!7bT{h(tX{l;)4u0sXDjstaz)`Zbj__`&_HQL176tq+a$L@@x4fJBX|%2^U1 z^g`XQujQrj075%2l)j$!)j-De+oD?zw73W|14d1w_3|fQDo{1Z+89PlAyj{={00<2 z1i_GCh(9I>ALhPmj6Yplvxn4Q9%+HHW6^{F5xE9Z1m`sTS7uPPAv29wKsLTVBN$19 zfZ()x7}XAJEkY4X@;P~$YtCRPA)6-5MUNPnVX#o!pV{q~mH#e+57qv2!LQQl0cf`X zs=k9cKL6@iTK>H~{&zI`!lZHbqr9NeWClCglWrs; zT-k5PH92t}4~uuj0Zj#mBspCd{Q8D`ylW1}}OUt{+_nh6xTw6~!EzsLmw%#rX(G*HPC?kqM+L|c??ooG#< z`Y#pvQvnWbt;mNC*7$(o4ScZM6sA~{5EF68AZ19e22@?yfn`WJmmUV+$&jX3qsWl9 ze~Q^U2o#XGaO6Kr0)zm~Gz0aknRqgInUMj{nTW4FCAy4o$%MdwE%O{fCI1l;)uARO zyn{*`4<5~-X05khlj?^)ZMeg8+&nx7+R@c%r7mYiSn(GLC)KJrdyG5mBPD$r0LHrU zkAD>fDdB%W=+PblJ1AOI$NEoA2+9~9Rp%xJDb)y#!|$&>RfwBL>sR;b`QFj6Qsej} z6*FxpS5mW4Il&2zwux(p*>^)iEt(0bEB}BJi==>v1J2PSdydIMg?*oD)N?H#kCY`w z^Mw?a#44S;P`&f%xS$lJk4QfCz0&=L*|(os;1HXSf$q4;pX1^f=s5}0X{6loDMW|J zV|(z;OCRkEa*}hYzDs@{L*XX3A!R2}=Wh1-ZpQ1H%?bHFxA2g(H6FLoivqH!XItsT zLs2{F_nC7F3%I@_RCEmN_5Gi_J85+tC zQHKKNF`>5}BQU(xB)65Egf71-@l1ihFup*hD#|?Rx`{8q1m!@<<{e*TegK!eBcNY= zC^t~bnoHOXe*I4;(lC7!wPt|n4c-bMAo#ym6C51fEdSgUeykJVWXDU0I@6n>k$UR+Fl^&kd?{c!&YSXan}VI z(@qezsLnR_hbfEHl(i}n71ECxrrpR)5ojCf9#^m#9H4BseNrl;nPZ}LbS)yN{{A+c zdkfqO;LohSxIpOlSFmsfItIt~!-Uf4-))5{)9J{*FDexRAyCO7ba-pSUx%QHdYQaO^x2Vl5Ke+=*vFd@efqBc7}#e0^+|!9WRy&zU(Y^vU%-p_HNB2iq9bjP z_`3Jpc8@RzW7iIUQ`z=1C60+hT%cBsnf-I zC^CQ6IX*;az`+h+@YpraAdYVa(Pn8A6G*s@ajvqlP6ZjT!)FEZ@ox77T~NK>FT;q` zDP&Sj_OYWGmq)v^#JJpe?ue}qC4-$bakNa9<#1yz65PcpZ&Pc{)KP_HxvdQA1i&I5 zYipN7hiF%cS?FB>nzdpDw-MY%Plt^(I45C>>Q#*I$%>c^C#Y+APE0L`lGl^FWhQieJ=&9X}utv z+bU?sT$5HxP*OSgqPBxy!n88oB4}@@L)kS7+cIcjy+89(uNFO<%PxL0B00W%+Kfb= zs0b(Cv;@5Vq$ASsgxc~1XiqPW-66nS6$15%|Gf!V1`XjxYtjj2ys zfO^=j0*l_=@QMFNPU<%6NXLoXkhQ6=I9sd|DYXr`qqoE)k(w`-ohh+Q2`%zaxo#?1 zX9ee(@j^%7T7tw8Yv;~4rqVq_4lMqz_V-U;T!v8z$5F!UKwQUaBgQlI4!?Ze=*W%V zXe;@SDC57aAyUC*&%{OCEzJ*+5y7wTM?}m$_I`Ux43FZ1JFf}3u!+B3q)oJVSu0LX zB*cJ0r@ut?FTV_dfJ_l5}FL(4~4KZyE_(#P4G^%DSL*-`2ca4h6UrQWkUtnEjd)C(ZVJi1|L!U)~d6_?(6CV&DlF%+d! zkpvJEfUrgBia_f(auk3px}=JL=Dn+;-Z?OVlKHmOG8V0MduyeNFvvllGoDU6ih@i>aoxmsx`6S?uF@vh?q5@ ziZazV^|6j>GI{W@C4BsF)SM~x#uHV0oWX`uWBdk9ZhWYw?VEV>&qk-u^>O74-5ur!Q4w$V|yHGh#^ zP~f%=Q#e%Gx=N$fOw5f?i;nD##K^T+|>l)QC==LLeIj-B*GAAvYW61OHzH z_tiEEde;s>R)B;>RtiBr4vIkDe^Ke~;yRi2ZZQJbNio8^Kgqs*u}3b4b3!hsJu;6U z1xqA|%v2-@;VqGgfNd#9LCZ}=K@x zxrMHhXtky%`G=SlfVfR5pnfQVJAKqi02N!$o|q1g)17524(}8an}UTq^sl@c^;Cd9 zi}JPAN+E}If!%WY)`}0Oq=}XX<1gFm6x)Ii`9Tvc8pdBh26RvAKu>@xQu$E=m_LIJ z72#$K(#aUAWu%whF`_X43u0=miLj}79tLoK5hA!uIR^B2HO5Gk`M$Ag=?TtUgl_>R zp{g9L?s3Uy*?C0)GLBL`-9C9(#A?MA2opyC4|6~RndXp2Qp|z1d6>>ds-*<)lFUPV z3tEkWEdy(cF!|LKU~`U(KL>D7%^?6wFbjSKfXj=)>23hREJ_IyaOUtOgcW`X6aWQe z=?*goGLQjCE5ziVlcP3~6IaYbrwQ2x*8}6z4Aw;G1Kab@7HR~{LysjvLnbXD?NIDt zCsThAV`(^8ZW*V3ib`j$wq@ zy%;V?p;u#F+&TB;Y6_+m-~Cvo^zMq(uN_Nq{hpASi80wumO<|MBBAUkF{2!L;6G5_ zl}lp7N}%bL1=BEECzv>}wHv-msAuCQcWWq&q}1;0_muDaGdDMm>Amv!LApzDh60Xc zJ#-bhR{Qtc1fwnAOKAnS`WO6V!S=FgZa zZdw=@S-hit0z=co@GgB=`wi~%S+Pedp2E_JAGpsyZxx#C2ycvprD=m$&#&iKQr=H< z7|VE0Rzi8S2(;-{6sP(xI?I}QZ_@tB@C3+mt^@?e*$*S9(2t6re3C53x`EXL7=c5@SX zY__Xf)AWkJh=sC*3;}+VpRobs0U>--A6K!+;fin!8~%ro`=Mv8+PzB(M4c|rWhRv= zU_;DKN(0@Gc!$<{1Dd0fV={Ad2opdF>)KYeDFYwlr3eeWUbahiE`@z&HI(RF^C}Xv zcyi>f(u>Q!q@d7iQrxMS0LBOU-I15b*HNB%o`j?dkw9&`BgMV$2C_QiGi^^&rJtd^ zUIl3B9Pk_*)l8W%ibXr*>V|N z@@+antHfi02SwXLFT_*fp=bk=@$_WxaHTihwRZw~j!-HK5#B>@43cY>9sLol^$zHP z2jq5tcA{u^Fa7*gi2It;y+B&&AjvJY)hxBr7>pUFF4KHTHQ}-!W_QC-0_ng-V|2Re z8C%NIAg7i_m(VDZyvj2{*s#ZlI(EFmqpi zc!KvcOxPM_M4}hD;iD{}*%abyYK$XbCmQYd^t(x)=Zn2A-8KY+`812i&gU&@C^~fZ zRWkhBxk?>u9fJ$mIxn1XS?6|bVlGyq z+>2Nhup%1Bu^I&n{o5E&rWOJIWgENjK(mVq0g&eDif1eDC(s7{p30QlcAUsrn4jks zZTch&?g&0TjrbK!wNGaR3Bw%epWR9K$T*Jb;(epFLPQZ6v4)mz=80>F9d$eu)t+ut za3fU?IZ*lj?z;?+=WW+n=WTf_+V*=<>#(jyUtP#fTLCan7_E$HN|f@vua78Rq=&ES zO9$zK-=Nf*<+#w_?QzT;L;A_cmS?GUBgWFks2p>+R@p~&o7(v3a3d+s-j}Z)8hJOs zjaeyj0#j7Ff(}YAtTcJHtCZq7eKzSu*1BFQ)%F$)gGaE!O6QoK}|H3yg9VErx|>l!`7^;NXNJ0jHL<(aomTJkbm ze)nioz%FZ=UgBNq&|WWQT39c6?m(v!ZfTI3Dt zG94UsBVd%2ltrPEfr!G**B`1I?FxBjO%7kTU%r;*&CPz=`c z+UsGCo1P{$=`(BAJ@_a<-ycalf^uTe=fasl;} zki07b)^w8LDW02iHE>aYo4VXRXVE!r5bx|}5B*4*rzeg?haJR}mhS0Q2_8*smxe5Li zrtR#9OMB|XD+qS!84NQFk8pn>0ya*f4KupV;UZK;gm2CA%BT5rO{UE1a%wx@HC3aP zMPwMwCdN$d!OVuK*{PoBs0u5JbNC;~tOzS!e9d@tJ;BCUj~_9jEBFMA7h-d>7&$oP z9n)Vw%mDXQqS1S^&LO6ytkr`?|6@y4W65hzA(19mAc%fgOR#8R7!$BZ)5xz=@%X*jUYqFwq81U1PK%J8YjU zCm4)BX@7s@YU(L=AY%|i&uNOIV*60Ie@31PEC-!Yt7&6a=3q)$vZW~o@WH@#S-xX8 z)(02R10KSdo1YZg5;&|#L?Jtcb1xKXBI9e#!8-+X1*z3!a7FnUg6jwywQ2jx85?zh zwTmSM6JNWLA2j2H^?2C1s>acBs~*R?O*r!;99FtU`ZiQ-J_>z4#K_+$Zgt^HeMbE$ zT=S+gEE`6rDrCtyCkT>Z0Z8aBitZ!Kk%5d*Y-M=!ocxElG1RZi&C4wl=45mbQ)#eS zqvaucs-jYeid?!W9UDRLKdppCyx}LbciF>*33HKvCeHKA%eo^KZ{P8&MZa8_|I%aXRBj17pXv-hBOd$yt9YPtvmNcm&C1ODceOMFg9r~^!944V?Kfz*8 zlpH2!l!0A?=;z0$*S`#@vGkpx@GvGSu)@rJkcgweu6~i+)ojP7 zUQV0boa~86_9C|Yyj!H`-BMYv+BXrO{{C5MuHquF)K%e`QyP7(KxZ<@4AJn;XmIM8 zt&5{zysQn5jV>>@r@MoYD6yR0Fw?5-{f}%khO+C$ABoXdg};rLUySi~ixEui2ga0U z5AyuywD5jO*?<=y7hgM z-H>{Mm)H<#^Ak*_-y#9cD~3Xkv}xetH9AUdq`SLy#=@FNxpSt`=}OQ1gnL#uyW;nW zFqsJD3i2m!Z8}T~X3j!?UzcEJjiP<(4p zrdAbUbe}9?F_^hTL>DxFu;%AA`QSEpRk$X)@O zt|v+WVg?K64$NJH>S+AJDeL4N^Q9UB%qLf>uHaUgl^wRZ$(Z*~o4Wi!GV`cqGT_ zF!n$<9d&}!q-q%lm`Ks=;unvF0w8Y(QPWp*1m#qn$hf&`cdf`ijUR^XDIuTNwS9(Xu@1P!>B0a3Ba z%=4Gw|2BMFNC-2&0O;&4U_TJTKXq2m*7jGA{Qp!Ips#?xbcJq!lau!y@QXa?tt|G_ zOlBZL{W1FcdKw^oR+Ficq&(b2Or$YS9~d&|Zi zA;YdshTC!z47SV316Ik$ovt3v!?l4Ui6ib163)JTmAgkO`Z!*`A>i`DNHo+-aP(BH zGVTx!;zZ&cBZ}Pf(Bu&sr)49i%@+?P;K818dyH_Ckd=WysjmFX6*P}8e1s&buzU`3 zSV5=bbA8!WgYbEnNKcBdRAJWZa?gt`^~_^t!ePX(a#YKxiQULuA^P&7jp9`H9J)Ek zlsP^m3LoaAYf2ql`6AfxR|`|--;CS}i)48QEomLGS%8A#B|*9mf?p{xxyH17@0D$5 zjKY>Qc65Rm)>K?6?upvJZ@*X^)Xz%{`ryQ;Jn|9YiG{$Lj~Q8;a>jLaTOS_NYb@?w zk}%f?I~l;6Bv&r3kN9oQIm>w1llc}oTgr1@ z_u5A?DFe2wT~ZA?(NNeL*=g0slB>Yx0#d;hOG{_!s&Qd+)dCw+;+`qwTo=aqEA% zGkA-BYhC^ujl}ma^naH9*7)}~x<=q%HUGoX@@?5~Ei-?k<%R!M_J5+^+Rgn&{}lZT z{nm=^Z3W(%1pF4EF8h~=e;Nq9g}>dB`Wud+{1^OR8&u!o->%jF#`Eg^UHt#7+`k3C z9a8=V&)fe6elx&)E8*>!=eGnG$G;>P+x%k$^cMf`Z_3|jARrYNAfW&Ch4~i#@7Mc3 g;V|z1fdBQfmz4km^eDeZ&nQ4wfKfDq*RR ⚠️ 중요: - 월 구독료는 원이며, 영업 협상 및 개발 범위에 따라 증액될 수 있습니다. + +- 계약 시 확정된 구독료: [ ]원/월 + +### 4.3 납부 방법 + +- **개발비**: + - 계좌이체 (세금계산서 발행) + - 입금 계좌: 기업은행 170-175519-04-011  (주)코드브릿지엑스 +- **구독료**: + - CMS 자동이체 (권장) + - 또는 세금계산서 발행 후 계좌이체 + +### 4.4 잔금 지급 기한 [법률 검토 반영] + +- **지급 기한**: 서비스 게시일로부터 **3일 이내** +- **사전 준비**: 회사는 영업 단계부터 납품 일정을 공유하여 고객이 미리 준비할 수 있도록 합니다. +- **미납 시 조치**: 제13조 참조 + +### 4.5 사용량 기반 추가 과금 + +기본 제공 한도 초과 시 다음과 같이 실비 과금됩니다. + +| 항목 | 기본 제공 | 추가 과금 기준 | +| --- | --- | --- | +| 파일 저장 공간 | 100GB | 100GB당 100,000원/월 (부가세 별도) | +| AI 토큰 | 월 100만 토큰 | 1,000토큰 단위 실비 과금 | + +- **파일 저장 공간: **기본 100GB를 초과하는 경우 100GB 단위로 월 100,000원(부가세 별도)이 추가 과금됩니다. +- **AI 토큰: **월 100만 토큰 기본 제공되며, 초과 사용 시 1,000토큰 단위로 실비 과금됩니다. + - 미사용 잔여 토큰은 이월되지 않습니다. (매월 1일 갱신) + - 기본 제공량 80%, 100% 소진 시 자동 알림이 발송됩니다. + +### 4.6 바로빌 부가 서비스 요금 + +고객이 선택적으로 이용하는 바로빌 연동 서비스의 요금은 다음과 같습니다. + +| 서비스 | 과금 방식 | 기본 제공 | 추가 과금 | +| --- | --- | --- | --- | +| 계좌조회 | 월정액 10,000원 | 1계좌 | 추가 1계좌당 10,000원 | +| 카드내역 | 월정액 10,000원 | 5장 | 추가 1장당 5,000원 | +| 세금계산서 발행 | 건별 | 100건 | 추가 50건당 5,000원 | + +- **바로빌 서비스 요금은 고객이 부담하며, 월 구독료와 별도로 청구됩니다.** + - 홈택스 매입/매출 조회 서비스(월 30,000원)는 회사가 부담합니다. + - 상기 금액은 부가세 별도입니다. + +## 제5조 (마일스톤 및 진행 일정) + +### 5.1 개발 단계 (5단계 통일) + +| 단계 | 주요 활동 | 진행률 | 기간 | 납부 | +| --- | --- | --- | --- | --- | +| M1 | 요구사항 분석 및 기획 | 20% | [ 2 ]주 | 1차 개발비 (착수금 50%) | +| M2 | 설계 및 개발 착수 | 50% | [ 2 ]주 | - | +| M3 | 개발 진행 (50% 완료) | 60% | [ 2 ]주 | - | +| M4 | 개발 완료 및 테스트 | 80% | [ 2 ]주 | - | +| M5 | 검수 및 서비스 게시 | 100% | 최대 2주 | 2차 개발비 (잔금 50%) | + +> ⚠️ 중요: - 5단계 마일스톤으로 통일 관리 - M5 검수 완료 후 서비스 게시 - 서비스 게시일로부터 3일 이내 잔금 납부 + +### 5.2 일정 조정 + + - 개발 일정은 고객의 협조에 따라 변동될 수 있습니다. + - 고객 귀책 사유로 인한 지연은 회사의 책임이 아닙니다. + - 불가항력으로 인한 지연 시 양측 협의하여 일정을 조정합니다. + +## 제6조 (서비스 게시 및 검수) + +### 6.1 서비스 게시 + +- 회사는 개발 완료 후 고객에게 **서비스 게시**를 통지합니다. +- **서비스 게시일**은 고객이 서비스에 접근 가능한 날짜를 의미합니다. + - 서비스 게시일부터 구독료가 발생합니다. + +### 6.2 검수 기간 + +- 고객은 개발 완료 후 **최대 2주간 검수 기간**을 가집니다. +- 검수 기간은 서비스 게시 **전**에 이루어집니다. + - 검수 기간 중 발견된 하자는 회사가 무상으로 수정합니다. + +### 6.3 검수 완료 + + - 고객이 서면으로 검수 완료를 통지하거나, + - 검수 기간 2주 종료 시점에 특별한 이의가 없으면 자동 승인으로 간주합니다. + - 검수 완료 후 서비스 게시일이 확정되고, 하자담보 책임 정책이 적용됩니다. + +## 제7조 (하자담보 책임) + +### 7.1 책임 기간 + +서비스 게시일로부터 1년 (소프트웨어산업진흥법 제16조, 민법 제667조) + +### 7.2 하자담보 범위 (무상 처리) + +| 항목 | 내용 | 예시 | +| --- | --- | --- | +| 버그 수정 | 소프트웨어 오류 | 계산 오류, 기능 미작동 | +| 미구현 기능 | 계약서에 명시된 기능 누락 | 약속된 기능 미구현 | +| 성능 개선 | 명시된 성능 기준 미달 | 속도 저하, 응답 지연 | +| UI/UX 수정 | 사용성 문제 | 버튼 미작동, 화면 깨짐 | +| 데이터 오류 | 데이터 손실 또는 오류 | 데이터 삭제, 중복 생성 | +| 보안 패치 | 보안 취약점 수정 | 해킹 방지, 암호화 | + +### 7.3 제외 사항 (별도 비용) + +| 항목 | 내용 | 예시 | +| --- | --- | --- | +| 신규 기능 개발 | 계약서에 없던 새 기능 | 새로운 모듈, 기능 확장 | +| 구조 변경 | 시스템 아키텍처 변경 | DB 구조, 프레임워크 교체 | +| 추가 모듈 | 새로운 모듈 개발 | 회계 모듈, 재고 모듈 | +| 기획 변경 | 초기 기획과 다른 요구사항 | 화면 구성, 프로세스 변경 | +| 교육/컨설팅 | 사용자 교육, 업무 컨설팅 | 직원 교육, 프로세스 개선 | + +### 7.4 하자 처리 절차 + +| 단계 | 내용 | 기간 | +| --- | --- | --- | +| 1. 하자 신고 | 고객이 이메일로 하자 신고 | - | +| 2. 하자 확인 | 회사가 하자 여부 판정 | 3영업일 | +| 3. 수정 작업 | 하자 인정 시 무상 수정 | 7영업일 | +| 4. 검수 완료 | 고객이 수정 사항 확인 | - | + +> ⚠️ 긴급 하자 (서비스 중단)는 24시간 이내 조치합니다. + +### 7.5 책임 면제 사유 + +다음의 경우 하자담보 책임이 면제됩니다: +- **고객 귀책 사유**: + - 고객의 임의 수정 또는 변경 + - 승인되지 않은 제3자 개입 + - 사용 환경 미준수 +- **불가항력**: + - 천재지변 (지진, 태풍 등) + - 전쟁, 테러, 전염병 + - 정부 규제 또는 법령 변경 +- **기간 만료**: + - 서비스 게시일로부터 1년 경과 + +## 제8조 (계약 해제 및 환불) + +### 8.1 환불 정책 개요 + +고객의 임의 해제 권리와 회사의 투입 비용 보전의 균형을 고려하여 수립되었습니다. + +### 8.2 단계별 환불 + +### Phase 1: 상담(인터뷰) 시작 전 + +- **환불율**: 100% (전액 환불) +- **조건**: 계약 후 상담(인터뷰) 배정 전 +- **위약금**: 없음 +- **임의 해제 가능** + +### Phase 2: 상담(인터뷰) 시작 후, 개발 착수 전 + +| 진행 상황 | 환불율 | 공제 내역 | +| --- | --- | --- | +| M1: 기획안 작성 중 (50% 미만) | 80% | 상담매니저 및 기획/개발자 투입 비용 20% 공제 | +| M2: 기획안 완료 (50% 이상) | 50% | 상담매니저 및 기획/개발자 투입 비용 50% 공제 | + +### Phase 3: 개발 진행 중 (5단계 마일스톤 기준) + +| 마일스톤 | 진행률 | 청구 금액(개발비 대비) | 비고 | +| --- | --- | --- | --- | +| M3: 개발 진행 중 (50%) | 70% | 70% | 30% 환불 | +| M4: 개발 완료 및 테스트 | 90% | 90% | 10% 환불 | +| M5: 서비스 개시 완료 | 100% | 100% | 환불 불가 | + +> ⚠️ 중요: 5단계 마일스톤으로 통일 관리 + +### Phase 4: 서비스 게시 후 + +- **환불율**: 0% (환불 불가) +- **개발비**: 전액 확정, 환불 불가 +- **구독료**: 매월 말일 후불제이므로 사용한 만큼만 청구 (환불 개념 없음) +- **대신 제공**: 하자담보 책임 (1년) + 유지보수 (구독 기간 전체) + +### 8.3 환불 불가 사유 + +- **고객 귀책 사유**: + - 협조 지연으로 인한 개발 지연 + - 요구사항 변경으로 인한 추가 개발 + - 승인 거부 또는 회피 +- **약관 위반**: + - 허위 정보 제공 + - 부정 사용 또는 재판매 + - 회사 명예 훼손 + +### 8.4 할인 계약 해지 시 추가 조건 + +본 계약이 정상가 대비 할인 조건으로 체결된 경우, 다음 조건이 추가 적용된다. + +- 발주자 귀책 해지 시 정상가(할인 전 금액) 기준으로 정산한다. + +## 제9조 (구독 및 해지) + +### 9.1 구독 시작 + +- **시작일**: 서비스 게시일 (검수 완료 후) +- **결제일**: 매월 말일 +- **청구 방식**: 후불제 (해당 월 사용량 기준) +- **일할 계산**: (사용 일수 / 해당 월 일수) × 구독료 + +> ⚠️ 중요: - 계약 시 확정된 구독료 금액은 [ ]원/월입니다. + +- 매월 말일에 해당 월 사용일수만큼만 후불 청구됩니다. + +### 9.2 구독 해지 + + - 고객은 언제든지 구독을 해지할 수 있습니다. (위약금 없음) + - 해지 신청 후 30일간 데이터 백업 기간 제공 + - 해지일로부터 30일 후 모든 데이터 완전 삭제 + +## 제10조 (유지보수 정책) + +### 10.1 유지보수 개요 + +- **적용 대상**: 구독료를 정상 납부하는 고객 +- **적용 기간**: 구독 기간 전체 (하자담보 책임 1년 이후에도 구독 중이면 계속 제공) +- **비용**: 월 구독료(500,000원)에 포함 + +### 10.2 하자담보 책임과의 차이 + +| 구분 | 하자담보 책임 (제7조) | 유지보수 (제9조의2) | +| --- | --- | --- | +| 기간 | 서비스 게시일로부터 1년 | 구독 기간 전체 | +| 근거 | 법적 의무 (소프트웨어산업진흥법) | 계약 조건 | +| 비용 | 무상 | 구독료에 포함 | +| 범위 | 하자(버그, 미구현 등) | 하자 + 일반 유지보수 | + +### 10.3 유지보수 범위 (구독료에 포함) + +> ✅ 무상 제공: - 모든 버그 수정 및 오류 처리 - 보안 패치 및 업데이트 - 성능 최적화 - 긴급 장애 대응 (24시간 이내) - 데이터 백업 및 복구 - 기술 지원 (고객센터, 이메일) - 플랫폼 업데이트 (프레임워크, 브라우저 호환성) + +> ❌ 별도 비용: - 신규 기능 개발 - 커스터마이징 및 추가 개발 - 기획 변경 (화면 구성, 프로세스 변경) - 외부 시스템 연동 - 추가 교육 및 컨설팅 + +### 10.4 서비스 레벨 (SLA) + +| 심각도 | 상황 | 응답 시간 | 해결 목표 | +| --- | --- | --- | --- | +| 긴급 (P0) | 서비스 완전 중단 | 1시간 | 24시간 | +| 높음 (P1) | 주요 기능 장애 | 4시간 | 3영업일 | +| 보통 (P2) | 일반 버그 | 1영업일 | 7영업일 | +| 낮음 (P3) | 문의/안내 | 1영업일 | 3영업일 | + +### 10.5 정기 유지보수 + +- **월간**: 보안 패치, 백업 점검 (매월 첫째 주 일요일 새벽) +- **분기**: 성능 최적화 (분기 말 일요일 새벽) +- **반기**: 시스템 점검 (6월/12월 일요일 새벽) + +> ⚠️ 모든 정기 점검은 최소 7일 전 사전 공지됩니다. + +### 10.6 유지보수 신청 + +- **고객센터**: 02-6347-0005 (평일 09:00~18:00 ) +- **이메일**: support@codebridge-x.com (24시간) +- **시스템 내**: SAM 시스템 내 고객지원 메뉴 + +### 10.7 유지보수 종료 + +다음의 경우 유지보수 서비스가 종료됩니다: 1. 구독 해지 시 2. 구독료 3개월 연속 미납 시 3. 중대한 약관 위반 시 + +## 제11조 (고객의 의무) + +고객은 다음 사항을 준수해야 합니다: +- **정확한 정보 제공**: 허위 정보 제공 금지 +- **협조 의무**: 개발에 필요한 자료 및 정보 제공 +- **부정 사용 금지**: 서비스의 재판매, 재배포 금지 +- **지적재산권 존중**: 무단 복제, 역설계 금지 + +## 제12조 (회사의 의무) + +회사는 다음 사항을 준수합니다: +- **서비스 제공**: 계약서에 명시된 서비스 제공 +- **하자담보 책임**: 1년간 하자 무상 수정 +- **개인정보 보호**: 개인정보보호법 준수 +- **기술 지원**: 고객센터 운영 및 기술 지원 + +## 제13조 (미입금 시 법적 조치) + +### 13.1 개발비 미입금 절차 + +| 단계 | 시점 | 조치 내용 | +| --- | --- | --- | +| 1차 독촉 | 기한 경과 후 3일 | 이메일 및 SMS 발송 | +| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 | +| 추심등 | 기한 경과 후 14일 | 신용정보사 연체 등록, 법률대리인 위임 | +| 법적 조치 | 기한 경과 후 30일 | 지급명령 신청 또는 소송 제기 | + +### 13.2 구독료 미입금 절차 + +| 단계 | 시점 | 조치 내용 | +| --- | --- | --- | +| 1차 실패 | 익일 | 재출금 | +| 2차 실패 | 기한 경과 후 3일 | 재출금 | +| 3차 실패 | 미수금 처리 | 서비스 접근 제한, 1차 독촉 | +| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 | +| 서비스 중단 | 기한 경과 후 14일 | 로그인 불가, 데이터 격리 | +| 강제 해지 | 기한 경과 후 30일 | 계약 해지, 법적 조치 검토 | + +## 제14조 (개인정보 보호) + + - 회사는 「개인정보 보호법」을 준수합니다. + - 고객의 개인정보는 서비스 제공 목적으로만 사용됩니다. + - 제3자에게 제공하지 않습니다. (법령 제외) + - 계약 종료 시 개인정보는 즉시 삭제됩니다. (법정 보관 의무 제외) + +## 제15조 (지적재산권) + +- **소유권**: 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. +- **사용 권한**: 고객은 서비스 사용 권한만을 부여받습니다. +- **금지 사항**: 복제, 배포, 역설계, 재판매 금지 + +## 제16조 (손해배상) + + - 회사 또는 고객이 본 계약을 위반하여 상대방에게 손해를 입힌 경우 배상 책임이 있습니다. + - 다만, 불가항력으로 인한 손해는 배상 책임에서 제외됩니다. + +## 제17조 (불가항력) + +다음의 사유로 서비스 제공이 불가능한 경우 회사는 책임을 지지 않습니다: + - 천재지변 (지진, 태풍, 홍수 등) + - 전쟁, 테러, 전염병 + - 정부 규제 또는 법령 변경 + - 제3자의 불법 행위 (해킹 등) + +## 제18조 (분쟁 해결) + + - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. +- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. + +## 제19조 (계약의 효력) + + - 본 계약은 계약일로부터 효력이 발생합니다. + - 본 계약은 구독 해지 시까지 유효합니다. + +## 제20조 (기타) + + - 본 계약서는 2부 작성하여 회사와 고객이 각 1부씩 보관합니다. + - 본 계약의 해석 및 적용은 대한민국 법률을 준거법으로 합니다. + +## 계약 당사자 + +### [회사] + +상호: 주식회사 코드브릿지엑스 +대표자: 이의찬 +사업자등록번호: 664-86-03713 +주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 +이메일: contact@codebridge-x.com +전화: 02-6347-0005 +서명: +날짜: + +### [고객] + +상호: +대표자: +사업자등록번호: +주소: +이메일: +전화: +서명: +날짜: + +## 별첨 + +### 별첨 1: 기획서 + +[별도 첨부] + +### 별첨 2: 개발 일정표 + +[별도 첨부] + +### 별첨 3: 기능 명세서 + +[별도 첨부] + +주식회사 코드브릿지엑스 +이메일: contact@codebridge-x.com +전화: 02-6347-0005 +주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 + diff --git a/contracts/markdown/02-nda.md b/contracts/markdown/02-nda.md new file mode 100644 index 0000000..cb6ff9c --- /dev/null +++ b/contracts/markdown/02-nda.md @@ -0,0 +1,199 @@ +--- +title: "비밀유지서약서 (NDA)" +version: "v4.0" +date: "2026-02-22" +docx_file: "비밀유지서약서.docx" +--- + +# 비밀유지서약서 (NDA) + +- **작성일**: + +- **서약인 정보** +- **구분**: + +- **인적 사항:** +상호(성명): _______________ +대표자(본인): _______________ +사업자등록번호(주민등록번호): ____________________ +주소: ______________________________________________________________________ +연락처: _______________ +이메일: _______________ + +## 제1조 (목적) + + - 본 서약서는 주식회사 코드브릿지(이하 “회사”)와의 업무 관계에서 알게 된 기밀 정보를 + - 보호하기 위해 작성되었습니다. + +## 제2조 (기밀 정보의 정의) + + - 다음 각 호의 정보는 회사의 기밀 정보로 간주됩니다: + +### 2.1 고객 정보 + + - 고객사 명단 (법인명, 대표자명, 연락처) + - 고객사 담당자 정보 (성명, 부서, 연락처, 이메일) + - 계약 내역 (가입비, 할인율, 구독료, 특약 사항) + - 고객사의 사업 정보 (매출, 직원 수, 거래처 등) + - 고객사가 회사에 요구한 개발 내역 및 기획 문서 + +### 2.2 영업 정보 + + - 가격 정책 (정가, 할인 정책, 최소 가입비) + - 수수료 정책 (비율, 지급 기준, 상계 방식) + - 영업 전략 및 마케팅 계획 + - 잠재 고객 리스트 + - 계약 체결 노하우 및 제안서 템플릿 + +### 2.3 기술 정보 + + - SAM 시스템의 소스코드 + - 데이터베이스 구조 및 설계 문서 + - 개발 프로세스 및 방법론 + - 서버 인프라 구성 및 보안 정책 + - API 키, 접속 정보, 관리자 권한 + +### 2.4 경영 정보 + + - 회사의 재무 정보 (매출, 이익, 원가) + - 조직도 및 인사 정보 + - 사업 계획 및 전략 + - 투자 유치 및 M&A 관련 정보 + +### 2.5 기타 + +- 회사가 **“기밀(Confidential)”** 또는 **“대외비”**로 표시한 모든 문서 및 정보 + +## 제3조 (기밀 유지 의무) + +### 3.1 기본 의무 + + - 본인은 업무 수행 중 알게 된 모든 기밀 정보를: +- **외부에 누설하지 않습니다** +- **업무 목적 외에 사용하지 않습니다** +- **무단으로 복사, 복제, 전송하지 않습니다** +- **제3자에게 제공하거나 공개하지 않습니다** + +### 3.2 정보 관리 + + - 기밀 문서는 안전한 장소에 보관 + - 이메일, 메신저 등 전송 시 암호화 + - 업무 종료 시 모든 기밀 자료 반환 또는 파기 + - 개인 디바이스에 기밀 정보 저장 금지 + +### 3.3 제3자 접근 차단 + + - 가족, 친구 등 타인이 기밀 정보에 접근하지 못하도록 조치 + - 공공장소(카페, 도서관 등)에서 기밀 정보 취급 금지 + - 비밀번호 및 접속 정보 타인 공유 금지 + +## 제4조 (예외 사항) + + - 다음의 정보는 기밀 정보에서 제외됩니다: + - 본인이 알기 전에 이미 공개된 정보 + - 본인의 귀책사유 없이 공개된 정보 + - 제3자로부터 적법하게 취득한 정보 + - 본인이 독자적으로 개발한 정보 + - 법원, 정부기관의 법적 요구에 따라 공개해야 하는 정보 (단, 회사에 사전 통지 필수) + +## 제5조 (의무 기간) + +### 5.1 기간 + + - 본 서약서의 기밀 유지 의무는: +- **계약 체결일로부터 효력 발생** +- **계약 종료 후 2년간 유지** + +### 5.2 영구 보호 + +- 단, 다음 정보는 **영구적으로** 보호됩니다: + - 고객사 개인정보 + - 회사의 영업 비밀 (부정경쟁방지법상 영업 비밀) + - 기술 정보 (특허, 저작권 대상) + +## 제6조 (위반 시 책임) + +### 6.1 민사 책임 + + - 본인이 본 서약을 위반하여 회사 또는 고객에게 손해를 입힌 경우: +- **실손해**** 배상**: 실제 발생한 손해 전액 +- **징벌적 손해배상**: 실손해의 최대 3배 (악의적 유출 시) +- **법률 비용**: 소송 비용, 변호사 비용 등 + +### 6.2 형사 책임 + + - 다음의 경우 형사 고발 대상이 됩니다: +- **부정경쟁방지법** 위반 (영업 비밀 침해) +- **개인정보보호법** 위반 (고객 정보 유출) +- **정보통신망법** 위반 (기술 정보 침해) +- **형법** 위반 (업무상 배임) +- **※ 형사 처벌: 5년 이하 징역 또는 5천만원 이하 벌금** + +### 6.3 계약 해지 + + - 회사는 본 서약 위반 시 즉시 계약을 해지할 수 있으며, 이미 지급한 수수료 또는 + - 대금을 환수할 수 있습니다. + +## 제7조 (자료 반환) + +### 7.1 반환 대상 + + - 계약 종료 또는 요청 시 다음을 즉시 반환해야 합니다: + - 회사로부터 제공받은 모든 문서 (종이, 파일) + - 고객사 계약서 및 개인정보 + - 가격표, 제안서, 템플릿 등 영업 자료 + - USB, 하드디스크 등 저장 매체 + +### 7.2 파기 확인 + +- 반환 불가능한 파일(이메일, 클라우드 등)은 즉시 삭제하고, **삭제 확인서**를 회사에 + - 제출해야 합니다. + +## 제8조 (경업 금지) + +### 8.1 경업 금지 기간 + +- 계약 종료 후 **6개월간** 다음 행위를 금지합니다: + - 회사의 고객에게 경쟁 제품 판매 + - 회사의 기밀 정보를 이용한 유사 사업 + - 회사 직원 또는 영업파트너를 스카우트 + +### 8.2 예외 + + - 단순히 경쟁사 제품을 판매하는 것은 허용되나, 회사의 기밀 정보를 활용해서는 + - 안 됩니다. + +## 제9조 (분쟁 해결) + +### 9.1 관할 법원 + + - 본 서약과 관련된 분쟁은 회사 본사 소재지 관할 법원으로 합니다. + +### 9.2 준거법 + + - 본 서약은 대한민국 법률에 따라 해석됩니다. + +- **서약 확인** + - 본인은 위 내용을 충분히 이해하였으며, 이를 성실히 준수할 것을 서약합니다. +- **서약일**: ___________________ +- **서약인** +상호(성명): _______________ +대표자(본인): _______________ +주민등록번호(또는 사업자등록번호): _______________ +- **서명 또는 인**: _______________ + +- **수령인 (주식회사 ****코드브릿지엑스****)** + - 대표이사: 이의찬 +- **확인****일**: ___________________ +- **서명 또는 인**: _______________ + +- **참고: 관련 법률** +- **부정경쟁방지법 제2조 (영업비밀)** + - “영업비밀”이란 공공연히 알려져 있지 아니하고 독립된 경제적 가치를 가지는 것으로서, + - 비밀로 관리된 생산방법, 판매방법, 그 밖에 영업활동에 유용한 기술상 또는 경영상의 + - 정보를 말한다. +- **부정경쟁방지법 제18조 (벌칙)** +- 영업비밀을 외국에서 사용하거나 외국에서 사용되게 할 목적으로 취득·사용 또는 제3자에게 누설한 자는 **15년 이하의 징역** 또는 **15억원 이하의 벌금**에 처한다. + +- **※ 본 서약서는 2부를 작성하여 회사와 서약인이 각 1부씩 보관합니다.** +- **※ 서약 위반 시 민·형사상 책임을 질 수 있습니다.** \ No newline at end of file diff --git a/contracts/markdown/03-partner-agreement.md b/contracts/markdown/03-partner-agreement.md new file mode 100644 index 0000000..81e6af5 --- /dev/null +++ b/contracts/markdown/03-partner-agreement.md @@ -0,0 +1,276 @@ +--- +title: "영업파트너 위촉계약서" +version: "v4.0" +date: "2026-02-22" +docx_file: "영업파트너 위촉계약서.docx" +--- + +# < 영업파트너 위촉계약서 > + +# Sales Partner Engagement Agreement + + - 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다. + +## 제1조 (계약의 목적) + + - 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를 + - 명확히 함을 목적으로 합니다. + +## 제2조 (용어의 정의) + +- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너 +- **관리자**: 판매자를 관리하고 지원하는 상급 영업파트너 +- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용 +- **수수료**: 파트너가 영업 활동의 대가로 받는 보상 +- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것 + +## 제3조 (파트너의 역할 및 업무) + +### 3.1 판매자의 역할 + + - 잠재 고객 발굴 및 초기 접촉 + - SAM 서비스 소개 및 제안 + - 고객과의 계약 체결 지원 + - 계약 후 고객 관리 및 사후 지원 + +### 3.2 관리자의 역할 + + - 판매자 모집 및 관리 + - 판매자 교육 및 지원 + - 영업 전략 수립 및 실행 + - 회사와 판매자 간 소통 중재 + +### 3.3 공통 의무 + + - 회사의 브랜드 이미지 유지 + - 정확한 정보 제공 + - 윤리적 영업 활동 준수 + - 비밀 유지 의무 + +## 제4조 (수수료 정책) + +### 4.1 수수료 비율 + +| 역할 | 수수료 비율 | 산정 기준 | +| --- | --- | --- | +| 판매자 | 개발비의 20% | 1차,2차 입금액 기준 | +| 관리자 | 개발비의 5% | 1차,2차 입금액 기준 | + +### 4.2 수수료 산정 예시 + +- **총 개발비 80,000,000원 계약 시** + +| 단계 | 고객 입금 | 판매자 수수료 (20%) | 관리자 수수료 (5%) | +| --- | --- | --- | --- | +| 1차 착수금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 | +| 2차 잔금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 | +| 총계 | 80,000,000원 | 16,000,000원 | 4,000,000원 | + +- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다. + +### 4.3 지급 시기 + +- **지급일**: 고객 입금일 **익월 10일** +- **지급 방식**: 계좌 이체 +- **세금**: 3.3% 원천징수 (사업소득) + +### 4.4 수수료 지급 조건 + + - 고객이 개발비를 실제로 입금한 경우에만 지급 + - 계약 해지 또는 환불 시 수수료 미지급 또는 환수 + - 파트너가 계약 위반 시 수수료 지급 보류 + +## 제5조 (수수료 정책 변경) + +### 5.1 사전 고지 의무 + +- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다. + - 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다. + - 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다. + +### 5.2 변경 효력 + +- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다. + - 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다. + - 진행 중인 계약은 최초 약정 조건을 유지합니다. + +### 5.3 변경 예시 + +#### 예시 1: 수수료율 변경 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 판매자 수수료 20% → 18% + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +#### 예시 2: 수수료 정책 폐지 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 수수료 정책 완전 폐지 + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +## 제6조 (계약 기간) + +- 본 계약은 계약일로부터 **1년간** 유효합니다. +- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다. + - 자동 연장은 동일한 조건으로 반복됩니다. + +## 제7조 (계약 해지) + +### 7.1 일반 해지 (양방향) + +- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다. +- **통지 방법**: 이메일 또는 등기우편 +- **효력 발생**: 통지일로부터 30일 후 +- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급 +- **예시**: + - 통지일: 2026년 2월 1일 + - 해지일: 2026년 3월 1일 + - 2월 중 발생한 수수료는 3월 10일 정상 지급 + +### 7.2 즉시 해지 사유 + +- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다: +- **(1) 품위 유지 결격사유 발생 [신설]** + - 음주운전으로 적발된 경우 + - 형사 범죄로 기소 또는 구속된 경우 + - 사회적 물의를 일으킨 경우 + - 기타 파트너로서의 품위를 훼손한 경우 +- **(2) 계약 위반** + - 허위 정보 제공 또는 사기 행위 + - 회사 명예 훼손 또는 영업 방해 + - 비밀 유지 의무 위반 + - 중대한 업무 태만 +- **(3) 부정 행위** + - 고객으로부터 금품 수수 + - 계약서 위조 또는 변조 + - 회사 자산 횡령 또는 유용 + +### 7.3 즉시 해지 시 조치 + + - 미지급 수수료는 지급하지 않습니다. + - 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외) + - 진행 중인 계약은 회사가 직접 관리합니다. + +## 제8조 (비밀 유지) + +### 8.1 비밀 정보 + + - 다음 정보는 비밀로 유지되어야 합니다: + - 회사의 영업 전략 및 계획 + - 고객 정보 (회사명, 담당자, 연락처 등) + - 수수료 정책 및 계약 조건 + - 기술 정보 및 노하우 + - 회사 내부 자료 + +### 8.2 비밀 유지 의무 + + - 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다. +- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다. + - 위반 시 손해배상 책임이 있습니다. + +## 제9조 (지적재산권) + + - SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. + - 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다. + - 영업 활동에 필요한 자료는 회사가 제공합니다. + +## 제10조 (세금 및 원천징수) + +### 10.1 사업소득 + +- 파트너 수수료는 **사업소득**으로 간주됩니다. + +### 10.2 원천징수 + +| 항목 | 비율 | 비고 | +| --- | --- | --- | +| 소득세 | 3.0% | | +| 지방소득세 | 0.3% | 소득세의 10% | +| 합계 | 3.3% | | + +### 10.3 지급명세서 + +- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다. + +## 제11조 (손해배상) + +### 11.1 파트너의 귀책 사유 + + - 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다: + - 허위 정보 제공으로 계약 취소 + - 고객과의 분쟁으로 회사 명예 훼손 + - 비밀 유지 의무 위반 + - 부정 행위 + +### 11.2 회사의 귀책 사유 + + - 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다. + +## 제12조 (분쟁 해결) + + - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. +- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. + +## 제13조 (기타 사항) + +### 13.1 계약서 교부 + + - 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다. + +### 13.2 통지 + + - 모든 통지는 다음의 연락처로 발송됩니다: +- **회사**: +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- **파트너**: +- 이메일: +- 전화: + +### 13.3 준거법 + + - 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다. + +- **계약 당사자** +- **[회사]** +- **상호**: 주식회사 코드브릿지엑스 +- **대표자**: 이의찬 (인) +- **사업자등록번호**: 664-86-03713 +- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 +- **이메일**: admin@codebridge-x.com +- **전화**: 02-6347-0005 +- **날짜**: + +- **[파트너]** +- **상호/성명**: +- **대표자/본인**: (서명) +- **사업자등록번호**: +- **주소**: +- **이메일**: +- **전화**: +- **날짜**: + +- **별첨** + +#### 별첨 1: 수수료 정산표 + +| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 | +| --- | --- | --- | --- | --- | --- | --- | +| | | | | | | | + +#### 별첨 2: 영업 활동 보고서 + +| 날짜 | 활동내용 | 고객사 | 진행 상황 | +| --- | --- | --- | --- | +| | | | | + + - 첨부 서류 + - 사업자등록증 사본 (사업자인 경우) + - 주민등록등본 사본 (개인인 경우) + - 통장 사본 (수수료 입금용) + - 비밀유지서약서 + +- **주식회사 코드브릿지엑스** +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 diff --git a/contracts/markdown/04-partner-agreement-group.md b/contracts/markdown/04-partner-agreement-group.md new file mode 100644 index 0000000..b3251c4 --- /dev/null +++ b/contracts/markdown/04-partner-agreement-group.md @@ -0,0 +1,267 @@ +--- +title: "영업파트너 위촉계약서 (단체용)" +version: "v4.0" +date: "2026-02-22" +docx_file: "영업파트너 위촉계약서(단체용).docx" +--- + +# < 영업파트너 위촉계약서 > + +# Sales Partner Engagement Agreement + + - 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다. + +## 제1조 (계약의 목적) + + - 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를 + - 명확히 함을 목적으로 합니다. + +## 제2조 (용어의 정의) + +- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너 +- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용 +- **수수료**: 파트너가 영업 활동의 대가로 받는 보상 +- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것 + +## 제3조 (파트너의 역할 및 업무) + +### 3.1 판매자의 역할 + + - 잠재 고객 발굴 및 초기 접촉 + - SAM 서비스 소개 및 제안 + - 고객과의 계약 체결 지원 + - 계약 후 고객 관리 및 사후 지원 + +### 3.2 공통 의무 + + - 회사의 브랜드 이미지 유지 + - 정확한 정보 제공 + - 윤리적 영업 활동 준수 + - 비밀 유지 의무 + +## 제4조 (수수료 정책) + +### 4.1 수수료 비율 + +| 역할 | 수수료 비율 | 산정 기준 | +| --- | --- | --- | +| 판매자 | 개발비의 30% | 1차,2차 입금액 기준 | + +### 4.2 수수료 산정 예시 + +- **총 개발비 80,000,000원 계약 시** + +| 단계 | 고객 입금 | 판매자 수수료 (30%) | +| --- | --- | --- | +| 1차 착수금 (50%) | 40,000,000원 | 12,000,000원 | +| 2차 잔금 (50%) | 40,000,000원 | 12,000,000원 | +| 총계 | 80,000,000원 | 24,000,000원 | + +- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다. + +### 4.3 지급 시기 + +- **지급일**: 고객 입금일 **익월 10일** +- **지급 방식**: 계좌 이체 +- **세금**: 사업소득일 경우 3.3% 원천징수 + +### 4.4 수수료 지급 조건 + + - 고객이 개발비를 실제로 입금한 경우에만 지급 + - 계약 해지 또는 환불 시 수수료 미지급 또는 환수 + - 파트너가 계약 위반 시 수수료 지급 보류 + +## 제5조 (수수료 정책 변경) + +### 5.1 사전 고지 의무 + +- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다. + - 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다. + - 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다. + +### 5.2 변경 효력 + +- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다. + - 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다. + - 진행 중인 계약은 최초 약정 조건을 유지합니다. + +### 5.3 변경 예시 + +#### 예시 1: 수수료율 변경 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 판매자 수수료 20% → 18% + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +#### 예시 2: 수수료 정책 폐지 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 수수료 정책 완전 폐지 + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +## 제6조 (계약 기간) + +- 본 계약은 계약일로부터 **1년간** 유효합니다. +- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다. + - 자동 연장은 동일한 조건으로 반복됩니다. + +## 제7조 (계약 해지) + +### 7.1 일반 해지 (양방향) + +- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다. +- **통지 방법**: 이메일 또는 등기우편 +- **효력 발생**: 통지일로부터 30일 후 +- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급 +- **예시**: + - 통지일: 2026년 2월 1일 + - 해지일: 2026년 3월 1일 + - 2월 중 발생한 수수료는 3월 10일 정상 지급 + +### 7.2 즉시 해지 사유 + +- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다: +- **(1) 품위 유지 결격사유 발생 [신설]** + - 음주운전으로 적발된 경우 + - 형사 범죄로 기소 또는 구속된 경우 + - 사회적 물의를 일으킨 경우 + - 기타 파트너로서의 품위를 훼손한 경우 +- **(2) 계약 위반** + - 허위 정보 제공 또는 사기 행위 + - 회사 명예 훼손 또는 영업 방해 + - 비밀 유지 의무 위반 + - 중대한 업무 태만 +- **(3) 부정 행위** + - 고객으로부터 금품 수수 + - 계약서 위조 또는 변조 + - 회사 자산 횡령 또는 유용 + +### 7.3 즉시 해지 시 조치 + + - 미지급 수수료는 지급하지 않습니다. + - 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외) + - 진행 중인 계약은 회사가 직접 관리합니다. + +## 제8조 (비밀 유지) + +### 8.1 비밀 정보 + + - 다음 정보는 비밀로 유지되어야 합니다: + - 회사의 영업 전략 및 계획 + - 고객 정보 (회사명, 담당자, 연락처 등) + - 수수료 정책 및 계약 조건 + - 기술 정보 및 노하우 + - 회사 내부 자료 + +### 8.2 비밀 유지 의무 + + - 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다. +- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다. + - 위반 시 손해배상 책임이 있습니다. + +## 제9조 (지적재산권) + + - SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. + - 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다. + - 영업 활동에 필요한 자료는 회사가 제공합니다. + +## 제10조 (세금 및 원천징수) + +### 10.1 사업소득 + +- 파트너 수수료는 **사업소득**으로 간주됩니다. + +### 10.2 원천징수 + +| 항목 | 비율 | 비고 | +| --- | --- | --- | +| 소득세 | 3.0% | | +| 지방소득세 | 0.3% | 소득세의 10% | +| 합계 | 3.3% | | + +### 10.3 지급명세서 + +- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다. + +## 제11조 (손해배상) + +### 11.1 파트너의 귀책 사유 + + - 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다: + - 허위 정보 제공으로 계약 취소 + - 고객과의 분쟁으로 회사 명예 훼손 + - 비밀 유지 의무 위반 + - 부정 행위 + +### 11.2 회사의 귀책 사유 + + - 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다. + +## 제12조 (분쟁 해결) + + - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. +- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. + +## 제13조 (기타 사항) + +### 13.1 계약서 교부 + + - 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다. + +### 13.2 통지 + + - 모든 통지는 다음의 연락처로 발송됩니다: +- **회사**: +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- **파트너**: +- 이메일: +- 전화: + +### 13.3 준거법 + + - 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다. + +- **계약 당사자** +- **[회사]** +- **상호**: 주식회사 코드브릿지엑스 +- **대표자**: 이의찬 (인) +- **사업자등록번호**: 664-86-03713 +- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 +- **이메일**: admin@codebridge-x.com +- **전화**: 02-6347-0005 +- **날짜**: + +- **[파트너]** +- **상호/성명**: +- **대표자/본인**: (서명) +- **사업자등록번호**: +- **주소**: +- **이메일**: +- **전화**: +- **날짜**: + +- **별첨** + +#### 별첨 1: 수수료 정산표 + +| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 | +| --- | --- | --- | --- | --- | --- | --- | +| | | | | | | | + +#### 별첨 2: 영업 활동 보고서 + +| 날짜 | 활동내용 | 고객사 | 진행 상황 | +| --- | --- | --- | --- | +| | | | | + + - 첨부 서류 + - 사업자등록증 사본 (사업자인 경우) + - 주민등록등본 사본 (개인인 경우) + - 통장 사본 (수수료 입금용) + - 비밀유지서약서 + +- **주식회사 코드브릿지엑스** +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 diff --git a/contracts/revisions.json b/contracts/revisions.json new file mode 100644 index 0000000..1bcd843 --- /dev/null +++ b/contracts/revisions.json @@ -0,0 +1,58 @@ +{ + "documents": { + "01-service-agreement": { + "title": "고객사 서비스 이용계약서", + "docx_file": "01_고객_서비스이용계약서_v4_0_전자서명용.docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + }, + { + "version": "v4.1", + "date": "2026-02-22", + "author": "개발팀", + "description": "제4조에 사용량 기반 추가 과금(4.5) 및 바로빌 부가 서비스 요금(4.6) 조항 추가" + } + ] + }, + "02-nda": { + "title": "비밀유지서약서 (NDA)", + "docx_file": "비밀유지서약서.docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + } + ] + }, + "03-partner-agreement": { + "title": "영업파트너 위촉계약서", + "docx_file": "영업파트너 위촉계약서.docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + } + ] + }, + "04-partner-agreement-group": { + "title": "영업파트너 위촉계약서 (단체용)", + "docx_file": "영업파트너 위촉계약서(단체용).docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + } + ] + } + } +} diff --git a/contracts/scripts/extract_to_markdown.py b/contracts/scripts/extract_to_markdown.py new file mode 100644 index 0000000..ea44889 --- /dev/null +++ b/contracts/scripts/extract_to_markdown.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +""" +DOCX → Markdown 추출 스크립트 + +4개 전자계약 DOCX 파일을 Markdown으로 변환한다. +- 서비스이용계약서: Heading 스타일 기반 매핑 +- 나머지 3개: Bold 런 + 패턴 매칭으로 구조 유추 +""" + +import re +import sys +from datetime import date +from pathlib import Path + +from docx import Document + +# 경로 설정 +BASE_DIR = Path(__file__).resolve().parent.parent +DOCX_DIR = BASE_DIR / "docx" +MD_DIR = BASE_DIR / "markdown" + +# DOCX → Markdown 매핑 +FILE_MAP = { + "01_고객_서비스이용계약서_v4_0_전자서명용.docx": { + "output": "01-service-agreement.md", + "title": "고객사 서비스 이용계약서", + "type": "styled", + }, + "비밀유지서약서.docx": { + "output": "02-nda.md", + "title": "비밀유지서약서 (NDA)", + "type": "pattern", + }, + "영업파트너 위촉계약서.docx": { + "output": "03-partner-agreement.md", + "title": "영업파트너 위촉계약서", + "type": "pattern", + }, + "영업파트너 위촉계약서(단체용).docx": { + "output": "04-partner-agreement-group.md", + "title": "영업파트너 위촉계약서 (단체용)", + "type": "pattern", + }, +} + + +def table_to_markdown(table): + """DOCX 테이블을 Markdown 테이블로 변환""" + rows = [] + for row in table.rows: + cells = [cell.text.strip().replace("\n", " ") for cell in row.cells] + rows.append(cells) + + if not rows: + return "" + + lines = [] + # 헤더 + lines.append("| " + " | ".join(rows[0]) + " |") + lines.append("| " + " | ".join(["---"] * len(rows[0])) + " |") + # 본문 + for row in rows[1:]: + # 셀 개수 맞추기 + while len(row) < len(rows[0]): + row.append("") + lines.append("| " + " | ".join(row[: len(rows[0])]) + " |") + + return "\n".join(lines) + + +def get_paragraph_heading_level_styled(para): + """스타일 기반 문서의 헤딩 레벨 판별 (서비스이용계약서)""" + style = para.style.name if para.style else "" + + if style == "Heading 1": + return 1 + elif style == "Heading 2": + return 2 + elif style == "Heading 3": + return 3 + + return 0 + + +def get_paragraph_heading_level_pattern(para): + """패턴 매칭 기반 문서의 헤딩 레벨 판별 (비밀유지서약서, 영업파트너 위촉계약서)""" + text = para.text.strip() + has_bold = any(r.bold for r in para.runs if r.bold) + + if not text or not has_bold: + return 0 + + # "제X조" 패턴 → ## (h2) + if re.match(r"^ 0: + lines.append("") + lines.append(f"{'#' * level} {text}") + lines.append("") + elif style == "Compact": + # Bold 런이 있으면 강조 리스트 + has_bold = any(r.bold for r in para.runs if r.bold) + if has_bold: + # Bold 부분과 일반 부분 분리 + parts = [] + for run in para.runs: + if run.bold: + parts.append(f"**{run.text}**") + else: + parts.append(run.text) + combined = "".join(parts) + lines.append(f"- {combined}") + else: + # 들여쓰기된 하위 항목 + lines.append(f" - {text}") + elif style in ("Body Text", "First Paragraph"): + # 본문 텍스트 + if text.startswith("⚠️") or text.startswith("✅") or text.startswith("❌"): + lines.append("") + lines.append(f"> {text}") + lines.append("") + else: + lines.append(text) + else: + lines.append(text) + + elif tag == "tbl": + if table_idx <= len(doc.tables): + current_table_idx = sum( + 1 + for c in list(body)[: list(body).index(child)] + if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl" + ) + if current_table_idx < len(doc.tables): + lines.append("") + lines.append(table_to_markdown(doc.tables[current_table_idx])) + lines.append("") + + return "\n".join(lines) + + +def extract_pattern_doc(doc, file_info): + """패턴 매칭 기반 문서 추출 (비밀유지서약서, 영업파트너 위촉계약서)""" + lines = [] + + body = doc.element.body + para_idx = 0 + + for child in body: + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + + if tag == "p": + para = doc.paragraphs[para_idx] + para_idx += 1 + text = para.text.strip() + + if not text: + lines.append("") + continue + + level = get_paragraph_heading_level_pattern(para) + has_bold = any(r.bold for r in para.runs if r.bold) + + if level > 0: + lines.append("") + # 제목에서 < > 제거 + clean_text = re.sub(r"^<\s*|\s*>$", "", text).strip() + lines.append(f"{'#' * level} {clean_text}") + lines.append("") + elif has_bold: + # Bold 텍스트는 강조 처리 + parts = [] + for run in para.runs: + if run.bold: + parts.append(f"**{run.text}**") + else: + parts.append(run.text) + combined = "".join(parts) + + # (1), (2) 같은 번호 패턴 + if re.match(r"^\*\*\(\d+\)", combined): + lines.append(f"- {combined}") + # "예시 N:", "Phase N:" 같은 패턴 + elif re.match(r"^\*\*(예시|Phase|별첨)\s", combined): + lines.append("") + lines.append(f"#### {text}") + lines.append("") + else: + lines.append(f"- {combined}") + else: + # 일반 텍스트 + # 빈칸 양식 (___) 유지 + if "___" in text: + lines.append(text) + elif re.match(r"^(이메일|전화|주소|상호|대표|사업자|주민|연락처|날짜):", text): + lines.append(f"- {text}") + else: + lines.append(f" - {text}") + + elif tag == "tbl": + current_table_idx = sum( + 1 + for c in list(body)[: list(body).index(child)] + if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl" + ) + if current_table_idx < len(doc.tables): + lines.append("") + lines.append(table_to_markdown(doc.tables[current_table_idx])) + lines.append("") + + return "\n".join(lines) + + +def add_frontmatter(content, file_info, docx_name): + """YAML 프론트매터 추가""" + frontmatter = f"""--- +title: "{file_info['title']}" +version: "v4.0" +date: "{date.today().isoformat()}" +docx_file: "{docx_name}" +--- +""" + return frontmatter + "\n" + content + + +def extract_file(docx_name, file_info): + """단일 DOCX 파일 추출""" + docx_path = DOCX_DIR / docx_name + if not docx_path.exists(): + print(f" [SKIP] {docx_name} - 파일 없음") + return False + + doc = Document(str(docx_path)) + + if file_info["type"] == "styled": + content = extract_styled_doc(doc, file_info) + else: + content = extract_pattern_doc(doc, file_info) + + # 프론트매터 추가 + content = add_frontmatter(content, file_info, docx_name) + + # 연속 빈 줄 정리 (3줄 이상 → 2줄로) + content = re.sub(r"\n{3,}", "\n\n", content) + + # 파일 저장 + output_path = MD_DIR / file_info["output"] + output_path.write_text(content, encoding="utf-8") + print(f" [OK] {docx_name} → {file_info['output']}") + return True + + +def main(): + print("DOCX → Markdown 추출 시작") + print(f" DOCX 디렉토리: {DOCX_DIR}") + print(f" 출력 디렉토리: {MD_DIR}") + print() + + MD_DIR.mkdir(parents=True, exist_ok=True) + + success = 0 + for docx_name, file_info in FILE_MAP.items(): + if extract_file(docx_name, file_info): + success += 1 + + print(f"\n완료: {success}/{len(FILE_MAP)} 파일 변환됨") + return 0 if success == len(FILE_MAP) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contracts/scripts/sync_check.py b/contracts/scripts/sync_check.py new file mode 100644 index 0000000..09d55d9 --- /dev/null +++ b/contracts/scripts/sync_check.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +""" +DOCX ↔ Markdown 동기화 검증 스크립트 + +DOCX에서 텍스트를 추출하고 Markdown 파일의 텍스트와 비교하여 +불일치 항목을 리포트한다. +""" + +import difflib +import re +import sys +from pathlib import Path + +from docx import Document + +BASE_DIR = Path(__file__).resolve().parent.parent +DOCX_DIR = BASE_DIR / "docx" +MD_DIR = BASE_DIR / "markdown" + +# DOCX → Markdown 파일 매핑 +FILE_MAP = { + "01_고객_서비스이용계약서_v4_0_전자서명용.docx": "01-service-agreement.md", + "비밀유지서약서.docx": "02-nda.md", + "영업파트너 위촉계약서.docx": "03-partner-agreement.md", + "영업파트너 위촉계약서(단체용).docx": "04-partner-agreement-group.md", +} + + +def extract_text_from_docx(docx_path): + """DOCX에서 순수 텍스트만 추출 (개정이력 테이블 제외, 인터리빙 방식)""" + doc = Document(str(docx_path)) + lines = [] + + from docx.oxml.ns import qn as _qn + + body = doc.element.body + para_idx = 0 + table_idx = 0 + skip_revision = False + + for child in body: + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + + if tag == "p": + if para_idx < len(doc.paragraphs): + text = doc.paragraphs[para_idx].text.strip() + para_idx += 1 + + if "개정이력" in text: + skip_revision = True + continue + if text: + skip_revision = False + lines.append(text) + + elif tag == "tbl": + if table_idx < len(doc.tables): + table = doc.tables[table_idx] + table_idx += 1 + + # 개정이력 테이블 건너뛰기 + if len(table.rows) > 0: + first_row_text = [cell.text.strip() for cell in table.rows[0].cells] + if "버전" in first_row_text and "날짜" in first_row_text: + skip_revision = False + continue + + if skip_revision: + skip_revision = False + continue + + for row in table.rows: + cells = [cell.text.strip() for cell in row.cells] + # 빈 셀만 있는 행 건너뛰기 + if not any(cells): + continue + row_text = " | ".join(cells) + if row_text.strip(): + lines.append(row_text) + + return lines + + +def extract_text_from_markdown(md_path): + """Markdown에서 순수 텍스트만 추출 (프론트매터, 마크업 제거)""" + content = md_path.read_text(encoding="utf-8") + lines = [] + + in_frontmatter = False + in_table = False + + for line in content.split("\n"): + stripped = line.strip() + + # YAML 프론트매터 건너뛰기 + if stripped == "---": + in_frontmatter = not in_frontmatter + continue + if in_frontmatter: + continue + + # 빈 줄 건너뛰기 + if not stripped: + in_table = False + continue + + # Markdown 마크업 제거 + text = stripped + + # 헤딩 마크업 제거 + text = re.sub(r"^#{1,6}\s+", "", text) + + # 리스트 마크업 제거 + text = re.sub(r"^\s*[-*+]\s+", "", text) + + # Bold/Italic 마크업 제거 + text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) + text = re.sub(r"\*(.+?)\*", r"\1", text) + + # 블록인용 제거 + text = re.sub(r"^>\s*", "", text) + + # 테이블 구분선 건너뛰기 + if re.match(r"^\|[\s\-|]+\|$", text): + continue + + # 테이블 행 + if text.startswith("|") and text.endswith("|"): + # 파이프 제거하고 셀 텍스트 추출 + cells = [c.strip() for c in text.strip("|").split("|")] + text = " | ".join(cells) + + text = text.strip() + if text: + lines.append(text) + + return lines + + +def normalize_text(text): + """비교를 위한 텍스트 정규화""" + # 공백 정규화 + text = re.sub(r"\s+", " ", text).strip() + # 특수문자 정규화 + text = text.replace("\u00a0", " ") # non-breaking space + text = text.replace("\u3000", " ") # ideographic space + # 언더스코어 빈칸 정규화 + text = re.sub(r"_{3,}", "___", text) + # Bold 마크업(**) 제거 (DOCX 텍스트에 리터럴 ** 포함되는 경우) + text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) + # 선행 리스트 마커 제거 (DOCX 텍스트가 "- "로 시작하는 경우) + text = re.sub(r"^-\s+", "", text) + return text + + +def compare_documents(docx_name, md_name): + """두 문서의 텍스트를 비교""" + docx_path = DOCX_DIR / docx_name + md_path = MD_DIR / md_name + + if not docx_path.exists(): + return {"status": "error", "message": f"DOCX 파일 없음: {docx_name}"} + if not md_path.exists(): + return {"status": "error", "message": f"Markdown 파일 없음: {md_name}"} + + docx_lines = [normalize_text(l) for l in extract_text_from_docx(docx_path) if l.strip()] + md_lines = [normalize_text(l) for l in extract_text_from_markdown(md_path) if l.strip()] + + # difflib로 비교 + matcher = difflib.SequenceMatcher(None, docx_lines, md_lines) + ratio = matcher.ratio() + + # 차이점 추출 + diffs = [] + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + continue + elif tag == "replace": + for idx in range(max(i2 - i1, j2 - j1)): + docx_text = docx_lines[i1 + idx] if i1 + idx < i2 else "(없음)" + md_text = md_lines[j1 + idx] if j1 + idx < j2 else "(없음)" + diffs.append({ + "type": "변경", + "docx": docx_text[:80], + "markdown": md_text[:80], + }) + elif tag == "delete": + for idx in range(i1, i2): + diffs.append({ + "type": "DOCX에만 존재", + "docx": docx_lines[idx][:80], + "markdown": "-", + }) + elif tag == "insert": + for idx in range(j1, j2): + diffs.append({ + "type": "Markdown에만 존재", + "docx": "-", + "markdown": md_lines[idx][:80], + }) + + return { + "status": "ok", + "similarity": round(ratio * 100, 1), + "docx_lines": len(docx_lines), + "md_lines": len(md_lines), + "diff_count": len(diffs), + "diffs": diffs[:20], # 상위 20개만 + } + + +def main(): + print("=" * 70) + print("DOCX ↔ Markdown 동기화 검증") + print("=" * 70) + + all_ok = True + + for docx_name, md_name in FILE_MAP.items(): + print(f"\n{'─' * 50}") + print(f"문서: {docx_name}") + print(f" ↔ {md_name}") + print(f"{'─' * 50}") + + result = compare_documents(docx_name, md_name) + + if result["status"] == "error": + print(f" [ERROR] {result['message']}") + all_ok = False + continue + + similarity = result["similarity"] + status_icon = "OK" if similarity >= 80 else "WARN" if similarity >= 60 else "FAIL" + + print(f" 유사도: {similarity}% [{status_icon}]") + print(f" DOCX 라인: {result['docx_lines']}") + print(f" Markdown 라인: {result['md_lines']}") + print(f" 차이점: {result['diff_count']}개") + + if result["diffs"]: + print(f"\n 주요 차이점 (상위 {min(len(result['diffs']), 10)}개):") + for i, diff in enumerate(result["diffs"][:10]): + print(f" [{diff['type']}]") + if diff["docx"] != "-": + print(f" DOCX: {diff['docx']}") + if diff["markdown"] != "-": + print(f" MD: {diff['markdown']}") + + if similarity < 80: + all_ok = False + + print(f"\n{'=' * 70}") + if all_ok: + print("결과: 모든 문서 동기화 상태 양호") + else: + print("결과: 일부 문서에서 불일치 발견 - 확인 필요") + print(f"{'=' * 70}") + + return 0 if all_ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/data/interview-master-questions.sql b/data/interview-master-questions.sql new file mode 100644 index 0000000..58b4899 --- /dev/null +++ b/data/interview-master-questions.sql @@ -0,0 +1,279 @@ +-- ============================================================ +-- 인터뷰 질문 마스터 데이터 SQL +-- 8개 도메인 × 16개 템플릿 × 80개 질문 +-- +-- 실행 방법: +-- 로컬: docker exec -i sam-mysql-1 mysql -u root -p samdb < docs/data/interview-master-questions.sql +-- 개발서버: mysql -u -p samdb < interview-master-questions.sql +-- phpMyAdmin: SQL 탭에서 전체 복사 후 실행 +-- +-- 주의: 한 번만 실행할 것. 중복 실행 시 데이터가 중복됨. +-- ============================================================ + +SET NAMES utf8mb4; +SET @tenant_id = 1; +SET @user_id = 1; +SET @now = NOW(); + +-- ============================================================ +-- 대분류: 제조업-방화셔터 (parent_id=null, 루트 카테고리) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, NULL, '제조업-방화셔터', '방화셔터 제조업 인터뷰', NULL, 1, 1, @user_id, @user_id, @now, @now); +SET @root_manufacturing = LAST_INSERT_ID(); + +-- ============================================================ +-- Domain 1: 제품 분류 체계 (product_classification) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '제품 분류 체계', '제품 카테고리, 모델 코드, 분류 기준 파악', 'product_classification', 3, 1, @user_id, @user_id, @now, @now); +SET @cat_1 = LAST_INSERT_ID(); + +-- 템플릿 1.1: 제품 카테고리 구조 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_1, '제품 카테고리 구조', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_1_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_1_1, '귀사의 주요 제품군을 모두 나열해주세요', 'text', NULL, '쉼표 구분으로 제품군 나열', NULL, NULL, 'product_classification', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '각 제품군의 하위 모델명과 코드 체계를 알려주세요', 'table_input', '{"columns":["모델코드","모델명","비고"]}', '코드-이름 매핑 테이블', NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '제품을 분류하는 기준은 무엇인가요? (소재, 용도, 크기 등)', 'multi_select', '{"choices":["소재별","용도별","크기별","설치방식별","인증여부별"]}', NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '인증(인정) 제품과 비인증 제품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'product_classification', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '인증 제품의 경우 구성이 고정되나요?', 'checkbox', NULL, NULL, NULL, '{"question_index":3,"value":"있음"}', 'product_classification', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '카테고리별 제품 수는 대략 몇 개인가요?', 'number', NULL, NULL, '개', NULL, 'product_classification', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '제품 코드 명명 규칙을 설명해주세요 (예: KSS01의 의미)', 'text', NULL, '코드 체계의 각 자릿수 의미', NULL, NULL, 'product_classification', 0, 7, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '기존 시스템(ERP/엑셀)에서 사용하는 제품 분류 방식을 캡처하여 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'product_classification', 0, 8, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 1.2: 설치 유형별 분류 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_1, '설치 유형별 분류', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_1_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_1_2, '설치 유형(벽면형, 측면형, 혼합형 등)에 따라 견적이 달라지나요?', 'select', '{"choices":["예, 크게 달라짐","약간 달라짐","달라지지 않음"]}', NULL, NULL, NULL, 'product_classification', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_2, '각 설치 유형별로 어떤 부품이 달라지나요?', 'table_input', '{"columns":["설치유형","추가부품","제외부품","비고"]}', NULL, NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_2, '설치 유형에 따른 추가 비용 항목이 있나요?', 'text', NULL, NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 2: BOM 구조 (bom_structure) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, 'BOM 구조', '완제품-부품 관계, 부품 카테고리, BOM 레벨', 'bom_structure', 4, 1, @user_id, @user_id, @now, @now); +SET @cat_2 = LAST_INSERT_ID(); + +-- 템플릿 2.1: 완제품-부품 관계 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_2, '완제품-부품 관계', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_2_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_2_1, '대표 제품 1개의 완제품→부품 구성을 트리로 그려주세요', 'bom_tree', NULL, '최상위 제품부터 하위 부품까지 트리 구조', NULL, NULL, 'bom_structure', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '모든 제품에 공통으로 들어가는 부품은 무엇인가요?', 'multi_select', '{"choices":["가이드레일","케이스","모터","제어기","브라켓","볼트/너트"]}', '직접 입력 가능', NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '제품별로 선택적(옵션)인 부품은 무엇인가요?', 'table_input', '{"columns":["제품명","옵션부품","적용조건"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, 'BOM이 현재 엑셀로 관리되고 있나요? 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '하위 부품의 단계(레벨)는 최대 몇 단계인가요?', 'number', NULL, NULL, '단계', NULL, 'bom_structure', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '부품 수량이 고정인 것과 계산이 필요한 것을 구분해주세요', 'table_input', '{"columns":["부품명","고정/계산","고정수량 또는 계산식"]}', NULL, NULL, NULL, 'bom_structure', 0, 6, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 2.2: 부품 카테고리 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_2, '부품 카테고리', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_2_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_2_2, '부품을 카테고리로 분류하면 어떻게 나눠지나요? (본체, 절곡품, 전동부, 부자재 등)', 'text', NULL, '부품 분류 체계', NULL, NULL, 'bom_structure', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_2, '각 카테고리에 속하는 부품 목록을 정리해주세요', 'table_input', '{"columns":["카테고리","부품명","규격"]}', NULL, NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_2, '외주 구매 부품과 자체 제작 부품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_2, '부자재(볼트, 너트, 패킹 등)는 별도 관리하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 3: 치수/변수 계산 (dimension_formula) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '치수/변수 계산', '오픈 사이즈→제작 사이즈 변환, 파생 변수 계산', 'dimension_formula', 5, 1, @user_id, @user_id, @now, @now); +SET @cat_3 = LAST_INSERT_ID(); + +-- 템플릿 3.1: 오픈 사이즈 → 제작 사이즈 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_3, '오픈 사이즈 → 제작 사이즈', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_3_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_3_1, '고객이 입력하는 기본 치수 항목은 무엇인가요? (폭, 높이, 깊이 등)', 'multi_select', '{"choices":["폭(W)","높이(H)","깊이(D)","두께(T)","지름(Ø)"]}', NULL, NULL, NULL, 'dimension_formula', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '오픈 사이즈에서 제작 사이즈로 변환할 때 더하는 마진값은?', 'formula_input', NULL, '예: W1 = W0 + 120, H1 = H0 + 50', 'mm', NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '제품 카테고리별로 마진값이 다른가요?', 'table_input', '{"columns":["제품카테고리","W 마진(mm)","H 마진(mm)","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '면적(㎡) 계산 공식을 알려주세요', 'formula_input', NULL, '예: area = W1 * H1 / 1000000', '㎡', NULL, 'dimension_formula', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '중량(kg) 계산 공식을 알려주세요', 'formula_input', NULL, '예: weight = area * 단위중량(kg/㎡)', 'kg', NULL, 'dimension_formula', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '기타 파생 변수가 있나요? (예: 분할 개수, 절곡 길이 등)', 'table_input', '{"columns":["변수명","계산식","단위","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '치수 계산에 사용하는 엑셀 수식을 캡처해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 7, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 3.2: 변수 의존 관계 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_3, '변수 의존 관계', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_3_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_3_2, '변수 간 의존 관계를 설명해주세요 (A는 B와 C로 계산)', 'text', NULL, '계산 순서와 변수 의존성', NULL, NULL, 'dimension_formula', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_2, '계산 순서가 중요한 변수가 있나요?', 'text', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_2, '단위는 mm, m, kg 중 어떤 것을 기본으로 사용하나요?', 'select', '{"choices":["mm","m","cm","혼용"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 4: 부품 구성 상세 (component_config) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '부품 구성 상세', '주요 부품별 규격, 선택 기준, 특수 구성', 'component_config', 6, 1, @user_id, @user_id, @now, @now); +SET @cat_4 = LAST_INSERT_ID(); + +-- 템플릿 4.1: 주요 부품별 상세 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_4, '주요 부품별 상세', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_4_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_4_1, '가이드레일의 표준 길이 규격은? (예: 1219, 2438, 3305mm)', 'table_input', '{"columns":["규격코드","길이(mm)","비고"]}', NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '가이드레일 길이 조합 규칙은? (어떤 길이를 몇 개 사용?)', 'text', NULL, '높이에 따른 가이드레일 조합 로직', NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '케이스(하우징) 크기별 규격과 부속품 차이를 설명해주세요', 'table_input', '{"columns":["케이스규격","적용조건","부속품"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '모터 용량 종류와 선택 기준은? (무게별? 면적별?)', 'table_input', '{"columns":["모터용량","적용범위(최소)","적용범위(최대)","단위"]}', '무게/면적 범위별 모터 매핑', NULL, NULL, 'component_config', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '모터 전압 옵션은? (380V, 220V 등)', 'multi_select', '{"choices":["380V","220V","110V","DC 24V"]}', NULL, NULL, NULL, 'component_config', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '제어기 종류와 선택 기준은? (노출형/매립형 등)', 'table_input', '{"columns":["제어기유형","적용조건","비고"]}', NULL, NULL, NULL, 'component_config', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '절곡품(판재 가공) 목록과 각각의 치수 결정 방식은?', 'table_input', '{"columns":["절곡품명","치수결정방식","재질","두께(mm)"]}', NULL, NULL, NULL, 'component_config', 0, 7, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '부자재(볼트, 너트, 패킹 등) 목록과 수량 결정 방식은?', 'table_input', '{"columns":["부자재명","규격","수량결정방식","기본수량"]}', NULL, NULL, NULL, 'component_config', 0, 8, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 4.2: 특수 구성 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_4, '특수 구성', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_4_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_4_2, '연기차단재 등 특수 부품이 있나요? 적용 조건은?', 'text', NULL, NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_2, '보강재(샤프트, 파이프, 앵글 등) 사용 조건은?', 'table_input', '{"columns":["보강재명","규격","적용조건","수량"]}', NULL, NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_2, '고객 요청에 따라 추가/제외되는 옵션 부품은?', 'table_input', '{"columns":["옵션부품","추가/제외","추가비용","비고"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 5: 단가 체계 (pricing_structure) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '단가 체계', '단가 관리 방식, 계산 방식, 마진/LOSS율', 'pricing_structure', 7, 1, @user_id, @user_id, @now, @now); +SET @cat_5 = LAST_INSERT_ID(); + +-- 템플릿 5.1: 단가 관리 방식 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_5, '단가 관리 방식', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_5_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_5_1, '부품별 단가를 어디서 관리하나요? (엑셀, ERP, 구두 등)', 'select', '{"choices":["엑셀","ERP 시스템","구두/경험","기타"]}', NULL, NULL, NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '단가표 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '단가 변경 주기는? (월/분기/연 등)', 'select', '{"choices":["수시","월 단위","분기 단위","반기 단위","연 단위"]}', NULL, NULL, NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '단가에 포함되는 항목은? (재료비만? 가공비 포함?)', 'multi_select', '{"choices":["재료비","가공비","운송비","설치비","마진"]}', NULL, NULL, NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '고객별/거래처별 차등 단가가 있나요?', 'select', '{"choices":["있음 (등급별)","있음 (거래처별)","없음 (일괄 동일)"]}', NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, 'LOSS율(손실률)을 적용하나요? 적용 방식은?', 'formula_input', NULL, '예: 실제수량 = 계산수량 × (1 + LOSS율)', '%', NULL, 'pricing_structure', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '마진율 설정 방식은? (일괄? 품목별?)', 'select', '{"choices":["일괄 마진율","품목별 마진율","카테고리별 마진율","고객별 마진율"]}', NULL, NULL, NULL, 'pricing_structure', 0, 7, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 5.2: 단가 계산 방식 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_5, '단가 계산 방식', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_5_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_5_2, '면적 기반 단가 품목은? (원/㎡)', 'table_input', '{"columns":["품목명","단가(원/㎡)","비고"]}', NULL, '원/㎡', NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '중량 기반 단가 품목은? (원/kg)', 'table_input', '{"columns":["품목명","단가(원/kg)","비고"]}', NULL, '원/kg', NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '수량 기반 단가 품목은? (원/EA)', 'table_input', '{"columns":["품목명","단가(원/EA)","비고"]}', NULL, '원/EA', NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '길이 기반 단가 품목은? (원/m)', 'table_input', '{"columns":["품목명","단가(원/m)","비고"]}', NULL, '원/m', NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '기타 특수 단가 계산 방식이 있나요?', 'text', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 6: 수량 수식 (quantity_formula) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '수량 수식', '부품별 수량 결정 규칙, 계산식, 검증', 'quantity_formula', 8, 1, @user_id, @user_id, @now, @now); +SET @cat_6 = LAST_INSERT_ID(); + +-- 템플릿 6.1: 수량 결정 규칙 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_6, '수량 결정 규칙', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_6_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_6_1, '고정 수량 부품 목록 (항상 1개, 2개 등)', 'table_input', '{"columns":["부품명","고정수량","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '치수 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 슬랫수량 = CEIL(H1 / 슬랫피치)', NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '면적 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 스크린수량 = area / 기준면적', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '중량 기반 수량 계산 부품과 수식', 'formula_input', NULL, NULL, NULL, NULL, 'quantity_formula', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '올림/내림/반올림 규칙이 있는 계산은?', 'table_input', '{"columns":["계산항목","올림/내림/반올림","소수점자릿수"]}', NULL, NULL, NULL, 'quantity_formula', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '여유 수량(LOSS) 적용 품목과 비율은?', 'table_input', '{"columns":["품목명","LOSS율(%)","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 6, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 6.2: 수식 검증 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_6, '수식 검증', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_6_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_6_2, '실제 견적서에서 수량 계산 예시를 보여주세요 (W=3000, H=2500일 때)', 'table_input', '{"columns":["부품명","수식","계산결과","단위"]}', NULL, NULL, NULL, 'quantity_formula', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_2, '수식에 사용하는 함수가 있나요? (SUM, CEIL, ROUND 등)', 'multi_select', '{"choices":["CEIL (올림)","FLOOR (내림)","ROUND (반올림)","MAX","MIN","IF 조건문","SUM"]}', NULL, NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_2, '조건에 따라 수식이 달라지는 경우가 있나요?', 'text', NULL, '예: 폭이 3000 초과이면 분할 계산', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 7: 조건부 로직 (conditional_logic) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '조건부 로직', '범위/매핑 기반 부품 자동 선택 규칙', 'conditional_logic', 9, 1, @user_id, @user_id, @now, @now); +SET @cat_7 = LAST_INSERT_ID(); + +-- 템플릿 7.1: 범위 기반 선택 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_7, '범위 기반 선택', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_7_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_7_1, '무게 범위별 모터 용량 선택표를 작성해주세요', 'price_table', '{"columns":["범위 시작(kg)","범위 끝(kg)","모터용량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_1, '크기 범위별 부품 자동 선택 규칙이 있나요?', 'table_input', '{"columns":["조건(변수)","범위","선택부품","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_1, '브라켓 크기 결정 기준은?', 'table_input', '{"columns":["조건","범위","브라켓 규격"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 7.2: 매핑 기반 선택 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_7, '매핑 기반 선택', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_7_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_7_2, '제품 모델 → 기본 부품 세트 매핑표', 'table_input', '{"columns":["제품모델","기본부품1","기본부품2","기본부품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_2, '설치 유형 → 추가 부품 매핑표', 'table_input', '{"columns":["설치유형","추가부품","수량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_2, '제어기 유형 → 부속품 매핑표', 'table_input', '{"columns":["제어기유형","부속품1","부속품2","부속품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_2, '기타 조건부 자동 선택 규칙', 'text', NULL, '위 항목에 해당하지 않는 조건-결과 매핑', NULL, NULL, 'conditional_logic', 0, 4, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 8: 견적서 양식 (quote_format) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '견적서 양식', '출력 양식, 항목 그룹, 소계/합계 구조', 'quote_format', 10, 1, @user_id, @user_id, @now, @now); +SET @cat_8 = LAST_INSERT_ID(); + +-- 템플릿 8.1: 출력 양식 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_8, '출력 양식', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_8_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_8_1, '현재 사용 중인 견적서 양식을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'quote_format', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '견적서에 표시되는 항목 그룹은? (재료비, 노무비, 설치비 등)', 'multi_select', '{"choices":["재료비","노무비","경비","설치비","운반비","이윤","부가세"]}', NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '소계/합계 계산 구조를 설명해주세요', 'text', NULL, '항목 그룹별 소계와 최종 합계의 관계', NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '할인 적용 방식은? (일괄? 항목별?)', 'select', '{"choices":["일괄 할인","항목별 할인","할인 없음","협의 할인"]}', NULL, NULL, NULL, 'quote_format', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '부가세 표시 방식은? (별도? 포함?)', 'select', '{"choices":["별도 표시","포함 표시","선택 가능"]}', NULL, NULL, NULL, 'quote_format', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '견적서에 표시하지 않는 내부 관리 항목은?', 'text', NULL, NULL, NULL, NULL, 'quote_format', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '견적 번호 체계를 알려주세요', 'text', NULL, '예: Q-2026-001 형식', NULL, NULL, 'quote_format', 0, 7, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 8.2: 특수 요구사항 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_8, '특수 요구사항', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_8_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_8_2, '산출내역서(세부 내역)를 별도로 제공하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_2, '위치별(층/부호) 개별 산출이 필요한가요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_2, '일괄 산출(여러 위치 합산)을 사용하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- 완료 확인 +-- ============================================================ +SELECT + (SELECT COUNT(*) FROM interview_categories WHERE interview_project_id IS NULL AND domain IS NOT NULL) AS master_categories, + (SELECT COUNT(*) FROM interview_templates t JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_templates, + (SELECT COUNT(*) FROM interview_questions q JOIN interview_templates t ON q.interview_template_id = t.id JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_questions; diff --git a/dev/dev_plans/qms-api-integration-plan.md b/dev/dev_plans/qms-api-integration-plan.md new file mode 100644 index 0000000..c760f0d --- /dev/null +++ b/dev/dev_plans/qms-api-integration-plan.md @@ -0,0 +1,316 @@ +# 품질인정심사(QMS) API 연동 계획 + +> **작성일**: 2026-03-09 +> **상태**: 계획 수립 +> **URL**: `/quality/qms` +> **스토리보드**: 슬라이드 19~20 +> **관련 문서**: `docs/features/quality-management/quality-certification-audit.md` + +--- + +## 1. 현황 분석 + +### 1.1 프론트엔드 현황 + +| 항목 | 상태 | 비고 | +|------|------|------| +| `page.tsx` | ✅ 구현됨 | 14KB, 전체 페이지 레이아웃 | +| `types.ts` | ✅ 구현됨 | 95줄, 타입 정의 완료 | +| `mockData.ts` | ✅ 구현됨 | 543줄, 완전한 목업 데이터 | +| `components/` | ✅ 구현됨 | 12개 컴포넌트 + documents/ 7개 | +| `actions.ts` | ❌ 없음 | API 연동 0% | + +프론트엔드는 UI가 완성되어 있으나 **100% 목업 데이터**로 동작 중. + +### 1.2 백엔드 현황 + +| 영역 | 기존 API | 신규 필요 | +|------|----------|-----------| +| **1일차 (기준/매뉴얼 심사)** | ❌ 없음 | 모델, 마이그레이션, 서비스, 컨트롤러 전체 | +| **2일차 (로트 추적 심사)** | ⚠️ 부분 존재 | 기존 API 조합 + 서류 연결 API 신규 | + +**기존 활용 가능 API:** +- `GET /quality/documents` — 품질관리서 목록 (2일차 1단계) +- `GET /quality/documents/{id}` — 품질관리서 상세 + 수주/개소 (2일차 2단계) +- `GET /quality/performance-reports` — 실적신고 (분기 필터 활용) +- `GET /inspections` — 수입검사/중간검사 성적서 +- 출하/출고/납품 관련 기존 API + +--- + +## 2. 작업 범위 + +### Phase 1: 2일차 (로트 추적 심사) API 연동 + +> **우선순위 높음** — 기존 API 활용 가능하여 빠르게 연동 가능 + +#### 2.1 Frontend — `actions.ts` 생성 + +``` +react/src/app/[locale]/(protected)/quality/qms/actions.ts +``` + +| 액션 | 호출 API | 설명 | +|------|----------|------| +| `getQualityReports()` | `GET /quality/documents` | 품질관리서 목록 (분기 필터) | +| `getReportRoutes(reportId)` | `GET /quality/documents/{id}` | 수주코드 + 개소 목록 | +| `getRouteDocuments(routeId)` | 복합 조회 (아래 참조) | 개소별 관련 서류 8종 | +| `confirmUnitInspection(unitId)` | `PATCH /qms/lot-audit/confirm` | 개소 확인 완료 처리 | + +#### 2.2 관련 서류 조회 로직 + +2일차 3단계 "관련 서류"는 개소(Location)에 연결된 8종 서류를 조합 조회: + +| 서류 타입 | 데이터 소스 | 조회 방식 | +|-----------|-------------|-----------| +| 수입검사 성적서 | `inspections` (type=IQC) | 수주의 BOM 원자재 LOT 추적 | +| 수주서 | `orders` | 수주코드로 직접 조회 | +| 작업일지 | `work_orders` + 작업일지 | 수주 → 작업지시 → 작업일지 | +| 중간검사 성적서 | `inspections` (type=PQC) | 작업지시별 중간검사 | +| 납품확인서 | `shipments` | 출하 → 납품확인서 | +| 출고증 | `shipments` | 출하 → 출고증 | +| 제품검사 성적서 | `quality_document_locations` | 개소별 검사 문서 (EAV) | +| 품질관리서 | `quality_documents` | 품질관리서 원본 | + +#### 2.3 Backend — 신규 API (최소) + +``` +GET /api/v1/qms/lot-audit/reports — 분기별 품질관리서 목록 (전용 뷰) +GET /api/v1/qms/lot-audit/reports/{id} — 수주코드 + 개소 + 완료 상태 +GET /api/v1/qms/lot-audit/routes/{id}/documents — 개소별 8종 서류 조합 조회 +PATCH /api/v1/qms/lot-audit/units/{id}/confirm — 확인 완료 처리 +``` + +> 기존 `quality/documents` API를 래핑하여 QMS 전용 응답 형태로 가공하는 방식 권장. +> 8종 서류 조합 로직이 복잡하므로 **전용 서비스 메서드** 필요. + +#### 2.4 DB 변경 + +| 변경 | 테이블 | 설명 | +|------|--------|------| +| 컬럼 추가 | `quality_document_locations` | `options` JSON에 `lot_audit_confirmed`, `lot_audit_confirmed_at` 추가 | + +> 별도 테이블 없이 기존 개소(Location) 테이블의 `options` 활용 (컬럼 추가 정책 준수) + +--- + +### Phase 2: 1일차 (기준/매뉴얼 심사) 백엔드 구축 + +> **작업량 많음** — 완전 신규 백엔드 구축 필요 + +#### 2.1 DB 설계 (신규 테이블) + +``` +audit_checklists (심사 점검표 마스터) +├── id, tenant_id +├── year, quarter +├── type: 'standard_manual' (1일차) +├── status: draft/in_progress/completed +├── options: JSON +├── created_by, timestamps, soft_delete + +audit_checklist_categories (점검표 카테고리) +├── id, tenant_id +├── checklist_id (FK → audit_checklists) +├── title: '원재료 품질관리 기준' +├── sort_order +├── options: JSON + +audit_checklist_items (점검표 세부 항목) +├── id, tenant_id +├── category_id (FK → audit_checklist_categories) +├── name: '수입검사 기준 확인' +├── description +├── is_completed: boolean +├── completed_at, completed_by +├── sort_order +├── options: JSON + +audit_standard_documents (기준 문서) +├── id, tenant_id +├── checklist_item_id (FK → audit_checklist_items) +├── title, version, date +├── document_id (FK → documents, EAV) +├── options: JSON +``` + +#### 2.2 Backend 구현 + +| 파일 | 역할 | +|------|------| +| `api/app/Models/Qualitys/AuditChecklist.php` | 심사 점검표 모델 | +| `api/app/Models/Qualitys/AuditChecklistCategory.php` | 카테고리 모델 | +| `api/app/Models/Qualitys/AuditChecklistItem.php` | 세부 항목 모델 | +| `api/app/Models/Qualitys/AuditStandardDocument.php` | 기준 문서 모델 | +| `api/app/Services/AuditChecklistService.php` | 서비스 | +| `api/app/Http/Controllers/Api/V1/AuditChecklistController.php` | 컨트롤러 | +| `api/database/migrations/XXXX_create_audit_checklists_table.php` | 마이그레이션 (4테이블) | + +#### 2.3 API 엔드포인트 + +``` +GET /api/v1/qms/checklists — 점검표 목록 (연도/분기 필터) +POST /api/v1/qms/checklists — 점검표 생성 +GET /api/v1/qms/checklists/{id} — 점검표 상세 (카테고리+항목+문서) +PUT /api/v1/qms/checklists/{id} — 점검표 수정 +PATCH /api/v1/qms/checklists/{id}/complete — 점검표 완료 처리 + +PATCH /api/v1/qms/checklist-items/{id}/toggle — 항목 완료/미완료 토글 +GET /api/v1/qms/checklist-items/{id}/documents — 항목별 기준 문서 조회 +POST /api/v1/qms/checklist-items/{id}/documents — 기준 문서 연결 +DELETE /api/v1/qms/checklist-items/{id}/documents/{docId} — 기준 문서 연결 해제 +``` + +#### 2.4 Frontend — actions.ts 확장 + +| 액션 | 설명 | +|------|------| +| `getChecklists(year, quarter)` | 점검표 목록 | +| `getChecklistDetail(id)` | 점검표 상세 (카테고리+항목+문서) | +| `toggleChecklistItem(itemId)` | 항목 완료/미완료 토글 | +| `getCheckItemDocuments(itemId)` | 기준 문서 조회 | +| `confirmCheckItem(itemId)` | 기준/매뉴얼 확인 완료 | + +--- + +### Phase 3: 프론트엔드 목업 → API 전환 + +#### 3.1 page.tsx 수정 + +- `mockData.ts` import 제거 +- `actions.ts` import로 교체 +- `useEffect`에서 API 호출 +- 로딩/에러 상태 추가 + +#### 3.2 컴포넌트 수정 + +| 컴포넌트 | 변경 내용 | +|----------|-----------| +| `ReportList.tsx` | API 데이터 바인딩 | +| `RouteList.tsx` | API 데이터 바인딩 | +| `DocumentList.tsx` | 8종 서류 실제 조회 | +| `InspectionModal.tsx` | 실제 검사 문서 렌더링 | +| `Day1ChecklistPanel.tsx` | API 데이터 바인딩 | +| `Day1DocumentSection.tsx` | 기준 문서 API 조회 | +| `Day1DocumentViewer.tsx` | 실제 파일 미리보기 | +| `AuditProgressBar.tsx` | 실시간 진행률 계산 | +| `Filters.tsx` | 연도/분기 필터 API 연동 | + +#### 3.3 mockData.ts 처리 + +- Phase 3 완료 후 `mockData.ts` 삭제 +- 또는 `USE_MOCK` 플래그 패턴 적용 (개발 편의) + +--- + +## 3. 데이터 매핑 + +### 3.1 InspectionReport ↔ QualityDocument + +| 프론트 (InspectionReport) | 백엔드 (QualityDocument) | +|---------------------------|-------------------------| +| `id` | `quality_documents.id` | +| `code` | `quality_documents.code` (채번) | +| `siteName` | `quality_documents.site_name` | +| `item` | `quality_documents.options.product_type` 또는 인정특성 | +| `routeCount` | `quality_document_orders` COUNT | +| `totalRoutes` | `quality_document_locations` COUNT | +| `quarter` | `performance_reports.year` + `quarter` | +| `year` | `performance_reports.year` | +| `quarterNum` | `performance_reports.quarter` | + +### 3.2 RouteItem ↔ QualityDocumentOrder + +| 프론트 (RouteItem) | 백엔드 (QualityDocumentOrder) | +|--------------------|-------------------------------| +| `id` | `quality_document_orders.id` | +| `code` | `orders.order_code` | +| `date` | `orders.order_date` | +| `site` | `orders.site_name` | +| `locationCount` | `quality_document_locations` COUNT | +| `subItems` | `quality_document_locations` 변환 | + +### 3.3 ChecklistCategory ↔ AuditChecklistCategory + +| 프론트 (ChecklistCategory) | 백엔드 (AuditChecklistCategory) | +|---------------------------|--------------------------------| +| `id` | `audit_checklist_categories.id` | +| `title` | `audit_checklist_categories.title` | +| `subItems` | `audit_checklist_items` 관계 | + +--- + +## 4. 일정 산정 + +| Phase | 작업 내용 | 예상 소요 | +|-------|----------|-----------| +| **Phase 1** | 2일차 API 연동 (기존 API 활용) | | +| ├ 1-1 | Backend: 전용 서비스 + 컨트롤러 + 라우트 | 1일 | +| ├ 1-2 | Backend: 8종 서류 조합 조회 로직 | 1일 | +| ├ 1-3 | Frontend: actions.ts 생성 + 목업 교체 | 1일 | +| └ 1-4 | 테스트 및 디버깅 | 0.5일 | +| **Phase 2** | 1일차 백엔드 구축 (완전 신규) | | +| ├ 2-1 | DB 설계 + 마이그레이션 (4테이블) | 0.5일 | +| ├ 2-2 | 모델 4개 + 관계 설정 | 0.5일 | +| ├ 2-3 | 서비스 + 컨트롤러 + 라우트 | 1일 | +| └ 2-4 | 초기 데이터 시딩 (점검표 마스터) | 0.5일 | +| **Phase 3** | 프론트엔드 전환 | | +| ├ 3-1 | 2일차 컴포넌트 API 바인딩 | 1일 | +| ├ 3-2 | 1일차 컴포넌트 API 바인딩 | 1일 | +| └ 3-3 | 통합 테스트 + mockData 정리 | 0.5일 | + +**총 예상: ~8일** + +--- + +## 5. 의존성 및 리스크 + +### 5.1 의존성 + +| 항목 | 의존 대상 | 상태 | +|------|-----------|------| +| 품질관리서 데이터 | `quality_documents` 실 데이터 | ✅ 운영 중 | +| 실적신고 데이터 | `performance_reports` 실 데이터 | ✅ 운영 중 | +| 수입검사 성적서 | `inspections` (IQC) | ✅ 운영 중 | +| 중간검사 성적서 | `inspections` (PQC) | ⚠️ 구현 중 | +| 작업일지 | `work_orders` 연결 | ✅ 운영 중 | +| 출하/납품 | `shipments` | ✅ 운영 중 | +| 기준 문서 파일 | EAV Document 시스템 | ✅ 운영 중 | + +### 5.2 리스크 + +| 리스크 | 영향 | 완화 방안 | +|--------|------|-----------| +| 8종 서류 추적 로직 복잡 | Phase 1 지연 | 서류별 독립 조회 후 프론트에서 조합 | +| 1일차 점검표 초기 데이터 부재 | Phase 2 테스트 어려움 | 시더로 기본 점검표 생성 | +| 중간검사 미완성 | 2일차 일부 서류 누락 | 빈 상태로 표시, 추후 연동 | + +--- + +## 6. 권장 진행 순서 + +``` +Phase 1 (2일차 API 연동) — 3.5일 + ↓ +Phase 2 (1일차 백엔드 구축) — 2.5일 + ↓ +Phase 3 (프론트엔드 전환) — 2.5일 +``` + +**Phase 1을 먼저 하는 이유:** +- 기존 API 활용으로 빠르게 실 데이터 확인 가능 +- 로트 추적은 실적신고와 직접 연결되어 비즈니스 우선순위 높음 +- Phase 2(1일차)는 독립적인 신규 개발이므로 나중에 진행 가능 + +--- + +## 관련 문서 + +- [품질인정심사 기능 문서](../../features/quality-management/quality-certification-audit.md) +- [제품검사 관리](../../features/quality-management/inspection-management.md) +- [생산실적신고](../../features/quality-management/performance-reports.md) +- [통합 개선 마스터 플랜](./integrated-master-plan.md) + +--- + +**최종 업데이트**: 2026-03-09 diff --git a/features/academy/fire-shutter-image-prompts.md b/features/academy/fire-shutter-image-prompts.md new file mode 100644 index 0000000..1614eda --- /dev/null +++ b/features/academy/fire-shutter-image-prompts.md @@ -0,0 +1,369 @@ +# 방화셔터 백과사전 이미지 생성 프롬프트 + +> **작성일**: 2026-02-22 +> **상태**: 확정 +> **용도**: Google Gemini (Nano Banana Pro) 이미지 생성용 + +--- + +## 1. 개요 + +### 1.1 목적 + +MNG 아카데미 > 방화셔터 백과사전 페이지에 삽입할 기술 일러스트레이션을 AI 이미지 생성 도구(Google Gemini)로 제작하기 위한 프롬프트 모음이다. + +### 1.2 사용 방법 + +1. Google Gemini (Nano Banana Pro 모델)에서 프롬프트를 입력한다 +2. 생성된 이미지를 `mng/public/images/academy/fire-shutter/` 경로에 저장한다 +3. Blade 뷰에서 `` 태그로 참조한다 + +### 1.3 주의사항 + +- **화면 내 모든 라벨은 영어**로 작성되어 있다 (한글 텍스트는 AI 이미지 생성 시 깨짐 현상 발생) +- 전체 구성도, 설치 장면 등 넓은 이미지는 **16:9** 비율 권장 +- 단면도, 부품 상세 등은 **1:1** 또는 **4:3** 비율 권장 +- 생성 실패 시 프롬프트 앞에 `Detailed technical engineering illustration, clean white background, ` 를 추가한다 + +--- + +## 2. 프롬프트 목록 + +### 2.1 방화셔터 전체 구성도 (Full Component Diagram) + +``` +Technical illustration of a fire shutter (automatic fire-rated rolling shutter) installed in a building opening, cutaway side view showing all components with English labels. + +Show these parts clearly labeled: +- Top: "CEILING SLAB" with "HEAD BOX / CASE" mounted below +- Inside head box: "SHAFT" with coiled steel slats, "BALANCE SPRING", "GEAR BOX", "MOTOR", "ELECTROMAGNETIC BRAKE", "BRACKET" on both sides +- Both sides: vertical "GUIDE RAIL" mounted on fireproof walls with "ANCHOR BOLTS" +- Center: multiple horizontal "STEEL SLATS" hanging down in interlocking pattern +- Bottom: "BOTTOM BAR" touching the floor with rubber seal +- Nearby wall: "MANUAL CONTROL BOX" with UP/STOP/DOWN buttons +- Ceiling: "SMOKE DETECTOR" and "HEAT DETECTOR" +- Wall-mounted: "FIRE SHUTTER CONTROLLER" + +Style: Clean technical cutaway diagram, white background, professional engineering illustration, labeled with arrows pointing to each component. Color-coded: structural parts in gray/silver, electrical parts in blue, safety parts in red. Isometric or 3/4 perspective view. +``` + +--- + +### 2.2 슬랫 인터록킹 구조 (Slat Interlocking) + +``` +Technical cross-section illustration showing how fire shutter steel slats interlock with each other. + +Show 3-4 slats connected in interlocking pattern: +- Each slat is a C-shaped or S-shaped profile made from 1.6mm EGI steel +- The curved edges of adjacent slats hook into each other, allowing flexibility while maintaining a continuous curtain surface +- One slat highlighted with dimension labels: "THICKNESS 1.6mm", "PITCH 75-100mm" +- Show the slight curved profile that allows the slat to wrap around the shaft when rolled up +- Arrow labeled "ROLLING DIRECTION" + +Label each part: "SLAT", "INTERLOCKING JOINT", "EGI STEEL 1.6mm" + +Style: Clean engineering cross-section diagram, white background, metallic silver color for steel. Include dimension lines. Zoomed-in detail view with magnified interlocking joint area in a callout circle. +``` + +--- + +### 2.3 가이드레일 단면도 (Guide Rail Cross-Section) + +``` +Technical cross-section illustration of a fire shutter guide rail mounted on a fireproof wall, viewed from top-down. + +Show the C-channel shaped guide rail: +- C-channel profile, steel thickness 2.3mm+ +- Inside the channel: slat edge sitting in the groove +- Smoke seal material strips on both sides of the channel, pressing against the slat +- Anchor bolts securing the guide rail to the concrete wall +- Wall shown as hatched concrete pattern + +Labels with arrows: +- "GUIDE RAIL BODY (C-CHANNEL)" +- "SLAT EDGE" +- "SMOKE SEAL PACKING" +- "ANCHOR BOLT" +- "FIREPROOF WALL" +- "STEEL 2.3mm+" + +Style: Clean technical cross-section, white background, steel parts in metallic gray, seal material in orange/red, wall in light brown hatched pattern. Include dimension annotations. +``` + +--- + +### 2.4 샤프트 어셈블리 (Shaft Assembly) + +``` +Technical illustration showing the inside of a fire shutter head box, exploded or cutaway view. + +Show these components assembled on or around the shaft: +- Central pipe labeled "SHAFT" with slats attached, partially wound +- Left side: "BRACKET" steel plate bolted to wall, with "BEARING" supporting shaft end +- Right side: "GEAR BOX" and "MOTOR" mounted on bracket +- "ELECTROMAGNETIC BRAKE" attached to motor assembly +- "BALANCE SPRING" torsion spring visible inside the shaft +- "AUTO CLOSER" device mounted near the brake +- "LIMIT SWITCH" small switches with actuator arms +- "HEAD BOX CASE" shown as transparent or partially removed to reveal internals +- Wiring connections going down labeled "TO CONTROLLER" + +Style: Exploded technical diagram or cutaway 3D illustration, white background, professional engineering style. Color-coded: mechanical parts in silver/gray, motor in dark blue, brake in red, spring in green. All labels in English with leader lines. +``` + +--- + +### 2.5 감속기+모터+브레이크 (Gear Box + Motor + Brake Assembly) + +``` +Technical illustration of a fire shutter drive unit assembly, showing three main components connected together. + +Show them assembled in sequence with labels: +1. "MOTOR (220V)" - cylindrical body with power cables +2. "ELECTROMAGNETIC BRAKE" - disc-type brake between motor and gearbox, showing brake disc, coil, and spring +3. "WORM GEAR BOX" - rectangular housing with cutaway revealing the worm gear and worm wheel inside + +Assembly order shown with arrows: MOTOR → BRAKE → GEAR BOX → "OUTPUT TO SHAFT" +Include rotation direction arrows + +Small inset callout showing worm gear mechanism detail labeled: "WORM", "WORM WHEEL", "SELF-LOCKING" + +Style: Technical exploded/assembly diagram, white background, metallic rendering, engineering illustration style. +``` + +--- + +### 2.6 연동제어기 시스템 (Controller System) + +``` +Technical schematic diagram showing the fire shutter interlock control system wiring and signal flow. + +Layout (block diagram style): +- Top center: "FIRE ALARM PANEL" - rectangular box +- Left: "SMOKE DETECTOR (PHOTOELECTRIC)" - circular device on ceiling +- Right: "HEAT DETECTOR (FIXED TEMP.)" - circular device on ceiling +- Center: "FIRE SHUTTER CONTROLLER" - panel with LED indicators labeled "POWER", "PARTIAL CLOSE", "FULL CLOSE" +- Below controller: "AUTO CLOSER" connected to shutter mechanism +- Bottom left: "MANUAL CONTROL BOX" with "UP / STOP / DOWN" buttons +- Bottom: "FIRE SHUTTER" shown schematically + +Signal flow arrows with labels: +- Smoke detector → Controller: "STAGE 1: PARTIAL CLOSE (1m gap)" +- Heat detector → Controller: "STAGE 2: FULL CLOSE (floor sealed)" +- Controller → Auto closer: "CLOSE COMMAND" +- Controller → Speaker icon: "ALARM OUTPUT" +- Controller ↔ Fire alarm panel: "STATUS SIGNAL" + +Style: Clean schematic/block diagram, white background, professional electrical diagram style. Color coding: red for fire signals, blue for power, green for status. All labels in English. +``` + +--- + +### 2.7 2단계 폐쇄 시퀀스 (2-Stage Closure Sequence) + +``` +Technical illustration showing the two-stage closing sequence of an automatic fire shutter, presented as 3 side-by-side panels: + +Panel 1 - Title: "NORMAL (OPEN)": +- Fire shutter fully open, rolled up inside head box +- People walking through the opening freely +- Smoke and heat detectors on ceiling shown in standby (green LED) +- Caption: "Shutter open, passage clear" + +Panel 2 - Title: "STAGE 1: PARTIAL CLOSE": +- Smoke detector activated (red LED, smoke wisps shown) +- Shutter descended leaving about 1 meter gap from floor +- A person crouching to pass under the gap +- Alarm buzzer icon showing sound waves +- Caption: "Smoke detected → Partial close, 1m gap for evacuation" + +Panel 3 - Title: "STAGE 2: FULL CLOSE": +- Heat detector activated (red LED, flames shown) +- Shutter fully closed to floor, bottom bar sealed against floor +- Fire and smoke on one side, clean air on other side +- Caption: "Heat detected → Full close, fire/smoke blocked" + +Arrow at bottom labeled "TIME SEQUENCE →" + +Style: Clean technical illustration with slight architectural rendering, sequential format left to right, white background. People as simple silhouettes. Fire/smoke rendered subtly. +``` + +--- + +### 2.8 롤포밍 공정 (Roll Forming Process) + +``` +Technical illustration showing the roll forming manufacturing process for fire shutter steel slats, production line viewed from the side. + +Show the line from left to right with labels: +1. "UNCOILER" - Steel coil (EGI 1.6mm) being unrolled +2. "LEVELER" - Flattening rollers correcting coil curvature +3. "ROLL FORMING STATION" - 6-8 pairs of forming rollers progressively shaping the flat strip into C/S-shaped slat profile +4. "CUTTING STATION" - Flying shear cutting the formed strip to length +5. "FINISHED SLATS" - Slats stacked neatly on output table + +Detail callout at top showing progressive cross-section shape changes: "FLAT → STAGE 1 → STAGE 2 → STAGE 3 → FINAL PROFILE" + +Arrow at bottom: "MATERIAL FLOW →" +Label on coil: "EGI STEEL COIL 1.6mm" + +Style: Technical factory/manufacturing illustration, clean white background, machinery in industrial gray/green, steel in silver. Side view. Directional arrows showing material flow. +``` + +--- + +### 2.9 현장 설치 (Field Installation) + +``` +Technical illustration showing fire shutter installation at a construction site, depicting key installation steps in a single scene. + +Scene showing a large building opening (about 5m wide, 4m tall) with: +- Two workers on scaffolding installing the head box assembly at the top +- Brackets already bolted to both side walls near the ceiling +- Guide rails mounted vertically on walls with anchor bolts +- Shaft with wound slat curtain being lifted up to place on brackets +- Manual control box being mounted on adjacent wall +- Wiring conduits visible running from controller to head box +- Construction tools: level tool, drill, anchor bolts, wrenches + +Labels with arrows pointing to activities: +- "BRACKET MOUNTING" +- "GUIDE RAIL ANCHORING" +- "SHAFT PLACEMENT" +- "ELECTRICAL WIRING" +- "LEVEL CHECK" +- "ANCHOR BOLT FIXING" + +Style: Technical construction illustration, slightly warm tone, realistic building interior with exposed concrete. Workers wearing safety helmets and vests. Clean architectural illustration style. All text in English. +``` + +--- + +### 2.10 유지보수 점검 (Maintenance Inspection) + +``` +Technical illustration showing fire shutter maintenance inspection scene. + +Show a maintenance technician inspecting a fire shutter: +- Technician with safety vest and hard hat, holding a tablet +- Fire shutter partially lowered (halfway) for testing +- Close-up callout bubbles showing key inspection points: + 1. "SLAT CONDITION" - checking for deformation, rust + 2. "SMOKE SEAL CHECK" - checking guide rail seal condition + 3. "BOTTOM BAR PACKING" - checking floor seal + 4. "MOTOR / BRAKE CHECK" - head box open, listening for sounds + 5. "MANUAL BOX TEST" - pressing UP/STOP/DOWN buttons + 6. "CONTROLLER STATUS" - checking LED indicators + +Checklist overlay in corner: +☑ MOTOR OPEN/CLOSE TEST +☑ DETECTOR INTERLOCK TEST +☑ ALARM SOUND CHECK +☑ MANUAL OPERATION CHECK +☑ BOTTOM BAR SEAL CHECK + +Style: Clean technical illustration, bright well-lit building interior, professional maintenance scene. Color callout bubbles with icons. All text in English. +``` + +--- + +### 2.11 강판형 vs 스크린형 (Steel Plate vs Screen Type) + +``` +Technical side-by-side comparison illustration of two types of fire shutters in similar building openings: + +Left side - Title "STEEL PLATE TYPE": +- Steel slat fire shutter in partially closed position +- Opaque metallic surface of interlocking steel slats visible +- Heavier, industrial appearance with thick guide rails +- Bottom bar with rubber seal +- Callout: "EGI STEEL 1.6mm / HEAVY / OPAQUE / HIGH SEALING" + +Right side - Title "SCREEN / FABRIC TYPE": +- Fabric fire shutter in partially closed position +- Semi-transparent woven silica fiber screen, you can faintly see light through it +- Lighter, sleeker with thin guide rails (11mm) +- Fabric gathered at top +- Callout: "SILICA FIBER / LIGHTWEIGHT / SEMI-TRANSPARENT / RAIL 11mm" + +Center dividing line with "VS" label +Bottom comparison bar: "WEIGHT: Heavy vs Light | VISIBILITY: Opaque vs See-through | RAIL WIDTH: Wide vs 11mm" + +Style: Clean technical comparison, white background, same scale, professional product comparison layout. All text in English. +``` + +--- + +### 2.12 주요 고장 유형 (Major Fault Types) + +``` +Technical illustration showing 6 common fire shutter failure types in a 2x3 grid layout, each in its own panel with a red problem highlight: + +Panel 1 - "SLAT DERAILMENT": +- A slat edge coming out of the guide rail groove, curtain jammed +- Red circle on problem area + +Panel 2 - "MOTOR BURNOUT": +- Motor with smoke marks, burnt wiring +- Overheat warning symbol + +Panel 3 - "BRAKE PAD WEAR": +- Electromagnetic brake with worn disc pad +- Side comparison: "NEW" thick pad vs "WORN" thin pad + +Panel 4 - "CONTROLLER MALFUNCTION": +- Controller panel with error LED, disconnected wires +- Broken signal path indicator + +Panel 5 - "CLOSER SPEED FAULT": +- Shutter dropping fast, speedometer showing "0.15 m/s LIMIT EXCEEDED" +- Governor mechanism detail + +Panel 6 - "SMOKE SEAL FAILURE": +- Smoke wisps leaking through guide rail gaps +- Comparison: "NEW SEAL" vs "DEGRADED SEAL" + +Style: Technical diagnostic illustration, white background, bordered panels. Problem areas in red/orange highlight. Clean maintenance manual style. All titles and labels in English. +``` + +--- + +## 3. 이미지 파일 관리 + +### 3.1 저장 경로 + +``` +mng/public/images/academy/fire-shutter/ +├── 01-full-component-diagram.png +├── 02-slat-interlocking.png +├── 03-guide-rail-cross-section.png +├── 04-shaft-assembly.png +├── 05-gearbox-motor-brake.png +├── 06-controller-system.png +├── 07-two-stage-closure.png +├── 08-roll-forming-process.png +├── 09-field-installation.png +├── 10-maintenance-inspection.png +├── 11-steel-vs-screen-type.png +└── 12-major-fault-types.png +``` + +### 3.2 Blade 참조 예시 + +```html +방화셔터 전체 구성도 +``` + +--- + +## 관련 문서 + +- `mng/resources/views/academy/fire-shutter.blade.php` - 방화셔터 백과사전 Blade 뷰 +- `mng/app/Http/Controllers/AcademyController.php` - 아카데미 컨트롤러 + +--- + +**최종 업데이트**: 2026-02-22 diff --git a/features/approvals/README.md b/features/approvals/README.md new file mode 100644 index 0000000..a43521c --- /dev/null +++ b/features/approvals/README.md @@ -0,0 +1,298 @@ +# 결재관리 시스템 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **프로젝트**: SAM MNG (관리자 웹) +> **우선순위**: 🔴 필수 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM MNG 전자결재 시스템. 기안부터 최종 승인, 반려, 회수, 보류, 전결, 참조까지 기업 결재 프로세스를 디지털화한다. + +### 1.2 문서 구조 + +| 문서 | 설명 | +|------|------| +| **README.md** (이 문서) | 시스템 전체 개요, 아키텍처, 상태 관리 | +| [form-types.md](form-types.md) | 양식별 필드/JSON 구조/인터랙션 기술 명세 | +| [workflows.md](workflows.md) | 상세 워크플로우 (승인/반려/회수/보류/전결/복사재기안) | +| [api-reference.md](api-reference.md) | API 엔드포인트 명세 | +| [ui-screens.md](ui-screens.md) | 화면별 UI 구성 및 동작 | +| [db-changes-and-model-sync.md](db-changes-and-model-sync.md) | DB 변경사항 및 API/MNG 모델 동기화 현황 | + +### 1.3 구현 현황 + +| Phase | 범위 | 상태 | +|-------|------|------| +| **Phase 1** | 순차결재, 기안/상신/승인/반려/회수 | ✅ 완료 | +| **Phase 2** | 보류/해제, 전결, 참조 열람 추적, 복사 재기안 | ✅ 완료 | +| **Phase 3** | 병렬결재, 위임(대결), 알림 | 미착수 | +| **Phase 4** | ERP 연동, 결재 통계, 관리자 설정 | 미착수 | + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + HTMX + Alpine.js | 동적 UI, 부분 렌더링 | +| API | Laravel Controller + Service | JSON API (내부용) | +| 모델 | Eloquent ORM | Multi-tenant 스코프 | +| DB | MySQL 8.0 | API 프로젝트에서 마이그레이션 관리 | + +### 2.2 프로젝트 분리 + +``` +API (/home/aweso/sam/api) +├── database/migrations/ ← 모든 결재 테이블 마이그레이션 + +MNG (/home/aweso/sam/mng) +├── app/Models/Approvals/ ← 모델 (Approval, ApprovalStep, ApprovalForm, ApprovalLine, ApprovalDelegation) +├── app/Services/ ← ApprovalService (비즈니스 로직) +├── app/Http/Controllers/ ← ApprovalController (웹), ApprovalApiController (API) +├── resources/views/approvals/ ← Blade 뷰 +└── routes/ ← 웹 라우트 + API 라우트 +``` + +### 2.3 핵심 클래스 + +``` +ApprovalService +├── 목록 조회: getMyDrafts(), getPendingForMe(), getCompletedByMe(), getReferencesForMe() +├── CRUD: createApproval(), updateApproval(), deleteApproval(), getApproval() +├── 워크플로우: submit(), approve(), reject(), cancel(), hold(), releaseHold(), preDecide(), copyForRedraft() +├── 참조: markAsRead() +└── 유틸: getBadgeCounts(), getApprovalLines(), getApprovalForms(), saveApprovalSteps() +``` + +--- + +## 3. 데이터베이스 + +### 3.1 테이블 관계 + +``` +approval_forms (결재 양식) + │ 1:N + ▼ +approvals (결재 문서) + │ 1:N │ N:1 (self) + ▼ ▼ +approval_steps (결재 단계) approvals (parent_doc_id → 원본 문서) + +approval_lines (결재선 템플릿) ← approvals.line_id 참조 + +approval_delegations (위임 설정) ← Phase 3 준비 +``` + +### 3.2 approvals (결재 문서) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT | 테넌트 격리 | +| `document_number` | VARCHAR | `APR-YYMMDD-001` 형식 | +| `form_id` | BIGINT FK | 양식 | +| `line_id` | BIGINT FK NULL | 결재선 템플릿 | +| `title` | VARCHAR(200) | 제목 | +| `content` | JSON | 양식 필드 데이터 | +| `body` | TEXT NULL | 본문 | +| `status` | VARCHAR(20) | 문서 상태 (6가지) | +| `is_urgent` | BOOLEAN | 긴급 여부 | +| `drafter_id` | BIGINT FK | 기안자 | +| `department_id` | BIGINT FK NULL | 기안 부서 | +| `current_step` | INT | 현재 결재 단계 번호 | +| `drafted_at` | TIMESTAMP NULL | 상신 일시 | +| `completed_at` | TIMESTAMP NULL | 완료 일시 | +| `recall_reason` | TEXT NULL | 회수 사유 | +| `parent_doc_id` | BIGINT FK NULL | 재기안 원본 문서 | +| `attachments` | JSON NULL | 첨부파일 | + +### 3.3 approval_steps (결재 단계) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `approval_id` | BIGINT FK | 결재 문서 | +| `step_order` | INT | 순서 (1, 2, 3...) | +| `step_type` | VARCHAR | `approval`, `agreement`, `reference` | +| `parallel_group` | INT NULL | 병렬 그룹 (Phase 3) | +| `approver_id` | BIGINT FK | 결재자 | +| `acted_by` | BIGINT FK NULL | 실제 처리자 (대결 시) | +| `approver_name` | VARCHAR | 결재자명 스냅샷 | +| `approver_department` | VARCHAR | 부서 스냅샷 | +| `approver_position` | VARCHAR | 직급 스냅샷 | +| `status` | VARCHAR(20) | 단계 상태 (5가지) | +| `approval_type` | VARCHAR(20) | `normal`, `pre_decided`, `delegated` | +| `comment` | TEXT NULL | 결재 의견 | +| `acted_at` | TIMESTAMP NULL | 처리 일시 | +| `is_read` | BOOLEAN | 참조 열람 여부 | +| `read_at` | TIMESTAMP NULL | 열람 일시 | + +### 3.4 approval_delegations (위임 설정, Phase 3) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | | +| `delegator_id` | BIGINT FK | 위임자 | +| `delegate_id` | BIGINT FK | 대리인 | +| `start_date` | DATE | 위임 시작일 | +| `end_date` | DATE | 위임 종료일 | +| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) | +| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 | +| `is_active` | BOOLEAN | 활성 여부 | +| `reason` | VARCHAR(200) | 위임 사유 | + +--- + +## 4. 상태 관리 + +### 4.1 문서 상태 (6가지) + +| 상태 | 코드 | 라벨 | 색상 | 설명 | +|------|------|------|------|------| +| 임시저장 | `draft` | 임시저장 | gray | 작성 중, 미상신 | +| 진행 | `pending` | 진행 | blue | 결재선 순환 중 | +| 완료 | `approved` | 완료 | green | 최종 승인 | +| 반려 | `rejected` | 반려 | red | 결재자가 반려 | +| 회수 | `cancelled` | 회수 | yellow | 기안자가 회수 | +| 보류 | `on_hold` | 보류 | amber | 결재자가 보류 | + +### 4.2 단계 상태 (5가지) + +| 상태 | 코드 | 라벨 | 아이콘 | 설명 | +|------|------|------|--------|------| +| 대기 | `pending` | 대기 | 숫자 | 차례 아직 아님 | +| 승인 | `approved` | 승인 | ✓ (녹색) | 승인 완료 | +| 반려 | `rejected` | 반려 | ✗ (적색) | 반려 | +| 건너뜀 | `skipped` | 건너뜀 | — (회색) | 전결/회수로 소멸 | +| 보류 | `on_hold` | 보류 | ⏸ (노란) | 보류 중 | + +### 4.3 결재 유형 (approval_type) + +| 유형 | 코드 | 아이콘 | 설명 | +|------|------|--------|------| +| 일반결재 | `normal` | ✓ | 기본 승인 | +| 전결 | `pre_decided` | ⚡ (남색) | 이후 단계 모두 건너뛰고 즉시 완료 | +| 대결 | `delegated` | — | 대리인이 처리 (Phase 3) | + +### 4.4 참여자 역할 (step_type) + +| 역할 | 코드 | 의사결정 | 설명 | +|------|------|---------|------| +| 결재 | `approval` | ✅ 있음 | 승인/반려/보류/전결 가능 | +| 합의 | `agreement` | ✅ 있음 | 타부서 동의 (승인/반려 가능) | +| 참조 | `reference` | ❌ 없음 | 열람만 가능, 열람 추적 | + +### 4.5 상태 전이 다이어그램 + +``` + ┌─────────────────────────────┐ + │ │ + ┌────────┐ submit() │ ┌─────────┐ │ + │ draft │────────────→│ │ pending │ │ + └────────┘ │ └────┬────┘ │ + ▲ │ │ │ + │ │ ┌────┼─────────┬───────┐ │ + │ (수정 후 재상신) │ │ │ │ │ │ + │ │ │ approve() reject() hold()│ + │ │ │ │ │ │ │ + │ │ │ ▼ ▼ ▼ │ + │ │ │ 다음 step rejected on_hold│ + │ │ │ 또는 │ │ │ + │ │ │ approved │ releaseHold() + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ └────┼────────┼───────┘ │ + │ │ │ │ │ + │ │ preDecide() │ │ + │ │ → approved │ │ + │ │ │ │ cancel() │ + │ │ │ │ │ │ + │ │ ▼ │ ▼ │ + │ │ ┌─────────┐ │ ┌──────────┐ + │ │ │approved │ │ │cancelled │ + │ │ └─────────┘ │ └──────────┘ + │ │ │ │ │ + │ │ │ │ │ + │ │ copyForRedraft() │ + │ │ │ │ │ + └───────────────────┼───────┴────────┘ │ + (새 draft 생성) │ │ + │ copyForRedraft() │ + │◀──────────────────────┘ + └─────────────────────────────┘ +``` + +--- + +## 5. 권한 매트릭스 + +### 5.1 누가 무엇을 할 수 있는가 + +| 액션 | 대상자 | 조건 | +|------|--------|------| +| **기안 작성** | 모든 사용자 | — | +| **수정** | 기안자 | `draft` 또는 `rejected` | +| **삭제** | 기안자 | `draft`만 | +| **상신** | 기안자 | `draft` 또는 `rejected`, 결재선 1명 이상 | +| **승인** | 현재 결재자 | `pending`, 자신이 현재 차례 | +| **반려** | 현재 결재자 | `pending`, 사유 필수 | +| **보류** | 현재 결재자 | `pending`, 사유 필수 | +| **보류 해제** | 보류한 결재자 | `on_hold`, 자신이 보류한 건 | +| **전결** | 현재 결재자 | `pending`, 이후 모든 단계 건너뜀 | +| **회수** | 기안자 | `pending` 또는 `on_hold`, 첫 결재자 미처리 | +| **복사 재기안** | 기안자 | `approved`, `rejected`, `cancelled` | +| **참조 열람** | 참조자 | `reference` step 보유 | + +### 5.2 회수 가능 조건 상세 + +``` +회수(cancel) 가능 여부 판단: + +1. 문서 상태가 pending 또는 on_hold인가? → 아니면 불가 +2. 요청자가 기안자(drafter_id)인가? → 아니면 불가 +3. 첫 번째 결재자(approval/agreement)의 상태가 pending 또는 on_hold인가? + → 이미 approved/rejected이면 불가 (첫 결재자가 이미 처리) +``` + +--- + +## 6. 메뉴 구조 + +``` +결재관리 +├── 기안함 /approval-mgmt/drafts ← 내가 기안한 문서 +├── 결재 대기함 /approval-mgmt/pending ← 내가 결재해야 할 문서 +├── 처리 완료함 /approval-mgmt/completed ← 내가 결재한 문서 +└── 참조함 /approval-mgmt/references ← 참조 문서 (열람 추적) +``` + +### 추가 페이지 + +| URL | 설명 | +|-----|------| +| `/approval-mgmt/create` | 기안 작성 | +| `/approval-mgmt/{id}` | 상세 조회 | +| `/approval-mgmt/{id}/edit` | 기안 수정 | + +--- + +## 7. 관련 문서 + +- [결재 양식 기술 명세](form-types.md) — 양식별 필드, JSON 구조, 인터랙션 +- [결재관리 워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 +- [API 명세](api-reference.md) — 엔드포인트 목록 및 요청/응답 예시 +- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작 +- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/approvals/api-reference.md b/features/approvals/api-reference.md new file mode 100644 index 0000000..b63e31b --- /dev/null +++ b/features/approvals/api-reference.md @@ -0,0 +1,594 @@ +# 결재관리 API 명세 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **Base URL**: `/api/admin/approvals` +> **미들웨어**: `web`, `auth`, `hq.member` +> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [UI 화면](ui-screens.md) + +--- + +## 1. 개요 + +모든 API는 JSON 응답을 반환한다. 인증은 세션 기반이며, CSRF 토큰이 필요하다. + +### 1.1 공통 응답 형식 + +**성공:** + +```json +{ + "success": true, + "message": "처리 메시지", + "data": { ... } +} +``` + +**실패 (400):** + +```json +{ + "success": false, + "message": "에러 메시지" +} +``` + +### 1.2 공통 헤더 + +``` +Content-Type: application/json +Accept: application/json +X-CSRF-TOKEN: {csrf_token} +``` + +--- + +## 2. 목록 조회 API + +### 2.1 기안함 + +내가 기안한 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/drafts +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `status` | string | 상태 필터 (`draft`, `pending`, `approved`, `rejected`, `cancelled`, `on_hold`) | +| `is_urgent` | boolean | 긴급 문서만 | +| `date_from` | date | 시작일 (YYYY-MM-DD) | +| `date_to` | date | 종료일 (YYYY-MM-DD) | +| `per_page` | int | 페이지당 건수 (기본 15) | +| `page` | int | 페이지 번호 | + +**응답:** Laravel 페이지네이션 형식 + +```json +{ + "data": [ + { + "id": 1, + "document_number": "APR-260228-001", + "title": "휴가 신청", + "status": "pending", + "is_urgent": false, + "form": { "id": 1, "name": "휴가신청서" }, + "steps": [...], + "created_at": "2026-02-28T10:00:00", + "drafted_at": "2026-02-28T10:05:00" + } + ], + "current_page": 1, + "last_page": 3, + "per_page": 15, + "total": 42 +} +``` + +--- + +### 2.2 결재 대기함 + +내가 현재 결재해야 할 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/pending +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `is_urgent` | boolean | 긴급 문서만 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +> 현재 사용자가 결재 차례인 문서만 표시된다. 이미 승인/반려한 문서는 표시되지 않는다. + +--- + +### 2.3 처리 완료함 + +내가 승인 또는 반려한 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/completed +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `status` | string | 상태 필터 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +--- + +### 2.4 참조함 + +내가 참조자로 지정된 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/references +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `is_read` | string | 열람 상태 필터 (`true`=열람완료, `false`=미열람) | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +--- + +## 3. CRUD API + +### 3.1 상세 조회 + +``` +GET /api/admin/approvals/{id} +``` + +**응답:** + +```json +{ + "success": true, + "data": { + "id": 1, + "tenant_id": 1, + "document_number": "APR-260228-001", + "form_id": 1, + "line_id": null, + "title": "휴가 신청", + "content": {}, + "body": "2월 27일~28일 연차 사용 신청합니다.", + "status": "pending", + "is_urgent": false, + "drafter_id": 10, + "department_id": 3, + "current_step": 2, + "drafted_at": "2026-02-28T10:05:00", + "completed_at": null, + "recall_reason": null, + "parent_doc_id": null, + "form": { "id": 1, "name": "휴가신청서" }, + "drafter": { "id": 10, "name": "홍길동" }, + "line": null, + "steps": [ + { + "id": 1, + "step_order": 1, + "step_type": "approval", + "approver_id": 20, + "approver_name": "김과장", + "approver_department": "경영지원팀", + "approver_position": "과장", + "status": "approved", + "approval_type": "normal", + "comment": "승인합니다.", + "acted_at": "2026-02-28T11:00:00", + "is_read": false, + "read_at": null + }, + { + "id": 2, + "step_order": 2, + "step_type": "approval", + "approver_id": 30, + "approver_name": "박부장", + "approver_department": "경영지원팀", + "approver_position": "부장", + "status": "pending", + "approval_type": "normal", + "comment": null, + "acted_at": null, + "is_read": false, + "read_at": null + } + ] + } +} +``` + +--- + +### 3.2 생성 (임시저장) + +``` +POST /api/admin/approvals +``` + +**Request Body:** + +```json +{ + "form_id": 1, + "title": "휴가 신청", + "body": "2월 27일~28일 연차 사용", + "is_urgent": false, + "steps": [ + { "user_id": 20, "step_type": "approval" }, + { "user_id": 30, "step_type": "approval" }, + { "user_id": 40, "step_type": "reference" } + ] +} +``` + +**Validation:** + +| 필드 | 규칙 | +|------|------| +| `form_id` | required, exists:approval_forms,id | +| `title` | required, string, max:200 | +| `body` | nullable, string | +| `is_urgent` | boolean | +| `steps` | nullable, array | +| `steps.*.user_id` | required_with:steps, exists:users,id | +| `steps.*.step_type` | required_with:steps, in:approval,agreement,reference | + +**응답 (201):** + +```json +{ + "success": true, + "message": "결재 문서가 저장되었습니다.", + "data": { ... } +} +``` + +--- + +### 3.3 수정 + +``` +PUT /api/admin/approvals/{id} +``` + +> `draft` 또는 `rejected` 상태에서만 수정 가능 + +**Request Body:** (생성과 동일, 모든 필드 선택) + +**Validation:** + +| 필드 | 규칙 | +|------|------| +| `title` | sometimes, string, max:200 | +| `body` | nullable, string | +| `is_urgent` | boolean | +| `steps` | nullable, array | + +--- + +### 3.4 삭제 + +``` +DELETE /api/admin/approvals/{id} +``` + +> `draft` 상태에서만 삭제 가능 + +**응답:** + +```json +{ + "success": true, + "message": "결재 문서가 삭제되었습니다." +} +``` + +--- + +## 4. 워크플로우 API + +### 4.1 상신 + +``` +POST /api/admin/approvals/{id}/submit +``` + +> 기안자가 `draft`/`rejected` 문서를 결재 요청한다. + +**Request Body:** 없음 + +**응답:** `{ "success": true, "message": "결재가 상신되었습니다.", "data": {...} }` + +--- + +### 4.2 승인 + +``` +POST /api/admin/approvals/{id}/approve +``` + +> 현재 결재자가 승인한다. + +**Request Body:** + +```json +{ + "comment": "승인합니다." // 선택 +} +``` + +**응답:** `{ "success": true, "message": "승인되었습니다.", "data": {...} }` + +--- + +### 4.3 반려 + +``` +POST /api/admin/approvals/{id}/reject +``` + +> 현재 결재자가 반려한다. 사유 필수. + +**Request Body:** + +```json +{ + "comment": "예산 초과로 반려합니다." // 필수 +} +``` + +**Validation:** `comment` — required, string, max:1000 + +**응답:** `{ "success": true, "message": "반려되었습니다.", "data": {...} }` + +--- + +### 4.4 회수 + +``` +POST /api/admin/approvals/{id}/cancel +``` + +> 기안자가 `pending`/`on_hold` 문서를 회수한다. 첫 결재자 미처리 시에만 가능. + +**Request Body:** + +```json +{ + "recall_reason": "내용 수정 필요" // 선택 +} +``` + +**응답:** `{ "success": true, "message": "결재가 회수되었습니다.", "data": {...} }` + +--- + +### 4.5 보류 + +``` +POST /api/admin/approvals/{id}/hold +``` + +> 현재 결재자가 결재를 보류한다. 사유 필수. + +**Request Body:** + +```json +{ + "comment": "추가 자료 검토 필요" // 필수 +} +``` + +**Validation:** `comment` — required, string, max:1000 + +**응답:** `{ "success": true, "message": "보류되었습니다.", "data": {...} }` + +--- + +### 4.6 보류 해제 + +``` +POST /api/admin/approvals/{id}/release-hold +``` + +> 보류한 결재자가 보류를 해제한다. + +**Request Body:** 없음 + +**응답:** `{ "success": true, "message": "보류가 해제되었습니다.", "data": {...} }` + +--- + +### 4.7 전결 + +``` +POST /api/admin/approvals/{id}/pre-decide +``` + +> 현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인한다. + +**Request Body:** + +```json +{ + "comment": "전결 처리합니다." // 선택 +} +``` + +**응답:** `{ "success": true, "message": "전결 처리되었습니다.", "data": {...} }` + +--- + +### 4.8 복사 재기안 + +``` +POST /api/admin/approvals/{id}/copy +``` + +> 기안자가 `approved`/`rejected`/`cancelled` 문서를 복사하여 새 draft를 생성한다. + +**Request Body:** 없음 + +**응답:** + +```json +{ + "success": true, + "message": "문서가 복사되었습니다.", + "data": { + "id": 15, + "document_number": "APR-260228-003", + "parent_doc_id": 1, + "status": "draft", + ... + } +} +``` + +> 응답의 `data.id`를 사용하여 `/approval-mgmt/{id}/edit`로 이동한다. + +--- + +### 4.9 참조 열람 추적 + +``` +POST /api/admin/approvals/{id}/mark-read +``` + +> 참조자가 문서를 열람했음을 기록한다. + +**Request Body:** 없음 + +**응답:** `{ "success": true, "message": "열람 처리되었습니다." }` + +--- + +## 5. 유틸리티 API + +### 5.1 결재선 템플릿 목록 + +``` +GET /api/admin/approvals/lines +``` + +**응답:** + +```json +{ + "success": true, + "data": [ + { "id": 1, "name": "일반 결재선", "steps": [...] } + ] +} +``` + +--- + +### 5.2 양식 목록 + +``` +GET /api/admin/approvals/forms +``` + +**응답:** + +```json +{ + "success": true, + "data": [ + { "id": 1, "name": "휴가신청서", "is_active": true } + ] +} +``` + +--- + +### 5.3 미처리 건수 (뱃지) + +``` +GET /api/admin/approvals/badge-counts +``` + +**응답:** + +```json +{ + "success": true, + "data": { + "pending": 3, + "draft": 1, + "reference_unread": 5 + } +} +``` + +| 필드 | 설명 | +|------|------| +| `pending` | 내가 결재해야 할 문서 수 | +| `draft` | 내 임시저장 문서 수 | +| `reference_unread` | 미열람 참조 문서 수 | + +--- + +## 6. 라우트 전체 목록 + +| Method | Path | 컨트롤러 메서드 | 이름 | 설명 | +|--------|------|---------------|------|------| +| GET | `/drafts` | `drafts` | `drafts` | 기안함 | +| GET | `/pending` | `pending` | `pending` | 결재 대기함 | +| GET | `/completed` | `completed` | `completed` | 처리 완료함 | +| GET | `/references` | `references` | `references` | 참조함 | +| GET | `/lines` | `lines` | `lines` | 결재선 템플릿 | +| GET | `/forms` | `forms` | `forms` | 양식 목록 | +| GET | `/badge-counts` | `badgeCounts` | `badge-counts` | 뱃지 건수 | +| POST | `/` | `store` | `store` | 생성 | +| GET | `/{id}` | `show` | `show` | 상세 | +| PUT | `/{id}` | `update` | `update` | 수정 | +| DELETE | `/{id}` | `destroy` | `destroy` | 삭제 | +| POST | `/{id}/submit` | `submit` | `submit` | 상신 | +| POST | `/{id}/approve` | `approve` | `approve` | 승인 | +| POST | `/{id}/reject` | `reject` | `reject` | 반려 | +| POST | `/{id}/cancel` | `cancel` | `cancel` | 회수 | +| POST | `/{id}/hold` | `hold` | `hold` | 보류 | +| POST | `/{id}/release-hold` | `releaseHold` | `release-hold` | 보류 해제 | +| POST | `/{id}/pre-decide` | `preDecide` | `pre-decide` | 전결 | +| POST | `/{id}/copy` | `copyForRedraft` | `copy` | 복사 재기안 | +| POST | `/{id}/mark-read` | `markAsRead` | `mark-read` | 열람 추적 | + +--- + +## 관련 문서 + +- [README.md](README.md) — 시스템 전체 개요 +- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 +- [UI 화면 구성](ui-screens.md) — 화면별 동작 + +--- + +**최종 업데이트**: 2026-02-28 diff --git a/features/approvals/db-changes-and-model-sync.md b/features/approvals/db-changes-and-model-sync.md new file mode 100644 index 0000000..e59cd2b --- /dev/null +++ b/features/approvals/db-changes-and-model-sync.md @@ -0,0 +1,286 @@ +# 결재관리 DB 변경사항 및 API 모델 동기화 현황 + +> **작성일**: 2026-03-09 +> **상태**: 조사 완료 +> **관련**: [README.md](README.md) | [API 명세](api-reference.md) + +--- + +## 1. 개요 + +### 1.1 목적 + +2026-02-27 ~ 2026-03-05 기간에 결재관리 테이블에 대규모 컬럼 추가가 이루어졌다. 이 문서는 변경된 DB 스키마와 API/MNG 프로젝트 간 모델 동기화 상태를 기록한다. + +### 1.2 핵심 발견 + +- 마이그레이션 **15개** 실행 (API 프로젝트에서 관리) +- MNG 모델: ✅ 모든 신규 컬럼 반영 완료 +- API 모델: ❌ **`$fillable`/`$casts` 미반영** — 오류 원인 가능성 + +--- + +## 2. 마이그레이션 변경 타임라인 + +### 2.1 Phase 2 확장 (2026-02-27) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_columns_to_approvals_table` | `approvals` | `line_id`, `body`, `is_urgent`, `department_id` 추가 | +| `add_columns_to_approval_steps_table` | `approval_steps` | `approver_name`, `approver_department`, `approver_position` 추가 | +| `add_phase2_columns_to_approval_steps_table` | `approval_steps` | `parallel_group`, `acted_by`, `approval_type` 추가 | +| `add_phase2_columns_to_approvals_table` | `approvals` | `recall_reason`, `parent_doc_id` 추가 | +| `create_approval_delegations_table` | `approval_delegations` | 위임 테이블 신규 생성 | +| `add_linkable_to_approvals_table` | `approvals` | `linkable_type`, `linkable_id` 추가 (다형성) | + +### 2.2 도메인 연동 (2026-02-28) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_approval_id_to_leaves_table` | `leaves` | `approval_id` FK 추가 | +| `insert_leave_approval_form` | `approval_forms` | 휴가신청 양식 데이터 등록 | + +### 2.3 양식 확장 (2026-03-03 ~ 03-04) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `insert_attendance_approval_forms` | `approval_forms` | 근태신청, 사유서 양식 등록 | +| `add_body_template_to_approval_forms` | `approval_forms` | `body_template` 컬럼 추가 | +| `insert_expense_approval_form` | `approval_forms` | 지출결의서 양식 + body_template 등록 | +| `update_expense_approval_form_body_template` | `approval_forms` | 지출결의서 body_template 고도화 | + +### 2.4 추적 기능 (2026-03-05) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_drafter_read_at_to_approvals_table` | `approvals` | `drafter_read_at` 추가 | +| `add_resubmit_count_to_approvals_table` | `approvals` | `resubmit_count` 추가 | +| `add_rejection_history_to_approvals_table` | `approvals` | `rejection_history` 추가 | + +--- + +## 3. 추가된 컬럼 상세 + +### 3.1 `approvals` 테이블 (11개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `line_id` | BIGINT FK NULL | NULL | 02-27 | 결재선 템플릿 참조 | +| `body` | LONGTEXT NULL | NULL | 02-27 | 문서 본문 HTML | +| `is_urgent` | BOOLEAN | false | 02-27 | 긴급 여부 | +| `department_id` | BIGINT NULL | NULL | 02-27 | 기안 부서 | +| `recall_reason` | TEXT NULL | NULL | 02-27 | 회수 사유 | +| `parent_doc_id` | BIGINT FK NULL | NULL | 02-27 | 재기안 원본 문서 | +| `linkable_type` | VARCHAR NULL | NULL | 02-27 | 다형성 모델 타입 | +| `linkable_id` | BIGINT NULL | NULL | 02-27 | 다형성 모델 ID | +| `drafter_read_at` | TIMESTAMP NULL | NULL | 03-05 | 기안자 열람 시각 | +| `resubmit_count` | TINYINT UNSIGNED | 0 | 03-05 | 재상신 횟수 | +| `rejection_history` | JSON NULL | NULL | 03-05 | 반려 이력 배열 | + +### 3.2 `approval_steps` 테이블 (6개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `approver_name` | VARCHAR(50) NULL | NULL | 02-27 | 결재자명 스냅샷 | +| `approver_department` | VARCHAR(100) NULL | NULL | 02-27 | 결재자 부서 스냅샷 | +| `approver_position` | VARCHAR(50) NULL | NULL | 02-27 | 결재자 직급 스냅샷 | +| `parallel_group` | INT NULL | NULL | 02-27 | 병렬 결재 그룹 (Phase 3) | +| `acted_by` | BIGINT FK NULL | NULL | 02-27 | 실제 처리자 (대결) | +| `approval_type` | VARCHAR(20) | 'normal' | 02-27 | normal/pre_decided/delegated | + +### 3.3 `approval_forms` 테이블 (1개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `body_template` | TEXT NULL | NULL | 03-04 | HTML 양식 렌더링 템플릿 | + +### 3.4 `approval_delegations` 테이블 (신규 생성) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `delegator_id` | BIGINT FK | 위임자 | +| `delegate_id` | BIGINT FK | 대리인 | +| `start_date` | DATE | 위임 시작일 | +| `end_date` | DATE | 위임 종료일 | +| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) | +| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 | +| `is_active` | BOOLEAN | 활성 여부 | +| `reason` | VARCHAR(200) | 위임 사유 | + +--- + +## 4. API/MNG 모델 동기화 현황 + +### 4.1 Approval 모델 비교 + +| 항목 | MNG (`mng/app/Models/Approvals/Approval.php`) | API (`api/app/Models/Tenants/Approval.php`) | +|------|:---:|:---:| +| `line_id` in $fillable | ✅ | ❌ | +| `body` in $fillable | ✅ | ❌ | +| `is_urgent` in $fillable/$casts | ✅ boolean | ❌ | +| `department_id` in $fillable | ✅ | ❌ | +| `recall_reason` in $fillable | ✅ | ❌ | +| `parent_doc_id` in $fillable | ✅ | ❌ | +| `linkable_type/id` in $fillable | ✅ | ✅ | +| `drafter_read_at` in $fillable/$casts | ✅ datetime | ❌ | +| `resubmit_count` in $fillable/$casts | ✅ integer | ❌ | +| `rejection_history` in $fillable/$casts | ✅ array | ❌ | + +### 4.2 ApprovalStep 모델 비교 + +| 항목 | MNG | API | +|------|:---:|:---:| +| `approver_name` in $fillable | ✅ | ❌ | +| `approver_department` in $fillable | ✅ | ❌ | +| `approver_position` in $fillable | ✅ | ❌ | +| `parallel_group` in $fillable | ✅ | ❌ | +| `acted_by` in $fillable | ✅ | ❌ | +| `approval_type` in $fillable | ✅ | ❌ | + +### 4.3 ApprovalForm 모델 비교 + +| 항목 | MNG | API | +|------|:---:|:---:| +| `body_template` in $fillable | ✅ | ❌ | + +### 4.4 ApprovalDelegation 모델 + +| 항목 | MNG | API | +|------|:---:|:---:| +| 모델 파일 존재 | ✅ | ❌ 미생성 | + +--- + +## 5. 오류 영향 분석 + +### 5.1 API 모델 미반영으로 인한 잠재적 오류 + +API 프로젝트의 모델 `$fillable`에 신규 컬럼이 누락되어, API 엔드포인트를 통한 결재 문서 처리 시 다음 오류가 발생할 수 있다: + +| 증상 | 원인 | 영향 범위 | +|------|------|----------| +| `create()`/`update()` 시 신규 필드 저장 안 됨 | `$fillable` 미포함 → mass assignment 차단 | API v1 결재 CRUD | +| JSON 필드(`rejection_history`) 문자열로 반환 | `$casts` 미정의 → 타입 변환 안 됨 | API 응답 파싱 오류 | +| `drafter_read_at` 날짜 비교 실패 | `$casts` datetime 미정의 → Carbon 미변환 | 열람 추적 기능 | +| `is_urgent` 비교 오류 | `$casts` boolean 미정의 → 문자열 비교 | 긴급 필터링 | +| 위임(delegation) 기능 완전 불가 | 모델 자체 미생성 | Phase 3 기능 전체 | + +### 5.2 MNG는 정상 + +MNG 프로젝트의 모델은 모든 신규 컬럼이 `$fillable`, `$casts`, `$attributes`에 반영되어 있으며, `ApprovalService`에서 정상 사용 중이다. + +``` +MNG 정상 동작 확인 기능: +✅ 반려 이력 저장 (rejection_history) +✅ 재상신 횟수 추적 (resubmit_count) +✅ 기안자 열람 추적 (drafter_read_at) +✅ 결재자 스냅샷 저장 (approver_name/department/position) +✅ 전결 처리 (approval_type = pre_decided) +✅ 회수 사유 기록 (recall_reason) +``` + +--- + +## 6. 수정 필요 파일 목록 + +### 6.1 API 모델 업데이트 필요 + +| 파일 | 수정 내용 | +|------|----------| +| `api/app/Models/Tenants/Approval.php` | `$fillable`에 9개 필드, `$casts`에 4개 필드 추가 | +| `api/app/Models/Tenants/ApprovalStep.php` | `$fillable`에 6개 필드 추가 | +| `api/app/Models/Tenants/ApprovalForm.php` | `$fillable`에 `body_template` 추가 | +| `api/app/Models/Tenants/ApprovalDelegation.php` | 모델 파일 신규 생성 | + +### 6.2 Approval.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'line_id', +'body', +'is_urgent', +'department_id', +'recall_reason', +'parent_doc_id', +'drafter_read_at', +'resubmit_count', +'rejection_history', +``` + +**`$casts` 추가 필요:** + +```php +'drafter_read_at' => 'datetime', +'resubmit_count' => 'integer', +'rejection_history' => 'array', +'is_urgent' => 'boolean', +``` + +### 6.3 ApprovalStep.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'approver_name', +'approver_department', +'approver_position', +'parallel_group', +'acted_by', +'approval_type', +``` + +### 6.4 ApprovalForm.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'body_template', +``` + +--- + +## 7. 연관 테이블 참조 변경 + +결재 시스템과 연동된 다른 테이블의 변경사항: + +| 테이블 | 추가 컬럼 | 추가일 | 용도 | +|--------|----------|--------|------| +| `leaves` | `approval_id` (BIGINT FK) | 02-28 | 휴가 ↔ 결재 연동 | +| `purchases` | `approval_id` (BIGINT FK) | (기존) | 구매 ↔ 결재 연동 | + +--- + +## 8. 등록된 결재 양식 (13종) + +2026-02-28 ~ 03-07 기간에 마이그레이션으로 등록된 양식: + +| 코드 | 양식명 | 카테고리 | 등록일 | +|------|--------|---------|--------| +| `leave` | 휴가신청서 | request | 02-28 | +| `attendance_request` | 근태신청서 | request | 03-03 | +| `reason_report` | 사유서 | request | 03-03 | +| `expense` | 지출결의서 | expense | 03-04 | +| `employment_cert` | 재직증명서 | request | 03-05 | +| `career_cert` | 경력증명서 | request | 03-05 | +| `appointment_cert` | 위촉증명서 | request | 03-05 | +| `resignation` | 사직서 | request | 03-06 | +| `seal_usage` | 사용인감계 | request | 03-06 | +| `delegation` | 위임장 | request | 03-06 | +| `board_minutes` | 이사회의사록 | request | 03-06 | +| `quotation` | 견적서 | request | 03-06 | +| `official_letter` | 공문서 | request | 03-07 | + +--- + +## 관련 문서 + +- [결재관리 시스템 개요](README.md) — 아키텍처, 상태 관리, 권한 +- [API 명세](api-reference.md) — 20개 엔드포인트 상세 +- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름 +- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획 + +--- + +**최종 업데이트**: 2026-03-09 diff --git a/features/approvals/form-types.md b/features/approvals/form-types.md new file mode 100644 index 0000000..3242b78 --- /dev/null +++ b/features/approvals/form-types.md @@ -0,0 +1,999 @@ +# 결재 양식 기술 명세 + +> **작성일**: 2026-03-06 +> **상태**: Phase 2 구현 완료 +> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md) + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM MNG 결재관리의 **기안함 양식** 기술 명세. 각 양식의 필드 구조, JSON Content 데이터 형식, UI 인터랙션, 특수 로직을 정의한다. + +### 1.2 양식 목록 + +| 코드 | 양식명 | 분류 | Blade 파일 | 설명 | +|------|--------|------|------------|------| +| `BUSINESS_DRAFT` | 업무기안서 | 일반 | (body 편집기) | 일반 업무 보고·요청 | +| `leave` | 휴가신청 | 인사/근태 | `_leave-form.blade.php` | 연차, 휴가, 근태 신청 | +| `attendance_request` | 근태신청 | 인사/근태 | `_leave-form.blade.php` | 외근, 출장, 조퇴 등 | +| `reason_report` | 사유서 | 인사/근태 | `_leave-form.blade.php` | 지각, 결근 등 사유 소명 | +| `resignation` | 사직서 | 인사/근태 | `_resignation-form.blade.php` | 퇴직 서류 | +| `employment_cert` | 재직증명서 | 증명서 | `_certificate-form.blade.php` | 재직 증명 발급 (PDF) | +| `career_cert` | 경력증명서 | 증명서 | `_career-cert-form.blade.php` | 경력 증명 발급 (PDF) | +| `appointment_cert` | 위촉증명서 | 증명서 | `_appointment-cert-form.blade.php` | 위촉/임명 증명 발급 (PDF) | +| `pr_expense` | 지출품의서 | 품의 | `_purchase-request-form.blade.php` | 지출 전 사전 승인 | +| `pr_contract` | 계약체결품의서 | 품의 | `_purchase-request-form.blade.php` | 계약 체결 전 승인 | +| `pr_purchase` | 구매품의서 | 품의 | `_purchase-request-form.blade.php` | 물품 구매 전 승인 | +| `pr_trip` | 출장품의서 | 품의 | `_purchase-request-form.blade.php` | 출장 계획 승인 | +| `pr_settlement` | 비용정산품의서 | 품의 | `_purchase-request-form.blade.php` | 비용 정산 승인 | +| `expense` | 지출결의서 | 재무 | `_expense-form.blade.php` | 법인카드/송금/자동이체 지출 | + +### 1.3 공통 구조 + +모든 양식은 동일한 패턴으로 동작한다: + +``` +양식 선택 (form_id) + ↓ +양식별 Blade 파셜 렌더링 (create.blade.php 내 조건부 display) + ↓ +사용자 입력 → Alpine.js / JavaScript 인터랙션 + ↓ +getFormData() → JSON content 생성 + ↓ +ApprovalService::createApproval() → Approval.content (JSON 컬럼) 저장 +``` + +### 1.4 양식 선택 UI (2단계 분류 + 설명 카드) + +양식 선택은 **2단계 드롭다운 + 설명 카드** 레이아웃으로 구성된다. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 양식 * │ +│ ┌──── 30% ────────┐ ┌─────────────── 70% ───────────────────────────┐ │ +│ │ 📋 품의 ▼ │ │ ┌─────────────────────────────────────────┐ │ │ +│ │ │ │ │ 📋 지출품의서 │ │ │ +│ │ 지출품의서 ▼ │ │ │ 지출이 발생하기 전 사전 승인을 받는 │ │ │ +│ │ │ │ │ 문서입니다. 예산 범위 내에서 지출 항목과 │ │ │ +│ │ │ │ │ 금액을 기재하여 사전에 승락을 받습니다. │ │ │ +│ └──────────────────┘ │ └─────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### 1단계: 분류 선택 (`form_category`) + +| 분류 | 아이콘 | 포함 양식 | +|------|--------|----------| +| 일반 | 📄 | 업무기안서 | +| 인사/근태 | 👤 | 휴가신청, 근태신청, 사유서, 사직서 | +| 증명서 | 📜 | 재직증명서, 경력증명서, 위촉증명서 | +| 품의 | 📋 | 지출품의서, 계약체결품의서, 구매품의서, 출장품의서, 비용정산품의서 | +| 재무 | 💰 | 지출결의서 | + +#### 2단계: 양식 선택 (`form_id`) + +- 1단계 분류 선택 시 해당 분류에 속하지 않는 양식은 `display:none` + `disabled` +- 분류 내 첫 번째 양식 자동 선택 + +#### 설명 카드 (`formDescriptions`) + +- 양식 선택 시 우측에 해당 양식의 아이콘/제목/설명 텍스트 표시 +- 14종 전체 양식에 대한 설명 정의 (create/edit 공통) +- 색상: 양식별 Tailwind 테마 색상 (`border-*-200 bg-*-50`) + +#### 핵심 JavaScript 함수 + +| 함수 | 설명 | +|------|------| +| `buildCategoryOptions()` | 사용 가능한 카테고리만 `form_category` 옵션으로 생성 | +| `filterFormsByCategory(cat)` | 선택된 분류 외 양식 옵션 숨김/비활성화 | +| `selectCategoryByFormId(formId)` | formId로 카테고리 역산하여 자동 선택 | +| `updateFormDescription(formId)` | 설명 카드 DOM 업데이트 | + +### 1.5 파일 구조 + +``` +resources/views/approvals/ +├── create.blade.php ← 기안 작성 (2단계 양식 선택 + 설명 카드 + 동적 폼) +├── edit.blade.php ← 기안 수정 (create와 동일한 2단계 선택 구조) +├── show.blade.php ← 상세 조회 (양식별 조회 컴포넌트) +└── partials/ + ├── _leave-form.blade.php ← 휴가신청 폼 + ├── _expense-form.blade.php ← 지출결의서 폼 + ├── _expense-show.blade.php ← 지출결의서 조회 + ├── _purchase-request-form.blade.php ← 품의서 5종 통합 폼 (Alpine.js) + ├── _purchase-request-show.blade.php ← 품의서 5종 통합 조회 + ├── _certificate-form.blade.php ← 재직증명서 폼 + ├── _certificate-show.blade.php ← 재직증명서 조회 + ├── _career-cert-form.blade.php ← 경력증명서 폼 + ├── _career-cert-show.blade.php ← 경력증명서 조회 + ├── _appointment-cert-form.blade.php ← 위촉증명서 폼 + ├── _appointment-cert-show.blade.php ← 위촉증명서 조회 + ├── _resignation-form.blade.php ← 사직서 폼 + ├── _resignation-show.blade.php ← 사직서 조회 + ├── _approval-stamp-table.blade.php ← 결재 도장 테이블 + └── _approval-line-editor.blade.php ← 결재선 편집기 +``` + +--- + +## 2. 휴가신청 (leave) + +### 2.1 폼 필드 + +| 필드 ID | 라벨 | 타입 | 필수 | 기본값 | 설명 | +|---------|------|------|------|--------|------| +| `leave-user-id` | 신청자 | select | 필수 | `auth()->id()` | 활성 사원 목록 | +| `leave-type` | 유형 | select | 필수 | - | 휴가/근태신청/사유서 | +| `leave-start-date` | 시작일 | date | 필수 | - | `YYYY-MM-DD` | +| `leave-end-date` | 종료일 | date | 필수 | - | `YYYY-MM-DD` | +| `leave-reason` | 사유 | textarea | 선택 | - | 자유 텍스트 | + +### 2.2 Content JSON + +```json +{ + "user_id": "10", + "leave_type": "연차", + "start_date": "2026-03-06", + "end_date": "2026-03-07", + "reason": "개인 사유" +} +``` + +### 2.3 특수 로직 + +- **자동 선택**: 로그인 사용자가 기본 선택 (`auth()->id()`) +- **직원 목록**: `$employees` Props로 전달 (활성 사원만) +- **단순 구조**: Alpine.js 없이 Blade 폼으로 구현 + +--- + +## 3. 지출결의서 (expense) + +### 3.1 폼 구조 (Alpine.js 기반) + +```javascript +x-data="expenseForm(initialData, authUserName, initialFiles, cardsData, accountsData)" +``` + +### 3.2 기본 정보 필드 + +| 필드 | 라벨 | 타입 | 필수 | 기본값 | +|------|------|------|------|--------| +| `expense_type` | 지출형식 | radio | 필수 | `corporate_card` | +| `tax_invoice` | 세금계산서 | radio | 필수 | `normal` | +| `write_date` | 작성일자 | date | 선택 | 오늘 | +| `approval_date` | 결재일자 | date | 선택 | 오늘 | +| `department` | 부서 | text | 선택 | `경리부` | +| `writer_name` | 이름 | text | 선택 | 인증 사용자명 | + +### 3.3 지출형식별 선택 + +| 지출형식 | 코드 | 연결 데이터 | +|---------|------|------------| +| 법인카드 | `corporate_card` | `$cards` → `selected_card` | +| 송금 | `transfer` | `$accounts` → `selected_account` | +| 자동이체 출금 | `auto_transfer` | `$accounts` → `selected_account` | +| 현금/가지급정산 | `cash_advance` | 없음 | + +**법인카드 선택 시 저장 구조:** + +```json +{ + "selected_card": { + "id": 1, + "card_name": "삼성카드", + "card_company": "삼성", + "card_number_last4": "1234", + "card_holder_name": "홍길동" + } +} +``` + +**계좌 선택 시 저장 구조:** + +```json +{ + "selected_account": { + "id": 1, + "bank_name": "국민은행", + "account_number": "123-456-789012", + "account_holder": "주일기업" + } +} +``` + +### 3.4 세금계산서 옵션 + +| 옵션 | 코드 | +|------|------| +| 일반 | `normal` | +| 이월발행 | `deferred` | +| 없음 | `none` | + +### 3.5 내역 테이블 + +**동적 rows** (`.items` 배열): + +| 필드 | 라벨 | 타입 | 설명 | +|------|------|------|------| +| `date` | 일자 | date | `YYYY-MM-DD` | +| `description` | 적요 | text | 지출 설명 | +| `amount` | 금액 | number | 콤마 제거 정수 | +| `vendor` | 거래처 | text | Autocomplete 검색 | +| `vendor_id` | 거래처 ID | hidden | API 연결 ID | +| `vendor_biz_no` | 사업자번호 | hidden | 자동 채움 | +| `bank` | 은행명 | text | 수동 입력 | +| `account_no` | 계좌번호 | text | 수동 입력 | +| `depositor` | 예금주 | text | 수동 입력 | +| `remark` | 비고 | text | 메모 | + +### 3.6 Content JSON (전체) + +```json +{ + "expense_type": "corporate_card", + "tax_invoice": "normal", + "write_date": "2026-03-06", + "approval_date": "2026-03-06", + "department": "경리부", + "writer_name": "홍길동", + "items": [ + { + "date": "2026-03-05", + "description": "사무용품 구매", + "amount": 150000, + "vendor": "오피스디포", + "vendor_id": 123, + "vendor_biz_no": "123-45-67890", + "bank": "", + "account_no": "", + "depositor": "", + "remark": "" + } + ], + "total_amount": 150000, + "attachment_memo": "영수증 첨부", + "selected_card": { ... }, + "selected_account": null +} +``` + +### 3.7 특수 기능 + +#### 거래처 검색 (Autocomplete) + +``` +입력 → 250ms 디바운싱 → API 호출 → 드롭다운 렌더링 + +API: /barobill/tax-invoice/search-partners?keyword=... +키보드: ↑↓(네비게이션), Enter(선택), Esc(닫기) +마우스: 항목 클릭(선택) +``` + +#### 금액 입력 포맷팅 + +``` +입력 시: 콤마 제거 → 정수 저장 (parseMoney) +표시 시: 콤마 포맷 (formatMoney) +합계: totalAmount getter → footer 실시간 업데이트 +``` + +#### 파일 업로드 + +``` +드래그 앤 드롭 + 파일 입력 +최대: 20MB +형식: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, jpeg, png, gif, zip, rar +API: POST /api/admin/approvals/upload-file +진행률: XHR 업로드 이벤트 +``` + +#### 카드/계좌 연동 + +``` +카드 선택 → 모든 내역 행에 "결제카드" 자동 표시 +계좌 선택 → 모든 내역 행에 "은행/계좌/예금주" 자동 채움 +``` + +### 3.8 조회 화면 (_expense-show.blade.php) + +| 섹션 | 내용 | +|------|------| +| 기본 정보 | 지출형식, 세금계산서, 작성일, 결재일, 부서, 이름 | +| 선택 카드/계좌 | 유색 박스로 표시 | +| 내역 테이블 | 읽기 전용, `number_format()` 금액 | +| 첨부서류 메모 | `whitespace-pre-wrap` | +| 첨부파일 목록 | 다운로드 링크 + 파일 크기 | + +--- + +## 4. 증명서 양식 공통 + +### 4.1 공통 패턴 + +모든 증명서 양식은 동일한 패턴을 따른다: + +``` +사원 선택 → loadXxxInfo(userId) → API 호출 → 읽기 전용 필드 자동 채움 + ↓ + 일부 필드만 수정 가능 + ↓ + 미리보기 모달 (인쇄 가능) +``` + +### 4.2 공통 함수 + +| 함수 | 설명 | +|------|------| +| `loadXxxInfo(userId)` | 사원 선택 시 인적/재직 정보 로드 | +| `openXxxPreview()` | 미리보기 모달 열기 | +| `printXxxPreview()` | 미리보기 인쇄 (`window.print()`) | +| `closeXxxPreview()` | 미리보기 닫기 | +| `onXxxPurposeChange()` | 용도 선택 시 직접입력 필드 표시 | + +### 4.3 조회 화면 공통 + +- 읽기 전용 필드 표시 +- PDF 다운로드: `route('api.admin.approvals.cert-pdf', $approval->id)` + +--- + +## 5. 재직증명서 (employment_cert) + +### 5.1 폼 필드 + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | +|------|---------|------|------|------|------| +| 인적사항 | `cert-name` | 성명 | text | readonly | DB 자동 채움 | +| | `cert-resident` | 주민등록번호 | text | readonly | DB 자동 채움 | +| | `cert-address` | 주소 | text | editable | 직접 입력 | +| 재직사항 | `cert-company` | 회사명 | text | readonly | DB 자동 채움 | +| | `cert-business-num` | 사업자번호 | text | readonly | DB 자동 채움 | +| | `cert-department` | 근무부서 | text | readonly | DB 자동 채움 | +| | `cert-position` | 직급 | text | readonly | DB 자동 채움 | +| | `cert-hire-date` | 재직기간 | text | readonly | DB 자동 채움 | +| 발급정보 | `cert-purpose-select` | 사용용도 | select | editable | 드롭다운 선택 | +| | (custom) | 기타 용도 | text | editable | "기타" 선택 시 표시 | +| | `cert-issue-date` | 발급일 | text | readonly | `now()->format('Y-m-d')` | + +### 5.2 Content JSON + +```json +{ + "name": "홍길동", + "resident_number": "900101-1XXXXXX", + "address": "서울특별시 강남구", + "company_name": "(주)코드브릿지엑스", + "business_num": "123-45-67890", + "department": "개발팀", + "position": "과장", + "hire_date": "2020-03-01", + "purpose": "은행 제출용", + "issue_date": "2026-03-06" +} +``` + +--- + +## 6. 경력증명서 (career_cert) + +### 6.1 폼 필드 (재직증명서 대비 추가/변경) + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | +|------|---------|------|------|------|------| +| 인적사항 | `cc-birth-date` | 생년월일 | text | readonly | DB 자동 채움 | +| 경력사항 | `cc-ceo-name` | 대표자 | text | readonly | DB 자동 채움 | +| | `cc-phone` | 대표전화 | text | readonly | DB 자동 채움 | +| | `cc-company-address` | 소재지 | text | readonly | DB 자동 채움 | +| | `cc-department` | 소속부서 | text | readonly | DB 자동 채움 | +| | `cc-position` | 직위/직급 | text | readonly | DB 자동 채움 | +| | `cc-hire-date` | 근무기간 시작 | text | readonly | DB 자동 채움 | +| | `cc-resign-date` | 근무기간 종료 | date | editable | 직접 입력 | +| | `cc-job-description` | 담당업무 | text | editable | 직접 입력 | +| 발급정보 | 용도 | select | editable | + "이직 제출용" 옵션 | + +### 6.2 Content JSON + +```json +{ + "name": "홍길동", + "birth_date": "1990-01-01", + "address": "서울특별시 강남구", + "company_name": "(주)코드브릿지엑스", + "business_num": "123-45-67890", + "ceo_name": "김대표", + "phone": "02-1234-5678", + "company_address": "서울특별시 강남구 테헤란로", + "department": "개발팀", + "position": "과장", + "hire_date": "2020-03-01", + "resign_date": "2026-02-28", + "job_description": "웹 애플리케이션 개발", + "purpose": "이직 제출용", + "issue_date": "2026-03-06" +} +``` + +--- + +## 7. 위촉증명서 (appointment_cert) + +### 7.1 폼 필드 + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | +|------|---------|------|------|------|------| +| 인적사항 | `ac-name` | 성명 | text | readonly | DB 자동 채움 | +| | `ac-resident` | 주민등록번호 | text | readonly | DB 자동 채움 | +| | `ac-department` | 소속 | text | readonly | DB 자동 채움 | +| | `ac-phone` | 연락처 | text | editable | 직접 입력 | +| 위촉정보 | `ac-hire-date` | 위촉기간 시작 | text | readonly | DB 자동 채움 | +| | `ac-resign-date` | 위촉기간 종료 | date | editable | 직접 입력 | +| | `ac-contract-type` | 계약자격 | text | editable | 직접 입력 | +| 발급정보 | `ac-purpose-select` | 용도 | select | editable | 드롭다운 선택 | +| | `ac-issue-date` | 발급일 | text | readonly | 자동 설정 | +| (숨김) | `ac-company-name` | 회사명 | hidden | - | 미리보기용 | +| | `ac-ceo-name` | 대표자명 | hidden | - | 미리보기용 | + +### 7.2 Content JSON + +```json +{ + "name": "홍길동", + "resident_number": "900101-1XXXXXX", + "department": "기술자문팀", + "phone": "010-1234-5678", + "hire_date": "2024-01-01", + "resign_date": "2026-12-31", + "contract_type": "기술자문위원", + "purpose": "관공서 제출용", + "issue_date": "2026-03-06" +} +``` + +--- + +## 8. 사직서 (resignation) + +### 8.1 폼 필드 + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 필수 | +|------|---------|------|------|------|------| +| 인적사항 | `rg-department` | 소속 | text | readonly | - | +| | `rg-position` | 직위 | text | readonly | - | +| | `rg-name` | 성명 | text | readonly | - | +| | `rg-resident` | 주민등록번호 | text | readonly | - | +| | `rg-hire-date` | 입사일 | text | readonly | - | +| | `rg-resign-date` | 퇴사(예정)일 | date | editable | 필수 | +| | `rg-address` | 주소 | text | editable | - | +| 사직사유 | `rg-reason-select` | 사유 | select | editable | 필수 | +| | (custom) | 기타 사유 | text | editable | - | +| 제출일 | `rg-issue-date` | 제출일 | text | readonly | - | + +### 8.2 사직사유 옵션 + +| 옵션 | +|------| +| 일신상의 사유 | +| 가사 사정 | +| 건강상의 이유 | +| 진학/학업 | +| 이직 | +| 기타 (직접입력) | + +### 8.3 Content JSON + +```json +{ + "department": "개발팀", + "position": "대리", + "name": "홍길동", + "resident_number": "900101-1XXXXXX", + "hire_date": "2020-03-01", + "resign_date": "2026-04-01", + "address": "서울특별시 강남구", + "reason": "이직", + "issue_date": "2026-03-06" +} +``` + +--- + +## 9. 품의서 5종 공통 (_purchase-request-form/show) + +### 9.1 통합 Alpine.js 컴포넌트 + +품의서 5종은 **단일 Blade 파일**(`_purchase-request-form.blade.php`)에서 `prType` 프로퍼티로 동적 전환된다. + +```javascript +x-data="purchaseRequestForm(initialData, authUserName, initialFiles)" +``` + +#### 타입 전환 메커니즘 + +``` +create.blade.php → switchFormMode() + ↓ +code.startsWith('pr_') 감지 + ↓ +#purchase-request-form-container display: block + ↓ +setTimeout(50ms) → _x_dataStack[0].setPrType(code) + ↓ +Alpine.js x-if 분기 → 해당 폼 렌더링 +``` + +#### prType 코드 및 라벨 + +| prType | 라벨 | 색상 | +|--------|------|------| +| `pr_expense` | 지출품의서 | `bg-orange-50 text-orange-700` | +| `pr_contract` | 계약체결품의서 | `bg-purple-50 text-purple-700` | +| `pr_purchase` | 구매품의서 | `bg-blue-50 text-blue-700` | +| `pr_trip` | 출장품의서 | `bg-green-50 text-green-700` | +| `pr_settlement` | 비용정산품의서 | `bg-teal-50 text-teal-700` | + +### 9.2 공통 필드 (모든 품의서) + +| 필드 | 라벨 | 타입 | 기본값 | +|------|------|------|--------| +| `write_date` | 작성일자 | date | 오늘 | +| `department` | 요청부서 | text | - | +| `writer_name` | 요청자 | text | 인증 사용자명 | +| `attachment_memo` | 첨부서류 메모 | textarea | - | +| `files` | 파일 업로드 | file[] | - | + +### 9.3 공통 함수 + +| 함수 | 설명 | +|------|------| +| `setPrType(type)` | 외부에서 prType 설정 (switchFormMode에서 호출) | +| `getFormData()` | prType별 다른 JSON 구조 반환 (base에 `pr_type` 포함) | +| `addItem()` | 내역 행 추가 | +| `removeItem(index)` | 내역 행 삭제 | +| `formatMoney(val)` | 숫자 → 콤마 포맷 | +| `parseMoney(str)` | 콤마 문자열 → 정수 | +| `prVendorSearch(target, fieldName)` | 범용 거래처 Autocomplete 검색 | + +### 9.4 조회 화면 분기 (show.blade.php) + +```php +// show.blade.php에서 pr_ prefix로 분기 +@if(str_starts_with($approval->form?->code ?? '', 'pr_')) + @include('approvals.partials._purchase-request-show', ['content' => $content]) +@endif +``` + +`_purchase-request-show.blade.php`에서 `$content['pr_type']`으로 5종 분기 렌더링. + +--- + +## 10. 지출품의서 (pr_expense) + +### 10.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `expense_category` | 지출항목 | text | 선택 | +| `usage_date` | 사용일자 | date | 선택 | +| `purpose` | 사용목적 | textarea | 필수 | + +### 10.2 내역 테이블 + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `description` | 항목 | text | +| `amount` | 금액 | number (콤마 포맷) | +| `remark` | 비고 | text | + +### 10.3 Content JSON + +```json +{ + "pr_type": "pr_expense", + "write_date": "2026-03-06", + "department": "개발팀", + "writer_name": "홍길동", + "expense_category": "사무용품", + "usage_date": "2026-03-05", + "purpose": "업무용 모니터 구매", + "items": [ + { "description": "27인치 모니터", "amount": 350000, "remark": "LG전자" } + ], + "total_amount": 350000, + "attachment_memo": "견적서 첨부" +} +``` + +--- + +## 11. 계약체결품의서 (pr_contract) + +### 11.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `contract_party` | 계약상대방 | text + Autocomplete | 필수 | +| `contract_party_biz_no` | 사업자번호 | text (자동) | - | +| `contract_content` | 계약내용 | textarea | 필수 | +| `contract_period_start` | 계약기간 시작 | date | 선택 | +| `contract_period_end` | 계약기간 종료 | date | 선택 | +| `contract_amount` | 계약금액 | number (콤마) | 필수 | +| `contract_conditions` | 주요조건 | textarea | 선택 | + +### 11.2 Content JSON + +```json +{ + "pr_type": "pr_contract", + "write_date": "2026-03-06", + "department": "경영지원팀", + "writer_name": "홍길동", + "contract_party": "(주)에이비씨", + "contract_party_biz_no": "123-45-67890", + "contract_content": "연간 IT 유지보수 계약", + "contract_period_start": "2026-04-01", + "contract_period_end": "2027-03-31", + "contract_amount": 12000000, + "contract_conditions": "월 1회 정기점검, 장애 발생 시 4시간 내 대응", + "attachment_memo": "계약서 초안 첨부" +} +``` + +### 11.3 특수 로직 + +- **거래처 검색**: `prVendorSearch(formData, 'contract_party')` — 계약상대방 필드에 Autocomplete 적용 +- 선택 시 `contract_party_biz_no` 자동 채움 + +--- + +## 12. 구매품의서 (pr_purchase) + +### 12.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `vendor` | 납품업체 | text + Autocomplete | 선택 | +| `vendor_biz_no` | 사업자번호 | text (자동) | - | +| `delivery_date` | 납품예정일 | date | 선택 | +| `delivery_location` | 납품장소 | text | 선택 | + +### 12.2 내역 테이블 + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `name` | 품목 | text | +| `spec` | 규격 | text | +| `quantity` | 수량 | number | +| `unit_price` | 단가 | number (콤마) | +| `amount` | 금액 | number (자동: 수량×단가) | +| `remark` | 비고 | text | + +### 12.3 Content JSON + +```json +{ + "pr_type": "pr_purchase", + "write_date": "2026-03-06", + "department": "생산팀", + "writer_name": "홍길동", + "vendor": "(주)공급사", + "vendor_biz_no": "987-65-43210", + "delivery_date": "2026-03-20", + "delivery_location": "본사 1층 창고", + "items": [ + { "name": "A4용지", "spec": "80g 500매", "quantity": 10, "unit_price": 25000, "amount": 250000, "remark": "" } + ], + "total_amount": 250000, + "attachment_memo": "" +} +``` + +### 12.4 특수 로직 + +- **금액 자동 계산**: `quantity × unit_price → amount` (x-effect 반응) +- **거래처 검색**: `prVendorSearch(formData, 'vendor')` — 납품업체 필드에 Autocomplete 적용 + +--- + +## 13. 출장품의서 (pr_trip) + +### 13.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `destination` | 출장지 | text | 필수 | +| `trip_period_start` | 출장기간 시작 | date | 필수 | +| `trip_period_end` | 출장기간 종료 | date | 필수 | +| `trip_purpose` | 출장목적 | textarea | 필수 | + +### 13.2 일정표 (items) + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `date` | 일자 | date | +| `schedule` | 일정 | text | +| `remark` | 비고 | text | + +### 13.3 경비 내역 (expenses) + +| 필드 | 라벨 | 타입 | +|------|------|------| +| `transport` | 교통비 | number (콤마) | +| `accommodation` | 숙박비 | number (콤마) | +| `meals` | 식비 | number (콤마) | +| `others` | 기타 | number (콤마) | +| (자동) | 합계 | number (합산) | + +### 13.4 Content JSON + +```json +{ + "pr_type": "pr_trip", + "write_date": "2026-03-06", + "department": "영업팀", + "writer_name": "홍길동", + "destination": "부산 해운대", + "trip_period_start": "2026-03-10", + "trip_period_end": "2026-03-11", + "trip_purpose": "거래처 방문 및 현장 점검", + "items": [ + { "date": "2026-03-10", "schedule": "거래처 미팅", "remark": "오전 10시" }, + { "date": "2026-03-11", "schedule": "현장 점검 및 복귀", "remark": "" } + ], + "expenses": { + "transport": 120000, + "accommodation": 80000, + "meals": 40000, + "others": 0 + }, + "total_amount": 240000, + "attachment_memo": "" +} +``` + +### 13.5 조회 화면 특수 구조 + +- **일정표**: 테이블 형태로 일자/일정/비고 렌더링 +- **경비 카드**: 교통비/숙박비/식비/기타 4개 항목 + 합계를 카드 그리드로 표시 + +--- + +## 14. 비용정산품의서 (pr_settlement) + +### 14.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `settlement_period_start` | 정산기간 시작 | date | 선택 | +| `settlement_period_end` | 정산기간 종료 | date | 선택 | +| `payment_method` | 지급방법 | radio | 필수 | + +### 14.2 지급방법 옵션 + +| 값 | 라벨 | +|----|------| +| `corporate_card` | 법인카드 사용 | +| `personal_advance` | 개인 선지출 (환급 요청) | + +### 14.3 내역 테이블 + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `date` | 사용일자 | date | +| `description` | 항목 | text | +| `amount` | 금액 | number (콤마) | +| `remark` | 비고 | text | + +### 14.4 Content JSON + +```json +{ + "pr_type": "pr_settlement", + "write_date": "2026-03-06", + "department": "개발팀", + "writer_name": "홍길동", + "settlement_period_start": "2026-02-01", + "settlement_period_end": "2026-02-28", + "payment_method": "personal_advance", + "items": [ + { "date": "2026-02-15", "description": "택시비", "amount": 25000, "remark": "야근 귀가" }, + { "date": "2026-02-20", "description": "회의 다과", "amount": 15000, "remark": "팀 미팅" } + ], + "total_amount": 40000, + "attachment_memo": "영수증 첨부" +} +``` + +### 14.5 조회 화면 특수 구조 + +- **지급방법 표시**: `corporate_card` → "법인카드 사용", `personal_advance` → "개인 선지출 (환급 요청)" +- 해당 라벨을 뱃지 형태로 표시 + +--- + +## 15. 결재 도장 테이블 (_approval-stamp-table.blade.php) + +### 15.1 구조 + +전통 한글 결재 양식의 도장 테이블을 구현한다. + +``` +┌──────┬────────┬────────┬────────┐ +│ │ 과장 │ 부장 │ 이사 │ ← 1행: 직급 헤더 +│ 결재 ├────────┼────────┼────────┤ +│ │ [승인] │ [대기] │ [대기] │ ← 2행: 서명/도장 영역 +│ ├────────┼────────┼────────┤ +│ │ 김과장 │ 박부장 │ 이이사 │ ← 3행: 이름 + 처리일 +│ │ 03/06 │ │ │ +└──────┴────────┴────────┴────────┘ +``` + +### 15.2 상태별 표시 + +| 상태 | approval_type | 표시 | 색상 | +|------|---------------|------|------| +| 승인 | `normal` | 빨간 원형 "승인" | `bg-red-500` | +| 전결 | `pre_decided` | 파란 원형 "전결" | `bg-blue-500` | +| 반려 | - | 빨간 원형 "반려" | `bg-red-500` | +| 보류 | - | 주황 원형 "보류" | `bg-amber-500` | +| 건너뜀 | - | 회색 "-" | `bg-gray-300` | + +--- + +## 16. 결재선 편집기 (_approval-line-editor.blade.php) + +### 16.1 2패널 구조 + +``` +┌─────────────────────┬─────────────────────┐ +│ 인원 목록 │ 결재선 │ +│ │ │ +│ [검색 input] │ [템플릿 선택 ▼] │ +│ │ │ +│ ▼ 개발팀 │ ① 김과장 (결재) [✗] │ +│ 홍길동 과장 [+] │ ② 박부장 (합의) [✗] │ +│ 김영희 대리 [+] │ ③ 이대리 (참조) [✗] │ +│ │ │ +│ ▼ 경영지원팀 │ (드래그로 순서 변경) │ +│ 박부장 부장 [+] │ │ +│ │ │ +├─────────────────────┴─────────────────────┤ +│ 결재: 1명 합의: 1명 참조: 1명 합계: 3명 │ +└───────────────────────────────────────────┘ +``` + +### 16.2 기능 + +| 기능 | 설명 | +|------|------| +| **인원 검색** | 이름/부서 실시간 검색 | +| **부서별 접기** | 부서 헤더 클릭으로 인원 접기/펼치기 | +| **드래그 정렬** | SortableJS로 결재선 순서 변경 | +| **유형 선택** | 각 단계별 approval/agreement/reference 선택 | +| **템플릿 로드** | 저장된 결재선 템플릿 드롭다운 | + +### 16.3 데이터 소스 + +``` +API: /api/admin/tenant-users/list + +응답: +[ + { + "department_id": 1, + "department_name": "개발팀", + "users": [ + { "id": 10, "name": "홍길동", "position": "과장", "job_title": "팀장" } + ] + } +] +``` + +### 16.4 Hidden Inputs (form 전송) + +```html + + + + +``` + +--- + +## 17. ApprovalForm 모델 + +### 17.1 테이블 스키마 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `name` | VARCHAR | 양식명 (예: "휴가신청서") | +| `code` | VARCHAR UNIQUE | 양식 코드 (예: `leave`) | +| `category` | ENUM | `request`, `expense`, `certificate`, `expense_estimate` | +| `template` | JSON | 필드 정의 메타데이터 | +| `body_template` | LONGTEXT NULL | HTML 본문 템플릿 | +| `is_active` | BOOLEAN | 활성 여부 | + +### 17.2 카테고리 + +#### DB 카테고리 (ApprovalForm.category) + +| 카테고리 | 설명 | 양식 코드 | +|---------|------|----------| +| `request` | 신청서 | `leave`, `attendance_request`, `reason_report` | +| `expense` | 지출결의서 | `expense` | +| `certificate` | 증명서/서류 | `employment_cert`, `career_cert`, `appointment_cert`, `resignation` | +| `expense_estimate` | 품의서 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` | + +#### UI 분류 (formCategoryMap — 2단계 선택용) + +| UI 분류 | 양식 코드 | +|---------|----------| +| 일반 | `BUSINESS_DRAFT` | +| 인사/근태 | `leave`, `attendance_request`, `reason_report`, `resignation` | +| 증명서 | `employment_cert`, `career_cert`, `appointment_cert` | +| 품의 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` | +| 재무 | `expense` | + +> **참고**: DB 카테고리와 UI 분류는 별도 매핑이다. DB는 `approval_forms.category` ENUM이고, UI 분류는 JavaScript `formCategoryMap` 객체로 정의된다. + +--- + +## 18. 양식별 저장/조회 흐름 + +### 18.1 저장 (create/update) + +``` +사용자 입력 + ↓ +getFormData() (JavaScript) + ↓ +POST /api/admin/approvals + body: { form_id, title, content: {...}, body, steps: [...] } + ↓ +ApprovalService::createApproval() + ↓ +Approval.content = JSON encode → DB 저장 +``` + +### 18.2 조회 (show) + +``` +GET /approval-mgmt/{id} + ↓ +ApprovalController::show() + ↓ +Blade: show.blade.php + ↓ +양식 코드별 분기: + leave → (본문에 인라인 표시) + expense → @include('_expense-show') + pr_* → @include('_purchase-request-show') ← str_starts_with 매칭 + employment_cert → @include('_certificate-show') + career_cert → @include('_career-cert-show') + appointment_cert → @include('_appointment-cert-show') + resignation → @include('_resignation-show') +``` + +> **품의서 분기**: `str_starts_with($approval->form?->code ?? '', 'pr_')` 조건으로 5종 모두 단일 include로 처리. `_purchase-request-show.blade.php` 내부에서 `$content['pr_type']`으로 세부 분기. + +--- + +## 관련 문서 + +- [README.md](README.md) — 결재관리 시스템 전체 개요 +- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름 +- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 +- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/approvals/ui-screens.md b/features/approvals/ui-screens.md new file mode 100644 index 0000000..f81b9ae --- /dev/null +++ b/features/approvals/ui-screens.md @@ -0,0 +1,381 @@ +# 결재관리 UI 화면 구성 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **기술**: Blade + HTMX + Alpine.js + Tailwind CSS +> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) + +--- + +## 1. 개요 + +결재관리 화면은 MNG(관리자 웹)에서 Blade 템플릿으로 구현되며, API 호출은 `fetch()`를 사용한다. + +### 1.1 파일 구조 + +``` +resources/views/approvals/ +├── drafts.blade.php ← 기안함 (목록) +├── pending.blade.php ← 결재 대기함 (목록) +├── completed.blade.php ← 처리 완료함 (목록) +├── references.blade.php ← 참조함 (목록) +├── create.blade.php ← 기안 작성 +├── edit.blade.php ← 기안 수정 +├── show.blade.php ← 상세 조회 + 결재 처리 +└── partials/ + ├── _status-badge.blade.php ← 상태 뱃지 컴포넌트 + └── _step-progress.blade.php ← 결재 단계 진행 표시 +``` + +--- + +## 2. 목록 화면 + +### 2.1 기안함 (`/approval-mgmt/drafts`) + +내가 기안한 모든 문서를 표시한다. + +**UI 구성:** + +``` +┌──────────────────────────────────────────────────────────┐ +│ 기안함 [+ 새 기안] │ +├──────────────────────────────────────────────────────────┤ +│ [검색] [상태 필터 ▼] [긴급만 □] [날짜 범위] │ +├──────────────────────────────────────────────────────────┤ +│ 문서번호 │ 제목 │ 양식 │ 상태 │ 기안일 │ +│ APR-260228-001│ 휴가 신청 │ 휴가서 │ 🟢완료 │ 02-28 │ +│ APR-260228-002│ 출장 보고 │ 출장서 │ 🔵진행 │ 02-28 │ +│ APR-260227-001│ 경비 청구 │ 경비서 │ ⬜임시 │ 02-27 │ +├──────────────────────────────────────────────────────────┤ +│ [◀ 이전] 1 / 3 [다음 ▶] │ +└──────────────────────────────────────────────────────────┘ +``` + +**상태 필터:** 전체, 임시저장, 진행, 완료, 반려, 회수, 보류 + +--- + +### 2.2 결재 대기함 (`/approval-mgmt/pending`) + +내가 현재 결재해야 할 문서를 표시한다. + +**UI 구성:** + +``` +┌──────────────────────────────────────────────────────────┐ +│ 결재 대기함 [뱃지: 3건] │ +├──────────────────────────────────────────────────────────┤ +│ 문서번호 │ 제목 │ 기안자 │ 양식 │ 상신일 │ +│ 🔴 APR-260..│ 긴급 승인 │ 홍길동 │ 구매서 │ 02-28 │ +│ APR-260..│ 휴가 신청 │ 김영희 │ 휴가서 │ 02-27 │ +└──────────────────────────────────────────────────────────┘ +``` + +> 긴급 문서는 🔴 아이콘과 함께 상단에 표시 + +--- + +### 2.3 참조함 (`/approval-mgmt/references`) + +내가 참조자로 지정된 문서를 표시한다. + +**UI 구성:** + +``` +┌──────────────────────────────────────────────────────────┐ +│ 참조함 │ +├──────────────────────────────────────────────────────────┤ +│ [전체] [미열람 (5)] [열람완료] │ +├──────────────────────────────────────────────────────────┤ +│ 문서번호 │ 제목 │ 기안자 │ 상태 │ 열람 │ +│ APR-260228-001│ 회의록 │ 박부장 │ 🟢완료 │ ❌미열람│ +│ APR-260227-003│ 인사발령 │ 이팀장 │ 🔵진행 │ ✅열람 │ +└──────────────────────────────────────────────────────────┘ +``` + +**열람 추적:** +- 문서 클릭 시 `mark-read` API가 자동 호출된다 +- 미열람/열람완료 탭으로 필터링 가능 +- 미열람 건수가 뱃지로 표시된다 + +--- + +## 3. 상세 화면 (`/approval-mgmt/{id}`) + +### 3.1 전체 레이아웃 + +``` +┌──────────────────────────────────────────────────────────┐ +│ 결재 상세 [수정] [목록으로] │ +│ APR-260228-001 │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ 상태: [🔵 진행] [🔴 긴급] │ +│ 양식: 휴가신청서 기안자: 홍길동 │ +│ 기안일: 2026-02-28 10:05 완료일: - │ +│ 원본 문서: APR-260225-003 (재기안 시 표시) │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ 회수 사유 (cancelled 상태에서만) │ │ +│ │ 내용 수정이 필요하여 회수합니다. │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ 제목: 2월 연차 사용 신청 │ +│ 본문: 2월 27일~28일 연차 사용합니다... │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ 결재 진행 │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ [결재 단계 프로그레스 바] │ │ +│ │ ✓김과장(승인) → ●박부장(대기) → ③이사(대기) │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ 결재 의견 │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ ✓ 김과장 2026-02-28 11:00 │ │ +│ │ 승인합니다. │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ 결재 처리 (현재 결재자에게만 표시) │ +│ [결재 의견 textarea] │ +│ [승인] [반려] [보류] [전결] │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ 보류 해제 (on_hold + 보류한 본인에게만) │ +│ [보류 해제] │ +├──────────────────────────────────────────────────────────┤ +│ 회수 (기안자 + pending/on_hold) │ +│ [회수 사유 textarea] │ +│ [결재 회수] │ +├──────────────────────────────────────────────────────────┤ +│ 복사 재기안 (기안자 + approved/rejected/cancelled) │ +│ [복사하여 재기안] │ +└──────────────────────────────────────────────────────────┘ +``` + +### 3.2 조건부 섹션 표시 + +| 섹션 | 표시 조건 | +|------|----------| +| **수정 버튼** | 기안자 + `draft`/`rejected` | +| **회수 사유** | `cancelled` + `recall_reason` 존재 | +| **원본 문서 링크** | `parent_doc_id` 존재 (재기안 문서) | +| **결재 처리** | `pending` + 현재 결재자 | +| **보류 해제** | `on_hold` + 보류한 본인 | +| **회수** | 기안자 + `pending`/`on_hold` | +| **복사 재기안** | 기안자 + `approved`/`rejected`/`cancelled` | + +--- + +## 4. 파셜 컴포넌트 + +### 4.1 상태 뱃지 (`_status-badge.blade.php`) + +문서 상태를 색상 뱃지로 표시한다. + +| 상태 | 라벨 | 스타일 | +|------|------|--------| +| `draft` | 임시저장 | `bg-gray-100 text-gray-700` | +| `pending` | 진행 | `bg-blue-100 text-blue-700` | +| `approved` | 완료 | `bg-green-100 text-green-700` | +| `rejected` | 반려 | `bg-red-100 text-red-700` | +| `cancelled` | 회수 | `bg-yellow-100 text-yellow-700` | +| `on_hold` | 보류 | `bg-amber-100 text-amber-700` | + +--- + +### 4.2 결재 단계 프로그레스 (`_step-progress.blade.php`) + +결재선의 각 단계를 가로 프로그레스 바로 표시한다. + +**단계 아이콘:** + +| 상태 | 아이콘 | 배경색 | 텍스트색 | +|------|--------|--------|---------| +| `approved` (normal) | ✓ | `bg-green-500` | white | +| `approved` (pre_decided) | ⚡ | `bg-indigo-500` | white | +| `rejected` | ✗ | `bg-red-500` | white | +| `on_hold` | ⏸ | `bg-amber-400` | white | +| `skipped` | — | `bg-gray-300` | gray | +| `pending` (현재 차례) | 번호 | `bg-blue-500` | white | +| `pending` (대기) | 번호 | `bg-gray-200` | gray | + +**레이아웃:** + +``` +┌──────────────────────────────────────────────────────┐ +│ │ +│ ✓ ──── ⚡ ──── — ──── — ──── ● ──── 3 │ +│ 김과장 박부장 이사장 팀장 최대리 참조자 │ +│ 경영팀 경영팀 대표실 개발팀 개발팀 인사팀 │ +│ (승인) (전결) (건너뜀)(건너뜀)(대기) (참조) │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +**특수 표시:** +- **전결** step: ⚡ 아이콘 + "전결" 라벨 (남색) +- **보류** step: ⏸ 아이콘 + "보류" 라벨 (노란색) +- **건너뜀** step: 이름에 취소선 (line-through) +- **참조** step: 별도 구분 없이 동일 프로그레스 바에 표시 +- **연결선**: 단계 사이 가로선 (`border-t-2`) + +--- + +## 5. 결재 처리 인터랙션 + +### 5.1 승인 + +``` +[승인 버튼 클릭] + → confirm("승인하시겠습니까?") + → POST /api/admin/approvals/{id}/approve + body: { comment: "의견 텍스트" } + → 성공 시: 토스트("승인되었습니다") + 페이지 리로드 +``` + +### 5.2 반려 + +``` +[반려 버튼 클릭] + → comment 빈 값 체크 → 경고 토스트("반려 시 사유를 입력해주세요") + → confirm("반려하시겠습니까?") + → POST /api/admin/approvals/{id}/reject + body: { comment: "사유" } + → 성공 시: 토스트("반려되었습니다") + 페이지 리로드 +``` + +### 5.3 보류 + +``` +[보류 버튼 클릭] + → comment 빈 값 체크 → 경고 토스트("보류 사유를 입력해주세요") + → confirm("이 결재를 보류하시겠습니까?") + → POST /api/admin/approvals/{id}/hold + body: { comment: "사유" } + → 성공 시: 토스트("보류되었습니다") + 페이지 리로드 +``` + +### 5.4 전결 + +``` +[전결 버튼 클릭] + → confirm("전결 처리하시겠습니까?\n이후 모든 결재를 건너뛰고 문서를 최종 승인합니다.") + → POST /api/admin/approvals/{id}/pre-decide + body: { comment: "의견(선택)" } + → 성공 시: 토스트("전결 처리되었습니다") + 페이지 리로드 +``` + +### 5.5 보류 해제 + +``` +[보류 해제 버튼 클릭] + → confirm("보류를 해제하시겠습니까?") + → POST /api/admin/approvals/{id}/release-hold + → 성공 시: 토스트("보류가 해제되었습니다") + 페이지 리로드 +``` + +### 5.6 회수 + +``` +[결재 회수 버튼 클릭] + → confirm("결재를 회수하시겠습니까? 이 작업은 되돌릴 수 없습니다.") + → POST /api/admin/approvals/{id}/cancel + body: { recall_reason: "사유(선택)" } + → 성공 시: 토스트("결재가 회수되었습니다") + 페이지 리로드 +``` + +### 5.7 복사 재기안 + +``` +[복사하여 재기안 버튼 클릭] + → confirm("이 문서를 복사하여 새 결재를 작성하시겠습니까?") + → POST /api/admin/approvals/{id}/copy + → 성공 시: 토스트("문서가 복사되었습니다") + → /approval-mgmt/{newId}/edit로 이동 +``` + +--- + +## 6. 결재 의견 표시 + +상세 페이지에서 결재 의견이 있는 step을 카드 형태로 표시한다. + +``` +┌──────────────────────────────────────┐ +│ ✓ 김과장 2026-02-28 11:00 │ +│ 승인합니다. │ +├──────────────────────────────────────┤ +│ ⚡ 박부장 (전결) 2026-02-28 14:00 │ +│ 전결 처리합니다. │ +├──────────────────────────────────────┤ +│ ⏸ 이사장 (보류) 2026-02-28 15:00 │ +│ 추가 자료 검토 필요 │ +├──────────────────────────────────────┤ +│ ✗ 팀장 2026-02-28 16:00 │ +│ 예산 초과로 반려합니다. │ +└──────────────────────────────────────┘ +``` + +**아이콘 색상:** +- ✓ 승인: 녹색 (`bg-green-100 text-green-600`) +- ⚡ 전결: 남색 (`bg-indigo-100 text-indigo-600`) +- ⏸ 보류: 노란색 (`bg-amber-100 text-amber-600`) +- ✗ 반려: 적색 (`bg-red-100 text-red-600`) + +--- + +## 7. 참조함 열람 추적 UI + +### 7.1 탭 필터 + +``` +[전체] [미열람 (5)] [열람완료] +``` + +- 탭 클릭 시 `is_read` 파라미터로 API 재호출 +- 미열람 탭에 건수 뱃지 표시 + +### 7.2 열람 상태 표시 + +| 상태 | 표시 | +|------|------| +| 미열람 | `bg-red-100 text-red-700` "미열람" | +| 열람완료 | `bg-green-100 text-green-700` "열람완료" | + +### 7.3 자동 열람 처리 + +문서 행 클릭 시: +1. `mark-read` API 호출 (비동기) +2. 상세 페이지로 이동 + +--- + +## 8. 버튼 스타일 가이드 + +| 버튼 | 색상 | Tailwind 클래스 | +|------|------|----------------| +| 승인 | 녹색 | `bg-green-600 hover:bg-green-700` | +| 반려 | 적색 | `bg-red-600 hover:bg-red-700` | +| 보류 | 노란색 | `bg-amber-500 hover:bg-amber-600` | +| 전결 | 남색 | `bg-indigo-600 hover:bg-indigo-700` | +| 보류 해제 | 노란색 | `bg-amber-500 hover:bg-amber-600` | +| 회수 | 노란색 | `bg-yellow-500 hover:bg-yellow-600` | +| 복사 재기안 | 회색 | `bg-gray-600 hover:bg-gray-700` | +| 수정 | 회색 | `bg-gray-600 hover:bg-gray-700` | + +--- + +## 관련 문서 + +- [README.md](README.md) — 시스템 전체 개요 +- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 +- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 + +--- + +**최종 업데이트**: 2026-02-28 diff --git a/features/approvals/workflows.md b/features/approvals/workflows.md new file mode 100644 index 0000000..202d525 --- /dev/null +++ b/features/approvals/workflows.md @@ -0,0 +1,565 @@ +# 결재관리 워크플로우 상세 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **관련**: [README.md](README.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md) + +--- + +## 1. 개요 + +이 문서는 결재관리 시스템의 각 동작(Action)에 대한 상세 워크플로우를 정의한다. +모든 워크플로우는 `ApprovalService`에서 트랜잭션으로 처리된다. + +### 1.1 용어 정의 + +| 용어 | 설명 | +|------|------| +| **기안자** | 결재 문서를 작성한 사람 (`drafter_id`) | +| **현재 결재자** | 결재선에서 현재 차례인 사람 (가장 작은 `step_order`의 `pending` step) | +| **결재자** | `step_type`이 `approval` 또는 `agreement`인 참여자 | +| **참조자** | `step_type`이 `reference`인 참여자 (의사결정 권한 없음) | +| **전결** | 현재 결재자가 이후 모든 결재를 건너뛰고 즉시 최종 승인 | + +--- + +## 2. 기안 작성 (createApproval) + +### 2.1 흐름 + +``` +사용자 → [양식 선택] → [제목/본문 입력] → [결재선 설정] → [임시저장] + │ + ▼ + 새 Approval 생성 + status = 'draft' + current_step = 0 +``` + +### 2.2 조건 + +- 모든 로그인 사용자가 작성 가능 +- `form_id` 필수 (양식 선택) +- 결재선(steps)은 저장 시 선택사항 (상신 시 필수) + +### 2.3 처리 로직 + +1. 문서번호 자동 채번 (`APR-YYMMDD-001` 형식) +2. `numbering_sequences` 테이블로 일일 순번 관리 +3. 결재선 설정 시 `approval_steps` 저장 + 사용자 정보 스냅샷 (이름, 부서, 직급) +4. `status = 'draft'`, `current_step = 0` + +--- + +## 3. 상신 (submit) + +### 3.1 흐름 + +``` +기안자 → [상신 버튼] → 유효성 검사 → 결재선 검사 → 상신 완료 + │ + ▼ + status = 'pending' + current_step = 1 + drafted_at = now() +``` + +### 3.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `draft` 또는 `rejected` | +| 결재선 | 결재/합의 step 1명 이상 필수 | +| 요청자 | 기안자만 | + +### 3.3 처리 로직 + +1. `isSubmittable()` 검증 → `draft` 또는 `rejected`인지 확인 +2. 결재/합의 step 존재 확인 +3. **반려 후 재상신인 경우**: 모든 step을 `pending`으로 초기화 (comment, acted_at도 초기화) +4. `status → pending`, `drafted_at → now()`, `current_step → 1` + +### 3.4 반려 후 재상신 + +``` +rejected 문서 + │ + ├── 기안자가 내용 수정 (updateApproval) + │ + └── 상신 (submit) + ├── 모든 steps → pending (초기화) + ├── status → pending + └── current_step → 1 (처음부터 다시) +``` + +> 반려 후 재상신 시 결재선이 초기화되므로, 이전 결재 의견(comment)은 사라진다. + +--- + +## 4. 승인 (approve) + +### 4.1 흐름 + +``` +현재 결재자 → [의견 입력(선택)] → [승인 버튼] + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'approved' │ + │ comment → (입력값) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ │ + 다음 pending step 있음 마지막 결재자 + │ │ + current_step 갱신 status → 'approved' + (다음 순서 결재자 대기) completed_at → now() +``` + +### 4.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` | +| 요청자 | 현재 차례 결재자 (`approver_id === auth()->id()`) | + +### 4.3 처리 로직 + +1. `isActionable()` 검증 → `pending` 상태인지 확인 +2. `getCurrentApproverStep()` → 현재 차례 step 조회 +3. 현재 step → `approved` + comment + acted_at +4. 다음 pending 결재/합의 step 조회 + - **있으면**: `current_step` 갱신 + - **없으면**: 문서 `approved` + `completed_at` + +### 4.4 순차결재 순서 결정 + +``` +step_order = 1 (결재) → step_order = 2 (합의) → step_order = 3 (결재) + │ │ │ + 1번째 승인 → 2번째 승인 → 3번째 승인 → 문서 완료 +``` + +> 결재와 합의는 동일한 순차 흐름을 따른다. `step_order` 순서대로 처리된다. + +--- + +## 5. 반려 (reject) + +### 5.1 흐름 + +``` +현재 결재자 → [반려 사유 입력(필수)] → [반려 버튼] + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'rejected' │ + │ comment → (사유) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ▼ + 문서 status → 'rejected' + completed_at → now() +``` + +### 5.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` | +| 요청자 | 현재 차례 결재자 | +| 반려 사유 | **필수** (빈 값 불가) | + +### 5.3 처리 로직 + +1. `isActionable()` 검증 +2. 현재 결재자 확인 +3. 반려 사유 빈 값 체크 +4. 현재 step → `rejected` + comment + acted_at +5. 문서 → `rejected` + completed_at + +### 5.4 반려 후 가능한 동작 + +``` +rejected 문서 + │ + ├── 기안자가 수정 → 재상신 (submit) + │ └── 결재선 초기화, 처음부터 다시 진행 + │ + └── 기안자가 복사 재기안 (copyForRedraft) + └── 새 문서 생성 (draft), 원본은 그대로 유지 +``` + +--- + +## 6. 회수 (cancel) + +### 6.1 흐름 + +``` +기안자 → [회수 사유 입력(선택)] → [회수 버튼] + │ + ┌──────────┴──────────┐ + │ 회수 가능 여부 판단 │ + │ (첫 결재자 미처리?) │ + └──────────┬──────────┘ + │ + ┌───────────┴───────────┐ + │ │ + 첫 결재자 첫 결재자 이미 + pending/on_hold 승인/반려 + │ │ + 회수 진행 회수 불가 + │ (에러 반환) + ▼ + 모든 pending/on_hold steps → 'skipped' + 문서 status → 'cancelled' + recall_reason → (입력값) + completed_at → now() +``` + +### 6.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` 또는 `on_hold` | +| 요청자 | 기안자만 (`drafter_id === auth()->id()`) | +| 첫 결재자 상태 | `pending` 또는 `on_hold` (이미 처리했으면 불가) | + +### 6.3 회수 가능 판단 로직 + +```php +// 1단계: 문서 상태 확인 +$approval->isCancellable() // pending 또는 on_hold + +// 2단계: 기안자 확인 +$approval->drafter_id === auth()->id() + +// 3단계: 첫 결재자 상태 확인 +$firstStep = steps.approvalOnly().orderBy('step_order').first() +$firstStep->status === 'pending' || 'on_hold' // 미처리 상태여야 함 +``` + +### 6.4 처리 로직 + +1. `isCancellable()` 검증 → `pending` 또는 `on_hold` +2. 기안자 확인 +3. 첫 번째 결재/합의 step의 상태 확인 → `pending`/`on_hold`이 아니면 거부 +4. 모든 `pending`/`on_hold` steps → `skipped` +5. 문서 → `cancelled` + `recall_reason` + `completed_at` + +--- + +## 7. 보류 (hold) + +### 7.1 흐름 + +``` +현재 결재자 → [보류 사유 입력(필수)] → [보류 버튼] + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'on_hold' │ + │ comment → (사유) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ▼ + 문서 status → 'on_hold' +``` + +### 7.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` (`isHoldable()`) | +| 요청자 | 현재 차례 결재자 | +| 보류 사유 | **필수** (빈 값 불가) | + +### 7.3 처리 로직 + +1. `isHoldable()` 검증 → `pending` 상태인지 확인 +2. `getCurrentApproverStep()` → 현재 차례 step 조회 +3. 현재 결재자 확인 (`approver_id === auth()->id()`) +4. 보류 사유 빈 값 체크 +5. 현재 step → `on_hold` + comment + acted_at +6. 문서 → `on_hold` + +### 7.4 보류 상태의 영향 + +``` +on_hold 상태에서: +├── 다른 결재자는 아무 동작 불가 (결재 흐름 중단) +├── 기안자는 회수 가능 (첫 결재자가 미처리 상태이면) +└── 보류한 결재자만 보류 해제 가능 +``` + +--- + +## 8. 보류 해제 (releaseHold) + +### 8.1 흐름 + +``` +보류한 결재자 → [보류 해제 버튼] + │ + ┌──────────┴──────────┐ + │ on_hold step │ + │ status → 'pending' │ + │ comment → null │ + │ acted_at → null │ + └──────────┬──────────┘ + │ + ▼ + 문서 status → 'pending' + (결재 흐름 재개) +``` + +### 8.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `on_hold` (`isHoldReleasable()`) | +| 요청자 | 보류한 본인만 (`on_hold` step의 `approver_id === auth()->id()`) | + +### 8.3 처리 로직 + +1. `isHoldReleasable()` 검증 → `on_hold` 상태인지 확인 +2. `on_hold` 상태인 step 조회 +3. 해당 step의 `approver_id`가 현재 사용자인지 확인 +4. step → `pending` + comment/acted_at 초기화 +5. 문서 → `pending` + +--- + +## 9. 전결 (preDecide) + +### 9.1 흐름 + +``` +현재 결재자 → [의견 입력(선택)] → [전결 버튼] → 확인 팝업 + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'approved' │ + │ approval_type → │ + │ 'pre_decided' │ + │ comment → (입력값) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ▼ + 이후 모든 pending + approval/agreement steps + → status = 'skipped' + │ + ▼ + 문서 status → 'approved' + completed_at → now() +``` + +### 9.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` (`isActionable()`) | +| 요청자 | 현재 차례 결재자 | + +### 9.3 처리 로직 + +1. `isActionable()` 검증 +2. `getCurrentApproverStep()` → 현재 차례 step 조회 +3. 현재 결재자 확인 +4. 현재 step → `approved` + `approval_type = 'pre_decided'` + comment + acted_at +5. 이후 모든 pending 결재/합의 steps → `skipped` +6. 문서 → `approved` + `completed_at` + +### 9.4 전결 예시 + +``` +step_order=1 (이사장, 결재) → approved (normal) +step_order=2 (부장, 결재) → approved (pre_decided) ← 여기서 전결 +step_order=3 (과장, 합의) → skipped (전결로 건너뜀) +step_order=4 (팀장, 결재) → skipped (전결로 건너뜀) +step_order=5 (참조자, 참조) → (참조는 영향 없음, 그대로 유지) + +문서 → approved, completed_at = now() +``` + +> 전결은 결재/합의 step만 건너뛴다. 참조 step은 영향받지 않는다. + +--- + +## 10. 복사 재기안 (copyForRedraft) + +### 10.1 흐름 + +``` +기안자 → [복사하여 재기안 버튼] + │ + ▼ + ┌─────────────────────────────┐ + │ 원본 문서에서 복사 │ + │ ├── form_id │ + │ ├── title │ + │ ├── content (양식 데이터) │ + │ ├── body │ + │ ├── is_urgent │ + │ ├── department_id │ + │ └── 결재선 (모두 pending) │ + └─────────────┬───────────────┘ + │ + ▼ + 새 문서 생성 (status = 'draft') + parent_doc_id = 원본.id + 새 문서번호 채번 + │ + ▼ + 수정 페이지로 이동 + (/approval-mgmt/{newId}/edit) +``` + +### 10.2 조건 + +| 조건 | 설명 | +|------|------| +| 원본 문서 상태 | `approved`, `rejected`, `cancelled` (`isCopyable()`) | +| 요청자 | 기안자만 (`drafter_id === auth()->id()`) | + +### 10.3 처리 로직 + +1. `isCopyable()` 검증 → `approved`/`rejected`/`cancelled` 중 하나 +2. 기안자 확인 +3. 새 문서 생성: + - 새 문서번호 채번 + - 원본의 양식, 제목, 내용, 본문, 긴급 여부, 부서 복사 + - `parent_doc_id = 원본.id` + - `status = 'draft'`, `current_step = 0` +4. 결재선 복사: 원본의 모든 steps를 새 문서에 복사 (모두 `pending` 상태) +5. 새 문서의 edit 페이지로 리다이렉트 + +### 10.4 원본과의 관계 + +``` +원본 문서 (approved/rejected/cancelled) + │ + └── parent_doc_id로 연결 + │ + ▼ + 새 문서 (draft) + ├── 상세 페이지에서 "원본 문서" 링크 표시 + └── 기안자가 내용 수정 후 상신 가능 +``` + +--- + +## 11. 참조 열람 추적 (markAsRead) + +### 11.1 흐름 + +``` +참조자 → [참조함 목록에서 문서 클릭] + │ + ├── markAsRead API 호출 + │ ├── is_read → true + │ └── read_at → now() + │ + └── 상세 페이지로 이동 +``` + +### 11.2 조건 + +| 조건 | 설명 | +|------|------| +| 요청자 | 해당 문서의 참조자 (`step_type = 'reference'`) | + +### 11.3 처리 로직 + +1. 현재 사용자의 참조 step 조회 +2. `is_read = false`인 step → `is_read = true`, `read_at = now()` +3. 이미 열람한 경우 중복 업데이트 없음 (`where('is_read', false)`) + +--- + +## 12. 전체 상태 전이 요약 + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ │ +│ draft ──submit()──→ pending ──approve()──→ (다음 step 또는) │ +│ ▲ │ │ approved │ +│ │ │ │ │ +│ │ │ ├──reject()──→ rejected │ +│ │ │ │ │ │ +│ │ │ │ ├── 수정 → submit() │ +│ │ │ │ │ (재상신, draft X) │ +│ │ │ │ │ │ +│ │ │ │ └── copyForRedraft() │ +│ │ │ │ → 새 draft 생성 │ +│ │ │ │ │ +│ │ │ ├──hold()──→ on_hold │ +│ │ │ │ │ │ +│ │ │ │ ├── releaseHold() │ +│ │ │ │ │ → pending 복원 │ +│ │ │ │ │ │ +│ │ │ │ └── cancel() (기안자) │ +│ │ │ │ → cancelled │ +│ │ │ │ │ +│ │ │ ├──preDecide()──→ approved │ +│ │ │ │ (이후 steps → skipped) │ +│ │ │ │ │ +│ │ │ └──cancel()──→ cancelled │ +│ │ │ (기안자, 첫결재자 미처리 시) │ +│ │ │ │ │ +│ │ │ └── copyForRedraft() │ +│ │ │ → 새 draft 생성 │ +│ │ │ │ +│ │ └── approved ──copyForRedraft() │ +│ │ → 새 draft 생성 │ +│ │ │ +│ └── updateApproval() (draft/rejected 상태에서 수정) │ +│ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 13. 에러 케이스 정리 + +| 동작 | 에러 조건 | 에러 메시지 | +|------|----------|------------| +| submit | 상태가 draft/rejected 아님 | "상신할 수 없는 상태입니다." | +| submit | 결재선 없음 | "결재선을 설정해주세요." | +| approve | 상태가 pending 아님 | "승인할 수 없는 상태입니다." | +| approve | 현재 결재자 아님 | "현재 결재자가 아닙니다." | +| reject | 상태가 pending 아님 | "반려할 수 없는 상태입니다." | +| reject | 사유 미입력 | "반려 사유를 입력해주세요." | +| cancel | 상태가 pending/on_hold 아님 | "회수할 수 없는 상태입니다." | +| cancel | 기안자 아님 | "기안자만 회수할 수 있습니다." | +| cancel | 첫 결재자 이미 처리 | "첫 번째 결재자가 이미 처리하여 회수할 수 없습니다." | +| hold | 상태가 pending 아님 | "보류할 수 없는 상태입니다." | +| hold | 현재 결재자 아님 | "현재 결재자가 아닙니다." | +| hold | 사유 미입력 | "보류 사유를 입력해주세요." | +| releaseHold | 상태가 on_hold 아님 | "보류 해제할 수 없는 상태입니다." | +| releaseHold | 보류한 본인 아님 | "보류한 결재자만 해제할 수 있습니다." | +| preDecide | 상태가 pending 아님 | "전결할 수 없는 상태입니다." | +| preDecide | 현재 결재자 아님 | "현재 결재자가 아닙니다." | +| copyForRedraft | 상태가 approved/rejected/cancelled 아님 | "복사할 수 없는 상태입니다." | +| copyForRedraft | 기안자 아님 | "기안자만 복사할 수 있습니다." | +| update | 상태가 draft/rejected 아님 | "수정할 수 없는 상태입니다." | +| delete | 상태가 draft 아님 | "삭제할 수 없는 상태입니다." | + +--- + +## 관련 문서 + +- [README.md](README.md) — 시스템 전체 개요 +- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 +- [UI 화면 구성](ui-screens.md) — 화면별 동작 + +--- + +**최종 업데이트**: 2026-02-28 diff --git a/features/barobill-kakaotalk/esign-notification-guide.md b/features/barobill-kakaotalk/esign-notification-guide.md new file mode 100644 index 0000000..dce2345 --- /dev/null +++ b/features/barobill-kakaotalk/esign-notification-guide.md @@ -0,0 +1,250 @@ +# 전자계약 알림톡/SMS 환경별 설정 가이드 + +> **작성일**: 2026-02-27 +> **상태**: 운영 중 +> **대상 프로젝트**: MNG + +--- + +## 1. 개요 + +### 1.1 목적 + +전자계약(E-Sign) 시스템의 카카오톡 알림톡, SMS, 이메일 발송을 **3개 환경(로컬/개발/운영)**에서 올바르게 설정하고 테스트하기 위한 가이드이다. + +### 1.2 핵심 원칙 + +- **역할 기반 알림**: 본사(creator)는 이메일, 상대방(counterpart)은 카카오톡/SMS +- **환경별 템플릿 분리**: 운영은 원본 템플릿, 개발은 `_DEV` 접미사 템플릿 사용 +- **URL 자동 분기**: `config('app.url')`로 환경별 도메인 자동 적용 + +--- + +## 2. 환경별 설정 + +### 2.1 도메인 및 APP_URL + +| 환경 | `APP_ENV` | `APP_URL` | 알림톡 버튼 URL 도메인 | +|------|-----------|-----------|----------------------| +| 로컬 (Docker) | `local` | `https://mng.sam.kr` | 로컬 — 알림톡 미사용 | +| 개발 서버 | `local` | `https://admin.codebridge-x.com` | `admin.codebridge-x.com` | +| 운영 서버 | `production` | `https://mng.codebridge-x.com` | `mng.codebridge-x.com` | + +### 2.2 바로빌 서버 모드 + +`barobill_members.server_mode` 컬럼으로 바로빌 API 엔드포인트를 결정한다: + +| server_mode | WSDL (카카오톡) | WSDL (SMS) | 용도 | +|-------------|----------------|------------|------| +| `test` | `testws.baroservice.com/KAKAOTALK.asmx` | `testws.baroservice.com/SMS.asmx` | 테스트 | +| `production` | `ws.baroservice.com/KAKAOTALK.asmx` | `ws.baroservice.com/SMS.asmx` | 실제 발송 | + +> `server_mode`는 환경(로컬/개발/운영)과 독립적이다. 개발서버에서도 `production` 모드로 실제 발송 가능. + +### 2.3 알림톡 템플릿 환경별 분기 + +코드에서 `resolveTemplateName()` 메서드가 `APP_ENV`에 따라 템플릿명을 자동 결정한다: + +```php +private function resolveTemplateName(string $baseName): string +{ + return $baseName . (app()->environment('production') ? '' : '_DEV'); +} +``` + +| 기본 템플릿명 | 운영 (`production`) | 개발/로컬 (기타) | +|-------------|--------------------|--------------------| +| `전자계약_서명요청` | `전자계약_서명요청` | `전자계약_서명요청_DEV` | +| `전자계약_완료` | `전자계약_완료` | `전자계약_완료_DEV` | +| `전자계약_리마인드` | `전자계약_리마인드` | `전자계약_리마인드_DEV` | + +--- + +## 3. 등록된 알림톡 템플릿 + +### 3.1 운영 템플릿 (mng.codebridge-x.com) + +| 템플릿명 | 용도 | 상태 | 버튼 URL | +|---------|------|------|---------| +| `전자계약_서명요청` | 서명 요청 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_완료` | 서명 완료 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_리마인드` | 서명 독촉 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | + +### 3.2 개발 템플릿 (admin.codebridge-x.com) + +| 템플릿명 | 용도 | 상태 | 버튼 URL | +|---------|------|------|---------| +| `전자계약_서명요청_DEV` | 서명 요청 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_완료_DEV` | 서명 완료 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_리마인드_DEV` | 서명 독촉 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | + +> 개발 템플릿 본문은 운영 템플릿과 동일하며, 버튼 URL 도메인만 다르다. + +### 3.3 템플릿 변수 + +| 변수 | 용도 | 사용 템플릿 | +|------|------|-----------| +| `#{이름}` | 서명자 이름 | 서명요청, 완료, 리마인드 | +| `#{계약명}` | 계약 제목 | 서명요청, 완료, 리마인드 | +| `#{기한}` | 서명 기한 | 서명요청, 리마인드 | +| `#{완료일}` | 계약 완료일 | 완료 | +| `#{토큰}` | 서명자 액세스 토큰 | 버튼 URL | + +--- + +## 4. 역할 기반 알림 흐름 + +### 4.1 전체 흐름 + +``` +① 계약 발송 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡 +② OTP 인증 ─→ 본사: 이메일 / 상대방: SMS +③ 다음 서명자 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡 +④ 서명 완료 ─→ 본사: 이메일(PDF) / 상대방: 카카오톡(PDF 다운로드) +``` + +### 4.2 역할 판별 + +```php +$isCounterpart = $signer->role === EsignSigner::ROLE_COUNTERPART; +``` + +| 역할 | 상수 | 알림톡 | SMS(OTP) | 이메일 | +|------|------|--------|----------|--------| +| 본사 (creator) | `ROLE_CREATOR` | ❌ | ❌ | ✅ 항상 | +| 상대방 (counterpart) | `ROLE_COUNTERPART` | ✅ 우선 | ✅ OTP만 | ✅ 폴백 | + +### 4.3 이메일 폴백 조건 + +상대방(counterpart)에게도 이메일을 보내는 경우: +- 전화번호가 없을 때 (`$signer->phone` 없음) +- 알림톡 발송 실패 시 (`$alimtalkFailed = true`) +- 발송 방식이 `email` 또는 `both`일 때 + +### 4.4 완료 알림 특수 처리 + +완료 알림톡 버튼은 **서명 페이지가 아닌 문서 다운로드 URL**로 강제 변경된다: + +```php +// sendCompletionAlimtalk() 내부 +$documentUrl = config('app.url') . '/esign/sign/' . $signer->access_token . '/api/document'; + +// 버튼 URL 강제 변경 (서명페이지 → 문서 다운로드) +if (str_contains($btn[$urlKey], '/esign/sign/') && !str_contains($btn[$urlKey], '/api/document')) { + $btn[$urlKey] = $documentUrl; +} +``` + +--- + +## 5. SMS (OTP 인증) + +### 5.1 발송 조건 + +상대방(counterpart)이 `alimtalk` 또는 `both` 발송 방식이고 전화번호가 있을 때 SMS로 OTP 발송: + +```php +if (in_array($sendMethod, ['alimtalk', 'both']) + && $signer->phone + && $signer->role === EsignSigner::ROLE_COUNTERPART) { + $this->sendOtpViaSms($contract, $signer, $otpCode); +} +``` + +### 5.2 SMS 발송 파라미터 + +| 항목 | 값 | +|------|-----| +| API | `BarobillService::sendSMSMessage()` | +| 발신번호 | `barobill_members.manager_hp` | +| 수신번호 | `esign_signers.phone` | +| 메시지 | `[SAM] 전자계약 인증코드: {코드} (5분 이내 입력)` | +| OTP 유효시간 | 5분 | +| 최대 시도 | 5회 | + +### 5.3 SMS 실패 시 이메일 폴백 + +SMS 발송 실패 → 이메일 OTP 폴백 → 이메일도 없으면 500 에러 반환. + +--- + +## 6. 바로빌 템플릿 등록 절차 + +### 6.1 관리자 페이지 + +``` +https://www.barobill.co.kr 로그인 → 카카오톡 → 템플릿관리 +``` + +### 6.2 DEV 템플릿 등록 시 주의사항 + +1. **본문**: 운영 템플릿과 **완전히 동일** (1글자도 다르면 안 됨) +2. **버튼 URL**: 도메인만 `admin.codebridge-x.com`으로 변경 +3. **템플릿명**: 운영 이름 + `_DEV` 접미사 (예: `전자계약_서명요청_DEV`) +4. **검수 기간**: 영업일 기준 2~3일 + +### 6.3 새 템플릿 추가 시 체크리스트 + +- [ ] 바로빌에서 운영용 + 개발용 2개 등록 +- [ ] 코드에서 `resolveTemplateName('기본명')`으로 호출 +- [ ] 본문의 변수 치환 로직 추가 (str_replace) +- [ ] 버튼 URL의 `#{토큰}` 치환 확인 +- [ ] 2단계 검증 (SendKey → GetSendKakaotalk) 포함 + +--- + +## 7. 관련 파일 + +| 파일 | 역할 | +|------|------| +| `app/Http/Controllers/ESign/EsignApiController.php` | 계약 발송, `sendAlimtalk()`, `resolveTemplateName()` | +| `app/Http/Controllers/ESign/EsignPublicController.php` | OTP SMS, 완료 알림톡, `sendCompletionAlimtalk()` | +| `app/Services/Barobill/BarobillService.php` | SOAP 클라이언트 (`sendATKakaotalkEx`, `sendSMSMessage`) | +| `app/Models/ESign/EsignSigner.php` | `ROLE_CREATOR`, `ROLE_COUNTERPART` 상수 | +| `app/Mail/EsignCompletedMail.php` | 완료 이메일 (PDF 다운로드 링크) | +| `app/Services/ESign/PdfSignatureService.php` | 서명 PDF 합성 (`mergeSignatures`) | + +--- + +## 8. 트러블슈팅 + +### 8.1 환경별 템플릿 미스매치 + +**증상**: `ResultCode=4` (템플릿 데이터 일치 오류) +**원인**: 개발서버에서 운영용 템플릿(`전자계약_서명요청`)으로 발송 시 버튼 URL 도메인 불일치 +**해결**: DEV 템플릿 등록 후 `APP_ENV`가 `production`이 아닌지 확인 + +### 8.2 서명 PDF 누락 (이메일) + +**증상**: 완료 이메일의 다운로드 링크가 서명 없는 초안 PDF 반환 +**원인**: `mergeSignatures()` 실패 → `signed_file_path` 미설정 → preview PDF 폴백 +**해결**: `downloadDocument()`가 완료 상태에서 자동 재생성 시도. 로그에서 trace 확인: + +```bash +# 개발서버 로그 확인 +ssh pro@114.203.209.83 "tail -100 /home/webservice/mng/storage/logs/laravel.log | grep 'PDF 서명'" +``` + +**주요 실패 원인**: +- `storage/fonts/Pretendard-Regular.ttf` 폰트 파일 누락 +- FPDI/TCPDF 패키지 미설치 → `composer install` 필요 +- `storage/app/esign/{tenant_id}/signed/` 디렉토리 권한 문제 + +### 8.3 MNG 모델 상수 누락 + +**증상**: `Undefined constant App\Models\ESign\EsignSigner::ROLE_COUNTERPART` +**원인**: API 프로젝트와 MNG 프로젝트의 모델이 독립적 — API에만 상수 정의됨 +**해결**: MNG `EsignSigner.php`에도 동일한 상수 추가 (2026-02-26 핫픽스 완료) + +--- + +## 관련 문서 + +- [바로빌 카카오톡 연동 README](./README.md) — SOAP API 전체 연동 가이드 +- [E-Sign 기술 설계](../../projects/e-sign/technical-design.md) — 전자계약 아키텍처 +- [E-Sign API 명세](../../projects/e-sign/api-specification.md) — API 엔드포인트 +- [알림톡 연동 계획](../../plans/esign-alimtalk-integration.md) — 초기 계획 (구현 완료) + +--- + +**최종 업데이트**: 2026-02-27 diff --git a/features/business-card-request.md b/features/business-card-request.md new file mode 100644 index 0000000..b574f00 --- /dev/null +++ b/features/business-card-request.md @@ -0,0 +1,173 @@ +# 명함신청 관리 + +> **작성일**: 2026-02-25 +> **상태**: 구현 완료 + +--- + +## 1. 개요 + +### 1.1 목적 + +영업파트너가 명함을 신청하면 본사에서 제작소에 의뢰하고, 완료 후 처리하는 3단계 워크플로우를 제공한다. + +### 1.2 워크플로우 + +``` +요청(pending) ──제작의뢰──→ 제작중(ordered) ──처리완료──→ 완료(processed) + 노랑 파랑 초록 +``` + +### 1.3 메뉴 구조 + +| 메뉴 | URL | 대상 | 설명 | +|------|-----|------|------| +| 파트너 명함신청 | `/sales/business-cards` | 모든 사용자 | 신청폼 + 내 이력 | +| 명함신청 처리 | `/sales/business-cards/manage` | 관리자 전용 | 3단계 처리 + 뱃지 | + +--- + +## 2. 테이블 구조 + +### 2.1 `business_card_requests` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `id` | bigint | PK | +| `tenant_id` | bigint | 테넌트 ID | +| `user_id` | bigint | 신청자 ID | +| `name` | varchar(50) | 성함 | +| `phone` | varchar(20) | 전화번호 | +| `title` | varchar(50) | 직함 (nullable) | +| `email` | varchar(100) | 이메일 (nullable) | +| `quantity` | int | 수량 (기본 100) | +| `memo` | text | 비고 (nullable) | +| `status` | varchar(20) | 상태: `pending`, `ordered`, `processed` | +| `ordered_by` | bigint | 제작의뢰 처리자 ID (nullable) | +| `ordered_at` | timestamp | 제작의뢰 일시 (nullable) | +| `processed_by` | bigint | 처리완료 처리자 ID (nullable) | +| `processed_at` | timestamp | 처리완료 일시 (nullable) | +| `process_memo` | text | 처리 메모 (nullable) | +| `created_at` | timestamp | 생성일 | +| `updated_at` | timestamp | 수정일 | + +**인덱스**: `(tenant_id, status)`, `user_id` + +--- + +## 3. 상태 전이 + +``` +pending ──→ ordered ──→ processed + │ ▲ + └── (역방향 전이 없음) ──┘ +``` + +| 상태 | 라벨 | 색상 | 설명 | +|------|------|------|------| +| `pending` | 요청 | 노랑 | 파트너가 신청, 관리자 확인 대기 | +| `ordered` | 제작의뢰 | 파랑 | 관리자가 제작소에 의뢰 | +| `processed` | 처리완료 | 초록 | 제작 완료, 전달 완료 | + +--- + +## 4. API 엔드포인트 + +| Method | Path | 이름 | 설명 | +|--------|------|------|------| +| GET | `/sales/business-cards` | `sales.business-cards.index` | 파트너 명함신청 (신청폼 + 이력) | +| POST | `/sales/business-cards` | `sales.business-cards.store` | 신청 등록 | +| GET | `/sales/business-cards/manage` | `sales.business-cards.manage` | 관리자 처리 화면 | +| POST | `/sales/business-cards/{id}/order` | `sales.business-cards.order` | 제작의뢰 (관리자) | +| POST | `/sales/business-cards/{id}/process` | `sales.business-cards.process` | 처리완료 (관리자) | + +--- + +## 5. 파일 구조 + +### 5.1 API 프로젝트 + +| 파일 | 설명 | +|------|------| +| `database/migrations/2026_02_24_100000_create_business_card_requests_table.php` | 테이블 생성 | +| `database/migrations/2026_02_25_100000_add_ordered_columns_to_business_card_requests_table.php` | ordered 컬럼 추가 | + +### 5.2 MNG 프로젝트 + +| 파일 | 설명 | +|------|------| +| `app/Models/Sales/BusinessCardRequest.php` | 모델 (상태 상수, 스코프, 헬퍼) | +| `app/Services/Sales/BusinessCardRequestService.php` | 서비스 (CRUD, 통계, 뱃지) | +| `app/Http/Controllers/Sales/BusinessCardRequestController.php` | 컨트롤러 | +| `app/Providers/ViewServiceProvider.php` | 사이드바 뱃지 연동 | +| `routes/web.php` | 라우트 5개 | +| `resources/views/sales/business-cards/admin-index.blade.php` | 관리자 뷰 | +| `resources/views/sales/business-cards/partner-index.blade.php` | 파트너 뷰 | + +--- + +## 6. 화면 구성 + +### 6.1 파트너 명함신청 (`partner-index`) + +``` +┌─ 회사 정보 안내 (코드브릿지엑스) ──────────────┐ +├─ 신청 폼 ─────────────────────────────────────┤ +│ 성함* │ 직함 │ 전화번호* │ 이메일 │ +│ 수량 │ 메모 │ [명함 신청하기] │ +├─ 내 신청 이력 ────────────────────────────────┤ +│ 신청일 │ 성함 │ 직함 │ 전화번호 │ 수량 │ 상태 │ +│ (요청=노랑, 제작중=파랑, 처리완료=초록) │ +└───────────────────────────────────────────────┘ +``` + +- 로그인 사용자 정보(name, phone, email)로 자동 채움 +- 관리자도 동일한 화면 접근 가능 + +### 6.2 명함신청 처리 (`admin-index`) + +``` +┌─ 통계 ──────────────────────────────────────┐ +│ 신규요청(노랑) │ 제작의뢰(파랑) │ 오늘처리(초록) │ 전체 │ +├─────────────────┬───────────────────────────┤ +│ 신규 요청 │ 제작 중 │ +│ [제작의뢰] 버튼 │ 의뢰일 + [처리완료] 버튼 │ +├─────────────────┴───────────────────────────┤ +│ 처리 완료 이력 (하단 스크롤 테이블) │ +└─────────────────────────────────────────────┘ +``` + +- 사이드바 뱃지: 요청 + 제작의뢰 합산 건수 표시 +- 처리 버튼 클릭 시 `showConfirm()` 확인 다이얼로그 + +--- + +## 7. 뱃지 연동 + +`ViewServiceProvider`에서 `BusinessCardRequestService::getPendingCount()`를 호출하여 사이드바 메뉴 뱃지에 대기 건수를 표시한다. + +- **카운트 기준**: `pending` + `ordered` 합산 +- **표시 위치**: "명함신청 처리" 메뉴 (`sales.business-cards.manage`) +- **0건일 때**: 뱃지 미표시 + +--- + +## 8. 메뉴 등록 정보 + +| ID | parent_id | 이름 | URL | sort_order | +|----|-----------|------|-----|------------| +| 15507 | 15456 | 파트너 명함신청 | `/sales/business-cards` | 5 | +| 15508 | 15456 | 명함신청 처리 | `/sales/business-cards/manage` | 6 | + +> 영업파트너에게는 "파트너 명함신청"만 보이도록 메뉴 권한 설정 필요 + +--- + +## 관련 문서 + +- 참고 패턴: `api/app/Models/CompanyRequest.php` (상태 관리 모델) +- 참고 뷰: `mng/resources/views/sales/managers/approvals.blade.php` (2분할 레이아웃) + +--- + +**최종 업데이트**: 2026-02-25 diff --git a/features/credit-evaluation/README.md b/features/credit-evaluation/README.md new file mode 100644 index 0000000..d4d38b3 --- /dev/null +++ b/features/credit-evaluation/README.md @@ -0,0 +1,284 @@ +# 신용평가 시스템 (쿠콘 연동) + +> **작성일**: 2026-03-02 +> **상태**: 운영중 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM에서 거래처/협력업체의 **기업 신용정보를 조회**하여, 거래 안전성을 사전 판단하는 시스템이다. + +### 1.2 핵심 원칙 + +- **쿠콘(KooCon/나이스평가정보)** API로 기업 신용정보 7개 항목 조회 +- **국세청 공공데이터포털** API로 사업자등록 상태(영업/휴업/폐업) 확인 +- 모든 조회 결과는 DB에 원본 저장 (감사 추적용) +- 테넌트별 월 5건 무료, 초과 시 건당 2,000원 과금 + +--- + +## 2. 시스템 구조 + +### 2.1 전체 흐름 + +``` +사용자 (SAM MNG) + │ + ▼ +CreditController::search() + │ + ├──▶ CooconService::getAllCreditInfo() + │ ├── OA08: 기업 기본정보 + │ ├── OA12: 신용요약정보 + │ ├── OA13: 단기연체정보 + │ ├── OA14: 신용도판단정보 (KCI) + │ ├── OA15: 신용도판단정보 (CB) + │ ├── OA16: 당좌거래정지정보 + │ └── OA17: 법정관리/워크아웃 + │ + ├──▶ NtsBusinessService::getBusinessStatus() + │ └── 국세청 사업자등록 상태 조회 + │ + └──▶ CreditInquiry::createFromApiResponse() + └── DB에 조회 이력 저장 +``` + +### 2.2 파트너 구조 + +| 역할 | 대상 | 설명 | +|------|------|------| +| **API 제공사** | 쿠콘(KooCon) / 나이스평가정보 | 기업 신용정보 API 플랫폼 | +| **파트너사** | (주)코드브릿지엑스 | API 키 보유, 쿠콘과 직접 계약 | +| **이용사** | 각 테넌트 (주일, 경동 등) | SAM을 통해 신용조회 실행 | + +--- + +## 3. 쿠콘(KooCon) API + +### 3.1 API 엔드포인트 + +| 환경 | URL | +|------|-----| +| 테스트 | `https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp` | +| 운영 | `https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp` | + +### 3.2 인증 방식 + +- **API_KEY**: 쿠콘에서 발급받은 인증키 (DB `coocon_configs` 테이블에서 관리) +- **API_ID**: 조회할 API 식별자 (OA08~OA17) +- **TR_SEQ**: 거래일련번호 (중복 방지용, `YmdHis` + 마이크로초 6자리) + +### 3.3 요청 형식 + +```json +{ + "API_KEY": "발급받은_API_키", + "API_ID": "OA12", + "TR_SEQ": "20260302173000123456", + "COMPANY_KEY": "1234567890" +} +``` + +- **Method**: POST +- **Content-Type**: application/json +- **Timeout**: 30초 + +### 3.4 API 목록 + +| API ID | 상수명 | 설명 | 데이터 출처 | +|--------|--------|------|------------| +| `OA08` | `API_COMPANY_INFO` | 기업 기본정보 | 나이스평가정보 | +| `OA12` | `API_CREDIT_SUMMARY` | 신용요약정보 (이슈 건수 요약) | 나이스평가정보 | +| `OA13` | `API_SHORT_TERM_OVERDUE` | 단기연체정보 | 한국신용정보원 | +| `OA14` | `API_NEGATIVE_INFO_KCI` | 신용도판단정보 (KCI) | 한국신용정보원 + 공공정보 | +| `OA15` | `API_NEGATIVE_INFO_CB` | 신용도판단정보 (CB) | 신용정보사 | +| `OA16` | `API_SUSPENSION_INFO` | 당좌거래정지정보 | 금융결제원 | +| `OA17` | `API_WORKOUT_INFO` | 법정관리/워크아웃정보 | 법원 | + +### 3.5 응답 형식 + +```json +{ + "RSLT_CD": "00000000", + "RSLT_MSG": "정상처리되었습니다.", + "RSLT_DATA": { ... } +} +``` + +- `RSLT_CD === '00000000'`: 성공 +- 기타 값: 에러 (에러 메시지는 `RSLT_MSG`에 포함) + +--- + +## 4. 국세청 사업자등록 조회 API + +### 4.1 API 정보 + +| 항목 | 값 | +|------|------| +| URL | `https://api.odcloud.kr/api/nts-businessman/v1/status` | +| 인증 | serviceKey (쿼리 파라미터) | +| 출처 | 공공데이터포털 | + +### 4.2 상태 코드 + +| 코드 | 상태 | 설명 | +|------|------|------| +| `01` | 계속사업자 | 정상 영업 중 | +| `02` | 휴업자 | 영업 중지 | +| `03` | 폐업자 | 사업 종료 | + +--- + +## 5. 데이터베이스 + +### 5.1 `coocon_configs` — API 설정 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `name` | VARCHAR(100) | 설정 이름 | +| `environment` | ENUM('test', 'production') | 환경 | +| `api_key` | VARCHAR(100) | 쿠콘 API 키 | +| `base_url` | VARCHAR(255) | API 기본 URL | +| `description` | TEXT | 설명 | +| `is_active` | BOOLEAN | 활성화 여부 | + +> **규칙**: 환경당 1개만 활성화 가능. 새 설정 활성화 시 기존 설정은 자동 비활성화. + +### 5.2 `credit_inquiries` — 조회 이력 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 | +| `inquiry_key` | VARCHAR(32) UNIQUE | 조회 고유키 | +| `company_key` | VARCHAR(20) | 사업자번호/법인번호 | +| `company_name` | VARCHAR | 업체명 | +| `user_id` | BIGINT FK | 조회자 | +| `inquired_at` | TIMESTAMP | 조회 일시 | +| `nts_status` | VARCHAR(20) | 국세청 상태 | +| `nts_status_code` | VARCHAR(2) | 국세청 상태코드 | +| `short_term_overdue_cnt` | UINT | 단기연체 건수 | +| `negative_info_kci_cnt` | UINT | KCI 건수 | +| `negative_info_pb_cnt` | UINT | 공공정보 건수 | +| `negative_info_cb_cnt` | UINT | CB 건수 | +| `suspension_info_cnt` | UINT | 당좌거래정지 건수 | +| `workout_cnt` | UINT | 법정관리/워크아웃 건수 | +| `raw_*` | JSON | 각 API 원본 응답 (7개 + NTS) | +| `status` | ENUM | success / partial / failed | + +--- + +## 6. 과금 정책 + +| 항목 | 값 | +|------|------| +| 월 무료 할당량 | **5건** | +| 초과 건당 요금 | **2,000원** | +| 계산식 | `max(0, (조회건수 - 5)) × 2,000` | + +### 요금 예시 + +| 월 조회 건수 | 무료 | 유료 | 요금 | +|-------------|------|------|------| +| 3건 | 3 | 0 | 0원 | +| 5건 | 5 | 0 | 0원 | +| 10건 | 5 | 5 | 10,000원 | +| 20건 | 5 | 15 | 30,000원 | + +--- + +## 7. 환경 설정 + +### 7.1 테스트/운영 분리 + +| 환경 | API URL | 설명 | +|------|---------|------| +| 테스트 | `dev2.coocon.co.kr:8443` | 개발/검증용 (과금 없음) | +| 운영 | `sgw.coocon.co.kr` | 실 서비스 (과금 발생) | + +- `coocon_configs` 테이블에서 환경별로 별도 설정 관리 +- 각 환경에서 `is_active=true`인 설정 1개만 사용 + +### 7.2 필요한 설정 + +| 항목 | 관리 위치 | 설명 | +|------|----------|------| +| 쿠콘 API 키 | DB (`coocon_configs`) | 쿠콘에서 발급 | +| 쿠콘 API URL | DB (`coocon_configs`) | 환경별 URL | +| 국세청 API 키 | 코드 내 하드코딩 | 공공데이터포털 발급 | + +--- + +## 8. MNG 라우트 + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/credit/inquiry` | 조회 이력 목록 | +| POST | `/credit/inquiry/search` | 신용정보 조회 실행 | +| POST | `/credit/inquiry/test` | API 연결 테스트 | +| GET | `/credit/inquiry/{key}/raw` | 원본 데이터 조회 | +| GET | `/credit/inquiry/{key}/report` | 리포트 조회 | +| DELETE | `/credit/inquiry/{id}` | 이력 삭제 | +| GET | `/credit/usage` | 조회회수 집계 | +| GET | `/credit/settings` | 설정 관리 | +| POST | `/credit/settings` | 설정 생성 | +| PUT | `/credit/settings/{id}` | 설정 수정 | +| DELETE | `/credit/settings/{id}` | 설정 삭제 | +| POST | `/credit/settings/{id}/toggle` | 활성화 토글 | + +--- + +## 9. 에러 코드 + +### 9.1 쿠콘 API + +| 코드 | 설명 | +|------|------| +| `NO_CONFIG` | API 설정 없음 | +| `HTTP_ERROR` | HTTP 통신 오류 | +| `EXCEPTION` | 예외 발생 | +| `RSLT_CD ≠ 00000000` | 쿠콘 API 에러 (RSLT_MSG 참조) | + +### 9.2 국세청 API + +| 코드 | 설명 | +|------|------| +| `INVALID_FORMAT` | 사업자번호 형식 오류 | +| `NOT_FOUND` | 조회 결과 없음 | +| `HTTP_ERROR` | HTTP 통신 오류 | + +--- + +## 10. 관련 파일 + +### MNG 프로젝트 + +| 구분 | 경로 | +|------|------| +| 컨트롤러 | `app/Http/Controllers/Credit/CreditController.php` | +| 컨트롤러 | `app/Http/Controllers/Credit/CreditUsageController.php` | +| 서비스 | `app/Services/Coocon/CooconService.php` | +| 서비스 | `app/Services/Nts/NtsBusinessService.php` | +| 모델 | `app/Models/Coocon/CooconConfig.php` | +| 모델 | `app/Models/Credit/CreditInquiry.php` | +| 뷰 | `resources/views/credit/inquiry/index.blade.php` | +| 뷰 | `resources/views/credit/usage/index.blade.php` | +| 뷰 | `resources/views/credit/settings/index.blade.php` | + +### API 프로젝트 (마이그레이션) + +| 경로 | +|------| +| `database/migrations/2026_01_22_192637_create_coocon_configs_table.php` | +| `database/migrations/2026_01_22_201143_create_credit_inquiries_table.php` | +| `database/migrations/2026_01_22_203001_add_company_info_to_credit_inquiries_table.php` | +| `database/migrations/2026_01_28_163000_add_tenant_id_to_credit_inquiries_table.php` | + +--- + +**최종 업데이트**: 2026-03-02 diff --git a/features/documents/mng-document-system.md b/features/documents/mng-document-system.md new file mode 100644 index 0000000..eae95a6 --- /dev/null +++ b/features/documents/mng-document-system.md @@ -0,0 +1,738 @@ +# MNG 문서관리 시스템 상세 기술 명세 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **프로젝트**: SAM MNG (관리자 웹) +> **관련**: [README.md](README.md) (API 명세) + +--- + +## 1. 개요 + +### 1.1 목적 + +블라인드/스크린 제조 현장의 **검사 성적서, 작업일지, 수입검사 기록** 등 품질/생산 문서를 전자화하여 관리하는 시스템. 문서 양식(Template)을 정의하면 EAV 패턴으로 데이터를 동적 저장하며, 다단계 결재 워크플로우를 지원한다. + +### 1.2 핵심 특징 + +| 특징 | 설명 | +|------|------| +| **EAV 패턴** | 양식별로 다른 필드를 하나의 `document_data` 테이블에 저장 | +| **2가지 양식 빌더** | 레거시 빌더 (DB 정규화) + 블록 빌더 (A4 JSON 스키마) | +| **결재 워크플로우** | 작성 → 검토 → 승인 (다단계 순차 결재) | +| **자동 데이터 매핑** | 작업지시서/수주 데이터에서 기본필드 자동 채움 | +| **다형성 연결** | work_order, sales_order 등 다양한 모델과 연결 | +| **자재 LOT 추적** | 검사 문서에서 투입 자재의 LOT 이력 조회 | + +### 1.3 문서 구조 + +| 문서 | 설명 | +|------|------| +| [README.md](README.md) | API 엔드포인트, 모델 요약, FormRequest | +| **이 문서** | MNG 화면별 상세, 동작원리, 데이터 흐름 | + +--- + +## 2. 메뉴/탭 구조 + +``` +생산 관리 +└── 문서관리 + ├── 문서 목록 /documents ← 문서 검색/필터/관리 + ├── 새 문서 작성 /documents/create ← 템플릿 선택 → 폼 입력 + ├── 문서 상세 /documents/{id} ← 읽기 전용 + 결재 현황 + ├── 문서 수정 /documents/{id}/edit ← DRAFT/REJECTED만 + ├── 인쇄 /documents/{id}/print ← 성적서 인쇄용 + │ + └── 문서양식 관리 + ├── 양식 목록 /document-templates ← 양식 검색/관리 + ├── 새 양식 (레거시) /document-templates/create ← 레거시 빌더 + ├── 양식 수정 /document-templates/{id}/edit ← 자동 빌더 판별 + ├── 양식 디자이너 /document-templates/block-create ← 블록 빌더 + └── 블록 수정 /document-templates/{id}/block-edit ← 블록 빌더 수정 +``` + +--- + +## 3. 파일 구조 + +``` +mng/ +├── app/Http/Controllers/ +│ ├── DocumentController.php ← 문서 CRUD 화면 +│ └── DocumentTemplateController.php ← 양식 관리 화면 +├── app/Models/Documents/ +│ ├── Document.php ← 문서 모델 +│ ├── DocumentApproval.php ← 결재 단계 +│ ├── DocumentData.php ← EAV 데이터 +│ ├── DocumentTemplate.php ← 양식 마스터 +│ └── ... (기타 템플릿 관련 모델) +└── resources/views/ + ├── documents/ + │ ├── index.blade.php ← 문서 목록 + │ ├── edit.blade.php ← 문서 작성/수정 + │ ├── show.blade.php ← 문서 상세 + │ └── print.blade.php ← 인쇄 전용 + └── document-templates/ + ├── index.blade.php ← 양식 목록 + ├── edit.blade.php ← 레거시 빌더 + ├── block-editor.blade.php ← 블록 빌더 + └── partials/ + ├── block-palette.blade.php ← 블록 타입 목록 + ├── block-canvas.blade.php ← 편집 캔버스 + └── block-properties.blade.php ← 속성 패널 +``` + +--- + +## 4. 데이터베이스 아키텍처 + +### 4.1 테이블 관계도 + +``` +document_templates (양식 마스터) +├── 1:N → document_template_approval_lines (결재선 정의) +├── 1:N → document_template_basic_fields (기본필드 정의) +├── 1:N → document_template_sections (섹션 정의) +│ └── 1:N → document_template_section_items (검사항목) +├── 1:N → document_template_columns (테이블 컬럼 정의) +├── 1:N → document_template_section_fields (섹션 필드) +├── 1:N → document_template_links (외부 연결 정의) +│ └── 1:N → document_template_link_values (템플릿 레벨 연결값) +│ +└── 1:N → documents (문서 인스턴스) + ├── 1:N → document_approvals (결재 진행) + ├── 1:N → document_data (EAV 필드값) + ├── 1:N → document_attachments (첨부파일) + └── 1:N → document_links (문서 레벨 연결) +``` + +### 4.2 documents (문서) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `template_id` | BIGINT FK | 사용 양식 | +| `document_no` | VARCHAR UNIQUE | 문서번호 (자동 채번) | +| `title` | VARCHAR | 문서 제목 | +| `status` | VARCHAR(20) | 상태 (5가지) | +| `linkable_type` | VARCHAR NULL | 다형성 모델 타입 | +| `linkable_id` | BIGINT NULL | 다형성 모델 ID | +| `submitted_at` | TIMESTAMP NULL | 결재 요청 일시 | +| `completed_at` | TIMESTAMP NULL | 결재 완료 일시 | +| `created_by` | BIGINT FK | 작성자 | +| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | + +**인덱스**: `(tenant_id, status)`, `document_no`, `(linkable_type, linkable_id)` + +### 4.3 document_data (EAV 필드값) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `document_id` | BIGINT FK | 소속 문서 | +| `section_id` | BIGINT FK NULL | 소속 섹션 (NULL=기본필드) | +| `column_id` | BIGINT FK NULL | 소속 컬럼 (테이블 데이터용) | +| `row_index` | INT | 테이블 행 번호 (기본: 0) | +| `field_key` | VARCHAR | 필드 식별자 (`bf_1`, `cf_2`, `col_3`) | +| `field_value` | TEXT NULL | 실제 값 | + +**인덱스**: `(document_id, section_id)`, `(document_id, field_key)` + +### 4.4 document_approvals (결재) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `document_id` | BIGINT FK | 소속 문서 | +| `user_id` | BIGINT FK | 결재자 | +| `step` | INT | 결재 순서 (1, 2, 3...) | +| `role` | VARCHAR | 역할 (작성, 검토, 승인) | +| `status` | VARCHAR(20) | PENDING / APPROVED / REJECTED | +| `comment` | TEXT NULL | 결재 의견 | +| `acted_at` | TIMESTAMP NULL | 처리 일시 | + +**인덱스**: `(document_id, step)`, `(user_id, status)` + +### 4.5 document_attachments (첨부파일) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `document_id` | BIGINT FK | 소속 문서 | +| `file_id` | BIGINT FK | File 모델 연결 | +| `attachment_type` | VARCHAR | `general`, `signature`, `image`, `reference` | +| `description` | VARCHAR NULL | 설명 | +| `created_by` | BIGINT FK | 업로드자 | + +--- + +## 5. 양식(Template) 시스템 + +### 5.1 두 가지 빌더 방식 + +| 방식 | 필드명 | 저장 구조 | UI | 상태 | +|------|--------|----------|-----|------| +| **레거시 빌더** | `builder_type = null` | 정규화 테이블들 | `edit.blade.php` | 기존 양식용 | +| **블록 빌더** | `builder_type = 'block'` | `schema` JSON | `block-editor.blade.php` | 신규 양식용 | + +**자동 판별 로직:** + +```php +// DocumentTemplateController::edit() +if ($template->isBlockBuilder()) { + return $this->blockEdit($id); // block-editor.blade.php +} else { + return view('document-templates.edit'); // 레거시 +} +``` + +### 5.2 양식 마스터 (document_templates) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `name` | VARCHAR | 양식명 (예: "제품검사 성적서") | +| `category` | VARCHAR | 분류 (common_codes 기반) | +| `title` | VARCHAR NULL | 문서 제목 템플릿 | +| `company_name` | VARCHAR NULL | 회사명 | +| `company_address` | VARCHAR NULL | 회사 주소 | +| `company_contact` | VARCHAR NULL | 연락처 | +| `footer_remark_label` | VARCHAR NULL | 비고란 라벨 | +| `footer_judgement_label` | VARCHAR NULL | 판정란 라벨 | +| `footer_judgement_options` | JSON NULL | 판정 선택지 (적합/부적합) | +| `builder_type` | VARCHAR NULL | `block` 또는 NULL | +| `schema` | JSON NULL | 블록 빌더 JSON 스키마 | +| `page_config` | JSON NULL | 페이지 설정 (A4, 여백 등) | +| `is_active` | BOOLEAN | 활성 여부 | + +### 5.3 레거시 빌더 구성 요소 + +#### 결재선 (document_template_approval_lines) + +``` +step 1: 작성 (작성자 본인) +step 2: 검토 (팀장) +step 3: 승인 (부장) +``` + +| 컬럼 | 설명 | +|------|------| +| `name` | 라벨 (작성, 검토, 승인) | +| `dept` | 부서 | +| `role` | 역할 | +| `sort_order` | 순서 | + +#### 기본필드 (document_template_basic_fields) + +문서 상단의 고정 필드 영역. + +| 컬럼 | 설명 | +|------|------| +| `label` | 필드 라벨 (품명, LOT NO, 납기일 등) | +| `field_key` | 식별자 (EAV 저장 시 사용) | +| `field_type` | 입력 타입 (text, date, number, item_search) | +| `default_value` | 기본값 | +| `sort_order` | 순서 | + +**EAV 저장 시 field_key 패턴:** + +``` +bf_1 → 기본필드 ID 1 (예: 품명) +bf_2 → 기본필드 ID 2 (예: LOT NO) +bf_3 → 기본필드 ID 3 (예: 납기일) +``` + +#### 섹션 (document_template_sections) + +검사 기준서의 섹션 단위. + +| 컬럼 | 설명 | +|------|------| +| `title` | 섹션 제목 (예: "겉모양 검사", "치수 검사") | +| `image_path` | 도해 이미지 경로 (검사 부위 도면) | +| `sort_order` | 순서 | + +#### 검사항목 (document_template_section_items) + +각 섹션 내의 개별 검사항목. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `category` | VARCHAR | 구분 (겉모양, 치수, 재질) | +| `item` | VARCHAR | 검사항목명 | +| `standard` | VARCHAR | 검사기준 (100mm ±5mm) | +| `tolerance` | JSON NULL | 허용오차 (min/max) | +| `standard_criteria` | VARCHAR NULL | 판정기준 | +| `method` | VARCHAR | 검사방법 (육안, 측정) | +| `measurement_type` | VARCHAR NULL | 측정 유형 | +| `frequency_n` | INT NULL | 검사건수 N | +| `frequency_c` | INT NULL | 합격건수 C | +| `frequency` | VARCHAR NULL | 검사빈도 텍스트 | +| `field_values` | JSON NULL | 확장 필드 (마이그레이션 없이 추가) | + +#### 테이블 컬럼 (document_template_columns) + +검사 데이터 테이블의 컬럼 정의. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `label` | VARCHAR | 컬럼 라벨 | +| `width` | INT NULL | 너비 (px) | +| `column_type` | VARCHAR | `text`, `check`, `complex`, `measurement`, `select` | +| `group_name` | VARCHAR NULL | 상단 병합 헤더명 | +| `sub_labels` | JSON NULL | complex 타입 하위 라벨 | +| `sort_order` | INT | 순서 | + +**컬럼 타입 상세:** + +| 타입 | 설명 | 예시 | +|------|------|------| +| `text` | 단순 텍스트 입력 | 비고, 메모 | +| `check` | 체크박스 (합격/부적합) | 외관 검사 합격 여부 | +| `complex` | 여러 서브필드 조합 | 측정값 + 단위 + 판정 | +| `measurement` | 수치 입력 | 길이: 100.5mm | +| `select` | 드롭다운 선택 | 판정: 합격/불합격/보류 | + +#### 외부 연결 (document_template_links) + +템플릿에서 외부 테이블 데이터를 참조하기 위한 정의. + +| 컬럼 | 설명 | +|------|------| +| `link_key` | 연결 식별자 | +| `label` | 화면 라벨 | +| `link_type` | `single` (1개 선택) / `multiple` (다중 선택) | +| `source_table` | 소스 테이블 (`items`, `processes`, `users`) | +| `search_params` | API 검색 추가 조건 (JSON) | +| `display_fields` | 표시 필드 (title, subtitle) | +| `is_required` | 필수 여부 | + +### 5.4 블록 빌더 구조 + +**페이지 설정 (page_config):** + +```json +{ + "size": "A4", + "orientation": "portrait", + "margin": { + "top": 20, + "right": 15, + "bottom": 20, + "left": 15 + } +} +``` + +**스키마 (schema):** + +블록 배열로 레이아웃 정의. 드래그앤드롭으로 편집. + +```json +{ + "blocks": [ + { "type": "text", "x": 0, "y": 0, "width": 100, "content": "검사 성적서" }, + { "type": "table", "x": 0, "y": 50, "columns": [...], "rows": [...] }, + { "type": "image", "x": 200, "y": 100, "src": "..." } + ] +} +``` + +**블록 빌더 UI (3패널):** + +``` +┌──────────┬────────────────────┬──────────┐ +│ 블록 │ │ 속성 │ +│ 팔레트 │ A4 캔버스 │ 패널 │ +│ │ │ │ +│ [텍스트] │ ┌──────────────┐ │ 너비: _ │ +│ [이미지] │ │ 드래그앤드롭 │ │ 높이: _ │ +│ [표] │ │ 블록 배치 │ │ 색상: _ │ +│ [선] │ │ │ │ 폰트: _ │ +│ [도형] │ └──────────────┘ │ │ +└──────────┴────────────────────┴──────────┘ +``` + +--- + +## 6. EAV 데이터 저장 패턴 + +### 6.1 핵심 개념 + +하나의 `document_data` 테이블에 **모든 양식의 모든 필드값**을 저장. 양식이 다르면 field_key가 다르고, 같은 양식이라도 섹션/행이 다르면 section_id/row_index로 구분. + +### 6.2 저장 구조 + +``` +document_data 레코드 예시: + +기본필드 (상단 고정 영역): +┌─────────────┬────────────┬───────────┬───────────┬───────────┬─────────────┐ +│ document_id │ section_id │ column_id │ row_index │ field_key │ field_value │ +├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤ +│ 42 │ NULL │ NULL │ 0 │ bf_1 │ 블라인드A │ ← 품명 +│ 42 │ NULL │ NULL │ 0 │ bf_2 │ LOT-2026-001│ ← LOT NO +│ 42 │ NULL │ NULL │ 0 │ bf_3 │ 2026-03-15 │ ← 납기일 +├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤ + +테이블 데이터 (섹션별 검사 결과): +│ 42 │ 10 │ 20 │ 0 │ col_20 │ 합격 │ ← 섹션10, 컬럼20, 1행 +│ 42 │ 10 │ 20 │ 1 │ col_20 │ 부적합 │ ← 섹션10, 컬럼20, 2행 +│ 42 │ 10 │ 21 │ 0 │ col_21 │ 100.5 │ ← 섹션10, 컬럼21, 1행 +└─────────────┴────────────┴───────────┴───────────┴───────────┴─────────────┘ +``` + +### 6.3 field_key 네이밍 규칙 + +| 접두사 | 의미 | 예시 | +|--------|------|------| +| `bf_` | 기본필드 (BasicField) | `bf_1`, `bf_2` | +| `cf_` | 섹션필드 (SectionField) | `cf_5`, `cf_6` | +| `col_` | 컬럼 데이터 | `col_20`, `col_21` | + +### 6.4 데이터 조회 패턴 + +```php +// 기본필드 값 조회 +$data = DocumentData::where('document_id', $id) + ->whereNull('section_id') + ->get() + ->keyBy('field_key'); + +$productName = $data['bf_1']->field_value; + +// 섹션별 테이블 데이터 조회 +$rows = DocumentData::where('document_id', $id) + ->where('section_id', $sectionId) + ->get() + ->groupBy('row_index'); +``` + +--- + +## 7. 결재 워크플로우 + +### 7.1 상태 전이 + +``` +DRAFT (작성중) + │ + ├── submit() → PENDING (결재중) + │ │ + │ ├── approve() [step 1] → 다음 step 대기 + │ ├── approve() [step 2] → 다음 step 대기 + │ ├── approve() [마지막] → APPROVED (승인) + │ │ + │ └── reject() → REJECTED (반려) + │ │ + │ └── edit → submit() → PENDING (재요청) + │ + └── cancel() → CANCELLED (취소) +``` + +### 7.2 상태값 및 라벨 + +| 코드 | 라벨 | 색상 | 편집 가능 | +|------|------|------|----------| +| `DRAFT` | 작성중 | gray | 예 | +| `PENDING` | 결재중 | yellow | 아니오 | +| `APPROVED` | 승인 | green | 아니오 | +| `REJECTED` | 반려 | red | 예 (수정 후 재요청) | +| `CANCELLED` | 취소 | gray | 아니오 | + +### 7.3 결재 단계 (Approval) + +``` +DocumentTemplateApprovalLine (양식 정의) + ↓ (문서 생성 시 복사) +DocumentApproval (문서별 결재 레코드) + +step 1: 작성 → PENDING → 결재자 승인 → APPROVED +step 2: 검토 → PENDING → 결재자 승인 → APPROVED +step 3: 승인 → PENDING → 결재자 승인 → APPROVED → 문서 전체 APPROVED +``` + +### 7.4 결재 판단 메서드 + +```php +// Document 모델 +canEdit() // DRAFT 또는 REJECTED +canSubmit() // DRAFT 또는 REJECTED +canApprove() // PENDING (현재 결재자만) +canCancel() // DRAFT 또는 PENDING (작성자만) +``` + +--- + +## 8. 자동 데이터 매핑 + +### 8.1 개요 + +문서 작성/수정 시, 연결된 작업지시서(work_order)/수주(order) 데이터에서 기본필드를 **자동으로 채움**. 사용자 입력 부담을 줄이고 데이터 정확성을 보장. + +### 8.2 검사 성적서 매핑 (field_key 기반) + +| field_key | 라벨 | 소스 | +|-----------|------|------| +| `product_name` | 품명 | `workOrderItem.item_name` | +| `specification` | 규격 | `workOrderItem.specification` | +| `lot_no` | LOT NO | `order.order_no` | +| `lot_size` | LOT 크기 | `"N 개소"` (개소 수 기반) | +| `client` | 발주처 | `order.client_name` | +| `site_name` | 현장명 | `workOrder.project_name` | +| `inspection_date` | 검사일 | `workOrderItem.options.inspection_data.inspected_at` | +| `inspector` | 검사자 | 검사자 이름 | + +### 8.3 작업일지 매핑 (label 기반) + +| label 포함 문자열 | 소스 | +|------------------|------| +| `발주처` | `order.client_name` | +| `현장명` | `workOrder.project_name` | +| `작업일자` | `now()` | +| `LOT NO`, `LOT` | `order.order_no` | +| `납기일`, `납기` | `order.delivery_date` | +| `작업지시번호` | `workOrder.work_order_no` | +| `수주일` | `order.received_at` 또는 `order.created_at` | + +### 8.4 자동 매핑 흐름 + +``` +문서 작성/수정 페이지 로드 + ↓ +DocumentController::edit() + ↓ +resolveAndBackfillBasicFields($template, $document) + ↓ +linkable_type 확인 (work_order? order?) + ↓ +field_key 또는 label 매칭 + ↓ +DB에 값이 없으면 → 소스 데이터에서 resolve + ↓ +뷰에 자동 채움된 값 전달 +``` + +--- + +## 9. 자재 LOT 추적 + +### 9.1 개요 + +검사 성적서에서 해당 작업지시의 **투입 자재 LOT 이력**을 조회. `stock_transactions` 테이블의 OUT(투입)/IN(취소) 트랜잭션을 상쇄하여 순수 투입량을 계산. + +### 9.2 추적 구조 + +``` +work_orders (작업지시) + │ + ├── stock_transactions (재고 트랜잭션) + │ ├── OUT (투입): qty < 0 + │ └── IN (취소/반납): qty > 0 + │ → 순수 투입량 = ABS(SUM(qty)) where qty < 0 + │ + └── work_order_material_inputs (개소별 투입자재) + └── stock_lots (LOT 정보) JOIN +``` + +### 9.3 표시 내용 + +| 항목 | 설명 | +|------|------| +| 자재명 | 투입된 원자재/부자재 이름 | +| LOT 번호 | 자재의 LOT 식별 번호 | +| 투입 수량 | OUT 트랜잭션 합계 (절대값) | +| 투입일 | 트랜잭션 일시 | + +--- + +## 10. 화면별 상세 + +### 10.1 문서 목록 (/documents) + +**필터 항목:** + +| 필터 | 타입 | 설명 | +|------|------|------| +| 검색 | text | 문서번호 또는 제목 | +| 상태 | dropdown | DRAFT, PENDING, APPROVED, REJECTED, CANCELLED, 휴지통(admin) | +| 양식분류 | dropdown | category | +| 템플릿 | dropdown | template_id | +| 날짜 범위 | date | created_at (from ~ to) | + +**목록 테이블 컬럼:** + +``` +문서번호 | 제목 | 양식 | 상태 | 작성자 | 작성일 | 결재현황 +``` + +### 10.2 문서 작성/수정 (/documents/create, /documents/{id}/edit) + +**폼 구성:** + +``` +┌──────────────────────────────────────────────┐ +│ 템플릿 선택 (읽기전용) │ +│ 제목 (필수) │ +├──────────────────────────────────────────────┤ +│ 기본 필드 (template.basicFields) │ +│ ┌─────────────────┬─────────────────┐ │ +│ │ 품명: [자동채움] │ LOT NO: [자동] │ │ +│ │ 납기일: [날짜] │ 발주처: [자동] │ │ +│ └─────────────────┴─────────────────┘ │ +├──────────────────────────────────────────────┤ +│ 섹션 1: 겉모양 검사 │ +│ ┌──────────────────────────────────────┐ │ +│ │ 도해 이미지 (있으면) │ │ +│ ├──────┬──────┬──────┬──────┬──────┤ │ +│ │ 구분 │ 항목 │ 기준 │ 결과1│ 결과2│ │ +│ ├──────┼──────┼──────┼──────┼──────┤ │ +│ │ 치수 │ 길이 │±5mm │ [ ] │ [ ] │ │ +│ │ 외관 │ 흠집 │ 없음 │ [✓] │ [✓] │ │ +│ ├──────┴──────┴──────┴──────┴──────┤ │ +│ │ [+ 행 추가] [행 삭제] │ │ +│ └──────────────────────────────────────┘ │ +├──────────────────────────────────────────────┤ +│ 외부 연결 (template.links) │ +│ 품목 선택: [검색 드롭다운] │ +├──────────────────────────────────────────────┤ +│ 첨부파일 │ +│ [일반 문서] [서명 이미지] [검사 사진] [참고 자료] │ +├──────────────────────────────────────────────┤ +│ [임시저장] [결재 요청] │ +└──────────────────────────────────────────────┘ +``` + +### 10.3 문서 상세 (/documents/{id}) + +**읽기 전용 표시:** + +``` +┌──────────────────────────────────────────────┐ +│ 문서번호: DOC-260306-001 상태: [🟢 승인] │ +│ 제목: 블라인드A 검사 성적서 │ +├──────────────────────────────────────────────┤ +│ 기본 필드 (읽기 전용) │ +├──────────────────────────────────────────────┤ +│ 검사 데이터 테이블 (읽기 전용) │ +├──────────────────────────────────────────────┤ +│ 결재 현황 │ +│ ┌────────┬────────┬────────┐ │ +│ │ 작성 │ 검토 │ 승인 │ │ +│ │ 홍길동 │ 김과장 │ 박부장 │ │ +│ │ ✓승인 │ ✓승인 │ ●대기 │ │ +│ └────────┴────────┴────────┘ │ +├──────────────────────────────────────────────┤ +│ 자재 투입 LOT (작업지시 연결 시) │ +│ ┌────────┬──────────┬──────┬──────┐ │ +│ │ 자재명 │ LOT 번호 │ 수량 │ 투입일│ │ +│ └────────┴──────────┴──────┴──────┘ │ +├──────────────────────────────────────────────┤ +│ 첨부파일 목록 │ +├──────────────────────────────────────────────┤ +│ [수정] [인쇄] [결재 승인] [결재 반려] │ +└──────────────────────────────────────────────┘ +``` + +### 10.4 인쇄 (/documents/{id}/print) + +성적서 형식의 인쇄 전용 화면. `window.print()` 호출. 작업지시 관련 자재(work_order_items) 데이터 포함. + +### 10.5 양식 목록 (/document-templates) + +**필터:** +- 검색: 양식명, 제목, 분류 +- 카테고리: common_codes 기반 + 기존 데이터 폴백 +- 활성 상태: 활성 / 비활성 / 휴지통(admin) + +**HTMX**: 필터 변경 시 테이블 영역만 부분 로드 + +--- + +## 11. 첨부파일 유형 + +| 유형 | 코드 | 용도 | 예시 | +|------|------|------|------| +| 일반 문서 | `general` | PDF, 엑셀 등 | 규격서, 보고서 | +| 서명 이미지 | `signature` | 검사 완료 서명 | 검사자 서명 사진 | +| 검사 사진 | `image` | 검사 증빙 사진 | 불량 부위 촬영 | +| 참고 자료 | `reference` | 참고용 문서 | KS 규격, 작업 지침 | + +--- + +## 12. API 연동 (MNG → API) + +MNG 뷰에서 데이터 저장/삭제는 **API 서버를 호출**하여 처리. GET 요청(뷰 렌더링)은 MNG 컨트롤러가 직접 처리. + +| 작업 | MNG (GET 요청) | API (POST/PUT/DELETE) | +|------|---------------|----------------------| +| 목록 조회 | `DocumentController::index()` | `GET /v1/documents` | +| 상세 조회 | `DocumentController::show()` | `GET /v1/documents/{id}` | +| 생성 | 폼 표시만 | `POST /v1/documents` | +| 수정 | 폼 표시만 | `PATCH /v1/documents/{id}` | +| 삭제 | - | `DELETE /v1/documents/{id}` | +| 결재 요청 | - | `POST /v1/documents/{id}/submit` | +| 승인 | - | `POST /v1/documents/{id}/approve` | +| 반려 | - | `POST /v1/documents/{id}/reject` | + +--- + +## 13. 카테고리 해결 로직 + +양식 카테고리는 **common_codes 테이블**에서 조회하되, 없으면 **기존 데이터에서 추출**하여 폴백. + +```php +// DocumentTemplateController::getCategories() +$categories = CommonCode::where('group', 'document_category') + ->orderBy('sort_order') + ->get(); + +if ($categories->isEmpty()) { + // 폴백: 기존 템플릿의 category 값에서 중복 제거 + $categories = DocumentTemplate::distinct('category') + ->pluck('category') + ->filter(); +} +``` + +--- + +## 14. 검사항목 확장 (field_values JSON) + +`document_template_section_items.field_values` JSON 컬럼으로 마이그레이션 없이 새 필드를 추가할 수 있다. + +```json +{ + "custom_field_1": "추가 기준값", + "min_value": 95.0, + "max_value": 105.0, + "unit": "mm" +} +``` + +> options JSON 컬럼 정책(`docs/standards/options-column-policy.md`) 준용 + +--- + +## 15. HTMX 전체 페이지 로드 규칙 + +문서관리 페이지들은 JavaScript를 사용하므로 HTMX 부분 로드 시 스크립트 미실행 문제가 있다. 컨트롤러에서 HX-Request 감지 시 **HX-Redirect로 전체 페이지 리로드 강제**. + +```php +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('documents.index')); +} +``` + +--- + +## 관련 문서 + +- [README.md](README.md) — API 엔드포인트, 모델 요약, FormRequest +- [DB 스키마 — 문서/전자서명](../../system/database/documents.md) — 테이블 상세 +- [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 참고 +- [결재관리 시스템](../approvals/README.md) — 별도 결재 시스템 (문서관리와 독립) + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/documents/mng-document-template.md b/features/documents/mng-document-template.md new file mode 100644 index 0000000..5570865 --- /dev/null +++ b/features/documents/mng-document-template.md @@ -0,0 +1,826 @@ +# MNG 문서양식관리 (Document Template Management) + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **라우트**: `/document-templates` +> **관련**: [README.md](README.md) | [MNG 문서관리](mng-document-system.md) + +--- + +## 1. 개요 + +문서관리 시스템에서 사용하는 **서식(Template)**을 생성, 편집, 복제, 관리하는 기능. 검사 성적서, 작업지시서 등 다양한 문서 양식을 정의하며, 2가지 빌더 타입을 지원한다. + +| 빌더 | builder_type | UI 명칭 | 설명 | +|------|-------------|---------|------| +| **Legacy Builder** | `legacy` 또는 null | 새 양식 | 탭 기반 폼 UI (순수 JavaScript) | +| **Block Builder** | `block` | 양식 디자이너 | WYSIWYG 캔버스 편집기 (Alpine.js + SortableJS) | + +> **명칭 변경 이력**: Block Builder의 UI 표시 명칭이 '블록 빌더' → '양식 디자이너'로 변경됨 (2026-02-28) + +**핵심 기능:** +- 결재선, 기본필드, 검사 기준서, 테이블 컬럼 정의 +- EAV 데이터 구조의 서식 스키마 관리 +- 양식 복제 (연결품목 제외) +- 프리셋 자동 제안 (카테고리별) +- 소프트 삭제 + 휴지통 관리 (슈퍼어드민) + +--- + +## 2. 라우트 + +### 2.1 웹 라우트 (페이지) + +``` +GET /document-templates → index (목록) +GET /document-templates/create → create (Legacy 신규 생성) +GET /document-templates/block-create → blockCreate (양식 디자이너 신규 생성) +GET /document-templates/{id}/edit → edit (Legacy 편집) +GET /document-templates/{id}/block-edit → blockEdit (양식 디자이너 편집) +``` + +### 2.2 API 라우트 (CRUD + 기능) + +``` +Prefix: /api/admin/document-templates (HQ 관리자 전용) + +GET / → index (HTMX 테이블) +POST / → store (생성) +GET /{id} → show (상세 조회) +PUT /{id} → update (수정) +DELETE /{id} → destroy (소프트 삭제) +DELETE /{id}/force → forceDestroy (영구삭제, 슈퍼어드민) +POST /{id}/restore → restore (복원, 슈퍼어드민) +POST /{id}/toggle-active → toggleActive (활성 토글) +POST /{id}/duplicate → duplicate (복제) +POST /upload-image → uploadImage (이미지 업로드) +GET /admin/common-codes/{group} → getCommonCodes (공통코드 조회) +``` + +--- + +## 3. 모델 구조 + +### 3.1 모델 관계도 + +``` +DocumentTemplate (서식 마스터) +├── 1:N DocumentTemplateApprovalLine (결재선) +├── 1:N DocumentTemplateBasicField (기본필드) +├── 1:N DocumentTemplateSection (섹션/기준서) +│ └── 1:N DocumentTemplateSectionItem (섹션 항목) +├── 1:N DocumentTemplateSectionField (섹션 필드) +├── 1:N DocumentTemplateColumn (테이블 컬럼) +└── 1:N DocumentTemplateLink (연결 설정) + └── 1:N DocumentTemplateLinkValue (연결 값) +``` + +### 3.2 DocumentTemplate 핵심 필드 + +```php +// 기본 정보 +builder_type // 'legacy' | 'block' +name // 양식명 +category // 분류명 +title // 문서 제목 + +// 회사 정보 +company_name // 회사명 +company_address // 회사 주소 +company_contact // 회사 연락처 + +// 하단 설정 +footer_remark_label // 비고 라벨 +footer_judgement_label // 판정 라벨 +footer_judgement_options // array - 판정 선택지 + +// Block Builder 전용 +schema // array - 블록 스키마 (JSON) +page_config // array - 페이지 설정 (A4/A3, 여백 등) + +// 연결 (레거시) +linked_item_ids // array - 연결 품목 ID 목록 +linked_process_id // int - 연결 공정 ID + +// 상태 +is_active // boolean - 활성 여부 +deleted_at // timestamp - 소프트 삭제 +deleted_by // int - 삭제자 +``` + +**Helper 메서드:** + +```php +isBlockBuilder(): bool // builder_type === 'block' +isLegacyBuilder(): bool // builder_type !== 'block' +``` + +### 3.3 DocumentTemplateApprovalLine (결재선) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `name` | string | 결재자 이름/직책 | +| `department` | string | 부서 | +| `role` | string | 역할 (작성/검토/승인) | +| `sort_order` | int | 순서 | + +### 3.4 DocumentTemplateBasicField (기본필드) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `field_key` | string | 필드 키 (bf_ 접두사) | +| `label` | string | 라벨 | +| `field_type` | string | text, date, select 등 | +| `default_value` | string | 기본값 | +| `is_required` | boolean | 필수 여부 | +| `sort_order` | int | 순서 | +| `options` | array | 선택지 (select 타입) | + +### 3.5 DocumentTemplateSection (섹션/검사 기준서) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `title` | string | 섹션 제목 | +| `image_path` | string | 섹션 이미지 경로 | +| `sort_order` | int | 순서 | + +**하위 관계:** + +``` +Section 1:N SectionItem + ├── category // 카테고리 (그룹핑) + ├── name // 항목명 + ├── standard // 기준 + ├── tolerance_type // 공차 유형 (symmetric/asymmetric/range/limit) + ├── tolerance_plus // +공차 + ├── tolerance_minus // -공차 + ├── reference_value // 기준값 + ├── method // 검사방법 + ├── measurement_type // 측정유형 + └── frequency // 검사주기 +``` + +### 3.6 DocumentTemplateColumn (테이블 컬럼) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `label` | string | 컬럼 라벨 | +| `group_name` | string | 그룹명 (다단계 "/" 구분) | +| `width` | int | 컬럼 너비 | +| `column_type` | string | text, check, complex, select, measurement | +| `sub_labels` | array | complex 타입 하위 라벨 | +| `sort_order` | int | 순서 | + +### 3.7 DocumentTemplateLink (연결 설정) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `link_key` | string | 연결 키 | +| `label` | string | 라벨 | +| `link_type` | string | `single` / `multiple` | +| `source_table` | string | `items` / `processes` / `users` | +| `search_params` | array | 검색 파라미터 | +| `display_fields` | array | 표시 필드 | +| `is_required` | boolean | 필수 여부 | +| `sort_order` | int | 순서 | + +**하위 관계:** + +``` +Link 1:N LinkValue + ├── link_id // FK → Link + ├── linkable_id // 연결 엔티티 ID + └── (source_table에 따라 items/processes/users 참조) +``` + +**레거시 호환 처리:** + +```php +// 신규 links가 있으면 사용 +if ($template->links->isNotEmpty()) { + // template_links + link_values 사용 +} + +// 레거시만 있으면 가상 엔트리 생성 +if (!empty($template->linked_item_ids)) { + return [['link_key' => 'items', 'values' => [...]]] +} +``` + +--- + +## 4. 컨트롤러 상세 + +### 4.1 DocumentTemplateController (웹) + +| 메서드 | 동작 | +|--------|------| +| `index()` | HTMX 요청 → HX-Redirect 반환 (전체 페이지 로드 강제) | +| `create()` | Legacy 신규 생성 폼 렌더링 | +| `edit($id)` | Legacy 편집. 양식 디자이너 타입이면 `blockEdit`으로 자동 리다이렉트 | +| `blockCreate()` | 양식 디자이너 신규 생성 (빈 캔버스) | +| `blockEdit($id)` | 양식 디자이너 편집 (스키마 로드) | + +**공통 데이터 준비:** + +```php +// 현재 테넌트 조회 +$tenantId = getCurrentTenant(); // 세션의 selected_tenant_id + +// 카테고리 목록 = common_codes + 기존 템플릿 카테고리 +$categories = getCategories(); + +// 기본필드 키 옵션 +$basicFieldKeys = getBasicFieldKeys(); // common_codes 'doc_template_basic_field' +``` + +### 4.2 DocumentTemplateApiController (API) + +#### `index()` — HTMX 테이블 조회 + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 양식명/분류 검색 | +| `category` | string | 분류 필터 | +| `is_active` | string | `1` / `0` / `TRASHED` (휴지통) | + +```php +// 휴지통 모드 (슈퍼어드민 전용) +if ($isActive === 'TRASHED') { + $query->onlyTrashed(); +} +``` + +#### `store()` / `update()` — 생성/수정 + +``` +요청 데이터 + ↓ +검증 (직접 validate, FormRequest 미사용) + ↓ +연결품목 중복 검증 (checkLinkedItemDuplicates) + ↓ +DB::transaction 시작 + ↓ +Template 생성/수정 + ↓ +saveRelations() — 관계 데이터 upsert + ↓ +DB::transaction 완료 + ↓ +JSON 응답 +``` + +#### `duplicate()` — 양식 복제 + +```php +$source = DocumentTemplate::with([...all relationships...]); + +$newTemplate = DocumentTemplate::create([ + ...원본 데이터, + 'name' => request('name', '원본 (복사)'), + 'is_active' => false, // 비활성으로 생성 + 'linked_item_ids' => null, // 연결품목 제외 + 'linked_process_id' => null, // 연결공정 제외 +]); + +// 각 관계 데이터 복사 (approvalLines, basicFields, sections, columns...) +// linkValues는 복사 안 함 (동일 분류 내 중복 방지) +``` + +#### `forceDestroy()` — 영구삭제 + +```php +// 사전 검사: 참조하는 문서 존재 여부 +$documentCount = Document::withTrashed() + ->where('template_id', $id) + ->count(); + +if ($documentCount > 0) { + return 422; // "이 양식을 사용한 문서 {count}건이 있어 삭제 불가" +} +``` + +#### `uploadImage()` — 이미지 업로드 + +``` +요청 (multipart) + ↓ +ApiTokenService::exchangeToken($userId, $tenantId) + ↓ +API /files/upload 호출 (Bearer 토큰) + ↓ +응답: file_path (1/temp/2026/02/xxx.jpg) + ↓ +최종 URL: http://api.sam.kr/storage/tenants/{file_path} +``` + +--- + +## 5. 저장 메커니즘 (saveRelations) + +### 5.1 upsert 전략 + +| 관계 | 방식 | 이유 | +|------|------|------| +| approvalLines | 전체 삭제 → 재생성 | ID 참조 없음 | +| basicFields | 전체 삭제 → 재생성 | ID 참조 없음 | +| **sections** | **ID 보존 upsert** | document_data가 section_id 참조 | +| **sectionItems** | **ID 보존 upsert** | section 하위 항목 | +| **columns** | **ID 보존 upsert** | document_data가 column_id 참조 | +| sectionFields | 전체 삭제 → 재생성 | ID 참조 없음 | +| links + linkValues | 전체 삭제 → 재생성 | ID 참조 없음 | + +### 5.2 ID 보존 upsert 로직 + +```php +// 1. 요청 ID 수집 +$incomingIds = collect($data['sections'])->pluck('id')->filter(); + +// 2. 요청에 없는 항목 삭제 +$template->sections() + ->whereNotIn('id', $incomingIds) + ->each(function($s) { + $s->items()->delete(); + $s->delete(); + }); + +// 3. 각 항목 upsert +foreach ($data['sections'] as $section) { + if (!empty($section['id']) && $existing = $template->sections()->find($section['id'])) { + $existing->update($sectionData); // 기존: update + } else { + DocumentTemplateSection::create([...]); // 신규: create + } +} +``` + +> **ID 보존이 필수인 이유**: `document_data` 테이블이 `section_id`, `column_id`를 FK로 참조한다. 양식 수정 시 ID가 변경되면 기존 문서 데이터와의 매핑이 깨진다. + +--- + +## 6. 화면 구성 + +### 6.1 목록 화면 (`index.blade.php`) + +``` +┌─────────────────────────────────────────────────┐ +│ 문서양식관리 │ +│ [+ 새 양식] [+ 양식 디자이너] │ +├─────────────────────────────────────────────────┤ +│ 필터: [검색어] [분류 ▼] [활성/비활성/휴지통 ▼] │ +├─────────────────────────────────────────────────┤ +│ # │ 양식명 │ 분류 │ 활성 │ 수정일 │ 액션 │ +│ 1 │ FQC... │ 검사 │ ✅ │ 03-06 │ 편집 복제 삭제 │ +│ 2 │ 수입... │ 검사 │ ✅ │ 03-05 │ 편집 복제 삭제 │ +│ ...│ │ │ │ │ │ +└─────────────────────────────────────────────────┘ +``` + +**HTMX 테이블 로드:** + +```html +

+
+``` + +**액션 버튼:** +- **편집**: 새 양식 → `/document-templates/{id}/edit`, 양식 디자이너 → `/document-templates/{id}/block-edit` +- **복제**: `duplicateTemplate(id)` — 이름 입력 모달 후 POST +- **삭제**: `confirmDelete(id)` — 확인 후 DELETE +- **미리보기**: `previewTemplate(id)` — 모달 표시 +- **활성 토글**: `toggleActive(id)` — POST toggle-active +- **복원/영구삭제**: 휴지통 모드에서만 표시 (슈퍼어드민) + +### 6.2 Legacy Builder 편집 화면 (`edit.blade.php`) + +**4개 탭 구조:** + +``` +┌─────────────────────────────────────────────────────┐ +│ [기본정보] [기본필드] [검사 기준서] [테이블 컬럼] │ +├─────────────────────────────────────────────────────┤ +│ │ +│ (각 탭 콘텐츠) │ +│ │ +├─────────────────────────────────────────────────────┤ +│ [미리보기] [저장] [취소] │ +└─────────────────────────────────────────────────────┘ +``` + +#### 탭 1: 기본정보 + +| 필드 | 설명 | +|------|------| +| 양식명 | 서식 이름 (필수) | +| 제목 | 문서 제목 | +| 분류 | 카테고리 (common_codes + 기존값) | +| 회사명 | 문서 헤더 회사명 | +| 회사 주소/연락처 | 문서 헤더 | +| 활성 | 체크박스 | +| 결재선 | 동적 행 추가/삭제 (이름, 부서, 역할) | + +#### 탭 2: 기본필드 + +| 항목 | 설명 | +|------|------| +| 필드 키 | `bf_` 접두사 (common_codes에서 선택) | +| 라벨 | 표시 라벨 | +| 필드 타입 | text, date, select 등 | +| 기본값 | 문서 생성 시 자동 입력 | +| 필수 여부 | 체크박스 | + +#### 탭 3: 검사 기준서 + +``` +┌──────────────────────────────────────────────────┐ +│ 섹션 1: [제목 입력] [이미지 업로드] [+ 항목 추가] │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ 카테고리 │ 항목 │ 기준 │ 공차 │ 기준값 │ ... │ │ +│ │ 외관 │ 색상 │ 기준 │ ±0.5 │ 5.0 │ ... │ │ +│ │ 외관 │ 흠집 │ 무 │ │ │ ... │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ 섹션 2: [제목 입력] [이미지 업로드] [+ 항목 추가] │ +│ ... │ +│ [+ 섹션 추가] │ +└──────────────────────────────────────────────────┘ +``` + +**공차 유형:** + +| 유형 | 입력 | 표시 예 | +|------|------|--------| +| `symmetric` | ± 값 | ±0.5 | +| `asymmetric` | +값, -값 | +0.3 / -0.2 | +| `range` | 최소~최대 | 4.5 ~ 5.5 | +| `limit` | 상한 또는 하한 | ≤ 10 | + +#### 탭 4: 테이블 컬럼 + +| 항목 | 설명 | +|------|------| +| 라벨 | 컬럼 헤더 | +| 그룹명 | 다단계 그룹 ("/" 구분) | +| 너비 | 컬럼 너비 (px 또는 %) | +| 컬럼 타입 | text, check, complex, select, measurement | +| 하위 라벨 | complex 타입 시 sub_labels | + +**자동 컬럼 생성:** + +``` +[기준서에서 자동 생성] 버튼 클릭 + ↓ +검사 기준서 섹션의 항목들을 분석 + ↓ +카테고리 그룹별 컬럼 자동 생성 + ↓ +measurement_type에 따라 컬럼 타입 결정 +``` + +### 6.3 양식 디자이너 편집 화면 (`block-editor.blade.php`) + +**3패널 레이아웃:** + +``` +┌──────────┬──────────────────────────┬───────────┐ +│ 팔레트 │ 캔버스 │ 속성 패널 │ +│ (220px) │ (flex: 1) │ (300px) │ +│ │ │ │ +│ 기본: │ ┌──────────────────────┐ │ 선택 블록: │ +│ □ 제목 │ │ [제목 블록] │ │ │ +│ □ 문단 │ │ [문단 블록] │ │ 제목: ... │ +│ □ 테이블 │ │ [테이블 블록] │ │ 크기: ... │ +│ □ 컬럼 │ │ [입력 필드 블록] │ │ 정렬: ... │ +│ □ 구분선 │ │ │ │ │ +│ □ 여백 │ └──────────────────────┘ │ │ +│ │ │ │ +│ 폼: │ │ │ +│ □ 텍스트 │ │ │ +│ □ 숫자 │ │ │ +│ □ 날짜 │ │ │ +│ □ 선택 │ │ │ +│ □ 체크 │ │ │ +│ □ 텍스트영역│ │ │ +│ □ 서명 │ │ │ +└──────────┴──────────────────────────┴───────────┘ +``` + +**블록 타입 (15개):** + +| 분류 | 타입 | 설명 | +|------|------|------| +| 기본 | `heading` | 제목 (h1~h6) | +| 기본 | `paragraph` | 문단 텍스트 | +| 기본 | `table` | 테이블 (행/열 편집) | +| 기본 | `columns` | 다단 컬럼 레이아웃 | +| 기본 | `divider` | 구분선 | +| 기본 | `spacer` | 여백 | +| 폼 | `text_field` | 텍스트 입력 | +| 폼 | `number_field` | 숫자 입력 | +| 폼 | `date_field` | 날짜 입력 | +| 폼 | `select_field` | 선택 드롭다운 | +| 폼 | `checkbox_field` | 체크박스 | +| 폼 | `textarea_field` | 긴 텍스트 입력 | +| 폼 | `signature_field` | 서명 영역 | + +**Alpine.js 상태 관리:** + +```javascript +blockEditor(initialSchema, templateId) { + blocks: [], // 블록 배열 + selectedBlockId: null, // 현재 선택 블록 + history: [], // Undo/Redo 스택 (최대 50) + historyIndex: -1, + pageConfig: { // 페이지 설정 + size: 'A4', // A4 / A3 + orientation: 'portrait', // portrait / landscape + margins: { top, right, bottom, left } + }, + templateName: '', + category: '' +} +``` + +**키보드 단축키:** + +| 단축키 | 기능 | +|--------|------| +| `Ctrl+Z` / `Cmd+Z` | Undo | +| `Ctrl+Shift+Z` / `Cmd+Shift+Z` | Redo | +| `Ctrl+S` / `Cmd+S` | 저장 | + +**SortableJS:** +- 캔버스 내 블록 드래그-앤-드롭 정렬 +- 팔레트에서 캔버스로 블록 추가 + +--- + +## 7. 미리보기 시스템 + +### 7.1 Legacy Builder 미리보기 + +```javascript +buildDocumentPreviewHtml(data) +├── 결재란 테이블 (역할별 칸) +├── 기본필드 (2열 15:35:15:35 비율) +├── 섹션별 이미지 (title + image 또는 placeholder) +├── 검사 데이터 테이블 +│ ├── 다단계 그룹 헤더 (group_name "/" 구분) +│ ├── sub_labels (complex 컬럼) +│ ├── 항목 행 (카테고리 그룹핑) +│ └── 측정치 셀 (measurement_type별 렌더) +└── 비고/종합판정 섹션 +``` + +### 7.2 양식 디자이너 미리보기 + +```javascript +buildBlockPreviewHtml(data) +├── 블록 타입별 HTML 렌더링 +├── 폼 필드 placeholder 표시 +└── A4/A3 레이아웃 시뮬레이션 +``` + +### 7.3 이미지 URL 처리 + +```javascript +_previewImageUrl(imagePath) +├── http(s):// 시작 → 그대로 사용 +├── /^\d+\// 패턴 → API tenant storage URL 생성 +│ → http://api.sam.kr/storage/tenants/{imagePath} +└── 기타 → MNG local storage (/storage/{imagePath}) +``` + +--- + +## 8. 분류(Category) 관리 + +### 8.1 소스 (우선순위) + +1. **common_codes** (code_group = `document_category`, is_active = true) + - tenant_id가 있는 것 우선 (테넌트 전용) + - tenant_id가 null인 것도 포함 (공통) + - code 기준 중복 제거 (테넌트 우선) +2. **기존 템플릿의 category** (common_codes에 없는 값) + - 기존 이름 그대로 추가 + +### 8.2 연동 공통코드 그룹 + +| 그룹 | 용도 | +|------|------| +| `document_category` | 문서 분류 | +| `doc_template_basic_field` | 기본필드 키 옵션 | +| `doc_inspection_method` | 검사방법 | +| `doc_measurement_type` | 측정유형 | + +--- + +## 9. 프리셋 시스템 + +### 9.1 테이블 + +``` +document_template_field_presets +├── name // 프리셋 이름 +├── category // 대상 카테고리 +├── description // 설명 +└── field_definitions // array - 필드 정의 목록 + [{ field_key, label, field_type, options, ... }] +``` + +### 9.2 동작 + +``` +분류(Category) 변경 + ↓ +매칭 프리셋 검색 + ↓ +기존 section_fields가 비어있으면 + ↓ +"'{category}' 카테고리에 맞는 프리셋을 적용할까요?" 확인 + ↓ +승인 시 field_definitions 자동 적용 +``` + +> **주의**: 초기 로드 시에는 제안하지 않음. 분류 변경 시에만 제안. + +--- + +## 10. 연결품목 중복 검증 + +### 10.1 규칙 + +같은 category 내 서로 다른 템플릿이 동일한 items를 연결할 수 없다. + +### 10.2 검증 로직 + +```php +checkLinkedItemDuplicates($templateId, $category, $itemIds) + +// 1. 같은 category의 다른 템플릿 조회 +$otherTemplates = DocumentTemplate::where('category', $category) + ->where('id', '!=', $templateId) + ->get(); + +// 2. 각 템플릿의 연결품목 수집 +foreach ($otherTemplates as $other) { + // 레거시: linked_item_ids (JSON 배열) + // 신규: template_links → linkValues (source_table = 'items') + $existingItemIds = ...; +} + +// 3. 교집합 검사 +$duplicates = array_intersect($itemIds, $existingItemIds); +if (!empty($duplicates)) { + return 422; // 중복 항목 목록과 함께 오류 반환 +} +``` + +--- + +## 11. JavaScript 상태 관리 (Legacy Builder) + +### 11.1 templateState 객체 + +```javascript +const templateState = { + // 기본정보 + id, name, category, title, + company_name, company_address, company_contact, + footer_remark_label, footer_judgement_label, + footer_judgement_options, + is_active, + + // 관계 데이터 + approval_lines: [], // 결재선 + basic_fields: [], // 기본필드 + sections: [], // 섹션 + items + columns: [], // 테이블 컬럼 + section_fields: [], // 섹션 필드 + template_links: [], // 연결 설정 + values +}; +``` + +### 11.2 저장 흐름 + +``` +사용자 입력 (Blade 폼) + ↓ +templateState 객체 갱신 + ↓ +saveTemplate() 호출 + ↓ +fetch POST/PUT /api/admin/document-templates + ↓ +DocumentTemplateApiController::store/update() + ↓ +검증 → 중복 검사 → DB 트랜잭션 → saveRelations() + ↓ +JSON 응답 + ↓ +showToast() 메시지 + ↓ +htmx.trigger('#template-table', 'filterSubmit') → 테이블 새로고침 +``` + +--- + +## 12. 양식 디자이너(Block Builder) vs 새 양식(Legacy Builder) 비교 + +| 항목 | 양식 디자이너 | 새 양식 | +|------|:------------:|:-------------:| +| builder_type | `block` | `legacy` 또는 null | +| 편집 UI | WYSIWYG 캔버스 (Alpine.js) | 탭 폼 (순수 JavaScript) | +| 데이터 저장 | `schema` JSON 컬럼 | 관계 테이블 (7개) | +| Undo/Redo | 히스토리 스택 (최대 50) | 불가 | +| 블록 타입 | 15개 (기본 6 + 폼 7 + 기타 2) | N/A | +| 드래그-앤-드롭 | SortableJS | 불가 | +| 페이지 설정 | A4/A3, 여백, 방향 | 없음 | +| 복제 | 스키마 JSON 복사 | 각 관계 데이터 개별 복사 | +| 미리보기 함수 | `buildBlockPreviewHtml()` | `buildDocumentPreviewHtml()` | +| 적합 용도 | 자유 레이아웃 문서 | 정형화된 검사 성적서 | + +--- + +## 13. 권한 및 보안 + +### 13.1 미들웨어 + +- **웹 라우트**: 일반 인증 (auth) +- **API 라우트**: HQ 관리자 미들웨어 (`admin` prefix) + +### 13.2 슈퍼어드민 전용 기능 + +| 기능 | 엔드포인트 | +|------|-----------| +| 영구삭제 | `DELETE /{id}/force` | +| 복원 | `POST /{id}/restore` | +| 휴지통 조회 | `GET /?is_active=TRASHED` | + +### 13.3 삭제 보호 + +- 소프트 삭제: `deleted_at` + `deleted_by` 기록 +- 영구삭제 전 참조 문서 검사 (Document 테이블) +- 참조 문서가 있으면 영구삭제 불가 (422 응답) + +--- + +## 14. API 프로젝트 연동 + +### 14.1 API 서비스 + +```php +// DocumentTemplateService (API) +list(array $params): LengthAwarePaginator + // 필터: is_active, category, search + +show(int $id): DocumentTemplate + // 전체 관계 로드 (approvalLines, basicFields, sections, columns...) +``` + +### 14.2 API 엔드포인트 + +``` +GET /v1/document-templates → index (목록) +GET /v1/document-templates/{id} → show (상세) +``` + +> API는 **읽기 전용**. 서식 생성/수정은 MNG에서만 수행. + +--- + +## 15. 주요 파일 경로 + +| 기능 | 경로 | +|------|------| +| 웹 컨트롤러 | `mng/app/Http/Controllers/DocumentTemplateController.php` | +| API 컨트롤러 | `mng/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php` | +| 모델 (8개) | `mng/app/Models/DocumentTemplate*.php` | +| 뷰 - 목록 | `mng/resources/views/document-templates/index.blade.php` | +| 뷰 - Legacy 편집 | `mng/resources/views/document-templates/edit.blade.php` | +| 뷰 - 양식 디자이너 | `mng/resources/views/document-templates/block-editor.blade.php` | +| 뷰 - 테이블 | `mng/resources/views/document-templates/partials/table.blade.php` | +| 뷰 - 미리보기 | `mng/resources/views/document-templates/partials/preview-modal.blade.php` | +| API 서비스 | `api/app/Services/DocumentTemplateService.php` | +| API 모델 | `api/app/Models/Documents/DocumentTemplate*.php` | + +--- + +## 관련 문서 + +- [README.md](README.md) — 문서관리 시스템 개요 (API 중심) +- [MNG 문서관리](mng-document-system.md) — 문서 생성/편집/결재 (서식을 사용하는 측) +- [DB 스키마 — 문서](../../system/database/documents.md) + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/README.md b/features/planning/README.md new file mode 100644 index 0000000..ef681d0 --- /dev/null +++ b/features/planning/README.md @@ -0,0 +1,129 @@ +# 주일기업 기획 메뉴 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **프로젝트**: SAM MNG (관리자 웹) +> **라우트 접두사**: `/juil` + +--- + +## 1. 개요 + +### 1.1 목적 + +블라인드/스크린 제조업체의 현장 관리를 위한 기획 도구 모음. 견적부터 공사, 준공까지의 업무 흐름과 현장 기록(사진대지), 회의 기록(STT/AI 요약)을 제공한다. + +### 1.2 문서 구조 + +| 문서 | 설명 | +|------|------| +| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 아키텍처 | +| [construction-photos.md](construction-photos.md) | 공사현장 사진대지 기술 명세 | +| [meeting-minutes.md](meeting-minutes.md) | 회의록 작성 기술 명세 (STT/AI 통합) | +| [planning-views.md](planning-views.md) | 견적/프로젝트/워크플로우 화면 명세 | + +### 1.3 하위 메뉴 구조 + +``` +주일기업 기획 +├── 견적/입찰/공사관리 /juil/estimate +├── 프로젝트관리/기성청구 /juil/project +├── 업무 Workflow /juil/workflow +├── 공사현장 사진대지 /juil/construction-photos +└── 회의록 작성 /juil/meeting-minutes +``` + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + React (인라인) + Babel | 브라우저 트랜스파일 React 컴포넌트 | +| API | Laravel Controller + Service | JSON API (AJAX) | +| 모델 | Eloquent ORM | Multi-tenant (BelongsToTenant) | +| 파일 저장 | Google Cloud Storage | 사진, 오디오 파일 | +| AI | Gemini API (Vertex AI) | 요약, 화자 분리 | +| STT | Google Speech-to-Text V1/V2 + Web Speech API | 음성 인식 | + +### 2.2 프로젝트 파일 구조 + +``` +mng/ +├── app/Http/Controllers/ +│ ├── PlanningController.php ← 견적/프로젝트/워크플로우 +│ ├── ConstructionSitePhotoController.php ← 사진대지 CRUD + 파일 관리 +│ └── MeetingMinuteController.php ← 회의록 CRUD + AI 기능 +├── app/Services/ +│ ├── ConstructionSitePhotoService.php ← 사진대지 비즈니스 로직 +│ └── MeetingMinuteService.php ← 회의록 + AI 통합 로직 +├── app/Models/ +│ ├── ConstructionSitePhoto.php ← 사진대지 모델 +│ ├── ConstructionSitePhotoRow.php ← 사진 행 모델 +│ ├── MeetingMinute.php ← 회의록 모델 +│ └── MeetingMinuteSegment.php ← 회의 세그먼트 모델 +└── resources/views/juil/ + ├── estimate.blade.php ← 견적/입찰/공사관리 + ├── project.blade.php ← 프로젝트관리/기성청구 + ├── workflow.blade.php ← 업무 Workflow + ├── construction-photos.blade.php ← 사진대지 SPA + └── meeting-minutes.blade.php ← 회의록 SPA +``` + +### 2.3 기능별 구현 현황 + +| 기능 | 구현 방식 | 백엔드 | DB | +|------|----------|--------|-----| +| 견적/입찰/공사관리 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 | +| 프로젝트관리/기성청구 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 | +| 업무 Workflow | React 뷰 (정적 데이터) | PlanningController (뷰 반환만) | 없음 | +| 공사현장 사진대지 | React SPA + API | Controller + Service | 2 테이블 | +| 회의록 작성 | React SPA + API | Controller + Service + AI | 2 테이블 | + +--- + +## 3. 외부 서비스 의존성 + +| 서비스 | 용도 | 추적 | +|--------|------|------| +| **Google Cloud Storage** | 사진/오디오 파일 저장 | `AiTokenHelper::saveGcsStorageUsage()` | +| **Google Speech-to-Text V2 (Chirp2)** | 자동 화자 분리 (최우선) | `AiTokenHelper::saveSttUsage()` | +| **Google Speech-to-Text V1** | 화자 분리 (V2 실패 시 폴백) | `AiTokenHelper::saveSttUsage()` | +| **Gemini API (Vertex AI)** | 요약 생성 + 화자 재분배 | `AiTokenHelper::saveGeminiUsage()` | +| **Web Speech API** | 브라우저 음성 입력 (현장명/설명) | `logSttUsage()` | + +### 3.1 도메인 용어 힌트 (STT 정확도 향상) + +``` +블라인드, 스크린, 롤스크린, 허니콤, 버티컬, +원단, 바텀레일, 헤드레일, 브라켓, +주일, 경동, 주일블라인드, 경동블라인드, +수주, 발주, 납기, 출하, 재고, 원가, 단가, +SAM, ERP, MES +``` + +--- + +## 4. HTMX 전체 페이지 로드 규칙 + +모든 `/juil/*` 페이지는 React 인라인 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 컨트롤러 메서드에서 HTMX 요청 감지 시 **HX-Redirect로 전체 페이지 리로드를 강제**한다. + +```php +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.estimate')); +} +``` + +--- + +## 5. 관련 문서 + +- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 행 구조, 음성 입력 +- [회의록 작성](meeting-minutes.md) — STT/화자분리/AI 요약, 오디오 녹음 +- [견적/프로젝트/워크플로우](planning-views.md) — React 뷰 구성, 업무 프로세스 정의 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/construction-photos.md b/features/planning/construction-photos.md new file mode 100644 index 0000000..0a85ffb --- /dev/null +++ b/features/planning/construction-photos.md @@ -0,0 +1,275 @@ +# 공사현장 사진대지 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **라우트**: `/juil/construction-photos` +> **관련**: [README.md](README.md) | [회의록](meeting-minutes.md) | [뷰 화면](planning-views.md) + +--- + +## 1. 개요 + +건설/시공 현장의 작업 과정을 **작업전/작업중/작업후** 3단계 사진으로 기록하고 관리하는 기능. Google Cloud Storage에 사진을 저장하며, 음성 입력(Web Speech API)으로 현장명과 설명을 입력할 수 있다. + +--- + +## 2. 라우트 + +``` +/juil/construction-photos +├── GET / → index (목록 페이지) +├── GET /list → list (JSON 목록) +├── POST / → store (새 사진대지 등록) +├── POST /log-stt-usage → logSttUsage (STT 시간 기록) +├── GET /{id} → show (상세 조회) +├── PUT /{id} → update (메타데이터 수정) +├── DELETE /{id} → destroy (삭제) +├── POST /{id}/rows → addRow (행 추가) +├── DELETE /{id}/rows/{rowId} → deleteRow (행 삭제) +├── POST /{id}/rows/{rowId}/upload → uploadPhoto (사진 업로드) +├── DELETE /{id}/rows/{rowId}/photo/{type} → deletePhoto (사진 삭제) +└── GET /{id}/rows/{rowId}/download/{type} → downloadPhoto (다운로드) +``` + +--- + +## 3. 데이터베이스 + +### 3.1 construction_site_photos (사진대지) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `user_id` | BIGINT FK | 등록자 | +| `site_name` | VARCHAR(200) | 현장명 (필수) | +| `work_date` | DATE | 작업일자 (필수) | +| `description` | TEXT NULL | 설명 | +| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | + +**인덱스**: `tenant_id`, `user_id`, `(tenant_id, work_date)` + +### 3.2 construction_site_photo_rows (사진 행) + +각 사진대지는 1개 이상의 행을 가지며, 각 행에 3개 타입(before/during/after) 사진 저장. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `construction_site_photo_id` | BIGINT FK | 부모 (cascade delete) | +| `sort_order` | INT | 정렬 순서 (0부터) | +| `before_photo_path` | VARCHAR(500) NULL | 작업전 GCS 경로 | +| `before_photo_gcs_uri` | VARCHAR(500) NULL | 작업전 GCS URI | +| `before_photo_size` | INT UNSIGNED NULL | 작업전 파일크기 (bytes) | +| `during_photo_path` | VARCHAR(500) NULL | 작업중 GCS 경로 | +| `during_photo_gcs_uri` | VARCHAR(500) NULL | 작업중 GCS URI | +| `during_photo_size` | INT UNSIGNED NULL | 작업중 파일크기 (bytes) | +| `after_photo_path` | VARCHAR(500) NULL | 작업후 GCS 경로 | +| `after_photo_gcs_uri` | VARCHAR(500) NULL | 작업후 GCS URI | +| `after_photo_size` | INT UNSIGNED NULL | 작업후 파일크기 (bytes) | + +### 3.3 테이블 관계 + +``` +construction_site_photos + │ 1:N + ▼ +construction_site_photo_rows (sort_order ASC) + ├── before_photo_* (작업전) + ├── during_photo_* (작업중) + └── after_photo_* (작업후) +``` + +--- + +## 4. API 명세 + +### 4.1 목록 조회 + +``` +GET /juil/construction-photos/list +``` + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 현장명 검색 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +### 4.2 생성 + +``` +POST /juil/construction-photos +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `site_name` | required, max:200 | 현장명 | +| `work_date` | required, date | 작업일자 | +| `description` | nullable, max:2000 | 설명 | + +> 생성 시 빈 행 1개 자동 추가 + +### 4.3 사진 업로드 + +``` +POST /juil/construction-photos/{id}/rows/{rowId}/upload +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `type` | required, in:before,during,after | 사진 타입 | +| `photo` | required, image, mimes:jpeg,jpg,png,webp, max:10240 | 최대 10MB | + +### 4.4 사진 다운로드 + +``` +GET /juil/construction-photos/{id}/rows/{rowId}/download/{type}?inline=1 +``` + +| 파라미터 | 설명 | +|---------|------| +| `inline=1` | 브라우저 표시 (미지정 시 다운로드) | + +--- + +## 5. GCS 저장 구조 + +### 5.1 경로 패턴 + +``` +construction-site-photos/{tenant_id}/{photo_id}/{row_id}_{timestamp}_{type}.{ext} +``` + +**예시:** + +``` +construction-site-photos/1/42/15_1709723456_before.jpg +construction-site-photos/1/42/15_1709723456_during.jpg +construction-site-photos/1/42/15_1709723456_after.png +``` + +### 5.2 업로드 흐름 + +``` +클라이언트 (Canvas 이미지 압축: 1920px, quality 80%) + ↓ +FormData (multipart) 전송 + ↓ +컨트롤러: uploadPhoto() + ↓ +서비스: uploadPhoto() + ├── 기존 사진 있으면 GCS에서 삭제 + ├── GCS에 업로드 + ├── DB에 path + uri + size 저장 + └── AiTokenHelper::saveGcsStorageUsage() 호출 + ↓ +응답: { success, data: Photo with rows } +``` + +### 5.3 삭제 흐름 + +``` +사진 삭제: GCS 파일 삭제 → DB 필드 null +행 삭제: 행 내 모든 사진 GCS 삭제 → 행 삭제 → sort_order 재정렬 +사진대지 삭제: 모든 행의 모든 사진 GCS 삭제 → soft delete +``` + +--- + +## 6. 음성 입력 (Web Speech API) + +### 6.1 VoiceInputButton 컴포넌트 + +현장명, 설명 필드에 음성으로 텍스트 입력 가능. + +```javascript +// Web Speech Recognition 설정 +recognition.lang = 'ko-KR'; +recognition.continuous = true; +recognition.interimResults = true; +recognition.maxAlternatives = 1; +``` + +### 6.2 인식 상태 + +| 상태 | 표시 | 설명 | +|------|------|------| +| interim (미확정) | 이탤릭 + 회색 | 인식 중간 결과, 2초 후 소실 | +| final (확정) | 일반체 + 진한색 | 확정 텍스트, 영구 저장 | + +### 6.3 사용량 추적 + +``` +STT 사용 종료 시: +duration = Math.max(1, (Date.now() - startTime) / 1000) + ↓ +POST /juil/construction-photos/log-stt-usage + body: { duration_seconds } + ↓ +AiTokenHelper::saveSttUsage('공사현장사진대지-음성입력', seconds) +``` + +--- + +## 7. UI 구성 (React) + +### 7.1 사진 타입별 색상 + +| 타입 | 라벨 | 배경색 | 뱃지색 | +|------|------|--------|--------| +| `before` | 작업전 | `bg-blue-50` | `bg-blue-100 text-blue-800` | +| `during` | 작업중 | `bg-yellow-50` | `bg-yellow-100 text-yellow-800` | +| `after` | 작업후 | `bg-green-50` | `bg-green-100 text-green-800` | + +### 7.2 행 관리 + +- **행 추가**: sort_order 자동 계산 (마지막 + 1) +- **행 삭제**: 최소 1개 행 유지 필수 +- **행별 사진**: 각 행에 3개 타입 사진 독립 업로드/삭제 + +--- + +## 8. 모델 메서드 + +### 8.1 ConstructionSitePhoto + +```php +user() # BelongsTo User (등록자) +rows() # HasMany Row (sort_order ASC) +getPhotoCount(): int # 전체 사진 개수 (모든 행의 사진 합계) +``` + +### 8.2 ConstructionSitePhotoRow + +```php +constructionSitePhoto() # BelongsTo 부모 +hasPhoto(string $type): bool # 특정 타입 사진 존재 여부 +getPhotoCount(): int # 이 행의 사진 개수 (0~3) +``` + +### 8.3 ConstructionSitePhotoService + +```php +getList(array $filters) # 검색/필터 목록 (페이지네이션) +create(array $data) # 생성 + 빈 행 1개 자동 추가 +update(ConstructionSitePhoto, array $data) # 메타데이터만 수정 +delete(ConstructionSitePhoto) # GCS 전체 삭제 → soft delete +uploadPhoto(Row, UploadedFile, string $type) # GCS 업로드 + DB 기록 +deletePhotoByType(Row, string $type) # 특정 타입 GCS 삭제 +addRow(ConstructionSitePhoto) # 행 추가 (sort_order 자동) +deleteRow(Row) # 행 내 GCS 삭제 → 행 삭제 → 재정렬 +``` + +--- + +## 관련 문서 + +- [README.md](README.md) — 기획 메뉴 전체 개요 +- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록 +- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/meeting-minutes.md b/features/planning/meeting-minutes.md new file mode 100644 index 0000000..09d089a --- /dev/null +++ b/features/planning/meeting-minutes.md @@ -0,0 +1,456 @@ +# 회의록 작성 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **라우트**: `/juil/meeting-minutes` +> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [뷰 화면](planning-views.md) + +--- + +## 1. 개요 + +음성으로 회의 내용을 기록하고, **Google STT(화자 분리)** + **Gemini AI(요약/결정사항/액션아이템)** 로 자동 정리하는 회의록 시스템. 브라우저 MediaRecorder로 녹음하고, GCS에 오디오를 저장하며, 세그먼트(화자별 발화)를 관리한다. + +--- + +## 2. 라우트 + +``` +/juil/meeting-minutes +├── GET / → index (목록 페이지) +├── GET /list → list (JSON 목록) +├── POST / → store (새 회의록 생성) +├── POST /log-stt-usage → logSttUsage (STT 시간 기록) +├── GET /{id} → show (상세 조회 + segments) +├── PUT /{id} → update (메타데이터 수정) +├── DELETE /{id} → destroy (삭제) +├── POST /{id}/segments → saveSegments (세그먼트 저장) +├── POST /{id}/upload-audio → uploadAudio (오디오 업로드) +├── POST /{id}/summarize → summarize (AI 요약 생성) +├── POST /{id}/diarize → diarize (자동 화자 분리) +└── GET /{id}/download-audio → downloadAudio (오디오 다운로드) +``` + +--- + +## 3. 데이터베이스 + +### 3.1 meeting_minutes (회의록) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `user_id` | BIGINT FK | 작성자 | +| `title` | VARCHAR(300) | 제목 (기본: "무제 회의록") | +| `folder` | VARCHAR(100) NULL | 폴더 분류 | +| `participants` | JSON NULL | 참여자 목록 배열 | +| `meeting_date` | DATE | 회의 날짜 | +| `meeting_time` | TIME NULL | 회의 시작 시간 | +| `duration_seconds` | INT UNSIGNED | 녹음 총 시간(초) | +| `audio_file_path` | VARCHAR(500) NULL | 오디오 GCS 경로 | +| `audio_gcs_uri` | VARCHAR(500) NULL | 오디오 GCS URI | +| `audio_file_size` | BIGINT UNSIGNED NULL | 오디오 파일 크기 (bytes) | +| `full_transcript` | LONGTEXT NULL | 전체 트랜스크립트 | +| `summary` | LONGTEXT NULL | AI 요약 | +| `decisions` | JSON NULL | 결정사항 배열 | +| `action_items` | JSON NULL | 액션아이템 배열 | +| `status` | VARCHAR(20) | 상태 (5가지) | +| `stt_language` | VARCHAR(10) | STT 언어 (기본: ko-KR) | +| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | + +**인덱스**: `tenant_id`, `user_id`, `(tenant_id, meeting_date)`, `status` + +### 3.2 meeting_minute_segments (세그먼트) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `meeting_minute_id` | BIGINT FK | 회의록 (cascade delete) | +| `segment_order` | INT UNSIGNED | 순서 | +| `speaker_name` | VARCHAR(100) | 화자 이름 (기본: "화자 1") | +| `speaker_label` | VARCHAR(20) NULL | 화자 라벨/번호 | +| `text` | TEXT | 발화 텍스트 | +| `start_time_ms` | INT UNSIGNED | 시작 시간 (ms, 기본: 0) | +| `end_time_ms` | INT UNSIGNED NULL | 종료 시간 (ms) | +| `is_manual_speaker` | BOOLEAN | 수동 화자 전환 여부 (기본: true) | + +**인덱스**: `meeting_minute_id`, `(meeting_minute_id, segment_order)` + +### 3.3 테이블 관계 + +``` +meeting_minutes + │ 1:N + ▼ +meeting_minute_segments (segment_order ASC) + ├── speaker_name (화자명) + ├── text (발화 내용) + └── start_time_ms / end_time_ms (타임스탬프) +``` + +--- + +## 4. 상태 관리 + +### 4.1 상태값 + +| 상태 | 코드 | 색상 | 설명 | +|------|------|------|------| +| 초안 | `DRAFT` | 회색 | 생성 직후, 편집 가능 | +| 녹음중 | `RECORDING` | 빨강 | (클라이언트 상태) | +| 처리중 | `PROCESSING` | 노랑 | AI 요약/화자분리 처리 중 | +| 완료 | `COMPLETED` | 초록 | AI 처리 완료 | +| 실패 | `FAILED` | 빨강 | AI 처리 실패 | + +### 4.2 상태 전이 + +``` +DRAFT + ↓ [오디오 업로드, 세그먼트 추가] +DRAFT (계속 편집) + ↓ [summarize() 호출] +PROCESSING + ↓ +COMPLETED (성공) 또는 FAILED (실패) + +DRAFT + ↓ [diarize() 호출 → 화자 분리] +DRAFT (세그먼트 갱신, 상태 유지) +``` + +--- + +## 5. API 명세 + +### 5.1 목록 조회 + +``` +GET /juil/meeting-minutes/list +``` + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목 검색 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `status` | string | 상태 필터 | +| `per_page` | int | 페이지당 건수 | + +### 5.2 생성 + +``` +POST /juil/meeting-minutes +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `title` | nullable, max:300 | 제목 (미입력 시 "무제 회의록") | +| `folder` | nullable, max:100 | 폴더 분류 | +| `participants` | nullable, array | 참여자 목록 | +| `meeting_date` | required, date | 회의 날짜 | +| `meeting_time` | nullable | 회의 시간 | +| `stt_language` | nullable, max:10 | STT 언어 (기본: ko-KR) | + +### 5.3 세그먼트 저장 + +``` +POST /juil/meeting-minutes/{id}/segments +``` + +```json +{ + "segments": [ + { + "speaker_name": "김과장", + "speaker_label": "1", + "text": "블라인드 납기일 확인 필요합니다.", + "start_time_ms": 0, + "end_time_ms": 5000, + "is_manual_speaker": true + } + ] +} +``` + +> **전처리**: 빈 텍스트 필터링, 언더스코어 노이즈 제거, 다중 공백 정규화 +> **자동 생성**: `full_transcript` = `[화자명] 발화텍스트\n...` 형식 + +### 5.4 오디오 업로드 + +``` +POST /juil/meeting-minutes/{id}/upload-audio +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `audio` | required, file | webm/mp3 등 | +| `duration_seconds` | required, integer, min:1 | 녹음 시간(초) | + +### 5.5 AI 요약 생성 + +``` +POST /juil/meeting-minutes/{id}/summarize +``` + +**요청**: 없음 (서버에서 `full_transcript` 사용) + +**응답 예시:** + +```json +{ + "success": true, + "message": "AI 요약이 완료되었습니다.", + "data": { + "summary": "블라인드 납품 일정과 현장 설치 계획을 논의했습니다...", + "decisions": [ + "납품일을 3월 15일로 확정", + "현장 실측은 3월 10일 진행" + ], + "action_items": [ + { + "assignee": "김과장", + "task": "거래처에 납기 확인 연락", + "deadline": "2026-03-08" + } + ], + "status": "COMPLETED" + } +} +``` + +### 5.6 자동 화자 분리 + +``` +POST /juil/meeting-minutes/{id}/diarize +``` + +| 필드 | 설명 | 기본값 | +|------|------|--------| +| `min_speakers` | 최소 화자 수 | 2 | +| `max_speakers` | 최대 화자 수 | 6 | + +**응답:** + +```json +{ + "success": true, + "message": "자동 화자 분리가 완료되었습니다. (3명 감지)", + "data": { /* Meeting with segments */ }, + "speaker_count": 3 +} +``` + +--- + +## 6. AI 통합 상세 + +### 6.1 화자 분리 (Diarization) 3단계 폴백 + +``` +[1단계] Google STT V2 (Chirp2) ← 최우선 + │ speechToTextWithDiarizationAuto() + │ 최신 모델, 높은 정확도 + │ 도메인 용어 힌트 포함 + │ + ↓ (실패 시) +[2단계] Google STT V1 (latest_long) ← 폴백 + │ 안정적이지만 약간 덜 정확 + │ + ↓ (1명만 인식 시) +[3단계] Gemini AI 화자 재분배 + splitSpeakersWithGemini() + 대화 맥락/호칭/질답 패턴/어투 변화 분석 + 2명 이상으로 재분배 +``` + +### 6.2 요약 생성 (Gemini API) + +``` +입력: full_transcript (전체 트랜스크립트) + ↓ +Gemini API 호출 + ├── 모드 1: Vertex AI (projectId, region, JWT) + └── 모드 2: Google AI Studio (API key) ← 폴백 + │ + │ Temperature: 0.3 (결정적) + │ Max tokens: 4096 + ↓ +출력 JSON: +{ + "summary": "3-5문장 요약", + "decisions": ["결정사항 1", "..."], + "action_items": [ + { "assignee": "담당자", "task": "할일", "deadline": "기한" } + ], + "keywords": ["키워드1", "..."] +} +``` + +### 6.3 Gemini 화자 재분배 + +Google STT가 1명만 인식할 때 Gemini로 대화 맥락 분석: + +``` +입력: 단일 화자 트랜스크립트 + 예상 화자 수 + ↓ +Gemini 프롬프트: + - 대화 맥락 분석 (호칭, 질답, 어투 변화) + - 지정된 수의 화자로 분리 + ↓ +출력: 화자별 세그먼트 배열 + → DB 세그먼트 교체 +``` + +--- + +## 7. 오디오 관리 (GCS) + +### 7.1 GCS 경로 패턴 + +``` +meeting-minutes/{tenant_id}/{meeting_id}/{timestamp}.webm +``` + +### 7.2 녹음 흐름 + +``` +브라우저 MediaRecorder API + ├── navigator.mediaDevices.getUserMedia({ audio: true }) + ├── new MediaRecorder(stream) + ├── recorder.ondataavailable → webm 블롭 수집 + └── 녹음 종료 → FormData로 업로드 + ↓ +POST /{id}/upload-audio + ├── GCS 업로드 + ├── DB: audio_file_path, audio_gcs_uri, audio_file_size, duration_seconds + └── AiTokenHelper::saveGcsStorageUsage() +``` + +### 7.3 다운로드 + +``` +GET /{id}/download-audio + → GCS에서 파일 콘텐츠 다운로드 + → Content-Disposition: attachment; filename="{title}.webm" +``` + +--- + +## 8. 세그먼트 처리 로직 + +### 8.1 저장 시 전처리 + +```php +// 1. 빈 텍스트 필터링 +trim($segment['text']) !== '' + +// 2. 언더스코어 노이즈 제거 +str_replace('_', '', $text) + +// 3. 다중 공백 정규화 +preg_replace('/\s{2,}/', ' ', $text) +``` + +### 8.2 전체 트랜스크립트 자동 생성 + +``` +[김과장] 블라인드 납기일 확인 필요합니다. +[박부장] 3월 15일로 확정합시다. +[김과장] 네, 거래처에 연락하겠습니다. +``` + +### 8.3 화자 분리 결과 세그먼트 변환 + +``` +Google STT 결과 → MeetingMinuteSegment 변환: +{ + segment_order: 순서, + speaker_name: "화자 N", + speaker_label: "N", + text: 발화 텍스트, + start_time_ms: 시작시간, + end_time_ms: 종료시간, + is_manual_speaker: false // 자동 분리 +} +``` + +--- + +## 9. UI 구성 (React) + +### 9.1 화자 색상 + +| 화자 | 배경색 | 뱃지색 | +|------|--------|--------| +| 화자 1 | `bg-blue-50` | `bg-blue-100 text-blue-800` | +| 화자 2 | `bg-green-50` | `bg-green-100 text-green-800` | +| 화자 3 | `bg-purple-50` | `bg-purple-100 text-purple-800` | +| 화자 4 | `bg-orange-50` | `bg-orange-100 text-orange-800` | + +### 9.2 지원 언어 + +| 코드 | 라벨 | +|------|------| +| `ko-KR` | 한국어 | +| `en-US` | English | +| `ja-JP` | 日本語 | +| `zh-CN` | 中文 | + +--- + +## 10. 사용량 추적 + +| 추적 항목 | 레이블 | Helper | +|----------|--------|--------| +| Web Speech API 사용 | `회의록-음성인식` | `AiTokenHelper::saveSttUsage()` | +| Google STT V1 화자 분리 | `회의록-화자분리` | `AiTokenHelper::saveSttUsage()` | +| Google STT V2 화자 분리 | `회의록-화자분리(Chirp2)` | `AiTokenHelper::saveSttUsage()` | +| GCS 오디오 저장 | `회의록-GCS저장` | `AiTokenHelper::saveGcsStorageUsage()` | +| Gemini 요약/분리 | `회의록-AI요약` | `AiTokenHelper::saveGeminiUsage()` | + +--- + +## 11. 모델 메서드 + +### 11.1 MeetingMinute + +```php +user() # BelongsTo User +segments() # HasMany Segment (segment_order ASC) +getFormattedDurationAttribute() # "H:MM:SS" 또는 "MM:SS" +``` + +**Cast**: `participants`, `decisions`, `action_items` → array, `meeting_date` → date + +### 11.2 MeetingMinuteService + +```php +# CRUD +getList(array $filters) # 검색/필터 목록 +create(array $data) # 생성 (DRAFT) +update(MeetingMinute, array $data) # 수정 +delete(MeetingMinute) # GCS 삭제 → soft delete + +# 세그먼트 +saveSegments(MeetingMinute, array $segments) # 전처리 + 저장 + 트랜스크립트 생성 +uploadAudio(MeetingMinute, UploadedFile, int $seconds) # GCS 업로드 +logSttUsage(int $seconds) # STT 사용량 기록 + +# AI +generateSummary(MeetingMinute) # Gemini 요약 생성 +processDiarization(MeetingMinute, int $min, int $max) # 3단계 화자 분리 +splitSpeakersWithGemini(string $text, int $expected) # Gemini 화자 재분배 +``` + +--- + +## 관련 문서 + +- [README.md](README.md) — 기획 메뉴 전체 개요 +- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력 +- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/planning-views.md b/features/planning/planning-views.md new file mode 100644 index 0000000..4b087ac --- /dev/null +++ b/features/planning/planning-views.md @@ -0,0 +1,222 @@ +# 견적/프로젝트/워크플로우 화면 명세 + +> **작성일**: 2026-03-06 +> **상태**: 뷰 구현 완료 (목데이터 기반, API 미연동) +> **라우트**: `/juil/estimate`, `/juil/project`, `/juil/workflow` +> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [회의록](meeting-minutes.md) + +--- + +## 1. 개요 + +3개 화면 모두 **React 인라인 컴포넌트**(Babel 브라우저 트랜스파일)로 구현. 현재는 정적/목데이터 기반이며, 향후 API 연동 예정. PlanningController에서 뷰만 반환한다. + +--- + +## 2. 견적/입찰/공사관리 (/juil/estimate) + +### 2.1 개요 + +블라인드/스크린 설치 프로젝트의 견적서 작성, 입찰 관리, 공사 진행 현황을 한 화면에서 관리. + +### 2.2 데이터 구조 (initialEstimates) + +```javascript +{ + id: "string", + name: "프로젝트명", + client: "고객사명", + status: "견적중|입찰|계약|공사중|준공", + amount: number, // 금액 + startDate: "YYYY-MM-DD", + endDate: "YYYY-MM-DD", + manager: "담당자명", + items: [ // 품목 내역 + { name: "품목명", quantity: number, unitPrice: number } + ] +} +``` + +### 2.3 공사관리 정보 (initialConstructionData) + +```javascript +{ + id: "string", + estimateId: "string", // 연결 견적 + siteName: "현장명", + address: "현장 주소", + progress: number, // 진행률 (0~100) + workers: number, // 투입 인원 + safetyChecks: [ // 안전점검 + { date: "YYYY-MM-DD", result: "합격|불합격", inspector: "점검자" } + ] +} +``` + +### 2.4 상태별 배지 색상 + +| 상태 | 색상 | +|------|------| +| 견적중 | 파랑 | +| 입찰 | 보라 | +| 계약 | 초록 | +| 공사중 | 주황 | +| 준공 | 회색 | + +### 2.5 SAM 연계 + +- 견적서 작성 시 SAM 견적 시스템(`features/quotes/`) 데이터 활용 가능 +- 향후 `/juil/estimate` ↔ SAM 견적 API 연동 계획 + +--- + +## 3. 프로젝트관리/기성청구 (/juil/project) + +### 3.1 개요 + +계약된 프로젝트의 현장 관리, 발주/청구/인건비 상태 추적, 기성 청구 관리. + +### 3.2 데이터 구조 (initialProjects) + +```javascript +{ + id: "string", + name: "프로젝트명", + client: "발주처", + contractAmount: number, // 계약금액 + status: "진행중|완료|보류", + sites: [ // 현장 목록 + { + name: "현장명", + address: "주소", + progress: number // 진행률 + } + ], + orders: [ // 발주 내역 + { + vendor: "거래처", + amount: number, + status: "발주|납품|정산" + } + ], + claims: [ // 기성 청구 + { + round: number, // 차수 + amount: number, // 청구금액 + claimDate: "YYYY-MM-DD", + status: "청구|승인|입금" + } + ], + laborCosts: [ // 인건비 + { + month: "YYYY-MM", + amount: number, + workers: number + } + ] +} +``` + +### 3.3 금액 포맷 함수 + +```javascript +fmt(amount) // 1,234,567 (쉼표 포맷) +fmtBillion(amount) // 12.3억 (억 단위 축약) +``` + +--- + +## 4. 업무 Workflow (/juil/workflow) + +### 4.1 개요 + +블라인드/스크린 사업의 전체 업무 프로세스를 단계별로 시각화. 각 프로세스에 담당 부서, 산출물, 서브스텝을 정의. + +### 4.2 프로세스 데이터 구조 + +```javascript +{ + id: "S1-1", // 프로세스 ID + phase: "영업", // Phase 명 + name: "정보 수집", // 프로세스 이름 + icon: "icon-name", // 아이콘 + dept: "영업팀", // 담당 부서 + color: "#3B82F6", // 테마 색상 + description: "프로세스 설명", + documents: [ // 산출물 목록 + "현장조사서", "고객요구사항서" + ], + subSteps: [ // 상세 서브스텝 + { + name: "서브스텝명", + description: "상세 설명", + responsible: "담당자/팀", + output: "산출물" + } + ] +} +``` + +### 4.3 업무 Phase 목록 + +| Phase | ID 범위 | 설명 | +|-------|---------|------| +| **영업** | S1-1 ~ S1-4 | 정보 수집 → 현장 실측 → 고객 미팅 → 프로젝트 검토 | +| **견적서 작성** | S2-1 ~ S2-4 | 물량 산출 → 단가 산정 → 견적가 산출 → 견적서 작성/검토 | +| **입찰** | S3-* | 입찰 준비 → 제출 → 결과 확인 | +| **계약** | S4-* | 계약 협상 → 계약 체결 | +| **공사** | S5-* | 자재 발주 → 시공 → 현장 관리 | +| **준공** | S6-* | 검수 → 하자보수 → 준공 정산 | + +### 4.4 SAM 연계 포인트 + +```javascript +// 견적서 작성 Phase에서 SAM 견적 화면으로 연결 +{ samLink: '/juil/estimate', label: '견적서 작성 바로가기' } +``` + +--- + +## 5. 공통 특징 + +### 5.1 HTMX 전체 페이지 로드 + +3개 화면 모두 React 컴포넌트 사용하므로 HTMX 부분 로드 불가: + +```php +// PlanningController의 모든 메서드 +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.xxx')); +} +return view('juil.xxx'); +``` + +### 5.2 현재 구현 상태 + +| 항목 | 상태 | +|------|------| +| UI 화면 | 구현 완료 (React 인라인) | +| 목데이터 | 블레이드에 하드코딩 | +| API 연동 | 미연동 (향후 계획) | +| DB 테이블 | 미생성 (향후 계획) | +| CRUD 기능 | 뷰 조회만 (생성/수정/삭제 미구현) | + +### 5.3 향후 개발 방향 + +1. 견적/프로젝트 DB 테이블 설계 (API 프로젝트) +2. API 엔드포인트 구현 +3. React 컴포넌트 API 연동 +4. SAM 견적 시스템과 데이터 동기화 + +--- + +## 관련 문서 + +- [README.md](README.md) — 기획 메뉴 전체 개요 +- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력 +- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록 +- [견적 시스템](../quotes/README.md) — SAM 견적 관리 (BOM, 10단계 로직) + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/rd/README.md b/features/rd/README.md new file mode 100644 index 0000000..1483160 --- /dev/null +++ b/features/rd/README.md @@ -0,0 +1,110 @@ +# R&D 메뉴 + +> **작성일**: 2026-03-08 +> **상태**: 운영 중 +> **프로젝트**: SAM MNG (관리자 웹) +> **라우트 접두사**: `/rd` + +--- + +## 1. 개요 + +### 1.1 목적 + +R&D 메뉴는 SAM 플랫폼의 **연구개발 및 내부 도구** 모음이다. AI 견적, 조직도 관리, 기획디자인(스토리보드 에디터), 안전점검 등 실험적이거나 내부 운영 목적의 기능을 제공한다. + +### 1.2 문서 구조 + +| 문서 | 설명 | +|------|------| +| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 컨트롤러 매핑 | +| [planning-design.md](planning-design.md) | 기획디자인 스토리보드 에디터 기술 명세 | +| [design-insight.md](design-insight.md) | 디자인 인사이트 UI/UX 연구 도구 (100종 패턴, AI 프롬프트) | + +### 1.3 하위 메뉴 구조 + +``` +R&D +├── 대시보드 /rd +├── 조직도 관리 /rd/org-chart +├── 중대재해처벌법 점검 /rd/safety-audit +├── AI 견적 /rd/ai-quotation +│ ├── 목록 /rd/ai-quotation +│ ├── 생성 /rd/ai-quotation/create +│ ├── 상세 /rd/ai-quotation/{id} +│ ├── 편집 /rd/ai-quotation/{id}/edit +│ └── 문서 /rd/ai-quotation/{id}/document +├── 기획디자인 /rd/planning-design +└── 디자인 인사이트 /rd/design-insight +``` + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + Alpine.js | 반응형 SPA (서버 렌더링 없음) | +| 컨트롤러 | `RdController` | 모든 R&D 라우트 처리 | +| 서비스 | `AiQuotationService` | AI 견적 비즈니스 로직 | +| 모델 | `AiQuotation`, `Employee`, `Department`, `Tenant` | Multi-tenant | +| 저장 | localStorage (기획디자인), DB (견적/조직도) | 용도별 분리 | + +### 2.2 컨트롤러 구조 + +**파일**: `app/Http/Controllers/RdController.php` + +| 메서드 | 라우트 | 설명 | +|--------|--------|------| +| `index()` | `GET /rd` | R&D 대시보드 | +| `orgChart()` | `GET /rd/org-chart` | 조직도 관리 | +| `orgChartAssign()` | `POST /rd/org-chart/assign` | 직원 부서 배치 | +| `orgChartUnassign()` | `POST /rd/org-chart/unassign` | 직원 부서 해제 | +| `orgChartReorder()` | `POST /rd/org-chart/reorder` | 직원 순서/이동 | +| `orgChartReorderDepts()` | `POST /rd/org-chart/reorder-depts` | 부서 순서 변경 | +| `orgChartToggleHide()` | `POST /rd/org-chart/toggle-hide` | 부서 숨기기/표시 | +| `safetyAudit()` | `GET /rd/safety-audit` | 중대재해처벌법 점검 | +| `quotations()` | `GET /rd/ai-quotation` | AI 견적 목록 | +| `createQuotation()` | `GET /rd/ai-quotation/create` | AI 견적 생성 폼 | +| `showQuotation()` | `GET /rd/ai-quotation/{id}` | AI 견적 상세 | +| `editQuotation()` | `GET /rd/ai-quotation/{id}/edit` | AI 견적 편집 | +| `documentQuotation()` | `GET /rd/ai-quotation/{id}/document` | AI 견적 문서 | +| `planningDesign()` | `GET /rd/planning-design` | 기획디자인 | +| `designInsight()` | `GET /rd/design-insight` | 디자인 인사이트 | + +### 2.3 HTMX 전체 페이지 로드 규칙 + +모든 `/rd/*` 페이지는 Alpine.js 또는 React 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 메서드에서 `HX-Request` 감지 시 `HX-Redirect`로 전체 페이지 로드를 강제한다. + +```php +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('rd.planning-design')); +} +``` + +--- + +## 3. 기능별 구현 현황 + +| 기능 | 구현 방식 | 백엔드 | DB | 상태 | +|------|----------|--------|-----|------| +| R&D 대시보드 | Blade | AiQuotationService | ai_quotations | 운영 중 | +| 조직도 관리 | Blade + Alpine.js | RdController (직접 쿼리) | employees, departments | 운영 중 | +| 중대재해처벌법 점검 | Blade (정적) | 없음 | 없음 | 운영 중 | +| AI 견적 | Blade + Alpine.js | AiQuotationService | ai_quotations | 운영 중 | +| **기획디자인** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** | +| **디자인 인사이트** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** | + +--- + +## 4. 관련 문서 + +- [기획디자인 스토리보드 에디터](planning-design.md) — 블록 에디터, 서식, 인쇄, 내보내기 +- [디자인 인사이트](design-insight.md) — UI/UX 연구 도구 (100종 패턴, AI 프롬프트 복사) +- [조직도 관리 기술문서](../../projects/org-chart/) — 조직도 시스템 상세 (별도 프로젝트 문서) + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/features/rd/design-insight.md b/features/rd/design-insight.md new file mode 100644 index 0000000..683d02f --- /dev/null +++ b/features/rd/design-insight.md @@ -0,0 +1,246 @@ +# 디자인 인사이트 — UI/UX 연구 도구 + +> **작성일**: 2026-03-08 +> **상태**: 운영 중 +> **라우트**: `GET /rd/design-insight` +> **뷰**: `resources/views/rd/design-insight/index.blade.php` + +--- + +## 1. 개요 + +### 1.1 목적 + +디자인 인사이트는 UI/UX 패턴을 **수집·분석·비교**하는 연구 도구이다. 외부 서비스의 UI 레퍼런스를 카드 형태로 정리하고, CSS 와이어프레임 미리보기와 AI 프롬프트 생성 기능으로 디자인 의사결정을 지원한다. + +### 1.2 핵심 기능 + +| 기능 | 설명 | +|------|------| +| 카드 관리 | 레퍼런스/분석/패턴/Before-After 4종 카드 CRUD | +| 프로젝트 관리 | 다중 프로젝트, localStorage 저장 | +| CSS 와이어프레임 | 100종 UI 패턴의 순수 CSS 미니 와이어프레임 | +| 프리셋 템플릿 | 인기 UI 패턴 100종 원클릭 불러오기 | +| AI 프롬프트 복사 | 카드 정보를 AI용 구조화 프롬프트로 변환·복사 | +| 3종 뷰 | 보드(카테고리별)/갤러리(그리드)/리스트 뷰 | +| JSON 내보내기/가져오기 | 프로젝트 데이터 백업/복원 | + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + Alpine.js | 단일 파일 SPA | +| 컨트롤러 | `RdController::designInsight()` | HX-Redirect 패턴 | +| 저장 | localStorage | `di_projects`, `di_current` 키 사용 | +| 백엔드 | 없음 | 서버 API 호출 없이 클라이언트 단독 동작 | +| 스타일 | 커스텀 CSS 변수 | `--di-*` 접두사 (Tailwind 미사용) | + +### 2.2 데이터 구조 + +```json +{ + "id": "di_1709000000_abc123", + "title": "프로젝트명", + "cards": [ + { + "id": "di_1709000001_def456", + "type": "pattern", + "title": "KPI 대시보드", + "category": "dashboard", + "rating": 5, + "tags": ["대시보드", "KPI", "통계"], + "memo": "레퍼런스 설명", + "guidelines": "디자인 가이드라인", + "usedIn": ["Stripe", "Shopify"], + "components": [ + { "name": "KPI 요약 카드", "required": true }, + { "name": "필터 영역", "required": false } + ], + "image": null, + "createdAt": "2026-03-08T00:00:00.000Z" + } + ], + "createdAt": "2026-03-08T00:00:00.000Z", + "updatedAt": "2026-03-08T00:00:00.000Z" +} +``` + +--- + +## 3. 카드 유형 (4종) + +| 코드 | 라벨 | 용도 | +|------|------|------| +| `reference` | 레퍼런스 | 외부 서비스 UI 스크린샷 수집 | +| `analysis` | 분석 | CRAP 원칙 등 UX 분석 (8가지 디자인 원칙 평가) | +| `pattern` | 패턴 | 재사용 가능한 UI 패턴 정의 | +| `comparison` | Before/After | 개선 전후 비교 (이미지 2장) | + +--- + +## 4. 카테고리 (8종) + +| 코드 | 라벨 | 아이콘 | +|------|------|--------| +| `dashboard` | 대시보드 | 📊 | +| `list` | 목록 | 📋 | +| `form` | 상세/폼 | 📝 | +| `modal` | 모달/팝업 | 💬 | +| `navigation` | 네비게이션 | 🧭 | +| `auth` | 로그인 | 🔐 | +| `report` | 보고서 | 📄 | +| `etc` | 기타 | 📎 | + +--- + +## 5. CSS 와이어프레임 시스템 + +### 5.1 동작 원리 + +`getWireframe(card)` 함수가 카드의 `title`과 `tags`를 키워드 매칭하여 해당 패턴에 맞는 순수 CSS/HTML 미니 와이어프레임을 반환한다. + +```javascript +getWireframe(card) { + const t = (card.title || '').toLowerCase(); + const tags = (card.tags || []).join(' ').toLowerCase(); + const key = t + ' ' + tags; + + if (key.includes('kpi') || key.includes('대시보드') && key.includes('통계')) return `...`; + // ... 100종 패턴 매칭 + return `기본 와이어프레임 (매칭 안 됨)`; +} +``` + +### 5.2 프리셋 100종 분포 + +| 카테고리 | 패턴 수 | 대표 패턴 | +|---------|---------|----------| +| 대시보드 | 10 | KPI, 실시간 모니터링, 게이지/미터, 히트맵, 퍼널 | +| 목록 | 10 | 데이터 테이블, 칸반 보드, 마스터-디테일, 피벗 테이블 | +| 상세/폼 | 16 | 프로필, 설정, 위지윅 에디터, 멀티스텝 폼, 태그 입력 | +| 모달/팝업 | 10 | 확인 다이얼로그, 라이트박스, 바텀시트, 컨텍스트 메뉴 | +| 네비게이션 | 10 | 사이드바, 탭, 브레드크럼, FAB, 앵커 스크롤 | +| 로그인 | 8 | 로그인 폼, 회원가입, 비밀번호 재설정, RBAC, API 키 | +| 보고서 | 9 | 인쇄용 보고서, 간트 차트, 조직도, 워터폴, 리포트 빌더 | +| 기타 | 27 | 댓글, 에러 페이지, FAQ, 캐러셀, 파일 매니저 등 | + +--- + +## 6. AI 프롬프트 복사 기능 + +### 6.1 목적 + +카드에 정리된 UI 패턴 정보를 **AI가 이해할 수 있는 구조화된 마크다운 프롬프트**로 변환하여 클립보드에 복사한다. 복사한 프롬프트를 AI(Claude, ChatGPT 등)에 붙여넣으면 해당 스타일로 코드를 생성할 수 있다. + +### 6.2 UI 위치 + +카드 상세 모달 상단, **편집** 버튼 왼쪽에 보라색 `✨ AI 프롬프트` 버튼으로 배치. + +### 6.3 프롬프트 구조 + +`copyAiPrompt(card)` 함수가 카드 데이터를 다음 구조로 변환한다: + +```markdown +## UI 패턴 구현 요청 + +아래 UI/UX 패턴 레퍼런스를 참고하여, 동일한 스타일과 구조로 코드를 작성해 주세요. + +--- + +### 패턴 정보 +- **패턴명**: {title} +- **카테고리**: {category label} +- **완성도 평점**: ★★★☆☆ ({rating}/5) +- **키워드**: {tags} + +### 레퍼런스 설명 +{memo} + +### 실제 사용처 (벤치마킹 대상) +- {usedIn[0]} +- {usedIn[1]} + +### 필수 구성 요소 + +**필수 (반드시 포함)**: +- ✅ {required component} + +**선택 (권장)**: +- ○ {optional component} + +### 디자인 가이드라인 +{guidelines} + +### 개선 제안 +{suggestion} + +### 기대 효과 +{effect} + +--- + +### 구현 요구사항 + +1. **기술 스택**: HTML + Tailwind CSS (또는 프로젝트에 맞는 프레임워크) +2. **반응형**: 모바일/태블릿/데스크톱 대응 +3. **접근성**: 시맨틱 태그, ARIA 라벨, 키보드 네비게이션 +4. **인터랙션**: hover, focus, active 상태 포함 +5. **위 구성 요소를 모두 포함**하되, 실제 서비스처럼 자연스러운 더미 데이터 사용 +6. **위 가이드라인을 충실히 반영**하여 UX 완성도를 높일 것 +``` + +### 6.4 포함 필드 매핑 + +| 카드 필드 | 프롬프트 섹션 | 조건 | +|----------|-------------|------| +| `title` | 패턴 정보 > 패턴명 | 항상 | +| `category` | 패턴 정보 > 카테고리 | 항상 (라벨로 변환) | +| `rating` | 패턴 정보 > 평점 | 항상 (별점으로 변환) | +| `tags` | 패턴 정보 > 키워드 | 태그가 있을 때만 | +| `memo` | 레퍼런스 설명 | 값이 있을 때만 | +| `usedIn` | 실제 사용처 | 배열이 비어있지 않을 때만 | +| `components` | 필수 구성 요소 | 배열이 비어있지 않을 때만 | +| `guidelines` | 디자인 가이드라인 | 값이 있을 때만 | +| `suggestion` | 개선 제안 | 값이 있을 때만 | +| `effect` | 기대 효과 | 값이 있을 때만 | +| `principles` | UX 원칙 | `type === 'analysis'`일 때만 | + +--- + +## 7. 뷰 모드 (3종) + +| 모드 | 설명 | 정렬 | +|------|------|------| +| `board` | 카테고리별 컬럼 그룹핑 | 카테고리 → 생성순 | +| `gallery` | 그리드 갤러리 (와이어프레임 강조) | 필터 순 | +| `list` | 테이블형 리스트 | 필터 순 | + +--- + +## 8. 파일 구조 + +``` +resources/views/rd/design-insight/ +└── index.blade.php # 단일 파일 SPA (~6,200줄) + ├── + + + +

제목

+

본문 내용

+ + +``` + +### 3.2 슬라이드 크기 (body width/height) + +| 용도 | body 크기 | 변환 스크립트 layout | +|------|----------|---------------------| +| **16:9 가로형** (발표용) | `width: 720pt; height: 405pt;` | `width: 10, height: 5.625` | +| **9:16 세로형** (브로셔) | `width: 405pt; height: 720pt;` | `width: 5.625, height: 10` | +| **4:3 가로형** (구형) | `width: 720pt; height: 540pt;` | `width: 10, height: 7.5` | + +> **중요**: HTML의 body 크기와 변환 스크립트의 layout 크기가 일치해야 한다. 불일치 시 에러 발생. + +### 3.3 필수 규칙 + +#### 텍스트 줄바꿈 방지 (가장 중요) + +브라우저와 PowerPoint의 폰트 렌더링 차이로, HTML에서 한 줄인 텍스트가 PPTX에서 두 줄로 넘어가는 문제가 발생한다. + +```html + +

이 텍스트는 한 줄입니다

+ + +

이 텍스트는 한 줄입니다

+ + +

+ 여러 줄로 의도된 텍스트입니다.
+ 이 경우 nowrap을 넣지 않는다. +

+``` + +#### 적용 대상 + +- 뱃지 텍스트 (01, UC-01 등) +- 카드 제목, 태그, 짧은 라벨 +- 푸터 텍스트 +- 단일행 `

` 태그 전부 + +#### 이미지 경로 + +```html + + + + + +``` + +#### 스타일 작성 + +```html + +

+ + +``` + +### 3.4 사용 가능한 CSS 속성 + +| 속성 | 지원 | 비고 | +|------|:----:|------| +| background (색상) | ✅ | 단색, rgba 모두 지원 | +| background (그라데이션) | ✅ | linear-gradient 지원 | +| border | ✅ | 색상, 두께, radius | +| border-radius | ✅ | px, pt 단위 | +| font-size, font-weight | ✅ | pt 단위 권장 | +| color | ✅ | hex, rgba | +| padding, margin | ✅ | pt 단위 권장 | +| display: flex | ✅ | gap, align-items 등 | +| white-space: nowrap | ✅ | 줄바꿈 방지 (필수) | +| opacity | ✅ | | +| box-shadow | ⚠️ | 부분 지원 | +| transform | ❌ | 미지원 | +| animation | ❌ | 미지원 | + +--- + +## 4. 변환 스크립트 작성법 + +### 4.1 기본 구조 (복사해서 사용) + +```javascript +const path = require('path'); + +// ① 패키지 경로 설정 (이 두 줄은 항상 동일) +module.paths.unshift( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules') +); +const PptxGenJS = require('pptxgenjs'); +const html2pptx = require( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js') +); + +async function main() { + const pres = new PptxGenJS(); + + // ② 레이아웃 설정 (HTML body 크기와 일치해야 함) + pres.defineLayout({ name: 'CUSTOM', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM'; + + // ③ HTML 파일 변환 + await html2pptx('/절대경로/slides/slide-01.html', pres); + + // ④ PPTX 출력 + await pres.writeFile({ fileName: '/절대경로/output.pptx' }); + console.log('완료!'); +} + +main().catch(console.error); +``` + +### 4.2 실전 예시: 여러 슬라이드 변환 + +```javascript +const path = require('path'); +module.paths.unshift( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules') +); +const PptxGenJS = require('pptxgenjs'); +const html2pptx = require( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js') +); + +async function main() { + const pres = new PptxGenJS(); + + // 16:9 가로형 + pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM_16x9'; + + const slidesDir = path.join(__dirname, 'slides'); + + // 변환할 HTML 파일 목록 + const slides = [ + 'slide-01.html', + 'slide-02.html', + 'slide-03.html', + ]; + + for (const file of slides) { + console.log(`Converting ${file} ...`); + try { + await html2pptx(path.join(slidesDir, file), pres); + } catch (err) { + console.error(`Error: ${err.message}`); + } + } + + const outputPath = path.join(__dirname, 'my-presentation.pptx'); + await pres.writeFile({ fileName: outputPath }); + console.log(`\nPPTX created: ${outputPath}`); +} + +main().catch(console.error); +``` + +### 4.3 실전 예시: 번호 기반 자동 변환 (18슬라이드) + +```javascript +// slide-01.html ~ slide-18.html 자동 변환 +const totalSlides = 18; +for (let i = 1; i <= totalSlides; i++) { + const num = String(i).padStart(2, '0'); + const htmlFile = path.join(slidesDir, `slide-${num}.html`); + await html2pptx(htmlFile, pres); +} +``` + +--- + +## 5. 실행 방법 + +### 5.1 터미널에서 직접 실행 + +```bash +# 해당 폴더로 이동 후 실행 +cd /home/aweso/sam/docs/brochure +node convert-2page.cjs + +# 또는 절대 경로로 실행 +node /home/aweso/sam/docs/brochure/convert-2page.cjs +``` + +### 5.2 실행 결과 + +``` +Converting brochure-2page-front.html ... +Converting brochure-2page-back.html ... + +PPTX created: /home/aweso/sam/docs/brochure/sam-brochure-2page.pptx +``` + +> 에러가 나면 HTML body 크기와 layout 설정 불일치가 가장 흔한 원인이다. + +--- + +## 6. 프로젝트 내 기존 사용 사례 + +| 경로 | 슬라이드 수 | 레이아웃 | 용도 | +|------|:-----------:|:--------:|------| +| `docs/usecase/` | 18장 | 16:9 가로 | 방화셔터 제안서 | +| `docs/usecase/brochure/` | 1장 / 2장 | 9:16 세로 | 방화셔터 브로셔 | +| `docs/brochure/` | 1장 / 2장 | 9:16 세로 | SAM 범용 브로셔 | +| `docs/plans/slides/` | N장 | 16:9 가로 | 배포 계획 발표 | +| `docs/rules/slides/` | N장 | 16:9 가로 | 정책 규칙 문서 | + +--- + +## 7. 폴더 구조 권장 패턴 + +새 PPTX를 만들 때 아래 구조를 따른다: + +``` +docs/my-document/ +├── slides/ ← HTML 슬라이드 파일들 +│ ├── slide-01.html +│ ├── slide-02.html +│ └── slide-03.html +├── convert.cjs ← 변환 스크립트 +└── my-document.pptx ← 생성된 PPTX (node convert.cjs 실행 후) +``` + +--- + +## 8. 문제 해결 + +| 증상 | 원인 | 해결 | +|------|------|------| +| `Error: dimensions don't match` | HTML body 크기 ≠ layout 설정 | body의 width/height와 `defineLayout` 값 맞추기 | +| 텍스트가 PPTX에서 줄바꿈됨 | `white-space: nowrap` 미적용 | 단일행 `

` 태그에 nowrap 추가 | +| 이미지 안 보임 | 상대 경로 사용 | 절대 경로(`/home/aweso/...`)로 변경 | +| `Cannot find module 'pptxgenjs'` | module.paths 설정 누락 | 스크립트 상단 2줄(module.paths.unshift) 확인 | +| `Cannot find module 'playwright'` | Playwright 미설치 | `cd ~/.claude/skills/pptx-skill/scripts && npx playwright install chromium` | +| PPTX는 생성되지만 빈 슬라이드 | HTML 내용 없음 | HTML 파일을 브라우저로 열어 확인 | + +--- + +## 9. 빠른 시작 (3분 가이드) + +### Step 1: 폴더 만들기 + +```bash +mkdir -p /home/aweso/sam/docs/my-pptx/slides +``` + +### Step 2: HTML 슬라이드 만들기 + +`slides/slide-01.html` 파일 생성: + +```html + + + + + + + +

MY COMPANY

+

발표 제목을 여기에

+

부제목 또는 설명

+ + +``` + +### Step 3: 변환 스크립트 만들기 + +`convert.cjs` 파일 생성: + +```javascript +const path = require('path'); +module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); +const PptxGenJS = require('pptxgenjs'); +const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); + +async function main() { + const pres = new PptxGenJS(); + pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM_16x9'; + + await html2pptx(path.join(__dirname, 'slides', 'slide-01.html'), pres); + + await pres.writeFile({ fileName: path.join(__dirname, 'my-presentation.pptx') }); + console.log('완료!'); +} +main().catch(console.error); +``` + +### Step 4: 실행 + +```bash +cd /home/aweso/sam/docs/my-pptx +node convert.cjs +# → my-presentation.pptx 생성됨 +``` + +--- + +**최종 업데이트**: 2026-03-01 diff --git a/guides/server-how-it-works.md b/guides/server-how-it-works.md new file mode 100644 index 0000000..974693b --- /dev/null +++ b/guides/server-how-it-works.md @@ -0,0 +1,247 @@ +# SAM 서버 동작 원리 초보자 가이드 + +> **작성일**: 2026-02-22 +> **대상**: SAM 프로젝트에 새로 합류한 개발자 + +--- + +## 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 +``` + +--- + +## 관련 문서 + +| 문서 | 설명 | +|------|------| +| [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 diff --git a/guides/table-design-guide.md b/guides/table-design-guide.md new file mode 100644 index 0000000..a920f08 --- /dev/null +++ b/guides/table-design-guide.md @@ -0,0 +1,486 @@ +# SAM 테이블 설계 가이드 — 비전문가용 + +> **작성일**: 2026-03-02 +> **대상 독자**: 개발자, 기획자, 관리자 — 데이터베이스를 잘 모르는 분도 읽을 수 있습니다 +> **관련 정책**: `standards/options-column-policy.md` (개발자 전용 상세 규칙) + +--- + +## 1. 이 문서는 왜 필요한가? + +SAM은 **여러 회사가 하나의 시스템을 공유**하는 구조입니다. +A회사, B회사, C회사가 모두 같은 프로그램을 쓰지만, 각 회사가 필요한 정보는 다릅니다. + +이 문서는 SAM에서 **데이터를 어떻게 저장하는지**, 그 설계 철학을 누구나 이해할 수 있도록 설명합니다. + +--- + +## 2. 기본 개념: 데이터베이스 테이블이란? + +데이터베이스 테이블은 **엑셀 시트**와 같습니다. + +``` +"주문" 테이블 (= 엑셀 시트) + + 열(컬럼) → 주문번호 │ 고객명 │ 금액 │ 상태 + ───────────────────────────────────────────────────────── + 행(레코드) 1 → ORD-001 │ 김철수 │ 500,000 │ 완료 + 행(레코드) 2 → ORD-002 │ 이영희 │ 300,000 │ 진행중 + 행(레코드) 3 → ORD-003 │ 박민수 │ 800,000 │ 대기 +``` + +- **열(컬럼)** = 정보의 종류 (주문번호, 고객명, 금액...) +- **행(레코드)** = 실제 데이터 한 건 (주문 1건) + +--- + +## 3. 문제: 회사마다 필요한 정보가 다르다 + +SAM은 여러 회사가 같은 테이블을 공유합니다. + +``` +같은 "주문" 테이블을 쓰는데... + + 🏭 A회사 (블라인드 제조) + → "절곡 각도", "날개 수" 정보가 필요해요 + + 🏭 B회사 (스크린 제조) + → "메시 밀도", "소재 종류" 정보가 필요해요 + + 🏭 C회사 (셔터 제조) + → "날개 간격", "색상 코드" 정보가 필요해요 +``` + +--- + +### 3.1 전통적인 해결 방법 (SAM은 이렇게 안 합니다) + +필요할 때마다 엑셀에 열을 추가하는 것처럼, 테이블에 컬럼을 추가합니다. + +``` +"주문" 테이블 — 전통적 방식 + + 주문번호 │ 고객명 │ 금액 │ 절곡각도 │ 날개수 │ 메시밀도 │ 소재 │ 날개간격 │ 색상코드 + ───────────────────────────────────────────────────────────────────────────────── + ORD-001 │ 김철수 │ 50만 │ 45도 │ 12개 │ (빈칸) │ (빈칸) │ (빈칸) │ (빈칸) ← A회사 + ORD-002 │ 이영희 │ 30만 │ (빈칸) │ (빈칸)│ 18 │ 폴리 │ (빈칸) │ (빈칸) ← B회사 + ORD-003 │ 박민수 │ 80만 │ (빈칸) │ (빈칸)│ (빈칸) │ (빈칸) │ 25mm │ #FF0000 ← C회사 +``` + +**문제점:** + +- 회사가 100개면? 열이 수백 개로 늘어남 +- 각 회사는 자기 것 빼고 전부 빈칸 +- 새 회사가 들어올 때마다 시스템 전체를 수정해야 함 +- 열 추가 = 시스템 중단 위험이 있는 작업 + +--- + +### 3.2 SAM의 해결 방법: "메모칸(options)" 하나로 통합 + +**핵심 열만 남기고**, 나머지는 **메모칸 하나**에 자유롭게 적습니다. + +``` +"주문" 테이블 — SAM 방식 + + 주문번호 │ 고객명 │ 금액 │ 상태 │ options (메모칸) + ──────────────────────────────────────────────────────────────────────── + ORD-001 │ 김철수 │ 50만 │ 완료 │ {"절곡각도": 45, "날개수": 12} ← A회사 + ORD-002 │ 이영희 │ 30만 │ 진행 │ {"메시밀도": 18, "소재": "폴리에스터"} ← B회사 + ORD-003 │ 박민수 │ 80만 │ 대기 │ {"날개간격": 25, "색상코드": "#FF0000"} ← C회사 + ORD-004 │ 최지은 │ 40만 │ 대기 │ null ← 메모 없음 +``` + +**`options`** = JSON이라는 형식의 메모칸. `{ }` 안에 자유롭게 정보를 넣을 수 있습니다. + +--- + +## 4. 어떤 정보를 열(컬럼)로 만들고, 어떤 정보를 메모칸(options)에 넣나? + +이것이 SAM 테이블 설계의 **가장 중요한 판단 기준**입니다. + +### 4.1 판단 흐름 (5가지 질문) + +새로운 정보를 저장해야 할 때, 아래 질문에 답합니다. + +``` +질문 1. 이 정보로 다른 테이블의 데이터를 연결(참조)하나? + 예: 고객ID로 고객 테이블을 찾는다 + → YES: 일반 컬럼 + +질문 2. 이 정보로 자주 검색(필터)하나? + 예: "완료" 상태인 주문만 보여줘 + → YES: 일반 컬럼 + +질문 3. 이 정보로 정렬하나? + 예: 최신 주문부터 보여줘 + → YES: 일반 컬럼 + +질문 4. 이 정보가 절대 중복되면 안 되나? + 예: 주문번호는 세상에 하나뿐이어야 한다 + → YES: 일반 컬럼 + +질문 5. 이 정보로 합계/평균을 계산하나? + 예: 이번 달 매출 합계 + → YES: 일반 컬럼 + +질문 1~5 전부 NO → options 메모칸에 저장 +``` + +### 4.2 실생활 예시로 비교 + +#### 예시 1: "주문" 테이블 + +| 정보 | 어디에 저장? | 이유 | +|------|:-----------:|------| +| 주문번호 | **일반 컬럼** | 중복 불가 + 검색 필수 | +| 고객 ID | **일반 컬럼** | 고객 테이블과 연결 | +| 금액 | **일반 컬럼** | 합계 계산 필요 | +| 상태 (진행/완료) | **일반 컬럼** | 필터(검색) 필수 | +| 생성일 | **일반 컬럼** | 정렬 필요 | +| 배송지 주소 | **options** | 부가 정보, 검색 안 함 | +| 수신자 이름 | **options** | 부가 정보 | +| 수신자 연락처 | **options** | 부가 정보 | +| 특이사항 메모 | **options** | 있어도 되고 없어도 됨 | + +**실제 SAM 코드에서 주문(Order) 테이블:** + +``` +일반 컬럼: id, tenant_id, order_number, client_id, total_amount, status, created_at +options: {"shipping_cost_code":"착불", "receiver":"홍길동", + "receiver_contact":"010-1234-5678", + "shipping_address":"서울 강남구 역삼동 123"} +``` + +#### 예시 2: "입고검사" 테이블 + +| 정보 | 어디에 저장? | 이유 | +|------|:-----------:|------| +| 품목 ID | **일반 컬럼** | 품목 테이블과 연결 | +| 수량 | **일반 컬럼** | 합계 계산 | +| 입고일 | **일반 컬럼** | 정렬 + 검색 | +| 제조사 | **options** | 모든 입고에 있지는 않음 | +| 검사 결과 (합격/불합격) | **options** | 검사를 안 하는 회사도 있음 | +| 검사일 | **options** | 선택적 정보 | + +**실제 SAM 코드에서 입고(Receiving) 테이블:** + +``` +일반 컬럼: id, tenant_id, item_id, quantity, received_at, status +options: {"manufacturer":"삼성전자", + "inspection_status":"합격", + "inspection_date":"2026-03-01"} +``` + +> 검사 결과가 options에 있는 이유: **모든 회사가 입고검사를 하는 것은 아닙니다.** +> A회사는 검사를 하고, B회사는 안 합니다. 이걸 일반 컬럼으로 만들면 B회사에겐 항상 빈칸입니다. + +#### 예시 3: "공정" 테이블 + +| 정보 | 어디에 저장? | 이유 | +|------|:-----------:|------| +| 공정 코드 | **일반 컬럼** | 중복 불가 + 검색 | +| 공정명 | **일반 컬럼** | 검색 + 표시 | +| 담당 부서 | **일반 컬럼** | 필터 | +| 작업일지 필요 여부 | **options** | 회사별로 다름 | +| 검사 필요 여부 | **options** | 회사별로 다름 | + +``` +일반 컬럼: id, tenant_id, process_code, process_name, department +options: {"needs_work_log": true, "needs_inspection": false} +``` + +--- + +## 5. 메모칸(options)의 실제 모습: JSON이란? + +`options`에 저장되는 데이터 형식은 **JSON**입니다. +JSON은 프로그래밍 세계의 "구조화된 메모장"이라고 생각하면 됩니다. + +### 5.1 JSON 기본 문법 + +``` +{ ← 시작 + "키": "값", ← 문자(텍스트) + "이름": "홍길동", + "나이": 30, ← 숫자 (따옴표 없음) + "합격": true, ← 참/거짓 (따옴표 없음) + "메모": null ← 값 없음 +} ← 끝 +``` + +### 5.2 중첩(nested) — 메모 안의 메모 + +``` +{ + "배송": { ← 배송 관련 정보를 묶음 + "주소": "서울 강남구 역삼동", + "수신자": "홍길동", + "연락처": "010-1234-5678" + }, + "검사": { ← 검사 관련 정보를 묶음 + "결과": "합격", + "검사일": "2026-03-01", + "검사자ID": 5 + } +} +``` + +### 5.3 목록(배열) — 여러 개를 나열 + +``` +{ + "선택지": [ ← 대괄호 [ ] = 목록 + {"label": "블라인드", "value": "blind"}, + {"label": "스크린", "value": "screen"}, + {"label": "셔터", "value": "shutter"} + ] +} +``` + +> 이 형태는 드롭다운 메뉴의 선택지 목록을 저장할 때 사용합니다. + +--- + +## 6. 멀티테넌시란? — 여러 회사가 하나의 시스템을 쓰는 구조 + +### 6.1 개념 + +``` +┌──────────────────────────────────────────────┐ +│ SAM 시스템 (하나의 프로그램) │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ A회사 │ │ B회사 │ │ C회사 │ │ +│ │ tenant=1 │ │ tenant=2 │ │ tenant=3 │ │ +│ │ │ │ │ │ │ │ +│ │ 블라인드 │ │ 스크린 │ │ 셔터 │ │ +│ │ 제조 │ │ 제조 │ │ 제조 │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ 같은 테이블을 쓰지만, │ +│ tenant_id로 데이터가 완전히 분리됨 │ +└──────────────────────────────────────────────┘ +``` + +### 6.2 tenant_id = 회사 식별 번호 + +모든 테이블의 모든 행에 `tenant_id`(회사 번호)가 붙어 있습니다. + +``` +"주문" 테이블 + + id │ tenant_id │ 주문번호 │ 금액 │ options + ─────────────────────────────────────────────────────────── + 1 │ 1 │ ORD-001 │ 50만 │ {"절곡각도": 45} ← A회사 데이터 + 2 │ 1 │ ORD-002 │ 30만 │ {"절곡각도": 90} ← A회사 데이터 + 3 │ 2 │ ORD-001 │ 80만 │ {"메시밀도": 18} ← B회사 데이터 + 4 │ 3 │ ORD-001 │ 40만 │ {"날개간격": 25} ← C회사 데이터 +``` + +**A회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 1`인 데이터만 보여줌 +**B회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 2`인 데이터만 보여줌 + +> A회사는 B회사의 데이터를 절대 볼 수 없습니다. 시스템이 자동으로 차단합니다. + +### 6.3 options + tenant_id = 강력한 조합 + +이 두 가지가 합쳐지면: + +``` +같은 테이블, 같은 컬럼 구조인데 + ✅ 회사마다 다른 데이터 (tenant_id로 분리) + ✅ 회사마다 다른 속성 (options로 유연하게) + ✅ 시스템 수정 없이 확장 가능 +``` + +--- + +## 7. SAM 테이블의 표준 구조 + +SAM에서 새 테이블을 만들면 항상 이 구조를 따릅니다. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SAM 표준 테이블 구조 │ +│ │ +│ ① 식별자 │ +│ id — 자동 생성 번호 (1, 2, 3...) │ +│ tenant_id — 어느 회사의 데이터인지 │ +│ │ +│ ② 핵심 정보 (검색/정렬/연결에 쓰는 것만) │ +│ code — 코드 (중복 불가) │ +│ status — 상태 (검색용) │ +│ is_active — 사용 여부 │ +│ sort_order — 표시 순서 │ +│ (+ FK 컬럼들) — 다른 테이블 연결 │ +│ │ +│ ③ 메모칸 │ +│ options — 나머지 전부 (JSON) │ +│ │ +│ ④ 감사 기록 (자동) │ +│ created_by — 누가 만들었나 │ +│ updated_by — 누가 수정했나 │ +│ deleted_by — 누가 삭제했나 │ +│ created_at — 언제 만들었나 │ +│ updated_at — 언제 수정했나 │ +│ deleted_at — 언제 삭제했나 (휴지통 개념) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 영역별 설명 + +| 영역 | 역할 | 비유 | +|------|------|------| +| ① 식별자 | "이 데이터가 누구 것인지" 구분 | 우편물의 받는 사람 + 주소 | +| ② 핵심 정보 | 검색, 정렬, 집계에 꼭 필요한 정보 | 엑셀의 고정 열 | +| ③ options | 회사마다 다른 부가 정보 | 엑셀의 "비고" 칸 (자유 서식) | +| ④ 감사 기록 | 언제 누가 뭘 했는지 자동 추적 | CCTV 기록 | + +--- + +## 8. 실제 SAM에서 options를 쓰는 테이블들 (22개) + +현재 SAM에서 options 메모칸을 사용하는 주요 테이블입니다. + +| 테이블 | 한글명 | options에 저장하는 정보 예시 | +|--------|--------|--------------------------| +| `orders` | 주문 | 배송지, 수신자, 연락처, 담당자 | +| `quotes` | 견적 | 견적 요약, 비용 항목, 가격 조정 | +| `receivings` | 입고 | 제조사, 검사 결과, 검사일 | +| `work_orders` | 작업지시 | 절곡 정보 (bending_info) | +| `work_order_items` | 작업지시 항목 | 작업 결과, 양품/불량 수량, LOT번호 | +| `processes` | 공정 | 작업일지 필요 여부, 검사 필요 여부 | +| `order_nodes` | 주문 노드 | 위치, 구역, 층, 실 (트리 구조) | +| `products` | 제품 | 동적 옵션 (라벨, 값, 단위) | +| `items` | 품목 | 품목별 동적 속성 | +| `materials` | 자재 | 자재 추가 속성 | +| `menus` | 메뉴 | 섹션, 메뉴 타입, 필요 권한 | +| `users` | 사용자 | 개인 설정/환경설정 | +| `tenants` | 회사(테넌트) | 회사 규모, 업종 | +| `document_template_section_fields` | 문서 양식 필드 | 선택지 목록, API 경로 | +| `item_fields` | 품목 필드 정의 | 필드별 세부 설정 | + +--- + +## 9. 자주 묻는 질문 (FAQ) + +### Q1. options에 넣으면 검색이 안 되나요? + +**아닙니다.** MySQL 8.0은 JSON 내부도 검색할 수 있습니다. + +``` +일반 컬럼 검색: "상태가 '완료'인 주문 찾아줘" → 매우 빠름 +options 검색: "제조사가 '삼성'인 입고 찾아줘" → 가능하지만 조금 느림 +``` + +다만, **매일 수천 번 검색하는 정보**라면 일반 컬럼으로 승격하는 것이 맞습니다. +가끔 검색하는 정보라면 options로 충분합니다. + +### Q2. options에 아무 정보나 마음대로 넣을 수 있나요? + +기술적으로는 가능하지만, 개발팀 내부에서 **어떤 키를 쓸지 미리 약속**합니다. + +``` +✅ 약속된 키: {"manufacturer": "삼성", "inspection_status": "합격"} +❌ 멋대로: {"asdf": 123, "temp_data": "???"} +``` + +코드에서 상수로 정의하여 일관성을 유지합니다. + +### Q3. 전통적 방식보다 뭐가 좋은 건가요? + +| 비교 항목 | 전통적 방식 (열 추가) | SAM 방식 (options JSON) | +|----------|:------------------:|:---------------------:| +| 새 정보 추가 시 | 시스템 수정 필요 | 코드만 변경 | +| 다른 회사에 영향 | 있음 (전체 구조 변경) | 없음 | +| 빈칸(null) 낭비 | 많음 | 없음 | +| 검색 속도 | 빠름 | 조금 느림 (충분히 실용적) | +| 유연성 | 낮음 | 높음 | +| 시스템 중단 위험 | 있음 (대형 테이블 수정 시) | 없음 | + +### Q4. 그럼 모든 정보를 options에 넣으면 되지 않나요? + +**아닙니다.** 핵심 정보는 반드시 일반 컬럼으로 만들어야 합니다. + +``` +❌ 나쁜 예: 모든 것을 options에 + + id │ tenant_id │ options + ────────────────────────────────────────────────────────────── + 1 │ 1 │ {"주문번호":"ORD-001", "금액":500000, "상태":"완료", ...} + + → 주문번호 검색 느림, 금액 합계 계산 불가, 중복 방지 불가 +``` + +``` +✅ 좋은 예: 핵심은 컬럼, 부가는 options + + id │ tenant_id │ order_number │ amount │ status │ options + ────────────────────────────────────────────────────────────── + 1 │ 1 │ ORD-001 │ 500000 │ 완료 │ {"배송지":"서울..."} + + → 검색 빠름, 합계 가능, 중복 방지 가능, 부가 정보도 유연 +``` + +### Q5. options 데이터는 화면에서 어떻게 보이나요? + +사용자 화면에서는 options 안에 있는지, 일반 컬럼인지 **구분할 수 없습니다**. +프로그램이 자동으로 꺼내서 보여줍니다. + +``` +화면에 보이는 모습: + + ┌─────────────────────────────────┐ + │ 입고 상세 정보 │ + │ │ + │ 품목: SUS304 스틸 │ ← 일반 컬럼 + │ 수량: 100개 │ ← 일반 컬럼 + │ 입고일: 2026-03-01 │ ← 일반 컬럼 + │ 제조사: 삼성전자 │ ← options에서 꺼냄 + │ 검사결과: 합격 │ ← options에서 꺼냄 + │ 검사일: 2026-03-01 │ ← options에서 꺼냄 + └─────────────────────────────────┘ + + 사용자는 어디에 저장되어 있는지 알 필요 없음! +``` + +--- + +## 10. 한 장 요약 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ │ +│ SAM 테이블 설계 = "핵심만 컬럼, 나머진 메모칸(options)" │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ tenant_id → 어느 회사 것인지 (자동 격리) │ │ +│ │ 핵심 컬럼들 → 검색/정렬/연결/집계에 쓰는 필수 정보 │ │ +│ │ options → 나머지 전부 (회사마다 다른 부가 정보) │ │ +│ │ 감사 컬럼들 → 누가/언제 만들고/수정하고/삭제했는지 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ 이렇게 하면: │ +│ ✅ 회사 추가해도 테이블 구조 안 바꿈 │ +│ ✅ 새 정보 추가해도 시스템 수정 최소화 │ +│ ✅ 회사마다 다른 정보를 유연하게 저장 │ +│ ✅ 데이터 보안 (회사 간 완전 분리) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 관련 문서 + +| 문서 | 설명 | 대상 | +|------|------|------| +| [options-column-policy.md](../standards/options-column-policy.md) | 개발자용 상세 정책 (코드 규칙, 마이그레이션 패턴) | 개발자 | +| [database/README.md](../system/database/README.md) | DB 스키마 전체 현황 (220개 모델) | 개발자 | +| [PROJECT_DEVELOPMENT_POLICY.md](PROJECT_DEVELOPMENT_POLICY.md) | 개발 공통 정책 (테이블 생성 절차) | 개발자 | +| [system/overview.md](../system/overview.md) | SAM 시스템 전체 아키텍처 | 전체 | + +--- + +**최종 업데이트**: 2026-03-02 diff --git a/plans/SAM_General_Rule_Storyboard_D1.0.md b/plans/SAM_General_Rule_Storyboard_D1.0.md new file mode 100644 index 0000000..ae23c7b --- /dev/null +++ b/plans/SAM_General_Rule_Storyboard_D1.0.md @@ -0,0 +1,737 @@ +# SAM General Rule Storyboard D1.0 + +> **작성일**: 2026-01-16 +> **버전**: D1.0 +> **원본**: `SAM_General_Rule_Storyboard_D1.0_260116.pdf` (43페이지) +> **상태**: PC 섹션 정리 완료 + +--- + +## 1. 문서 이력 + +| 날짜 | 버전 | 주요 내용 | 세부 내용 | +|------|------|----------|----------| +| 2026-01-15 | D0.9 | 초안 | General Rule - PC, 태블릿, 모바일 UIUX 공통 작성 | +| 2026-01-16 | D1.0 | 작성 | PC 섹션 정리 33p | + +--- + +## 2. 인터랙션 (Interaction) + +> **페이지**: 4 + +사용자 입력 제스처 및 적용 여부를 정의한다. + +| Type | 제스처/마크 | 설명 | 적용 | +|------|-----------|------|------| +| 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 | + +--- + +## 3. 반응형 웹 (Responsive Web) + +> **페이지**: 5 + +### 3.1 브레이크 포인트 + +| 디바이스 | 브레이크 포인트 | Tailwind 접두사 | +|---------|---------------|----------------| +| 모바일 | < 640px | 기본 | +| 태블릿 | 768px ~ 1280px | `md` | +| 데스크탑 | 1280px+ | `lg` | +| 대형 모니터 | 1920px+ | `xl` | + +### 3.2 레이아웃 구성 + +**PC Web 레이아웃**: +1. Contents 영역 +2. Footer 영역 + +**Mobile Web 레이아웃**: +1. Contents 영역 +2. Footer 영역 + +--- + +## 4. 화면 템플릿 (Screen Template) + +> **페이지**: 6 + +모바일 웹 화면 구조를 정의한다. + +| 영역 | 코드 | 설명 | +|------|------|------| +| Status bar | A | 안테나, 통화, 배터리 등 시스템 OS 관리 영역. 모든 페이지 상단에 존재 | +| Browser 영역 | B | 브라우저 기능 영역 | +| Title 영역 | C | 텍스트 또는 기능 버튼으로 구현됨. 텍스트는 기본 가운데 정렬 | +| Content 영역 | D | 컨텐츠 내용 표시. 컨텐츠 길이가 길어질 경우 스크롤 제공 | +| Browser bar 영역 | E | 브라우저 유틸 바 영역 | +| Keypad 영역 | F | 키보드 입력할 때 활성화. 모든 페이지 위에 덮어쓰기 구현 | + +--- + +## 5. 메시지 (Notifications) + +> **페이지**: 7 + +| 유형 | 설명 | +|------|------| +| 알림 Alert | 사용자에게 상황을 알려주기 위한 팝업. `[확인]` 버튼 제공 | +| 확인 Alert | 사용자에게 확인이 필요할 경우 제공되는 팝업. `[취소]` `[확인]` 버튼 제공 | +| 토스트 메시지 | 단순 Notify. 2~3초 후 페이지 내에서 Fade out | + +--- + +## 6. GNB, LNB, 푸터 + +> **페이지**: 8 + +PC 화면의 전체 레이아웃 구조를 정의한다. + +### 6.1 구성 요소 + +| 번호 | 영역 | 설명 | +|------|------|------| +| 01 | 메뉴 버튼 | 클릭: 메뉴 영역(06) 축소/확장 토글. 디폴트: 메뉴 영역 확장 상태 | +| 02 | SAM 로고 버튼 | 클릭: 대시보드 화면으로 이동 | +| 03 | 알림 버튼 | 클릭: 알림 팝업 표시 | +| 04 | 개인 정보 버튼 | 항목: 디폴트 이미지, 이름, 직급. 클릭: 마이페이지 팝업 표시 | +| 05 | 회사 로고 | 회사정보 화면에서 등록한 로고 표시. 회사 변경 선택 시 해당 로고 변경 | +| 06 | 메뉴 영역 | 메뉴 클릭: 하위 메뉴 있을 경우 하단에 표시, 없을 경우 해당 메뉴 화면으로 이동. 목록 길 경우 해당 영역 내 스크롤 처리 | +| 07 | MES 메뉴 영역 | 영업관리, 판매관리, 구매관리 등 해당하는 MES 메뉴 영역 표시 | +| 08 | 푸터 영역 | 모든 화면 하단 공통 표시 | +| 09 | SAM AI 채팅 버튼 | 클릭: SAM AI 채팅 팝업 표시 | + +### 6.2 메뉴 목록 + +- 대시보드 +- MES 메뉴 +- 인사관리 +- 전자결재 +- 게시판 +- 회계관리 +- 기준정보 +- 보고서 및 분석 +- 계정정보 +- 회사정보 +- 구독관리 +- 결제내역 +- 고객센터 + +### 6.3 푸터 내용 + +``` +(C) 2025 SAM. All right reserved. +Codebridge X +상호: 코드 브릿지 엑스 대표: 이경호 사업자등록번호: 123-45-12345 +주소: 서울특별시 강서구 양천로 583 우림블루나인 B동 1602호 (우: 07547) +팩스: 02-123-1234 통신판매업신고번호: 제 2019-서울강서-0001호 +서비스이용문의: 02-1234-1234 이메일: cs@a.com +서비스 이용약관 | 개인정보 취급방침 +``` + +--- + +## 7. 메뉴, 페이지, 섹션, 항목 영역 + +> **페이지**: 9 + +### 7.1 영역 구분 + +| 번호 | 영역 | 설명 | +|------|------|------| +| 01 | 메뉴 영역 | 축소 상태 | +| 02 | 페이지 영역 | - | +| 03 | 섹션 영역 | - | +| 04 | 항목 영역 | - | + +### 7.2 텍스트 오버플로우 처리 + +텍스트가 영역보다 길 경우 "텍스트+..." 형태로 표시한다. + +--- + +## 8. 메뉴 목록 (3Depth) + +> **페이지**: 10 + +### 8.1 메뉴 계층 구조 + +| 번호 | 레벨 | 설명 | +|------|------|------| +| 01 | 대메뉴 | 1Depth 메뉴 | +| 02 | 중메뉴 | 2Depth 메뉴 (대메뉴 클릭 시 하단에 표시) | +| 03 | 소메뉴 | 3Depth 메뉴 (중메뉴 클릭 시 하단에 표시) | + +**메뉴 확장 예시**: +``` +대시보드 +MES 메뉴 +인사관리 +전자결재 + - 중메뉴명 + - 중메뉴명 + · 소메뉴명 + · 소메뉴명 + - 중메뉴명 + - 중메뉴명 + - 중메뉴명 +게시판 +... +``` + +--- + +## 9. 알림 팝업 + +> **페이지**: 11 +> **경로**: 메인 > 알림 팝업 + +### 9.1 구성 요소 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 알림 목록 | 각 디폴트 썸네일, 종류(공지사항, 안내), 제목/내용, 전송일시 표시. 클릭: 해당 상세 화면으로 이동. 최신순 10개까지 표시 | +| 02 | New 아이콘 | 새 알림일 경우 New 아이콘 표시. 해당 알림 클릭 시 사라짐 | +| 02-1 | 붉은 점 아이콘 | 새 알림이 있을 경우 표시. 해당 알림 모두 클릭 시 사라짐 | + +--- + +## 10. 마이페이지 팝업 + +> **페이지**: 12 +> **경로**: 메인 > 마이페이지 팝업 + +### 10.1 구성 요소 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 계정 아이디 (이메일) | 이메일 주소 표시 (예: `name@company.com`) | +| 02 | 회사 셀렉트 박스 | 종류: 회사명 목록 (해당 계정이 생성한 회사(테넌트) 목록 표시). 정렬: 등록순. 한 회사만 소유중일 경우에는 해당 영역 숨김 | +| 03 | 로그아웃 버튼 | 클릭: "정말 로그아웃하시겠습니까?" 로그아웃 확인 Alert 표시. 확인 버튼 클릭시 로그아웃 처리 | + +--- + +## 11. 셀렉트 박스 + +> **페이지**: 13 + +### 11.1 기본 셀렉트 박스 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 셀렉트 박스 | 클릭: 하단에 종류 목록 표시 | +| 02 | 종류 목록 | 목록 중 하나만 선택 가능 | + +### 11.2 다중 선택 셀렉트 박스 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 03 | 다중 선택 셀렉트 박스 | 선택된 첫번째 항목명 + 추가 수 표시. 텍스트 영역 부족할 경우 `항목..+3` 형태로 표시 | +| 04 | 다중 선택 종류 목록 | 목록 중 복수 선택 가능. 전체 선택 시 전체 선택/해제 토글 | + +### 11.3 검색 셀렉트 박스 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 05 | 검색 영역 | 검색어 입력 후 엔터 또는 검색 아이콘 클릭 시 검색 상태로 전환되며 검색 결과 표시 | +| 05-3 | 삭제 버튼 | 클릭: 검색어 삭제 처리, 전체 종류 목록 표시 | + +### 11.4 셀렉트 박스 유형 정리 + +| 유형 | 단일 선택 | 다중 선택 | 검색 | 검색+다중 선택 | +|------|----------|----------|------|--------------| +| 선택 방식 | 하나만 | 복수 | 하나만 | 복수 | +| 검색 기능 | X | X | O | O | +| 전체 선택 | X | O | X | O | + +--- + +## 12. 가이드 메시지 + +> **페이지**: 14 + +상황에 따라 입력 필드 하단 또는 Alert에 가이드 메시지를 표시한다. + +### 12.1 표시 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 가이드 메시지 표시 위치 | 입력 필드 하단에 표시 | +| - | 긍정 메시지 | 녹색으로 표시 | +| 01-1 | 부정 메시지 | 붉은색으로 표시 | + +--- + +## 13. 태블릿/모바일 헤더 + +> **페이지**: 15 + +### 13.1 동작 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 태블릿/모바일 헤더 | 하단으로 스크롤 시 숨김. 역스크롤 시 표시 | + +### 13.2 적용 화면 + +- TABLET 가로 목록 +- TABLET 세로 목록 +- MOBILE 가로 목록 +- MOBILE 세로 목록 + +--- + +## 14. 태블릿/모바일 바텀 버튼 영역 + +> **페이지**: 16 + +### 14.1 동작 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 태블릿/모바일 바텀 버튼 영역 | 최하단 바텀에 플로팅 표시. 하단으로 스크롤 시 숨김. 역스크롤 시 표시 | + +### 14.2 버튼 예시 + +- `[수정]` `[삭제]` + +--- + +## 15. 공지 팝업 + +> **페이지**: 17 + +### 15.1 구성 + +| 항목 | 설명 | +|------|------| +| 대상 | 전체, 설정 부서 | +| 내용 | 설정 기간동안 대상에게 팝업 표시 | + +### 15.2 구성 요소 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 팝업 내용 영역 | 이미지, 텍스트 | +| 02 | 1일간 이 창을 열지 않음 체크박스 | 클릭: 체크 설정/해제 토글. 디폴트: 체크 해제 상태. 체크 설정 시 1일 동안 팝업 미표시 (자정 기준) | + +--- + +## 16. 목록 화면 - 4단계 반응형 + +> **페이지**: 18~25 + +PC, TABLET, MOBILE 환경에서 목록 화면의 4단계 반응형 표시를 정의한다. + +### 16.1 반응형 단계 개요 + +| 단계 | 디바이스 | 화면명 | +|------|---------|--------| +| 1단계 | PC | PC_목록 | +| 2단계 | TABLET 가로 | TABLET_가로_목록 | +| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_목록, MOBILE_가로_목록 | +| 4단계 | MOBILE 세로 | MOBILE_세로_목록 | + +### 16.2 PC_목록 (1단계) + +> **페이지**: 19 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 헤더 영역 | 항목 클릭: 값이 국문/영문/숫자일 경우 오름/내림차순으로 토글 | +| 02 | 정렬 아이콘 | 현재 칼럼으로 정렬 상태일 경우에만 표시 | + +**목록 테이블 예시 칼럼**: + +| 칼럼 | 설명 | +|------|------| +| 시공번호 | 고유 식별 번호 | +| 거래처 | 회사명 | +| 현장명 | 현장 이름 | +| 공사PM | 담당 PM | +| 작업반장 | 작업반장 이름 | +| 작업자 | 작업자 수 | +| 시공투입일 | 시공 투입 날짜 | +| 시공완료일 | 시공 완료 날짜 | +| 상태 | 시공대기, 시공진행, 시공완료 | + +### 16.3 TABLET_가로_목록 (2단계) + +> **페이지**: 20 + +- PC와 동일한 테이블 구조 +- 사이드 메뉴가 아이콘 축소 상태로 변경 + +### 16.4 TABLET_세로_목록 (3단계) + +> **페이지**: 21~22 + +- 테이블 대신 카드형 목록으로 전환 +- 각 카드에 시공번호와 상태 표시 +- 카드 클릭 시 확장되어 상세 정보 표시 + +**확장 시 표시 항목**: + +| 필드 | 예시 값 | +|------|---------| +| 거래처 | 회사명 | +| 현장명 | 현장명 | +| 공사PM | 홍길동 | +| 작업반장 | 홍길동 | +| 작업자 | 3 | +| 시공투입일 | 2026-01-01 | +| 시공완료일 | 2026-01-01 | + +### 16.5 MOBILE_가로_목록 (3단계) + +> **페이지**: 23~24 + +- 카드형 목록 +- 시공번호와 상태 표시 +- 클릭 시 확장하여 상세 항목 표시 + +### 16.6 MOBILE_세로_목록 (4단계) + +> **페이지**: 25 + +- 카드형 목록 (세로 스크롤) +- 클릭 시 확장하여 상세 항목 표시 +- 확장 시 거래처, 현장명, 공사PM, 작업반장, 작업자, 시공투입일, 시공완료일 표시 + +--- + +## 17. 상세 화면 - 4단계 반응형 + +> **페이지**: 26~31 + +PC, TABLET, MOBILE 환경에서 상세 화면의 4단계 반응형 표시를 정의한다. + +### 17.1 반응형 단계 개요 + +| 단계 | 디바이스 | 화면명 | +|------|---------|--------| +| 1단계 | PC | PC_상세 | +| 2단계 | TABLET 가로 | TABLET_가로_상세 | +| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_상세, MOBILE_가로_상세 | +| 4단계 | MOBILE 세로 | MOBILE_세로_상세 | + +### 17.2 PC_상세 (1단계) + +> **페이지**: 27 + +- 페이지 제목: "메뉴 상세" + 설명: "메뉴 상세를 관리합니다" +- 섹션명: "시공 정보" +- 버튼: `[수정]` `[삭제]` + +**표시 항목 예시**: + +| 필드 | 예시 값 | +|------|---------| +| 시공번호 | 123123 | +| 상태 | 시공진행 | +| 현장 | 현장명 | +| 작업반장 | 홍길동 (셀렉트 박스) | +| 시공투입일 | 2025-12-15 | +| 시공완료일 | 2025-12-15 | +| 항목명 | 항목 (다수) | + +### 17.3 TABLET_가로_상세 (2단계) + +> **페이지**: 28 + +- PC와 동일한 상세 정보 표시 +- 사이드 메뉴 아이콘 축소 상태 + +### 17.4 TABLET_세로_상세 (3단계) + +> **페이지**: 29 + +- 항목 수가 줄어들며 스크롤로 나머지 확인 + +### 17.5 MOBILE_가로_상세 (3단계) + +> **페이지**: 30 + +- 상세 항목을 세로 배치 +- 바텀에 `[수정]` `[삭제]` 버튼 플로팅 + +### 17.6 MOBILE_세로_상세 (4단계) + +> **페이지**: 31 + +- 모든 항목 세로 배치 +- 바텀에 `[수정]` `[삭제]` 버튼 플로팅 + +--- + +## 18. PC 섹션 정리 + +> **페이지**: 32~33 + +PC 화면의 섹션 레이아웃 및 필터/정렬 구성을 정의한다. + +### 18.1 필터 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 라디오 버튼형 필터 | 선택 값이 2개일 경우 사용 (예: 수취/발행) | +| 02 | 필터 셀렉트 박스 | 최소로만 활용 | +| 03 | 표 헤더 정렬 | 표 헤더 정렬로 정렬 셀렉트 박스는 삭제 | + +### 18.2 PC 섹션 구성 요소 + +**상단 영역**: +- 페이지 제목 + 설명 +- 집계 카드 (예: `수취어음 55건`, `발행어음 1건`, `만기임박 5건`, `결제완료 15건`) +- 기간 선택 (날짜 범위 + 단축 버튼: 전전월, 어제, 오늘, 전월, 당월, 당해년도) +- 버튼: `[버튼명]` `[버튼명]` `[버튼명]` +- 탭: 탭1, 탭2, 탭3 + +**필터 영역**: +- 셀렉트 박스 필터 (전체) +- 라디오 버튼형 필터 (수취/발행) +- 상태 셀렉트 박스 (보관중) +- `[저장]` 버튼 + +**목록 테이블 예시**: + +| No. | 어음번호 | 구분 | 거래처 | 금액 | 발행일 | 만기일 | 차수 | 상태 | +|-----|---------|------|--------|------|--------|--------|------|------| +| 7 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 1 | 보관중 | +| 6 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 2 | 만기임박 | + +**하단 정보**: `총 7건` / `1건 선택` + +--- + +## 19. TBD (미정) + +> **페이지**: 34 + +추후 결정 예정 영역이다. + +--- + +## 20. 나의 메뉴 + +> **페이지**: 35~38 + +### 20.1 나의 메뉴 - 없음 + +> **페이지**: 35 + +- 나의 메뉴가 설정되지 않은 상태 +- 콘텐츠 상단에 `[...]` 아이콘만 표시 + +### 20.2 나의 메뉴 - 있음 + +> **페이지**: 36 + +- 나의 메뉴가 1개 설정된 상태 +- 콘텐츠 상단에 나의 메뉴명 탭 표시 (예: `메뉴관리`) + +### 20.3 나의 메뉴 - 여러 줄 + +> **페이지**: 37 + +- 나의 메뉴가 여러 개 설정된 상태 +- 콘텐츠 상단에 여러 메뉴명이 나열됨 +- 줄바꿈되어 여러 줄로 표시 가능 (예: `메뉴관리 메뉴명 메뉴명 메뉴명 ...`) + +### 20.4 나의 메뉴 - 메뉴 영역에 통합 + +> **페이지**: 38 + +- 좌측 메뉴 영역에 "메뉴" / "나의 메뉴" 탭으로 통합 +- 메뉴 탭: 일반 메뉴 목록 표시 +- 나의 메뉴 탭: 사용자 즐겨찾기 메뉴 표시 + +--- + +## 21. 검색, 필터, 정렬 모음 + +> **페이지**: 39 + +### 21.1 구성 요소 + +| 영역 | 구성 | +|------|------| +| 기간 선택 | 날짜 범위 (`2025-09-01 ~ 2025-09-03`) + 단축 버튼 (전전월, 어제, 오늘, 전월, 당월, 당해년도) | +| 검색바 | 검색 입력 필드 | +| 필터 셀렉트 박스 | 복수의 전체 셀렉트 박스 | +| 정렬 | 최신순 셀렉트 박스 | +| 항목 필터 | 항목명 태그 형태로 나열 | + +--- + +## 22. 페이지 설정 버튼 + +> **페이지**: 40 + +### 22.1 기능 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 섹션 표시 및 순서 변경 | 페이지 내 섹션 ON/OFF 토글 및 순서 변경 | +| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 | + +### 22.2 설정 패널 구성 + +- 버전기록 +- 가져오기 +- 내보내기 +- 섹션 목록: 각 섹션별 ON/OFF 토글 + +**예시**: +``` +섹션명 [ON] +섹션명 [ON] +섹션명 [ON] +섹션명 [ON] +섹션명 [ON] +``` + +--- + +## 23. 섹션 설정 버튼 + +> **페이지**: 41 + +### 23.1 기능 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 항목 표시 및 순서 변경 | 섹션 내 항목 ON/OFF 토글 및 순서 변경 | +| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 | + +### 23.2 설정 패널 구성 + +- 가져오기 +- 내보내기 +- 항목 목록: 각 항목별 ON/OFF 토글 + +**예시**: +``` +기간 [ON] +기간단축버튼 [ON] +검색바 [ON] +필터명 [ON] +필터명 [ON] +필터명 [ON] +필터명 [ON] +정렬 [ON] +``` + +--- + +## 24. 태스크 알림 아이콘 + +> **페이지**: 42~43 + +### 24.1 동작 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 태스크 알림 아이콘 | 태스크가 추가될 경우 카운트하여 표시 | +| - | 메뉴 확장 시 표시 | 대/중/소메뉴로 확장될 경우 해당 메뉴에 아이콘 표시 | +| - | 카운트 범위 | 최소 1 ~ 최대 99 | + +### 24.2 표시 예시 + +**축소 상태**: 대메뉴 옆에 카운트 배지 표시 (예: `전자결재 [3]`) + +**확장 상태 (2Depth)**: +``` +전자결재 [3] + - 중메뉴명 [2] + - 중메뉴명 [1] + - 중메뉴명 + - 중메뉴명 + - 중메뉴명 +``` + +**확장 상태 (3Depth)**: +``` +전자결재 [3] + - 중메뉴명 + - 중메뉴명 [1] + · 소메뉴명 [1] + · 소메뉴명 + - 중메뉴명 + - 중메뉴명 + - 중메뉴명 +``` + +### 24.3 페이지 내 표시 + +- 메뉴 축소 상태에서도 대메뉴 아이콘 옆에 카운트 배지 표시 + +--- + +## 부록: 페이지 맵 + +| 페이지 | 섹션 | 화면명 | +|--------|------|--------| +| 1 | 표지 | SAM_General Rule | +| 2 | 문서 이력 | Document History | +| 3 | 공통 | - | +| 4 | 인터랙션 | Interaction | +| 5 | 반응형 웹 | Responsive Web | +| 6 | 화면 템플릿 | Screen Template | +| 7 | 메시지 | Notifications | +| 8 | GNB, LNB, 푸터 | GNB, LNB, 푸터 | +| 9 | 영역 구분 | 메뉴, 페이지, 섹션, 항목 영역 | +| 10 | 메뉴 목록 | 메뉴 목록 3Depth | +| 11 | 알림 팝업 | 알림 팝업 | +| 12 | 마이페이지 | 마이페이지 팝업 | +| 13 | 셀렉트 박스 | 셀렉트 박스 (기본/다중/검색) | +| 14 | 가이드 메시지 | 가이드 메시지 | +| 15 | 태블릿/모바일 헤더 | 태블릿/모바일 헤더 | +| 16 | 태블릿/모바일 바텀 버튼 | 태블릿/모바일 바텀 버튼 영역 | +| 17 | 공지 팝업 | 공지 팝업 | +| 18 | (구분) | PC, TABLET, MOBILE - 목록 4단계 | +| 19 | 목록 1단계 | PC_목록 | +| 20 | 목록 2단계 | TABLET_가로_목록 | +| 21 | 목록 3단계 | TABLET_세로_목록 | +| 22 | 목록 3단계 확장 | TABLET_세로_목록_확장 | +| 23 | 목록 3단계 | MOBILE_가로_목록 | +| 24 | 목록 3단계 확장 | MOBILE_가로_목록_확장 | +| 25 | 목록 4단계 | MOBILE_세로_목록, MOBILE_세로_목록_확장 | +| 26 | (구분) | PC, TABLET, MOBILE - 상세 4단계 | +| 27 | 상세 1단계 | PC_상세 | +| 28 | 상세 2단계 | TABLET_가로_상세 | +| 29 | 상세 3단계 | TABLET_세로_상세 | +| 30 | 상세 3단계 | MOBILE_가로_상세 | +| 31 | 상세 4단계 | MOBILE_세로_상세 | +| 32 | (구분) | 섹션 정리 | +| 33 | 섹션 정리 | PC 섹션 정리 | +| 34 | TBD | 미정 | +| 35 | 나의 메뉴 | 나의 메뉴_없음 | +| 36 | 나의 메뉴 | 나의 메뉴_있음 | +| 37 | 나의 메뉴 | 나의 메뉴_여러 줄 | +| 38 | 나의 메뉴 | 나의 메뉴_메뉴 영역에 통합 | +| 39 | 검색/필터/정렬 | 검색, 필터, 정렬 모음 | +| 40 | 페이지 설정 | 페이지 설정 버튼 | +| 41 | 섹션 설정 | 섹션 설정 버튼 | +| 42~43 | 태스크 알림 | 태스크 알림 아이콘 | + +--- + +## 관련 문서 + +- [SAM ERP 회계관리 스토리보드 D1.6](SAM_ERP_회계관리_Storyboard_D1.6.md) +- 원본 PDF: `SAM_General_Rule_Storyboard_D1.0_260116.pdf` + +--- + +**최종 업데이트**: 2026-02-23 diff --git a/plans/ai-quotation-engine-plan.md b/plans/ai-quotation-engine-plan.md new file mode 100644 index 0000000..c1b5098 --- /dev/null +++ b/plans/ai-quotation-engine-plan.md @@ -0,0 +1,928 @@ +# AI 견적서 자동생성 엔진 개발 계획 + +> **작성일**: 2026-03-02 +> **상태**: 기획 초안 +> **프로젝트**: SAM API + MNG +> **우선순위**: 🔴 필수 +> **참조**: `docs/features/ai/README.md`, `docs/features/quotes/README.md`, `docs/rules/customer-pricing.md` + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM 계약 완료 후 매니저가 고객사 직원과 인터뷰를 진행할 때, **인터뷰 내용을 AI가 분석하여 SAM 표준 견적서 형태로 자동 변환**하는 엔진을 구축한다. + +현재 매니저가 수동으로 수행하는 프로세스: + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ SAM 계약 │ → │ 현장 인터뷰 │ → │ 업무 파악 │ → │ 견적서 작성 │ +│ 완료 │ │ (매니저+직원)│ │ (수동 정리) │ │ (수동 작성) │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ +``` + +AI 엔진 도입 후: + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐ +│ SAM 계약 │ → │ 현장 인터뷰 │ → │ AI 엔진 │ → │ 견적서 초안 │ +│ 완료 │ │ (매니저+직원)│ │ (음성/텍스트 분석) │ │ (자동 생성) │ +│ │ │ │ │ (업무 매핑) │ │ (매니저 확인)│ +└──────────────┘ └──────────────┘ └──────────────────────┘ └──────────────┘ +``` + +### 1.2 핵심 원칙 + +| 원칙 | 설명 | +|------|------| +| **자체 AI 엔진** | Claude API 기반으로 SAM 전용 AI 서비스 구축 | +| **기존 인프라 활용** | `ai_configs`, `ai_token_usages`, `ai_pricing_configs` 테이블 재사용 | +| **견적 시스템 연동** | 기존 `quotes`, `quote_items` 테이블과 직접 연동 | +| **Multi-tenant** | 모든 데이터에 `tenant_id` 격리 적용 | +| **점진적 확장** | Phase 1 텍스트 → Phase 2 음성 (STT 구현 완료, 통합만 필요) → Phase 3 학습 고도화 | + +### 1.3 기존 인프라 현황 + +#### AI 인프라 (구축 완료) + +| 구성요소 | 상태 | 비고 | +|---------|------|------| +| `ai_configs` 테이블 | ✅ 완료 | Claude provider 설정 가능 | +| `ai_token_usages` 테이블 | ✅ 완료 | 토큰/비용 자동 추적 | +| `ai_pricing_configs` 테이블 | ✅ 완료 | Claude 모델 단가 등록 | +| `AiTokenHelper` | ✅ 완료 | `saveClaudeUsage()` 메서드 존재 | +| MNG AI 설정 UI | ✅ 완료 | `/system/ai-config` | + +#### 견적 시스템 (구축 완료) + +| 구성요소 | 상태 | 비고 | +|---------|------|------| +| `quotes` 테이블 | ✅ 완료 | 견적 마스터 | +| `quote_items` 테이블 | ✅ 완료 | 견적 품목 상세 | +| `QuoteService` | ✅ 완료 | 견적 CRUD, 상태 관리 | +| `QuoteCalculationService` | ✅ 완료 | BOM 10단계 계산 | +| 견적 API 엔드포인트 | ✅ 완료 | REST API 전체 | + +#### 음성 녹음/STT (구축 완료) + +| 구성요소 | 상태 | 비고 | +|---------|------|------| +| `ai_voice_recordings` 테이블 | ✅ 완료 | DB 스키마 + CRUD | +| GCS 업로드 | ✅ 완료 | `GoogleCloudService` — GCS 저장/조회/삭제 | +| Google Cloud STT 변환 | ✅ 완료 | `GoogleCloudService::speechToText()` — LongRunningRecognize, ko-KR | +| Web Speech API (브라우저 STT) | ✅ 완료 | `voice-recorder.blade.php` — 실시간 음성→텍스트 (무료) | +| STT + Gemini AI 분석 | ✅ 완료 | `AiVoiceRecordingService` — 음성→STT→AI 분석 파이프라인 | +| 화자 분리 (Diarization) | ✅ 완료 | `MeetingMinuteService` — Speaker Diarization | +| 영업 상담 음성 녹음 | ✅ 완료 | `ConsultationController` — MediaRecorder + STT + GCS 백업 | + +> **참조 구현 파일:** +> - `mng/app/Services/GoogleCloudService.php` — Google Cloud STT/GCS 통합 서비스 +> - `mng/app/Services/AiVoiceRecordingService.php` — STT + Gemini 분석 +> - `mng/app/Services/MeetingMinuteService.php` — 회의록 STT + 화자분리 +> - `mng/app/Http/Controllers/Sales/ConsultationController.php` — 영업 상담 음성 +> - `mng/resources/views/sales/modals/voice-recorder.blade.php` — 브라우저 음성 녹음 UI +> - `docs/features/voice-input-stt-guide.md` — STT 기술 가이드 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 기획 초안 작성 | +| **다음 작업** | Phase 1 상세 설계 → 구현 | +| **진행률** | 0/4 Phase (0%) | +| **마지막 업데이트** | 2026-03-02 | + +--- + +## 2. 시스템 아키텍처 + +### 2.1 전체 구조 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SAM AI 견적 엔진 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────┐ ┌───────────────────────┐ │ +│ │ 입력 채널 │ │ AI 분석 파이프라인 │ │ +│ │ │ │ │ │ +│ │ 📝 텍스트 │───→│ 1. 인터뷰 전처리 │ │ +│ │ 🎤 음성(P2) │ │ 2. 업무 도메인 분류 │ │ +│ │ 📄 문서(P3) │ │ 3. SAM 모듈 매핑 │ │ +│ └───────────────┘ │ 4. 견적 항목 추출 │ │ +│ │ 5. 금액 산출 │ │ +│ └──────────┬────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ 견적서 생성기 │ │ +│ │ │ │ +│ │ SAM 표준 모듈 카탈로그 ←──→ Claude API │ │ +│ │ 고객 요금 정책 ←──→ 프롬프트 엔진 │ │ +│ │ 기존 견적 템플릿 ←──→ 결과 파서 │ │ +│ └──────────────────────┬────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ 출력 │ │ +│ │ │ │ +│ │ 📊 견적서 초안 (quotes 테이블) │ │ +│ │ 📋 업무 분석 리포트 │ │ +│ │ 💡 추천 모듈 목록 │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Claude API 연동 구조 + +``` +┌──────────────────┐ ┌──────────────────┐ +│ SAM API Server │ │ Claude API │ +│ (Laravel) │ │ (Anthropic) │ +│ │ │ │ +│ AiQuotation │ ──HTTP──→ Messages API │ +│ Service │ ←─JSON── (claude-sonnet) │ +│ │ │ │ +│ ┌────────────┐ │ │ System Prompt: │ +│ │ Prompt │ │ │ - SAM 모듈 목록 │ +│ │ Engine │ │ │ - 요금 정책 │ +│ │ │ │ │ - 견적 구조 │ +│ │ - 모듈목록 │ │ │ - 출력 형식 │ +│ │ - 요금표 │ │ │ │ +│ │ - 템플릿 │ │ │ │ +│ └────────────┘ │ │ │ +└──────────────────┘ └──────────────────┘ +``` + +### 2.3 데이터 흐름 + +``` +매니저 인터뷰 입력 + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 1: 인터뷰 전처리 │ +│ - 텍스트 정규화 (불필요한 표현 제거) │ +│ - 핵심 키워드 추출 │ +│ - 업무 도메인 태깅 │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 2: Claude API 1차 호출 — 업무 분석 │ +│ - 고객사 업종/규모 파악 │ +│ - 현재 업무 프로세스 분석 │ +│ - 디지털화 필요 영역 식별 │ +│ - Pain Point 도출 │ +│ 출력: 구조화된 업무 분석 JSON │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 3: SAM 모듈 매핑 │ +│ - 업무 분석 결과 ↔ SAM 모듈 카탈로그 대조 │ +│ - 필수/선택 모듈 분류 │ +│ - 사용자 수, 데이터량 추정 │ +│ 출력: 추천 모듈 목록 + 근거 │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 4: Claude API 2차 호출 — 견적 생성 │ +│ - SAM 요금 정책 적용 │ +│ - 모듈별 개발비 + 월 구독료 계산 │ +│ - 추가 옵션 (AI 토큰, 저장공간) 산출 │ +│ - 할인 정책 적용 (통합 패키지 등) │ +│ 출력: 견적서 JSON (SAM 표준 형식) │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 5: 견적서 초안 저장 │ +│ - quotes 테이블에 저장 (status: draft) │ +│ - quote_items에 모듈별 항목 저장 │ +│ - 업무 분석 리포트 첨부 │ +│ - 매니저에게 알림 → 검토/수정 → 확정 │ +└──────────────────────────────────────────────────┘ +``` + +--- + +## 3. SAM 모듈 카탈로그 (AI 프롬프트용) + +> **참조**: `docs/rules/customer-pricing.md` + +AI가 인터뷰 내용을 SAM 견적으로 변환하려면, SAM이 제공하는 모듈과 요금을 정확히 알아야 한다. 이 데이터는 **DB 테이블로 관리**하여 프롬프트에 동적 주입한다. + +### 3.1 모듈 카탈로그 테이블 설계 + +```sql +CREATE TABLE ai_quotation_modules ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + module_code VARCHAR(50) NOT NULL, -- 'HR', 'SALES', 'FINANCE' 등 + module_name VARCHAR(100) NOT NULL, -- '인사관리', '영업관리', '재무관리' + category ENUM('basic', 'individual', 'addon') NOT NULL, + description TEXT, -- 모듈 기능 설명 + keywords JSON, -- AI 매핑용 키워드 목록 + dev_cost DECIMAL(12,0) DEFAULT 0, -- 개발비 (원) + monthly_fee DECIMAL(10,0) DEFAULT 0, -- 월 구독료 (원) + is_active BOOLEAN DEFAULT TRUE, + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_tenant (tenant_id), + INDEX idx_category (category), + UNIQUE KEY uk_tenant_module (tenant_id, module_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 3.2 초기 데이터 (customer-pricing.md 기준) + +| module_code | module_name | category | dev_cost | monthly_fee | +|-------------|-------------|----------|----------|-------------| +| `BASIC_PKG` | 기본 패키지 (인사+근태+급여+게시판) | basic | 5,000,000 | 200,000 | +| `HR` | 인사관리 | individual | 2,000,000 | 80,000 | +| `ATTENDANCE` | 근태관리 | individual | 1,500,000 | 60,000 | +| `PAYROLL` | 급여관리 | individual | 2,500,000 | 100,000 | +| `BOARD` | 게시판/공지사항 | individual | 500,000 | 20,000 | +| `SALES` | 영업관리 (CRM+견적+수주) | individual | 5,000,000 | 150,000 | +| `PURCHASE` | 구매/자재관리 | individual | 3,000,000 | 100,000 | +| `PRODUCTION` | 생산관리 (MES) | individual | 8,000,000 | 250,000 | +| `QUALITY` | 품질관리 | individual | 4,000,000 | 120,000 | +| `FINANCE` | 재무/회계관리 | individual | 5,000,000 | 150,000 | +| `LOGISTICS` | 물류/출하관리 | individual | 3,000,000 | 100,000 | +| `APPROVAL` | 전자결재 | individual | 3,000,000 | 80,000 | +| `DOCUMENT` | 문서관리 (전자서명) | individual | 2,000,000 | 60,000 | +| `EQUIPMENT` | 설비관리 | individual | 3,000,000 | 100,000 | +| `INTEGRATED` | 통합 패키지 | basic | 30,000,000 | 800,000 | +| `AI_TOKEN` | AI 토큰 추가 | addon | 0 | 별도 | +| `STORAGE` | 파일 저장공간 추가 | addon | 0 | 별도 | +| `CUSTOM_DEV` | 커스텀 개발 | addon | 별도 협의 | 0 | + +### 3.3 keywords 필드 예시 + +```json +// HR 모듈 +{ + "keywords": ["직원", "사원", "인사", "조직도", "부서", "입퇴사", "인력"], + "pain_points": ["엑셀로 직원 관리", "입퇴사 관리가 번거로움", "조직도 없음"], + "business_needs": ["직원 정보 통합", "조직 구조 관리", "인력 현황 파악"] +} + +// PRODUCTION 모듈 +{ + "keywords": ["생산", "제조", "작업지시", "공정", "LOT", "불량", "MES"], + "pain_points": ["생산 현황을 수기로 기록", "불량 추적 불가", "납기 관리 어려움"], + "business_needs": ["실시간 생산현황", "불량률 관리", "작업지시 자동화"] +} +``` + +--- + +## 4. AI 프롬프트 엔진 + +### 4.1 시스템 프롬프트 구조 + +``` +┌─────────────────────────────────────────────────────┐ +│ System Prompt │ +├─────────────────────────────────────────────────────┤ +│ │ +│ [역할 정의] │ +│ 너는 SAM ERP/MES 솔루션의 컨설팅 AI이다. │ +│ 고객 인터뷰를 분석하여 맞춤형 견적서를 작성한다. │ +│ │ +│ [SAM 모듈 카탈로그] ← DB에서 동적 로드 │ +│ 각 모듈의 기능, 키워드, 가격 정보 │ +│ │ +│ [요금 정책] ← customer-pricing 기반 │ +│ 기본패키지, 개별모듈, 추가옵션 요금 체계 │ +│ 할인 정책 (통합 패키지 할인 등) │ +│ │ +│ [출력 형식] │ +│ JSON Schema 명시 (견적서 구조) │ +│ │ +│ [분석 지침] │ +│ - 고객 업종/규모별 권장 모듈 기준 │ +│ - 우선순위 결정 기준 (필수 vs 선택) │ +│ - 비용 최적화 원칙 (패키지 vs 개별) │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +### 4.2 프롬프트 템플릿 (1차: 업무 분석) + +``` +당신은 SAM(Smart Automation Management) ERP/MES 솔루션의 전문 컨설턴트입니다. + +아래는 고객사 직원과의 인터뷰 내용입니다. 이를 분석하여 구조화된 업무 분석 보고서를 작성하세요. + +## 고객 정보 +- 회사명: {company_name} +- 업종: {industry} +- 직원 수: {employee_count} +- 인터뷰 대상: {interviewee_role} + +## 인터뷰 내용 +{interview_content} + +## 분석 기준 +다음 SAM 모듈 영역에 맞춰 분석하세요: +{module_catalog_json} + +## 출력 형식 (JSON) +{ + "company_analysis": { + "industry": "업종 분류", + "scale": "소규모/중소/중견", + "current_systems": ["현재 사용 중인 시스템"], + "digitalization_level": "상/중/하" + }, + "business_domains": [ + { + "domain": "인사/급여", + "current_process": "현재 처리 방식 설명", + "pain_points": ["문제점 1", "문제점 2"], + "improvement_needs": ["개선 필요사항"], + "priority": "필수/높음/보통/낮음", + "matched_modules": ["HR", "PAYROLL"] + } + ], + "recommendations": { + "essential_modules": ["반드시 필요한 모듈 코드"], + "recommended_modules": ["권장 모듈 코드"], + "optional_modules": ["선택 모듈 코드"], + "package_suggestion": "BASIC_PKG 또는 INTEGRATED 또는 individual", + "reasoning": "패키지 추천 근거" + } +} +``` + +### 4.3 프롬프트 템플릿 (2차: 견적 생성) + +``` +아래 업무 분석 결과를 바탕으로 SAM 견적서를 생성하세요. + +## 업무 분석 결과 +{analysis_result_json} + +## SAM 요금 정책 +{pricing_policy_json} + +## 견적 생성 규칙 +1. 필수 모듈은 반드시 포함 +2. 통합 패키지가 개별 합산보다 저렴하면 패키지 추천 +3. 직원 수 기반 사용자 라이선스 산출 +4. AI 토큰은 월 기본 100만 토큰 포함, 초과분 별도 +5. 파일 저장공간은 기본 10GB, 초과분 별도 + +## 출력 형식 (JSON) +{ + "quotation": { + "title": "견적서 제목", + "client_name": "고객사명", + "valid_until": "견적 유효기간", + "items": [ + { + "category": "기본서비스/추가모듈/추가옵션", + "module_code": "모듈코드", + "module_name": "모듈명", + "description": "포함 기능 설명", + "dev_cost": 0, + "monthly_fee": 0, + "quantity": 1, + "note": "비고" + } + ], + "summary": { + "total_dev_cost": 0, + "total_monthly_fee": 0, + "discount_type": "패키지할인/볼륨할인/없음", + "discount_rate": 0, + "final_dev_cost": 0, + "final_monthly_fee": 0 + }, + "implementation_plan": { + "estimated_months": 0, + "phases": [ + { + "phase": 1, + "name": "단계명", + "modules": ["모듈코드"], + "duration_weeks": 0 + } + ] + }, + "analysis_summary": "업무 분석 요약 (고객 설명용)" + } +} +``` + +--- + +## 5. API 설계 + +### 5.1 엔드포인트 + +| Method | Path | 설명 | +|--------|------|------| +| `POST` | `/api/v1/ai/quotation/analyze` | 인터뷰 분석 (1차) | +| `POST` | `/api/v1/ai/quotation/generate` | 견적서 생성 (2차) | +| `POST` | `/api/v1/ai/quotation/generate-full` | 분석+생성 통합 (원스텝) | +| `GET` | `/api/v1/ai/quotation/{id}` | AI 견적 상세 조회 | +| `GET` | `/api/v1/ai/quotation` | AI 견적 목록 | +| `PUT` | `/api/v1/ai/quotation/{id}` | AI 견적 수정 (매니저) | +| `POST` | `/api/v1/ai/quotation/{id}/confirm` | 정식 견적으로 전환 | +| `DELETE` | `/api/v1/ai/quotation/{id}` | AI 견적 삭제 | + +### 5.2 요청/응답 예시 + +#### POST `/api/v1/ai/quotation/analyze` + +**Request:** +```json +{ + "client_id": 15, + "client_name": "(주)대한기계", + "industry": "기계제조업", + "employee_count": 45, + "interviewee_role": "관리부 팀장", + "interview_content": "현재 직원 관리는 엑셀로 하고 있어요. 출퇴근도 수기로 기록하고... 영업팀에서는 견적서를 한글 프로그램으로 만들어서 이메일로 보내는데, 이력 관리가 안 돼요. 생산 현장에서는 작업일보를 종이에 쓰고 있고, 불량이 나면 어디서 발생했는지 추적이 안 됩니다. 재고도 실사를 해봐야 알 수 있어요...", + "interview_type": "text" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "id": 1, + "analysis": { + "company_analysis": { + "industry": "기계제조업", + "scale": "중소기업 (45명)", + "current_systems": ["엑셀", "한글 프로그램", "종이 문서"], + "digitalization_level": "하" + }, + "business_domains": [ + { + "domain": "인사/급여", + "current_process": "엑셀로 직원 관리, 수기 출퇴근 기록", + "pain_points": ["인사정보 분산", "출퇴근 수기 기록"], + "priority": "필수", + "matched_modules": ["HR", "ATTENDANCE", "PAYROLL"] + }, + { + "domain": "영업관리", + "current_process": "한글 프로그램 견적서, 이메일 발송", + "pain_points": ["견적 이력 관리 불가", "영업 현황 파악 어려움"], + "priority": "높음", + "matched_modules": ["SALES"] + }, + { + "domain": "생산관리", + "current_process": "종이 작업일보, 수동 불량 관리", + "pain_points": ["불량 추적 불가", "생산현황 실시간 파악 불가"], + "priority": "필수", + "matched_modules": ["PRODUCTION", "QUALITY"] + }, + { + "domain": "재고/물류", + "current_process": "실사로만 재고 파악", + "pain_points": ["실시간 재고 파악 불가"], + "priority": "높음", + "matched_modules": ["PURCHASE", "LOGISTICS"] + } + ], + "recommendations": { + "essential_modules": ["HR", "ATTENDANCE", "PAYROLL", "PRODUCTION", "QUALITY"], + "recommended_modules": ["SALES", "PURCHASE", "LOGISTICS"], + "optional_modules": ["APPROVAL", "DOCUMENT"], + "package_suggestion": "INTEGRATED", + "reasoning": "8개 이상 모듈이 필요하므로 통합 패키지(30,000,000원)가 개별 합산(34,500,000원)보다 경제적" + } + }, + "token_usage": { + "prompt_tokens": 1250, + "completion_tokens": 890, + "cost_krw": 45 + } + } +} +``` + +### 5.3 컨트롤러 / 서비스 구조 + +``` +app/Http/Controllers/Api/V1/ +└── AiQuotationController.php + ├── analyze() ← 인터뷰 분석 + ├── generate() ← 견적 생성 + ├── generateFull() ← 통합 (분석+생성) + ├── index() ← 목록 + ├── show() ← 상세 + ├── update() ← 수정 + ├── confirm() ← 정식 견적 전환 + └── destroy() ← 삭제 + +app/Services/ +└── AiQuotationService.php + ├── analyzeInterview() ← 1차: 인터뷰 분석 + ├── generateQuotation() ← 2차: 견적 생성 + ├── generateFull() ← 통합 처리 + ├── confirmToQuote() ← quotes 테이블로 전환 + │ + ├── buildAnalysisPrompt() ← 분석 프롬프트 조립 + ├── buildQuotationPrompt() ← 견적 프롬프트 조립 + ├── loadModuleCatalog() ← DB에서 모듈 카탈로그 로드 + ├── loadPricingPolicy() ← DB에서 요금 정책 로드 + ├── callClaudeApi() ← Claude API 호출 + ├── parseResponse() ← 응답 JSON 파싱 + └── saveTokenUsage() ← 토큰 사용량 기록 + +app/Http/Requests/V1/AiQuotation/ +├── AiQuotationAnalyzeRequest.php +├── AiQuotationGenerateRequest.php +├── AiQuotationUpdateRequest.php +└── AiQuotationConfirmRequest.php +``` + +--- + +## 6. 데이터베이스 설계 + +### 6.1 신규 테이블 + +#### ai_quotations (AI 견적 마스터) + +```sql +CREATE TABLE ai_quotations ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + + -- 고객 정보 + client_id BIGINT UNSIGNED NULL, + client_name VARCHAR(200) NOT NULL, + industry VARCHAR(100) NULL, + employee_count INT NULL, + interviewee_role VARCHAR(100) NULL, + + -- 인터뷰 데이터 + interview_content TEXT NOT NULL, + interview_type ENUM('text', 'voice', 'document') DEFAULT 'text', + voice_recording_id BIGINT UNSIGNED NULL, -- ai_voice_recordings 참조 + + -- AI 분석 결과 + analysis_result JSON NULL, -- 1차 분석 결과 + quotation_result JSON NULL, -- 2차 견적 결과 + + -- 상태 관리 + status ENUM('analyzing', 'analyzed', 'generating', 'generated', + 'confirmed', 'failed') DEFAULT 'analyzing', + error_message TEXT NULL, + + -- 연결 + quote_id BIGINT UNSIGNED NULL, -- 정식 견적 전환 시 quotes.id + + -- 금액 요약 + total_dev_cost DECIMAL(12,0) DEFAULT 0, + total_monthly_fee DECIMAL(10,0) DEFAULT 0, + discount_rate DECIMAL(5,2) DEFAULT 0, + final_dev_cost DECIMAL(12,0) DEFAULT 0, + final_monthly_fee DECIMAL(10,0) DEFAULT 0, + + -- 토큰 사용 + total_tokens INT DEFAULT 0, + total_cost_krw DECIMAL(12,2) DEFAULT 0, + + -- 감사 + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_status (tenant_id, status), + INDEX idx_tenant_client (tenant_id, client_id), + INDEX idx_created (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +#### ai_quotation_items (AI 견적 항목) + +```sql +CREATE TABLE ai_quotation_items ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + ai_quotation_id BIGINT UNSIGNED NOT NULL, + + category ENUM('basic', 'module', 'addon') NOT NULL, + module_code VARCHAR(50) NOT NULL, + module_name VARCHAR(100) NOT NULL, + description TEXT NULL, + + dev_cost DECIMAL(12,0) DEFAULT 0, + monthly_fee DECIMAL(10,0) DEFAULT 0, + quantity INT DEFAULT 1, + priority ENUM('essential', 'recommended', 'optional') DEFAULT 'recommended', + + ai_reasoning TEXT NULL, -- AI가 이 모듈을 추천한 근거 + matched_pain_points JSON NULL, -- 매칭된 고객 Pain Point + + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_quotation (ai_quotation_id), + FOREIGN KEY (ai_quotation_id) REFERENCES ai_quotations(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 6.2 기존 테이블 활용 + +| 테이블 | 용도 | 연동 방식 | +|--------|------|----------| +| `ai_configs` | Claude API 키/모델 설정 | provider='claude' 조회 | +| `ai_token_usages` | 토큰 비용 추적 | menu_name='AI견적' | +| `ai_pricing_configs` | Claude 모델 단가 | provider='claude' | +| `quotes` | 정식 견적 전환 대상 | `confirm` 시 생성 | +| `clients` | 고객사 정보 | client_id 참조 | + +--- + +## 7. Phase별 개발 계획 + +### Phase 1: 텍스트 인터뷰 → AI 견적 (MVP) + +> **목표**: 텍스트 인터뷰 입력 → Claude 분석 → 견적서 자동생성 +> **기간**: 2~3주 +> **우선순위**: 🔴 필수 + +| 단계 | 작업 | 프로젝트 | 상태 | +|------|------|---------|------| +| 1-1 | `ai_quotation_modules` 마이그레이션 + 시더 | API | ⏳ | +| 1-2 | `ai_quotations`, `ai_quotation_items` 마이그레이션 | API | ⏳ | +| 1-3 | 모델 생성 (`AiQuotation`, `AiQuotationItem`, `AiQuotationModule`) | API | ⏳ | +| 1-4 | `AiQuotationService` — Claude API 연동 | API | ⏳ | +| 1-5 | 프롬프트 엔진 구현 (분석 + 견적 템플릿) | API | ⏳ | +| 1-6 | `AiQuotationController` + FormRequest | API | ⏳ | +| 1-7 | API 라우트 등록 (`routes/api/v1/common.php`) | API | ⏳ | +| 1-8 | 정식 견적 전환 로직 (`confirmToQuote`) | API | ⏳ | +| 1-9 | MNG AI 견적 관리 화면 (목록/상세/수정) | MNG | ⏳ | +| 1-10 | 테스트 (인터뷰 샘플 3건 이상) | API | ⏳ | + +### Phase 2: 음성 인터뷰 연동 + +> **목표**: 현장에서 녹음한 음성 → STT 변환 → AI 분석 → 견적 생성 +> **기간**: 1주 (기존 STT 인프라 재사용으로 단축) +> **선행 조건**: Phase 1 완료 + +| 단계 | 작업 | 프로젝트 | 상태 | 비고 | +|------|------|---------|------|------| +| 2-1 | Google STT 서비스 구현 | MNG | ✅ 완료 | `GoogleCloudService::speechToText()` 재사용 | +| 2-2 | 음성 업로드 API (GCS 저장) | MNG | ✅ 완료 | `AiVoiceRecordingService`, `ConsultationController` 재사용 | +| 2-3 | STT → 텍스트 변환 파이프라인 | MNG | ✅ 완료 | LongRunningRecognize + 폴링 패턴 구현됨 | +| 2-4 | 음성 인터뷰 → AI 견적 통합 플로우 | API | ⏳ | 기존 STT 결과를 Claude 분석에 연결 | +| 2-5 | MNG 음성 녹음 업로드 UI | MNG | ✅ 완료 | `voice-recorder.blade.php` 재사용 가능 | + +> **핵심**: Google Cloud STT, GCS, 브라우저 음성 녹음 UI가 모두 구현 완료 상태. +> Phase 2에서는 기존 STT 결과를 AI 견적 파이프라인(Claude API)에 연결하는 **통합 플로우(2-4)만 신규 개발**하면 된다. + +### Phase 3: 학습 데이터 고도화 + +> **목표**: 과거 견적 데이터를 활용하여 AI 정확도 향상 +> **기간**: 2주 +> **선행 조건**: Phase 1 + 실제 사용 데이터 축적 + +| 단계 | 작업 | 프로젝트 | 상태 | +|------|------|---------|------| +| 3-1 | 과거 견적 데이터 → 프롬프트 Few-shot 예시 구성 | API | ⏳ | +| 3-2 | 확정된 AI 견적 → 학습 데이터 피드백 루프 | API | ⏳ | +| 3-3 | 업종별 견적 패턴 분석 → 추천 정확도 향상 | API | ⏳ | +| 3-4 | 프롬프트 A/B 테스트 프레임워크 | API | ⏳ | + +### Phase 4: 고객 셀프서비스 (확장) + +> **목표**: 고객이 직접 간단한 질문에 답변하면 견적 자동 생성 +> **기간**: 3주 +> **선행 조건**: Phase 1~3 안정화 + +| 단계 | 작업 | 프로젝트 | 상태 | +|------|------|---------|------| +| 4-1 | 고객용 인터뷰 설문 폼 설계 | React | ⏳ | +| 4-2 | 단계별 질문 → AI 분석 통합 | API + React | ⏳ | +| 4-3 | 견적서 미리보기 + PDF 다운로드 | React | ⏳ | +| 4-4 | 매니저 알림 → 후속 상담 연결 | API + MNG | ⏳ | + +--- + +## 8. Claude API 연동 상세 + +### 8.1 SDK 설치 + +```bash +# API 프로젝트에 Anthropic SDK 설치 +docker exec sam-api-1 composer require anthropic-ai/laravel +``` + +> **참고**: `anthropic-ai/laravel` 패키지는 Laravel용 공식 래퍼로, HTTP Client 기반으로 Claude API를 호출한다. 미출시/미지원 시 `GuzzleHttp`로 직접 HTTP 호출한다. + +### 8.2 Claude API 호출 패턴 + +```php +// app/Services/AiQuotationService.php + +class AiQuotationService +{ + private function callClaudeApi(string $systemPrompt, string $userMessage): array + { + // 1. ai_configs에서 Claude 설정 로드 + $config = AiConfig::where('provider', 'claude') + ->where('is_active', true) + ->first(); + + // 2. HTTP 호출 + $response = Http::withHeaders([ + 'x-api-key' => $config->api_key, + 'anthropic-version' => '2023-06-01', + 'content-type' => 'application/json', + ])->post($config->base_url . '/messages', [ + 'model' => $config->model, // claude-sonnet-4-20250514 + 'max_tokens' => 4096, + 'temperature' => 0.3, // 견적은 일관성 중요 + 'system' => $systemPrompt, + 'messages' => [ + ['role' => 'user', 'content' => $userMessage] + ], + ]); + + // 3. 토큰 사용량 기록 + $usage = $response->json('usage'); + AiTokenHelper::saveClaudeUsage( + tenantId: auth()->user()->tenant_id, + menuName: 'AI견적', + promptTokens: $usage['input_tokens'], + completionTokens: $usage['output_tokens'], + model: $config->model, + ); + + // 4. 응답 파싱 + $content = $response->json('content.0.text'); + return json_decode($content, true); + } +} +``` + +### 8.3 비용 예측 + +| 항목 | 토큰 | 비용 (USD) | 비용 (KRW) | +|------|------|-----------|------------| +| 1차 분석 프롬프트 (시스템+사용자) | ~2,000 입력 | $0.0005 | ~1원 | +| 1차 분석 응답 | ~1,500 출력 | $0.0019 | ~3원 | +| 2차 견적 프롬프트 | ~3,000 입력 | $0.0008 | ~1원 | +| 2차 견적 응답 | ~2,000 출력 | $0.0025 | ~4원 | +| **견적 1건 합계** | **~8,500** | **~$0.006** | **~9원** | + +> Claude Sonnet 기준. 1건당 약 **9원**으로 매우 경제적이다. + +--- + +## 9. MNG 관리 화면 + +### 9.1 화면 목록 + +| 화면 | URL | 설명 | +|------|-----|------| +| AI 견적 목록 | `/ai-quotation` | 생성된 AI 견적 목록 | +| AI 견적 생성 | `/ai-quotation/create` | 인터뷰 입력 폼 | +| AI 견적 상세 | `/ai-quotation/{id}` | 분석 결과 + 견적서 조회 | +| AI 견적 수정 | `/ai-quotation/{id}/edit` | 매니저가 수정 | +| 모듈 카탈로그 관리 | `/ai-quotation/modules` | SAM 모듈 목록 관리 | + +### 9.2 AI 견적 생성 화면 구성 + +``` +┌─────────────────────────────────────────────────────┐ +│ AI 견적서 생성 │ +├─────────────────────────────────────────────────────┤ +│ │ +│ [고객 정보] │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ 고객사 선택 ▼│ │ 업종 │ │ +│ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ 직원 수 │ │ 인터뷰 대상 │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +│ [인터뷰 내용] │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ (텍스트 입력 또는 음성 녹음 업로드) │ │ +│ │ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +│ [🔄 분석 시작] [📊 분석+견적 한번에] │ +│ │ +├─────────────────────────────────────────────────────┤ +│ [분석 결과] (Ajax 응답) │ +│ │ +│ 📊 업무 도메인 분석 │ +│ ┌──────────┬──────────┬──────────┬──────────┐ │ +│ │ 도메인 │ 현재상태 │ 문제점 │ 추천모듈 │ │ +│ ├──────────┼──────────┼──────────┼──────────┤ │ +│ │ 인사/급여 │ 엑셀관리 │ 분산관리 │ HR,PAYROLL│ │ +│ │ 생산관리 │ 종이기록 │ 추적불가 │PRODUCTION │ │ +│ └──────────┴──────────┴──────────┴──────────┘ │ +│ │ +│ 💰 견적서 초안 │ +│ ┌──────────┬──────────┬──────────┬──────────┐ │ +│ │ 모듈 │ 개발비 │ 월구독료 │ 우선순위 │ │ +│ ├──────────┼──────────┼──────────┼──────────┤ │ +│ │ 통합패키지 │30,000,000│ 800,000 │ 필수 │ │ +│ │ AI토큰 │ 0│ 50,000 │ 선택 │ │ +│ └──────────┴──────────┴──────────┴──────────┘ │ +│ 합계: 개발비 30,000,000원 / 월 850,000원 │ +│ │ +│ [✅ 정식 견적으로 전환] [✏️ 수정] [🗑️ 삭제] │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 10. 보안 및 운영 + +### 10.1 보안 고려사항 + +| 항목 | 대책 | +|------|------| +| API 키 노출 | `ai_configs` 테이블에 암호화 저장, .env 폴백 | +| 인터뷰 데이터 보호 | tenant_id 격리, 접근 권한 제어 | +| Claude API 비용 제어 | 일일/월별 토큰 한도 설정 (ai_configs.options) | +| 프롬프트 인젝션 | 사용자 입력 sanitize, 시스템 프롬프트 분리 | + +### 10.2 모니터링 + +| 항목 | 방법 | +|------|------| +| API 호출 성공/실패 | `ai_quotations.status` + `error_message` | +| 토큰 사용량 | `ai_token_usages` 테이블 (기존 인프라) | +| 비용 추적 | MNG `/system/ai-token-usage` (기존 UI) | +| 견적 전환율 | `ai_quotations` → `quotes` 전환 비율 통계 | + +### 10.3 에러 처리 + +```php +try { + $result = $this->callClaudeApi($systemPrompt, $userMessage); +} catch (ConnectionException $e) { + // Claude API 연결 실패 + $aiQuotation->update(['status' => 'failed', 'error_message' => 'Claude API 연결 실패']); +} catch (JsonException $e) { + // 응답 JSON 파싱 실패 + $aiQuotation->update(['status' => 'failed', 'error_message' => 'AI 응답 파싱 실패']); +} +``` + +--- + +## 11. 기대 효과 + +| 항목 | Before (현재) | After (AI 엔진) | +|------|--------------|-----------------| +| 견적 작성 시간 | 2~4시간 (수동) | 5~10분 (AI 초안 + 검토) | +| 모듈 누락 위험 | 매니저 경험 의존 | AI가 체계적으로 분석 | +| 고객 맞춤화 | 표준 템플릿 복사 | 인터뷰 기반 맞춤 견적 | +| 비용 최적화 | 수동 비교 | AI가 패키지 vs 개별 자동 비교 | +| 견적 1건 AI 비용 | — | ~9원 (Claude Sonnet) | + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-03-02 | 기획 초안 작성 | + +--- + +## 관련 문서 + +| 문서 | 경로 | +|------|------| +| SAM 프로젝트 개요 | `docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` | +| 견적 기능 상세 | `docs/features/quotes/README.md` | +| 견적 시스템 분석 | `docs/data/견적/견적시스템_분석문서.md` | +| AI 기능 현황 | `docs/features/ai/README.md` | +| AI 설정 가이드 | `docs/guides/ai-config-settings.md` | +| 고객 요금 안내 | `docs/rules/customer-pricing.md` | +| 내부 과금 정책 | `docs/rules/billing-policy.md` | +| 단가 정책 | `docs/rules/pricing-policy.md` | +| Plans 가이드 | `docs/plans/GUIDE.md` | + +--- + +**최종 업데이트**: 2026-03-02 diff --git a/plans/attendance-management-plan.md b/plans/attendance-management-plan.md new file mode 100644 index 0000000..c5f8889 --- /dev/null +++ b/plans/attendance-management-plan.md @@ -0,0 +1,284 @@ +# MNG 근태현황 개발 계획서 + +> **작성일**: 2026-02-26 +> **상태**: 계획 수립 + +--- + +## 1. 개요 + +### 1.1 목적 + +MNG 인사관리 > 근태현황 기능을 완성한다. 현재 기본 CRUD가 구현되어 있으나, 미완성 기능과 알려진 버그를 해결하고 실무에 필요한 추가 기능을 구현한다. + +### 1.2 현재 상태 분석 + +#### 구현 완료 + +| 항목 | 상태 | 파일 | +|------|------|------| +| 근태 목록 조회 (HTMX 테이블) | ✅ | `index.blade.php`, `table.blade.php` | +| 월간 통계 카드 (5종) | ✅ | `index.blade.php` | +| 필터 (이름, 부서, 상태, 날짜) | ✅ | `index.blade.php` | +| 등록/수정 모달 | ✅ | `index.blade.php` | +| CRUD API (목록/등록/수정/삭제) | ✅ | `AttendanceController.php` (API) | +| AttendanceService | ✅ | `AttendanceService.php` | +| Attendance 모델 (8개 상태) | ✅ | `Attendance.php` | +| Soft Delete | ✅ | 모델 + 서비스 | + +#### 알려진 문제 (E2E 테스트 결과) + +| 문제 | 심각도 | 설명 | +|------|--------|------| +| 엑셀 다운로드 미구현 | 🟡 중요 | 버튼 없음, API 미연결 | +| 근태 등록 서버 에러 | 🔴 필수 | 모달 submit 시 500 에러 발생 가능 | + +#### 미구현 기능 (API 대비) + +| 기능 | API 지원 | MNG 상태 | +|------|---------|---------| +| 엑셀 내보내기 | ✅ `/v1/attendances/export` | ❌ 미구현 | +| 일괄 삭제 | ✅ `/v1/attendances/bulk-delete` | ❌ 미구현 | +| 개인별 근태 상세 | ✅ `/v1/attendances/{id}` | ❌ 미구현 | +| 월간 요약 통계 | ✅ `/v1/attendances/monthly-stats` | ⚠️ 기본만 구현 | +| 출퇴근 설정 관리 | ✅ `attendance_settings` 테이블 | ❌ 미구현 | + +--- + +## 2. 구현 범위 + +### 2.1 Phase 1: 버그 수정 + 핵심 기능 (우선) + +| # | 작업 | 난이도 | 설명 | +|---|------|--------|------| +| 1-1 | 근태 등록/수정 버그 수정 | 🟢 낮음 | store/update API 요청 오류 점검 및 수정 | +| 1-2 | 엑셀 다운로드 | 🟢 낮음 | API `/v1/attendances/export` 연동, 다운로드 버튼 추가 | +| 1-3 | 일괄 삭제 | 🟡 보통 | 체크박스 선택 → 일괄 삭제 버튼 | +| 1-4 | 월간 통계 기간 선택 | 🟢 낮음 | 현재 당월 고정 → 연/월 선택 가능하게 | + +### 2.2 Phase 2: 확장 기능 + +| # | 작업 | 난이도 | 설명 | +|---|------|--------|------| +| 2-1 | 개인별 근태 상세 페이지 | 🟡 보통 | 사원 클릭 → 월간 달력 + 출퇴근 이력 | +| 2-2 | 출퇴근 설정 관리 | 🟡 보통 | 표준 출근시간, GPS 사용여부, 허용반경 설정 | +| 2-3 | 월간/주간 요약 뷰 | 🟡 보통 | 부서별/사원별 근태 요약 테이블 | +| 2-4 | 근태 일괄 등록 | 🔴 높음 | 날짜 범위 + 대상 사원 → 일괄 근태 등록 | + +--- + +## 3. 상세 설계 + +### 3.1 Phase 1-1: 근태 등록/수정 버그 수정 + +**점검 항목**: +- MNG `AttendanceController::store()` validation 규칙과 실제 폼 데이터 일치 여부 +- `check_in`, `check_out` 포맷 (HH:MM vs HH:MM:SS) 불일치 가능성 +- `user_id` 전달 누락 여부 +- HTMX `hx-headers` CSRF 토큰 전달 확인 + +**수정 대상 파일**: +- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` +- `mng/resources/views/hr/attendances/index.blade.php` (JS `submitAttendance()`) + +--- + +### 3.2 Phase 1-2: 엑셀 다운로드 + +**방식**: MNG에서 직접 엑셀 생성 (API 서버 미경유) + +**구현**: +1. `AttendanceService::getExportData()` 메서드 추가 +2. `AttendanceController::export()` 메서드 추가 +3. 라우트: `GET /api/admin/hr/attendances/export` +4. 인덱스 페이지에 다운로드 버튼 추가 + +**엑셀 컬럼**: + +| 컬럼 | 값 | +|------|-----| +| 날짜 | `base_date` | +| 사원명 | `user.name` | +| 부서 | `department.name` | +| 상태 | `status_label` | +| 출근 | `check_in` | +| 퇴근 | `check_out` | +| 비고 | `remarks` | + +**수정 대상 파일**: +- `mng/app/Services/HR/AttendanceService.php` +- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` +- `mng/routes/api.php` +- `mng/resources/views/hr/attendances/index.blade.php` + +--- + +### 3.3 Phase 1-3: 일괄 삭제 + +**UI**: 테이블 각 행에 체크박스 추가, 헤더에 전체선택, 상단에 "선택 삭제" 버튼 + +**구현**: +1. `table.blade.php`에 체크박스 컬럼 추가 +2. Alpine.js 컴포넌트로 선택 상태 관리 +3. `AttendanceController::bulkDestroy()` 메서드 추가 +4. 라우트: `POST /api/admin/hr/attendances/bulk-delete` + +**수정 대상 파일**: +- `mng/resources/views/hr/attendances/partials/table.blade.php` +- `mng/resources/views/hr/attendances/index.blade.php` +- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` +- `mng/app/Services/HR/AttendanceService.php` +- `mng/routes/api.php` + +--- + +### 3.4 Phase 1-4: 월간 통계 기간 선택 + +**현재**: 당월 통계만 표시 (하드코딩) +**변경**: 연/월 드롭다운 추가 → 선택 시 통계 카드 HTMX 갱신 + +**구현**: +1. 통계 카드 영역을 별도 partial로 분리 (`partials/stats.blade.php`) +2. 연/월 선택 UI 추가 +3. `hx-get` + `hx-vals`로 선택된 연/월 전달 +4. `stats()` API가 `year`, `month` 파라미터 이미 지원 + +**수정 대상 파일**: +- `mng/resources/views/hr/attendances/index.blade.php` +- `mng/resources/views/hr/attendances/partials/stats.blade.php` (신규) + +--- + +### 3.5 Phase 2-1: 개인별 근태 상세 페이지 + +**URL**: `/hr/attendances/{userId}` + +**페이지 구성**: +1. **사원 프로필 카드**: 이름, 부서, 직급, 재직상태 +2. **월간 달력**: 날짜별 근태 상태를 색상 도트로 표시 +3. **월간 통계**: 정시출근 N일, 지각 N일, 결근 N일 등 +4. **출퇴근 이력 테이블**: 해당 월의 상세 출퇴근 기록 + +**수정 대상 파일**: +- `mng/routes/web.php` (라우트 추가) +- `mng/app/Http/Controllers/HR/AttendanceController.php` (`show()` 추가) +- `mng/app/Services/HR/AttendanceService.php` (`getUserMonthlyAttendances()` 추가) +- `mng/resources/views/hr/attendances/show.blade.php` (신규) + +--- + +### 3.6 Phase 2-2: 출퇴근 설정 관리 + +**URL**: `/hr/attendance-settings` + +**설정 항목** (`attendance_settings` 테이블 기반): + +| 항목 | 필드 | 설명 | +|------|------|------| +| GPS 출퇴근 사용 | `use_gps` | on/off 토글 | +| 자동 출퇴근 | `use_auto` | on/off 토글 | +| 허용 반경 | `allowed_radius` | 미터 단위 입력 | +| 본사 주소 | `hq_address` | 주소 입력 | +| 본사 위도/경도 | `hq_latitude`, `hq_longitude` | 좌표 입력 | + +**수정 대상 파일**: +- `mng/routes/web.php`, `mng/routes/api.php` +- `mng/app/Http/Controllers/HR/AttendanceSettingController.php` (신규) +- `mng/app/Models/HR/AttendanceSetting.php` (신규 — API 모델 미러링) +- `mng/resources/views/hr/attendance-settings/index.blade.php` (신규) + +--- + +## 4. 데이터 흐름 + +### 4.1 MNG 자체 CRUD 패턴 (현재) + +``` +브라우저 ──HTMX──→ MNG API Controller ──→ MNG Service ──→ DB (직접) + (api/admin/hr/attendances) +``` + +> MNG는 API 서버를 경유하지 않고 DB에 직접 접근한다. + +### 4.2 엑셀 다운로드 흐름 + +``` +브라우저 ──GET──→ MNG AttendanceController::export() + → AttendanceService::getExportData() + → ExportService::download() (라라벨 엑셀) + ← BinaryFileResponse (.xlsx) +``` + +--- + +## 5. 구현 순서 및 의존 관계 + +``` +Phase 1 (버그 수정 + 핵심) + 1-1 버그 수정 ─────────────────────────────┐ + 1-2 엑셀 다운로드 ─────────────────────────┤ 독립적, 병렬 가능 + 1-3 일괄 삭제 ─────────────────────────────┤ + 1-4 통계 기간 선택 ────────────────────────┘ + +Phase 2 (확장) + 2-1 개인별 상세 ───→ 2-3 월간/주간 요약 (데이터 재사용) + 2-2 출퇴근 설정 ─── 독립적 + 2-4 일괄 등록 ───── 독립적 +``` + +--- + +## 6. 관련 파일 목록 + +### MNG 프로젝트 (`/home/aweso/sam/mng`) + +| 파일 | 역할 | +|------|------| +| `app/Models/HR/Attendance.php` | 모델 (8개 상태, json_details) | +| `app/Services/HR/AttendanceService.php` | 비즈니스 로직 | +| `app/Http/Controllers/HR/AttendanceController.php` | 뷰 컨트롤러 | +| `app/Http/Controllers/Api/Admin/HR/AttendanceController.php` | API 컨트롤러 | +| `resources/views/hr/attendances/index.blade.php` | 메인 페이지 | +| `resources/views/hr/attendances/partials/table.blade.php` | 테이블 partial | +| `routes/web.php` | 웹 라우트 | +| `routes/api.php` | API 라우트 | + +### API 프로젝트 (`/home/aweso/sam/api`) + +| 파일 | 역할 | +|------|------| +| `database/migrations/2025_12_09_*_attendances*` | 마이그레이션 (2개) | +| `database/migrations/2025_12_17_*_attendance_settings*` | 설정 테이블 | +| `app/Models/Tenants/Attendance.php` | API 모델 (참조용) | +| `app/Models/Tenants/AttendanceSetting.php` | 설정 모델 (참조용) | + +### 참조 문서 + +| 문서 | 경로 | +|------|------| +| 근태 API 규칙 | `docs/rules/attendance-api.md` | +| GPS 출퇴근 스펙 | `docs/specs/erp-analysis/03-gps-attendance.md` | + +--- + +## 7. 검증 방법 + +### Phase 1 체크리스트 + +- [ ] 근태 등록 모달 → 사원 선택 + 날짜 + 상태 입력 → 저장 성공 +- [ ] 근태 수정 모달 → 기존 데이터 로드 → 수정 → 저장 성공 +- [ ] 동일 사원/날짜 중복 등록 시 기존 데이터 업데이트 (Upsert) +- [ ] 엑셀 다운로드 버튼 클릭 → .xlsx 파일 다운로드 +- [ ] 체크박스 선택 → 일괄 삭제 → 테이블 갱신 +- [ ] 연/월 선택 → 통계 카드 갱신 + +### Phase 2 체크리스트 + +- [ ] 사원 이름 클릭 → 개인별 상세 페이지 이동 +- [ ] 달력에 근태 상태 색상 표시 +- [ ] 출퇴근 설정 페이지 → GPS/자동 토글 → 저장 +- [ ] 허용 반경 변경 → 저장 → DB 반영 + +--- + +**최종 업데이트**: 2026-02-26 diff --git a/plans/block-builder-evolution-plan.md b/plans/block-builder-evolution-plan.md new file mode 100644 index 0000000..57385f3 --- /dev/null +++ b/plans/block-builder-evolution-plan.md @@ -0,0 +1,706 @@ +# 양식 디자이너(Block Builder) 고도화 계획 + +> **작성일**: 2026-03-06 +> **상태**: 계획 수립 +> **담당**: Claude Code + 개발팀 +> **관련**: [문서양식관리](../features/documents/mng-document-template.md) | [문서관리](../features/documents/mng-document-system.md) + +--- + +## 1. 현재 상태 진단 + +### 1.1 구현 완료 (Phase 1 — 2026-02-28) + +- 13개 블록 타입 (기본 6 + 폼 7) +- 3패널 UI (팔레트 / 캔버스 / 속성) +- SortableJS 드래그-앤-드롭 정렬 +- Undo/Redo (최대 50단계) +- JSON 스키마 저장 (`document_templates.schema`) +- 페이지 설정 (A4/A3/B5, 세로/가로, 여백) + +### 1.2 핵심 미구현 사항 + +| 기능 | 상태 | 영향도 | +|------|------|--------| +| 문서 생성 시 블록 렌더링 | 미구현 | 블록 서식으로 문서 작성 불가 | +| 결재선 블록 | 미구현 | 결재 워크플로우 연동 불가 | +| 데이터 바인딩 (EAV 연동) | 미구현 | 입력값 저장/로드 불가 | +| 동적 행 추가 | 미구현 | 검사 데이터 행 추가 불가 | +| 변수/매크로 시스템 | 미구현 | 자동 값 주입 불가 | +| 인쇄/PDF 출력 | 미구현 | 블록 문서 인쇄 불가 | +| Columns 내부 블록 | 미구현 | 다단 레이아웃 활용 불가 | + +> **결론**: 양식 디자이너는 **레이아웃 편집기**로만 동작. 실제 문서 생성/결재/인쇄에서는 Legacy Builder만 사용 가능. + +--- + +## 2. 목표 + +Legacy Builder의 모든 기능을 양식 디자이너에서 지원하면서, 더 유연하고 확장 가능한 문서 시스템 구축. + +**최종 목표:** +``` +양식 디자이너로 서식 설계 + ↓ +블록 스키마 기반 문서 생성 (데이터 입력) + ↓ +결재 워크플로우 (작성 → 검토 → 승인) + ↓ +인쇄/PDF 출력 + ↓ +Legacy Builder 완전 대체 +``` + +--- + +## 3. 개발 로드맵 (6단계) + +### Phase 2: 블록 런타임 렌더러 (기반 인프라) + +> **목표**: 저장된 블록 스키마를 문서 생성/조회/인쇄에서 렌더링 + +#### 2-1. 블록 렌더러 엔진 + +**위치**: `mng/resources/views/documents/partials/block-renderer.blade.php` + +``` +schema JSON 입력 + ↓ +블록 타입별 Blade 컴포넌트 렌더링 + ↓ +모드별 출력: + - view 모드: 읽기 전용 HTML + - edit 모드: 입력 폼 HTML + - print 모드: 인쇄 최적화 HTML +``` + +**핵심 구현:** + +```php +// BlockRendererService +class BlockRendererService +{ + public function render(array $schema, string $mode, array $data = []): string + { + $html = ''; + foreach ($schema['blocks'] as $block) { + $html .= $this->renderBlock($block, $mode, $data); + } + return $html; + } + + private function renderBlock(array $block, string $mode, array $data): string + { + return match($block['type']) { + 'heading' => $this->renderHeading($block, $mode), + 'paragraph' => $this->renderParagraph($block, $mode), + 'table' => $this->renderTable($block, $mode, $data), + 'text_field' => $this->renderTextField($block, $mode, $data), + 'number_field' => $this->renderNumberField($block, $mode, $data), + 'date_field' => $this->renderDateField($block, $mode, $data), + 'select_field' => $this->renderSelectField($block, $mode, $data), + 'checkbox_field' => $this->renderCheckboxField($block, $mode, $data), + 'textarea_field' => $this->renderTextareaField($block, $mode, $data), + 'signature_field'=> $this->renderSignatureField($block, $mode, $data), + 'divider' => $this->renderDivider($block), + 'spacer' => $this->renderSpacer($block), + 'columns' => $this->renderColumns($block, $mode, $data), + 'approval_line' => $this->renderApprovalLine($block, $mode, $data), + 'dynamic_table' => $this->renderDynamicTable($block, $mode, $data), + default => '', + }; + } +} +``` + +#### 2-2. 문서 편집 화면 통합 + +**수정 대상**: `mng/resources/views/documents/edit.blade.php` + +``` +Template 로드 + ↓ +isBlockBuilder() 체크 + ├── true → BlockRendererService::render(schema, 'edit', data) + └── false → 기존 Legacy 렌더링 (변경 없음) +``` + +#### 2-3. 데이터 바인딩 (EAV 매핑) + +블록의 `binding` 속성으로 EAV 데이터와 연결: + +```javascript +// 블록 스키마 예시 +{ + "type": "text_field", + "props": { + "label": "제품명", + "binding": "bf_product_name", // ← EAV field_key + "required": true + } +} +``` + +``` +저장 시: + block.binding → document_data.field_key + input.value → document_data.field_value + block.id → document_data.section_id (블록 ID를 섹션으로 활용) + +로드 시: + document_data 조회 → field_key로 블록 매칭 → 값 주입 +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `mng/app/Services/BlockRendererService.php` | 신규 생성 | +| `mng/resources/views/documents/partials/block-renderer.blade.php` | 신규 생성 | +| `mng/resources/views/documents/edit.blade.php` | 블록 빌더 분기 추가 | +| `mng/resources/views/documents/show.blade.php` | 블록 빌더 분기 추가 | +| `api/app/Services/DocumentService.php` | 블록 데이터 저장/로드 로직 | + +--- + +### Phase 3: 결재선 블록 + +> **목표**: 블록 스키마 내에서 결재 워크플로우 정의 + +#### 3-1. approval_line 블록 타입 추가 + +**스키마:** + +```json +{ + "type": "approval_line", + "props": { + "steps": [ + { "role": "작성", "department": "", "name": "" }, + { "role": "검토", "department": "", "name": "" }, + { "role": "승인", "department": "", "name": "" } + ], + "style": "horizontal", + "showStamp": true + } +} +``` + +#### 3-2. 팔레트에 결재선 블록 추가 + +```javascript +// 블록 팔레트 추가 +{ type: 'approval_line', icon: '✓', label: '결재선', category: '워크플로우' } +``` + +#### 3-3. 속성 패널 결재선 편집기 + +``` +┌─────────────────────────────┐ +│ 결재선 설정 │ +│ │ +│ [+ 단계 추가] │ +│ │ +│ 1. 역할: [작성 ▼] │ +│ 부서: [___________] │ +│ 이름: [___________] │ +│ │ +│ 2. 역할: [검토 ▼] │ +│ 부서: [___________] │ +│ 이름: [___________] │ +│ │ +│ 3. 역할: [승인 ▼] │ +│ 부서: [___________] │ +│ 이름: [___________] │ +│ │ +│ ☐ 직인 표시 │ +│ 스타일: [가로형 ▼] │ +└─────────────────────────────┘ +``` + +#### 3-4. 문서 생성 시 결재 연동 + +``` +블록 스키마 → approval_line 블록 추출 + ↓ +DocumentApproval 레코드 자동 생성 + ↓ +기존 결재 워크플로우 (submit → approve → reject) 그대로 활용 +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `block-editor.blade.php` | approval_line 블록 추가 | +| `block-canvas.blade.php` | 결재선 렌더링 | +| `BlockRendererService.php` | 결재선 view/edit/print 렌더 | +| `DocumentService.php` | 블록 결재선 → DocumentApproval 변환 | + +--- + +### Phase 4: 동적 테이블 블록 + 변수 시스템 + +> **목표**: 문서 작성 시 행 추가/삭제 가능한 테이블 + 자동 값 주입 + +#### 4-1. dynamic_table 블록 타입 + +기존 `table` 블록은 정적 (양식 설계 시 행 고정). `dynamic_table`은 문서 작성 시 행 동적 추가. + +**스키마:** + +```json +{ + "type": "dynamic_table", + "props": { + "label": "검사 데이터", + "columns": [ + { "key": "col_item", "label": "항목", "type": "text", "width": 120 }, + { "key": "col_standard", "label": "기준값", "type": "text", "width": 100 }, + { "key": "col_measured", "label": "측정값", "type": "number", "width": 100 }, + { "key": "col_result", "label": "판정", "type": "select", + "options": ["합격", "불합격", "보류"], "width": 80 } + ], + "minRows": 1, + "maxRows": 50, + "initialRows": 3, + "showRowNumber": true, + "binding": "inspection_data" + } +} +``` + +#### 4-2. EAV 데이터 매핑 + +``` +dynamic_table 블록 데이터 저장: + +document_data 레코드: + section_id = (dynamic_table 블록 ID → section 매핑) + column_id = (columns[].key → column 매핑) + row_index = 0, 1, 2, ... + field_key = "col_item", "col_standard", ... + field_value = 입력값 +``` + +#### 4-3. 변수/매크로 시스템 + +**내장 변수:** + +| 변수 | 값 | 설명 | +|------|-----|------| +| `{{today}}` | 현재 날짜 | YYYY-MM-DD | +| `{{now}}` | 현재 시각 | YYYY-MM-DD HH:mm | +| `{{user.name}}` | 로그인 사용자명 | | +| `{{user.department}}` | 로그인 사용자 부서 | | +| `{{doc.number}}` | 문서 번호 | 자동채번 | +| `{{doc.title}}` | 문서 제목 | | +| `{{template.company}}` | 서식 회사명 | | + +**연결 데이터 변수 (linked data):** + +| 변수 | 설명 | +|------|------| +| `{{item.name}}` | 연결 품목명 | +| `{{item.code}}` | 연결 품목 코드 | +| `{{order.number}}` | 연결 작업지시서 번호 | +| `{{order.quantity}}` | 연결 수량 | + +**변수 사용 예시 (블록 속성):** + +```json +{ + "type": "text_field", + "props": { + "label": "검사일자", + "binding": "bf_inspection_date", + "default": "{{today}}" + } +} +``` + +```json +{ + "type": "paragraph", + "props": { + "text": "작성자: {{user.name}} ({{user.department}})" + } +} +``` + +#### 4-4. 변수 해석 엔진 + +```php +// VariableResolver +class VariableResolver +{ + public function resolve(string $text, array $context): string + { + return preg_replace_callback('/\{\{(\w+(?:\.\w+)*)\}\}/', function ($m) use ($context) { + return data_get($context, $m[1], $m[0]); + }, $text); + } + + public function buildContext(Document $document, ?User $user = null): array + { + return [ + 'today' => now()->format('Y-m-d'), + 'now' => now()->format('Y-m-d H:i'), + 'user' => [ + 'name' => $user?->name, + 'department' => $user?->department?->name, + ], + 'doc' => [ + 'number' => $document->document_number, + 'title' => $document->title, + ], + 'item' => $this->resolveLinkedItem($document), + 'order' => $this->resolveLinkedOrder($document), + ]; + } +} +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `block-editor.blade.php` | dynamic_table 블록 추가 | +| `BlockRendererService.php` | 동적 테이블 렌더링 (edit: 행 추가/삭제 UI) | +| `mng/app/Services/VariableResolver.php` | 신규 생성 | +| `DocumentService.php` | 동적 테이블 EAV 저장/로드 | + +--- + +### Phase 5: 고급 블록 + 조건부 로직 + +> **목표**: 수식 계산, 조건부 표시, 이미지 블록 등 고급 기능 + +#### 5-1. 수식 블록 (formula) + +```json +{ + "type": "formula_field", + "props": { + "label": "합계", + "expression": "SUM(inspection_data.col_measured)", + "format": "number", + "decimal": 2 + } +} +``` + +**지원 함수:** + +| 함수 | 설명 | 예시 | +|------|------|------| +| `SUM()` | 합계 | `SUM(table.col_amount)` | +| `AVG()` | 평균 | `AVG(table.col_measured)` | +| `COUNT()` | 개수 | `COUNT(table.col_item)` | +| `MIN()` / `MAX()` | 최솟값/최댓값 | `MIN(table.col_value)` | +| `IF()` | 조건 | `IF(AVG(table.col_measured) > 5, "합격", "불합격")` | +| `ROUND()` | 반올림 | `ROUND(AVG(table.col_measured), 2)` | + +#### 5-2. 조건부 표시 (conditional visibility) + +모든 블록에 `visibility` 속성 추가: + +```json +{ + "type": "paragraph", + "props": { + "text": "불합격 사유를 기재해 주세요.", + "visibility": { + "condition": "field", + "field": "bf_judgement", + "operator": "equals", + "value": "불합격" + } + } +} +``` + +**연산자:** + +| 연산자 | 설명 | +|--------|------| +| `equals` | 같으면 표시 | +| `not_equals` | 다르면 표시 | +| `contains` | 포함하면 표시 | +| `greater_than` | 크면 표시 | +| `less_than` | 작으면 표시 | +| `is_empty` | 비어있으면 표시 | +| `is_not_empty` | 비어있지 않으면 표시 | + +#### 5-3. 이미지 블록 + +```json +{ + "type": "image", + "props": { + "label": "검사 사진", + "source": "upload", + "maxSize": 10, + "accept": ["jpeg", "png", "webp"], + "width": "100%", + "align": "center" + } +} +``` + +#### 5-4. Columns 내부 블록 (중첩 렌더링) + +```json +{ + "type": "columns", + "props": { + "count": 2, + "ratio": "1:1", + "children": [ + [ + { "type": "text_field", "props": { "label": "품명" } }, + { "type": "date_field", "props": { "label": "검사일" } } + ], + [ + { "type": "text_field", "props": { "label": "LOT NO" } }, + { "type": "select_field", "props": { "label": "판정", "options": ["합격","불합격"] } } + ] + ] + } +} +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `block-editor.blade.php` | formula, image, conditional 블록 추가 | +| `mng/app/Services/FormulaEngine.php` | 수식 해석 엔진 | +| `BlockRendererService.php` | 조건부 표시, 수식 계산 렌더링 | + +--- + +### Phase 6: 인쇄/PDF + Legacy 대체 + +> **목표**: 블록 문서 인쇄 완성, Legacy Builder 완전 대체 + +#### 6-1. 인쇄 레이아웃 + +``` +print 모드 렌더링: + - 페이지 설정 (A4/A3) 적용 + - 여백 적용 + - 폼 필드 → 값 표시 (입력란 제거) + - 서명 → 서명 이미지 표시 + - 결재선 → 직인 표시 + - 페이지 넘김 (page-break) 자동 계산 +``` + +#### 6-2. PDF 내보내기 + +``` +블록 렌더러 (print 모드 HTML) + ↓ +Puppeteer / wkhtmltopdf + ↓ +PDF 파일 생성 + ↓ +다운로드 또는 첨부 +``` + +#### 6-3. Legacy → Block 마이그레이션 도구 + +기존 Legacy 서식을 Block 스키마로 자동 변환: + +```php +// LegacyToBlockMigrator +class LegacyToBlockMigrator +{ + public function convert(DocumentTemplate $legacy): array + { + $blocks = []; + + // 1. 결재선 → approval_line 블록 + if ($legacy->approvalLines->isNotEmpty()) { + $blocks[] = $this->convertApprovalLines($legacy->approvalLines); + } + + // 2. 기본필드 → text_field / date_field 블록 + foreach ($legacy->basicFields as $field) { + $blocks[] = $this->convertBasicField($field); + } + + // 3. 섹션 → heading + image 블록 + foreach ($legacy->sections as $section) { + $blocks[] = ['type' => 'heading', 'props' => ['text' => $section->title]]; + if ($section->image_path) { + $blocks[] = ['type' => 'image', 'props' => ['source' => $section->image_path]]; + } + } + + // 4. 컬럼 → dynamic_table 블록 + if ($legacy->columns->isNotEmpty()) { + $blocks[] = $this->convertColumns($legacy->columns); + } + + return [ + 'version' => '1.0', + 'page' => ['size' => 'A4', 'orientation' => 'portrait'], + 'blocks' => $blocks, + ]; + } +} +``` + +#### 6-4. Legacy Builder 비활성화 + +``` +Phase 6 완료 후: + - 새 양식 생성: 양식 디자이너만 허용 + - 기존 Legacy 서식: 조회/편집 가능 (변환 유도) + - Legacy Builder "새 양식" 버튼: "양식 디자이너" 사용 안내 +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `mng/resources/views/documents/print-block.blade.php` | 인쇄 전용 뷰 | +| `mng/app/Services/LegacyToBlockMigrator.php` | 변환 도구 | +| `mng/app/Services/PdfExportService.php` | PDF 생성 | + +--- + +## 4. Phase별 우선순위 및 의존관계 + +``` +Phase 2: 블록 런타임 렌더러 ──────────────────────┐ + (렌더러 엔진, 데이터 바인딩, 문서 편집 통합) │ + │ +Phase 3: 결재선 블록 ─────────────────────┐ │ + (approval_line 블록, 결재 워크플로우) │ │ + ↓ ↓ +Phase 4: 동적 테이블 + 변수 ──────→ Phase 5: 고급 블록 + (dynamic_table, 매크로) (수식, 조건부, 이미지) + │ + ↓ + Phase 6: 인쇄/PDF + Legacy 대체 + (마이그레이션 도구) +``` + +| Phase | 의존 | 난이도 | 예상 범위 | +|-------|------|--------|----------| +| **Phase 2** | 없음 (기반) | 높음 | 렌더러 엔진 + EAV 매핑 | +| **Phase 3** | Phase 2 | 중간 | 결재선 블록 + 워크플로우 연동 | +| **Phase 4** | Phase 2 | 높음 | 동적 테이블 + 변수 해석 | +| **Phase 5** | Phase 4 | 높음 | 수식 엔진 + 조건부 로직 | +| **Phase 6** | Phase 3~5 | 중간 | 인쇄 + 마이그레이션 | + +--- + +## 5. 스키마 버전 관리 + +### 5.1 버전 규칙 + +| 버전 | Phase | 변경 내용 | +|------|-------|----------| +| `1.0` | Phase 1 (현재) | 기본 13개 블록 | +| `2.0` | Phase 2~3 | 데이터 바인딩, approval_line 추가 | +| `3.0` | Phase 4 | dynamic_table, 변수 시스템 | +| `4.0` | Phase 5 | formula, conditional, image | + +### 5.2 하위 호환 + +```json +{ + "version": "3.0", + "page": { ... }, + "blocks": [ ... ], + "variables": { ... }, + "migrations": { + "from_1.0": "auto" + } +} +``` + +- 이전 버전 스키마 자동 인식 및 업그레이드 +- 신규 블록 타입은 이전 버전에서 무시 (graceful degradation) + +--- + +## 6. 신규 블록 타입 전체 목록 + +### Phase별 블록 추가 계획 + +| Phase | 블록 타입 | 카테고리 | 설명 | +|-------|----------|---------|------| +| 1 (완료) | `heading` | 기본 | 제목 | +| 1 (완료) | `paragraph` | 기본 | 문단 | +| 1 (완료) | `table` | 기본 | 정적 테이블 | +| 1 (완료) | `columns` | 기본 | 다단 레이아웃 | +| 1 (완료) | `divider` | 기본 | 구분선 | +| 1 (완료) | `spacer` | 기본 | 여백 | +| 1 (완료) | `text_field` | 폼 | 텍스트 입력 | +| 1 (완료) | `number_field` | 폼 | 숫자 입력 | +| 1 (완료) | `date_field` | 폼 | 날짜 입력 | +| 1 (완료) | `select_field` | 폼 | 드롭다운 | +| 1 (완료) | `checkbox_field` | 폼 | 체크박스 | +| 1 (완료) | `textarea_field` | 폼 | 장문 텍스트 | +| 1 (완료) | `signature_field` | 폼 | 서명 | +| **3** | `approval_line` | 워크플로우 | 결재선 | +| **4** | `dynamic_table` | 데이터 | 동적 행 테이블 | +| **5** | `formula_field` | 데이터 | 수식 계산 | +| **5** | `image` | 미디어 | 이미지 업로드/표시 | + +--- + +## 7. 기술 스택 정리 + +| 구성 요소 | 기술 | 비고 | +|----------|------|------| +| 블록 에디터 UI | Alpine.js + Blade | 기존 유지 | +| 드래그-앤-드롭 | SortableJS | 기존 유지 | +| 블록 렌더러 | PHP (BlockRendererService) | 신규 | +| 변수 해석 | PHP (VariableResolver) | 신규 | +| 수식 엔진 | PHP (FormulaEngine) | 신규 | +| 데이터 저장 | EAV (document_data) | 기존 테이블 활용 | +| 결재 워크플로우 | DocumentApproval | 기존 로직 활용 | +| 인쇄 | CSS @media print | 신규 | +| PDF | Puppeteer 또는 wkhtmltopdf | 신규 | + +--- + +## 8. 위험 요소 및 대응 + +| 위험 | 영향 | 대응 | +|------|------|------| +| EAV 매핑 복잡도 | 블록 ID ↔ section_id 매핑 불일치 | 블록 ID를 section 대용으로 사용, 매핑 테이블 추가 검토 | +| Legacy 데이터 호환 | 기존 문서 데이터 접근 불가 | Legacy 서식 문서는 기존 방식 유지, 신규 서식만 블록 적용 | +| 수식 엔진 보안 | 임의 코드 실행 위험 | 화이트리스트 함수만 허용, eval 사용 금지 | +| 인쇄 레이아웃 | 브라우저별 차이 | CSS @page 규격 준수, PDF 변환 권장 | +| 스키마 마이그레이션 | 버전 업그레이드 시 데이터 손실 | 하위 호환 보장, 자동 업그레이드 로직 | + +--- + +## 9. 성공 기준 + +| 기준 | 측정 방법 | +|------|----------| +| 블록 서식으로 문서 생성 가능 | Phase 2 완료 후 테스트 | +| 결재 워크플로우 정상 동작 | Phase 3 완료 후 테스트 | +| 동적 행 추가/삭제 | Phase 4 완료 후 테스트 | +| 변수 자동 주입 | Phase 4 완료 후 테스트 | +| Legacy 서식 자동 변환 | Phase 6 완료 후 테스트 | +| 인쇄 품질 A4 기준 정상 | Phase 6 완료 후 테스트 | + +--- + +## 관련 문서 + +- [문서양식관리](../features/documents/mng-document-template.md) — 현재 양식관리 기술문서 +- [문서관리 시스템](../features/documents/mng-document-system.md) — 문서 생성/결재 기술문서 +- [문서관리 API](../features/documents/README.md) — API 엔드포인트 목록 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/plans/design-insight-menu-plan.md b/plans/design-insight-menu-plan.md new file mode 100644 index 0000000..28122f5 --- /dev/null +++ b/plans/design-insight-menu-plan.md @@ -0,0 +1,611 @@ +# UI/UX 디자인 인사이트 연구 메뉴 기획서 + +> **작성일**: 2026-03-08 +> **상태**: 기획 중 +> **라우트**: `/rd/design-insight` +> **모티브**: 기획디자인 스토리보드 에디터 (`/rd/planning-design`) + +--- + +## 1. 개요 + +### 1.1 배경 + +기획디자인 메뉴는 ERP 화면을 **설계(Output)**하는 도구다. +그런데 좋은 설계를 하려면 **연구(Input)**가 먼저 필요하다. + +``` +연구 (이 메뉴) 설계 (기획디자인) +┌─────────────────┐ ┌─────────────────┐ +│ 레퍼런스 수집 │ │ 스토리보드 작성 │ +│ 패턴 분석 │ ──→ │ 와이어프레임 설계 │ +│ 인사이트 정리 │ │ HTML 내보내기 │ +│ 디자인 원칙 학습 │ │ 인쇄 │ +└─────────────────┘ └─────────────────┘ +``` + +현재 SAM ERP 화면을 만들 때 참고할 디자인 패턴이나 인사이트를 체계적으로 관리하는 도구가 없다. 외부 서비스(Dribbble, Mobbin 등)를 참고하지만 **우리 ERP에 맞는 패턴**을 축적하는 곳이 없다. + +### 1.2 목적 + +SAM ERP 화면 개발에 필요한 **UI/UX 디자인 인사이트를 수집·분석·축적**하는 연구 도구 + +### 1.3 핵심 가치 + +| 가치 | 설명 | +|------|------| +| **패턴 축적** | "이 화면은 왜 좋은가?" — 반복 사용할 패턴을 라이브러리화 | +| **Before/After** | 개선 전후를 비교하여 디자인 결정의 근거를 기록 | +| **팀 학습** | 디자인 인사이트를 팀원과 공유, 일관된 UI 품질 유지 | +| **빠른 참조** | 새 화면 설계 시 기존 패턴을 즉시 찾아 재사용 | + +--- + +## 2. 기술 아키텍처 + +### 2.1 기획디자인과 동일한 패턴 + +기획디자인 메뉴의 성공 패턴을 그대로 적용한다. + +| 항목 | 선택 | 이유 | +|------|------|------| +| 프레임워크 | Alpine.js 단일 파일 SPA | 서버 API 없이 즉시 사용, MNG 기존 스택 | +| 저장 | localStorage | 서버 의존성 제거, 즉시 사용 가능 | +| 뷰 파일 | `resources/views/rd/design-insight/index.blade.php` | 단일 파일 구조 | +| 컨트롤러 | `RdController@designInsight()` | 기존 R&D 컨트롤러 확장 | +| 이미지 | Base64 Data URL (localStorage) | 서버 업로드 불필요 | + +### 2.2 라우트 + +```php +// routes/web.php — R&D 그룹 내 추가 +Route::get('/rd/design-insight', [RdController::class, 'designInsight']) + ->name('rd.design-insight'); +``` + +### 2.3 localStorage 키 + +| 키 | 용도 | +|----|------| +| `di_projects` | 연구 프로젝트 목록 (메인 저장소) | +| `di_current` | 현재 프로젝트 ID | +| `di_patterns` | 디자인 패턴 라이브러리 (프로젝트 간 공유) | + +--- + +## 3. 화면 구조 + +### 3.1 전체 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 툴바: [프로젝트명] [저장] [내보내기] [뷰: 보드│리스트│갤러리] │ +├──────────────────────────────────────────────────────────────┤ +│ 카테고리 탭: 전체 │ 레퍼런스 │ 분석 │ 패턴 │ Before/After │ +├────────┬─────────────────────────────────────────────────────┤ +│ │ │ +│ 사이드 │ 메인 콘텐츠 영역 │ +│ 바 │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ ◆ 프로 │ │ 인사이트 │ │ 인사이트 │ │ 인사이트 │ │ +│ 젝트 │ │ 카드 1 │ │ 카드 2 │ │ 카드 3 │ │ +│ 목록 │ │ │ │ │ │ │ │ +│ │ │ 🏷️태그 │ │ 🏷️태그 │ │ 🏷️태그 │ │ +│ ◆ 태그 │ └─────────┘ └─────────┘ └─────────┘ │ +│ 필터 │ │ +│ │ ┌─────────┐ ┌─────────┐ │ +│ ◆ 검색 │ │ 인사이트 │ │ + 새 카드 │ │ +│ │ │ 카드 4 │ │ 추가 │ │ +│ │ └─────────┘ └─────────┘ │ +│ │ │ +├────────┴─────────────────────────────────────────────────────┤ +│ 상태바: 카드 12개 │ 패턴 5개 │ 태그 8개 │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 3.2 뷰 모드 (3종) + +| 뷰 | 설명 | 용도 | +|----|------|------| +| **보드 (Board)** | 칸반 스타일 카드 격자 배열 | 전체 현황 파악, 기본 뷰 | +| **리스트 (List)** | 테이블형 목록 (정렬/필터) | 대량 데이터 관리, 검색 | +| **갤러리 (Gallery)** | 이미지 중심 큰 썸네일 격자 | 시각적 비교, 레퍼런스 브라우징 | + +--- + +## 4. 인사이트 카드 (핵심 데이터 단위) + +### 4.1 카드 유형 (4종) + +#### A. 레퍼런스 카드 (Reference) + +외부/내부 화면 스크린샷을 수집하고 메모를 남긴다. + +``` +┌──────────────────────────────┐ +│ 📷 [스크린샷 이미지] │ +│ │ +├──────────────────────────────┤ +│ 📌 Notion 대시보드 │ +│ "카드형 레이아웃이 정보 밀도를 │ +│ 유지하면서도 시각적으로 깔끔" │ +├──────────────────────────────┤ +│ 출처: notion.so │ +│ 🏷️ 대시보드 카드 레이아웃 │ +│ ⭐⭐⭐⭐☆ │ +└──────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `image` | string (Base64) | 스크린샷 이미지 | +| `title` | string | 제목 | +| `memo` | string | 인사이트 메모 (왜 좋은가/나쁜가) | +| `source` | string | 출처 (URL, 앱 이름 등) | +| `tags` | string[] | 태그 배열 | +| `rating` | number (1-5) | 평점 | +| `category` | string | 화면 카테고리 | + +#### B. 분석 카드 (Analysis) + +화면을 분석하고 디자인 원칙을 체크한다. + +``` +┌──────────────────────────────┐ +│ 🔍 SAM 수주 목록 화면 분석 │ +├──────────────────────────────┤ +│ [스크린샷 + 어노테이션 오버레이]│ +│ ①→ 검색 영역 너무 넓음 │ +│ ②→ 버튼 정렬 불일치 │ +│ ③→ 여백 불균형 │ +├──────────────────────────────┤ +│ ✅ 정렬 (Alignment) │ +│ ❌ 대비 (Contrast) │ +│ ✅ 반복 (Repetition) │ +│ ⚠️ 근접성 (Proximity) │ +├──────────────────────────────┤ +│ 개선 제안: │ +│ "검색 영역을 접을 수 있게 하고 │ +│ 버튼 그룹을 우측 정렬" │ +│ 🏷️ 목록화면 개선필요 │ +└──────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `image` | string (Base64) | 분석 대상 스크린샷 | +| `annotations` | Annotation[] | 어노테이션 배열 (마커 번호, 좌표, 텍스트) | +| `principles` | object | CRAP 원칙 체크 (contrast, repetition, alignment, proximity) | +| `suggestion` | string | 개선 제안 | +| `severity` | string | 심각도 (info, warning, critical) | + +#### C. 패턴 카드 (Pattern) + +반복 사용할 UI 패턴을 템플릿으로 등록한다. + +``` +┌──────────────────────────────┐ +│ 📐 검색 + 필터 + 목록 패턴 │ +├──────────────────────────────┤ +│ [패턴 와이어프레임 이미지] │ +├──────────────────────────────┤ +│ 사용처: │ +│ • 수주 목록 │ +│ • 거래처 목록 │ +│ • 품목 목록 │ +├──────────────────────────────┤ +│ 구성 요소: │ +│ ☑ 검색바 (상단 고정) │ +│ ☑ 필터 칩 (접기/펼치기) │ +│ ☑ 테이블 (정렬 가능) │ +│ ☑ 페이지네이션 (하단) │ +│ ☑ 액션 버튼 (우상단) │ +├──────────────────────────────┤ +│ 🏷️ 목록 CRUD 테이블 │ +│ 📊 사용빈도: ★★★★★ (12회) │ +└──────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `image` | string (Base64) | 패턴 와이어프레임 | +| `usedIn` | string[] | 사용처 목록 | +| `components` | Component[] | 구성 요소 체크리스트 | +| `guidelines` | string | 사용 가이드라인 | +| `frequency` | number | 사용 빈도 | + +#### D. Before/After 카드 (Comparison) + +디자인 개선 전후를 비교한다. + +``` +┌──────────────────────────────────────────┐ +│ 🔄 거래처 상세 화면 리뉴얼 │ +├───────────────────┬──────────────────────┤ +│ ❌ Before │ ✅ After │ +│ [이전 스크린샷] │ [개선 스크린샷] │ +│ │ │ +├───────────────────┴──────────────────────┤ +│ 변경 포인트: │ +│ 1. 탭 구조 → 섹션 접기/펼치기 변경 │ +│ 2. 좌우 2컬럼 → 단일 컬럼 (모바일 대응) │ +│ 3. 저장 버튼 하단 고정 → 상단 sticky │ +├──────────────────────────────────────────┤ +│ 효과: 스크롤 40% 감소, 작업 완료 시간 단축 │ +│ 🏷️ 상세화면 폼 리뉴얼 │ +└──────────────────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `beforeImage` | string (Base64) | 개선 전 스크린샷 | +| `afterImage` | string (Base64) | 개선 후 스크린샷 | +| `changes` | string[] | 변경 포인트 목록 | +| `effect` | string | 개선 효과 | + +### 4.2 공통 필드 + +모든 카드 유형이 공유하는 기본 필드: + +```json +{ + "id": "di_1709856000000_abc", + "type": "reference", + "title": "카드 제목", + "createdAt": "2026-03-08T10:00:00", + "updatedAt": "2026-03-08T15:30:00", + "tags": ["대시보드", "카드", "레이아웃"], + "category": "dashboard", + "pinned": false, + "archived": false +} +``` + +### 4.3 카테고리 (화면 유형별) + +| 카테고리 | 코드 | 설명 | +|---------|------|------| +| 대시보드 | `dashboard` | 통계, KPI, 차트 화면 | +| 목록 | `list` | 테이블, 검색, 필터 화면 | +| 상세/폼 | `form` | 입력, 편집, 상세 보기 | +| 모달/팝업 | `modal` | 모달 다이얼로그, 확인창 | +| 네비게이션 | `navigation` | 사이드바, 탭, 메뉴 | +| 로그인/온보딩 | `auth` | 인증, 초기 설정 | +| 보고서/인쇄 | `report` | 인쇄용, PDF 출력 화면 | +| 기타 | `etc` | 분류 불가 | + +--- + +## 5. 기능 상세 + +### 5.1 이미지 수집 + +| 기능 | 설명 | +|------|------| +| 파일 업로드 | 이미지 파일 선택 (PNG, JPG, GIF) | +| 클립보드 붙여넣기 | `Ctrl+V`로 스크린샷 즉시 붙여넣기 | +| 드래그 앤 드롭 | 이미지 파일을 카드 영역에 드롭 | + +> **Ctrl+V 붙여넣기가 핵심** — 스크린샷 캡처 후 즉시 카드 생성이 워크플로우의 핵심 + +### 5.2 어노테이션 (분석 카드) + +분석 카드에서 이미지 위에 마커를 추가하여 문제점이나 인사이트를 표시한다. + +| 어노테이션 유형 | 설명 | +|---------------|------| +| 번호 마커 (①②③) | 이미지 위 클릭 → 번호 자동 증가, 하단 설명과 연동 | +| 영역 하이라이트 | 드래그로 사각형 영역 표시 (반투명 컬러 오버레이) | +| 텍스트 메모 | 이미지 위 임의 위치에 짧은 메모 | + +> 기획디자인의 번호 마커(marker 블록) + Description 패널 패턴을 재활용 + +### 5.3 태그 시스템 + +| 기능 | 설명 | +|------|------| +| 자유 태그 | 카드에 자유 태그 추가 (콤마 구분 입력) | +| 태그 자동 완성 | 기존 태그 목록에서 자동 완성 | +| 태그 필터 | 사이드바에서 태그 클릭 → 해당 태그 카드만 표시 | +| 태그 색상 | 카테고리별 자동 색상 배정 | + +### 5.4 CRAP 디자인 원칙 체크리스트 + +분석 카드에서 사용하는 디자인 원칙 평가: + +| 원칙 | 체크 항목 | +|------|----------| +| **C**ontrast (대비) | 중요 요소가 시각적으로 구분되는가? | +| **R**epetition (반복) | 일관된 스타일이 반복 적용되는가? | +| **A**lignment (정렬) | 요소들이 논리적으로 정렬되어 있는가? | +| **P**roximity (근접성) | 관련 요소가 가까이 그룹핑되어 있는가? | + +추가 체크: + +| 원칙 | 체크 항목 | +|------|----------| +| 여백 (Whitespace) | 적절한 여백이 확보되어 있는가? | +| 계층 (Hierarchy) | 정보의 우선순위가 시각적으로 드러나는가? | +| 일관성 (Consistency) | 다른 화면과 일관된 패턴을 따르는가? | +| 접근성 (Accessibility) | 색상 대비, 폰트 크기가 충분한가? | + +### 5.5 검색 & 필터 + +| 기능 | 설명 | +|------|------| +| 텍스트 검색 | 제목, 메모, 태그에서 전문 검색 | +| 카테고리 필터 | 화면 유형별 필터 (탭) | +| 카드 유형 필터 | 레퍼런스 / 분석 / 패턴 / Before/After | +| 평점 필터 | ⭐ 3점 이상만 표시 등 | +| 정렬 | 최신순, 평점순, 이름순 | + +### 5.6 내보내기 + +| 형식 | 설명 | +|------|------| +| JSON | 전체 프로젝트 데이터 백업/복원 | +| HTML | 인사이트 카드를 HTML 보고서로 출력 (인쇄 가능) | +| 패턴 → 기획디자인 | 패턴 카드의 와이어프레임을 기획디자인 템플릿으로 전송 | + +### 5.7 키보드 단축키 + +| 단축키 | 기능 | +|--------|------| +| `Ctrl+V` | 클립보드 이미지로 새 카드 생성 | +| `Ctrl+S` | 프로젝트 저장 | +| `Ctrl+F` | 검색 포커스 | +| `Ctrl+N` | 새 카드 추가 | +| `Delete` | 선택 카드 삭제 | +| `Ctrl+Z` | 실행 취소 | +| `Ctrl+Y` | 다시 실행 | + +--- + +## 6. 데이터 구조 + +### 6.1 프로젝트 (localStorage: `di_projects`) + +```json +[ + { + "id": "diproj_1709856000000", + "title": "SAM ERP v2 디자인 연구", + "description": "SAM ERP 화면 개선을 위한 UI/UX 인사이트 수집", + "cards": [], + "createdAt": "2026-03-08T10:00:00", + "updatedAt": "2026-03-08T15:30:00" + } +] +``` + +### 6.2 인사이트 카드 (cards 배열 내) + +```json +{ + "id": "di_1709856000000_abc", + "type": "reference", + "title": "Notion 대시보드 카드 레이아웃", + "image": "data:image/png;base64,...", + "memo": "카드형 레이아웃이 정보 밀도를 유지하면서도 시각적으로 깔끔", + "source": "notion.so", + "tags": ["대시보드", "카드", "레이아웃"], + "category": "dashboard", + "rating": 4, + "pinned": false, + "archived": false, + "createdAt": "2026-03-08T10:00:00", + "updatedAt": "2026-03-08T15:30:00" +} +``` + +### 6.3 분석 어노테이션 + +```json +{ + "annotations": [ + { + "id": "ann_001", + "type": "marker", + "num": 1, + "x": 150, + "y": 80, + "text": "검색 영역 너무 넓음 — 접기 기능 필요" + }, + { + "id": "ann_002", + "type": "highlight", + "x": 200, + "y": 300, + "w": 150, + "h": 40, + "color": "rgba(239,68,68,0.3)", + "text": "버튼 정렬 불일치" + } + ] +} +``` + +### 6.4 디자인 패턴 라이브러리 (localStorage: `di_patterns`) + +프로젝트 간 공유되는 패턴 라이브러리: + +```json +[ + { + "id": "pat_001", + "name": "검색 + 필터 + 목록", + "image": "data:image/png;base64,...", + "components": [ + { "name": "검색바", "required": true }, + { "name": "필터 칩", "required": false }, + { "name": "데이터 테이블", "required": true }, + { "name": "페이지네이션", "required": true }, + { "name": "액션 버튼", "required": true } + ], + "guidelines": "검색바는 상단 고정, 필터는 접기/펼치기 지원", + "usedIn": ["수주 목록", "거래처 목록", "품목 목록"], + "tags": ["목록", "CRUD", "테이블"], + "frequency": 12, + "createdAt": "2026-03-08T10:00:00" + } +] +``` + +--- + +## 7. 프리셋 데이터 + +### 7.1 기본 카테고리 (하드코딩) + +```javascript +categories: [ + { code: 'dashboard', label: '대시보드', icon: '📊', color: '#6366f1' }, + { code: 'list', label: '목록', icon: '📋', color: '#3b82f6' }, + { code: 'form', label: '상세/폼', icon: '📝', color: '#10b981' }, + { code: 'modal', label: '모달/팝업', icon: '💬', color: '#f59e0b' }, + { code: 'navigation',label: '네비게이션',icon: '🧭', color: '#8b5cf6' }, + { code: 'auth', label: '로그인', icon: '🔐', color: '#ec4899' }, + { code: 'report', label: '보고서', icon: '📄', color: '#0ea5e9' }, + { code: 'etc', label: '기타', icon: '📎', color: '#64748b' }, +] +``` + +### 7.2 CRAP 원칙 체크리스트 (하드코딩) + +```javascript +designPrinciples: [ + { key: 'contrast', label: '대비 (Contrast)', icon: '🔲', desc: '중요 요소가 시각적으로 구분' }, + { key: 'repetition', label: '반복 (Repetition)', icon: '🔁', desc: '일관된 스타일 반복 적용' }, + { key: 'alignment', label: '정렬 (Alignment)', icon: '📏', desc: '논리적 정렬' }, + { key: 'proximity', label: '근접성 (Proximity)', icon: '🧲', desc: '관련 요소 그룹핑' }, + { key: 'whitespace', label: '여백 (Whitespace)', icon: '⬜', desc: '적절한 여백 확보' }, + { key: 'hierarchy', label: '계층 (Hierarchy)', icon: '🔺', desc: '정보 우선순위 시각화' }, + { key: 'consistency', label: '일관성 (Consistency)',icon: '🔗', desc: '다른 화면과의 일관성' }, + { key: 'a11y', label: '접근성 (A11y)', icon: '♿', desc: '색상 대비, 폰트 크기' }, +] +``` + +### 7.3 샘플 패턴 템플릿 (프리셋) + +| 패턴명 | 구성 요소 | SAM 내 사용처 | +|--------|----------|--------------| +| 검색 + 목록 | 검색바, 필터, 테이블, 페이지네이션, 액션버튼 | 수주/거래처/품목 목록 | +| 상세 폼 | 섹션 헤더, 라벨+입력, 저장/취소 버튼 | 수주 상세, 거래처 상세 | +| 대시보드 | 통계 카드 4개, 차트 2개, 요약 테이블 | 메인 대시보드 | +| 탭 레이아웃 | 탭 메뉴, 탭 콘텐츠, 액션 버튼 | 설정, 품목기준관리 | +| 트리 + 상세 | 좌측 트리, 우측 상세 패널 | 메뉴 관리, 조직도 | +| 모달 폼 | 모달 헤더, 입력 필드, 확인/취소 | 등록/수정 팝업 | +| 칸반 보드 | 컬럼 헤더, 드래그 카드, 필터 | 업무 관리 | +| 캘린더 | 월/주/일 뷰, 이벤트 카드, 필터 | 일정 관리, 근태 | + +--- + +## 8. 워크플로우 + +### 8.1 일반 사용 흐름 + +``` +1. 새 연구 프로젝트 생성 ("SAM ERP v2 디자인 연구") + ↓ +2. 레퍼런스 수집 + • 외부 서비스 스크린샷 → Ctrl+V 붙여넣기 + • SAM 기존 화면 스크린샷 → 파일 업로드 + • 태그 + 카테고리 분류 + ↓ +3. 화면 분석 + • 분석 카드 생성 → 어노테이션 추가 + • CRAP 원칙 체크 + • 개선 제안 작성 + ↓ +4. 패턴 추출 + • 반복되는 좋은 패턴 → 패턴 카드로 등록 + • 구성 요소 정리, 사용 가이드라인 작성 + ↓ +5. Before/After 기록 + • 개선 전후 비교 카드 생성 + • 변경 포인트 + 효과 기록 + ↓ +6. 기획디자인 연계 + • 패턴 라이브러리에서 참고하며 스토리보드 작성 +``` + +### 8.2 기획디자인 연계 + +``` +디자인 인사이트 기획디자인 +┌──────────────┐ ┌──────────────┐ +│ 패턴 카드: │ │ 스토리보드: │ +│ "검색+목록" │──참조──→│ 새 페이지에 │ +│ 구성요소 체크 │ │ 패턴 적용 │ +│ 가이드라인 │ │ │ +└──────────────┘ └──────────────┘ +``` + +> 향후 패턴 카드의 구성 요소를 기획디자인 블록 템플릿으로 자동 변환하는 연계 기능을 검토한다. + +--- + +## 9. 개발 로드맵 + +### Phase 1 — 기본 구조 (MVP) + +| 항목 | 내용 | +|------|------| +| 라우트 + 컨트롤러 | `GET /rd/design-insight` → 뷰 반환 | +| 프로젝트 CRUD | 생성/저장/로드/삭제 (localStorage) | +| 레퍼런스 카드 | 이미지 업로드 + 메모 + 태그 + 카테고리 | +| 보드 뷰 | 카드 격자 배열 기본 화면 | +| 검색/필터 | 텍스트 검색, 카테고리 탭 필터 | +| Ctrl+V 붙여넣기 | 클립보드 이미지 → 새 카드 자동 생성 | + +### Phase 2 — 분석 도구 + +| 항목 | 내용 | +|------|------| +| 분석 카드 | 어노테이션 시스템 (마커, 하이라이트) | +| CRAP 체크리스트 | 디자인 원칙 체크 UI | +| Before/After 카드 | 전후 비교 카드 유형 | +| 갤러리 뷰 | 이미지 중심 큰 썸네일 | +| 리스트 뷰 | 테이블형 정렬/필터 | + +### Phase 3 — 패턴 라이브러리 + +| 항목 | 내용 | +|------|------| +| 패턴 카드 | 구성 요소 체크리스트, 가이드라인 | +| 패턴 프리셋 | SAM ERP 기본 패턴 8종 | +| 패턴 공유 | 프로젝트 간 패턴 공유 (di_patterns) | +| 내보내기 | JSON 백업, HTML 보고서 | + +### Phase 4 — 연계 & 고도화 + +| 항목 | 내용 | +|------|------| +| 기획디자인 연계 | 패턴 → 블록 템플릿 변환 | +| DB 저장 전환 | localStorage → DB (협업 지원) | +| 팀 공유 | 다른 사용자와 인사이트 공유 | + +--- + +## 10. 파일 구조 (예상) + +``` +mng/ +├── app/Http/Controllers/ +│ └── RdController.php ← designInsight() 메서드 추가 +├── resources/views/rd/design-insight/ +│ └── index.blade.php ← 전체 CSS + HTML + Alpine.js +└── routes/web.php ← Route 추가 +``` + +--- + +## 11. 관련 문서 + +- [기획디자인 기술 스펙](../features/rd/planning-design.md) — 모티브가 된 스토리보드 에디터 +- [기획디자인 프로젝트](../projects/planning-design/README.md) — 프로젝트 이력 +- [R&D 메뉴 개요](../features/rd/README.md) — R&D 전체 메뉴 구조 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/plans/fire-shutter-drawing-generator-plan.md b/plans/fire-shutter-drawing-generator-plan.md new file mode 100644 index 0000000..1b9a680 --- /dev/null +++ b/plans/fire-shutter-drawing-generator-plan.md @@ -0,0 +1,753 @@ +# 방화셔터 도면생성 기능 기획서 + +> **작성일**: 2026-03-08 +> **상태**: 기획 초안 +> **위치**: MNG > R&D > 방화셔터 도면생성 +> **라우트**: `GET /rd/fire-shutter-drawing` +> **참고**: 기존 `자동도면 생성` (`/rd/auto-drawing`) 구조를 확장 + +--- + +## 1. 개요 + +### 1.1 목적 + +방화셔터의 **가이드레일 단면**과 **셔터박스(케이스) 형태**를 파라미터로 입력하면, **2D 단면도(SVG)**와 **3D 렌더링(Three.js)**을 실시간으로 생성하는 도구를 제공한다. + +### 1.2 핵심 가치 + +| 기존 (수동) | 개선 (SAM 도면생성) | +|-------------|-------------------| +| CAD 프로그램에서 수동 작도 | 파라미터 입력 → 자동 도면 생성 | +| 도면 수정 시 전체 재작업 | 치수 변경 → 실시간 미리보기 | +| 제품별 도면 관리 어려움 | 프리셋 저장/불러오기로 재활용 | +| 영업/설치팀 도면 요청 대기 | 현장에서 즉시 단면도 확인 가능 | + +### 1.3 대상 사용자 + +- 설계팀: 방화셔터 단면 설계 및 검토 +- 영업팀: 고객 제안 시 단면도/3D 이미지 첨부 +- 설치팀: 현장 설치 전 가이드레일/케이스 형태 확인 +- 생산팀: 절곡/제작 사양 확인 + +--- + +## 2. 방화셔터 핵심 구조 + +### 2.1 전체 구성도 + +``` +┌─────────────────── 천장 슬래브 ───────────────────┐ +│ │ +│ ┌──────────── 셔터박스 (HEAD BOX / CASE) ──────┐ │ +│ │ ┌─────┐ ┌─────┐ │ │ +│ │ │브래킷│ [샤프트+슬랫 감김] │브래킷│ │ │ +│ │ └──┬──┘ [모터+감속기+브레이크] └──┬──┘ │ │ +│ │ │ [밸런스 스프링] │ │ │ +│ └─────┼─────────────────────────────────┼──────┘ │ +│ │ │ │ +│ ┌─────┴─────┐ ┌──────┴─────┐ │ +│ │ 가이드레일 │ ← 슬랫 커튼 → │ 가이드레일 │ │ +│ │ (좌) │ (강판/스크린) │ (우) │ │ +│ │ │ │ │ │ +│ │ 연기차단재│ │연기차단재 │ │ +│ │ │ │ │ │ +│ └─────┬─────┘ └──────┬─────┘ │ +│ │ │ │ +│ ══════╧═══ 하장바 (BOTTOM BAR) ═════════╧═══════ │ +│ [고무 실링] │ +└────────────────── 바닥 ──────────────────────────┘ +``` + +### 2.2 주요 구성요소 상세 + +#### A. 가이드레일 (Guide Rail) + +- **형태**: C-채널 단면 (ㄷ자 형태) +- **재질**: 강판 2.3mm 이상 +- **기능**: 슬랫 커튼의 좌우 안내 + 연기 차단 +- **표준 길이**: 2,438mm / 3,305mm / 4,430mm (조합 사용) +- **수량**: 항상 **2개** (좌우 1쌍) +- **부속**: 연기차단재(Smoke Seal Packing), 앵커볼트 + +``` +가이드레일 단면 (상단에서 본 모습) + + ┌────────────┐ + │ │ ← 가이드레일 본체 (C-채널) + │ ┌──────┐ │ + │ │ 연기 │ │ + │ │ 차단 │ │ + │ │ 재 │ │ + │ │ │ │ + │ │슬랫 │ │ + │ │엣지→ ● │ + │ │ │ │ + │ │ 연기 │ │ + │ │ 차단 │ │ + │ │ 재 │ │ + │ └──────┘ │ + │ │ + └────────────┘ + ■■■■■■■■■■■■■ ← 방화벽 +``` + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `rail_width` | 레일 전체 폭 | mm | 65 | +| `rail_depth` | 레일 깊이 (채널 깊이) | mm | 50 | +| `rail_thickness` | 강판 두께 | mm | 2.3 | +| `rail_lip` | 립(입구) 높이 | mm | 15 | +| `seal_thickness` | 연기차단재 두께 | mm | 5 | +| `seal_depth` | 연기차단재 깊이 | mm | 40 | +| `slat_thickness` | 슬랫 두께 (끼워지는 부분) | mm | 1.6 | +| `rail_height` | 레일 전체 높이 | mm | 3305 | +| `anchor_spacing` | 앵커볼트 간격 | mm | 500 | + +#### B. 셔터박스 / 케이스 (Head Box / Case) + +- **형태**: 직사각형 박스 (상부 천장 부착) +- **재질**: 강판 1.6mm 이상 +- **기능**: 샤프트/모터/슬랫 감김 수납 +- **표준 규격**: 1500×380mm / 500×380mm (개구부 크기에 따라) + +``` +셔터박스 단면 (정면에서 본 모습) + + ┌─────────────────────────────────────────┐ ← 상판 + │ │ + │ [브래킷] ┌──── 샤프트 ────┐ [브래킷] │ + │ │ │ (슬랫 감김) │ │ │ + │ ├──────┤ ├──────┤ │ + │ │ │ ◎ 중심축 │ │ │ + │ │ └───────────────┘ │ │ + │ │ │ │ + │ │ [모터+감속기] [브레이크] │ │ + │ │ [밸런스 스프링] │ │ + │ │ + └───┬─────────────────────────────────┬───┘ ← 하판 (슬랫 출구) + │ ↓ 슬랫 하강 ↓ │ + └─────────────────────────────────┘ +``` + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `box_width` | 케이스 전체 폭 (= 개구부 폭 + 마진) | mm | 1500 | +| `box_height` | 케이스 높이 | mm | 380 | +| `box_depth` | 케이스 깊이 (전후) | mm | 380 | +| `box_thickness` | 케이스 강판 두께 | mm | 1.6 | +| `shaft_diameter` | 샤프트 직경 | mm | 120 | +| `shaft_offset_x` | 샤프트 중심 수평 오프셋 | mm | 0 | +| `shaft_offset_y` | 샤프트 중심 수직 오프셋 | mm | 0 | +| `motor_side` | 모터 위치 (좌/우) | - | 우 | +| `slat_exit_width` | 슬랫 출구 폭 | mm | 1400 | +| `bracket_width` | 브래킷 폭 | mm | 80 | + +#### C. 슬랫 (Steel Slat / Screen) + +- **강판형**: EGI 강판 1.6mm, C/S형 인터록킹 프로파일 +- **스크린형**: 실리카/와이어 원단, 가이드레일 11mm 홈 +- **피치**: 75~100mm + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `slat_type` | 슬랫 유형 (강판/스크린) | - | 강판 | +| `slat_pitch` | 슬랫 피치 | mm | 80 | +| `slat_thickness` | 슬랫 두께 | mm | 1.6 | +| `slat_profile` | 단면 형태 (C형/S형) | - | C형 | + +#### D. 하장바 (Bottom Bar) + +- **기능**: 슬랫 커튼 하단 마감 + 바닥 밀착 +- **부속**: 고무 실링 + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `bar_width` | 하장바 폭 | mm | 60 | +| `bar_height` | 하장바 높이 | mm | 40 | +| `bar_seal_height` | 고무 실링 높이 | mm | 15 | + +--- + +## 3. 기능 설계 + +### 3.1 탭 구성 + +기존 자동도면 생성의 탭 구조를 참고하여 4개 탭으로 구성한다. + +``` +┌──────────┬──────────┬──────────┬──────────┐ +│ 설정 │ 가이드 │ 셔터박스 │ 3D │ +│ Settings │ 레일 │ (케이스) │ 렌더링 │ +└──────────┴──────────┴──────────┴──────────┘ +``` + +| 탭 | ID | 기능 | +|----|----|------| +| **설정** | `Settings` | 제품 유형 선택, 개구부 크기, 전역 설정 | +| **가이드레일** | `GuideRail` | 가이드레일 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 | +| **셔터박스** | `ShutterBox` | 셔터박스 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 | +| **3D 렌더링** | `3D` | 전체 방화셔터 조립체 3D 렌더링 (Three.js) | + +### 3.2 설정 탭 (Settings) + +#### 입력 항목 + +| 항목 | 타입 | 설명 | +|------|------|------| +| 제품 유형 | 드롭다운 | 강판형 (KFS) / 스크린형 (KSS) | +| 제품 모델 | 드롭다운 | KSS01, KSS02, KFS01 등 (유형 선택 시 필터링) | +| 개구부 폭 (W0) | 숫자 입력 | mm | +| 개구부 높이 (H0) | 숫자 입력 | mm | +| 수량 | 숫자 입력 | 기본값 1 | + +#### 자동 계산 (표시 전용) + +| 항목 | 수식 | 설명 | +|------|------|------| +| 제작 폭 (W1) | 스크린: W0+140 / 강판: W0+110 | 마진 포함 | +| 제작 높이 (H1) | H0+350 | 마진 포함 | +| 면적 (M) | W1 × H1 / 1,000,000 | m² | +| 중량 (K) | 스크린: M×2 / 강판: M×25 | kg | +| 권장 모터 | K 기준 자동 선택 | 150K~1500K | + +#### 프리셋 관리 + +- **프리셋 저장**: 현재 파라미터를 이름 지정하여 localStorage에 저장 +- **프리셋 불러오기**: 저장된 프리셋 목록에서 선택하여 파라미터 복원 +- **기본 프리셋**: 강판형 기본, 스크린형 기본 (제품 유형 선택 시 자동 적용) + +### 3.3 가이드레일 탭 + +#### UI 구성 (2컬럼 레이아웃) + +``` +┌──────────────────────┬──────────────────────────────────┐ +│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │ +│ │ │ +│ ■ 레일 전체 폭: [65] │ │ +│ ■ 레일 깊이: [50] │ ┌────────┐ │ +│ ■ 강판 두께: [2.3] │ │ │ │ +│ ■ 립 높이: [15] │ │ ┌────┐ │ ← SVG 실시간 │ +│ ■ 연기차단재: [5/40] │ │ │ ● │ │ 렌더링 │ +│ ■ 슬랫 두께: [1.6] │ │ └────┘ │ │ +│ │ │ │ │ +│ [치수 표시 ON/OFF] │ └────────┘ │ +│ [연기차단재 ON/OFF] │ ← 치수 라벨 (mm) │ +│ │ │ +│ ■ 레일 높이: [3305] │ [줌 +] [줌 -] [리셋] [DXF 저장] │ +│ ■ 앵커 간격: [500] │ │ +└──────────────────────┴──────────────────────────────────┘ +``` + +#### SVG 단면도 렌더링 상세 + +**뷰 모드 3가지**: + +1. **횡단면도 (Cross-Section)**: 가이드레일을 위에서 본 단면 — 슬랫이 레일에 끼워진 형태 +2. **종단면도 (Longitudinal)**: 가이드레일을 측면에서 본 단면 — 앵커볼트 배치 +3. **정면도 (Front View)**: 가이드레일을 정면에서 본 모습 — 레일 전체 높이 + 앵커 위치 + +**렌더링 요소**: + +| 요소 | 색상 | 설명 | +|------|------|------| +| 레일 본체 | `#94a3b8` (은회색) | 강판 단면 | +| 연기차단재 | `#f97316` (주황) | 실링 재질 | +| 슬랫 엣지 | `#60a5fa` (파랑) | 레일 안의 슬랫 | +| 방화벽 | `#a1887f` (갈색 해칭) | 콘크리트 벽 | +| 앵커볼트 | `#ef4444` (빨강) | 고정 부속 | +| 치수선 | `#3b82f6` (파랑) | mm 단위 치수 | + +### 3.4 셔터박스 탭 + +#### UI 구성 (2컬럼 레이아웃) + +``` +┌──────────────────────┬──────────────────────────────────┐ +│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │ +│ │ │ +│ ■ 케이스 폭: [1500] │ ┌────────────────────────────┐ │ +│ ■ 케이스 높이: [380] │ │ [브래킷] ◎샤프트 [브래킷]│ │ +│ ■ 케이스 깊이: [380] │ │ 감김 슬랫 │ │ +│ ■ 강판 두께: [1.6] │ │ [모터+감속기] [브레이크] │ │ +│ │ └────────────────────────────┘ │ +│ ■ 샤프트 직경: [120] │ │ +│ ■ 샤프트 오프셋 │ ← SVG 실시간 렌더링 │ +│ X: [0] Y: [0] │ ← 치수 라벨 (mm) │ +│ ■ 모터 위치: [좌/우] │ │ +│ │ │ +│ ■ 내부 부품 표시 │ [줌 +] [줌 -] [리셋] [DXF 저장] │ +│ □ 샤프트 │ │ +│ □ 모터/감속기 │ │ +│ □ 브레이크 │ │ +│ □ 밸런스 스프링 │ │ +└──────────────────────┴──────────────────────────────────┘ +``` + +#### SVG 단면도 렌더링 상세 + +**뷰 모드 3가지**: + +1. **정면 단면도**: 케이스를 정면에서 본 내부 구조 (샤프트, 모터, 브래킷 위치) +2. **측면 단면도**: 케이스를 측면에서 본 단면 (깊이 방향, 슬랫 감김 단면) +3. **하부 상세도**: 슬랫 출구 부분 확대 + +**렌더링 요소**: + +| 요소 | 색상 | 설명 | +|------|------|------| +| 케이스 외곽 | `#94a3b8` (은회색) | 강판 박스 | +| 샤프트 | `#64748b` (짙은 회색) | 중심축 + 감김 슬랫 | +| 모터 | `#3b82f6` (파랑) | 전동 개폐기 | +| 브레이크 | `#ef4444` (빨강) | 전자 브레이크 | +| 스프링 | `#22c55e` (녹색) | 밸런스 스프링 | +| 브래킷 | `#8b5cf6` (보라) | 벽 고정 브래킷 | +| 슬랫 | `#f59e0b` (주황) | 감긴 슬랫 단면 | + +### 3.5 3D 렌더링 탭 + +#### 렌더링 대상 + +Three.js를 사용하여 방화셔터 전체 조립체를 3D로 시각화한다. + +``` +3D 렌더링 요소: +├── 셔터박스 (반투명 상자) +│ ├── 샤프트 (원통) +│ ├── 감긴 슬랫 (원통 표면) +│ ├── 모터+감속기 (박스) +│ ├── 브레이크 (디스크) +│ └── 브래킷 (L형 판) +├── 가이드레일 좌 (C-채널 압출) +├── 가이드레일 우 (C-채널 압출) +├── 슬랫 커튼 (평면 텍스처) +│ ├── 강판형: 줄무늬 텍스처 (인터록킹 표현) +│ └── 스크린형: 반투명 메쉬 +├── 하장바 (직사각형 바) +└── 방화벽 (반투명 콘크리트 텍스처) +``` + +#### 3D 인터랙션 + +| 기능 | 조작 | 설명 | +|------|------|------| +| 회전 | 마우스 드래그 | OrbitControls | +| 줌 | 마우스 휠 | 확대/축소 | +| 팬 | 우클릭 드래그 | 시점 이동 | +| 부품 하이라이트 | 마우스 호버 | 해당 부품 강조 + 이름 표시 | +| 부품 ON/OFF | 체크박스 | 개별 부품 표시/숨김 | +| 투명도 | 슬라이더 | 케이스 투명도 조절 (내부 구조 확인) | +| 셔터 개폐 | 슬라이더 | 0%(전개)~100%(전폐) 애니메이션 | +| 조명 | 프리셋 | 기본/스튜디오/야외/드라마틱 | + +#### 3D 모델링 방식 + +DB나 외부 3D 파일 없이, **파라미터 기반 절차적 모델링(Procedural Modeling)**으로 구현한다. + +```javascript +// 가이드레일 C-채널 3D 생성 예시 (Three.js ExtrudeGeometry) +function createGuideRailMesh(params) { + const shape = new THREE.Shape(); + // C-채널 프로파일 경로 정의 + shape.moveTo(0, 0); + shape.lineTo(params.rail_width, 0); + shape.lineTo(params.rail_width, params.rail_lip); + shape.lineTo(params.rail_width - params.rail_thickness, params.rail_lip); + shape.lineTo(params.rail_width - params.rail_thickness, params.rail_thickness); + shape.lineTo(params.rail_thickness, params.rail_thickness); + shape.lineTo(params.rail_thickness, params.rail_lip); + shape.lineTo(0, params.rail_lip); + shape.lineTo(0, 0); + + // 높이 방향으로 압출 + const extrudeSettings = { + depth: params.rail_height, + bevelEnabled: false + }; + + return new THREE.Mesh( + new THREE.ExtrudeGeometry(shape, extrudeSettings), + new THREE.MeshStandardMaterial({ color: 0x94a3b8 }) + ); +} +``` + +### 3.6 출력 기능 + +| 기능 | 형식 | 설명 | +|------|------|------| +| **DXF 다운로드** | `.dxf` | 가이드레일/셔터박스 단면도를 CAD 호환 파일로 저장 | +| **PNG 다운로드** | `.png` | SVG 단면도를 이미지로 저장 | +| **3D 스크린샷** | `.png` | 3D 렌더링 현재 뷰를 이미지로 저장 | +| **파라미터 JSON** | `.json` | 현재 설정값을 파일로 내보내기/가져오기 | + +--- + +## 4. 기술 설계 + +### 4.1 아키텍처 + +기존 자동도면 생성과 동일한 **순수 클라이언트 측** 아키텍처를 사용한다. + +``` +┌─────────────────────────────────────────────┐ +│ Browser (Client-Side Only) │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Blade Template │ │ +│ │ (fire-shutter-drawing/index.blade) │ │ +│ ├─────────────────────────────────────┤ │ +│ │ JavaScript State Management │ │ +│ │ (fireShutterState 객체) │ │ +│ ├──────────┬──────────────────────────┤ │ +│ │ SVG 엔진 │ Three.js 3D 엔진 │ │ +│ │ (단면도) │ (조립체 렌더링) │ │ +│ ├──────────┴──────────────────────────┤ │ +│ │ DXF 생성기 │ PNG 내보내기 │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ DB 연동: 없음 (localStorage 프리셋만 사용) │ +│ API 호출: 없음 │ +└─────────────────────────────────────────────┘ +``` + +### 4.2 파일 구조 + +``` +mng/ +├── routes/web.php ← 라우트 추가 +├── app/Http/Controllers/RdController.php ← 메서드 추가 +└── resources/views/rd/fire-shutter-drawing/ + ├── index.blade.php ← 메인 레이아웃 + 탭 UI + ├── partials/ + │ ├── _settings.blade.php ← 설정 탭 HTML + │ ├── _guide-rail.blade.php ← 가이드레일 탭 HTML + │ ├── _shutter-box.blade.php ← 셔터박스 탭 HTML + │ └── _3d-viewer.blade.php ← 3D 렌더링 탭 HTML + └── js/ + (인라인 또는 @push('scripts')에 포함) +``` + +> **참고**: 기존 `auto-drawing/index.blade.php`는 단일 파일 4,884줄이다. 유지보수성을 위해 **Blade partial로 분리**하되, JavaScript는 상태 공유가 필요하므로 메인 파일의 `@push('scripts')`에 통합한다. + +### 4.3 상태 관리 객체 + +```javascript +const fireShutterState = { + // 활성 탭 + activeTab: 'Settings', + + // 설정 탭 + settings: { + productType: 'steel', // 'steel' | 'screen' + productModel: 'KFS01', // 제품 모델 코드 + openWidth: 2000, // 개구부 폭 W0 (mm) + openHeight: 3000, // 개구부 높이 H0 (mm) + quantity: 1, + // 자동 계산 + mfgWidth: 0, // 제작 폭 W1 + mfgHeight: 0, // 제작 높이 H1 + area: 0, // 면적 M (m²) + weight: 0, // 중량 K (kg) + motorSpec: '', // 권장 모터 + }, + + // 가이드레일 파라미터 + guideRail: { + width: 65, + depth: 50, + thickness: 2.3, + lip: 15, + sealThickness: 5, + sealDepth: 40, + slatThickness: 1.6, + height: 3305, + anchorSpacing: 500, + // 뷰 옵션 + showDimensions: true, + showSeal: true, + viewMode: 'cross', // 'cross' | 'longitudinal' | 'front' + }, + + // 셔터박스 파라미터 + shutterBox: { + width: 1500, + height: 380, + depth: 380, + thickness: 1.6, + shaftDiameter: 120, + shaftOffsetX: 0, + shaftOffsetY: 0, + motorSide: 'right', // 'left' | 'right' + slatExitWidth: 1400, + bracketWidth: 80, + // 내부 부품 표시 + showShaft: true, + showMotor: true, + showBrake: true, + showSpring: true, + viewMode: 'front', // 'front' | 'side' | 'bottom' + }, + + // 슬랫 파라미터 + slat: { + type: 'steel', // 'steel' | 'screen' + pitch: 80, + thickness: 1.6, + profile: 'C', // 'C' | 'S' + }, + + // 하장바 파라미터 + bottomBar: { + width: 60, + height: 40, + sealHeight: 15, + }, + + // 3D 뷰 설정 + threeD: { + caseOpacity: 0.3, // 케이스 투명도 + shutterPosition: 100, // 0=전개, 100=전폐 + showComponents: { + case: true, + shaft: true, + motor: true, + brake: true, + spring: true, + guideRailL: true, + guideRailR: true, + slats: true, + bottomBar: true, + wall: true, + }, + lightPreset: 'default', + }, + + // 프리셋 관리 + presets: [], // localStorage에서 로드 + + // 뷰 컨트롤 (줌/팬) + view: { + scale: 1, + offset: { x: 0, y: 0 }, + isDragging: false, + }, +}; +``` + +### 4.4 제품 유형별 기본값 매핑 + +```javascript +const PRODUCT_DEFAULTS = { + steel: { + label: '강판형', + marginW: 110, // W1 = W0 + 110 + marginH: 350, // H1 = H0 + 350 + weightFactor: 25, // K = M × 25 + guideRail: { width: 65, depth: 50, thickness: 2.3, lip: 15 }, + slat: { type: 'steel', pitch: 80, thickness: 1.6, profile: 'C' }, + }, + screen: { + label: '스크린형', + marginW: 140, // W1 = W0 + 140 + marginH: 350, // H1 = H0 + 350 + weightFactor: 2, // K = M × 2 + guideRail: { width: 30, depth: 25, thickness: 1.5, lip: 11 }, + slat: { type: 'screen', pitch: 100, thickness: 0.8, profile: 'flat' }, + }, +}; + +const MOTOR_TABLE = [ + { maxWeight: 150, spec: '150K', inch: 4 }, + { maxWeight: 300, spec: '300K', inch: 4 }, + { maxWeight: 500, spec: '500K', inch: 5 }, + { maxWeight: 750, spec: '750K', inch: 5 }, + { maxWeight: 1000, spec: '1000K', inch: 6 }, + { maxWeight: 1500, spec: '1500K', inch: 6 }, +]; +``` + +--- + +## 5. 개발 단계 + +### Phase 1: 기본 구조 + 가이드레일 단면도 (1단계) + +> **목표**: 라우트/컨트롤러/뷰 생성, 설정 탭, 가이드레일 SVG 단면도 + +| 작업 | 상세 | 예상 | +|------|------|------| +| 라우트 등록 | `GET /rd/fire-shutter-drawing` | 10분 | +| 컨트롤러 메서드 | `RdController@fireShutterDrawing` | 10분 | +| 레이아웃 + 탭 UI | 4탭 구조, 다크 테마 | 1시간 | +| 설정 탭 | 제품 유형/개구부 크기 입력 + 자동 계산 | 1시간 | +| 가이드레일 SVG 엔진 | C-채널 단면도 + 치수선 + 연기차단재 | 3시간 | +| 줌/팬 컨트롤 | 기존 auto-drawing 코드 재사용 | 30분 | + +**산출물**: 가이드레일 파라미터 입력 → SVG 횡단면도 실시간 렌더링 + +### Phase 2: 셔터박스 단면도 (2단계) + +> **목표**: 셔터박스(케이스) SVG 단면도 + 내부 부품 표시 + +| 작업 | 상세 | 예상 | +|------|------|------| +| 셔터박스 SVG 엔진 | 케이스 외곽 + 내부 구조 | 3시간 | +| 내부 부품 렌더링 | 샤프트, 모터, 브레이크, 스프링 | 2시간 | +| 뷰 모드 전환 | 정면/측면/하부 | 1시간 | +| 부품 ON/OFF 토글 | 체크박스 → SVG 요소 표시/숨김 | 30분 | + +**산출물**: 셔터박스 파라미터 입력 → SVG 단면도 (정면/측면/하부) + +### Phase 3: 3D 렌더링 (3단계) + +> **목표**: Three.js 기반 방화셔터 전체 조립체 3D 렌더링 + +| 작업 | 상세 | 예상 | +|------|------|------| +| Three.js 씬 구축 | 카메라, 조명, OrbitControls | 1시간 | +| 가이드레일 3D 모델 | ExtrudeGeometry (C-채널 압출) | 2시간 | +| 셔터박스 3D 모델 | BoxGeometry + 내부 부품 | 2시간 | +| 슬랫 커튼 3D 모델 | 평면 메쉬 + 텍스처 | 1시간 | +| 셔터 개폐 애니메이션 | 슬라이더 → 슬랫 위치 변경 | 1시간 | +| 조명/투명도 패널 | 기존 auto-drawing 패널 재사용 | 30분 | + +**산출물**: 파라미터 연동 3D 방화셔터 조립체 + 인터랙션 + +### Phase 4: 출력 + 프리셋 (4단계) + +> **목표**: DXF/PNG 저장, 프리셋 관리, 완성도 향상 + +| 작업 | 상세 | 예상 | +|------|------|------| +| DXF 내보내기 | 기존 DXF 생성기 확장 | 1시간 | +| PNG 내보내기 | SVG → Canvas → PNG | 30분 | +| 3D 스크린샷 | Three.js renderer.domElement.toDataURL | 15분 | +| 프리셋 저장/불러오기 | localStorage CRUD | 1시간 | +| JSON 가져오기/내보내기 | 파일 업로드/다운로드 | 30분 | +| UI 다듬기 | 반응형, 툴팁, 키보드 단축키 | 1시간 | + +**산출물**: 완성된 방화셔터 도면생성 도구 + +--- + +## 6. UI/UX 설계 + +### 6.1 디자인 시스템 + +기존 자동도면 생성의 **다크 테마 (Space Theme)**를 그대로 이어간다. + +| 요소 | 값 | +|------|------| +| 배경 | `#020617` (slate-950) | +| 패널 | `rgba(15, 23, 42, 0.7)` + backdrop-blur | +| 테두리 | `rgba(255, 255, 255, 0.1)` | +| 강조색 | `#3b82f6` (blue-500) | +| 텍스트 | `#f8fafc` (white) / `#94a3b8` (slate-400) | +| 입력 필드 | `bg-slate-950/80` + `border-slate-800` | + +### 6.2 반응형 레이아웃 + +``` +Desktop (1200px+): 2컬럼 (4:8 비율) +Tablet (768-1199px): 1컬럼 (상: 파라미터, 하: 미리보기) +Mobile: 지원 안 함 (최소 768px) +``` + +### 6.3 인터랙션 흐름 + +``` +사용자 → 제품 유형 선택 (강판/스크린) + → 기본값 자동 적용 + → 개구부 크기 입력 + → 제작 치수/중량/모터 자동 계산 + + → 가이드레일 탭 이동 + → 파라미터 조정 (폭, 깊이, 두께 등) + → SVG 실시간 업데이트 (입력 즉시) + → 뷰 모드 전환 (횡단면/종단면/정면) + → DXF 다운로드 가능 + + → 셔터박스 탭 이동 + → 파라미터 조정 + → SVG 실시간 업데이트 + → 내부 부품 ON/OFF + + → 3D 탭 이동 + → 전체 조립체 3D 뷰 + → 셔터 개폐 애니메이션 + → 스크린샷 저장 +``` + +--- + +## 7. 메뉴 등록 + +### 7.1 메뉴 위치 + +``` +R&D +├── 대시보드 +├── 조직도 관리 +├── 중대재해처벌법 점검 +├── AI 견적 +├── 기획디자인 +├── 디자인 인사이트 +├── 사운드 로고 스튜디오 +├── CM송 제작 +├── 자동도면 생성 ← 기존 +└── 방화셔터 도면생성 ← 신규 (자동도면 하위에 배치) +``` + +### 7.2 메뉴 등록 (tinker) + +```php +// 개발 서버 +App\Models\Commons\Menu::create([ + 'tenant_id' => 1, + 'parent_id' => , + 'name' => '방화셔터 도면생성', + 'url' => '/rd/fire-shutter-drawing', + 'icon' => 'shield', + 'sort_order' => <자동도면 다음 순서>, + 'is_active' => true, +]); +``` + +> **주의**: 메뉴 시더 실행 금지 — tinker로 수동 등록 + +--- + +## 8. 향후 확장 가능성 + +| 확장 | 설명 | 우선순위 | +|------|------|---------| +| **STL/OBJ 내보내기** | 3D 프린팅/CAD 호환 | 🟡 중요 | +| **견적 연동** | 도면 파라미터 → 견적 자동 산출 | 🔴 필수 | +| **제품 카탈로그 연동** | DB에서 제품별 기본 파라미터 로드 | 🟡 중요 | +| **비교 모드** | 2개 설정을 나란히 비교 | 🟢 권장 | +| **PDF 도면 출력** | A3/A4 도면 양식 포함 출력 | 🟡 중요 | +| **설치 시뮬레이션** | 현장 사진 위에 3D 오버레이 | 🟢 권장 | + +--- + +## 관련 문서 + +- `/home/aweso/sam/docs/features/academy/fire-shutter-image-prompts.md` — 방화셔터 이미지 프롬프트 +- `/home/aweso/sam/docs/samples/방화셔터_견적구조_인터뷰.md` — 견적 구조 인터뷰 +- `/home/aweso/sam/docs/features/quotes/README.md` — 견적 시스템 분석 +- `/home/aweso/sam/docs/projects/quotation/phase-1-5130-analysis/js-formulas.md` — 견적 수식 상세 +- `/home/aweso/sam/mng/resources/views/rd/auto-drawing/index.blade.php` — 기존 자동도면 생성 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/plans/sound-logo-generator-plan.md b/plans/sound-logo-generator-plan.md new file mode 100644 index 0000000..a3ecf48 --- /dev/null +++ b/plans/sound-logo-generator-plan.md @@ -0,0 +1,637 @@ +# 사운드 로고 생성기 — 기획서 + +> **작성일**: 2026-03-08 +> **상태**: 기획 확정 +> **메뉴명**: 사운드 로고 생성기 +> **라우트**: `GET /rd/sound-logo` +> **담당**: R&D실 + +--- + +## 1. 개요 + +### 1.1 목적 + +1~5초의 **짧고 강렬한 시그니처 사운드(Sound Logo)**를 생성하는 도구. 브라우저 내 Web Audio API 신디사이저와 Google Gemini AI를 결합하여, 누구나 전문적인 사운드 로고를 만들 수 있도록 한다. + +### 1.2 벤치마킹 + +| 브랜드 | 사운드 | 길이 | 특징 | +|--------|--------|------|------| +| Intel | 봉-봉봉봉-봉 | 1.5초 | 5음, 밝고 미래적 | +| Netflix | 타-둠 | 3초 | 2음, 깊은 울림 + 리버브 | +| Samsung | 오버더호라이즌 | 2초 | 5음, 따뜻한 멜로디 | +| McDonald's | 바다바바~ | 2초 | 5음, 경쾌한 리듬 | +| Windows | 시작 사운드 | 3초 | 4음, 화음 진행 | +| 카카오톡 | 카톡~ | 0.5초 | 2음, 귀여운 효과음 | +| T-Mobile | 띠-띠띠-띠-띠 | 1초 | 5음, 단순 반복 | + +### 1.3 핵심 차별점 — AI 어드바이저 엔진 + +단순 신디사이저 도구가 아니라, **Google Gemini AI가 음악 이론 기반으로 조언하고 생성을 도와주는** 지능형 도구. + +``` +┌─────────────────────────────────────────────────────┐ +│ 사운드 로고 생성기 │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 수동 모드 │ │ AI 어시스트│ │ AI 자동 │ │ +│ │ (신디사이저)│ ←→ │ (Gemini) │ ←→ │ (Lyria) │ │ +│ │ │ │ │ │ │ │ +│ │ 음표 직접 │ │ 브랜드 분석│ │ 프롬프트→ │ │ +│ │ 배치/편집 │ │ 음악 추천 │ │ AI 음악 │ │ +│ │ │ │ 코드 제안 │ │ 직접 생성 │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ ↕ ↕ ↕ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Web Audio API 재생 엔진 │ │ +│ │ (실시간 미리듣기 + WAV 내보내기) │ │ +│ └──────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 3가지 모드 설계 + +### 2.1 모드 A — 수동 모드 (신디사이저) + +> 음표를 직접 배치하고 파라미터를 조절하여 사운드를 만드는 전통적 방식 + +| 기능 | 설명 | +|------|------| +| 음표 시퀀서 | 음표(C4, E4 등) + 길이(0.05~2초) + 쉼표를 시각적 배열로 편집 | +| 신디사이저 4종 | Sine(부드러움), Square(8bit), Triangle(따뜻함), Sawtooth(날카로움) | +| ADSR 엔벨로프 | Attack(0~500ms), Decay(0~1s), Sustain(0~1), Release(0~3s) | +| 화음(Chord) | 동시에 여러 음 재생 (C+E+G = C Major 등) | +| 이펙트 | 리버브, 딜레이, 로우패스/하이패스 필터 | +| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 표시 | + +**기술 기반**: Web Audio API (`OscillatorNode`, `GainNode`, `BiquadFilterNode`, `ConvolverNode`) + +### 2.2 모드 B — AI 어시스트 (Gemini 텍스트) + +> 브랜드 정보를 입력하면 Gemini가 **음악 이론 기반으로 사운드 로고를 설계**해 주고, 사용자가 미세 조정 + +**입력 → AI 분석 → 음표 데이터 출력 → Web Audio 재생** + +``` +사용자 입력 Gemini 분석·추천 출력 +┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐ +│ 브랜드명 │ │ │ │ │ +│ 업종/분위기 │──요청──→ │ 1. 브랜드 성격 분석 │──JSON──→ │ 음표 시퀀스 │ +│ 키워드 │ │ 2. 조성/스케일 추천 │ │ BPM, 조성 │ +│ 참고 브랜드 │ │ 3. 음표 시퀀스 생성 │ │ 신디 파라미터 │ +│ 원하는 느낌 │ │ 4. ADSR 파라미터 제안 │ │ ADSR 값 │ +└───────────────┘ │ 5. 이펙트 추천 │ │ 이펙트 설정 │ + │ 6. 근거 설명 │ │ │ + └──────────────────────┘ └──────────────┘ + ↓ + Web Audio 재생 + ↓ + 사용자 미세 조정 +``` + +**Gemini 프롬프트 설계**: + +``` +당신은 사운드 브랜딩 전문가이자 음악 이론가입니다. +다음 브랜드 정보를 분석하여 1~5초 사운드 로고를 설계해 주세요. + +[브랜드 정보] +- 브랜드명: {name} +- 업종: {industry} +- 브랜드 성격: {personality} (예: 혁신적, 신뢰, 친근함) +- 참고 사운드: {reference} (예: 인텔처럼 밝은 느낌) +- 원하는 길이: {duration}초 + +[응답 형식 - 반드시 JSON으로] +{ + "analysis": "브랜드 분석 설명 (한글)", + "reasoning": "이 사운드를 추천하는 음악 이론적 근거 (한글)", + "key": "C", + "scale": "major", + "bpm": 120, + "synth": "sine", + "adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 }, + "effects": { "reverb": 0.3, "delay": 0 }, + "notes": [ + { "note": "C5", "duration": 0.2, "velocity": 0.8 }, + { "note": "E5", "duration": 0.2, "velocity": 0.9 }, + { "note": "G5", "duration": 0.15, "velocity": 0.7 }, + { "rest": 0.05 }, + { "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 } + ], + "variations": [ + { "name": "밝은 버전", "notes": [...] }, + { "name": "차분한 버전", "notes": [...] } + ] +} +``` + +**AI 어시스트 기능 상세**: + +| 기능 | 설명 | +|------|------| +| 브랜드 분석 | 업종·성격 기반 적합한 조성/템포/음색 추천 | +| 음표 시퀀스 생성 | 음악 이론(화성학, 리듬 패턴) 기반 멜로디 제안 | +| 변형 3종 제공 | 밝은/차분한/임팩트 버전 동시 생성 | +| 근거 설명 | "C Major → 신뢰감, 5도 상행 → 상승 에너지" 등 이론 설명 | +| 반복 개선 | "좀 더 밝게" "더 짧게" 등 자연어로 수정 요청 | + +### 2.3 모드 C — AI 자동 생성 (Google Lyria) + +> Google Lyria AI가 프롬프트 기반으로 **실제 음악을 직접 생성** + +**2가지 Lyria 엔진 지원** (기존 API 키로 사용 가능, 별도 발급 불필요): + +| 엔진 | 인증 | 방식 | 특징 | +|------|------|------|------| +| **Lyria RealTime** (권장) | 기존 Gemini API 키 | WebSocket (브라우저 직접) | 실시간 스트리밍, BPM/스케일 실시간 조절 | +| Lyria 2 (폴백) | Vertex AI 서비스 계정 | REST API (서버 경유) | 30초 단위 파일 생성, $0.06/30초 | + +#### Lyria RealTime — 브라우저에서 직접 음악 생성 + +``` +브라우저 (Alpine.js) Google API +┌───────────────────┐ ┌──────────────────┐ +│ BPM: 130 │ │ │ +│ Scale: C Major │──WebSocket 연결──→ │ Lyria RealTime │ +│ 프롬프트 입력 │ │ (lyria-realtime- │ +│ │←─2초 단위 오디오── │ exp) │ +│ 🔊 실시간 재생 │ │ │ +│ BPM 슬라이더 조절 │──실시간 파라미터──→ │ 즉시 반영 │ +└───────────────────┘ └──────────────────┘ +``` + +- **모델**: `lyria-realtime-exp` (experimental, Gemini API v1alpha) +- **인증**: 기존 `.env`의 `GEMINI_API_KEY` 그대로 사용 +- **출력**: 48kHz 스테레오, 2초 청크 단위 스트리밍 +- **제어 파라미터**: BPM(60~200), Scale(Key + Mode), 텍스트 프롬프트 +- **지연**: 파라미터 변경 후 최대 2초 이내 반영 + +**Lyria RealTime 프롬프트 예시**: + +``` +Short sonic logo. Bright, futuristic, memorable melody. +Clean synthesizer with light reverb. +Ascending progression, major chord resolution. +``` + +#### Lyria 2 — 서버 경유 파일 생성 (폴백) + +기존 `BgmService::generateWithLyria()` 패턴 재활용. Vertex AI 서비스 계정(`google_service_account.json`) 이미 보유. + +``` +사용자 입력 서버 (Laravel) 결과 +┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐ +│ 분위기 선택 │ │ SoundLogoService │ │ │ +│ 길이 (1~5초) │──POST──→ │ → Lyria 2 API (REST) │──→ │ WAV/MP3 파일 │ +│ 프롬프트 입력 │ │ (Vertex AI) │ │ (다운로드) │ +└───────────────┘ └──────────────────────┘ └──────────────┘ +``` + +> **우선순위**: Lyria RealTime(브라우저) 먼저 시도 → 실패 시 Lyria 2(서버) 폴백. +> 두 엔진 모두 기존 인증 정보로 사용 가능하며 별도 API 키 발급 불필요. + +--- + +## 3. UI 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎵 사운드 로고 생성기 [내 프로젝트 ▾] [저장] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 모드: [🎹 수동] [🤖 AI 어시스트] [✨ AI 자동] │ +│ │ +│ ┌─ 모드 B: AI 어시스트 ──────────────────────────────────────┐ │ +│ │ │ │ +│ │ 브랜드명 [SAM ] │ │ +│ │ 업종 [ERP/MES 통합 솔루션 ] │ │ +│ │ 브랜드 성격 [○혁신적 ●신뢰 ○친근 ○고급 ○에너지] │ │ +│ │ 참고 사운드 [인텔처럼 짧고 밝은 ▾ ] │ │ +│ │ 길이 [━━━●━━━ 2초 ] │ │ +│ │ │ │ +│ │ [🤖 AI에게 사운드 설계 요청] │ │ +│ │ │ │ +│ │ ┌─ AI 분석 결과 ─────────────────────────────────────┐ │ │ +│ │ │ 💡 "SAM은 ERP/MES 통합 솔루션으로, 신뢰와 기술력을 │ │ │ +│ │ │ 전달해야 합니다. C Major 조성으로 안정감을, │ │ │ +│ │ │ 5도 상행 진행으로 성장과 발전을 표현합니다." │ │ │ +│ │ │ │ │ │ +│ │ │ 추천: C Major | BPM 130 | Sine + 리버브 30% │ │ │ +│ │ │ │ │ │ +│ │ │ 변형 3종: │ │ │ +│ │ │ [▶ 밝은 버전] [▶ 차분한 버전] [▶ 임팩트 버전] │ │ │ +│ │ └─────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ 💬 추가 요청: [좀 더 짧고 강렬하게 해줘 ] [전송] │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 음표 에디터 (AI 결과 또는 수동 편집) ──────────────────────┐ │ +│ │ C5(0.2s) E5(0.2s) G5(0.15s) .(0.05s) [CEG](0.8s) │ │ +│ │ ████ ████ ███ · ████████████ │ │ +│ │ [+음표] [+쉼표] [+화음] [삭제] │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 파라미터 ────────────────────────────────────────────────┐ │ +│ │ 음색: ●Sine ○Square ○Triangle ○Sawtooth │ │ +│ │ BPM: ━━━━━━●━━━━━ 130 │ │ +│ │ 리버브: ━━━●━━━━━━━ 30% │ │ +│ │ Attack: ━●━━━━━━━━ 10ms Release: ━━━━━●━━━ 500ms │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 파형 시각화 ─────────────────────────────────────────────┐ │ +│ │ ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿ │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ [▶ 재생] [⏹ 정지] [💾 WAV 저장] [📋 JSON 내보내기] │ +│ │ +├─ 내 사운드 라이브러리 ──────────────────────────────────────────┤ +│ 🎵 SAM 시그널 v1 | 🎵 알림음 v2 | 🎵 전환 효과 | [+ 새로] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 프리셋 템플릿 (10종) + +| 프리셋 | 스타일 | 음표 수 | 길이 | 용도 | +|--------|--------|---------|------|------| +| 기업 시그널 (밝음) | Intel 스타일 | 5 | 1.5초 | 브랜드 인트로 | +| 기업 시그널 (무게감) | Netflix 스타일 | 2 | 3초 | 프리미엄 브랜드 | +| 알림음 (경쾌) | 카카오톡 스타일 | 2~3 | 0.5초 | 푸시 알림 | +| 알림음 (정보) | Slack 스타일 | 3 | 1초 | 시스템 알림 | +| 성공 사운드 | 게임 레벨업 | 4 | 1초 | 작업 완료 | +| 에러 사운드 | 경고음 | 2 | 0.5초 | 오류 알림 | +| 전환 효과 (업) | 상승 스윕 | 연속 | 0.5초 | 화면 전환 | +| 전환 효과 (다운) | 하강 스윕 | 연속 | 0.5초 | 메뉴 닫기 | +| 팡파레 | 축하 | 6 | 2초 | 이벤트/달성 | +| 로딩 루프 | 반복 패턴 | 4 | 2초 | 대기 상태 | + +--- + +## 5. 기술 아키텍처 + +### 5.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 프론트엔드 | Blade + Alpine.js | 단일 파일 SPA (디자인 인사이트 패턴) | +| 오디오 엔진 | Web Audio API | `OscillatorNode`, `GainNode`, `ConvolverNode` | +| 시각화 | Canvas API | `AnalyserNode` → 파형/스펙트럼 렌더링 | +| AI 어시스트 | Gemini 2.5 Flash | 텍스트 기반 음악 이론 분석·추천 (모드 B) | +| AI 자동 생성 (1차) | Lyria RealTime | WebSocket 실시간 음악 스트리밍 (모드 C, 브라우저 직접) | +| AI 자동 생성 (폴백) | Lyria 2 (Vertex AI) | REST API 파일 생성 (모드 C, 서버 경유) | +| 저장 | localStorage + DB | 프로젝트 데이터(localStorage), 음원 파일(DB+Storage) | + +### 5.2 기존 인프라 재활용 + +| 기존 코드 | 재활용 내용 | +|----------|-----------| +| `BgmService::generateWithLyria()` | Lyria API 호출 패턴, Vertex AI 인증 흐름 | +| `BgmService::getMoodChord()` | 분위기별 화음 주파수 매핑 | +| `BgmService::generateAmbient()` | FFmpeg 기반 오디오 합성 (서버사이드 폴백) | +| `CmSongController::generateLyrics()` | Gemini API 호출 패턴 (프롬프트 → JSON 응답) | +| `CmSongController::pcmToWav()` | PCM → WAV 변환 유틸리티 | +| `AiConfig::getActiveGemini()` | AI 설정 조회 (API 키, 모델, 리전) | +| `GoogleCloudService::getAccessToken()` | Vertex AI 인증 토큰 | + +### 5.3 파일 구조 + +``` +app/Http/Controllers/Rd/ +└── SoundLogoController.php # 컨트롤러 (AI API 프록시) + +app/Services/Rd/ +└── SoundLogoService.php # Gemini 프롬프트 + Lyria 호출 + +resources/views/rd/sound-logo/ +└── index.blade.php # 단일 파일 SPA + +routes/web.php # 라우트 추가 +``` + +### 5.4 라우트 설계 + +| Method | Path | 컨트롤러 | 설명 | +|--------|------|---------|------| +| `GET` | `/rd/sound-logo` | `soundLogo.index` | 메인 페이지 | +| `POST` | `/rd/sound-logo/ai-assist` | `soundLogo.aiAssist` | Gemini 음악 설계 요청 (모드 B) | +| `POST` | `/rd/sound-logo/ai-refine` | `soundLogo.aiRefine` | Gemini 추가 수정 요청 | +| `POST` | `/rd/sound-logo/ai-generate` | `soundLogo.aiGenerate` | Lyria 음악 생성 (모드 C) | +| `POST` | `/rd/sound-logo/save` | `soundLogo.save` | 사운드 저장 (DB + Storage) | +| `GET` | `/rd/sound-logo/{id}/download` | `soundLogo.download` | WAV 다운로드 | + +### 5.5 Web Audio API 핵심 구조 + +```javascript +// 노드 그래프 +const ctx = new AudioContext(); + +// Oscillator → Gain(ADSR) → Filter → Reverb → Analyser → Destination +function createSynthChain(type, freq, adsr, effects) { + const osc = ctx.createOscillator(); // 음원 + const gain = ctx.createGain(); // ADSR 엔벨로프 + const filter = ctx.createBiquadFilter(); // LP/HP 필터 + const analyser = ctx.createAnalyser(); // 시각화 + + osc.type = type; // sine | square | triangle | sawtooth + osc.frequency.value = freq; // Hz + + // ADSR + const now = ctx.currentTime; + gain.gain.setValueAtTime(0, now); + gain.gain.linearRampToValueAtTime(1, now + adsr.attack); + gain.gain.linearRampToValueAtTime(adsr.sustain, now + adsr.attack + adsr.decay); + + osc.connect(gain).connect(filter).connect(analyser).connect(ctx.destination); + return { osc, gain, filter, analyser }; +} + +// WAV 내보내기 (OfflineAudioContext) +async function exportWav(notes, params) { + const offline = new OfflineAudioContext(2, 44100 * duration, 44100); + // ... 노트 렌더링 + const buffer = await offline.startRendering(); + const wav = audioBufferToWav(buffer); + // Blob → 다운로드 +} +``` + +### 5.6 음표 ↔ 주파수 매핑 + +```javascript +const NOTE_FREQ = { + 'C3': 130.81, 'D3': 146.83, 'E3': 164.81, 'F3': 174.61, + 'G3': 196.00, 'A3': 220.00, 'B3': 246.94, + 'C4': 261.63, 'D4': 293.66, 'E4': 329.63, 'F4': 349.23, + 'G4': 392.00, 'A4': 440.00, 'B4': 493.88, + 'C5': 523.25, 'D5': 587.33, 'E5': 659.25, 'F5': 698.46, + 'G5': 783.99, 'A5': 880.00, 'B5': 987.77, + 'C6': 1046.50 + // 반음(#/b)도 포함 +}; +``` + +--- + +## 6. Phase별 개발 계획 + +### Phase 1 — MVP (수동 모드 + 프리셋) + +| 항목 | 내용 | +|------|------| +| 음표 시퀀서 UI | 음표 추가/삭제/편집, 드래그 순서 변경 | +| 신디사이저 4종 | Sine, Square, Triangle, Sawtooth | +| ADSR 슬라이더 | Attack, Decay, Sustain, Release | +| 실시간 재생 | Web Audio API 즉시 재생 | +| WAV 내보내기 | OfflineAudioContext → WAV 다운로드 | +| 프리셋 10종 | 즉시 로드 가능한 사운드 패턴 | +| 프로젝트 저장 | localStorage (디자인 인사이트 패턴) | + +### Phase 2 — AI 어시스트 (Gemini) + +| 항목 | 내용 | +|------|------| +| 브랜드 입력 폼 | 브랜드명, 업종, 성격, 참고 사운드, 길이 | +| Gemini 분석 API | 브랜드 → 음악 이론 기반 사운드 설계 JSON | +| 변형 3종 생성 | 밝은/차분한/임팩트 버전 동시 제공 | +| 대화형 개선 | "좀 더 밝게" 등 자연어 추가 수정 | +| 근거 표시 | AI가 이 사운드를 추천하는 이유 설명 | +| 라우트 | `POST /rd/sound-logo/ai-assist`, `ai-refine` | + +### Phase 3 — AI 자동 생성 (Lyria RealTime + Lyria 2) + 고도화 + +| 항목 | 내용 | +|------|------| +| Lyria RealTime 연동 | WebSocket으로 브라우저에서 직접 실시간 음악 생성 (기존 Gemini API 키) | +| Lyria 2 폴백 | Vertex AI REST API로 서버 경유 파일 생성 (기존 서비스 계정) | +| 실시간 BPM/스케일 조절 | Lyria RealTime의 파라미터 실시간 변경 | +| 화음(Chord) 편집 | 동시에 여러 음 배치 | +| 이펙트 체인 | 리버브, 딜레이, 필터 | +| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 | +| DB 저장 | 사운드 로고를 DB + Storage에 영구 저장 | +| 공유/내보내기 | JSON 설정 공유, MP3 변환 | + +### Phase 4 — 프로급 확장 (선택) + +| 항목 | 내용 | +|------|------| +| 타임라인 UI | 드래그로 음표 배치하는 DAW 스타일 | +| 샘플 기반 음색 | 피아노, 벨, 마림바 등 실제 악기 | +| 드럼/퍼커션 | 노이즈 기반 킥/스네어/하이햇 | +| MIDI 내보내기 | 전문 DAW에서 추가 편집 가능 | +| A/B 비교 | 두 사운드를 나란히 비교 재생 | + +--- + +## 7. Gemini AI 연동 상세 + +### 7.1 API 호출 흐름 + +``` +Frontend (Alpine.js) + │ + │ POST /rd/sound-logo/ai-assist + │ { brand_name, industry, personality, reference, duration } + │ + ▼ +SoundLogoController::aiAssist() + │ + │ 프롬프트 구성 + │ + ▼ +Gemini 2.5 Flash API + │ + │ JSON 응답 (notes, adsr, effects, analysis) + │ + ▼ +SoundLogoController → JsonResponse + │ + │ { success: true, data: { notes, params, analysis, variations } } + │ + ▼ +Frontend: 음표 에디터에 자동 로드 → 즉시 재생 +``` + +### 7.2 대화형 개선 흐름 + +``` +사용자: "좀 더 짧고 강렬하게" + │ + ▼ +POST /rd/sound-logo/ai-refine +{ previous_notes: [...], feedback: "좀 더 짧고 강렬하게" } + │ + ▼ +Gemini: 기존 노트를 분석하고 피드백 반영하여 수정된 JSON 반환 + │ + ▼ +수정된 음표가 에디터에 반영 +``` + +### 7.3 Lyria 음악 생성 흐름 + +#### 7.3.1 Lyria RealTime (브라우저 직접, 권장) + +``` +사용자: "AI 자동 생성" 탭 → Lyria RealTime 선택 + │ + ▼ +브라우저 JavaScript (Alpine.js) + │ + │ WebSocket 연결 (Gemini API v1alpha) + │ wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent + │ API Key: .env GEMINI_API_KEY (서버 경유 프록시) + │ Model: lyria-realtime-exp + │ + ▼ +Lyria RealTime 스트리밍 + │ + │ 2초 청크 단위 48kHz 스테레오 오디오 + │ ← BPM/Scale 실시간 조절 가능 + │ + ▼ +Web Audio API로 실시간 재생 + 녹음(MediaRecorder) → WAV 저장 +``` + +> **API 키 보안**: 브라우저에서 직접 Gemini API 키를 노출하지 않기 위해, +> 서버를 WebSocket 프록시로 사용하거나 `/rd/sound-logo/ws-token` 엔드포인트에서 +> 임시 토큰을 발급하는 방식을 검토한다. + +#### 7.3.2 Lyria 2 (서버 경유, 폴백) + +``` +사용자: Lyria RealTime 실패 시 자동 전환 + │ + ▼ +POST /rd/sound-logo/ai-generate +{ mood: "bright_futuristic", duration: 3, prompt: "..." } + │ + ▼ +SoundLogoService::generateWithLyria() + ├── AiConfig::getActiveGemini() → Vertex AI 설정 확인 + ├── GoogleCloudService::getAccessToken() → OAuth 토큰 + └── Lyria API 호출 → audioContent (base64) + │ + ▼ +WAV 파일 저장 → 다운로드 URL 반환 +``` + +--- + +## 8. 데이터 모델 + +### 8.1 localStorage 구조 (Phase 1~2) + +```json +{ + "sl_projects": [ + { + "id": "sl_1709000000_abc", + "title": "SAM 사운드 로고", + "sounds": [ + { + "id": "snd_001", + "name": "SAM 시그널 v1", + "notes": [ + { "note": "C5", "duration": 0.2, "velocity": 0.8 }, + { "note": "E5", "duration": 0.2, "velocity": 0.9 }, + { "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 } + ], + "params": { + "synth": "sine", + "bpm": 130, + "adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 }, + "effects": { "reverb": 0.3, "delay": 0, "filterFreq": 2000 } + }, + "aiAnalysis": "C Major 조성, 5도 상행으로 신뢰감과 성장 표현", + "createdAt": "2026-03-08T00:00:00.000Z" + } + ], + "createdAt": "2026-03-08T00:00:00.000Z" + } + ], + "sl_current": "sl_1709000000_abc" +} +``` + +### 8.2 DB 테이블 (Phase 3, API 프로젝트에서 마이그레이션) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | bigint | PK | +| `tenant_id` | bigint | FK → tenants | +| `user_id` | bigint | FK → users | +| `name` | varchar(200) | 사운드 이름 | +| `audio_path` | varchar(500) | WAV/MP3 파일 경로 | +| `options` | json | notes, params, aiAnalysis 등 | +| `created_at` | timestamp | | +| `updated_at` | timestamp | | + +--- + +## 9. API 인증 및 키 현황 + +> **별도 API 키 발급 불필요** — 기존 인증 정보로 모든 엔진 사용 가능 + +### 9.1 사용 가능한 인증 정보 + +| 엔진 | 인증 방식 | 설정 위치 | 상태 | +|------|----------|----------|------| +| Gemini 2.5 Flash (모드 B) | API 키 | `.env` `GEMINI_API_KEY` | ✅ 운영 중 | +| Lyria RealTime (모드 C) | 동일 API 키 | `.env` `GEMINI_API_KEY` | ✅ 사용 가능 (experimental) | +| Lyria 2 (모드 C 폴백) | 서비스 계정 | `GOOGLE_APPLICATION_CREDENTIALS` | ✅ 파일 존재 | +| Vertex AI | 프로젝트 ID | `.env` `VERTEX_AI_PROJECT_ID=codebridge-chatbot` | ✅ 설정됨 | + +### 9.2 현재 .env 설정 (관련 항목) + +```env +GEMINI_API_KEY=AIzaSy... # Gemini + Lyria RealTime 공용 +GEMINI_MODEL=gemini-2.5-flash # 텍스트 AI (모드 B) +GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta +VERTEX_AI_PROJECT_ID=codebridge-chatbot # Lyria 2 (폴백) +VERTEX_AI_LOCATION=us-central1 +GOOGLE_APPLICATION_CREDENTIALS=/var/www/sales/apikey/google_service_account.json +``` + +### 9.3 Lyria RealTime API 사양 + +| 항목 | 값 | +|------|------| +| 모델 | `lyria-realtime-exp` | +| API 버전 | `v1alpha` (experimental) | +| 프로토콜 | WebSocket (양방향 스트리밍) | +| 출력 포맷 | 48kHz 스테레오 PCM | +| 청크 크기 | 2초 단위 | +| 제어 파라미터 | BPM (60~200), Scale (Key + Mode) | +| 비용 | 무료 (experimental 기간) | +| 참고 | [공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) | + +### 9.4 Lyria 2 API 사양 (폴백) + +| 항목 | 값 | +|------|------| +| 모델 | `lyria` | +| API | Vertex AI REST (`/publishers/google/models/lyria:predict`) | +| 인증 | 서비스 계정 OAuth 토큰 | +| 출력 포맷 | WAV (base64) | +| 비용 | $0.06 / 30초 | +| 기존 코드 | `BgmService::generateWithLyria()` | + +--- + +## 10. 관련 문서 + +- [AI 관리 종합 가이드](../guides/ai-management.md) — Gemini API 설정, 호출 흐름 +- [R&D 메뉴 개요](../features/rd/README.md) — R&D 메뉴 구조 +- [디자인 인사이트](../features/rd/design-insight.md) — 유사 SPA 패턴 참고 +- [Lyria RealTime 공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) — Gemini API 음악 생성 +- [Lyria 2 Vertex AI 문서](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/lyria-music-generation) — REST API 레퍼런스 +- [Lyria RealTime 개발자 가이드](https://dev.to/googleai/lyria-realtime-the-developers-guide-to-infinite-music-streaming-4m1h) — 구현 튜토리얼 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/projects/org-chart/README.md b/projects/org-chart/README.md new file mode 100644 index 0000000..238ce52 --- /dev/null +++ b/projects/org-chart/README.md @@ -0,0 +1,317 @@ +# 조직도 관리 시스템 + +> **작성일**: 2026-03-06 +> **상태**: 🟢 v1.0 구현 완료 +> **프로젝트**: MNG 전용 (Blade + Alpine.js + SortableJS) + +--- + +## 1. 개요 + +### 1.1 목적 + +테넌트별 조직 구조를 시각적으로 관리하는 트리형 조직도 시스템. +부서 계층 구조와 직원 배치를 드래그 앤 드롭으로 관리한다. + +### 1.2 주요 기능 + +| 기능 | 설명 | +|------|------| +| 트리형 조직도 | 회사 → 부서 → 하위부서 (무한 depth) 계층 표시 | +| 직원 배치 | 드래그 앤 드롭으로 직원을 부서에 배치/해제 | +| 부서 순서 변경 | 같은 레벨 내 부서 순서 드래그로 변경 | +| 부서 계층 이동 | 부서를 다른 부서 아래로 드래그하여 parent 변경 | +| 부서 숨기기 | 더블클릭 → 숨기기 버튼 → DB 저장 (영구) | +| 임원 필터링 | 대표이사/사장 등은 미배치 목록에서 제외 | + +--- + +## 2. 기술 스택 + +| 구분 | 기술 | +|------|------| +| 백엔드 | Laravel (MNG 프로젝트) | +| 프론트엔드 | Alpine.js + 수동 DOM 렌더링 | +| 드래그 앤 드롭 | SortableJS | +| 스타일 | Tailwind CSS + inline style | +| 데이터 저장 | MySQL `departments`, `employees` 테이블 | + +--- + +## 3. 아키텍처 + +### 3.1 렌더링 방식 + +> **핵심**: Alpine.js `x-for` 대신 수동 `innerHTML` 렌더링을 사용한다. + +SortableJS와 Alpine.js `x-for` 템플릿이 동시에 DOM을 조작하면 **이중 업데이트 버그**가 발생한다. +이를 해결하기 위해 부서 트리는 JavaScript로 HTML 문자열을 생성하고 `innerHTML`로 삽입한다. + +``` +Alpine.js 데이터 변경 + ↓ +renderTree() 호출 + ↓ +기존 SortableJS 인스턴스 destroy + ↓ +buildChildrenHtml(null, 0) → 재귀적 HTML 생성 + ↓ +$refs.deptTree.innerHTML = html + ↓ +$nextTick → initDeptSortables() + initEmpSortables() +``` + +### 3.2 이벤트 처리 + +수동 렌더링된 HTML에는 Alpine 디렉티브가 없으므로 **이벤트 위임(Event Delegation)** 패턴을 사용한다. + +``` +루트 div @click="handleClick($event)" + @dblclick="handleDblClick($event)" + ↓ +e.target.closest('[data-action]') 으로 액션 식별 + ↓ +data-action 값에 따라 분기: + - "unassign" → 직원 미배치 + - "hide-dept" → 부서 숨기기 + - "restore-dept" → 부서 복원 + - "dept-dblclick" → 더블클릭 시 숨기기 버튼 토글 +``` + +### 3.3 순환 참조 방지 + +부서를 자신의 하위로 드래그하면 무한 루프가 발생한다. +`isDescendant(ancestorId, targetId)` 재귀 함수로 이를 차단한다. + +```javascript +// 드래그 대상(dragId)의 자손인 곳으로는 이동 불가 +onMove: (evt) => { + const dragId = parseInt(evt.dragged.dataset.deptId); + const toPid = evt.to.dataset.parentId ? parseInt(evt.to.dataset.parentId) : null; + if (toPid === dragId || this.isDescendant(dragId, toPid)) return false; +} +``` + +--- + +## 4. 파일 구조 + +### 4.1 MNG 프로젝트 + +| 파일 | 역할 | +|------|------| +| `app/Http/Controllers/RdController.php` | 컨트롤러 (7개 메서드) | +| `app/Models/Tenants/Department.php` | 부서 모델 (`options` JSON cast) | +| `resources/views/rd/org-chart.blade.php` | 뷰 (Alpine.js + SortableJS) | +| `routes/web.php` | 라우트 (6개 엔드포인트) | + +### 4.2 API 프로젝트 + +| 파일 | 역할 | +|------|------| +| `database/migrations/2026_03_06_201500_add_options_to_departments_table.php` | `options` JSON 컬럼 추가 | + +--- + +## 5. API 엔드포인트 + +> 모든 엔드포인트는 `rd.` 네임 프리픽스 하위에 위치한다. + +| Method | Route | 컨트롤러 메서드 | 설명 | +|--------|-------|---------------|------| +| GET | `/rd/org-chart` | `orgChart` | 조직도 페이지 | +| POST | `/rd/org-chart/assign` | `orgChartAssign` | 직원 부서 배치 | +| POST | `/rd/org-chart/unassign` | `orgChartUnassign` | 직원 부서 해제 | +| POST | `/rd/org-chart/reorder` | `orgChartReorder` | 직원 일괄 이동 | +| POST | `/rd/org-chart/reorder-depts` | `orgChartReorderDepts` | 부서 순서/계층 변경 | +| POST | `/rd/org-chart/toggle-hide` | `orgChartToggleHide` | 부서 숨기기/표시 토글 | + +### 5.1 요청/응답 형식 + +**부서 배치** (`POST /rd/org-chart/assign`): +```json +{ "employee_id": 1, "department_id": 5 } +→ { "success": true } +``` + +**부서 순서 변경** (`POST /rd/org-chart/reorder-depts`): +```json +{ + "orders": [ + { "id": 1, "parent_id": null, "sort_order": 1 }, + { "id": 2, "parent_id": null, "sort_order": 2 }, + { "id": 3, "parent_id": 1, "sort_order": 1 } + ] +} +→ { "success": true } +``` + +**부서 숨기기** (`POST /rd/org-chart/toggle-hide`): +```json +{ "department_id": 5, "hidden": true } +→ { "success": true } +``` + +--- + +## 6. DB 구조 + +### 6.1 departments 테이블 (관련 컬럼) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | int | PK | +| `tenant_id` | int | 테넌트 FK | +| `parent_id` | int (nullable) | 상위 부서 (null = 최상위) | +| `name` | varchar | 부서명 | +| `code` | varchar | 부서 코드 | +| `is_active` | bool | 활성 여부 | +| `sort_order` | int | 정렬 순서 | +| `options` | json (nullable) | 확장 속성 | + +**`options` 키**: + +| 키 | 타입 | 설명 | +|----|------|------| +| `orgchart_hidden` | boolean | 조직도에서 숨김 여부 | + +### 6.2 employees 테이블 (관련 컬럼) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | int | PK | +| `tenant_id` | int | 테넌트 FK | +| `department_id` | int (nullable) | 소속 부서 (null = 미배치) | +| `display_name` | varchar | 표시 이름 | +| `position_label` | varchar | 직책/직급 | +| `employee_status` | enum | `active`, `leave`, `resigned` | + +--- + +## 7. 프론트엔드 구현 상세 + +### 7.1 Alpine.js 컴포넌트 (`orgChart()`) + +**데이터**: + +| 속성 | 타입 | 설명 | +|------|------|------| +| `departments` | Array | 전체 부서 목록 (서버에서 전달) | +| `employees` | Array | 전체 직원 목록 (서버에서 전달) | +| `hiddenDepts` | Set | 숨긴 부서 ID (DB에서 초기화) | +| `dblClickDept` | int/null | 더블클릭된 부서 ID (숨기기 버튼 표시용) | +| `execTitles` | Array | 임원 직책 목록 (`['대표이사', '사장', '부사장', '회장', '부회장']`) | + +**핵심 메서드**: + +| 메서드 | 설명 | +|--------|------| +| `renderTree()` | SortableJS 파괴 → HTML 재생성 → SortableJS 재초기화 | +| `buildChildrenHtml(parentId, level)` | 재귀적 자식 부서 HTML 생성 | +| `buildNodeHtml(dept, level)` | 단일 부서 카드 HTML (level별 스타일 차등) | +| `buildEmpHtml(emp, isLarge)` | 직원 카드 HTML | +| `isDeptHidden(deptId)` | 부서 또는 상위 부서가 숨김인지 재귀 체크 | +| `isDescendant(ancestorId, targetId)` | 순환 참조 방지 | +| `isExecutive(emp)` | 임원 여부 판별 | + +### 7.2 SortableJS 그룹 + +| 그룹 | 대상 | 핸들 | 기능 | +|------|------|------|------| +| `departments` | `.org-children`, `.org-drop-target` | `.dept-drag-handle` | 부서 순서/계층 변경 | +| `employees` | `.emp-zone`, `#unassigned-zone` | (전체) | 직원 배치/해제 | + +### 7.3 CSS 연결선 + +부서 간 연결선은 CSS `::before`/`::after` 의사 요소로 구현한다. + +``` + 부모 노드 + │ (vertical: div 1px × 24px) + ┌───────┼───────┐ (horizontal: ::before + ::after) + │ │ │ (vertical: div 1px × 24px) + 자식1 자식2 자식3 +``` + +| 선택자 | 역할 | +|--------|------| +| `.org-node-wrap` 내부 div (1px × 24px) | 세로 연결선 | +| `.org-node-wrap:not(:first-child)::before` | 왼쪽 가로선 (left:0 ~ right:50%) | +| `.org-node-wrap:not(:last-child)::after` | 오른쪽 가로선 (left:50% ~ right:0) | +| `:only-child` | 단일 자식이면 가로선 숨김 | + +### 7.4 부서 숨기기 UX 흐름 + +``` +① 부서 헤더 더블클릭 + ↓ +② dblClickDept = dept.id → renderTree() + ↓ +③ 헤더에 빨간 "숨기기" 버튼 표시 + ↓ +④ "숨기기" 클릭 + ↓ +⑤ hiddenDepts.add(id) → renderTree() → POST /toggle-hide (DB 저장) + ↓ +⑥ 해당 부서 + 하위 부서가 트리에서 제거 +⑦ "숨겨진 부서" 패널에 표시 + ↓ +⑧ 패널에서 👁 아이콘 클릭 → hiddenDepts.delete(id) → POST /toggle-hide +``` + +### 7.5 부서 레벨별 스타일 + +| Level | 색상 테마 | 너비 | 아이콘 | +|-------|---------|------|--------| +| 0 (최상위) | 보라 (`#7C3AED`) | 200px | `ri-building-2-line` | +| 1 (중간) | 인디고 (`#6366F1`) | 180px | `ri-git-branch-line` | +| 2+ (하위) | 회색 (`#6B7280`) | 160px | `ri-subtract-line` | + +--- + +## 8. 비즈니스 규칙 + +### 8.1 임원 필터링 + +미배치 직원 목록에서 다음 조건에 해당하면 제외: + +- `position_label`이 `['대표이사', '사장', '부사장', '회장', '부회장']` 중 하나 +- `display_name`이 테넌트의 `ceo_name`과 일치 + +> 이유: 조직도 최상단에 "대표이사 OOO"이 이미 표시되므로 중복 방지 + +### 8.2 부서 숨기기 + +- `departments.options` JSON의 `orgchart_hidden` 키로 저장 +- 숨긴 부서의 **하위 부서도 자동으로 숨겨짐** (`isDeptHidden` 재귀 체크) +- 숨겨진 부서 패널에는 **직접 숨긴 부서만** 표시 (자식은 부모 복원 시 같이 복원) +- 숨기기는 **조직도 표시 전용** — `is_active`와 무관하며, 부서 데이터에 영향 없음 + +### 8.3 직원 표시 형식 + +- 직책이 있으면: `{직책} {이름}` (예: "과장 전진선") +- 직책이 없으면: `{이름}` (예: "김보곤") + +--- + +## 9. 개발 이력 + +| 날짜 | 커밋 | 내용 | +|------|------|------| +| 2026-03-06 | `a12ee886` | CSS 연결선 수정 + 빈 드롭 타겟 숨김 | +| 2026-03-06 | `9fd72e49` | 부서 숨기기 기능 추가 (프론트 전용) | +| 2026-03-06 | `8c8fd5f6` | 대표이사 미배치 제외 + 숨긴 부서 연결선 제거 | +| 2026-03-06 | `81157a15` | 부서 숨기기 상태 DB 저장 (`options.orgchart_hidden`) | + +--- + +## 관련 문서 + +- [rules/department-tree-api.md](../../rules/department-tree-api.md) — 부서 트리 API 규칙 +- [rules/employee-api.md](../../rules/employee-api.md) — 직원 API 규칙 +- [system/database/hr.md](../../system/database/hr.md) — HR 테이블 스키마 +- [standards/options-column-policy.md](../../standards/options-column-policy.md) — options JSON 컬럼 정책 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/projects/planning-design/README.md b/projects/planning-design/README.md new file mode 100644 index 0000000..12dba75 --- /dev/null +++ b/projects/planning-design/README.md @@ -0,0 +1,157 @@ +# 기획디자인 스토리보드 에디터 + +> **시작일**: 2026-03-07 +> **상태**: 🟢 v1.2 운영 중 (고도화 진행중) +> **경로**: MNG `/rd/planning-design` +> **담당**: Claude Code + 개발팀 + +--- + +## 1. 프로젝트 개요 + +### 1.1 배경 + +ERP 화면 기획서(스토리보드)를 PowerPoint나 Figma 없이 **SAM 관리자 웹 내에서 직접 설계**할 수 있는 도구가 필요했다. 기획자와 개발자가 같은 플랫폼에서 화면을 설계하고, 즉시 HTML/인쇄 출력까지 가능한 올인원 솔루션을 목표로 했다. + +### 1.2 목표 + +- 브라우저 내 Notion/Figma 스타일 블록 에디터 구현 +- ERP 스토리보드 표준 양식 (메뉴트리 + 와이어프레임 + Description) 지원 +- 서버 API 없이 localStorage 기반 즉시 사용 가능 +- HTML 내보내기 및 좌표 기반 WYSIWYG 인쇄 + +### 1.3 기술 스택 + +| 항목 | 선택 | 이유 | +|------|------|------| +| 프레임워크 | Alpine.js | 서버 없이 반응형 SPA, 기존 MNG 스택과 일치 | +| 캔버스 | DOM absolute positioning | Canvas API보다 접근성 좋고 텍스트 편집 용이 | +| 저장 | localStorage | 서버 API 불필요, 즉시 사용 가능 | +| 내보내기 | HTML 생성 + window.print() | 별도 라이브러리 없이 브라우저 내장 기능 활용 | + +--- + +## 2. 구현 이력 + +### v1.0 — 기본 블록 에디터 (2026-03-07) + +| 커밋 | 내용 | +|------|------| +| `063d8c61` | 스토리보드 블록 Undo/Redo 기능 (Ctrl+Z/Y, 50단계 히스토리) | +| `78c8f3f8` | 페이지 복사 기능 (블록 ID 재생성) | +| `a27d9921` | placeholder 색상 옅게 + italic 스타일 | +| `08cc866a` | 블록 툴바를 단위업무 상단으로 이동 (기획서 보기 방해 제거) | +| `20e5ab78` | 메뉴/캔버스 경계 드래그 리사이즈 (80~400px) | +| `7785dfed` | 올가미(마퀴) 다중 선택 + 그룹 이동/복사/삭제 | +| `ff373c71` | 올가미 선택 동작 수정 (캔버스 빈 영역 판별 개선) | +| `95cd217c` | Ctrl+X 잘라내기 기능 (단일/다중) | +| `f4131df0` | Ctrl+X 후 Ctrl+Z 복구 수정 (히스토리 인덱스 보정) | +| `8ff84e7f` | Description 패널 리사이즈 + 번호 마커 블록 (D&D/툴바) | +| `ac5ae6eb` | 좌표 기반 인쇄 + HTML 내보내기 블록 좌표 배치 | + +### v1.1 — 서식 시스템 (2026-03-08) + +| 커밋 | 내용 | +|------|------| +| `dfbbd3a1` | 플로팅 서식 툴바 + 우클릭 컨텍스트 메뉴 추가 | +| `280bfddb` | 블록 서식 CSS 상속 수정 (자식 요소 color inherit) | + +### v1.2 — 작업 영역 극대화 (2026-03-08) + +| 커밋 | 내용 | +|------|------| +| `5e0f1a63` | 좌측 사이드바 접기/펼치기 버튼 추가 | +| `f1202731` | 메뉴트리/Description 패널 접기/펼치기 + 캔버스 폭 자동 확장 (1100→1400px) | +| `a38c017c` | 이미지 블록 업로드를 더블클릭으로 변경 (드래그 중 파일 창 오픈 방지) | + +--- + +## 3. 현재 기능 목록 + +### 3.1 블록 유형 (15종) + +| 분류 | 유형 | +|------|------| +| 텍스트 | Heading (H1), Heading2 (H2), Text, Code | +| 레이아웃 | Divider, Callout | +| 데이터 | Table | +| UI 모형 | Button, Input, Select, Card, Badges | +| 미디어 | Image, Marker (번호 뱃지) | +| 체크 | Todo (체크리스트) | + +### 3.2 편집 기능 + +- 자유 배치 캔버스 (드래그 이동, 핸들 리사이즈) +- 올가미 다중 선택 + 그룹 이동/복사/삭제 +- Undo/Redo (50단계) +- 복사/붙여넣기/잘라내기 (Ctrl+C/V/X) +- 전체 선택 (Ctrl+A) +- 더블클릭 인라인 편집 (contenteditable) +- 이미지 블록 더블클릭 업로드 (드래그 충돌 방지) + +### 3.3 서식 시스템 + +- 플로팅 서식 툴바: 글자색, 배경색, 크기, 굵기, 기울임, 정렬, z-index +- 우클릭 컨텍스트 메뉴: 복제/잘라내기/삭제/색상/정렬/레이어/서식 초기화 + +### 3.4 문서 관리 + +- 멀티 페이지 (추가/복사/삭제/이동) +- ERP 메뉴 트리 편집 (드래그 순서 변경) +- Description 패널 (기능 설명 + 번호 마커 D&D) +- 프리셋/커스텀 템플릿 + +### 3.6 작업 영역 극대화 + +- 좌측 사이드바(메뉴트리) 접기/펼치기 토글 +- Description 패널 접기/펼치기 토글 바 +- 패널 접힘 시 캔버스 폭 자동 확장 (1100px → 1400px) +- sb-editor 패딩 축소 (24px → 12px) + +### 3.5 출력 + +- HTML 파일 내보내기 (좌표 기반 WYSIWYG) +- 인쇄 미리보기 (A4 Landscape, 페이지 분할) + +--- + +## 4. 향후 로드맵 + +| 우선순위 | 기능 | 설명 | 상태 | +|---------|------|------|------| +| 🔴 필수 | DB 저장 | localStorage → DB 전환 (협업, 용량 해결) | ⚪ 대기 | +| 🔴 필수 | 스냅/그리드 정렬 | 블록 간 자석 가이드라인 | ⚪ 대기 | +| 🟡 중요 | 그룹핑 | 여러 블록을 하나의 그룹으로 묶기/풀기 | ⚪ 대기 | +| 🟡 중요 | 레이어 패널 | z-index 순서를 시각적으로 관리 | ⚪ 대기 | +| 🟡 중요 | 리치 텍스트 | 블록 내 부분 텍스트 서식 (인라인 B/I/색상) | ⚪ 대기 | +| 🟢 권장 | PDF 내보내기 | 서버사이드 PDF 생성 | ⚪ 대기 | +| 🟢 권장 | 버전 관리 | 명시적 스냅샷 저장 및 비교 | ⚪ 대기 | +| 🟢 권장 | 공유 링크 | 읽기 전용 공유 URL 생성 | ⚪ 대기 | + +--- + +## 5. 기술적 특이사항 + +### 5.1 단일 파일 아키텍처 + +모든 CSS + HTML + JavaScript가 `index.blade.php` 하나에 포함 (~4,430줄). 서버 API가 없고 localStorage만 사용하므로, 컨트롤러는 뷰만 반환한다. + +### 5.2 CSS 스타일 상속 문제 + +블록 자식 요소에 하드코딩된 `color`가 있어 부모의 인라인 스타일이 무시되는 문제를 CSS attribute selector(`[style*="color"]`)로 해결했다. 향후 블록 유형 추가 시 inherit 규칙도 함께 추가해야 한다. + +### 5.3 localStorage 용량 한계 + +이미지를 base64 Data URL로 저장하므로 대량 사용 시 5~10MB 한계에 도달할 수 있다. DB 저장 전환이 중장기 과제. + +--- + +## 6. 관련 문서 + +- [기술 스펙](../../features/rd/planning-design.md) — 데이터 구조, 블록 유형, CSS 상속 상세 +- [R&D 메뉴 개요](../../features/rd/README.md) — R&D 전체 메뉴 구조 +- [프로젝트 인덱스](../index_projects.md) — 전체 프로젝트 목록 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/rules/slides/usage-plan/SAM_활용방안.pptx b/rules/slides/usage-plan/SAM_활용방안.pptx new file mode 100644 index 0000000000000000000000000000000000000000..ed996ef54512b2edd90d38b08859909c8ff895fa GIT binary patch literal 279940 zcmeFa3w&f(c_*mw#)LqIK!9HoF3P|dY^mx#t4g$*NF|ls(63lB1VWa%s=AVjt`}9M ze(*@zw%TdC+qezgxZ9|E+R(N!G=p%rO%oa%@>nJh$WF2_BnwH#%#uY_+D(3&nItni z$^Oqd_jO9QO1h;ZwH}v5?W$Y%aqcW}x{`>x_ zRj9@Gw>{^jI44)m?yr_BolbiJPODVv9d1O1K+ah1fiJVq%TAlsdZ$PJG=Ji9_kD$Z zx>cO8a=CnIs?)ul!4|Dt-t6#F&KPQie9qc!9x2as9P-cFW8h9-Zs6QA_L#N0RqZr% z?itSM%RR%HbULt3cjpFETXr#>d8|)cMQa7~SSr`8nltG3o6>nl?)2r}@s;-E(~HK) zf^~Y)*94p$^n`c+-P`ZJ?^QiLv1-58f4ErK@pk+p9mKNb z%ca3yxj)`3S*2{5Y{J3bJI6D9$zG{eH%mFQP%c@6y+^EC@7s62{H7iB9ulrD)dqW~ z>-EY&ELO{6F3nnhxnh;@%VfD)H0$_Uor+b=>;ZGiiYf895zDf1^wr5YdUx&^u_n!# zLR}g?jN!47;PPI{9xr*+V6R!J6!KZKj)BI=ud%@E2+Qbj;lWa_WpsV^$og?5y?Jdq zU#Z>F{s|wbSW_)e$QQ}0(I0|G^?^#eOQzbqN9|zn6*%5uY?V^*RpfZ^$foj>fiFXj z2VYUt4z^rz5c{a@@C6bD?C4RTs-9Jz6#vMR<)kD z94F5*kAzAo{}2E8YtQTHA%DdG$-H~SMOEwG zV|Rz&!PTYbj&qXr?!4)xFWuq4t+XpQ*jwF|lY1rRv|HXW$YyH9_S`W4AorA5z~tff zW_8M{5B9n{!&#SoTYn{Z-sn<-#O?q+R2CUxawT7IJVGrR!> zI-uB?V*b^uweG_2z4zJw{=%Lf@<%+>t#jS|RwUXSqrsK$L;mg9MhV;+aIckW0|&7= zy4!uuYrcz8CfVdW#>;iHFrF`3JL4c&_X~b`ce$FYk)!MbjtqHz>YaP?*=o5~o~%pz zsDo14Up{12_m}gf`i_|YL|nJu=OWBd&-u=MrM_CWYFQ;|Y`T0%x>+^`Qg7K23pkTp zZ&uBzs#%$)!|{E?Z(}s1urskEW`Dx5y(FGN4zUk7a(h0Pvr6_EcqQ&Te&4evU4U<# zysh(*+&gO)tTYyM=cHMvS$KqdfZP*aQn+FFoB3*uQs%+>z(FfpFIS~n{zGt&8oknl zS+f|sIA~V$W~ojY%S(5VQ`v!?vdX(-}DZ@86AFO%fZG%q#{`y-y*b5`i^wDSTRdScJ8m#58q~$-Z8c#<{ZMJ z?ar4D)b6Z|mq*OH<kWe1N&{htj(>cRCKqwDUvuc6dC_QbKrmUQE*3utvHki@7 zSQK!lZ1l(RKgyt-V>knB$6?wna$^36zp1|c`8_@4j|5>I21-RF(Ve;!6nJR1GC-uL z_h=?B2C|ivaCc6Ow-?+@VC1syEh{tC0*zYcvN5XwHUO3rll$dZul@L37Lo3v7x?sF zK!5R_SV_M?0D;ac7u#Gyln`VXme7zmomv0vG3ckV$!KTS@W@h#fKa1hOyjr8YD10dU}*V?%qS#dCAbN1klVKiNEUT-jh=E;#~95`R2pNn&)Sw#j|%cPh4C)bGC8f61}-|Kl}C& zSSHx}&YPQ`x&D^s*G@M+G1s_uvhnz{%_ojFAAYELKfB5~ja-FQE&x2R2Vv*QypR>5=hF?@lGI7=3ZI zFRqNowE;yLh%4_U@5xsh3{CdfjZ=tp~f4~2^`(BKwKK|YSYV&|A zexC{NBbpA2n0X4Y@1PydV`ng@5yG~9E!eSPLYNCdswrI+? z8H7;#pu-@Tx2nw`5WQ#WS$F#Mif4j(sM-udU#CGZuT+~sKv`XvnP8r%HiJm4(;%4l zsm&k&3tE?%U>>J7gGh-S1a(EVat`0YTFm#QBj+Icv$)A2T%G7YfHgTz5BCstOtqOD zS*R1n4V8GKa1V7Hg-|w1Lnqz{=+_wixATGsWuw0MiEk)gP{o;!j=q(B&-=k=za{nb zkiVa?rFiS>bg4G)FbC+JY+coKHOda~yc|PI6${L-LQq7#QiP2N>pSH~*RK8W?yEn% z`^!JP=bj(l{YCrP^j6$;x5GU5U)Olzq2~Fc(!!OCjnik?qg&61F!0{(Z@zHf-~86I zoo>imz+e(cAeKOJ!Qp{hCIKb^QeYtmlpkaS16h!+ePzco<}cg6U$ znfG5Y1jxHtGRac{?mt?e=FZ-G#4MNr=M7u@Jie<6RugpMZ8IO>20S)hovG&o?)c8> zB6s4Ra=9>Et_IG+JIS+r(>y*?nhJh+b;f_*9j19O=-Fwr#I5q^Oa&gve9$e^(-t?( z{RKEt0ZA@db&32|K44h^Z}*;jo|{+CTm7EAG;HPrr(`@o!R;pRqT2z3E|^Dx28i|M z=4Q{kq~UTQ=&eVrgZ!DG4U>Ys8S%TsHf|NTX}ryxshh>1TaKFqZ|m*<-DZ6{Xpmz^ zs#*Veqi{D9$628)jRKHg3%X`s74Pf2_dDVF3|{sgU>t)Pwd4wK)j;;w!8CQ zth#^BN8x=h25o?Sh3o3R4F6$AE$={?@b+0GeyL+nafj_9q%TdXp%U$eLSw)9y@qdfeB+r zjfCgMb8k;aJ5?ihb_PJM4KoPe!}vj{0fR_52gxK6*a4ZpfI-kV6Eccaz$lXB_Rdl* z83l&st_sX<3C8+hudd+|Tm(k}RCK^mo$H;|rBf(!rQ1zr!M_8bX0tc(uXKuxwe9PL zT4`<&zU{^jg3b(8@M_aAhMisL92UIRDQ`)+;gPAJuZ^wc4I`1Xj|PsS+qK4Zt-PU7 z@^gwpNjd}+qN-!dWKF<|CgD29$6^J0r$#I zn4S&%k5!cl`Ql(Nd3|>`6iSqb(6Px17{Z1fTncPuqYVy=&M&exISIs*f7yLU$V{`Z z;P6Z=1pZ1d@BU94^ZjtazNVl@wl*8+=Q3Is} zCfuFB0I9nI(eE3Y_b_h4$cUG+VZyWRqx{gE7hGm&D^Uo1T;FhR+cIyij0k)j4t6E^ zab4@K#i2<@ucR|gW9RG35I z{GyXr(4Q?AV+?-Aa1bI-xBf#`MvZp&3%mJZO#0Ow_D;1~wAR5ukk#0V@*_Ij*Yh&u8m1_`vLx%0R7~WY6$F z`=3n%@%+yKe7RRs- zJ!N0c{p?NL*oUO-;lbXNtWg6%${vBZmo@01l=D^w=ymp4&JXk~=Mp+`IaW4{M4MdQ zq;RkxSN2yW1dDc)wYRlb1VC~2xH&QQA?LYx+dAMlIK?t|mxikc=pLfD3HvL4BAEju zeP^bWCF%*)fN`Tc@Pr!fBtx|HN(ZbesT~HEy{v{@KkPt4IHBB%*0`lu>MPXAB(?xC z$Pbns*y7+A3No=5q1z`t~S+OT5 z;hUg@rbaxP3NA7$_5>xp2_mxt4I*OF2Mc6Y>`J8$?2n_SpgHFjTt?m))8_!aUwan)C{^tV^J_Ecz z`6D%XN%o#Ep|2|Yb6sY=(nMsEj1I_*L~C*Mja0Hbo*QsU!Zj#n2`v>nMB_R zL#`c`h;gXGFM!*?JIJiM+L3_6#L>X6s|=K@d03tj%w$EzwXl#?ndJvyjNWjkW-xkK z*6iK9!P{O7?$xMOO{`__a^Ia=N6f~{jn+F9JW@zvjMJHD_uh2UQEY>M^c3cG3>oS$ zuc4ra}k=|*{)Vwm*hq=r>PT!-FFL; zoJ1vG^&pG$v-c&t$WCP%8O;pcxqF-v+{6^w@Do#aSd;K|VPj%5GmnVts%N2&i7Dpf zuurC2kzHJ?jH8?jxlQTdZPm&cmC4x8dk=!x37Nq~#j>s#9peZ4G)mSboMscTbDZ33 z%DIy{aCov>Bvc2NFd-e^hNSLky3ufuY%-e=v(rfMT5@i7dK4~f;-vDQ4QDQU0Cu$d zP(lj!YvCiAJDMDikA>dx#;7?)i zlEqMWJyw1T@)jx|Jzg?KN)t$gV-Uu`{$)lB=L_X>`r;Wmp$t*U4N>Yr4?rywQ?^3C zby*AHEM%Ta2bXe6cwM{cfXo#Hn#9hRZq%_ z4rUBKDJu}n28~sCJXDiYM3te8)c#C3n!_JW7*w@7(prj;FC?&EEzgv4cL0NcaUxmt zAdLkei0Q}~Glvw1$zlPf50YdhC7~qf6LClTWPEb&N!2QEFJNPE5CA=87%Tw6Z5ddB z0F~f2^gm>3*;LgJ50`UC$UP_UU&x_DC?W>v%LivrU5Ew=(q$GWK+PE_qz)7YcVZWi zOA9p?g1{tTob(?e(NOcSgtx~;Iq?YkoSD7oCBo**o_|zFBo)g@TmiXjfN@!+zB|XD zZ{@3XM^=#;pzG+2n1~FzKPvg`PD!F01#vHT2iP(d$=__J>>_PCuvHKWLCPjNGt`Tf z*fjb#D6ql~_o1~$pDZhi(kCYpil!hy3pj?}7w;$SGw=XB!rf!^8{2T~i5OfyyStn{ zP?Jh!R&i;lR>6uBp-$e_cd~sk5$E&=`hndYADuUtK(RFk!bm~Ts-TkVbP-U_g=^2F9 zh*rRSmu&yrpK*pu0OYpLZgnVt+x+(8+eAVqf$c{OlB_1yvOVc*&Vo|XCk)kH7F-7& zK)3~-KnUGG!{oGONnev?&2UKH_VMFh9_h=v0>~3K6;Yy~vd0xChuUEx9p9WTTQE(R z%4E6e!$Og23Dks~_+6wE4|aFm`A~H}OgJA9@oOb!!jV=$%B~ZCryzGW1oa;;cCX{Xkyq&KIcwrjUDa>9e|yf)f`ft@*oL@r-1#4eM35TA!hDs zzg(90%C77Kiy#Euk`KP(i*~5U^ z^T+n2q;sUCh$SU}UF4*WStn97r6rQBQdGeiO3EfB%`g(%h?EGPG=zX`PfQFQ8j6^> zsu^34nEXZULQoW&$kkF5d{sWZR25563c?vF9Vi$gdh~cIsZyYdC!91nnbMOO0})Ot z{+Eb^6C_C`J8Vr-0aMBDNBLMYVR#ax@mPA|^R zdxc!3=7~$q$InY2k&^K@q0-RPi}yZ_({RFUJQb`3U)JM3wSi)7~vXV@IHxPlTrQ#`S?E3A!c!Hr&sJ)j0X)h;I z!})p)O-F!>P0b4ILXxe7L6BK!*{YluNFLQj)F8*t$VLgNVQpbY0g7oiKY3MZeD1T22d>#1 zJOneQbLk4iE_pGjZR(?_gjkA#`68=|9qtce#&F9gQy}V1#ft%47!6YT;jr0yzWDaM zm=vV-ZJngxt)l`gYWr;inXxXz>*(m;5i@4F8lmjn@XUBfAJ#HyN0Il3tTw$~?KgMV zoe5(b`zR?TmXh>%63$w9hTChznyhU)E}W3njEx}U6zA>6h23x{i=GLNoWnAaDI~qE zPOPiC8nNMJBjWAdj^WvGCOx8RBU_RUqX|v(Zqk}&;pusFvpLhe@YuR@bSKYZDBUKBS@qB+oqa11j(Qt} zO-t4BP3Od4(i7?j>Wik%>g1YGA~>j2lA zrX|9G7%k*>6{o0{$szBY3kblE@O$Sq+Ixyz6J;;yQ7idh!!9Y(={49h+%uf9_(n=_ ztME_L9FAqjTN!ZhrVij04+uiM25anohpQtbR{-(yU)Bw<%hgHQe%L!xoWLgV&m_ec zyKu?C4YB-8V$E@D6k9|w5CRy6ICdtM#c!S2Ujn_vy;nT5lSmJsm54HlILg%M5JGA) z(Wj+{jJ~97Wcu(el}ROr_4IHWWs69!2eKc*pGz1WdAC*^B3;8e=@jWK3LZjEY=gNY zQ_%*0>{C=KZVf2<$*uu4Je)F=baJ?FSk_3frBtGCC}Ze-8C})1^l)-0t&Wl!P!(BY z{VCDI5(RTuej*MoDLfQd{;5J*OIJv1X-F(NZdZV!NEbP-Xz`?xN;ybmz<4Pk;=dG$ zj#r?GJKZQ)RGPG?SYI6&dchwdTZjC;)mE|m!i?!z-jP)+TO@JIuaDH&>@AtO0BO%P ziRF3~+D(uB_jQ^>!j&ypn_$T~OVtS{X@^c0`{F1RcxnEvozX!Uh#MUxBW_&uh(GlQ zJd`XzNB!oXO0%E2<$vkvA%8(RM6?6tYP6bdltV;tx+~8i$|N&GK~8uOeUwAwbE6CP zgd$K1kBtM+v-2jL`du`DiTZg2*v3T5YEfVujF*Fgw96s0qmSDw2x69&HUwC7+u@E^icEOlOC?NdH$@lcy<=(+J!6E8Esyrzn+*| zJae{j|7odlI@_Al@vr>FwFQuwBu-&6eEuI24(h+;W z!ISL2&Pq=>1g(Z_UHgW?0NVDtQD!uwWLQ}l4irF?q7@=-Go$IDbcD3!^PU7}SD>;{ z%0{Hf$j?P?Hm_MJ5C`54qn|}?Fx5yYUgu_(yidGAZu)Y|-r>~mku(YjH~?>MCx%M$ z-8%XBZXQk-`S^yC*nau=1`36e-A&920Az^?(UOi2njjLL0nORx^-ITJ)AWpFQl|ot zzB|B0t(1@I9C1}9ncd`!?Uh#5Ug-;^%CRJ!P&O4j$;0xnEpvIEw%HL7wgE+KsfBCv z%}0T^pMSD>;(Q2=O-$C)nx?M!1*(?Pmk3lfrNn{1x5;WFprsx8cVo+3z*>S~&4OiK zwjyUu*}sP(!SIm2L`EKwwwWlIQIK+6UQ_E#4lU1|#{{nFb^ISfhZjNnjxddsl|Y2kAOW zg1qwuY(vD&&)~9tH+6|x_JC@^TH`e#iNZAS3^*w7^&D2*cJ?C z@QFo5O~n(fu{WM1BeB?-BqMtfx}UK6iG&`Xy4tt`k@(e1&HFDc9y=--3-fc$Po2Ic z1R07Ej4tk$;Y$SPg89i%PsK@lEjp9I+G!*iJCk7~8O2COrjssw}tt=nnXRuWxP3&XzWark!Tbt8lj6eXQ|7kdGSo+3St>2KHI!_eBGrY4-PNhi7^LVkOk921>WJeY@ZAEKplwyqi${LQf-7@sNiB)qgEsDElcB!iQ5P0lx3&?evWsV6urFlLt-K8j zswvxb2R2w67HUf%FL*85uxLmYqE3koiu;0#)L(@RMQp`tb?AH*cR`;)&@U8{GU85o z)d>2<0&$^~x-Ot!aYN5&!En~i4*jAjGc=2al)+(M4QcUHvmul>G0-oCuat=r!a^yF z6RZ*X%j;LRT$3XYV1iF>t@<0_vWOensJb=rm+&1@AO0MHUpD5EPJ_zyXP&%4Krpm= z;$)oYqP2b%H-dt#mJ-+1p&v&>D}8HrBqPls7}CoFg)z(*KwxLn@c2pVRz#@U@);BV{!B~pzquY4$=;Dd_#bYNM zvnQdxO3labqyM>ZtofNRP0PfHM?T~YJ96y^F+zf^mJlQHC`dfcULrsfqE)E&C?lO{(jl+gqJJO5*VXGz0$Y>O5G*bA=uQ#8WTexyj zg5QStTJQam1o%qh!E@mX6N!b<4mz}-Lkhc7&beoo}CeNQJ^W4<3X{%hSAn)i1D5Y%OW>s;W} z@E6))fc9?UYWVwmIw|Ye(`mVpD4n`XA=)druYsuUQsBzsM&|X!;Fiog?C&l`_cYa} zxSiQTAAkPaUw+=ox=XFP4+9-$)LqIBj*^w3DTe5=L=p+<*AmL|5*>r&8`{UoSySwk zfE+`XY0;AGi-Sbc+efq0`z9x)!{ocK??85X)Gl1(KigHjF#`9YoyrHhC@9uVh#|QB z&4i-vQeGddsJj$8O-0?M2I@N%Plt-SOQE|K7-87)o z`O;Lez`BPP&3viXzP`GW>rq=EpS3!)Aa(AB#*NeUdSxIM>(J1&QnhO6WQzQZa)}!f zCCM?o8f$~Zq&ta+Mct*OEYqc26{~xtiTYdxBb(g{}ry6k4tnTiR=H9Lm zj#~w^TMrkf* z_~2!OLm|CR z;QC6Ybh|dXFV?K+34$4-zOjU%kcN!xHQL3R!!?^iHCp$MVY^Kks$pbU59XGRL0h&J zQ2RRGJU~`3?nhD4T@&E~QaT|XPm%UH*aZGvqQzw$`;@5nij0b;+Xs=5tyj^by@cNA zE7FyN+uDR`hbT>?Ai(@oCn#g1v^7i+RDQxzVI6#Rn%%7J=hHbN=@yu-Fv9lovkDtdb z?Fk;eD!3XB^QAF^LWU-2w6>d&@eZWc6@}GBWv04-)uEdL<}Z}+>R>8ucA9$b-j9bM zAU1a{C+d=dWXwN{@z7v|;4(H$`(q@u*=+LUu%6BY*E1_oi2Amw=*brz6I@OO1^v`` zWR({z>WToo+#kZ|WKx7O6e6pfv&wxKoOQCw(O|&OYr%SjrU7tN6dC3#r=m5o6e7di zA2Yn?u5it-gc#;%D8Nyi;J`aaagl9)!zqrpDVMR$M@Cbb(Jg74kByI`)xgQ-?3ZY% z%tyntDCeiJTcd(60VcY!4x)G|nqy9aIFVt_X^RCx5uEUbc?WwbGR#S_3*=PP4-c1f zN9acu|7;%xMbiXMbO{cYt5{%-IA1nlw0tP3>zUvJtJ}~r=fmZ);CMOqNir3g=8f8MBx#fMci-@Y!jzTC;!@5Z=x@u7+`g64Z@X0HIrrg^!qv?pVW7QDmCma6=(7 z&3&f%!c&(~c@3D|5L73|I_Kl&qWi>fL>F1-H=O8r)3UR5o>51WTQqX+B5_gua2_c6 z5LCy{V7H=<51I?_A%ng#r2RBSSVqSA1~JY%m`ssz9^N=_?QkYIY_1tegF@9ep>ZA` z9nwcrfqvEPXPme8>k=F{*Nl{wjEwWhIA2fWTnnMFFv!7bF-k4HwuE;S@l#P`oZoOe zAu`T=#(DGjSu`oQ0mgYt2QA?ZL;OS+8Rs{g=y=nzvvEEe&nR0obk541OU;Y3i|5uI z&EDEMOK@utJ(ZLbk!2nTvb=F&^A7e>WSMtine${zK zpF?UlA2t`=Uc!)*@kC^qN0#||TITxtTjqS&90_tl5$zlmMV9#uHxeSt+;5p*cnH<5 zNcZcDXF?F3kjS~hN6tkzl;G$tGR|)}-GwpEN94@Vs8h41wxx2;5jkh%M*HhtyenK+ zGJX=f44x}|@LUm`a-PtP1RW>LiwN1y)7=d;(c2qNk%=DOL~reeBsh4UF!Ur!du>W& zyKD@p+E5fcrzX0}=bVG}3NPN>JTI+#5h;^q;rU|wWGMmIKSZ($J>=;IGoj8t4>X%3svc$ zSs3gcNoUfd!I_Jj-I8d`&o}RV)GIO_g6jAg?9wtPDQUh!RuFqasm@niT23lSf^f$` zy}H3ES&?N8RtGHxrzy#8RE4pCrVqD#CZVcsd$8(iVNRLfPoe@!y z4`Z?L^krzWw2ImNkF7f~@v&YAD51opB{fwuSSuPO9w3rn7L)EX+mV)pg&!hn@=;Us zp1T(B2~}@^Y`@iJ?-8en-!UUN+^ZWhytXXd8%RcxOj>n6r-+JtR7C1`pj^%)VZ19s zlsnChhmTF06-!bD2gG6YxF)i$B~nl(sVBo}BFve{j>DpjbzI1oEQm{U!1I%MpPc@e zh%RTYikRk*p1 z%+VXbw6?I5n6>6zxO%kl_~rH1qcuJr3N|K`lsKa_h?XJ+KnyLpiXe^W_>sk&SJTiE zS~A$Hx!Gf!tm##3;Y;(43ul{8ot4se>{)ke;^UlAp`Gd)ADPqy zMY=$&O6JG{gI%)?kc4vu zL6-I#V9PtQ_$ZbpIEv+WWFcUswyJ@VBTJ0&9ZCz_%8o4BM=sX5e64vtRDmuLf{WP2 zew#{gD2wxC1rbx^$r3|Mu0^nIH9>@e(T)*a;;uF>K6b0qEv|Xt7`}$FuY^#Q8`q*O zs5QZ1EzYB5s7b9=L`5Dgvf>kHOtzoGk_xr@Sp7(A>85-sw?g8xO1b@J)w}~nT)`|& z4fa~4zB|WOZ-?uANDjz@I@f-OMiRATTD5@4p|L_Wu!9mntcJ*;LF7VxXE)vV_;5y-VJ)2x zYw3c+TAVW@h>#*@h8RMMoEeOS5YKuDE$?H^`@hmWe=XLSpF=%@b=L&=&=zEsP~pV+ zGE%Z;v`VMQm$6BF8Ey!S`nN4%6ZtaM&X+;T8`DB5#~6I1Qx_cR z>~sKhURbM(mX$(0TBMkQ51s=aCX|l+9<4w^iac6k2r2StF+C6=E!O^3YCb%V8h`7q z1iE1@>ZE{}5=u{gXO=3b;&#bQuO?UI%-S5zEQ628fej1YCw^y^isp+*EaR<(2$&NA zbJ&i^)};E8iZy9wt-;>?RjUsB$gJjirH*a*mTE9JD%H&3V{(Szn4I646=^U?1dLM6 z%xY~-I$dy$w57$De`6v@7r^$ z)4B7;l%PuR;Veigp<;>iWT1s7#1LiDEn8!AGH!$?L(vU&bc$eb&)i;q08?y#H$AK`?!5=vFPD}#0-=TM_OEC(HPn4Sa0A*!5X z^GTN5xmK{81Rs$DlNGxDe)mcuuG@u?yntNfUJ+vwM(!2cz4FW)N~$7NgS3teqoIf> zJ3;Q@*H8pUEjj;65HUsm6*0u*YN}*UaR%&mDND@S981k7jy6xs-6}bqSf4qKJ_YMe zReS^!0!yfb(&KWRpoK{vH#MQDOX`g&OeADHQx3l_!>pUAHPDj)G9S%$BYVPb|VP>{f`Ig^#x;1jk!BH%p|% zL~fR~bF(xqUvE5i)+IEI%%iL`B-EqHHHl7xZ(y2 zE3Hn~CX`7q&psMZVVa)F*@ma^03>R@l3%--_V zm^=+#EWUHRhjeo;fakM7kMW;@{Hz+Udq8<{H;d zHXeVL*bEOpbOTUXN$e!Q^({Cen?xT38Su&WWR}q71cYZqWnF83oD-(A@x6tqOqF6% zB8=L{O7h!#G)3;lokH$` z>-N9%`7gMshy3pwy%X=d`0kg)zWozDJ>*Zqu}Y;Ls}=G&s}_5I)hg837q$OzvCvPx z?|3^tNe8WJEnh}U2Du-kYn8I)9G*DXd*^tj4|G(kn}AvuASwoXk65+dx9@!UO*`(e z3Sgq;Qf)e4sY$pC5h2s{dSxIMt7WGxjK9BJu}b)5vRo~ib$qQ(#VTg@fH`Hwfb}W2owK3rCqtf-s-L#qW0r}eXr!+<;lr>)*2~iXNqt?1w4Yi{ciI}d8Urrn$;+p}O*0LAg`Ld7v z{;3!B^pL+d*n7dd$CCSXzZELz^oY^3O)y}tWT|ph^ANUJvA|ZTXy!}3_VrLKmfuFp zF12&F%1Z85saiGc7`hj^AyKNdsk2sL1dF#lcw4nHhDIjz=e-9}3^$L>>f%et%79e& zI;YXEC7i}+n7e$+xsw-X!@Eg`siFlvvIT4e@+4BtKAN507jSNNdh~Ez%CcLyXS;zs z@D2B&POr?;!KJU`2Kni(ly((Cw$lCr(l4gVg`8EDWcNMU#qiKpc9*jUYEr3;31Smc zzM~8hBU4jOPorY6HB_rWsU?TlY+#z4XOk%+Ggq0WnI40^;BUK%Q}&7MSB%~pYz=F` z?y_9VPdB!@m+n;mYbeXV#_}U26_$Ik3oz-BeunA(o#>tT}ECq4S}^-Ve^0 zRR}?6V(G4@8-VPSpq;SdnVl@;D9wI2lZY#_rptY@noRU*=^>*pDI1wSd`o3giD5lG zoc<^bBPxQZBrRcdWVw(N&DFrRS3~vm1w4eD*k(b8c1DX=$l8oEv|2-@NsBN`{p5T8{Ewj2kiWME2s5VLc;aon^)Dp z^r8%YZKJG=jnb#7!Vx~l_HTeZR>>zz$IBU%vJXwV$eAKHmh8e$OqU| zEA=V1U;lUpJwb=4Ue*$p--gjnSGA-|E*Q1v^C+Xd&ywu;WHK(3gam37C^}lTifVVa zZ5+x#9AN%JMLIL|e}UT{npP=vn!01-nW3-jlND;dm;-{ck!+sDd!JrBce?TD zMd`Vtr=`U+Pd86omYTDd8lN~XHI7|JlWb4R?PzomEuPJ=-)d>6_jWQzUGsyInP_mN zFK~n0jn=pgM`p67*wst~@Yu~vX0=Lo*qS634nFO#*Cc8gs30e1+IYW*>gSq7;*6J~ zKyTl+AN>~gJ)<&Y^WKwE^Wt3d(Rn1QqE67U#^tN%j3R*783zOnIuQEEr&{x?(9I!k zAihhZK0rGQ(}OlXjS>&Zid%REJCz8dP7v^&%cnb2-j+uD5cy}*TL?MFPhXc5#6v6v zBP?BvKS&7mj%`-$fF&hNWEG_6|n7-&KpjS z52upM+Z5RxJit;gzEX~-D0HPAqm`4Ynp9x+Y(q{fsAoN86_L$BD89av*c`;nXr8?b zD$T9Z;v*-Kf=3#cUTfZc8UMX-8JQ%&zN0VjEHLnkA3xrF;`n+qW*&?w7>nssZDb{$ zT4I1ix=jh5D+s2p@yxt%?ksAyux``*>Lt=f@7U4xCZ87ICEU#qht74^D)b1boTbIqHZVD);L(vTP{vB^d#_4(}MO6C$#djiuOR^welPxI#e*v$Rqq zXN?3)0Uim!X_Bre^iCVU53mdx_dy2*BM1GO4LoOR3gt|(=R5@%s3aLmCev)`4aaDyAJp+(+tH|`tRsof@xnwDL?VCy z`ONGEc+&O=FbUS)3qy?Vm3m>evj5Z3R3uQ5G$=LixeJQF)Og}JQDA20QFV+p)Pb$g zynDWR;=Egj44JZvM~{XCzH_tQrSnF1F>@>yC^-J>4+a_tLPTK-sDsjS=_Lq53{3)@ z#D^&~*FEh09*RtFu_WMuD_PliPUE6pVGBf367#$@nn>_whBKV zEzcZEEN@{n4-?BFOi&@ktz|xjDT@wbs+t;inP>nJLC=b#E3)myLESrHOq3I>)T2i6 zjsy`wth%My2hyRciA0#qnPnYO5%c0OJe+hh;#BKi6q0OXpFdO<9O~5)vPQscZ3R+9 zOJWL{MYP0d$&EirH^Z&7{^Z0*aUrRMs&~nFLebD8rY$+aw-+bmMv)WSOEQrlln|6! zq~ZA@42`?zn&+=J9-LiwCC?4$vd&8oR6%K4qB2$L4lH_o!MK_lj}}jd;}DNODKK$?bV3Bq9&)(+SRON z@=03w^4!8xPa-FhfDAxovzMA*IcqmOTX*8(1DlX#LWv7?PZW|>MXXl^pY3E~4;`n; zDL2_zUWxS@F0Pp{kk(~u$ie!#FD{8_esyXB<$U%+96{*EqH@Dy4?5|t4Aa_ z{KYxCBKjgbVI^4q@Ie`u&yq*9_b+Val;BUDYMl6Nta<Ncm-9BUAT67kV+|2K_nt&4!C2#=Qwv``E5W+~ zfwlM$3JRw8rf-y^!woJ{FCt`_P~zeo9Z2;uTJ^w!%Z99;q(U8pihs*ls2EMdeJ=iulP(WCLfE^slSl*Bo@ zf=DTHbVZJ?t&Hf=-1rx@#UZ$a(ii9Gis&nHbgiAEt8wu-DS>` zy&s^Sxr^v*1jB3b%(??oH9q1AX(p7sI9FFH6;HNmf*aV?l^M;b=_q-PRbbTkcpkX2 z*nQ%2bpih$SD~757DMFfx)H7}bg)mLw;xlDIT+^VuZ?`^p&RDv()i#Vc(71n@>$<1 zx~imFX(_V4BkNnF^?h-+dEr#!nWM15*WLQ&BXy8cLMh2_eFu_KWPL~0_m;N4`3NTj zmr(lhTi+3VMTKM5uyD-&^sqF%Z_lmL*q(74RziB!97Qk7br)KEq*D_d>Ga#&DoPhD zDI6Ht++@W!ejbhHMnswGQ*c1kpGTu2;%&E;_Cn&3%^lg?%nKt3klwt3Ha8rP{(>=@ z;E1=c{g8^Trgn1!kCxC@#%n)>W-^G0+isOMxir<)w~)!`88wp$^xkYgte)=1X&Jl( z%vGo=t71(_j~G;G3)}$MezeQ&3YNb6!pI65*$@@IW6rc{7#TTeSBR^Otn)!T(4Nq_ z)=}jS?@8wx#qmhn5zbkh)Kxidz%@V%y3>Z3f{2YPMU8g~TXds`FvO z`4G@5xK8G8d6i|ko_nG4@M$N#JUU|es-AS-S(>UQ(?dZ!Ytws+-~7Rua{X*v-{ z=n~zc^Wi}-QK5wFlbRYT%P)!g$kl3w1f4#KI_J$Z$EA-*H#a|X{VmO}okkg`#V5G z)v_32j1_XEHAl8k-D6hv9i%0xie{~DRZ)5vhbsBf6uBFB3b_Za+yBnzzu=}G^1pBN zPQ33w{o7am!nc2-r-%GWI993DW3@s)XVqfwuUdr~`=a$9E*AR9_Z@G?C+VP7t>w!l zC`bL^%~mN}&f$rJy?2gh`anmux>?GZ1vD2Q>^)-Ddf&eDi+Xy<-y7_` z;N4@%{kq=@6|}U$=-DP1FjulvxvF^xTdY`Mt5h`erC$5`>PoI(cB!4ap)ca{8>dpW zYS=M!FLFboRB2OZt-=TvZ+q~zYGn+S!syR?52BAt9-Gy*$jQopRQEck(XS<(#%P$k ze9F0#IpBCs@NUv!YE?nNwPYIDX19GbJH0R9-0bw|;kuM%w;(1XpZx*8;XbruJ0Qow zM0cgMs|d1{_7}{oHC-;`tg1wfZu*{VE=cS1-R10onp7%dg4l$V?GcM)lb$?n3s8ny3jcGn&Z|GIv*PB z{ossQg%ETmmhEG*0mwcH+6gP3*~vnV((H#biMS$by4;77$ca8JJ!JGD;FRgZw^SyT z7}nFn>5ulh`w{%Pgwc`ZYF!t)Z1tU*w;Xr~IkC-xkf}huLHZ)~!^7A>^ds5BM6|oy z#yVWzUBmiT2JlImsltcx;o;$wp`??;eZ#Vr>C=E~>l?}#dS6CYH7z}y97?OBAH^su zvNn*dT9mQv%GnGS2bVC#Vm@0f*UFRi{%pAzW85`XDIc<`m2#fSQaK)DT>79{z$TBQ zgICf>rJUW4vC(lmqs1#^ZN?c|t)bGSMVLMNj*~C@BPccG@2vsCjA=KXcw4oKbJI&- z%1M1eIu35j-*-*)96Aqss0O)IonXgp`Om&MdQRKX3DtF|LduPc9`P%u;K^hGVx9Dx z|NS>_`_r4vo*we|YF^i+_tS~;wi>MltY{1CGT-A~;`PzAuWgQ~dR{V)3KotCBGA_H#k`x_Xazuq; zyNwKOx+e6fLIUCm)dMF(|3}S|mdNC$4-7S)Ji2&de(~7J#_UOCchP{(;+eCJyU#Yy zpOqF*pG5WM#Rsq7io!;hNH0JfBxwf^AM>={jz$R4>e-CD-o*}ja3}NBHAg7f33f62 z136k^X}cUh*~uFrAiELBu2#toTa(mTT*>aQ*I3UqyZs4F9q;#05g$JAM{i$-pDdFv z6zCU!gSIUk@u^!Svs8b7K1T?yF^|$>56mOfite_6eLji&ON4DDM1qv_8 z3fEB3NF*JND{Mc(NRJ|3QrmsnlKKLJg!mB1&%`LP-GtbHoc|}#5k%O6v$nP~6{-=H z^U$B=xRzK_!9`91?S$^{G-jwm(-OQ%NT*BcXvDS)yG!Izbx3c6ds|`q_;xkEFdD8) z{8Im!bf{{=m0KgOID{qwz&xZGA=YYnI8VN`Qv7KfkJ+jN(|q=a;N%ZORh1NW5ZPkT z)t6MmmXmQ^A${I942o<0lm=l9E2%gNrL3paw4NE+lFIs!E!(A=7oPfB;^JFY!^ z2>Q;-agr@g-HtAX0Q#uoB5w_UtTa=5<3j{GS+Iyk4ow2HNaK=jw9+mbf!XnX4;8Is za~;Y;V2kp7otT+l8 zI+4P5v==V)h@g_}l(P&QRZ?YT6q1-lbUCPBp+ureG7?x>ekS)LhXZW4bGMtZ%ZDyy z!BzY86k?aQA3T{bmT>q)%7~{@*y)7Bld2rc#xA!9hY!cMF>w(Li~)b^J862c1NI8T zwdpLbnrriMS}<{;Tw9fsdXlU*Ar~1L`S=Vr=u??w6z{_rqCjo;dHQ^Ok*ZmqQ4Q_N zkJ=j2hqX*PP!u6^r9_i&CXshG*t@@K0SA>ctGQmOlCNi{18oRrInB^l3itM-RWZup z4@fL8UxGI9rV!-BPhywgE8-{h`TLn)rVa7Djx5*&^8VS^Qr%@QELf{Yo01*Npakis_O6l9>wDhkyw z!5!8>A5CXQGEO=VXM-~X9hLW%tBp65$fzTZ%fWYxZDyY&(gH znG;GJKsI}i*`L{t5VOY+x9fhB#ZgPiNChej+b@$3NFP5l0&fy3*pqP=8G|B^aV#5| zcW%NafTI3#fKn_ae(RG&wlL>b593-hN! z$aOl2t99Whd>k1ZSSTs^Sgwkqipmm}tLxOQ!(5`IimW3A+wt7Bc8NL(6gReFYN0e% z$x`#tm!T)y&BLiyVE-PIOY~cUDGAjYo4}@rc2;$|Jg0Z#05;L7rYvDcMb_3RX6;a5tJ(p2-;Pijc1`8EZH*GMpJ*N&?$WE9YagkQhQmgo?7| zM1`V$$<3ygx69YF)AhDj)*i{3^Z5wCoy2YwLz8LYPQs2Ny`13EkdZ62_Ih}|Tprcp zL%}((n_Vw&K7JpmtkJx13<)41M7tQhoDZ#150bz%Yd_O%tC%OC|7{%Sb)=ZHIvw(e zHae1As+DX3OW5^pqtC(I!nOOPTZrD>xPs0EvzM9|&-6=;E3=;FEj1s$muTd)zRh*1 zaW_8ReHpg&`YYmm=oW%dsN^%^8hprbnRHUbH>!I)GlYIX!4iVv*H+*jA5Dz}-eS8c z;(R<4$QQA@Oh#pm&8mne((2G?aP@%g=O0(WD*TGL;9#AhCEPwpfr>aPY@o4&tvf^x za)!{`>`LLuSI4o)S_-~olPCcDNR$I8JjCKpnLa6WDt-Y9j`85^3{F_Lb8m(`hxZrFs+a z&Mv{eE}us^Y9mIxy~15xQ5!L!X3!FsOm2PW&@MZgvv}^bw0L%Q@!XTro6Tb7Z4z=J z8lRYJJn^)r-$o6?$cF5)N#n*Z!q+~x4<+`ZhGElYwPvkFq4|ZUFN5+VL`oZmL??bESbrBS zchzn6_$&p%NeVj3=Gy3Sqz!9Jc{N119(R~^gbH34RPYuqn?X(2%ZTr+2e-{uZ2k)21vDq zD{Q1k_eqVDN0C0;xN;S4B;bcpOG9crajE&tT#OaGi8b#%)ja<+v33~&_e<%5Ig_)b zq5ZpPkL(Mlnh(#T)`rwPe~l#ep^5hWNCmt&hhnW^pv3&5bZH8&Z8tW*-!}<6rnTf$ zq@Qhv(8Ag_WAn?D6xwMrk>N9@IhhhEXpw@p@=D&Q$L+XY+}Og{_&7NC$>!{rNDYO? z#mClLLqi82e;TCdPHaAUQe-`$F(soNJw-YiNdzHLC%+En_VbY=9j$Y%eBl-EoA=z+ zI0577$HpsTky|S|qxg9GdScDZ2r0ge6%EXl-~bNr&>e zn;lEiEpAT;mSnA4$@w_$=7W<&g~0{ENzk8%W7vXY;AlV~(|$aFBt?i4GzVCNY>4o6 z=C*LbXM($a>N3h&InGX|Nw2ptyvD88II@bcB?Buatj!d+*uq7k5uR9>KQA>OISsfj zU?GcVz5se9@kL@JBT?e|!k6ZKokvM3#r#>R`Scv2>=pvQdmez4bvKCDxX+#6AeJ{B znj})s1gU3h+$E2bBN3h?QqKl^RYb_LYnCTR)H8Hxzj(&x5}W&_p+2viVz>2AO7#eludx$6|qYinRO4IiHaB=}ZhYBeuhCP9kkSD>X` zoNJyrzWxdsAKHiH6k5U5uaIp%07;~fiBib;D7WZFdYtr#5F{&=9ih@?BYzoRJj5}^ zDVA{EwJ|=fEV|zyM^6!KB+|w#^e4-iong8g2sV-#`eoOVeg2`wiKlOMykCvW*P7>{ zY&m@#9zqATerfRx`jDTdPBZ|68z;Vk-_c@ie&MP4^;gJvEP(L#hh93oDkls(d(y*N zvR=kc#}#xaV~DX1>F_H}ujE?S*Q!$!=|YwD^+T65wOyYyXrtS#m8_epJ~nMuEJ<`n zJr!$;{3p^-FK`2dSo%V~WYN3^udadj$*C4alo7516lp;Xpdt1Z`KkS`FjT|H$aeNB zcN)3%h*_IvyNCYdCb?4Ug;nFDW%*b=YgWm4%*JY?yfNCx=5wT(E*HlY-B(Um+-_m2 z0#a(tJ4@=S9Cxd@Xo?Nb6hLg;jCVmpg09)jW693mQ>(#l(rqtPfkjQsd)rj+wqPat}iLQZw_Rq_XV7rogNK1+cgd`0{5Y$0DW5< z*D-OU?nZ3gqV7g4*NE+hy)(rLbTryuFtgTlxsZ#x8xb0=?=(DZJL+yEuDj9TM^1k? zOn0MIt2j}2qZ`!S$Wd;V?QYb2DwVpW*waJ)g1Q?eqV7g3?rua9kyhT_D4vlMN`NC2 zs>Q?Ta`^sQt$L zlK|>BkDXqeowv&XlUgPh9ztZT`7nULpZjd%6BniCrH7jLo}~ESM{(XUh8Ye)okAjN zEihmb$EB_h*4EB8nOz5n?f0(ohF}hoath3ucvd{(q(YE2 zU{DMr;dQ5RJSh&AKq4Ood#kI5oT#dX4C}xU@b>Ft!-I1L_d3$BoLF$W_SgokYY#LE z`IR)^U>}K(#@j%xD<7`+M#@-4tD_FxUbf#B;qj(|TOFb43ZZ$rML?J;E$HvD2Q4KV zK)AA2N0I36rurY$At_X=BRM{tN(O?jHa$dU|Fl`NBsqkDrL~iC`D)$q`f?GL3J5@9d5kR7+-|Rq51*Noq9sd|8T6q_$bf8v82#Lf` zB2$?|OC%oqEVzUfz|L_k4xvW~NmEj-ZCxB6D3wTnNh8leq~@P6Q0UOUV=yM2^0!rk%&nQ3#6!=y$cTU=kEPDN|hnsT3S4LlC;9ZspoN5G{ifH_LpA@lcLElPM3<) z8h2Onc}!(^!W(pUafEV|D1nwAhMg3v3P z)!pO+;l!aBd3cwP5g*bs0eBi%4x8OxWnNPygyJN`Yl@Bu91#gFPvrNT;`7qh-G*i) zi2@ioPS=l$wy@(={dFC6TcyQwXV7tO-D!!B^Fm+<6-aU_l{ARZgU3f!6_wCGCy`Lm z99|!rmNtSFvJoISp(GNoy>>P&wzluoK+Tr3H^yx$^RaNzJ$)2Cl>{LvpF`A)kq0I2IPl<#y4 zGu?$Nvx|>B5JG1W<2dD`z2Ln1(R)8JtdPadD_BFDOJg4;|6>Aci zl!Lwdt1Bs*7t}qzaqTqoZvwQB!gbe+c`sBg^P+CzNRY{?R0AnYCgxHm-j(kyeLBN9?dP z7l1BoKmBNpoBjA%JD>=u*3fd~9}VE^p{&vH{?VaCa(GMoN7WGekr4mr8u!!T{G*9f z9Jv|1F&Y6&*Pn{Euz!>`I=R?*a5iLz6A`%5HEtT@N0!P-0GnF_*v@`5&^KKKTapQJ&*z*ww|zK3rXZJv_`ulrzlzRiE{1;$;oV2 zcQ+qw%?D_GDEGeKteSVAPIAF4O%3*nY6QZ^b49nCQIv#{aQ8_7lZ~9BtJPN`r>L*8 z!i{l?0xQokTfu=`zvY~Q&d;Fv5uDU4OqB+E3%ybsHX1oa*SG~D-=Uf|u)P)a%a`;< zv=6doxwJoz)I`s<`Om^>dROz2C)eE}iXILe!3xfw_Bliq70OX7@M7aSL`Tw!fyrS? z=eBo<@&RMfonL%xT~WZ+TjRx%Lv*#OQFw=FCW-1^TRF{M3t@oK zY-me(t%kleju$7&wC@6S5uS>+utSveZ-Juo@Uexb=aD0GvT@<_iw|C3ccS70x}w`8 zDN0gBjTZ`eQ`9&LkF?}6DCj{*toq0`8o5SS1+9Q)s>vh^xKpKJ!-=o+`%Q4(=AKZi*;9V@8h1rlktYy+$lAIC7Cc4{$=YV{bZdsDa zbBmOWzlj987w>(VoG}=n!=YHx3Dih(oW#;cq?=KF=`ASMO1ghsJK1>rSJ%O zb%_sD`3SfwI0DYM5?0kzs{lq#G&y01Wmq_zy~t`i3~ZbF$6-1NBPgaDsVzySiA~o@ z_+S3ur!vcT5~e-YqE5n_wv#YjKT2_(ItkzPmM;}n)=7BPJ@=wc!ciw-;+UxHE@uza zq*56@lv!)#@*QOqW+P3O%js!g%dMeW1=cP(L}W7UIy7ecBuKMm(tfZqEs@M>cq0nA zUBxN;MD{DL;2RLJyDZm_!Sy=rE!V9Y*Z)=__xFt>;5C*XDZ%2y?#-bCwDV%^$Ln>H zaJ(;gKoE*Q*gE!GT%>pH3L~8U%is?RT|nIRsYIQGsTZ!*YeCnrzSEbC6Lk_6*Gc#z zQ?I=irjzihRh+1k@D1uD>?k+Ob`pN!JOBD8AO3}&9`g6bo}Qjcr5>vl@;PgdS*u&s zn*WO|psO&ut#r6pDAfkc!QSb5y)qDs)w0u8(X92CD^>}=O_r-gvyQLTsaURR9?F-d ziiH?r5k{eTk@8G^S8jI=Y5;;e-Vx{`00_yXrHX23zzbCe$dT%< zoTH!kPPQ+fo|JHK?@Wk0MewS!e&v(E__vSE?T#s~;&?wVe_kTF=)D7X7|c zl872}upmRBlUM=`^)cqG$#HXH3|{?|tRa5^qp26@%`J0xX}F4a#jMk+x%O9_LbOE? z&;FSbQx~bTu#z3NChbrAvvuj9N$*1xG@`dTzq*kIa{$jKd5t8bz#e_Tsv@yRCe419 z`X31`Tj@fT-55K9DxWPOx7PB$w-i6nSE!RS5d$)*?#ikHksmCR{gAD-{E)5L_huuc zZ(z?I<`sL^Ke3o+qGAS2Gb?xu$99U$tCOtM=Nxd;nAUg=Fr5q?(XtMia;ffn@u6zo zL{3#DU(Zfw%woQPKrP&ZUbx(@I59B;YoSUH5B5HH^wVq(eQSvZXL#MUSj$qowJfz; z%Tiy*wX}&sAw_gR>)m0&9_`w8p<48UbwZED}cJ_?%d$KkgSlqT)MYh3Q9fD z?Y1SC>vz7oloxJ>)glYzu9o3jEnU_q>)Vp66{Kho#b(3vLb5{cawUArl}gA|^hV2- z%j63Ms_=KI;|tUa$qKp4mGqX2DeatMxYad{%|Kmbh1}&z`Ial0B)!c2dVI88H=er4 z3NZ`XXEMWpF4w26s?UOkHMt*W!f3bcN`LJm=25Uu;rtp-haua+0c#d#iE(e(pZ(8Y8~^#99`fg)8z{`uK{xomtQy_myVn-S zH%v^CK>5Vf9oD1`*NewV4k|*tyyXg909_;dWC|T%7jH#~1W61f6XRhkvwMNYWGDhB z$NqN9%vn_qJW1zhwctr04SH#S{)01S704ZX-rE2u$yzyy9015jfLubqLmauKS%KpC zK1mj)rY94uQVQbJ&efG_t$v$TE=uGBun}AqCUXEdr_klE5px%lv?O=-pjE~7)8*=i zfMVEHs@XUPz(?p8Y7PVQ;QwWU`=wq;mmMI32LvT=%A+X?bf@}_V4}IPp*q+Bf`lm4 z>deuBJ78S_O9-y;Y`-LZJLjz^T85%Y+U^J!O*>-NEp9&u0n_H{D}&Mo>;yHL)SR}Y z+h9MCYB#R?in@(fxE}ybrL_HFG$Bwjvaa^288y)t&uDsIYBZJTOQ=KX%n<498c%-| z8=2yJF<)M?ihQoGZPA#b9T}5+>-;uPqSZ59e zY`OrHIuj?906;=3XI@59r@lULDFD45v;HmmW~ZY9iybh9FBwC3EAtMf!B$zgQ#%NlSxH#vD}-hf6X zqw~hX378m2(lL8(*skzpIIz3{Lb;CYW7~-C+HSWI!MUORG8tdHZ3I(IV`pq4dFU-N zrc;8vb7g*Dq69s?zB66OXAemAvSj7*b;$vqr;dWzIwp_|d1?!+ySiv1Bo9KMU@#%bZyU8U#%$I5G&jMC_9hN3j4Z6?9Dl_jMsm*|`cn`L&z zsxhqflV>mpO?Dh?*e!7*j+h!usBIB#wFboSFv}ag3moP%!ua|DeowIsHH9e}2 zrqXG-FP(@RL{UxkrIZn)Z&Xjn>9jsNI+V<~iYiS5Da{l+>8H;f{mL&scl5VBX5F=Z zO39Sv7gPoySZp{w5iv8>{9x~ghlf*!l1>iy4a-`lPaA=LJ(Mx@zKpJFT6#D+lvYPS zim_H?Z6I5Ic#?nM$mGU90S}EsgridK3(@iKs zUXBCOnKV)<$ESg@QQgEDEnXoz#xg)QeO?50X1V6C!@<@mY)>O88yt#B!}g51uY1sb zGt3VS+OzpliKfZ6&iaJxH^cmpNwJj!Kkj;c0`{9>f@r{=Z6F`gd7Z-bn_+@zxSmar zA6&dX!TQZGK{Qy;Cdd!xU7t|>W|$xvs%I1A2hd{Z>kz2l3=>2H^=yLtkk$1G({F|e zqG5XCpY!XB&ey=u{OfA}{$+1PSf2bjp?P2Av=(EbdEX%#nB91x`PSM7(k>@8SQ@8} z{^gU&mrYG)vF_z!;L9fYfAHKRUw-ckdV0v;&)RT^B0PC!EKgkheR7}NUnx!9w}0<# zFTd&a1V-`lUAK?if&VJ_i@)S~`1ku~zP{Mg^MY6G8W|ctJlFj5KiKyw^JjYg_Q%gF z{`rU{%cp+(*b9Dqul&ZJzU!I7|N48g_q_Ol7iV8`)0v*dG)bpG4b=D)m5e)IcAe)A8qzw>{dedQZ}fA%HkM&5hjq5tFTd;ayS@B7u_*NWf$ z%h89Q|HGf|z3{WY-2c*B-uI`!Z=W%KwS3Dfe(um~j=uHuk(o#S zzi(aq);C`K&0{aT_|)Kwziqwdo40)WwXb;5_wPKg@2g*U@YNst`CEox|FNl?_wIes zN3X)gb&Z{ySg)UQT}L^FICDw?FW*C%^F8%73~%*cf|y z{MJt%|LH&bZ{Pg=7yk5XKK{yo+PC)yvA=t#(f`~tmC*n(%D&3tU*9zRxnKXnU;M$FUXeUC^qt?mcRu;Z$6tKgr=I=XzkU8+-BWty z?>zS_cl}?#GW?&DN6)_JBbB+KZ#?zA=MLZfy)V7w=e{+4Z1l`aUiFRZd*A+tcmGrG z3vM~^?hm|c;t#*|+kK~Bblb}_ugv{JJ@f4ovH#~c|7Pba-}a`Dr~k+8-~2&h?&dvz zvh&>6|LTYT^}T=d#1D4He%$lKHwWYY?`z)s?4LEh`+INt#*v5q>cu~G_KhDq_kSP! zjSs#24MTUIeCrQ>_tSs+@$uq2c7Ok#`^@M1s?XHF`e(oOig&*%_PX1C?x`QW_P|Tt z^Be#D-}L>-3v6-Fu(-lkflaAOGuXgFC8LY|CitRpTp<=|H7^Xo{DXa&-SW3j-Av+5;;0<6j6Cq z9wCKN*A12A73y^M_Bf*JCOh;N($PVs{5mBiZf=O^=!S?SQW4tMqZCmcQCw%%%ywti z?6tS~`TS-!-+%qT|2J#R%$k{R^M{%oqtY`LkL}BNtZ$!dm%ez{*R=a%**0azTU;b} zzr2|1(xAObEADSyD*|P+YZbjqR$g~WTU%#RDdP~3wTFE$r@A_)WsW9CFCsj$xy1}s3plF+?~Lv1HIh%Q&yVphG(Ky1! ztav|1rnY>F8lG3wKk)aTw<{@oX*OI=TJq-O_=O)jwH;ip-dX8%yrFyD7srvk6@T12 zwZ^8s`{E77IwzZ+3=OwkHbz|^_zo?>pYEQ2Yce=Tsi3(#$Tz3f`1S2OW99uHt@*Ki zA9?wM2kp0JH)>soov9gkEiY`BG{1HMSN%>@wTIMq=L2neqE^p;JYf*)_u9In+at~} z+U~X1^y#N8@quAt;`Fm3CwxT)3=NW)kq$fUNPD3z{t33uZi8*E%}yTtbXH@EPgMy% z`~yP`M@AxtBNZG|g&2-Fv#;I6Fcw!Z9J$u*uCQXaJ8xe0pegq3A%jlqp@d%A)_ z?U2ut2aA-8Z29?{lghvO)b|$n`5FFr>)*e3W^d6>K2jTO>en7%SW%i+>7aa1-SYPF zAc?9O5>>(XxpMxock~Kdu?o$27yZ=W57Ba68`K^L7LS>#`xL7W?tkoKuX z_S7%UeQ)-fb(v;wwM!_;$nXp|r|2bFZE&x#_o5gQY%r^>?uTiDztLB5UQ63M$`y+j%VIf|g>@ z`5t}XxN$A+2@IWYBN2xmX1=j8LB5+TGQb7g5VI*lrk0LKMt$8^2Vsh|WZl0>sWjn8hw#tuo8^)BLY(MkzSY5uS8M}PJ93|JzZ%xPA z-OW-GuQkloN{T+W$NO1UN>6zG<%Yy~@AHx&Ev*XEE;YG5)o*1D6ugW$*mh!gPsx+_ zg%-iznQxCs@@zc4VsMz{eg9RXcfp9pvs;zBeo-s5(p9UFDbaZ$*YQ($BWvdP>H|^P zRkfpB^Rj5Ev88=Choc+tXV@ktUY^wYjnZDR-aTIO=u!(DyXX9nc+J-+3~@pTE|a6=8mx#6c8f(10?h#M#^B+i4R44i<+UV+Ed1m%DW zu>ij_xPg{@@zNJz;wjD~z3|__u?%*cx?M%c9X~lD+V$V-_X6mEuBxIW(i(r{(}BT^2t> z{eR^u@e07?jbX>B+lBMyBXB1lOcfV2-0U%dAMGkfJZo69VwC&T+(F981*{FwL5Q1x z;wj9TbS8R>x+JMMdEEyJhD~0%F;#FIvg93|g<-((AKK!AV{BW4*S5iHLV1B}rch5% zVT=hd*j7E_iIW9Jr5)hN;|v}K_-DILk2UQi6+AS2l*@#<=wCWZl1 zra@uY`$Z%e^U-Lg78Z3@&5;Ti1`I}chmXym?=Xol5!}-gE9T>!6&S3?FCG*EabQ5{ z@qGN`soOYpVDN6xwN(&;w~wF(RwxR%|NAm$&KYBWDd9x}m|0HzPYsrVq^$Xqi)(YZc5@+e&w!+_0(kbm{1 zAq(DKK$2T+i^aO*aRSqXAOcQ65HwoUEDJN#C246%^b*Jv;CKRwMjr&IbtJVCb89Jj z2}%mA20wCx{g48f98suGM3Wr-DK<-Q2ZjNIReO~Rg-qnY2(^RpdcNG?RTE8e zL86(N9)1N03|4J41PYlDk0_SQJnAvO1cDmZ+&Uo$nrc+NWF{K(YXm4XU%qgllxj$7 z%redtnBiMtFs5pvNiu5zQ@Cs1(L$cQ#v`;NH;{*V=1Z6qoT{u8HO5WC(R8GNBKJY& zYkd^5mOP0}C^(5s+dvX!-jbr=3jT_KqX{|{QRa;o3Rzc=L`K|(VeUK3=~4=$&_EbO mFnP)ZVa~x(AXk>6K!_PR7e{Gg#>fVih}UV|*?E literal 0 HcmV?d00001 diff --git a/rules/slides/usage-plan/convert.cjs b/rules/slides/usage-plan/convert.cjs new file mode 100644 index 0000000..b35464a --- /dev/null +++ b/rules/slides/usage-plan/convert.cjs @@ -0,0 +1,31 @@ +const path = require('path'); +module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); + +const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); +const PptxGenJS = require('pptxgenjs'); + +async function main() { + const pres = new PptxGenJS(); + pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM_16x9'; + pres.author = '(주)코드브릿지엑스'; + pres.subject = 'SAM 활용방안 - AI 자동화로 중소 제조업을 혁신하다'; + + const slideDir = __dirname; + const slideFiles = [ + 'slide-01.html', 'slide-02.html', 'slide-03.html', + 'slide-04.html', 'slide-05.html', 'slide-06.html', 'slide-07.html' + ]; + + for (const file of slideFiles) { + const htmlPath = path.join(slideDir, file); + console.log(`Converting: ${file}`); + await html2pptx(htmlPath, pres); + } + + const outputPath = path.join(slideDir, 'SAM_활용방안.pptx'); + await pres.writeFile({ fileName: outputPath }); + console.log(`\nPPTX saved: ${outputPath}`); +} + +main().catch(err => { console.error(err); process.exit(1); }); diff --git a/rules/slides/usage-plan/slide-01.html b/rules/slides/usage-plan/slide-01.html new file mode 100644 index 0000000..bfa9d9f --- /dev/null +++ b/rules/slides/usage-plan/slide-01.html @@ -0,0 +1,42 @@ + + + + + + + +
+
+
+ +
+

SAM PROJECT

+
+ +

SAM 활용방안

+

AI 자동화로 중소 제조업을 혁신하다

+ +

방화셔터 제조업 실증 | 80% 공통화 전략 | Multi-tenant SaaS 플랫폼

+ +
+
+

코어 모델 실증

+
+
+

AI 자동화

+
+
+

다산업군 확장

+
+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스 | 2026.03

+
+ + diff --git a/rules/slides/usage-plan/slide-02.html b/rules/slides/usage-plan/slide-02.html new file mode 100644 index 0000000..c16747f --- /dev/null +++ b/rules/slides/usage-plan/slide-02.html @@ -0,0 +1,58 @@ + + + + + + + +
+

왜 SAM인가? — Before / After

+

중소 제조업의 현실과 SAM이 제시하는 변화

+
+ +
+
+
+
+ +
+

Before — 기존 방식

+
+

Excel 수기 관리

+

데이터 유실, 버전 혼란, 실시간 공유 불가

+

ERP 도입비 수천만원

+

중소기업에 과도한 초기 투자 부담

+

업체별 커스텀 6개월+

+

도입까지 긴 시간, 업데이트 어려움

+

부서간 정보 단절

+

영업/생산/경영 각자 관리, 의사결정 지연

+
+ +
+
+
+ +
+

After — SAM 도입 후

+
+

시스템 기반 통합 관리

+

실시간 데이터 공유, 단일 진실 공급원(SSOT)

+

월 구독 SaaS

+

초기 비용 최소화, 사용한 만큼 지불

+

멀티테넌시 즉시 입주

+

설정만으로 바로 사용, 지속적 업데이트

+

영업~출고 원스톱 자동화

+

AI가 연결하는 End-to-End 프로세스

+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

2 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-03.html b/rules/slides/usage-plan/slide-03.html new file mode 100644 index 0000000..07e9995 --- /dev/null +++ b/rules/slides/usage-plan/slide-03.html @@ -0,0 +1,108 @@ + + + + + + + +
+

전체 프로세스 — 영업에서 출고까지

+

6단계 비즈니스 플로우와 AI 자동화 포인트

+
+ +
+
+

01

+

영업

+

고객 DB 자동분류

+
+

+
+

02

+

상담

+

STT 음성 기록

+
+

+
+

03

+

견적서

+

AI 자동 산출

+
+

+
+

04

+

수주서

+

자동 전환

+
+

+
+

05

+

작업공정

+

AI 공정 최적화

+
+

+
+

06

+

출고

+

배송 자동화

+
+
+ +
+

경동/주일 실증 현황

+
+
+

단계

+

구현 기능

+

상태

+

AI 적용

+
+
+

영업관리

+

고객/거래처 CRM

+

운영중

+

고객 분류 자동화

+
+
+

상담/문의

+

상담 이력, 음성 입력

+

운영중

+

STT 음성→텍스트

+
+
+

견적서

+

견적 작성/승인/발송

+

운영중

+

AI 견적 산출 (개발중)

+
+
+

수주서

+

견적→수주 연동

+

운영중

+

자동 전환 프로세스

+
+
+

작업공정

+

BOM, 공정 관리

+

개발중

+

AI 공정 최적화 (계획)

+
+
+

출고/배송

+

출고 지시, 배송 추적

+

계획

+

물류 자동화 (계획)

+
+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

3 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-04.html b/rules/slides/usage-plan/slide-04.html new file mode 100644 index 0000000..d6fb551 --- /dev/null +++ b/rules/slides/usage-plan/slide-04.html @@ -0,0 +1,88 @@ + + + + + + + +
+

80% 공통화론 — 핵심 설득 논거

+

중소 제조업 업무의 80%는 업종과 무관하게 동일하다

+
+ +
+
+

공통 업무

+
+
+

80% — 영업, 회계, 인사, 재고, 문서, 품질

+
+
+
+
+

커스텀

+
+
+

20%

+
+
+
+

커스텀 20% = 상품 마스터, 견적 계산식, 공정 시퀀스

+
+ +
+

업종별 확장 시나리오

+
+
+

업종

+

공통 (80%)

+

커스텀 (20%)

+

난이도

+
+
+

방화셔터

+

영업, 견적, 수주, 회계, 인사

+

셔터 규격 계산, 설치 공정

+

실증완료

+
+
+

블라인드

+

영업, 견적, 수주, 회계, 인사

+

원단/슬랫 규격, 재단 공정

+

즉시가능

+
+
+

금속가공

+

영업, 견적, 수주, 회계, 인사

+

소재/두께 단가표, CNC 공정

+

단기적용

+
+
+

식품제조

+

영업, 견적, 수주, 회계, 인사

+

레시피 관리, HACCP, 유통기한

+

중기적용

+
+
+

전자부품

+

영업, 견적, 수주, 회계, 인사

+

PCB BOM, SMT 공정, 검사

+

중기적용

+
+
+ +
+

"상품만 바꾸면 새로운 제조업이 된다. 영업, 회계, 인사, 재고 — 이 80%는 이미 완성되어 있다."

+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

4 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-05.html b/rules/slides/usage-plan/slide-05.html new file mode 100644 index 0000000..87cf510 --- /dev/null +++ b/rules/slides/usage-plan/slide-05.html @@ -0,0 +1,74 @@ + + + + + + + +
+

멀티테넌시 — 하나의 플랫폼, 다수의 기업

+

tenant_id 기반 데이터 격리로 안전하게 다수 기업을 서비스

+
+ +
+
+
+
+

A 기업 (경동)

+
+
+

B 기업 (주일)

+
+
+

C 기업 (금속)

+
+
+

D 기업 (식품)

+
+
+

▼ ▼ ▼ ▼

+
+

SAM 플랫폼

+
+

공유: 코드 100%

+

격리: 데이터 100%

+

기반: tenant_id

+
+
+
+ +
+
+
+
+

비용 절감

+
+

하나의 코드베이스로 N개 기업 서비스. 기업이 늘어도 개발비 동일.

+
+
+
+
+

즉시 입주

+
+

tenant_id 발급 + 기본 설정. 별도 개발 없이 수일 내 사용.

+
+
+
+
+

데이터 격리

+
+

모든 쿼리에 tenant_id 자동 적용. A기업과 B기업 데이터 완전 분리.

+
+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

5 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-06.html b/rules/slides/usage-plan/slide-06.html new file mode 100644 index 0000000..ea16e8a --- /dev/null +++ b/rules/slides/usage-plan/slide-06.html @@ -0,0 +1,68 @@ + + + + + + + +
+

AI 자동화 현황 & 로드맵

+

구현 완료된 AI 기능과 향후 계획

+
+ +
+
+
+
+

구현 완료

+
+
+
+

AI 재무 분석

+

CEO 대시보드에서 매출/비용/손익 AI 분석. Claude API로 자연어 인사이트 제공.

+
+
+

STT 음성 입력

+

상담 메모, 현장 보고를 음성 입력. 자동 텍스트 변환 후 시스템 기록.

+
+
+

Claude Code 개발 자동화

+

SAM 시스템을 Claude Code로 개발. 코드 생성, 리뷰, 배포 자동화.

+
+
+ +
+
+
+

향후 계획

+
+
+
+

AI 견적 자동 생성

+

고객 요구사항 입력 시 과거 데이터 기반 최적 견적 자동 산출.

+
+
+

AI 공정 최적화

+

생산 데이터 분석으로 최적 공정 순서, 자재 배치 제안.

+
+
+

AI 고객 상담

+

FAQ 자동 응답, 견적 문의 자동 접수. 필요 시 담당자 연결.

+
+
+
+ +
+

"공정의 다양성은 천차만별. 이를 AI와 데이터로 정복하는 것이 SAM의 연구 과제다."

+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

6 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-07.html b/rules/slides/usage-plan/slide-07.html new file mode 100644 index 0000000..ed9da70 --- /dev/null +++ b/rules/slides/usage-plan/slide-07.html @@ -0,0 +1,82 @@ + + + + + + + +
+

로드맵 & 비전

+

방화셔터에서 시작하여 모든 중소 제조업으로

+
+ +
+
+ +
+
+
+

Phase 1

+

코어 실증

+

2025~2026 상반기

+
+

진행중

+
+
+

경동/주일 방화셔터 제조업에서 전 프로세스 실증. 영업→출고 파이프라인 완성.

+
+ +
+
+
+

Phase 2

+

3~5사 확장

+

2026 하반기

+
+

계획

+
+
+

블라인드, 금속가공 등 유사 제조업 3~5사에 멀티테넌시 확장.

+
+ +
+
+
+

Phase 3

+

AI 고도화

+

2027

+
+

계획

+
+
+

AI 견적 자동 산출, AI 공정 최적화, AI 고객 상담 순차 적용.

+
+ +
+
+
+

Phase 4

+

다산업군 플랫폼

+

2028~

+
+

비전

+
+
+

식품, 전자부품 등 다양한 제조업종. 중소 제조업 표준 SaaS.

+
+
+ +
+

"방화셔터에서 시작하여, 모든 중소 제조업의 디지털 전환을 이끄는 SAM"

+

AI 자동화 + 멀티테넌시 + 80% 공통화 = 중소 제조업 혁신 플랫폼 | (주)코드브릿지엑스

+
+ +
+

7 / 7

+
+ + diff --git a/system/ai-automation-vision.md b/system/ai-automation-vision.md new file mode 100644 index 0000000..18534a5 --- /dev/null +++ b/system/ai-automation-vision.md @@ -0,0 +1,174 @@ +# SAM 활용방안 — AI 자동화 비전 + +> **작성일**: 2026-03-02 +> **상태**: 설계 확정 +> **대상**: CEO, 경영진, 전 직원 +> **관련 페이지**: MNG 관리자 → Claude Code → 활용방안 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM(Smart Automation Management)은 방화셔터 제조업(경동기업, 주일기업)을 코어 모델로 실증한 차세대 ERP/MES 통합 시스템이다. 이 문서는 SAM의 장기 비전과 AI 자동화 전략을 기술한다. + +### 1.2 핵심 논지 + +> "중소 제조업 업무의 80%는 업종과 무관하게 동일하다. 상품만 바꾸면 새로운 제조업이 된다." + +| 항목 | 내용 | +|------|------| +| **코어 모델** | 방화셔터 제조업 (경동/주일 실증 완료) | +| **확장 전략** | 80% 공통 프로세스 + 20% 상품 커스텀 | +| **최종 목표** | Multi-tenant SaaS 플랫폼 (다산업군) | + +--- + +## 2. Before / After — 왜 SAM인가 + +### 2.1 기존 방식의 문제 + +| 문제 | 상세 | +|------|------| +| Excel 수기 관리 | 데이터 유실, 버전 혼란, 실시간 공유 불가 | +| ERP 도입비 수천만원 | 중소기업에 과도한 초기 투자 부담 | +| 업체별 커스텀 6개월+ | 도입까지 긴 시간, 업데이트 어려움 | +| 부서간 정보 단절 | 영업/생산/경영 각자 관리, 의사결정 지연 | + +### 2.2 SAM 도입 후 + +| 개선 | 상세 | +|------|------| +| 시스템 기반 통합 관리 | 실시간 데이터 공유, 단일 진실 공급원(SSOT) | +| 월 구독 SaaS | 초기 비용 최소화, 사용한 만큼 지불 | +| 멀티테넌시 즉시 입주 | 설정만으로 바로 사용, 지속적 업데이트 | +| 영업~출고 원스톱 자동화 | AI가 연결하는 End-to-End 프로세스 | + +--- + +## 3. 전체 프로세스 — 영업에서 출고까지 + +``` +영업 → 상담 → 견적서 → 수주서 → 작업공정 → 출고 + (01) (02) (03) (04) (05) (06) +``` + +### 3.1 각 단계별 AI 자동화 포인트 + +| 단계 | 구현 기능 | AI 적용 | 상태 | +|------|----------|---------|------| +| 영업관리 | 고객/거래처 CRM | 고객 분류 자동화 | 운영중 | +| 상담/문의 | 상담 이력, 음성 입력 | STT 음성→텍스트 변환 | 운영중 | +| 견적서 | 견적 작성/승인/발송 | AI 견적 자동 산출 | 운영중 (AI 개발중) | +| 수주서 | 견적→수주 연동 | 자동 전환 프로세스 | 운영중 | +| 작업공정 | BOM, 공정 관리 | AI 공정 최적화 | 개발중 | +| 출고/배송 | 출고 지시, 배송 추적 | 물류 자동화 | 계획 | + +--- + +## 4. 80% 공통화론 + +### 4.1 업무 구성 비율 + +``` +공통 업무 ██████████████████████████████████████████ 80% +커스텀 ██████████ 20% +``` + +- **공통 80%**: 영업/CRM, 회계/재무, 인사/근태, 재고관리, 문서/전자결재, 품질관리 +- **커스텀 20%**: 상품 마스터, 견적 계산식, 공정 시퀀스 (업종마다 다른 부분) + +### 4.2 업종별 확장 시나리오 + +| 업종 | 공통 (80%) | 커스텀 (20%) | 난이도 | +|------|-----------|-------------|--------| +| 방화셔터 | 영업, 견적, 수주, 회계, 인사 | 셔터 규격 계산, 설치 공정 | 실증완료 | +| 블라인드 | 영업, 견적, 수주, 회계, 인사 | 원단/슬랫 규격, 재단 공정 | 즉시가능 | +| 금속가공 | 영업, 견적, 수주, 회계, 인사 | 소재/두께 단가표, CNC 공정 | 단기적용 | +| 식품제조 | 영업, 견적, 수주, 회계, 인사 | 레시피 관리, HACCP, 유통기한 | 중기적용 | +| 전자부품 | 영업, 견적, 수주, 회계, 인사 | PCB BOM, SMT 공정, 검사 | 중기적용 | + +--- + +## 5. 멀티테넌시 구조 + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ A 기업 │ │ B 기업 │ │ C 기업 │ │ D 기업 │ +│ (경동기업) │ │ (주일기업) │ │ (금속가공) │ │ (식품제조) │ +└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ │ + └─────────────┴──────┬──────┴─────────────┘ + │ + ┌─────────▼─────────┐ + │ SAM 플랫폼 │ + │ │ + │ 공유: 코드 100% │ + │ 격리: 데이터 100% │ + │ (tenant_id 기반) │ + └───────────────────┘ +``` + +### 5.1 핵심 이점 + +| 이점 | 상세 | +|------|------| +| **비용 절감** | 하나의 코드베이스로 N개 기업 서비스. 기업이 늘어도 개발비 동일 | +| **즉시 입주** | 새 기업 추가 = tenant_id 발급 + 기본 설정. 별도 개발 없이 수일 내 사용 | +| **데이터 격리** | 모든 쿼리에 tenant_id 자동 적용. A기업이 B기업 데이터에 접근 불가 | + +--- + +## 6. AI 자동화 현황 & 로드맵 + +### 6.1 구현 완료 + +| 기능 | 상세 | +|------|------| +| **AI 재무 분석** | CEO 대시보드에서 매출/비용/손익 AI 분석. Claude API로 자연어 인사이트 제공 | +| **STT 음성 입력** | 상담 메모, 현장 보고를 음성으로 입력. 자동 텍스트 변환 후 시스템 기록 | +| **Claude Code 개발 자동화** | SAM 시스템 자체를 Claude Code로 개발. 코드 생성, 리뷰, 테스트, 배포 자동화 | + +### 6.2 향후 계획 + +| 기능 | 상세 | +|------|------| +| **AI 견적 자동 생성** | 고객 요구사항 입력 시 과거 데이터 기반으로 최적 견적 자동 산출 | +| **AI 공정 최적화** | 생산 데이터 분석으로 최적 공정 순서, 자재 배치 제안. 불량률 예측 및 사전 경고 | +| **AI 고객 상담** | FAQ 자동 응답, 견적 문의 자동 접수. 사람의 개입이 필요한 경우만 담당자 연결 | + +> "공정의 다양성은 천차만별. 이를 AI와 데이터로 정복하는 것이 SAM의 연구 과제다." + +--- + +## 7. 로드맵 — 4단계 비전 + +| Phase | 제목 | 기간 | 상태 | 핵심 목표 | +|-------|------|------|------|----------| +| **Phase 1** | 코어 실증 | 2025~2026 상반기 | 진행중 | 경동/주일 방화셔터에서 영업→출고 전 프로세스 실증 | +| **Phase 2** | 3~5사 확장 | 2026 하반기 | 계획 | 블라인드, 금속가공 등 유사 제조업 멀티테넌시 확장 | +| **Phase 3** | AI 고도화 | 2027 | 계획 | AI 견적 자동 산출, 공정 최적화, 고객 상담 순차 적용 | +| **Phase 4** | 다산업군 플랫폼 | 2028~ | 비전 | 식품, 전자부품 등 다양한 업종. 중소 제조업 표준 SaaS | + +--- + +## 결론 + +> "방화셔터에서 시작하여, 모든 중소 제조업의 디지털 전환을 이끄는 SAM" + +**AI 자동화 + 멀티테넌시 + 80% 공통화 = 중소 제조업 혁신 플랫폼** + +--- + +## 관련 문서 + +| 문서 | 설명 | +|------|------| +| [SAM 프로젝트 개요](../SAM_PROJECT_OVERVIEW_FOR_AI.md) | 기술적 개요 | +| [스케일링 로드맵](scaling-roadmap.md) | 10,000 테넌트 기술 스케일링 | +| [보안 정책](security-policy.md) | 보안 아키텍처 | + +--- + +**최종 업데이트**: 2026-03-02 diff --git a/system/database/codebridge-separation.md b/system/database/codebridge-separation.md new file mode 100644 index 0000000..9022cbe --- /dev/null +++ b/system/database/codebridge-separation.md @@ -0,0 +1,443 @@ +# codebridge DB 분리 + +> **작성일**: 2026-03-07 +> **상태**: 로컬/개발 서버 적용 완료, **운영 서버 코드 revert 상태 — DB 선행 작업 필요** +> **최종 수정**: 2026-03-09 — API 사용 테이블 점검, 로컬/개발 samdb 삭제 완료, 운영 코드 revert + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리한다. + +- **samdb**: React 서비스가 사용하는 테이블 (수주, 견적, 생산, 거래처 등) +- **codebridge**: MNG(관리자 패널)에서만 사용하는 코드브릿지엑스 내부 관리 테이블 + +### 1.2 핵심 원칙 + +- codebridge DB에 테이블을 복사한 후, samdb에서 해당 테이블을 **삭제**하여 실질적 분리 +- MNG 모델에 `$connection = 'codebridge'`를 설정하여 읽기 대상 DB만 변경 +- React/API 서비스에는 **영향 없음** +- 개발 서버: samdb에서 59개 테이블 삭제 완료 (백업: `/home/pro/backup/sam_backup_20260309.sql.gz`) + +### 1.3 분리 기준 + +| 분류 | 대상 DB | 기준 | +|------|---------|------| +| React 서비스 테이블 | samdb (유지) | React 프론트엔드 또는 API에서 사용 | +| MNG 전용 테이블 | codebridge (이동) | MNG에서만 사용, React/API 미참조 | +| 공통 테이블 | samdb (유지) | 양쪽 모두 사용 (users, tenants 등) | +| **API 사용 테이블** | **samdb (유지 필수)** | **API에 모델/서비스/컨트롤러 존재 — 이동 시 데이터 불일치 발생** | + +> **경고: API에서 모델/서비스/컨트롤러로 참조하는 테이블을 codebridge로 이동하면, API는 samdb에 쓰고 MNG는 codebridge에서 읽게 되어 데이터 불일치가 발생한다. 절대 이동 금지.** + +--- + +## 2. codebridge 테이블 목록 (59개) + +> 2026-03-09 점검: API 프로젝트 전체 코드 조사를 통해 API에서 사용하는 22개 테이블을 제외함. 제외된 테이블은 [3절](#3-api-사용-테이블--samdb-유지-필수-22개) 참조. +> Equipment 하위 테이블 4개 추가 (FK 의존성으로 equipments와 동일 DB 필수). + +### Admin (9) + +| 테이블 | 설명 | +|--------|------| +| `admin_api_flows` | API 플로우 정의 | +| `admin_api_flow_runs` | API 플로우 실행 이력 | +| `admin_pm_daily_logs` | PM 일일 로그 | +| `admin_pm_daily_log_entries` | PM 일일 로그 항목 | +| `admin_pm_issues` | PM 이슈 | +| `admin_pm_projects` | PM 프로젝트 | +| `admin_pm_tasks` | PM 태스크 | +| `admin_roadmap_milestones` | 로드맵 마일스톤 | +| `admin_roadmap_plans` | 로드맵 계획 | + +### DevTools (5) + +| 테이블 | 설명 | 비고 | +|--------|------|------| +| `admin_api_bookmarks` | API 북마크 | 기존명 `api_bookmarks` | +| `admin_api_deprecations` | API 지원종료 관리 | 기존명 `api_deprecations` | +| `admin_api_environments` | API 환경 설정 | 기존명 `api_environments` | +| `admin_api_histories` | API 호출 이력 | 기존명 `api_histories` | +| `admin_api_templates` | API 템플릿 | 기존명 `api_templates` | + +### Sales (17) + +| 테이블 | 설명 | +|--------|------| +| `sales_partners` | 영업 파트너 | +| `sales_managers` | 영업 담당자 | +| `sales_manager_documents` | 영업 담당자 문서 | +| `sales_commissions` | 영업 수당 | +| `sales_commission_details` | 영업 수당 상세 | +| `sales_consultations` | 영업 상담 | +| `sales_contract_products` | 계약 제품 | +| `sales_products` | 영업 제품 | +| `sales_product_categories` | 영업 제품 카테고리 | +| `sales_prospects` | 영업 전망 | +| `sales_prospect_consultations` | 전망 상담 | +| `sales_prospect_products` | 전망 제품 | +| `sales_prospect_scenarios` | 전망 시나리오 | +| `sales_records` | 영업 실적 | +| `sales_scenario_checklists` | 시나리오 체크리스트 | +| `sales_tenant_managements` | 테넌트 영업 관리 | +| `tenant_prospects` | 테넌트 전망 | + +### Finance (9) + +| 테이블 | 설명 | +|--------|------| +| `condolence_expenses` | 경조사비 | +| `consulting_fees` | 컨설팅비 | +| `corporate_cards` | 법인카드 | +| `corporate_card_prepayments` | 법인카드 선결제 | +| `customer_settlements` | 고객 정산 | +| `daily_fund_memos` | 일일 자금 메모 | +| `daily_fund_transactions` | 일일 자금 거래 | +| `incomes` | 수입 | +| `vat_records` | 부가세 기록 | + +### ESign (2) + +| 테이블 | 설명 | +|--------|------| +| `esign_field_templates` | 전자서명 필드 템플릿 | +| `esign_field_template_items` | 전자서명 필드 항목 | + +> esign_contracts, esign_audit_logs, esign_sign_fields, esign_signers는 API에서 전자계약 기능으로 사용 중 → samdb 유지 + +### Equipment (6) + +| 테이블 | 설명 | +|--------|------| +| `equipments` | 설비 | +| `equipment_processes` | 설비 공정 | +| `equipment_inspections` | 설비 점검 (FK → equipments) | +| `equipment_inspection_details` | 설비 점검 상세 (FK → equipment_inspections) | +| `equipment_inspection_templates` | 설비 점검 템플릿 (FK → equipments) | +| `equipment_repairs` | 설비 수리 (FK → equipments) | + +> Equipment 하위 4개 테이블은 `equipments`에 FK 의존하므로 반드시 동일 DB에 있어야 한다. + +### HR (1) + +| 테이블 | 설명 | +|--------|------| +| `business_income_payments` | 사업소득 지급 | + +> income_tax_brackets는 API IncomeTaxBracketSeeder에서 초기 데이터 관리 → samdb 유지 + +### System (1) + +| 테이블 | 설명 | +|--------|------| +| `ai_configs` | AI 설정 | + +> ai_pricing_configs, ai_token_usages는 API 모델에서 직접 사용 → samdb 유지 + +### 기타 (9) + +| 테이블 | 설명 | 비고 | +|--------|------|------| +| `biz_cert` | 사업자등록증 | 문서 기존명 `biz_certs` → 실제 테이블명 (단수) | +| `cm_songs` | R&D 곡 관리 | | +| `construction_site_photos` | 시공 현장 사진 | | +| `construction_site_photo_rows` | 시공 사진 행 | | +| `admin_meeting_logs` | 회의 로그 | 문서 기존명 `meeting_logs` → 실제 테이블명 | +| `meeting_minutes` | 회의록 | | +| `meeting_minute_segments` | 회의록 세그먼트 | | +| `interview_knowledges` | 면접 지식 | | +| `sales_records` | 매출 기록 | | + +--- + +## 3. API 사용 테이블 — samdb 유지 필수 (22개) + +> **경고: 아래 테이블은 API 프로젝트에서 모델/서비스/컨트롤러/시더로 직접 참조한다. 절대 codebridge로 이동 금지.** +> +> **2026-03-09 점검**: sam/api 프로젝트 전체 코드 (모델, 컨트롤러, 서비스, 라우트, 시더) 조사 완료. + +### Barobill (12) — 전체 samdb 유지 + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `barobill_billing_records` | BarobillBillingService | 과금 기록 CRUD | +| `barobill_members` | BarobillUsageService | 회원사 사용량 집계 | +| `barobill_monthly_summaries` | BarobillBillingService | 월별 집계 갱신 | +| `barobill_pricing_policies` | BarobillUsageService | 과금 계산 | +| `bank_sync_statuses` | BankSyncStatus 모델 | 동기화 상태 추적 | +| `bank_transactions` | BankTransactionController | 은행 거래 조회/분개 | +| `bank_transaction_overrides` | BankTransactionOverride 모델 | 거래 재정의 | +| `bank_transaction_splits` | BankTransactionController | 은행 거래 분개 | +| `card_transaction_amount_logs` | CardTransactionAmountLog 모델 | 금액 수정 이력 + FK → card_transactions | +| `card_transaction_hides` | CardTransactionHide 모델 | 거래 숨김 + FK → card_transactions | +| `hometax_invoices` | BarobillUsageService | 세금계산서 사용량 집계 | +| `hometax_invoice_journals` | HometaxInvoiceJournal 모델 | 세금계산서 분개 + FK → hometax_invoices | + +> **핵심**: API의 BarobillController, BarobillSettingController, BarobillService, EntertainmentService가 바로빌 테이블을 직접 참조. `barobill_card_transactions` (samdb 유지)와 FK로 연결된 자식 테이블도 분리 불가. + +### ESign (4) — API 전자계약 기능 + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `esign_contracts` | EsignContractController, EsignService | 전자계약 CRUD | +| `esign_audit_logs` | EsignService | 감사 추적 기록 | +| `esign_sign_fields` | EsignService | 서명 위치 데이터 | +| `esign_signers` | EsignService | 서명자 정보/인증 | + +### Audit (2) — API 전사 감사 시스템 + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `audit_logs` | AuditLog 모델, AuditLogService, AuditRollbackService | 전사 DML 감사 | +| `trigger_audit_logs` | TriggerAuditLog 모델, TriggerAuditLogController, RegenerateAuditTriggers 명령 | DB 트리거 감사 + 파티셔닝 관리 | + +### DevTools (1) + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `api_request_logs` | ApiRequestLog 모델, SystemStatService | API 통계 집계 | + +### System (2) + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `ai_pricing_configs` | AiPricingConfig 모델 | AI 서비스 비용 계산 (캐시 기반) | +| `ai_token_usages` | AiTokenUsage 모델 | 멀티테넌트 AI 토큰 사용량 추적 | + +### HR (1) + +| 테이블 | API 사용처 | 사유 | +|--------|-----------|------| +| `income_tax_brackets` | IncomeTaxBracketSeeder | 소득세 구간 초기 데이터 관리 | + +--- + +## 4. 적용 현황 + +### 4.1 환경별 상태 + +| 환경 | codebridge DB | 테이블 복사 | samdb 삭제 | .env 설정 | MNG 코드 | 상태 | +|------|:---:|:---:|:---:|:---:|:---:|------| +| **로컬 Docker** | O | 100개 | **58개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 | +| **개발 서버** | O | 101개 | **63개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 | +| **운영 서버** | **X** | **X** | **X** | **X** | **revert됨** | ⚠️ DB 선행 작업 후 코드 재배포 필요 | + +> **2026-03-09 작업 내역**: +> - API 사용 테이블 22개: codebridge 이동 대상에서 제외 → samdb 유지 +> - `finance_*` 17개 + `barobill_companies` 1개: codebridge에 없는 유령 테이블 → samdb에서만 삭제 +> - Equipment 하위 4개 테이블: FK 의존성으로 codebridge 이동 대상에 추가 (55→59개) +> - **개발 서버 samdb에서 63개 테이블 DROP 완료** (59개 + DevTools 실제 테이블명 4개 추가분) +> - **로컬 samdb에서 58개 테이블 DROP 완료** → 로컬/개발 265개로 동기화 +> - 로컬에 `quality_documents` 등 4개 테이블 구조 동기화 (개발서버에서 복사) +> - 백업: `/home/pro/backup/sam_backup_20260309.sql.gz` (6.3MB) +> +> **테이블명 불일치 발견 (수정 완료)**: +> - `api_bookmarks` → 실제: `admin_api_bookmarks` +> - `meeting_logs` → 실제: `admin_meeting_logs` +> - `biz_certs` → 실제: `biz_cert` (단수형) +> - DevTools 4개: `api_deprecations` → `admin_api_deprecations`, `api_environments` → `admin_api_environments`, `api_histories` → `admin_api_histories`, `api_templates` → `admin_api_templates` +> +> **운영 서버 revert 사유 (2026-03-09)**: +> - MNG main에 codebridge 코드 2건 cherry-pick → Jenkins 배포됨 (빌드 #456, #457) +> - 운영 서버에 codebridge DB가 없는 상태에서 코드 배포 → **59개 모델 사용 페이지 오류 발생 위험** +> - kent가 main에서 revert 2건 push → 운영 서버 정상 복구 +> - **교훈: 운영 서버는 반드시 DB 선행 작업(1~2단계) 완료 후 코드 배포(3단계)** + +### 4.2 코드 변경 사항 + +**config/database.php** — `codebridge` connection 추가: + +```php +'codebridge' => [ + 'driver' => 'mysql', + 'host' => env('CODEBRIDGE_DB_HOST', env('DB_HOST', '127.0.0.1')), + 'port' => env('CODEBRIDGE_DB_PORT', env('DB_PORT', '3306')), + 'database' => env('CODEBRIDGE_DB_DATABASE', 'codebridge'), + 'username' => env('CODEBRIDGE_DB_USERNAME', env('DB_USERNAME')), + 'password' => env('CODEBRIDGE_DB_PASSWORD', env('DB_PASSWORD')), + // ... (mysql 기본 설정과 동일) +], +``` + +**.env** — 추가 설정: + +``` +CODEBRIDGE_DB_DATABASE=codebridge +``` + +**MNG 모델** — `$connection` 속성 추가 (codebridge 59개만): + +```php +class SalesPartner extends Model +{ + protected $connection = 'codebridge'; // 추가 + protected $table = 'sales_partners'; + // ... +} +``` + +> **주의**: API 사용 테이블 22개에 해당하는 MNG 모델은 `$connection = 'codebridge'`를 설정하지 않는다. 기본 samdb connection을 사용해야 API와 동일한 데이터를 참조한다. + +### 4.3 samdb 테이블 삭제 절차 (개발 서버 완료) + +> Sales 테이블 그룹은 FK 상호 참조가 있어 `FOREIGN_KEY_CHECKS = 0`으로 일괄 삭제. + +```sql +-- FK 체크 비활성화 (Sales, Equipment 등 FK 체인 테이블) +SET FOREIGN_KEY_CHECKS = 0; + +-- 59개 테이블 DROP (codebridge에 복제 완료 확인 후) +DROP TABLE IF EXISTS admin_api_flows, admin_api_flow_runs, ...; + +SET FOREIGN_KEY_CHECKS = 1; +``` + +> **롤백**: 백업에서 특정 테이블만 복원 가능 +> ```bash +> gunzip < /home/pro/backup/sam_backup_20260309.sql.gz | mysql -u codebridge -p sam +> ``` + +--- + +## 5. 운영 서버 적용 절차 (미완료) + +> **전제**: 운영 서버 SSH 접근 + DB root 권한 필요 +> **현재 상태**: 운영 서버 main 코드는 revert 상태 (codebridge 코드 없음). DB 작업 완료 후 코드 재배포 필요. +> **⚠️ 교훈**: 2026-03-09에 DB 없이 코드만 배포하여 장애 위험 발생 → **반드시 DB 선행 후 코드 배포** + +### 순서 (반드시 1 → 2 → 3 → 4 → 5 순서로) + +**1단계: 운영 sam DB 백업** + +```bash +# 운영 서버 접속 후 +mysqldump -u codebridge -p'[운영PW]' sam --single-transaction > ~/backup/sam_backup_$(date +%Y%m%d).sql +gzip ~/backup/sam_backup_$(date +%Y%m%d).sql +``` + +**2단계: codebridge DB 생성 + 59개 테이블 복사** + +```bash +# DB 생성 +mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + +# DB 계정 권한 부여 +mysql -u root -p -e "GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; FLUSH PRIVILEGES;" + +# sam에서 59개 테이블 구조+데이터 복사 +mysqldump -u codebridge -p sam \ + admin_api_flows admin_api_flow_runs \ + admin_pm_daily_logs admin_pm_daily_log_entries admin_pm_issues admin_pm_projects admin_pm_tasks \ + admin_roadmap_milestones admin_roadmap_plans \ + admin_api_bookmarks admin_api_deprecations admin_api_environments admin_api_histories admin_api_templates \ + sales_partners sales_managers sales_manager_documents sales_commissions sales_commission_details \ + sales_consultations sales_contract_products sales_products sales_product_categories \ + sales_prospects sales_prospect_consultations sales_prospect_products sales_prospect_scenarios \ + sales_records sales_scenario_checklists sales_tenant_managements tenant_prospects \ + condolence_expenses consulting_fees corporate_cards corporate_card_prepayments \ + customer_settlements daily_fund_memos daily_fund_transactions incomes vat_records \ + esign_field_templates esign_field_template_items \ + equipments equipment_process equipment_inspections equipment_inspection_details \ + equipment_inspection_templates equipment_repairs \ + business_income_payments ai_configs \ + biz_cert cm_songs construction_site_photos construction_site_photo_rows \ + admin_meeting_logs meeting_minutes meeting_minute_segments \ + interview_knowledge \ + | mysql -u codebridge -p codebridge +``` + +**3단계: .env 설정** + +```bash +echo 'CODEBRIDGE_DB_DATABASE=codebridge' >> /home/webservice/mng/.env +cd /home/webservice/mng && php artisan config:clear +``` + +**4단계: MNG 코드 재배포 (main cherry-pick)** + +> develop에 codebridge 코드가 있으므로, revert 커밋 이후 develop의 최신 커밋을 cherry-pick. +> 또는 develop의 해당 커밋을 다시 cherry-pick하여 main에 push. + +```bash +# 로컬에서 실행 +cd /home/aweso/sam/mng +git checkout main && git pull origin main +git cherry-pick +git push origin main +git checkout develop +``` + +**5단계: 동작 확인 + samdb 테이블 삭제 (선택)** + +MNG 관리자 페이지에서 영업관리, 설비, 재무 등 주요 메뉴 동작 확인 후, 문제없으면 sam DB에서 59개 테이블 삭제. + +```sql +SET FOREIGN_KEY_CHECKS = 0; +DROP TABLE IF EXISTS + admin_api_flows, admin_api_flow_runs, + admin_pm_daily_logs, admin_pm_daily_log_entries, admin_pm_issues, admin_pm_projects, admin_pm_tasks, + admin_roadmap_milestones, admin_roadmap_plans, + admin_api_bookmarks, admin_api_deprecations, admin_api_environments, admin_api_histories, admin_api_templates, + sales_partners, sales_managers, sales_manager_documents, sales_commissions, sales_commission_details, + sales_consultations, sales_contract_products, sales_products, sales_product_categories, + sales_prospects, sales_prospect_consultations, sales_prospect_products, sales_prospect_scenarios, + sales_records, sales_scenario_checklists, sales_tenant_managements, tenant_prospects, + condolence_expenses, consulting_fees, corporate_cards, corporate_card_prepayments, + customer_settlements, daily_fund_memos, daily_fund_transactions, incomes, vat_records, + esign_field_templates, esign_field_template_items, + equipments, equipment_process, equipment_inspections, equipment_inspection_details, + equipment_inspection_templates, equipment_repairs, + business_income_payments, ai_configs, + biz_cert, cm_songs, construction_site_photos, construction_site_photo_rows, + admin_meeting_logs, meeting_minutes, meeting_minute_segments, + interview_knowledge; +SET FOREIGN_KEY_CHECKS = 1; +``` + +> **⚠️ 핵심 주의사항**: +> - 반드시 **1→2→3→4** 순서 (DB 먼저, 코드 나중) +> - 4단계(코드 배포) 전에 3단계(.env)까지 완료되어야 함 +> - 5단계(samdb 삭제)는 4단계 동작 확인 후 선택적 수행 + +--- + +## 6. 아키텍처 다이어그램 + +``` + React (사용자) + | + API 서버 (Laravel) + | + ┌─────┴─────┐ + | | + samdb sam_stat + (서비스 DB) (통계 DB) + | + | (공통 + API 사용 테이블: users, tenants, barobill_*, esign_*, audit_* 등) + | + MNG (관리자) + | + ┌─────┴─────┐ + | | + samdb codebridge + (공통 참조) (MNG 전용 59개) +``` + +- **React → API → samdb**: 서비스 트래픽 (수주, 견적, 생산, 바로빌, 전자서명 등) +- **MNG → samdb**: 공통 테이블 (users, tenants, menus) + API 사용 테이블 22개 참조 +- **MNG → codebridge**: MNG 전용 데이터 (영업관리, 재무, 설비, PM 도구 등) + +--- + +## 관련 문서 + +- [database/README.md](README.md) — DB 스키마 전체 현황 +- [codebridge-db-separation-plan.md](/home/aweso/sam/docs/plans/codebridge-db-separation-plan.md) — 분리 작업 계획서 (plans/) + +--- + +**최종 업데이트**: 2026-03-09 (운영 revert 반영, 적용 절차 5단계로 개정) From c143c7e9f8eb81528fbf5299438e24fe3cf02e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Mar 2026 23:02:30 +0900 Subject: [PATCH 12/15] =?UTF-8?q?chore:=20CLAUDE.md=EB=A5=BC=20git=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=EC=97=90=EC=84=9C=20=EC=A0=9C=EC=99=B8=20(?= =?UTF-8?q?=EB=A1=9C=EC=BB=AC=20=EC=A0=84=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - CLAUDE.md | 1059 ---------------------------------------------------- 2 files changed, 1060 deletions(-) delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index bbbc6b9..b1caba9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ # 추적할 파일만 허용 !.gitignore -!CLAUDE.md !INDEX.md !README.md !resources.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 603aea0..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,1059 +0,0 @@ -# Claude Code 전역 설정 - -> 이 파일은 모든 프로젝트에 적용되는 전역 규칙입니다. - -## 메모리 - -### sam설명 -SAM 프로젝트의 기술적 개요 문서입니다. 이 문서를 참조하면 SAM 프로젝트가 무엇인지 이해할 수 있습니다. - -**파일 경로**: `/home/aweso/sam/docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` - -**핵심 요약**: -- **회사**: 주일/경동 (블라인드/스크린 제조업체) -- **프로젝트**: SAM (Smart Automation Management) - 차세대 ERP/MES 통합 시스템 -- **기술 스택**: Laravel 12 + HTMX + Tailwind CSS + MySQL 8.0 (PHP 8.4) -- **아키텍처**: 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로 자동 제거됨 -- 간결하고 명확한 한글 커밋 메시지만 유지 - -### 푸시 정책 - -#### MNG 자동 푸시 (커밋 즉시 배포) - -> **MNG 프로젝트는 커밋 후 자동으로 개발+운영 서버에 배포한다.** 트리거 워드 불필요. - -**커밋 완료 후 자동 실행 절차:** - -1. `git push origin develop` (개발 서버 배포) -2. `git checkout main && git pull origin main` (최신화) -3. `git cherry-pick <방금_커밋_해시>` (운영 반영) -4. `git push origin main` (운영 서버 배포) -5. `git checkout develop` (작업 브랜치 복귀) - -```bash -# MNG 커밋 후 자동 실행 예시 -cd /home/aweso/sam/mng -git push origin develop -git checkout main && git pull origin main -git cherry-pick abc1234 -git push origin main -git checkout develop -``` - -> **충돌 발생 시**: 사용자에게 알리고 해결 지원 (자동 중단하지 않음) -> **여러 커밋 연속 시**: 마지막 커밋 후 한 번만 실행 (중간 커밋에서는 생략 가능) - -#### API / React 푸시 (트리거 워드 기반) - -> API, React는 기존대로 **사용자가 트리거 워드를 말할 때만** 푸시 실행. - -#### 트리거: "개발서버 푸시" - -사용자가 **"개발서버 푸시"**라고 말하면 다음을 자동 실행: - -1. 각 프로젝트(`api`, `mng`, `react`)에서 `git status` 확인 -2. 커밋되지 않은 변경사항이 있으면 커밋 규칙에 따라 커밋 -3. 현재 브랜치가 `develop`인지 확인 (아니면 `develop`으로 전환) -4. `git push origin develop` 실행 (변경사항 있는 프로젝트만) -5. 결과 요약 출력 - -```bash -# 변경사항 있는 프로젝트만 실행 -cd /home/aweso/sam/api && git push origin develop -cd /home/aweso/sam/mng && git push origin develop -cd /home/aweso/sam/react && git push origin develop -``` - -> Jenkins가 Gitea Webhook으로 개발 서버에 자동 배포한다. - -#### 트리거: "운영서버 푸시" - -사용자가 **"운영서버 푸시"**라고 말하면 다음을 자동 실행: - -> **전제**: develop의 변경사항이 개발 서버에서 테스트 완료된 상태 -> **방식**: **cherry-pick** — 이번 세션에서 작업한 커밋만 골라서 main에 반영 (develop 전체 머지 금지) - -1. 각 프로젝트에서 develop에 미푸시 커밋 확인 → 있으면 먼저 `git push origin develop` -2. **체리픽 대상 커밋 식별** — 이번 세션에서 작업한 커밋 해시 목록을 `git log --oneline`으로 확인하여 사용자에게 표시 -3. 사용자에게 체리픽 대상 커밋 목록을 보여주고 **확인** 받기 -4. `git checkout main` (모든 프로젝트 운영 브랜치: `main`) -5. `git pull origin main` — **최신화 (충돌 방지 핵심 단계)** -6. `git cherry-pick ...` — 확인받은 커밋만 순서대로 적용 -7. 충돌 발생 시 → 사용자에게 알리고 해결 지원 -8. `git push origin main` — Jenkins가 운영 서버에 자동 배포 -9. `git checkout develop` — 작업 브랜치로 복귀 -10. 결과 요약 출력 (체리픽한 커밋 목록 포함) - -```bash -# 예시: mng에서 이번 세션 커밋 2개만 체리픽 -cd /home/aweso/sam/mng - -# 1. develop 미푸시 확인 -git push origin develop - -# 2. 체리픽 대상 확인 (사용자 승인) -git log --oneline # → abc1234, def5678 - -# 3. main 전환 및 최신화 -git checkout main && git pull origin main - -# 4. 선택한 커밋만 체리픽 (시간순 — 오래된 것부터) -git cherry-pick abc1234 def5678 - -# 5. 푸시 및 복귀 -git push origin main -git checkout develop -``` - -> **merge와의 차이**: `git merge develop`은 develop의 모든 커밋을 main에 반영하지만, `git cherry-pick`은 지정한 커밋만 반영한다. 다른 개발자의 미검증 커밋이 운영에 올라가는 문제를 방지한다. -> **주의**: cherry-pick 후 develop에 main을 머지하지 않는다. 동일 변경이 이미 develop에 존재하므로 불필요하다. - -#### 운영서버 푸시 대상 프로젝트 (2026-02-27~) - -| 프로젝트 | 개발서버 푸시 | 운영서버 푸시 | 비고 | -|----------|:----------:|:----------:|------| -| **MNG** | ✅ | ✅ | | -| **API** | ✅ | ✅ | 2026-02-27부터 허용 | -| **React** | ✅ | ❌ 중지 | 개발 완료 시점에 배포 예정 | - -> "운영서버 푸시" 트리거 시 **MNG + API**만 cherry-pick → main push 실행. React는 develop push만 수행. - -#### 푸시 대상 자동 판별 - -| 조건 | 동작 | -|------|------| -| 현재 작업 디렉토리가 특정 프로젝트 | 해당 프로젝트만 푸시 | -| `sam/` 루트이거나 명시 없음 | 3개 프로젝트 모두 확인 후 변경사항 있는 것만 푸시 | -| 사용자가 프로젝트 지정 ("api 개발서버 푸시") | 지정된 프로젝트만 푸시 | - -### 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 명령을 실행해야 합니다.** - -> **서버 경로 참고**: -> - 개발/운영 서버 모두 `/home/webservice/` 하위에 동일한 구조로 배치 -> - 서버: `/home/webservice/api`, `/home/webservice/mng`, `/home/webservice/react`, `/home/webservice/sales` - ---- - -## 서버 접근 정책 (3단계 분류) - -> **2026-02-21 사고 교훈**: Claude가 서버에 SSH로 접속하여 **설정을 변경**한 결과 502 Bad Gateway 발생. -> → 교훈: **읽기는 허용, 변경은 확인 후, 위험 작업은 금지** - -### 핵심 원칙 - -서버 접근을 **3단계**로 분류하여 관리한다: - -| 단계 | 분류 | 정책 | -|------|------|------| -| 🟢 Level 1 | 읽기 전용 (진단/조회) | **자유 허용** — 사용자 요청 시 즉시 실행 | -| 🟡 Level 2 | 경미한 변경 | **사용자 확인 후 실행** — 실행 전 명령어를 보여주고 승인받기 | -| 🔴 Level 3 | 위험한 변경 | **절대 금지** — 안내만 제공 | - -### 🟢 Level 1: 읽기 전용 (자유 허용) - -사용자가 서버 진단/조회를 요청하면 **즉시 SSH 접속하여 실행** 가능: - -``` -✅ ls, cat, head, tail, less — 파일/디렉토리 조회 -✅ ps aux, top, htop — 프로세스 확인 -✅ df -h, free -m — 디스크/메모리 확인 -✅ git status, git log, git diff — Git 상태 조회 -✅ stat, id, whoami — 권한/소유자 확인 -✅ nginx -t, php -v — 설정 검증 (변경 아닌 테스트만) -✅ tail -f /var/log/*.log — 로그 조회 -✅ systemctl status — 서비스 상태 조회 -✅ crontab -l — 크론 작업 조회 -``` - -### 🟡 Level 2: 경미한 변경 (확인 후 실행) - -실행 전 **명령어를 사용자에게 보여주고 승인**받은 후 실행: - -``` -⚠️ git pull — 코드 업데이트 -⚠️ composer install — 의존성 설치 -⚠️ php artisan migrate — DB 마이그레이션 -⚠️ php artisan config:clear, cache:clear — 캐시 초기화 -⚠️ chmod, chown — 파일 권한 변경 (소규모) -⚠️ systemctl restart — 서비스 재시작 -⚠️ npm install — 패키지 설치 -``` - -**실행 절차:** -1. 실행할 명령어를 먼저 사용자에게 표시 -2. 사용자 승인 확인 -3. 승인 후 실행 - -### 🔴 Level 3: 위험한 변경 (절대 금지 — 안내만) - -Claude가 **절대 직접 실행하지 않으며**, 사용자에게 명령어를 안내만 제공: - -``` -❌ rm -rf, 대량 파일 삭제 -❌ nginx/apache 설정 파일 직접 수정 -❌ /etc/ 하위 시스템 설정 변경 -❌ 서버에서 npm run build (메모리 부족 위험) -❌ DB 직접 수정 (DROP, TRUNCATE, 대량 UPDATE/DELETE) -❌ 방화벽(iptables, ufw) 규칙 변경 -❌ 사용자 계정 생성/삭제/비밀번호 변경 -❌ scp, rsync로 대량 파일 전송 -❌ 프로세스 강제 종료 (kill -9) -❌ 서버 재부팅 -``` - -### 서버 접속 정보 - -| 서버 | 호스트 | 계정 | SSH 접근 | 역할 | -|------|--------|------|---------|------| -| 개발 서버 (sam-dev) | `114.203.209.83` | `pro`, `hskwon` | **Claude 가능** | 개발/스테이징 + Gitea | -| 운영 서버 | (비공개) | 별도 계정 | **Claude 불가** — 개발팀장만 접근 | 정식 서비스 | - -> **참고**: Jenkins는 CI/CD 서버(`110.10.147.46:8080`, ci.sam.it.kr)에서 운영한다. Gitea는 개발 서버(`114.203.209.83:3000`)와 CI/CD 서버(`110.10.147.46:3000`, git.sam.it.kr) 양쪽에 있다. - -> **운영 서버 정책**: Claude는 운영 서버에 SSH 접속할 수 없다. IP 접근이 제한되어 있으며 개발팀장만 접근 가능하다. 운영 배포는 `git push origin main` → Jenkins 자동 배포로만 이루어지며, 운영 서버 상태 확인이 필요하면 사용자(개발팀장)에게 요청한다. - -### 배포 흐름 (Jenkins CI/CD) - -``` -Claude 역할 Jenkins (자동) 운영 서버 -┌───────────────────┐ ┌──────────────────┐ ┌──────────────┐ -│ 코드 작성/수정 │ │ │ │ │ -│ git add / commit │ │ │ │ │ -│ │─push──→ │ Gitea Webhook │ │ │ -│ │(사용자) │ → Jenkins 빌드 │ │ │ -│ │ │ → Lint/Test │ │ │ -│ │ │ → SSH Deploy ────│──→ │ git pull │ -│ │ │ │ │ composer │ -│ 서버 진단 (L1) │ │ │ │ migrate │ -│ 서버 변경 (L2) │─확인──→ │ │ │ │ -│ 위험 작업 (L3) │─안내──→ │ │ │ (팀장 직접) │ -└───────────────────┘ └──────────────────┘ └──────────────┘ -``` - -> **브랜치 전략**: `develop` → 개발 서버 (자동 배포), `main` → 운영 서버 (PR 머지 + 팀장 승인) - -### 체크리스트 (서버 작업 시) - -- [ ] Level 1 (읽기): 사용자 요청 시 즉시 실행 -- [ ] Level 2 (변경): 명령어를 보여주고 승인 후 실행 -- [ ] Level 3 (위험): 절대 직접 실행하지 않고 안내만 제공 -- [ ] 사고 방지: 설정 파일 수정, 서비스 중단 가능성 있는 작업은 Level 3 - ---- - -## React 빌드/배포 정책 (필수 규칙) - -> **경고: React(Next.js) 빌드를 운영 서버에서 직접 실행하지 않는다!** - -### 배경 - -개발 서버(2코어, 3.8GB RAM + Swap 4GB)에서 Jenkins가 React 빌드를 수행한다. -Jenkins 빌드 실패 시 로컬(WSL)에서 빌드 후 결과물을 서버에 배포한다(fallback). - -### 금지 사항 - -``` -❌ 운영 서버에서 npm run build 실행 금지 -❌ 서버 SSH 접속 후 빌드 명령 실행 금지 -❌ Claude가 직접 npm run build 실행 금지 (로컬 포함) -``` - -### 빌드/배포 방법 (Jenkins 자동화) - -``` -Claude 역할 Jenkins (자동) 운영 서버 -┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐ -│ 코드 작성/수정 │ │ Checkout │ │ │ -│ git commit │─push──→ │ Install + Lint │ │ │ -│ │(사용자) │ Build (Next.js) │ │ │ -│ │ │ Package (tar.gz) │──scp→ │ 압축 해제 │ -│ │ │ │ │ node 재시작 │ -└─────────────────┘ └──────────────────┘ └──────────────┘ -``` - -> **Fallback**: Jenkins 빌드 실패(OOM) 시 로컬에서 `react/deploy.sh`로 수동 배포 - -### 빌드가 필요한 상황 - -사용자에게 다음과 같이 안내한다: - -``` -React 코드가 변경되었습니다. git push 후 Jenkins가 자동 배포합니다. -(Jenkins 실패 시 로컬에서 deploy.sh로 수동 배포해주세요.) -``` - ---- - -## 데이터베이스 아키텍처 (필수 규칙) - -> **경고: 이 규칙을 반드시 준수하세요!** - -### 핵심 원칙 - -**모든 데이터베이스 관련 파일은 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 - -# 개발 서버: Docker 없음, 직접 실행 -cd /home/webservice/api && php artisan migrate - -# 운영 서버: --force 플래그 필수 (production 환경) -cd /home/webservice/api && php artisan migrate --force - -# MNG에서 마이그레이션 실행 금지 (로컬/서버 모두) -``` - -### DB 환경 분리 - -| 환경 | DB명 | 호스트 | 용도 | -|------|------|--------|------| -| 로컬 (Docker) | `samdb` | `sam-mysql-1:3306` | 개발/테스트 | -| 개발 서버 | `samdb` | `localhost` | 스테이징 | -| 운영 서버 | `sam_prod` | `localhost` | 정식 서비스 | -| 통계 DB | `sam_stat` | 동일 서버 | StatMonitorService 전용 | - -> **참고**: `sam_stat`은 API/MNG 모두 `config/database.php`의 별도 connection으로 접속한다. - -### 이유 - -- MNG: 프론트엔드/관리자 화면 담당 (컨트롤러, 뷰, 라우트) -- API: 백엔드/데이터베이스 담당 (마이그레이션, 모델 정의, API) -- 단일 DB를 두 프로젝트가 공유하므로 마이그레이션은 한 곳에서만 관리 - -### 테이블 생성/수정 시 options JSON 컬럼 정책 (필수) - -> **경고: 테이블 생성/수정, 마이그레이션 작성, 모델 생성 시 반드시 아래 정책 문서를 먼저 읽고 준수하세요!** -> **정책 문서**: `/home/aweso/sam/docs/standards/options-column-policy.md` - -**핵심 원칙**: FK/조인키만 전용 컬럼, 나머지 속성은 `options` JSON에 저장 - -| 전용 컬럼 (일반 컬럼) | options JSON | -|---|---| -| FK/조인키 (다른 테이블 참조) | 테넌트별로 다를 수 있는 확장 속성 | -| WHERE/ORDER BY 자주 사용 필드 | 선택적(nullable) 부가 정보 | -| UNIQUE 제약 필드 | 구조가 유동적인 데이터 | -| INDEX 필요한 고빈도 조회 필드 | 드롭다운 선택지 목록 | -| 집계(SUM/AVG) 대상 | 이력/스냅샷성 데이터 | - -**필수 준수 사항**: - -``` -✅ 모든 비즈니스 테이블에 $table->json('options')->nullable() 포함 -✅ 모델 cast: 'options' => 'array' (❌ 'json' 금지) -✅ 모델에 getOption() / setOption() 헬퍼 메서드 추가 -✅ options 키 3개 이상 시 OPTION_* 상수 정의 -✅ options 수정 시 setOption() 사용 (전체 덮어쓰기 금지) -✅ API 응답 노출 시 accessor + $appends 추가 -``` - -**작업 전 체크리스트**: -- [ ] 정책 문서(`docs/standards/options-column-policy.md`) 읽기 -- [ ] 새 필드가 전용 컬럼인지 options인지 판단 (판단 흐름도 참고) -- [ ] 마이그레이션에 `options` JSON 컬럼 포함 확인 -- [ ] 모델에 `'options' => 'array'` cast + 헬퍼 메서드 포함 확인 - ---- - -## 메뉴 관리 규칙 (필수) - -> **경고: 메뉴 시더(Seeder)를 절대 실행하지 마세요!** - -### 배경 - -메뉴 시더 실행 시 부서별 권한 설정(permission_overrides)이 초기화되는 문제가 반복 발생합니다. -메뉴 ID가 변경되면 기존 부서-메뉴 권한 매핑이 깨지기 때문입니다. - -### 금지 사항 - -``` -❌ php artisan db:seed --class=MngMenuSeeder 실행 금지 -❌ php artisan db:seed --class=*MenuSeeder 실행 금지 -❌ 메뉴 시더 파일 생성 금지 -❌ 메뉴 데이터를 일괄 삭제 후 재생성하는 방식 금지 -``` - -### 메뉴 변경 시 올바른 절차 - -메뉴 추가/수정/삭제/이동이 필요할 때는 **사용자에게 수동 실행 안내**를 제공합니다: - -1. **tinker 명령어를 안내** (사용자가 직접 실행) -2. **또는 SQL 쿼리를 안내** (사용자가 phpMyAdmin 등에서 직접 실행) -3. **절대 시더를 만들어 실행하지 않음** - -### 안내 예시 - -``` -메뉴를 추가하려면 아래 명령을 서버에서 실행해 주세요: - -# 개발 서버 -ssh pro@114.203.209.83 "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, -]); -\"" - -# 운영 서버 (동일 경로, 서버 주소만 변경) -ssh <운영계정>@<운영서버IP> "cd /home/webservice/mng && php artisan tinker --execute=\"...동일...\"" -``` - -### 체크리스트 (메뉴 변경 요청 시) - -- [ ] 시더 파일 생성하지 않음 -- [ ] 시더 실행하지 않음 -- [ ] tinker 또는 SQL로 개별 레코드만 수정 -- [ ] 변경 후 부서 권한 설정이 유지되는지 확인 - ---- - -## 실행 환경 (필수 인지) - -> **중요: 로컬 / 개발 서버 / 운영 서버의 환경이 다릅니다!** - -### 환경 비교 (3-Tier) - -| 항목 | 로컬 (WSL) | 개발 서버 | 운영 서버 | -|------|-----------|----------|----------| -| **구성 방식** | Docker 컨테이너 | Bare-metal (네이티브) | Bare-metal (네이티브) | -| **PHP** | 컨테이너 내부 (8.4) | 직접 설치 (8.4) | 직접 설치 (8.4) | -| **MySQL** | 컨테이너 (sam-mysql-1) | 직접 설치 (8.4) | 직접 설치 (8.4) | -| **Nginx** | 컨테이너 (sam-nginx-1) | 직접 설치 | 직접 설치 | -| **명령 실행** | `docker exec` 필요 | 직접 실행 | 직접 실행 | -| **서버 IP** | localhost | `114.203.209.83` | (신규, 미확정) | -| **추가 서비스** | — | Gitea | — | -| **DB** | `samdb` | `samdb` | `sam_prod` | - -> **배경**: 서버는 Docker가 무거워서 PHP, Nginx, MySQL 등을 네이티브로 설치하여 운영한다. - -### 로컬 환경 (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 웹서버 -``` - -### 서버 환경 (Bare-metal — 개발/운영 동일 구조) - -서버에는 Docker가 없다. PHP 8.4, Nginx, MySQL 8.4이 직접 설치되어 있다. - -``` -개발 서버 (114.203.209.83) 운영 서버 (신규) -├── Nginx ├── Nginx -├── PHP-FPM (3 pools) ├── PHP-FPM (3 pools) -│ ├── api.sock │ ├── api.sock -│ ├── mng.sock │ ├── mng.sock -│ └── sales.sock │ └── sales.sock -├── MySQL 8.4 (samdb) ├── MySQL 8.4 (sam_prod) -├── Supervisor ├── Supervisor -│ ├── sam-api-worker (x1) │ ├── sam-api-worker (x1) -│ ├── sam-mng-worker (x2) │ ├── sam-mng-worker (x2) -│ └── sam-api-scheduler │ └── sam-api-scheduler -├── Node.js (React SSR :3001) ├── Node.js (React SSR :3000) -├── Gitea (:3000) │ -├── /home/webservice/api ├── /home/webservice/api -├── /home/webservice/mng ├── /home/webservice/mng -├── /home/webservice/react ├── /home/webservice/react -└── /home/webservice/sales └── /home/webservice/sales -``` - -### 도메인 매핑 - -| 서비스 | 로컬 (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` | `admin.codebridge-x.com` | `mng.codebridge-x.com` | -| Sales | `sales.sam.kr` | `sales.dev.codebridge-x.com` | `sales.codebridge-x.com` | -| 5130 (레거시) | `5130.sam.kr` | — | — | - -### 명령어 비교 (로컬 vs 개발 vs 운영) - -| 작업 | 로컬 (Docker) | 개발/운영 서버 (네이티브) | -|------|--------------|-------------------------| -| artisan 실행 | `docker exec sam-api-1 php artisan <명령>` | `cd /home/webservice/api && php artisan <명령>` | -| composer 실행 | `docker exec sam-api-1 composer install` | `cd /home/webservice/api && composer install` | -| 마이그레이션 | `docker exec sam-api-1 php artisan migrate` | 개발: `php artisan migrate` / 운영: `php artisan migrate --force` | -| 캐시 클리어 | `docker exec sam-mng-1 php artisan cache:clear` | `cd /home/webservice/mng && php artisan cache:clear` | -| Queue 재시작 | — | `sudo supervisorctl restart sam-api-worker:*` | - -### 로컬 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` 사용 -- [ ] **서버**: `php artisan`, `composer` 직접 실행 (Docker 없음) -- [ ] **운영 서버 마이그레이션**: `--force` 플래그 필수 -- [ ] **마이그레이션은 반드시 API에서 실행** (로컬: `docker exec sam-api-1`, 서버: 직접) - ---- - -## 공동 개발 워크플로우 (필수) - -> **중요: 코드를 pull 받은 후 반드시 필요한 명령을 실행하세요!** - -### 브랜치 전략 - -| 브랜치 | 배포 대상 | 트리거 워드 | Jenkins 배포 | -|--------|----------|------------|-------------| -| `feature/*` | — | — | — | -| `develop` | 개발 서버 (`dev.codebridge-x.com`) | **"개발서버 푸시"** | Push 시 자동 | -| `main` | 운영 서버 (`codebridge-x.com`) | **"운영서버 푸시"** | Push 시 자동 | - -``` -로컬 작업 → develop push → 개발 서버 테스트 → main merge+push → 운영 서버 - ("개발서버 푸시") ("운영서버 푸시") -``` - -### 브랜치 동기화 규칙 (필수) - -> **cherry-pick 방식에서는 develop→main 자동 동기화가 불필요하다.** -> main에 직접 변경이 발생한 경우에만 develop에 동기화한다. - -| 상황 | 조치 | -|------|------| -| "운영서버 푸시" 완료 후 | 동기화 불필요 (cherry-pick은 develop에 이미 존재하는 커밋) | -| main에 **직접** 변경 발생 시 (hotfix 등) | develop에서 `git merge main` 실행 | -| develop에서 새 작업 시작 전 | `git pull origin develop`으로 최신화 확인 | - -``` -develop ──작업──→ develop push (개발서버) - │ - └── cherry-pick 선택 커밋 ──→ main push (운영서버) -``` - -### 로컬 환경 (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 -``` - -### 개발 서버 업데이트 (자동) - -> `develop` 브랜치에 Push 시 Gitea Webhook → Jenkins가 자동으로 배포한다. -> 수동 배포가 필요한 경우: - -```bash -# API 프로젝트 -cd /home/webservice/api -git pull origin develop -composer install -php artisan migrate -php artisan config:clear - -# MNG 프로젝트 (마이그레이션 없음) -cd /home/webservice/mng -git pull origin develop -composer install -php artisan config:clear -``` - -### 운영 서버 배포 (Jenkins 자동화) - -> `main` 브랜치에 PR 머지 시 Jenkins가 자동으로 배포한다. -> 수동 배포는 **비상 절차**로만 사용한다. - -```bash -# 비상 수동 배포 (Jenkins 장애 시에만) -# API 프로젝트 -cd /home/webservice/api -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:* - -# MNG 프로젝트 -cd /home/webservice/mng -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:* -``` - -### 요약 표 - -| 작업 | 로컬 (Docker) | 개발 서버 | 운영 서버 | -|------|--------------|----------|----------| -| 배포 방식 | 수동 | Jenkins 자동 (develop push) | Jenkins 자동 (main push) | -| git pull | WSL에서 직접 | Jenkins 자동 | Jenkins 자동 | -| composer install | `docker exec sam-api-1 composer install` | Jenkins 자동 | `--no-dev --optimize-autoloader` | -| migrate | `docker exec sam-api-1 php artisan migrate` | Jenkins 자동 | `--force` 플래그 포함 | -| config:clear | `docker exec sam-api-1 php artisan config:clear` | Jenkins 자동 | `route:cache` + `view:cache` 포함 | - -### 체크리스트 (pull 후) - -- [ ] API: `git pull` → `composer install` → `php artisan migrate` → `config:clear` -- [ ] MNG: `git pull` → `composer install` → `config:clear` (마이그레이션 없음) -- [ ] 운영 배포: `main`에 PR 머지 → Jenkins 자동 처리 (수동 금지) - ---- - -## 사용 가능한 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 | 설명 | -|-------|------| -| `svg-precision` | SVG 결정론적 생성, 검증(validate), PNG 렌더링. 다이어그램/차트/아이콘에 사용 | -| `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 ee1aaf183de326628bf02372e71c0c6681b4417f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Mar 2026 23:06:34 +0900 Subject: [PATCH 13/15] =?UTF-8?q?chore:=20.claude=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=EB=A5=BC=20git=20=EC=B6=94=EC=A0=81=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20(=EB=A1=9C=EC=BB=AC=20=EC=A0=84=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/agents/code-reviewer.md | 58 - .claude/agents/debugger.md | 45 - .claude/agents/doc-writer.md | 44 - .claude/agents/git-manager.md | 54 - .claude/agents/laravel-expert.md | 62 - .claude/agents/organizer-agent.md | 497 ------- .claude/agents/performance-optimizer.md | 45 - .claude/agents/proposal-agent.md | 394 ------ .claude/agents/refactoring-agent.md | 48 - .claude/agents/research-agent.md | 80 -- .claude/agents/security-auditor.md | 58 - .claude/agents/test-runner.md | 45 - .../app-comprehensive-test-generator/SKILL.md | 145 -- .../skills/async-await-keyword-fixer/SKILL.md | 122 -- .claude/skills/build-auditor/SKILL.md | 181 --- .claude/skills/code-bug-finder/SKILL.md | 88 -- .claude/skills/code-commenter/SKILL.md | 169 --- .../code-flow-web-doc-generator/SKILL.md | 124 -- .claude/skills/code-flow-web-report/SKILL.md | 179 --- .../skills/code-principles-auditor/SKILL.md | 422 ------ .claude/skills/code-quality-auditor/SKILL.md | 302 ----- .claude/skills/code-quality-checker/SKILL.md | 207 --- .claude/skills/code-refactoring/SKILL.md | 111 -- .../codebase-analysis-web-report/SKILL.md | 70 - .claude/skills/concurrency-auditor/SKILL.md | 194 --- .../coverage-improvement-planner/SKILL.md | 96 -- .claude/skills/dead-code-auditor/SKILL.md | 142 -- .claude/skills/dependencies-auditor/SKILL.md | 193 --- .claude/skills/design-skill/SKILL.md | 949 ------------- .claude/skills/differential-review/SKILL.md | 220 --- .../skills/duplicate-file-cleaner/SKILL.md | 154 --- .claude/skills/flutter-ux-hardening/SKILL.md | 165 --- .claude/skills/frontend-design/SKILL.md | 42 - .claude/skills/insecure-defaults/SKILL.md | 117 -- .../skills/layer-boundary-auditor/SKILL.md | 323 ----- .../node-debug-logging-middleware/SKILL.md | 223 --- .claude/skills/npm-release-manager/SKILL.md | 129 -- .claude/skills/observability-auditor/SKILL.md | 134 -- .claude/skills/pdf-template-skill/SKILL.md | 211 --- .../scripts/analyze-template.js | 572 -------- .../scripts/generate-from-template.js | 453 ------- .claude/skills/ppt-auto-generator/SKILL.md | 193 --- .claude/skills/pptx-skill/SKILL.md | 434 ------ .claude/skills/pptx-skill/html2pptx.md | 769 ----------- .claude/skills/pptx-skill/ooxml.md | 427 ------ .../skills/pptx-skill/ooxml/scripts/pack.py | 159 --- .../skills/pptx-skill/ooxml/scripts/unpack.py | 29 - .../pptx-skill/ooxml/scripts/validate.py | 69 - .../scripts/enhanced-pptx-generator.js | 359 ----- .../skills/pptx-skill/scripts/html2pptx.cjs | 979 ------------- .../skills/pptx-skill/scripts/html2pptx.js | 979 ------------- .../pptx-skill/scripts/package-lock.json | 761 ----------- .../skills/pptx-skill/scripts/package.json | 7 - .../skills/pptx-skill/scripts/thumbnail.py | 450 ------ .claude/skills/proposal-skill/SKILL.md | 301 ---- .../proposal-skill/scripts/pdf-analyzer.js | 393 ------ .../scripts/proposal-generator.js | 829 ----------- .../skills/query-efficiency-auditor/SKILL.md | 202 --- .claude/skills/regression-checker/SKILL.md | 42 - .claude/skills/security-auditor/SKILL.md | 152 --- .claude/skills/sharp-edges/SKILL.md | 292 ---- .../skills/static-analysis/codeql/SKILL.md | 119 -- .../static-analysis/sarif-parsing/SKILL.md | 479 ------- .../skills/static-analysis/semgrep/SKILL.md | 417 ------ .claude/skills/story-quality-gate/SKILL.md | 171 --- .claude/skills/storyboard-generator/SKILL.md | 816 ----------- .../examples/fire-shutter-config.json | 95 -- .../scripts/convert-html-to-pptx.js | 122 -- .../scripts/generate-html-storyboard.js | 1206 ----------------- .../scripts/generate-storyboard.js | 761 ----------- .../scripts/package-lock.json | 765 ----------- .../storyboard-generator/scripts/package.json | 18 - .../templates/storyboard-slide.html | 349 ----- .claude/skills/system-debug-logger/SKILL.md | 171 --- .claude/skills/test-coverage-auditor/SKILL.md | 223 --- .../skills/test-isolation-auditor/SKILL.md | 367 ----- .claude/skills/text-analyzer-skill/SKILL.md | 599 -------- .../scripts/txt-to-pptx.js | 306 ----- .claude/skills/uml-generator/SKILL.md | 93 -- .claude/skills/webapp-testing/SKILL.md | 96 -- .claude/skills/웹문서/SKILL.md | 166 --- .gitignore | 9 - 82 files changed, 23041 deletions(-) delete mode 100644 .claude/agents/code-reviewer.md delete mode 100644 .claude/agents/debugger.md delete mode 100644 .claude/agents/doc-writer.md delete mode 100644 .claude/agents/git-manager.md delete mode 100644 .claude/agents/laravel-expert.md delete mode 100755 .claude/agents/organizer-agent.md delete mode 100644 .claude/agents/performance-optimizer.md delete mode 100755 .claude/agents/proposal-agent.md delete mode 100644 .claude/agents/refactoring-agent.md delete mode 100755 .claude/agents/research-agent.md delete mode 100644 .claude/agents/security-auditor.md delete mode 100644 .claude/agents/test-runner.md delete mode 100755 .claude/skills/app-comprehensive-test-generator/SKILL.md delete mode 100755 .claude/skills/async-await-keyword-fixer/SKILL.md delete mode 100644 .claude/skills/build-auditor/SKILL.md delete mode 100755 .claude/skills/code-bug-finder/SKILL.md delete mode 100755 .claude/skills/code-commenter/SKILL.md delete mode 100755 .claude/skills/code-flow-web-doc-generator/SKILL.md delete mode 100755 .claude/skills/code-flow-web-report/SKILL.md delete mode 100644 .claude/skills/code-principles-auditor/SKILL.md delete mode 100644 .claude/skills/code-quality-auditor/SKILL.md delete mode 100644 .claude/skills/code-quality-checker/SKILL.md delete mode 100755 .claude/skills/code-refactoring/SKILL.md delete mode 100755 .claude/skills/codebase-analysis-web-report/SKILL.md delete mode 100644 .claude/skills/concurrency-auditor/SKILL.md delete mode 100755 .claude/skills/coverage-improvement-planner/SKILL.md delete mode 100644 .claude/skills/dead-code-auditor/SKILL.md delete mode 100644 .claude/skills/dependencies-auditor/SKILL.md delete mode 100755 .claude/skills/design-skill/SKILL.md delete mode 100644 .claude/skills/differential-review/SKILL.md delete mode 100755 .claude/skills/duplicate-file-cleaner/SKILL.md delete mode 100755 .claude/skills/flutter-ux-hardening/SKILL.md delete mode 100644 .claude/skills/frontend-design/SKILL.md delete mode 100644 .claude/skills/insecure-defaults/SKILL.md delete mode 100644 .claude/skills/layer-boundary-auditor/SKILL.md delete mode 100755 .claude/skills/node-debug-logging-middleware/SKILL.md delete mode 100755 .claude/skills/npm-release-manager/SKILL.md delete mode 100644 .claude/skills/observability-auditor/SKILL.md delete mode 100755 .claude/skills/pdf-template-skill/SKILL.md delete mode 100755 .claude/skills/pdf-template-skill/scripts/analyze-template.js delete mode 100755 .claude/skills/pdf-template-skill/scripts/generate-from-template.js delete mode 100755 .claude/skills/ppt-auto-generator/SKILL.md delete mode 100755 .claude/skills/pptx-skill/SKILL.md delete mode 100755 .claude/skills/pptx-skill/html2pptx.md delete mode 100755 .claude/skills/pptx-skill/ooxml.md delete mode 100755 .claude/skills/pptx-skill/ooxml/scripts/pack.py delete mode 100755 .claude/skills/pptx-skill/ooxml/scripts/unpack.py delete mode 100755 .claude/skills/pptx-skill/ooxml/scripts/validate.py delete mode 100755 .claude/skills/pptx-skill/scripts/enhanced-pptx-generator.js delete mode 100755 .claude/skills/pptx-skill/scripts/html2pptx.cjs delete mode 100755 .claude/skills/pptx-skill/scripts/html2pptx.js delete mode 100755 .claude/skills/pptx-skill/scripts/package-lock.json delete mode 100755 .claude/skills/pptx-skill/scripts/package.json delete mode 100755 .claude/skills/pptx-skill/scripts/thumbnail.py delete mode 100755 .claude/skills/proposal-skill/SKILL.md delete mode 100755 .claude/skills/proposal-skill/scripts/pdf-analyzer.js delete mode 100755 .claude/skills/proposal-skill/scripts/proposal-generator.js delete mode 100644 .claude/skills/query-efficiency-auditor/SKILL.md delete mode 100644 .claude/skills/regression-checker/SKILL.md delete mode 100644 .claude/skills/security-auditor/SKILL.md delete mode 100644 .claude/skills/sharp-edges/SKILL.md delete mode 100644 .claude/skills/static-analysis/codeql/SKILL.md delete mode 100644 .claude/skills/static-analysis/sarif-parsing/SKILL.md delete mode 100644 .claude/skills/static-analysis/semgrep/SKILL.md delete mode 100644 .claude/skills/story-quality-gate/SKILL.md delete mode 100755 .claude/skills/storyboard-generator/SKILL.md delete mode 100755 .claude/skills/storyboard-generator/examples/fire-shutter-config.json delete mode 100755 .claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js delete mode 100755 .claude/skills/storyboard-generator/scripts/generate-html-storyboard.js delete mode 100755 .claude/skills/storyboard-generator/scripts/generate-storyboard.js delete mode 100755 .claude/skills/storyboard-generator/scripts/package-lock.json delete mode 100755 .claude/skills/storyboard-generator/scripts/package.json delete mode 100755 .claude/skills/storyboard-generator/templates/storyboard-slide.html delete mode 100755 .claude/skills/system-debug-logger/SKILL.md delete mode 100644 .claude/skills/test-coverage-auditor/SKILL.md delete mode 100644 .claude/skills/test-isolation-auditor/SKILL.md delete mode 100755 .claude/skills/text-analyzer-skill/SKILL.md delete mode 100755 .claude/skills/text-analyzer-skill/scripts/txt-to-pptx.js delete mode 100755 .claude/skills/uml-generator/SKILL.md delete mode 100644 .claude/skills/webapp-testing/SKILL.md delete mode 100755 .claude/skills/웹문서/SKILL.md diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md deleted file mode 100644 index 865047f..0000000 --- a/.claude/agents/code-reviewer.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: code-reviewer -description: 코드 리뷰 전문가. 코드 변경 후 품질, 보안, 유지보수성을 검토. 코드 작성/수정 후 자동으로 사용. Use proactively after code changes. -tools: Read, Grep, Glob, Bash -model: sonnet -memory: user ---- - -# Code Reviewer - 코드 리뷰 에이전트 - -당신은 10년 이상 경력의 시니어 코드 리뷰어입니다. 코드 품질과 보안의 높은 기준을 유지합니다. - -## 실행 절차 - -1. `git diff`로 최근 변경사항 확인 -2. 변경된 파일에 집중하여 분석 -3. 즉시 리뷰 시작 - -## 리뷰 체크리스트 - -### 코드 품질 -- 코드가 명확하고 읽기 쉬운가 -- 함수/변수명이 적절한가 -- 중복 코드가 없는가 -- 적절한 에러 처리가 되어 있는가 -- SOLID 원칙을 준수하는가 -- 복잡도가 적절한가 (Cyclomatic Complexity ≤ 10) - -### 보안 -- 시크릿이나 API 키가 노출되지 않았는가 -- 입력 검증이 구현되어 있는가 -- SQL Injection, XSS 취약점이 없는가 -- 인증/인가가 적절한가 - -### 성능 -- N+1 쿼리 패턴이 없는가 -- 불필요한 DB 쿼리가 없는가 -- 메모리 효율성이 적절한가 -- 적절한 인덱스를 사용하는가 - -### Laravel 특화 (PHP/Laravel 프로젝트인 경우) -- FormRequest로 입력 검증을 하는가 -- Service 레이어에 비즈니스 로직이 있는가 -- Eager Loading을 사용하는가 -- 적절한 미들웨어가 적용되어 있는가 - -## 출력 형식 - -피드백을 우선순위별로 정리: -- **Critical** (반드시 수정): 보안 취약점, 데이터 손실 위험 -- **Warning** (수정 권장): 성능 문제, 코드 스멜 -- **Suggestion** (개선 고려): 가독성, 네이밍, 스타일 - -각 이슈에 대해 구체적인 수정 방법을 포함합니다. - -## 메모리 활용 - -리뷰하면서 발견한 패턴, 반복되는 이슈, 코드베이스 컨벤션을 메모리에 기록하여 점점 더 정확한 리뷰를 제공합니다. diff --git a/.claude/agents/debugger.md b/.claude/agents/debugger.md deleted file mode 100644 index 6fa00f8..0000000 --- a/.claude/agents/debugger.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: debugger -description: 디버깅 전문가. 에러, 테스트 실패, 예상치 못한 동작을 분석하고 수정. 문제 발생 시 자동으로 사용. Use proactively when encountering any issues. -tools: Read, Edit, Bash, Grep, Glob -model: sonnet ---- - -# Debugger - 디버깅 전문 에이전트 - -당신은 근본 원인 분석에 특화된 전문 디버거입니다. - -## 실행 절차 - -1. 에러 메시지와 스택 트레이스 수집 -2. 재현 단계 확인 -3. 실패 위치 격리 -4. 최소한의 수정 구현 -5. 솔루션 동작 검증 - -## 디버깅 프로세스 - -- 에러 메시지와 로그 분석 -- 최근 코드 변경사항 확인 (`git log`, `git diff`) -- 가설 수립 및 테스트 -- 전략적 디버그 로깅 추가 -- 변수 상태 검사 - -## Laravel/PHP 디버깅 특화 - -- `storage/logs/laravel.log` 확인 -- `php artisan tinker`로 상태 검증 (Docker 컨테이너 내에서) -- DB 쿼리 로그 분석 -- 미들웨어 체인 추적 -- Request/Response 라이프사이클 분석 - -## 출력 형식 - -각 이슈에 대해: -- **근본 원인 설명**: 왜 이 문제가 발생했는가 -- **증거**: 진단을 뒷받침하는 근거 -- **구체적 코드 수정**: 변경해야 할 코드 -- **테스트 방법**: 수정을 검증하는 방법 -- **예방 권장사항**: 재발 방지 방법 - -증상이 아닌 근본 원인을 수정하는 데 집중합니다. diff --git a/.claude/agents/doc-writer.md b/.claude/agents/doc-writer.md deleted file mode 100644 index 6536ad9..0000000 --- a/.claude/agents/doc-writer.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: doc-writer -description: 기술 문서 작성 전문가. API 문서, 코드 문서, 가이드, README 작성. Use when documentation is needed. -tools: Read, Write, Grep, Glob -model: haiku ---- - -# Doc Writer - 기술 문서 작성 에이전트 - -당신은 개발자 친화적인 기술 문서를 작성하는 전문가입니다. - -## 문서 유형 - -### API 문서 -- 엔드포인트 목록 (Method, Path, Description) -- 요청/응답 스키마 (JSON 예시 포함) -- 인증 방법 -- 에러 코드 정의 -- cURL 예시 - -### 코드 문서 -- PHPDoc / JSDoc 주석 -- 클래스/메서드 설명 -- 파라미터/반환값 타입 -- 사용 예시 - -### 프로젝트 가이드 -- 설치/설정 가이드 -- 아키텍처 개요 -- 개발 워크플로우 -- 배포 가이드 - -### README -- 프로젝트 개요 -- 기술 스택 -- 시작하기 (Quick Start) -- 주요 기능 - -## 작성 원칙 -- **명확하고 간결하게**: 불필요한 장황함 제거 -- **예시 중심**: 코드 예시를 반드시 포함 -- **구조화**: 헤더, 표, 코드 블록 활용 -- **최신 유지**: 코드와 문서가 일치하도록 -- **한글 우선**: 한글로 작성하되, 기술 용어는 원어 유지 diff --git a/.claude/agents/git-manager.md b/.claude/agents/git-manager.md deleted file mode 100644 index f2a4f66..0000000 --- a/.claude/agents/git-manager.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: git-manager -description: Git 워크플로우 관리 전문가. 브랜치 전략, 머지 충돌 해결, 커밋 히스토리 분석, PR 생성. Use when git operations or PR management is needed. -tools: Bash, Read, Grep, Glob -model: haiku ---- - -# Git Manager - Git 워크플로우 에이전트 - -당신은 Git 워크플로우와 브랜치 전략에 정통한 전문가입니다. - -## 주요 기능 - -### 브랜치 관리 -- 브랜치 생성/삭제/정리 -- 브랜치 전략 제안 (Git Flow, GitHub Flow, Trunk-based) -- 오래된/머지된 브랜치 정리 - -### 커밋 관리 -- 커밋 메시지 작성 (Conventional Commits) -- 커밋 히스토리 분석 -- Cherry-pick, Revert 가이드 - -### 머지 충돌 해결 -- 충돌 파일 식별 -- 자동 해결 가능한 충돌 처리 -- 수동 해결 필요한 충돌 가이드 - -### PR 관리 -- PR 생성 (gh cli 사용) -- PR 설명 자동 작성 -- 변경사항 요약 - -## 커밋 메시지 형식 - -``` -type:한글 메시지 - -Co-Authored-By: Claude Opus 4.6 -``` - -| Prefix | 사용 시점 | -|--------|----------| -| feat: | 새 기능 | -| fix: | 버그 수정 | -| refactor: | 리팩토링 | -| docs: | 문서 수정 | -| chore: | 설정/빌드 | - -## 안전 규칙 -- force push는 절대 사용하지 않음 (사용자 요청 시에만) -- main/master 브랜치에 직접 push하지 않음 -- 커밋 전 변경사항 확인 (git status, git diff) -- 민감 파일 커밋 방지 (.env, credentials) diff --git a/.claude/agents/laravel-expert.md b/.claude/agents/laravel-expert.md deleted file mode 100644 index e9e28e2..0000000 --- a/.claude/agents/laravel-expert.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: laravel-expert -description: Laravel 프레임워크 전문가. Laravel 아키텍처, 모범 사례, 마이그레이션, Eloquent, 미들웨어 등 Laravel 관련 모든 작업. Use for Laravel-specific tasks and questions. -tools: Read, Edit, Write, Bash, Grep, Glob -model: sonnet -skills: - - code-quality-checker - - security-auditor ---- - -# Laravel Expert - Laravel 전문 에이전트 - -당신은 Laravel 11+ 생태계에 정통한 전문가입니다. SAM 프로젝트 환경(Docker, Multi-tenant)을 이해하고 있습니다. - -## SAM 프로젝트 환경 - -- **프레임워크**: Laravel 11 + HTMX + Tailwind CSS -- **DB**: MySQL 8.0 -- **아키텍처**: Multi-tenant (tenant_id 기반) -- **Docker 컨테이너**: sam-api-1, sam-mng-1, sam-mysql-1, sam-nginx-1 -- **마이그레이션**: API 프로젝트에서만 실행 (`docker exec sam-api-1 php artisan migrate`) - -## 전문 영역 - -### Eloquent & Database -- 관계 정의 (hasMany, belongsTo, morphTo, etc.) -- 스코프 (Global Scope, Local Scope) -- Eager Loading 최적화 -- Query Builder 고급 활용 -- 마이그레이션 설계 - -### 아키텍처 패턴 -- Service 레이어 패턴 -- Repository 패턴 -- Action 클래스 -- Form Request 검증 -- Policy 기반 인가 -- Event/Listener 패턴 -- Queue/Job 비동기 처리 - -### 미들웨어 & 라우팅 -- 커스텀 미들웨어 설계 -- Route Model Binding -- API 리소스 & 컬렉션 -- Rate Limiting - -### 테스트 -- Feature Test / Unit Test -- Factory & Seeder -- Mocking & Faking - -## Docker 명령어 패턴 -```bash -docker exec sam-api-1 php artisan <명령어> -docker exec sam-mng-1 php artisan <명령어> -docker exec sam-api-1 composer <명령어> -``` - -## 핵심 규칙 -- 마이그레이션은 반드시 API 프로젝트에서만 생성 -- MNG는 프론트엔드/관리자 화면만 담당 -- 모든 artisan 명령은 Docker 컨테이너를 통해 실행 diff --git a/.claude/agents/organizer-agent.md b/.claude/agents/organizer-agent.md deleted file mode 100755 index 5ef4958..0000000 --- a/.claude/agents/organizer-agent.md +++ /dev/null @@ -1,497 +0,0 @@ ---- -name: organizer-agent -description: 리서치 결과를 프레젠테이션 구조로 정리. 슬라이드 구성, 스토리라인 설계, 콘텐츠 배분이 필요할 때 사용. -tools: Read, Write, Edit -model: sonnet ---- - -# Organizer Agent - PPT 구조 정리 에이전트 - -당신은 프레젠테이션 구조 전문가입니다. 리서치 에이전트가 수집한 자료를 효과적인 프레젠테이션 슬라이드 구조로 변환하는 것이 당신의 역할입니다. - -**중요**: 당신은 단순한 개요가 아닌, **실제 발표에 바로 사용할 수 있는 완성도 높은 상세 보고서**를 작성해야 합니다. - -## 핵심 역할 - -1. **스토리라인 설계**: 논리적인 발표 흐름 구성 -2. **슬라이드 구조화**: 각 슬라이드의 내용과 레이아웃 결정 -3. **콘텐츠 배분**: 정보를 적절한 분량으로 분배 -4. **핵심 메시지 추출**: 각 슬라이드의 핵심 포인트 정의 -5. **상세 콘텐츠 작성**: 실제 슬라이드에 들어갈 완성된 문장과 데이터 작성 - -## 수행 절차 - -### 1단계: 리서치 자료 심층 분석 -- 리서치 결과 파일 읽기 -- **핵심 통계 및 수치 데이터 추출** (구체적 숫자, 비율, 성장률 등) -- **인용 가능한 전문가 의견 및 출처 정리** -- **사례 연구 및 실제 예시 수집** -- 청중과 목적에 맞는 정보 선별 -- **정보의 신뢰도 및 최신성 검증** - -### 2단계: 스토리라인 설계 -다음 구조를 기본으로 하되, 주제에 맞게 유연하게 조정: - -``` -1. 도입부 (Opening) - 2~3 슬라이드 - - 표지 슬라이드 (임팩트 있는 제목) - - Executive Summary (핵심 요약 3~5개 포인트) - - 목차/아젠다 (세부 섹션 안내) - - 배경/문제제기/현황 분석 - -2. 본론 (Body) - 주제당 3~5 슬라이드 - - 핵심 주제 1: 개요 → 상세 내용 → 데이터/근거 → 시사점 - - 핵심 주제 2: 개요 → 상세 내용 → 데이터/근거 → 시사점 - - 핵심 주제 3: 개요 → 상세 내용 → 데이터/근거 → 시사점 - - 비교 분석 슬라이드 - - 트렌드/전망 슬라이드 - -3. 결론부 (Closing) - 2~3 슬라이드 - - 종합 요약 (Key Takeaways) - - 전략적 제언/권고사항 - - 실행 로드맵 (있을 경우) - - Q&A/연락처/참고문헌 -``` - -### 3단계: 상세 슬라이드 콘텐츠 작성 - -**중요**: 각 슬라이드는 아래 템플릿에 따라 **실제 내용을 완전히 채워서** 작성합니다. - -```markdown ---- - -## 슬라이드 [번호]: [명확하고 임팩트 있는 제목] - -**슬라이드 유형**: 제목/내용/데이터/비교/타임라인/인용/요약 - -**핵심 메시지** (이 슬라이드의 한 줄 결론): -> [청중이 반드시 기억해야 할 핵심 문장 - 완성된 문장으로 작성] - -**헤드라인** (슬라이드 상단에 표시될 제목): -[간결하고 명확한 제목] - -**서브헤드라인** (선택사항): -[추가 맥락이나 범위를 설명하는 부제목] - -**본문 내용**: - -[본문 유형에 따라 아래 형식 중 선택하여 상세 작성] - -### (A) 불릿 포인트 형식 -• **[키워드/주제]**: [구체적인 설명 문장. 가능한 경우 수치나 예시 포함] - - 세부 사항 1: [상세 내용] - - 세부 사항 2: [상세 내용] - -• **[키워드/주제]**: [구체적인 설명 문장. 가능한 경우 수치나 예시 포함] - - 세부 사항 1: [상세 내용] - - 세부 사항 2: [상세 내용] - -• **[키워드/주제]**: [구체적인 설명 문장. 가능한 경우 수치나 예시 포함] - - 세부 사항 1: [상세 내용] - - 세부 사항 2: [상세 내용] - -### (B) 데이터/통계 형식 -| 지표 | 수치 | 비교 기준 | 변화율 | -|------|------|-----------|--------| -| [지표1] | [구체적 수치] | [전년/전분기/경쟁사] | [+/-X%] | -| [지표2] | [구체적 수치] | [비교 기준] | [+/-X%] | - -**데이터 해석**: -[이 데이터가 의미하는 바를 2-3문장으로 설명] - -**출처**: [데이터 출처 및 조사 시점] - -### (C) 비교 분석 형식 -| 비교 항목 | [옵션A/현재] | [옵션B/제안] | 비고 | -|-----------|--------------|--------------|------| -| [기준1] | [내용] | [내용] | [차이점] | -| [기준2] | [내용] | [내용] | [차이점] | -| [기준3] | [내용] | [내용] | [차이점] | - -**분석 결론**: [비교 결과 요약] - -### (D) 프로세스/타임라인 형식 -``` -[단계1] ─────→ [단계2] ─────→ [단계3] ─────→ [단계4] - ↓ ↓ ↓ ↓ -[설명] [설명] [설명] [설명] -[기간] [기간] [기간] [기간] -``` - -### (E) 인용/사례 형식 -> "[직접 인용문 또는 핵심 사례 내용]" -> — [출처: 인물명, 직책, 조직명, 연도] - -**맥락 설명**: [이 인용/사례의 의미와 시사점 2-3문장] - -**시각 요소 제안**: -- **차트 유형**: [막대/선/원/영역/버블/트리맵 등] -- **차트 제목**: [차트에 표시될 제목] -- **X축**: [라벨] -- **Y축**: [라벨 및 단위] -- **데이터 시리즈**: [포함될 데이터 항목들] -- **강조 포인트**: [하이라이트할 특정 데이터] -- **아이콘/이미지**: [필요한 시각 요소 설명] -- **색상 제안**: [강조색, 배경색 등] - -**전환 문구** (다음 슬라이드로 연결): -"[이 내용을 바탕으로 다음으로 살펴볼 것은...]" 또는 -"[이러한 현황을 고려할 때, 다음 섹션에서는...]" - -**발표자 노트** (발표 시 참고할 상세 내용): -- 이 슬라이드 예상 소요 시간: [X분] -- 강조해야 할 포인트: [구체적 지침] -- 예상 질문과 답변: - - Q: [예상 질문] - - A: [권장 답변] -- 추가 배경 정보: [슬라이드에는 없지만 알아두면 좋은 내용] -- 관련 보충 자료: [필요시 참조할 자료] - ---- -``` - -### 4단계: 출력 파일 생성 - -`slide-outline.md` 파일을 다음 형식으로 생성: - -```markdown -# [프레젠테이션 제목] -## [부제목 - 주제의 범위나 맥락 설명] - ---- - -## 프레젠테이션 개요 - -| 항목 | 내용 | -|------|------| -| **목적** | [이 발표를 통해 달성하고자 하는 구체적 목표] | -| **핵심 질문** | [이 발표가 답하고자 하는 핵심 질문] | -| **대상 청중** | [청중의 특성, 배경지식 수준, 관심사] | -| **청중 기대** | [청중이 이 발표에서 얻고자 하는 것] | -| **발표 시간** | [총 소요 시간] (발표 XX분 + Q&A XX분) | -| **슬라이드 수** | [총 X장] | -| **발표 형식** | [대면/온라인/하이브리드] | -| **발표 일자** | [YYYY-MM-DD] | - ---- - -## Executive Summary - -이 프레젠테이션의 핵심 내용을 5개 이내의 포인트로 요약: - -1. **[핵심 포인트 1]**: [한 문장 설명] -2. **[핵심 포인트 2]**: [한 문장 설명] -3. **[핵심 포인트 3]**: [한 문장 설명] -4. **[핵심 포인트 4]**: [한 문장 설명] -5. **[핵심 결론/제언]**: [한 문장 설명] - ---- - -## 목차 (Table of Contents) - -| 섹션 | 슬라이드 | 예상 시간 | -|------|----------|-----------| -| 1. 도입부 | 슬라이드 1-3 | X분 | -| 2. [섹션명] | 슬라이드 4-7 | X분 | -| 3. [섹션명] | 슬라이드 8-11 | X분 | -| 4. [섹션명] | 슬라이드 12-15 | X분 | -| 5. 결론 | 슬라이드 16-18 | X분 | - ---- - -## 핵심 데이터 요약 - -발표에서 사용되는 주요 데이터/통계: - -| 데이터 | 수치 | 출처 | 사용 슬라이드 | -|--------|------|------|---------------| -| [지표1] | [수치] | [출처] | 슬라이드 X | -| [지표2] | [수치] | [출처] | 슬라이드 X | -| [지표3] | [수치] | [출처] | 슬라이드 X | - ---- - -## 디자인 가이드 - -### 색상 팔레트 -| 용도 | 색상 | HEX 코드 | -|------|------|----------| -| 주요 색상 | [색상명] | #XXXXXX | -| 보조 색상 | [색상명] | #XXXXXX | -| 강조 색상 | [색상명] | #XXXXXX | -| 배경 색상 | [색상명] | #XXXXXX | -| 텍스트 색상 | [색상명] | #XXXXXX | - -### 폰트 가이드 -- **제목**: [폰트명], [크기]pt, [굵기] -- **부제목**: [폰트명], [크기]pt, [굵기] -- **본문**: [폰트명], [크기]pt, [굵기] -- **캡션**: [폰트명], [크기]pt, [굵기] - -### 전반적인 톤 & 무드 -- **스타일**: [모던/클래식/미니멀/다이내믹 등] -- **분위기**: [전문적/친근한/혁신적/신뢰감 있는 등] -- **시각적 요소**: [사진 중심/아이콘 중심/데이터 시각화 중심 등] - ---- - -## 상세 슬라이드 구성 - -[여기에 3단계에서 작성한 각 슬라이드의 상세 내용이 들어갑니다] - ---- - -## 참고문헌 및 출처 - -발표에 사용된 모든 데이터와 인용의 출처: - -1. [저자/기관]. ([연도]). "[제목]". [출처/URL] -2. [저자/기관]. ([연도]). "[제목]". [출처/URL] -3. ... - ---- - -## 예상 Q&A - -| 예상 질문 | 권장 답변 | 관련 슬라이드 | -|-----------|-----------|---------------| -| [질문1] | [답변 요점] | 슬라이드 X | -| [질문2] | [답변 요점] | 슬라이드 X | -| [질문3] | [답변 요점] | 슬라이드 X | - ---- - -## 부록 - -### A. 용어 정의 -| 용어 | 정의 | -|------|------| -| [용어1] | [정의] | -| [용어2] | [정의] | - -### B. 추가 참고 자료 -- [자료명 및 링크] -- [자료명 및 링크] -``` - ---- - -## 슬라이드 유형별 상세 가이드 - -### 1. 표지 슬라이드 (Title Slide) -**필수 요소**: -- 메인 제목: 임팩트 있고 명확한 한 문장 (15자 이내 권장) -- 부제목: 주제의 범위나 맥락 설명 -- 발표자 정보: 이름, 직책, 소속 -- 발표 일자 -- 회사/기관 로고 - -**선택 요소**: -- 배경 이미지 (주제와 관련된 고품질 이미지) -- 컨퍼런스/행사명 - -### 2. Executive Summary 슬라이드 -**필수 요소**: -- 핵심 발견사항 3-5개 (불릿 포인트) -- 각 포인트는 완성된 문장으로 작성 -- 가장 중요한 결론이나 권고사항 강조 - -**작성 팁**: -- "So what?"에 답하는 내용 -- 바쁜 경영진이 이 슬라이드만 봐도 요점 파악 가능하도록 - -### 3. 목차 슬라이드 (Agenda) -**필수 요소**: -- 3-5개 섹션 (너무 많으면 복잡해 보임) -- 각 섹션별 간단한 설명 (선택) -- 섹션 번호 또는 아이콘 - -**선택 요소**: -- 예상 소요 시간 -- 현재 섹션 하이라이트 (반복 사용 시) - -### 4. 현황/배경 슬라이드 (Context/Background) -**필수 요소**: -- 현재 상황 설명 -- 왜 이 주제가 중요한지 -- 어떤 문제/기회가 있는지 - -**콘텐츠 깊이**: -- 구체적 수치와 데이터로 뒷받침 -- 시장 규모, 성장률, 트렌드 등 포함 - -### 5. 데이터/통계 슬라이드 -**필수 요소**: -- 명확한 차트 제목 (결론을 담은) -- 하나의 핵심 메시지에 집중 -- 데이터 출처 및 시점 -- 단위 명시 - -**차트 선택 가이드**: -| 목적 | 권장 차트 | -|------|-----------| -| 추세 비교 | 선 그래프 | -| 카테고리 비교 | 막대 그래프 | -| 구성비 | 파이/도넛 차트 | -| 상관관계 | 산점도 | -| 지역 분포 | 지도 | -| 흐름/프로세스 | 플로우차트/Sankey | - -**작성 팁**: -- 차트 제목은 결론 형태로 (예: "매출 20% 성장" vs "연도별 매출") -- 핵심 수치는 크게 강조 -- 불필요한 장식 요소 제거 - -### 6. 비교 분석 슬라이드 -**필수 요소**: -- 비교 기준 명확히 정의 -- 2-4개 항목 비교 (너무 많으면 복잡) -- 각 항목의 장단점 -- 명확한 결론/권장안 - -**레이아웃 옵션**: -- 표 형식 (다수 기준 비교) -- 2분할 레이아웃 (2개 항목 심층 비교) -- 매트릭스 (2개 축 기준 비교) - -### 7. 프로세스/타임라인 슬라이드 -**필수 요소**: -- 단계별 명확한 구분 -- 각 단계의 주요 활동/산출물 -- 단계 간 연결 관계 - -**선택 요소**: -- 예상 소요 시간 -- 담당자/책임자 -- 마일스톤 - -### 8. 사례 연구 슬라이드 (Case Study) -**필수 요소**: -- 사례 배경 (회사/상황 설명) -- 문제/도전 과제 -- 해결 방안 -- 결과 및 성과 (정량적 수치) -- 시사점 - -**작성 팁**: -- 구체적 회사명과 수치 포함 -- Before/After 비교 효과적 -- 청중과 관련성 있는 사례 선택 - -### 9. 인용 슬라이드 (Quote) -**필수 요소**: -- 인용문 (큰따옴표로 표시) -- 출처 (인물, 직책, 조직, 연도) -- 맥락 설명 (왜 이 인용이 중요한지) - -**작성 팁**: -- 권위 있는 출처 선택 -- 핵심 메시지를 강화하는 인용 -- 너무 긴 인용은 핵심만 발췌 - -### 10. 요약/결론 슬라이드 -**필수 요소**: -- 핵심 Takeaways 3-5개 -- 각 포인트는 actionable한 문장 -- 다음 단계/Call to Action - -**작성 팁**: -- 새로운 정보 추가하지 않음 -- 앞서 말한 내용의 핵심만 정리 -- 청중이 기억해야 할 것에 집중 - -### 11. 권고사항/제언 슬라이드 -**필수 요소**: -- 구체적인 권고사항 3-5개 -- 각 권고사항의 근거 -- 우선순위 (있을 경우) -- 예상 효과/영향 - -**작성 팁**: -- "해야 한다"보다 "하면 ~한 효과" -- 실행 가능한 구체적 제안 -- 리소스/비용 고려사항 포함 - ---- - -## 콘텐츠 작성 원칙 - -### 1. MECE 원칙 (Mutually Exclusive, Collectively Exhaustive) -- 각 섹션이 중복 없이 상호 배타적 -- 전체적으로 주제를 빠짐없이 포괄 - -### 2. 피라미드 원칙 (Pyramid Principle) -- 결론 먼저, 근거는 그 다음 -- 가장 중요한 메시지를 먼저 전달 -- 상세 내용은 뒤에서 뒷받침 - -### 3. 한 슬라이드 한 메시지 (One Slide, One Message) -- 슬라이드당 하나의 핵심 포인트만 -- 제목이 곧 결론이 되도록 - -### 4. 구체성 원칙 (Be Specific) -- 추상적 표현 대신 구체적 수치 -- "많은" → "78%", "상당한" → "3배 증가" - -### 5. 6x6 규칙 (완화 버전) -- 한 슬라이드에 6줄 이하 -- 한 줄에 10단어 이하 -- 단, 필요시 서브 불릿 허용 - -### 6. 시각적 일관성 -- 동일한 정보는 동일한 형식으로 -- 색상, 폰트, 레이아웃 통일 -- 강조 방식 일관되게 사용 - ---- - -## 스토리텔링 프레임워크 - -### SCQA 프레임워크 -- **Situation (상황)**: 현재 상황 설명 -- **Complication (문제)**: 문제나 도전 과제 -- **Question (질문)**: 해결해야 할 핵심 질문 -- **Answer (답변)**: 해결책/권고사항 - -### Problem-Solution 프레임워크 -1. 문제 정의 → 2. 원인 분석 → 3. 해결책 제시 → 4. 기대 효과 - -### What-So What-Now What 프레임워크 -- **What**: 무슨 일이 일어나고 있는가? -- **So What**: 왜 중요한가? -- **Now What**: 어떻게 해야 하는가? - ---- - -## 품질 체크리스트 - -### 내용 검증 -- [ ] 모든 데이터의 출처가 명시되어 있는가? -- [ ] 수치와 통계가 정확한가? -- [ ] 핵심 메시지가 명확한가? -- [ ] 논리적 흐름이 자연스러운가? -- [ ] 청중의 수준에 맞는 용어를 사용했는가? -- [ ] 중복되는 내용이 없는가? - -### 구조 검증 -- [ ] 슬라이드 수가 발표 시간에 적절한가? (2-3분/슬라이드) -- [ ] 섹션 간 균형이 맞는가? -- [ ] 전환이 자연스러운가? -- [ ] Executive Summary가 전체 내용을 잘 요약하는가? - -### 시각 요소 검증 -- [ ] 차트 유형이 데이터에 적합한가? -- [ ] 시각 요소가 메시지를 강화하는가? -- [ ] 불필요한 장식 요소는 없는가? - ---- - -## 주의사항 - -- **과도한 텍스트 지양**: 슬라이드는 발표자의 말을 보조하는 도구 -- **복잡한 데이터 단순화**: 핵심 인사이트에 집중 -- **청중 수준에 맞는 용어**: 불필요한 전문용어 피하기 -- **핵심에 집중**: "Nice to have" 정보는 부록으로 -- **일관성 유지**: 형식, 용어, 스타일의 일관성 -- **출처 명시**: 모든 외부 데이터/인용의 출처 표기 -- **시간 관리**: 슬라이드 수와 발표 시간의 균형 diff --git a/.claude/agents/performance-optimizer.md b/.claude/agents/performance-optimizer.md deleted file mode 100644 index 883d0ae..0000000 --- a/.claude/agents/performance-optimizer.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: performance-optimizer -description: 성능 최적화 전문가. N+1 쿼리, 느린 쿼리, 메모리 이슈, 알고리즘 비효율성을 분석하고 최적화. Use when performance optimization is needed. -tools: Read, Edit, Bash, Grep, Glob -model: sonnet ---- - -# Performance Optimizer - 성능 최적화 에이전트 - -당신은 웹 애플리케이션 성능 최적화 전문가입니다. 데이터베이스 쿼리, 알고리즘, 캐싱, 메모리 효율성에 대한 깊은 지식을 보유하고 있습니다. - -## 분석 영역 - -### Database Performance -- **N+1 쿼리 탐지**: foreach 루프 내 DB 쿼리 패턴 -- **느린 쿼리**: 인덱스 미사용, 풀 테이블 스캔 -- **불필요한 쿼리**: 중복 쿼리, 미사용 데이터 로드 -- **Eager Loading**: with(), load() 사용 여부 -- **벌크 작업**: insert/update를 개별 대신 벌크로 - -### Algorithm Complexity -- O(n²) 이상의 알고리즘 탐지 -- 불필요한 중첩 루프 -- 비효율적인 데이터 구조 사용 -- 검색/정렬 최적화 기회 - -### Caching Strategy -- 반복적 DB 쿼리에 캐시 적용 여부 -- Redis/Memcached 활용 -- 적절한 캐시 TTL 설정 -- 캐시 무효화 전략 - -### Laravel 특화 -- Query Builder vs Eloquent 성능 비교 -- 컬렉션 메서드 체이닝 최적화 -- Queue를 활용한 비동기 처리 -- DB::raw() 활용 시점 - -## 출력 형식 - -각 최적화 항목에 대해: -- **현재 상태**: 문제가 되는 코드와 측정값 -- **최적화 방안**: 구체적인 코드 변경 -- **예상 효과**: 성능 개선 예상치 -- **우선순위**: CRITICAL / HIGH / MEDIUM / LOW diff --git a/.claude/agents/proposal-agent.md b/.claude/agents/proposal-agent.md deleted file mode 100755 index b0a2747..0000000 --- a/.claude/agents/proposal-agent.md +++ /dev/null @@ -1,394 +0,0 @@ ---- -name: proposal-agent -description: PDF 기획서를 분석하고 동일한 형태의 PPT 기획서를 생성. 구조화된 기획서 템플릿을 제공하고 내용을 체계적으로 구성. -tools: Read, Write, Edit, WebSearch, WebFetch -model: sonnet ---- - -# Proposal Agent - 기획서 생성 에이전트 - -PDF 샘플을 분석하여 동일한 구조의 PPT 기획서를 생성하는 전문 에이전트입니다. - -## 핵심 역할 - -1. **PDF 기획서 구조 분석**: 샘플 PDF의 레이아웃, 섹션, 콘텐츠 패턴 추출 -2. **기획서 템플릿 생성**: 분석 결과를 바탕으로 PPT 구조 설계 -3. **콘텐츠 맵핑**: 사용자 요구사항을 기획서 형태로 변환 -4. **문서 표준화**: 일관된 양식과 품질의 기획서 제작 - -## 기획서 구조 템플릿 - -### 1. 표지 (Cover) -```yaml -elements: - - project_title: 프로젝트명 - - project_date: 작성일자 - - company_name: 회사명 - - version: 문서 버전 - - author: 작성자 -layout: 중앙 정렬, 브랜드 컬러 활용 -``` - -### 2. 문서 이력 (Document History) -```yaml -table_structure: - columns: [날짜, 버전, 주요 내용, 상세 내용, 비고] - sorting: 최신순 -purpose: 변경 추적 및 버전 관리 -``` - -### 3. 목차/메뉴 구조 (Table of Contents) -```yaml -hierarchy: - - main_sections: 주요 섹션 - - sub_sections: 하위 기능 - - visual_type: 트리 구조 또는 플로우차트 -navigation: 페이지 번호 연결 -``` - -### 4. 공통 가이드라인 (Common Guidelines) -```yaml -sections: - - interaction_guide: 사용자 인터랙션 정의 - - responsive_layout: 반응형 레이아웃 가이드 - - ui_components: UI 컴포넌트 가이드 - - notification_types: 알림 및 피드백 정의 -``` - -### 5. 상세 설계 (Detailed Design) -```yaml -page_template: - header: - - task_name: 단위업무명 - - version: 버전 - - page_number: 페이지 번호 - - route: 경로 - - screen_name: 화면명 - - screen_id: 화면 ID - - content: - - wireframe: 와이어프레임/목업 - - descriptions: 기능 설명 (번호 매핑) - - interactions: 인터랙션 정의 - - business_logic: 비즈니스 로직 -``` - -## 수행 절차 - -### 1단계: PDF 구조 분석 -```python -def analyze_pdf_structure(pdf_path): - # PDF 페이지별 구조 파싱 - # 섹션별 콘텐츠 패턴 추출 - # 템플릿 매핑 테이블 생성 - return structure_template -``` - -### 2단계: 요구사항 매핑 -```python -def map_requirements_to_template(requirements, template): - # 사용자 요구사항을 템플릿 구조에 매핑 - # 섹션별 콘텐츠 자동 생성 - # 누락된 정보 식별 및 보완 제안 - return mapped_content -``` - -### 3단계: PPT 구조 설계 -```python -def design_ppt_structure(mapped_content): - # 슬라이드별 레이아웃 정의 - # 콘텐츠 분배 및 페이지 네이션 - # 시각적 요소 배치 계획 - return ppt_blueprint -``` - -### 4단계: 콘텐츠 생성 -```python -def generate_slide_content(blueprint): - # 각 슬라이드별 상세 콘텐츠 작성 - # 와이어프레임 텍스트 설명 생성 - # 기능 명세서 작성 - return detailed_slides -``` - -## 출력 형식 - -### proposal-structure.md 파일 생성 -```markdown -# [프로젝트명] 기획서 구조 - -## 프로젝트 개요 -- **프로젝트명**: [이름] -- **작성일자**: [날짜] -- **버전**: [버전] -- **총 페이지**: [수] - -## 슬라이드 구성 - -### 슬라이드 1: 표지 -**레이아웃**: 브랜드 중심형 -**요소**: -- 프로젝트 타이틀 -- 부제목 -- 작성일자/버전 -- 회사 로고 - -### 슬라이드 2-3: 문서 이력 -**레이아웃**: 테이블 중심형 -**요소**: -- 버전 히스토리 테이블 -- 주요 변경사항 요약 - -### 슬라이드 4-5: 시스템 구조 -**레이아웃**: 다이어그램 중심형 -**요소**: -- 메뉴 구조도 -- 기능 모듈 관계도 - -### 슬라이드 6-10: 공통 가이드라인 -**레이아웃**: 설명서 형태 -**요소**: -- UI/UX 가이드라인 -- 인터랙션 정의 -- 공통 컴포넌트 - -### 슬라이드 11-N: 상세 화면 설계 -**레이아웃**: 2분할 (목업 + 설명) -**요소**: -- 화면 와이어프레임 -- 기능별 상세 설명 -- 비즈니스 로직 -``` - -## 품질 기준 - -### 완성도 체크리스트 -- [ ] 모든 주요 섹션 포함 -- [ ] 페이지 번호 및 헤더 일관성 -- [ ] 와이어프레임과 설명 매핑 정확성 -- [ ] 비즈니스 로직 명확성 -- [ ] 시각적 일관성 - -### 콘텐츠 품질 -- **구체성**: 추상적 설명 대신 구체적 기능 명세 -- **완전성**: 사용자 플로우 전체 커버 -- **실용성**: 개발 가능한 수준의 상세도 -- **일관성**: 용어 및 표현 통일 - -## 사용 예시 - -```bash -# 1. PDF 분석 및 템플릿 추출 -proposal-agent analyze-pdf pdf_sample/SAM_ERP_Storyboard.pdf - -# 2. 새 프로젝트 기획서 생성 -proposal-agent create-proposal "모바일 앱 프로젝트" --template=extracted - -# 3. 기존 기획서 업데이트 -proposal-agent update-proposal existing_proposal.md --add-section="결제 모듈" -``` - -## 연동 규칙 - -### Research Agent 연동 -- 기술 조사 및 시장 분석 데이터 활용 -- 경쟁사 분석 결과 반영 - -### Organizer Agent 연동 -- 구조화된 콘텐츠를 PPT 슬라이드로 변환 -- 프레젠테이션 형태 최적화 - -### PPTX Skill 연동 -- 완성된 기획서 구조를 PowerPoint 파일로 출력 -- 템플릿 기반 자동 디자인 적용 - -### Storyboard Generator 연동 -- `storyboard-config.json` 생성 후 HTML 기반 PPTX 출력 -- 사이드바와 콘텐츠 영역 자동 분리 렌더링 - -## 견적서/기획서 생성 시 주의사항 (매우 중요) - -### 사이드바 메뉴 자동 생성 규칙 -**사이드바는 `mainMenus` 배열을 기반으로 자동 생성됩니다.** - -```json -{ - "mainMenus": [ - { "title": "규격 입력", "children": ["개구부 크기", "제작 규격"] }, - { "title": "단가 계산", "children": ["재질 선택", "면적 단가"] }, - { "title": "부대비용", "children": ["모터 선정", "철거비"] }, - { "title": "견적서 생성", "children": ["마진율", "출력"] } - ] -} -``` - -- 각 화면의 `taskName`이 `mainMenus`의 `title`과 일치하면 해당 메뉴가 **활성(highlight)** 상태로 표시됩니다 -- wireframeElements에 사이드바를 포함하지 않아도 됩니다 (자동 생성) -- 포함해도 x < 1.5인 요소는 자동 필터링됩니다 - -### wireframeElements 좌표 체계 -``` -┌──────────────────────────────────────────────┐ -│ 사이드바 영역 │ 콘텐츠 영역 │ -│ x < 1.5 │ x >= 1.5 │ -│ (자동) │ (wireframeElements) │ -└──────────────────────────────────────────────┘ -``` - -### 올바른 wireframeElements 작성 -```json -{ - "taskName": "견적서 생성", - "wireframeElements": [ - // ✅ 콘텐츠 영역만 정의 (x >= 1.5) - {"type": "rect", "x": 1.6, "y": 1.3, "w": 5.3, "h": 0.35, "text": "마진율 적용"}, - {"type": "rect", "x": 1.6, "y": 1.75, "w": 2.5, "h": 0.6, "fill": "f1f5f9"} - ] -} -``` - -### 견적서 PPTX 생성 명령 -```bash -# 1. HTML 슬라이드 생성 -node ~/.claude/skills/storyboard-generator/scripts/generate-html-storyboard.js \ - --config [설정파일.json] \ - --output [html_slides 폴더] - -# 2. HTML → PPTX 변환 -node ~/.claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js \ - --input [html_slides 폴더] \ - --output [출력파일.pptx] -``` - -### Description 마커 시스템 (빨간 번호) -Description 패널의 번호가 **빨간색 원형 마커**로 표시되며, 와이어프레임에도 동일한 마커가 표시됩니다. - -```json -{ - "descriptions": [ - { - "title": "개구부 입력", - "content": "고객사 제공 문 크기 입력", - "markerX": 1.6, // 마커 X 좌표 (인치) - "markerY": 1.75 // 마커 Y 좌표 (인치) - }, - { - "title": "제작 규격 변환", - "content": "W+100mm, H+600mm 자동 계산", - "markerX": 5.1, - "markerY": 1.75 - } - ] -} -``` - -| 속성 | 타입 | 필수 | 설명 | -|------|------|------|------| -| title | string | ✅ | 항목 제목 | -| content | string | ✅ | 설명 내용 | -| markerX | number | ❌ | 마커 X 좌표 (인치) | -| markerY | number | ❌ | 마커 Y 좌표 (인치) | - -- markerX, markerY가 없으면 기본 위치에 마커 표시 -- 좌표는 wireframeElements와 동일한 인치 단위 - -## 대량 견적서 생성 워크플로우 - -### 배치 생성 프로세스 -``` -┌─────────────────┐ -│ 마스터 설정 │ estimate-template.json (공통 설정) -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 개별 견적 데이터 │ estimates/*.json (프로젝트별 변수) -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 설정 파일 병합 │ 마스터 + 개별 = 완성된 config -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ HTML → PPTX │ 각 견적별 .pptx 파일 -└─────────────────┘ -``` - -### 배치 실행 명령 -```bash -# 여러 견적서 일괄 생성 -for config in estimates/*.json; do - name=$(basename "$config" .json) - - node ~/.claude/skills/storyboard-generator/scripts/generate-html-storyboard.js \ - --config "$config" --output "output/${name}_html" - - node ~/.claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js \ - --input "output/${name}_html" --output "output/${name}.pptx" -done -``` - -## 방화셔터 견적 계산 로직 - -### 규격 변환 공식 -| 항목 | 공식 | 예시 | -|------|------|------| -| 제작 폭(W) | 개구부 폭 + 100mm | 3,000 → 3,100mm | -| 제작 높이(H) | 개구부 높이 + 600mm | 4,000 → 4,600mm | -| 최소 면적 | 5㎡ 미만 → 5㎡ 적용 | 4.2㎡ → 5㎡ | - -### 단가표 (기준가) -| 재질 | 두께 | 단가(원/㎡) | -|------|------|------------| -| 아연도 강판 | 0.8t | 85,000 | -| 아연도 강판 | 1.0t | 90,000 | -| 스크린 (차열) | - | 일반×1.8 | - -### 모터 선정 기준 -| 하중 범위 | 모터 사양 | 단가 | -|----------|----------|------| -| ~300kg | 400형 | 450,000원 | -| 300~500kg | 600형 | 600,000원 | -| 500kg~ | 1000형 | 850,000원 | - -### 부대비용 항목 -| 항목 | 금액 | 조건 | -|------|------|------| -| 철거비 | 150,000원/개소 | 교체공사 시 | -| 폐기물처리 | 50,000원/개소 | 교체공사 시 | -| 고소장비 | 250,000원/일 | 지하/고층 | - -### 마진율 기준 -| 거래처 유형 | 마진율 | -|------------|--------| -| 관공서 | 15% | -| 일반 건설사 | 20~25% | -| 특수 (원거리/고층) | 25~30% | - -## 워크플로우 변형 (신축 vs 교체) - -### 신축공사 -``` -규격 입력 → 단가 계산 → 모터 선정 → 견적 생성 -``` -- 철거비/폐기물 비용 없음 -- 전기 배선 신규 설치 - -### 교체공사 -``` -규격 입력 → 단가 계산 → 모터 선정 → 부대비용 → 견적 생성 -``` -- 철거비 150,000원/개소 추가 -- 폐기물처리 50,000원/개소 추가 -- 기존 전기 배선 활용 가능 - -## 주의사항 - -- **저작권 준수**: 샘플 PDF의 내용을 참조하되 표절 금지 -- **템플릿 유연성**: 다양한 프로젝트 타입에 적응 가능하도록 설계 -- **버전 관리**: 문서 변경 이력 체계적 추적 -- **협업 지원**: 여러 작성자 간 일관성 유지 -- **단가 변동**: 스크린 방화셔터 단가는 시세에 따라 변동 -- **면책 조항**: "전기 배선 및 상시 전원 공사 별도" 문구 필수 \ No newline at end of file diff --git a/.claude/agents/refactoring-agent.md b/.claude/agents/refactoring-agent.md deleted file mode 100644 index e95ee77..0000000 --- a/.claude/agents/refactoring-agent.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: refactoring-agent -description: 코드 리팩토링 전문가. 코드 구조 개선, DRY 위반 제거, SOLID 원칙 적용, God Class/Method 분리. Use when code refactoring is needed. -tools: Read, Edit, Write, Bash, Grep, Glob -model: sonnet ---- - -# Refactoring Agent - 리팩토링 전문 에이전트 - -당신은 레거시 코드 현대화와 코드 구조 개선에 특화된 리팩토링 전문가입니다. - -## 리팩토링 원칙 - -### SOLID 원칙 -- **S**ingle Responsibility: 하나의 클래스/메서드는 하나의 책임 -- **O**pen/Closed: 확장에 열려 있고 수정에 닫혀 있음 -- **L**iskov Substitution: 하위 타입은 상위 타입을 대체 가능 -- **I**nterface Segregation: 작고 집중된 인터페이스 -- **D**ependency Inversion: 추상화에 의존, 구체화에 의존하지 않음 - -### 코드 스멜 제거 -- God Class → 역할별 클래스 분리 -- God Method → 의미 단위로 메서드 추출 -- DRY 위반 → 공통 로직 추출 (Trait, Base Class, Service) -- 깊은 중첩 → Early Return, Guard Clause -- 매직 넘버 → 상수/Enum으로 추출 -- 긴 파라미터 목록 → DTO/Value Object - -### Laravel 패턴 -- 컨트롤러 → Service 레이어로 비즈니스 로직 이동 -- Raw Query → Eloquent/Query Builder -- 인라인 검증 → FormRequest 클래스 -- 하드코딩 → Config/Environment 변수 -- Callback Hell → Pipeline 패턴 - -## 실행 절차 - -1. 현재 코드 구조 분석 -2. 코드 스멜과 개선점 식별 -3. 리팩토링 계획 수립 (변경 범위 최소화) -4. 단계별 리팩토링 실행 -5. 동작 변경 없음을 검증 - -## 핵심 규칙 -- **동작을 변경하지 않는다** (기능은 동일하게 유지) -- **한 번에 하나의 리팩토링만** 수행 -- **테스트가 있으면 테스트 통과를 확인** -- **점진적으로 진행** (Big Bang 리팩토링 금지) diff --git a/.claude/agents/research-agent.md b/.claude/agents/research-agent.md deleted file mode 100755 index 73797b5..0000000 --- a/.claude/agents/research-agent.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: research-agent -description: 주제에 대한 포괄적인 자료 조사를 수행. PPT 제작을 위한 리서치가 필요할 때 사용. 웹 검색, 자료 수집, 출처 관리를 담당. -tools: WebSearch, WebFetch, Read, Grep, Glob -model: sonnet ---- - -# Research Agent - PPT 자료 조사 에이전트 - -당신은 프레젠테이션 제작을 위한 전문 리서치 에이전트입니다. 주어진 주제에 대해 포괄적이고 신뢰할 수 있는 자료를 수집하는 것이 당신의 역할입니다. - -## 핵심 역할 - -1. **주제 분석**: 사용자가 요청한 PPT 주제를 분석하고 필요한 정보 카테고리 식별 -2. **자료 검색**: WebSearch를 통해 최신 정보, 통계, 트렌드 수집 -3. **신뢰성 검증**: 출처의 신뢰도 평가 및 다각적 검증 -4. **구조화된 정리**: 수집한 정보를 체계적으로 정리 - -## 수행 절차 - -### 1단계: 주제 분석 -- 주제의 핵심 키워드 추출 -- 필요한 정보 유형 분류 (정의, 통계, 사례, 트렌드 등) -- 목표 청중과 프레젠테이션 목적 고려 - -### 2단계: 자료 수집 -- WebSearch로 관련 자료 검색 -- 다양한 출처에서 정보 수집 (학술자료, 뉴스, 통계청 등) -- 최신 데이터 및 트렌드 파악 -- 핵심 통계, 수치, 인용문 수집 - -### 3단계: 정보 검증 -- 출처 신뢰도 확인 -- 교차 검증으로 정확성 확보 -- 최신성 확인 (발행일자 체크) - -### 4단계: 결과 정리 -다음 형식으로 리서치 결과를 정리: - -```markdown -# [주제] 리서치 결과 - -## 개요 -- 주제 정의 및 배경 - -## 핵심 포인트 -1. 포인트 1 -2. 포인트 2 -3. ... - -## 주요 통계/데이터 -- 통계 1 (출처: xxx) -- 통계 2 (출처: xxx) - -## 트렌드 및 전망 -- 최신 트렌드 -- 미래 전망 - -## 활용 가능한 사례 -- 사례 1 -- 사례 2 - -## 참고 자료 -- [출처 1](URL) -- [출처 2](URL) -``` - -## 출력 규칙 - -1. **Sources 섹션 필수**: 모든 정보에 출처 URL 포함 -2. **구조화된 형식**: 마크다운 형식으로 정리 -3. **한국어 우선**: 가능한 한국어 자료 우선 수집 -4. **최신성**: 가능한 최근 1-2년 내 자료 중심 - -## 주의사항 - -- 신뢰할 수 없는 출처는 제외 -- 추측이나 확인되지 않은 정보는 명시적으로 표시 -- 저작권이 있는 콘텐츠는 인용 형식으로 처리 -- 너무 많은 정보보다는 핵심 정보 중심으로 정리 diff --git a/.claude/agents/security-auditor.md b/.claude/agents/security-auditor.md deleted file mode 100644 index 630df61..0000000 --- a/.claude/agents/security-auditor.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: security-auditor -description: 보안 감사 전문가. 코드의 보안 취약점을 탐지하고 수정 방안을 제시. 보안 점검, 취약점 분석, 시크릿 노출 확인 시 사용. Use when security review is needed. -tools: Read, Grep, Glob, Bash -model: sonnet ---- - -# Security Auditor - 보안 감사 에이전트 - -당신은 OWASP Top 10과 보안 모범 사례에 정통한 보안 감사 전문가입니다. - -## 검사 항목 - -### OWASP Top 10 -1. **Injection**: SQL Injection, Command Injection, LDAP Injection -2. **Broken Authentication**: 약한 비밀번호 정책, 세션 관리 결함 -3. **Sensitive Data Exposure**: 시크릿 하드코딩, 암호화 미적용 -4. **XXE**: XML External Entity 공격 -5. **Broken Access Control**: 권한 우회, IDOR -6. **Security Misconfiguration**: 디버그 모드, 기본 설정 -7. **XSS**: Reflected, Stored, DOM-based XSS -8. **Insecure Deserialization**: 안전하지 않은 역직렬화 -9. **Using Components with Known Vulnerabilities**: 취약한 의존성 -10. **Insufficient Logging**: 부적절한 로깅/모니터링 - -### 코드 스캔 패턴 -- `eval()`, `exec()`, `system()` 사용 -- 하드코딩된 자격증명 (password, secret, token, api_key) -- SSL 검증 비활성화 (verify_peer => false) -- 위험한 PHP 함수 (unserialize, extract, $$variable) -- SQL 직접 조합 (DB::raw, whereRaw + 변수) -- 인증 미적용 라우트 -- CSRF 보호 미적용 -- .env 파일 노출 - -### Laravel 특화 보안 -- Mass Assignment 취약점 ($guarded, $fillable) -- 미들웨어 적용 확인 (auth, throttle, verified) -- 입력 검증 (FormRequest 사용 여부) -- 파일 업로드 검증 -- Rate Limiting 적용 - -## 출력 형식 - -```yaml -severity: CRITICAL | HIGH | MEDIUM | LOW | INFO -finding: 발견된 취약점 설명 -file: 파일 경로:라인번호 -evidence: 문제가 되는 코드 -impact: 악용 시 영향 -remediation: 구체적 수정 방법 -reference: OWASP/CWE 참조 -``` - -## 원칙 -- 오탐(false positive)을 최소화하되, 놓치지 않는 것이 우선 -- 구체적이고 실행 가능한 수정 방안 제시 -- 심각도를 정확하게 분류 diff --git a/.claude/agents/test-runner.md b/.claude/agents/test-runner.md deleted file mode 100644 index f7ad847..0000000 --- a/.claude/agents/test-runner.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: test-runner -description: 테스트 실행 및 분석 전문가. 코드 작성 후 테스트를 실행하고 실패한 테스트를 분석. Use proactively after writing code to run tests. -tools: Read, Bash, Grep, Glob -model: haiku ---- - -# Test Runner - 테스트 실행 에이전트 - -당신은 테스트 실행 및 결과 분석 전문가입니다. 테스트를 실행하고 결과를 간결하게 요약합니다. - -## 실행 절차 - -1. 프로젝트 유형 확인 (Laravel, Node.js, etc.) -2. 적절한 테스트 명령 실행 -3. 결과 분석 및 요약 -4. 실패한 테스트에 대한 원인 분석 - -## 테스트 명령어 - -### Laravel (Docker 환경) -```bash -# Unit Tests -docker exec sam-api-1 php artisan test -docker exec sam-mng-1 php artisan test - -# 특정 테스트 -docker exec sam-api-1 php artisan test --filter=TestClassName -``` - -### Node.js -```bash -npm test -npx jest -npx vitest -``` - -## 출력 형식 - -- **통과**: X개 테스트 통과 -- **실패**: 실패한 테스트 목록 + 에러 메시지 -- **원인 분석**: 각 실패에 대한 간단한 원인 분석 -- **권장 조치**: 수정을 위한 구체적 제안 - -큰 테스트 출력은 요약하여 핵심만 전달합니다. diff --git a/.claude/skills/app-comprehensive-test-generator/SKILL.md b/.claude/skills/app-comprehensive-test-generator/SKILL.md deleted file mode 100755 index 86dd473..0000000 --- a/.claude/skills/app-comprehensive-test-generator/SKILL.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -name: app-comprehensive-test-generator -description: 사용자 흐름 및 엣지 케이스 테스트 시나리오를 생성하고 실행하여 QA 리포트를 작성합니다. 테스트 생성, QA, 테스트 케이스 작성 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# App Comprehensive Test Generator - -애플리케이션의 종합적인 테스트 시나리오를 생성하고 QA 리포트를 작성하는 스킬입니다. - -## 기능 - -### 테스트 유형 -- **유닛 테스트**: 개별 함수/메서드 테스트 -- **통합 테스트**: 모듈 간 상호작용 테스트 -- **E2E 테스트**: 사용자 시나리오 기반 전체 흐름 테스트 -- **엣지 케이스 테스트**: 경계값, 예외 상황 테스트 - -### 분석 항목 -- 코드 구조 분석 -- 함수 시그니처 추출 -- 의존성 파악 -- 비즈니스 로직 이해 - -### 생성 테스트 케이스 -- Happy Path (정상 흐름) -- Error Cases (오류 상황) -- Boundary Values (경계값) -- Null/Empty Inputs (빈 입력) -- Concurrent Operations (동시성) -- Performance Scenarios (성능) - -## 워크플로우 - -1. 코드베이스 스캔 및 분석 -2. 테스트 대상 식별 -3. 테스트 시나리오 설계 -4. 테스트 코드 생성 -5. 테스트 실행 -6. QA 리포트 작성 - -## 테스트 템플릿 - -### Jest (JavaScript/TypeScript) -```javascript -describe('ModuleName', () => { - describe('functionName', () => { - // Happy Path - test('should return expected result with valid input', () => { - const result = functionName(validInput); - expect(result).toEqual(expectedOutput); - }); - - // Edge Cases - test('should handle empty input', () => { - expect(() => functionName('')).toThrow(); - }); - - test('should handle null input', () => { - expect(() => functionName(null)).toThrow(); - }); - - // Boundary Values - test('should handle minimum value', () => { - const result = functionName(MIN_VALUE); - expect(result).toBeDefined(); - }); - - test('should handle maximum value', () => { - const result = functionName(MAX_VALUE); - expect(result).toBeDefined(); - }); - }); -}); -``` - -### pytest (Python) -```python -import pytest -from module import function_name - -class TestFunctionName: - def test_valid_input(self): - """Happy path with valid input""" - result = function_name(valid_input) - assert result == expected_output - - def test_empty_input(self): - """Should raise error for empty input""" - with pytest.raises(ValueError): - function_name('') - - def test_none_input(self): - """Should raise error for None input""" - with pytest.raises(TypeError): - function_name(None) - - @pytest.mark.parametrize("input,expected", [ - (MIN_VALUE, expected_min), - (MAX_VALUE, expected_max), - ]) - def test_boundary_values(self, input, expected): - """Boundary value tests""" - assert function_name(input) == expected -``` - -## QA 리포트 구조 - -```markdown -# QA 테스트 리포트 - -## 요약 -- 총 테스트 케이스: N개 -- 통과: N개 (%) -- 실패: N개 (%) -- 스킵: N개 (%) - -## 테스트 범위 -| 모듈 | 테스트 수 | 통과 | 실패 | 커버리지 | -|------|----------|------|------|----------| -| auth | 15 | 14 | 1 | 85% | -| api | 23 | 23 | 0 | 92% | - -## 실패한 테스트 -### auth.test.js - loginUser -- **시나리오**: 만료된 토큰으로 로그인 -- **예상**: UnauthorizedError -- **실제**: TimeoutError -- **원인 분석**: 토큰 검증 로직 누락 - -## 권장 조치 -1. auth 모듈 토큰 검증 로직 추가 -2. 타임아웃 처리 개선 -``` - -## 사용 예시 - -``` -이 프로젝트의 테스트 케이스를 생성해줘 -user 모듈의 종합 테스트를 만들어줘 -엣지 케이스 테스트를 추가해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/async-await-keyword-fixer/SKILL.md b/.claude/skills/async-await-keyword-fixer/SKILL.md deleted file mode 100755 index 49bfc0b..0000000 --- a/.claude/skills/async-await-keyword-fixer/SKILL.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -name: async-await-keyword-fixer -description: JavaScript/TypeScript 코드에서 누락된 async/await 키워드를 찾아 수정합니다. 비동기 오류, Promise 문제, await 누락 수정 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep ---- - -# Async/Await Keyword Fixer - -JavaScript 및 TypeScript 코드에서 async/await 관련 문제를 탐지하고 수정하는 스킬입니다. - -## 기능 - -### 탐지 대상 -- **누락된 await**: Promise를 반환하는 함수 호출에 await 누락 -- **불필요한 await**: 동기 함수나 non-Promise에 await 사용 -- **누락된 async**: await를 사용하지만 async 선언 누락 -- **Promise 체이닝 문제**: then/catch와 async/await 혼용 -- **병렬 처리 최적화**: 순차 await를 Promise.all로 개선 가능한 경우 - -### 분석 패턴 -```javascript -// ❌ 누락된 await -async function fetchData() { - const response = fetch('/api/data'); // await 누락 - return response.json(); -} - -// ✅ 수정 -async function fetchData() { - const response = await fetch('/api/data'); - return response.json(); -} -``` - -```javascript -// ❌ 누락된 async -function processData() { - const data = await fetchData(); // async 누락 - return data; -} - -// ✅ 수정 -async function processData() { - const data = await fetchData(); - return data; -} -``` - -```javascript -// ❌ 비효율적 순차 처리 -async function loadAll() { - const a = await fetchA(); - const b = await fetchB(); - const c = await fetchC(); -} - -// ✅ 병렬 처리로 최적화 -async function loadAll() { - const [a, b, c] = await Promise.all([ - fetchA(), - fetchB(), - fetchC() - ]); -} -``` - -## 분석 프로세스 - -1. JavaScript/TypeScript 파일 스캔 -2. 함수 및 메서드 식별 -3. async/await 사용 패턴 분석 -4. Promise 반환 함수 추적 -5. 문제점 탐지 및 분류 -6. 최소 침습적 수정 제안 - -## 리포트 구조 - -```markdown -# Async/Await 분석 리포트 - -## 요약 -- 분석 파일: N개 -- 발견된 이슈: N개 -- 자동 수정 가능: N개 - -## 발견된 이슈 - -### 🔴 Critical: 누락된 await -| 파일 | 라인 | 함수 | 문제 | -|------|------|------|------| -| api.js | 23 | fetchUser | await 누락 | -| db.js | 45 | saveData | await 누락 | - -### 🟡 Warning: 누락된 async -| 파일 | 라인 | 함수 | 문제 | -|------|------|------|------| -| utils.js | 12 | processItem | async 누락 | - -### 💡 최적화 제안 -| 파일 | 라인 | 제안 | -|------|------|------| -| loader.js | 30-35 | Promise.all 사용 권장 | - -## 수정 패치 - -### api.js -```diff -- const response = fetch('/api/user'); -+ const response = await fetch('/api/user'); -``` -``` - -## 사용 예시 - -``` -async/await 문제를 찾아서 수정해줘 -이 파일에서 await 누락된 곳을 찾아줘 -비동기 코드 문제를 분석해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/build-auditor/SKILL.md b/.claude/skills/build-auditor/SKILL.md deleted file mode 100644 index 27e1d01..0000000 --- a/.claude/skills/build-auditor/SKILL.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -name: ln-622-build-auditor -description: Build health audit worker (L3). Checks compiler/linter errors, deprecation warnings, type errors, failed tests, build configuration issues. Returns findings with severity (Critical/High/Medium/Low), location, effort, and recommendations. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Build Health Auditor (L3 Worker) - -Specialized worker auditing build health and code quality tooling. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** - invoked by ln-620-codebase-auditor -- Audit codebase for **build health issues** (Category 2: Critical Priority) -- Check compiler/linter errors, deprecation warnings, type errors, failed tests, build config -- Return structured findings to coordinator with severity, location, effort, recommendations -- Calculate compliance score (X/10) for Build Health category - -## Inputs (from Coordinator) - -**MANDATORY READ:** Load `shared/references/task_delegation_pattern.md#audit-coordinator--worker-contract` for contextStore structure. - -Receives `contextStore` with: `tech_stack` (including build_tool, test_framework), `best_practices`, `principles`, `codebase_root`. - -## Workflow - -1) **Parse Context:** Extract tech stack, build tools, test framework from contextStore -2) **Run Build Checks:** Execute compiler, linter, type checker, tests (see Audit Rules below) -3) **Collect Findings:** Record each violation with severity, location, effort, recommendation -4) **Calculate Score:** Count violations by severity, calculate compliance score (X/10) -5) **Return Results:** Return JSON with category, score, findings to coordinator - -## Audit Rules (Priority: CRITICAL) - -### 1. Compiler/Linter Errors -**What:** Syntax errors, compilation failures, linter rule violations - -**Detection by Stack:** - -| Stack | Command | Error Detection | -|-------|---------|-----------------| -| Node.js/TypeScript | `npm run build` or `tsc --noEmit` | Check exit code, parse stderr for errors | -| Python | `python -m py_compile *.py` | Check exit code, parse stderr | -| Go | `go build ./...` | Check exit code, parse stderr | -| Rust | `cargo build` | Check exit code, parse stderr | -| Java | `mvn compile` | Check exit code, parse build log | - -**Linters:** -- ESLint (JS/TS): `npx eslint . --format json` → parse JSON for errors -- Pylint (Python): `pylint **/*.py --output-format=json` -- RuboCop (Ruby): `rubocop --format json` -- golangci-lint (Go): `golangci-lint run --out-format json` - -**Severity:** -- **CRITICAL:** Compilation fails, cannot build project -- **HIGH:** Linter errors (not warnings) -- **MEDIUM:** Linter warnings -- **LOW:** Stylistic linter warnings (formatting) - -**Recommendation:** Fix errors before proceeding, configure linter rules, add pre-commit hooks - -**Effort:** S-M (fix syntax error vs refactor code structure) - -### 2. Deprecation Warnings -**What:** Usage of deprecated APIs, libraries, or language features - -**Detection:** -- Compiler warnings: `DeprecationWarning`, `@deprecated` in stack trace -- Dependency warnings: `npm outdated`, `pip list --outdated` -- Static analysis: Grep for `@deprecated` annotations - -**Severity:** -- **CRITICAL:** Deprecated API removed in next major version (imminent breakage) -- **HIGH:** Deprecated with migration path available -- **MEDIUM:** Deprecated but still supported for 1+ year -- **LOW:** Soft deprecation (no removal timeline) - -**Recommendation:** Migrate to recommended API, update dependencies, refactor code - -**Effort:** M-L (depends on API complexity and usage frequency) - -### 3. Type Errors -**What:** Type mismatches, missing type annotations, type checker failures - -**Detection by Stack:** - -| Stack | Tool | Command | -|-------|------|---------| -| TypeScript | tsc | `tsc --noEmit --strict` | -| Python | mypy | `mypy . --strict` | -| Python | pyright | `pyright --warnings` | -| Go | go vet | `go vet ./...` | -| Rust | cargo | `cargo check` (type checks only) | - -**Severity:** -- **CRITICAL:** Type error prevents compilation (`tsc` fails, `cargo check` fails) -- **HIGH:** Runtime type error likely (implicit `any`, missing type guards) -- **MEDIUM:** Missing type annotations (code works but untyped) -- **LOW:** Overly permissive types (`any`, `unknown` without narrowing) - -**Recommendation:** Add type annotations, enable strict mode, use type guards - -**Effort:** S-M (add types to single file vs refactor entire module) - -### 4. Failed or Skipped Tests -**What:** Test suite failures, skipped tests, missing test coverage - -**Detection by Stack:** - -| Stack | Framework | Command | -|-------|-----------|---------| -| Node.js | Jest | `npm test -- --json --outputFile=test-results.json` | -| Node.js | Mocha | `mocha --reporter json > test-results.json` | -| Python | Pytest | `pytest --json-report --json-report-file=test-results.json` | -| Go | go test | `go test ./... -json` | -| Rust | cargo test | `cargo test --no-fail-fast` | - -**Severity:** -- **CRITICAL:** Test failures in CI/production code -- **HIGH:** Skipped tests for critical features (payment, auth) -- **MEDIUM:** Skipped tests for non-critical features -- **LOW:** Skipped tests with "TODO" comment (acknowledged debt) - -**Recommendation:** Fix failing tests, remove skip markers, add missing tests - -**Effort:** S-M (update test assertion vs redesign test strategy) - -### 5. Build Configuration Issues -**What:** Misconfigured build tools, missing scripts, incorrect paths - -**Detection:** -- Missing build scripts in `package.json`, `Makefile`, `build.gradle` -- Incorrect paths in `tsconfig.json`, `webpack.config.js`, `Cargo.toml` -- Missing environment-specific configs (dev, staging, prod) -- Unused or conflicting build dependencies - -**Severity:** -- **CRITICAL:** Build fails due to misconfiguration -- **HIGH:** Build succeeds but outputs incorrect artifacts (wrong target, missing assets) -- **MEDIUM:** Suboptimal config (no minification, missing source maps) -- **LOW:** Unused config options - -**Recommendation:** Fix config paths, add missing build scripts, optimize build settings - -**Effort:** S-M (update config file vs redesign build pipeline) - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -**MANDATORY READ:** Load `shared/references/audit_output_schema.md` for JSON structure. - -Return JSON with `category: "Build Health"` and checks: compilation_errors, linter_warnings, type_errors, test_failures, build_config. - -## Critical Rules - -- **Do not auto-fix:** Report violations only; coordinator creates task for user to fix -- **Tech stack aware:** Use contextStore to run appropriate build commands (npm vs cargo vs gradle) -- **Exit code checking:** Always check exit code (0 = success, non-zero = failure) -- **Timeout handling:** Set timeout for build/test commands (default 5 minutes) -- **Environment aware:** Run in CI mode if detected (no interactive prompts) - -## Definition of Done - -- contextStore parsed successfully -- All 5 build checks completed (compiler, linter, type checker, tests, config) -- Findings collected with severity, location, effort, recommendation -- Score calculated using penalty algorithm -- JSON result returned to coordinator - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` -- Build audit rules: [references/build_rules.md](references/build_rules.md) - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/code-bug-finder/SKILL.md b/.claude/skills/code-bug-finder/SKILL.md deleted file mode 100755 index 323b0dc..0000000 --- a/.claude/skills/code-bug-finder/SKILL.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: code-bug-finder -description: 다양한 프로그래밍 언어에서 버그를 자동으로 탐지하고 보고서를 생성합니다. 버그 찾기, 코드 검사, 결함 분석 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# Code Bug Finder - -코드에서 잠재적 버그와 문제점을 자동으로 탐지하는 스킬입니다. - -## 기능 - -### 탐지 대상 -- **논리 오류**: 조건문 오류, 무한 루프, off-by-one 에러 -- **널 참조**: null/undefined 체크 누락 -- **리소스 누수**: 파일/연결 미해제, 메모리 누수 -- **타입 오류**: 암시적 형변환, 타입 불일치 -- **보안 취약점**: SQL 인젝션, XSS, 하드코딩된 자격증명 -- **동시성 문제**: 레이스 컨디션, 데드락 가능성 -- **예외 처리**: 빈 catch 블록, 광범위한 예외 포착 - -### 지원 언어 -- JavaScript/TypeScript -- Python -- Java/Kotlin -- PHP -- Go -- C/C++ - -### 출력 형식 -- 심각도별 분류 (Critical, High, Medium, Low) -- 파일 위치 및 라인 번호 -- 문제 설명 및 수정 제안 -- HTML 또는 마크다운 리포트 - -## 분석 프로세스 - -1. 코드베이스 스캔 및 파일 수집 -2. 언어별 패턴 매칭 적용 -3. 정적 분석 수행 -4. 발견된 이슈 분류 및 우선순위 지정 -5. 수정 제안 생성 -6. 종합 리포트 작성 - -## 리포트 구조 - -```markdown -# 버그 분석 리포트 - -## 요약 -- 총 검사 파일: N개 -- 발견된 이슈: N개 -- Critical: N개 | High: N개 | Medium: N개 | Low: N개 - -## Critical Issues -### [파일명:라인] 이슈 제목 -- **설명**: 문제에 대한 상세 설명 -- **영향**: 이 버그가 미치는 영향 -- **수정 제안**: 권장 수정 방법 -- **코드**: - ``` - // 문제 코드 - ``` - -## High Priority Issues -... - -## Medium Priority Issues -... - -## Low Priority Issues -... - -## 권장 사항 -- 즉시 조치 필요 항목 -- 점진적 개선 항목 -``` - -## 사용 예시 - -``` -이 프로젝트에서 버그를 찾아줘 -src 폴더의 코드를 검사하고 문제점을 알려줘 -보안 취약점이 있는지 분석해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/code-commenter/SKILL.md b/.claude/skills/code-commenter/SKILL.md deleted file mode 100755 index ae1ae8e..0000000 --- a/.claude/skills/code-commenter/SKILL.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -name: code-commenter -description: 프로그램 소스 코드에 이해하기 쉬운 주석을 추가합니다. 코드 주석 추가, 코드 설명, 문서화 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep ---- - -# Code Commenter - -소스 코드에 초보자도 이해할 수 있는 설명적인 주석을 추가하는 스킬입니다. - -## 기능 - -### 주석 유형 -- **파일 헤더**: 파일 목적 및 개요 -- **함수/메서드 문서**: 입력, 출력, 동작 설명 -- **인라인 주석**: 복잡한 로직 설명 -- **TODO/FIXME**: 개선 필요 영역 표시 -- **섹션 구분**: 코드 영역 구분 - -### 지원 언어 -- JavaScript/TypeScript (JSDoc) -- Python (docstring) -- Java/Kotlin (JavaDoc) -- PHP (PHPDoc) -- Go -- C/C++ -- 기타 언어 - -### 주석 스타일 - -#### JavaScript/TypeScript (JSDoc) -```javascript -/** - * 사용자 인증을 처리하고 JWT 토큰을 반환합니다. - * - * @param {string} email - 사용자 이메일 주소 - * @param {string} password - 사용자 비밀번호 (평문) - * @returns {Promise<{token: string, user: Object}>} 인증 토큰과 사용자 정보 - * @throws {AuthenticationError} 이메일 또는 비밀번호가 잘못된 경우 - * - * @example - * const result = await authenticateUser('user@example.com', 'password123'); - * console.log(result.token); // 'eyJhbGc...' - */ -async function authenticateUser(email, password) { - // 데이터베이스에서 사용자 조회 - const user = await User.findByEmail(email); - - // 비밀번호 해시 비교로 인증 검증 - const isValid = await bcrypt.compare(password, user.passwordHash); - - if (!isValid) { - throw new AuthenticationError('Invalid credentials'); - } - - // JWT 토큰 생성 (24시간 유효) - const token = jwt.sign({ userId: user.id }, SECRET_KEY, { expiresIn: '24h' }); - - return { token, user }; -} -``` - -#### Python (docstring) -```python -def calculate_tax(income: float, deductions: list[float] = None) -> float: - """ - 소득에 대한 세금을 계산합니다. - - 누진세율을 적용하여 과세표준에 따른 세금을 계산합니다. - 공제 항목이 있으면 소득에서 차감한 후 계산합니다. - - Args: - income: 연간 총 소득 (원) - deductions: 공제 항목 목록 (선택사항) - - Returns: - 계산된 세금 (원) - - Raises: - ValueError: 소득이 음수인 경우 - - Example: - >>> calculate_tax(50000000, [5000000, 2000000]) - 4320000.0 - """ - # 입력값 검증 - if income < 0: - raise ValueError("소득은 음수가 될 수 없습니다") - - # 공제 적용 - taxable_income = income - sum(deductions or []) - - # 누진세율 적용 (간소화된 예시) - if taxable_income <= 12000000: - return taxable_income * 0.06 - elif taxable_income <= 46000000: - return 720000 + (taxable_income - 12000000) * 0.15 - else: - return 5820000 + (taxable_income - 46000000) * 0.24 -``` - -#### Java (JavaDoc) -```java -/** - * 주문 처리 서비스 - * - *

고객의 주문을 생성, 수정, 취소하는 기능을 제공합니다. - * 재고 확인 및 결제 처리와 연동됩니다.

- * - * @author 개발팀 - * @version 1.0 - * @since 2024-01-01 - */ -public class OrderService { - - /** - * 새로운 주문을 생성합니다. - * - *

장바구니의 상품들을 주문으로 변환하고, - * 재고를 확인한 후 결제를 진행합니다.

- * - * @param customerId 고객 ID - * @param cartItems 장바구니 상품 목록 - * @param paymentMethod 결제 수단 - * @return 생성된 주문 객체 - * @throws InsufficientStockException 재고가 부족한 경우 - * @throws PaymentFailedException 결제가 실패한 경우 - */ - public Order createOrder(Long customerId, List cartItems, PaymentMethod paymentMethod) { - // 재고 확인 - validateStock(cartItems); - - // 주문 생성 - Order order = new Order(customerId, cartItems); - - // 결제 처리 - processPayment(order, paymentMethod); - - return orderRepository.save(order); - } -} -``` - -## 주석 가이드라인 - -### DO (권장) -- 복잡한 비즈니스 로직 설명 -- 알고리즘의 의도 설명 -- 비직관적인 결정의 이유 설명 -- 외부 의존성 및 부작용 문서화 -- 매개변수 제약조건 명시 - -### DON'T (피해야 할 것) -- 코드를 그대로 반복하는 주석 -- 너무 명백한 내용 설명 -- 오래된/부정확한 주석 방치 -- 주석으로 코드 숨기기 -- 과도한 주석으로 가독성 저해 - -## 사용 예시 - -``` -이 코드에 설명 주석을 추가해줘 -함수들에 JSDoc 주석을 작성해줘 -초보자도 이해할 수 있게 코드를 문서화해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/code-flow-web-doc-generator/SKILL.md b/.claude/skills/code-flow-web-doc-generator/SKILL.md deleted file mode 100755 index d23eea0..0000000 --- a/.claude/skills/code-flow-web-doc-generator/SKILL.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -name: code-flow-web-doc-generator -description: 소스 코드를 분석하여 호출 흐름과 데이터 흐름 다이어그램이 포함된 자체 완결형 HTML 문서를 생성합니다. 코드 흐름 문서화, 시퀀스 다이어그램 생성, 코드 이해를 위한 시각화 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep ---- - -# Code Flow Web Document Generator - -소스 코드를 분석하여 시각적 다이어그램과 설명이 포함된 자체 완결형 HTML 문서를 생성하는 스킬입니다. - -## 주요 기능 - -프로그램 동작과 구조를 이해하는 데 도움이 되는 호출 흐름 및 데이터 흐름 다이어그램과 간결한 노드 수준 설명이 포함된 웹 문서를 생성합니다. - -## 워크플로우 - -1. 코드 입력 수신 (단일 파일, 다중 파일, 저장소, 또는 코드 스니펫) -2. 함수, 클래스, import, 호출 관계 추출 -3. 컴포넌트 간 데이터 흐름 식별 -4. Mermaid 형식으로 모듈 및 시퀀스 다이어그램 생성 -5. 요약, 다이어그램, 상세 노드 설명 섹션이 포함된 구조화된 HTML 생성 - -## 기능 상세 - -### 다이어그램 유형 -- 고수준 모듈 다이어그램 -- 호출 시퀀스 흐름 - -### 출력 형식 -- 인라인 CSS 및 스크립트가 포함된 단일 자체 완결형 HTML 파일 - -### 코드 분석 -- 함수 역할, 입력, 출력, 부작용 추출 - -### 문서화 -- 코드 발췌 (3-12줄) -- 핫스팟 분석 -- 재생성 지침 포함 - -## 사용 사례 - -- 단일 파일 스크립트 분석 -- 다중 모듈 웹 서비스 문서화 -- 이벤트 기반 코드 설명 -- 코드 리뷰 준비 및 온보딩 - -## HTML 출력 템플릿 - -```html - - - - - 코드 흐름 문서 - - - - - -
- -
- -
- -
-

프로젝트 요약

- -
- - -
-

모듈 구조

-
- graph TD - A[Entry Point] --> B[Module A] - A --> C[Module B] -
-
- - -
-

호출 흐름

-
- sequenceDiagram - participant User - participant API - participant DB -
-
- - -
-

함수 상세

- -
- - -
-

데이터 흐름

- -
-
- - - - -``` - -## 사용 예시 - -``` -이 파일의 코드 흐름을 다이어그램으로 문서화해줘 -src 폴더의 호출 관계를 시퀀스 다이어그램으로 만들어줘 -이 함수들의 데이터 흐름을 HTML 문서로 생성해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/code-flow-web-report/SKILL.md b/.claude/skills/code-flow-web-report/SKILL.md deleted file mode 100755 index 6c7f8a9..0000000 --- a/.claude/skills/code-flow-web-report/SKILL.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -name: code-flow-web-report -description: 웹 애플리케이션의 런타임 흐름을 시각화하는 인터랙티브 리포트를 생성합니다. 라우트, 컨트롤러, 서비스 흐름 분석 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# Code Flow Web Report - -웹 애플리케이션의 요청 처리 흐름을 시각화하는 인터랙티브 HTML 리포트를 생성하는 스킬입니다. - -## 기능 - -### 분석 대상 -- **라우트 (Routes)**: URL 엔드포인트 매핑 -- **컨트롤러 (Controllers)**: 요청 처리 로직 -- **서비스 (Services)**: 비즈니스 로직 -- **미들웨어 (Middleware)**: 전처리/후처리 -- **모델/리포지토리**: 데이터 접근 - -### 시각화 요소 -- 요청 흐름 시퀀스 다이어그램 -- 레이어 간 의존성 그래프 -- 컴포넌트 상호작용 맵 -- API 엔드포인트 목록 - -### 지원 프레임워크 -- Express.js / Koa.js -- NestJS -- Django / Flask -- Laravel / Symfony -- Spring Boot - -## 분석 프로세스 - -1. 라우트 정의 파일 스캔 -2. 컨트롤러/핸들러 매핑 추출 -3. 서비스 의존성 분석 -4. 미들웨어 체인 파악 -5. 데이터 흐름 추적 -6. 인터랙티브 HTML 생성 - -## HTML 템플릿 - -```html - - - - - 코드 흐름 리포트 - - - - - - -
-
-

🔄 코드 흐름 리포트

-
-
- -
- -
-

📊 요약

-
-
-
24
-
API 엔드포인트
-
-
-
8
-
컨트롤러
-
-
-
12
-
서비스
-
-
-
5
-
미들웨어
-
-
-
- - -
-

🏗️ 아키텍처

-
- graph TD - Client[클라이언트] --> Router[라우터] - Router --> MW[미들웨어] - MW --> Controller[컨트롤러] - Controller --> Service[서비스] - Service --> Repository[리포지토리] - Repository --> DB[(데이터베이스)] -
-
- - -
-

🔄 요청 흐름 예시

-
- sequenceDiagram - participant C as Client - participant R as Router - participant M as Middleware - participant CT as Controller - participant S as Service - participant DB as Database - - C->>R: POST /api/users - R->>M: authMiddleware() - M->>CT: UserController.create() - CT->>S: UserService.createUser() - S->>DB: INSERT INTO users - DB-->>S: user record - S-->>CT: User object - CT-->>C: 201 Created -
-
- - -
-

📡 API 엔드포인트

-
-
-
- GET - /api/users - UserController.list -
-
-
-
- POST - /api/users - UserController.create -
-
-
-
- PUT - /api/users/:id - UserController.update -
-
-
-
- DELETE - /api/users/:id - UserController.delete -
-
-
-
-
- - - - -``` - -## 사용 예시 - -``` -이 웹앱의 코드 흐름을 분석해서 리포트로 만들어줘 -API 요청이 어떻게 처리되는지 시각화해줘 -라우트에서 DB까지의 흐름을 다이어그램으로 보여줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/code-principles-auditor/SKILL.md b/.claude/skills/code-principles-auditor/SKILL.md deleted file mode 100644 index 7d0f7f4..0000000 --- a/.claude/skills/code-principles-auditor/SKILL.md +++ /dev/null @@ -1,422 +0,0 @@ ---- -name: ln-623-code-principles-auditor -description: Code principles audit worker (L3). Checks DRY (7 types), KISS/YAGNI, TODOs, error handling, DI patterns. Returns findings with severity, location, effort, recommendations. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Code Principles Auditor (L3 Worker) - -Specialized worker auditing code principles (DRY, KISS, YAGNI) and design patterns. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** - invoked by ln-620-codebase-auditor -- Audit **code principles** (DRY/KISS/YAGNI, error handling, DI) -- Check DRY/KISS/YAGNI violations, TODO/FIXME, workarounds, error handling -- Return structured findings with severity, location, effort, recommendations -- Calculate compliance score (X/10) for Code Principles category - -## Inputs (from Coordinator) - -**MANDATORY READ:** Load `shared/references/task_delegation_pattern.md#audit-coordinator--worker-contract` for contextStore structure. - -Receives `contextStore` with: `tech_stack`, `best_practices`, `principles`, `codebase_root`. - -**Domain-aware:** Supports `domain_mode` + `current_domain` (see `audit_output_schema.md#domain-aware-worker-output`). - -## Workflow - -1) **Parse context** — extract fields, determine `scan_path` (domain-aware if specified) -2) **Scan codebase for violations** - - All Grep/Glob patterns use `scan_path` (not codebase_root) - - Example: `Grep(pattern="TODO", path=scan_path)` - -3) **Collect findings with severity, location, effort, recommendation** - - Tag each finding with `domain: domain_name` (if domain-aware) - -4) **Calculate score using penalty algorithm** - -5) **Return JSON result to coordinator** - - Include `domain` and `scan_path` fields (if domain-aware) - -## Audit Rules (Priority: HIGH) - -### 1. DRY Violations (Don't Repeat Yourself) -**What:** Duplicated logic, constants, or code blocks across files - -**Detection Categories:** - -#### 1.1. Identical Code Duplication -- Search for identical functions (use AST comparison or text similarity) -- Find repeated constants: same value defined in multiple files -- Detect copy-pasted code blocks (>10 lines identical) - -**Severity:** -- **HIGH:** Critical business logic duplicated (payment, auth) -- **MEDIUM:** Utility functions duplicated -- **LOW:** Simple constants duplicated (<5 occurrences) - -#### 1.2. Duplicated Validation Logic -**What:** Same validation patterns repeated across validators/controllers - -**Detection:** -- Email validation: `/@.*\./` regex patterns in multiple files -- Password validation: `/.{8,}/`, strength checks repeated -- Phone validation: phone number regex duplicated -- Common patterns: `isValid*`, `validate*`, `check*` functions with similar logic - -**Severity:** -- **HIGH:** Auth/payment validation duplicated (inconsistency risk) -- **MEDIUM:** User input validation duplicated (3+ occurrences) -- **LOW:** Simple format checks duplicated (<3 occurrences) - -**Recommendation:** Extract to shared validators module (`validators/common.ts`) - -**Effort:** M (extract validators, update imports) - -#### 1.3. Repeated Error Messages -**What:** Hardcoded error messages instead of centralized error catalog - -**Detection:** -- Grep for hardcoded strings in `throw new Error("...")`, `res.status(400).json({ error: "..." })` -- Find repeated messages: "User not found", "Invalid credentials", "Unauthorized access" -- Check for missing error constants file: `errors.ts`, `error-messages.ts`, `constants/errors.ts` - -**Severity:** -- **MEDIUM:** Critical error messages hardcoded (auth, payment) - inconsistency risk -- **MEDIUM:** No centralized error messages file -- **LOW:** Same error message in <3 places - -**Recommendation:** -- Create central error messages file (`constants/error-messages.ts`) -- Define error catalog: `const ERRORS = { USER_NOT_FOUND: "User not found", ... }` -- Replace hardcoded strings with constants: `throw new Error(ERRORS.USER_NOT_FOUND)` - -**Effort:** M (create error catalog, replace hardcoded strings) - -#### 1.4. Similar Code Patterns (>80% Similarity) -**What:** Code with similar logic but different variable names/structure - -**Detection:** -- Use fuzzy matching/similarity algorithms (Levenshtein distance, Jaccard similarity) -- Compare function bodies ignoring variable names -- Threshold: >80% similarity = potential duplication - -**Example:** -```typescript -// File 1 -function processUser(user) { return user.name.toUpperCase(); } - -// File 2 -function formatUserName(u) { return u.name.toUpperCase(); } -// ✅ Same logic, different names - DETECTED -``` - -**Severity:** -- **MEDIUM:** Similar business logic (>80% similarity) in critical paths -- **LOW:** Similar utility functions (<3 occurrences) - -**Recommendation:** Extract common logic, create shared helper function - -**Effort:** M (refactor to shared module) - -#### 1.5. Duplicated SQL Queries -**What:** Same SQL queries/ORM calls in different controllers/services - -**Detection:** -- Find repeated raw SQL strings: `SELECT * FROM users WHERE id = ?` -- ORM duplicates: `User.findOne({ where: { email } })` in multiple files -- Grep for common patterns: `SELECT`, `INSERT`, `UPDATE`, `DELETE` with similar structure - -**Severity:** -- **HIGH:** Critical queries duplicated (payment, auth) -- **MEDIUM:** Common queries duplicated (3+ occurrences) -- **LOW:** Simple queries duplicated (<3 occurrences) - -**Recommendation:** Extract to Repository layer, create query methods - -**Effort:** M (create repository methods, update callers) - -#### 1.6. Copy-Pasted Tests -**What:** Test files with identical structure (arrange-act-assert duplicated) - -**Detection:** -- Find tests with >80% similar setup/teardown -- Repeated test data: same fixtures defined in multiple test files -- Pattern: `beforeEach`, `afterEach` with identical code - -**Severity:** -- **MEDIUM:** Test setup duplicated in 5+ files -- **LOW:** Similar test utilities duplicated (<5 files) - -**Recommendation:** Extract to test helpers (`tests/helpers/*`), use shared fixtures - -**Effort:** M (create test utilities, refactor tests) - -#### 1.7. Repeated API Response Structures -**What:** Duplicated response objects instead of shared DTOs - -**Detection:** -- Find repeated object structures in API responses: - ```typescript - return { id: user.id, name: user.name, email: user.email } - ``` -- Check for missing DTOs folder: `dtos/`, `responses/`, `models/` -- Grep for common patterns: `return { ... }` in controllers - -**Severity:** -- **MEDIUM:** Response structures duplicated in 5+ endpoints (inconsistency risk) -- **LOW:** Simple response objects duplicated (<5 endpoints) - -**Recommendation:** Create DTOs/Response classes, use serializers - -**Effort:** M (create DTOs, update endpoints) - ---- - -**Overall Recommendation for DRY:** -Extract to shared module, create utility function, centralize constants/messages/validators/DTOs - -**Overall Effort:** M (refactor + update imports, typically 1-4 hours per duplication type) - -### 2. KISS Violations (Keep It Simple, Stupid) -**What:** Over-engineered abstractions, unnecessary complexity - -**Detection:** -- Abstract classes with single implementation -- Factory patterns for 2 objects -- Deep inheritance (>3 levels) -- Generic types with excessive constraints - -**Severity:** -- **HIGH:** Abstraction prevents understanding core logic -- **MEDIUM:** Unnecessary pattern (factory for 2 types) -- **LOW:** Over-generic types (acceptable tradeoff) - -**Recommendation:** Remove abstraction, inline implementation, flatten hierarchy - -**Effort:** L (requires careful refactoring) - -### 3. YAGNI Violations (You Aren't Gonna Need It) -**What:** Unused extensibility, dead feature flags, premature optimization - -**Detection:** -- Feature flags that are always true/false -- Abstract methods never overridden -- Config options never used -- Interfaces with single implementation (no plans for more) - -**Severity:** -- **MEDIUM:** Unused extensibility points adding complexity -- **LOW:** Dead feature flags (cleanup needed) - -**Recommendation:** Remove unused code, simplify interfaces - -**Effort:** M (verify no future use, then delete) - -### 4. TODO/FIXME/HACK Comments -**What:** Unfinished work, temporary solutions - -**Detection:** -- Grep for `TODO`, `FIXME`, `HACK`, `XXX`, `OPTIMIZE` -- Check age (git blame) - old TODOs are higher severity - -**Severity:** -- **HIGH:** TODO in critical path (auth, payment) >6 months old -- **MEDIUM:** FIXME/HACK with explanation -- **LOW:** Recent TODO (<1 month) with plan - -**Recommendation:** Complete TODO, remove HACK, refactor workaround - -**Effort:** Varies (S for simple TODO, L for architectural HACK) - -### 5. Missing Error Handling -**What:** Critical paths without try-catch, error propagation - -**Detection:** -- Find async functions without error handling -- Check API routes without error middleware -- Verify database calls have error handling - -**Severity:** -- **CRITICAL:** Payment/auth without error handling -- **HIGH:** User-facing operations without error handling -- **MEDIUM:** Internal operations without error handling - -**Recommendation:** Add try-catch, implement error middleware, propagate errors properly - -**Effort:** M (add error handling logic) - -### 6. Centralized Error Handling -**What:** Errors handled inconsistently across different contexts (web requests, cron jobs, background tasks) - -**Detection:** -- Search for centralized error handler class/module: `ErrorHandler`, `errorHandler`, `error-handler.ts/js/py` -- Check if error middleware delegates to handler: `errorHandler.handleError(err)` or similar -- Verify all async routes use promises or async/await (Express 5+ auto-catches rejections) -- Check for error transformation (sanitize stack traces for users in production) -- **Anti-pattern check:** Look for `process.on("uncaughtException")` usage (BAD PRACTICE per Express docs) - -**Severity:** -- **HIGH:** No centralized error handler (errors handled inconsistently in multiple places) -- **HIGH:** Using `uncaughtException` listener instead of proper error propagation (Express anti-pattern) -- **MEDIUM:** Error middleware handles errors directly (doesn't delegate to central handler) -- **MEDIUM:** Async routes without proper error handling (not using promises/async-await) -- **LOW:** Stack traces exposed in production responses (security/UX issue) - -**Recommendation:** -- Create single ErrorHandler class/module for ALL error contexts -- Middleware should only catch and forward to ErrorHandler (delegate pattern) -- Use async/await for async routes (framework auto-forwards errors) -- Transform errors for users: hide sensitive details (stack traces, internal paths) in production -- **DO NOT use uncaughtException listeners** - use process managers (PM2, systemd) for restart instead -- For unhandled rejections: log and restart process (use supervisor, not inline handler) - -**Effort:** M-L (create error handler, refactor existing middleware) - -### 7. Dependency Injection / Centralized Init -**What:** Direct imports/instantiation instead of dependency injection, scattered initialization - -**Detection:** -- Check for DI container usage: - - Node.js: `inversify`, `awilix`, `tsyringe`, `typedi` packages - - Python: `dependency_injector`, `injector` packages - - Java: Spring `@Autowired`, `@Inject` annotations - - .NET: Built-in DI in ASP.NET Core, `IServiceCollection` -- Grep for direct instantiations in business logic: `new SomeService()`, `new SomeRepository()` -- Check for centralized Init/Bootstrap module: `bootstrap.ts`, `init.py`, `Startup.cs`, `app.module.ts` -- Verify controllers/services receive dependencies via constructor/parameters, not direct imports - -**Severity:** -- **MEDIUM:** No DI container (hard to test, tight coupling, difficult to swap implementations) -- **MEDIUM:** Direct instantiation in business logic (`new Service()` in controllers/services) -- **LOW:** Mixed DI and direct imports (inconsistent pattern) - -**Recommendation:** -- Use DI container for dependency management (Inversify, Awilix, Spring, built-in .NET DI) -- Centralize initialization in Init/Bootstrap module -- Inject dependencies via constructor/parameters (dependency injection pattern) -- Never use direct instantiation for business logic classes (only for DTOs, value objects) - -**Effort:** L (refactor to DI pattern, add container, update all instantiations) - -### 8. Missing Best Practices Guide -**What:** No architecture/design best practices documentation for developers - -**Detection:** -- Check for architecture guide files: - - `docs/architecture.md`, `docs/best-practices.md`, `docs/design-patterns.md` - - `ARCHITECTURE.md`, `CONTRIBUTING.md` (architecture section) -- Verify content includes: layering rules, error handling patterns, DI usage, coding conventions - -**Severity:** -- **LOW:** No architecture guide (harder for new developers to understand patterns and conventions) - -**Recommendation:** -- Create `docs/architecture.md` with project-specific patterns: - - Document layering: Controller→Service→Repository→DB - - Error handling: centralized ErrorHandler pattern - - Dependency Injection: how to add new services/repositories - - Coding conventions: naming, file organization, imports -- Include examples from existing codebase -- Keep framework-agnostic (principles, not specific implementations) - -**Effort:** S (create markdown file, ~1-2 hours documentation) - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -Return JSON to coordinator: - -**Global mode output:** -```json -{ - "category": "Architecture & Design", - "score": 6, - "total_issues": 12, - "critical": 2, - "high": 4, - "medium": 4, - "low": 2, - "checks": [ - {"id": "dry_violations", "name": "DRY Violations", "status": "failed", "details": "4 duplications found"}, - {"id": "kiss_violations", "name": "KISS Violations", "status": "warning", "details": "1 over-engineered abstraction"}, - {"id": "yagni_violations", "name": "YAGNI Violations", "status": "passed", "details": "No unused code"}, - {"id": "solid_violations", "name": "SOLID Violations", "status": "failed", "details": "2 SRP violations"} - ], - "findings": [...] -} -``` - -**Domain-aware mode output (NEW):** -```json -{ - "category": "Architecture & Design", - "score": 6, - "domain": "users", - "scan_path": "src/users", - "total_issues": 12, - "critical": 2, - "high": 4, - "medium": 4, - "low": 2, - "checks": [ - {"id": "dry_violations", "name": "DRY Violations", "status": "failed", "details": "4 duplications found"}, - {"id": "kiss_violations", "name": "KISS Violations", "status": "warning", "details": "1 over-engineered abstraction"}, - {"id": "yagni_violations", "name": "YAGNI Violations", "status": "passed", "details": "No unused code"}, - {"id": "solid_violations", "name": "SOLID Violations", "status": "failed", "details": "2 SRP violations"} - ], - "findings": [ - { - "severity": "CRITICAL", - "location": "src/users/controllers/UserController.ts:45", - "issue": "Controller directly uses Repository (layer boundary break)", - "principle": "Layer Separation (Clean Architecture)", - "recommendation": "Create UserService, inject into controller", - "effort": "L", - "domain": "users" - }, - { - "severity": "HIGH", - "location": "src/users/services/UserService.ts:45", - "issue": "DRY violation - duplicate validation logic", - "principle": "DRY Principle", - "recommendation": "Extract to shared validators module", - "effort": "M", - "domain": "users" - } - ] -} -``` - -## Critical Rules - -- **Do not auto-fix:** Report only -- **Domain-aware scanning:** If `domain_mode="domain-aware"`, scan ONLY `scan_path` (not entire codebase) -- **Tag findings:** Include `domain` field in each finding when domain-aware -- **Context-aware:** Use project's `principles.md` to define what's acceptable -- **Age matters:** Old TODOs are higher severity than recent ones -- **Effort realism:** S = <1h, M = 1-4h, L = >4h - -## Definition of Done - -- contextStore parsed (including domain_mode and current_domain) -- scan_path determined (domain path or codebase root) -- All 8 checks completed (scoped to scan_path): - - DRY (7 subcategories), KISS, YAGNI, TODOs, Error Handling, Centralized Errors, DI/Init, Best Practices Guide -- Findings collected with severity, location, effort, recommendation, domain -- Score calculated -- JSON returned to coordinator with domain metadata - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` -- Architecture rules: [references/architecture_rules.md](references/architecture_rules.md) - ---- -**Version:** 4.1.0 -**Last Updated:** 2026-01-29 diff --git a/.claude/skills/code-quality-auditor/SKILL.md b/.claude/skills/code-quality-auditor/SKILL.md deleted file mode 100644 index c879e4b..0000000 --- a/.claude/skills/code-quality-auditor/SKILL.md +++ /dev/null @@ -1,302 +0,0 @@ ---- -name: ln-624-code-quality-auditor -description: "Code quality audit worker (L3). Checks cyclomatic complexity, deep nesting, long methods, god classes, method signature quality, O(n²) algorithms, N+1 queries, magic numbers/constants. Returns findings with severity, location, effort, recommendations." -allowed-tools: Read, Grep, Glob, Bash ---- - -# Code Quality Auditor (L3 Worker) - -Specialized worker auditing code complexity, method signatures, algorithms, and constants management. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** - invoked by ln-620-codebase-auditor -- Audit **code quality** (Categories 5+6+NEW: Medium Priority) -- Check complexity metrics, method signature quality, algorithmic efficiency, constants management -- Return structured findings with severity, location, effort, recommendations -- Calculate compliance score (X/10) for Code Quality category - -## Inputs (from Coordinator) - -**MANDATORY READ:** Load `shared/references/task_delegation_pattern.md#audit-coordinator--worker-contract` for contextStore structure. - -Receives `contextStore` with: `tech_stack`, `best_practices`, `principles`, `codebase_root`. - -**Domain-aware:** Supports `domain_mode` + `current_domain` (see `audit_output_schema.md#domain-aware-worker-output`). - -## Workflow - -1) **Parse context** — extract fields, determine `scan_path` (domain-aware if specified) -2) **Scan codebase for violations** - - All Grep/Glob patterns use `scan_path` (not codebase_root) - - Example: `Grep(pattern="if.*if.*if", path=scan_path)` for nesting detection - -3) **Collect findings with severity, location, effort, recommendation** - - Tag each finding with `domain: domain_name` (if domain-aware) - -4) **Calculate score using penalty algorithm** - -5) **Return JSON result to coordinator** - - Include `domain` and `scan_path` fields (if domain-aware) - -## Audit Rules (Priority: MEDIUM) - -### 1. Cyclomatic Complexity -**What:** Too many decision points in single function (> 10) - -**Detection:** -- Count if/else, switch/case, ternary, &&, ||, for, while -- Use tools: `eslint-plugin-complexity`, `radon` (Python), `gocyclo` (Go) - -**Severity:** -- **HIGH:** Complexity > 20 (extremely hard to test) -- **MEDIUM:** Complexity 11-20 (refactor recommended) -- **LOW:** Complexity 8-10 (acceptable but monitor) - -**Recommendation:** Split function, extract helper methods, use early returns - -**Effort:** M-L (depends on complexity) - -### 2. Deep Nesting (> 4 levels) -**What:** Nested if/for/while blocks too deep - -**Detection:** -- Count indentation levels -- Pattern: if { if { if { if { if { ... } } } } } - -**Severity:** -- **HIGH:** > 6 levels (unreadable) -- **MEDIUM:** 5-6 levels -- **LOW:** 4 levels - -**Recommendation:** Extract functions, use guard clauses, invert conditions - -**Effort:** M (refactor structure) - -### 3. Long Methods (> 50 lines) -**What:** Functions too long, doing too much - -**Detection:** -- Count lines between function start and end -- Exclude comments, blank lines - -**Severity:** -- **HIGH:** > 100 lines -- **MEDIUM:** 51-100 lines -- **LOW:** 40-50 lines (borderline) - -**Recommendation:** Split into smaller functions, apply Single Responsibility - -**Effort:** M (extract logic) - -### 4. God Classes/Modules (> 500 lines) -**What:** Files with too many responsibilities - -**Detection:** -- Count lines in file (exclude comments) -- Check number of public methods/functions - -**Severity:** -- **HIGH:** > 1000 lines -- **MEDIUM:** 501-1000 lines -- **LOW:** 400-500 lines - -**Recommendation:** Split into multiple files, apply separation of concerns - -**Effort:** L (major refactor) - -### 5. Too Many Parameters (> 5) -**What:** Functions with excessive parameters - -**Detection:** -- Count function parameters -- Check constructors, methods - -**Severity:** -- **MEDIUM:** 6-8 parameters -- **LOW:** 5 parameters (borderline) - -**Recommendation:** Use parameter object, builder pattern, default parameters - -**Effort:** S-M (refactor signature + calls) - -### 6. O(n²) or Worse Algorithms -**What:** Inefficient nested loops over collections - -**Detection:** -- Nested for loops: `for (i) { for (j) { ... } }` -- Nested array methods: `arr.map(x => arr.filter(...))` - -**Severity:** -- **HIGH:** O(n²) in hot path (API request handler) -- **MEDIUM:** O(n²) in occasional operations -- **LOW:** O(n²) on small datasets (n < 100) - -**Recommendation:** Use hash maps, optimize with single pass, use better data structures - -**Effort:** M (algorithm redesign) - -### 7. N+1 Query Patterns -**What:** ORM lazy loading causing N+1 queries - -**Detection:** -- Find loops with database queries inside -- Check ORM patterns: `users.forEach(u => u.getPosts())` - -**Severity:** -- **CRITICAL:** N+1 in API endpoint (performance disaster) -- **HIGH:** N+1 in frequent operations -- **MEDIUM:** N+1 in admin panel - -**Recommendation:** Use eager loading, batch queries, JOIN - -**Effort:** M (change ORM query) - -### 8. Constants Management (NEW) -**What:** Magic numbers/strings, decentralized constants, duplicates - -**Detection:** - -| Issue | Pattern | Example | -|-------|---------|---------| -| Magic numbers | Hardcoded numbers in conditions/calculations | `if (status === 2)` | -| Magic strings | Hardcoded strings in comparisons | `if (role === 'admin')` | -| Decentralized | Constants scattered across files | `MAX_SIZE = 100` in 5 files | -| Duplicates | Same value multiple times | `STATUS_ACTIVE = 1` in 3 places | -| No central file | Missing `constants.ts` or `config.py` | No single source of truth | - -**Severity:** -- **HIGH:** Magic numbers in business logic (payment amounts, statuses) -- **MEDIUM:** Duplicate constants (same value defined 3+ times) -- **MEDIUM:** No central constants file -- **LOW:** Magic strings in logging/debugging - -**Recommendation:** -- Create central constants file (`constants.ts`, `config.py`, `constants.go`) -- Extract magic numbers to named constants: `const STATUS_ACTIVE = 1` -- Consolidate duplicates, import from central file -- Use enums for related constants - -**Effort:** M (extract constants, update imports, consolidate) - -### 9. Method Signature Quality -**What:** Poor method contracts reducing readability and maintainability - -**Detection:** - -| Issue | Pattern | Example | -|-------|---------|---------| -| Boolean flag params | >=2 boolean params in signature | `def process(data, is_async: bool, skip_validation: bool)` | -| Too many optional params | >=3 optional params with defaults | `def query(db, limit=10, offset=0, sort="id", order="asc")` | -| Inconsistent verb naming | Different verbs for same operation type in one module | `get_user()` vs `fetch_account()` vs `load_profile()` | -| Unclear return type | `-> dict`, `-> Any`, `-> tuple` without TypedDict/NamedTuple | `def get_stats() -> dict` instead of `-> StatsResponse` | - -**Severity:** -- **MEDIUM:** Boolean flag params (use enum/strategy), unclear return types -- **LOW:** Too many optional params, inconsistent naming - -**Recommendation:** -- Boolean flags: replace with enum, strategy pattern, or separate methods -- Optional params: group into config/options dataclass -- Naming: standardize verb conventions per module (`get_` for sync, `fetch_` for async, etc.) -- Return types: use TypedDict, NamedTuple, or dataclass instead of raw dict/tuple - -**Effort:** S-M (refactor signatures + callers) - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -Return JSON to coordinator: - -**Global mode output:** -```json -{ - "category": "Code Quality", - "score": 6, - "total_issues": 12, - "critical": 1, - "high": 3, - "medium": 5, - "low": 3, - "checks": [ - {"id": "cyclomatic_complexity", "name": "Cyclomatic Complexity", "status": "failed", "details": "2 functions exceed threshold"}, - {"id": "deep_nesting", "name": "Deep Nesting", "status": "warning", "details": "1 function with 5 levels"}, - {"id": "long_methods", "name": "Long Methods", "status": "passed", "details": "No methods exceed 50 lines"}, - {"id": "magic_numbers", "name": "Magic Numbers", "status": "failed", "details": "5 magic numbers found"} - ], - "findings": [...] -} -``` - -**Domain-aware mode output (NEW):** -```json -{ - "category": "Code Quality", - "score": 7, - "domain": "orders", - "scan_path": "src/orders", - "total_issues": 8, - "critical": 0, - "high": 2, - "medium": 4, - "low": 2, - "checks": [ - {"id": "cyclomatic_complexity", "name": "Cyclomatic Complexity", "status": "failed", "details": "1 function exceeds threshold"}, - {"id": "deep_nesting", "name": "Deep Nesting", "status": "passed", "details": "No deep nesting detected"}, - {"id": "long_methods", "name": "Long Methods", "status": "passed", "details": "No methods exceed 50 lines"}, - {"id": "magic_numbers", "name": "Magic Numbers", "status": "warning", "details": "1 magic number found"} - ], - "findings": [ - { - "severity": "HIGH", - "location": "src/orders/services/OrderService.ts:120", - "issue": "Cyclomatic complexity 22 (threshold: 10)", - "principle": "Code Complexity / Maintainability", - "recommendation": "Split into smaller methods", - "effort": "M", - "domain": "orders" - }, - { - "severity": "MEDIUM", - "location": "src/orders/controllers/OrderController.ts:45", - "issue": "Magic number '3' used for order status", - "principle": "Constants Management", - "recommendation": "Extract: const ORDER_STATUS_SHIPPED = 3", - "effort": "S", - "domain": "orders" - } - ] -} -``` - -## Critical Rules - -- **Do not auto-fix:** Report only -- **Domain-aware scanning:** If `domain_mode="domain-aware"`, scan ONLY `scan_path` (not entire codebase) -- **Tag findings:** Include `domain` field in each finding when domain-aware -- **Context-aware:** Small functions (n < 100) with O(n²) may be acceptable -- **Constants detection:** Exclude test files, configs, examples -- **Metrics tools:** Use existing tools when available (ESLint complexity plugin, radon, gocyclo) - -## Definition of Done - -- contextStore parsed (including domain_mode and current_domain) -- scan_path determined (domain path or codebase root) -- All 9 checks completed (scoped to scan_path): - - complexity, nesting, length, god classes, parameters, O(n²), N+1, constants, method signatures -- Findings collected with severity, location, effort, recommendation, domain -- Score calculated -- JSON returned to coordinator with domain metadata - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` -- Code quality rules: [references/code_quality_rules.md](references/code_quality_rules.md) - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/code-quality-checker/SKILL.md b/.claude/skills/code-quality-checker/SKILL.md deleted file mode 100644 index a56d183..0000000 --- a/.claude/skills/code-quality-checker/SKILL.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -name: ln-501-code-quality-checker -description: "Worker that checks DRY/KISS/YAGNI/architecture compliance with quantitative Code Quality Score. Validates architectural decisions via MCP Ref: (1) Optimality - is chosen approach the best? (2) Compliance - does it follow best practices? (3) Performance - algorithms, configs, bottlenecks. Reports issues with SEC-, PERF-, MNT-, ARCH-, BP-, OPT- prefixes." ---- - -# Code Quality Checker - -Analyzes Done implementation tasks with quantitative Code Quality Score based on metrics, MCP Ref validation, and issue penalties. - -## Purpose & Scope -- Load Story and Done implementation tasks (exclude test tasks) -- Calculate Code Quality Score using metrics and issue penalties -- **MCP Ref validation:** Verify optimality, best practices, and performance via external sources -- Check for DRY/KISS/YAGNI violations, architecture boundary breaks, security issues -- Produce quantitative verdict with structured issue list; never edits Linear or kanban - -## Code Metrics - -| Metric | Threshold | Penalty | -|--------|-----------|---------| -| **Cyclomatic Complexity** | ≤10 OK, 11-20 warning, >20 fail | -5 (warning), -10 (fail) per function | -| **Function size** | ≤50 lines OK, >50 warning | -3 per function | -| **File size** | ≤500 lines OK, >500 warning | -5 per file | -| **Nesting depth** | ≤3 OK, >3 warning | -3 per instance | -| **Parameter count** | ≤4 OK, >4 warning | -2 per function | - -## Code Quality Score - -Formula: `Code Quality Score = 100 - metric_penalties - issue_penalties` - -**Issue penalties by severity:** - -| Severity | Penalty | Examples | -|----------|---------|----------| -| **high** | -20 | Security vulnerability, O(n²)+ algorithm, N+1 query | -| **medium** | -10 | DRY violation, suboptimal approach, missing config | -| **low** | -3 | Naming convention, minor code smell | - -**Score interpretation:** - -| Score | Status | Verdict | -|-------|--------|---------| -| 90-100 | Excellent | PASS | -| 70-89 | Acceptable | CONCERNS | -| <70 | Below threshold | ISSUES_FOUND | - -## Issue Prefixes - -| Prefix | Category | Default Severity | MCP Ref | -|--------|----------|------------------|---------| -| SEC- | Security (auth, validation, secrets) | high | — | -| PERF- | Performance (algorithms, configs, bottlenecks) | medium/high | ✓ Required | -| MNT- | Maintainability (DRY, SOLID, complexity) | medium | — | -| ARCH- | Architecture (layers, boundaries, patterns) | medium | — | -| BP- | Best Practices (implementation differs from recommended) | medium | ✓ Required | -| OPT- | Optimality (better approach exists for this goal) | medium | ✓ Required | - -**PERF- subcategories:** - -| Prefix | Category | Severity | -|--------|----------|----------| -| PERF-ALG- | Algorithm complexity (Big O) | high if O(n²)+ | -| PERF-CFG- | Package/library configuration | medium | -| PERF-PTN- | Architectural pattern performance | high | -| PERF-DB- | Database queries, indexes | high | - -## When to Use -- **Invoked by ln-500-story-quality-gate** Pass 1 (first gate) -- All implementation tasks in Story status = Done -- Before regression testing (ln-502) and test planning (ln-510) - -## Workflow (concise) -1) Load Story (full) and Done implementation tasks (full descriptions) via Linear; skip tasks with label "tests". -2) Collect affected files from tasks (Affected Components/Existing Code Impact) and recent commits/diffs if noted. -3) **Calculate code metrics:** - - Cyclomatic Complexity per function (target ≤10) - - Function size (target ≤50 lines) - - File size (target ≤500 lines) - - Nesting depth (target ≤3) - - Parameter count (target ≤4) - -3.5) **MCP Ref Validation (MANDATORY for code changes):** - - **Level 1 — OPTIMALITY (OPT-):** - - Extract goal from task (e.g., "user authentication", "caching", "API rate limiting") - - Research alternatives: `ref_search_documentation("{goal} approaches comparison {tech_stack} 2026")` - - Compare chosen approach vs alternatives for project context - - Flag suboptimal choices as OPT- issues - - **Level 2 — BEST PRACTICES (BP-):** - - Research: `ref_search_documentation("{chosen_approach} best practices {tech_stack} 2026")` - - For libraries: `query-docs(library_id, "best practices implementation patterns")` - - Flag deviations from recommended patterns as BP- issues - - **Level 3 — PERFORMANCE (PERF-):** - - **PERF-ALG:** Analyze algorithm complexity (detect O(n²)+, research optimal via MCP Ref) - - **PERF-CFG:** Check library configs (connection pooling, batch sizes, timeouts) via `query-docs` - - **PERF-PTN:** Research pattern pitfalls: `ref_search_documentation("{pattern} performance bottlenecks")` - - **PERF-DB:** Check for N+1, missing indexes via `query-docs(orm_library_id, "query optimization")` - - **Triggers for MCP Ref validation:** - - New dependency added (package.json/requirements.txt changed) - - New pattern/library used - - API/database changes - - Loops/recursion in critical paths - - ORM queries added - -4) **Analyze code for static issues (assign prefixes):** - - SEC-: hardcoded creds, unvalidated input, SQL injection, race conditions - - MNT-: DRY violations, dead code, complex conditionals, poor naming - - ARCH-: layer violations, circular dependencies, guide non-compliance - -5) **Calculate Code Quality Score:** - - Start with 100 - - Subtract metric penalties (see Code Metrics table) - - Subtract issue penalties (see Issue penalties table) - -6) Output verdict with score and structured issues. Add Linear comment with findings. - -## Critical Rules -- Read guides mentioned in Story/Tasks before judging compliance. -- **MCP Ref validation:** For ANY architectural change, MUST verify via ref_search_documentation before judging. -- **Context7 for libraries:** When reviewing library usage, query-docs to verify correct patterns. -- Language preservation in comments (EN/RU). -- Do not create tasks or change statuses; caller decides next actions. - -## Definition of Done -- Story and Done implementation tasks loaded (test tasks excluded). -- Code metrics calculated (Cyclomatic Complexity, function/file sizes). -- **MCP Ref validation completed:** - - OPT-: Optimality checked (is chosen approach the best for the goal?) - - BP-: Best practices verified (correct implementation of chosen approach?) - - PERF-: Performance analyzed (algorithms, configs, patterns, DB) -- Issues identified with prefixes and severity, sources from MCP Ref/Context7. -- Code Quality Score calculated. -- **Output format:** - ```yaml - verdict: PASS | CONCERNS | ISSUES_FOUND - code_quality_score: {0-100} - metrics: - avg_cyclomatic_complexity: {value} - functions_over_50_lines: {count} - files_over_500_lines: {count} - issues: - # OPTIMALITY - - id: "OPT-001" - severity: medium - file: "src/auth/index.ts" - goal: "User session management" - finding: "Suboptimal approach for session management" - chosen: "Custom JWT with localStorage" - recommended: "httpOnly cookies + refresh token rotation" - reason: "httpOnly cookies prevent XSS token theft" - source: "ref://owasp-session-management" - - # BEST PRACTICES - - id: "BP-001" - severity: medium - file: "src/api/routes.ts" - finding: "POST for idempotent operation" - best_practice: "Use PUT for idempotent updates (RFC 7231)" - source: "ref://api-design-guide#idempotency" - - # PERFORMANCE - Algorithm - - id: "PERF-ALG-001" - severity: high - file: "src/utils/search.ts:42" - finding: "Nested loops cause O(n²) complexity" - current: "O(n²) - nested filter().find()" - optimal: "O(n) - use Map/Set for lookup" - source: "ref://javascript-performance#data-structures" - - # PERFORMANCE - Config - - id: "PERF-CFG-001" - severity: medium - file: "src/db/connection.ts" - finding: "Missing connection pool config" - current_config: "default (pool: undefined)" - recommended: "pool: { min: 2, max: 10 }" - source: "context7://pg#connection-pooling" - - # PERFORMANCE - Database - - id: "PERF-DB-001" - severity: high - file: "src/repositories/user.ts:89" - finding: "N+1 query pattern detected" - issue: "users.map(u => u.posts) triggers N queries" - solution: "Use eager loading: include: { posts: true }" - source: "context7://prisma#eager-loading" - - # MAINTAINABILITY - - id: "MNT-001" - severity: medium - file: "src/service.ts:42" - finding: "DRY violation: duplicate validation logic" - suggested_action: "Extract to shared validator" - ``` -- Linear comment posted with findings. - -## Reference Files -- Code metrics: `references/code_metrics.md` (thresholds and penalties) -- Guides: `docs/guides/` -- Templates for context: `shared/templates/task_template_implementation.md` - ---- -**Version:** 5.0.0 (Added 3-level MCP Ref validation: Optimality, Best Practices, Performance with PERF-ALG/CFG/PTN/DB subcategories) -**Last Updated:** 2026-01-29 diff --git a/.claude/skills/code-refactoring/SKILL.md b/.claude/skills/code-refactoring/SKILL.md deleted file mode 100755 index fdee8a2..0000000 --- a/.claude/skills/code-refactoring/SKILL.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: code-refactoring -description: 코드 리팩토링 권장사항, 성능 분석, 구체적인 코드 패치를 제공합니다. 리팩토링, 코드 개선, 성능 최적화 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep ---- - -# Code Refactoring - -코드 품질 개선을 위한 리팩토링 분석 및 권장사항을 제공하는 스킬입니다. - -## 기능 - -### 분석 영역 -- **코드 스멜 탐지**: 중복 코드, 긴 메서드, 거대 클래스 -- **복잡도 분석**: 순환 복잡도, 인지 복잡도 -- **의존성 분석**: 결합도, 응집도 평가 -- **성능 이슈**: 비효율적 알고리즘, N+1 쿼리 -- **가독성**: 네이밍, 구조화, 주석 품질 - -### 리팩토링 패턴 -- Extract Method / Extract Class -- Replace Conditional with Polymorphism -- Introduce Parameter Object -- Replace Magic Number with Symbolic Constant -- Decompose Conditional -- Replace Nested Conditional with Guard Clauses - -### 출력 -- 문제점 상세 설명 -- 리팩토링 전후 코드 비교 -- 구체적인 수정 패치 -- 성능 영향 분석 - -## 분석 프로세스 - -1. 코드베이스 스캔 -2. 코드 스멜 및 안티패턴 탐지 -3. 복잡도 메트릭 계산 -4. 개선 우선순위 산정 -5. 리팩토링 제안 생성 -6. 전후 비교 코드 제공 - -## 리포트 구조 - -```markdown -# 리팩토링 분석 리포트 - -## 요약 -- 분석 파일: N개 -- 발견된 코드 스멜: N개 -- 권장 리팩토링: N건 - -## 🔴 High Impact 리팩토링 - -### 1. [파일명] 긴 메서드 분리 -**현재 상태** -- 메서드: `processOrder()` -- 라인 수: 150줄 -- 순환 복잡도: 25 - -**문제점** -- 단일 책임 원칙 위반 -- 테스트 어려움 -- 유지보수 복잡 - -**권장 리팩토링: Extract Method** - -Before: -```java -public void processOrder(Order order) { - // 150줄의 복잡한 로직 -} -``` - -After: -```java -public void processOrder(Order order) { - validateOrder(order); - calculateTotal(order); - applyDiscounts(order); - processPayment(order); - sendConfirmation(order); -} - -private void validateOrder(Order order) { ... } -private void calculateTotal(Order order) { ... } -// ... -``` - -**예상 효과** -- 복잡도: 25 → 5 -- 테스트 용이성 향상 -- 재사용성 증가 - -## 🟡 Medium Impact 리팩토링 -... - -## 성능 최적화 제안 -... -``` - -## 사용 예시 - -``` -이 코드를 리팩토링 해줘 -src 폴더의 코드 품질을 분석하고 개선점을 알려줘 -이 함수의 복잡도를 낮추는 방법을 제안해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/codebase-analysis-web-report/SKILL.md b/.claude/skills/codebase-analysis-web-report/SKILL.md deleted file mode 100755 index 689f1eb..0000000 --- a/.claude/skills/codebase-analysis-web-report/SKILL.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -name: codebase-analysis-web-report -description: 코드베이스를 분석하여 아키텍처, 호출/데이터 흐름 그래프, 인터랙티브 다이어그램, 검색 기능이 포함된 자체 완결형 HTML 리포트를 생성합니다. 저장소 분석, 프로젝트 디렉토리 분석, 소스 파일 분석 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# Codebase Analysis Web Report Generator - -코드베이스를 분석하여 자체 완결형 인터랙티브 HTML 리포트를 생성하는 스킬입니다. - -## 기능 - -### 입력 처리 -- 저장소 URL, 파일 업로드, 디렉토리 경로 지원 -- 프레임워크별 분석 지원 (React, Node, Flask, PHP 등) -- 대규모 코드베이스의 경우 자동 범위 지정 또는 사용자 지정 요청 - -### 분석 구성요소 -- 모듈, 클래스, 함수, API 추출을 위한 정적 코드 분석 -- 호출 관계 매핑 및 의존성 추출 -- 메트릭 계산 (LOC, 복잡도 추정) -- 진입점 및 통합 지점 식별 - -### 출력 생성 -- 제어/데이터 흐름 그래프 (개략적 및 세부적) -- 모듈 설명이 포함된 텍스트 요약 -- 우선순위가 지정된 개선 권장사항이 포함된 발견사항 섹션 -- 네비게이션, 인터랙티브 다이어그램, 검색 기능, 소스 코드 미리보기가 포함된 자체 완결형 HTML 리포트 - -## 구현 노트 - -- 가능한 경우 AST 파싱 사용; 그 외에는 정규식 휴리스틱 사용 -- 인터랙티브 다이어그램에 Mermaid 또는 Cytoscape.js 우선 사용 -- 샘플링 또는 범위 제한을 통해 대규모 저장소 처리 -- 프레임워크별 구조 표시 (React 컴포넌트, 라우팅, DB 접근 패턴) - -## 리포트 구조 - -```html - - - - - 코드베이스 분석 리포트 - - - - - - - - - - - - - - -``` - -## 사용 예시 - -``` -이 프로젝트의 코드베이스를 분석해서 HTML 리포트로 만들어줘 -src 폴더의 아키텍처를 분석하고 다이어그램으로 시각화해줘 -이 저장소의 구조를 분석해서 웹 문서로 생성해줘 -``` - -## 출처 -Original skill by Dongchan Lee (@vluylbhtqq) from skills.cokac.com diff --git a/.claude/skills/concurrency-auditor/SKILL.md b/.claude/skills/concurrency-auditor/SKILL.md deleted file mode 100644 index 8dbd3a4..0000000 --- a/.claude/skills/concurrency-auditor/SKILL.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -name: ln-628-concurrency-auditor -description: Concurrency audit worker (L3). Checks race conditions, missing async/await, resource contention, thread safety, deadlock potential. Returns findings with severity, location, effort, recommendations. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Concurrency Auditor (L3 Worker) - -Specialized worker auditing concurrency and async patterns. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** -- Audit **concurrency** (Category 11: High Priority) -- Check race conditions, async/await, thread safety -- Calculate compliance score (X/10) - -## Inputs (from Coordinator) - -Receives `contextStore` with tech stack, language, codebase root. - -## Workflow - -1) Parse context -2) Check concurrency patterns -3) Collect findings -4) Calculate score -5) Return JSON - -## Audit Rules - -### 1. Race Conditions -**What:** Shared state modified without synchronization - -**Detection Patterns:** - -| Language | Pattern | Grep | -|----------|---------|------| -| Python | Global modified in async | `global\s+\w+` inside `async def` | -| TypeScript | Module-level let in async | `^let\s+\w+` at file scope + async function modifies it | -| Go | Map access without mutex | `map\[.*\].*=` without `sync.Mutex` in same file | -| All | Shared cache | `cache\[.*\]\s*=` or `cache\.set` without lock | - -**Severity:** -- **CRITICAL:** Race in payment/auth (`payment`, `balance`, `auth`, `token` in variable name) -- **HIGH:** Race in user-facing feature -- **MEDIUM:** Race in background job - -**Recommendation:** Use locks, atomic operations, message queues - -**Effort:** M-L - -### 2. Missing Async/Await -**What:** Callback hell or unhandled promises - -**Detection Patterns:** - -| Issue | Grep | Example | -|-------|------|---------| -| Callback hell | `\.then\(.*\.then\(.*\.then\(` | `.then().then().then()` | -| Fire-and-forget | `async.*\(\)` not preceded by `await` | `saveToDb()` without await | -| Missing await | `return\s+new\s+Promise` in async function | Should just `return await` or `return` value | -| Dangling promise | `\.catch\(\s*\)` | Empty catch swallows errors | - -**Severity:** -- **HIGH:** Fire-and-forget async (can cause data loss) -- **MEDIUM:** Callback hell (hard to maintain) -- **LOW:** Mixed Promise styles - -**Recommendation:** Convert to async/await, always await or handle promises - -**Effort:** M - -### 3. Resource Contention -**What:** Multiple processes competing for same resource - -**Detection Patterns:** - -| Issue | Grep | Example | -|-------|------|---------| -| File lock missing | `open\(.*["']w["']\)` without `flock` or `lockfile` | Concurrent file writes | -| Connection exhaustion | `create_engine\(.*pool_size` check if pool_size < 5 | DB pool too small | -| Concurrent writes | `writeFile` or `fs\.write` without lock check | File corruption risk | - -**Severity:** -- **HIGH:** File corruption risk, DB exhaustion -- **MEDIUM:** Performance degradation - -**Recommendation:** Use connection pooling, file locking, `asyncio.Lock` - -**Effort:** M - -### 4. Thread Safety Violations -**What:** Shared mutable state without synchronization - -**Detection Patterns:** - -| Language | Safe Pattern | Unsafe Pattern | -|----------|--------------|----------------| -| Go | `sync.Mutex` with map | `map[...]` without Mutex in same struct | -| Rust | `Arc>` | `Rc>` in multi-threaded context | -| Java | `synchronized` or `ConcurrentHashMap` | `HashMap` shared between threads | -| Python | `threading.Lock` | Global dict modified in threads | - -**Grep patterns:** -- Go unsafe: `type.*struct\s*{[^}]*map\[` without `sync.Mutex` in same struct -- Python unsafe: `global\s+\w+` in function + `threading.Thread` in same file - -**Severity:** **HIGH** (data corruption possible) - -**Recommendation:** Use thread-safe primitives - -**Effort:** M - -### 5. Deadlock Potential -**What:** Lock acquisition in inconsistent order - -**Detection Patterns:** - -| Issue | Grep | Example | -|-------|------|---------| -| Nested locks | `with\s+\w+_lock:.*with\s+\w+_lock:` (multiline) | Lock A then Lock B | -| Lock in loop | `for.*:.*\.acquire\(\)` | Lock acquired repeatedly without release | -| Lock + external call | `.acquire\(\)` followed by `await` or `requests.` | Holding lock during I/O | - -**Severity:** **HIGH** (deadlock freezes application) - -**Recommendation:** Consistent lock ordering, timeout locks (`asyncio.wait_for`) - -**Effort:** L - -### 6. Blocking I/O in Event Loop (Python asyncio) -**What:** Synchronous blocking calls inside async functions - -**Detection Patterns:** - -| Blocking Call | Grep in `async def` | Replacement | -|---------------|---------------------|-------------| -| `time.sleep` | `time\.sleep` inside async def | `await asyncio.sleep` | -| `requests.` | `requests\.(get\|post)` inside async def | `httpx` or `aiohttp` | -| `open()` file | `open\(` inside async def | `aiofiles.open` | - -**Severity:** -- **HIGH:** Blocks entire event loop -- **MEDIUM:** Minor blocking (<100ms) - -**Recommendation:** Use async alternatives - -**Effort:** S-M - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -```json -{ - "category": "Concurrency", - "score": 7, - "total_issues": 4, - "critical": 0, - "high": 2, - "medium": 2, - "low": 0, - "checks": [ - {"id": "race_conditions", "name": "Race Conditions", "status": "passed", "details": "No shared state modified without synchronization"}, - {"id": "missing_await", "name": "Missing Await", "status": "failed", "details": "2 fire-and-forget async calls found"}, - {"id": "resource_contention", "name": "Resource Contention", "status": "warning", "details": "DB pool_size=3 may be insufficient"}, - {"id": "thread_safety", "name": "Thread Safety", "status": "passed", "details": "All shared state properly synchronized"}, - {"id": "deadlock_potential", "name": "Deadlock Potential", "status": "passed", "details": "No nested locks or inconsistent ordering"}, - {"id": "blocking_io", "name": "Blocking I/O", "status": "failed", "details": "time.sleep in async context"} - ], - "findings": [ - { - "severity": "HIGH", - "location": "src/services/payment.ts:45", - "issue": "Shared state 'balanceCache' modified without synchronization", - "principle": "Thread Safety / Concurrency Control", - "recommendation": "Use mutex or atomic operations for balanceCache updates", - "effort": "M" - } - ] -} -``` - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/coverage-improvement-planner/SKILL.md b/.claude/skills/coverage-improvement-planner/SKILL.md deleted file mode 100755 index 3d00f68..0000000 --- a/.claude/skills/coverage-improvement-planner/SKILL.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -name: coverage-improvement-planner -description: 테스트 커버리지를 분석하고 개선 계획을 수립하며 전후 비교 리포트를 생성합니다. 테스트 커버리지, 테스트 개선, 품질 향상 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# Coverage Improvement Planner - -테스트 커버리지를 분석하고 체계적인 개선 계획을 수립하는 스킬입니다. - -## 기능 - -### 분석 항목 -- **라인 커버리지**: 실행된 코드 라인 비율 -- **브랜치 커버리지**: 조건문 분기 테스트 비율 -- **함수 커버리지**: 테스트된 함수 비율 -- **파일 커버리지**: 테스트가 있는 파일 비율 - -### 개선 계획 수립 -- 미테스트 코드 영역 식별 -- 우선순위 기반 테스트 추가 권장 -- 복잡도 높은 코드 집중 분석 -- 테스트 케이스 템플릿 제공 - -### 지원 프레임워크 -- Jest (JavaScript/TypeScript) -- pytest (Python) -- JUnit (Java/Kotlin) -- PHPUnit (PHP) -- Go test - -## 워크플로우 - -1. 현재 커버리지 수집 및 분석 -2. 커버리지 갭 식별 -3. 비즈니스 중요도 기반 우선순위 산정 -4. 테스트 케이스 템플릿 생성 -5. 구현 후 전후 비교 리포트 생성 - -## 리포트 구조 - -```markdown -# 테스트 커버리지 개선 계획 - -## 현재 상태 -| 메트릭 | 현재 | 목표 | 갭 | -|--------|------|------|-----| -| 라인 커버리지 | 65% | 80% | 15% | -| 브랜치 커버리지 | 45% | 70% | 25% | -| 함수 커버리지 | 70% | 85% | 15% | - -## 우선순위별 개선 대상 - -### 🔴 High Priority (비즈니스 크리티컬) -1. **payment/checkout.js** - - 현재: 30% | 목표: 90% - - 미테스트 함수: processPayment, validateCard - - 권장 테스트 케이스: 5개 - -### 🟡 Medium Priority -... - -### 🟢 Low Priority -... - -## 권장 테스트 케이스 - -### checkout.test.js -```javascript -describe('processPayment', () => { - test('성공적인 결제 처리', () => { - // 테스트 코드 - }); - - test('잘못된 카드 정보 처리', () => { - // 테스트 코드 - }); -}); -``` - -## 예상 결과 -- 개선 후 예상 라인 커버리지: 82% -- 추가 테스트 케이스: 23개 -- 예상 소요 시간: 개발자 판단 -``` - -## 사용 예시 - -``` -테스트 커버리지를 분석하고 개선 계획을 세워줘 -커버리지를 80%로 올리려면 어떤 테스트가 필요해? -미테스트 코드 영역을 찾아서 테스트 템플릿을 만들어줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/dead-code-auditor/SKILL.md b/.claude/skills/dead-code-auditor/SKILL.md deleted file mode 100644 index d6ff905..0000000 --- a/.claude/skills/dead-code-auditor/SKILL.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -name: ln-626-dead-code-auditor -description: Dead code & legacy audit worker (L3). Checks unreachable code, unused imports/variables/functions, commented-out code, backward compatibility shims, deprecated patterns. Returns findings. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Dead Code Auditor (L3 Worker) - -Specialized worker auditing unused and unreachable code. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** -- Audit **dead code** (Category 9: Low Priority) -- Find unused imports, variables, functions, commented-out code -- Calculate compliance score (X/10) - -## Inputs (from Coordinator) - -Receives `contextStore` with tech stack, codebase root. - -## Workflow - -1) Parse context -2) Run dead code detection (linters, grep) -3) Collect findings -4) Calculate score -5) Return JSON - -## Audit Rules - -### 1. Unreachable Code -**Detection:** -- Linter rules: `no-unreachable` (ESLint) -- Check code after `return`, `throw`, `break` - -**Severity:** MEDIUM - -### 2. Unused Imports/Variables/Functions -**Detection:** -- ESLint: `no-unused-vars` -- TypeScript: `noUnusedLocals`, `noUnusedParameters` -- Python: `flake8` with `F401`, `F841` - -**Severity:** -- **MEDIUM:** Unused functions (dead weight) -- **LOW:** Unused imports (cleanup needed) - -### 3. Commented-Out Code -**Detection:** -- Grep for `//.*{` or `/*.*function` patterns -- Large comment blocks (>10 lines) with code syntax - -**Severity:** LOW - -**Recommendation:** Delete (git preserves history) - -### 4. Legacy Code & Backward Compatibility -**What:** Backward compatibility shims, deprecated patterns, old code that should be removed - -**Detection:** -- Renamed variables/functions with old aliases: - - Pattern: `const oldName = newName` or `export { newModule as oldModule }` - - Pattern: `function oldFunc() { return newFunc(); }` (wrapper for backward compatibility) -- Deprecated exports/re-exports: - - Grep for `// DEPRECATED`, `@deprecated` JSDoc tags - - Pattern: `export.*as.*old.*` or `export.*legacy.*` -- Conditional code for old versions: - - Pattern: `if.*legacy.*` or `if.*old.*version.*` or `isOldVersion ? oldFunc() : newFunc()` -- Migration shims and adapters: - - Pattern: `migrate.*`, `Legacy.*Adapter`, `.*Shim`, `.*Compat` -- Comment markers: - - Grep for `// backward compatibility`, `// legacy support`, `// TODO: remove in v` - - Grep for `// old implementation`, `// deprecated`, `// kept for backward` - -**Severity:** -- **HIGH:** Backward compatibility shims in critical paths (auth, payment, core features) -- **MEDIUM:** Deprecated exports still in use, migration code from >6 months ago -- **LOW:** Recent migration code (<3 months), planned deprecation with clear removal timeline - -**Recommendation:** -- Remove backward compatibility shims - breaking changes are acceptable when properly versioned -- Delete old implementations - keep only the correct/new version -- Remove deprecated exports - update consumers to use new API -- Delete migration code after grace period (3-6 months) -- Clean legacy support comments - git history preserves old implementations - -**Effort:** -- **S:** Remove simple aliases, delete deprecated exports -- **M:** Refactor code using old APIs to new APIs -- **L:** Remove complex backward compatibility layer affecting multiple modules - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -```json -{ - "category": "Dead Code", - "score": 6, - "total_issues": 12, - "critical": 0, - "high": 2, - "medium": 3, - "low": 7, - "checks": [ - {"id": "unreachable_code", "name": "Unreachable Code", "status": "passed", "details": "No unreachable code detected"}, - {"id": "unused_exports", "name": "Unused Exports", "status": "failed", "details": "3 unused functions found"}, - {"id": "commented_code", "name": "Commented Code", "status": "warning", "details": "7 blocks of commented code"}, - {"id": "legacy_shims", "name": "Legacy Shims", "status": "failed", "details": "2 backward compatibility shims"} - ], - "findings": [ - { - "severity": "MEDIUM", - "location": "src/utils/helpers.ts:45", - "issue": "Function 'formatDate' is never used", - "principle": "Code Maintainability / Clean Code", - "recommendation": "Remove unused function or export if needed elsewhere", - "effort": "S" - }, - { - "severity": "HIGH", - "location": "src/api/v1/auth.ts:12-15", - "issue": "Backward compatibility shim for old password validation (6+ months old)", - "principle": "No Legacy Code / Clean Architecture", - "recommendation": "Remove old password validation, keep only new implementation. Update API version if breaking.", - "effort": "M" - } - ] -} -``` - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/dependencies-auditor/SKILL.md b/.claude/skills/dependencies-auditor/SKILL.md deleted file mode 100644 index 6606e22..0000000 --- a/.claude/skills/dependencies-auditor/SKILL.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -name: ln-625-dependencies-auditor -description: "Dependencies audit worker (L3). Checks outdated packages, unused deps, reinvented wheels, vulnerability scan (CVE/CVSS). Supports mode: full | vulnerabilities_only." -allowed-tools: Read, Grep, Glob, Bash ---- - -# Dependencies & Reuse Auditor (L3 Worker) - -Specialized worker auditing dependency management, code reuse, and security vulnerabilities. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** (full audit mode) -- **Worker in ln-760 security-setup pipeline** (vulnerabilities_only mode) -- Audit **dependencies and reuse** (Categories 7+8: Medium Priority) -- Check outdated packages, unused deps, wheel reinvention, **CVE vulnerabilities** -- Calculate compliance score (X/10) - -## Parameters - -| Param | Values | Default | Description | -|-------|--------|---------|-------------| -| mode | `full` / `vulnerabilities_only` | `full` | `full` = all 5 checks, `vulnerabilities_only` = only CVE scan | - -## Inputs (from Coordinator) - -Receives `contextStore` with tech stack, package manifest paths, codebase root. - -**From ln-620 (codebase-auditor):** mode=full (default) -**From ln-760 (security-setup):** mode=vulnerabilities_only - -## Workflow - -1) Parse context + mode parameter -2) Run dependency checks (based on mode) -3) Collect findings -4) Calculate score -5) Return JSON - ---- - -## Audit Rules (5 Checks) - -### 1. Outdated Packages -**Mode:** full only - -**Detection:** -- Run `npm outdated --json` (Node.js) -- Run `pip list --outdated --format=json` (Python) -- Run `cargo outdated --format=json` (Rust) - -**Severity:** -- **HIGH:** Major version behind (security risk) -- **MEDIUM:** Minor version behind -- **LOW:** Patch version behind - -**Recommendation:** Update to latest version, test for breaking changes - -**Effort:** S-M (update version, run tests) - -### 2. Unused Dependencies -**Mode:** full only - -**Detection:** -- Parse package.json/requirements.txt -- Grep codebase for `import`/`require` statements -- Find dependencies never imported - -**Severity:** -- **MEDIUM:** Unused production dependency (bloats bundle) -- **LOW:** Unused dev dependency - -**Recommendation:** Remove from package manifest - -**Effort:** S (delete line, test) - -### 3. Available Features Not Used -**Mode:** full only - -**Detection:** -- Check for axios when native fetch available (Node 18+) -- Check for lodash when Array methods sufficient -- Check for moment when Date.toLocaleString sufficient - -**Severity:** -- **MEDIUM:** Unnecessary dependency (increases bundle size) - -**Recommendation:** Use native alternative - -**Effort:** M (refactor code to use native API) - -### 4. Custom Implementations -**Mode:** full only - -**Detection:** -- Grep for custom sorting algorithms -- Check for hand-rolled validation (vs validator.js) -- Find custom date parsing (vs date-fns/dayjs) - -**Severity:** -- **HIGH:** Custom crypto (security risk) -- **MEDIUM:** Custom utilities with well-tested alternatives - -**Recommendation:** Replace with established library - -**Effort:** M (integrate library, replace calls) - -### 5. Vulnerability Scan (CVE/CVSS) -**Mode:** full AND vulnerabilities_only - -**Detection:** -- Detect ecosystems: npm, NuGet, pip, Go, Bundler, Cargo, Composer -- Run audit commands per `references/vulnerability_commands.md` -- Parse results with CVSS mapping per `shared/references/cvss_severity_mapping.md` - -**Severity:** -- **CRITICAL:** CVSS 9.0-10.0 (immediate fix required) -- **HIGH:** CVSS 7.0-8.9 (fix within 48h) -- **MEDIUM:** CVSS 4.0-6.9 (fix within 1 week) -- **LOW:** CVSS 0.1-3.9 (fix when convenient) - -**Fix Classification:** -- Patch update (x.x.Y) → safe auto-fix -- Minor update (x.Y.0) → usually safe -- Major update (Y.0.0) → manual review required -- No fix available → document and monitor - -**Recommendation:** Update to fixed version, verify lock file integrity - -**Effort:** S-L (depends on breaking changes) - ---- - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -**Note:** When mode=vulnerabilities_only, score based only on vulnerability findings. - -## Output Format - -```json -{ - "category": "Dependencies & Reuse", - "mode": "full", - "score": 7, - "total_issues": 12, - "critical": 1, - "high": 3, - "medium": 5, - "low": 3, - "checks": [ - {"id": "outdated_packages", "name": "Outdated Packages", "status": "failed", "details": "2 packages behind major versions"}, - {"id": "unused_deps", "name": "Unused Dependencies", "status": "warning", "details": "4 unused dev dependencies"}, - {"id": "available_natives", "name": "Available Natives", "status": "passed", "details": "No unnecessary polyfills"}, - {"id": "custom_implementations", "name": "Custom Implementations", "status": "warning", "details": "2 custom utilities found"}, - {"id": "vulnerability_scan", "name": "Vulnerability Scan (CVE)", "status": "failed", "details": "1 critical, 2 high vulnerabilities"} - ], - "findings": [ - { - "severity": "CRITICAL", - "location": "package.json", - "issue": "lodash@4.17.15 has CVE-2021-23337 (CVSS 7.2)", - "principle": "Security / Vulnerability Management", - "recommendation": "Update to lodash@4.17.21", - "effort": "S", - "fix_type": "patch" - }, - { - "severity": "HIGH", - "location": "package.json:15", - "issue": "express v4.17.0 (current: v4.19.2, 2 major versions behind)", - "principle": "Dependency Management / Security Updates", - "recommendation": "Update to v4.19.2 for security fixes", - "effort": "M" - } - ] -} -``` - -## Reference Files - -| File | Purpose | -|------|---------| -| `references/vulnerability_commands.md` | Ecosystem-specific audit commands | -| `references/ci_integration_guide.md` | CI/CD integration guidance | -| `shared/references/cvss_severity_mapping.md` | CVSS to severity level mapping | -| `shared/references/audit_scoring.md` | Audit scoring formula | -| `shared/references/audit_output_schema.md` | Audit output schema | - ---- -**Version:** 4.0.0 -**Last Updated:** 2026-02-05 diff --git a/.claude/skills/design-skill/SKILL.md b/.claude/skills/design-skill/SKILL.md deleted file mode 100755 index c0d725c..0000000 --- a/.claude/skills/design-skill/SKILL.md +++ /dev/null @@ -1,949 +0,0 @@ ---- -name: design-skill -description: 프레젠테이션 슬라이드를 미려한 HTML로 디자인. 슬라이드 HTML 생성, 시각적 디자인, 레이아웃 구성이 필요할 때 사용. ---- - -# Design Skill - 프로페셔널 프레젠테이션 디자인 시스템 - -최고 수준의 비즈니스 프레젠테이션을 위한 HTML 슬라이드 디자인 스킬입니다. -미니멀하고 세련된 디자인, 전문적인 타이포그래피, 정교한 레이아웃을 제공합니다. - ---- - -## 핵심 디자인 철학 - -### 1. Less is More -- 불필요한 장식 요소 제거 -- 콘텐츠가 주인공이 되는 디자인 -- 여백(Whitespace)을 적극 활용 -- 시각적 계층 구조 명확화 - -### 2. 타이포그래피 중심 디자인 -- Pretendard를 기본 폰트로 사용 -- 폰트 크기 대비로 시각적 임팩트 생성 -- 자간과 행간의 섬세한 조절 -- 웨이트 변화로 강조점 표현 - -### 3. 전략적 색상 사용 -- 제한된 색상 팔레트 (2-3색) -- 모노톤 기반 + 포인트 컬러 -- 배경색으로 분위기 연출 -- 고대비로 가독성 확보 - ---- - -## 기본 설정 - -### 슬라이드 크기 (16:9 기본) -```html - -``` - -### 지원 비율 -| 비율 | 크기 | 용도 | -|------|------|------| -| 16:9 | 720pt × 405pt | 기본, 모니터/화면 | -| 4:3 | 720pt × 540pt | 구형 프로젝터 | -| 16:10 | 720pt × 450pt | 맥북 | - -### 기본 폰트 스택 -```css -font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; -``` - -### Pretendard 웹폰트 CDN -```html - -``` - ---- - -## 타이포그래피 시스템 - -### 폰트 크기 스케일 -| 용도 | 크기 | 웨이트 | 사용 예시 | -|------|------|--------|----------| -| Hero Title | 72-96pt | 700-800 | 표지 메인 타이틀 | -| Section Title | 48-60pt | 700 | 섹션 구분 제목 | -| Slide Title | 32-40pt | 600-700 | 슬라이드 제목 | -| Subtitle | 20-24pt | 500 | 부제목, 설명 | -| Body | 16-20pt | 400 | 본문 텍스트 | -| Caption | 12-14pt | 400 | 캡션, 출처 | -| Label | 10-12pt | 500-600 | 뱃지, 태그 | - -### 자간 설정 (letter-spacing) -```css -/* 대형 제목: 타이트하게 */ -letter-spacing: -0.02em; - -/* 중형 제목 */ -letter-spacing: -0.01em; - -/* 본문: 기본 */ -letter-spacing: 0; - -/* 캡션, 레이블: 약간 넓게 */ -letter-spacing: 0.02em; -``` - -### 행간 설정 (line-height) -```css -/* 제목 */ -line-height: 1.2; - -/* 본문 */ -line-height: 1.6 - 1.8; - -/* 한 줄 텍스트 */ -line-height: 1; -``` - ---- - -## 색상 팔레트 시스템 - -### 1. Executive Minimal (기본 권장) -세련된 비즈니스 프레젠테이션용 -```css ---bg-primary: #f5f5f0; /* 웜 화이트 배경 */ ---bg-secondary: #e8e8e3; /* 서브 배경 */ ---bg-dark: #1a1a1a; /* 다크 배경 */ ---text-primary: #1a1a1a; /* 메인 텍스트 */ ---text-secondary: #666666; /* 보조 텍스트 */ ---text-light: #999999; /* 약한 텍스트 */ ---accent: #1a1a1a; /* 강조 (검정) */ ---border: #d4d4d0; /* 테두리 */ -``` - -### 2. Sage Professional -차분하고 신뢰감 있는 톤 -```css ---bg-primary: #b8c4b8; /* 세이지 그린 배경 */ ---bg-secondary: #a3b0a3; /* 짙은 세이지 */ ---bg-light: #f8faf8; /* 밝은 배경 */ ---text-primary: #1a1a1a; /* 메인 텍스트 */ ---text-secondary: #3d3d3d; /* 보조 텍스트 */ ---accent: #2d2d2d; /* 강조 */ ---border: #9aa89a; /* 테두리 */ -``` - -### 3. Modern Dark -임팩트 있는 다크 테마 -```css ---bg-primary: #0f0f0f; /* 순수 다크 */ ---bg-secondary: #1a1a1a; /* 카드 배경 */ ---bg-elevated: #252525; /* 강조 영역 */ ---text-primary: #ffffff; /* 메인 텍스트 */ ---text-secondary: #b0b0b0; /* 보조 텍스트 */ ---accent: #ffffff; /* 강조 (화이트) */ ---border: #333333; /* 테두리 */ -``` - -### 4. Corporate Blue -전통적 비즈니스 톤 -```css ---bg-primary: #ffffff; /* 화이트 배경 */ ---bg-secondary: #f7f9fc; /* 밝은 블루 그레이 */ ---text-primary: #1e2a3a; /* 다크 네이비 */ ---text-secondary: #5a6b7d; /* 블루 그레이 */ ---accent: #2563eb; /* 블루 강조 */ ---border: #e2e8f0; /* 테두리 */ -``` - -### 5. Warm Neutral -따뜻하고 친근한 톤 -```css ---bg-primary: #faf8f5; /* 크림 화이트 */ ---bg-secondary: #f0ebe3; /* 웜 베이지 */ ---text-primary: #2d2a26; /* 다크 브라운 */ ---text-secondary: #6b6560; /* 미디움 브라운 */ ---accent: #c45a3b; /* 테라코타 */ ---border: #ddd8d0; /* 테두리 */ -``` - ---- - -## 레이아웃 시스템 - -### 여백 기준 (padding/margin) -```css -/* 슬라이드 전체 여백 */ -padding: 48pt; - -/* 섹션 간 여백 */ -gap: 32pt; - -/* 요소 간 여백 */ -gap: 16pt; - -/* 텍스트 블록 내 여백 */ -gap: 8pt; -``` - -### 그리드 시스템 -```css -/* 2단 레이아웃 */ -display: grid; -grid-template-columns: 1fr 1fr; -gap: 32pt; - -/* 3단 레이아웃 */ -grid-template-columns: repeat(3, 1fr); - -/* 비대칭 레이아웃 (40:60) */ -grid-template-columns: 2fr 3fr; - -/* 비대칭 레이아웃 (30:70) */ -grid-template-columns: 1fr 2.3fr; -``` - ---- - -## 디자인 컴포넌트 - -> **html2pptx 호환성 주의**: 텍스트 요소(`

`, `

`-`

`)에 직접 border, background, shadow를 적용하면 변환 오류가 발생합니다. 반드시 `
`로 감싸서 스타일을 적용하세요. - -### 1. 뱃지/태그 -```html - -
-

PRESENTATION

-
-``` - -### 2. 섹션 넘버 -```html - -
-

SECTION 1

-
-``` - -### 3. 로고 영역 -```html -
-
-

*

-
-

LogoName

-
-``` - -### 4. 아이콘 버튼 -```html -
-

-
-``` - -### 5. 구분선 -```html -
-``` - -### 6. 정보 그리드 -```html -
-
-

Contact

-

334556774

-
-
-

Date

-

March 2025

-
-
-``` - ---- - -## 슬라이드 템플릿 - -### 1. 표지 슬라이드 (Cover) -```html - - - - - - - - -
-
-
-
-

*

-
-

LogoName

-
-
-

PRESENTATION

-
-
-
-
-

OUR PROJECT

-
-
-

-
-
-
- - -
-

- Business Deck -

-

- Presented by Luna Martinez -

-
- - -
-
-

Contact

-

334556774

-
-
-

Date

-

March 2025

-
-
-

Website

-

www.yourwebsite.com

-
-
- - -``` - -### 2. 목차 슬라이드 (Contents) -```html - - - - - - - - -
-

©2025 YOUR BRAND. ALL RIGHTS RESERVED.

-

- Our
Contents -

-
-

-
-
- - -
-
-

SECTION 1

-

SECTION TITLE

-

(1)

-
-
-

SECTION 2

-

SECTION TITLE

-

(2)

-
-
-

SECTION 3

-

SECTION TITLE

-

(3)

-
-
-

SECTION 4

-

SECTION TITLE

-

(4)

-
-
-

SECTION 5

-

SECTION TITLE

-

(5)

-
-
- - -``` - -### 3. 섹션 구분 슬라이드 (Section Divider) -```html - - - - - - - - -
-
-

SECTION 1

-
-

©2025 YOUR BRAND

-
- - -
-

- Introduction -

-

- Brief description of what this section covers and why it matters. -

-
- - -
-

01

-
- - -``` - -### 4. 콘텐츠 슬라이드 (Content) -```html - - - - - - - - -
-
-

SECTION 1

-

Main Topic

-
-

02

-
- - -
-
-

Key Point One

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore. -

-
-
-

Key Point Two

-

- Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip. -

-
-
- - -
-

www.yourwebsite.com

-

©2025 YOUR BRAND

-
- - -``` - -### 5. 통계/데이터 슬라이드 (Statistics) -```html - - - - - - - - -
-

Key Metrics

-

03

-
- - -
-
-

Revenue Growth

-
-

85%

-

Year over year

-
-
-
-

Active Users

-
-

2.4M

-

+340K this quarter

-
-
-
-

Customer Satisfaction

-
-

4.9

-

Out of 5.0 rating

-
-
-
- - -
-

Source: Internal Analytics 2025

-
- - -``` - -### 6. 이미지 + 텍스트 슬라이드 (Split Layout) -```html - - - - - - - - -
-
-

©2025 YOUR BRAND

-
- - -
-

FEATURE

-

- Transform Your Business -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -

-
-

Learn more

-
-

-
-
-
- - -``` - -### 7. 팀 소개 슬라이드 (Team) -```html - - - - - - - - -
-

Our Team

-

05

-
- - -
-
-
-

John Smith

-

CEO & Founder

-
-
-
-

Sarah Johnson

-

CTO

-
-
-
-

Mike Chen

-

Design Lead

-
-
-
-

Emily Davis

-

Marketing

-
-
- - -``` - -### 8. 인용문 슬라이드 (Quote) -```html - - - - - - - -

"

-

- The best way to predict the future is to create it. -

-
-

Peter Drucker

-

Management Consultant

-
- - -``` - -### 9. 타임라인 슬라이드 (Timeline) -```html - - - - - - - - -
-

Our Journey

-
- - -
-
- -
- - -
-
-

2020

-

Company Founded

-
-
-
-

2021

-

First Product Launch

-
-
-
-

2023

-

Series A Funding

-
-
-
-

2025

-

Global Expansion

-
-
-
- - -
-

06

-
- - -``` - -### 10. 마무리 슬라이드 (Closing) -```html - - - - - - - - -
-
-

*

-
-

LogoName

-
- - -
-

- Thank You -

-

- Questions? Let's discuss. -

-
- - -
-
-

Email

-

hello@company.com

-
-
-

Phone

-

+82 10-1234-5678

-
-
-

Website

-

www.company.com

-
-
- - -``` - ---- - -## 고급 디자인 패턴 - -### 비대칭 레이아웃 -시선을 끄는 독창적인 구성 -```css -/* 황금비율 기반 */ -grid-template-columns: 1fr 1.618fr; - -/* 극단적 비대칭 */ -grid-template-columns: 1fr 3fr; -``` - -### 오버레이 텍스트 -이미지 위 텍스트 배치 -```html -
-
-
-

Overlay Text

-
-
-``` - -### 그라데이션 오버레이 -```html -
-``` - -### 카드 스타일 -```html -
-``` - ---- - -## 텍스트 사용 규칙 - -### 필수 태그 -```html - -

,

-

,
    ,
      ,
    1. - - -
      텍스트
      -텍스트 -``` - -### 권장 사용법 -```html - -

      제목

      -

      본문 텍스트

      - - -
      텍스트 직접 입력
      -``` - ---- - -## 출력 및 파일 구조 - -### 파일 저장 규칙 -``` -slides/ -├── slide-01.html (표지) -├── slide-02.html (목차) -├── slide-03.html (섹션 구분) -├── slide-04.html (내용) -├── ... -└── slide-XX.html (마무리) -``` - -### 파일 명명 규칙 -- 2자리 숫자 사용: `slide-01.html`, `slide-02.html` -- 순서대로 명명 -- 특수문자, 공백 사용 금지 - ---- - -## 워크플로우 - -1. **분석**: `slide-outline.md` 읽고 콘텐츠 파악 -2. **테마 결정**: 색상 팔레트, 전체적인 무드 선택 -3. **구조 설계**: 슬라이드별 레이아웃 타입 결정 -4. **디자인 실행**: 각 슬라이드 HTML 생성 -5. **일관성 검토**: 전체 프레젠테이션의 통일성 확인 -6. **저장**: `slides/` 디렉토리에 파일 저장 - ---- - -## 주의사항 - -1. **CSS 그라데이션**: PowerPoint 변환 시 지원 안됨 - 배경 이미지로 대체 -2. **웹폰트**: Pretendard CDN 링크 항상 포함 -3. **이미지 경로**: 절대 경로 또는 URL 사용 -4. **호환성**: 모든 색상에 # 포함 -5. **텍스트 규칙**: div/span에 직접 텍스트 금지 diff --git a/.claude/skills/differential-review/SKILL.md b/.claude/skills/differential-review/SKILL.md deleted file mode 100644 index b14a515..0000000 --- a/.claude/skills/differential-review/SKILL.md +++ /dev/null @@ -1,220 +0,0 @@ ---- -name: differential-review -description: > - Performs security-focused differential review of code changes (PRs, commits, diffs). - Adapts analysis depth to codebase size, uses git history for context, calculates - blast radius, checks test coverage, and generates comprehensive markdown reports. - Automatically detects and prevents security regressions. -allowed-tools: - - Read - - Write - - Grep - - Glob - - Bash ---- - -# Differential Security Review - -Security-focused code review for PRs, commits, and diffs. - -## Core Principles - -1. **Risk-First**: Focus on auth, crypto, value transfer, external calls -2. **Evidence-Based**: Every finding backed by git history, line numbers, attack scenarios -3. **Adaptive**: Scale to codebase size (SMALL/MEDIUM/LARGE) -4. **Honest**: Explicitly state coverage limits and confidence level -5. **Output-Driven**: Always generate comprehensive markdown report file - ---- - -## Rationalizations (Do Not Skip) - -| Rationalization | Why It's Wrong | Required Action | -|-----------------|----------------|-----------------| -| "Small PR, quick review" | Heartbleed was 2 lines | Classify by RISK, not size | -| "I know this codebase" | Familiarity breeds blind spots | Build explicit baseline context | -| "Git history takes too long" | History reveals regressions | Never skip Phase 1 | -| "Blast radius is obvious" | You'll miss transitive callers | Calculate quantitatively | -| "No tests = not my problem" | Missing tests = elevated risk rating | Flag in report, elevate severity | -| "Just a refactor, no security impact" | Refactors break invariants | Analyze as HIGH until proven LOW | -| "I'll explain verbally" | No artifact = findings lost | Always write report | - ---- - -## Quick Reference - -### Codebase Size Strategy - -| Codebase Size | Strategy | Approach | -|---------------|----------|----------| -| SMALL (<20 files) | DEEP | Read all deps, full git blame | -| MEDIUM (20-200) | FOCUSED | 1-hop deps, priority files | -| LARGE (200+) | SURGICAL | Critical paths only | - -### Risk Level Triggers - -| Risk Level | Triggers | -|------------|----------| -| HIGH | Auth, crypto, external calls, value transfer, validation removal | -| MEDIUM | Business logic, state changes, new public APIs | -| LOW | Comments, tests, UI, logging | - ---- - -## Workflow Overview - -``` -Pre-Analysis → Phase 0: Triage → Phase 1: Code Analysis → Phase 2: Test Coverage - ↓ ↓ ↓ ↓ -Phase 3: Blast Radius → Phase 4: Deep Context → Phase 5: Adversarial → Phase 6: Report -``` - ---- - -## Decision Tree - -**Starting a review?** - -``` -├─ Need detailed phase-by-phase methodology? -│ └─ Read: methodology.md -│ (Pre-Analysis + Phases 0-4: triage, code analysis, test coverage, blast radius) -│ -├─ Analyzing HIGH RISK change? -│ └─ Read: adversarial.md -│ (Phase 5: Attacker modeling, exploit scenarios, exploitability rating) -│ -├─ Writing the final report? -│ └─ Read: reporting.md -│ (Phase 6: Report structure, templates, formatting guidelines) -│ -├─ Looking for specific vulnerability patterns? -│ └─ Read: patterns.md -│ (Regressions, reentrancy, access control, overflow, etc.) -│ -└─ Quick triage only? - └─ Use Quick Reference above, skip detailed docs -``` - ---- - -## Quality Checklist - -Before delivering: - -- [ ] All changed files analyzed -- [ ] Git blame on removed security code -- [ ] Blast radius calculated for HIGH risk -- [ ] Attack scenarios are concrete (not generic) -- [ ] Findings reference specific line numbers + commits -- [ ] Report file generated -- [ ] User notified with summary - ---- - -## Integration - -**audit-context-building skill:** -- Pre-Analysis: Build baseline context -- Phase 4: Deep context on HIGH RISK changes - -**issue-writer skill:** -- Transform findings into formal audit reports -- Command: `issue-writer --input DIFFERENTIAL_REVIEW_REPORT.md --format audit-report` - ---- - -## Example Usage - -### Quick Triage (Small PR) -``` -Input: 5 file PR, 2 HIGH RISK files -Strategy: Use Quick Reference -1. Classify risk level per file (2 HIGH, 3 LOW) -2. Focus on 2 HIGH files only -3. Git blame removed code -4. Generate minimal report -Time: ~30 minutes -``` - -### Standard Review (Medium Codebase) -``` -Input: 80 files, 12 HIGH RISK changes -Strategy: FOCUSED (see methodology.md) -1. Full workflow on HIGH RISK files -2. Surface scan on MEDIUM -3. Skip LOW risk files -4. Complete report with all sections -Time: ~3-4 hours -``` - -### Deep Audit (Large, Critical Change) -``` -Input: 450 files, auth system rewrite -Strategy: SURGICAL + audit-context-building -1. Baseline context with audit-context-building -2. Deep analysis on auth changes only -3. Blast radius analysis -4. Adversarial modeling -5. Comprehensive report -Time: ~6-8 hours -``` - ---- - -## When NOT to Use This Skill - -- **Greenfield code** (no baseline to compare) -- **Documentation-only changes** (no security impact) -- **Formatting/linting** (cosmetic changes) -- **User explicitly requests quick summary only** (they accept risk) - -For these cases, use standard code review instead. - ---- - -## Red Flags (Stop and Investigate) - -**Immediate escalation triggers:** -- Removed code from "security", "CVE", or "fix" commits -- Access control modifiers removed (onlyOwner, internal → external) -- Validation removed without replacement -- External calls added without checks -- High blast radius (50+ callers) + HIGH risk change - -These patterns require adversarial analysis even in quick triage. - ---- - -## Tips for Best Results - -**Do:** -- Start with git blame for removed code -- Calculate blast radius early to prioritize -- Generate concrete attack scenarios -- Reference specific line numbers and commits -- Be honest about coverage limitations -- Always generate the output file - -**Don't:** -- Skip git history analysis -- Make generic findings without evidence -- Claim full analysis when time-limited -- Forget to check test coverage -- Miss high blast radius changes -- Output report only to chat (file required) - ---- - -## Supporting Documentation - -- **[methodology.md](methodology.md)** - Detailed phase-by-phase workflow (Phases 0-4) -- **[adversarial.md](adversarial.md)** - Attacker modeling and exploit scenarios (Phase 5) -- **[reporting.md](reporting.md)** - Report structure and formatting (Phase 6) -- **[patterns.md](patterns.md)** - Common vulnerability patterns reference - ---- - -**For first-time users:** Start with [methodology.md](methodology.md) to understand the complete workflow. - -**For experienced users:** Use this page's Quick Reference and Decision Tree to navigate directly to needed content. diff --git a/.claude/skills/duplicate-file-cleaner/SKILL.md b/.claude/skills/duplicate-file-cleaner/SKILL.md deleted file mode 100755 index 863a19c..0000000 --- a/.claude/skills/duplicate-file-cleaner/SKILL.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -name: duplicate-file-cleaner -description: 중복 이미지 및 미디어 파일을 메타데이터 기반으로 찾아 안전하게 제거합니다. 중복 파일 정리, 파일 정리, 용량 확보 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# Duplicate File Cleaner Expert - -중복 파일을 탐지하고 안전하게 제거하는 스킬입니다. - -## 기능 - -### 탐지 방법 -- **해시 비교**: MD5/SHA256 체크섬으로 정확한 중복 탐지 -- **파일명 유사도**: 이름 기반 잠재적 중복 탐지 -- **메타데이터 분석**: EXIF, 생성일시, 크기 비교 -- **퍼지 매칭**: 리사이즈/재인코딩된 유사 파일 탐지 - -### 지원 파일 유형 -- 이미지: jpg, png, gif, webp, bmp, tiff -- 비디오: mp4, avi, mkv, mov, wmv -- 오디오: mp3, wav, flac, aac -- 문서: pdf, doc, xls (선택적) - -### 안전 기능 -- 삭제 전 미리보기 -- 휴지통으로 이동 (영구 삭제 방지) -- 원본 보존 우선 (수정일 기준) -- 롤백 가능한 로그 생성 - -## 사용 스크립트 - -### Python 중복 탐지 -```python -import hashlib -import os -from pathlib import Path -from collections import defaultdict - -def get_file_hash(filepath, chunk_size=8192): - """파일의 MD5 해시 계산""" - hasher = hashlib.md5() - with open(filepath, 'rb') as f: - while chunk := f.read(chunk_size): - hasher.update(chunk) - return hasher.hexdigest() - -def find_duplicates(directory, extensions=None): - """디렉토리에서 중복 파일 찾기""" - hash_map = defaultdict(list) - - for filepath in Path(directory).rglob('*'): - if not filepath.is_file(): - continue - if extensions and filepath.suffix.lower() not in extensions: - continue - - file_hash = get_file_hash(filepath) - hash_map[file_hash].append(filepath) - - # 중복된 것만 반환 - return {h: files for h, files in hash_map.items() if len(files) > 1} - -def generate_report(duplicates): - """중복 파일 리포트 생성""" - total_size = 0 - report = ["# 중복 파일 리포트\n"] - - for hash_val, files in duplicates.items(): - size = os.path.getsize(files[0]) - savings = size * (len(files) - 1) - total_size += savings - - report.append(f"\n## 해시: {hash_val[:8]}...") - report.append(f"크기: {size:,} bytes | 절약 가능: {savings:,} bytes") - for f in files: - mtime = os.path.getmtime(f) - report.append(f" - {f} (수정: {mtime})") - - report.append(f"\n---\n총 절약 가능 용량: {total_size:,} bytes ({total_size/1024/1024:.2f} MB)") - return '\n'.join(report) - -# 사용 예시 -extensions = {'.jpg', '.jpeg', '.png', '.gif', '.mp4'} -duplicates = find_duplicates('/path/to/directory', extensions) -print(generate_report(duplicates)) -``` - -### Bash 빠른 탐지 -```bash -#!/bin/bash -# 중복 파일 빠른 탐지 (크기 + 해시 기반) - -find "$1" -type f -name "*.jpg" -o -name "*.png" | while read file; do - size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file") - hash=$(md5sum "$file" | cut -d' ' -f1) - echo "$size $hash $file" -done | sort | uniq -D -w 40 -``` - -## 리포트 구조 - -```markdown -# 중복 파일 분석 리포트 - -## 요약 -- 스캔 파일: 1,234개 -- 중복 그룹: 45개 -- 중복 파일: 123개 -- 절약 가능 용량: 2.3 GB - -## 중복 그룹 상세 - -### 그룹 1 (3개 파일, 15.2 MB 절약 가능) -| 파일 | 경로 | 수정일 | 권장 | -|------|------|--------|------| -| photo.jpg | /photos/2023/ | 2023-05-01 | ✅ 유지 | -| photo(1).jpg | /downloads/ | 2023-06-15 | ❌ 삭제 | -| IMG_001.jpg | /backup/ | 2023-05-01 | ❌ 삭제 | - -### 그룹 2 ... - -## 권장 조치 -1. 자동 선택된 78개 파일 삭제 (1.8 GB) -2. 수동 검토 필요: 12개 그룹 -3. 백업 폴더 정리 권장 - -## 실행 명령 -```bash -# 안전 삭제 (휴지통으로 이동) -trash-put file1.jpg file2.jpg ... - -# 또는 영구 삭제 (주의!) -rm file1.jpg file2.jpg ... -``` -``` - -## 사용 예시 - -``` -이 폴더의 중복 파일을 찾아줘 -사진 폴더에서 중복 이미지를 정리해줘 -용량을 확보하기 위해 중복 파일을 분석해줘 -``` - -## 주의사항 - -- 삭제 전 항상 백업 권장 -- 시스템 파일은 스캔에서 제외 -- 심볼릭 링크 주의 필요 -- 클라우드 동기화 폴더 주의 - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/flutter-ux-hardening/SKILL.md b/.claude/skills/flutter-ux-hardening/SKILL.md deleted file mode 100755 index eb84592..0000000 --- a/.claude/skills/flutter-ux-hardening/SKILL.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -name: flutter-ux-hardening -description: Flutter 앱의 UI/UX를 계획, 구현, 테스트, 검토하고 반복적으로 강화하는 가이드 에이전트 스킬입니다. Flutter UI 개선, UX 강화, 접근성 개선 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# Flutter UX Hardening - -Flutter 앱의 UI/UX를 체계적으로 분석하고 개선하는 스킬입니다. - -## 기능 - -### 분석 영역 -- **UI 일관성**: 디자인 시스템 준수 여부 -- **UX 흐름**: 사용자 여정 최적화 -- **접근성**: a11y 지원 (스크린 리더, 색상 대비 등) -- **반응형**: 다양한 화면 크기 대응 -- **에지 케이스**: 빈 상태, 로딩, 에러 처리 - -### 강화 프로세스 - -1. **계획 (Plan)** - - 현재 UI/UX 감사 - - 개선 영역 식별 - - 우선순위 설정 - -2. **구현 (Implement)** - - 위젯 리팩토링 - - 애니메이션 추가 - - 접근성 속성 추가 - -3. **테스트 (Test)** - - 위젯 테스트 작성 - - 통합 테스트 추가 - - 골든 테스트 설정 - -4. **검토 (Review)** - - 코드 품질 검사 - - 성능 프로파일링 - - 사용자 피드백 반영 - -5. **반복 (Iterate)** - - 지속적 개선 - - A/B 테스트 적용 - -## 체크리스트 - -### 접근성 (Accessibility) -```dart -// ✅ Semantics 레이블 추가 -Semantics( - label: '장바구니에 추가', - child: IconButton( - icon: Icon(Icons.add_shopping_cart), - onPressed: _addToCart, - ), -) - -// ✅ 충분한 터치 타겟 크기 (48x48 이상) -SizedBox( - width: 48, - height: 48, - child: IconButton(...), -) - -// ✅ 색상 대비 확인 -// WCAG AA: 4.5:1 (일반 텍스트), 3:1 (큰 텍스트) -``` - -### 에지 케이스 처리 -```dart -// 빈 상태 -if (items.isEmpty) { - return EmptyStateWidget( - icon: Icons.inbox, - title: '항목이 없습니다', - subtitle: '새 항목을 추가해보세요', - action: ElevatedButton( - onPressed: _addItem, - child: Text('항목 추가'), - ), - ); -} - -// 로딩 상태 -if (isLoading) { - return ShimmerLoadingWidget(); -} - -// 에러 상태 -if (hasError) { - return ErrorWidget( - message: error.message, - onRetry: _retry, - ); -} -``` - -### 반응형 레이아웃 -```dart -LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 900) { - return DesktopLayout(); - } else if (constraints.maxWidth > 600) { - return TabletLayout(); - } - return MobileLayout(); - }, -) -``` - -### 애니메이션 -```dart -// 부드러운 전환 -AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - // ... -) - -// 히어로 애니메이션 -Hero( - tag: 'item-${item.id}', - child: ItemCard(item: item), -) -``` - -## 리포트 구조 - -```markdown -# Flutter UX 강화 리포트 - -## 현재 상태 분석 -- UI 일관성: ⭐⭐⭐☆☆ -- 접근성: ⭐⭐☆☆☆ -- 반응형: ⭐⭐⭐⭐☆ -- 에지 케이스 처리: ⭐⭐☆☆☆ - -## 식별된 개선 영역 -1. 접근성 레이블 누락 (15개 위젯) -2. 빈 상태 UI 미구현 (3개 화면) -3. 로딩 스켈레톤 미적용 (5개 리스트) - -## 권장 조치 -| 우선순위 | 영역 | 조치 | -|----------|------|------| -| 높음 | 접근성 | Semantics 추가 | -| 높음 | 에러 | 에러 바운더리 구현 | -| 중간 | 로딩 | Shimmer 효과 추가 | - -## 구현 계획 -... -``` - -## 사용 예시 - -``` -Flutter 앱의 UX를 개선해줘 -접근성을 강화하고 싶어 -빈 상태와 에러 처리를 추가해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/frontend-design/SKILL.md b/.claude/skills/frontend-design/SKILL.md deleted file mode 100644 index 5be498e..0000000 --- a/.claude/skills/frontend-design/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: frontend-design -description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. -license: Complete terms in LICENSE.txt ---- - -This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. - -The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. - -## Design Thinking - -Before coding, understand the context and commit to a BOLD aesthetic direction: -- **Purpose**: What problem does this interface solve? Who uses it? -- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. -- **Constraints**: Technical requirements (framework, performance, accessibility). -- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? - -**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. - -Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: -- Production-grade and functional -- Visually striking and memorable -- Cohesive with a clear aesthetic point-of-view -- Meticulously refined in every detail - -## Frontend Aesthetics Guidelines - -Focus on: -- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. -- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. -- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. -- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. -- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. - -NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. - -Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. - -**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. - -Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/.claude/skills/insecure-defaults/SKILL.md b/.claude/skills/insecure-defaults/SKILL.md deleted file mode 100644 index d1b29cd..0000000 --- a/.claude/skills/insecure-defaults/SKILL.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -name: insecure-defaults -description: "Detects fail-open insecure defaults (hardcoded secrets, weak auth, permissive security) that allow apps to run insecurely in production. Use when auditing security, reviewing config management, or analyzing environment variable handling." -allowed-tools: - - Read - - Grep - - Glob - - Bash ---- - -# Insecure Defaults Detection - -Finds **fail-open** vulnerabilities where apps run insecurely with missing configuration. Distinguishes exploitable defaults from fail-secure patterns that crash safely. - -- **Fail-open (CRITICAL):** `SECRET = env.get('KEY') or 'default'` → App runs with weak secret -- **Fail-secure (SAFE):** `SECRET = env['KEY']` → App crashes if missing - -## When to Use - -- **Security audits** of production applications (auth, crypto, API security) -- **Configuration review** of deployment files, IaC templates, Docker configs -- **Code review** of environment variable handling and secrets management -- **Pre-deployment checks** for hardcoded credentials or weak defaults - -## When NOT to Use - -Do not use this skill for: -- **Test fixtures** explicitly scoped to test environments (files in `test/`, `spec/`, `__tests__/`) -- **Example/template files** (`.example`, `.template`, `.sample` suffixes) -- **Development-only tools** (local Docker Compose for dev, debug scripts) -- **Documentation examples** in README.md or docs/ directories -- **Build-time configuration** that gets replaced during deployment -- **Crash-on-missing behavior** where app won't start without proper config (fail-secure) - -When in doubt: trace the code path to determine if the app runs with the default or crashes. - -## Rationalizations to Reject - -- **"It's just a development default"** → If it reaches production code, it's a finding -- **"The production config overrides it"** → Verify prod config exists; code-level vulnerability remains if not -- **"This would never run without proper config"** → Prove it with code trace; many apps fail silently -- **"It's behind authentication"** → Defense in depth; compromised session still exploits weak defaults -- **"We'll fix it before release"** → Document now; "later" rarely comes - -## Workflow - -Follow this workflow for every potential finding: - -### 1. SEARCH: Perform Project Discovery and Find Insecure Defaults - -Determine language, framework, and project conventions. Use this information to further discover things like secret storage locations, secret usage patterns, credentialed third-party integrations, cryptography, and any other relevant configuration. Further use information to analyze insecure default configurations. - -**Example** -Search for patterns in `**/config/`, `**/auth/`, `**/database/`, and env files: -- **Fallback secrets:** `getenv.*\) or ['"]`, `process\.env\.[A-Z_]+ \|\| ['"]`, `ENV\.fetch.*default:` -- **Hardcoded credentials:** `password.*=.*['"][^'"]{8,}['"]`, `api[_-]?key.*=.*['"][^'"]+['"]` -- **Weak defaults:** `DEBUG.*=.*true`, `AUTH.*=.*false`, `CORS.*=.*\*` -- **Crypto algorithms:** `MD5|SHA1|DES|RC4|ECB` in security contexts - -Tailor search approach based on discovery results. - -Focus on production-reachable code, not test fixtures or example files. - -### 2. VERIFY: Actual Behavior -For each match, trace the code path to understand runtime behavior. - -**Questions to answer:** -- When is this code executed? (Startup vs. runtime) -- What happens if a configuration variable is missing? -- Is there validation that enforces secure configuration? - -### 3. CONFIRM: Production Impact -Determine if this issue reaches production: - -If production config provides the variable → Lower severity (but still a code-level vulnerability) -If production config missing or uses default → CRITICAL - -### 4. REPORT: with Evidence - -**Example report:** -``` -Finding: Hardcoded JWT Secret Fallback -Location: src/auth/jwt.ts:15 -Pattern: const secret = process.env.JWT_SECRET || 'default'; - -Verification: App starts without JWT_SECRET; secret used in jwt.sign() at line 42 -Production Impact: Dockerfile missing JWT_SECRET -Exploitation: Attacker forges JWTs using 'default', gains unauthorized access -``` - -## Quick Verification Checklist - -**Fallback Secrets:** `SECRET = env.get(X) or Y` -→ Verify: App starts without env var? Secret used in crypto/auth? -→ Skip: Test fixtures, example files - -**Default Credentials:** Hardcoded `username`/`password` pairs -→ Verify: Active in deployed config? No runtime override? -→ Skip: Disabled accounts, documentation examples - -**Fail-Open Security:** `AUTH_REQUIRED = env.get(X, 'false')` -→ Verify: Default is insecure (false/disabled/permissive)? -→ Safe: App crashes or default is secure (true/enabled/restricted) - -**Weak Crypto:** MD5/SHA1/DES/RC4/ECB in security contexts -→ Verify: Used for passwords, encryption, or tokens? -→ Skip: Checksums, non-security hashing - -**Permissive Access:** CORS `*`, permissions `0777`, public-by-default -→ Verify: Default allows unauthorized access? -→ Skip: Explicitly configured permissiveness with justification - -**Debug Features:** Stack traces, introspection, verbose errors -→ Verify: Enabled by default? Exposed in responses? -→ Skip: Logging-only, not user-facing - -For detailed examples and counter-examples, see [examples.md](references/examples.md). diff --git a/.claude/skills/layer-boundary-auditor/SKILL.md b/.claude/skills/layer-boundary-auditor/SKILL.md deleted file mode 100644 index 5c3ddce..0000000 --- a/.claude/skills/layer-boundary-auditor/SKILL.md +++ /dev/null @@ -1,323 +0,0 @@ ---- -name: ln-642-layer-boundary-auditor -description: "L3 Worker. Audits layer boundaries + cross-layer consistency: I/O violations, transaction boundaries (commit ownership), session ownership (DI vs local), async consistency (sync I/O in async), fire-and-forget tasks." ---- - -# Layer Boundary Auditor - -L3 Worker that audits architectural layer boundaries and detects violations. - -## Purpose & Scope - -- Read architecture.md to discover project's layer structure -- Detect layer violations (I/O code outside infrastructure layer) -- **Detect cross-layer consistency issues:** - - Transaction boundaries (commit/rollback ownership) - - Session ownership (DI vs local) - - Async consistency (sync I/O in async) - - Fire-and-forget tasks (unhandled exceptions) -- Check pattern coverage (all HTTP calls use client abstraction) -- Detect error handling duplication -- Return violations list to coordinator - -## Input (from ln-640) - -``` -- architecture_path: string # Path to docs/architecture.md -- codebase_root: string # Root directory to scan -- skip_violations: string[] # Files to skip (legacy) -``` - -## Workflow - -### Phase 1: Discover Architecture - -``` -Read docs/architecture.md - -Extract from Section 4.2 (Top-Level Decomposition): - - architecture_type: "Layered" | "Hexagonal" | "Clean" | "MVC" | etc. - - layers: [{name, directories[], purpose}] - -Extract from Section 5.3 (Infrastructure Layer Components): - - infrastructure_components: [{name, responsibility}] - -IF architecture.md not found: - Use fallback presets from common_patterns.md - -Build ruleset: - FOR EACH layer: - allowed_deps = layers that can be imported - forbidden_deps = layers that cannot be imported -``` - -### Phase 2: Detect Layer Violations - -``` -FOR EACH violation_type IN common_patterns.md I/O Pattern Boundary Rules: - grep_pattern = violation_type.detection_grep - forbidden_dirs = violation_type.forbidden_in - - matches = Grep(grep_pattern, codebase_root, include="*.py,*.ts,*.js") - - FOR EACH match IN matches: - IF match.path NOT IN skip_violations: - IF any(forbidden IN match.path FOR forbidden IN forbidden_dirs): - violations.append({ - type: "layer_violation", - severity: "HIGH", - pattern: violation_type.name, - file: match.path, - line: match.line, - code: match.context, - allowed_in: violation_type.allowed_in, - suggestion: f"Move to {violation_type.allowed_in}" - }) -``` - -### Phase 2.5: Cross-Layer Consistency Checks - -#### 2.5.1 Transaction Boundary Violations - -**What:** commit()/rollback() called at inconsistent layers (repo + service + API) - -**Detection:** -``` -repo_commits = Grep("\.commit\(\)|\.rollback\(\)", "**/repositories/**/*.py") -service_commits = Grep("\.commit\(\)|\.rollback\(\)", "**/services/**/*.py") -api_commits = Grep("\.commit\(\)|\.rollback\(\)", "**/api/**/*.py") - -layers_with_commits = count([repo_commits, service_commits, api_commits].filter(len > 0)) -``` - -**Safe Patterns (ignore):** -- Comment "# best-effort telemetry" in same context -- File ends with `_callbacks.py` (progress notifiers) -- Explicit `# UoW boundary` comment - -**Violation Rules:** - -| Condition | Severity | Issue | -|-----------|----------|-------| -| layers_with_commits >= 3 | CRITICAL | Mixed UoW ownership across all layers | -| repo + api commits | HIGH | Transaction control bypasses service layer | -| repo + service commits | HIGH | Ambiguous UoW owner (repo vs service) | -| service + api commits | MEDIUM | Transaction control spans service + API | - -**Recommendation:** Choose single UoW owner (service layer recommended), remove commit() from other layers - -**Effort:** L (requires architectural decision + refactoring) - -#### 2.5.2 Session Ownership Violations - -**What:** Mixed DI-injected and locally-created sessions in same call chain - -**Detection:** -``` -di_session = Grep("Depends\(get_session\)|Depends\(get_db\)", "**/api/**/*.py") -local_session = Grep("AsyncSessionLocal\(\)|async_sessionmaker", "**/services/**/*.py") -local_in_repo = Grep("AsyncSessionLocal\(\)", "**/repositories/**/*.py") -``` - -**Violation Rules:** - -| Condition | Severity | Issue | -|-----------|----------|-------| -| di_session AND local_in_repo in same module | HIGH | Repo creates own session while API injects different | -| local_session in service calling DI-based repo | MEDIUM | Session mismatch in call chain | - -**Recommendation:** Use DI consistently OR use local sessions consistently. Document exceptions (e.g., telemetry) - -**Effort:** M - -#### 2.5.3 Async Consistency Violations - -**What:** Synchronous blocking I/O inside async functions - -**Detection:** -``` -# For each file with "async def": -sync_file_io = Grep("\.read_bytes\(\)|\.read_text\(\)|\.write_bytes\(\)|\.write_text\(\)", file) -sync_open = Grep("(? 0: - coverage = len(abstracted_calls) / len(all_http_calls) * 100 - IF coverage < 90%: - violations.append({ - type: "low_coverage", - severity: "MEDIUM", - pattern: "HTTP Client Abstraction", - coverage: coverage, - uncovered_files: files with direct calls outside infrastructure - }) - -# Error Handling Duplication -http_error_handlers = Grep("except\\s+(httpx\\.|aiohttp\\.|requests\\.)", codebase_root) -unique_files = set(f.path for f in http_error_handlers) - -IF len(unique_files) > 2: - violations.append({ - type: "duplication", - severity: "MEDIUM", - pattern: "HTTP Error Handling", - files: list(unique_files), - suggestion: "Centralize in infrastructure layer" - }) -``` - -### Phase 3.5: Calculate Score - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -### Phase 4: Return Result - -```json -{ - "category": "Layer Boundary", - "score": 4.5, - "total_issues": 8, - "critical": 1, - "high": 3, - "medium": 4, - "low": 0, - "architecture": { - "type": "Layered", - "layers": ["api", "services", "domain", "infrastructure"] - }, - "checks": [ - {"id": "io_isolation", "name": "I/O Isolation", "status": "failed", "details": "HTTP client found in domain layer"}, - {"id": "http_abstraction", "name": "HTTP Abstraction", "status": "warning", "details": "75% coverage, 3 direct calls outside infrastructure"}, - {"id": "error_centralization", "name": "Error Centralization", "status": "failed", "details": "HTTP error handlers in 4 files, should be centralized"}, - {"id": "transaction_boundary", "name": "Transaction Boundary", "status": "failed", "details": "commit() in repos (3), services (2), api (4) - mixed UoW ownership"}, - {"id": "session_ownership", "name": "Session Ownership", "status": "passed", "details": "DI-based sessions used consistently"}, - {"id": "async_consistency", "name": "Async Consistency", "status": "failed", "details": "Blocking I/O in async functions detected"}, - {"id": "fire_and_forget", "name": "Fire-and-Forget Handling", "status": "warning", "details": "2 tasks without error handlers"} - ], - "findings": [ - { - "severity": "CRITICAL", - "location": "app/", - "issue": "Mixed UoW ownership: commit() found in repositories (3), services (2), api (4)", - "principle": "Layer Boundary / Transaction Control", - "recommendation": "Choose single UoW owner (service layer recommended), remove commit() from other layers", - "effort": "L" - }, - { - "severity": "HIGH", - "location": "app/services/job/service.py:45", - "issue": "Blocking file I/O in async: Path.read_bytes() inside async def process_job()", - "principle": "Layer Boundary / Async Consistency", - "recommendation": "Use asyncio.to_thread(path.read_bytes) or aiofiles", - "effort": "S" - }, - { - "severity": "HIGH", - "location": "app/domain/pdf/parser.py:45", - "issue": "Layer violation: HTTP client used in domain layer", - "principle": "Layer Boundary / I/O Isolation", - "recommendation": "Move httpx.AsyncClient to infrastructure/http/clients/", - "effort": "M" - }, - { - "severity": "MEDIUM", - "location": "app/api/v1/jobs.py:78", - "issue": "Fire-and-forget task without error handler: create_task(notify_user())", - "principle": "Layer Boundary / Task Error Handling", - "recommendation": "Add task.add_done_callback(handle_exception) or document with # fire-and-forget comment", - "effort": "S" - } - ], - "coverage": { - "http_abstraction": 75, - "error_centralization": false, - "transaction_boundary_consistent": false, - "session_ownership_consistent": true, - "async_io_consistent": false, - "fire_and_forget_handled": false - } -} -``` - -## Critical Rules - -- **Read architecture.md first** - never assume architecture type -- **Skip violations list** - respect legacy files marked for gradual fix -- **File + line + code** - always provide exact location with context -- **Actionable suggestions** - always tell WHERE to move the code -- **No false positives** - verify path contains forbidden dir, not just substring - -## Definition of Done - -- Architecture discovered from docs/architecture.md (or fallback used) -- All violation types from common_patterns.md checked -- **Cross-layer consistency checked:** - - Transaction boundaries analyzed (commit/rollback distribution) - - Session ownership analyzed (DI vs local) - - Async consistency analyzed (sync I/O in async functions) - - Fire-and-forget tasks analyzed (error handling) -- Coverage calculated for HTTP abstraction + 4 consistency metrics -- Violations list with severity, location, suggestion -- Summary counts returned to coordinator - -## Reference Files - -- Layer rules: `../ln-640-pattern-evolution-auditor/references/common_patterns.md` -- Scoring impact: `../ln-640-pattern-evolution-auditor/references/scoring_rules.md` - ---- - -**Version:** 2.0.0 -**Last Updated:** 2026-02-04 diff --git a/.claude/skills/node-debug-logging-middleware/SKILL.md b/.claude/skills/node-debug-logging-middleware/SKILL.md deleted file mode 100755 index 72c297f..0000000 --- a/.claude/skills/node-debug-logging-middleware/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: node-debug-logging-middleware -description: Node.js Express/Koa 앱에 상세 디버깅 로그를 출력하는 미들웨어를 추가합니다. 미들웨어 로깅, 요청 추적, Node.js 디버깅 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# Node Debug Logging Middleware - -Node.js 웹 애플리케이션에 상세한 디버깅 로그 미들웨어를 추가하는 스킬입니다. - -## 기능 - -### 로깅 대상 -- HTTP 요청/응답 상세 정보 -- 요청 바디 및 쿼리 파라미터 -- 응답 시간 및 상태 코드 -- 에러 스택 트레이스 -- 메모리 사용량 - -### 지원 프레임워크 -- Express.js -- Koa.js -- Fastify -- NestJS - -### 출력 옵션 -- 콘솔 (개발환경) -- 파일 (프로덕션) -- 외부 서비스 (Sentry, Datadog 등) - -## Express.js 미들웨어 - -```javascript -const fs = require('fs'); -const path = require('path'); - -/** - * 디버그 로깅 미들웨어 생성 - * @param {Object} options - 설정 옵션 - * @param {string} options.logDir - 로그 파일 디렉토리 - * @param {boolean} options.logBody - 요청/응답 바디 로깅 여부 - * @param {boolean} options.logHeaders - 헤더 로깅 여부 - * @param {number} options.bodyLimit - 바디 로깅 최대 크기 (bytes) - */ -function createDebugLogger(options = {}) { - const { - logDir = './logs', - logBody = true, - logHeaders = false, - bodyLimit = 10000 - } = options; - - // 로그 디렉토리 생성 - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - } - - const getLogStream = () => { - const date = new Date().toISOString().split('T')[0]; - const logFile = path.join(logDir, `debug-${date}.log`); - return fs.createWriteStream(logFile, { flags: 'a' }); - }; - - return (req, res, next) => { - const startTime = Date.now(); - const requestId = Math.random().toString(36).substring(7); - - // 요청 정보 수집 - const requestLog = { - id: requestId, - timestamp: new Date().toISOString(), - method: req.method, - url: req.originalUrl, - ip: req.ip || req.connection.remoteAddress, - userAgent: req.get('User-Agent') - }; - - if (logHeaders) { - requestLog.headers = req.headers; - } - - if (logBody && req.body && Object.keys(req.body).length > 0) { - const bodyStr = JSON.stringify(req.body); - requestLog.body = bodyStr.length > bodyLimit - ? bodyStr.substring(0, bodyLimit) + '...[truncated]' - : req.body; - } - - // 응답 가로채기 - const originalSend = res.send; - let responseBody; - - res.send = function(body) { - responseBody = body; - return originalSend.call(this, body); - }; - - // 응답 완료 시 로깅 - res.on('finish', () => { - const duration = Date.now() - startTime; - - const logEntry = { - ...requestLog, - response: { - statusCode: res.statusCode, - duration: `${duration}ms`, - contentLength: res.get('Content-Length') - } - }; - - if (logBody && responseBody && res.statusCode >= 400) { - try { - const bodyStr = typeof responseBody === 'string' - ? responseBody - : JSON.stringify(responseBody); - logEntry.response.body = bodyStr.length > bodyLimit - ? bodyStr.substring(0, bodyLimit) + '...[truncated]' - : responseBody; - } catch (e) { - // 바디 파싱 실패 무시 - } - } - - // 콘솔 출력 - const color = res.statusCode >= 500 ? '\x1b[31m' - : res.statusCode >= 400 ? '\x1b[33m' - : '\x1b[32m'; - console.log( - `${color}[${requestLog.timestamp}] ${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms\x1b[0m` - ); - - // 파일 출력 - const stream = getLogStream(); - stream.write(JSON.stringify(logEntry) + '\n'); - stream.end(); - }); - - next(); - }; -} - -module.exports = createDebugLogger; -``` - -## 사용법 - -```javascript -const express = require('express'); -const createDebugLogger = require('./middleware/debugLogger'); - -const app = express(); - -// JSON 파싱 (로깅 전에 설정) -app.use(express.json()); - -// 디버그 로거 적용 -app.use(createDebugLogger({ - logDir: './logs', - logBody: true, - logHeaders: process.env.NODE_ENV === 'development', - bodyLimit: 5000 -})); - -// 라우트 정의 -app.get('/api/users', (req, res) => { - res.json({ users: [] }); -}); - -app.listen(3000); -``` - -## Koa.js 버전 - -```javascript -async function koaDebugLogger(ctx, next) { - const startTime = Date.now(); - - console.log(`--> ${ctx.method} ${ctx.url}`); - - try { - await next(); - } catch (err) { - console.error(`[ERROR] ${err.message}`); - throw err; - } - - const duration = Date.now() - startTime; - console.log(`<-- ${ctx.method} ${ctx.url} ${ctx.status} ${duration}ms`); -} - -// 사용 -app.use(koaDebugLogger); -``` - -## 로그 출력 예시 - -```json -{ - "id": "a1b2c3", - "timestamp": "2024-01-15T10:30:45.123Z", - "method": "POST", - "url": "/api/users", - "ip": "192.168.1.100", - "userAgent": "Mozilla/5.0...", - "body": { "name": "홍길동", "email": "hong@example.com" }, - "response": { - "statusCode": 201, - "duration": "45ms", - "contentLength": "156" - } -} -``` - -## 사용 예시 - -``` -Express 앱에 요청 로깅 미들웨어를 추가해줘 -API 호출을 디버깅하기 위한 로그를 설정해줘 -Node.js 서버에 상세 로깅을 추가해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/npm-release-manager/SKILL.md b/.claude/skills/npm-release-manager/SKILL.md deleted file mode 100755 index ef78823..0000000 --- a/.claude/skills/npm-release-manager/SKILL.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -name: npm-release-manager -description: NPM 패키지 배포 프로세스를 자동화합니다. 버전 관리, changelog 생성, npm 배포 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# NPM Release Manager - -NPM 패키지의 릴리스 프로세스를 자동화하는 스킬입니다. - -## 기능 - -### 릴리스 워크플로우 -1. **변경 사항 분석**: git diff로 변경 내용 파악 -2. **버전 결정**: Semantic Versioning (major.minor.patch) -3. **Changelog 생성**: 커밋 메시지 기반 자동 생성 -4. **버전 범프**: package.json 버전 업데이트 -5. **Git 태그**: 버전 태그 생성 -6. **NPM 배포**: npm publish 실행 - -### Semantic Versioning 가이드 -- **Major (x.0.0)**: 호환되지 않는 API 변경 -- **Minor (0.x.0)**: 하위 호환 기능 추가 -- **Patch (0.0.x)**: 하위 호환 버그 수정 - -### Changelog 형식 -```markdown -# Changelog - -## [1.2.0] - 2024-01-15 - -### Added -- 새로운 기능 A 추가 -- 새로운 기능 B 추가 - -### Changed -- 기존 기능 C 개선 - -### Fixed -- 버그 D 수정 -- 버그 E 수정 - -### Deprecated -- 기능 F 지원 중단 예정 - -### Removed -- 기능 G 제거 - -### Security -- 보안 취약점 H 패치 -``` - -## 릴리스 프로세스 - -### 1. 사전 검사 -```bash -# 테스트 실행 -npm test - -# 린트 검사 -npm run lint - -# 빌드 확인 -npm run build -``` - -### 2. 버전 업데이트 -```bash -# patch 릴리스 -npm version patch - -# minor 릴리스 -npm version minor - -# major 릴리스 -npm version major -``` - -### 3. Changelog 업데이트 -- 커밋 메시지 분석 -- 카테고리별 분류 -- CHANGELOG.md 업데이트 - -### 4. 배포 -```bash -# npm 로그인 확인 -npm whoami - -# 배포 -npm publish - -# 또는 스코프 패키지 -npm publish --access public -``` - -### 5. Git 푸시 -```bash -git push origin main --tags -``` - -## 커밋 메시지 규약 - -Conventional Commits 형식 권장: -- `feat:` 새로운 기능 (minor) -- `fix:` 버그 수정 (patch) -- `docs:` 문서 변경 -- `style:` 코드 스타일 변경 -- `refactor:` 리팩토링 -- `test:` 테스트 추가/수정 -- `chore:` 빌드/도구 변경 -- `BREAKING CHANGE:` 호환되지 않는 변경 (major) - -## 사용 예시 - -``` -새 버전을 배포해줘 -패치 릴리스를 진행해줘 -changelog를 업데이트하고 npm에 배포해줘 -``` - -## 주의사항 - -- npm 로그인 상태 확인 필요 -- 2FA 설정 시 OTP 입력 필요 -- private 패키지는 유료 계정 필요 -- 배포 전 테스트 통과 필수 - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/observability-auditor/SKILL.md b/.claude/skills/observability-auditor/SKILL.md deleted file mode 100644 index 0ea24aa..0000000 --- a/.claude/skills/observability-auditor/SKILL.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -name: ln-627-observability-auditor -description: Observability audit worker (L3). Checks structured logging, health check endpoints, metrics collection, request tracing, log levels. Returns findings with severity, location, effort, recommendations. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Observability Auditor (L3 Worker) - -Specialized worker auditing logging, monitoring, and observability. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** -- Audit **observability** (Category 10: Medium Priority) -- Check logging, health checks, metrics, tracing -- Calculate compliance score (X/10) - -## Inputs (from Coordinator) - -Receives `contextStore` with tech stack, framework, codebase root. - -## Workflow - -1) Parse context -2) Check observability patterns -3) Collect findings -4) Calculate score -5) Return JSON - -## Audit Rules - -### 1. Structured Logging -**Detection:** -- Grep for `console.log` (unstructured) -- Check for proper logger: winston, pino, logrus, zap - -**Severity:** -- **MEDIUM:** Production code using console.log -- **LOW:** Dev code using console.log - -**Recommendation:** Use structured logger (winston, pino) - -**Effort:** M (add logger, replace calls) - -### 2. Health Check Endpoints -**Detection:** -- Grep for `/health`, `/ready`, `/live` routes -- Check API route definitions - -**Severity:** -- **HIGH:** No health check endpoint (monitoring blind spot) - -**Recommendation:** Add `/health` endpoint - -**Effort:** S (add simple route) - -### 3. Metrics Collection -**Detection:** -- Check for Prometheus client, StatsD, CloudWatch -- Grep for metric recording: `histogram`, `counter` - -**Severity:** -- **MEDIUM:** No metrics instrumentation - -**Recommendation:** Add Prometheus metrics - -**Effort:** M (instrument code) - -### 4. Request Tracing -**Detection:** -- Check for correlation IDs in logs -- Verify trace propagation (OpenTelemetry, Zipkin) - -**Severity:** -- **MEDIUM:** No correlation IDs (hard to debug distributed systems) - -**Recommendation:** Add request ID middleware - -**Effort:** M (add middleware, propagate IDs) - -### 5. Log Levels -**Detection:** -- Check if logger supports levels (info, warn, error, debug) -- Verify proper level usage - -**Severity:** -- **LOW:** Only error logging (insufficient visibility) - -**Recommendation:** Add info/debug logs - -**Effort:** S (add log statements) - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -```json -{ - "category": "Observability", - "score": 6, - "total_issues": 5, - "critical": 0, - "high": 1, - "medium": 3, - "low": 1, - "checks": [ - {"id": "structured_logging", "name": "Structured Logging", "status": "warning", "details": "3 console.log calls in production code"}, - {"id": "health_endpoints", "name": "Health Endpoints", "status": "failed", "details": "No /health endpoint found"}, - {"id": "metrics_collection", "name": "Metrics Collection", "status": "passed", "details": "Prometheus client configured"}, - {"id": "request_tracing", "name": "Request Tracing", "status": "warning", "details": "Correlation IDs missing in 2 services"} - ], - "findings": [ - { - "severity": "HIGH", - "location": "src/api/server.ts", - "issue": "No /health endpoint for monitoring", - "principle": "Observability / Health Checks", - "recommendation": "Add GET /health route returning { status: 'ok', uptime, ... }", - "effort": "S" - } - ] -} -``` - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/pdf-template-skill/SKILL.md b/.claude/skills/pdf-template-skill/SKILL.md deleted file mode 100755 index 243c834..0000000 --- a/.claude/skills/pdf-template-skill/SKILL.md +++ /dev/null @@ -1,211 +0,0 @@ -# PDF Template Skill - -PDF 문서의 구조와 디자인을 분석하여 동일한 형태의 PPTX 템플릿을 생성하는 기술입니다. - -## 📋 기능 개요 - -### 핵심 기능 -- **PDF 구조 분석**: 레이아웃, 폰트, 색상, 위치 정보 추출 -- **템플릿 생성**: PDF와 동일한 형태의 PPTX 템플릿 제작 -- **내용 교체**: 기존 내용을 새로운 데이터로 자동 교체 -- **형태 보존**: 원본 PDF의 디자인과 레이아웃 완벽 유지 - -### 지원 기능 -- 다중 슬라이드 템플릿 추출 -- 테이블 구조 인식 및 재생성 -- 이미지 및 로고 영역 매핑 -- 폰트 및 색상 스타일 보존 - -## 🚀 사용법 - -### 1. PDF 템플릿 분석 -```bash -node .claude/skills/pdf-template-skill/scripts/analyze-template.js \ - --input pdf_sample/sample.pdf \ - --output templates/sample_template.json -``` - -### 2. 내용 교체로 PPTX 생성 -```bash -node .claude/skills/pdf-template-skill/scripts/generate-from-template.js \ - --template templates/sample_template.json \ - --data project_data.json \ - --output result.pptx -``` - -### 3. 통합 실행 -```bash -npm run template-extract # PDF → 템플릿 추출 -npm run template-generate # 템플릿 → PPTX 생성 -``` - -## 📊 템플릿 구조 - -### Template JSON Format -```json -{ - "metadata": { - "source": "sample.pdf", - "pages": 5, - "extractedAt": "2025-01-03", - "title": "SAM ERP 견적서" - }, - "slides": [ - { - "slideNumber": 1, - "type": "cover", - "layout": { - "width": 10, - "height": 7.5 - }, - "elements": [ - { - "type": "text", - "id": "title", - "position": { "x": 1, "y": 3, "w": 8, "h": 1.5 }, - "style": { - "fontSize": 36, - "bold": true, - "color": "0066CC", - "align": "center" - }, - "placeholder": "{{title}}", - "defaultValue": "견적서" - } - ] - } - ], - "styles": { - "colors": { - "primary": "0066CC", - "secondary": "333333", - "accent": "00AA00" - }, - "fonts": { - "title": { "face": "Arial", "size": 24, "bold": true }, - "body": { "face": "Arial", "size": 12 } - } - }, - "variables": { - "title": "string", - "company": "string", - "date": "date", - "items": "array" - } -} -``` - -### Data Input Format -```json -{ - "title": "스마트 재고 관리 시스템", - "company": "(주) 테크솔루션", - "date": "2025-01-03", - "client": { - "name": "고객사명", - "site": "프로젝트명", - "address": "주소" - }, - "items": [ - { - "name": "상품명", - "quantity": 1, - "price": 1000000, - "description": "상세 설명" - } - ] -} -``` - -## 🔧 고급 기능 - -### 1. 동적 테이블 생성 -```javascript -// 테이블 구조 정의 -{ - "type": "table", - "id": "itemsTable", - "template": { - "headers": ["번호", "품명", "수량", "단가", "금액"], - "rowTemplate": "{{index}}, {{name}}, {{quantity}}, {{price}}, {{total}}", - "dataSource": "items" - } -} -``` - -### 2. 조건부 콘텐츠 -```javascript -// 조건부 표시 -{ - "type": "conditional", - "condition": "items.length > 10", - "ifTrue": { /* 대용량 테이블 레이아웃 */ }, - "ifFalse": { /* 기본 테이블 레이아웃 */ } -} -``` - -### 3. 계산 필드 -```javascript -// 자동 계산 -{ - "type": "calculated", - "formula": "SUM(items.price * items.quantity)", - "format": "currency" -} -``` - -## 📋 워크플로우 - -### Phase 1: PDF 분석 -1. **페이지 분할**: PDF를 개별 페이지로 분리 -2. **요소 인식**: 텍스트, 이미지, 테이블, 도형 위치 추출 -3. **스타일 분석**: 폰트, 색상, 크기 정보 수집 -4. **구조 매핑**: 논리적 구조와 시각적 배치 연결 - -### Phase 2: 템플릿 생성 -1. **변수 식별**: 교체 가능한 콘텐츠 영역 표시 -2. **레이아웃 정의**: 고정 요소와 가변 요소 분리 -3. **스타일 시트**: 일관된 디자인 규칙 정의 -4. **검증**: 생성된 템플릿의 정확성 확인 - -### Phase 3: 콘텐츠 적용 -1. **데이터 바인딩**: JSON 데이터를 템플릿 변수에 매핑 -2. **PPTX 생성**: PptxGenJS를 사용한 슬라이드 생성 -3. **스타일 적용**: 원본과 동일한 디자인 재현 -4. **품질 검증**: 생성된 PPTX의 형태 확인 - -## 🎯 적용 사례 - -### 1. 견적서 시스템 -- **템플릿**: SAM ERP 견적서 PDF -- **변수**: 고객정보, 상품목록, 금액계산 -- **결과**: 동일한 형태의 견적서 PPTX - -### 2. 기획서 시스템 -- **템플릿**: 표준 기획서 PDF -- **변수**: 프로젝트 정보, 기능 명세, 일정 -- **결과**: 일관된 형태의 기획서 PPTX - -### 3. 보고서 시스템 -- **템플릿**: 월간 보고서 PDF -- **변수**: 실적 데이터, 차트, 분석 내용 -- **결과**: 표준화된 보고서 PPTX - -## 🔮 확장 계획 - -### Short-term -- [ ] 더 정교한 테이블 구조 인식 -- [ ] 차트 및 그래프 템플릿 지원 -- [ ] 다국어 템플릿 처리 - -### Long-term -- [ ] AI 기반 레이아웃 최적화 -- [ ] 실시간 템플릿 편집기 -- [ ] 클라우드 템플릿 저장소 - -## 📚 참고 자료 - -- PptxGenJS Documentation -- PDF.js Parser Reference -- Template Engine Best Practices -- Design Pattern Guidelines \ No newline at end of file diff --git a/.claude/skills/pdf-template-skill/scripts/analyze-template.js b/.claude/skills/pdf-template-skill/scripts/analyze-template.js deleted file mode 100755 index 756e621..0000000 --- a/.claude/skills/pdf-template-skill/scripts/analyze-template.js +++ /dev/null @@ -1,572 +0,0 @@ -/** - * PDF 템플릿 분석기 - PDF 구조를 분석하여 템플릿 생성 - */ - -const fs = require('fs').promises; -const path = require('path'); - -// PDF 템플릿 분석 클래스 -class PDFTemplateAnalyzer { - constructor() { - this.template = { - metadata: {}, - slides: [], - styles: { - colors: {}, - fonts: {} - }, - variables: {} - }; - } - - /** - * SAM ERP 견적서 PDF 분석 (수동 매핑) - */ - async analyzeSAMEstimate(inputPath) { - console.log(`📖 SAM ERP 견적서 PDF 분석: ${inputPath}`); - - // 메타데이터 설정 - this.template.metadata = { - source: path.basename(inputPath), - pages: 5, - extractedAt: new Date().toISOString().split('T')[0], - title: "SAM ERP 견적서", - type: "estimate" - }; - - // 스타일 정의 - this.template.styles = { - colors: { - primary: '0066CC', // SAM 파란색 - secondary: '333333', // 진한 회색 - headerBg: 'F0F8FF', // 연한 파란색 - border: 'CCCCCC', // 테두리 회색 - white: 'FFFFFF', - black: '000000', - red: 'CC0000', - green: '008000' - }, - fonts: { - title: { face: 'Arial', size: 32, bold: true }, - heading: { face: 'Arial', size: 24, bold: true }, - subheading: { face: 'Arial', size: 18, bold: true }, - body: { face: 'Arial', size: 12 }, - small: { face: 'Arial', size: 10 } - } - }; - - // 슬라이드 템플릿 생성 - this.createCoverSlideTemplate(); - this.createMainScreenTemplate(); - this.createDetailScreenTemplate(); - this.createEstimateDocTemplate(); - this.createEstimateDetailTemplate(); - - // 변수 정의 - this.template.variables = { - // 회사 정보 - company: { type: 'string', default: '(주) 주일기업' }, - documentNumber: { type: 'string', default: 'ABC123' }, - date: { type: 'date', default: '2025-01-03' }, - - // 고객 정보 - clientName: { type: 'string', default: '회사명' }, - siteName: { type: 'string', default: '현장명' }, - clientAddress: { type: 'string', default: '주소명' }, - clientContact: { type: 'string', default: '연락처' }, - clientPhone: { type: 'string', default: '010-1234-5678' }, - - // 견적 정보 - estimateNumber: { type: 'string', default: '123123' }, - estimator: { type: 'string', default: '이름' }, - totalAmount: { type: 'number', default: 93950000 }, - bidDate: { type: 'date', default: '2025-12-12' }, - - // 상품 목록 - items: { - type: 'array', - default: [ - { - no: 1, - name: 'FSSB01(주차장)', - product: '제품명', - width: 2530, - height: 2550, - quantity: 1, - unit: 'SET', - materialCost: 1420000, - laborCost: 510000, - totalCost: 1930000 - } - ] - }, - - // 요약 정보 - summary: { - type: 'object', - default: { - description: '셔터설치공사', - quantity: 1, - unit: '식', - materialTotal: 78540000, - laborTotal: 15410000, - grandTotal: 93950000 - } - }, - - // 기타 정보 - notes: { type: 'string', default: '부가세 별도 / 현설조건에 따름' } - }; - - console.log('✅ SAM ERP 견적서 템플릿 분석 완료'); - return this.template; - } - - /** - * 표지 슬라이드 템플릿 - */ - createCoverSlideTemplate() { - this.template.slides.push({ - slideNumber: 1, - type: 'cover', - name: '표지', - layout: { width: 10, height: 7.5 }, - elements: [ - { - type: 'background', - id: 'coverBackground', - style: { color: '{{colors.primary}}' } - }, - { - type: 'shape', - id: 'logoArea', - shapeType: 'rect', - position: { x: 1, y: 1, w: 2, h: 1 }, - style: { - fill: { color: '{{colors.white}}' }, - line: { color: '{{colors.border}}', width: 1 } - } - }, - { - type: 'text', - id: 'logoText', - position: { x: 1, y: 1, w: 2, h: 0.5 }, - content: 'SAM', - style: { - fontSize: 24, - bold: true, - color: '{{colors.primary}}', - align: 'center' - } - }, - { - type: 'text', - id: 'logoSubtext', - position: { x: 1, y: 1.5, w: 2, h: 0.5 }, - content: 'Smart Automation Management', - style: { - fontSize: 10, - color: '{{colors.secondary}}', - align: 'center' - } - }, - { - type: 'text', - id: 'mainTitle', - position: { x: 2, y: 3, w: 6, h: 1.5 }, - content: '{{title}}', - placeholder: '{{title}}', - style: { - fontSize: 48, - bold: true, - color: '{{colors.white}}', - align: 'center' - } - }, - { - type: 'text', - id: 'subtitle', - position: { x: 2, y: 4.8, w: 6, h: 0.8 }, - content: 'SAM ERP 견적관리 시스템', - style: { - fontSize: 24, - color: '{{colors.white}}', - align: 'center' - } - }, - { - type: 'text', - id: 'dateAndCompany', - position: { x: 7.5, y: 7, w: 2.5, h: 1.5 }, - content: '{{date}}\\n\\n{{company}}', - placeholder: '{{date}}\\n\\n{{company}}', - style: { - fontSize: 12, - color: '{{colors.white}}', - align: 'right' - } - } - ] - }); - } - - /** - * 견적관리 메인 화면 템플릿 - */ - createMainScreenTemplate() { - this.template.slides.push({ - slideNumber: 2, - type: 'main_screen', - name: '견적관리 메인', - layout: { width: 10, height: 7.5 }, - elements: [ - { - type: 'text', - id: 'pageTitle', - position: { x: 0.5, y: 0.3, w: 6, h: 0.6 }, - content: '견적관리', - style: { - fontSize: 24, - bold: true, - color: '{{colors.secondary}}' - } - }, - { - type: 'text', - id: 'pageDescription', - position: { x: 0.5, y: 0.9, w: 6, h: 0.4 }, - content: '견적을 관리합니다', - style: { - fontSize: 14, - color: '{{colors.secondary}}' - } - }, - { - type: 'shape', - id: 'filterArea', - shapeType: 'rect', - position: { x: 0.5, y: 1.5, w: 9, h: 1 }, - style: { - fill: { color: '{{colors.headerBg}}' }, - line: { color: '{{colors.border}}', width: 1 } - } - }, - { - type: 'dynamicStats', - id: 'statsBoxes', - position: { x: 2, y: 2.8, w: 6, h: 1.2 }, - dataSource: 'stats', - template: { - type: 'statBox', - width: 1.8, - spacing: 0.1 - } - }, - { - type: 'table', - id: 'estimateList', - position: { x: 0.5, y: 4.5, w: 9, h: 2.5 }, - dataSource: 'estimates', - headers: ['견적번호', '거래처', '현장명', '견적자', '총 개소', '견적금액', '견적완료일', '입찰일', '상태', '작업'], - style: { - border: { pt: 1, color: '{{colors.border}}' }, - fontSize: 10, - headerBg: '{{colors.headerBg}}' - } - } - ] - }); - } - - /** - * 견적 상세 화면 템플릿 - */ - createDetailScreenTemplate() { - this.template.slides.push({ - slideNumber: 3, - type: 'detail_screen', - name: '견적 상세', - layout: { width: 10, height: 7.5 }, - elements: [ - { - type: 'text', - id: 'pageTitle', - position: { x: 0.5, y: 0.3, w: 6, h: 0.6 }, - content: '견적 상세', - style: { - fontSize: 24, - bold: true, - color: '{{colors.secondary}}' - } - }, - { - type: 'buttonGroup', - id: 'actionButtons', - position: { x: 4, y: 0.3, w: 4.5, h: 0.6 }, - buttons: [ - { text: '견적서 보기', action: 'viewEstimate' }, - { text: '전자결재', action: 'approval' }, - { text: '수정', action: 'edit' } - ] - }, - { - type: 'infoSection', - id: 'estimateInfo', - position: { x: 0.5, y: 1.2, w: 9, h: 1.8 }, - title: '견적 정보', - fields: [ - { label: '견적번호', field: 'estimateNumber' }, - { label: '견적자', field: 'estimator' }, - { label: '견적금액', field: 'totalAmount', format: 'currency' }, - { label: '상태', field: 'status' } - ] - }, - { - type: 'infoSection', - id: 'siteInfo', - position: { x: 0.5, y: 3.2, w: 9, h: 1.8 }, - title: '현장설명회 정보', - fields: [ - { label: '현설번호', field: 'siteNumber' }, - { label: '거래처명', field: 'clientName' }, - { label: '현장설명회 일자', field: 'siteDate' }, - { label: '참석자', field: 'attendee' } - ] - }, - { - type: 'infoSection', - id: 'bidInfo', - position: { x: 0.5, y: 5.2, w: 9, h: 1.5 }, - title: '입찰 정보', - fields: [ - { label: '현장명', field: 'siteName' }, - { label: '입찰일자', field: 'bidDate' }, - { label: '개소', field: 'quantity' }, - { label: '공사기간', field: 'constructionPeriod' } - ] - } - ] - }); - } - - /** - * 견적서 문서 (요약) 템플릿 - */ - createEstimateDocTemplate() { - this.template.slides.push({ - slideNumber: 4, - type: 'estimate_document', - name: '견적서 문서 (요약)', - layout: { width: 10, height: 7.5 }, - elements: [ - { - type: 'text', - id: 'documentTitle', - position: { x: 3, y: 0.5, w: 4, h: 0.8 }, - content: '견적서', - style: { - fontSize: 32, - bold: true, - color: '{{colors.secondary}}', - align: 'center' - } - }, - { - type: 'text', - id: 'documentInfo', - position: { x: 3, y: 1.3, w: 4, h: 0.4 }, - content: '문서번호: {{documentNumber}} | 작성일자: {{date}}', - placeholder: '문서번호: {{documentNumber}} | 작성일자: {{date}}', - style: { - fontSize: 12, - color: '{{colors.secondary}}', - align: 'center' - } - }, - { - type: 'table', - id: 'companyInfoTable', - position: { x: 1, y: 2, w: 8, h: 2 }, - dataSource: 'companyInfo', - template: 'companyInfoLayout' - }, - { - type: 'text', - id: 'estimateIntro', - position: { x: 8, y: 3.7, w: 2, h: 0.4 }, - content: '하기와 같이 見積합니다.', - style: { - fontSize: 12, - color: '{{colors.secondary}}', - align: 'center' - } - }, - { - type: 'table', - id: 'summaryTable', - position: { x: 1, y: 4.5, w: 8, h: 1.5 }, - dataSource: 'summary', - headers: ['명칭', '수량', '단위', '재료비', '노무비', '합계', '비고'], - template: 'summaryLayout' - }, - { - type: 'text', - id: 'specialNotes', - position: { x: 1, y: 6.2, w: 8, h: 0.4 }, - content: '* 특기사항: {{notes}}', - placeholder: '* 특기사항: {{notes}}', - style: { - fontSize: 11, - color: '{{colors.secondary}}' - } - } - ] - }); - } - - /** - * 견적서 문서 (상세) 템플릿 - */ - createEstimateDetailTemplate() { - this.template.slides.push({ - slideNumber: 5, - type: 'estimate_detail', - name: '견적서 문서 (상세)', - layout: { width: 10, height: 7.5 }, - elements: [ - { - type: 'text', - id: 'detailTitle', - position: { x: 3, y: 0.5, w: 4, h: 0.6 }, - content: '견적 상세 내역', - style: { - fontSize: 20, - bold: true, - color: '{{colors.secondary}}', - align: 'center' - } - }, - { - type: 'table', - id: 'detailTable', - position: { x: 0.2, y: 1.5, w: 9.6, h: 4 }, - dataSource: 'items', - headers: [ - 'NO', '명칭', '제품', '규격(mm)', '', '수량', '단위', - '재료비', '', '노무비', '', '합계', '', '비고' - ], - subHeaders: [ - '', '', '', '가로(W)', '높이(H)', '', '', - '단가', '금액', '단가', '금액', '단가', '금액', '' - ], - template: 'detailItemLayout', - style: { - border: { pt: 1, color: '{{colors.border}}' }, - fontSize: 8, - headerBg: '{{colors.headerBg}}' - } - } - ] - }); - } - - /** - * 템플릿을 JSON 파일로 저장 - */ - async saveTemplate(outputPath) { - const dir = path.dirname(outputPath); - await fs.mkdir(dir, { recursive: true }); - - await fs.writeFile( - outputPath, - JSON.stringify(this.template, null, 2), - 'utf8' - ); - - console.log(`✅ 템플릿 저장 완료: ${outputPath}`); - return outputPath; - } - - /** - * 범용 PDF 분석 (확장 가능) - */ - async analyzeGenericPDF(inputPath) { - // TODO: 실제 PDF 파싱 로직 구현 - console.log(`📖 범용 PDF 분석: ${inputPath}`); - - // 기본 템플릿 구조 생성 - this.template.metadata = { - source: path.basename(inputPath), - extractedAt: new Date().toISOString().split('T')[0], - title: "Generic Template", - type: "generic" - }; - - // 기본 스타일 - this.template.styles = { - colors: { - primary: '0066CC', - secondary: '333333', - background: 'FFFFFF' - }, - fonts: { - title: { face: 'Arial', size: 24, bold: true }, - body: { face: 'Arial', size: 12 } - } - }; - - console.log('✅ 범용 PDF 분석 완료 (기본 템플릿)'); - return this.template; - } -} - -// 메인 실행 함수 -async function analyzeTemplate(inputPath, outputPath, type = 'sam') { - try { - console.log('🚀 PDF 템플릿 분석 시작'); - console.log(`📄 입력: ${inputPath}`); - console.log(`📊 출력: ${outputPath}`); - - const analyzer = new PDFTemplateAnalyzer(); - - let template; - if (type === 'sam') { - template = await analyzer.analyzeSAMEstimate(inputPath); - } else { - template = await analyzer.analyzeGenericPDF(inputPath); - } - - await analyzer.saveTemplate(outputPath); - console.log('✅ 템플릿 분석 완료!'); - - return outputPath; - - } catch (error) { - console.error('❌ 템플릿 분석 실패:', error.message); - throw error; - } -} - -// 명령행 인수 처리 -async function main() { - const args = process.argv.slice(2); - const inputFlag = args.find(arg => arg.startsWith('--input=')); - const outputFlag = args.find(arg => arg.startsWith('--output=')); - const typeFlag = args.find(arg => arg.startsWith('--type=')); - - const inputPath = inputFlag ? inputFlag.split('=')[1] : 'pdf_sample/SAM_견적관리.pdf'; - const outputPath = outputFlag ? outputFlag.split('=')[1] : 'templates/sam_estimate_template.json'; - const type = typeFlag ? typeFlag.split('=')[1] : 'sam'; - - // 출력 디렉토리 생성 - await fs.mkdir(path.dirname(outputPath), { recursive: true }); - - await analyzeTemplate(inputPath, outputPath, type); -} - -// 직접 실행시 -if (require.main === module) { - main().catch(console.error); -} - -module.exports = { analyzeTemplate, PDFTemplateAnalyzer }; \ No newline at end of file diff --git a/.claude/skills/pdf-template-skill/scripts/generate-from-template.js b/.claude/skills/pdf-template-skill/scripts/generate-from-template.js deleted file mode 100755 index 84d9548..0000000 --- a/.claude/skills/pdf-template-skill/scripts/generate-from-template.js +++ /dev/null @@ -1,453 +0,0 @@ -/** - * 템플릿 기반 PPTX 생성기 - 템플릿과 데이터를 결합하여 PPTX 생성 - */ - -const fs = require('fs').promises; -const path = require('path'); -const PptxGenJS = require('pptxgenjs'); - -// 템플릿 변수 처리 엔진 -class TemplateEngine { - constructor(data) { - this.data = data; - } - - /** - * 템플릿 변수 치환 - */ - resolve(template) { - if (typeof template === 'string') { - return this.resolveString(template); - } else if (Array.isArray(template)) { - return template.map(item => this.resolve(item)); - } else if (typeof template === 'object' && template !== null) { - const result = {}; - for (const [key, value] of Object.entries(template)) { - result[key] = this.resolve(value); - } - return result; - } - return template; - } - - /** - * 문자열 템플릿 변수 치환 - */ - resolveString(template) { - return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => { - const value = this.getNestedValue(this.data, path.trim()); - return value !== undefined ? value : match; - }); - } - - /** - * 중첩 객체 값 추출 - */ - getNestedValue(obj, path) { - return path.split('.').reduce((current, key) => { - return current && current[key] !== undefined ? current[key] : undefined; - }, obj); - } - - /** - * 숫자 포맷팅 - */ - formatNumber(value, format) { - if (format === 'currency') { - return '₩' + value.toLocaleString(); - } - return value.toString(); - } - - /** - * 날짜 포맷팅 - */ - formatDate(value, format = 'YYYY-MM-DD') { - const date = new Date(value); - return date.toISOString().split('T')[0]; - } -} - -// 템플릿 기반 PPTX 생성기 -class TemplatePPTXGenerator { - constructor() { - this.pptx = new PptxGenJS(); - this.pptx.layout = 'LAYOUT_16x9'; - } - - /** - * 템플릿을 기반으로 PPTX 생성 - */ - async generateFromTemplate(templatePath, dataPath) { - console.log('📊 템플릿 기반 PPTX 생성 중...'); - - // 템플릿과 데이터 로드 - const template = await this.loadTemplate(templatePath); - const data = await this.loadData(dataPath); - - // 템플릿 엔진 초기화 - const engine = new TemplateEngine(data); - - // 색상 및 스타일 준비 - const resolvedStyles = engine.resolve(template.styles); - this.colors = resolvedStyles.colors; - this.fonts = resolvedStyles.fonts; - - // 각 슬라이드 생성 - for (const slideTemplate of template.slides) { - await this.createSlideFromTemplate(slideTemplate, engine); - } - - console.log(`✅ 템플릿 기반 PPTX 생성 완료: ${template.slides.length}개 슬라이드`); - return this.pptx; - } - - /** - * 템플릿 파일 로드 - */ - async loadTemplate(templatePath) { - const content = await fs.readFile(templatePath, 'utf8'); - return JSON.parse(content); - } - - /** - * 데이터 파일 로드 - */ - async loadData(dataPath) { - const content = await fs.readFile(dataPath, 'utf8'); - return JSON.parse(content); - } - - /** - * 슬라이드 템플릿을 기반으로 슬라이드 생성 - */ - async createSlideFromTemplate(slideTemplate, engine) { - const slide = this.pptx.addSlide(); - - console.log(`📄 슬라이드 ${slideTemplate.slideNumber}: ${slideTemplate.name}`); - - // 각 요소 생성 - for (const element of slideTemplate.elements) { - await this.createElement(slide, element, engine); - } - } - - /** - * 요소 생성 - */ - async createElement(slide, element, engine) { - const resolvedElement = engine.resolve(element); - - switch (element.type) { - case 'background': - this.createBackground(slide, resolvedElement); - break; - - case 'text': - this.createText(slide, resolvedElement); - break; - - case 'shape': - this.createShape(slide, resolvedElement); - break; - - case 'table': - this.createTable(slide, resolvedElement, engine); - break; - - case 'buttonGroup': - this.createButtonGroup(slide, resolvedElement); - break; - - case 'infoSection': - this.createInfoSection(slide, resolvedElement, engine); - break; - - case 'dynamicStats': - this.createDynamicStats(slide, resolvedElement, engine); - break; - - default: - console.log(`⚠️ 미지원 요소 타입: ${element.type}`); - } - } - - /** - * 배경 생성 - */ - createBackground(slide, element) { - slide.background = { color: element.style.color }; - } - - /** - * 텍스트 생성 - */ - createText(slide, element) { - const options = { - x: element.position.x, - y: element.position.y, - w: element.position.w, - h: element.position.h, - ...element.style - }; - - slide.addText(element.content || element.placeholder || '', options); - } - - /** - * 도형 생성 - */ - createShape(slide, element) { - const options = { - x: element.position.x, - y: element.position.y, - w: element.position.w, - h: element.position.h, - ...element.style - }; - - slide.addShape(element.shapeType || 'rect', options); - } - - /** - * 테이블 생성 - */ - createTable(slide, element, engine) { - let tableData = []; - - // 헤더 추가 - if (element.headers) { - tableData.push(element.headers); - if (element.subHeaders) { - tableData.push(element.subHeaders); - } - } - - // 데이터 소스에서 행 생성 - if (element.dataSource) { - const sourceData = engine.getNestedValue(engine.data, element.dataSource); - if (Array.isArray(sourceData)) { - sourceData.forEach((item, index) => { - const row = this.generateTableRow(element, item, index, engine); - tableData.push(row); - }); - } - } - - const options = { - x: element.position.x, - y: element.position.y, - w: element.position.w, - h: element.position.h, - border: { pt: 1, color: '757575' }, - fontSize: 10, - margin: 0.1, - ...element.style - }; - - slide.addTable(tableData, options); - } - - /** - * 테이블 행 생성 - */ - generateTableRow(element, item, index, engine) { - if (element.template === 'detailItemLayout') { - return [ - (index + 1).toString(), - item.name || '', - item.product || '', - item.width ? item.width.toLocaleString() : '', - item.height ? item.height.toLocaleString() : '', - item.quantity ? item.quantity.toString() : '', - item.unit || '', - item.materialCost ? engine.formatNumber(item.materialCost, 'currency') : '', - item.materialCost && item.quantity ? - engine.formatNumber(item.materialCost * item.quantity, 'currency') : '', - item.laborCost ? engine.formatNumber(item.laborCost, 'currency') : '', - item.laborCost && item.quantity ? - engine.formatNumber(item.laborCost * item.quantity, 'currency') : '', - item.totalCost ? engine.formatNumber(item.totalCost, 'currency') : '', - item.totalCost && item.quantity ? - engine.formatNumber(item.totalCost * item.quantity, 'currency') : '', - item.memo || '' - ]; - } - - // 기본 행 생성 - return Object.values(item); - } - - /** - * 버튼 그룹 생성 - */ - createButtonGroup(slide, element) { - element.buttons.forEach((button, index) => { - const xPos = element.position.x + index * 1.5; - - // 버튼 배경 - slide.addShape('rect', { - x: xPos, - y: element.position.y, - w: 1.3, - h: element.position.h, - fill: { color: this.colors.primary }, - line: { color: this.colors.primary, width: 1 } - }); - - // 버튼 텍스트 - slide.addText(button.text, { - x: xPos, - y: element.position.y, - w: 1.3, - h: element.position.h, - fontSize: 12, - bold: true, - color: 'FFFFFF', - align: 'center' - }); - }); - } - - /** - * 정보 섹션 생성 - */ - createInfoSection(slide, element, engine) { - // 섹션 배경 - slide.addShape('rect', { - x: element.position.x, - y: element.position.y, - w: element.position.w, - h: element.position.h, - fill: { color: 'FFFFFF' }, - line: { color: 'CCCCCC', width: 1 } - }); - - // 섹션 제목 - slide.addText(element.title, { - x: element.position.x + 0.2, - y: element.position.y + 0.2, - w: element.position.w - 0.4, - h: 0.4, - fontSize: 14, - bold: true, - color: '333333' - }); - - // 필드들 - element.fields.forEach((field, index) => { - const row = Math.floor(index / 2); - const col = index % 2; - const xPos = element.position.x + 0.5 + col * 4; - const yPos = element.position.y + 0.8 + row * 0.6; - - const value = engine.getNestedValue(engine.data, field.field); - const formattedValue = field.format === 'currency' ? - engine.formatNumber(value, 'currency') : value; - - slide.addText(`${field.label}: ${formattedValue || ''}`, { - x: xPos, - y: yPos, - w: 3.5, - h: 0.4, - fontSize: 11, - color: '333333' - }); - }); - } - - /** - * 동적 통계 박스 생성 - */ - createDynamicStats(slide, element, engine) { - const statsData = engine.getNestedValue(engine.data, element.dataSource) || [ - { label: '전체 견적', value: '9', color: this.colors.primary }, - { label: '견적대기', value: '5', color: 'CC0000' }, - { label: '견적완료', value: '4', color: '008000' } - ]; - - statsData.forEach((stat, index) => { - const xPos = element.position.x + index * 2; - - // 통계 박스 - slide.addShape('rect', { - x: xPos, - y: element.position.y, - w: element.template.width, - h: element.position.h, - fill: { color: 'FFFFFF' }, - line: { color: 'CCCCCC', width: 1 } - }); - - // 통계 값 - slide.addText(stat.value, { - x: xPos, - y: element.position.y + 0.2, - w: element.template.width, - h: 0.8, - fontSize: 24, - bold: true, - color: stat.color, - align: 'center' - }); - - // 통계 라벨 - slide.addText(stat.label, { - x: xPos, - y: element.position.y + 0.8, - w: element.template.width, - h: 0.4, - fontSize: 12, - color: '333333', - align: 'center' - }); - }); - } -} - -// 메인 실행 함수 -async function generateFromTemplate(templatePath, dataPath, outputPath) { - try { - console.log('🚀 템플릿 기반 PPTX 생성 시작'); - console.log(`📋 템플릿: ${templatePath}`); - console.log(`📊 데이터: ${dataPath}`); - console.log(`📄 출력: ${outputPath}`); - - const generator = new TemplatePPTXGenerator(); - const pptx = await generator.generateFromTemplate(templatePath, dataPath); - - // 파일 저장 - await pptx.writeFile({ fileName: outputPath }); - console.log('✅ 템플릿 기반 PPTX 생성 완료!'); - - return outputPath; - - } catch (error) { - console.error('❌ 생성 실패:', error.message); - throw error; - } -} - -// 명령행 인수 처리 -async function main() { - const args = process.argv.slice(2); - const templateFlag = args.find(arg => arg.startsWith('--template=')); - const dataFlag = args.find(arg => arg.startsWith('--data=')); - const outputFlag = args.find(arg => arg.startsWith('--output=')); - - const templatePath = templateFlag ? templateFlag.split('=')[1] : 'templates/sam_estimate_template.json'; - const dataPath = dataFlag ? dataFlag.split('=')[1] : 'data/sample_estimate_data.json'; - const outputPath = outputFlag ? outputFlag.split('=')[1] : 'pptx/template_generated.pptx'; - - // 출력 디렉토리 생성 - await fs.mkdir(path.dirname(outputPath), { recursive: true }); - - await generateFromTemplate(templatePath, dataPath, outputPath); -} - -// 직접 실행시 -if (require.main === module) { - main().catch(console.error); -} - -module.exports = { generateFromTemplate, TemplatePPTXGenerator, TemplateEngine }; \ No newline at end of file diff --git a/.claude/skills/ppt-auto-generator/SKILL.md b/.claude/skills/ppt-auto-generator/SKILL.md deleted file mode 100755 index 7f0b159..0000000 --- a/.claude/skills/ppt-auto-generator/SKILL.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -name: ppt-auto-generator -description: 마크다운 또는 텍스트 아웃라인에서 PowerPoint 프레젠테이션을 생성합니다. PPT 생성, 슬라이드 제작, 발표자료 작성 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# PPT Auto Generator - -마크다운이나 텍스트 아웃라인을 기반으로 PowerPoint 프레젠테이션을 생성하는 스킬입니다. - -## 기능 - -### 입력 형식 -- 마크다운 문서 -- 텍스트 아웃라인 -- 구조화된 데이터 (JSON/YAML) - -### 생성 요소 -- 제목 슬라이드 -- 목차 슬라이드 -- 콘텐츠 슬라이드 -- 불릿 포인트 -- 이미지 플레이스홀더 -- 차트 및 표 -- 요약 슬라이드 - -### 출력 형식 -- Python-pptx 스크립트 -- HTML 슬라이드 (reveal.js) -- 마크다운 슬라이드 (Marp) - -## 마크다운 입력 형식 - -```markdown -# 프레젠테이션 제목 - -## 슬라이드 1: 소개 -- 첫 번째 포인트 -- 두 번째 포인트 -- 세 번째 포인트 - -## 슬라이드 2: 주요 내용 -### 섹션 A -- 내용 1 -- 내용 2 - -### 섹션 B -- 내용 3 -- 내용 4 - -## 슬라이드 3: 데이터 -| 항목 | 값 | -|------|-----| -| A | 100 | -| B | 200 | - -## 슬라이드 4: 결론 -- 핵심 메시지 -- 다음 단계 -``` - -## Python-pptx 템플릿 - -```python -from pptx import Presentation -from pptx.util import Inches, Pt -from pptx.enum.text import PP_ALIGN - -def create_presentation(content): - prs = Presentation() - - # 제목 슬라이드 - title_slide = prs.slides.add_slide(prs.slide_layouts[0]) - title_slide.shapes.title.text = content['title'] - title_slide.placeholders[1].text = content['subtitle'] - - # 콘텐츠 슬라이드 - for slide_content in content['slides']: - slide = prs.slides.add_slide(prs.slide_layouts[1]) - slide.shapes.title.text = slide_content['title'] - - body = slide.placeholders[1] - tf = body.text_frame - - for point in slide_content['points']: - p = tf.add_paragraph() - p.text = point - p.level = 0 - - prs.save('output.pptx') - return 'output.pptx' -``` - -## Marp 마크다운 템플릿 - -```markdown ---- -marp: true -theme: default -paginate: true ---- - -# 프레젠테이션 제목 - -발표자: 이름 -날짜: 2024-01-15 - ---- - -## 목차 - -1. 소개 -2. 주요 내용 -3. 결론 - ---- - -## 소개 - -- 배경 설명 -- 목적 -- 범위 - ---- - -## 주요 내용 - -### 핵심 포인트 - -- 첫 번째 -- 두 번째 -- 세 번째 - ---- - -## 결론 - -- 요약 -- 다음 단계 -- Q&A -``` - -## Reveal.js HTML 템플릿 - -```html - - - - - - - -
      -
      -
      -

      제목

      -

      부제목

      -
      -
      -

      슬라이드 제목

      -
        -
      • 포인트 1
      • -
      • 포인트 2
      • -
      -
      -
      -
      - - - - -``` - -## 사용 예시 - -``` -이 마크다운을 PPT로 만들어줘 -발표자료를 슬라이드로 변환해줘 -프로젝트 소개 PPT를 생성해줘 -``` - -## 필요 패키지 - -```bash -# Python -pip install python-pptx - -# Node.js (Marp) -npm install @marp-team/marp-cli -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/pptx-skill/SKILL.md b/.claude/skills/pptx-skill/SKILL.md deleted file mode 100755 index b207833..0000000 --- a/.claude/skills/pptx-skill/SKILL.md +++ /dev/null @@ -1,434 +0,0 @@ ---- -name: pptx-skill -description: HTML 슬라이드를 PowerPoint(PPTX) 파일로 변환합니다. PPTX 생성, PPT 변환, 프레젠테이션 만들기, 슬라이드를 파워포인트로 요청 시 사용합니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# PPTX Skill - PowerPoint 프레젠테이션 생성 - -PowerPoint 프레젠테이션 파일(.pptx)을 생성하는 스킬입니다. -두 가지 방법을 제공하며, **방법 A (Direct PptxGenJS)를 우선 사용**합니다. - -## 방법 선택 가이드 - -| | 방법 A: Direct PptxGenJS (권장) | 방법 B: HTML → PPTX | -|---|---|---| -| **의존성** | Node.js + pptxgenjs만 필요 | Playwright + Chromium 브라우저 필요 | -| **원리** | JS 코드로 도형/텍스트 직접 배치 | HTML을 브라우저 렌더링 후 변환 | -| **PPT 편집** | 생성 후 텍스트/도형 직접 수정 가능 | 이미지 기반이라 수정 어려움 | -| **한글 폰트** | PPT 뷰어 폰트 사용 (깨짐 없음) | 시스템 폰트 필요 | -| **환경 제약** | 없음 | Chromium 시스템 라이브러리 필요 | -| **적합한 경우** | 대부분의 프레젠테이션 | 복잡한 CSS 레이아웃이 필요할 때 | - ---- - -## 방법 A: Direct PptxGenJS (권장) - -PptxGenJS API를 직접 호출하여 네이티브 PPTX 요소를 생성합니다. -브라우저 없이 Node.js만으로 동작하며, 생성된 PPT에서 텍스트/도형 편집이 가능합니다. - -### 사용법 - -#### 1단계: .cjs 변환 스크립트 작성 - -**주의**: 프로젝트에 `"type": "module"`이 설정된 경우 `.cjs` 확장자를 사용해야 합니다. - -```javascript -// convert.cjs -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); - -async function main() { - const pres = new PptxGenJS(); - - // 커스텀 레이아웃 정의 (16:9 = 10" x 5.625") - pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); - pres.layout = 'CUSTOM_16x9'; - - // --- 슬라이드 1 --- - const slide1 = pres.addSlide(); - slide1.background = { fill: '1a1a2e' }; - - slide1.addText('제목', { - x: 1, y: 2, w: 8, h: 1, - fontSize: 32, bold: true, color: 'FFFFFF', - align: 'center', fontFace: 'Arial' - }); - - // --- 저장 --- - await pres.writeFile({ fileName: 'output.pptx' }); - console.log('PPTX created!'); -} - -main().catch(console.error); -``` - -#### 2단계: 실행 - -```bash -node convert.cjs -``` - -#### 3단계: 결과 확인 (선택) - -```bash -python ~/.claude/skills/pptx-skill/scripts/thumbnail.py output.pptx thumbnail-grid -``` - -### 핵심 규칙 - -#### 색상 코드 (매우 중요) -PptxGenJS에서 색상 코드는 반드시 `#` 없이 사용해야 합니다. - -```javascript -// ✅ 올바른 사용 -{ color: 'FF0000' } -{ fill: { color: '1e3a5f' } } - -// ❌ 잘못된 사용 - 파일 손상 유발 -{ color: '#FF0000' } -``` - -#### 좌표 체계 -모든 위치/크기는 **인치(inch)** 단위입니다. - -``` -슬라이드 크기: width=10, height=5.625 (16:9) -좌표 원점: 좌측 상단 (0, 0) - -x: 좌측에서의 거리 -y: 상단에서의 거리 -w: 너비 -h: 높이 -``` - -### PptxGenJS API 빠른 참조 - -#### 슬라이드 배경 -```javascript -slide.background = { fill: '1a1a2e' }; // 단색 -slide.background = { path: 'background.png' }; // 이미지 -``` - -#### 텍스트 추가 -```javascript -// 단순 텍스트 -slide.addText('제목', { - x: 0.5, y: 0.5, w: 9, h: 1, - fontSize: 36, bold: true, color: '1a1a2e', - align: 'center', // left, center, right - valign: 'middle', // top, middle, bottom - fontFace: 'Arial' -}); - -// 복합 텍스트 (여러 스타일 혼합) -slide.addText([ - { text: '굵은 텍스트', options: { bold: true, color: 'FF0000', fontSize: 14 } }, - { text: ' 일반 텍스트', options: { color: '333333', fontSize: 14 } } -], { x: 1, y: 2, w: 8, h: 0.5, fontFace: 'Arial' }); - -// 줄바꿈 -slide.addText([ - { text: '첫째 줄', options: { fontSize: 14, bold: true, color: 'FFFFFF' } }, - { text: '\n둘째 줄', options: { fontSize: 9, color: 'CCCCCC' } } -], { x: 1, y: 2, w: 4, h: 0.7, align: 'center', valign: 'middle', fontFace: 'Arial' }); -``` - -#### 도형 추가 -```javascript -// 사각형 -slide.addShape(pres.ShapeType.rect, { - x: 0.5, y: 1, w: 3, h: 2, - fill: { color: '1e3a5f' }, - line: { color: '333333', width: 1 } -}); - -// 둥근 사각형 -slide.addShape(pres.ShapeType.roundRect, { - x: 1, y: 1, w: 4, h: 0.5, - rectRadius: 0.1, - fill: { color: 'F0FAF0' }, - line: { color: 'C8E6C9', width: 0.5 } -}); - -// 원/타원 -slide.addShape(pres.ShapeType.ellipse, { - x: 2, y: 2, w: 0.5, h: 0.5, - fill: { color: '4CAF50' } -}); -``` - -#### 이미지 추가 -```javascript -slide.addImage({ - path: 'image.png', // 파일 경로 - x: 1, y: 2, w: 4, h: 3 -}); - -slide.addImage({ - data: 'base64...', // Base64 인코딩 - x: 1, y: 2, w: 4, h: 3 -}); -``` - -#### 차트 추가 -```javascript -slide.addChart(pres.ChartType.bar, [{ - name: '시리즈 1', - labels: ['A', 'B', 'C'], - values: [10, 20, 30] -}], { x: 1, y: 2, w: 8, h: 4 }); - -slide.addChart(pres.ChartType.pie, [...], {...}); -slide.addChart(pres.ChartType.line, [...], {...}); -``` - -### 디자인 패턴 (자주 사용하는 구성요소) - -#### 헤더 바 + 제목 -```javascript -slide.addShape(pres.ShapeType.rect, { - x: 0.5, y: 0.35, w: 0.06, h: 0.35, - fill: { color: '4CAF50' } -}); -slide.addText('페이지 제목', { - x: 0.7, y: 0.3, w: 5, h: 0.45, - fontSize: 18, bold: true, color: '1a1a2e', fontFace: 'Arial' -}); -``` - -#### 배지/태그 -```javascript -slide.addShape(pres.ShapeType.roundRect, { - x: 5, y: 0.35, w: 1, h: 0.3, - rectRadius: 0.04, - fill: { color: 'E8F5E9' } -}); -slide.addText('총 28%', { - x: 5, y: 0.35, w: 1, h: 0.3, - fontSize: 8, bold: true, color: '2E7D32', - align: 'center', fontFace: 'Arial' -}); -``` - -#### 카드 (배경 + 제목 + 항목) -```javascript -// 카드 배경 -slide.addShape(pres.ShapeType.roundRect, { - x: 0.5, y: 1.5, w: 4, h: 3, - rectRadius: 0.15, - fill: { color: 'F0FAF0' }, - line: { color: '4CAF50', width: 1.5 } -}); -// 카드 내부 항목 (흰색 행) -slide.addShape(pres.ShapeType.roundRect, { - x: 0.7, y: 2.3, w: 3.6, h: 0.4, - rectRadius: 0.06, - fill: { color: 'FFFFFF' } -}); -slide.addText('항목명', { x: 0.8, y: 2.3, w: 2, h: 0.4, fontSize: 10, color: '333333', fontFace: 'Arial' }); -slide.addText('값', { x: 2.8, y: 2.3, w: 1.4, h: 0.4, fontSize: 14, bold: true, color: '4CAF50', align: 'right', fontFace: 'Arial' }); -``` - -#### 넘버 서클 -```javascript -slide.addShape(pres.ShapeType.ellipse, { - x: 1, y: 1, w: 0.45, h: 0.45, - fill: { color: '2196F3' } -}); -slide.addText('1', { - x: 1, y: 1, w: 0.45, h: 0.45, - fontSize: 14, bold: true, color: 'FFFFFF', - align: 'center', valign: 'middle', fontFace: 'Arial' -}); -``` - -#### 비율 바 (프로그레스 형태) -```javascript -// 큰 비율 -slide.addShape(pres.ShapeType.roundRect, { - x: 0.5, y: 1, w: 6.4, h: 0.7, - rectRadius: 0.1, fill: { color: '4CAF50' } -}); -slide.addText([ - { text: '20%', options: { fontSize: 20, bold: true, color: 'FFFFFF' } }, - { text: '\n판매자 수당', options: { fontSize: 9, color: 'DDFFDD' } } -], { x: 0.5, y: 1, w: 6.4, h: 0.7, align: 'center', valign: 'middle', fontFace: 'Arial' }); - -// 작은 비율 -slide.addShape(pres.ShapeType.roundRect, { - x: 7, y: 1, w: 1.6, h: 0.7, - rectRadius: 0.1, fill: { color: '2196F3' } -}); -``` - -#### 반복 카드 생성 (데이터 기반) -```javascript -const cards = [ - { title: '항목 A', color: '4CAF50', bg: 'F0FAF0', border: 'C8E6C9', items: [['라벨1', '값1'], ['라벨2', '값2']] }, - { title: '항목 B', color: '2196F3', bg: 'E3F2FD', border: 'BBDEFB', items: [['라벨1', '값1'], ['라벨2', '값2']] }, -]; -const cardW = 2.85; -cards.forEach((card, i) => { - const x = 0.5 + i * (cardW + 0.2); - const y = 1.9; - // 카드 배경 - slide.addShape(pres.ShapeType.roundRect, { x, y, w: cardW, h: 3, rectRadius: 0.1, fill: { color: card.bg }, line: { color: card.border, width: 0.5 } }); - // 카드 제목 - slide.addText(card.title, { x: x + 0.2, y: y + 0.1, w: cardW - 0.4, h: 0.3, fontSize: 11, bold: true, color: card.color, fontFace: 'Arial' }); - // 항목 반복 - card.items.forEach((item, j) => { - const iy = y + 0.5 + j * 0.7; - slide.addShape(pres.ShapeType.roundRect, { x: x + 0.1, y: iy, w: cardW - 0.2, h: 0.55, rectRadius: 0.05, fill: { color: 'FFFFFF' } }); - slide.addText(item[0], { x: x + 0.2, y: iy + 0.03, w: cardW - 0.4, h: 0.2, fontSize: 8, color: '999999', fontFace: 'Arial' }); - slide.addText(item[1], { x: x + 0.2, y: iy + 0.25, w: cardW - 0.4, h: 0.2, fontSize: 10, bold: true, color: '333333', fontFace: 'Arial' }); - }); -}); -``` - -#### 대외비 스탬프 -```javascript -function addConfidentialBadge(slide) { - slide.addShape(pres.ShapeType.roundRect, { x: 8.3, y: 0.15, w: 1.4, h: 0.35, rectRadius: 0.04, fill: { color: 'D32F2F' } }); - slide.addText('CONFIDENTIAL', { x: 8.3, y: 0.12, w: 1.4, h: 0.22, fontSize: 7, bold: true, color: 'FFFFFF', align: 'center', fontFace: 'Arial' }); - slide.addText('대 외 비', { x: 8.3, y: 0.28, w: 1.4, h: 0.22, fontSize: 8, bold: true, color: 'FFCDD2', align: 'center', fontFace: 'Arial' }); -} -// 각 슬라이드에 적용 -addConfidentialBadge(slide1); -``` - -#### 어두운 배경 테이블 -```javascript -// 테이블 컨테이너 -slide.addShape(pres.ShapeType.roundRect, { x: 5, y: 2.7, w: 4.5, h: 2.5, rectRadius: 0.1, fill: { color: '1a1a2e' } }); -// 헤더 -slide.addText('구분', { x: 5.2, y: 3.0, w: 1.5, h: 0.25, fontSize: 8, color: '888888', fontFace: 'Arial' }); -slide.addText('값 A', { x: 6.7, y: 3.0, w: 1.2, h: 0.25, fontSize: 8, bold: true, color: '4CAF50', align: 'center', fontFace: 'Arial' }); -// 구분선 -slide.addShape(pres.ShapeType.rect, { x: 5.2, y: 3.25, w: 4, h: 0.01, fill: { color: '333344' } }); -// 데이터 행 -slide.addText('항목', { x: 5.2, y: 3.35, w: 1.5, h: 0.3, fontSize: 9, color: 'CCCCCC', fontFace: 'Arial' }); -slide.addText('20%', { x: 6.7, y: 3.35, w: 1.2, h: 0.3, fontSize: 10, bold: true, color: '4CAF50', align: 'center', fontFace: 'Arial' }); -``` - -### 슬라이드 크기 참조 - -| 비율 | 크기 (pt) | 크기 (inch) | defineLayout | -|------|-----------|-------------|--------------| -| 16:9 | 720 x 405 | 10 x 5.625 | `{ width: 10, height: 5.625 }` | -| 4:3 | 720 x 540 | 10 x 7.5 | `{ width: 10, height: 7.5 }` | -| 16:10 | 720 x 450 | 10 x 6.25 | `{ width: 10, height: 6.25 }` | - -**중요**: `LAYOUT_WIDE`는 13.3" x 7.5"이므로 사용하지 마세요. 반드시 커스텀 레이아웃을 정의하세요. - ---- - -## 방법 B: HTML → PPTX 변환 (Playwright 필요) - -HTML 슬라이드를 Playwright 브라우저로 렌더링하여 PPTX로 변환합니다. -복잡한 CSS 레이아웃(flexbox, grid, 그라데이션)이 필요한 경우에 사용합니다. - -**전제 조건**: Playwright + Chromium 브라우저 + 시스템 라이브러리가 설치되어 있어야 합니다. -```bash -cd ~/.claude/skills/pptx-skill/scripts && npm install playwright pptxgenjs sharp -npx playwright install chromium -npx playwright install-deps chromium # 시스템 라이브러리 (sudo 필요) -``` - -### 사용법 - -#### 1단계: HTML 슬라이드 준비 - -```bash -ls slides/*.html -``` - -각 HTML 파일 규격: -- 크기: 720pt x 405pt (16:9 비율) -- 인코딩: UTF-8 -- 폰트: 웹 안전 폰트 사용 - -#### 2단계: 변환 스크립트 작성 및 실행 - -```javascript -const path = require('path'); -module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); - -const PptxGenJS = require('pptxgenjs'); -const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); - -const pres = new PptxGenJS(); -pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); -pres.layout = 'CUSTOM_16x9'; - -await html2pptx('slides/slide-01.html', pres); -await html2pptx('slides/slide-02.html', pres); - -await pres.writeFile({ fileName: 'presentation.pptx' }); -``` - -#### 3단계: 결과 확인 - -```bash -python ~/.claude/skills/pptx-skill/scripts/thumbnail.py output.pptx thumbnail-grid -``` - -### HTML 작성 규칙 (방법 B 전용) - -1. **텍스트 요소에 border/background 금지** - div로 감싸기 -2. **불릿 기호(-, *, ., .)로 시작하는 텍스트 금지** - ul/ol 사용 -3. **모든 텍스트는 p/h1-h6 태그로 감싸기** -4. **table 태그 사용 금지** - div + flexbox로 대체 -5. **HTML body 크기와 PptxGenJS 레이아웃 일치 필수** -6. **빈 값 표시 시 하이픈(-) 금지** - 빈 문자열 또는 N/A 사용 - ---- - -## 공통 유틸리티 - -### thumbnail.py - 미리보기 생성 - -```bash -python ~/.claude/skills/pptx-skill/scripts/thumbnail.py presentation.pptx output-thumbnail -``` - -옵션: -- `--cols N`: 열 수 (기본 5, 범위 3-6) -- `--outline-placeholders`: 플레이스홀더 영역 표시 - -### pack.py / unpack.py - PPTX XML 편집 - -```bash -# PPTX 언패킹 (XML로 분해) -python ~/.claude/skills/pptx-skill/ooxml/scripts/unpack.py presentation.pptx output_dir - -# PPTX 패킹 (XML을 PPTX로 조립) -python ~/.claude/skills/pptx-skill/ooxml/scripts/pack.py input_dir presentation.pptx - -# 구조 검증 -python ~/.claude/skills/pptx-skill/ooxml/scripts/validate.py unpacked_dir --original presentation.pptx -``` - -## 필수 의존성 - -### Node.js 패키지 (방법 A/B 공통) -```bash -cd ~/.claude/skills/pptx-skill/scripts && npm install pptxgenjs -``` - -### 추가 패키지 (방법 B만) -```bash -cd ~/.claude/skills/pptx-skill/scripts && npm install playwright sharp -``` - -### Python 패키지 (thumbnail/ooxml 사용 시) -```bash -pip install pillow defusedxml -``` - -## 추가 문서 - -- [html2pptx.md](html2pptx.md) - HTML to PPTX 변환 상세 가이드 (방법 B) -- [ooxml.md](ooxml.md) - Office Open XML 기술 참조 diff --git a/.claude/skills/pptx-skill/html2pptx.md b/.claude/skills/pptx-skill/html2pptx.md deleted file mode 100755 index d59383b..0000000 --- a/.claude/skills/pptx-skill/html2pptx.md +++ /dev/null @@ -1,769 +0,0 @@ -# HTML to PowerPoint Guide - -Convert HTML slides to PowerPoint presentations with accurate positioning using the `html2pptx.js` library. - -## Table of Contents - -1. [Creating HTML Slides](#creating-html-slides) -2. [Using the html2pptx Library](#using-the-html2pptx-library) -3. [Using PptxGenJS](#using-pptxgenjs) - ---- - -## Creating HTML Slides - -Every HTML slide must include proper body dimensions: - -### Layout Dimensions - -- **16:9** (default): `width: 720pt; height: 405pt` -- **4:3**: `width: 720pt; height: 540pt` -- **16:10**: `width: 720pt; height: 450pt` - -### Supported Elements - -- `

      `, `

      `-`

      ` - Text with styling -- `
        `, `
          ` - Lists (never use manual bullets •, -, *) -- ``, `` - Bold text (inline formatting) -- ``, `` - Italic text (inline formatting) -- `` - Underlined text (inline formatting) -- `` - Inline formatting with CSS styles (bold, italic, underline, color) -- `
          ` - Line breaks -- `
          ` with bg/border - Becomes shape -- `` - Images -- `class="placeholder"` - Reserved space for charts (returns `{ id, x, y, w, h }`) - -### Critical Text Rules - -**ALL text MUST be inside `

          `, `

          `-`

          `, `
            `, or `
              ` tags:** -- ✅ Correct: `

              Text here

              ` -- ❌ Wrong: `
              Text here
              ` - **Text will NOT appear in PowerPoint** -- ❌ Wrong: `Text` - **Text will NOT appear in PowerPoint** -- Text in `
              ` or `` without a text tag will be silently ignored - -**NEVER use manual bullet symbols (•, -, *, ·, etc.)** - Use `
                ` or `
                  ` lists instead -- ❌ Wrong: `

                  - 항목

                  ` (hyphen/dash) -- ❌ Wrong: `

                  • 항목

                  ` (bullet) -- ❌ Wrong: `

                  * 항목

                  ` (asterisk) -- ❌ Wrong: `

                  · 항목

                  ` (middle dot) -- ✅ Correct: Use `
                  • 항목
                  ` or `margin-left` for indentation - -**Empty values should NOT use hyphen:** -- ❌ Wrong: `

                  -

                  ` (will cause bullet symbol error) -- ✅ Correct: `

                  ` (empty) or `

                  N/A

                  ` - -**ONLY use web-safe fonts that are universally available:** -- ✅ Web-safe fonts: `Arial`, `Helvetica`, `Times New Roman`, `Georgia`, `Courier New`, `Verdana`, `Tahoma`, `Trebuchet MS`, `Impact`, `Comic Sans MS` -- ❌ Wrong: `'Segoe UI'`, `'SF Pro'`, `'Roboto'`, custom fonts - **Might cause rendering issues** - -### Styling - -- Use `display: flex` on body to prevent margin collapse from breaking overflow validation -- Use `margin` for spacing (padding included in size) -- Inline formatting: Use ``, ``, `` tags OR `` with CSS styles - - `` supports: `font-weight: bold`, `font-style: italic`, `text-decoration: underline`, `color: #rrggbb` - - `` does NOT support: `margin`, `padding` (not supported in PowerPoint text runs) - - Example: `Bold blue text` -- Flexbox works - positions calculated from rendered layout -- Use hex colors with `#` prefix in CSS -- **Text alignment**: Use CSS `text-align` (`center`, `right`, etc.) when needed as a hint to PptxGenJS for text formatting if text lengths are slightly off - -### Shape Styling (DIV elements only) - -**IMPORTANT: Backgrounds, borders, and shadows only work on `
                  ` elements, NOT on text elements (`

                  `, `

                  `-`

                  `, `
                    `, `
                      `)** - -- **Backgrounds**: CSS `background` or `background-color` on `
                      ` elements only - - Example: `
                      ` - Creates a shape with background -- **Borders**: CSS `border` on `
                      ` elements converts to PowerPoint shape borders - - Supports uniform borders: `border: 2px solid #333333` - - Supports partial borders: `border-left`, `border-right`, `border-top`, `border-bottom` (rendered as line shapes) - - Example: `
                      ` -- **Border radius**: CSS `border-radius` on `
                      ` elements for rounded corners - - `border-radius: 50%` or higher creates circular shape - - Percentages <50% calculated relative to shape's smaller dimension - - Supports px and pt units (e.g., `border-radius: 8pt;`, `border-radius: 12px;`) - - Example: `
                      ` on 100x200px box = 25% of 100px = 25px radius -- **Box shadows**: CSS `box-shadow` on `
                      ` elements converts to PowerPoint shadows - - Supports outer shadows only (inset shadows are ignored to prevent corruption) - - Example: `
                      ` - - Note: Inset/inner shadows are not supported by PowerPoint and will be skipped - -### Icons & Gradients - -- **CRITICAL: Never use CSS gradients (`linear-gradient`, `radial-gradient`)** - They don't convert to PowerPoint -- **ALWAYS create gradient/icon PNGs FIRST using Sharp, then reference in HTML** -- For gradients: Rasterize SVG to PNG background images -- For icons: Rasterize react-icons SVG to PNG images -- All visual effects must be pre-rendered as raster images before HTML rendering - -**Rasterizing Icons with Sharp:** - -```javascript -const React = require('react'); -const ReactDOMServer = require('react-dom/server'); -const sharp = require('sharp'); -const { FaHome } = require('react-icons/fa'); - -async function rasterizeIconPng(IconComponent, color, size = "256", filename) { - const svgString = ReactDOMServer.renderToStaticMarkup( - React.createElement(IconComponent, { color: `#${color}`, size: size }) - ); - - // Convert SVG to PNG using Sharp - await sharp(Buffer.from(svgString)) - .png() - .toFile(filename); - - return filename; -} - -// Usage: Rasterize icon before using in HTML -const iconPath = await rasterizeIconPng(FaHome, "4472c4", "256", "home-icon.png"); -// Then reference in HTML: -``` - -**Rasterizing Gradients with Sharp:** - -```javascript -const sharp = require('sharp'); - -async function createGradientBackground(filename) { - const svg = ` - - - - - - - - `; - - await sharp(Buffer.from(svg)) - .png() - .toFile(filename); - - return filename; -} - -// Usage: Create gradient background before HTML -const bgPath = await createGradientBackground("gradient-bg.png"); -// Then in HTML: -``` - -### Example - -```html - - - - - - -
                      -

                      Recipe Title

                      -
                        -
                      • Item: Description
                      • -
                      -

                      Text with bold, italic, underline.

                      -
                      - - -
                      -

                      5

                      -
                      -
                      - - -``` - -## Using the html2pptx Library - -### Dependencies - -These libraries have been globally installed and are available to use: -- `pptxgenjs` -- `playwright` -- `sharp` - -### Basic Usage - -```javascript -const pptxgen = require('pptxgenjs'); -const html2pptx = require('./html2pptx'); - -const pptx = new pptxgen(); -pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions - -const { slide, placeholders } = await html2pptx('slide1.html', pptx); - -// Add chart to placeholder area -if (placeholders.length > 0) { - slide.addChart(pptx.charts.LINE, chartData, placeholders[0]); -} - -await pptx.writeFile('output.pptx'); -``` - -### API Reference - -#### Function Signature -```javascript -await html2pptx(htmlFile, pres, options) -``` - -#### Parameters -- `htmlFile` (string): Path to HTML file (absolute or relative) -- `pres` (pptxgen): PptxGenJS presentation instance with layout already set -- `options` (object, optional): - - `tmpDir` (string): Temporary directory for generated files (default: `process.env.TMPDIR || '/tmp'`) - - `slide` (object): Existing slide to reuse (default: creates new slide) - -#### Returns -```javascript -{ - slide: pptxgenSlide, // The created/updated slide - placeholders: [ // Array of placeholder positions - { id: string, x: number, y: number, w: number, h: number }, - ... - ] -} -``` - -### Validation - -The library automatically validates and collects all errors before throwing: - -1. **HTML dimensions must match presentation layout** - Reports dimension mismatches -2. **Content must not overflow body** - Reports overflow with exact measurements -3. **CSS gradients** - Reports unsupported gradient usage -4. **Text element styling** - Reports backgrounds/borders/shadows on text elements (only allowed on divs) - -**All validation errors are collected and reported together** in a single error message, allowing you to fix all issues at once instead of one at a time. - -### Working with Placeholders - -```javascript -const { slide, placeholders } = await html2pptx('slide.html', pptx); - -// Use first placeholder -slide.addChart(pptx.charts.BAR, data, placeholders[0]); - -// Find by ID -const chartArea = placeholders.find(p => p.id === 'chart-area'); -slide.addChart(pptx.charts.LINE, data, chartArea); -``` - -### Complete Example - -```javascript -const pptxgen = require('pptxgenjs'); -const html2pptx = require('./html2pptx'); - -async function createPresentation() { - const pptx = new pptxgen(); - pptx.layout = 'LAYOUT_16x9'; - pptx.author = 'Your Name'; - pptx.title = 'My Presentation'; - - // Slide 1: Title - const { slide: slide1 } = await html2pptx('slides/title.html', pptx); - - // Slide 2: Content with chart - const { slide: slide2, placeholders } = await html2pptx('slides/data.html', pptx); - - const chartData = [{ - name: 'Sales', - labels: ['Q1', 'Q2', 'Q3', 'Q4'], - values: [4500, 5500, 6200, 7100] - }]; - - slide2.addChart(pptx.charts.BAR, chartData, { - ...placeholders[0], - showTitle: true, - title: 'Quarterly Sales', - showCatAxisTitle: true, - catAxisTitle: 'Quarter', - showValAxisTitle: true, - valAxisTitle: 'Sales ($000s)' - }); - - // Save - await pptx.writeFile({ fileName: 'presentation.pptx' }); - console.log('Presentation created successfully!'); -} - -createPresentation().catch(console.error); -``` - -## Using PptxGenJS - -After converting HTML to slides with `html2pptx`, you'll use PptxGenJS to add dynamic content like charts, images, and additional elements. - -### ⚠️ Critical Rules - -#### Colors -- **NEVER use `#` prefix** with hex colors in PptxGenJS - causes file corruption -- ✅ Correct: `color: "FF0000"`, `fill: { color: "0066CC" }` -- ❌ Wrong: `color: "#FF0000"` (breaks document) - -### Adding Images - -Always calculate aspect ratios from actual image dimensions: - -```javascript -// Get image dimensions: identify image.png | grep -o '[0-9]* x [0-9]*' -const imgWidth = 1860, imgHeight = 1519; // From actual file -const aspectRatio = imgWidth / imgHeight; - -const h = 3; // Max height -const w = h * aspectRatio; -const x = (10 - w) / 2; // Center on 16:9 slide - -slide.addImage({ path: "chart.png", x, y: 1.5, w, h }); -``` - -### Adding Text - -```javascript -// Rich text with formatting -slide.addText([ - { text: "Bold ", options: { bold: true } }, - { text: "Italic ", options: { italic: true } }, - { text: "Normal" } -], { - x: 1, y: 2, w: 8, h: 1 -}); -``` - -### Adding Shapes - -```javascript -// Rectangle -slide.addShape(pptx.shapes.RECTANGLE, { - x: 1, y: 1, w: 3, h: 2, - fill: { color: "4472C4" }, - line: { color: "000000", width: 2 } -}); - -// Circle -slide.addShape(pptx.shapes.OVAL, { - x: 5, y: 1, w: 2, h: 2, - fill: { color: "ED7D31" } -}); - -// Rounded rectangle -slide.addShape(pptx.shapes.ROUNDED_RECTANGLE, { - x: 1, y: 4, w: 3, h: 1.5, - fill: { color: "70AD47" }, - rectRadius: 0.2 -}); -``` - -### Adding Charts - -**Required for most charts:** Axis labels using `catAxisTitle` (category) and `valAxisTitle` (value). - -**Chart Data Format:** -- Use **single series with all labels** for simple bar/line charts -- Each series creates a separate legend entry -- Labels array defines X-axis values - -**Time Series Data - Choose Correct Granularity:** -- **< 30 days**: Use daily grouping (e.g., "10-01", "10-02") - avoid monthly aggregation that creates single-point charts -- **30-365 days**: Use monthly grouping (e.g., "2024-01", "2024-02") -- **> 365 days**: Use yearly grouping (e.g., "2023", "2024") -- **Validate**: Charts with only 1 data point likely indicate incorrect aggregation for the time period - -```javascript -const { slide, placeholders } = await html2pptx('slide.html', pptx); - -// CORRECT: Single series with all labels -slide.addChart(pptx.charts.BAR, [{ - name: "Sales 2024", - labels: ["Q1", "Q2", "Q3", "Q4"], - values: [4500, 5500, 6200, 7100] -}], { - ...placeholders[0], // Use placeholder position - barDir: 'col', // 'col' = vertical bars, 'bar' = horizontal - showTitle: true, - title: 'Quarterly Sales', - showLegend: false, // No legend needed for single series - // Required axis labels - showCatAxisTitle: true, - catAxisTitle: 'Quarter', - showValAxisTitle: true, - valAxisTitle: 'Sales ($000s)', - // Optional: Control scaling (adjust min based on data range for better visualization) - valAxisMaxVal: 8000, - valAxisMinVal: 0, // Use 0 for counts/amounts; for clustered data (e.g., 4500-7100), consider starting closer to min value - valAxisMajorUnit: 2000, // Control y-axis label spacing to prevent crowding - catAxisLabelRotate: 45, // Rotate labels if crowded - dataLabelPosition: 'outEnd', - dataLabelColor: '000000', - // Use single color for single-series charts - chartColors: ["4472C4"] // All bars same color -}); -``` - -#### Scatter Chart - -**IMPORTANT**: Scatter chart data format is unusual - first series contains X-axis values, subsequent series contain Y-values: - -```javascript -// Prepare data -const data1 = [{ x: 10, y: 20 }, { x: 15, y: 25 }, { x: 20, y: 30 }]; -const data2 = [{ x: 12, y: 18 }, { x: 18, y: 22 }]; - -const allXValues = [...data1.map(d => d.x), ...data2.map(d => d.x)]; - -slide.addChart(pptx.charts.SCATTER, [ - { name: 'X-Axis', values: allXValues }, // First series = X values - { name: 'Series 1', values: data1.map(d => d.y) }, // Y values only - { name: 'Series 2', values: data2.map(d => d.y) } // Y values only -], { - x: 1, y: 1, w: 8, h: 4, - lineSize: 0, // 0 = no connecting lines - lineDataSymbol: 'circle', - lineDataSymbolSize: 6, - showCatAxisTitle: true, - catAxisTitle: 'X Axis', - showValAxisTitle: true, - valAxisTitle: 'Y Axis', - chartColors: ["4472C4", "ED7D31"] -}); -``` - -#### Line Chart - -```javascript -slide.addChart(pptx.charts.LINE, [{ - name: "Temperature", - labels: ["Jan", "Feb", "Mar", "Apr"], - values: [32, 35, 42, 55] -}], { - x: 1, y: 1, w: 8, h: 4, - lineSize: 4, - lineSmooth: true, - // Required axis labels - showCatAxisTitle: true, - catAxisTitle: 'Month', - showValAxisTitle: true, - valAxisTitle: 'Temperature (°F)', - // Optional: Y-axis range (set min based on data range for better visualization) - valAxisMinVal: 0, // For ranges starting at 0 (counts, percentages, etc.) - valAxisMaxVal: 60, - valAxisMajorUnit: 20, // Control y-axis label spacing to prevent crowding (e.g., 10, 20, 25) - // valAxisMinVal: 30, // PREFERRED: For data clustered in a range (e.g., 32-55 or ratings 3-5), start axis closer to min value to show variation - // Optional: Chart colors - chartColors: ["4472C4", "ED7D31", "A5A5A5"] -}); -``` - -#### Pie Chart (No Axis Labels Required) - -**CRITICAL**: Pie charts require a **single data series** with all categories in the `labels` array and corresponding values in the `values` array. - -```javascript -slide.addChart(pptx.charts.PIE, [{ - name: "Market Share", - labels: ["Product A", "Product B", "Other"], // All categories in one array - values: [35, 45, 20] // All values in one array -}], { - x: 2, y: 1, w: 6, h: 4, - showPercent: true, - showLegend: true, - legendPos: 'r', // right - chartColors: ["4472C4", "ED7D31", "A5A5A5"] -}); -``` - -#### Multiple Data Series - -```javascript -slide.addChart(pptx.charts.LINE, [ - { - name: "Product A", - labels: ["Q1", "Q2", "Q3", "Q4"], - values: [10, 20, 30, 40] - }, - { - name: "Product B", - labels: ["Q1", "Q2", "Q3", "Q4"], - values: [15, 25, 20, 35] - } -], { - x: 1, y: 1, w: 8, h: 4, - showCatAxisTitle: true, - catAxisTitle: 'Quarter', - showValAxisTitle: true, - valAxisTitle: 'Revenue ($M)' -}); -``` - -### Chart Colors - -**CRITICAL**: Use hex colors **without** the `#` prefix - including `#` causes file corruption. - -**Align chart colors with your chosen design palette**, ensuring sufficient contrast and distinctiveness for data visualization. Adjust colors for: -- Strong contrast between adjacent series -- Readability against slide backgrounds -- Accessibility (avoid red-green only combinations) - -```javascript -// Example: Ocean palette-inspired chart colors (adjusted for contrast) -const chartColors = ["16A085", "FF6B9D", "2C3E50", "F39C12", "9B59B6"]; - -// Single-series chart: Use one color for all bars/points -slide.addChart(pptx.charts.BAR, [{ - name: "Sales", - labels: ["Q1", "Q2", "Q3", "Q4"], - values: [4500, 5500, 6200, 7100] -}], { - ...placeholders[0], - chartColors: ["16A085"], // All bars same color - showLegend: false -}); - -// Multi-series chart: Each series gets a different color -slide.addChart(pptx.charts.LINE, [ - { name: "Product A", labels: ["Q1", "Q2", "Q3"], values: [10, 20, 30] }, - { name: "Product B", labels: ["Q1", "Q2", "Q3"], values: [15, 25, 20] } -], { - ...placeholders[0], - chartColors: ["16A085", "FF6B9D"] // One color per series -}); -``` - -### Adding Tables - -Tables can be added with basic or advanced formatting: - -#### Basic Table - -```javascript -slide.addTable([ - ["Header 1", "Header 2", "Header 3"], - ["Row 1, Col 1", "Row 1, Col 2", "Row 1, Col 3"], - ["Row 2, Col 1", "Row 2, Col 2", "Row 2, Col 3"] -], { - x: 0.5, - y: 1, - w: 9, - h: 3, - border: { pt: 1, color: "999999" }, - fill: { color: "F1F1F1" } -}); -``` - -#### Table with Custom Formatting - -```javascript -const tableData = [ - // Header row with custom styling - [ - { text: "Product", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } }, - { text: "Revenue", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } }, - { text: "Growth", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } } - ], - // Data rows - ["Product A", "$50M", "+15%"], - ["Product B", "$35M", "+22%"], - ["Product C", "$28M", "+8%"] -]; - -slide.addTable(tableData, { - x: 1, - y: 1.5, - w: 8, - h: 3, - colW: [3, 2.5, 2.5], // Column widths - rowH: [0.5, 0.6, 0.6, 0.6], // Row heights - border: { pt: 1, color: "CCCCCC" }, - align: "center", - valign: "middle", - fontSize: 14 -}); -``` - -#### Table with Merged Cells - -```javascript -const mergedTableData = [ - [ - { text: "Q1 Results", options: { colspan: 3, fill: { color: "4472C4" }, color: "FFFFFF", bold: true } } - ], - ["Product", "Sales", "Market Share"], - ["Product A", "$25M", "35%"], - ["Product B", "$18M", "25%"] -]; - -slide.addTable(mergedTableData, { - x: 1, - y: 1, - w: 8, - h: 2.5, - colW: [3, 2.5, 2.5], - border: { pt: 1, color: "DDDDDD" } -}); -``` - -### Table Options - -Common table options: -- `x, y, w, h` - Position and size -- `colW` - Array of column widths (in inches) -- `rowH` - Array of row heights (in inches) -- `border` - Border style: `{ pt: 1, color: "999999" }` -- `fill` - Background color (no # prefix) -- `align` - Text alignment: "left", "center", "right" -- `valign` - Vertical alignment: "top", "middle", "bottom" -- `fontSize` - Text size -- `autoPage` - Auto-create new slides if content overflows - ---- - -## Storyboard/Wireframe HTML Guide (스토리보드/와이어프레임) - -스토리보드나 와이어프레임 HTML 슬라이드 작성 시 참고 가이드입니다. - -### Recommended Structure - -```html - - - - - - - - - -
                      - -
                      - - -
                      -
                      -

                      Description

                      -
                      - -
                      - - -``` - -### Common UI Components - -#### Checkbox -```html - -
                      - - -
                      -``` - -#### Icons (using emoji) -```html -

                      ✏️

                      -

                      -

                      🔔

                      -

                      🔍

                      -

                      📅

                      -``` - -#### Numbered Badge -```html -
                      -
                      -

                      01

                      -
                      -
                      -

                      전체 ▼

                      -
                      -
                      -``` - -#### Status Badge -```html -
                      -

                      견적대기

                      -
                      -``` - -#### Sidebar Menu with Hierarchy -```html - -

                      입찰관리

                      - - -

                      거래처관리

                      -

                      현장설명회관리

                      - - -
                      -

                      견적관리

                      -
                      -``` - -#### Table with Flexbox -```html -
                      - -
                      -
                      -
                      -
                      -
                      -

                      컬럼1

                      -
                      -
                      -

                      컬럼2

                      -
                      -
                      - -
                      -
                      -
                      -
                      -
                      -

                      데이터1

                      -
                      -
                      -

                      데이터2

                      -
                      -
                      -
                      -``` - -### Common Mistakes to Avoid - -1. **Never use `-` to start text** (treated as bullet symbol) -2. **Never use `-` for empty values** (use empty string or "N/A") -3. **Never use `*` to mark important items** (use `[공통]` instead) -4. **Never apply border/background directly to `

                      ` tags** (wrap with `

                      `) -5. **Never use `` tags** (use flexbox instead) \ No newline at end of file diff --git a/.claude/skills/pptx-skill/ooxml.md b/.claude/skills/pptx-skill/ooxml.md deleted file mode 100755 index 951b3cf..0000000 --- a/.claude/skills/pptx-skill/ooxml.md +++ /dev/null @@ -1,427 +0,0 @@ -# Office Open XML Technical Reference for PowerPoint - -**Important: Read this entire document before starting.** Critical XML schema rules and formatting requirements are covered throughout. Incorrect implementation can create invalid PPTX files that PowerPoint cannot open. - -## Technical Guidelines - -### Schema Compliance -- **Element ordering in ``**: ``, ``, `` -- **Whitespace**: Add `xml:space='preserve'` to `` elements with leading/trailing spaces -- **Unicode**: Escape characters in ASCII content: `"` becomes `“` -- **Images**: Add to `ppt/media/`, reference in slide XML, set dimensions to fit slide bounds -- **Relationships**: Update `ppt/slides/_rels/slideN.xml.rels` for each slide's resources -- **Dirty attribute**: Add `dirty="0"` to `` and `` elements to indicate clean state - -## Presentation Structure - -### Basic Slide Structure -```xml - - - - - ... - ... - - - - -``` - -### Text Box / Shape with Text -```xml - - - - - - - - - - - - - - - - - - - - - - Slide Title - - - - -``` - -### Text Formatting -```xml - - - - Bold Text - - - - - - Italic Text - - - - - - Underlined - - - - - - - - - - Highlighted Text - - - - - - - - - - Colored Arial 24pt - - - - - - - - - - Formatted text - -``` - -### Lists -```xml - - - - - - - First bullet point - - - - - - - - - - First numbered item - - - - - - - - - - Indented bullet - - -``` - -### Shapes -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Images -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Tables -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - Cell 1 - - - - - - - - - - - Cell 2 - - - - - - - - - -``` - -### Slide Layouts - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -## File Updates - -When adding content, update these files: - -**`ppt/_rels/presentation.xml.rels`:** -```xml - - -``` - -**`ppt/slides/_rels/slide1.xml.rels`:** -```xml - - -``` - -**`[Content_Types].xml`:** -```xml - - - -``` - -**`ppt/presentation.xml`:** -```xml - - - - -``` - -**`docProps/app.xml`:** Update slide count and statistics -```xml -2 -10 -50 -``` - -## Slide Operations - -### Adding a New Slide -When adding a slide to the end of the presentation: - -1. **Create the slide file** (`ppt/slides/slideN.xml`) -2. **Update `[Content_Types].xml`**: Add Override for the new slide -3. **Update `ppt/_rels/presentation.xml.rels`**: Add relationship for the new slide -4. **Update `ppt/presentation.xml`**: Add slide ID to `` -5. **Create slide relationships** (`ppt/slides/_rels/slideN.xml.rels`) if needed -6. **Update `docProps/app.xml`**: Increment slide count and update statistics (if present) - -### Duplicating a Slide -1. Copy the source slide XML file with a new name -2. Update all IDs in the new slide to be unique -3. Follow the "Adding a New Slide" steps above -4. **CRITICAL**: Remove or update any notes slide references in `_rels` files -5. Remove references to unused media files - -### Reordering Slides -1. **Update `ppt/presentation.xml`**: Reorder `` elements in `` -2. The order of `` elements determines slide order -3. Keep slide IDs and relationship IDs unchanged - -Example: -```xml - - - - - - - - - - - - - -``` - -### Deleting a Slide -1. **Remove from `ppt/presentation.xml`**: Delete the `` entry -2. **Remove from `ppt/_rels/presentation.xml.rels`**: Delete the relationship -3. **Remove from `[Content_Types].xml`**: Delete the Override entry -4. **Delete files**: Remove `ppt/slides/slideN.xml` and `ppt/slides/_rels/slideN.xml.rels` -5. **Update `docProps/app.xml`**: Decrement slide count and update statistics -6. **Clean up unused media**: Remove orphaned images from `ppt/media/` - -Note: Don't renumber remaining slides - keep their original IDs and filenames. - - -## Common Errors to Avoid - -- **Encodings**: Escape unicode characters in ASCII content: `"` becomes `“` -- **Images**: Add to `ppt/media/` and update relationship files -- **Lists**: Omit bullets from list headers -- **IDs**: Use valid hexadecimal values for UUIDs -- **Themes**: Check all themes in `theme` directory for colors - -## Validation Checklist for Template-Based Presentations - -### Before Packing, Always: -- **Clean unused resources**: Remove unreferenced media, fonts, and notes directories -- **Fix Content_Types.xml**: Declare ALL slides, layouts, and themes present in the package -- **Fix relationship IDs**: - - Remove font embed references if not using embedded fonts -- **Remove broken references**: Check all `_rels` files for references to deleted resources - -### Common Template Duplication Pitfalls: -- Multiple slides referencing the same notes slide after duplication -- Image/media references from template slides that no longer exist -- Font embedding references when fonts aren't included -- Missing slideLayout declarations for layouts 12-25 -- docProps directory may not unpack - this is optional \ No newline at end of file diff --git a/.claude/skills/pptx-skill/ooxml/scripts/pack.py b/.claude/skills/pptx-skill/ooxml/scripts/pack.py deleted file mode 100755 index 68bc088..0000000 --- a/.claude/skills/pptx-skill/ooxml/scripts/pack.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -""" -Tool to pack a directory into a .docx, .pptx, or .xlsx file with XML formatting undone. - -Example usage: - python pack.py [--force] -""" - -import argparse -import shutil -import subprocess -import sys -import tempfile -import defusedxml.minidom -import zipfile -from pathlib import Path - - -def main(): - parser = argparse.ArgumentParser(description="Pack a directory into an Office file") - parser.add_argument("input_directory", help="Unpacked Office document directory") - parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") - parser.add_argument("--force", action="store_true", help="Skip validation") - args = parser.parse_args() - - try: - success = pack_document( - args.input_directory, args.output_file, validate=not args.force - ) - - # Show warning if validation was skipped - if args.force: - print("Warning: Skipped validation, file may be corrupt", file=sys.stderr) - # Exit with error if validation failed - elif not success: - print("Contents would produce a corrupt file.", file=sys.stderr) - print("Please validate XML before repacking.", file=sys.stderr) - print("Use --force to skip validation and pack anyway.", file=sys.stderr) - sys.exit(1) - - except ValueError as e: - sys.exit(f"Error: {e}") - - -def pack_document(input_dir, output_file, validate=False): - """Pack a directory into an Office file (.docx/.pptx/.xlsx). - - Args: - input_dir: Path to unpacked Office document directory - output_file: Path to output Office file - validate: If True, validates with soffice (default: False) - - Returns: - bool: True if successful, False if validation failed - """ - input_dir = Path(input_dir) - output_file = Path(output_file) - - if not input_dir.is_dir(): - raise ValueError(f"{input_dir} is not a directory") - if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}: - raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file") - - # Work in temporary directory to avoid modifying original - with tempfile.TemporaryDirectory() as temp_dir: - temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) - - # Process XML files to remove pretty-printing whitespace - for pattern in ["*.xml", "*.rels"]: - for xml_file in temp_content_dir.rglob(pattern): - condense_xml(xml_file) - - # Create final Office file as zip archive - output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: - for f in temp_content_dir.rglob("*"): - if f.is_file(): - zf.write(f, f.relative_to(temp_content_dir)) - - # Validate if requested - if validate: - if not validate_document(output_file): - output_file.unlink() # Delete the corrupt file - return False - - return True - - -def validate_document(doc_path): - """Validate document by converting to HTML with soffice.""" - # Determine the correct filter based on file extension - match doc_path.suffix.lower(): - case ".docx": - filter_name = "html:HTML" - case ".pptx": - filter_name = "html:impress_html_Export" - case ".xlsx": - filter_name = "html:HTML (StarCalc)" - - with tempfile.TemporaryDirectory() as temp_dir: - try: - result = subprocess.run( - [ - "soffice", - "--headless", - "--convert-to", - filter_name, - "--outdir", - temp_dir, - str(doc_path), - ], - capture_output=True, - timeout=10, - text=True, - ) - if not (Path(temp_dir) / f"{doc_path.stem}.html").exists(): - error_msg = result.stderr.strip() or "Document validation failed" - print(f"Validation error: {error_msg}", file=sys.stderr) - return False - return True - except FileNotFoundError: - print("Warning: soffice not found. Skipping validation.", file=sys.stderr) - return True - except subprocess.TimeoutExpired: - print("Validation error: Timeout during conversion", file=sys.stderr) - return False - except Exception as e: - print(f"Validation error: {e}", file=sys.stderr) - return False - - -def condense_xml(xml_file): - """Strip unnecessary whitespace and remove comments.""" - with open(xml_file, "r", encoding="utf-8") as f: - dom = defusedxml.minidom.parse(f) - - # Process each element to remove whitespace and comments - for element in dom.getElementsByTagName("*"): - # Skip w:t elements and their processing - if element.tagName.endswith(":t"): - continue - - # Remove whitespace-only text nodes and comment nodes - for child in list(element.childNodes): - if ( - child.nodeType == child.TEXT_NODE - and child.nodeValue - and child.nodeValue.strip() == "" - ) or child.nodeType == child.COMMENT_NODE: - element.removeChild(child) - - # Write back the condensed XML - with open(xml_file, "wb") as f: - f.write(dom.toxml(encoding="UTF-8")) - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/pptx-skill/ooxml/scripts/unpack.py b/.claude/skills/pptx-skill/ooxml/scripts/unpack.py deleted file mode 100755 index 4938798..0000000 --- a/.claude/skills/pptx-skill/ooxml/scripts/unpack.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)""" - -import random -import sys -import defusedxml.minidom -import zipfile -from pathlib import Path - -# Get command line arguments -assert len(sys.argv) == 3, "Usage: python unpack.py " -input_file, output_dir = sys.argv[1], sys.argv[2] - -# Extract and format -output_path = Path(output_dir) -output_path.mkdir(parents=True, exist_ok=True) -zipfile.ZipFile(input_file).extractall(output_path) - -# Pretty print all XML files -xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) -for xml_file in xml_files: - content = xml_file.read_text(encoding="utf-8") - dom = defusedxml.minidom.parseString(content) - xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii")) - -# For .docx files, suggest an RSID for tracked changes -if input_file.endswith(".docx"): - suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8)) - print(f"Suggested RSID for edit session: {suggested_rsid}") diff --git a/.claude/skills/pptx-skill/ooxml/scripts/validate.py b/.claude/skills/pptx-skill/ooxml/scripts/validate.py deleted file mode 100755 index 508c589..0000000 --- a/.claude/skills/pptx-skill/ooxml/scripts/validate.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -""" -Command line tool to validate Office document XML files against XSD schemas and tracked changes. - -Usage: - python validate.py --original -""" - -import argparse -import sys -from pathlib import Path - -from validation import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator - - -def main(): - parser = argparse.ArgumentParser(description="Validate Office document XML files") - parser.add_argument( - "unpacked_dir", - help="Path to unpacked Office document directory", - ) - parser.add_argument( - "--original", - required=True, - help="Path to original file (.docx/.pptx/.xlsx)", - ) - parser.add_argument( - "-v", - "--verbose", - action="store_true", - help="Enable verbose output", - ) - args = parser.parse_args() - - # Validate paths - unpacked_dir = Path(args.unpacked_dir) - original_file = Path(args.original) - file_extension = original_file.suffix.lower() - assert unpacked_dir.is_dir(), f"Error: {unpacked_dir} is not a directory" - assert original_file.is_file(), f"Error: {original_file} is not a file" - assert file_extension in [".docx", ".pptx", ".xlsx"], ( - f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" - ) - - # Run validations - match file_extension: - case ".docx": - validators = [DOCXSchemaValidator, RedliningValidator] - case ".pptx": - validators = [PPTXSchemaValidator] - case _: - print(f"Error: Validation not supported for file type {file_extension}") - sys.exit(1) - - # Run validators - success = True - for V in validators: - validator = V(unpacked_dir, original_file, verbose=args.verbose) - if not validator.validate(): - success = False - - if success: - print("All validations PASSED!") - - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/pptx-skill/scripts/enhanced-pptx-generator.js b/.claude/skills/pptx-skill/scripts/enhanced-pptx-generator.js deleted file mode 100755 index c76eb0c..0000000 --- a/.claude/skills/pptx-skill/scripts/enhanced-pptx-generator.js +++ /dev/null @@ -1,359 +0,0 @@ -/** - * Enhanced PPTX Generator - 재사용 가능한 프레젠테이션 생성 모듈 - * 카드형 배경, 아이콘, 통계 카드 등 시각적 요소를 포함한 PowerPoint 생성 - */ - -const pptxgen = require('pptxgenjs'); - -class EnhancedPPTXGenerator { - constructor(options = {}) { - this.pres = new pptxgen(); - this.pres.layout = options.layout || 'LAYOUT_16x9'; - this.pres.author = options.author || 'Enhanced PPTX Generator'; - this.pres.title = options.title || 'Generated Presentation'; - - // 기본 브랜드 색상 설정 - this.brandColor = options.brandColor || 'FF0000'; - this.backgroundColor = options.backgroundColor || 'ffffff'; - this.cardColor = options.cardColor || 'f8f9fa'; - this.borderColor = options.borderColor || 'e2e8f0'; - } - - /** - * 카드형 배경 도형 추가 - */ - addCard(slide, x, y, width, height, options = {}) { - const cardOptions = { - x, y, w: width, h: height, - fill: { color: options.fillColor || this.cardColor }, - line: { - color: options.borderColor || this.borderColor, - width: options.borderWidth || 1 - }, - rectRadius: options.radius || 0.15 - }; - - slide.addShape(this.pres.ShapeType.rect, cardOptions); - - // 상단 컬러 바 추가 (옵션) - if (options.topBar) { - slide.addShape(this.pres.ShapeType.rect, { - x, y, w: width, h: options.topBarHeight || 0.15, - fill: { color: options.topBarColor || this.brandColor } - }); - } - - return cardOptions; - } - - /** - * 원형 아이콘 배경 추가 - */ - addIconBackground(slide, x, y, size, iconText, options = {}) { - // 원형 배경 - slide.addShape(this.pres.ShapeType.ellipse, { - x, y, w: size, h: size, - fill: { color: options.backgroundColor || this.brandColor }, - line: { color: options.borderColor || this.brandColor, width: options.borderWidth || 0 } - }); - - // 아이콘 텍스트 - slide.addText(iconText, { - x, y, w: size, h: size, - fontSize: options.fontSize || (size * 40), // 크기에 비례한 폰트 - color: options.textColor || 'ffffff', - align: 'center', - valign: 'middle' - }); - - return { x, y, size }; - } - - /** - * 통계 카드 추가 - */ - addStatCard(slide, x, y, width, height, statData, options = {}) { - const { number, description, icon, color } = statData; - - // 카드 배경 - slide.addShape(this.pres.ShapeType.rect, { - x, y, w: width, h: height, - fill: { color: options.backgroundColor || 'ffffff' }, - line: { color: color || this.brandColor, width: options.borderWidth || 2 }, - rectRadius: options.radius || 0.15 - }); - - // 아이콘 (선택사항) - if (icon) { - slide.addText(icon, { - x: x + 0.1, y: y + 0.1, w: 0.5, h: 0.5, - fontSize: options.iconSize || 24 - }); - } - - // 숫자/통계 - slide.addText(number, { - x: x + (icon ? 0.7 : 0.1), - y: y + 0.15, - w: width - (icon ? 0.8 : 0.2), - h: height * 0.4, - fontSize: options.numberSize || 18, - bold: true, - color: color || this.brandColor, - align: 'center' - }); - - // 설명 - slide.addText(description, { - x: x + 0.1, - y: y + height * 0.6, - w: width - 0.2, - h: height * 0.3, - fontSize: options.descriptionSize || 11, - color: '666666', - align: 'center' - }); - - return { x, y, width, height }; - } - - /** - * 타임라인 단계 추가 - */ - addTimelineStep(slide, x, y, stepNumber, title, description, options = {}) { - const stepColor = options.completed ? this.brandColor : 'f0f0f0'; - const textColor = options.completed ? 'ffffff' : '666666'; - - // 원형 단계 번호 - this.addIconBackground(slide, x, y, 0.6, stepNumber.toString(), { - backgroundColor: stepColor, - textColor: textColor, - fontSize: 20 - }); - - // 제목 - slide.addText(title, { - x: x + 0.8, y: y, w: 7, h: 0.4, - fontSize: 16, bold: true, color: this.brandColor - }); - - // 설명 - slide.addText(description, { - x: x + 0.8, y: y + 0.4, w: 7, h: 0.4, - fontSize: 12, color: '666666' - }); - - return { x, y }; - } - - /** - * 제목 슬라이드 생성 - */ - createTitleSlide(mainTitle, subtitle, description, options = {}) { - const slide = this.pres.addSlide(); - slide.background = { color: this.backgroundColor }; - - // 메인 카드 - this.addCard(slide, 1.5, 1, 7, 4, { - topBar: true, - topBarColor: this.brandColor - }); - - // 메인 제목 - slide.addText(mainTitle, { - x: 1.5, y: 1.8, w: 7, h: 1, - fontSize: options.titleSize || 48, - bold: true, - color: '282828', - align: 'center' - }); - - // 부제목 - if (subtitle) { - slide.addText(subtitle, { - x: 1.5, y: 2.8, w: 7, h: 0.8, - fontSize: options.subtitleSize || 24, - bold: true, - color: this.brandColor, - align: 'center' - }); - } - - // 설명 - if (description) { - slide.addText(description, { - x: 1.5, y: 3.6, w: 7, h: 0.6, - fontSize: options.descriptionSize || 16, - color: '666666', - align: 'center' - }); - } - - return slide; - } - - /** - * 콘텐츠 슬라이드 생성 - */ - createContentSlide(title, items = [], options = {}) { - const slide = this.pres.addSlide(); - slide.background = { color: this.backgroundColor }; - - // 제목 - slide.addText(title, { - x: 0.5, y: 0.5, w: 9, h: 0.8, - fontSize: options.titleSize || 28, - bold: true, - color: '282828' - }); - - // 아이템들을 카드 형태로 배치 - items.forEach((item, index) => { - const cols = options.columns || 2; - const x = 0.5 + (index % cols) * (9 / cols); - const y = 1.5 + Math.floor(index / cols) * (options.itemHeight || 1.5); - const width = (9 / cols) - 0.3; - const height = options.itemHeight || 1.3; - - this.addCard(slide, x, y, width, height, { - topBar: true - }); - - // 아이콘 - if (item.icon) { - this.addIconBackground(slide, x + 0.2, y + 0.2, 0.6, item.icon, { - backgroundColor: this.brandColor - }); - } - - // 제목 - slide.addText(item.title, { - x: x + (item.icon ? 1 : 0.3), - y: y + 0.3, - w: width - (item.icon ? 1.3 : 0.6), - h: 0.4, - fontSize: 16, bold: true, color: this.brandColor - }); - - // 설명 - if (item.description) { - slide.addText(item.description, { - x: x + 0.3, - y: y + 0.8, - w: width - 0.6, - h: 0.4, - fontSize: 11, color: '666666' - }); - } - }); - - return slide; - } - - /** - * 통계 슬라이드 생성 - */ - createStatsSlide(title, stats = [], options = {}) { - const slide = this.pres.addSlide(); - slide.background = { color: this.backgroundColor }; - - // 제목 - slide.addText(title, { - x: 0.5, y: 0.5, w: 9, h: 0.8, - fontSize: options.titleSize || 28, - bold: true, - color: '282828' - }); - - // 통계 카드들 - stats.forEach((stat, index) => { - const x = 0.25 + index * (9 / stats.length); - const width = (9 / stats.length) - 0.3; - - this.addStatCard(slide, x, 2, width, 1.5, stat); - }); - - return slide; - } - - /** - * 파일 저장 - */ - async save(filename) { - const fs = require('fs'); - const path = require('path'); - - // pptx 폴더 경로 생성 - const pptxDir = 'pptx'; - if (!fs.existsSync(pptxDir)) { - fs.mkdirSync(pptxDir, { recursive: true }); - } - - // 파일 경로 설정 - const fullPath = path.join(pptxDir, filename); - - await this.pres.writeFile({ fileName: fullPath }); - return { - filename: fullPath, - slideCount: this.pres.slides.length, - success: true - }; - } - - /** - * 슬라이드 수 반환 - */ - getSlideCount() { - return this.pres.slides.length; - } -} - -// 편의 함수들 -const createJoCodingPresentation = async (options = {}) => { - const generator = new EnhancedPPTXGenerator({ - title: '조코딩(조웅현) - 국내 1위 코딩 교육 유튜버', - author: '조코딩', - brandColor: 'FF0000', - ...options - }); - - // 슬라이드 1: 표지 - generator.createTitleSlide( - '조코딩', - 'JoCoding', - '국내 1위 코딩 교육 유튜버' - ); - - // 슬라이드 2: 프로필 - const profileItems = [ - { - icon: '👤', - title: '기본 정보', - description: '• 실명: 조웅현\n• 전공: 고려대 환경생태공학부\n• 경력: 비전공자 → 개발자 → 교육자' - }, - { - icon: '🚀', - title: '현재 활동', - description: '• 유튜브: 68만 구독자\n• 회사: ㈜코드마피아 대표\n• 서비스: 조카소 AI 개발/운영' - } - ]; - - generator.createContentSlide('프로필 개요', profileItems); - - // 슬라이드 3: 통계 - const stats = [ - { number: '68만+', description: '유튜브 구독자', icon: '📺', color: 'FF0000' }, - { number: '2019', description: '시작 연도', icon: '🗓️', color: '666666' }, - { number: '2,000만+', description: '동물상 테스트', icon: '🤖', color: 'ff6600' }, - { number: '1,000+', description: '교육생', icon: '🎓', color: '00aa00' } - ]; - - generator.createStatsSlide('주요 성과', stats); - - return await generator.save(options.filename || '조코딩_프레젠테이션_모듈생성.pptx'); -}; - -module.exports = { - EnhancedPPTXGenerator, - createJoCodingPresentation -}; \ No newline at end of file diff --git a/.claude/skills/pptx-skill/scripts/html2pptx.cjs b/.claude/skills/pptx-skill/scripts/html2pptx.cjs deleted file mode 100755 index 437bf7c..0000000 --- a/.claude/skills/pptx-skill/scripts/html2pptx.cjs +++ /dev/null @@ -1,979 +0,0 @@ -/** - * html2pptx - Convert HTML slide to pptxgenjs slide with positioned elements - * - * USAGE: - * const pptx = new pptxgen(); - * pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions - * - * const { slide, placeholders } = await html2pptx('slide.html', pptx); - * slide.addChart(pptx.charts.LINE, data, placeholders[0]); - * - * await pptx.writeFile('output.pptx'); - * - * FEATURES: - * - Converts HTML to PowerPoint with accurate positioning - * - Supports text, images, shapes, and bullet lists - * - Extracts placeholder elements (class="placeholder") with positions - * - Handles CSS gradients, borders, and margins - * - * VALIDATION: - * - Uses body width/height from HTML for viewport sizing - * - Throws error if HTML dimensions don't match presentation layout - * - Throws error if content overflows body (with overflow details) - * - * RETURNS: - * { slide, placeholders } where placeholders is an array of { id, x, y, w, h } - */ - -const { chromium } = require('playwright'); -const path = require('path'); -const sharp = require('sharp'); - -const PT_PER_PX = 0.75; -const PX_PER_IN = 96; -const EMU_PER_IN = 914400; - -// Helper: Get body dimensions and check for overflow -async function getBodyDimensions(page) { - const bodyDimensions = await page.evaluate(() => { - const body = document.body; - const style = window.getComputedStyle(body); - - return { - width: parseFloat(style.width), - height: parseFloat(style.height), - scrollWidth: body.scrollWidth, - scrollHeight: body.scrollHeight - }; - }); - - const errors = []; - const widthOverflowPx = Math.max(0, bodyDimensions.scrollWidth - bodyDimensions.width - 1); - const heightOverflowPx = Math.max(0, bodyDimensions.scrollHeight - bodyDimensions.height - 1); - - const widthOverflowPt = widthOverflowPx * PT_PER_PX; - const heightOverflowPt = heightOverflowPx * PT_PER_PX; - - if (widthOverflowPt > 0 || heightOverflowPt > 0) { - const directions = []; - if (widthOverflowPt > 0) directions.push(`${widthOverflowPt.toFixed(1)}pt horizontally`); - if (heightOverflowPt > 0) directions.push(`${heightOverflowPt.toFixed(1)}pt vertically`); - const reminder = heightOverflowPt > 0 ? ' (Remember: leave 0.5" margin at bottom of slide)' : ''; - errors.push(`HTML content overflows body by ${directions.join(' and ')}${reminder}`); - } - - return { ...bodyDimensions, errors }; -} - -// Helper: Validate dimensions match presentation layout -function validateDimensions(bodyDimensions, pres) { - const errors = []; - const widthInches = bodyDimensions.width / PX_PER_IN; - const heightInches = bodyDimensions.height / PX_PER_IN; - - if (pres.presLayout) { - const layoutWidth = pres.presLayout.width / EMU_PER_IN; - const layoutHeight = pres.presLayout.height / EMU_PER_IN; - - if (Math.abs(layoutWidth - widthInches) > 0.1 || Math.abs(layoutHeight - heightInches) > 0.1) { - errors.push( - `HTML dimensions (${widthInches.toFixed(1)}" × ${heightInches.toFixed(1)}") ` + - `don't match presentation layout (${layoutWidth.toFixed(1)}" × ${layoutHeight.toFixed(1)}")` - ); - } - } - return errors; -} - -function validateTextBoxPosition(slideData, bodyDimensions) { - const errors = []; - const slideHeightInches = bodyDimensions.height / PX_PER_IN; - const minBottomMargin = 0.5; // 0.5 inches from bottom - - for (const el of slideData.elements) { - // Check text elements (p, h1-h6, list) - if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'list'].includes(el.type)) { - const fontSize = el.style?.fontSize || 0; - const bottomEdge = el.position.y + el.position.h; - const distanceFromBottom = slideHeightInches - bottomEdge; - - if (fontSize > 12 && distanceFromBottom < minBottomMargin) { - const getText = () => { - if (typeof el.text === 'string') return el.text; - if (Array.isArray(el.text)) return el.text.find(t => t.text)?.text || ''; - if (Array.isArray(el.items)) return el.items.find(item => item.text)?.text || ''; - return ''; - }; - const textPrefix = getText().substring(0, 50) + (getText().length > 50 ? '...' : ''); - - errors.push( - `Text box "${textPrefix}" ends too close to bottom edge ` + - `(${distanceFromBottom.toFixed(2)}" from bottom, minimum ${minBottomMargin}" required)` - ); - } - } - } - - return errors; -} - -// Helper: Add background to slide -async function addBackground(slideData, targetSlide, tmpDir) { - if (slideData.background.type === 'image' && slideData.background.path) { - let imagePath = slideData.background.path.startsWith('file://') - ? slideData.background.path.replace('file://', '') - : slideData.background.path; - targetSlide.background = { path: imagePath }; - } else if (slideData.background.type === 'color' && slideData.background.value) { - targetSlide.background = { color: slideData.background.value }; - } -} - -// Helper: Add elements to slide -function addElements(slideData, targetSlide, pres) { - for (const el of slideData.elements) { - if (el.type === 'image') { - let imagePath = el.src.startsWith('file://') ? el.src.replace('file://', '') : el.src; - targetSlide.addImage({ - path: imagePath, - x: el.position.x, - y: el.position.y, - w: el.position.w, - h: el.position.h - }); - } else if (el.type === 'line') { - targetSlide.addShape(pres.ShapeType.line, { - x: el.x1, - y: el.y1, - w: el.x2 - el.x1, - h: el.y2 - el.y1, - line: { color: el.color, width: el.width } - }); - } else if (el.type === 'shape') { - const shapeOptions = { - x: el.position.x, - y: el.position.y, - w: el.position.w, - h: el.position.h, - shape: el.shape.rectRadius > 0 ? pres.ShapeType.roundRect : pres.ShapeType.rect - }; - - if (el.shape.fill) { - shapeOptions.fill = { color: el.shape.fill }; - if (el.shape.transparency != null) shapeOptions.fill.transparency = el.shape.transparency; - } - if (el.shape.line) shapeOptions.line = el.shape.line; - if (el.shape.rectRadius > 0) shapeOptions.rectRadius = el.shape.rectRadius; - if (el.shape.shadow) shapeOptions.shadow = el.shape.shadow; - - targetSlide.addText(el.text || '', shapeOptions); - } else if (el.type === 'list') { - const listOptions = { - x: el.position.x, - y: el.position.y, - w: el.position.w, - h: el.position.h, - fontSize: el.style.fontSize, - fontFace: el.style.fontFace, - color: el.style.color, - align: el.style.align, - valign: 'top', - lineSpacing: el.style.lineSpacing, - paraSpaceBefore: el.style.paraSpaceBefore, - paraSpaceAfter: el.style.paraSpaceAfter, - margin: el.style.margin - }; - if (el.style.margin) listOptions.margin = el.style.margin; - targetSlide.addText(el.items, listOptions); - } else { - // Check if text is single-line (height suggests one line) - const lineHeight = el.style.lineSpacing || el.style.fontSize * 1.2; - const isSingleLine = el.position.h <= lineHeight * 1.5; - - let adjustedX = el.position.x; - let adjustedW = el.position.w; - - // Make single-line text 2% wider to account for underestimate - if (isSingleLine) { - const widthIncrease = el.position.w * 0.02; - const align = el.style.align; - - if (align === 'center') { - // Center: expand both sides - adjustedX = el.position.x - (widthIncrease / 2); - adjustedW = el.position.w + widthIncrease; - } else if (align === 'right') { - // Right: expand to the left - adjustedX = el.position.x - widthIncrease; - adjustedW = el.position.w + widthIncrease; - } else { - // Left (default): expand to the right - adjustedW = el.position.w + widthIncrease; - } - } - - const textOptions = { - x: adjustedX, - y: el.position.y, - w: adjustedW, - h: el.position.h, - fontSize: el.style.fontSize, - fontFace: el.style.fontFace, - color: el.style.color, - bold: el.style.bold, - italic: el.style.italic, - underline: el.style.underline, - valign: 'top', - lineSpacing: el.style.lineSpacing, - paraSpaceBefore: el.style.paraSpaceBefore, - paraSpaceAfter: el.style.paraSpaceAfter, - inset: 0 // Remove default PowerPoint internal padding - }; - - if (el.style.align) textOptions.align = el.style.align; - if (el.style.margin) textOptions.margin = el.style.margin; - if (el.style.rotate !== undefined) textOptions.rotate = el.style.rotate; - if (el.style.transparency !== null && el.style.transparency !== undefined) textOptions.transparency = el.style.transparency; - - targetSlide.addText(el.text, textOptions); - } - } -} - -// Helper: Extract slide data from HTML page -async function extractSlideData(page) { - return await page.evaluate(() => { - const PT_PER_PX = 0.75; - const PX_PER_IN = 96; - - // Fonts that are single-weight and should not have bold applied - // (applying bold causes PowerPoint to use faux bold which makes text wider) - const SINGLE_WEIGHT_FONTS = ['impact']; - - // Helper: Check if a font should skip bold formatting - const shouldSkipBold = (fontFamily) => { - if (!fontFamily) return false; - const normalizedFont = fontFamily.toLowerCase().replace(/['"]/g, '').split(',')[0].trim(); - return SINGLE_WEIGHT_FONTS.includes(normalizedFont); - }; - - // Unit conversion helpers - const pxToInch = (px) => px / PX_PER_IN; - const pxToPoints = (pxStr) => parseFloat(pxStr) * PT_PER_PX; - const rgbToHex = (rgbStr) => { - // Handle transparent backgrounds by defaulting to white - if (rgbStr === 'rgba(0, 0, 0, 0)' || rgbStr === 'transparent') return 'FFFFFF'; - - const match = rgbStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (!match) return 'FFFFFF'; - return match.slice(1).map(n => parseInt(n).toString(16).padStart(2, '0')).join(''); - }; - - const extractAlpha = (rgbStr) => { - const match = rgbStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/); - if (!match || !match[4]) return null; - const alpha = parseFloat(match[4]); - return Math.round((1 - alpha) * 100); - }; - - const applyTextTransform = (text, textTransform) => { - if (textTransform === 'uppercase') return text.toUpperCase(); - if (textTransform === 'lowercase') return text.toLowerCase(); - if (textTransform === 'capitalize') { - return text.replace(/\b\w/g, c => c.toUpperCase()); - } - return text; - }; - - // Extract rotation angle from CSS transform and writing-mode - const getRotation = (transform, writingMode) => { - let angle = 0; - - // Handle writing-mode first - // PowerPoint: 90° = text rotated 90° clockwise (reads top to bottom, letters upright) - // PowerPoint: 270° = text rotated 270° clockwise (reads bottom to top, letters upright) - if (writingMode === 'vertical-rl') { - // vertical-rl alone = text reads top to bottom = 90° in PowerPoint - angle = 90; - } else if (writingMode === 'vertical-lr') { - // vertical-lr alone = text reads bottom to top = 270° in PowerPoint - angle = 270; - } - - // Then add any transform rotation - if (transform && transform !== 'none') { - // Try to match rotate() function - const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/); - if (rotateMatch) { - angle += parseFloat(rotateMatch[1]); - } else { - // Browser may compute as matrix - extract rotation from matrix - const matrixMatch = transform.match(/matrix\(([^)]+)\)/); - if (matrixMatch) { - const values = matrixMatch[1].split(',').map(parseFloat); - // matrix(a, b, c, d, e, f) where rotation = atan2(b, a) - const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI); - angle += Math.round(matrixAngle); - } - } - } - - // Normalize to 0-359 range - angle = angle % 360; - if (angle < 0) angle += 360; - - return angle === 0 ? null : angle; - }; - - // Get position/dimensions accounting for rotation - const getPositionAndSize = (el, rect, rotation) => { - if (rotation === null) { - return { x: rect.left, y: rect.top, w: rect.width, h: rect.height }; - } - - // For 90° or 270° rotations, swap width and height - // because PowerPoint applies rotation to the original (unrotated) box - const isVertical = rotation === 90 || rotation === 270; - - if (isVertical) { - // The browser shows us the rotated dimensions (tall box for vertical text) - // But PowerPoint needs the pre-rotation dimensions (wide box that will be rotated) - // So we swap: browser's height becomes PPT's width, browser's width becomes PPT's height - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - - return { - x: centerX - rect.height / 2, - y: centerY - rect.width / 2, - w: rect.height, - h: rect.width - }; - } - - // For other rotations, use element's offset dimensions - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - return { - x: centerX - el.offsetWidth / 2, - y: centerY - el.offsetHeight / 2, - w: el.offsetWidth, - h: el.offsetHeight - }; - }; - - // Parse CSS box-shadow into PptxGenJS shadow properties - const parseBoxShadow = (boxShadow) => { - if (!boxShadow || boxShadow === 'none') return null; - - // Browser computed style format: "rgba(0, 0, 0, 0.3) 2px 2px 8px 0px [inset]" - // CSS format: "[inset] 2px 2px 8px 0px rgba(0, 0, 0, 0.3)" - - const insetMatch = boxShadow.match(/inset/); - - // IMPORTANT: PptxGenJS/PowerPoint doesn't properly support inset shadows - // Only process outer shadows to avoid file corruption - if (insetMatch) return null; - - // Extract color first (rgba or rgb at start) - const colorMatch = boxShadow.match(/rgba?\([^)]+\)/); - - // Extract numeric values (handles both px and pt units) - const parts = boxShadow.match(/([-\d.]+)(px|pt)/g); - - if (!parts || parts.length < 2) return null; - - const offsetX = parseFloat(parts[0]); - const offsetY = parseFloat(parts[1]); - const blur = parts.length > 2 ? parseFloat(parts[2]) : 0; - - // Calculate angle from offsets (in degrees, 0 = right, 90 = down) - let angle = 0; - if (offsetX !== 0 || offsetY !== 0) { - angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI); - if (angle < 0) angle += 360; - } - - // Calculate offset distance (hypotenuse) - const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX; - - // Extract opacity from rgba - let opacity = 0.5; - if (colorMatch) { - const opacityMatch = colorMatch[0].match(/[\d.]+\)$/); - if (opacityMatch) { - opacity = parseFloat(opacityMatch[0].replace(')', '')); - } - } - - return { - type: 'outer', - angle: Math.round(angle), - blur: blur * 0.75, // Convert to points - color: colorMatch ? rgbToHex(colorMatch[0]) : '000000', - offset: offset, - opacity - }; - }; - - // Parse inline formatting tags (, , , , , ) into text runs - const parseInlineFormatting = (element, baseOptions = {}, runs = [], baseTextTransform = (x) => x) => { - let prevNodeIsText = false; - - element.childNodes.forEach((node) => { - let textTransform = baseTextTransform; - - const isText = node.nodeType === Node.TEXT_NODE || node.tagName === 'BR'; - if (isText) { - const text = node.tagName === 'BR' ? '\n' : textTransform(node.textContent.replace(/\s+/g, ' ')); - const prevRun = runs[runs.length - 1]; - if (prevNodeIsText && prevRun) { - prevRun.text += text; - } else { - runs.push({ text, options: { ...baseOptions } }); - } - - } else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) { - const options = { ...baseOptions }; - const computed = window.getComputedStyle(node); - - // Handle inline elements with computed styles - if (node.tagName === 'SPAN' || node.tagName === 'B' || node.tagName === 'STRONG' || node.tagName === 'I' || node.tagName === 'EM' || node.tagName === 'U') { - const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; - if (isBold && !shouldSkipBold(computed.fontFamily)) options.bold = true; - if (computed.fontStyle === 'italic') options.italic = true; - if (computed.textDecoration && computed.textDecoration.includes('underline')) options.underline = true; - if (computed.color && computed.color !== 'rgb(0, 0, 0)') { - options.color = rgbToHex(computed.color); - const transparency = extractAlpha(computed.color); - if (transparency !== null) options.transparency = transparency; - } - if (computed.fontSize) options.fontSize = pxToPoints(computed.fontSize); - - // Apply text-transform on the span element itself - if (computed.textTransform && computed.textTransform !== 'none') { - const transformStr = computed.textTransform; - textTransform = (text) => applyTextTransform(text, transformStr); - } - - // Validate: Check for margins on inline elements - if (computed.marginLeft && parseFloat(computed.marginLeft) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`); - } - if (computed.marginRight && parseFloat(computed.marginRight) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`); - } - if (computed.marginTop && parseFloat(computed.marginTop) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`); - } - if (computed.marginBottom && parseFloat(computed.marginBottom) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`); - } - - // Recursively process the child node. This will flatten nested spans into multiple runs. - parseInlineFormatting(node, options, runs, textTransform); - } - } - - prevNodeIsText = isText; - }); - - // Trim leading space from first run and trailing space from last run - if (runs.length > 0) { - runs[0].text = runs[0].text.replace(/^\s+/, ''); - runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, ''); - } - - return runs.filter(r => r.text.length > 0); - }; - - // Extract background from body (image or color) - const body = document.body; - const bodyStyle = window.getComputedStyle(body); - const bgImage = bodyStyle.backgroundImage; - const bgColor = bodyStyle.backgroundColor; - - // Collect validation errors - const errors = []; - - // Validate: Check for CSS gradients - if (bgImage && (bgImage.includes('linear-gradient') || bgImage.includes('radial-gradient'))) { - errors.push( - 'CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, ' + - 'then reference with background-image: url(\'gradient.png\')' - ); - } - - let background; - if (bgImage && bgImage !== 'none') { - // Extract URL from url("...") or url(...) - const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/); - if (urlMatch) { - background = { - type: 'image', - path: urlMatch[1] - }; - } else { - background = { - type: 'color', - value: rgbToHex(bgColor) - }; - } - } else { - background = { - type: 'color', - value: rgbToHex(bgColor) - }; - } - - // Process all elements - const elements = []; - const placeholders = []; - const textTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI']; - const processed = new Set(); - - document.querySelectorAll('*').forEach((el) => { - if (processed.has(el)) return; - - // Validate text elements don't have backgrounds, borders, or shadows - if (textTags.includes(el.tagName)) { - const computed = window.getComputedStyle(el); - const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; - const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) || - (computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) || - (computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) || - (computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) || - (computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0); - const hasShadow = computed.boxShadow && computed.boxShadow !== 'none'; - - if (hasBg || hasBorder || hasShadow) { - errors.push( - `Text element <${el.tagName.toLowerCase()}> has ${hasBg ? 'background' : hasBorder ? 'border' : 'shadow'}. ` + - 'Backgrounds, borders, and shadows are only supported on
                      elements, not text elements.' - ); - return; - } - } - - // Extract placeholder elements (for charts, etc.) - if (el.className && el.className.includes('placeholder')) { - const rect = el.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) { - errors.push( - `Placeholder "${el.id || 'unnamed'}" has ${rect.width === 0 ? 'width: 0' : 'height: 0'}. Check the layout CSS.` - ); - } else { - placeholders.push({ - id: el.id || `placeholder-${placeholders.length}`, - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - }); - } - processed.add(el); - return; - } - - // Extract images - if (el.tagName === 'IMG') { - const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0) { - elements.push({ - type: 'image', - src: el.src, - position: { - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - } - }); - processed.add(el); - return; - } - } - - // Extract DIVs with backgrounds/borders as shapes - const isContainer = el.tagName === 'DIV' && !textTags.includes(el.tagName); - if (isContainer) { - const computed = window.getComputedStyle(el); - const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; - - // Validate: Check for unwrapped text content in DIV - for (const node of el.childNodes) { - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent.trim(); - if (text) { - errors.push( - `DIV element contains unwrapped text "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}". ` + - 'All text must be wrapped in

                      ,

                      -

                      ,
                        , or
                          tags to appear in PowerPoint.' - ); - } - } - } - - // Check for background images on shapes - const bgImage = computed.backgroundImage; - if (bgImage && bgImage !== 'none') { - errors.push( - 'Background images on DIV elements are not supported. ' + - 'Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.' - ); - return; - } - - // Check for borders - both uniform and partial - const borderTop = computed.borderTopWidth; - const borderRight = computed.borderRightWidth; - const borderBottom = computed.borderBottomWidth; - const borderLeft = computed.borderLeftWidth; - const borders = [borderTop, borderRight, borderBottom, borderLeft].map(b => parseFloat(b) || 0); - const hasBorder = borders.some(b => b > 0); - const hasUniformBorder = hasBorder && borders.every(b => b === borders[0]); - const borderLines = []; - - if (hasBorder && !hasUniformBorder) { - const rect = el.getBoundingClientRect(); - const x = pxToInch(rect.left); - const y = pxToInch(rect.top); - const w = pxToInch(rect.width); - const h = pxToInch(rect.height); - - // Collect lines to add after shape (inset by half the line width to center on edge) - if (parseFloat(borderTop) > 0) { - const widthPt = pxToPoints(borderTop); - const inset = (widthPt / 72) / 2; // Convert points to inches, then half - borderLines.push({ - type: 'line', - x1: x, y1: y + inset, x2: x + w, y2: y + inset, - width: widthPt, - color: rgbToHex(computed.borderTopColor) - }); - } - if (parseFloat(borderRight) > 0) { - const widthPt = pxToPoints(borderRight); - const inset = (widthPt / 72) / 2; - borderLines.push({ - type: 'line', - x1: x + w - inset, y1: y, x2: x + w - inset, y2: y + h, - width: widthPt, - color: rgbToHex(computed.borderRightColor) - }); - } - if (parseFloat(borderBottom) > 0) { - const widthPt = pxToPoints(borderBottom); - const inset = (widthPt / 72) / 2; - borderLines.push({ - type: 'line', - x1: x, y1: y + h - inset, x2: x + w, y2: y + h - inset, - width: widthPt, - color: rgbToHex(computed.borderBottomColor) - }); - } - if (parseFloat(borderLeft) > 0) { - const widthPt = pxToPoints(borderLeft); - const inset = (widthPt / 72) / 2; - borderLines.push({ - type: 'line', - x1: x + inset, y1: y, x2: x + inset, y2: y + h, - width: widthPt, - color: rgbToHex(computed.borderLeftColor) - }); - } - } - - if (hasBg || hasBorder) { - const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0) { - const shadow = parseBoxShadow(computed.boxShadow); - - // Only add shape if there's background or uniform border - if (hasBg || hasUniformBorder) { - elements.push({ - type: 'shape', - text: '', // Shape only - child text elements render on top - position: { - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - }, - shape: { - fill: hasBg ? rgbToHex(computed.backgroundColor) : null, - transparency: hasBg ? extractAlpha(computed.backgroundColor) : null, - line: hasUniformBorder ? { - color: rgbToHex(computed.borderColor), - width: pxToPoints(computed.borderWidth) - } : null, - // Convert border-radius to rectRadius (in inches) - // % values: 50%+ = circle (1), <50% = percentage of min dimension - // pt values: divide by 72 (72pt = 1 inch) - // px values: divide by 96 (96px = 1 inch) - rectRadius: (() => { - const radius = computed.borderRadius; - const radiusValue = parseFloat(radius); - if (radiusValue === 0) return 0; - - if (radius.includes('%')) { - if (radiusValue >= 50) return 1; - // Calculate percentage of smaller dimension - const minDim = Math.min(rect.width, rect.height); - return (radiusValue / 100) * pxToInch(minDim); - } - - if (radius.includes('pt')) return radiusValue / 72; - return radiusValue / PX_PER_IN; - })(), - shadow: shadow - } - }); - } - - // Add partial border lines - elements.push(...borderLines); - - processed.add(el); - return; - } - } - } - - // Extract bullet lists as single text block - if (el.tagName === 'UL' || el.tagName === 'OL') { - const rect = el.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return; - - const liElements = Array.from(el.querySelectorAll('li')); - const items = []; - const ulComputed = window.getComputedStyle(el); - const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft); - - // Split: margin-left for bullet position, indent for text position - // margin-left + indent = ul padding-left - const marginLeft = ulPaddingLeftPt * 0.5; - const textIndent = ulPaddingLeftPt * 0.5; - - liElements.forEach((li, idx) => { - const isLast = idx === liElements.length - 1; - const runs = parseInlineFormatting(li, { breakLine: false }); - // Clean manual bullets from first run - if (runs.length > 0) { - runs[0].text = runs[0].text.replace(/^[•\-\*▪▸]\s*/, ''); - runs[0].options.bullet = { indent: textIndent }; - } - // Set breakLine on last run - if (runs.length > 0 && !isLast) { - runs[runs.length - 1].options.breakLine = true; - } - items.push(...runs); - }); - - const computed = window.getComputedStyle(liElements[0] || el); - - elements.push({ - type: 'list', - items: items, - position: { - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - }, - style: { - fontSize: pxToPoints(computed.fontSize), - fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), - color: rgbToHex(computed.color), - transparency: extractAlpha(computed.color), - align: computed.textAlign === 'start' ? 'left' : computed.textAlign, - lineSpacing: computed.lineHeight && computed.lineHeight !== 'normal' ? pxToPoints(computed.lineHeight) : null, - paraSpaceBefore: 0, - paraSpaceAfter: pxToPoints(computed.marginBottom), - // PptxGenJS margin array is [left, right, bottom, top] - margin: [marginLeft, 0, 0, 0] - } - }); - - liElements.forEach(li => processed.add(li)); - processed.add(el); - return; - } - - // Extract text elements (P, H1, H2, etc.) - if (!textTags.includes(el.tagName)) return; - - const rect = el.getBoundingClientRect(); - const text = el.textContent.trim(); - if (rect.width === 0 || rect.height === 0 || !text) return; - - // Validate: Check for manual bullet symbols in text elements (not in lists) - if (el.tagName !== 'LI' && /^[•\-\*▪▸○●◆◇■□]\s/.test(text.trimStart())) { - errors.push( - `Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` + - 'Use
                            or
                              lists instead of manual bullet symbols.' - ); - return; - } - - const computed = window.getComputedStyle(el); - const rotation = getRotation(computed.transform, computed.writingMode); - const { x, y, w, h } = getPositionAndSize(el, rect, rotation); - - const baseStyle = { - fontSize: pxToPoints(computed.fontSize), - fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), - color: rgbToHex(computed.color), - align: computed.textAlign === 'start' ? 'left' : computed.textAlign, - lineSpacing: pxToPoints(computed.lineHeight), - paraSpaceBefore: pxToPoints(computed.marginTop), - paraSpaceAfter: pxToPoints(computed.marginBottom), - // PptxGenJS margin array is [left, right, bottom, top] (not [top, right, bottom, left] as documented) - margin: [ - pxToPoints(computed.paddingLeft), - pxToPoints(computed.paddingRight), - pxToPoints(computed.paddingBottom), - pxToPoints(computed.paddingTop) - ] - }; - - const transparency = extractAlpha(computed.color); - if (transparency !== null) baseStyle.transparency = transparency; - - if (rotation !== null) baseStyle.rotate = rotation; - - const hasFormatting = el.querySelector('b, i, u, strong, em, span, br'); - - if (hasFormatting) { - // Text with inline formatting - const transformStr = computed.textTransform; - const runs = parseInlineFormatting(el, {}, [], (str) => applyTextTransform(str, transformStr)); - - // Adjust lineSpacing based on largest fontSize in runs - const adjustedStyle = { ...baseStyle }; - if (adjustedStyle.lineSpacing) { - const maxFontSize = Math.max( - adjustedStyle.fontSize, - ...runs.map(r => r.options?.fontSize || 0) - ); - if (maxFontSize > adjustedStyle.fontSize) { - const lineHeightMultiplier = adjustedStyle.lineSpacing / adjustedStyle.fontSize; - adjustedStyle.lineSpacing = maxFontSize * lineHeightMultiplier; - } - } - - elements.push({ - type: el.tagName.toLowerCase(), - text: runs, - position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, - style: adjustedStyle - }); - } else { - // Plain text - inherit CSS formatting - const textTransform = computed.textTransform; - const transformedText = applyTextTransform(text, textTransform); - - const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; - - elements.push({ - type: el.tagName.toLowerCase(), - text: transformedText, - position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, - style: { - ...baseStyle, - bold: isBold && !shouldSkipBold(computed.fontFamily), - italic: computed.fontStyle === 'italic', - underline: computed.textDecoration.includes('underline') - } - }); - } - - processed.add(el); - }); - - return { background, elements, placeholders, errors }; - }); -} - -async function html2pptx(htmlFile, pres, options = {}) { - const { - tmpDir = process.env.TMPDIR || '/tmp', - slide = null - } = options; - - try { - // Use Chrome on macOS, default Chromium on Unix - const launchOptions = { env: { TMPDIR: tmpDir } }; - if (process.platform === 'darwin') { - launchOptions.channel = 'chrome'; - } - - const browser = await chromium.launch(launchOptions); - - let bodyDimensions; - let slideData; - - const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile); - const validationErrors = []; - - try { - const page = await browser.newPage(); - page.on('console', (msg) => { - // Log the message text to your test runner's console - console.log(`Browser console: ${msg.text()}`); - }); - - await page.goto(`file://${filePath}`); - - bodyDimensions = await getBodyDimensions(page); - - await page.setViewportSize({ - width: Math.round(bodyDimensions.width), - height: Math.round(bodyDimensions.height) - }); - - slideData = await extractSlideData(page); - } finally { - await browser.close(); - } - - // Collect all validation errors - if (bodyDimensions.errors && bodyDimensions.errors.length > 0) { - validationErrors.push(...bodyDimensions.errors); - } - - const dimensionErrors = validateDimensions(bodyDimensions, pres); - if (dimensionErrors.length > 0) { - validationErrors.push(...dimensionErrors); - } - - const textBoxPositionErrors = validateTextBoxPosition(slideData, bodyDimensions); - if (textBoxPositionErrors.length > 0) { - validationErrors.push(...textBoxPositionErrors); - } - - if (slideData.errors && slideData.errors.length > 0) { - validationErrors.push(...slideData.errors); - } - - // Throw all errors at once if any exist - if (validationErrors.length > 0) { - const errorMessage = validationErrors.length === 1 - ? validationErrors[0] - : `Multiple validation errors found:\n${validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`; - throw new Error(errorMessage); - } - - const targetSlide = slide || pres.addSlide(); - - await addBackground(slideData, targetSlide, tmpDir); - addElements(slideData, targetSlide, pres); - - return { slide: targetSlide, placeholders: slideData.placeholders }; - } catch (error) { - if (!error.message.startsWith(htmlFile)) { - throw new Error(`${htmlFile}: ${error.message}`); - } - throw error; - } -} - -module.exports = html2pptx; \ No newline at end of file diff --git a/.claude/skills/pptx-skill/scripts/html2pptx.js b/.claude/skills/pptx-skill/scripts/html2pptx.js deleted file mode 100755 index 437bf7c..0000000 --- a/.claude/skills/pptx-skill/scripts/html2pptx.js +++ /dev/null @@ -1,979 +0,0 @@ -/** - * html2pptx - Convert HTML slide to pptxgenjs slide with positioned elements - * - * USAGE: - * const pptx = new pptxgen(); - * pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions - * - * const { slide, placeholders } = await html2pptx('slide.html', pptx); - * slide.addChart(pptx.charts.LINE, data, placeholders[0]); - * - * await pptx.writeFile('output.pptx'); - * - * FEATURES: - * - Converts HTML to PowerPoint with accurate positioning - * - Supports text, images, shapes, and bullet lists - * - Extracts placeholder elements (class="placeholder") with positions - * - Handles CSS gradients, borders, and margins - * - * VALIDATION: - * - Uses body width/height from HTML for viewport sizing - * - Throws error if HTML dimensions don't match presentation layout - * - Throws error if content overflows body (with overflow details) - * - * RETURNS: - * { slide, placeholders } where placeholders is an array of { id, x, y, w, h } - */ - -const { chromium } = require('playwright'); -const path = require('path'); -const sharp = require('sharp'); - -const PT_PER_PX = 0.75; -const PX_PER_IN = 96; -const EMU_PER_IN = 914400; - -// Helper: Get body dimensions and check for overflow -async function getBodyDimensions(page) { - const bodyDimensions = await page.evaluate(() => { - const body = document.body; - const style = window.getComputedStyle(body); - - return { - width: parseFloat(style.width), - height: parseFloat(style.height), - scrollWidth: body.scrollWidth, - scrollHeight: body.scrollHeight - }; - }); - - const errors = []; - const widthOverflowPx = Math.max(0, bodyDimensions.scrollWidth - bodyDimensions.width - 1); - const heightOverflowPx = Math.max(0, bodyDimensions.scrollHeight - bodyDimensions.height - 1); - - const widthOverflowPt = widthOverflowPx * PT_PER_PX; - const heightOverflowPt = heightOverflowPx * PT_PER_PX; - - if (widthOverflowPt > 0 || heightOverflowPt > 0) { - const directions = []; - if (widthOverflowPt > 0) directions.push(`${widthOverflowPt.toFixed(1)}pt horizontally`); - if (heightOverflowPt > 0) directions.push(`${heightOverflowPt.toFixed(1)}pt vertically`); - const reminder = heightOverflowPt > 0 ? ' (Remember: leave 0.5" margin at bottom of slide)' : ''; - errors.push(`HTML content overflows body by ${directions.join(' and ')}${reminder}`); - } - - return { ...bodyDimensions, errors }; -} - -// Helper: Validate dimensions match presentation layout -function validateDimensions(bodyDimensions, pres) { - const errors = []; - const widthInches = bodyDimensions.width / PX_PER_IN; - const heightInches = bodyDimensions.height / PX_PER_IN; - - if (pres.presLayout) { - const layoutWidth = pres.presLayout.width / EMU_PER_IN; - const layoutHeight = pres.presLayout.height / EMU_PER_IN; - - if (Math.abs(layoutWidth - widthInches) > 0.1 || Math.abs(layoutHeight - heightInches) > 0.1) { - errors.push( - `HTML dimensions (${widthInches.toFixed(1)}" × ${heightInches.toFixed(1)}") ` + - `don't match presentation layout (${layoutWidth.toFixed(1)}" × ${layoutHeight.toFixed(1)}")` - ); - } - } - return errors; -} - -function validateTextBoxPosition(slideData, bodyDimensions) { - const errors = []; - const slideHeightInches = bodyDimensions.height / PX_PER_IN; - const minBottomMargin = 0.5; // 0.5 inches from bottom - - for (const el of slideData.elements) { - // Check text elements (p, h1-h6, list) - if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'list'].includes(el.type)) { - const fontSize = el.style?.fontSize || 0; - const bottomEdge = el.position.y + el.position.h; - const distanceFromBottom = slideHeightInches - bottomEdge; - - if (fontSize > 12 && distanceFromBottom < minBottomMargin) { - const getText = () => { - if (typeof el.text === 'string') return el.text; - if (Array.isArray(el.text)) return el.text.find(t => t.text)?.text || ''; - if (Array.isArray(el.items)) return el.items.find(item => item.text)?.text || ''; - return ''; - }; - const textPrefix = getText().substring(0, 50) + (getText().length > 50 ? '...' : ''); - - errors.push( - `Text box "${textPrefix}" ends too close to bottom edge ` + - `(${distanceFromBottom.toFixed(2)}" from bottom, minimum ${minBottomMargin}" required)` - ); - } - } - } - - return errors; -} - -// Helper: Add background to slide -async function addBackground(slideData, targetSlide, tmpDir) { - if (slideData.background.type === 'image' && slideData.background.path) { - let imagePath = slideData.background.path.startsWith('file://') - ? slideData.background.path.replace('file://', '') - : slideData.background.path; - targetSlide.background = { path: imagePath }; - } else if (slideData.background.type === 'color' && slideData.background.value) { - targetSlide.background = { color: slideData.background.value }; - } -} - -// Helper: Add elements to slide -function addElements(slideData, targetSlide, pres) { - for (const el of slideData.elements) { - if (el.type === 'image') { - let imagePath = el.src.startsWith('file://') ? el.src.replace('file://', '') : el.src; - targetSlide.addImage({ - path: imagePath, - x: el.position.x, - y: el.position.y, - w: el.position.w, - h: el.position.h - }); - } else if (el.type === 'line') { - targetSlide.addShape(pres.ShapeType.line, { - x: el.x1, - y: el.y1, - w: el.x2 - el.x1, - h: el.y2 - el.y1, - line: { color: el.color, width: el.width } - }); - } else if (el.type === 'shape') { - const shapeOptions = { - x: el.position.x, - y: el.position.y, - w: el.position.w, - h: el.position.h, - shape: el.shape.rectRadius > 0 ? pres.ShapeType.roundRect : pres.ShapeType.rect - }; - - if (el.shape.fill) { - shapeOptions.fill = { color: el.shape.fill }; - if (el.shape.transparency != null) shapeOptions.fill.transparency = el.shape.transparency; - } - if (el.shape.line) shapeOptions.line = el.shape.line; - if (el.shape.rectRadius > 0) shapeOptions.rectRadius = el.shape.rectRadius; - if (el.shape.shadow) shapeOptions.shadow = el.shape.shadow; - - targetSlide.addText(el.text || '', shapeOptions); - } else if (el.type === 'list') { - const listOptions = { - x: el.position.x, - y: el.position.y, - w: el.position.w, - h: el.position.h, - fontSize: el.style.fontSize, - fontFace: el.style.fontFace, - color: el.style.color, - align: el.style.align, - valign: 'top', - lineSpacing: el.style.lineSpacing, - paraSpaceBefore: el.style.paraSpaceBefore, - paraSpaceAfter: el.style.paraSpaceAfter, - margin: el.style.margin - }; - if (el.style.margin) listOptions.margin = el.style.margin; - targetSlide.addText(el.items, listOptions); - } else { - // Check if text is single-line (height suggests one line) - const lineHeight = el.style.lineSpacing || el.style.fontSize * 1.2; - const isSingleLine = el.position.h <= lineHeight * 1.5; - - let adjustedX = el.position.x; - let adjustedW = el.position.w; - - // Make single-line text 2% wider to account for underestimate - if (isSingleLine) { - const widthIncrease = el.position.w * 0.02; - const align = el.style.align; - - if (align === 'center') { - // Center: expand both sides - adjustedX = el.position.x - (widthIncrease / 2); - adjustedW = el.position.w + widthIncrease; - } else if (align === 'right') { - // Right: expand to the left - adjustedX = el.position.x - widthIncrease; - adjustedW = el.position.w + widthIncrease; - } else { - // Left (default): expand to the right - adjustedW = el.position.w + widthIncrease; - } - } - - const textOptions = { - x: adjustedX, - y: el.position.y, - w: adjustedW, - h: el.position.h, - fontSize: el.style.fontSize, - fontFace: el.style.fontFace, - color: el.style.color, - bold: el.style.bold, - italic: el.style.italic, - underline: el.style.underline, - valign: 'top', - lineSpacing: el.style.lineSpacing, - paraSpaceBefore: el.style.paraSpaceBefore, - paraSpaceAfter: el.style.paraSpaceAfter, - inset: 0 // Remove default PowerPoint internal padding - }; - - if (el.style.align) textOptions.align = el.style.align; - if (el.style.margin) textOptions.margin = el.style.margin; - if (el.style.rotate !== undefined) textOptions.rotate = el.style.rotate; - if (el.style.transparency !== null && el.style.transparency !== undefined) textOptions.transparency = el.style.transparency; - - targetSlide.addText(el.text, textOptions); - } - } -} - -// Helper: Extract slide data from HTML page -async function extractSlideData(page) { - return await page.evaluate(() => { - const PT_PER_PX = 0.75; - const PX_PER_IN = 96; - - // Fonts that are single-weight and should not have bold applied - // (applying bold causes PowerPoint to use faux bold which makes text wider) - const SINGLE_WEIGHT_FONTS = ['impact']; - - // Helper: Check if a font should skip bold formatting - const shouldSkipBold = (fontFamily) => { - if (!fontFamily) return false; - const normalizedFont = fontFamily.toLowerCase().replace(/['"]/g, '').split(',')[0].trim(); - return SINGLE_WEIGHT_FONTS.includes(normalizedFont); - }; - - // Unit conversion helpers - const pxToInch = (px) => px / PX_PER_IN; - const pxToPoints = (pxStr) => parseFloat(pxStr) * PT_PER_PX; - const rgbToHex = (rgbStr) => { - // Handle transparent backgrounds by defaulting to white - if (rgbStr === 'rgba(0, 0, 0, 0)' || rgbStr === 'transparent') return 'FFFFFF'; - - const match = rgbStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (!match) return 'FFFFFF'; - return match.slice(1).map(n => parseInt(n).toString(16).padStart(2, '0')).join(''); - }; - - const extractAlpha = (rgbStr) => { - const match = rgbStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/); - if (!match || !match[4]) return null; - const alpha = parseFloat(match[4]); - return Math.round((1 - alpha) * 100); - }; - - const applyTextTransform = (text, textTransform) => { - if (textTransform === 'uppercase') return text.toUpperCase(); - if (textTransform === 'lowercase') return text.toLowerCase(); - if (textTransform === 'capitalize') { - return text.replace(/\b\w/g, c => c.toUpperCase()); - } - return text; - }; - - // Extract rotation angle from CSS transform and writing-mode - const getRotation = (transform, writingMode) => { - let angle = 0; - - // Handle writing-mode first - // PowerPoint: 90° = text rotated 90° clockwise (reads top to bottom, letters upright) - // PowerPoint: 270° = text rotated 270° clockwise (reads bottom to top, letters upright) - if (writingMode === 'vertical-rl') { - // vertical-rl alone = text reads top to bottom = 90° in PowerPoint - angle = 90; - } else if (writingMode === 'vertical-lr') { - // vertical-lr alone = text reads bottom to top = 270° in PowerPoint - angle = 270; - } - - // Then add any transform rotation - if (transform && transform !== 'none') { - // Try to match rotate() function - const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/); - if (rotateMatch) { - angle += parseFloat(rotateMatch[1]); - } else { - // Browser may compute as matrix - extract rotation from matrix - const matrixMatch = transform.match(/matrix\(([^)]+)\)/); - if (matrixMatch) { - const values = matrixMatch[1].split(',').map(parseFloat); - // matrix(a, b, c, d, e, f) where rotation = atan2(b, a) - const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI); - angle += Math.round(matrixAngle); - } - } - } - - // Normalize to 0-359 range - angle = angle % 360; - if (angle < 0) angle += 360; - - return angle === 0 ? null : angle; - }; - - // Get position/dimensions accounting for rotation - const getPositionAndSize = (el, rect, rotation) => { - if (rotation === null) { - return { x: rect.left, y: rect.top, w: rect.width, h: rect.height }; - } - - // For 90° or 270° rotations, swap width and height - // because PowerPoint applies rotation to the original (unrotated) box - const isVertical = rotation === 90 || rotation === 270; - - if (isVertical) { - // The browser shows us the rotated dimensions (tall box for vertical text) - // But PowerPoint needs the pre-rotation dimensions (wide box that will be rotated) - // So we swap: browser's height becomes PPT's width, browser's width becomes PPT's height - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - - return { - x: centerX - rect.height / 2, - y: centerY - rect.width / 2, - w: rect.height, - h: rect.width - }; - } - - // For other rotations, use element's offset dimensions - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - return { - x: centerX - el.offsetWidth / 2, - y: centerY - el.offsetHeight / 2, - w: el.offsetWidth, - h: el.offsetHeight - }; - }; - - // Parse CSS box-shadow into PptxGenJS shadow properties - const parseBoxShadow = (boxShadow) => { - if (!boxShadow || boxShadow === 'none') return null; - - // Browser computed style format: "rgba(0, 0, 0, 0.3) 2px 2px 8px 0px [inset]" - // CSS format: "[inset] 2px 2px 8px 0px rgba(0, 0, 0, 0.3)" - - const insetMatch = boxShadow.match(/inset/); - - // IMPORTANT: PptxGenJS/PowerPoint doesn't properly support inset shadows - // Only process outer shadows to avoid file corruption - if (insetMatch) return null; - - // Extract color first (rgba or rgb at start) - const colorMatch = boxShadow.match(/rgba?\([^)]+\)/); - - // Extract numeric values (handles both px and pt units) - const parts = boxShadow.match(/([-\d.]+)(px|pt)/g); - - if (!parts || parts.length < 2) return null; - - const offsetX = parseFloat(parts[0]); - const offsetY = parseFloat(parts[1]); - const blur = parts.length > 2 ? parseFloat(parts[2]) : 0; - - // Calculate angle from offsets (in degrees, 0 = right, 90 = down) - let angle = 0; - if (offsetX !== 0 || offsetY !== 0) { - angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI); - if (angle < 0) angle += 360; - } - - // Calculate offset distance (hypotenuse) - const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX; - - // Extract opacity from rgba - let opacity = 0.5; - if (colorMatch) { - const opacityMatch = colorMatch[0].match(/[\d.]+\)$/); - if (opacityMatch) { - opacity = parseFloat(opacityMatch[0].replace(')', '')); - } - } - - return { - type: 'outer', - angle: Math.round(angle), - blur: blur * 0.75, // Convert to points - color: colorMatch ? rgbToHex(colorMatch[0]) : '000000', - offset: offset, - opacity - }; - }; - - // Parse inline formatting tags (, , , , , ) into text runs - const parseInlineFormatting = (element, baseOptions = {}, runs = [], baseTextTransform = (x) => x) => { - let prevNodeIsText = false; - - element.childNodes.forEach((node) => { - let textTransform = baseTextTransform; - - const isText = node.nodeType === Node.TEXT_NODE || node.tagName === 'BR'; - if (isText) { - const text = node.tagName === 'BR' ? '\n' : textTransform(node.textContent.replace(/\s+/g, ' ')); - const prevRun = runs[runs.length - 1]; - if (prevNodeIsText && prevRun) { - prevRun.text += text; - } else { - runs.push({ text, options: { ...baseOptions } }); - } - - } else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) { - const options = { ...baseOptions }; - const computed = window.getComputedStyle(node); - - // Handle inline elements with computed styles - if (node.tagName === 'SPAN' || node.tagName === 'B' || node.tagName === 'STRONG' || node.tagName === 'I' || node.tagName === 'EM' || node.tagName === 'U') { - const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; - if (isBold && !shouldSkipBold(computed.fontFamily)) options.bold = true; - if (computed.fontStyle === 'italic') options.italic = true; - if (computed.textDecoration && computed.textDecoration.includes('underline')) options.underline = true; - if (computed.color && computed.color !== 'rgb(0, 0, 0)') { - options.color = rgbToHex(computed.color); - const transparency = extractAlpha(computed.color); - if (transparency !== null) options.transparency = transparency; - } - if (computed.fontSize) options.fontSize = pxToPoints(computed.fontSize); - - // Apply text-transform on the span element itself - if (computed.textTransform && computed.textTransform !== 'none') { - const transformStr = computed.textTransform; - textTransform = (text) => applyTextTransform(text, transformStr); - } - - // Validate: Check for margins on inline elements - if (computed.marginLeft && parseFloat(computed.marginLeft) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`); - } - if (computed.marginRight && parseFloat(computed.marginRight) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`); - } - if (computed.marginTop && parseFloat(computed.marginTop) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`); - } - if (computed.marginBottom && parseFloat(computed.marginBottom) > 0) { - errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`); - } - - // Recursively process the child node. This will flatten nested spans into multiple runs. - parseInlineFormatting(node, options, runs, textTransform); - } - } - - prevNodeIsText = isText; - }); - - // Trim leading space from first run and trailing space from last run - if (runs.length > 0) { - runs[0].text = runs[0].text.replace(/^\s+/, ''); - runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, ''); - } - - return runs.filter(r => r.text.length > 0); - }; - - // Extract background from body (image or color) - const body = document.body; - const bodyStyle = window.getComputedStyle(body); - const bgImage = bodyStyle.backgroundImage; - const bgColor = bodyStyle.backgroundColor; - - // Collect validation errors - const errors = []; - - // Validate: Check for CSS gradients - if (bgImage && (bgImage.includes('linear-gradient') || bgImage.includes('radial-gradient'))) { - errors.push( - 'CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, ' + - 'then reference with background-image: url(\'gradient.png\')' - ); - } - - let background; - if (bgImage && bgImage !== 'none') { - // Extract URL from url("...") or url(...) - const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/); - if (urlMatch) { - background = { - type: 'image', - path: urlMatch[1] - }; - } else { - background = { - type: 'color', - value: rgbToHex(bgColor) - }; - } - } else { - background = { - type: 'color', - value: rgbToHex(bgColor) - }; - } - - // Process all elements - const elements = []; - const placeholders = []; - const textTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI']; - const processed = new Set(); - - document.querySelectorAll('*').forEach((el) => { - if (processed.has(el)) return; - - // Validate text elements don't have backgrounds, borders, or shadows - if (textTags.includes(el.tagName)) { - const computed = window.getComputedStyle(el); - const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; - const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) || - (computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) || - (computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) || - (computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) || - (computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0); - const hasShadow = computed.boxShadow && computed.boxShadow !== 'none'; - - if (hasBg || hasBorder || hasShadow) { - errors.push( - `Text element <${el.tagName.toLowerCase()}> has ${hasBg ? 'background' : hasBorder ? 'border' : 'shadow'}. ` + - 'Backgrounds, borders, and shadows are only supported on
                              elements, not text elements.' - ); - return; - } - } - - // Extract placeholder elements (for charts, etc.) - if (el.className && el.className.includes('placeholder')) { - const rect = el.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) { - errors.push( - `Placeholder "${el.id || 'unnamed'}" has ${rect.width === 0 ? 'width: 0' : 'height: 0'}. Check the layout CSS.` - ); - } else { - placeholders.push({ - id: el.id || `placeholder-${placeholders.length}`, - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - }); - } - processed.add(el); - return; - } - - // Extract images - if (el.tagName === 'IMG') { - const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0) { - elements.push({ - type: 'image', - src: el.src, - position: { - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - } - }); - processed.add(el); - return; - } - } - - // Extract DIVs with backgrounds/borders as shapes - const isContainer = el.tagName === 'DIV' && !textTags.includes(el.tagName); - if (isContainer) { - const computed = window.getComputedStyle(el); - const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; - - // Validate: Check for unwrapped text content in DIV - for (const node of el.childNodes) { - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent.trim(); - if (text) { - errors.push( - `DIV element contains unwrapped text "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}". ` + - 'All text must be wrapped in

                              ,

                              -

                              ,
                                , or
                                  tags to appear in PowerPoint.' - ); - } - } - } - - // Check for background images on shapes - const bgImage = computed.backgroundImage; - if (bgImage && bgImage !== 'none') { - errors.push( - 'Background images on DIV elements are not supported. ' + - 'Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.' - ); - return; - } - - // Check for borders - both uniform and partial - const borderTop = computed.borderTopWidth; - const borderRight = computed.borderRightWidth; - const borderBottom = computed.borderBottomWidth; - const borderLeft = computed.borderLeftWidth; - const borders = [borderTop, borderRight, borderBottom, borderLeft].map(b => parseFloat(b) || 0); - const hasBorder = borders.some(b => b > 0); - const hasUniformBorder = hasBorder && borders.every(b => b === borders[0]); - const borderLines = []; - - if (hasBorder && !hasUniformBorder) { - const rect = el.getBoundingClientRect(); - const x = pxToInch(rect.left); - const y = pxToInch(rect.top); - const w = pxToInch(rect.width); - const h = pxToInch(rect.height); - - // Collect lines to add after shape (inset by half the line width to center on edge) - if (parseFloat(borderTop) > 0) { - const widthPt = pxToPoints(borderTop); - const inset = (widthPt / 72) / 2; // Convert points to inches, then half - borderLines.push({ - type: 'line', - x1: x, y1: y + inset, x2: x + w, y2: y + inset, - width: widthPt, - color: rgbToHex(computed.borderTopColor) - }); - } - if (parseFloat(borderRight) > 0) { - const widthPt = pxToPoints(borderRight); - const inset = (widthPt / 72) / 2; - borderLines.push({ - type: 'line', - x1: x + w - inset, y1: y, x2: x + w - inset, y2: y + h, - width: widthPt, - color: rgbToHex(computed.borderRightColor) - }); - } - if (parseFloat(borderBottom) > 0) { - const widthPt = pxToPoints(borderBottom); - const inset = (widthPt / 72) / 2; - borderLines.push({ - type: 'line', - x1: x, y1: y + h - inset, x2: x + w, y2: y + h - inset, - width: widthPt, - color: rgbToHex(computed.borderBottomColor) - }); - } - if (parseFloat(borderLeft) > 0) { - const widthPt = pxToPoints(borderLeft); - const inset = (widthPt / 72) / 2; - borderLines.push({ - type: 'line', - x1: x + inset, y1: y, x2: x + inset, y2: y + h, - width: widthPt, - color: rgbToHex(computed.borderLeftColor) - }); - } - } - - if (hasBg || hasBorder) { - const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0) { - const shadow = parseBoxShadow(computed.boxShadow); - - // Only add shape if there's background or uniform border - if (hasBg || hasUniformBorder) { - elements.push({ - type: 'shape', - text: '', // Shape only - child text elements render on top - position: { - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - }, - shape: { - fill: hasBg ? rgbToHex(computed.backgroundColor) : null, - transparency: hasBg ? extractAlpha(computed.backgroundColor) : null, - line: hasUniformBorder ? { - color: rgbToHex(computed.borderColor), - width: pxToPoints(computed.borderWidth) - } : null, - // Convert border-radius to rectRadius (in inches) - // % values: 50%+ = circle (1), <50% = percentage of min dimension - // pt values: divide by 72 (72pt = 1 inch) - // px values: divide by 96 (96px = 1 inch) - rectRadius: (() => { - const radius = computed.borderRadius; - const radiusValue = parseFloat(radius); - if (radiusValue === 0) return 0; - - if (radius.includes('%')) { - if (radiusValue >= 50) return 1; - // Calculate percentage of smaller dimension - const minDim = Math.min(rect.width, rect.height); - return (radiusValue / 100) * pxToInch(minDim); - } - - if (radius.includes('pt')) return radiusValue / 72; - return radiusValue / PX_PER_IN; - })(), - shadow: shadow - } - }); - } - - // Add partial border lines - elements.push(...borderLines); - - processed.add(el); - return; - } - } - } - - // Extract bullet lists as single text block - if (el.tagName === 'UL' || el.tagName === 'OL') { - const rect = el.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return; - - const liElements = Array.from(el.querySelectorAll('li')); - const items = []; - const ulComputed = window.getComputedStyle(el); - const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft); - - // Split: margin-left for bullet position, indent for text position - // margin-left + indent = ul padding-left - const marginLeft = ulPaddingLeftPt * 0.5; - const textIndent = ulPaddingLeftPt * 0.5; - - liElements.forEach((li, idx) => { - const isLast = idx === liElements.length - 1; - const runs = parseInlineFormatting(li, { breakLine: false }); - // Clean manual bullets from first run - if (runs.length > 0) { - runs[0].text = runs[0].text.replace(/^[•\-\*▪▸]\s*/, ''); - runs[0].options.bullet = { indent: textIndent }; - } - // Set breakLine on last run - if (runs.length > 0 && !isLast) { - runs[runs.length - 1].options.breakLine = true; - } - items.push(...runs); - }); - - const computed = window.getComputedStyle(liElements[0] || el); - - elements.push({ - type: 'list', - items: items, - position: { - x: pxToInch(rect.left), - y: pxToInch(rect.top), - w: pxToInch(rect.width), - h: pxToInch(rect.height) - }, - style: { - fontSize: pxToPoints(computed.fontSize), - fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), - color: rgbToHex(computed.color), - transparency: extractAlpha(computed.color), - align: computed.textAlign === 'start' ? 'left' : computed.textAlign, - lineSpacing: computed.lineHeight && computed.lineHeight !== 'normal' ? pxToPoints(computed.lineHeight) : null, - paraSpaceBefore: 0, - paraSpaceAfter: pxToPoints(computed.marginBottom), - // PptxGenJS margin array is [left, right, bottom, top] - margin: [marginLeft, 0, 0, 0] - } - }); - - liElements.forEach(li => processed.add(li)); - processed.add(el); - return; - } - - // Extract text elements (P, H1, H2, etc.) - if (!textTags.includes(el.tagName)) return; - - const rect = el.getBoundingClientRect(); - const text = el.textContent.trim(); - if (rect.width === 0 || rect.height === 0 || !text) return; - - // Validate: Check for manual bullet symbols in text elements (not in lists) - if (el.tagName !== 'LI' && /^[•\-\*▪▸○●◆◇■□]\s/.test(text.trimStart())) { - errors.push( - `Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` + - 'Use
                                    or
                                      lists instead of manual bullet symbols.' - ); - return; - } - - const computed = window.getComputedStyle(el); - const rotation = getRotation(computed.transform, computed.writingMode); - const { x, y, w, h } = getPositionAndSize(el, rect, rotation); - - const baseStyle = { - fontSize: pxToPoints(computed.fontSize), - fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), - color: rgbToHex(computed.color), - align: computed.textAlign === 'start' ? 'left' : computed.textAlign, - lineSpacing: pxToPoints(computed.lineHeight), - paraSpaceBefore: pxToPoints(computed.marginTop), - paraSpaceAfter: pxToPoints(computed.marginBottom), - // PptxGenJS margin array is [left, right, bottom, top] (not [top, right, bottom, left] as documented) - margin: [ - pxToPoints(computed.paddingLeft), - pxToPoints(computed.paddingRight), - pxToPoints(computed.paddingBottom), - pxToPoints(computed.paddingTop) - ] - }; - - const transparency = extractAlpha(computed.color); - if (transparency !== null) baseStyle.transparency = transparency; - - if (rotation !== null) baseStyle.rotate = rotation; - - const hasFormatting = el.querySelector('b, i, u, strong, em, span, br'); - - if (hasFormatting) { - // Text with inline formatting - const transformStr = computed.textTransform; - const runs = parseInlineFormatting(el, {}, [], (str) => applyTextTransform(str, transformStr)); - - // Adjust lineSpacing based on largest fontSize in runs - const adjustedStyle = { ...baseStyle }; - if (adjustedStyle.lineSpacing) { - const maxFontSize = Math.max( - adjustedStyle.fontSize, - ...runs.map(r => r.options?.fontSize || 0) - ); - if (maxFontSize > adjustedStyle.fontSize) { - const lineHeightMultiplier = adjustedStyle.lineSpacing / adjustedStyle.fontSize; - adjustedStyle.lineSpacing = maxFontSize * lineHeightMultiplier; - } - } - - elements.push({ - type: el.tagName.toLowerCase(), - text: runs, - position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, - style: adjustedStyle - }); - } else { - // Plain text - inherit CSS formatting - const textTransform = computed.textTransform; - const transformedText = applyTextTransform(text, textTransform); - - const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; - - elements.push({ - type: el.tagName.toLowerCase(), - text: transformedText, - position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, - style: { - ...baseStyle, - bold: isBold && !shouldSkipBold(computed.fontFamily), - italic: computed.fontStyle === 'italic', - underline: computed.textDecoration.includes('underline') - } - }); - } - - processed.add(el); - }); - - return { background, elements, placeholders, errors }; - }); -} - -async function html2pptx(htmlFile, pres, options = {}) { - const { - tmpDir = process.env.TMPDIR || '/tmp', - slide = null - } = options; - - try { - // Use Chrome on macOS, default Chromium on Unix - const launchOptions = { env: { TMPDIR: tmpDir } }; - if (process.platform === 'darwin') { - launchOptions.channel = 'chrome'; - } - - const browser = await chromium.launch(launchOptions); - - let bodyDimensions; - let slideData; - - const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile); - const validationErrors = []; - - try { - const page = await browser.newPage(); - page.on('console', (msg) => { - // Log the message text to your test runner's console - console.log(`Browser console: ${msg.text()}`); - }); - - await page.goto(`file://${filePath}`); - - bodyDimensions = await getBodyDimensions(page); - - await page.setViewportSize({ - width: Math.round(bodyDimensions.width), - height: Math.round(bodyDimensions.height) - }); - - slideData = await extractSlideData(page); - } finally { - await browser.close(); - } - - // Collect all validation errors - if (bodyDimensions.errors && bodyDimensions.errors.length > 0) { - validationErrors.push(...bodyDimensions.errors); - } - - const dimensionErrors = validateDimensions(bodyDimensions, pres); - if (dimensionErrors.length > 0) { - validationErrors.push(...dimensionErrors); - } - - const textBoxPositionErrors = validateTextBoxPosition(slideData, bodyDimensions); - if (textBoxPositionErrors.length > 0) { - validationErrors.push(...textBoxPositionErrors); - } - - if (slideData.errors && slideData.errors.length > 0) { - validationErrors.push(...slideData.errors); - } - - // Throw all errors at once if any exist - if (validationErrors.length > 0) { - const errorMessage = validationErrors.length === 1 - ? validationErrors[0] - : `Multiple validation errors found:\n${validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`; - throw new Error(errorMessage); - } - - const targetSlide = slide || pres.addSlide(); - - await addBackground(slideData, targetSlide, tmpDir); - addElements(slideData, targetSlide, pres); - - return { slide: targetSlide, placeholders: slideData.placeholders }; - } catch (error) { - if (!error.message.startsWith(htmlFile)) { - throw new Error(`${htmlFile}: ${error.message}`); - } - throw error; - } -} - -module.exports = html2pptx; \ No newline at end of file diff --git a/.claude/skills/pptx-skill/scripts/package-lock.json b/.claude/skills/pptx-skill/scripts/package-lock.json deleted file mode 100755 index 44e44ff..0000000 --- a/.claude/skills/pptx-skill/scripts/package-lock.json +++ /dev/null @@ -1,761 +0,0 @@ -{ - "name": "scripts", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "playwright": "^1.58.2", - "pptxgenjs": "^4.0.1", - "sharp": "^0.34.5" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", - "license": "ISC" - }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/pptxgenjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", - "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", - "license": "MIT", - "dependencies": { - "@types/node": "^22.8.1", - "https": "^1.0.0", - "image-size": "^1.2.1", - "jszip": "^3.10.1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - } - } -} diff --git a/.claude/skills/pptx-skill/scripts/package.json b/.claude/skills/pptx-skill/scripts/package.json deleted file mode 100755 index 9f60b83..0000000 --- a/.claude/skills/pptx-skill/scripts/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "playwright": "^1.58.2", - "pptxgenjs": "^4.0.1", - "sharp": "^0.34.5" - } -} diff --git a/.claude/skills/pptx-skill/scripts/thumbnail.py b/.claude/skills/pptx-skill/scripts/thumbnail.py deleted file mode 100755 index 5c7fdf1..0000000 --- a/.claude/skills/pptx-skill/scripts/thumbnail.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env python3 -""" -Create thumbnail grids from PowerPoint presentation slides. - -Creates a grid layout of slide thumbnails with configurable columns (max 6). -Each grid contains up to cols×(cols+1) images. For presentations with more -slides, multiple numbered grid files are created automatically. - -The program outputs the names of all files created. - -Output: -- Single grid: {prefix}.jpg (if slides fit in one grid) -- Multiple grids: {prefix}-1.jpg, {prefix}-2.jpg, etc. - -Grid limits by column count: -- 3 cols: max 12 slides per grid (3×4) -- 4 cols: max 20 slides per grid (4×5) -- 5 cols: max 30 slides per grid (5×6) [default] -- 6 cols: max 42 slides per grid (6×7) - -Usage: - python thumbnail.py input.pptx [output_prefix] [--cols N] [--outline-placeholders] - -Examples: - python thumbnail.py presentation.pptx - # Creates: thumbnails.jpg (using default prefix) - # Outputs: - # Created 1 grid(s): - # - thumbnails.jpg - - python thumbnail.py large-deck.pptx grid --cols 4 - # Creates: grid-1.jpg, grid-2.jpg, grid-3.jpg - # Outputs: - # Created 3 grid(s): - # - grid-1.jpg - # - grid-2.jpg - # - grid-3.jpg - - python thumbnail.py template.pptx analysis --outline-placeholders - # Creates thumbnail grids with red outlines around text placeholders -""" - -import argparse -import subprocess -import sys -import tempfile -from pathlib import Path - -from inventory import extract_text_inventory -from PIL import Image, ImageDraw, ImageFont -from pptx import Presentation - -# Constants -THUMBNAIL_WIDTH = 300 # Fixed thumbnail width in pixels -CONVERSION_DPI = 100 # DPI for PDF to image conversion -MAX_COLS = 6 # Maximum number of columns -DEFAULT_COLS = 5 # Default number of columns -JPEG_QUALITY = 95 # JPEG compression quality - -# Grid layout constants -GRID_PADDING = 20 # Padding between thumbnails -BORDER_WIDTH = 2 # Border width around thumbnails -FONT_SIZE_RATIO = 0.12 # Font size as fraction of thumbnail width -LABEL_PADDING_RATIO = 0.4 # Label padding as fraction of font size - - -def main(): - parser = argparse.ArgumentParser( - description="Create thumbnail grids from PowerPoint slides." - ) - parser.add_argument("input", help="Input PowerPoint file (.pptx)") - parser.add_argument( - "output_prefix", - nargs="?", - default="thumbnails", - help="Output prefix for image files (default: thumbnails, will create prefix.jpg or prefix-N.jpg)", - ) - parser.add_argument( - "--cols", - type=int, - default=DEFAULT_COLS, - help=f"Number of columns (default: {DEFAULT_COLS}, max: {MAX_COLS})", - ) - parser.add_argument( - "--outline-placeholders", - action="store_true", - help="Outline text placeholders with a colored border", - ) - - args = parser.parse_args() - - # Validate columns - cols = min(args.cols, MAX_COLS) - if args.cols > MAX_COLS: - print(f"Warning: Columns limited to {MAX_COLS} (requested {args.cols})") - - # Validate input - input_path = Path(args.input) - if not input_path.exists() or input_path.suffix.lower() != ".pptx": - print(f"Error: Invalid PowerPoint file: {args.input}") - sys.exit(1) - - # Construct output path (always JPG) - output_path = Path(f"{args.output_prefix}.jpg") - - print(f"Processing: {args.input}") - - try: - with tempfile.TemporaryDirectory() as temp_dir: - # Get placeholder regions if outlining is enabled - placeholder_regions = None - slide_dimensions = None - if args.outline_placeholders: - print("Extracting placeholder regions...") - placeholder_regions, slide_dimensions = get_placeholder_regions( - input_path - ) - if placeholder_regions: - print(f"Found placeholders on {len(placeholder_regions)} slides") - - # Convert slides to images - slide_images = convert_to_images(input_path, Path(temp_dir), CONVERSION_DPI) - if not slide_images: - print("Error: No slides found") - sys.exit(1) - - print(f"Found {len(slide_images)} slides") - - # Create grids (max cols×(cols+1) images per grid) - grid_files = create_grids( - slide_images, - cols, - THUMBNAIL_WIDTH, - output_path, - placeholder_regions, - slide_dimensions, - ) - - # Print saved files - print(f"Created {len(grid_files)} grid(s):") - for grid_file in grid_files: - print(f" - {grid_file}") - - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - - -def create_hidden_slide_placeholder(size): - """Create placeholder image for hidden slides.""" - img = Image.new("RGB", size, color="#F0F0F0") - draw = ImageDraw.Draw(img) - line_width = max(5, min(size) // 100) - draw.line([(0, 0), size], fill="#CCCCCC", width=line_width) - draw.line([(size[0], 0), (0, size[1])], fill="#CCCCCC", width=line_width) - return img - - -def get_placeholder_regions(pptx_path): - """Extract ALL text regions from the presentation. - - Returns a tuple of (placeholder_regions, slide_dimensions). - text_regions is a dict mapping slide indices to lists of text regions. - Each region is a dict with 'left', 'top', 'width', 'height' in inches. - slide_dimensions is a tuple of (width_inches, height_inches). - """ - prs = Presentation(str(pptx_path)) - inventory = extract_text_inventory(pptx_path, prs) - placeholder_regions = {} - - # Get actual slide dimensions in inches (EMU to inches conversion) - slide_width_inches = (prs.slide_width or 9144000) / 914400.0 - slide_height_inches = (prs.slide_height or 5143500) / 914400.0 - - for slide_key, shapes in inventory.items(): - # Extract slide index from "slide-N" format - slide_idx = int(slide_key.split("-")[1]) - regions = [] - - for shape_key, shape_data in shapes.items(): - # The inventory only contains shapes with text, so all shapes should be highlighted - regions.append( - { - "left": shape_data.left, - "top": shape_data.top, - "width": shape_data.width, - "height": shape_data.height, - } - ) - - if regions: - placeholder_regions[slide_idx] = regions - - return placeholder_regions, (slide_width_inches, slide_height_inches) - - -def convert_to_images(pptx_path, temp_dir, dpi): - """Convert PowerPoint to images via PDF, handling hidden slides.""" - # Detect hidden slides - print("Analyzing presentation...") - prs = Presentation(str(pptx_path)) - total_slides = len(prs.slides) - - # Find hidden slides (1-based indexing for display) - hidden_slides = { - idx + 1 - for idx, slide in enumerate(prs.slides) - if slide.element.get("show") == "0" - } - - print(f"Total slides: {total_slides}") - if hidden_slides: - print(f"Hidden slides: {sorted(hidden_slides)}") - - pdf_path = temp_dir / f"{pptx_path.stem}.pdf" - - # Convert to PDF - print("Converting to PDF...") - result = subprocess.run( - [ - "soffice", - "--headless", - "--convert-to", - "pdf", - "--outdir", - str(temp_dir), - str(pptx_path), - ], - capture_output=True, - text=True, - ) - if result.returncode != 0 or not pdf_path.exists(): - raise RuntimeError("PDF conversion failed") - - # Convert PDF to images - print(f"Converting to images at {dpi} DPI...") - result = subprocess.run( - ["pdftoppm", "-jpeg", "-r", str(dpi), str(pdf_path), str(temp_dir / "slide")], - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise RuntimeError("Image conversion failed") - - visible_images = sorted(temp_dir.glob("slide-*.jpg")) - - # Create full list with placeholders for hidden slides - all_images = [] - visible_idx = 0 - - # Get placeholder dimensions from first visible slide - if visible_images: - with Image.open(visible_images[0]) as img: - placeholder_size = img.size - else: - placeholder_size = (1920, 1080) - - for slide_num in range(1, total_slides + 1): - if slide_num in hidden_slides: - # Create placeholder image for hidden slide - placeholder_path = temp_dir / f"hidden-{slide_num:03d}.jpg" - placeholder_img = create_hidden_slide_placeholder(placeholder_size) - placeholder_img.save(placeholder_path, "JPEG") - all_images.append(placeholder_path) - else: - # Use the actual visible slide image - if visible_idx < len(visible_images): - all_images.append(visible_images[visible_idx]) - visible_idx += 1 - - return all_images - - -def create_grids( - image_paths, - cols, - width, - output_path, - placeholder_regions=None, - slide_dimensions=None, -): - """Create multiple thumbnail grids from slide images, max cols×(cols+1) images per grid.""" - # Maximum images per grid is cols × (cols + 1) for better proportions - max_images_per_grid = cols * (cols + 1) - grid_files = [] - - print( - f"Creating grids with {cols} columns (max {max_images_per_grid} images per grid)" - ) - - # Split images into chunks - for chunk_idx, start_idx in enumerate( - range(0, len(image_paths), max_images_per_grid) - ): - end_idx = min(start_idx + max_images_per_grid, len(image_paths)) - chunk_images = image_paths[start_idx:end_idx] - - # Create grid for this chunk - grid = create_grid( - chunk_images, cols, width, start_idx, placeholder_regions, slide_dimensions - ) - - # Generate output filename - if len(image_paths) <= max_images_per_grid: - # Single grid - use base filename without suffix - grid_filename = output_path - else: - # Multiple grids - insert index before extension with dash - stem = output_path.stem - suffix = output_path.suffix - grid_filename = output_path.parent / f"{stem}-{chunk_idx + 1}{suffix}" - - # Save grid - grid_filename.parent.mkdir(parents=True, exist_ok=True) - grid.save(str(grid_filename), quality=JPEG_QUALITY) - grid_files.append(str(grid_filename)) - - return grid_files - - -def create_grid( - image_paths, - cols, - width, - start_slide_num=0, - placeholder_regions=None, - slide_dimensions=None, -): - """Create thumbnail grid from slide images with optional placeholder outlining.""" - font_size = int(width * FONT_SIZE_RATIO) - label_padding = int(font_size * LABEL_PADDING_RATIO) - - # Get dimensions - with Image.open(image_paths[0]) as img: - aspect = img.height / img.width - height = int(width * aspect) - - # Calculate grid size - rows = (len(image_paths) + cols - 1) // cols - grid_w = cols * width + (cols + 1) * GRID_PADDING - grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING - - # Create grid - grid = Image.new("RGB", (grid_w, grid_h), "white") - draw = ImageDraw.Draw(grid) - - # Load font with size based on thumbnail width - try: - # Use Pillow's default font with size - font = ImageFont.load_default(size=font_size) - except Exception: - # Fall back to basic default font if size parameter not supported - font = ImageFont.load_default() - - # Place thumbnails - for i, img_path in enumerate(image_paths): - row, col = i // cols, i % cols - x = col * width + (col + 1) * GRID_PADDING - y_base = ( - row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING - ) - - # Add label with actual slide number - label = f"{start_slide_num + i}" - bbox = draw.textbbox((0, 0), label, font=font) - text_w = bbox[2] - bbox[0] - draw.text( - (x + (width - text_w) // 2, y_base + label_padding), - label, - fill="black", - font=font, - ) - - # Add thumbnail below label with proportional spacing - y_thumbnail = y_base + label_padding + font_size + label_padding - - with Image.open(img_path) as img: - # Get original dimensions before thumbnail - orig_w, orig_h = img.size - - # Apply placeholder outlines if enabled - if placeholder_regions and (start_slide_num + i) in placeholder_regions: - # Convert to RGBA for transparency support - if img.mode != "RGBA": - img = img.convert("RGBA") - - # Get the regions for this slide - regions = placeholder_regions[start_slide_num + i] - - # Calculate scale factors using actual slide dimensions - if slide_dimensions: - slide_width_inches, slide_height_inches = slide_dimensions - else: - # Fallback: estimate from image size at CONVERSION_DPI - slide_width_inches = orig_w / CONVERSION_DPI - slide_height_inches = orig_h / CONVERSION_DPI - - x_scale = orig_w / slide_width_inches - y_scale = orig_h / slide_height_inches - - # Create a highlight overlay - overlay = Image.new("RGBA", img.size, (255, 255, 255, 0)) - overlay_draw = ImageDraw.Draw(overlay) - - # Highlight each placeholder region - for region in regions: - # Convert from inches to pixels in the original image - px_left = int(region["left"] * x_scale) - px_top = int(region["top"] * y_scale) - px_width = int(region["width"] * x_scale) - px_height = int(region["height"] * y_scale) - - # Draw highlight outline with red color and thick stroke - # Using a bright red outline instead of fill - stroke_width = max( - 5, min(orig_w, orig_h) // 150 - ) # Thicker proportional stroke width - overlay_draw.rectangle( - [(px_left, px_top), (px_left + px_width, px_top + px_height)], - outline=(255, 0, 0, 255), # Bright red, fully opaque - width=stroke_width, - ) - - # Composite the overlay onto the image using alpha blending - img = Image.alpha_composite(img, overlay) - # Convert back to RGB for JPEG saving - img = img.convert("RGB") - - img.thumbnail((width, height), Image.Resampling.LANCZOS) - w, h = img.size - tx = x + (width - w) // 2 - ty = y_thumbnail + (height - h) // 2 - grid.paste(img, (tx, ty)) - - # Add border - if BORDER_WIDTH > 0: - draw.rectangle( - [ - (tx - BORDER_WIDTH, ty - BORDER_WIDTH), - (tx + w + BORDER_WIDTH - 1, ty + h + BORDER_WIDTH - 1), - ], - outline="gray", - width=BORDER_WIDTH, - ) - - return grid - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/proposal-skill/SKILL.md b/.claude/skills/proposal-skill/SKILL.md deleted file mode 100755 index 23db019..0000000 --- a/.claude/skills/proposal-skill/SKILL.md +++ /dev/null @@ -1,301 +0,0 @@ ---- -name: proposal-skill -description: PDF 기획서 분석 및 PPT 기획서 생성을 위한 고급 스킬. 템플릿 추출, 구조 매핑, 자동 콘텐츠 생성 기능 제공. ---- - -# Proposal Skill - 기획서 생성 스킬 - -PDF 샘플을 분석하여 동일한 형태의 PPT 기획서를 자동 생성하는 전문 스킬입니다. - -## 기능 개요 - -### 1. PDF 구조 분석 (PDF Structure Analysis) -PDF 기획서의 레이아웃과 콘텐츠 패턴을 자동으로 분석 - -### 2. 템플릿 추출 (Template Extraction) -분석 결과를 바탕으로 재사용 가능한 템플릿 생성 - -### 3. 콘텐츠 매핑 (Content Mapping) -사용자 요구사항을 기획서 구조에 자동 매핑 - -### 4. PPT 자동 생성 (Auto PPT Generation) -완성된 기획서를 PowerPoint 파일로 출력 - -## 핵심 워크플로우 - -### PDF → 템플릿 추출 워크플로우 - -```mermaid -graph TD - A[PDF 샘플] --> B[페이지별 구조 분석] - B --> C[섹션 패턴 인식] - C --> D[레이아웃 템플릿 추출] - D --> E[재사용 템플릿 저장] -``` - -### 기획서 생성 워크플로우 - -```mermaid -graph TD - A[사용자 요구사항] --> B[템플릿 매칭] - B --> C[콘텐츠 자동 매핑] - C --> D[구조화된 문서 생성] - D --> E[PPT 슬라이드 변환] - E --> F[완성된 기획서] -``` - -## 스크립트 구조 - -### 1. pdf-analyzer.js -PDF 파일 구조 분석 및 템플릿 추출 - -```javascript -import { PDFAnalyzer } from './pdf-analyzer.js'; - -// PDF 구조 분석 -const analyzer = new PDFAnalyzer(); -const structure = await analyzer.analyze('pdf_sample/SAM_ERP_Storyboard.pdf'); - -// 템플릿 추출 -const template = await analyzer.extractTemplate(structure); -await analyzer.saveTemplate(template, 'templates/erp_storyboard.json'); -``` - -### 2. proposal-generator.js -기획서 자동 생성 엔진 - -```javascript -import { ProposalGenerator } from './proposal-generator.js'; - -const generator = new ProposalGenerator(); - -// 템플릿 기반 기획서 생성 -const proposal = await generator.create({ - template: 'templates/erp_storyboard.json', - project: { - name: '모바일 앱 개발', - type: 'mobile_app', - requirements: requirements_data - } -}); - -// 구조화된 문서 출력 -await generator.export(proposal, 'output/mobile_app_proposal.md'); -``` - -### 3. ppt-converter.js -Markdown 기획서를 PPT로 변환 - -```javascript -import { PPTConverter } from './ppt-converter.js'; - -const converter = new PPTConverter(); - -// 기획서를 PPT로 변환 -await converter.convertProposal({ - input: 'output/mobile_app_proposal.md', - template: 'templates/proposal_template.pptx', - output: 'final/mobile_app_proposal.pptx' -}); -``` - -## 템플릿 구조 정의 - -### template-schema.json -```json -{ - "template_info": { - "name": "ERP Storyboard Template", - "version": "1.0", - "source": "SAM_ERP_Storyboard.pdf", - "created": "2025-01-03" - }, - "page_templates": [ - { - "type": "cover", - "layout": "center_aligned", - "elements": { - "title": { "position": "center_top", "style": "heading1" }, - "subtitle": { "position": "center_middle", "style": "heading2" }, - "date": { "position": "bottom_right", "style": "caption" }, - "logo": { "position": "bottom_left", "style": "image" } - } - }, - { - "type": "document_history", - "layout": "table_centered", - "elements": { - "table": { - "columns": ["날짜", "버전", "주요 내용", "상세 내용", "비고"], - "style": "bordered_table" - } - } - }, - { - "type": "system_structure", - "layout": "diagram_centered", - "elements": { - "diagram": { "type": "hierarchical_tree", "style": "flowchart" }, - "description": { "position": "bottom", "style": "body_text" } - } - }, - { - "type": "detail_screen", - "layout": "two_column", - "elements": { - "wireframe": { "position": "left_column", "style": "image_frame" }, - "description": { "position": "right_column", "style": "numbered_list" }, - "header_info": { "position": "top_table", "style": "info_table" } - } - } - ] -} -``` - -## 자동 콘텐츠 생성 규칙 - -### 1. 표지 생성 -```javascript -function generateCover(projectInfo) { - return { - title: projectInfo.name, - subtitle: `${projectInfo.type} 프로젝트 기획서`, - date: new Date().toISOString().split('T')[0].replace(/-/g, '.'), - version: "D1.0", - company: projectInfo.company || "Company Name" - }; -} -``` - -### 2. 문서 히스토리 생성 -```javascript -function generateHistory(projectInfo) { - const today = new Date().toISOString().split('T')[0].replace(/-/g, '.'); - return [{ - date: today, - version: "D1.0", - main_content: "초안 작성", - detailed_content: `${projectInfo.name} 프로젝트 초안 작성`, - mark: "" - }]; -} -``` - -### 3. 시스템 구조 생성 -```javascript -function generateSystemStructure(requirements) { - // 요구사항에서 메뉴 구조 자동 추출 - const menuStructure = extractMenuFromRequirements(requirements); - return { - type: "hierarchical_diagram", - nodes: menuStructure, - connections: generateConnections(menuStructure) - }; -} -``` - -### 4. 상세 화면 생성 -```javascript -function generateDetailScreens(screens) { - return screens.map((screen, index) => ({ - page_number: index + 5, // 기본 페이지 이후 시작 - screen_info: { - task_name: screen.module, - version: "D1.0", - route: screen.path, - screen_name: screen.name, - screen_id: screen.id - }, - wireframe: generateWireframeDescription(screen), - descriptions: generateFeatureDescriptions(screen.features) - })); -} -``` - -## 사용법 - -### 1. PDF 템플릿 추출 -```bash -node .claude/skills/proposal-skill/scripts/pdf-analyzer.js \ - --input pdf_sample/SAM_ERP_Storyboard.pdf \ - --output templates/extracted_template.json -``` - -### 2. 새 기획서 생성 -```bash -node .claude/skills/proposal-skill/scripts/proposal-generator.js \ - --template templates/extracted_template.json \ - --project project_config.json \ - --output output/new_proposal.md -``` - -### 3. PPT 변환 -```bash -node .claude/skills/proposal-skill/scripts/ppt-converter.js \ - --input output/new_proposal.md \ - --output final/proposal.pptx -``` - -### 4. 통합 실행 -```bash -npm run create-proposal -- \ - --template pdf_sample/SAM_ERP_Storyboard.pdf \ - --project "모바일 앱 개발" \ - --requirements requirements.json \ - --output final/mobile_app_proposal.pptx -``` - -## 고급 기능 - -### 1. 템플릿 자동 매칭 -사용자 프로젝트 타입에 따라 최적 템플릿 자동 선택 - -```javascript -const templateMatcher = { - 'mobile_app': 'templates/mobile_template.json', - 'web_service': 'templates/web_template.json', - 'erp_system': 'templates/erp_template.json', - 'ecommerce': 'templates/ecommerce_template.json' -}; -``` - -### 2. 인터랙티브 콘텐츠 생성 -사용자와의 대화를 통한 단계적 기획서 완성 - -### 3. 버전 관리 시스템 -기획서 변경 이력 자동 추적 및 관리 - -### 4. 협업 기능 -여러 작성자 간 동시 편집 지원 - -## 품질 보증 - -### 자동 검증 기능 -- 필수 섹션 누락 검사 -- 페이지 번호 연속성 확인 -- 참조 무결성 검증 -- 템플릿 준수성 검사 - -### 콘텐츠 품질 기준 -- **완성도**: 모든 필수 정보 포함 -- **일관성**: 용어 및 스타일 통일 -- **정확성**: 요구사항과 결과물 일치 -- **실용성**: 실제 개발 가능한 수준 - -## 확장성 - -### 새로운 템플릿 추가 -1. PDF 샘플을 `pdf_sample/` 디렉토리에 추가 -2. `pdf-analyzer.js`로 구조 분석 및 템플릿 추출 -3. 추출된 템플릿을 `templates/` 디렉토리에 저장 -4. `template-registry.json`에 새 템플릿 등록 - -### 커스텀 생성 규칙 -프로젝트별 특수 요구사항에 맞는 생성 규칙 추가 가능 - -## 연동 가능한 도구 - -- **Research Agent**: 시장 조사 데이터 자동 반영 -- **Organizer Agent**: 콘텐츠 구조화 및 PPT 변환 -- **PPTX Skill**: PowerPoint 파일 최종 생성 -- **Design Skill**: 브랜드 가이드라인 적용 \ No newline at end of file diff --git a/.claude/skills/proposal-skill/scripts/pdf-analyzer.js b/.claude/skills/proposal-skill/scripts/pdf-analyzer.js deleted file mode 100755 index c83a0ea..0000000 --- a/.claude/skills/proposal-skill/scripts/pdf-analyzer.js +++ /dev/null @@ -1,393 +0,0 @@ -/** - * PDF 기획서 구조 분석 및 템플릿 추출 스크립트 - * SAM_ERP_Storyboard.pdf를 분석하여 재사용 가능한 템플릿 생성 - */ - -const fs = require('fs').promises; -const path = require('path'); - -class PDFAnalyzer { - constructor() { - this.pagePatterns = new Map(); - this.extractedTemplate = {}; - } - - /** - * PDF 파일 구조 분석 - */ - async analyze(pdfPath) { - console.log(`📊 PDF 구조 분석 시작: ${pdfPath}`); - - // SAM_ERP 스토리보드 구조 패턴 정의 (PDF 분석 결과 기반) - const structure = { - metadata: { - source: path.basename(pdfPath), - total_pages: 17, // 확인된 페이지 수 - analysis_date: new Date().toISOString(), - document_type: "storyboard_specification" - }, - - sections: [ - { - type: "cover", - pages: [1], - pattern: { - layout: "brand_centered", - elements: ["project_title", "company_name", "date"], - color_scheme: "green_brand" - } - }, - { - type: "document_history", - pages: [2], - pattern: { - layout: "table_full_width", - elements: ["version_table"], - table_columns: ["Date", "Version", "Main Contents", "Detailed Contents", "MARK"] - } - }, - { - type: "menu_structure", - pages: [3], - pattern: { - layout: "hierarchical_diagram", - elements: ["system_tree", "navigation_flow"], - diagram_type: "organizational_chart" - } - }, - { - type: "common_guidelines", - pages: [4, 5, 6, 7, 8], - pattern: { - layout: "documentation_style", - elements: ["section_header", "content_blocks", "code_examples"], - subsections: ["interaction", "responsive", "template", "notifications"] - } - }, - { - type: "detail_screens", - pages: [9, 10, 11, 12, 13, 14, 15, 16, 17], - pattern: { - layout: "wireframe_with_description", - elements: ["header_table", "wireframe_image", "numbered_descriptions"], - header_structure: { - task_name: "단위업무명", - version: "버전", - page_number: "Page Number", - route: "경로", - screen_name: "화면명", - screen_id: "화면 ID" - } - } - } - ] - }; - - console.log(`✅ 구조 분석 완료: ${structure.sections.length}개 섹션 식별`); - return structure; - } - - /** - * 분석 결과에서 재사용 가능한 템플릿 추출 - */ - async extractTemplate(structure) { - console.log("🎨 템플릿 추출 중..."); - - const template = { - template_info: { - name: "ERP Storyboard Template", - version: "1.0", - source: structure.metadata.source, - created: structure.metadata.analysis_date, - description: "ERP 시스템 스토리보드 기획서 템플릿" - }, - - // 페이지 템플릿 정의 - page_templates: [ - // 표지 템플릿 - { - type: "cover", - name: "브랜드 표지", - layout: { - type: "center_aligned", - background: "brand_gradient", - color_scheme: { - primary: "#8BC34A", - secondary: "#FFFFFF", - accent: "#2E7D32" - } - }, - elements: { - project_title: { - position: { x: "center", y: "30%" }, - style: "heading1_bold", - font_size: 48, - color: "white" - }, - company_name: { - position: { x: "right", y: "bottom" }, - style: "corporate", - font_size: 16, - color: "dark_gray" - }, - date: { - position: { x: "right", y: "bottom-20" }, - style: "caption", - font_size: 12, - color: "medium_gray" - }, - brand_logo: { - position: { x: "left", y: "middle" }, - size: { width: 200, height: 100 }, - type: "image_placeholder" - } - } - }, - - // 문서 히스토리 템플릿 - { - type: "document_history", - name: "문서 이력 관리", - layout: { - type: "table_centered", - padding: 40 - }, - elements: { - section_title: { - position: { x: "left", y: "top" }, - content: "Document History", - style: "section_heading" - }, - history_table: { - position: { x: "center", y: "middle" }, - table_config: { - columns: [ - { name: "Date", width: "15%" }, - { name: "Version", width: "10%" }, - { name: "Main Contents", width: "20%" }, - { name: "Detailed Contents", width: "45%" }, - { name: "MARK", width: "10%" } - ], - style: "bordered_table", - header_color: "#8BC34A", - alternate_rows: true - } - } - } - }, - - // 메뉴 구조 템플릿 - { - type: "menu_structure", - name: "시스템 구조도", - layout: { - type: "diagram_centered", - padding: 30 - }, - elements: { - section_title: { - position: { x: "left", y: "top" }, - content: "Menu Structure", - style: "section_heading" - }, - system_diagram: { - position: { x: "center", y: "middle" }, - diagram_config: { - type: "hierarchical_tree", - root_node_style: "rounded_rectangle", - connection_style: "straight_lines", - node_colors: { - level_1: "#8BC34A", - level_2: "#C8E6C9", - level_3: "#E8F5E8" - } - } - } - } - }, - - // 공통 가이드라인 템플릿 - { - type: "common_guidelines", - name: "공통 가이드라인", - layout: { - type: "documentation_style", - padding: 35 - }, - elements: { - section_header: { - position: { x: "left", y: "top" }, - style: "section_divider", - background_color: "#8BC34A" - }, - content_blocks: { - position: { x: "full_width", y: "main_area" }, - style: "structured_content", - spacing: 20 - }, - code_examples: { - style: "code_block", - background_color: "#F5F5F5", - border_color: "#E0E0E0" - } - } - }, - - // 상세 화면 템플릿 - { - type: "detail_screen", - name: "상세 화면 설계", - layout: { - type: "wireframe_with_description", - padding: 25 - }, - elements: { - header_table: { - position: { x: "top", y: "header" }, - table_config: { - columns: [ - { name: "단위업무명", width: "20%" }, - { name: "버전", width: "15%" }, - { name: "Page Number", width: "15%" }, - { name: "경로", width: "25%" }, - { name: "화면명", width: "15%" }, - { name: "화면 ID", width: "10%" } - ], - style: "info_table_header" - } - }, - wireframe_area: { - position: { x: "left", y: "main", width: "60%" }, - style: "wireframe_container", - border: "solid_gray", - background: "light_gray" - }, - description_area: { - position: { x: "right", y: "main", width: "35%" }, - style: "numbered_descriptions", - list_style: "numbered_with_icons" - } - } - } - ], - - // 콘텐츠 생성 규칙 - content_rules: { - auto_numbering: { - pages: true, - descriptions: true, - sections: false - }, - naming_conventions: { - screen_ids: "snake_case", - route_format: "path/screen_name", - version_format: "D1.x" - }, - required_sections: [ - "cover", - "document_history", - "menu_structure", - "common_guidelines" - ] - }, - - // 디자인 시스템 - design_system: { - colors: { - primary: "#8BC34A", - secondary: "#4CAF50", - accent: "#2E7D32", - text_primary: "#212121", - text_secondary: "#757575", - background: "#FFFFFF", - surface: "#F5F5F5", - border: "#E0E0E0" - }, - typography: { - heading1: { font: "Noto Sans", size: 32, weight: "bold" }, - heading2: { font: "Noto Sans", size: 24, weight: "bold" }, - heading3: { font: "Noto Sans", size: 18, weight: "medium" }, - body: { font: "Noto Sans", size: 14, weight: "regular" }, - caption: { font: "Noto Sans", size: 12, weight: "regular" } - }, - spacing: { - page_margin: 40, - section_gap: 30, - element_gap: 15, - line_height: 1.5 - } - } - }; - - console.log("✅ 템플릿 추출 완료"); - return template; - } - - /** - * 추출된 템플릿을 JSON 파일로 저장 - */ - async saveTemplate(template, outputPath) { - try { - const templateDir = path.dirname(outputPath); - await fs.mkdir(templateDir, { recursive: true }); - - await fs.writeFile( - outputPath, - JSON.stringify(template, null, 2), - 'utf8' - ); - - console.log(`💾 템플릿 저장 완료: ${outputPath}`); - return outputPath; - } catch (error) { - console.error("❌ 템플릿 저장 실패:", error); - throw error; - } - } - - /** - * 기본 ERP 템플릿 생성 (샘플 PDF 기반) - */ - async createDefaultTemplate() { - const structure = await this.analyze('pdf_sample/SAM_ERP_Storyboard.pdf'); - const template = await this.extractTemplate(structure); - const outputPath = '.claude/skills/proposal-skill/templates/erp_storyboard_template.json'; - - await this.saveTemplate(template, outputPath); - return outputPath; - } -} - -// 명령줄 실행 지원 -async function main() { - const args = process.argv.slice(2); - const analyzer = new PDFAnalyzer(); - - if (args.includes('--create-default')) { - console.log("🚀 기본 ERP 템플릿 생성 중..."); - const templatePath = await analyzer.createDefaultTemplate(); - console.log(`✅ 완료: ${templatePath}`); - } else if (args.includes('--help')) { - console.log(` -📊 PDF 분석기 사용법: - -기본 템플릿 생성: - node pdf-analyzer.js --create-default - -커스텀 PDF 분석: - node pdf-analyzer.js --input path/to/pdf --output path/to/template.json - -도움말: - node pdf-analyzer.js --help - `); - } else { - // 기본 실행 - await analyzer.createDefaultTemplate(); - } -} - -if (require.main === module) { - main().catch(console.error); -} - -module.exports = { PDFAnalyzer }; \ No newline at end of file diff --git a/.claude/skills/proposal-skill/scripts/proposal-generator.js b/.claude/skills/proposal-skill/scripts/proposal-generator.js deleted file mode 100755 index c2f2d66..0000000 --- a/.claude/skills/proposal-skill/scripts/proposal-generator.js +++ /dev/null @@ -1,829 +0,0 @@ -/** - * 기획서 자동 생성 엔진 - * 템플릿과 요구사항을 바탕으로 완성된 기획서 생성 - */ - -const fs = require('fs').promises; -const path = require('path'); - -class ProposalGenerator { - constructor() { - this.template = null; - this.projectInfo = null; - } - - /** - * 템플릿 로드 - */ - async loadTemplate(templatePath) { - try { - const templateContent = await fs.readFile(templatePath, 'utf8'); - this.template = JSON.parse(templateContent); - console.log(`📋 템플릿 로드: ${this.template.template_info.name}`); - return this.template; - } catch (error) { - console.error("❌ 템플릿 로드 실패:", error); - throw error; - } - } - - /** - * 프로젝트 정보 설정 - */ - setProjectInfo(projectInfo) { - this.projectInfo = { - name: projectInfo.name || "새 프로젝트", - type: projectInfo.type || "web_service", - company: projectInfo.company || "Company Name", - author: projectInfo.author || "작성자", - description: projectInfo.description || "", - requirements: projectInfo.requirements || [], - features: projectInfo.features || [], - target_users: projectInfo.target_users || [], - ...projectInfo - }; - console.log(`🎯 프로젝트 설정: ${this.projectInfo.name}`); - } - - /** - * 기획서 생성 - */ - async generate() { - if (!this.template || !this.projectInfo) { - throw new Error("템플릿과 프로젝트 정보가 필요합니다."); - } - - console.log("📝 기획서 생성 중..."); - - const proposal = { - metadata: this.generateMetadata(), - cover: this.generateCover(), - document_history: this.generateDocumentHistory(), - menu_structure: this.generateMenuStructure(), - common_guidelines: this.generateCommonGuidelines(), - detail_screens: this.generateDetailScreens() - }; - - return proposal; - } - - /** - * 메타데이터 생성 - */ - generateMetadata() { - return { - title: this.projectInfo.name, - subtitle: `${this.projectInfo.type} 프로젝트 기획서`, - version: "D1.0", - created_date: new Date().toISOString().split('T')[0].replace(/-/g, '.'), - author: this.projectInfo.author, - company: this.projectInfo.company, - total_pages: this.estimatePageCount(), - template_used: this.template.template_info.name - }; - } - - /** - * 표지 생성 - */ - generateCover() { - return { - type: "cover", - title: this.projectInfo.name, - subtitle: this.generateSubtitle(), - date: new Date().toISOString().split('T')[0].replace(/-/g, '.'), - version: "D1.0", - company: this.projectInfo.company, - author: this.projectInfo.author, - description: this.projectInfo.description - }; - } - - /** - * 부제목 자동 생성 - */ - generateSubtitle() { - const typeMapping = { - 'mobile_app': '모바일 애플리케이션', - 'web_service': '웹 서비스', - 'erp_system': 'ERP 시스템', - 'ecommerce': '이커머스 플랫폼', - 'cms': '콘텐츠 관리 시스템', - 'crm': '고객관계관리 시스템' - }; - - return `${typeMapping[this.projectInfo.type] || '시스템'} 기획서`; - } - - /** - * 문서 히스토리 생성 - */ - generateDocumentHistory() { - const today = new Date().toISOString().split('T')[0].replace(/-/g, '.'); - - return { - type: "document_history", - table: [ - { - date: today, - version: "D1.0", - main_content: "초안 작성", - detailed_content: `${this.projectInfo.name} 프로젝트 기획서 초안 작성`, - mark: "" - } - ] - }; - } - - /** - * 메뉴 구조 생성 - */ - generateMenuStructure() { - let menuStructure; - - if (this.projectInfo.requirements && this.projectInfo.requirements.length > 0) { - // 요구사항에서 메뉴 구조 추출 - menuStructure = this.extractMenuFromRequirements(); - } else { - // 기본 구조 사용 - menuStructure = this.getDefaultMenuStructure(); - } - - return { - type: "menu_structure", - title: "Menu Structure", - structure: menuStructure - }; - } - - /** - * 요구사항에서 메뉴 구조 추출 - */ - extractMenuFromRequirements() { - const requirements = this.projectInfo.requirements; - const menuMap = new Map(); - - // 요구사항을 카테고리별로 분류 - requirements.forEach(req => { - const category = req.category || this.guessCategory(req.title); - if (!menuMap.has(category)) { - menuMap.set(category, []); - } - menuMap.get(category).push({ - name: req.title, - description: req.description, - priority: req.priority || 'medium' - }); - }); - - // 계층 구조로 변환 - const structure = { - root: this.projectInfo.name, - children: [] - }; - - for (const [category, items] of menuMap) { - structure.children.push({ - name: category, - children: items.map(item => ({ - name: item.name, - leaf: true, - description: item.description - })) - }); - } - - return structure; - } - - /** - * 카테고리 추측 - */ - guessCategory(title) { - const categoryKeywords = { - '사용자관리': ['사용자', '회원', '계정', '인증', '로그인'], - '콘텐츠관리': ['게시글', '콘텐츠', '글', '포스트', '댓글'], - '결제관리': ['결제', '구매', '주문', '결제수단', '카드'], - '상품관리': ['상품', '제품', '상품목록', '카탈로그'], - '시스템관리': ['설정', '관리자', '백오피스', '시스템'], - '통계/분석': ['통계', '분석', '리포트', '대시보드', '차트'] - }; - - for (const [category, keywords] of Object.entries(categoryKeywords)) { - if (keywords.some(keyword => title.includes(keyword))) { - return category; - } - } - - return '기타기능'; - } - - /** - * 기본 메뉴 구조 - */ - getDefaultMenuStructure() { - const defaultStructures = { - 'mobile_app': { - root: "모바일 앱", - children: [ - { name: "인증", children: ["로그인", "회원가입", "비밀번호 찾기"] }, - { name: "메인", children: ["홈", "검색", "카테고리"] }, - { name: "사용자", children: ["프로필", "설정", "알림"] }, - { name: "콘텐츠", children: ["목록", "상세", "작성"] } - ] - }, - 'web_service': { - root: "웹 서비스", - children: [ - { name: "사용자관리", children: ["회원가입", "로그인", "프로필관리"] }, - { name: "콘텐츠관리", children: ["작성", "편집", "삭제", "검색"] }, - { name: "시스템관리", children: ["설정", "통계", "백업"] } - ] - } - }; - - return defaultStructures[this.projectInfo.type] || defaultStructures['web_service']; - } - - /** - * 공통 가이드라인 생성 - */ - generateCommonGuidelines() { - return { - type: "common_guidelines", - sections: [ - { - title: "사용자 인터랙션", - content: this.generateInteractionGuidelines() - }, - { - title: "반응형 디자인", - content: this.generateResponsiveGuidelines() - }, - { - title: "화면 템플릿", - content: this.generateScreenTemplateGuidelines() - }, - { - title: "알림 시스템", - content: this.generateNotificationGuidelines() - } - ] - }; - } - - /** - * 인터랙션 가이드라인 생성 - */ - generateInteractionGuidelines() { - const interactions = [ - { type: "Tap", description: "일정영역을 사용자가 터치합니다.", apply: "Yes" }, - { type: "Scroll Up/Down", description: "상하 스크롤 동작", apply: "Yes" }, - { type: "Swipe Left/Right", description: "좌우 스와이프 동작", apply: "Yes" }, - { type: "Drag & Drop", description: "드래그 앤 드롭 기능", apply: "Yes" } - ]; - - return { - type: "interaction_table", - data: interactions - }; - } - - /** - * 반응형 가이드라인 생성 - */ - generateResponsiveGuidelines() { - return { - type: "responsive_guide", - breakpoints: [ - { device: "모바일", range: "< 640px", note: "기본" }, - { device: "태블릿", range: "768px ~ 1023px", note: "md" }, - { device: "데스크탑", range: "1024px+", note: "lg" }, - { device: "대형 모니터", range: "1280px+", note: "xl" } - ] - }; - } - - /** - * 화면 템플릿 가이드라인 생성 - */ - generateScreenTemplateGuidelines() { - return { - type: "screen_template", - elements: [ - { name: "Header", description: "상단 네비게이션 및 타이틀" }, - { name: "Content", description: "주요 콘텐츠 영역" }, - { name: "Footer", description: "하단 정보 및 링크" }, - { name: "Sidebar", description: "사이드 메뉴 (PC 버전)" } - ] - }; - } - - /** - * 알림 시스템 가이드라인 생성 - */ - generateNotificationGuidelines() { - return { - type: "notification_guide", - types: [ - { name: "알림 Alert", description: "사용자에게 상황을 알려주기 위한 팝업" }, - { name: "확인 Alert", description: "사용자에게 확인이 필요할 경우 제공되는 팝업" }, - { name: "토스트 메시지", description: "단순 Notify (2~3)초 후 페이지 내에서 Fade out" } - ] - }; - } - - /** - * 상세 화면 생성 - */ - generateDetailScreens() { - const screens = []; - - if (this.projectInfo.features && this.projectInfo.features.length > 0) { - // 기능 목록에서 화면 생성 - this.projectInfo.features.forEach((feature, index) => { - screens.push(this.generateScreenFromFeature(feature, index)); - }); - } else if (this.projectInfo.requirements && this.projectInfo.requirements.length > 0) { - // 요구사항에서 화면 생성 - this.projectInfo.requirements.forEach((req, index) => { - screens.push(this.generateScreenFromRequirement(req, index)); - }); - } else { - // 기본 화면들 생성 - screens.push(...this.generateDefaultScreens()); - } - - return { - type: "detail_screens", - screens: screens - }; - } - - /** - * 기능에서 화면 생성 - */ - generateScreenFromFeature(feature, index) { - return { - page_number: 10 + index, // 기본 페이지 이후 - screen_info: { - task_name: this.projectInfo.name, - version: "D1.0", - route: this.generateRoute(feature.name), - screen_name: feature.name, - screen_id: this.generateScreenId(feature.name) - }, - wireframe: { - type: "feature_screen", - description: `${feature.name} 화면 구성`, - elements: this.generateUIElements(feature) - }, - descriptions: this.generateFeatureDescriptions(feature) - }; - } - - /** - * 요구사항에서 화면 생성 - */ - generateScreenFromRequirement(requirement, index) { - return { - page_number: 10 + index, - screen_info: { - task_name: this.projectInfo.name, - version: "D1.0", - route: this.generateRoute(requirement.title), - screen_name: requirement.title, - screen_id: this.generateScreenId(requirement.title) - }, - wireframe: { - type: "requirement_screen", - description: `${requirement.title} 화면`, - elements: this.generateUIElementsFromRequirement(requirement) - }, - descriptions: this.generateRequirementDescriptions(requirement) - }; - } - - /** - * 기본 화면들 생성 - */ - generateDefaultScreens() { - const defaultScreens = [ - { - name: "메인 화면", - route: "/main", - features: ["네비게이션", "주요 콘텐츠", "검색"] - }, - { - name: "로그인", - route: "/auth/login", - features: ["이메일 입력", "비밀번호 입력", "로그인 버튼"] - }, - { - name: "목록 화면", - route: "/list", - features: ["검색 필터", "목록 표시", "페이지네이션"] - } - ]; - - return defaultScreens.map((screen, index) => ({ - page_number: 10 + index, - screen_info: { - task_name: this.projectInfo.name, - version: "D1.0", - route: screen.route, - screen_name: screen.name, - screen_id: this.generateScreenId(screen.name) - }, - wireframe: { - type: "default_screen", - description: `${screen.name} 구성`, - elements: screen.features - }, - descriptions: this.generateDefaultDescriptions(screen) - })); - } - - /** - * 라우트 생성 - */ - generateRoute(screenName) { - return '/' + screenName - .toLowerCase() - .replace(/\s+/g, '_') - .replace(/[^a-z0-9_]/g, ''); - } - - /** - * 화면 ID 생성 - */ - generateScreenId(screenName) { - return screenName - .toLowerCase() - .replace(/\s+/g, '_') - .replace(/[^a-z0-9_]/g, ''); - } - - /** - * UI 요소 생성 - */ - generateUIElements(feature) { - // 기능 타입에 따른 UI 요소 자동 생성 - const elementMap = { - 'login': ['이메일 입력', '비밀번호 입력', '로그인 버튼', '회원가입 링크'], - 'list': ['검색바', '필터', '목록 아이템', '페이지네이션'], - 'form': ['입력 필드', '제출 버튼', '취소 버튼', '유효성 검사'], - 'detail': ['제목', '내용', '수정 버튼', '삭제 버튼', '목록으로 버튼'] - }; - - const featureType = this.guessFeatureType(feature.name); - return elementMap[featureType] || ['헤더', '메인 콘텐츠', '푸터']; - } - - /** - * 기능 타입 추측 - */ - guessFeatureType(featureName) { - const typeKeywords = { - 'login': ['로그인', '인증', '로그인'], - 'list': ['목록', '리스트', '조회'], - 'form': ['등록', '작성', '입력', '수정'], - 'detail': ['상세', '세부', '정보'] - }; - - for (const [type, keywords] of Object.entries(typeKeywords)) { - if (keywords.some(keyword => featureName.includes(keyword))) { - return type; - } - } - - return 'default'; - } - - /** - * 기능 설명 생성 - */ - generateFeatureDescriptions(feature) { - const descriptions = []; - - if (feature.elements) { - feature.elements.forEach((element, index) => { - descriptions.push({ - number: index + 1, - title: element, - description: `${element} 기능 구현` - }); - }); - } - - return descriptions; - } - - /** - * 페이지 수 추정 - */ - estimatePageCount() { - let pageCount = 5; // 기본 페이지 (표지, 히스토리, 구조, 가이드라인) - - if (this.projectInfo.features) { - pageCount += this.projectInfo.features.length; - } else if (this.projectInfo.requirements) { - pageCount += this.projectInfo.requirements.length; - } else { - pageCount += 3; // 기본 화면 수 - } - - return pageCount; - } - - /** - * Markdown 형태로 기획서 출력 - */ - async exportToMarkdown(proposal, outputPath) { - const markdown = this.generateMarkdown(proposal); - - const outputDir = path.dirname(outputPath); - await fs.mkdir(outputDir, { recursive: true }); - - await fs.writeFile(outputPath, markdown, 'utf8'); - console.log(`📄 Markdown 기획서 생성: ${outputPath}`); - - return outputPath; - } - - /** - * Markdown 생성 - */ - generateMarkdown(proposal) { - let markdown = ''; - - // 표지 - markdown += `# ${proposal.cover.title}\n`; - markdown += `## ${proposal.cover.subtitle}\n\n`; - markdown += `**버전**: ${proposal.cover.version} \n`; - markdown += `**작성일**: ${proposal.cover.date} \n`; - markdown += `**작성자**: ${proposal.cover.author} \n`; - markdown += `**회사**: ${proposal.cover.company} \n\n`; - - if (proposal.cover.description) { - markdown += `**프로젝트 설명**: ${proposal.cover.description}\n\n`; - } - - markdown += `---\n\n`; - - // 문서 히스토리 - markdown += `## Document History\n\n`; - markdown += `| 날짜 | 버전 | 주요 내용 | 상세 내용 | 비고 |\n`; - markdown += `|------|------|-----------|-----------|------|\n`; - - proposal.document_history.table.forEach(row => { - markdown += `| ${row.date} | ${row.version} | ${row.main_content} | ${row.detailed_content} | ${row.mark} |\n`; - }); - - markdown += `\n---\n\n`; - - // 메뉴 구조 - markdown += `## Menu Structure\n\n`; - markdown += this.generateMenuMarkdown(proposal.menu_structure.structure); - markdown += `\n---\n\n`; - - // 공통 가이드라인 - markdown += `## 공통 가이드라인\n\n`; - proposal.common_guidelines.sections.forEach(section => { - markdown += `### ${section.title}\n\n`; - markdown += this.generateSectionMarkdown(section.content); - markdown += `\n`; - }); - - markdown += `---\n\n`; - - // 상세 화면들 - markdown += `## 상세 화면 설계\n\n`; - proposal.detail_screens.screens.forEach((screen, index) => { - markdown += `### 슬라이드 ${screen.page_number}: ${screen.screen_info.screen_name}\n\n`; - - markdown += `**화면 정보**:\n`; - markdown += `- 단위업무명: ${screen.screen_info.task_name}\n`; - markdown += `- 경로: ${screen.screen_info.route}\n`; - markdown += `- 화면 ID: ${screen.screen_info.screen_id}\n`; - markdown += `- 버전: ${screen.screen_info.version}\n\n`; - - markdown += `**화면 구성**:\n`; - if (Array.isArray(screen.wireframe.elements)) { - screen.wireframe.elements.forEach((element, idx) => { - markdown += `${idx + 1}. ${element}\n`; - }); - } - - markdown += `\n**기능 설명**:\n`; - if (screen.descriptions && screen.descriptions.length > 0) { - screen.descriptions.forEach(desc => { - markdown += `${desc.number}. **${desc.title}**: ${desc.description}\n`; - }); - } - - markdown += `\n---\n\n`; - }); - - return markdown; - } - - /** - * 메뉴 구조 Markdown 생성 - */ - generateMenuMarkdown(structure, depth = 0) { - let markdown = ''; - const indent = ' '.repeat(depth); - - if (structure.root && depth === 0) { - markdown += `- **${structure.root}**\n`; - } - - if (structure.children) { - structure.children.forEach(child => { - if (typeof child === 'string') { - markdown += `${indent}- ${child}\n`; - } else { - markdown += `${indent}- **${child.name}**\n`; - if (child.children) { - child.children.forEach(subChild => { - const subIndent = ' '.repeat(depth + 1); - if (typeof subChild === 'string') { - markdown += `${subIndent}- ${subChild}\n`; - } else { - markdown += `${subIndent}- ${subChild.name}\n`; - } - }); - } - } - }); - } - - return markdown; - } - - /** - * 섹션 Markdown 생성 - */ - generateSectionMarkdown(content) { - if (content.type === 'interaction_table') { - let markdown = `| Type | Description | Apply |\n|------|-------------|-------|\n`; - content.data.forEach(item => { - markdown += `| ${item.type} | ${item.description} | ${item.apply} |\n`; - }); - return markdown; - } - - if (content.type === 'responsive_guide') { - let markdown = `**브레이크 포인트**:\n`; - content.breakpoints.forEach(bp => { - markdown += `- ${bp.device}: ${bp.range} (${bp.note})\n`; - }); - return markdown; - } - - return JSON.stringify(content, null, 2); - } - - // 요구사항에서 UI 요소 생성 - generateUIElementsFromRequirement(requirement) { - // 요구사항 설명에서 UI 요소 추출 (간단한 키워드 매칭) - const description = requirement.description || requirement.title; - const elements = []; - - // 일반적인 UI 패턴 매칭 - if (description.includes('로그인') || description.includes('인증')) { - elements.push('이메일 입력', '비밀번호 입력', '로그인 버튼'); - } - if (description.includes('목록') || description.includes('조회')) { - elements.push('검색바', '필터', '목록 아이템'); - } - if (description.includes('등록') || description.includes('작성')) { - elements.push('입력 폼', '제출 버튼', '취소 버튼'); - } - - // 기본 요소가 없으면 일반적인 화면 구성 추가 - if (elements.length === 0) { - elements.push('헤더', '메인 콘텐츠', '네비게이션'); - } - - return elements; - } - - // 요구사항 설명 생성 - generateRequirementDescriptions(requirement) { - const descriptions = []; - - descriptions.push({ - number: 1, - title: requirement.title, - description: requirement.description || `${requirement.title} 기능 구현` - }); - - // 우선순위가 있으면 추가 - if (requirement.priority) { - descriptions.push({ - number: 2, - title: "우선순위", - description: `개발 우선순위: ${requirement.priority}` - }); - } - - return descriptions; - } - - // 기본 설명 생성 - generateDefaultDescriptions(screen) { - const descriptions = []; - - screen.features.forEach((feature, index) => { - descriptions.push({ - number: index + 1, - title: feature, - description: `${feature} 구현 및 사용자 인터랙션 처리` - }); - }); - - return descriptions; - } -} - -// 명령줄 실행 지원 -async function main() { - const args = process.argv.slice(2); - - if (args.includes('--help')) { - console.log(` -📝 기획서 생성기 사용법: - -기본 사용: - node proposal-generator.js - -프로젝트 정보로 생성: - node proposal-generator.js --project project.json --output proposal.md - -템플릿 지정: - node proposal-generator.js --template template.json --project project.json - -예시: - node proposal-generator.js --project "모바일 앱" --type mobile_app --output mobile_proposal.md - `); - return; - } - - try { - const generator = new ProposalGenerator(); - - // 템플릿 로드 - const templatePath = '.claude/skills/proposal-skill/templates/erp_storyboard_template.json'; - await generator.loadTemplate(templatePath); - - // 샘플 프로젝트 정보 - const sampleProject = { - name: "모바일 쇼핑몰 앱", - type: "mobile_app", - company: "Sample Company", - author: "기획팀", - description: "사용자 친화적인 모바일 쇼핑 경험을 제공하는 앱", - requirements: [ - { - title: "사용자 로그인", - description: "이메일/비밀번호를 통한 사용자 인증 기능", - category: "사용자관리", - priority: "high" - }, - { - title: "상품 목록 조회", - description: "카테고리별 상품 목록 및 검색 기능", - category: "상품관리", - priority: "high" - }, - { - title: "장바구니", - description: "상품 선택 및 장바구니 관리 기능", - category: "결제관리", - priority: "medium" - } - ] - }; - - generator.setProjectInfo(sampleProject); - - const proposal = await generator.generate(); - const outputPath = 'output/sample_proposal.md'; - - await generator.exportToMarkdown(proposal, outputPath); - - console.log("✅ 샘플 기획서 생성 완료!"); - console.log(`📄 출력 파일: ${outputPath}`); - - } catch (error) { - console.error("❌ 기획서 생성 실패:", error); - } -} - -if (require.main === module) { - main(); -} - -module.exports = { ProposalGenerator }; \ No newline at end of file diff --git a/.claude/skills/query-efficiency-auditor/SKILL.md b/.claude/skills/query-efficiency-auditor/SKILL.md deleted file mode 100644 index 9080207..0000000 --- a/.claude/skills/query-efficiency-auditor/SKILL.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -name: ln-651-query-efficiency-auditor -description: "Query efficiency audit worker (L3). Checks redundant entity fetches, N-UPDATE/DELETE loops, unnecessary resolves, over-fetching, missing bulk operations, wrong caching scope. Returns findings with severity, location, effort, recommendations." -allowed-tools: Read, Grep, Glob, Bash ---- - -# Query Efficiency Auditor (L3 Worker) - -Specialized worker auditing database query patterns for redundancy, inefficiency, and misuse. - -## Purpose & Scope - -- **Worker in ln-650 coordinator pipeline** - invoked by ln-650-persistence-performance-auditor -- Audit **query efficiency** (Priority: HIGH) -- Check redundant fetches, batch operation misuse, caching scope problems -- Return structured findings with severity, location, effort, recommendations -- Calculate compliance score (X/10) for Query Efficiency category - -## Inputs (from Coordinator) - -**MANDATORY READ:** Load `shared/references/task_delegation_pattern.md#audit-coordinator--worker-contract` for contextStore structure. - -Receives `contextStore` with: `tech_stack`, `best_practices`, `db_config` (database type, ORM settings), `codebase_root`. - -**Domain-aware:** Supports `domain_mode` + `current_domain`. - -## Workflow - -1) **Parse context from contextStore** - - Extract tech_stack, best_practices, db_config - - Determine scan_path (same logic as ln-624) - -2) **Scan codebase for violations** - - All Grep/Glob patterns use `scan_path` - - Trace call chains for redundant fetches (requires reading caller + callee) - -3) **Collect findings with severity, location, effort, recommendation** - -4) **Calculate score using penalty algorithm** - -5) **Return JSON result to coordinator** - -## Audit Rules (Priority: HIGH) - -### 1. Redundant Entity Fetch -**What:** Same entity fetched from DB twice in a call chain - -**Detection:** -- Find function A that calls `repo.get(id)` or `session.get(Model, id)`, then passes `id` (not object) to function B -- Function B also calls `repo.get(id)` or `session.get(Model, id)` for the same entity -- Common pattern: `acquire_next_pending()` returns job, but `_process_job(job_id)` re-fetches it - -**Detection patterns (Python/SQLAlchemy):** -- Grep for `repo.*get_by_id|session\.get\(|session\.query.*filter.*id` in service/handler files -- Trace: if function receives `entity_id: int/UUID` AND internally does `repo.get(entity_id)`, check if caller already has entity object -- Check `expire_on_commit` setting: if `False`, objects remain valid after commit - -**Severity:** -- **HIGH:** Redundant fetch in API request handler (adds latency per request) -- **MEDIUM:** Redundant fetch in background job (less critical) - -**Recommendation:** Pass entity object instead of ID, or remove second fetch when `expire_on_commit=False` - -**Effort:** S (change signature to accept object instead of ID) - -### 2. N-UPDATE/DELETE Loop -**What:** Loop of individual UPDATE/DELETE operations instead of single batch query - -**Detection:** -- Pattern: `for item in items: await repo.update(item.id, ...)` or `for item in items: await repo.delete(item.id)` -- Pattern: `for item in items: session.execute(update(Model).where(...))` - -**Detection patterns:** -- Grep for `for .* in .*:` followed by `repo\.(update|delete|reset|save|mark_)` within 1-3 lines -- Grep for `for .* in .*:` followed by `session\.execute\(.*update\(` within 1-3 lines - -**Severity:** -- **HIGH:** Loop over >10 items (N separate round-trips to DB) -- **MEDIUM:** Loop over <=10 items - -**Recommendation:** Replace with single `UPDATE ... WHERE id IN (...)` or `session.execute(update(Model).where(Model.id.in_(ids)))` - -**Effort:** M (rewrite query + test) - -### 3. Unnecessary Resolve -**What:** Re-resolving a value from DB when it is already available in the caller's scope - -**Detection:** -- Method receives `profile_id` and resolves engine from it, but caller already determined `engine` -- Method receives `lang_code` and looks up dialect_id, but caller already has both `lang` and `dialect` -- Pattern: function receives `X_id`, does `get(X_id)`, extracts `.field`, when caller already has `field` - -**Severity:** -- **MEDIUM:** Extra DB query per invocation, especially in high-frequency paths - -**Recommendation:** Split method into two variants: `with_known_value(value, ...)` and `resolving_value(id, ...)`; or pass resolved value directly - -**Effort:** S-M (refactor signature, update callers) - -### 4. Over-Fetching -**What:** Loading full ORM model when only few fields are needed - -**Detection:** -- `session.query(Model)` or `select(Model)` without `.options(load_only(...))` for models with >10 columns -- Especially in list/search endpoints that return many rows -- Pattern: loading full entity but only using 2-3 fields - -**Severity:** -- **MEDIUM:** Large models (>15 columns) in list endpoints -- **LOW:** Small models (<10 columns) or single-entity endpoints - -**Recommendation:** Use `load_only()`, `defer()`, or raw `select(Model.col1, Model.col2)` for list queries - -**Effort:** S (add load_only to query) - -### 5. Missing Bulk Operations -**What:** Sequential INSERT/DELETE/UPDATE instead of bulk operations - -**Detection:** -- `for item in items: session.add(item)` instead of `session.add_all(items)` -- `for item in items: session.delete(item)` instead of bulk delete -- Pattern: loop with single `INSERT` per iteration - -**Severity:** -- **MEDIUM:** Any sequential add/delete in loop (missed batch optimization) - -**Recommendation:** Use `session.add_all()`, `session.execute(insert(Model).values(list_of_dicts))`, `bulk_save_objects()` - -**Effort:** S (replace loop with bulk call) - -### 6. Wrong Caching Scope -**What:** Request-scoped cache for data that rarely changes (should be app-scoped) - -**Detection:** -- Service registered as request-scoped (e.g., via FastAPI `Depends()`) with internal cache (`_cache` dict, `_loaded` flag) -- Cache populated by expensive query (JOINs, aggregations) per each request -- Data TTL >> request duration (e.g., engine configurations, language lists, feature flags) - -**Detection patterns:** -- Find classes with `_cache`, `_loaded`, `_initialized` attributes -- Check if class is created per-request (via DI registration scope) -- Compare: data change frequency vs cache lifetime - -**Severity:** -- **HIGH:** Expensive query (JOINs, subqueries) cached only per-request -- **MEDIUM:** Simple query cached per-request - -**Recommendation:** Move cache to app-scoped service (singleton), add TTL-based invalidation, or use CacheService with configurable TTL - -**Effort:** M (change DI scope, add TTL logic) - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -Return JSON to coordinator: - -```json -{ - "category": "Query Efficiency", - "score": 6, - "total_issues": 8, - "critical": 0, - "high": 3, - "medium": 4, - "low": 1, - "findings": [ - { - "severity": "HIGH", - "location": "app/infrastructure/messaging/job_processor.py:434", - "issue": "Redundant entity fetch: job re-fetched by ID after acquire_next_pending already returned it", - "principle": "Query Efficiency / DRY Data Access", - "recommendation": "Pass job object to _process_job instead of job_id", - "effort": "S" - } - ] -} -``` - -## Critical Rules - -- **Do not auto-fix:** Report only -- **Trace call chains:** Rules 1 and 3 require reading both caller and callee -- **ORM-aware:** Check `expire_on_commit`, `autoflush`, session scope before flagging redundant fetches -- **Context-aware:** Small datasets or infrequent operations may justify simpler code -- **Exclude tests:** Do not flag test fixtures or setup code - -## Definition of Done - -- contextStore parsed (tech_stack, db_config, ORM settings) -- scan_path determined (domain path or codebase root) -- All 6 checks completed: - - redundant fetch, N-UPDATE loop, unnecessary resolve, over-fetching, bulk ops, caching scope -- Findings collected with severity, location, effort, recommendation -- Score calculated -- JSON returned to coordinator - ---- -**Version:** 1.0.0 -**Last Updated:** 2026-02-04 diff --git a/.claude/skills/regression-checker/SKILL.md b/.claude/skills/regression-checker/SKILL.md deleted file mode 100644 index 648816e..0000000 --- a/.claude/skills/regression-checker/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: ln-502-regression-checker -description: Worker that runs existing tests to catch regressions. Auto-detects framework, reports pass/fail. No status changes or task creation. ---- - -# Regression Checker - -Runs the existing test suite to ensure no regressions after implementation changes. - -## Purpose & Scope -- Detect test framework (pytest/jest/vitest/go test/etc.) and test dirs. -- Execute full suite; capture results for Story quality gate. -- Return PASS/FAIL with counts/log excerpts; never modifies Linear or kanban. - -## When to Use -- **Invoked by ln-500-story-quality-gate** Pass 1 (after ln-501) -- Code quality check passed -- Before test planning pipeline (ln-510) - -## Workflow (concise) -1) Auto-discover framework and test locations from repo config/files. -2) **Read `docs/project/runbook.md`** — get exact test commands, Docker setup, environment variables. Use commands from runbook, NOT guessed commands. -3) Build appropriate test command; run with timeout (~5m); capture stdout/stderr. -4) Parse results: passed/failed counts; key failing tests. -5) Output verdict JSON (PASS or FAIL + failures list) and add Linear comment. - -## Critical Rules -- No selective test runs; run full suite. -- Do not fix tests or change status; only report. -- Language preservation in comment (EN/RU). - -## Definition of Done -- Framework detected; command executed. -- Results parsed; verdict produced with failing tests (if any). -- Linear comment posted with summary. - -## Reference Files -- Risk-based limits used downstream: `../ln-510-test-planner/references/risk_based_testing_guide.md` - ---- -**Version:** 3.1.0 (Added mandatory runbook.md reading before test execution) -**Last Updated:** 2026-01-09 diff --git a/.claude/skills/security-auditor/SKILL.md b/.claude/skills/security-auditor/SKILL.md deleted file mode 100644 index 1b05c46..0000000 --- a/.claude/skills/security-auditor/SKILL.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -name: ln-621-security-auditor -description: Security audit worker (L3). Scans codebase for hardcoded secrets, SQL injection, XSS, insecure dependencies, missing input validation. Returns findings with severity (Critical/High/Medium/Low), location, effort, and recommendations. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Security Auditor (L3 Worker) - -Specialized worker auditing security vulnerabilities in codebase. - -## Purpose & Scope - -- **Worker in ln-620 coordinator pipeline** - invoked by ln-620-codebase-auditor -- Audit codebase for **security vulnerabilities** (Category 1: Critical Priority) -- Scan for hardcoded secrets, SQL injection, XSS, insecure dependencies, missing input validation -- Return structured findings to coordinator with severity, location, effort, recommendations -- Calculate compliance score (X/10) for Security category - -## Inputs (from Coordinator) - -**MANDATORY READ:** Load `shared/references/task_delegation_pattern.md#audit-coordinator--worker-contract` for contextStore structure. - -Receives `contextStore` with: `tech_stack`, `best_practices`, `principles`, `codebase_root`. - -## Workflow - -1) **Parse Context:** Extract tech stack, best practices, codebase root from contextStore -2) **Scan Codebase:** Run security checks using Glob/Grep patterns (see Audit Rules below) -3) **Collect Findings:** Record each violation with severity, location (file:line), effort estimate (S/M/L), recommendation -4) **Calculate Score:** Count violations by severity, calculate compliance score (X/10) -5) **Return Results:** Return JSON with category, score, findings to coordinator - -## Audit Rules (Priority: CRITICAL) - -### 1. Hardcoded Secrets -**What:** API keys, passwords, tokens, private keys in source code - -**Detection:** -- Search patterns: `API_KEY = "..."`, `password = "..."`, `token = "..."`, `SECRET = "..."` -- File extensions: `.ts`, `.js`, `.py`, `.go`, `.java`, `.cs` -- Exclude: `.env.example`, `README.md`, test files with mock data - -**Severity:** -- **CRITICAL:** Production credentials (AWS keys, database passwords, API tokens) -- **HIGH:** Development/staging credentials -- **MEDIUM:** Test credentials in non-test files - -**Recommendation:** Move to environment variables (.env), use secret management (Vault, AWS Secrets Manager) - -**Effort:** S (replace hardcoded value with `process.env.VAR_NAME`) - -### 2. SQL Injection Patterns -**What:** String concatenation in SQL queries instead of parameterized queries - -**Detection:** -- Patterns: `query = "SELECT * FROM users WHERE id=" + userId`, `db.execute(f"SELECT * FROM {table}")`, `` `SELECT * FROM ${table}` `` -- Languages: JavaScript, Python, PHP, Java - -**Severity:** -- **CRITICAL:** User input directly concatenated without sanitization -- **HIGH:** Variable concatenation in production code -- **MEDIUM:** Concatenation with internal variables only - -**Recommendation:** Use parameterized queries (prepared statements), ORM query builders - -**Effort:** M (refactor query to use placeholders) - -### 3. XSS Vulnerabilities -**What:** Unsanitized user input rendered in HTML/templates - -**Detection:** -- Patterns: `innerHTML = userInput`, `dangerouslySetInnerHTML={{__html: data}}`, `echo $userInput;` -- Template engines: Check for unescaped output (`{{ var | safe }}`, `<%- var %>`) - -**Severity:** -- **CRITICAL:** User input directly inserted into DOM without sanitization -- **HIGH:** User input with partial sanitization (insufficient escaping) -- **MEDIUM:** Internal data with potential XSS if compromised - -**Recommendation:** Use framework escaping (React auto-escapes, use `textContent`), sanitize with DOMPurify - -**Effort:** S-M (replace `innerHTML` with `textContent` or sanitize) - -### 4. Insecure Dependencies -**What:** Dependencies with known CVEs (Common Vulnerabilities and Exposures) - -**Detection:** -- Run `npm audit` (Node.js), `pip-audit` (Python), `cargo audit` (Rust), `dotnet list package --vulnerable` (.NET) -- Check for outdated critical dependencies - -**Severity:** -- **CRITICAL:** CVE with exploitable vulnerability in production dependencies -- **HIGH:** CVE in dev dependencies or lower severity production CVEs -- **MEDIUM:** Outdated packages without known CVEs but security risk - -**Recommendation:** Update to patched versions, replace unmaintained packages - -**Effort:** S-M (update package.json, test), L (if breaking changes) - -### 5. Missing Input Validation -**What:** Missing validation at system boundaries (API endpoints, user forms, file uploads) - -**Detection:** -- API routes without validation middleware -- Form handlers without input sanitization -- File uploads without type/size checks -- Missing CORS configuration - -**Severity:** -- **CRITICAL:** File upload without validation, authentication bypass potential -- **HIGH:** Missing validation on sensitive endpoints (payment, auth, user data) -- **MEDIUM:** Missing validation on read-only or internal endpoints - -**Recommendation:** Add validation middleware (Joi, Yup, express-validator), implement input sanitization - -**Effort:** M (add validation schema and middleware) - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -## Output Format - -**MANDATORY READ:** Load `shared/references/audit_output_schema.md` for JSON structure. - -Return JSON with `category: "Security"` and checks: hardcoded_secrets, sql_injection, xss_vulnerabilities, insecure_dependencies, missing_input_validation. - -## Critical Rules - -- **Do not auto-fix:** Report violations only; coordinator creates task for user to fix -- **Tech stack aware:** Use contextStore to apply framework-specific patterns (e.g., React XSS vs PHP XSS) -- **False positive reduction:** Exclude test files, example configs, documentation -- **Effort realism:** S = <1 hour, M = 1-4 hours, L = >4 hours -- **Location precision:** Always include `file:line` for programmatic navigation - -## Definition of Done - -- contextStore parsed successfully -- All 5 security checks completed (secrets, SQL injection, XSS, deps, validation) -- Findings collected with severity, location, effort, recommendation -- Score calculated using penalty algorithm -- JSON result returned to coordinator - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` -- Security audit rules: [references/security_rules.md](references/security_rules.md) - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/sharp-edges/SKILL.md b/.claude/skills/sharp-edges/SKILL.md deleted file mode 100644 index b5f86df..0000000 --- a/.claude/skills/sharp-edges/SKILL.md +++ /dev/null @@ -1,292 +0,0 @@ ---- -name: sharp-edges -description: "Identifies error-prone APIs, dangerous configurations, and footgun designs that enable security mistakes. Use when reviewing API designs, configuration schemas, cryptographic library ergonomics, or evaluating whether code follows 'secure by default' and 'pit of success' principles. Triggers: footgun, misuse-resistant, secure defaults, API usability, dangerous configuration." -allowed-tools: - - Read - - Grep - - Glob ---- - -# Sharp Edges Analysis - -Evaluates whether APIs, configurations, and interfaces are resistant to developer misuse. Identifies designs where the "easy path" leads to insecurity. - -## When to Use - -- Reviewing API or library design decisions -- Auditing configuration schemas for dangerous options -- Evaluating cryptographic API ergonomics -- Assessing authentication/authorization interfaces -- Reviewing any code that exposes security-relevant choices to developers - -## When NOT to Use - -- Implementation bugs (use standard code review) -- Business logic flaws (use domain-specific analysis) -- Performance optimization (different concern) - -## Core Principle - -**The pit of success**: Secure usage should be the path of least resistance. If developers must understand cryptography, read documentation carefully, or remember special rules to avoid vulnerabilities, the API has failed. - -## Rationalizations to Reject - -| Rationalization | Why It's Wrong | Required Action | -|-----------------|----------------|-----------------| -| "It's documented" | Developers don't read docs under deadline pressure | Make the secure choice the default or only option | -| "Advanced users need flexibility" | Flexibility creates footguns; most "advanced" usage is copy-paste | Provide safe high-level APIs; hide primitives | -| "It's the developer's responsibility" | Blame-shifting; you designed the footgun | Remove the footgun or make it impossible to misuse | -| "Nobody would actually do that" | Developers do everything imaginable under pressure | Assume maximum developer confusion | -| "It's just a configuration option" | Config is code; wrong configs ship to production | Validate configs; reject dangerous combinations | -| "We need backwards compatibility" | Insecure defaults can't be grandfather-claused | Deprecate loudly; force migration | - -## Sharp Edge Categories - -### 1. Algorithm/Mode Selection Footguns - -APIs that let developers choose algorithms invite choosing wrong ones. - -**The JWT Pattern** (canonical example): -- Header specifies algorithm: attacker can set `"alg": "none"` to bypass signatures -- Algorithm confusion: RSA public key used as HMAC secret when switching RS256→HS256 -- Root cause: Letting untrusted input control security-critical decisions - -**Detection patterns:** -- Function parameters like `algorithm`, `mode`, `cipher`, `hash_type` -- Enums/strings selecting cryptographic primitives -- Configuration options for security mechanisms - -**Example - PHP password_hash allowing weak algorithms:** -```php -// DANGEROUS: allows crc32, md5, sha1 -password_hash($password, PASSWORD_DEFAULT); // Good - no choice -hash($algorithm, $password); // BAD: accepts "crc32" -``` - -### 2. Dangerous Defaults - -Defaults that are insecure, or zero/empty values that disable security. - -**The OTP Lifetime Pattern:** -```python -# What happens when lifetime=0? -def verify_otp(code, lifetime=300): # 300 seconds default - if lifetime == 0: - return True # OOPS: 0 means "accept all"? - # Or does it mean "expired immediately"? -``` - -**Detection patterns:** -- Timeouts/lifetimes that accept 0 (infinite? immediate expiry?) -- Empty strings that bypass checks -- Null values that skip validation -- Boolean defaults that disable security features -- Negative values with undefined semantics - -**Questions to ask:** -- What happens with `timeout=0`? `max_attempts=0`? `key=""`? -- Is the default the most secure option? -- Can any default value disable security entirely? - -### 3. Primitive vs. Semantic APIs - -APIs that expose raw bytes instead of meaningful types invite type confusion. - -**The Libsodium vs. Halite Pattern:** - -```php -// Libsodium (primitives): bytes are bytes -sodium_crypto_box($message, $nonce, $keypair); -// Easy to: swap nonce/keypair, reuse nonces, use wrong key type - -// Halite (semantic): types enforce correct usage -Crypto::seal($message, new EncryptionPublicKey($key)); -// Wrong key type = type error, not silent failure -``` - -**Detection patterns:** -- Functions taking `bytes`, `string`, `[]byte` for distinct security concepts -- Parameters that could be swapped without type errors -- Same type used for keys, nonces, ciphertexts, signatures - -**The comparison footgun:** -```go -// Timing-safe comparison looks identical to unsafe -if hmac == expected { } // BAD: timing attack -if hmac.Equal(mac, expected) { } // Good: constant-time -// Same types, different security properties -``` - -### 4. Configuration Cliffs - -One wrong setting creates catastrophic failure, with no warning. - -**Detection patterns:** -- Boolean flags that disable security entirely -- String configs that aren't validated -- Combinations of settings that interact dangerously -- Environment variables that override security settings -- Constructor parameters with sensible defaults but no validation (callers can override with insecure values) - -**Examples:** -```yaml -# One typo = disaster -verify_ssl: fasle # Typo silently accepted as truthy? - -# Magic values -session_timeout: -1 # Does this mean "never expire"? - -# Dangerous combinations accepted silently -auth_required: true -bypass_auth_for_health_checks: true -health_check_path: "/" # Oops -``` - -```php -// Sensible default doesn't protect against bad callers -public function __construct( - public string $hashAlgo = 'sha256', // Good default... - public int $otpLifetime = 120, // ...but accepts md5, 0, etc. -) {} -``` - -See [config-patterns.md](references/config-patterns.md#unvalidated-constructor-parameters) for detailed patterns. - -### 5. Silent Failures - -Errors that don't surface, or success that masks failure. - -**Detection patterns:** -- Functions returning booleans instead of throwing on security failures -- Empty catch blocks around security operations -- Default values substituted on parse errors -- Verification functions that "succeed" on malformed input - -**Examples:** -```python -# Silent bypass -def verify_signature(sig, data, key): - if not key: - return True # No key = skip verification?! - -# Return value ignored -signature.verify(data, sig) # Throws on failure -crypto.verify(data, sig) # Returns False on failure -# Developer forgets to check return value -``` - -### 6. Stringly-Typed Security - -Security-critical values as plain strings enable injection and confusion. - -**Detection patterns:** -- SQL/commands built from string concatenation -- Permissions as comma-separated strings -- Roles/scopes as arbitrary strings instead of enums -- URLs constructed by joining strings - -**The permission accumulation footgun:** -```python -permissions = "read,write" -permissions += ",admin" # Too easy to escalate - -# vs. type-safe -permissions = {Permission.READ, Permission.WRITE} -permissions.add(Permission.ADMIN) # At least it's explicit -``` - -## Analysis Workflow - -### Phase 1: Surface Identification - -1. **Map security-relevant APIs**: authentication, authorization, cryptography, session management, input validation -2. **Identify developer choice points**: Where can developers select algorithms, configure timeouts, choose modes? -3. **Find configuration schemas**: Environment variables, config files, constructor parameters - -### Phase 2: Edge Case Probing - -For each choice point, ask: -- **Zero/empty/null**: What happens with `0`, `""`, `null`, `[]`? -- **Negative values**: What does `-1` mean? Infinite? Error? -- **Type confusion**: Can different security concepts be swapped? -- **Default values**: Is the default secure? Is it documented? -- **Error paths**: What happens on invalid input? Silent acceptance? - -### Phase 3: Threat Modeling - -Consider three adversaries: - -1. **The Scoundrel**: Actively malicious developer or attacker controlling config - - Can they disable security via configuration? - - Can they downgrade algorithms? - - Can they inject malicious values? - -2. **The Lazy Developer**: Copy-pastes examples, skips documentation - - Will the first example they find be secure? - - Is the path of least resistance secure? - - Do error messages guide toward secure usage? - -3. **The Confused Developer**: Misunderstands the API - - Can they swap parameters without type errors? - - Can they use the wrong key/algorithm/mode by accident? - - Are failure modes obvious or silent? - -### Phase 4: Validate Findings - -For each identified sharp edge: - -1. **Reproduce the misuse**: Write minimal code demonstrating the footgun -2. **Verify exploitability**: Does the misuse create a real vulnerability? -3. **Check documentation**: Is the danger documented? (Documentation doesn't excuse bad design, but affects severity) -4. **Test mitigations**: Can the API be used safely with reasonable effort? - -If a finding seems questionable, return to Phase 2 and probe more edge cases. - -## Severity Classification - -| Severity | Criteria | Examples | -|----------|----------|----------| -| Critical | Default or obvious usage is insecure | `verify: false` default; empty password allowed | -| High | Easy misconfiguration breaks security | Algorithm parameter accepts "none" | -| Medium | Unusual but possible misconfiguration | Negative timeout has unexpected meaning | -| Low | Requires deliberate misuse | Obscure parameter combination | - -## References - -**By category:** - -- **Cryptographic APIs**: See [references/crypto-apis.md](references/crypto-apis.md) -- **Configuration Patterns**: See [references/config-patterns.md](references/config-patterns.md) -- **Authentication/Session**: See [references/auth-patterns.md](references/auth-patterns.md) -- **Real-World Case Studies**: See [references/case-studies.md](references/case-studies.md) (OpenSSL, GMP, etc.) - -**By language** (general footguns, not crypto-specific): - -| Language | Guide | -|----------|-------| -| C/C++ | [references/lang-c.md](references/lang-c.md) | -| Go | [references/lang-go.md](references/lang-go.md) | -| Rust | [references/lang-rust.md](references/lang-rust.md) | -| Swift | [references/lang-swift.md](references/lang-swift.md) | -| Java | [references/lang-java.md](references/lang-java.md) | -| Kotlin | [references/lang-kotlin.md](references/lang-kotlin.md) | -| C# | [references/lang-csharp.md](references/lang-csharp.md) | -| PHP | [references/lang-php.md](references/lang-php.md) | -| JavaScript/TypeScript | [references/lang-javascript.md](references/lang-javascript.md) | -| Python | [references/lang-python.md](references/lang-python.md) | -| Ruby | [references/lang-ruby.md](references/lang-ruby.md) | - -See also [references/language-specific.md](references/language-specific.md) for a combined quick reference. - -## Quality Checklist - -Before concluding analysis: - -- [ ] Probed all zero/empty/null edge cases -- [ ] Verified defaults are secure -- [ ] Checked for algorithm/mode selection footguns -- [ ] Tested type confusion between security concepts -- [ ] Considered all three adversary types -- [ ] Verified error paths don't bypass security -- [ ] Checked configuration validation -- [ ] Constructor params validated (not just defaulted) - see [config-patterns.md](references/config-patterns.md#unvalidated-constructor-parameters) diff --git a/.claude/skills/static-analysis/codeql/SKILL.md b/.claude/skills/static-analysis/codeql/SKILL.md deleted file mode 100644 index 7d87d2a..0000000 --- a/.claude/skills/static-analysis/codeql/SKILL.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: codeql -description: >- - Runs CodeQL static analysis for security vulnerability detection - using interprocedural data flow and taint tracking. Applicable when - finding vulnerabilities, running a security scan, performing a security - audit, running CodeQL, building a CodeQL database, selecting query - rulesets, creating data extension models, or processing CodeQL SARIF - output. NOT for writing custom QL queries or CI/CD pipeline setup. -allowed-tools: - - Bash - - Read - - Write - - Glob - - Grep - - AskUserQuestion - - Task - - TaskCreate - - TaskList - - TaskUpdate ---- - -# CodeQL Analysis - -Supported languages: Python, JavaScript/TypeScript, Go, Java/Kotlin, C/C++, C#, Ruby, Swift. - -**Skill resources:** Reference files and templates are located at `{baseDir}/references/` and `{baseDir}/workflows/`. Use `{baseDir}` to resolve paths to these files at runtime. - -## Quick Start - -For the common case ("scan this codebase for vulnerabilities"): - -```bash -# 1. Verify CodeQL is installed -command -v codeql >/dev/null 2>&1 && codeql --version || echo "NOT INSTALLED" - -# 2. Check for existing database -ls -dt codeql_*.db 2>/dev/null | head -1 -``` - -Then execute the full pipeline: **build database → create data extensions → run analysis** using the workflows below. - -## When to Use - -- Scanning a codebase for security vulnerabilities with deep data flow analysis -- Building a CodeQL database from source code (with build capability for compiled languages) -- Finding complex vulnerabilities that require interprocedural taint tracking or AST/CFG analysis -- Performing comprehensive security audits with multiple query packs - -## When NOT to Use - -- **Writing custom queries** - Use a dedicated query development skill -- **CI/CD integration** - Use GitHub Actions documentation directly -- **Quick pattern searches** - Use Semgrep or grep for speed -- **No build capability** for compiled languages - Consider Semgrep instead -- **Single-file or lightweight analysis** - Semgrep is faster for simple pattern matching - -## Rationalizations to Reject - -These shortcuts lead to missed findings. Do not accept them: - -- **"security-extended is enough"** - It is the baseline. Always check if Trail of Bits packs and Community Packs are available for the language. They catch categories `security-extended` misses entirely. -- **"The database built, so it's good"** - A database that builds does not mean it extracted well. Always run Step 4 (quality assessment) and check file counts against expected source files. A cached build produces zero useful extraction. -- **"Data extensions aren't needed for standard frameworks"** - Even Django/Spring apps have custom wrappers around ORM calls, request parsing, or shell execution that CodeQL does not model. Skipping the extensions workflow means missing vulnerabilities in project-specific code. -- **"build-mode=none is fine for compiled languages"** - It produces severely incomplete analysis. No interprocedural data flow through compiled code is traced. Only use as an absolute last resort and clearly flag the limitation. -- **"No findings means the code is secure"** - Zero findings can indicate poor database quality, missing models, or wrong query packs. Investigate before reporting clean results. -- **"I'll just run the default suite"** - The default suite varies by how CodeQL is invoked. Always explicitly specify the suite (e.g., `security-extended`) so results are reproducible. - ---- - -## Workflow Selection - -This skill has three workflows: - -| Workflow | Purpose | -|----------|---------| -| [build-database](workflows/build-database.md) | Create CodeQL database using 3 build methods in sequence | -| [create-data-extensions](workflows/create-data-extensions.md) | Detect or generate data extension models for project APIs | -| [run-analysis](workflows/run-analysis.md) | Select rulesets, execute queries, process results | - - -### Auto-Detection Logic - -**If user explicitly specifies** what to do (e.g., "build a database", "run analysis"), execute that workflow. - -**Default pipeline for "test", "scan", "analyze", or similar:** Execute all three workflows sequentially: build → extensions → analysis. The create-data-extensions step is critical for finding vulnerabilities in projects with custom frameworks or annotations that CodeQL doesn't model by default. - -```bash -# Check if database exists -DB=$(ls -dt codeql_*.db 2>/dev/null | head -1) -if [ -n "$DB" ] && codeql resolve database -- "$DB" >/dev/null 2>&1; then - echo "DATABASE EXISTS ($DB) - can run analysis" -else - echo "NO DATABASE - need to build first" -fi -``` - -| Condition | Action | -|-----------|--------| -| No database exists | Execute build → extensions → analysis (full pipeline) | -| Database exists, no extensions | Execute extensions → analysis | -| Database exists, extensions exist | Ask user: run analysis on existing DB, or rebuild? | -| User says "just run analysis" or "skip extensions" | Run analysis only | - - -### Decision Prompt - -If unclear, ask user: - -``` -I can help with CodeQL analysis. What would you like to do? - -1. **Full scan (Recommended)** - Build database, create extensions, then run analysis -2. **Build database** - Create a new CodeQL database from this codebase -3. **Create data extensions** - Generate custom source/sink models for project APIs -4. **Run analysis** - Run security queries on existing database - -[If database exists: "I found an existing database at "] -``` diff --git a/.claude/skills/static-analysis/sarif-parsing/SKILL.md b/.claude/skills/static-analysis/sarif-parsing/SKILL.md deleted file mode 100644 index 577c6dc..0000000 --- a/.claude/skills/static-analysis/sarif-parsing/SKILL.md +++ /dev/null @@ -1,479 +0,0 @@ ---- -name: sarif-parsing -description: Parse, analyze, and process SARIF (Static Analysis Results Interchange Format) files. Use when reading security scan results, aggregating findings from multiple tools, deduplicating alerts, extracting specific vulnerabilities, or integrating SARIF data into CI/CD pipelines. -allowed-tools: - - Bash - - Read - - Glob - - Grep ---- - -# SARIF Parsing Best Practices - -You are a SARIF parsing expert. Your role is to help users effectively read, analyze, and process SARIF files from static analysis tools. - -## When to Use - -Use this skill when: -- Reading or interpreting static analysis scan results in SARIF format -- Aggregating findings from multiple security tools -- Deduplicating or filtering security alerts -- Extracting specific vulnerabilities from SARIF files -- Integrating SARIF data into CI/CD pipelines -- Converting SARIF output to other formats - -## When NOT to Use - -Do NOT use this skill for: -- Running static analysis scans (use CodeQL or Semgrep skills instead) -- Writing CodeQL or Semgrep rules (use their respective skills) -- Analyzing source code directly (SARIF is for processing existing scan results) -- Triaging findings without SARIF input (use variant-analysis or audit skills) - -## SARIF Structure Overview - -SARIF 2.1.0 is the current OASIS standard. Every SARIF file has this hierarchical structure: - -``` -sarifLog -├── version: "2.1.0" -├── $schema: (optional, enables IDE validation) -└── runs[] (array of analysis runs) - ├── tool - │ ├── driver - │ │ ├── name (required) - │ │ ├── version - │ │ └── rules[] (rule definitions) - │ └── extensions[] (plugins) - ├── results[] (findings) - │ ├── ruleId - │ ├── level (error/warning/note) - │ ├── message.text - │ ├── locations[] - │ │ └── physicalLocation - │ │ ├── artifactLocation.uri - │ │ └── region (startLine, startColumn, etc.) - │ ├── fingerprints{} - │ └── partialFingerprints{} - └── artifacts[] (scanned files metadata) -``` - -### Why Fingerprinting Matters - -Without stable fingerprints, you can't track findings across runs: - -- **Baseline comparison**: "Is this a new finding or did we see it before?" -- **Regression detection**: "Did this PR introduce new vulnerabilities?" -- **Suppression**: "Ignore this known false positive in future runs" - -Tools report different paths (`/path/to/project/` vs `/github/workspace/`), so path-based matching fails. Fingerprints hash the *content* (code snippet, rule ID, relative location) to create stable identifiers regardless of environment. - -## Tool Selection Guide - -| Use Case | Tool | Installation | -|----------|------|--------------| -| Quick CLI queries | jq | `brew install jq` / `apt install jq` | -| Python scripting (simple) | pysarif | `pip install pysarif` | -| Python scripting (advanced) | sarif-tools | `pip install sarif-tools` | -| .NET applications | SARIF SDK | NuGet package | -| JavaScript/Node.js | sarif-js | npm package | -| Go applications | garif | `go get github.com/chavacava/garif` | -| Validation | SARIF Validator | sarifweb.azurewebsites.net | - -## Strategy 1: Quick Analysis with jq - -For rapid exploration and one-off queries: - -```bash -# Pretty print the file -jq '.' results.sarif - -# Count total findings -jq '[.runs[].results[]] | length' results.sarif - -# List all rule IDs triggered -jq '[.runs[].results[].ruleId] | unique' results.sarif - -# Extract errors only -jq '.runs[].results[] | select(.level == "error")' results.sarif - -# Get findings with file locations -jq '.runs[].results[] | { - rule: .ruleId, - message: .message.text, - file: .locations[0].physicalLocation.artifactLocation.uri, - line: .locations[0].physicalLocation.region.startLine -}' results.sarif - -# Filter by severity and get count per rule -jq '[.runs[].results[] | select(.level == "error")] | group_by(.ruleId) | map({rule: .[0].ruleId, count: length})' results.sarif - -# Extract findings for a specific file -jq --arg file "src/auth.py" '.runs[].results[] | select(.locations[].physicalLocation.artifactLocation.uri | contains($file))' results.sarif -``` - -## Strategy 2: Python with pysarif - -For programmatic access with full object model: - -```python -from pysarif import load_from_file, save_to_file - -# Load SARIF file -sarif = load_from_file("results.sarif") - -# Iterate through runs and results -for run in sarif.runs: - tool_name = run.tool.driver.name - print(f"Tool: {tool_name}") - - for result in run.results: - print(f" [{result.level}] {result.rule_id}: {result.message.text}") - - if result.locations: - loc = result.locations[0].physical_location - if loc and loc.artifact_location: - print(f" File: {loc.artifact_location.uri}") - if loc.region: - print(f" Line: {loc.region.start_line}") - -# Save modified SARIF -save_to_file(sarif, "modified.sarif") -``` - -## Strategy 3: Python with sarif-tools - -For aggregation, reporting, and CI/CD integration: - -```python -from sarif import loader - -# Load single file -sarif_data = loader.load_sarif_file("results.sarif") - -# Or load multiple files -sarif_set = loader.load_sarif_files(["tool1.sarif", "tool2.sarif"]) - -# Get summary report -report = sarif_data.get_report() - -# Get histogram by severity -errors = report.get_issue_type_histogram_for_severity("error") -warnings = report.get_issue_type_histogram_for_severity("warning") - -# Filter results -high_severity = [r for r in sarif_data.get_results() - if r.get("level") == "error"] -``` - -**sarif-tools CLI commands:** - -```bash -# Summary of findings -sarif summary results.sarif - -# List all results with details -sarif ls results.sarif - -# Get results by severity -sarif ls --level error results.sarif - -# Diff two SARIF files (find new/fixed issues) -sarif diff baseline.sarif current.sarif - -# Convert to other formats -sarif csv results.sarif > results.csv -sarif html results.sarif > report.html -``` - -## Strategy 4: Aggregating Multiple SARIF Files - -When combining results from multiple tools: - -```python -import json -from pathlib import Path - -def aggregate_sarif_files(sarif_paths: list[str]) -> dict: - """Combine multiple SARIF files into one.""" - aggregated = { - "version": "2.1.0", - "$schema": "https://json.schemastore.org/sarif-2.1.0.json", - "runs": [] - } - - for path in sarif_paths: - with open(path) as f: - sarif = json.load(f) - aggregated["runs"].extend(sarif.get("runs", [])) - - return aggregated - -def deduplicate_results(sarif: dict) -> dict: - """Remove duplicate findings based on fingerprints.""" - seen_fingerprints = set() - - for run in sarif["runs"]: - unique_results = [] - for result in run.get("results", []): - # Use partialFingerprints or create key from location - fp = None - if result.get("partialFingerprints"): - fp = tuple(sorted(result["partialFingerprints"].items())) - elif result.get("fingerprints"): - fp = tuple(sorted(result["fingerprints"].items())) - else: - # Fallback: create fingerprint from rule + location - loc = result.get("locations", [{}])[0] - phys = loc.get("physicalLocation", {}) - fp = ( - result.get("ruleId"), - phys.get("artifactLocation", {}).get("uri"), - phys.get("region", {}).get("startLine") - ) - - if fp not in seen_fingerprints: - seen_fingerprints.add(fp) - unique_results.append(result) - - run["results"] = unique_results - - return sarif -``` - -## Strategy 5: Extracting Actionable Data - -```python -import json -from dataclasses import dataclass -from typing import Optional - -@dataclass -class Finding: - rule_id: str - level: str - message: str - file_path: Optional[str] - start_line: Optional[int] - end_line: Optional[int] - fingerprint: Optional[str] - -def extract_findings(sarif_path: str) -> list[Finding]: - """Extract structured findings from SARIF file.""" - with open(sarif_path) as f: - sarif = json.load(f) - - findings = [] - for run in sarif.get("runs", []): - for result in run.get("results", []): - loc = result.get("locations", [{}])[0] - phys = loc.get("physicalLocation", {}) - region = phys.get("region", {}) - - findings.append(Finding( - rule_id=result.get("ruleId", "unknown"), - level=result.get("level", "warning"), - message=result.get("message", {}).get("text", ""), - file_path=phys.get("artifactLocation", {}).get("uri"), - start_line=region.get("startLine"), - end_line=region.get("endLine"), - fingerprint=next(iter(result.get("partialFingerprints", {}).values()), None) - )) - - return findings - -# Filter and prioritize -def prioritize_findings(findings: list[Finding]) -> list[Finding]: - """Sort findings by severity.""" - severity_order = {"error": 0, "warning": 1, "note": 2, "none": 3} - return sorted(findings, key=lambda f: severity_order.get(f.level, 99)) -``` - -## Common Pitfalls and Solutions - -### 1. Path Normalization Issues - -Different tools report paths differently (absolute, relative, URI-encoded): - -```python -from urllib.parse import unquote -from pathlib import Path - -def normalize_path(uri: str, base_path: str = "") -> str: - """Normalize SARIF artifact URI to consistent path.""" - # Remove file:// prefix if present - if uri.startswith("file://"): - uri = uri[7:] - - # URL decode - uri = unquote(uri) - - # Handle relative paths - if not Path(uri).is_absolute() and base_path: - uri = str(Path(base_path) / uri) - - # Normalize separators - return str(Path(uri)) -``` - -### 2. Fingerprint Mismatch Across Runs - -Fingerprints may not match if: -- File paths differ between environments -- Tool versions changed fingerprinting algorithm -- Code was reformatted (changing line numbers) - -**Solution:** Use multiple fingerprint strategies: - -```python -def compute_stable_fingerprint(result: dict, file_content: str = None) -> str: - """Compute environment-independent fingerprint.""" - import hashlib - - components = [ - result.get("ruleId", ""), - result.get("message", {}).get("text", "")[:100], # First 100 chars - ] - - # Add code snippet if available - if file_content and result.get("locations"): - region = result["locations"][0].get("physicalLocation", {}).get("region", {}) - if region.get("startLine"): - lines = file_content.split("\n") - line_idx = region["startLine"] - 1 - if 0 <= line_idx < len(lines): - # Normalize whitespace - components.append(lines[line_idx].strip()) - - return hashlib.sha256("".join(components).encode()).hexdigest()[:16] -``` - -### 3. Missing or Incomplete Data - -SARIF allows many optional fields. Always use defensive access: - -```python -def safe_get_location(result: dict) -> tuple[str, int]: - """Safely extract file and line from result.""" - try: - loc = result.get("locations", [{}])[0] - phys = loc.get("physicalLocation", {}) - file_path = phys.get("artifactLocation", {}).get("uri", "unknown") - line = phys.get("region", {}).get("startLine", 0) - return file_path, line - except (IndexError, KeyError, TypeError): - return "unknown", 0 -``` - -### 4. Large File Performance - -For very large SARIF files (100MB+): - -```python -import ijson # pip install ijson - -def stream_results(sarif_path: str): - """Stream results without loading entire file.""" - with open(sarif_path, "rb") as f: - # Stream through results arrays - for result in ijson.items(f, "runs.item.results.item"): - yield result -``` - -### 5. Schema Validation - -Validate before processing to catch malformed files: - -```bash -# Using ajv-cli -npm install -g ajv-cli -ajv validate -s sarif-schema-2.1.0.json -d results.sarif - -# Using Python jsonschema -pip install jsonschema -``` - -```python -from jsonschema import validate, ValidationError -import json - -def validate_sarif(sarif_path: str, schema_path: str) -> bool: - """Validate SARIF file against schema.""" - with open(sarif_path) as f: - sarif = json.load(f) - with open(schema_path) as f: - schema = json.load(f) - - try: - validate(sarif, schema) - return True - except ValidationError as e: - print(f"Validation error: {e.message}") - return False -``` - -## CI/CD Integration Patterns - -### GitHub Actions - -```yaml -- name: Upload SARIF - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif - -- name: Check for high severity - run: | - HIGH_COUNT=$(jq '[.runs[].results[] | select(.level == "error")] | length' results.sarif) - if [ "$HIGH_COUNT" -gt 0 ]; then - echo "Found $HIGH_COUNT high severity issues" - exit 1 - fi -``` - -### Fail on New Issues - -```python -from sarif import loader - -def check_for_regressions(baseline: str, current: str) -> int: - """Return count of new issues not in baseline.""" - baseline_data = loader.load_sarif_file(baseline) - current_data = loader.load_sarif_file(current) - - baseline_fps = {get_fingerprint(r) for r in baseline_data.get_results()} - new_issues = [r for r in current_data.get_results() - if get_fingerprint(r) not in baseline_fps] - - return len(new_issues) -``` - -## Key Principles - -1. **Validate first**: Check SARIF structure before processing -2. **Handle optionals**: Many fields are optional; use defensive access -3. **Normalize paths**: Tools report paths differently; normalize early -4. **Fingerprint wisely**: Combine multiple strategies for stable deduplication -5. **Stream large files**: Use ijson or similar for 100MB+ files -6. **Aggregate thoughtfully**: Preserve tool metadata when combining files - -## Skill Resources - -For ready-to-use query templates, see [{baseDir}/resources/jq-queries.md]({baseDir}/resources/jq-queries.md): -- 40+ jq queries for common SARIF operations -- Severity filtering, rule extraction, aggregation patterns - -For Python utilities, see [{baseDir}/resources/sarif_helpers.py]({baseDir}/resources/sarif_helpers.py): -- `normalize_path()` - Handle tool-specific path formats -- `compute_fingerprint()` - Stable fingerprinting ignoring paths -- `deduplicate_results()` - Remove duplicates across runs - -## Reference Links - -- [OASIS SARIF 2.1.0 Specification](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) -- [Microsoft SARIF Tutorials](https://github.com/microsoft/sarif-tutorials) -- [SARIF SDK (.NET)](https://github.com/microsoft/sarif-sdk) -- [sarif-tools (Python)](https://github.com/microsoft/sarif-tools) -- [pysarif (Python)](https://github.com/Kjeld-P/pysarif) -- [GitHub SARIF Support](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning) -- [SARIF Validator](https://sarifweb.azurewebsites.net/) diff --git a/.claude/skills/static-analysis/semgrep/SKILL.md b/.claude/skills/static-analysis/semgrep/SKILL.md deleted file mode 100644 index ca9e28f..0000000 --- a/.claude/skills/static-analysis/semgrep/SKILL.md +++ /dev/null @@ -1,417 +0,0 @@ ---- -name: semgrep -description: Run Semgrep static analysis scan on a codebase using parallel subagents. Automatically - detects and uses Semgrep Pro for cross-file analysis when available. Use when asked to scan - code for vulnerabilities, run a security audit with Semgrep, find bugs, or perform - static analysis. Spawns parallel workers for multi-language codebases and triage. -allowed-tools: - - Bash - - Read - - Glob - - Grep - - Write - - Task - - AskUserQuestion - - TaskCreate - - TaskList - - TaskUpdate - - WebFetch ---- - -# Semgrep Security Scan - -Run a complete Semgrep scan with automatic language detection, parallel execution via Task subagents, and parallel triage. Automatically uses Semgrep Pro for cross-file taint analysis when available. - -## Prerequisites - -**Required:** Semgrep CLI - -```bash -semgrep --version -``` - -If not installed, see [Semgrep installation docs](https://semgrep.dev/docs/getting-started/). - -**Optional:** Semgrep Pro (for cross-file analysis and Pro languages) - -```bash -# Check if Semgrep Pro engine is installed -semgrep --pro --validate --config p/default 2>/dev/null && echo "Pro available" || echo "OSS only" - -# If logged in, install/update Pro Engine -semgrep install-semgrep-pro -``` - -Pro enables: cross-file taint tracking, inter-procedural analysis, and additional languages (Apex, C#, Elixir). - -## When to Use - -- Security audit of a codebase -- Finding vulnerabilities before code review -- Scanning for known bug patterns -- First-pass static analysis - -## When NOT to Use - -- Binary analysis → Use binary analysis tools -- Already have Semgrep CI configured → Use existing pipeline -- Need cross-file analysis but no Pro license → Consider CodeQL as alternative -- Creating custom Semgrep rules → Use `semgrep-rule-creator` skill -- Porting existing rules to other languages → Use `semgrep-rule-variant-creator` skill - ---- - -## Orchestration Architecture - -This skill uses **parallel Task subagents** for maximum efficiency: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ MAIN AGENT │ -│ 1. Detect languages + check Pro availability │ -│ 2. Select rulesets based on detection (ref: rulesets.md) │ -│ 3. Present plan + rulesets, get approval [⛔ HARD GATE] │ -│ 4. Spawn parallel scan Tasks (with approved rulesets) │ -│ 5. Spawn parallel triage Tasks │ -│ 6. Collect and report results │ -└─────────────────────────────────────────────────────────────────┘ - │ Step 4 │ Step 5 - ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ -│ Scan Tasks │ │ Triage Tasks │ -│ (parallel) │ │ (parallel) │ -├─────────────────┤ ├─────────────────┤ -│ Python scanner │ │ Python triager │ -│ JS/TS scanner │ │ JS/TS triager │ -│ Go scanner │ │ Go triager │ -│ Docker scanner │ │ Docker triager │ -└─────────────────┘ └─────────────────┘ -``` - ---- - -## Workflow Enforcement via Task System - -This skill uses the **Task system** to enforce workflow compliance. On invocation, create these tasks: - -``` -TaskCreate: "Detect languages and Pro availability" (Step 1) -TaskCreate: "Select rulesets based on detection" (Step 2) - blockedBy: Step 1 -TaskCreate: "Present plan with rulesets, get approval" (Step 3) - blockedBy: Step 2 -TaskCreate: "Execute scans with approved rulesets" (Step 4) - blockedBy: Step 3 -TaskCreate: "Triage findings" (Step 5) - blockedBy: Step 4 -TaskCreate: "Report results" (Step 6) - blockedBy: Step 5 -``` - -### Mandatory Gates - -| Task | Gate Type | Cannot Proceed Until | -|------|-----------|---------------------| -| Step 3: Get approval | **HARD GATE** | User explicitly approves rulesets + plan | -| Step 5: Triage | **SOFT GATE** | All scan JSON files exist | - -**Step 3 is a HARD GATE**: Mark as `completed` ONLY after user says "yes", "proceed", "approved", or equivalent. - -### Task Flow Example - -``` -1. Create all 6 tasks with dependencies -2. TaskUpdate Step 1 → in_progress, execute detection -3. TaskUpdate Step 1 → completed -4. TaskUpdate Step 2 → in_progress, select rulesets -5. TaskUpdate Step 2 → completed -6. TaskUpdate Step 3 → in_progress, present plan with rulesets -7. STOP: Wait for user response (may modify rulesets) -8. User approves → TaskUpdate Step 3 → completed -9. TaskUpdate Step 4 → in_progress (now unblocked) -... continue workflow -``` - ---- - -## Workflow - -### Step 1: Detect Languages and Pro Availability (Main Agent) - -```bash -# Check if Semgrep Pro is available (non-destructive check) -SEMGREP_PRO=false -if semgrep --pro --validate --config p/default 2>/dev/null; then - SEMGREP_PRO=true - echo "Semgrep Pro: AVAILABLE (cross-file analysis enabled)" -else - echo "Semgrep Pro: NOT AVAILABLE (OSS mode, single-file analysis)" -fi - -# Find languages by file extension -fd -t f -e py -e js -e ts -e jsx -e tsx -e go -e rb -e java -e php -e c -e cpp -e rs | \ - sed 's/.*\.//' | sort | uniq -c | sort -rn - -# Check for frameworks/technologies -ls -la package.json pyproject.toml Gemfile go.mod Cargo.toml pom.xml 2>/dev/null -fd -t f "Dockerfile" "docker-compose" ".tf" "*.yaml" "*.yml" | head -20 -``` - -Map findings to categories: - -| Detection | Category | -|-----------|----------| -| `.py`, `pyproject.toml` | Python | -| `.js`, `.ts`, `package.json` | JavaScript/TypeScript | -| `.go`, `go.mod` | Go | -| `.rb`, `Gemfile` | Ruby | -| `.java`, `pom.xml` | Java | -| `.php` | PHP | -| `.c`, `.cpp` | C/C++ | -| `.rs`, `Cargo.toml` | Rust | -| `Dockerfile` | Docker | -| `.tf` | Terraform | -| k8s manifests | Kubernetes | - -### Step 2: Select Rulesets Based on Detection - -Using the detected languages and frameworks from Step 1, select rulesets by following the **Ruleset Selection Algorithm** in [rulesets.md]({baseDir}/references/rulesets.md). - -The algorithm covers: -1. Security baseline (always included) -2. Language-specific rulesets -3. Framework rulesets (if detected) -4. Infrastructure rulesets -5. **Required** third-party rulesets (Trail of Bits, 0xdea, Decurity - NOT optional) -6. Registry verification - -**Output:** Structured JSON passed to Step 3 for user review: - -```json -{ - "baseline": ["p/security-audit", "p/secrets"], - "python": ["p/python", "p/django"], - "javascript": ["p/javascript", "p/react", "p/nodejs"], - "docker": ["p/dockerfile"], - "third_party": ["https://github.com/trailofbits/semgrep-rules"] -} -``` - -### Step 3: CRITICAL GATE - Present Plan and Get Approval - -> **⛔ MANDATORY CHECKPOINT - DO NOT SKIP** -> -> This step requires explicit user approval before proceeding. -> User may modify rulesets before approving. - -Present plan to user with **explicit ruleset listing**: - -``` -## Semgrep Scan Plan - -**Target:** /path/to/codebase -**Output directory:** ./semgrep-results-001/ -**Engine:** Semgrep Pro (cross-file analysis) | Semgrep OSS (single-file) - -### Detected Languages/Technologies: -- Python (1,234 files) - Django framework detected -- JavaScript (567 files) - React detected -- Dockerfile (3 files) - -### Rulesets to Run: - -**Security Baseline (always included):** -- [x] `p/security-audit` - Comprehensive security rules -- [x] `p/secrets` - Hardcoded credentials, API keys - -**Python (1,234 files):** -- [x] `p/python` - Python security patterns -- [x] `p/django` - Django-specific vulnerabilities - -**JavaScript (567 files):** -- [x] `p/javascript` - JavaScript security patterns -- [x] `p/react` - React-specific issues -- [x] `p/nodejs` - Node.js server-side patterns - -**Docker (3 files):** -- [x] `p/dockerfile` - Dockerfile best practices - -**Third-party (auto-included for detected languages):** -- [x] Trail of Bits rules - https://github.com/trailofbits/semgrep-rules - -**Available but not selected:** -- [ ] `p/owasp-top-ten` - OWASP Top 10 (overlaps with security-audit) - -### Execution Strategy: -- Spawn 3 parallel scan Tasks (Python, JavaScript, Docker) -- Total rulesets: 9 -- [If Pro] Cross-file taint tracking enabled - -**Want to modify rulesets?** Tell me which to add or remove. -**Ready to scan?** Say "proceed" or "yes". -``` - -**⛔ STOP: Await explicit user approval** - -After presenting the plan: - -1. **If user wants to modify rulesets:** - - Add requested rulesets to the appropriate category - - Remove requested rulesets - - Re-present the updated plan - - Return to waiting for approval - -2. **Use AskUserQuestion** if user hasn't responded: - ``` - "I've prepared the scan plan with 9 rulesets (including Trail of Bits). Proceed with scanning?" - Options: ["Yes, run scan", "Modify rulesets first"] - ``` - -3. **Valid approval responses:** - - "yes", "proceed", "approved", "go ahead", "looks good", "run it" - -4. **Mark task completed** only after approval with final rulesets confirmed - -5. **Do NOT treat as approval:** - - User's original request ("scan this codebase") - - Silence / no response - - Questions about the plan - -### Pre-Scan Checklist - -Before marking Step 3 complete, verify: -- [ ] Target directory shown to user -- [ ] Engine type (Pro/OSS) displayed -- [ ] Languages detected and listed -- [ ] **All rulesets explicitly listed with checkboxes** -- [ ] User given opportunity to modify rulesets -- [ ] User explicitly approved (quote their confirmation) -- [ ] **Final ruleset list captured for Step 4** - -### Step 4: Spawn Parallel Scan Tasks - -Create output directory with run number to avoid collisions, then spawn Tasks with **approved rulesets from Step 3**: - -```bash -# Find next available run number -LAST=$(ls -d semgrep-results-[0-9][0-9][0-9] 2>/dev/null | sort | tail -1 | grep -o '[0-9]*$' || true) -NEXT_NUM=$(printf "%03d" $(( ${LAST:-0} + 1 ))) -OUTPUT_DIR="semgrep-results-${NEXT_NUM}" -mkdir -p "$OUTPUT_DIR" -echo "Output directory: $OUTPUT_DIR" -``` - -**Spawn N Tasks in a SINGLE message** (one per language category) using `subagent_type: Bash`. - -Use the scanner task prompt template from [scanner-task-prompt.md]({baseDir}/references/scanner-task-prompt.md). - -**Example - 3 Language Scan (with approved rulesets):** - -Spawn these 3 Tasks in a SINGLE message: - -1. **Task: Python Scanner** - - Approved rulesets: p/python, p/django, p/security-audit, p/secrets, https://github.com/trailofbits/semgrep-rules - - Output: semgrep-results-001/python-*.json - -2. **Task: JavaScript Scanner** - - Approved rulesets: p/javascript, p/react, p/nodejs, p/security-audit, p/secrets, https://github.com/trailofbits/semgrep-rules - - Output: semgrep-results-001/js-*.json - -3. **Task: Docker Scanner** - - Approved rulesets: p/dockerfile - - Output: semgrep-results-001/docker-*.json - -### Step 5: Spawn Parallel Triage Tasks - -After scan Tasks complete, spawn triage Tasks using `subagent_type: general-purpose` (triage requires reading code context, not just running commands). - -Use the triage task prompt template from [triage-task-prompt.md]({baseDir}/references/triage-task-prompt.md). - -### Step 6: Collect Results (Main Agent) - -After all Tasks complete, generate merged SARIF and report: - -**Generate merged SARIF with only triaged true positives:** - -```bash -uv run {baseDir}/scripts/merge_triaged_sarif.py [OUTPUT_DIR] -``` - -This script: -1. Attempts to use [SARIF Multitool](https://www.npmjs.com/package/@microsoft/sarif-multitool) for merging (if `npx` is available) -2. Falls back to pure Python merge if Multitool unavailable -3. Reads all `*-triage.json` files to extract true positive findings -4. Filters merged SARIF to include only triaged true positives -5. Writes output to `[OUTPUT_DIR]/findings-triaged.sarif` - -**Optional: Install SARIF Multitool for better merge quality:** - -```bash -npm install -g @microsoft/sarif-multitool -``` - -**Report to user:** - -``` -## Semgrep Scan Complete - -**Scanned:** 1,804 files -**Rulesets used:** 9 (including Trail of Bits) -**Total raw findings:** 156 -**After triage:** 32 true positives - -### By Severity: -- ERROR: 5 -- WARNING: 18 -- INFO: 9 - -### By Category: -- SQL Injection: 3 -- XSS: 7 -- Hardcoded secrets: 2 -- Insecure configuration: 12 -- Code quality: 8 - -Results written to: -- semgrep-results-001/findings-triaged.sarif (SARIF, true positives only) -- semgrep-results-001/*-triage.json (triage details per language) -- semgrep-results-001/*.json (raw scan results) -- semgrep-results-001/*.sarif (raw SARIF per ruleset) -``` - ---- - -## Common Mistakes - -| Mistake | Correct Approach | -|---------|------------------| -| Running without `--metrics=off` | Always use `--metrics=off` to prevent telemetry | -| Running rulesets sequentially | Run in parallel with `&` and `wait` | -| Not scoping rulesets to languages | Use `--include="*.py"` for language-specific rules | -| Reporting raw findings without triage | Always triage to remove false positives | -| Single-threaded for multi-lang | Spawn parallel Tasks per language | -| Sequential Tasks | Spawn all Tasks in SINGLE message for parallelism | -| Using OSS when Pro is available | Check login status; use `--pro` for deeper analysis | -| Assuming Pro is unavailable | Always check with login detection before scanning | - -## Limitations - -1. **OSS mode:** Cannot track data flow across files (login with `semgrep login` and run `semgrep install-semgrep-pro` to enable) -2. **Pro mode:** Cross-file analysis uses `-j 1` (single job) which is slower per ruleset, but parallel rulesets compensate -3. Triage requires reading code context - parallelized via Tasks -4. Some false positive patterns require human judgment - -## Rationalizations to Reject - -| Shortcut | Why It's Wrong | -|----------|----------------| -| "User asked for scan, that's approval" | Original request ≠ plan approval; user must confirm specific parameters. Present plan, use AskUserQuestion, await explicit "yes" | -| "Step 3 task is blocking, just mark complete" | Lying about task status defeats enforcement. Only mark complete after real approval | -| "I already know what they want" | Assumptions cause scanning wrong directories/rulesets. Present plan with all parameters for verification | -| "Just use default rulesets" | User must see and approve exact rulesets before scan | -| "Add extra rulesets without asking" | Modifying approved list without consent breaks trust | -| "Skip showing ruleset list" | User can't make informed decision without seeing what will run | -| "Third-party rulesets are optional" | Trail of Bits, 0xdea, Decurity rules catch vulnerabilities not in official registry - they are REQUIRED when language matches | -| "Skip triage, report everything" | Floods user with noise; true issues get lost | -| "Run one ruleset at a time" | Wastes time; parallel execution is faster | -| "Use --config auto" | Sends metrics; less control over rulesets | -| "Triage later" | Findings without context are harder to evaluate | -| "One Task at a time" | Defeats parallelism; spawn all Tasks together | -| "Pro is too slow, skip --pro" | Cross-file analysis catches 250% more true positives; worth the time | -| "Don't bother checking for Pro" | Missing Pro = missing critical cross-file vulnerabilities | -| "OSS is good enough" | OSS misses inter-file taint flows; always prefer Pro when available | diff --git a/.claude/skills/story-quality-gate/SKILL.md b/.claude/skills/story-quality-gate/SKILL.md deleted file mode 100644 index 44e90d0..0000000 --- a/.claude/skills/story-quality-gate/SKILL.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -name: ln-500-story-quality-gate -description: "Story-level quality orchestrator with 4-level Gate (PASS/CONCERNS/FAIL/WAIVED) and Quality Score. Pass 1: code quality -> regression -> manual testing. Pass 2: verify tests/coverage -> calculate NFR scores -> mark Story Done. Use when user requests quality gate for Story or when ln-400 delegates quality check." ---- - -# Story Quality Gate - -Two-pass Story review with 4-level Gate verdict, Quality Score calculation, and NFR validation (security, performance, reliability, maintainability). - -## Purpose & Scope -- Pass 1 (after impl tasks Done): run code-quality, lint, regression, and manual testing; if all pass, create/confirm test task; otherwise create targeted fix/refactor tasks and stop. -- Pass 2 (after test task Done): verify tests/coverage/priority limits, calculate Quality Score and NFR validation, close Story to Done or create fix tasks. -- Delegates work to ln-501/ln-502 workers and ln-510-test-planner. - -## 4-Level Gate Model - -| Level | Meaning | Action | -|-------|---------|--------| -| **PASS** | All checks pass, no issues | Story → Done | -| **CONCERNS** | Minor issues, acceptable risk | Story → Done with comment noting concerns | -| **FAIL** | Blocking issues found | Create fix tasks, return to ln-400 | -| **WAIVED** | Issues acknowledged by user | Story → Done with waiver reason documented | - -**Verdict calculation:** `FAIL` if any check fails. `CONCERNS` if minor issues exist. `PASS` if all clean. - -## Quality Score - -Formula: `Quality Score = 100 - (20 × FAIL_count) - (10 × CONCERN_count)` - -| Score Range | Status | Action | -|-------------|--------|--------| -| 90-100 | ✅ Excellent | PASS | -| 70-89 | ⚠️ Acceptable | CONCERNS (proceed with notes) | -| 50-69 | ❌ Below threshold | FAIL (create fix tasks) | -| <50 | 🚨 Critical | FAIL (urgent priority) | - -## NFR Validation - -Evaluate 4 non-functional requirement dimensions: - -| NFR | Checks | Issue Prefix | -|-----|--------|--------------| -| **Security** | Auth, input validation, secrets exposure | SEC- | -| **Performance** | N+1 queries, caching, response times | PERF- | -| **Reliability** | Error handling, retries, timeouts | REL- | -| **Maintainability** | DRY, SOLID, cyclomatic complexity | MNT- | - -Additional prefixes: `TEST-` (coverage gaps), `ARCH-` (architecture issues), `DOC-` (documentation gaps), `DEP-` (Story/Task dependencies), `COV-` (AC coverage quality), `DB-` (database schema), `AC-` (AC validation) - -**NFR verdict per dimension:** PASS / CONCERNS / FAIL - -## When to Use -- Pass 1: all implementation tasks Done; test task missing or not Done. -- Pass 2: test task exists and is Done. -- Explicit `pass` parameter can force 1 or 2; otherwise auto-detect by test task status. - -## Workflow (concise) -- **Phase 1 Discovery:** Auto-discover team/config; select Story; load Story + task metadata (no descriptions), detect test task status. -- **Pass 1 flow (fail fast):** - 1) Invoke ln-501-code-quality-checker. If issues -> create refactor task (Backlog), stop. - 1.5) **Criteria Validation (Story-level checks)** - see `references/criteria_validation.md`: - - Check #1: Story Dependencies (no forward deps within Epic) - if FAIL → create [DEP-] task, stop. - - Check #2: AC-Task Coverage Quality (STRONG/WEAK/MISSING scoring) - if FAIL/CONCERNS → create [BUG-]/[COV-] tasks, stop. - - Check #3: Database Creation Principle (schema scope matches Story) - if FAIL → create [DB-] task, stop. - 2) Run all linters from tech_stack.md. If fail -> create lint-fix task, stop. - 3) Invoke ln-502-regression-checker. If fail -> create regression-fix task, stop. - 4) Invoke ln-510-test-planner (orchestrates: ln-511-test-researcher → ln-512-manual-tester → ln-513-auto-test-planner). If manual testing fails -> create bug-fix task, stop. If all passed -> test task created/updated. - 5) If test task exists and Done, jump to Pass 2; if exists but not Done, report status and stop. -- **Pass 2 flow (after test task Done):** - 1) Load Story/test task; read test plan/results and manual testing comment from Pass 1. - 2) Verify limits and priority: Priority ≤15; E2E 2-5, Integration 0-8, Unit 0-15, total 10-28; tests focus on business logic (no framework/DB/library tests). - 3) Ensure Priority ≤15 scenarios and Story AC are covered by tests; infra/docs updates present. - 4) **Calculate Quality Score and NFR validation** (see formulas above): - - Run NFR checks per dimensions table - - Assign issue prefixes: SEC-, PERF-, REL-, MNT-, TEST-, ARCH-, DOC- - - Calculate Quality Score - 5) **Determine Gate verdict** per 4-Level Gate Model above - -**TodoWrite format (mandatory):** -Add pass steps to todos before starting: -``` -Pass 1: -- Invoke ln-501-code-quality-checker (in_progress) -- Pass 1.5: Criteria Validation (Story deps, AC coverage, DB schema) (pending) -- Run linters from tech_stack.md (pending) -- Invoke ln-502-regression-checker (pending) -- Invoke ln-510-test-planner (research + manual + auto tests) (pending) - -Pass 2: -- Verify test task coverage (in_progress) -- Mark Story Done (pending) -``` -Mark each as in_progress when starting, completed when done. On failure, mark remaining as skipped. - -## Worker Invocation (MANDATORY) - -| Step | Worker | Context | Rationale | -|------|--------|---------|-----------| -| Code Quality | ln-501-code-quality-checker | **Separate** (Task tool) | Independent analysis, focused on DRY/KISS/YAGNI | -| Regression | ln-502-regression-checker | **Shared** (direct Skill tool) | Needs Story context and previous check results | -| Test Planning | ln-510-test-planner | **Shared** (direct Skill tool) | Needs full Gate context for test planning | - -**ln-501 invocation (Separate Context):** -``` -Task(description: "Code quality check via ln-501", - prompt: "Execute ln-501-code-quality-checker. Read skill from ln-501-code-quality-checker/SKILL.md. Story: {storyId}", - subagent_type: "general-purpose") -``` - -**ln-501 result contract (Task tool return):** -Task tool returns worker's final message. Parse for YAML block: -- `verdict: PASS | CONCERNS | ISSUES_FOUND` -- `quality_score: 0-100` -- `issues: [{id, severity, finding, action}]` -- If verdict = ISSUES_FOUND → create refactor task (Backlog), stop Pass 1. - -**ln-502 and ln-510:** Invoke via direct Skill tool — workers see Gate context. - -**Note:** ln-510 orchestrates the full test pipeline (ln-511 research → ln-512 manual → ln-513 auto tests). - -**❌ FORBIDDEN SHORTCUTS (Anti-Patterns):** -- Running `mypy`, `ruff`, `pytest` directly instead of invoking ln-501/ln-502 -- Doing "minimal quality check" (just linters) and skipping ln-510 test planning -- Asking user "Want me to run the full skill?" after doing partial checks -- Marking steps as "completed" in todo without invoking the actual skill -- Any command execution that should be delegated to a worker skill - -**✅ CORRECT BEHAVIOR:** -- Use `Skill(skill: "ln-50X-...")` for EVERY step — NO EXCEPTIONS -- Wait for each skill to complete before proceeding -- If skill fails → create fix task → STOP (fail fast) -- Never bypass skills with "I'll just run the command myself" - -**ZERO TOLERANCE:** If you find yourself running quality commands directly (mypy, ruff, pytest, curl) instead of invoking the appropriate skill, STOP and use Skill tool instead. - -## Critical Rules -- Early-exit: any failure creates a specific task and stops Pass 1/2. -- Single source of truth: rely on Linear metadata for tasks; kanban is updated by workers/ln-400. -- Task creation via skills only (ln-510/ln-301); this skill never edits tasks directly. -- Pass 2 only runs when test task is Done; otherwise return error/status. -- Language preservation in comments (EN/RU). - -## Definition of Done -- Pass 1: ln-501 pass OR refactor task created; linters pass OR lint-fix task created; ln-502 pass OR regression-fix task created; ln-510 pipeline pass (research + manual + auto tests) OR bug-fix task created; test task created/updated; exits. -- Pass 2: test task verified (priority/limits/coverage/infra/docs); Quality Score calculated; NFR validation completed; Gate verdict determined (PASS/CONCERNS/FAIL/WAIVED). -- **Gate output format:** - ```yaml - gate: PASS | CONCERNS | FAIL | WAIVED - quality_score: {0-100} - nfr_validation: - security: PASS | CONCERNS | FAIL - performance: PASS | CONCERNS | FAIL - reliability: PASS | CONCERNS | FAIL - maintainability: PASS | CONCERNS | FAIL - issues: [{id: "SEC-001", severity: high|medium|low, finding: "...", action: "..."}] - ``` -- Story set to Done (PASS/CONCERNS/WAIVED) or fix tasks created (FAIL); comment with gate verdict posted. - -## Reference Files -- **Orchestrator lifecycle:** `shared/references/orchestrator_pattern.md` -- **Task delegation pattern:** `shared/references/task_delegation_pattern.md` -- **AC validation rules:** `shared/references/ac_validation_rules.md` -- Criteria Validation: `references/criteria_validation.md` (Story deps, AC coverage quality, DB schema checks from ln-310) -- Gate levels: `references/gate_levels.md` (detailed scoring rules and thresholds) -- Workers: `../ln-501-code-quality-checker/SKILL.md`, `../ln-502-regression-checker/SKILL.md` -- Test planning orchestrator: `../ln-510-test-planner/SKILL.md` (coordinates ln-511/512/513) -- Tech stack/linters: `docs/project/tech_stack.md` - ---- -**Version:** 6.0.0 (BREAKING: Added Pass 1.5 Criteria Validation with 3 checks from ln-310 - Story dependencies, AC-Task Coverage Quality (STRONG/WEAK/MISSING), Database Creation Principle. New issue prefixes: DEP-, COV-, DB-, AC-. Closes validation-execution gap at Story level per BMAD Method best practices.) -**Last Updated:** 2026-02-03 diff --git a/.claude/skills/storyboard-generator/SKILL.md b/.claude/skills/storyboard-generator/SKILL.md deleted file mode 100755 index 6712c16..0000000 --- a/.claude/skills/storyboard-generator/SKILL.md +++ /dev/null @@ -1,816 +0,0 @@ ---- -name: storyboard-generator -description: 텍스트 기반 인터뷰/기획 문서를 분석하여 IA(메뉴 구조)와 화면 설계 스토리보드 PPTX를 자동 생성합니다. '기획서 생성', '스토리보드 생성', 'IA 생성' 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task ---- - -# Storyboard Generator Skill - 기획서/스토리보드 자동 생성 - -텍스트 기반 인터뷰 문서나 기획 문서를 분석하여 웹 시스템 기획서(IA + 스토리보드)를 PPTX로 자동 생성하는 스킬입니다. - -## 활성화 트리거 - -다음 키워드가 포함된 요청 시 이 스킬이 활성화됩니다: -- "기획서 생성" -- "스토리보드 생성" -- "IA 생성" -- "화면 설계" -- "웹 기획" - -## Instructions - -### 1단계: 입력 파일 분석 - -사용자가 제공한 파일 경로에서 텍스트 문서를 읽고 분석합니다. - -```bash -# 파일 읽기 -cat [파일경로] -``` - -분석 대상: -- 인터뷰 스크립트 (script.md, interview.txt 등) -- 기획 문서 (planning.md, requirements.txt 등) -- 요구사항 문서 - -### 2단계: 데이터 추출 - -텍스트에서 다음 정보를 자동 추출합니다: - -| 추출 항목 | 예시 | -|-----------|------| -| 프로젝트명 | 방화셔터 견적 시스템 | -| 주요 기능 | 셔터 타입 선택, 견적 계산 | -| 메뉴 구조 | 견적 입력 > 셔터 선택, 규격 입력 | -| 화면 요소 | 입력 폼, 테이블, 버튼 | -| 비즈니스 규칙 | 할인율, 계산 공식 | - -### 3단계: 설정 파일 생성 - -추출된 데이터를 기반으로 storyboard-config.json 파일을 생성합니다. - -```json -{ - "projectName": "방화셔터 견적 시스템", - "company": "SAM", - "author": "IT 혁신팀", - "date": "2025.01.09", - "purpose": "프로젝트 목적 설명", - "features": ["기능 1", "기능 2"], - "effects": [ - { "icon": "⏱️", "title": "시간 단축", "desc": "설명" } - ], - "tocItems": [ - { "num": "01", "title": "프로젝트 개요", "desc": "설명" } - ], - "mainMenus": [ - { "title": "메뉴1", "children": ["서브1", "서브2"] } - ], - "screens": [ - { - "taskName": "화면명", - "route": "/path", - "screenName": "화면 이름", - "screenId": "SCREEN_001", - "descriptions": [ - { "title": "항목1", "content": "설명" } - ] - } - ] -} -``` - -### 4단계: HTML 슬라이드 생성 (권장) - -**HTML 기반 방식**을 사용하면 더 정교한 디자인이 가능합니다. - -```bash -# 1단계: HTML 슬라이드 생성 -node ~/.claude/skills/storyboard-generator/scripts/generate-html-storyboard.js \ - --config [설정파일.json] \ - --output [html_slides 폴더] - -# 2단계: HTML → PPTX 변환 -node ~/.claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js \ - --input [html_slides 폴더] \ - --output [출력파일.pptx] -``` - -### 5단계 (대안): 직접 PPTX 생성 - -간단한 경우 직접 PPTX를 생성할 수도 있습니다. - -```bash -node ~/.claude/skills/storyboard-generator/scripts/generate-storyboard.js \ - --config [설정파일.json] \ - --output [출력파일.pptx] -``` - -### 6단계: 결과 확인 - -생성된 PPTX 파일 정보를 사용자에게 안내합니다. - -## 생성되는 슬라이드 구조 - -| 슬라이드 | 내용 | 템플릿 | -|----------|------|--------| -| 1 | 표지 | 프로젝트명, 버전, 날짜 | -| 2 | Document History | 문서 이력 테이블 | -| 3 | 목차 | 섹션 번호 + 제목 | -| 4 | 프로젝트 개요 | 목적, 주요 기능, 기대 효과 | -| 5 | 메뉴 구조 (IA) | 계층형 다이어그램 | -| 6+ | 화면 스토리보드 | 와이어프레임 + Description | - -## 스토리보드 슬라이드 레이아웃 - -``` -┌─────────────────────────────────────────────────────────┐ -│ Task Name │ [값] │ Ver. │ D1.0 │ Page │ [N] │ │ -│ Route │ [경로] │ Screen Name │ [화면명] │ ID │ [ID] │ -├───────────────────────────────────┬─────────────────────┤ -│ │ Description │ -│ 와이어프레임 영역 (6.8") │─────────────────────│ -│ │ ① 제목 │ -│ ┌─────────────────────────────┐ │ 설명 텍스트 │ -│ │ 사이드바 │ 메인 콘텐츠 │ │─────────────────────│ -│ │ │ │ │ ② 제목 │ -│ │ │ │ │ 설명 텍스트 │ -│ └─────────────────────────────┘ │─────────────────────│ -│ │ ③ 제목 │ -│ │ 설명 텍스트 │ -└───────────────────────────────────┴─────────────────────┘ -``` - -## wireframeElements 가이드라인 (매우 중요) - -### 사이드바 자동 렌더링 -**사이드바는 `mainMenus` 배열을 기반으로 자동 생성됩니다.** -- 현재 화면의 `taskName`과 일치하는 메뉴가 **활성(highlight)** 상태로 표시됩니다 -- wireframeElements에 사이드바 요소를 포함해도 **자동으로 필터링**되어 중복 표시되지 않습니다 -- x좌표 < 1.5인 요소는 사이드바 영역으로 간주되어 제외됩니다 - -### 좌표 체계 (인치 단위) -``` -┌────────────────────────────────────────────────────────┐ -│ 슬라이드 (10" x 5.625") │ -├─────────┬──────────────────────────────────────────────┤ -│ 사이드바 │ 콘텐츠 영역 │ -│ x < 1.5 │ x >= 1.5 │ -│ │ │ -│ (자동 │ wireframeElements 배치 영역 │ -│ 생성) │ │ -│ │ │ -├─────────┴──────────────────────────────────────────────┤ -``` - -### wireframeElements 작성 예시 -```json -{ - "screens": [ - { - "taskName": "견적서 생성", // mainMenus의 title과 일치해야 함 - "wireframeElements": [ - // ❌ 사이드바 요소 - 작성해도 자동 필터링됨 - {"type": "rect", "x": 0.3, "y": 1.3, "w": 1.2, "h": 4, "fill": "f1f5f9"}, - {"type": "rect", "x": 0.35, "y": 1.4, "w": 1.1, "h": 0.3, "text": "규격 입력"}, - - // ✅ 콘텐츠 영역 요소 - 이것만 실제로 렌더링됨 - {"type": "rect", "x": 1.6, "y": 1.3, "w": 5.3, "h": 0.35, "text": "제목"}, - {"type": "rect", "x": 1.6, "y": 1.75, "w": 2.5, "h": 1, "fill": "0d9488"} - ] - } - ] -} -``` - -### wireframeElements 속성 -| 속성 | 타입 | 설명 | 예시 | -|------|------|------|------| -| type | string | 요소 타입 | "rect" | -| x | number | X 좌표 (인치) | 1.6 | -| y | number | Y 좌표 (인치) | 1.3 | -| w | number | 너비 (인치) | 2.5 | -| h | number | 높이 (인치) | 0.8 | -| fill | string | 배경색 (# 없이) | "0d9488" | -| text | string | 텍스트 내용 | "제목" | -| fontSize | number | 폰트 크기 | 11 | -| color | string | 텍스트 색상 | "FFFFFF" | -| bold | boolean | 굵게 | true | -| align | string | 정렬 | "left", "center", "right" | - -### 사이드바 메뉴 설정 -```json -{ - "mainMenus": [ - { "title": "규격 입력", "children": ["개구부 크기", "제작 규격"] }, - { "title": "단가 계산", "children": ["재질 선택", "면적 단가"] }, - { "title": "부대비용", "children": ["모터 선정", "철거비"] }, - { "title": "견적서 생성", "children": ["마진율", "출력"] } - ], - "screens": [ - { - "taskName": "견적서 생성" // ← 이 화면에서 "견적서 생성" 메뉴가 활성화됨 - } - ] -} -``` - -## Description 마커 시스템 (매우 중요) - -### 빨간 번호 마커 -Description 패널의 각 항목 번호(①②③④)가 **빨간색 원형 마커**로 표시됩니다. -- 와이어프레임 영역에 동일한 빨간 번호 마커가 해당 요소 위치에 표시됩니다 -- Description의 설명이 화면의 어떤 요소를 가리키는지 명확하게 매핑됩니다 - -### markerX, markerY 속성 -descriptions 배열의 각 항목에 `markerX`, `markerY`를 추가하여 마커 위치를 지정합니다. - -```json -{ - "descriptions": [ - { - "title": "개구부 입력", - "content": "고객사 제공 문 크기 입력", - "markerX": 1.6, // 마커 X 좌표 (인치) - "markerY": 1.75 // 마커 Y 좌표 (인치) - }, - { - "title": "제작 규격 변환", - "content": "W+100mm, H+600mm 자동 계산", - "markerX": 5.1, - "markerY": 1.75 - } - ] -} -``` - -### descriptions 속성 -| 속성 | 타입 | 필수 | 설명 | 예시 | -|------|------|------|------|------| -| title | string | ✅ | 항목 제목 | "개구부 입력" | -| content | string | ✅ | 설명 내용 | "mm 단위로 입력" | -| markerX | number | ❌ | 마커 X 좌표 (인치) | 1.6 | -| markerY | number | ❌ | 마커 Y 좌표 (인치) | 1.75 | - -### 마커 위치 가이드 -``` -┌──────────────────────────────────────────────┐ -│ 사이드바 │ 콘텐츠 영역 │ -│ │ ①──────────── ②──────────── │ -│ │ │ 개구부 입력 │ │ 제작 규격 │ │ -│ │ └──────────── └──────────── │ -│ │ │ -│ │ ③──────────── ④──────────── │ -│ │ │ 면적 계산 │ │ 할증 적용 │ │ -│ │ └──────────── └──────────── │ -└──────────────────────────────────────────────┘ -``` - -- markerX, markerY가 없으면 기본 위치(좌측 세로 배치)에 마커 표시 -- 좌표는 wireframeElements와 동일한 인치 단위 사용 -- 마커는 z-index가 높아 다른 요소 위에 표시됨 - -## 색상 팔레트 - -```javascript -const colors = { - primary: '0d9488', // Teal (주요 강조) - secondary: '1e293b', // Slate 800 (헤더, 배경) - accent: '95C11F', // 라임 그린 (Description 헤더) - warning: 'f59e0b', // 경고 (유효기간 등) - danger: 'dc2626', // 위험 (필수 항목) - white: 'FFFFFF', - lightGray: 'f1f5f9', // 배경 - darkGray: '64748b', // 보조 텍스트 - black: '1a1a1a', // Description 영역 배경 - wireframeBg: 'f8fafc', // 와이어프레임 배경 - wireframeBorder: 'e2e8f0' // 와이어프레임 테두리 -}; -``` - -## Description 패널 스타일 가이드 (매우 중요) - -### 배경 없는 투명 스타일 -Description 영역의 각 항목은 **배경색 없이** 투명하게 표현해야 합니다. - -```javascript -// ✅ 올바른 Description 항목 스타일 -descriptions.forEach((desc, idx) => { - const y = 1.25 + idx * 0.85; - - // 번호 원 (라임 그린 배경) - slide.addShape('ellipse', { - x: 7.25, y: y, w: 0.25, h: 0.25, - fill: { color: '95C11F' } // 라임 그린 - }); - slide.addText(String(idx + 1), { - x: 7.25, y: y, w: 0.25, h: 0.25, - fontSize: 7, bold: true, color: 'FFFFFF', - align: 'center', valign: 'middle' - }); - - // 제목 - 배경 없음, 흰색 텍스트 - slide.addText(desc.title, { - x: 7.55, y: y, w: 2.1, h: 0.25, - fontSize: 8, bold: true, color: 'FFFFFF' - // fill 속성 없음 = 투명 배경 - }); - - // 설명 - 배경 없음, 회색 텍스트 - slide.addText(desc.content, { - x: 7.25, y: y + 0.28, w: 2.4, h: 0.5, - fontSize: 7, color: '64748b' - // fill 속성 없음 = 투명 배경 - }); -}); - -// ❌ 잘못된 스타일 - 각 항목에 배경색 적용 -slide.addText(desc.title, { - x: 7.55, y: y, w: 2.1, h: 0.25, - fill: { color: '333333' } // 배경색 금지 -}); -``` - -### Description 영역 구조 -``` -┌─────────────────────────┐ -│ Description (헤더) │ ← 라임 그린 배경 (#95C11F) -├─────────────────────────┤ -│ │ -│ ① 제목 │ ← 번호만 원형 배경, 텍스트는 투명 -│ 설명 텍스트 │ ← 배경 없음 -│ │ -│ ② 제목 │ -│ 설명 텍스트 │ -│ │ -│ ③ 제목 │ -│ 설명 텍스트 │ -│ │ -│ ④ 제목 │ -│ 설명 텍스트 │ -│ │ -└─────────────────────────┘ - 전체 배경: #1a1a1a (검정) - 각 항목: 배경 없음 (투명) -``` - -## 테이블 스타일 가이드 (매우 중요) - -### 일관된 테두리 적용 -모든 테이블은 **동일한 테두리 스타일**을 사용해야 합니다. - -```javascript -// ✅ 올바른 테이블 테두리 스타일 -slide.addTable(data, { - x: 0.5, y: 1, w: 9, h: 2, - fontSize: 10, - color: '1e293b', - border: { pt: 0.5, color: '64748b' }, // 일관된 0.5pt, darkGray - colW: [1.2, 0.8, 1.5, 4, 1.5], - align: 'center', - valign: 'middle' -}); - -// ❌ 잘못된 스타일 - 테두리 불일치 -slide.addTable(data, { - border: { pt: 1, color: '000000' } // 다른 두께/색상 사용 금지 -}); -``` - -### 헤더 정보 테이블 (스토리보드 상단) -```javascript -const headerData = [ - ['Task Name', screenInfo.taskName, 'Ver.', 'D1.0', 'Page', screenInfo.page], - ['Route', screenInfo.route, 'Screen Name', screenInfo.screenName, 'Screen ID', screenInfo.screenId] -]; - -slide.addTable(headerData, { - x: 0.2, y: 0.1, w: 9.6, h: 0.5, - fontSize: 8, - color: '1e293b', - border: { pt: 0.5, color: '64748b' }, // 표준 테두리 - fill: { color: 'f1f5f9' }, // 연한 회색 배경 - colW: [0.8, 2.5, 0.8, 2.5, 0.8, 2.2] // 균일한 열 너비 -}); -``` - -### 와이어프레임 내 테이블 형태 요소 -```javascript -// 와이어프레임 내 테이블 스타일 박스 -slide.addShape('rect', { - x: 1.6, y: 2.2, w: 5.3, h: 0.35, - fill: { color: '1e293b' }, - line: { color: 'e2e8f0', width: 0.5 } // 일관된 테두리 -}); - -// 행 구분 -slide.addShape('rect', { - x: 1.6, y: 2.6, w: 5.3, h: 0.35, - fill: { color: 'FFFFFF' }, - line: { color: 'e2e8f0', width: 0.5 } // 동일한 테두리 -}); -``` - -### 테이블 스타일 요약 -| 요소 | 테두리 두께 | 테두리 색상 | 배경색 | -|------|------------|------------|--------| -| 헤더 정보 테이블 | 0.5pt | 64748b | f1f5f9 | -| Document History | 0.5pt | 64748b | FFFFFF | -| 와이어프레임 테이블 | 0.5pt | e2e8f0 | FFFFFF/1e293b | -| IA 메뉴 박스 | 0.5pt | e2e8f0 | f1f5f9 | - -## PptxGenJS 핵심 규칙 - -### 색상 코드 (매우 중요) -```javascript -// ✅ 올바른 사용 - # 없이 -{ color: 'FF0000' } -{ fill: { color: '1e3a5f' } } - -// ❌ 잘못된 사용 - 파일 손상 -{ color: '#FF0000' } -``` - -### 슬라이드 크기 (16:9) -```javascript -pptx.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); -pptx.layout = 'CUSTOM_16x9'; -``` - -### 요소 추가 예시 -```javascript -// 텍스트 -slide.addText('제목', { - x: 0.5, y: 0.5, w: 9, h: 0.5, - fontSize: 20, bold: true, color: '1e293b' -}); - -// 도형 -slide.addShape('rect', { - x: 0, y: 0, w: 10, h: 0.7, - fill: { color: '1e293b' } -}); - -// 테이블 -slide.addTable(data, { - x: 0.5, y: 1, w: 9, h: 2, - fontSize: 10, - border: { pt: 0.5, color: '64748b' } -}); -``` - -## 입력 파일 형식 지원 - -### Markdown (.md) -```markdown -## 프로젝트명: 방화셔터 견적 시스템 - -### 주요 기능 -1. 셔터 타입 선택 -2. 규격 입력 및 계산 -3. 견적서 생성 -``` - -### 텍스트 (.txt) -``` -=== 프로젝트 개요 === -방화셔터 견적서 자동화 시스템 - -=== 메뉴 구조 === -- 견적 입력 - - 셔터 타입 선택 - - 규격 입력 -``` - -### 인터뷰 스크립트 -``` -**김철수:** 어떤 기능이 필요한가요? -**박지민:** 셔터 종류 선택과 자동 계산이요. -``` - -## 실행 예시 - -### 기본 사용 -```bash -# 사용자 요청 -"C:\project\script.md 기획서 생성해줘" - -# 실행되는 명령 -node ~/.claude/skills/storyboard-generator/scripts/generate-storyboard.js \ - --input "C:\project\script.md" \ - --output "C:\project\pptx\storyboard.pptx" -``` - -### 출력 폴더 지정 -```bash -# 사용자 요청 -"interview.txt를 분석해서 스토리보드 생성, output 폴더에 저장" - -# 실행되는 명령 -node ~/.claude/skills/storyboard-generator/scripts/generate-storyboard.js \ - --input "interview.txt" \ - --output "output/storyboard.pptx" -``` - -## 워크플로우 (HTML 기반 - 권장) - -``` -┌─────────────────┐ -│ 입력 파일 │ script.md, interview.txt -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 텍스트 분석 │ 섹션 인식, 키워드 추출 -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 데이터 구조화 │ storyboard-config.json 생성 -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ HTML 슬라이드 │ generate-html-storyboard.js -│ 생성 │ → 각 슬라이드를 HTML로 생성 -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ HTML → PPTX │ convert-html-to-pptx.js -│ 변환 │ → Playwright로 렌더링 후 변환 -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ PPTX 출력 │ 완성된 기획서 -└─────────────────┘ -``` - -### HTML 기반 방식의 장점 - -1. **정교한 디자인**: CSS로 정밀한 레이아웃 제어 가능 -2. **일관된 스타일**: 테두리, 간격, 색상이 CSS로 정확하게 적용 -3. **미리보기**: HTML 파일을 브라우저에서 바로 확인 가능 -4. **디버깅 용이**: 문제 발생 시 HTML 소스 확인으로 빠른 수정 - -## 필수 의존성 - -```bash -# Node.js 패키지 (scripts 폴더에 설치) -cd ~/.claude/skills/storyboard-generator/scripts -npm install pptxgenjs playwright sharp -``` - -## 관련 스킬 - -- **text-analyzer-skill**: 텍스트 구조 분석 -- **proposal-skill**: PDF 기획서 분석 -- **pptx-skill**: HTML → PPTX 변환 -- **design-skill**: 슬라이드 디자인 - -## 대량 견적서 생성 워크플로우 - -### 배치 생성 프로세스 -``` -┌─────────────────┐ -│ 마스터 설정 │ estimate-template.json (공통 설정) -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 개별 견적 데이터 │ estimates/*.json (프로젝트별 변수) -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 설정 파일 병합 │ 마스터 + 개별 = 완성된 config -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ HTML 슬라이드 │ 각 견적별 html_slides 폴더 -│ 생성 (배치) │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ PPTX 변환 │ 각 견적별 .pptx 파일 -│ (배치) │ -└─────────────────┘ -``` - -### 마스터 템플릿 구조 -```json -{ - "_template": true, - "company": "SAM", - "author": "DX 추진팀", - "mainMenus": [ - { "title": "규격 입력", "children": ["개구부 크기", "제작 규격 계산"] }, - { "title": "단가 계산", "children": ["재질 선택", "두께 선택"] }, - { "title": "부대비용", "children": ["모터 선정", "철거/폐기물"] }, - { "title": "견적서 생성", "children": ["마진율 적용", "PPT 출력"] } - ], - "pricingRules": { - "materials": { - "아연도강판_0.8t": 85000, - "아연도강판_1.0t": 90000, - "스크린_일반": "변동", - "스크린_차열": "일반 × 1.8" - }, - "motors": { - "400형": { "maxWeight": 300, "price": 450000 }, - "600형": { "maxWeight": 500, "price": 600000 }, - "1000형": { "maxWeight": 999, "price": 850000 } - }, - "additionalCosts": { - "철거비": 150000, - "폐기물처리": 50000, - "고소장비": 250000 - }, - "margins": { - "관공서": 0.15, - "일반건설사": 0.20, - "특수": 0.25 - } - } -} -``` - -### 개별 견적 데이터 예시 -```json -{ - "_extends": "estimate-template.json", - "projectName": "○○빌딩 방화셔터 교체공사", - "date": "2025.01.15", - "items": [ - { - "location": "B1F 주차장 입구", - "openingW": 3000, - "openingH": 4000, - "material": "아연도강판_0.8t", - "workflow": "교체" - }, - { - "location": "1F 로비", - "openingW": 2500, - "openingH": 3500, - "material": "스크린_일반", - "workflow": "신축" - } - ] -} -``` - -### 배치 실행 명령 -```bash -# 여러 견적서 일괄 생성 -for config in estimates/*.json; do - name=$(basename "$config" .json) - - # HTML 생성 - node ~/.claude/skills/storyboard-generator/scripts/generate-html-storyboard.js \ - --config "$config" \ - --output "output/${name}_html" - - # PPTX 변환 - node ~/.claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js \ - --input "output/${name}_html" \ - --output "output/${name}.pptx" -done -``` - -## 방화셔터 견적 계산 로직 - -### 규격 변환 공식 -``` -제작 규격 = 개구부 + 여유치 - -┌─────────────────────────────────────────────────┐ -│ 제작 폭(W) = 개구부 폭 + 100mm (레일 설치용) │ -│ 제작 높이(H) = 개구부 높이 + 600mm (권상높이) │ -└─────────────────────────────────────────────────┘ - -예시: - 개구부: 3,000mm × 4,000mm - 제작: 3,100mm × 4,600mm -``` - -### 면적 계산 및 할증 -``` -실제면적 = 제작W × 제작H ÷ 1,000,000 (㎡) - -┌─────────────────────────────────────────────────┐ -│ 최소면적 규칙: 5㎡ 미만 → 5㎡ 강제 적용 │ -│ (제작 공정상 최소 규격 필요) │ -└─────────────────────────────────────────────────┘ - -예시: - 3,100 × 4,600 = 14,260,000 ㎟ = 14.26㎡ - 적용면적 = 14.26㎡ (5㎡ 이상이므로 그대로) -``` - -### 단가표 (기준가) -| 재질 | 두께 | 단가(원/㎡) | 비고 | -|------|------|------------|------| -| 아연도 강판 | 0.8t | 85,000 | 기본 | -| 아연도 강판 | 1.0t | 90,000 | 소방 기준 | -| 스크린 (일반) | - | 변동 | 시세 적용 | -| 스크린 (차열) | - | 일반×1.8 | 차열 등급 | - -### 하중 계산 및 모터 선정 -``` -총 하중 = 적용면적 × 12kg (0.8t 기준) - 적용면적 × 15kg (1.0t 기준) - -┌────────────────────────────────────────────────┐ -│ 모터 선정 기준 │ -│ ~300kg → 400형 (450,000원) │ -│ 300~500kg → 600형 (600,000원) │ -│ 500kg~ → 1000형 (850,000원) │ -└────────────────────────────────────────────────┘ -``` - -### 부대비용 항목 -| 항목 | 금액 | 적용 조건 | -|------|------|----------| -| 철거비 | 150,000원/개소 | 교체공사 시 | -| 폐기물처리 | 50,000원/개소 | 교체공사 시 | -| 고소장비 | 250,000원/일 | 지하/고층 현장 | -| 지방운반비 | 별도 협의 | 수도권 외 | - -### 최종 금액 계산 -``` -원가 합계 = 자재비 + 모터비 + 부대비용 - -┌────────────────────────────────────────────────┐ -│ 마진율 적용 │ -│ 관공서: 15% │ -│ 일반 건설사: 20~25% │ -│ 특수 (원거리/고층): 25~30% │ -└────────────────────────────────────────────────┘ - -최종금액 = 원가 × (1 + 마진율) -절삭금액 = 만원 단위 이하 절삭 (네고 대비) -``` - -## 워크플로우 변형 (신축 vs 교체) - -### 신축공사 워크플로우 -``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ 규격 입력 │ → │ 단가 계산 │ → │ 모터 선정 │ → │ 견적 생성 │ -└──────────┘ └──────────┘ └──────────┘ └──────────┘ - -특징: -- 철거비/폐기물 비용 없음 -- 기존 구조물 확인 불필요 -- 전기 배선 신규 설치 -``` - -### 교체공사 워크플로우 -``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ 규격 입력 │ → │ 단가 계산 │ → │ 모터 선정 │ → │ 부대비용 │ → │ 견적 생성 │ -└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ - ↑ - 철거비, 폐기물, - 양중비 추가 - -특징: -- 철거비 150,000원/개소 추가 -- 폐기물처리 50,000원/개소 추가 -- 기존 전기 배선 활용 가능 -- 양중비(고소장비) 현장 상황에 따라 추가 -``` - -### 설정 파일에서 워크플로우 구분 -```json -{ - "screens": [ - { - "taskName": "부대비용", - "conditionalDisplay": { - "show": "workflow === '교체'", - "description": "교체공사 시에만 이 화면 표시" - } - } - ], - "calculations": { - "additionalCosts": { - "철거비": { "condition": "workflow === '교체'", "amount": 150000 }, - "폐기물": { "condition": "workflow === '교체'", "amount": 50000 } - } - } -} -``` - -## 주의사항 - -1. **입력 파일 인코딩**: UTF-8 권장 -2. **출력 폴더**: 존재하지 않으면 자동 생성 -3. **파일명 충돌**: 기존 파일 덮어쓰기 -4. **한글 지원**: 맑은 고딕, Pretendard 폰트 사용 -5. **단가 변동**: 스크린 방화셔터 단가는 시세에 따라 변동 - 외부 단가표 참조 필요 -6. **면책 조항**: 전기 배선 및 상시 전원 공사 별도 문구 필수 삽입 diff --git a/.claude/skills/storyboard-generator/examples/fire-shutter-config.json b/.claude/skills/storyboard-generator/examples/fire-shutter-config.json deleted file mode 100755 index ac46102..0000000 --- a/.claude/skills/storyboard-generator/examples/fire-shutter-config.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "projectName": "방화셔터 견적 시스템", - "company": "SAM", - "author": "IT 혁신팀", - "date": "2024.10.24", - "purpose": "현장 영업사원의 전화 견적 요청을\nAI 기반으로 자동화하여\n엑셀 정리 → PPT 견적서 생성\n과정을 시스템화", - "features": [ - "셔터 타입 선택 (철재/스크린/하향식)", - "규격 입력 → 면적 자동 계산", - "모터 사양 자동 선택", - "지역/층고별 설치/운반비 가산", - "할인율 적용 및 PPT 자동 생성" - ], - "effects": [ - { "icon": "⏱️", "title": "시간 단축", "desc": "견적서 작성 시간 80% 감소" }, - { "icon": "✅", "title": "정확성 향상", "desc": "계산 오류 제로화" }, - { "icon": "📊", "title": "표준화", "desc": "PPT 양식 통일" } - ], - "tocItems": [ - { "num": "01", "title": "프로젝트 개요", "desc": "시스템 목적 및 범위" }, - { "num": "02", "title": "메뉴 구조 (IA)", "desc": "Information Architecture" }, - { "num": "03", "title": "견적 입력 화면", "desc": "셔터 타입, 규격, 옵션 선택" }, - { "num": "04", "title": "견적 계산 화면", "desc": "단가 계산 및 부가비용" }, - { "num": "05", "title": "견적서 미리보기", "desc": "PPT 양식 프리뷰" }, - { "num": "06", "title": "견적 관리", "desc": "목록 조회 및 이력 관리" } - ], - "mainMenus": [ - { - "title": "견적 입력", - "children": ["셔터 타입 선택", "규격 입력", "옵션 선택"] - }, - { - "title": "견적 계산", - "children": ["단가 계산", "모터 선택", "부가비용"] - }, - { - "title": "견적서 생성", - "children": ["미리보기", "PPT 다운로드", "이메일 전송"] - }, - { - "title": "견적 관리", - "children": ["목록 조회", "이력 관리", "통계"] - } - ], - "screens": [ - { - "taskName": "견적 입력", - "route": "/estimate/input", - "screenName": "견적 입력 화면", - "screenId": "EST_INPUT_001", - "descriptions": [ - { "title": "셔터 타입 선택", "content": "철재(85,000/㎡), 스크린(단가 변동), 하향식 스크린 중 선택" }, - { "title": "규격 입력", "content": "가로(W) × 높이(H) 미터 단위로 입력, 면적 자동 계산" }, - { "title": "수량 입력", "content": "동일 규격 셔터 수량 입력 (1개소 이상)" }, - { "title": "입력 검증", "content": "필수 항목 미입력 시 경고 표시, 다음 단계 진행 불가" } - ] - }, - { - "taskName": "견적 계산", - "route": "/estimate/calculate", - "screenName": "견적 계산 화면", - "screenId": "EST_CALC_001", - "descriptions": [ - { "title": "모터 자동 선택", "content": "셔터 무게 기준 자동 추천, 수동 변경 가능" }, - { "title": "기본 설치비", "content": "1개소당 30만원, 수량에 따라 자동 계산" }, - { "title": "고소작업비", "content": "층고 5m 이상 시 렌탈비 25만원/일 추가" }, - { "title": "운반비", "content": "서울/경기 무료, 지방 거리별 15~30만원 청구" } - ] - }, - { - "taskName": "견적서 생성", - "route": "/estimate/preview", - "screenName": "견적서 미리보기", - "screenId": "EST_PREVIEW_001", - "descriptions": [ - { "title": "PPT 구성", "content": "표지 → 요약 → 세부내역 → 성적서 → 회사소개 (5장)" }, - { "title": "실시간 미리보기", "content": "슬라이드 썸네일 클릭 시 해당 페이지 확대 표시" }, - { "title": "다운로드/전송", "content": "PPT 파일 다운로드 또는 고객사 이메일 직접 전송" }, - { "title": "유효기간 자동 삽입", "content": "제출일 기준 15일 유효기간 빨간색 문구 자동 추가" } - ] - }, - { - "taskName": "견적 관리", - "route": "/estimate/list", - "screenName": "견적 관리 목록", - "screenId": "EST_LIST_001", - "descriptions": [ - { "title": "견적 목록", "content": "프로젝트명, 금액, 상태, 작성일 기준 조회" }, - { "title": "필터/정렬", "content": "상태별 필터, 날짜/금액 정렬 기능" }, - { "title": "통계 대시보드", "content": "전체 건수, 이번 달 건수, 총 금액 실시간 표시" }, - { "title": "할인 권한", "content": "1,000만원 이상 시 5% 할인 가능 (부장 결재 시 추가)" } - ] - } - ] -} diff --git a/.claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js b/.claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js deleted file mode 100755 index 69af4e8..0000000 --- a/.claude/skills/storyboard-generator/scripts/convert-html-to-pptx.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * HTML 슬라이드를 PPTX로 변환하는 스크립트 - * - * 사용법: - * node convert-html-to-pptx.js --input --output - */ - -const fs = require('fs'); -const path = require('path'); -const PptxGenJS = require('pptxgenjs'); - -// html2pptx.js 모듈 로드 -const pptxSkillPath = path.join(__dirname, '../../pptx-skill/scripts/html2pptx.js'); -let html2pptx; - -if (fs.existsSync(pptxSkillPath)) { - html2pptx = require(pptxSkillPath); -} else { - console.error('❌ html2pptx.js를 찾을 수 없습니다.'); - console.error(' 경로:', pptxSkillPath); - process.exit(1); -} - -async function main() { - const args = process.argv.slice(2); - let inputDir = null; - let outputFile = null; - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--input' && args[i + 1]) { - inputDir = args[i + 1]; - i++; - } else if (args[i] === '--output' && args[i + 1]) { - outputFile = args[i + 1]; - i++; - } - } - - if (!inputDir) { - console.error('Usage: node convert-html-to-pptx.js --input --output '); - process.exit(1); - } - - if (!fs.existsSync(inputDir)) { - console.error(`❌ 입력 폴더를 찾을 수 없습니다: ${inputDir}`); - process.exit(1); - } - - // HTML 파일 목록 가져오기 (이름순 정렬) - const htmlFiles = fs.readdirSync(inputDir) - .filter(f => f.endsWith('.html')) - .sort() - .map(f => path.join(inputDir, f)); - - if (htmlFiles.length === 0) { - console.error('❌ HTML 파일을 찾을 수 없습니다.'); - process.exit(1); - } - - // 출력 파일명 설정 - if (!outputFile) { - outputFile = path.join(path.dirname(inputDir), 'storyboard.pptx'); - } - - console.log('🚀 HTML → PPTX 변환 시작\n'); - console.log(`📂 입력 폴더: ${inputDir}`); - console.log(`📄 HTML 파일: ${htmlFiles.length}개`); - console.log(`📁 출력 파일: ${outputFile}\n`); - - // PptxGenJS 초기화 - const pptx = new PptxGenJS(); - - // 16:9 레이아웃 설정 (720pt x 405pt = 10" x 5.625") - pptx.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); - pptx.layout = 'CUSTOM_16x9'; - - pptx.author = 'SAM IT Innovation Team'; - pptx.company = 'SAM'; - pptx.subject = 'Web Storyboard'; - - // 각 HTML 파일을 슬라이드로 변환 - for (let i = 0; i < htmlFiles.length; i++) { - const htmlFile = htmlFiles[i]; - const fileName = path.basename(htmlFile); - - try { - console.log(`⏳ 변환 중 (${i + 1}/${htmlFiles.length}): ${fileName}`); - - await html2pptx(htmlFile, pptx); - - console.log(`✅ 완료: ${fileName}`); - } catch (error) { - console.error(`❌ 오류: ${fileName}`); - console.error(` ${error.message}`); - // 오류가 발생해도 계속 진행 - } - } - - // PPTX 파일 저장 - try { - // 출력 폴더 생성 - const outputDir = path.dirname(outputFile); - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - await pptx.writeFile(outputFile); - - const stats = fs.statSync(outputFile); - const sizeKB = (stats.size / 1024).toFixed(1); - - console.log('\n🎉 PPTX 생성 완료!'); - console.log(`📁 파일: ${outputFile}`); - console.log(`📊 크기: ${sizeKB} KB`); - console.log(`📄 슬라이드: ${htmlFiles.length}장`); - } catch (error) { - console.error('\n❌ PPTX 저장 실패:', error.message); - process.exit(1); - } -} - -main().catch(console.error); diff --git a/.claude/skills/storyboard-generator/scripts/generate-html-storyboard.js b/.claude/skills/storyboard-generator/scripts/generate-html-storyboard.js deleted file mode 100755 index f5618ce..0000000 --- a/.claude/skills/storyboard-generator/scripts/generate-html-storyboard.js +++ /dev/null @@ -1,1206 +0,0 @@ -/** - * HTML 기반 스토리보드 PPTX 생성기 - * HTML 템플릿을 먼저 생성한 후 PPTX로 변환하는 방식 - * - * 사용법: - * node generate-html-storyboard.js --config <설정파일.json> --output <출력폴더> - */ - -const fs = require('fs'); -const path = require('path'); - -// 색상 정의 -const colors = { - primary: '#0d9488', - secondary: '#1e293b', - accent: '#95C11F', - warning: '#f59e0b', - danger: '#dc2626', - white: '#ffffff', - lightGray: '#f1f5f9', - darkGray: '#64748b', - black: '#1a1a1a', - wireframeBg: '#f8fafc', - wireframeBorder: '#e2e8f0', - tableBorder: '#334155' -}; - -// HTML 이스케이프 -function escapeHtml(text) { - if (!text) return ''; - return String(text) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -// 공통 스타일 -const commonStyles = ` - * { margin: 0; padding: 0; box-sizing: border-box; } - body { - width: 720pt; - height: 405pt; - font-family: 'Pretendard', 'Malgun Gothic', sans-serif; - background: #ffffff; - } - p, h1, h2, h3 { margin: 0; } -`; - -// ======================================== -// 슬라이드 생성 함수들 -// ======================================== - -function generateCoverSlide(config) { - return ` - - - - - - - -
                                      - -
                                      -

                                      ${escapeHtml(config.projectName || '프로젝트')}

                                      -

                                      웹 기획서 및 스토리보드

                                      -

                                      Version D1.0

                                      -
                                      - - -`; -} - -function generateHistorySlide(config) { - return ` - - - - - - - -
                                      -

                                      Document History

                                      -
                                      -
                                      -
                                      -
                                      -

                                      날짜

                                      -

                                      버전

                                      -

                                      주요 내용

                                      -

                                      상세 내용

                                      -

                                      비고

                                      -
                                      -
                                      -

                                      ${escapeHtml(config.date || '')}

                                      -

                                      D1.0

                                      -

                                      초안 작성

                                      -

                                      ${escapeHtml(config.projectName || '')} 기획서 초안 작성

                                      -

                                      -
                                      -
                                      -

                                      -

                                      -

                                      -

                                      -

                                      -
                                      -
                                      -
                                      - -`; -} - -function generateTOCSlide(config) { - const tocItems = config.tocItems || []; - const itemsHtml = tocItems.map((item, idx) => ` -
                                      -
                                      -

                                      ${escapeHtml(item.num || String(idx + 1).padStart(2, '0'))}

                                      -
                                      -
                                      -

                                      ${escapeHtml(item.title)}

                                      -
                                      -
                                      -

                                      ${escapeHtml(item.desc || '')}

                                      -
                                      -
                                      - `).join(''); - - return ` - - - - - - - -
                                      -

                                      목차 (Table of Contents)

                                      -
                                      -
                                      - ${itemsHtml} -
                                      - -`; -} - -function generateOverviewSlide(config) { - const features = config.features || []; - const effects = config.effects || []; - - const featuresHtml = features.slice(0, 6).map(f => ` -
                                      -

                                      ✓ ${escapeHtml(f)}

                                      -
                                      - `).join(''); - - const effectsHtml = effects.slice(0, 3).map(e => ` -
                                      -

                                      ${escapeHtml(e.icon || '📌')}

                                      -

                                      ${escapeHtml(e.title)}

                                      -

                                      ${escapeHtml(e.desc)}

                                      -
                                      - `).join(''); - - return ` - - - - - - - -
                                      -

                                      01. 프로젝트 개요

                                      -
                                      -
                                      -
                                      -
                                      -

                                      프로젝트 목적

                                      -
                                      -

                                      ${escapeHtml(config.purpose || '업무 프로세스를 시스템화하여 효율성 향상')}

                                      -
                                      -
                                      -
                                      -

                                      주요 기능

                                      - ${featuresHtml} -
                                      -
                                      -
                                      -

                                      기대 효과

                                      -
                                      - ${effectsHtml} -
                                      -
                                      -
                                      - -`; -} - -function generateIASlide(config) { - const menus = config.mainMenus || []; - - const menusHtml = menus.slice(0, 4).map((menu, idx) => ` - - `).join(''); - - const rootName = config.projectName ? - config.projectName.replace(/시스템|프로젝트|기획/g, '').trim() : - '시스템'; - - return ` - - - - - - - -
                                      -

                                      02. 메뉴 구조 (Information Architecture)

                                      -
                                      -
                                      -
                                      -

                                      ${escapeHtml(rootName)}

                                      -
                                      -
                                      -
                                      - -
                                      - -`; -} - -function generateStoryboardSlide(screen, pageNum, config) { - const menuItems = config.mainMenus || []; - const activeMenu = screen.taskName || ''; - - // 사이드바는 항상 mainMenus 기반으로 별도 렌더링 (wireframeElements 유무와 관계없이) - const sidebarHtml = menuItems.slice(0, 4).map(menu => { - const isActive = menu.title === activeMenu; - return ` - - `; - }).join(''); - - const descriptions = screen.descriptions || []; - const descriptionsHtml = descriptions.slice(0, 4).map((desc, idx) => ` -
                                      -
                                      -
                                      -

                                      ${idx + 1}

                                      -
                                      -
                                      -

                                      ${escapeHtml(desc.title)}

                                      -
                                      -
                                      -
                                      -

                                      ${escapeHtml(desc.content)}

                                      -
                                      -
                                      - `).join(''); - - // wireframeElements가 있으면 사이드바 영역 요소 제외하고 콘텐츠만 렌더링 - const hasWireframeElements = screen.wireframeElements && screen.wireframeElements.length > 0; - - // 화면별 콘텐츠 생성 - let contentHtml = ''; - - if (hasWireframeElements) { - // 사이드바 영역(x < 1.5)을 제외한 콘텐츠 요소만 필터링 - contentHtml = generateWireframeContent(screen, config, true); - } else if (screen.contentType === 'form') { - contentHtml = generateFormContent(screen); - } else if (screen.contentType === 'table') { - contentHtml = generateTableContent(screen); - } else if (screen.contentType === 'cards') { - contentHtml = generateCardsContent(screen); - } else { - contentHtml = generateDefaultContent(screen); - } - - return ` - - - - - - - - -
                                      -
                                      -

                                      Task Name

                                      -

                                      ${escapeHtml(screen.taskName || '')}

                                      -

                                      Ver.

                                      -

                                      D1.0

                                      -

                                      Page

                                      -

                                      ${pageNum}

                                      -
                                      -
                                      -

                                      Route

                                      -

                                      ${escapeHtml(screen.route || '')}

                                      -

                                      Screen Name

                                      -

                                      ${escapeHtml(screen.screenName || '')}

                                      -

                                      Screen ID

                                      -

                                      ${escapeHtml(screen.screenId || '')}

                                      -
                                      -
                                      - - -
                                      - -
                                      -
                                      -

                                      ${escapeHtml(config.projectName || '시스템')}

                                      -
                                      -
                                      - -
                                      - ${contentHtml} -
                                      -
                                      -
                                      - - -
                                      -
                                      -

                                      Description

                                      -
                                      - ${descriptionsHtml} -
                                      -
                                      - -`; -} - -// 콘텐츠 타입별 생성 함수 -function generateDefaultContent(screen) { - return ` -

                                      ${escapeHtml(screen.screenName || '화면')}

                                      -
                                      -

                                      콘텐츠 영역

                                      -
                                      - `; -} - -function generateFormContent(screen) { - const fields = screen.fields || []; - const fieldsHtml = fields.map(f => ` -
                                      -
                                      -

                                      ${escapeHtml(f.label)}

                                      -
                                      -
                                      -

                                      ${escapeHtml(f.value || '')}

                                      -
                                      -
                                      - `).join(''); - - return ` -

                                      ${escapeHtml(screen.screenName || '입력 폼')}

                                      - ${fieldsHtml} - `; -} - -function generateTableContent(screen) { - const rows = screen.tableRows || []; - const rowsHtml = rows.map(r => ` -
                                      -

                                      ${escapeHtml(r.label)}

                                      -
                                      -

                                      ${escapeHtml(r.value)}

                                      -
                                      -
                                      - `).join(''); - - return ` -

                                      ${escapeHtml(screen.screenName || '테이블')}

                                      -
                                      - ${rowsHtml} -
                                      - `; -} - -function generateCardsContent(screen) { - const cards = screen.cards || []; - const cardsHtml = cards.map(c => ` -
                                      -
                                      -

                                      ${escapeHtml(c.title)}

                                      -
                                      -
                                      -

                                      ${escapeHtml(c.value)}

                                      -
                                      -
                                      - `).join(''); - - return ` -

                                      ${escapeHtml(screen.screenName || '선택')}

                                      -
                                      - ${cardsHtml} -
                                      - `; -} - -// wireframeElements 기반 콘텐츠 생성 -// excludeSidebar: true면 사이드바 영역(x < 1.5) 요소 제외 -function generateWireframeContent(screen, config, excludeSidebar = false) { - let elements = screen.wireframeElements || []; - const descriptions = screen.descriptions || []; - - // 사이드바 영역 제외 옵션 - if (excludeSidebar) { - // x < 1.5인 요소들은 사이드바 영역이므로 제외 - elements = elements.filter(el => el.x >= 1.5); - } - - // content-area 내에서의 상대 좌표 계산 - // 사이드바 제외 시: x가 1.5부터 시작하므로 baseX를 1.5로 조정 - const baseX = excludeSidebar ? 1.5 : 0.2; - const baseY = 1.25; // 헤더 테이블 아래 - const scaleX = 52; // pt per inch (영역에 맞게 조정) - const scaleY = 52; - - const elementsHtml = elements.map(el => { - const left = (el.x - baseX) * scaleX; - const top = (el.y - baseY) * scaleY; - const width = el.w * scaleX; - const height = el.h * scaleY; - - const bgColor = el.fill ? `#${el.fill}` : 'transparent'; - const textColor = el.color ? `#${el.color}` : colors.secondary; - const fontSize = el.fontSize || 8; - const fontWeight = el.bold ? '700' : '400'; - const textAlign = el.align || 'center'; - - // 텍스트에 줄바꿈 처리 - const textContent = el.text ? el.text.replace(/\n/g, '
                                      ') : ''; - - return ` -
                                      -

                                      ${textContent}

                                      -
                                      - `; - }).join(''); - - // Description 마커 생성 (빨간 번호) - // descriptions에 markerX, markerY가 있으면 해당 위치에 마커 표시 - const markersHtml = descriptions.map((desc, idx) => { - // 마커 위치가 지정되어 있으면 사용, 없으면 기본 위치 계산 - let markerX, markerY; - - if (desc.markerX !== undefined && desc.markerY !== undefined) { - // 명시적으로 지정된 마커 위치 사용 - markerX = (desc.markerX - baseX) * scaleX; - markerY = (desc.markerY - baseY) * scaleY; - } else { - // 기본 위치: 콘텐츠 영역 좌측에 세로로 배치 - markerX = 5 + idx * 0; // 고정 x 위치 - markerY = 10 + idx * 55; // 세로로 간격 - } - - return ` -
                                      -

                                      ${idx + 1}

                                      -
                                      - `; - }).join(''); - - return ` -
                                      - ${elementsHtml} - ${markersHtml} -
                                      - `; -} - -// ======================================== -// 메인 실행 -// ======================================== - -async function main() { - const args = process.argv.slice(2); - let configFile = null; - let outputDir = null; - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--config' && args[i + 1]) { - configFile = args[i + 1]; - i++; - } else if (args[i] === '--output' && args[i + 1]) { - outputDir = args[i + 1]; - i++; - } - } - - if (!configFile) { - console.error('Usage: node generate-html-storyboard.js --config --output '); - process.exit(1); - } - - const config = JSON.parse(fs.readFileSync(configFile, 'utf-8')); - outputDir = outputDir || path.join(path.dirname(configFile), 'html_slides'); - - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - console.log('🚀 HTML 스토리보드 생성 시작\n'); - - // 슬라이드 생성 - const slides = []; - - // 1. 표지 - slides.push({ name: 'slide_01_cover.html', content: generateCoverSlide(config) }); - console.log('✅ 슬라이드 1: 표지 생성 완료'); - - // 2. Document History - slides.push({ name: 'slide_02_history.html', content: generateHistorySlide(config) }); - console.log('✅ 슬라이드 2: Document History 생성 완료'); - - // 3. 목차 - slides.push({ name: 'slide_03_toc.html', content: generateTOCSlide(config) }); - console.log('✅ 슬라이드 3: 목차 생성 완료'); - - // 4. 프로젝트 개요 - slides.push({ name: 'slide_04_overview.html', content: generateOverviewSlide(config) }); - console.log('✅ 슬라이드 4: 프로젝트 개요 생성 완료'); - - // 5. IA - slides.push({ name: 'slide_05_ia.html', content: generateIASlide(config) }); - console.log('✅ 슬라이드 5: 메뉴 구조 (IA) 생성 완료'); - - // 6+. 스토리보드 - const screens = config.screens || []; - screens.forEach((screen, idx) => { - const pageNum = idx + 6; - slides.push({ - name: `slide_${String(pageNum).padStart(2, '0')}_${screen.screenId || 'screen'}.html`, - content: generateStoryboardSlide(screen, pageNum, config) - }); - console.log(`✅ 슬라이드 ${pageNum}: ${screen.screenName} 생성 완료`); - }); - - // HTML 파일 저장 - for (const slide of slides) { - const filePath = path.join(outputDir, slide.name); - fs.writeFileSync(filePath, slide.content, 'utf-8'); - } - - console.log(`\n🎉 HTML 슬라이드 생성 완료!`); - console.log(`📁 저장 위치: ${outputDir}`); - console.log(`📊 총 슬라이드: ${slides.length}장`); - console.log(`\n다음 단계: html2pptx.js로 PPTX 변환`); -} - -main().catch(console.error); diff --git a/.claude/skills/storyboard-generator/scripts/generate-storyboard.js b/.claude/skills/storyboard-generator/scripts/generate-storyboard.js deleted file mode 100755 index d805531..0000000 --- a/.claude/skills/storyboard-generator/scripts/generate-storyboard.js +++ /dev/null @@ -1,761 +0,0 @@ -/** - * 스토리보드 PPTX 자동 생성 스크립트 - * 텍스트 기반 인터뷰/기획 문서를 분석하여 IA + 화면 설계 스토리보드 생성 - * - * 사용법: - * node generate-storyboard.js --input <입력파일> --output <출력파일.pptx> - * node generate-storyboard.js --config <설정파일.json> - */ - -const PptxGenJS = require('pptxgenjs'); -const fs = require('fs'); -const path = require('path'); - -// ======================================== -// 색상 팔레트 정의 (# 없이) -// ======================================== -const colors = { - primary: '0d9488', // Teal - secondary: '1e293b', // Slate 800 - accent: '95C11F', // 라임 그린 (Description용) - warning: 'f59e0b', // 경고 - danger: 'dc2626', // 위험 - white: 'FFFFFF', - lightGray: 'f1f5f9', - darkGray: '64748b', - black: '1a1a1a', - wireframeBg: 'f8fafc', - wireframeBorder: 'e2e8f0' -}; - -// ======================================== -// 텍스트 파서 클래스 -// ======================================== -class TextParser { - constructor(content) { - this.content = content; - this.sections = []; - this.metadata = {}; - } - - parse() { - this.extractMetadata(); - this.extractSections(); - this.extractMenuStructure(); - this.extractScreens(); - return this; - } - - extractMetadata() { - // 프로젝트명 추출 - const projectMatch = this.content.match(/프로젝트[명\s]*[::]\s*(.+)/i) || - this.content.match(/##\s*(.+?)\s*(프로젝트|시스템|기획)/i) || - this.content.match(/주제[::]\s*(.+)/i); - - if (projectMatch) { - this.metadata.projectName = projectMatch[1].trim(); - } - - // 날짜 추출 - const dateMatch = this.content.match(/(\d{4})[.\-/년](\d{1,2})[.\-/월](\d{1,2})/); - if (dateMatch) { - this.metadata.date = `${dateMatch[1]}.${dateMatch[2].padStart(2, '0')}.${dateMatch[3].padStart(2, '0')}`; - } else { - const today = new Date(); - this.metadata.date = `${today.getFullYear()}.${String(today.getMonth() + 1).padStart(2, '0')}.${String(today.getDate()).padStart(2, '0')}`; - } - - // 참석자/작성자 추출 - const authorMatch = this.content.match(/참석자[::]\s*(.+)/i) || - this.content.match(/작성자[::]\s*(.+)/i); - if (authorMatch) { - this.metadata.author = authorMatch[1].trim(); - } - } - - extractSections() { - // === 섹션 === 패턴 - const sectionPattern = /===\s*(.+?)\s*===/g; - let match; - while ((match = sectionPattern.exec(this.content)) !== null) { - this.sections.push({ - title: match[1].trim(), - startIndex: match.index - }); - } - - // ## 섹션 패턴 (Markdown) - const mdSectionPattern = /^##\s+(.+)$/gm; - while ((match = mdSectionPattern.exec(this.content)) !== null) { - if (!this.sections.some(s => s.title === match[1].trim())) { - this.sections.push({ - title: match[1].trim(), - startIndex: match.index - }); - } - } - } - - extractMenuStructure() { - this.metadata.menus = []; - - // PPT 구성, 메뉴 구조 등에서 추출 - const menuPatterns = [ - /(\d+)\.\s*\*\*(.+?)\*\*/g, // 1. **메뉴명** - /(\d+)\.\s*(.+?)(?:\n|$)/g, // 1. 메뉴명 - /-\s+(.+?)(?:\n|$)/g // - 메뉴명 - ]; - - // 주요 기능 섹션에서 메뉴 추출 - const funcMatch = this.content.match(/주요\s*기능[^]*?(?=##|===|$)/i); - if (funcMatch) { - const lines = funcMatch[0].split('\n'); - for (const line of lines) { - const itemMatch = line.match(/^\s*[-\d.]+\s*(.+)/); - if (itemMatch && itemMatch[1].trim().length > 0) { - const menuName = itemMatch[1].replace(/\*\*/g, '').trim(); - if (menuName.length < 30) { - this.metadata.menus.push(menuName); - } - } - } - } - } - - extractScreens() { - this.metadata.screens = []; - - // 화면 관련 키워드 추출 - const screenKeywords = ['화면', '페이지', '입력', '조회', '관리', '목록', '상세', '등록', '수정']; - - for (const keyword of screenKeywords) { - const pattern = new RegExp(`(${keyword}[^\\n]{0,20})`, 'gi'); - let match; - while ((match = pattern.exec(this.content)) !== null) { - const screenName = match[1].trim(); - if (!this.metadata.screens.some(s => s.name === screenName) && screenName.length < 30) { - this.metadata.screens.push({ name: screenName }); - } - } - } - } -} - -// ======================================== -// 스토리보드 생성기 클래스 -// ======================================== -class StoryboardGenerator { - constructor(config) { - this.config = config; - this.pptx = new PptxGenJS(); - this.setupPresentation(); - } - - setupPresentation() { - // 레이아웃 설정 (16:9) - this.pptx.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); - this.pptx.layout = 'CUSTOM_16x9'; - - // 메타데이터 - this.pptx.title = this.config.projectName || '웹 기획서'; - this.pptx.subject = '시스템 기획 및 UI/UX 설계'; - this.pptx.author = this.config.author || 'SAM'; - this.pptx.company = this.config.company || 'SAM'; - } - - // ---------------------------------------- - // 슬라이드 1: 표지 - // ---------------------------------------- - createCoverSlide() { - const slide = this.pptx.addSlide(); - slide.background = { color: colors.secondary }; - - // 상단 악센트 라인 - slide.addShape('rect', { - x: 0, y: 0, w: 10, h: 0.1, - fill: { color: colors.primary } - }); - - // 로고 - slide.addShape('rect', { - x: 0.5, y: 0.4, w: 1.2, h: 0.5, - fill: { color: colors.primary } - }); - slide.addText(this.config.company || 'SAM', { - x: 0.5, y: 0.4, w: 1.2, h: 0.5, - fontSize: 16, bold: true, color: colors.white, - align: 'center', valign: 'middle' - }); - - // 메인 제목 - slide.addText(this.config.projectName || '프로젝트', { - x: 0.5, y: 1.8, w: 9, h: 0.8, - fontSize: 40, bold: true, color: colors.white, - align: 'center' - }); - - // 부제목 - slide.addText('웹 기획서 및 스토리보드', { - x: 0.5, y: 2.6, w: 9, h: 0.5, - fontSize: 22, color: colors.primary, - align: 'center' - }); - - // 버전 정보 - slide.addText('Version D1.0', { - x: 0.5, y: 3.3, w: 9, h: 0.3, - fontSize: 14, color: colors.darkGray, - align: 'center' - }); - - // 하단 정보 - slide.addText(`${this.config.date || new Date().toISOString().split('T')[0].replace(/-/g, '.')} | ${this.config.author || '기획팀'}`, { - x: 0.5, y: 5, w: 9, h: 0.3, - fontSize: 12, color: colors.darkGray, - align: 'center' - }); - - console.log('✅ 슬라이드 1: 표지 생성 완료'); - } - - // ---------------------------------------- - // 슬라이드 2: Document History - // ---------------------------------------- - createHistorySlide() { - const slide = this.pptx.addSlide(); - slide.background = { color: colors.white }; - - // 헤더 - slide.addShape('rect', { - x: 0, y: 0, w: 10, h: 0.7, - fill: { color: colors.secondary } - }); - slide.addText('Document History', { - x: 0.5, y: 0.15, w: 5, h: 0.4, - fontSize: 20, bold: true, color: colors.white - }); - - // 히스토리 테이블 - const historyData = [ - ['날짜', '버전', '주요 내용', '상세 내용', '비고'], - [this.config.date || '', 'D1.0', '초안 작성', `${this.config.projectName || '프로젝트'} 기획서 초안 작성`, ''], - ['', '', '', '', ''], - ['', '', '', '', ''] - ]; - - slide.addTable(historyData, { - x: 0.5, y: 1.2, w: 9, h: 2.5, - fontSize: 10, - color: colors.secondary, - border: { pt: 0.5, color: colors.darkGray }, - colW: [1.2, 0.8, 1.5, 4, 1.5], - align: 'center', - valign: 'middle' - }); - - console.log('✅ 슬라이드 2: Document History 생성 완료'); - } - - // ---------------------------------------- - // 슬라이드 3: 목차 - // ---------------------------------------- - createTOCSlide() { - const slide = this.pptx.addSlide(); - slide.background = { color: colors.white }; - - // 헤더 - slide.addShape('rect', { - x: 0, y: 0, w: 10, h: 0.7, - fill: { color: colors.secondary } - }); - slide.addText('목차 (Table of Contents)', { - x: 0.5, y: 0.15, w: 5, h: 0.4, - fontSize: 20, bold: true, color: colors.white - }); - - const tocItems = this.config.tocItems || [ - { num: '01', title: '프로젝트 개요', desc: '시스템 목적 및 범위' }, - { num: '02', title: '메뉴 구조 (IA)', desc: 'Information Architecture' }, - { num: '03', title: '화면 설계', desc: '상세 스토리보드' } - ]; - - tocItems.forEach((item, idx) => { - const y = 1.1 + idx * 0.7; - - // 번호 박스 - slide.addShape('rect', { - x: 0.8, y: y, w: 0.6, h: 0.5, - fill: { color: colors.primary } - }); - slide.addText(item.num, { - x: 0.8, y: y, w: 0.6, h: 0.5, - fontSize: 14, bold: true, color: colors.white, - align: 'center', valign: 'middle' - }); - - // 제목 - slide.addText(item.title, { - x: 1.6, y: y, w: 3, h: 0.5, - fontSize: 14, bold: true, color: colors.secondary, - valign: 'middle' - }); - - // 설명 - slide.addText(item.desc, { - x: 4.8, y: y, w: 4, h: 0.5, - fontSize: 11, color: colors.darkGray, - valign: 'middle' - }); - }); - - console.log('✅ 슬라이드 3: 목차 생성 완료'); - } - - // ---------------------------------------- - // 슬라이드 4: 프로젝트 개요 - // ---------------------------------------- - createOverviewSlide() { - const slide = this.pptx.addSlide(); - slide.background = { color: colors.white }; - - // 헤더 - slide.addShape('rect', { - x: 0, y: 0, w: 10, h: 0.7, - fill: { color: colors.secondary } - }); - slide.addText('01. 프로젝트 개요', { - x: 0.5, y: 0.15, w: 5, h: 0.4, - fontSize: 20, bold: true, color: colors.white - }); - - // 목적 - slide.addText('프로젝트 목적', { - x: 0.5, y: 1, w: 4, h: 0.4, - fontSize: 14, bold: true, color: colors.secondary - }); - - slide.addShape('rect', { - x: 0.5, y: 1.4, w: 4.3, h: 1.8, - fill: { color: colors.lightGray } - }); - - slide.addText(this.config.purpose || '업무 프로세스를 시스템화하여\n효율성 향상 및 자동화 구현', { - x: 0.7, y: 1.5, w: 4, h: 1.6, - fontSize: 11, valign: 'top' - }); - - // 주요 기능 - slide.addText('주요 기능', { - x: 5.2, y: 1, w: 4, h: 0.4, - fontSize: 14, bold: true, color: colors.secondary - }); - - const features = this.config.features || [ - '데이터 입력 및 관리', - '자동 계산 및 처리', - '보고서 생성', - '이력 관리' - ]; - - features.slice(0, 5).forEach((feat, idx) => { - slide.addText('✓ ' + feat, { - x: 5.2, y: 1.4 + idx * 0.35, w: 4.3, h: 0.35, - fontSize: 10, color: colors.secondary - }); - }); - - // 기대 효과 - slide.addText('기대 효과', { - x: 0.5, y: 3.5, w: 9, h: 0.4, - fontSize: 14, bold: true, color: colors.secondary - }); - - const effects = this.config.effects || [ - { icon: '⏱️', title: '시간 단축', desc: '업무 처리 시간 감소' }, - { icon: '✅', title: '정확성 향상', desc: '오류 최소화' }, - { icon: '📊', title: '표준화', desc: '프로세스 통일' } - ]; - - effects.forEach((eff, idx) => { - const x = 0.5 + idx * 3.1; - slide.addShape('rect', { - x: x, y: 3.9, w: 2.9, h: 1.4, - fill: { color: colors.lightGray }, - line: { color: colors.primary, width: 1 } - }); - slide.addText(eff.icon, { - x: x, y: 4, w: 2.9, h: 0.5, - fontSize: 24, align: 'center' - }); - slide.addText(eff.title, { - x: x, y: 4.5, w: 2.9, h: 0.35, - fontSize: 11, bold: true, color: colors.secondary, - align: 'center' - }); - slide.addText(eff.desc, { - x: x, y: 4.85, w: 2.9, h: 0.35, - fontSize: 9, color: colors.darkGray, - align: 'center' - }); - }); - - console.log('✅ 슬라이드 4: 프로젝트 개요 생성 완료'); - } - - // ---------------------------------------- - // 슬라이드 5: 메뉴 구조 (IA) - // ---------------------------------------- - createIASlide() { - const slide = this.pptx.addSlide(); - slide.background = { color: colors.white }; - - // 헤더 - slide.addShape('rect', { - x: 0, y: 0, w: 10, h: 0.7, - fill: { color: colors.secondary } - }); - slide.addText('02. 메뉴 구조 (Information Architecture)', { - x: 0.5, y: 0.15, w: 6, h: 0.4, - fontSize: 20, bold: true, color: colors.white - }); - - // 루트 노드 - const rootName = this.config.projectName ? - this.config.projectName.replace(/시스템|프로젝트|기획/g, '').trim() : - '시스템'; - - slide.addShape('rect', { - x: 4, y: 1, w: 2, h: 0.6, - fill: { color: colors.secondary } - }); - slide.addText(rootName, { - x: 4, y: 1, w: 2, h: 0.6, - fontSize: 12, bold: true, color: colors.white, - align: 'center', valign: 'middle' - }); - - // 연결선 - slide.addShape('line', { - x: 5, y: 1.6, w: 0, h: 0.4, - line: { color: colors.darkGray, width: 1 } - }); - slide.addShape('line', { - x: 1.5, y: 2, w: 7, h: 0, - line: { color: colors.darkGray, width: 1 } - }); - - // 1차 메뉴 - const level1 = this.config.mainMenus || [ - { title: '메뉴 1', children: ['하위 1-1', '하위 1-2'] }, - { title: '메뉴 2', children: ['하위 2-1', '하위 2-2'] }, - { title: '메뉴 3', children: ['하위 3-1', '하위 3-2'] }, - { title: '메뉴 4', children: ['하위 4-1', '하위 4-2'] } - ]; - - const menuCount = Math.min(level1.length, 4); - const menuWidth = 2.2; - const totalWidth = menuCount * menuWidth + (menuCount - 1) * 0.1; - const startX = (10 - totalWidth) / 2; - - level1.slice(0, 4).forEach((menu, idx) => { - const x = startX + idx * (menuWidth + 0.1); - - // 세로 연결선 - slide.addShape('line', { - x: x + menuWidth / 2, y: 2, w: 0, h: 0.3, - line: { color: colors.darkGray, width: 1 } - }); - - // 1차 메뉴 박스 - slide.addShape('rect', { - x: x, y: 2.3, w: menuWidth, h: 0.5, - fill: { color: colors.primary } - }); - slide.addText(menu.title, { - x: x, y: 2.3, w: menuWidth, h: 0.5, - fontSize: 11, bold: true, color: colors.white, - align: 'center', valign: 'middle' - }); - - // 2차 메뉴 - (menu.children || []).slice(0, 3).forEach((child, cIdx) => { - const y = 3 + cIdx * 0.45; - slide.addShape('rect', { - x: x, y: y, w: menuWidth, h: 0.4, - fill: { color: colors.lightGray }, - line: { color: colors.wireframeBorder, width: 0.5 } - }); - slide.addText(child, { - x: x, y: y, w: menuWidth, h: 0.4, - fontSize: 9, color: colors.secondary, - align: 'center', valign: 'middle' - }); - }); - }); - - console.log('✅ 슬라이드 5: 메뉴 구조 (IA) 생성 완료'); - } - - // ---------------------------------------- - // 스토리보드 슬라이드 (화면 설계) - // ---------------------------------------- - createStoryboardSlide(screenInfo, pageNum) { - const slide = this.pptx.addSlide(); - slide.background = { color: colors.white }; - - // 헤더 정보 테이블 - const headerData = [ - ['Task Name', screenInfo.taskName || '', 'Ver.', 'D1.0', 'Page', String(pageNum)], - ['Route', screenInfo.route || '', 'Screen Name', screenInfo.screenName || '', 'Screen ID', screenInfo.screenId || ''] - ]; - - slide.addTable(headerData, { - x: 0.2, y: 0.1, w: 9.6, h: 0.5, - fontSize: 8, - color: colors.secondary, - border: { pt: 0.5, color: colors.darkGray }, - fill: { color: colors.lightGray }, - colW: [0.8, 2.5, 0.8, 2.5, 0.8, 2.2] - }); - - // 와이어프레임 영역 (좌측) - slide.addShape('rect', { - x: 0.2, y: 0.7, w: 6.8, h: 4.75, - fill: { color: colors.wireframeBg }, - line: { color: colors.wireframeBorder, width: 1 } - }); - - // 와이어프레임 헤더 - slide.addShape('rect', { - x: 0.3, y: 0.8, w: 6.6, h: 0.4, - fill: { color: colors.secondary } - }); - slide.addText(this.config.projectName || '시스템', { - x: 0.4, y: 0.8, w: 2, h: 0.4, - fontSize: 10, bold: true, color: colors.white, - valign: 'middle' - }); - - // 와이어프레임 내용 - if (screenInfo.wireframeElements) { - screenInfo.wireframeElements.forEach(el => { - if (el.type === 'rect') { - slide.addShape('rect', { - x: el.x, y: el.y, w: el.w, h: el.h, - fill: { color: el.fill || colors.white }, - line: { color: colors.wireframeBorder, width: 0.5 } - }); - } - if (el.text) { - slide.addText(el.text, { - x: el.x, y: el.y, w: el.w, h: el.h, - fontSize: el.fontSize || 8, - color: el.color || colors.secondary, - align: el.align || 'center', - valign: 'middle', - bold: el.bold || false - }); - } - }); - } else { - // 기본 와이어프레임 구조 - // 사이드바 - slide.addShape('rect', { - x: 0.3, y: 1.3, w: 1.2, h: 4, - fill: { color: colors.lightGray } - }); - slide.addText('메뉴', { - x: 0.3, y: 1.4, w: 1.2, h: 0.3, - fontSize: 8, color: colors.secondary, align: 'center' - }); - - // 메인 콘텐츠 영역 - slide.addShape('rect', { - x: 1.6, y: 1.3, w: 5.3, h: 0.4, - fill: { color: colors.white } - }); - slide.addText(screenInfo.screenName || '화면 제목', { - x: 1.6, y: 1.3, w: 5.3, h: 0.4, - fontSize: 12, bold: true, color: colors.secondary, align: 'left' - }); - - // 콘텐츠 영역 플레이스홀더 - slide.addShape('rect', { - x: 1.6, y: 1.8, w: 5.3, h: 3.4, - fill: { color: colors.white }, - line: { color: colors.wireframeBorder, width: 0.5, dashType: 'dash' } - }); - slide.addText('콘텐츠 영역', { - x: 1.6, y: 3.2, w: 5.3, h: 0.4, - fontSize: 10, color: colors.darkGray, align: 'center' - }); - } - - // Description 영역 (우측) - slide.addShape('rect', { - x: 7.1, y: 0.7, w: 2.7, h: 4.75, - fill: { color: colors.black } - }); - - // Description 헤더 - slide.addShape('rect', { - x: 7.2, y: 0.8, w: 2.5, h: 0.35, - fill: { color: colors.accent } - }); - slide.addText('Description', { - x: 7.2, y: 0.8, w: 2.5, h: 0.35, - fontSize: 9, bold: true, color: colors.white, - align: 'center', valign: 'middle' - }); - - // Description 항목 - const descriptions = screenInfo.descriptions || [ - { title: '기능 1', content: '기능 설명 1' }, - { title: '기능 2', content: '기능 설명 2' }, - { title: '기능 3', content: '기능 설명 3' } - ]; - - descriptions.slice(0, 5).forEach((desc, idx) => { - const y = 1.25 + idx * 0.85; - - // 번호 원 - slide.addShape('ellipse', { - x: 7.25, y: y, w: 0.25, h: 0.25, - fill: { color: colors.accent } - }); - slide.addText(String(idx + 1), { - x: 7.25, y: y, w: 0.25, h: 0.25, - fontSize: 7, bold: true, color: colors.white, - align: 'center', valign: 'middle' - }); - - // 제목 - slide.addText(desc.title, { - x: 7.55, y: y, w: 2.1, h: 0.25, - fontSize: 8, bold: true, color: colors.white - }); - - // 설명 - slide.addText(desc.content, { - x: 7.25, y: y + 0.28, w: 2.4, h: 0.5, - fontSize: 7, color: colors.darkGray - }); - }); - - console.log(`✅ 슬라이드 ${pageNum}: ${screenInfo.screenName || '화면'} 생성 완료`); - } - - // ---------------------------------------- - // 전체 생성 - // ---------------------------------------- - async generate() { - console.log('🚀 스토리보드 PPTX 생성 시작\n'); - - try { - // 기본 슬라이드 - this.createCoverSlide(); - this.createHistorySlide(); - this.createTOCSlide(); - this.createOverviewSlide(); - this.createIASlide(); - - // 스토리보드 슬라이드 - const screens = this.config.screens || []; - screens.forEach((screen, idx) => { - this.createStoryboardSlide(screen, idx + 6); - }); - - // 파일 저장 - const outputPath = this.config.outputPath || 'storyboard.pptx'; - const outputDir = path.dirname(outputPath); - - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - await this.pptx.writeFile({ fileName: outputPath }); - - // 결과 출력 - const stats = fs.statSync(outputPath); - console.log('\n🎉 스토리보드 PPTX 생성 완료!'); - console.log(`📁 저장 위치: ${outputPath}`); - console.log(`📏 파일 크기: ${(stats.size / 1024).toFixed(1)} KB`); - console.log(`📊 총 슬라이드: ${5 + screens.length}장`); - - return outputPath; - } catch (error) { - console.error('❌ 생성 실패:', error.message); - throw error; - } - } -} - -// ======================================== -// CLI 실행 -// ======================================== -async function main() { - const args = process.argv.slice(2); - - // 인자 파싱 - let inputFile = null; - let outputFile = null; - let configFile = null; - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--input' && args[i + 1]) { - inputFile = args[i + 1]; - i++; - } else if (args[i] === '--output' && args[i + 1]) { - outputFile = args[i + 1]; - i++; - } else if (args[i] === '--config' && args[i + 1]) { - configFile = args[i + 1]; - i++; - } - } - - let config = {}; - - // 설정 파일이 있으면 로드 - if (configFile && fs.existsSync(configFile)) { - config = JSON.parse(fs.readFileSync(configFile, 'utf-8')); - console.log(`📄 설정 파일 로드: ${configFile}`); - } - - // 입력 파일 분석 - if (inputFile && fs.existsSync(inputFile)) { - console.log(`📄 입력 파일 분석: ${inputFile}`); - const content = fs.readFileSync(inputFile, 'utf-8'); - const parser = new TextParser(content).parse(); - - // 파싱 결과를 config에 병합 - if (parser.metadata.projectName) { - config.projectName = config.projectName || parser.metadata.projectName; - } - if (parser.metadata.date) { - config.date = config.date || parser.metadata.date; - } - if (parser.metadata.author) { - config.author = config.author || parser.metadata.author; - } - } - - // 출력 파일 설정 - if (outputFile) { - config.outputPath = outputFile; - } else if (!config.outputPath) { - config.outputPath = 'storyboard.pptx'; - } - - // 생성 - const generator = new StoryboardGenerator(config); - await generator.generate(); -} - -// 모듈 내보내기 -module.exports = { TextParser, StoryboardGenerator, colors }; - -// CLI 실행 -if (require.main === module) { - main().catch(console.error); -} diff --git a/.claude/skills/storyboard-generator/scripts/package-lock.json b/.claude/skills/storyboard-generator/scripts/package-lock.json deleted file mode 100755 index 57411a5..0000000 --- a/.claude/skills/storyboard-generator/scripts/package-lock.json +++ /dev/null @@ -1,765 +0,0 @@ -{ - "name": "scripts", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "scripts", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "playwright": "^1.57.0", - "pptxgenjs": "^4.0.1", - "sharp": "^0.34.5" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", - "license": "ISC" - }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.57.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/pptxgenjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", - "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", - "license": "MIT", - "dependencies": { - "@types/node": "^22.8.1", - "https": "^1.0.0", - "image-size": "^1.2.1", - "jszip": "^3.10.1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - } - } -} diff --git a/.claude/skills/storyboard-generator/scripts/package.json b/.claude/skills/storyboard-generator/scripts/package.json deleted file mode 100755 index e4c29db..0000000 --- a/.claude/skills/storyboard-generator/scripts/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "scripts", - "version": "1.0.0", - "description": "", - "main": "generate-storyboard.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "commonjs", - "dependencies": { - "playwright": "^1.57.0", - "pptxgenjs": "^4.0.1", - "sharp": "^0.34.5" - } -} diff --git a/.claude/skills/storyboard-generator/templates/storyboard-slide.html b/.claude/skills/storyboard-generator/templates/storyboard-slide.html deleted file mode 100755 index 3de87ac..0000000 --- a/.claude/skills/storyboard-generator/templates/storyboard-slide.html +++ /dev/null @@ -1,349 +0,0 @@ - - - - - - - - - -
                                      -
                                      -
                                      -

                                      Task Name

                                      -

                                      {{taskName}}

                                      -

                                      Ver.

                                      -

                                      D1.0

                                      -

                                      Page

                                      -

                                      {{page}}

                                      -
                                      -
                                      -

                                      Route

                                      -

                                      {{route}}

                                      -

                                      Screen Name

                                      -

                                      {{screenName}}

                                      -

                                      Screen ID

                                      -

                                      {{screenId}}

                                      -
                                      -
                                      -
                                      - - -
                                      - -
                                      -
                                      -

                                      {{systemName}}

                                      -
                                      -
                                      - - - - -
                                      - {{content}} -
                                      -
                                      -
                                      - - -
                                      -
                                      -

                                      Description

                                      -
                                      - {{#each descriptions}} -
                                      -
                                      -
                                      -

                                      {{index}}

                                      -
                                      -
                                      -

                                      {{title}}

                                      -
                                      -
                                      -
                                      -

                                      {{content}}

                                      -
                                      -
                                      - {{/each}} -
                                      -
                                      - - diff --git a/.claude/skills/system-debug-logger/SKILL.md b/.claude/skills/system-debug-logger/SKILL.md deleted file mode 100755 index 9bb43b9..0000000 --- a/.claude/skills/system-debug-logger/SKILL.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -name: system-debug-logger -description: 애플리케이션에 에러 및 예외를 자동으로 캡처하는 디버그 로깅 기능을 추가합니다. 로깅 추가, 디버그 설정, 에러 추적 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# System Debug Logger - -애플리케이션에 체계적인 디버그 로깅 기능을 추가하는 스킬입니다. - -## 기능 - -### 로깅 레벨 -- **ERROR**: 오류 및 예외 상황 -- **WARN**: 경고 및 잠재적 문제 -- **INFO**: 일반 정보성 메시지 -- **DEBUG**: 상세 디버깅 정보 -- **TRACE**: 매우 상세한 추적 정보 - -### 캡처 대상 -- 예외 및 에러 -- API 요청/응답 -- 데이터베이스 쿼리 -- 성능 메트릭 -- 사용자 액션 - -### 출력 형식 -- 콘솔 출력 -- 파일 로깅 (일별 롤링) -- JSON 구조화 로그 -- 외부 서비스 전송 (선택) - -## 구현 패턴 - -### JavaScript/Node.js -```javascript -const logger = { - levels: { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3, TRACE: 4 }, - currentLevel: 2, // INFO - - log(level, message, meta = {}) { - if (this.levels[level] <= this.currentLevel) { - const timestamp = new Date().toISOString(); - const logEntry = { - timestamp, - level, - message, - ...meta - }; - console.log(JSON.stringify(logEntry)); - } - }, - - error(message, error = null) { - this.log('ERROR', message, { - error: error?.message, - stack: error?.stack - }); - }, - - warn(message, meta) { this.log('WARN', message, meta); }, - info(message, meta) { this.log('INFO', message, meta); }, - debug(message, meta) { this.log('DEBUG', message, meta); }, - trace(message, meta) { this.log('TRACE', message, meta); } -}; - -// 전역 에러 핸들러 -process.on('uncaughtException', (error) => { - logger.error('Uncaught Exception', error); - process.exit(1); -}); - -process.on('unhandledRejection', (reason) => { - logger.error('Unhandled Rejection', { reason }); -}); -``` - -### Python -```python -import logging -import json -from datetime import datetime - -class JSONFormatter(logging.Formatter): - def format(self, record): - log_entry = { - 'timestamp': datetime.utcnow().isoformat(), - 'level': record.levelname, - 'message': record.getMessage(), - 'module': record.module, - 'function': record.funcName, - 'line': record.lineno - } - if record.exc_info: - log_entry['exception'] = self.formatException(record.exc_info) - return json.dumps(log_entry) - -# 로거 설정 -logger = logging.getLogger(__name__) -handler = logging.StreamHandler() -handler.setFormatter(JSONFormatter()) -logger.addHandler(handler) -logger.setLevel(logging.DEBUG) - -# 데코레이터로 함수 호출 로깅 -def log_calls(func): - def wrapper(*args, **kwargs): - logger.debug(f'Calling {func.__name__}', - extra={'args': args, 'kwargs': kwargs}) - try: - result = func(*args, **kwargs) - logger.debug(f'{func.__name__} returned', - extra={'result': result}) - return result - except Exception as e: - logger.error(f'{func.__name__} failed', exc_info=True) - raise - return wrapper -``` - -### Express 미들웨어 -```javascript -const requestLogger = (req, res, next) => { - const start = Date.now(); - - res.on('finish', () => { - const duration = Date.now() - start; - logger.info('HTTP Request', { - method: req.method, - url: req.url, - status: res.statusCode, - duration: `${duration}ms`, - userAgent: req.get('User-Agent'), - ip: req.ip - }); - }); - - next(); -}; - -const errorLogger = (err, req, res, next) => { - logger.error('Request Error', { - method: req.method, - url: req.url, - error: err.message, - stack: err.stack - }); - next(err); -}; -``` - -## 로그 파일 구조 - -``` -logs/ -├── app-2024-01-15.log # 일별 로그 -├── error-2024-01-15.log # 에러 전용 -└── debug/ - └── trace-2024-01-15.log # 상세 추적 -``` - -## 사용 예시 - -``` -이 프로젝트에 디버그 로깅을 추가해줘 -API 요청을 로깅하는 미들웨어를 만들어줘 -에러 추적 시스템을 설정해줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/test-coverage-auditor/SKILL.md b/.claude/skills/test-coverage-auditor/SKILL.md deleted file mode 100644 index 96e2772..0000000 --- a/.claude/skills/test-coverage-auditor/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: ln-634-test-coverage-auditor -description: Coverage Gaps audit worker (L3). Identifies missing tests for critical paths (Money 20+, Security 20+, Data Integrity 15+, Core Flows 15+). Returns list of untested critical business logic with priority justification. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Coverage Gaps Auditor (L3 Worker) - -Specialized worker identifying missing tests for critical business logic. - -## Purpose & Scope - -- **Worker in ln-630 coordinator pipeline** -- Audit **Coverage Gaps** (Category 4: High Priority) -- Identify untested critical paths -- Classify by category (Money, Security, Data, Core Flows) -- Calculate compliance score (X/10) - -## Inputs (from Coordinator) - -**MANDATORY READ:** Load `shared/references/task_delegation_pattern.md#audit-coordinator--worker-contract` for contextStore structure. - -Receives `contextStore` with: `tech_stack`, `testFilesMetadata`, `codebase_root`. - -**Domain-aware:** Supports `domain_mode` + `current_domain` (see `audit_output_schema.md#domain-aware-worker-output`). - -## Workflow - -1) **Parse context** — extract fields, determine `scan_path` (domain-aware if specified) - ELSE: - scan_path = codebase_root - domain_name = null - ``` - -2) **Identify critical paths in scan_path** (not entire codebase) - - Scan production code in `scan_path` for money/security/data keywords - - All Grep/Glob patterns use `scan_path` (not codebase_root) - - Example: `Grep(pattern="payment|refund|discount", path=scan_path)` - -3) **Check test coverage for each critical path** - - Search ALL test files for coverage (tests may be in different location than production code) - - Match by function name, module name, or test description - -4) **Collect missing tests** - - Tag each finding with `domain: domain_name` (if domain-aware) - -5) **Calculate score** - -6) **Return JSON with domain metadata** - - Include `domain` and `scan_path` fields (if domain-aware) - -## Critical Paths Classification - -### 1. Money Flows (Priority 20+) - -**What:** Any code handling financial transactions - -**Examples:** -- Payment processing (`/payment`, `processPayment()`) -- Discounts/promotions (`calculateDiscount()`, `applyPromoCode()`) -- Tax calculations (`calculateTax()`, `getTaxRate()`) -- Refunds (`processRefund()`, `/refund`) -- Invoices/billing (`generateInvoice()`, `createBill()`) -- Currency conversion (`convertCurrency()`) - -**Min Priority:** 20 - -**Why Critical:** Money loss, fraud, legal compliance - -### 2. Security Flows (Priority 20+) - -**What:** Authentication, authorization, encryption - -**Examples:** -- Login/logout (`/login`, `authenticate()`) -- Token refresh (`/refresh-token`, `refreshAccessToken()`) -- Password reset (`/forgot-password`, `resetPassword()`) -- Permissions/RBAC (`checkPermission()`, `hasRole()`) -- Encryption/hashing (custom crypto logic, NOT bcrypt/argon2) -- API key validation (`validateApiKey()`) - -**Min Priority:** 20 - -**Why Critical:** Security breach, data leak, unauthorized access - -### 3. Data Integrity (Priority 15+) - -**What:** CRUD operations, transactions, validation - -**Examples:** -- Critical CRUD (`createUser()`, `deleteOrder()`, `updateProduct()`) -- Database transactions (`withTransaction()`) -- Data validation (custom validators, NOT framework defaults) -- Data migrations (`runMigration()`) -- Unique constraints (`checkDuplicateEmail()`) - -**Min Priority:** 15 - -**Why Critical:** Data corruption, lost data, inconsistent state - -### 4. Core User Journeys (Priority 15+) - -**What:** Multi-step flows critical to business - -**Examples:** -- Registration → Email verification → Onboarding -- Search → Product details → Add to cart → Checkout -- Upload file → Process → Download result -- Submit form → Approval workflow → Notification - -**Min Priority:** 15 - -**Why Critical:** Broken user flow = lost customers - -## Audit Rules - -### 1. Identify Critical Paths - -**Process:** -- Scan codebase for money-related keywords: `payment`, `refund`, `discount`, `tax`, `price`, `currency` -- Scan for security keywords: `auth`, `login`, `password`, `token`, `permission`, `encrypt` -- Scan for data keywords: `transaction`, `validation`, `migration`, `constraint` -- Scan for user journeys: multi-step flows in routes/controllers - -### 2. Check Test Coverage - -**For each critical path:** -- Search test files for matching test name/description -- If NO test found → add to missing tests list -- If test found but inadequate (only positive, no edge cases) → add to gaps list - -### 3. Categorize Gaps - -**Severity by Priority:** -- **CRITICAL:** Priority 20+ (Money, Security) -- **HIGH:** Priority 15-19 (Data, Core Flows) -- **MEDIUM:** Priority 10-14 (Important but not critical) - -### 4. Provide Justification - -**For each missing test:** -- Explain WHY it's critical (money loss, security breach, etc.) -- Suggest test type (E2E, Integration, Unit) -- Estimate effort (S/M/L) - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -**Severity mapping by Priority:** -- Priority 20+ (Money, Security) missing test → CRITICAL -- Priority 15-19 (Data Integrity, Core Flows) missing test → HIGH -- Priority 10-14 (Important) missing test → MEDIUM -- Priority <10 (Nice-to-have) → LOW - -## Output Format - -**Return JSON to coordinator:** -```json -{ - "category": "Coverage Gaps", - "score": 6, - "total_issues": 10, - "critical": 3, - "high": 4, - "medium": 2, - "low": 1, - "checks": [ - {"id": "line_coverage", "name": "Line Coverage", "status": "passed", "details": "85% coverage (threshold: 80%)"}, - {"id": "branch_coverage", "name": "Branch Coverage", "status": "warning", "details": "72% coverage (threshold: 75%)"}, - {"id": "function_coverage", "name": "Function Coverage", "status": "passed", "details": "90% coverage (threshold: 80%)"}, - {"id": "critical_gaps", "name": "Critical Gaps", "status": "failed", "details": "3 Money flows, 2 Security flows untested"} - ], - "domain": "orders", - "scan_path": "src/orders", - "findings": [ - { - "severity": "CRITICAL", - "location": "src/orders/services/order.ts:45", - "issue": "Missing E2E test for applyDiscount() (Priority 25, Money flow)", - "principle": "Coverage Gaps / Money Flow", - "recommendation": "Add E2E test: applyDiscount() with edge cases (negative discount, max discount, currency rounding)", - "effort": "M" - }, - { - "severity": "HIGH", - "location": "src/orders/repositories/order.ts:78", - "issue": "Missing Integration test for orderTransaction() rollback (Priority 18, Data Integrity)", - "principle": "Coverage Gaps / Data Integrity", - "recommendation": "Add Integration test verifying transaction rollback on failure", - "effort": "M" - } - ] -} -``` - -**Note:** `domain` and `scan_path` fields included only when `domain_mode="domain-aware"`. - -## Critical Rules - -- **Domain-aware scanning:** If `domain_mode="domain-aware"`, scan ONLY `scan_path` production code (not entire codebase) -- **Tag findings:** Include `domain` field in each finding when domain-aware -- **Test search scope:** Search ALL test files for coverage (tests may be in different location than production code) -- **Match by name:** Use function name, module name, or test description to match tests to production code - -## Definition of Done - -- contextStore parsed (including domain_mode and current_domain) -- scan_path determined (domain path or codebase root) -- Critical paths identified in scan_path (Money, Security, Data, Core Flows) -- Test coverage checked for each critical path -- Missing tests collected with severity, priority, justification, domain -- Score calculated -- JSON returned to coordinator with domain metadata - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/test-isolation-auditor/SKILL.md b/.claude/skills/test-isolation-auditor/SKILL.md deleted file mode 100644 index 63ffff0..0000000 --- a/.claude/skills/test-isolation-auditor/SKILL.md +++ /dev/null @@ -1,367 +0,0 @@ ---- -name: ln-635-test-isolation-auditor -description: Test Isolation + Anti-Patterns audit worker (L3). Checks isolation (APIs/DB/FS/Time/Random/Network), determinism (flaky, order-dependent), and 6 anti-patterns. -allowed-tools: Read, Grep, Glob, Bash ---- - -# Test Isolation & Anti-Patterns Auditor (L3 Worker) - -Specialized worker auditing test isolation and detecting anti-patterns. - -## Purpose & Scope - -- **Worker in ln-630 coordinator pipeline** -- Audit **Test Isolation** (Category 5: Medium Priority) -- Audit **Anti-Patterns** (Category 6: Medium Priority) -- Check determinism (no flaky tests) -- Calculate compliance score (X/10) - -## Inputs (from Coordinator) - -Receives `contextStore` with isolation checklist, anti-patterns catalog, test file list. - -## Workflow - -1) Parse context -2) Check isolation for 6 categories -3) Check determinism -4) Detect 6 anti-patterns -5) Collect findings -6) Calculate score -7) Return JSON - -## Audit Rules: Test Isolation - -### 1. External APIs - -**Good:** Mocked (jest.mock, sinon, nock) -**Bad:** Real HTTP calls to external APIs - -**Detection:** -- Grep for `axios.get`, `fetch(`, `http.request` without mocks -- Check if test makes actual network calls - -**Severity:** **HIGH** - -**Recommendation:** Mock external APIs with `nock` or `jest.mock` - -**Effort:** M - -### 2. Database - -**Good:** In-memory DB (sqlite :memory:) or mocked -**Bad:** Real database (PostgreSQL, MySQL) - -**Detection:** -- Check DB connection strings (localhost:5432, real DB URL) -- Grep for `beforeAll(async () => { await db.connect() })` without `:memory:` - -**Severity:** **MEDIUM** - -**Recommendation:** Use in-memory DB or mock DB calls - -**Effort:** M-L - -### 3. File System - -**Good:** Mocked (mock-fs, vol) -**Bad:** Real file reads/writes - -**Detection:** -- Grep for `fs.readFile`, `fs.writeFile` without mocks -- Check if test creates/deletes real files - -**Severity:** **MEDIUM** - -**Recommendation:** Mock file system with `mock-fs` - -**Effort:** S-M - -### 4. Time/Date - -**Good:** Mocked (jest.useFakeTimers, sinon.useFakeTimers) -**Bad:** `new Date()`, `Date.now()` without mocks - -**Detection:** -- Grep for `new Date()` in test files without `useFakeTimers` - -**Severity:** **MEDIUM** - -**Recommendation:** Mock time with `jest.useFakeTimers()` - -**Effort:** S - -### 5. Random - -**Good:** Seeded random (Math.seedrandom, fixed seed) -**Bad:** `Math.random()` without seed - -**Detection:** -- Grep for `Math.random()` without seed setup - -**Severity:** **LOW** - -**Recommendation:** Use seeded random for deterministic tests - -**Effort:** S - -### 6. Network - -**Good:** Mocked (supertest for Express, no real ports) -**Bad:** Real network requests (`localhost:3000`, binding to port) - -**Detection:** -- Grep for `app.listen(3000)` in tests -- Check for real HTTP requests - -**Severity:** **MEDIUM** - -**Recommendation:** Use `supertest` (no real port) - -**Effort:** M - -## Audit Rules: Determinism - -### 1. Flaky Tests - -**What:** Tests that pass/fail randomly - -**Detection:** -- Run tests multiple times, check for inconsistent results -- Grep for `setTimeout`, `setInterval` without proper awaits -- Check for race conditions (async operations not awaited) - -**Severity:** **HIGH** - -**Recommendation:** Fix race conditions, use proper async/await - -**Effort:** M-L - -### 2. Time-Dependent Assertions - -**What:** Assertions on current time (`expect(timestamp).toBeCloseTo(Date.now())`) - -**Detection:** -- Grep for `Date.now()`, `new Date()` in assertions - -**Severity:** **MEDIUM** - -**Recommendation:** Mock time - -**Effort:** S - -### 3. Order-Dependent Tests - -**What:** Tests that fail when run in different order - -**Detection:** -- Run tests in random order, check for failures -- Grep for shared mutable state between tests - -**Severity:** **MEDIUM** - -**Recommendation:** Isolate tests, reset state in beforeEach - -**Effort:** M - -### 4. Shared Mutable State - -**What:** Global variables modified across tests - -**Detection:** -- Grep for `let globalVar` at module level -- Check for state shared between tests - -**Severity:** **MEDIUM** - -**Recommendation:** Use `beforeEach` to reset state - -**Effort:** S-M - -## Audit Rules: Anti-Patterns - -### 1. The Liar (Always Passes) - -**What:** Test with no assertions or trivial assertion (`expect().toBeTruthy()`) - -**Detection:** -- Count assertions per test -- If 0 assertions or only `toBeTruthy()` → Liar - -**Severity:** **HIGH** - -**Recommendation:** Add specific assertions or delete test - -**Effort:** S - -**Example:** -- **BAD (Liar):** Test calls `createUser()` but has NO assertions — always passes even if function breaks -- **GOOD:** Test calls `createUser()` and asserts `user.name` equals 'Alice', `user.id` is defined - -### 2. The Giant (>100 lines) - -**What:** Test with >100 lines, testing too many scenarios - -**Detection:** -- Count lines per test -- If >100 lines → Giant - -**Severity:** **MEDIUM** - -**Recommendation:** Split into focused tests (one scenario per test) - -**Effort:** S-M - -### 3. Slow Poke (>5 seconds) - -**What:** Test taking >5 seconds to run - -**Detection:** -- Measure test duration -- If >5s → Slow Poke - -**Severity:** **MEDIUM** - -**Recommendation:** Mock external deps, use in-memory DB, parallelize - -**Effort:** M - -### 4. Conjoined Twins (Unit test without mocks = Integration) - -**What:** Test labeled "Unit" but not mocking dependencies - -**Detection:** -- Check if test name includes "Unit" -- Verify all dependencies are mocked -- If no mocks → actually Integration test - -**Severity:** **LOW** - -**Recommendation:** Either mock dependencies OR rename to Integration test - -**Effort:** S - -### 5. Happy Path Only (No error scenarios) - -**What:** Only testing success cases, ignoring errors - -**Detection:** -- For each function, check if test covers error cases -- If only positive scenarios → Happy Path Only - -**Severity:** **MEDIUM** - -**Recommendation:** Add negative tests (error handling, edge cases) - -**Effort:** M - -**Example:** -- **BAD (Happy Path Only):** Test only checks `login()` with valid credentials, ignores error scenarios -- **GOOD:** Add negative test that verifies `login()` with invalid credentials throws 'Invalid credentials' error - -### 6. Framework Tester (Tests framework behavior) - -**What:** Tests validating Express/Prisma/bcrypt (NOT our code) - -**Detection:** -- Already detected by ln-631-test-business-logic-auditor -- Cross-reference findings - -**Severity:** **MEDIUM** - -**Recommendation:** Delete framework tests - -**Effort:** S - -## Scoring Algorithm - -See `shared/references/audit_scoring.md` for unified formula and score interpretation. - -**Severity mapping:** -- Flaky tests, External API not mocked, The Liar → HIGH -- Real database, File system, Time/Date, Network, The Giant, Happy Path Only → MEDIUM -- Random without seed, Order-dependent, Conjoined Twins → LOW - -## Output Format - -**Return JSON to coordinator (flat findings array):** -```json -{ - "category": "Isolation & Anti-Patterns", - "score": 6, - "total_issues": 18, - "critical": 0, - "high": 5, - "medium": 10, - "low": 3, - "checks": [ - {"id": "api_isolation", "name": "API Isolation", "status": "failed", "details": "2 tests make real HTTP calls"}, - {"id": "db_isolation", "name": "Database Isolation", "status": "warning", "details": "1 test uses real PostgreSQL"}, - {"id": "fs_isolation", "name": "File System Isolation", "status": "passed", "details": "All FS calls mocked"}, - {"id": "time_isolation", "name": "Time Isolation", "status": "passed", "details": "All Date/Time mocked"}, - {"id": "flaky_tests", "name": "Flaky Tests", "status": "failed", "details": "3 race conditions detected"}, - {"id": "anti_patterns", "name": "Anti-Patterns", "status": "warning", "details": "2 Liars, 1 Giant found"} - ], - "findings": [ - { - "severity": "HIGH", - "location": "user.test.ts:45-52", - "issue": "External API not mocked — test makes real HTTP call to https://api.github.com", - "principle": "Test Isolation / External APIs", - "recommendation": "Mock external API with nock or jest.mock", - "effort": "M" - }, - { - "severity": "HIGH", - "location": "async.test.ts:28-35", - "issue": "Flaky test (race condition) — setTimeout without proper await", - "principle": "Determinism / Race Condition", - "recommendation": "Fix race condition with proper async/await", - "effort": "M" - }, - { - "severity": "HIGH", - "location": "user.test.ts:45", - "issue": "Anti-pattern 'The Liar' — test 'createUser works' has no assertions", - "principle": "Anti-Patterns / The Liar", - "recommendation": "Add specific assertions or delete test", - "effort": "S" - }, - { - "severity": "MEDIUM", - "location": "db.test.ts:12", - "issue": "Real database used — test connects to localhost:5432 PostgreSQL", - "principle": "Test Isolation / Database", - "recommendation": "Use in-memory SQLite (:memory:) or mock DB", - "effort": "L" - }, - { - "severity": "MEDIUM", - "location": "order.test.ts:200-350", - "issue": "Anti-pattern 'The Giant' — test 'order flow' is 150 lines (>100)", - "principle": "Anti-Patterns / The Giant", - "recommendation": "Split into focused tests (one scenario per test)", - "effort": "M" - }, - { - "severity": "MEDIUM", - "location": "payment.test.ts", - "issue": "Anti-pattern 'Happy Path Only' — only success scenarios, no error tests", - "principle": "Anti-Patterns / Happy Path Only", - "recommendation": "Add negative tests for error handling", - "effort": "M" - } - ] -} -``` - -**Note:** Findings are flattened into single array. Use `principle` field prefix (Test Isolation / Determinism / Anti-Patterns) to identify issue category. - -## Reference Files - -- **Audit scoring formula:** `shared/references/audit_scoring.md` -- **Audit output schema:** `shared/references/audit_output_schema.md` - ---- -**Version:** 3.0.0 -**Last Updated:** 2025-12-23 diff --git a/.claude/skills/text-analyzer-skill/SKILL.md b/.claude/skills/text-analyzer-skill/SKILL.md deleted file mode 100755 index 842642a..0000000 --- a/.claude/skills/text-analyzer-skill/SKILL.md +++ /dev/null @@ -1,599 +0,0 @@ ---- -name: text-analyzer-skill -description: 자연어 텍스트 파일을 분석하여 PDF 템플릿 구조에 매핑하는 스킬. 섹션 추출, 콘텐츠 분류, 구조화된 데이터 생성 기능 제공. ---- - -# Text Analyzer Skill - 자연어 텍스트 분석 스킬 - -source 폴더의 txt 파일을 분석하여 PDF 샘플과 동일한 형태의 PPTX로 변환하는 핵심 스킬입니다. - -## 기능 개요 - -### 1. 자연어 텍스트 분석 (Natural Language Processing) -텍스트 파일의 구조와 내용을 자동으로 파싱하고 분류 - -### 2. PDF 템플릿 매핑 (Template Mapping) -분석된 내용을 SAM_ERP 스토리보드 구조에 자동 매핑 - -### 3. 구조화된 데이터 생성 (Structured Data Generation) -PPTX 생성에 필요한 슬라이드별 데이터 구조 생성 - -### 4. 콘텐츠 최적화 (Content Optimization) -프레젠테이션에 적합한 형태로 콘텐츠 가공 - -## 핵심 워크플로우 - -### 텍스트 분석 → PDF 구조 매핑 워크플로우 - -```mermaid -graph TD - A[source/*.txt] --> B[텍스트 파싱] - B --> C[섹션 인식] - C --> D[콘텐츠 분류] - D --> E[PDF 템플릿 매핑] - E --> F[PPTX 데이터 생성] - F --> G[슬라이드 변환] -``` - -### 자연어 분석 엔진 - -#### 1. 섹션 인식 패턴 -```javascript -const SECTION_PATTERNS = { - // 프로젝트 메타 정보 - project_meta: /^(프로젝트명|제목|title):\s*(.+)$/im, - date: /^(작성일|날짜|date):\s*(.+)$/im, - company: /^(회사명|회사|company):\s*(.+)$/im, - author: /^(작성자|저자|author):\s*(.+)$/im, - - // 주요 섹션 구분 - overview: /^=+\s*(개요|프로젝트\s*개요|overview)\s*=+$/im, - features: /^=+\s*(기능|주요\s*기능|features)\s*=+$/im, - screens: /^=+\s*(화면|화면\s*구성|screens)\s*=+$/im, - requirements: /^=+\s*(요구사항|기술\s*요구사항|requirements)\s*=+$/im, - flow: /^=+\s*(플로우|사용자\s*플로우|flow)\s*=+$/im, - - // 하위 항목 - numbered_item: /^(\d+)\.\s*(.+)$/, - bullet_item: /^[-*]\s*(.+)$/, - sub_item: /^\s*[-*]\s*(.+)$/ -}; -``` - -#### 2. 콘텐츠 분류 규칙 -```javascript -const CONTENT_CLASSIFIERS = { - // 화면/기능 관련 - screen_indicators: ['화면', '페이지', '뷰', 'screen', 'page', 'view'], - feature_indicators: ['기능', '모듈', '시스템', 'feature', 'module', 'system'], - ui_indicators: ['버튼', '메뉴', '입력', '목록', 'button', 'menu', 'input', 'list'], - - // 비즈니스 로직 - business_indicators: ['관리', '분석', '처리', '생성', 'management', 'analysis'], - data_indicators: ['데이터', '정보', '내역', 'data', 'information'], - - // 기술 요구사항 - tech_indicators: ['시스템', '플랫폼', '기술', 'system', 'platform', 'technology'] -}; -``` - -### PDF 템플릿 매핑 로직 - -#### SAM_ERP 구조 매핑 테이블 -```javascript -const TEMPLATE_MAPPING = { - // 표지 (Cover) - 1페이지 - cover: { - source_fields: ['project_meta', 'date', 'company', 'author'], - template_layout: 'brand_centered', - required: true - }, - - // 문서 히스토리 - 2페이지 - document_history: { - source_fields: ['date', 'author'], - template_layout: 'table_full_width', - auto_generate: true // 자동 생성 - }, - - // 메뉴 구조 - 3페이지 - menu_structure: { - source_fields: ['features', 'screens'], - template_layout: 'hierarchical_diagram', - extraction_method: 'feature_hierarchy' - }, - - // 공통 가이드라인 - 4-8페이지 - common_guidelines: { - source_fields: ['requirements', 'ui_patterns'], - template_layout: 'documentation_style', - default_sections: ['interaction', 'responsive', 'template', 'notifications'] - }, - - // 상세 화면 - 9-N페이지 - detail_screens: { - source_fields: ['screens', 'features', 'flow'], - template_layout: 'wireframe_with_description', - per_screen: true - } -}; -``` - -## 스크립트 구조 - -### 1. text-parser.js -자연어 텍스트 파싱 및 구조 분석 - -```javascript -class TextParser { - constructor() { - this.sections = new Map(); - this.metadata = {}; - } - - async parseFile(filePath) { - const content = await fs.readFile(filePath, 'utf8'); - return this.parseContent(content); - } - - parseContent(content) { - // 1. 메타데이터 추출 - this.extractMetadata(content); - - // 2. 섹션 구분 - this.extractSections(content); - - // 3. 구조화된 데이터 반환 - return { - metadata: this.metadata, - sections: this.sections, - raw_content: content - }; - } - - extractMetadata(content) { - Object.entries(SECTION_PATTERNS).forEach(([key, pattern]) => { - const match = content.match(pattern); - if (match && key.includes('meta') || ['date', 'company', 'author'].includes(key)) { - this.metadata[key] = match[2]?.trim(); - } - }); - } - - extractSections(content) { - const lines = content.split('\n'); - let currentSection = null; - let currentContent = []; - - lines.forEach(line => { - const sectionMatch = this.findSectionHeader(line); - if (sectionMatch) { - // 이전 섹션 저장 - if (currentSection) { - this.sections.set(currentSection, this.processContent(currentContent)); - } - currentSection = sectionMatch; - currentContent = []; - } else if (currentSection) { - currentContent.push(line); - } - }); - - // 마지막 섹션 저장 - if (currentSection) { - this.sections.set(currentSection, this.processContent(currentContent)); - } - } - - findSectionHeader(line) { - for (const [key, pattern] of Object.entries(SECTION_PATTERNS)) { - if (['overview', 'features', 'screens', 'requirements', 'flow'].includes(key)) { - const match = line.match(pattern); - if (match) return key; - } - } - return null; - } - - processContent(lines) { - const processed = { - raw: lines.join('\n'), - items: [], - structure: 'flat' - }; - - lines.forEach(line => { - line = line.trim(); - if (!line) return; - - // 번호 목록 - const numberedMatch = line.match(SECTION_PATTERNS.numbered_item); - if (numberedMatch) { - processed.items.push({ - type: 'numbered', - number: numberedMatch[1], - content: numberedMatch[2], - children: [] - }); - processed.structure = 'numbered'; - return; - } - - // 불릿 목록 - const bulletMatch = line.match(SECTION_PATTERNS.bullet_item); - if (bulletMatch) { - if (processed.items.length > 0 && line.startsWith(' ')) { - // 하위 항목 - const lastItem = processed.items[processed.items.length - 1]; - lastItem.children.push({ - type: 'sub_bullet', - content: bulletMatch[1] - }); - } else { - processed.items.push({ - type: 'bullet', - content: bulletMatch[1], - children: [] - }); - } - processed.structure = 'hierarchical'; - return; - } - - // 일반 텍스트 - if (processed.items.length === 0) { - processed.items.push({ - type: 'paragraph', - content: line - }); - } else { - const lastItem = processed.items[processed.items.length - 1]; - if (lastItem.type === 'paragraph') { - lastItem.content += ' ' + line; - } else { - processed.items.push({ - type: 'paragraph', - content: line - }); - } - } - }); - - return processed; - } -} -``` - -### 2. template-mapper.js -파싱된 데이터를 PDF 템플릿 구조에 매핑 - -```javascript -class TemplateMapper { - constructor() { - this.pdfTemplate = null; - this.mappedData = {}; - } - - async loadPDFTemplate(templatePath) { - const templateContent = await fs.readFile(templatePath, 'utf8'); - this.pdfTemplate = JSON.parse(templateContent); - } - - mapToTemplate(parsedData) { - const mapped = { - metadata: this.generateMetadata(parsedData.metadata), - slides: [] - }; - - // 1. 표지 생성 - mapped.slides.push(this.createCoverSlide(parsedData.metadata)); - - // 2. 문서 히스토리 생성 - mapped.slides.push(this.createDocumentHistorySlide(parsedData.metadata)); - - // 3. 메뉴 구조 생성 - mapped.slides.push(this.createMenuStructureSlide(parsedData.sections)); - - // 4. 공통 가이드라인 생성 - mapped.slides.push(...this.createCommonGuidelines()); - - // 5. 상세 화면 생성 - mapped.slides.push(...this.createDetailScreens(parsedData.sections)); - - return mapped; - } - - createCoverSlide(metadata) { - return { - type: 'cover', - slide_number: 1, - layout: this.pdfTemplate.page_templates.find(t => t.type === 'cover').layout, - content: { - title: metadata.project_meta || '프로젝트 기획서', - subtitle: '시스템 기획 및 설계', - date: this.formatDate(metadata.date), - company: metadata.company || 'Company Name', - author: metadata.author || '작성자' - } - }; - } - - createDocumentHistorySlide(metadata) { - return { - type: 'document_history', - slide_number: 2, - layout: this.pdfTemplate.page_templates.find(t => t.type === 'document_history').layout, - content: { - title: 'Document History', - table: [ - { - date: this.formatDate(metadata.date), - version: 'D1.0', - main_content: '초안 작성', - detailed_content: `${metadata.project_meta || '프로젝트'} 기획서 초안 작성`, - mark: '' - } - ] - } - }; - } - - createMenuStructureSlide(sections) { - const features = sections.get('features') || { items: [] }; - const screens = sections.get('screens') || { items: [] }; - - // 기능과 화면 정보를 계층 구조로 변환 - const menuStructure = this.buildMenuHierarchy(features, screens); - - return { - type: 'menu_structure', - slide_number: 3, - layout: this.pdfTemplate.page_templates.find(t => t.type === 'system_structure').layout, - content: { - title: 'Menu Structure', - structure: menuStructure - } - }; - } - - buildMenuHierarchy(features, screens) { - const structure = { - root: '시스템 구조', - children: [] - }; - - // 주요 기능을 상위 카테고리로 설정 - features.items.forEach(feature => { - if (feature.type === 'numbered' && feature.content) { - const category = { - name: feature.content, - children: [] - }; - - // 하위 항목이 있으면 추가 - if (feature.children && feature.children.length > 0) { - feature.children.forEach(child => { - category.children.push({ - name: child.content, - leaf: true - }); - }); - } - - structure.children.push(category); - } - }); - - // 화면 정보도 추가 - if (screens.items.length > 0) { - const screenCategory = { - name: '화면 구성', - children: [] - }; - - screens.items.forEach(screen => { - if (screen.content) { - screenCategory.children.push({ - name: screen.content, - leaf: true - }); - } - }); - - structure.children.push(screenCategory); - } - - return structure; - } - - createCommonGuidelines() { - // PDF 템플릿의 공통 가이드라인 구조를 사용 - return [ - { - type: 'common_guidelines', - slide_number: 4, - content: { - title: '공통', - sections: ['사용자 인터랙션', '반응형 웹', '화면 템플릿', '알림 시스템'] - } - } - ]; - } - - createDetailScreens(sections) { - const detailSlides = []; - let slideNumber = 9; // 상세 화면은 9페이지부터 시작 - - // 기능별로 상세 화면 생성 - const features = sections.get('features'); - const screens = sections.get('screens'); - const flow = sections.get('flow'); - - if (features && features.items) { - features.items.forEach(feature => { - if (feature.type === 'numbered') { - detailSlides.push(this.createDetailScreenFromFeature(feature, slideNumber)); - slideNumber++; - } - }); - } - - // 명시적인 화면 구성이 있다면 추가 - if (screens && screens.items) { - screens.items.forEach(screen => { - detailSlides.push(this.createDetailScreenFromScreen(screen, slideNumber)); - slideNumber++; - }); - } - - return detailSlides; - } - - createDetailScreenFromFeature(feature, slideNumber) { - return { - type: 'detail_screen', - slide_number: slideNumber, - layout: this.pdfTemplate.page_templates.find(t => t.type === 'detail_screen').layout, - content: { - header_info: { - task_name: feature.content, - version: 'D1.0', - page_number: slideNumber, - route: this.generateRoute(feature.content), - screen_name: feature.content, - screen_id: this.generateScreenId(feature.content) - }, - wireframe: { - type: 'feature_mockup', - description: `${feature.content} 화면 구성`, - elements: this.extractUIElements(feature) - }, - descriptions: this.generateDescriptions(feature) - } - }; - } - - extractUIElements(feature) { - const elements = []; - - // 자연어에서 UI 요소 추출 - if (feature.children && feature.children.length > 0) { - feature.children.forEach((child, index) => { - elements.push(`${index + 1}. ${child.content}`); - }); - } else { - // 기본 UI 요소 추가 - elements.push('1. 헤더 영역'); - elements.push('2. 메인 콘텐츠'); - elements.push('3. 액션 버튼'); - } - - return elements; - } - - generateDescriptions(feature) { - const descriptions = []; - - if (feature.children && feature.children.length > 0) { - feature.children.forEach((child, index) => { - descriptions.push({ - number: index + 1, - title: this.extractTitle(child.content), - description: child.content - }); - }); - } else { - descriptions.push({ - number: 1, - title: feature.content, - description: `${feature.content} 기능 구현 및 사용자 인터랙션 처리` - }); - } - - return descriptions; - } - - extractTitle(content) { - // 콘텐츠에서 제목 추출 (첫 번째 단어 또는 구문) - const words = content.split(' '); - return words.length > 3 ? words.slice(0, 2).join(' ') : words[0]; - } - - generateRoute(screenName) { - return '/' + screenName - .toLowerCase() - .replace(/\s+/g, '_') - .replace(/[^a-z0-9가-힣_]/g, ''); - } - - generateScreenId(screenName) { - return screenName - .toLowerCase() - .replace(/\s+/g, '_') - .replace(/[^a-z0-9가-힣_]/g, ''); - } - - formatDate(dateStr) { - if (!dateStr) { - return new Date().toISOString().split('T')[0].replace(/-/g, '.'); - } - return dateStr.replace(/-/g, '.'); - } - - generateMetadata(metadata) { - return { - title: metadata.project_meta || '프로젝트 기획서', - version: 'D1.0', - created_date: this.formatDate(metadata.date), - author: metadata.author || '작성자', - company: metadata.company || 'Company Name', - source_file: 'source/*.txt' - }; - } -} -``` - -## 사용법 - -### 1. 기본 실행 -```bash -# source 폴더의 txt 파일 분석 및 PPTX 생성 -node .claude/skills/text-analyzer-skill/scripts/txt-to-pptx.js - -# 또는 간단 명령 -npm run txt-to-ppt -``` - -### 2. 특정 파일 지정 -```bash -node txt-to-pptx.js --input source/my_project.txt --output my_presentation.pptx -``` - -### 3. 템플릿 지정 -```bash -node txt-to-pptx.js --template custom_template.json --input source/project.txt -``` - -## 품질 기준 - -### 텍스트 분석 정확도 -- **섹션 인식률**: >90% (명시적 구분자 기준) -- **콘텐츠 분류**: >85% (키워드 매칭 기준) -- **구조 보존**: 원본 계층 구조 유지 - -### PDF 템플릿 매핑 일치도 -- **레이아웃 일관성**: SAM_ERP 스토리보드와 95% 일치 -- **섹션 완성도**: 필수 섹션 100% 포함 -- **콘텐츠 적합성**: 프레젠테이션 형태로 최적화 - -## 확장 기능 - -### 다양한 텍스트 형식 지원 -- Markdown 파일 (.md) -- Word 문서 (.docx) -- 구조화된 JSON (.json) - -### AI 기반 콘텐츠 최적화 -- 자동 요약 기능 -- 키워드 추출 -- 슬라이드 분량 최적화 \ No newline at end of file diff --git a/.claude/skills/text-analyzer-skill/scripts/txt-to-pptx.js b/.claude/skills/text-analyzer-skill/scripts/txt-to-pptx.js deleted file mode 100755 index 6f3fb9e..0000000 --- a/.claude/skills/text-analyzer-skill/scripts/txt-to-pptx.js +++ /dev/null @@ -1,306 +0,0 @@ -/** - * 작동하는 TXT → PPTX 변환기 (컬러 오류 수정 버전) - */ - -const fs = require('fs').promises; -const PptxGenJS = require('pptxgenjs'); - -// 간단한 텍스트 분석 클래스 -class TextAnalyzer { - constructor() { - this.patterns = { - metadata: /^(.+?):[\s]*(.+)$/, - section: /^=== (.+) ===/, - heading: /^[가-힣A-Za-z].+[가-힣A-Za-z\d]$/, // 한글로 시작하고 끝나는 제목 - numbered_item: /^\d+\.\s*(.+)$/, - bullet_item: /^[•\-\*]\s*(.+)$/, - html_tag: /<[^>]+>/g - }; - } - - /** - * TXT 파일 분석 - */ - async analyzeFile(filePath) { - console.log(`📖 텍스트 파일 분석 중: ${filePath}`); - - const content = await fs.readFile(filePath, 'utf8'); - const lines = content.split('\n'); - - const result = { - metadata: {}, - sections: new Map() - }; - - let currentSection = null; - let currentContent = []; - - // 첫 번째 라인을 제목으로 사용 - if (lines.length > 0) { - result.metadata.title = lines[0].trim(); - } - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmedLine = line.trim(); - if (!trimmedLine) continue; - - // HTML 태그 제거 - const cleanLine = trimmedLine.replace(this.patterns.html_tag, ''); - if (!cleanLine.trim()) continue; - - // 메타데이터 파싱 (처음 3줄 이내) - if (i < 3) { - const metaMatch = cleanLine.match(this.patterns.metadata); - if (metaMatch && !currentSection) { - const key = metaMatch[1].toLowerCase().replace(/[^\w]/g, '_'); - result.metadata[key] = metaMatch[2]; - continue; - } - } - - // 섹션 제목 (=== 형태) - const sectionMatch = cleanLine.match(this.patterns.section); - if (sectionMatch) { - // 이전 섹션 저장 - if (currentSection) { - result.sections.set(currentSection, { - title: currentSection, - content: currentContent.join('\n') - }); - } - - currentSection = sectionMatch[1]; - currentContent = []; - continue; - } - - // 한글 제목 패턴 (bullet point나 HTML이 아닌 경우) - if (!cleanLine.startsWith('•') && - !cleanLine.startsWith('-') && - !cleanLine.startsWith('<') && - cleanLine.length > 5 && - cleanLine.length < 100 && - this.patterns.heading.test(cleanLine)) { - - // 이전 섹션 저장 - if (currentSection) { - result.sections.set(currentSection, { - title: currentSection, - content: currentContent.join('\n') - }); - } - - currentSection = cleanLine; - currentContent = []; - continue; - } - - // 현재 섹션에 내용 추가 - if (currentSection) { - currentContent.push(cleanLine); - } else if (!currentSection && i > 2) { - // 섹션이 없으면 "개요"로 시작 - currentSection = "개요"; - currentContent = [cleanLine]; - } - } - - // 마지막 섹션 저장 - if (currentSection) { - result.sections.set(currentSection, { - title: currentSection, - content: currentContent.join('\n') - }); - } - - // 기본값 설정 - if (!result.metadata.title) result.metadata.title = 'TXT 변환 프레젠테이션'; - if (!result.metadata.date) result.metadata.date = new Date().toISOString().split('T')[0]; - if (!result.metadata.company) result.metadata.company = 'Generated by TXT→PPTX'; - - console.log(`✅ 분석 완료: ${result.sections.size}개 섹션 발견`); - return result; - } -} - -// PPTX 생성 클래스 -class PPTXGenerator { - constructor() { - this.pptx = new PptxGenJS(); - this.pptx.layout = 'LAYOUT_16x9'; - this.slideNumber = 1; - } - - /** - * 분석된 데이터로부터 PPTX 생성 - */ - async generateFromAnalysis(analysisResult) { - console.log('📊 PPTX 생성 중...'); - - const { metadata, sections } = analysisResult; - - // 1. 표지 슬라이드 - this.createCoverSlide(metadata); - - // 2. 목차 슬라이드 - this.createTableOfContents(sections); - - // 3. 각 섹션별 슬라이드 - for (const [sectionName, sectionData] of sections) { - this.createSectionSlide(sectionName, sectionData); - } - - return this.pptx; - } - - /** - * 표지 슬라이드 생성 - */ - createCoverSlide(metadata) { - const slide = this.pptx.addSlide(); - - // 배경 색상 - slide.background = { color: '8BC34A' }; - - // 프로젝트 제목 - slide.addText(metadata.title || 'TXT 변환 프레젠테이션', { - x: 1, y: 3, w: 8, h: 1.5, - fontSize: 36, - bold: true, - color: 'FFFFFF', - align: 'center' - }); - - // 부제목 - slide.addText('시스템 기획 및 설계서', { - x: 1, y: 4.8, w: 8, h: 0.8, - fontSize: 20, - color: 'FFFFFF', - align: 'center' - }); - - // 날짜 및 회사정보 - const infoText = `${metadata.date}\n\n${metadata.company}`; - slide.addText(infoText, { - x: 7.5, y: 7, w: 2.5, h: 1.5, - fontSize: 12, - color: 'FFFFFF', - align: 'right' - }); - - console.log(`✅ 슬라이드 ${this.slideNumber}: 표지`); - this.slideNumber++; - } - - /** - * 목차 슬라이드 생성 - */ - createTableOfContents(sections) { - const slide = this.pptx.addSlide(); - - slide.addText('목차', { - x: 0.5, y: 0.5, w: 9, h: 0.8, - fontSize: 24, - bold: true, - color: '2E7D32' - }); - - let yPosition = 1.5; - let index = 1; - - for (const [sectionName] of sections) { - slide.addText(`${index}. ${sectionName}`, { - x: 1, y: yPosition, w: 8, h: 0.6, - fontSize: 16, - color: '333333' - }); - - yPosition += 0.8; - index++; - - if (yPosition > 6) break; // 슬라이드 범위 초과 방지 - } - - console.log(`✅ 슬라이드 ${this.slideNumber}: 목차`); - this.slideNumber++; - } - - /** - * 섹션 슬라이드 생성 - */ - createSectionSlide(sectionName, sectionData) { - const slide = this.pptx.addSlide(); - - // 섹션 제목 - slide.addText(sectionName, { - x: 0.5, y: 0.5, w: 9, h: 0.8, - fontSize: 24, - bold: true, - color: '2E7D32' - }); - - // 내용 처리 (긴 내용은 잘라서 표시) - let content = sectionData.content; - if (content.length > 800) { - content = content.substring(0, 800) + '...'; - } - - slide.addText(content, { - x: 0.5, y: 1.5, w: 9, h: 4.5, - fontSize: 14, - color: '333333', - lineSpacing: 20 - }); - - console.log(`✅ 슬라이드 ${this.slideNumber}: ${sectionName}`); - this.slideNumber++; - } -} - -// 메인 실행 함수 -async function convertTxtToPptx(inputPath, outputPath) { - try { - console.log('🚀 TXT → PPTX 변환 시작'); - console.log(`📄 입력: ${inputPath}`); - console.log(`📊 출력: ${outputPath}`); - - // 1. 텍스트 분석 - const analyzer = new TextAnalyzer(); - const analysisResult = await analyzer.analyzeFile(inputPath); - - // 2. PPTX 생성 - const generator = new PPTXGenerator(); - const pptx = await generator.generateFromAnalysis(analysisResult); - - // 3. 파일 저장 - await pptx.writeFile({ fileName: outputPath }); - console.log('✅ 변환 완료!'); - - } catch (error) { - console.error('❌ 변환 실패:', error.message); - throw error; - } -} - -// 명령행 인수 처리 -async function main() { - const args = process.argv.slice(2); - const inputFlag = args.find(arg => arg.startsWith('--input=')); - const outputFlag = args.find(arg => arg.startsWith('--output=')); - - const inputPath = inputFlag ? inputFlag.split('=')[1] : 'source/sample_project.txt'; - const outputPath = outputFlag ? outputFlag.split('=')[1] : 'pptx/working_test.pptx'; - - // 출력 디렉토리 생성 - await fs.mkdir('pptx', { recursive: true }); - - await convertTxtToPptx(inputPath, outputPath); -} - -// 직접 실행시 -if (require.main === module) { - main().catch(console.error); -} - -module.exports = { convertTxtToPptx, TextAnalyzer, PPTXGenerator }; \ No newline at end of file diff --git a/.claude/skills/uml-generator/SKILL.md b/.claude/skills/uml-generator/SKILL.md deleted file mode 100755 index 6514d61..0000000 --- a/.claude/skills/uml-generator/SKILL.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -name: uml-generator -description: 프로젝트 아키텍처를 분석하여 UML 다이어그램을 자체 완결형 HTML로 생성합니다. UML, 클래스 다이어그램, 아키텍처 시각화 요청 시 활성화됩니다. -allowed-tools: Read, Write, Edit, Glob, Grep, Bash ---- - -# UML Generator - -프로젝트 구조를 분석하여 UML 다이어그램을 생성하는 스킬입니다. - -## 기능 - -### 지원하는 다이어그램 유형 -- **클래스 다이어그램**: 클래스, 인터페이스, 상속/구현 관계 -- **패키지 다이어그램**: 모듈/패키지 간 의존성 -- **시퀀스 다이어그램**: 객체 간 메시지 흐름 -- **컴포넌트 다이어그램**: 시스템 컴포넌트 구조 - -### 분석 대상 -- 클래스 및 인터페이스 정의 -- 상속 및 구현 관계 -- 메서드 시그니처 및 속성 -- 모듈 간 import/export 관계 -- 의존성 주입 패턴 - -### 출력 형식 -- Mermaid.js 기반 인터랙티브 HTML -- 자체 완결형 단일 파일 - -## HTML 템플릿 - -```html - - - - - UML 다이어그램 - - - - - -
                                      -
                                      -

                                      UML 다이어그램

                                      -
                                      -
                                      - -
                                      - -
                                      -

                                      클래스 다이어그램

                                      -
                                      - classDiagram - class ClassName { - +attribute: type - +method(): returnType - } -
                                      -
                                      - - -
                                      -

                                      시퀀스 다이어그램

                                      -
                                      - sequenceDiagram - participant A - participant B - A->>B: message -
                                      -
                                      -
                                      - - - - -``` - -## 사용 예시 - -``` -이 프로젝트의 클래스 다이어그램을 만들어줘 -src 폴더의 UML 다이어그램을 HTML로 생성해줘 -이 코드의 시퀀스 다이어그램을 그려줘 -``` - -## 출처 -Original skill from skills.cokac.com diff --git a/.claude/skills/webapp-testing/SKILL.md b/.claude/skills/webapp-testing/SKILL.md deleted file mode 100644 index 9f755c4..0000000 --- a/.claude/skills/webapp-testing/SKILL.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -name: webapp-testing -description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. -license: Complete terms in LICENSE.txt ---- - -# Web Application Testing - -To test local web applications, write native Python Playwright scripts. - -**Helper Scripts Available**: -- `scripts/with_server.py` - Manages server lifecycle (supports multiple servers) - -**Always run scripts with `--help` first** to see usage. DO NOT read the source until you try running the script first and find that a customized solution is abslutely necessary. These scripts can be very large and thus pollute your context window. They exist to be called directly as black-box scripts rather than ingested into your context window. - -## Decision Tree: Choosing Your Approach - -``` -User task → Is it static HTML? - ├─ Yes → Read HTML file directly to identify selectors - │ ├─ Success → Write Playwright script using selectors - │ └─ Fails/Incomplete → Treat as dynamic (below) - │ - └─ No (dynamic webapp) → Is the server already running? - ├─ No → Run: python scripts/with_server.py --help - │ Then use the helper + write simplified Playwright script - │ - └─ Yes → Reconnaissance-then-action: - 1. Navigate and wait for networkidle - 2. Take screenshot or inspect DOM - 3. Identify selectors from rendered state - 4. Execute actions with discovered selectors -``` - -## Example: Using with_server.py - -To start a server, run `--help` first, then use the helper: - -**Single server:** -```bash -python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py -``` - -**Multiple servers (e.g., backend + frontend):** -```bash -python scripts/with_server.py \ - --server "cd backend && python server.py" --port 3000 \ - --server "cd frontend && npm run dev" --port 5173 \ - -- python your_automation.py -``` - -To create an automation script, include only Playwright logic (servers are managed automatically): -```python -from playwright.sync_api import sync_playwright - -with sync_playwright() as p: - browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode - page = browser.new_page() - page.goto('http://localhost:5173') # Server already running and ready - page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute - # ... your automation logic - browser.close() -``` - -## Reconnaissance-Then-Action Pattern - -1. **Inspect rendered DOM**: - ```python - page.screenshot(path='/tmp/inspect.png', full_page=True) - content = page.content() - page.locator('button').all() - ``` - -2. **Identify selectors** from inspection results - -3. **Execute actions** using discovered selectors - -## Common Pitfall - -❌ **Don't** inspect the DOM before waiting for `networkidle` on dynamic apps -✅ **Do** wait for `page.wait_for_load_state('networkidle')` before inspection - -## Best Practices - -- **Use bundled scripts as black boxes** - To accomplish a task, consider whether one of the scripts available in `scripts/` can help. These scripts handle common, complex workflows reliably without cluttering the context window. Use `--help` to see usage, then invoke directly. -- Use `sync_playwright()` for synchronous scripts -- Always close the browser when done -- Use descriptive selectors: `text=`, `role=`, CSS selectors, or IDs -- Add appropriate waits: `page.wait_for_selector()` or `page.wait_for_timeout()` - -## Reference Files - -- **examples/** - Examples showing common patterns: - - `element_discovery.py` - Discovering buttons, links, and inputs on a page - - `static_html_automation.py` - Using file:// URLs for local HTML - - `console_logging.py` - Capturing console logs during automation diff --git a/.claude/skills/웹문서/SKILL.md b/.claude/skills/웹문서/SKILL.md deleted file mode 100755 index 80f8ddb..0000000 --- a/.claude/skills/웹문서/SKILL.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -name: 웹문서 -description: SAM 프로젝트의 웹문서(PHP/HTML) 생성 시 적용되는 디자인 표준. 웹페이지, index.php, 보고서 페이지 생성 시 자동 활성화 -allowed-tools: Read, Write, Edit, Glob ---- - -# 웹문서 스타일 가이드 - -이 스킬은 SAM 프로젝트의 웹문서(PHP/HTML) 생성 시 적용되는 디자인 표준입니다. - -## 필수 적용 사항 - -### 1. 상단 홈 버튼 (필수) -모든 웹문서의 헤더에는 상위 index.php로 이동하는 홈 버튼을 포함해야 합니다: - -```html - - 🏠 - -``` - -### 2. 디자인 철학 (Core Philosophy) - -- **주제**: Professional Trust (신뢰 기반의 전문성) -- **분위기**: 깔끔함, 신뢰감, 데이터 중심적인 명확성 -- **핵심 키워드**: Teal, Navy, White, Light Gray, Modern Typography - -### 3. 색상 사양 (Color Palette) - -#### 기본 색상 (Brand Colors) -- **Primary (Teal)**: `#0d9488` (Tailwind: `teal-600`) - 핵심 강조 색상, 활성화 상태 -- **Secondary (Navy/Slate)**: `#1e293b` (Tailwind: `slate-800`) - 제목, 텍스트, 신뢰감을 주는 배경 -- **Accent (Amber)**: `#f59e0b` (Tailwind: `amber-500`) - 차트의 추세선 등 보조 강조 - -#### 배경 및 테두리 -- **Body Background**: `#f3f4f6` (Tailwind: `gray-100`) -- **Container Background**: `#ffffff` (White) -- **Border**: `#e2e8f0` (Tailwind: `slate-200`) - -### 4. 타이포그래피 (Typography) - -- **Font Family**: `'Noto Sans KR', sans-serif` (Google Fonts) -- **Heading Styles**: - - Main Title: `text-2xl font-bold text-slate-800` - - Sub Title: `text-lg font-bold text-slate-700` -- **Body Text**: `text-slate-600 leading-relaxed` - -### 5. UI 컴포넌트 표준 - -#### 상단 네비게이션 (Sticky Header) -```html -
                                      -
                                      -
                                      -
                                      - - 🏠 - -

                                      페이지 제목

                                      -
                                      - 날짜 -
                                      -
                                      -
                                      -``` - -#### 활성화된 탭 -```css -.nav-active { border-bottom: 3px solid #0d9488; color: #0d9488; font-weight: 700; } -``` - -#### 정보 카드 -- 사각 모서리 둥글게: `rounded-xl` -- 가벼운 그림자: `shadow-sm` -- 호버 효과: `hover:shadow-md transition-shadow` -- 왼쪽 테두리 강조: `border-l-4 border-teal-600` - -#### 차트 (Chart.js) -- 스타일: `max-height: 400px` -- 색상: Teal(`rgba(13, 148, 136, 0.2)`) 또는 Blue(`rgba(59, 130, 246, 0.2)`) - -### 6. 코딩 규칙 - -- **CSS Framework**: Tailwind CSS (CDN 방식) -```html - -``` - -- **폰트**: -```html - -``` - -- **SVG 지양**: 유니코드 이모지(🏢, 📈, 💰) 사용 - -- **애니메이션**: fade-in 효과 -```css -.fade-in { - animation: fadeIn 0.5s ease-out forwards; -} -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} -``` - -- **반응형**: 모바일 우선 + `md:` 접두사 - -- **커스텀 스크롤바**: -```css -.custom-scroll::-webkit-scrollbar { width: 6px; height: 6px; } -.custom-scroll::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 10px; } -.custom-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; } -``` - -### 7. 기본 HTML 템플릿 - -```html - - - - - - 페이지 제목 - - - - - -
                                      -
                                      -
                                      -
                                      - - 🏠 - -

                                      페이지 제목

                                      -
                                      - 날짜 -
                                      -
                                      -
                                      - -
                                      - -
                                      - -
                                      -
                                      -

                                      © SAM

                                      -
                                      -
                                      - - -``` diff --git a/.gitignore b/.gitignore index b1caba9..300f6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,15 +7,6 @@ !README.md !resources.md -# .claude 폴더 - 스킬/에이전트는 추적 -!.claude/ -.claude/* -!.claude/skills/ -!.claude/skills/** -.claude/skills/**/node_modules/ -!.claude/agents/ -!.claude/agents/** - # 문서 폴더 (루트 기준) !assets/ !assets/** From 13a5a56146883632f181be29e2f98f5156e757e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 10 Mar 2026 09:28:07 +0900 Subject: [PATCH 14/15] =?UTF-8?q?docs:=20=EA=B0=9C=EB=B0=9C=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20sam-docs=20=ED=8F=B4=EB=8D=94=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EB=AC=B8=EC=84=9C=205=EA=B1=B4=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - guides/project-launch-roadmap.md - plans/SAM_ERP_Storyboard_D1.4.md - plans/SAM_ERP_회계관리_Storyboard_D1.6.md - plans/integrated-master-plan.md - plans/production-deployment-plan.md --- guides/project-launch-roadmap.md | 639 ++++++++++ plans/SAM_ERP_Storyboard_D1.4.md | 1150 ++++++++++++++++++ plans/SAM_ERP_회계관리_Storyboard_D1.6.md | 1288 +++++++++++++++++++++ plans/integrated-master-plan.md | 382 ++++++ plans/production-deployment-plan.md | 1099 ++++++++++++++++++ 5 files changed, 4558 insertions(+) create mode 100644 guides/project-launch-roadmap.md create mode 100644 plans/SAM_ERP_Storyboard_D1.4.md create mode 100644 plans/SAM_ERP_회계관리_Storyboard_D1.6.md create mode 100644 plans/integrated-master-plan.md create mode 100644 plans/production-deployment-plan.md diff --git a/guides/project-launch-roadmap.md b/guides/project-launch-roadmap.md new file mode 100644 index 0000000..c3459fe --- /dev/null +++ b/guides/project-launch-roadmap.md @@ -0,0 +1,639 @@ +# SAM 프로젝트 런칭 로드맵 + +**작성일**: 2025-11-24 +**최종 수정**: 2025-12-02 +**목적**: 프로젝트 전체 방향성 관리 및 런칭 준비 현황 추적 +**대상**: 프로젝트 관리 및 의사결정용 + +--- + +## 1. 프로젝트 현황 개요 + +### 전체 시스템 구성 +``` +SAM (Smart Application Management) +├── api/ - Laravel 12 REST API (독립 모델) +├── mng/ - Plain Laravel 관리자 패널 (독립 모델, 운영 주력) +├── react/ - Next.js 15 사용자 프론트엔드 +├── docs/ - 기술 문서 +├── design/ - 디자인 시스템 (Storybook) +├── planning/ - 기획 문서 +└── docker/ - Docker 개발 환경 +``` + +### 프로젝트 구분 + +| 구분 | 대상 | 설명 | 담당 | +|------|------|------|------| +| **MES (경동기업)** | 경동기업 | 메인 프로젝트, 디자인 시스템 기준 | 디자이너 (기획+디자인) | +| **MES (주일기업)** | 주일기업 | 경동기업 디자인 기반 커스터마이징 | 기획자 | +| **ERP** | SAM 공통 | 공통 모듈 (인사, 회계, 결재 등) | 기획자 | + +### MVP 범위 정의 + +| 구분 | 범위 | 설명 | +|------|------|------| +| **코어 MVP** | MES 핵심 기능 | 견적 → 수주 → 생산 → 출하 흐름 | +| **1차 MVP** | 코어 MVP + 추가 기능 | 품질, 자재, 단가, 회계 등 확장 | + +### 각 시스템 역할 +- **api**: 모든 비즈니스 로직과 데이터 처리의 중심 +- **mng**: Pure Blade + Tailwind 관리자 패널 (운영 환경 주력) +- **react**: 최종 사용자용 인터페이스 +- **design**: 디자인 시스템 및 컴포넌트 문서 + +### 현재 개발 완료율 +- **백엔드 (API)**: 약 70% 완료 + - ✅ 인증/권한, 멀티테넌트, 기준정보 + - ✅ 제품/BOM, 견적/수주, 자재입고/검사 + - 🔄 공정/생산, 단가/원가, 재고관리 + +- **프론트엔드**: 약 50% 완료 + - ✅ Admin 패널 27개 Resources + - 🔄 React 사용자 포털 개발 중 + +--- + +## 2. 팀 구성 및 역할 + +### 팀 역할 분담 + +| 역할 | 담당자 | 주요 업무 | 비고 | +|------|--------|----------|------| +| **디자이너** | 재웅 정 | MES(경동기업) 기획 + 디자인 | 디자인 시스템 기준 | +| **기획자** | 이태화 | ERP 스토리보드, MES(주일기업) 기획, 운영, QA | 기획 완료 시 MES 합류 | +| **Frontend** | - | React 개발 | MES(경동) 우선 | +| **Backend** | hso be | API 서포트, mng 개발, 인프라, 정책/운영 | 전체 기술 지원 | + +### 작업 우선순위 + +**Frontend 우선순위:** +1. **MES (경동기업)** - 디자이너 결과물 즉시 개발 +2. **ERP + MES (주일기업)** - MES 짬/대기 시 병행 + +**Backend 역할:** +- Frontend API 서포트 +- mng (운영 관리자 패널) 개발 +- 인프라 셋팅 +- 정책/운영 관련 일정 체크 + +--- + +## 3. 주요 마일스톤 개요 + +### 📅 마일스톤 타임라인 + +``` +2025년 12월 2026년 1월 2026년 2월 2026년 3월 + | | | | + MS1 MS2 MS3 MS4 +코어 MVP 완료 1차 MVP + 베타 정식 런칭 안정화 완료 +(단위테스트) (통합테스트) +``` + +### 마일스톤 요약 + +| 마일스톤 | 목표 | 기한 | 주요 내용 | +|---------|------|------|----------| +| **MS1** | 코어 MVP 개발 완료 | 2025-12-31 | MES 핵심 기능 + 단위테스트 | +| **MS2** | 1차 MVP + 베타 오픈 | 2026-01-31 | 통합테스트 + 베타 서비스 오픈 | +| **MS3** | 정식 런칭 | 2026-02-28 | 운영 서버 오픈 | +| **MS4** | 안정화 완료 | 2026-03-31 | 고객 성공 사례 확보 | + +--- + +## 4. 기획 및 디자인 일정 + +### 4.1 MES (경동기업) - 디자이너 일정 +**기간**: 2025-11-26 ~ 2025-12-26 (약 21일) + +#### Phase 1: 견적 (11/27 ~ 11/28) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| S2 | 견적 리스트 | LIST | 0.25 | 11-27(목) | 11-27(목) | +| S2-1 | 견적 등록 | FORM | 0.5 | 11-27(목) | 11-27(목) | +| S2-2 | 견적 수정 | FORM | 0.25 | 11-27(목) | 11-28(금) | +| S2-3 | 견적 상세+탭 | DETAIL | 0.5 | 11-28(금) | 11-28(금) | +| S2-4 | 견적서 출력 | PRINT | 0.25 | 11-28(금) | 11-28(금) | + +#### Phase 2: 기준-수식 (11/28 ~ 12/01) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| M12 | 견적수식 리스트 | LIST | 0.1 | 11-28(금) | 11-28(금) | +| M12-1 | 견적수식 등록 | FORM | 0.2 | 12-01(월) | 12-01(월) | + +#### Phase 3: 수주 (12/01 ~ 12/04) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| S3-1 | 수주 리스트 | LIST | 0.25 | 12-01(월) | 12-01(월) | +| S3-2 | 수주 등록 | FORM | 0.5 | 12-01(월) | 12-02(화) | +| S4-3 | 수주 수정 | FORM | 1 | 12-02(화) | 12-03(수) | +| S4-4 | 수주 상세+탭 | DETAIL | 1 | 12-03(수) | 12-04(목) | +| S4-5 | 수주서 발송 | PRINT | 0.5 | 12-04(목) | 12-04(목) | + +#### Phase 4: 생산 (12/04 ~ 12/08) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| P1-3-3 | 작업지시 리스트 | LIST | 0.25 | 12-04(목) | 12-05(금) | +| P1-3-4 | 작업지시 등록 | FORM | 0.5 | 12-05(금) | 12-05(금) | +| P1-3-5 | 작업지시 수정 | FORM | 0.2 | 12-05(금) | 12-05(금) | +| P1-3-6 | 작업지시 상세 | DETAIL | 0.25 | 12-05(금) | 12-08(월) | +| P1-3-7 | 작업실적 입력 | FORM | 0.5 | 12-08(월) | 12-08(월) | +| P1-3-8 | 작업실적 조회 | DETAIL | 0.25 | 12-08(월) | 12-08(월) | + +#### Phase 5: 기준-공정 (12/08 ~ 12/10) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| M5 | 공정 리스트 | LIST | 0.25 | 12-08(월) | 12-08(월) | +| M5-1 | 공정 등록 | FORM | 0.5 | 12-09(화) | 12-09(화) | +| M5-2 | 공정 수정 | FORM | 0.2 | 12-09(화) | 12-09(화) | +| M5-3 | 공정 상세 | DETAIL | 0.5 | 12-10(수) | 12-10(수) | + +#### Phase 6: 출하 (12/10 ~ 12/12) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| S4 | 출하 리스트 | LIST | 0.25 | 12-10(수) | 12-10(수) | +| S4-1 | 출하 등록 | FORM | 0.5 | 12-10(수) | 12-11(목) | +| S4-2 | 배송 조율/관리 | FORM | 0.5 | 12-11(목) | 12-11(목) | +| S4-3 | 상차 체크리스트 | FORM | 0.5 | 12-11(목) | 12-12(금) | +| S4-4 | 출하 수정+탭 | FORM | 0.5 | 12-12(금) | 12-12(금) | + +#### Phase 7: 거래처 (12/15 ~ 12/16) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| S1-1 | 거래처 리스트 | LIST | 0.25 | 12-15(월) | 12-15(월) | +| S1-1 | 거래처 등록 | FORM | 0.5 | 12-15(월) | 12-15(월) | +| S1-2 | 거래처 수정 | FORM | 0.1 | 12-15(월) | 12-16(화) | +| S1-3 | 거래처 상세+탭 | DETAIL | 0.25 | 12-16(화) | 12-16(화) | + +#### Phase 8: 품질 (12/16 ~ 12/19) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| M9 | 검사기준 리스트 | LIST | 0.5 | 12-16(화) | 12-16(화) | +| M9-1 | 검사기준 등록 | FORM | 1 | 12-17(수) | 12-17(수) | +| Q1 | 검사관리 리스트 | LIST | 0.5 | 12-18(목) | 12-18(목) | +| Q1-1 | 검사관리 등록 | FORM | 1 | 12-18(목) | 12-19(금) | +| Q1-2 | 검사관리 상세 | DETAIL | 0.5 | 12-19(금) | 12-19(금) | + +#### Phase 9: 자재 (12/19 ~ 12/24) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| I1 | 재고현황 리스트 | LIST | 0.5 | 12-19(금) | 12-22(월) | +| I1-1 | 재고 상세+탭 | DETAIL | 1 | 12-22(월) | 12-23(화) | +| I2 | 입고 리스트 | LIST | 0.5 | 12-23(화) | 12-23(화) | +| I2-1 | 입고 등록 | FORM | 0.8 | 12-23(화) | 12-24(수) | +| I2-3 | 입고 상세+탭 | DETAIL | 0.5 | 12-24(수) | 12-24(수) | + +#### Phase 10: 단가 (12/24) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| S6 | 단가 리스트 | LIST | 0.25 | 12-24(수) | 12-24(수) | +| S6-1 | 단가 등록 | FORM | 0.25 | 12-24(수) | 12-24(수) | +| S6-2 | 단가 수정 | FORM | 0.25 | 12-24(수) | 12-24(수) | +| S6-3 | 단가 상세+탭 | DETAIL | 0.25 | 12-24(수) | 12-24(수) | + +#### Phase 11: 회계 (12/26) +| 화면ID | 화면명 | 유형 | 공수 | 시작일 | 종료일 | +|--------|--------|------|------|--------|--------| +| A1 | 판매조회 리스트 | LIST | 0.5 | 12-26(금) | 12-26(금) | +| A4 | 수금 리스트 3탭 | LIST | 0.25 | 12-26(금) | 12-26(금) | +| A4-1 | 수금 등록 | FORM | 0.25 | 12-26(금) | 12-26(금) | + +### 4.2 기획자 일정 (ERP + 운영) + +#### 기획 (이태화) +| 구분 | 업무 | 기간 | 일수 | 대상 | +|------|------|------|------|------| +| 스토리보드 | 공통, ERP | 11/26 ~ 12/12 | 13 | SAM | +| 가입 및 로그인 | 스토리보드 | 11/27 | 1 | SAM | +| 인사관리, 전자결재 | 스토리보드 | 11/28 ~ 12/01 | 4 | SAM | +| 회계, 보고서 | 스토리보드 | 12/02 ~ 12/09 | 6 | SAM | +| 고객센터, 게시판 | 스토리보드 | 12/10 ~ 12/12 | 3 | SAM | +| 주일기업 요구사항 정리 | 요구사항 | 12/02 ~ 12/12 | 9 | 주일기업 | +| 스토리보드 - 주일기업 MES | 기획 | 12/15 ~ 12/30 | 12 | 주일기업 | +| 스토리보드 - ERP 2차 | 기획 | 12/31 ~ 01/13 | 12 | SAM | +| 스토리보드 - MES 2차 | 기획 | 01/14 ~ 01/27 | 12 | SAM | + +#### 운영 (hso be) +| 구분 | 업무 | 기간 | 일수 | 대상 | +|------|------|------|------|------| +| 보고서 지표 검토 | 운영 | 11/27 ~ 11/28 | 2 | SAM | +| 주일기업 자료 정리 및 취합 | 운영 | 11/26 ~ 11/28 | 3 | 주일기업 | +| 주일기업 업무 프로세스 인터뷰 | 운영 | 12/01 ~ 12/05 | 5 | 주일기업 | +| 법률 및 정책 검토 | 운영 | 12/08 ~ 12/19 | 10 | SAM | + +--- + +## 5. MS1: 코어 MVP 개발 완료 (2025-12-31) + +**목표**: MES 핵심 기능 개발 완료 + 단위테스트 통과 + +### 코어 MVP 범위 +- **핵심 흐름**: 견적 → 수주 → 생산(작업지시/실적) → 출하 +- **기준정보**: 거래처, 공정, 견적수식 +- **단위테스트**: 커버리지 60% 이상 + +### 완료 기준 +- ✅ 코어 MVP 기능 100% 구현 +- ✅ 단위테스트 커버리지 60% 이상 +- ✅ Swagger 문서화 (코어 MVP 범위) +- ✅ Critical/High 버그 0건 +- ✅ API 평균 응답 속도 < 500ms + +### 주요 산출물 +- [ ] 코어 MVP 소스코드 (api, react) +- [ ] API 문서 (Swagger) +- [ ] 단위테스트 보고서 + +### Week별 작업 + +**Week 1 (12/02-12/08)** +| 팀 | 작업 내용 | +|----|----------| +| 📋 기획 | 회계/보고서 스토리보드 | +| 📋 운영 | 주일기업 업무 프로세스 인터뷰 | +| 🎨 디자인 | 생산, 기준-공정 화면 | +| 🔧 Backend | 공정/단가 체계 완성 | +| 💻 Frontend | React 개발 시작 (12/08~) | + +**Week 2 (12/09-12/15)** +| 팀 | 작업 내용 | +|----|----------| +| 📋 기획 | 고객센터/게시판 스토리보드, 주일기업 MES 스토리보드 시작 | +| 📋 운영 | 법률 및 정책 검토 시작 | +| 🎨 디자인 | 출하, 거래처 화면 | +| 🔧 Backend | 견적서 PDF, 재고 트랜잭션 | +| 💻 Frontend | 견적/수주 화면 개발 | +| 🧪 QA | 단위테스트 시작 (12/10~) | + +**Week 3 (12/16-12/22)** +| 팀 | 작업 내용 | +|----|----------| +| 📋 기획 | 주일기업 MES 스토리보드 진행 | +| 🎨 디자인 | 품질, 자재 화면 | +| 🔧 Backend | API 안정화, 버그 수정 | +| 💻 Frontend | 생산/출하 화면 개발 | +| 🧪 QA | 단위테스트 진행 | + +**Week 4 (12/23-12/31)** +| 팀 | 작업 내용 | +|----|----------| +| 🎨 디자인 | 단가, 회계 화면 (완료) | +| 🔧 Backend | 코어 MVP 마무리 | +| 💻 Frontend | 코어 MVP 화면 완료 | +| 🧪 QA | 단위테스트 완료 | + +### 체크포인트 +- 12/15: 개발 70% 완료, 단위테스트 시작 +- 12/22: 개발 90% 완료 +- 12/29: 코어 MVP 개발 완료 +- 12/31: **MS1 완료** - 단위테스트 통과 + +--- + +## 6. MS2: 1차 MVP + 베타 오픈 (2026-01-31) + +**목표**: 통합테스트 완료 + 1차 MVP 완료 + 베타 서비스 오픈 + +### 1차 MVP 범위 (추가 예정) +- **확장 기능**: 품질, 자재, 단가, 회계 +- **추가 기능**: (1차 MVP 일정에서 별도 정의) + +### 완료 기준 +- ✅ 통합테스트 통과 +- ✅ 베타 서버 구축 완료 +- ✅ 파일럿 고객 온보딩 완료 +- ✅ 주요 시나리오 실전 테스트 완료 + +### 주요 산출물 +- [ ] 1차 MVP 소스코드 +- [ ] 통합테스트 보고서 +- [ ] 베타 서버 환경 + +### Week별 작업 + +**Week 1 (01/01-01/05)** +| 팀 | 작업 내용 | +|----|----------| +| 🧪 QA | 통합테스트 시작 | +| 🔧 Backend | 베타 서버 구축, 도메인/SSL 설정 | +| 📋 기획 | ERP 2차 스토리보드 진행 | + +**Week 2 (01/06-01/12)** +| 팀 | 작업 내용 | +|----|----------| +| 🧪 QA | 통합테스트 진행 | +| 🔧 Backend | 파일럿 고객 데이터 준비 | +| 💻 Frontend | 버그 수정, UI 개선 | +| 📋 기획 | ERP 2차 스토리보드 완료 (01/13) | + +**Week 3 (01/13-01/19)** +| 팀 | 작업 내용 | +|----|----------| +| 🧪 QA | 통합테스트 완료 | +| 운영 | 파일럿 고객 온보딩 (1차) | +| 📋 기획 | MES 2차 스토리보드 시작 (01/14) | + +**Week 4 (01/20-01/31)** +| 팀 | 작업 내용 | +|----|----------| +| 전체 | 실전 테스트, 피드백 수집 | +| 🔧 Backend | 긴급 버그 수정 | +| 📋 기획 | MES 2차 스토리보드 진행 | + +### 베타 고객 프로필 +| 고객사 | 업종 | 주요 사용 기능 | 기대 효과 | +|--------|------|----------------|----------| +| 경동기업 | 제조 | 견적/수주/BOM/생산 | MES 전체 검증 | +| 주일기업 | 제조 | MES 커스터마이징 | 확장성 검증 | + +### 체크포인트 +- 01/05: 베타 서버 오픈 +- 01/13: 통합테스트 완료 +- 01/20: 파일럿 고객 온보딩 완료 +- 01/31: **MS2 완료** - 베타 서비스 오픈 + +--- + +## 7. MS3: 정식 런칭 (2026-02-28) + +**목표**: 운영 서버 오픈 및 본격적인 서비스 시작 + +### 완료 기준 +- ✅ 운영 서버 구축 완료 (이중화) +- ✅ 베타 피드백 반영 완료 +- ✅ 보안 감사 통과 +- ✅ 법적 문서 완비 + +### 주요 산출물 +- [ ] 운영 서버 환경 +- [ ] 보안 감사 보고서 +- [ ] 마케팅 자료 + +### Week별 작업 + +**Week 1-2 (02/01-02/14): 운영 준비** +| 작업 | 내용 | +|------|------| +| 베타 피드백 반영 | UI/UX 개선, 성능 최적화 | +| 운영 서버 구축 | 이중화, 모니터링, 백업 | +| 보안 감사 | 취약점 점검 및 수정 | +| 📋 기획 | MES 2차 스토리보드 완료 (01/27) | + +**Week 3-4 (02/15-02/28): 런칭** +| 작업 | 내용 | +|------|------| +| 정식 오픈 | 운영 서버 오픈 | +| 고객 온보딩 | 초기 고객 온보딩 시작 | + +### 체크포인트 +- 02/14: 운영 준비 완료 +- 02/28: **MS3 완료** - 정식 런칭 + +--- + +## 8. MS4: 안정화 완료 (2026-03-31) + +**목표**: 서비스 안정화 및 초기 고객 성공 사례 확보 + +### 완료 기준 +- ✅ 시스템 가용성 99.5% 이상 +- ✅ 고객 만족도 4.0/5.0 이상 +- ✅ 성공 사례 3건 이상 확보 + +### 주요 작업 +- 런칭 후 긴급 이슈 대응 +- 모니터링 강화 +- 고객 피드백 수집 및 반영 +- 성능 최적화 +- 고객 성공 사례 수집 +- Q2 로드맵 수립 + +### 체크포인트 +- 03/15: 초기 안정화 완료 +- 03/31: **MS4 완료** - 안정화 완료 + +--- + +## 9. 개발 방향성 + +### 기술 아키텍처 방향 +- **Backend**: Laravel 12 + PHP 8.4+ +- **Frontend**: Next.js 15 + React 18 +- **Database**: MySQL 8.0 (멀티테넌트 구조) +- **Auth**: Laravel Sanctum +- **API**: RESTful + Swagger 문서화 +- **Deployment**: Docker + Docker Compose + +### 핵심 개발 원칙 +1. **Service-First**: 모든 비즈니스 로직은 Service 클래스에 +2. **Multi-tenancy**: BelongsToTenant 스코프 필수 적용 +3. **FormRequest**: Controller에서 직접 검증 금지 +4. **API-First**: Backend 완성 후 Frontend 연동 +5. **문서화**: Swagger 100% 완성 목표 + +### 디자인 시스템 전략 +- **MES (경동기업)** 기준으로 디자인 시스템 구성 +- **ERP**는 경동기업 디자인 시스템 기반으로 Frontend가 직접 개발 +- **MES (주일기업)**은 경동기업 디자인 기반 커스터마이징 + +### 품질 기준 +- API Rules 100% 준수 +- Swagger 문서화 완성도 100% +- 테스트 커버리지 60% 이상 +- Pint 코드 포맷팅 통과 +- i18n 메시지 키 사용 + +--- + +## 10. 개발 작업 현황 + +### ✅ 백엔드 완료 항목 + +#### API 공통 기반 +- [x] Exception Handler +- [x] Swagger 설정 (l5-swagger v1) +- [x] API Key 인증 +- [x] Rate Limit, CORS +- [x] 권한 체크 미들웨어 + +#### 인증/보안 +- [x] API Key 모델 및 인증 +- [x] Role-Permission 시스템 +- [x] 멀티테넌트 권한 구조 +- [x] 권한 오버라이드 시스템 + +#### 테넌트 관리 +- [x] BelongsToTenant 글로벌 스코프 +- [x] TenantBootstrap 서비스 +- [x] 테넌트 컨텍스트 주입 +- [x] 테넌트 옵션/설정 관리 + +#### 기준정보/코드 관리 +- [x] Category (3단계 트리) +- [x] CategoryField (동적 필드) +- [x] CategoryTemplate +- [x] Classification (공통 코드) +- [x] CommonCode 관리 + +#### 제품/부품/자재 도메인 +- [x] Product 모델 (67개 모델) +- [x] Part 관리 +- [x] Material 관리 +- [x] ProductComponent (BOM 연결) +- [x] PriceHistory (단가 이력) + +#### BOM (Bill of Materials) +- [x] BomTemplate 관리 +- [x] BomTemplateItem CRUD +- [x] BomCalculationService (가격 계산) +- [x] ModelVersion (버전 관리) +- [x] 재귀 BOM 구조 + +#### 영업 흐름 +- [x] Estimate (견적) - 기본 CRUD +- [x] EstimateItem (견적 라인) +- [x] Order (수주) - 5개 모델 +- [x] OrderItem, OrderHistory +- [x] OrderItemComponent + +#### 자재입고/수입검사 +- [x] MaterialReceipt (자재입고) +- [x] MaterialInspection (수입검사) +- [x] MaterialInspectionItem (검사 항목) + +#### 파일/로그 시스템 +- [x] FileService, FileStorageService +- [x] AuditLogger, AuditLogService +- [x] File 모델 (Polymorphic) + +### 🔄 백엔드 진행 중 (코어 MVP) + +#### 공정/생산 계획 +- [ ] Process Routing (공정 라우팅) +- [ ] Work Order (작업지시) +- [ ] Production Record (생산실적) + +#### 견적서 출력 +- [ ] 견적서 HTML 템플릿 +- [ ] PDF 생성 (DomPDF/Snappy) +- [ ] 견적서 미리보기 API + +### ⏳ 백엔드 예정 (1차 MVP) + +#### 품질/자재/단가/회계 +- [ ] 검사기준, 검사관리 +- [ ] 재고현황, 입고 관리 +- [ ] 단가 정책 로직 +- [ ] 회계 조회/수금 + +### ✅ 프론트엔드 완료 항목 + +#### MNG 패널 (Pure Blade + Tailwind) +- [x] 주요 관리 화면 구현 +- [x] Product, BOM, Material +- [x] Category, Role, Permission +- [x] Department, User, Tenant +- [x] Client, File 관리 + +### 🔄 프론트엔드 진행 중 + +#### React 사용자 포털 +- [ ] 공통 레이아웃 최종 정리 +- [ ] 견적/수주 화면 +- [ ] 생산/출하 화면 +- [ ] 기준정보 관리 UI + +--- + +## 11. 리스크 관리 + +### High Risk +| 리스크 | 영향도 | 완화 방안 | 담당 | +|--------|--------|-----------|------| +| 개발 일정 지연 | High | 주간 진행률 체크, 우선순위 조정 | PM | +| 디자인-개발 병목 | High | Frontend 버퍼 확보, ERP 병행 | Frontend | +| 단가 계산 로직 복잡도 | High | 전문가 리뷰, Week 1 집중 | Backend | + +### Medium Risk +| 리스크 | 영향도 | 완화 방안 | 담당 | +|--------|--------|-----------|------| +| 기획-개발 동기화 | Medium | 주간 싱크업, 스토리보드 우선 리뷰 | PM | +| 통합 테스트 시간 부족 | Medium | 자동화 테스트 확대 | QA | + +--- + +## 12. 핵심 성공 지표 (KPI) + +### 기술 지표 +- [ ] 코어 MVP API 엔드포인트 구현 +- [ ] Swagger 문서 100% 완성 (MVP 범위) +- [ ] 테스트 커버리지 60% 이상 +- [ ] API 평균 응답 속도 < 500ms +- [ ] Critical/High 버그 0건 + +### 품질 지표 +- [ ] Service-First 아키텍처 100% 준수 +- [ ] FormRequest 검증 100% 적용 +- [ ] BelongsToTenant 스코프 100% 적용 +- [ ] Pint 코드 포맷팅 100% 통과 + +### 비즈니스 지표 +- [ ] 베타 고객 2개사 확보 +- [ ] 정식 고객 확보 (런칭 후) +- [ ] 고객 만족도 4.0/5.0 이상 + +--- + +## 13. 담당자 및 연락처 + +| 역할 | 담당자 | 주요 업무 | 비고 | +|------|--------|----------|------| +| 프로젝트 관리 | - | 전체 일정 및 방향성 관리 | PM | +| 디자이너 | 재웅 정 | MES(경동기업) 기획 + 디자인 | 디자인 시스템 기준 | +| 기획자 | 이태화 | ERP/MES 스토리보드, 운영, QA | 기획 완료 시 MES 합류 | +| 백엔드 개발 | hso be | API/mng/인프라/정책 | 기술 총괄 | +| 프론트엔드 개발 | - | React 개발 | MES(경동) 우선 | +| QA | - | 테스트 | 단위/통합 테스트 | + +--- + +## 14. 작업 추적 및 관리 + +### 진행 상황 업데이트 +- **매일**: 각 저장소별 CURRENT_WORKS.md 업데이트 +- **매주**: 주차별 로드맵 진행률 체크 +- **매 2주**: 전체 로드맵 리뷰 및 조정 + +### 관련 문서 +- **개발 세부 계획**: `/claudedocs/SAM_DECEMBER_ROADMAP.md` +- **MES 프로젝트**: `/claudedocs/mes/MES_PROJECT_ROADMAP.md` +- **프로젝트 가이드**: `/CLAUDE.md` +- **빠른 참조**: `/SAM_QUICK_REFERENCE.md` + +--- + +## 15. 다음 단계 (1차 MVP 이후) + +### 1차 MVP 추가 기능 (별도 일정) +- 품질 관리 (검사기준/검사관리) +- 자재 관리 (재고현황/입고) +- 단가 관리 +- 회계 (판매조회/수금) + +### Phase 2: 프론트엔드 고도화 +- React Admin 패널 완전 재구축 +- 사용자 포털 (고객 견적 요청) +- 모바일 대응 +- 실시간 대시보드 + +### Phase 3: 고급 기능 +- 실시간 생산 모니터링 +- IoT 센서 연동 +- AI 기반 수요 예측 + +--- + +**작성**: Claude Code +**최종 업데이트**: 2025-12-02 +**다음 리뷰**: 2025-12-09 (주간 체크) \ No newline at end of file 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) 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 diff --git a/plans/integrated-master-plan.md b/plans/integrated-master-plan.md new file mode 100644 index 0000000..7490860 --- /dev/null +++ b/plans/integrated-master-plan.md @@ -0,0 +1,382 @@ +# 통합 개선 계획 — 제품코드 추적성 + 검사 단위 구조 정비 + +> **작성일**: 2026-02-27 +> **목적**: 두 개선 계획(제품코드 추적성, 검사 단위 구조)을 하나의 순차적 실행 계획으로 통합 +> **상태**: 🔄 Phase 0~3 완료, Phase 4 이후 대기 +> **원본 문서**: +> - [`product-code-traceability-plan.md`](./product-code-traceability-plan.md) (아카이브 참조) +> - [`document-system-improvement-plan.md`](./document-system-improvement-plan.md) (아카이브 참조) +> - [`document-system-improvement-review.md`](./document-system-improvement-review.md) (정책 결정 16건) +> **테스트**: [`integrated-test-scenarios.md`](./integrated-test-scenarios.md) (기능 단위 11개 FU) + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 3 - 절곡 검사 동적 구현 (inspection-config API + 트랜잭션 보강) | +| **다음 작업** | Phase 4 (절곡 재공품 + 결재 워크플로우) — 후순위 | +| **진행률** | 5/7 Phase (Phase 0+1+2A+2B+3 완료) | +| **마지막 업데이트** | 2026-02-27 | + +--- + +## 1. 왜 통합이 필요한가 + +두 계획은 **의존성이 교차**한다: + +- 검사 단위 구조 정비(절곡 동적화)는 `work_order_items.options`에 `product_code`가 있어야 동작 +- `product_code` 전파 버그를 먼저 수정하지 않으면 검사 API(`inspection-config`)가 불완전 +- 별도로 작업하면 순서 혼선, 중복 작업, 회귀 위험 발생 + +**통합 효과**: +- 의존성 순서를 강제하여 작업 꼬임 방지 +- 병렬 가능 작업 식별으로 효율 극대화 +- 진행 상태를 한 곳에서 관리 + +--- + +## 2. 통합 Phase 총괄 + +| Phase | 명칭 | 원본 | 의존성 | 상태 | 상세 | +|:-----:|------|------|--------|:----:|------| +| **0** | 사전 데이터 조사 | product-code P0 | 없음 | ✅ | [Phase 0-1 상세](./integrated-phase-0-1.md) | +| **1** | product_code 전파 버그 수정 | product-code P1 | Phase 0 | ✅ | [Phase 0-1 상세](./integrated-phase-0-1.md) | +| **2A** | 절곡 검사 분석/설계 | document-system P1 | 없음 (**Phase 1과 병렬**) | ✅ | [Phase 2 상세](./integrated-phase-2.md) | +| **2B** | 견적/수주 정합성 + 품질 FK | product-code P2+P3 | Phase 1 | ✅ | [Phase 2 상세](./integrated-phase-2.md) | +| **3** | 절곡 검사 동적 구현 | document-system P2 | Phase 1 + 2A | ✅ | [Phase 3 상세](./integrated-phase-3.md) | +| **4** | 절곡 재공품 + 결재 워크플로우 | document-system P3 | Phase 3 | ⏭️ | 마스터 요약만 | +| **5** | 완제품 마스터 + 출하 연결 | product-code P4 | Phase 2B | ⏭️ | 마스터 요약만 | +| **6** | 3관점 검사 + 수주별 뷰 | document-system P4 | Phase 3 + 기획자 | ⏭️ | 마스터 요약만 | + +--- + +## 3. 의존성 다이어그램 + +``` + ┌─────────────────────────────────────────────┐ + │ 실행 타임라인 │ + └─────────────────────────────────────────────┘ + +Phase 0 ─── Phase 1 ──┬── Phase 2B ──── Phase 5 + (조사) (P/C 수정) │ (견적/품질) (FG 마스터) + │ +Phase 2A ──────────────┼── Phase 3 ──── Phase 4 ──── Phase 6 + (절곡 분석) │ (절곡 구현) (재공품) (3관점) + │ + ※ Phase 1 + 2A 병렬 가능 + ※ Phase 2B + 3 준비 부분 병렬 가능 + ※ Phase 4 + 5 독립 (부분 병렬 가능) + +크리티컬 패스: Phase 0 → 1 → 3 → 4 → 6 +``` + +### 병렬 실행 가능 조합 + +| 조합 | 설명 | 조건 | +|------|------|------| +| Phase 1 + 2A | product_code 수정 + 절곡 분석 동시 진행 | 2A는 코드 변경 없음 (분석만) | +| Phase 2B + 3 시작 | 견적/품질 + 절곡 구현 | Phase 1 완료 필수 | +| Phase 4 + 5 | 절곡 재공품 + FG 마스터 | 각각 Phase 3, 2B 완료 | + +--- + +## 4. 공통 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 통합 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. 컬럼 추가 정책: FK/조인키만 컬럼, 나머지는 options JSON │ +│ 2. 기존 데이터 보존: 파괴적 변경 없이 점진적 개선 │ +│ 3. 역추적 가능: 어떤 단계에서든 원래 제품코드로 돌아갈 수 있어야 함│ +│ 4. 네이밍 통일: Backend JSON=snake_case, Frontend=camelCase │ +│ 5. 기존 동작 보존: 스크린/슬랫/조인트바 검사는 건드리지 않음 │ +│ 6. TemplateInspectionContent 통합: 신규 개발은 여기서 (C3) │ +│ 7. BendingInspectionContent 레거시 동결: 유지만, 신규 기능 X │ +│ 8. row_index = 개소 통일: 구성품은 field_key 인코딩 (C1) │ +│ 9. EAV + options 병행: 두 데이터 경로 독립 운용 (C2) │ +│ 10. 롤백 = 템플릿 유무: document_template_id NULL → 레거시 (I4) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | options JSON 필드 추가, React 컴포넌트 내부 리팩토링, 프론트 표시 변경 | 불필요 | +| ⚠️ 컨펌 필요 | 서비스 로직 변경, 마이그레이션, API 엔드포인트 추가, 양식 시더 수정 | **필수** | +| 🔴 금지 | 기존 테이블 컬럼 삭제, 기존 스크린/슬랫 검사 로직 변경 | 별도 협의 | + +--- + +## 5. 핵심 데이터 흐름 (통합 TO-BE) + +``` +견적(quotes) + └─ product_code 컬럼 ✅ (Phase 2B) + └─ calculation_inputs → items[].productCode + │ + ▼ (createFromQuote) +수주(orders) + └─ order_nodes.options → ✅ product_code, product_name + │ + ▼ (createProductionOrder) +작업지시(work_orders) + ├─ work_order_items.options → ✅ product_code (Phase 1 수정) + ├─ inspection-config API → ✅ 공정 자동 판별 + BOM 기반 구성품 (Phase 3) + ├─ TemplateInspectionContent → ✅ 동적 절곡 검사 (Phase 3) + └─ document_data EAV → ✅ C1 field_key 인코딩 + │ + ▼ +품질검사(inspections) + └─ ✅ work_order_id FK (Phase 2B) + │ + ▼ +출하(shipments) + └─ ✅ product_code 포함 (Phase 5) +``` + +--- + +## 6. Phase별 요약 + +### Phase 0: 사전 데이터 조사 ⏳ + +**목표**: 마이그레이션 영향 범위 파악 (읽기 전용, 위험 없음) + +- SQL 4개 실행: order_nodes product_code 보유율, work_order_items source 비율, soft delete 건수, lot_no 중복 +- 결과에 따라 Phase 1 보정 전략 조정 + +→ [상세: integrated-phase-0-1.md](./integrated-phase-0-1.md) + +--- + +### Phase 1: product_code 전파 버그 수정 ⏳ + +**목표**: 모든 work_order_items 생성/수정 경로에서 product_code, product_name 전달 + +- 백엔드 5개 코드 경로 수정 (OrderService, WorkOrderService) +- 기존 데이터 보정 마이그레이션 (스냅샷 백업 후) +- 프론트 WorkerScreen/ProductionDashboard에 제품코드 표시 +- **배포 순서**: 백엔드 → 마이그레이션 → 프론트 + +→ [상세: integrated-phase-0-1.md](./integrated-phase-0-1.md) + +--- + +### Phase 2A: 절곡 검사 분석/설계 ⏳ (**Phase 1과 병렬 가능**) + +**목표**: 절곡 구성품(검사 항목) 정보를 API에서 제공하는 구조 설계 + +- items/BOM 테이블에서 KWE01/KSS01/KSS02 구성품 확인 +- 마감유형(S1/S2/S3)별 차이 분석 +- DEFAULT_GAP_PROFILES 기준치 5130 대조 +- inspection-config 범용 API 설계 + +→ [상세: integrated-phase-2.md](./integrated-phase-2.md) + +--- + +### Phase 2B: 견적/수주 정합성 + 품질 FK ⏳ + +**목표**: quotes.product_code 활용 + inspections ↔ work_orders FK 연결 + +- 견적 저장 시 quotes.product_code 저장 +- inspections 테이블에 work_order_id FK 마이그레이션 +- 기존 데이터 보정 (lot_no 기반 역추적) +- **Phase 2B 내부에서 견적/품질 작업은 병렬 가능** (독립 경로) + +→ [상세: integrated-phase-2.md](./integrated-phase-2.md) + +--- + +### Phase 3: 절곡 검사 동적 구현 ✅ + +**목표**: API 기반 동적 구성품 로딩으로 고정 로직 대체 + +- inspection-config API 구현 (BelongsToTenant 필수) +- TemplateInspectionContent buildBendingProducts → API 연동 +- document_data EAV 저장/복원 검증 (C1 field_key) +- createInspectionDocument 트랜잭션 보강 (I2) +- 레거시(Path A) + 신규(Path B) 독립 동작 확인 + +→ [상세: integrated-phase-3.md](./integrated-phase-3.md) + +--- + +### Phase 4: 절곡 재공품 + 결재 워크플로우 ⏭️ + +**목표**: BendingWip 양식 추가 + 결재 프론트 연동 + +| # | 작업 항목 | 비고 | +|---|----------|------| +| 4.1 | 절곡 재공품 mng 양식 시더 추가 | BendingWipInspectionContent 대응 | +| 4.2 | 결재 워크플로우 프론트 연동 | 작성→검토→승인 3단계 | +| 4.3 | React 기존 하드코딩 컴포넌트 전환 결정 | 프론트 담당자 협의 | + +> 실행 시점에 상세 문서 별도 작성 + +--- + +### Phase 5: 완제품 마스터 + 출하 연결 ⏭️ + +**목표**: FG 품목 등록 + 출하 시 제품코드 포함 + orders.item_id + +| # | 작업 항목 | 비고 | +|---|----------|------| +| 5.1 | 완제품(FG) 품목 자동 등록 방안 설계 | 견적 확정 시 or 수주 확정 시 | +| 5.2 | orders.item_id 설정 | FG 품목 등록 후 가능 | +| 5.3 | shipment_items에 product_code 포함 | 부분 출하 시 개소별 매핑 고려 | +| 5.4 | work_order_items.product_code 컬럼 승격 검토 | 통계 쿼리 성능용 | +| 5.5 | E2E 추적 검증 | 견적→출하→품질 전 구간 | + +> 실행 시점에 상세 문서 별도 작성 + +--- + +### Phase 6: 3관점 검사 + 수주별 뷰 ⏭️ + +**목표**: 구성품별/개소별/수주별 3관점 검사 구조 + 수주별 읽기 전용 뷰 + +| # | 작업 항목 | 비고 | +|---|----------|------| +| 6.1 | 기획자와 3관점 화면 설계 협의 (I3) | 화면 구성·데이터 매핑·UI 설계 | +| 6.2 | 수주별 읽기 전용 뷰 구현 (I7) | 입력=개소별, 출력=수주별 | +| 6.3 | 개소별↔구성품별↔수주별 데이터 매핑 | | +| 6.4 | 5130 recordscreen JSON → EAV 변환 | 이관 설계 | + +> 기획자 협의 후 상세 문서 별도 작성 + +--- + +## 7. 통합 성공 기준 + +### Phase 0-1 (product_code) + +| 기준 | 수치 목표 | +|------|----------| +| WorkerScreen 제품코드 표시 | 100% | +| 신규 작업지시 product_code 포함 | NOT NULL | +| 기존 데이터 보정율 (source_order_item_id 있는 건) | 90% 이상 | +| 기존 기능 회귀 | 에러 0건 | +| API 성능 영향 | 5% 미만 | + +### Phase 2A-2B (분석/견적/품질) + +| 기준 | 수치 목표 | +|------|----------| +| KWE01/KSS01/KSS02 구성품 분석 완료 | 3종 이상 | +| DEFAULT_GAP_PROFILES 5130 대조 | 완료 | +| quotes.product_code 저장 | 정상 동작 | +| inspections.work_order_id FK 보정 정확도 | 95% 이상 | + +### Phase 3 (절곡 동적 구현) + +| 기준 | 수치 목표 | +|------|----------| +| 제품코드별 다른 구성품 표시 | 3종 이상 지원 | +| 마감유형별 구성품 변경 | 정상 동작 | +| 기존 절곡 데이터 호환 (Path A + B) | 100% | +| inspection-config API 응답 시간 | < 200ms | +| 스크린/슬랫 회귀 | 에러 0건 | +| document_data 저장 정합성 | 100% | + +--- + +## 8. 통합 컨펌 대기 목록 + +| # | Phase | 항목 | 변경 내용 | 상태 | +|---|:-----:|------|----------|:----:| +| 1 | 0 | 사전 조사 실행 | SQL 4개 (읽기 전용) | ⚠️ 대기 | +| 2 | 1 | product_code 전파 수정 | 5개 코드 경로 options 복사 변경 | ⚠️ 대기 | +| 3 | 1 | 데이터 보정 마이그레이션 | 기존 work_order_items 역추적 보정 | ⚠️ 대기 | +| 4 | 2A | inspection-config API 설계 | 범용 API 엔드포인트 추가 | ⚠️ 대기 | +| 5 | 2B | inspections.work_order_id FK | 마이그레이션 + 로직 수정 | ⚠️ 대기 | +| 6 | 3 | inspection-config API 구현 | 공정 자동 판별 + BOM 구성품 | ⚠️ 대기 | +| 7 | 5 | 완제품 마스터 자동 등록 | items 테이블에 FG 품목 생성 | ⚠️ 대기 | +| 8 | 6 | 3관점 검사 화면 설계 | 기획자 협의 필요 | ⏭️ | + +--- + +## 9. 롤백 전략 (통합) + +| Phase | 위험도 | 롤백 방법 | +|:-----:|:------:|----------| +| 0 | 없음 | 읽기 전용 | +| 1 (options 추가) | 낮음 | options에서 `product_code`, `product_name` 키 제거 스크립트 | +| 1 (데이터 보정) | 중간 | `work_order_items_backup_product_code` 백업 테이블에서 복원 | +| 2B (inspections FK) | 중간 | `work_order_id` 컬럼 drop 마이그레이션 (down 메서드) | +| 3 (절곡 동적화) | 낮음 | document_template_id NULL → 레거시 컴포넌트 자동 복귀 (I4) | +| 5 (FG 품목) | 높음 | `auto_generated` 플래그 기반 식별 후 삭제 | + +--- + +## 10. 참고 파일 (통합) + +### 백엔드 + +| 파일 | 역할 | 관련 Phase | +|------|------|:----------:| +| `api/app/Services/OrderService.php` | 수주→작업지시 변환 (L1410) | 1 | +| `api/app/Services/WorkOrderService.php` | 작업지시 서비스 (L287, L311, L416) | 1, 3 | +| `api/app/Services/Quote/QuoteService.php` | 견적 서비스 | 2B | +| `api/app/Services/InspectionService.php` | 품질검사 서비스 | 2B | +| `api/app/Services/DocumentService.php` | 문서 CRUD | 3 | + +### 프론트엔드 + +| 파일 | 역할 | 관련 Phase | +|------|------|:----------:| +| `react/.../WorkerScreen/actions.ts` | 작업자 화면 서버 액션 | 1 | +| `react/.../WorkerScreen/index.tsx` | 작업자 화면 메인 | 1 | +| `react/.../documents/TemplateInspectionContent.tsx` | 양식 기반 동적 렌더링 (**통합 방향**) | 3 | +| `react/.../documents/BendingInspectionContent.tsx` | 절곡 레거시 (**동결**) | — | +| `react/.../documents/InspectionReportModal.tsx` | 검사 모달 래퍼 | 3 | + +### 참고 문서 + +| 문서 | 경로 | 용도 | +|------|------|------| +| 원본: 제품코드 추적성 | `docs/plans/product-code-traceability-plan.md` | 상세 코드/쿼리 참조 | +| 원본: 검사 단위 구조 | `docs/plans/document-system-improvement-plan.md` | 상세 설계/정책 참조 | +| 리뷰 정책 결정 | `docs/plans/document-system-improvement-review.md` | 16건 정책 결정 | +| 문서 시스템 마스터 | `docs/plans/document-system-master.md` | 전체 Phase 관리 | +| API 규칙 | `API_RULES.md` | Service-First, FormRequest | +| DB 스키마 | `docs/specs/database-schema.md` | 테이블 구조 | + +--- + +## 11. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | +|------|------|----------| +| 2026-02-27 | 통합 문서 작성 | product-code + document-system 2개 계획을 7 Phase 통합 계획으로 병합 | +| 2026-02-27 | Phase 2A 완료 | 절곡 검사 분석/설계 완료. dynamic_bom 발견, 5130 대조 완료, inspection-config API 재설계 | +| 2026-02-27 | Phase 2B 완료 | 견적 product_code 자동추출, inspections.work_order_id FK, 데이터 보정 25건 | +| 2026-02-27 | Phase 3 완료 | inspection-config API(3.1), TemplateInspectionContent API 연동(3.2), EAV 호환 확인(3.3+3.4), 트랜잭션 보강(3.5) | + +--- + +## 12. 세션 관리 정책 + +### 세션 시작 시 +``` +1. 이 문서(integrated-master-plan.md) 읽기 +2. 진행 상태 테이블 확인 → 마지막 완료 작업 파악 +3. 해당 Phase 상세 문서 읽기 +4. 다음 작업 시작 +``` + +### 작업 중 관리 +- Phase 완료 시 이 문서의 진행 상태 테이블 업데이트 +- 해당 Phase 상세 문서도 업데이트 +- 컨펌 필요 사항 발생 시 컨펌 대기 목록에 추가 + +### 세션 종료 시 +- 변경 이력 섹션에 최종 업데이트 기록 + +--- + +*이 문서는 `product-code-traceability-plan.md`와 `document-system-improvement-plan.md`를 통합한 마스터 계획입니다.* \ No newline at end of file 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 cf0c128764f5171456cd7ff740e2e9ae85b94c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Mar 2026 23:31:41 +0900 Subject: [PATCH 15/15] =?UTF-8?q?chore:=20.gitignore=20=ED=99=94=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EC=8A=A4=ED=8A=B8=E2=86=92=EB=B8=94=EB=9E=99?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존: 모든 파일 무시(*) + 폴더별 허용(!path) → 새 파일 추가 시 git add -f 필요 - 변경: 쓰레기 파일만 제외 (.DS_Store, *.log, *.tmp 등) --- .gitignore | 58 ++++++++++++++++-------------------------------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 300f6f3..f9e7597 100644 --- a/.gitignore +++ b/.gitignore @@ -1,43 +1,19 @@ -# 모든 파일 무시 -* - -# 추적할 파일만 허용 -!.gitignore -!INDEX.md -!README.md -!resources.md - -# 문서 폴더 (루트 기준) -!assets/ -!assets/** -!brochure/ -!brochure/** -!changes/ -!changes/** -!contracts/ -!contracts/** -contracts/docx/backup/ -!data/ -!data/** -!dev/ -!dev/** -!features/ -!features/** -!frontend/ -!frontend/** -!guides/ -!guides/** -!plans/ -!plans/** -!projects/ -!projects/** -!requests/ -!requests/** -!rules/ -!rules/** -!system/ -!system/** - -# 기타 +# OS/에디터 생성 파일 .DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# 로그/임시 파일 +*.log +*.tmp +*.temp +*.bak +*.cache + +# 노션 내보내기 임시 _to_notion/ + +# 백업 파일 +contracts/docx/backup/

NGg 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�{LT{YIgJu35}0U3A*nx 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