diff --git a/docs/00_OVERVIEW.md b/docs/00_OVERVIEW.md new file mode 100644 index 00000000..a11602d3 --- /dev/null +++ b/docs/00_OVERVIEW.md @@ -0,0 +1,259 @@ +# MNG 애플리케이션 전체 개발 계획 + +## 📋 프로젝트 개요 + +**목적:** Admin(Filament) 기능을 Plain Laravel(Blade + Tailwind)로 마이그레이션하여 운영 주력 관리자 패널 구축 + +**개발 기간:** 8-11주 (Phase별 상세 타임라인 참조) + +**기술 스택:** +- Backend: Laravel 12 + PHP 8.2+ +- Frontend: Blade + Tailwind CSS + DaisyUI + HTMX + Vite +- 인증: Laravel Sanctum +- 아키텍처: Multi-tenant + RBAC + Audit Log + +## 🎯 핵심 목표 + +1. **독립성:** Admin(Filament)과 별개로 독립 실행 가능한 관리자 패널 +2. **사용성:** Plain Laravel (Blade + Tailwind + DaisyUI + HTMX)로 수정 용이한 UI/UX +3. **확장성:** Multi-tenant, RBAC 완벽 지원 +4. **품질:** SAM API Rules 준수 - Service-First, FormRequest, i18n, Audit Log 일관성 + +## 📊 전체 메뉴 구조 + +``` +MNG 애플리케이션 +├── 회원관리 (User Management) +├── 테넌트관리 (Tenant Management) +├── 거래처관리 (Client Management) +├── 영업관리 (Sales Management) +├── 전자결재관리 (Approval Management) +├── 템플릿관리 (Template Management) +├── 게시판관리 (Board Management) +├── 견적서관리 (Quotation Management) +├── 구독관리 (Subscription Management) +├── 결제관리 (Payment Management) +├── 환불관리 (Refund Management) +├── 설정 (Settings) +│ ├── 관리자 계정 관리 +│ ├── 카테고리 관리 +│ ├── 배너 관리 +│ ├── 팝업 관리 +│ ├── 이메일 관리 +│ └── 문자 관리 +└── 통계 (Statistics & Analytics) +``` + +## 🗓️ Phase별 개발 계획 + +### Phase 1: 기반 마스터 데이터 (1-2주) +**문서:** `01_PHASE1_MASTER_DATA.md` + +**목표:** 모든 기능의 기반이 되는 핵심 마스터 데이터 구축 + +**포함 기능:** +- ✅ 회원관리 (User Management) +- ✅ 테넌트관리 (Tenant Management) +- ✅ 거래처관리 (Client Management) + +**핵심 산출물:** +- Users, Tenants, Clients 테이블 및 모델 +- CRUD 기능 완성 +- 권한/역할 할당 기능 + +--- + +### Phase 2: 시스템 설정 (1주) +**문서:** `02_PHASE2_SETTINGS.md` + +**목표:** 다른 기능들이 참조하는 공통 설정 기능 구축 + +**포함 기능:** +- ✅ 설정 - 카테고리 관리 +- ✅ 설정 - 관리자 계정 관리 + +**핵심 산출물:** +- Categories 테이블 및 계층 구조 +- 슈퍼관리자 계정 관리 UI + +--- + +### Phase 3: 비즈니스 핵심 기능 (2-3주) +**문서:** `03_PHASE3_BUSINESS_CORE.md` + +**목표:** 실제 비즈니스 가치를 창출하는 핵심 기능 구현 + +**포함 기능:** +- ✅ 영업관리 (Sales Management) +- ✅ 견적서관리 (Quotation Management) +- ✅ 전자결재관리 (Approval Management) + +**핵심 산출물:** +- 영업 파이프라인 시스템 +- 견적서 생성 및 PDF 출력 +- 결재선 설정 및 승인 워크플로우 + +--- + +### Phase 4: 콘텐츠 관리 (1-2주) +**문서:** `04_PHASE4_CONTENT.md` + +**목표:** 사용자 경험 향상을 위한 콘텐츠 관리 기능 구현 + +**포함 기능:** +- ✅ 템플릿관리 (Template Management) +- ✅ 게시판관리 (Board Management) - EAV 패턴 +- ✅ 설정 - 배너/팝업 관리 + +**핵심 산출물:** +- 문서 템플릿 변수 치환 시스템 +- EAV 기반 유연한 게시판 시스템 +- 배너/팝업 노출 관리 + +**특이사항:** 게시판은 EAV + Atomic Design 전략 적용 (CLAUDE.md 참조) + +--- + +### Phase 5: 수익 관리 (2주) +**문서:** `05_PHASE5_REVENUE.md` + +**목표:** SaaS 비즈니스 모델의 수익 관리 시스템 구축 + +**포함 기능:** +- ✅ 구독관리 (Subscription Management) +- ✅ 결제관리 (Payment Management) +- ✅ 환불관리 (Refund Management) + +**핵심 산출물:** +- 구독 플랜 및 갱신 시스템 +- PG 연동 (토스페이먼츠 등) +- 환불 요청 및 처리 워크플로우 + +--- + +### Phase 6: 커뮤니케이션 & 통계 (1-2주) +**문서:** `06_PHASE6_COMM_STATS.md` + +**목표:** 고객 커뮤니케이션 및 데이터 분석 기능 완성 + +**포함 기능:** +- ✅ 설정 - 이메일 관리 +- ✅ 설정 - 문자 관리 +- ✅ 통계 (Statistics & Analytics) + +**핵심 산출물:** +- 이메일/SMS 발송 시스템 +- 대시보드 차트 및 통계 +- 엑셀 내보내기 기능 + +--- + +## 🛠️ 공통 개발 원칙 + +### 1. Architecture Pattern +``` +Controller (라우팅, 요청/응답) + ↓ +FormRequest (유효성 검증) + ↓ +Service (비즈니스 로직) ← Repository (선택적) + ↓ +Model (Eloquent ORM) + ↓ +Database +``` + +### 2. Multi-tenancy & Security +- **BelongsToTenant trait:** 모든 tenant 데이터 모델에 필수 +- **Tenant Scope:** 자동으로 tenant_id 필터링 +- **RBAC:** 메뉴 기반 권한 체크 (Menu → Permission → Role) +- **Audit Log:** 모든 CUD 작업 기록 (13개월 보관) + +### 3. Frontend Stack +- **Blade Templates:** 서버 사이드 렌더링 +- **Tailwind CSS:** 유틸리티 우선 스타일링 +- **DaisyUI:** Tailwind 기반 컴포넌트 라이브러리 +- **HTMX:** 선언적 AJAX, CSS 전환 및 WebSocket 지원 +- **Vite:** 빠른 빌드 및 HMR + +### 4. Code Quality Standards +- **Laravel Pint:** 코드 스타일 자동 포맷 +- **PHPStan:** 정적 분석 (Level 5+) +- **i18n:** 한글 직접 금지, `__('key')` 사용 +- **Soft Delete:** 기본 삭제 정책 +- **Service-First:** 비즈니스 로직은 반드시 Service 계층 +- **FormRequest:** Controller에서 검증 금지 + +### 5. API 연동 +- **API 서버:** 별도 저장소 (독립 실행) +- **Product/BOM:** API에서 조회 (로컬 DB 복제 금지) +- **인증:** Sanctum 토큰 기반 +- **에러 처리:** API 장애 시 graceful degradation + +--- + +## 📁 문서 구조 + +``` +claudedocs/mng/ +├── 00_OVERVIEW.md (본 문서) +├── 01_PHASE1_MASTER_DATA.md +├── 02_PHASE2_SETTINGS.md +├── 03_PHASE3_BUSINESS_CORE.md +├── 04_PHASE4_CONTENT.md +├── 05_PHASE5_REVENUE.md +├── 06_PHASE6_COMM_STATS.md +└── 99_TECHNICAL_STANDARDS.md +``` + +각 Phase 문서 포함 내용: +- **기능 목록 및 우선순위** +- **DB 스키마 설계** +- **API 엔드포인트 명세** +- **UI/UX 와이어프레임** (텍스트 기반) +- **개발 체크리스트** + +--- + +## 🔗 참고 문서 + +- **SAM 빠른 참조:** `SAM_QUICK_REFERENCE.md` +- **API 규칙:** `API_RULES.md` +- **개발 명령어:** `DEV_COMMANDS.md` +- **품질 체크리스트:** `QUALITY_CHECKLIST.md` +- **MES 프로젝트:** `claudedocs/mes/README.md` +- **EAV + Atomic Design:** `CLAUDE.md` (게시판 시스템 전략) + +--- + +## ⚠️ 중요 고려사항 + +### Admin과의 관계 +- **독립 실행:** MNG는 Admin과 별개로 독립적으로 동작 +- **중복 허용:** 일부 기능이 Admin과 중복될 수 있으나, UI/UX 개선이 목표 +- **점진적 전환:** Admin은 점차 deprecated, MNG가 운영 주력 + +### 개발 우선순위 +1. **Phase 1-2 필수:** 다른 Phase의 선행 조건 +2. **Phase 3-4 핵심:** 비즈니스 가치 창출 +3. **Phase 5-6 확장:** 완성도 및 부가 기능 + +### 품질 보증 +- **매 Phase 완료 시:** Pint, PHPStan, 테스트 실행 +- **코드 리뷰:** `code-workflow` 스킬 활용 +- **문서화:** 각 기능별 README 및 주석 + +--- + +## 📈 성공 지표 + +- **개발 속도:** Phase별 예상 기간 준수 +- **코드 품질:** PHPStan Level 5+ 통과 +- **사용자 경험:** Admin 대비 클릭 수 30% 감소 +- **유지보수성:** 신규 기능 추가 시간 50% 단축 + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code (Sequential Thinking MCP) +**버전:** 1.0.0 \ No newline at end of file diff --git a/docs/01_PHASE1_MASTER_DATA.md b/docs/01_PHASE1_MASTER_DATA.md new file mode 100644 index 00000000..fa7cadf9 --- /dev/null +++ b/docs/01_PHASE1_MASTER_DATA.md @@ -0,0 +1,454 @@ +# Phase 1: 기반 마스터 데이터 + +**기간:** 1-2주 +**우선순위:** 최고 (모든 기능의 선행 조건) + +## 📋 Phase 개요 + +모든 비즈니스 기능의 기반이 되는 핵심 마스터 데이터를 구축합니다. + +**포함 기능:** +1. 회원관리 (User Management) +2. 테넌트관리 (Tenant Management) +3. 거래처관리 (Client Management) + +--- + +## 1️⃣ 회원관리 (User Management) + +### 기능 목록 + +#### 1.1 회원 목록 조회 +- **경로:** `/mng/users` +- **기능:** + - 페이지네이션 (기본 20개/페이지) + - 검색 (이름, 이메일, 부서, 역할) + - 필터 (활성/비활성, 역할별, 부서별) + - 정렬 (가입일, 이름, 이메일) +- **권한:** `users.index` + +#### 1.2 회원 상세 조회 +- **경로:** `/mng/users/{id}` +- **기능:** + - 기본 정보 표시 + - 소속 테넌트 정보 + - 역할/부서 정보 + - 최근 활동 로그 +- **권한:** `users.show` + +#### 1.3 회원 생성 +- **경로:** `/mng/users/create` +- **기능:** + - 기본 정보 입력 (이름, 이메일, 비밀번호) + - 역할 할당 (다중 선택 가능) + - 부서 할당 + - 활성/비활성 설정 +- **권한:** `users.create` +- **검증:** + - 이메일 중복 체크 + - 비밀번호 강도 검증 (8자 이상, 영문+숫자) + - 필수 필드 검증 + +#### 1.4 회원 수정 +- **경로:** `/mng/users/{id}/edit` +- **기능:** + - 기본 정보 수정 + - 역할/부서 변경 + - 비밀번호 재설정 + - 활성/비활성 전환 +- **권한:** `users.update` + +#### 1.5 회원 삭제 +- **경로:** `/mng/users/{id}` +- **기능:** + - Soft Delete (복구 가능) + - 삭제 확인 모달 + - 연관 데이터 처리 (담당 영업 등) +- **권한:** `users.delete` + +#### 1.6 비밀번호 관리 +- **경로:** `/mng/users/{id}/password` +- **기능:** + - 관리자 비밀번호 재설정 + - 임시 비밀번호 발급 (이메일 전송) + - 비밀번호 변경 이력 기록 +- **권한:** `users.password.reset` + +### DB 스키마 + +```sql +-- users 테이블 (Laravel 기본 + 확장) +CREATE TABLE users ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL COMMENT '소속 테넌트 ID', + name VARCHAR(255) NOT NULL COMMENT '사용자 이름', + email VARCHAR(255) UNIQUE NOT NULL COMMENT '이메일 (로그인 ID)', + email_verified_at TIMESTAMP NULL COMMENT '이메일 인증 시각', + password VARCHAR(255) NOT NULL COMMENT '비밀번호 (해시)', + phone VARCHAR(20) NULL COMMENT '연락처', + department_id BIGINT UNSIGNED NULL COMMENT '부서 ID', + is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부', + last_login_at TIMESTAMP NULL COMMENT '마지막 로그인', + remember_token VARCHAR(100) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL COMMENT 'Soft Delete', + + INDEX idx_tenant_id (tenant_id), + INDEX idx_email (email), + INDEX idx_department_id (department_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- model_has_roles 테이블 (Spatie Permission) +-- 이미 존재하는 테이블 활용 +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/users` | 회원 목록 | - | +| GET | `/mng/users/{id}` | 회원 상세 | - | +| GET | `/mng/users/create` | 회원 생성 폼 | - | +| POST | `/mng/users` | 회원 생성 | `StoreUserRequest` | +| GET | `/mng/users/{id}/edit` | 회원 수정 폼 | - | +| PUT | `/mng/users/{id}` | 회원 수정 | `UpdateUserRequest` | +| DELETE | `/mng/users/{id}` | 회원 삭제 | - | +| POST | `/mng/users/{id}/password/reset` | 비밀번호 재설정 | `ResetPasswordRequest` | + +### Service 클래스 + +```php +// app/Services/UserService.php +class UserService +{ + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): User; + public function create(array $data): User; + public function update(User $user, array $data): User; + public function delete(User $user): bool; + public function resetPassword(User $user, string $newPassword): bool; + public function assignRoles(User $user, array $roleIds): void; +} +``` + +### UI/UX 와이어프레임 (텍스트) + +``` +┌─────────────────────────────────────────────────────────┐ +│ 회원 관리 [+ 새 회원] │ +├─────────────────────────────────────────────────────────┤ +│ 🔍 [검색: 이름, 이메일] [부서▼] [역할▼] [상태▼] [검색]│ +├─────────────────────────────────────────────────────────┤ +│ ☑ | 이름 | 이메일 | 부서 | 역할 | 상태 | 작업 │ +│ ☐ | 홍길동 | hong@ex.com | 개발팀 | Admin | 활성 | [수정][삭제] │ +│ ☐ | 김철수 | kim@ex.com | 영업팀 | User | 활성 | [수정][삭제] │ +├─────────────────────────────────────────────────────────┤ +│ « 1 2 3 ... 10 » │ +└─────────────────────────────────────────────────────────┘ +``` + +### 개발 체크리스트 + +- [ ] `User` 모델에 `BelongsToTenant` trait 추가 +- [ ] `UserService` 클래스 작성 (비즈니스 로직) +- [ ] `StoreUserRequest`, `UpdateUserRequest` 작성 (검증) +- [ ] `UserController` 작성 (라우팅 처리) +- [ ] Blade 템플릿 작성 (`users/index.blade.php` 등) +- [ ] Alpine.js 인터랙션 추가 (모달, 검색 필터) +- [ ] 권한 체크 미들웨어 적용 (`can:users.index`) +- [ ] Audit Log 자동 기록 (UserObserver) +- [ ] i18n 키 작성 (`lang/ko/users.php`) +- [ ] Pint 포맷팅 및 PHPStan 검사 통과 +- [ ] 테스트 작성 (`UserServiceTest`, `UserControllerTest`) + +--- + +## 2️⃣ 테넌트관리 (Tenant Management) + +### 기능 목록 + +#### 2.1 테넌트 목록 조회 +- **경로:** `/mng/tenants` +- **기능:** + - 페이지네이션 + - 검색 (회사명, 도메인) + - 필터 (구독 상태, 플랜) + - 정렬 (생성일, 회사명) +- **권한:** `tenants.index` + +#### 2.2 테넌트 상세 조회 +- **경로:** `/mng/tenants/{id}` +- **기능:** + - 기본 정보 (회사명, 도메인, 연락처) + - 구독 정보 (플랜, 만료일) + - 사용 통계 (회원 수, 저장 용량) + - 최근 결제 내역 +- **권한:** `tenants.show` + +#### 2.3 테넌트 생성 +- **경로:** `/mng/tenants/create` +- **기능:** + - 회사 정보 입력 + - 도메인 설정 (예: company.sam.kr) + - 초기 구독 플랜 선택 + - 관리자 계정 생성 +- **권한:** `tenants.create` + +#### 2.4 테넌트 수정 +- **경로:** `/mng/tenants/{id}/edit` +- **기능:** + - 회사 정보 수정 + - 구독 플랜 변경 + - 활성/비활성 전환 +- **권한:** `tenants.update` + +#### 2.5 테넌트 삭제 +- **경로:** `/mng/tenants/{id}` +- **기능:** + - Soft Delete + - 연관 데이터 처리 (사용자, 게시물 등) + - 삭제 전 데이터 백업 권장 +- **권한:** `tenants.delete` + +### DB 스키마 + +```sql +CREATE TABLE tenants ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) NOT NULL COMMENT '회사명', + domain VARCHAR(100) UNIQUE NOT NULL COMMENT '도메인 (예: company)', + email VARCHAR(255) NOT NULL COMMENT '대표 이메일', + phone VARCHAR(20) NULL COMMENT '대표 전화', + address TEXT NULL COMMENT '주소', + business_number VARCHAR(50) NULL COMMENT '사업자번호', + subscription_plan ENUM('free', 'basic', 'pro', 'enterprise') DEFAULT 'free' COMMENT '구독 플랜', + subscription_expires_at TIMESTAMP NULL COMMENT '구독 만료일', + is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부', + max_users INT DEFAULT 10 COMMENT '최대 사용자 수', + storage_limit BIGINT DEFAULT 1073741824 COMMENT '저장 용량 제한 (바이트, 기본 1GB)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_domain (domain), + INDEX idx_subscription_plan (subscription_plan), + INDEX idx_is_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/tenants` | 테넌트 목록 | - | +| GET | `/mng/tenants/{id}` | 테넌트 상세 | - | +| GET | `/mng/tenants/create` | 테넌트 생성 폼 | - | +| POST | `/mng/tenants` | 테넌트 생성 | `StoreTenantRequest` | +| GET | `/mng/tenants/{id}/edit` | 테넌트 수정 폼 | - | +| PUT | `/mng/tenants/{id}` | 테넌트 수정 | `UpdateTenantRequest` | +| DELETE | `/mng/tenants/{id}` | 테넌트 삭제 | - | + +### Service 클래스 + +```php +// app/Services/TenantService.php +class TenantService +{ + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): Tenant; + public function create(array $data): Tenant; + public function update(Tenant $tenant, array $data): Tenant; + public function delete(Tenant $tenant): bool; + public function changePlan(Tenant $tenant, string $plan): bool; + public function getUsageStats(Tenant $tenant): array; +} +``` + +### 개발 체크리스트 + +- [ ] `Tenant` 모델 작성 (BelongsToTenant 제외 - 최상위) +- [ ] `TenantService` 클래스 작성 +- [ ] `StoreTenantRequest`, `UpdateTenantRequest` 작성 +- [ ] `TenantController` 작성 +- [ ] Blade 템플릿 작성 +- [ ] 구독 플랜 변경 로직 구현 +- [ ] 사용량 통계 계산 로직 (회원 수, 저장 용량) +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 3️⃣ 거래처관리 (Client Management) + +### 기능 목록 + +#### 3.1 거래처 목록 조회 +- **경로:** `/mng/clients` +- **기능:** + - 페이지네이션 + - 검색 (회사명, 담당자명) + - 필터 (거래 상태, 업종) + - 정렬 (생성일, 회사명) +- **권한:** `clients.index` + +#### 3.2 거래처 상세 조회 +- **경로:** `/mng/clients/{id}` +- **기능:** + - 기본 정보 (회사명, 연락처, 주소) + - 담당자 정보 (이름, 직책, 연락처) + - 거래 이력 (견적서, 계약) + - 메모/코멘트 +- **권한:** `clients.show` + +#### 3.3 거래처 생성 +- **경로:** `/mng/clients/create` +- **기능:** + - 회사 정보 입력 + - 담당자 정보 입력 (다중 가능) + - 업종/분류 선택 + - 메모 작성 +- **권한:** `clients.create` + +#### 3.4 거래처 수정 +- **경로:** `/mng/clients/{id}/edit` +- **기능:** + - 기본 정보 수정 + - 담당자 추가/수정/삭제 + - 거래 상태 변경 +- **권한:** `clients.update` + +#### 3.5 거래처 삭제 +- **경로:** `/mng/clients/{id}` +- **기능:** + - Soft Delete + - 연관 데이터 체크 (진행 중인 견적서 등) +- **권한:** `clients.delete` + +### DB 스키마 + +```sql +CREATE TABLE clients ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '소속 테넌트', + name VARCHAR(255) NOT NULL COMMENT '회사명', + business_number VARCHAR(50) NULL COMMENT '사업자번호', + industry VARCHAR(100) NULL COMMENT '업종', + phone VARCHAR(20) NULL COMMENT '대표 전화', + email VARCHAR(255) NULL COMMENT '대표 이메일', + address TEXT NULL COMMENT '주소', + status ENUM('active', 'inactive', 'potential') DEFAULT 'potential' COMMENT '거래 상태', + notes TEXT NULL COMMENT '메모', + assigned_user_id BIGINT UNSIGNED NULL COMMENT '담당 영업 사원', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_status (status), + INDEX idx_assigned_user_id (assigned_user_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (assigned_user_id) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE client_contacts ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + client_id BIGINT UNSIGNED NOT NULL COMMENT '거래처 ID', + name VARCHAR(255) NOT NULL COMMENT '담당자 이름', + position VARCHAR(100) NULL COMMENT '직책', + phone VARCHAR(20) NULL COMMENT '연락처', + email VARCHAR(255) NULL COMMENT '이메일', + is_primary BOOLEAN DEFAULT FALSE COMMENT '주 담당자 여부', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_client_id (client_id), + FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/clients` | 거래처 목록 | - | +| GET | `/mng/clients/{id}` | 거래처 상세 | - | +| GET | `/mng/clients/create` | 거래처 생성 폼 | - | +| POST | `/mng/clients` | 거래처 생성 | `StoreClientRequest` | +| GET | `/mng/clients/{id}/edit` | 거래처 수정 폼 | - | +| PUT | `/mng/clients/{id}` | 거래처 수정 | `UpdateClientRequest` | +| DELETE | `/mng/clients/{id}` | 거래처 삭제 | - | +| POST | `/mng/clients/{id}/contacts` | 담당자 추가 | `StoreContactRequest` | + +### Service 클래스 + +```php +// app/Services/ClientService.php +class ClientService +{ + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): Client; + public function create(array $data): Client; + public function update(Client $client, array $data): Client; + public function delete(Client $client): bool; + public function addContact(Client $client, array $contactData): ClientContact; + public function getTransactionHistory(Client $client): Collection; +} +``` + +### 개발 체크리스트 + +- [ ] `Client`, `ClientContact` 모델 작성 (BelongsToTenant) +- [ ] `ClientService` 클래스 작성 +- [ ] FormRequest 작성 +- [ ] `ClientController` 작성 +- [ ] Blade 템플릿 작성 (담당자 다중 입력 UI) +- [ ] 거래 이력 연동 (견적서, 계약) +- [ ] 담당 영업 사원 할당 기능 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 🎯 Phase 1 완료 조건 + +### 기능 완성도 +- [ ] 3개 모듈 모두 CRUD 완성 +- [ ] 각 모듈별 검색/필터/정렬 동작 +- [ ] 권한 체크 정상 작동 + +### 코드 품질 +- [ ] Service-First 패턴 준수 +- [ ] FormRequest 검증 구현 +- [ ] BelongsToTenant trait 적용 +- [ ] i18n 키 사용 (한글 직접 사용 금지) +- [ ] Pint 포맷팅 통과 +- [ ] PHPStan Level 5+ 통과 + +### 데이터 무결성 +- [ ] Multi-tenant 격리 확인 +- [ ] Soft Delete 동작 확인 +- [ ] Audit Log 자동 기록 확인 +- [ ] Foreign Key 제약 조건 정상 작동 + +### 테스트 +- [ ] Service 계층 유닛 테스트 +- [ ] Controller 계층 Feature 테스트 +- [ ] 권한 체크 테스트 +- [ ] 검증 로직 테스트 + +--- + +## 📚 참고 자료 + +- **SAM API Rules:** `API_RULES.md` +- **개발 명령어:** `DEV_COMMANDS.md` +- **품질 체크리스트:** `QUALITY_CHECKLIST.md` + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code +**버전:** 1.0.0 \ No newline at end of file diff --git a/docs/02_PHASE2_SETTINGS.md b/docs/02_PHASE2_SETTINGS.md new file mode 100644 index 00000000..86072bb9 --- /dev/null +++ b/docs/02_PHASE2_SETTINGS.md @@ -0,0 +1,300 @@ +# Phase 2: 시스템 설정 + +**기간:** 1주 +**우선순위:** 높음 (Phase 3-6의 선행 조건) +**의존성:** Phase 1 (회원관리, 테넌트관리) + +## 📋 Phase 개요 + +다른 기능들이 참조하는 공통 설정 기능을 구축합니다. + +**포함 기능:** +1. 설정 - 카테고리 관리 +2. 설정 - 관리자 계정 관리 + +--- + +## 1️⃣ 설정 - 카테고리 관리 + +### 기능 목록 + +#### 1.1 카테고리 목록 조회 +- **경로:** `/mng/settings/categories` +- **기능:** + - 계층 구조 트리 뷰 + - 타입별 필터 (제품, 게시판, 파일 등) + - 드래그 앤 드롭 정렬 (순서 변경) + - 검색 (카테고리명) +- **권한:** `settings.categories.index` + +#### 1.2 카테고리 생성 +- **경로:** `/mng/settings/categories/create` +- **기능:** + - 카테고리명 입력 (다국어 지원) + - 타입 선택 (product, board, file, custom) + - 부모 카테고리 선택 (계층 구조) + - 정렬 순서 설정 + - 활성/비활성 +- **권한:** `settings.categories.create` + +#### 1.3 카테고리 수정 +- **경로:** `/mng/settings/categories/{id}/edit` +- **기능:** + - 기본 정보 수정 + - 부모 카테고리 변경 + - 정렬 순서 변경 +- **권한:** `settings.categories.update` + +#### 1.4 카테고리 삭제 +- **경로:** `/mng/settings/categories/{id}` +- **기능:** + - 하위 카테고리 확인 (있으면 삭제 불가) + - 사용 중인 항목 확인 (제품, 게시물 등) + - Soft Delete +- **권한:** `settings.categories.delete` + +### DB 스키마 + +```sql +CREATE TABLE categories ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '소속 테넌트', + parent_id BIGINT UNSIGNED NULL COMMENT '부모 카테고리 ID', + type ENUM('product', 'board', 'file', 'custom') DEFAULT 'custom' COMMENT '카테고리 타입', + name VARCHAR(255) NOT NULL COMMENT '카테고리명', + slug VARCHAR(255) NOT NULL COMMENT 'URL 슬러그', + description TEXT NULL COMMENT '설명', + sort_order INT DEFAULT 0 COMMENT '정렬 순서', + is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부', + meta_data JSON NULL COMMENT '추가 메타데이터', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_parent_id (parent_id), + INDEX idx_type (type), + INDEX idx_slug (slug), + INDEX idx_sort_order (sort_order), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/settings/categories` | 카테고리 목록 (트리 구조) | - | +| GET | `/mng/settings/categories/create` | 카테고리 생성 폼 | - | +| POST | `/mng/settings/categories` | 카테고리 생성 | `StoreCategoryRequest` | +| GET | `/mng/settings/categories/{id}/edit` | 카테고리 수정 폼 | - | +| PUT | `/mng/settings/categories/{id}` | 카테고리 수정 | `UpdateCategoryRequest` | +| DELETE | `/mng/settings/categories/{id}` | 카테고리 삭제 | - | +| POST | `/mng/settings/categories/reorder` | 드래그앤드롭 정렬 | `ReorderCategoryRequest` | + +### Service 클래스 + +```php +// app/Services/CategoryService.php +class CategoryService +{ + public function getTree(string $type = null): Collection; + public function list(array $filters): Collection; + public function find(int $id): Category; + public function create(array $data): Category; + public function update(Category $category, array $data): Category; + public function delete(Category $category): bool; + public function reorder(array $order): bool; + public function getDescendants(Category $category): Collection; + public function canDelete(Category $category): bool; // 하위/사용 확인 +} +``` + +### UI/UX 와이어프레임 (텍스트) + +``` +┌─────────────────────────────────────────────────────────┐ +│ 카테고리 관리 [+ 새 카테고리]│ +├─────────────────────────────────────────────────────────┤ +│ [타입: 전체 ▼] [검색: 카테고리명] [검색] │ +├─────────────────────────────────────────────────────────┤ +│ ▼ 제품 카테고리 (Product) [수정][삭제] │ +│ ├─ ▶ 전자제품 [수정][삭제] │ +│ ├─ ▼ 의류 [수정][삭제] │ +│ │ ├─ 상의 [수정][삭제] │ +│ │ └─ 하의 [수정][삭제] │ +│ └─ ▶ 식품 [수정][삭제] │ +│ │ +│ ▼ 게시판 카테고리 (Board) [수정][삭제] │ +│ ├─ 공지사항 [수정][삭제] │ +│ ├─ FAQ [수정][삭제] │ +│ └─ 자료실 [수정][삭제] │ +└─────────────────────────────────────────────────────────┘ + +* 드래그 앤 드롭으로 순서 변경 가능 (Alpine.js + Sortable.js) +``` + +### 개발 체크리스트 + +- [ ] `Category` 모델 작성 (BelongsToTenant, 계층 구조) +- [ ] `CategoryService` 클래스 작성 +- [ ] FormRequest 작성 (중복 체크, 순환 참조 방지) +- [ ] `CategoryController` 작성 +- [ ] Blade 템플릿 작성 (트리 뷰, 재귀 렌더링) +- [ ] Alpine.js + Sortable.js 드래그앤드롭 구현 +- [ ] 계층 구조 재귀 쿼리 최적화 (Nested Set 또는 Closure Table) +- [ ] 삭제 전 사용 여부 체크 로직 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 (계층 구조, 정렬) + +--- + +## 2️⃣ 설정 - 관리자 계정 관리 + +### 기능 목록 + +#### 2.1 관리자 목록 조회 +- **경로:** `/mng/settings/admins` +- **기능:** + - 슈퍼관리자 목록 (일반 회원과 구분) + - 검색 (이름, 이메일) + - 필터 (활성/비활성, 권한 레벨) + - 정렬 (생성일, 이름) +- **권한:** `settings.admins.index` (슈퍼관리자만) + +#### 2.2 관리자 생성 +- **경로:** `/mng/settings/admins/create` +- **기능:** + - 기본 정보 입력 + - 권한 레벨 선택 (super_admin, admin) + - 관리 범위 설정 (전체 테넌트 or 특정 테넌트) +- **권한:** `settings.admins.create` + +#### 2.3 관리자 수정 +- **경로:** `/mng/settings/admins/{id}/edit` +- **기능:** + - 기본 정보 수정 + - 권한 레벨 변경 + - 활성/비활성 전환 +- **권한:** `settings.admins.update` + +#### 2.4 관리자 삭제 +- **경로:** `/mng/settings/admins/{id}` +- **기능:** + - Soft Delete + - 본인 계정 삭제 방지 + - 최소 1명 슈퍼관리자 유지 체크 +- **권한:** `settings.admins.delete` + +### DB 스키마 + +```sql +-- users 테이블 확장 (추가 컬럼) +ALTER TABLE users ADD COLUMN is_super_admin BOOLEAN DEFAULT FALSE COMMENT '슈퍼관리자 여부'; +ALTER TABLE users ADD COLUMN admin_level ENUM('user', 'admin', 'super_admin') DEFAULT 'user' COMMENT '관리자 레벨'; +ALTER TABLE users ADD COLUMN accessible_tenants JSON NULL COMMENT '접근 가능한 테넌트 ID 목록 (super_admin은 전체)'; + +-- 또는 별도 테이블 생성 (선택적) +CREATE TABLE admins ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT UNSIGNED UNIQUE NOT NULL COMMENT '사용자 ID', + admin_level ENUM('admin', 'super_admin') DEFAULT 'admin' COMMENT '관리자 레벨', + accessible_tenants JSON NULL COMMENT '접근 가능한 테넌트 (null = 전체)', + permissions JSON NULL COMMENT '추가 권한', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +**권장 방식:** `users` 테이블에 `is_super_admin`, `admin_level` 컬럼 추가 (간결함) + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/settings/admins` | 관리자 목록 | - | +| GET | `/mng/settings/admins/create` | 관리자 생성 폼 | - | +| POST | `/mng/settings/admins` | 관리자 생성 | `StoreAdminRequest` | +| GET | `/mng/settings/admins/{id}/edit` | 관리자 수정 폼 | - | +| PUT | `/mng/settings/admins/{id}` | 관리자 수정 | `UpdateAdminRequest` | +| DELETE | `/mng/settings/admins/{id}` | 관리자 삭제 | - | + +### Service 클래스 + +```php +// app/Services/AdminService.php +class AdminService +{ + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): User; + public function create(array $data): User; + public function update(User $admin, array $data): User; + public function delete(User $admin): bool; + public function grantSuperAdmin(User $user): bool; + public function revokeSuperAdmin(User $user): bool; + public function canDelete(User $admin): bool; // 최소 1명 체크 +} +``` + +### 개발 체크리스트 + +- [ ] `users` 테이블 마이그레이션 (컬럼 추가) +- [ ] `User` 모델에 `isSuperAdmin()`, `isAdmin()` 메서드 추가 +- [ ] `AdminService` 클래스 작성 +- [ ] FormRequest 작성 (본인 삭제 방지, 최소 1명 체크) +- [ ] `AdminController` 작성 +- [ ] Blade 템플릿 작성 +- [ ] 미들웨어 작성 (`EnsureSuperAdmin`) +- [ ] 권한 체크 로직 구현 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 🎯 Phase 2 완료 조건 + +### 기능 완성도 +- [ ] 카테고리 계층 구조 완벽 동작 +- [ ] 드래그앤드롭 정렬 기능 동작 +- [ ] 관리자 계정 생성/수정/삭제 동작 +- [ ] 슈퍼관리자 권한 체크 정상 작동 + +### 코드 품질 +- [ ] Service-First 패턴 준수 +- [ ] FormRequest 검증 구현 +- [ ] BelongsToTenant trait 적용 (Category) +- [ ] i18n 키 사용 +- [ ] Pint 포맷팅 통과 +- [ ] PHPStan Level 5+ 통과 + +### 데이터 무결성 +- [ ] 카테고리 순환 참조 방지 +- [ ] 하위 카테고리 있을 때 삭제 방지 +- [ ] 최소 1명 슈퍼관리자 유지 +- [ ] Soft Delete 동작 확인 + +### 테스트 +- [ ] CategoryService 유닛 테스트 +- [ ] AdminService 유닛 테스트 +- [ ] 계층 구조 재귀 테스트 +- [ ] 권한 체크 테스트 + +--- + +## 📚 다음 단계 (Phase 3) + +Phase 2가 완료되면 **Phase 3: 비즈니스 핵심 기능**으로 진행합니다. + +**Phase 3 포함 기능:** +- 영업관리 (Sales Management) +- 견적서관리 (Quotation Management) +- 전자결재관리 (Approval Management) + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code +**버전:** 1.0.0 \ No newline at end of file diff --git a/docs/03_PHASE3_BUSINESS_CORE.md b/docs/03_PHASE3_BUSINESS_CORE.md new file mode 100644 index 00000000..13e7d303 --- /dev/null +++ b/docs/03_PHASE3_BUSINESS_CORE.md @@ -0,0 +1,491 @@ +# Phase 3: 비즈니스 핵심 기능 + +**기간:** 2-3주 +**우선순위:** 최고 (실제 비즈니스 가치 창출) +**의존성:** Phase 1 (회원, 거래처), Phase 2 (카테고리) + +## 📋 Phase 개요 + +실제 비즈니스 가치를 창출하는 핵심 기능을 구현합니다. + +**포함 기능:** +1. 영업관리 (Sales Management) +2. 견적서관리 (Quotation Management) +3. 전자결재관리 (Approval Management) + +--- + +## 1️⃣ 영업관리 (Sales Management) + +### 기능 목록 + +#### 1.1 영업 기회 목록 조회 +- **경로:** `/mng/sales` +- **기능:** + - 칸반 보드 뷰 (파이프라인 단계별) + - 리스트 뷰 (테이블 형식) + - 검색 (거래처명, 담당자, 제목) + - 필터 (단계, 담당 영업, 예상 매출) + - 정렬 (생성일, 예상 매출, 마감 예정일) +- **권한:** `sales.index` + +#### 1.2 영업 기회 상세 조회 +- **경로:** `/mng/sales/{id}` +- **기능:** + - 기본 정보 (제목, 거래처, 예상 매출) + - 파이프라인 단계 (Lead → Qualified → Proposal → Negotiation → Won/Lost) + - 활동 이력 (상담, 미팅, 통화) + - 관련 견적서 + - 메모/코멘트 +- **권한:** `sales.show` + +#### 1.3 영업 기회 생성 +- **경로:** `/mng/sales/create` +- **기능:** + - 제목, 거래처 선택 + - 예상 매출, 마감 예정일 + - 초기 단계 설정 + - 담당 영업 할당 +- **권한:** `sales.create` + +#### 1.4 영업 기회 수정 +- **경로:** `/mng/sales/{id}/edit` +- **기능:** + - 기본 정보 수정 + - 단계 변경 (드래그앤드롭 또는 드롭다운) + - 담당자 변경 + - Win/Loss 사유 기록 +- **권한:** `sales.update` + +#### 1.5 활동 이력 추가 +- **경로:** `/mng/sales/{id}/activities` +- **기능:** + - 활동 유형 (통화, 미팅, 이메일, 방문) + - 활동 내용, 날짜/시간 + - 다음 액션 계획 +- **권한:** `sales.activities.create` + +### DB 스키마 + +```sql +CREATE TABLE sales_opportunities ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '소속 테넌트', + client_id BIGINT UNSIGNED NOT NULL COMMENT '거래처 ID', + title VARCHAR(255) NOT NULL COMMENT '영업 기회 제목', + description TEXT NULL COMMENT '상세 설명', + stage ENUM('lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost') DEFAULT 'lead' COMMENT '파이프라인 단계', + expected_revenue DECIMAL(15,2) DEFAULT 0 COMMENT '예상 매출', + probability INT DEFAULT 50 COMMENT '성공 확률 (0-100)', + expected_close_date DATE NULL COMMENT '마감 예정일', + actual_close_date DATE NULL COMMENT '실제 마감일', + assigned_user_id BIGINT UNSIGNED NULL COMMENT '담당 영업', + win_loss_reason TEXT NULL COMMENT 'Win/Loss 사유', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_client_id (client_id), + INDEX idx_stage (stage), + INDEX idx_assigned_user_id (assigned_user_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE, + FOREIGN KEY (assigned_user_id) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE sales_activities ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + opportunity_id BIGINT UNSIGNED NOT NULL COMMENT '영업 기회 ID', + user_id BIGINT UNSIGNED NOT NULL COMMENT '활동 수행자', + activity_type ENUM('call', 'meeting', 'email', 'visit', 'note') DEFAULT 'note' COMMENT '활동 유형', + subject VARCHAR(255) NOT NULL COMMENT '제목', + description TEXT NULL COMMENT '내용', + activity_date DATETIME NOT NULL COMMENT '활동 일시', + next_action TEXT NULL COMMENT '다음 액션 계획', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_opportunity_id (opportunity_id), + INDEX idx_user_id (user_id), + INDEX idx_activity_date (activity_date), + FOREIGN KEY (opportunity_id) REFERENCES sales_opportunities(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/sales` | 영업 기회 목록 (칸반/리스트) | - | +| GET | `/mng/sales/{id}` | 영업 기회 상세 | - | +| POST | `/mng/sales` | 영업 기회 생성 | `StoreSalesRequest` | +| PUT | `/mng/sales/{id}` | 영업 기회 수정 | `UpdateSalesRequest` | +| DELETE | `/mng/sales/{id}` | 영업 기회 삭제 | - | +| POST | `/mng/sales/{id}/activities` | 활동 이력 추가 | `StoreActivityRequest` | +| PUT | `/mng/sales/{id}/stage` | 단계 변경 | `UpdateStageRequest` | + +### Service 클래스 + +```php +// app/Services/SalesService.php +class SalesService +{ + public function list(array $filters, string $view = 'list'): Collection|LengthAwarePaginator; + public function find(int $id): SalesOpportunity; + public function create(array $data): SalesOpportunity; + public function update(SalesOpportunity $opportunity, array $data): SalesOpportunity; + public function delete(SalesOpportunity $opportunity): bool; + public function changeStage(SalesOpportunity $opportunity, string $stage): bool; + public function addActivity(SalesOpportunity $opportunity, array $activityData): SalesActivity; + public function getPipelineStats(): array; // 단계별 통계 +} +``` + +### 개발 체크리스트 + +- [ ] `SalesOpportunity`, `SalesActivity` 모델 작성 +- [ ] `SalesService` 클래스 작성 +- [ ] FormRequest 작성 +- [ ] `SalesController` 작성 +- [ ] 칸반 보드 UI (Alpine.js + Drag & Drop) +- [ ] 리스트 뷰 UI (테이블) +- [ ] 파이프라인 통계 차트 (Chart.js) +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 2️⃣ 견적서관리 (Quotation Management) + +### 기능 목록 + +#### 2.1 견적서 목록 조회 +- **경로:** `/mng/quotations` +- **기능:** + - 페이지네이션 + - 검색 (견적서 번호, 거래처명, 제목) + - 필터 (상태, 날짜 범위) + - 정렬 (생성일, 총액) +- **권한:** `quotations.index` + +#### 2.2 견적서 상세 조회 +- **경로:** `/mng/quotations/{id}` +- **기능:** + - 견적서 정보 (번호, 날짜, 유효기간) + - 거래처 정보 + - 품목 목록 (제품, 수량, 단가, 금액) + - 총액, 부가세, 합계 + - PDF 미리보기 +- **권한:** `quotations.show` + +#### 2.3 견적서 생성 +- **경로:** `/mng/quotations/create` +- **기능:** + - 거래처 선택 + - 품목 추가 (API에서 제품 조회) + - 수량, 단가 입력 + - 할인, 부가세 계산 + - 템플릿 선택 + - 메모/비고 +- **권한:** `quotations.create` + +#### 2.4 견적서 수정 +- **경로:** `/mng/quotations/{id}/edit` +- **기능:** + - 품목 추가/삭제/수정 + - 할인율 변경 + - 유효기간 변경 +- **권한:** `quotations.update` + +#### 2.5 견적서 PDF 출력 +- **경로:** `/mng/quotations/{id}/pdf` +- **기능:** + - 템플릿 기반 PDF 생성 (DomPDF 또는 Laravel Snappy) + - 다운로드 또는 이메일 발송 +- **권한:** `quotations.pdf` + +#### 2.6 견적서 승인 워크플로우 +- **경로:** `/mng/quotations/{id}/approve` +- **기능:** + - 상태 변경 (Draft → Pending → Approved → Rejected) + - 승인자 지정 + - 승인/반려 사유 +- **권한:** `quotations.approve` + +### DB 스키마 + +```sql +CREATE TABLE quotations ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '소속 테넌트', + client_id BIGINT UNSIGNED NOT NULL COMMENT '거래처 ID', + sales_opportunity_id BIGINT UNSIGNED NULL COMMENT '연관 영업 기회', + quotation_number VARCHAR(50) UNIQUE NOT NULL COMMENT '견적서 번호 (자동 생성)', + title VARCHAR(255) NOT NULL COMMENT '견적서 제목', + issue_date DATE NOT NULL COMMENT '발행일', + valid_until DATE NOT NULL COMMENT '유효기간', + status ENUM('draft', 'pending', 'approved', 'rejected', 'sent') DEFAULT 'draft' COMMENT '상태', + subtotal DECIMAL(15,2) DEFAULT 0 COMMENT '소계', + discount_rate DECIMAL(5,2) DEFAULT 0 COMMENT '할인율 (%)', + discount_amount DECIMAL(15,2) DEFAULT 0 COMMENT '할인 금액', + tax_amount DECIMAL(15,2) DEFAULT 0 COMMENT '부가세', + total_amount DECIMAL(15,2) DEFAULT 0 COMMENT '총액', + notes TEXT NULL COMMENT '비고', + template_id BIGINT UNSIGNED NULL COMMENT '템플릿 ID', + created_by BIGINT UNSIGNED NOT NULL COMMENT '생성자', + approved_by BIGINT UNSIGNED NULL COMMENT '승인자', + approved_at TIMESTAMP NULL COMMENT '승인 일시', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_client_id (client_id), + INDEX idx_quotation_number (quotation_number), + INDEX idx_status (status), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE, + FOREIGN KEY (sales_opportunity_id) REFERENCES sales_opportunities(id) ON DELETE SET NULL, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE quotation_items ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + quotation_id BIGINT UNSIGNED NOT NULL COMMENT '견적서 ID', + product_id VARCHAR(100) NULL COMMENT '제품 ID (API 참조)', + product_name VARCHAR(255) NOT NULL COMMENT '제품명', + description TEXT NULL COMMENT '설명', + quantity DECIMAL(10,2) NOT NULL COMMENT '수량', + unit_price DECIMAL(15,2) NOT NULL COMMENT '단가', + discount_rate DECIMAL(5,2) DEFAULT 0 COMMENT '할인율 (%)', + discount_amount DECIMAL(15,2) DEFAULT 0 COMMENT '할인 금액', + subtotal DECIMAL(15,2) NOT NULL COMMENT '소계', + sort_order INT DEFAULT 0 COMMENT '정렬 순서', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_quotation_id (quotation_id), + FOREIGN KEY (quotation_id) REFERENCES quotations(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/quotations` | 견적서 목록 | - | +| GET | `/mng/quotations/{id}` | 견적서 상세 | - | +| POST | `/mng/quotations` | 견적서 생성 | `StoreQuotationRequest` | +| PUT | `/mng/quotations/{id}` | 견적서 수정 | `UpdateQuotationRequest` | +| DELETE | `/mng/quotations/{id}` | 견적서 삭제 | - | +| GET | `/mng/quotations/{id}/pdf` | PDF 출력 | - | +| POST | `/mng/quotations/{id}/approve` | 승인/반려 | `ApproveQuotationRequest` | +| POST | `/mng/quotations/{id}/send` | 이메일 발송 | `SendQuotationRequest` | + +### Service 클래스 + +```php +// app/Services/QuotationService.php +class QuotationService +{ + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): Quotation; + public function create(array $data): Quotation; + public function update(Quotation $quotation, array $data): Quotation; + public function delete(Quotation $quotation): bool; + public function generatePDF(Quotation $quotation): string; // PDF 경로 + public function approve(Quotation $quotation, int $approverId): bool; + public function reject(Quotation $quotation, string $reason): bool; + public function send(Quotation $quotation, string $email): bool; + public function calculateTotals(array $items): array; // 금액 계산 + public function generateQuotationNumber(): string; // QT-20251121-0001 +} +``` + +### 개발 체크리스트 + +- [ ] `Quotation`, `QuotationItem` 모델 작성 +- [ ] `QuotationService` 클래스 작성 +- [ ] FormRequest 작성 +- [ ] `QuotationController` 작성 +- [ ] 품목 추가 UI (Alpine.js 동적 행 추가) +- [ ] PDF 생성 기능 (DomPDF) +- [ ] 템플릿 시스템 연동 +- [ ] 견적서 번호 자동 생성 로직 +- [ ] API 서버 제품 조회 연동 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 3️⃣ 전자결재관리 (Approval Management) + +### 기능 목록 + +#### 3.1 결재 문서 목록 조회 +- **경로:** `/mng/approvals` +- **기능:** + - 내 결재 대기 문서 + - 내가 요청한 문서 + - 완료된 문서 + - 검색 (제목, 요청자) + - 필터 (상태, 문서 유형) +- **권한:** `approvals.index` + +#### 3.2 결재 문서 상세 조회 +- **경로:** `/mng/approvals/{id}` +- **기능:** + - 문서 정보 (제목, 내용, 첨부파일) + - 결재선 (요청자 → 결재자1 → 결재자2 → ...) + - 결재 이력 (승인/반려 사유, 일시) + - 현재 결재 단계 +- **권한:** `approvals.show` + +#### 3.3 결재 요청 +- **경로:** `/mng/approvals/create` +- **기능:** + - 문서 유형 선택 (휴가, 지출, 구매 등) + - 템플릿 불러오기 + - 내용 작성 + - 결재선 설정 (순차/병렬) + - 첨부파일 업로드 +- **권한:** `approvals.create` + +#### 3.4 결재 승인/반려 +- **경로:** `/mng/approvals/{id}/approve` 또는 `/reject` +- **기능:** + - 승인/반려 선택 + - 코멘트 작성 + - 다음 결재자에게 알림 +- **권한:** `approvals.approve` + +#### 3.5 결재선 설정 +- **경로:** `/mng/approvals/approval-lines` +- **기능:** + - 결재선 템플릿 관리 + - 부서별 기본 결재선 + - 순차/병렬 결재 설정 +- **권한:** `approvals.lines.manage` + +### DB 스키마 + +```sql +CREATE TABLE approvals ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '소속 테넌트', + document_type VARCHAR(100) NOT NULL COMMENT '문서 유형 (leave, expense, purchase 등)', + title VARCHAR(255) NOT NULL COMMENT '제목', + content TEXT NOT NULL COMMENT '내용', + requester_id BIGINT UNSIGNED NOT NULL COMMENT '요청자', + status ENUM('pending', 'in_progress', 'approved', 'rejected', 'cancelled') DEFAULT 'pending' COMMENT '상태', + current_step INT DEFAULT 1 COMMENT '현재 결재 단계', + total_steps INT NOT NULL COMMENT '전체 결재 단계', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_requester_id (requester_id), + INDEX idx_status (status), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE approval_steps ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + approval_id BIGINT UNSIGNED NOT NULL COMMENT '결재 문서 ID', + step_order INT NOT NULL COMMENT '결재 순서', + approver_id BIGINT UNSIGNED NOT NULL COMMENT '결재자', + status ENUM('waiting', 'approved', 'rejected', 'skipped') DEFAULT 'waiting' COMMENT '상태', + comment TEXT NULL COMMENT '결재 코멘트', + approved_at TIMESTAMP NULL COMMENT '결재 일시', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_approval_id (approval_id), + INDEX idx_approver_id (approver_id), + INDEX idx_status (status), + FOREIGN KEY (approval_id) REFERENCES approvals(id) ON DELETE CASCADE, + FOREIGN KEY (approver_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/approvals` | 결재 문서 목록 | - | +| GET | `/mng/approvals/{id}` | 결재 문서 상세 | - | +| POST | `/mng/approvals` | 결재 요청 | `StoreApprovalRequest` | +| PUT | `/mng/approvals/{id}` | 결재 문서 수정 (대기 상태만) | `UpdateApprovalRequest` | +| POST | `/mng/approvals/{id}/approve` | 승인 | `ApproveRequest` | +| POST | `/mng/approvals/{id}/reject` | 반려 | `RejectRequest` | +| DELETE | `/mng/approvals/{id}` | 결재 취소 | - | + +### Service 클래스 + +```php +// app/Services/ApprovalService.php +class ApprovalService +{ + public function list(int $userId, array $filters): LengthAwarePaginator; + public function find(int $id): Approval; + public function create(array $data, array $approvers): Approval; + public function update(Approval $approval, array $data): Approval; + public function approve(Approval $approval, int $approverId, string $comment = null): bool; + public function reject(Approval $approval, int $approverId, string $reason): bool; + public function cancel(Approval $approval): bool; + public function getMyPendingApprovals(int $userId): Collection; + public function notifyNextApprover(Approval $approval): void; +} +``` + +### 개발 체크리스트 + +- [ ] `Approval`, `ApprovalStep` 모델 작성 +- [ ] `ApprovalService` 클래스 작성 +- [ ] FormRequest 작성 +- [ ] `ApprovalController` 작성 +- [ ] 결재선 UI (순차 흐름 시각화) +- [ ] 알림 시스템 연동 (이메일, 실시간 알림) +- [ ] 문서 유형별 템플릿 연동 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 🎯 Phase 3 완료 조건 + +### 기능 완성도 +- [ ] 3개 모듈 모두 CRUD 완성 +- [ ] 영업 기회 칸반 보드 동작 +- [ ] 견적서 PDF 생성 동작 +- [ ] 전자결재 승인 워크플로우 동작 + +### 코드 품질 +- [ ] Service-First 패턴 준수 +- [ ] FormRequest 검증 구현 +- [ ] BelongsToTenant trait 적용 +- [ ] i18n 키 사용 +- [ ] Pint, PHPStan 통과 + +### 비즈니스 로직 +- [ ] 영업 파이프라인 단계 전환 정상 +- [ ] 견적서 금액 계산 정확 +- [ ] 결재선 순차 승인 정상 +- [ ] 알림 발송 동작 + +### 테스트 +- [ ] Service 계층 테스트 +- [ ] 워크플로우 통합 테스트 +- [ ] PDF 생성 테스트 +- [ ] 권한 체크 테스트 + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code +**버전:** 1.0.0 \ No newline at end of file diff --git a/docs/04_PHASE4_CONTENT.md b/docs/04_PHASE4_CONTENT.md new file mode 100644 index 00000000..59ec1c43 --- /dev/null +++ b/docs/04_PHASE4_CONTENT.md @@ -0,0 +1,387 @@ +# Phase 4: 콘텐츠 관리 + +**기간:** 1-2주 +**우선순위:** 중간 (사용자 경험 향상) +**의존성:** Phase 1 (회원), Phase 2 (카테고리), Phase 3 (템플릿 참조) + +## 📋 Phase 개요 + +사용자 경험 향상을 위한 콘텐츠 관리 기능을 구현합니다. + +**포함 기능:** +1. 템플릿관리 (Template Management) +2. 게시판관리 (Board Management) - EAV 패턴 +3. 설정 - 배너/팝업 관리 + +--- + +## 1️⃣ 템플릿관리 (Template Management) + +### 기능 목록 + +#### 1.1 템플릿 목록 조회 +- **경로:** `/mng/templates` +- **기능:** + - 타입별 필터 (견적서, 계약서, 결재 문서) + - 검색 (템플릿명) + - 미리보기 +- **권한:** `templates.index` + +#### 1.2 템플릿 생성 +- **경로:** `/mng/templates/create` +- **기능:** + - 템플릿명, 타입 선택 + - HTML 에디터 (Tiptap 또는 TinyMCE) + - 변수 치환 시스템 ({{company_name}}, {{date}} 등) + - 미리보기 기능 +- **권한:** `templates.create` + +#### 1.3 템플릿 수정 +- **경로:** `/mng/templates/{id}/edit` +- **기능:** + - 내용 수정 + - 변수 추가/수정 + - 버전 관리 (히스토리) +- **권한:** `templates.update` + +### DB 스키마 + +```sql +CREATE TABLE templates ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL COMMENT '템플릿명', + type ENUM('quotation', 'contract', 'approval', 'email', 'custom') NOT NULL COMMENT '템플릿 타입', + content TEXT NOT NULL COMMENT 'HTML 내용', + variables JSON NULL COMMENT '사용 가능한 변수 목록', + is_default BOOLEAN DEFAULT FALSE COMMENT '기본 템플릿 여부', + version INT DEFAULT 1 COMMENT '버전', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_type (type), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### Service 클래스 + +```php +// app/Services/TemplateService.php +class TemplateService +{ + public function list(array $filters): Collection; + public function find(int $id): Template; + public function create(array $data): Template; + public function update(Template $template, array $data): Template; + public function delete(Template $template): bool; + public function render(Template $template, array $variables): string; // 변수 치환 + public function preview(Template $template, array $sampleData): string; +} +``` + +### 개발 체크리스트 + +- [ ] `Template` 모델 작성 +- [ ] `TemplateService` 클래스 작성 +- [ ] 변수 치환 로직 구현 (정규식) +- [ ] HTML 에디터 통합 (Tiptap) +- [ ] 미리보기 기능 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 2️⃣ 게시판관리 (Board Management) - EAV 패턴 + +### 기능 목록 + +#### 2.1 게시판 목록 관리 +- **경로:** `/mng/boards` +- **기능:** + - 게시판 생성/수정/삭제 + - 게시판 설정 (공지사항, FAQ, 자료실 등) + - 필드 구성 (EAV 동적 필드) +- **권한:** `boards.manage` + +#### 2.2 게시물 목록 조회 +- **경로:** `/mng/boards/{board}/posts` +- **기능:** + - 페이지네이션 + - 검색 (제목, 내용, 작성자) + - 필터 (카테고리, 날짜) + - 정렬 (최신순, 조회순) +- **권한:** `boards.posts.index` + +#### 2.3 게시물 상세 조회 +- **경로:** `/mng/boards/{board}/posts/{id}` +- **기능:** + - 제목, 내용, 첨부파일 + - 동적 필드 표시 (EAV) + - 댓글 목록 + - 조회수 증가 +- **권한:** `boards.posts.show` + +#### 2.4 게시물 작성 +- **경로:** `/mng/boards/{board}/posts/create` +- **기능:** + - 제목, 내용 (에디터) + - 카테고리 선택 + - 동적 필드 입력 (게시판별 설정) + - 첨부파일 업로드 + - 공지사항 설정 +- **권한:** `boards.posts.create` + +### DB 스키마 + +```sql +-- 게시판 설정 +CREATE TABLE board_settings ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL COMMENT '게시판명', + slug VARCHAR(100) UNIQUE NOT NULL COMMENT 'URL 슬러그', + description TEXT NULL, + allow_comments BOOLEAN DEFAULT TRUE, + allow_attachments BOOLEAN DEFAULT TRUE, + require_approval BOOLEAN DEFAULT FALSE COMMENT '작성 시 승인 필요', + custom_fields JSON NULL COMMENT '동적 필드 설정 (EAV)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 게시물 +CREATE TABLE posts ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + board_id BIGINT UNSIGNED NOT NULL, + category_id BIGINT UNSIGNED NULL, + user_id BIGINT UNSIGNED NOT NULL COMMENT '작성자', + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + is_notice BOOLEAN DEFAULT FALSE, + is_approved BOOLEAN DEFAULT TRUE, + view_count INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_board_id (board_id), + INDEX idx_category_id (category_id), + INDEX idx_user_id (user_id), + FULLTEXT idx_search (title, content), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (board_id) REFERENCES board_settings(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- EAV 동적 필드 값 +CREATE TABLE post_custom_field_values ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + post_id BIGINT UNSIGNED NOT NULL, + field_name VARCHAR(100) NOT NULL COMMENT '필드명', + field_value TEXT NULL COMMENT '필드값', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_post_id (post_id), + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 댓글 +CREATE TABLE post_comments ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + post_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + parent_id BIGINT UNSIGNED NULL COMMENT '대댓글', + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_post_id (post_id), + INDEX idx_user_id (user_id), + INDEX idx_parent_id (parent_id), + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES post_comments(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### Service 클래스 + +```php +// app/Services/BoardService.php +class BoardService +{ + public function listBoards(): Collection; + public function createBoard(array $data): BoardSetting; + public function updateBoard(BoardSetting $board, array $data): BoardSetting; + + public function listPosts(BoardSetting $board, array $filters): LengthAwarePaginator; + public function findPost(int $id): Post; + public function createPost(BoardSetting $board, array $data): Post; + public function updatePost(Post $post, array $data): Post; + public function deletePost(Post $post): bool; + + public function addComment(Post $post, array $data): PostComment; + public function getComments(Post $post): Collection; +} +``` + +### 개발 체크리스트 + +- [ ] EAV 패턴 구현 (동적 필드) +- [ ] `BoardSetting`, `Post`, `PostComment` 모델 작성 +- [ ] `BoardService` 클래스 작성 +- [ ] 게시판별 동적 필드 렌더링 +- [ ] 에디터 통합 (Tiptap) +- [ ] 파일 첨부 기능 +- [ ] 댓글/대댓글 UI +- [ ] 전문 검색 (FULLTEXT) +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +**중요:** `CLAUDE.md`의 **EAV + Atomic Design 전략** 참조하여 구현 + +--- + +## 3️⃣ 설정 - 배너/팝업 관리 + +### 기능 목록 + +#### 3.1 배너 관리 +- **경로:** `/mng/settings/banners` +- **기능:** + - 배너 생성/수정/삭제 + - 이미지 업로드 + - 링크 URL 설정 + - 노출 기간, 위치 설정 + - 드래그앤드롭 정렬 +- **권한:** `settings.banners.manage` + +#### 3.2 팝업 관리 +- **경로:** `/mng/settings/popups` +- **기능:** + - 팝업 생성/수정/삭제 + - 내용 편집 (HTML) + - 노출 기간, 타겟 (전체/특정 테넌트) + - 오늘 하루 보지 않기 설정 +- **권한:** `settings.popups.manage` + +### DB 스키마 + +```sql +CREATE TABLE banners ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL COMMENT '특정 테넌트 or NULL (전체)', + title VARCHAR(255) NOT NULL, + image_url VARCHAR(500) NOT NULL COMMENT '배너 이미지', + link_url VARCHAR(500) NULL COMMENT '클릭 시 이동 URL', + position ENUM('main_top', 'main_middle', 'main_bottom', 'sidebar') DEFAULT 'main_top', + display_from DATETIME NOT NULL, + display_until DATETIME NOT NULL, + sort_order INT DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_position (position), + INDEX idx_is_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE popups ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL COMMENT 'HTML 내용', + width INT DEFAULT 600, + height INT DEFAULT 400, + display_from DATETIME NOT NULL, + display_until DATETIME NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_is_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### Service 클래스 + +```php +// app/Services/BannerService.php +class BannerService +{ + public function list(): Collection; + public function create(array $data): Banner; + public function update(Banner $banner, array $data): Banner; + public function delete(Banner $banner): bool; + public function reorder(array $order): bool; + public function getActiveBanners(string $position, int $tenantId = null): Collection; +} + +// app/Services/PopupService.php +class PopupService +{ + public function list(): Collection; + public function create(array $data): Popup; + public function update(Popup $popup, array $data): Popup; + public function delete(Popup $popup): bool; + public function getActivePopups(int $tenantId = null): Collection; +} +``` + +### 개발 체크리스트 + +- [ ] `Banner`, `Popup` 모델 작성 +- [ ] Service 클래스 작성 +- [ ] 이미지 업로드 기능 (파일 저장소) +- [ ] 드래그앤드롭 정렬 UI +- [ ] 노출 기간 검증 로직 +- [ ] 프론트엔드 표시 기능 (MNG 메인) +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 🎯 Phase 4 완료 조건 + +### 기능 완성도 +- [ ] 템플릿 변수 치환 동작 +- [ ] 게시판 EAV 동적 필드 동작 +- [ ] 배너/팝업 노출 정상 작동 + +### 코드 품질 +- [ ] Service-First, FormRequest 준수 +- [ ] EAV 패턴 올바른 구현 +- [ ] i18n 키 사용 +- [ ] Pint, PHPStan 통과 + +### 데이터 무결성 +- [ ] 게시판별 동적 필드 격리 +- [ ] 노출 기간 검증 +- [ ] 파일 첨부 안전성 + +### 테스트 +- [ ] EAV 패턴 테스트 +- [ ] 템플릿 렌더링 테스트 +- [ ] 배너/팝업 노출 로직 테스트 + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code +**버전:** 1.0.0 \ No newline at end of file diff --git a/docs/05_PHASE5_REVENUE.md b/docs/05_PHASE5_REVENUE.md new file mode 100644 index 00000000..5bf3378c --- /dev/null +++ b/docs/05_PHASE5_REVENUE.md @@ -0,0 +1,447 @@ +# Phase 5: 수익 관리 + +**기간:** 2주 +**우선순위:** 높음 (SaaS 비즈니스 수익 관리) +**의존성:** Phase 1 (테넌트관리) + +## 📋 Phase 개요 + +SaaS 비즈니스 모델의 수익 관리 시스템을 구축합니다. + +**포함 기능:** +1. 구독관리 (Subscription Management) +2. 결제관리 (Payment Management) +3. 환불관리 (Refund Management) + +--- + +## 1️⃣ 구독관리 (Subscription Management) + +### 기능 목록 + +#### 1.1 구독 플랜 관리 +- **경로:** `/mng/subscriptions/plans` +- **기능:** + - 플랜 생성/수정/삭제 (Free, Basic, Pro, Enterprise) + - 가격, 기능 제한 설정 + - 사용자 수, 저장 용량 한도 + - 월간/연간 결제 옵션 +- **권한:** `subscriptions.plans.manage` + +#### 1.2 테넌트 구독 목록 +- **경로:** `/mng/subscriptions` +- **기능:** + - 전체 테넌트 구독 현황 + - 검색 (테넌트명, 플랜) + - 필터 (플랜, 상태, 만료 임박) + - 정렬 (만료일, 가입일) +- **권한:** `subscriptions.index` + +#### 1.3 구독 상세 조회 +- **경로:** `/mng/subscriptions/{id}` +- **기능:** + - 현재 플랜 정보 + - 구독 이력 (업그레이드/다운그레이드) + - 사용량 통계 (사용자 수, 저장 용량) + - 다음 결제 예정일 +- **권한:** `subscriptions.show` + +#### 1.4 구독 플랜 변경 +- **경로:** `/mng/subscriptions/{id}/change-plan` +- **기능:** + - 플랜 업그레이드/다운그레이드 + - 즉시 적용 or 다음 결제일 적용 + - 차액 정산 (프로레이션) +- **권한:** `subscriptions.change-plan` + +#### 1.5 구독 해지 +- **경로:** `/mng/subscriptions/{id}/cancel` +- **기능:** + - 즉시 해지 or 기간 만료 후 해지 + - 해지 사유 기록 + - 데이터 백업 안내 +- **권한:** `subscriptions.cancel` + +### DB 스키마 + +```sql +CREATE TABLE subscription_plans ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(100) NOT NULL COMMENT '플랜명 (Free, Basic, Pro, Enterprise)', + slug VARCHAR(50) UNIQUE NOT NULL, + description TEXT NULL, + price_monthly DECIMAL(10,2) NOT NULL COMMENT '월 가격', + price_yearly DECIMAL(10,2) NOT NULL COMMENT '연 가격', + max_users INT DEFAULT 10 COMMENT '최대 사용자 수', + storage_limit BIGINT DEFAULT 1073741824 COMMENT '저장 용량 (바이트)', + features JSON NULL COMMENT '플랜 기능 목록', + 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_slug (slug), + INDEX idx_is_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE subscriptions ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED UNIQUE NOT NULL, + plan_id BIGINT UNSIGNED NOT NULL, + status ENUM('active', 'past_due', 'cancelled', 'expired') DEFAULT 'active', + billing_cycle ENUM('monthly', 'yearly') DEFAULT 'monthly', + current_period_start DATE NOT NULL, + current_period_end DATE NOT NULL, + next_billing_date DATE NOT NULL, + cancelled_at TIMESTAMP NULL, + cancel_reason TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_plan_id (plan_id), + INDEX idx_status (status), + INDEX idx_next_billing_date (next_billing_date), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (plan_id) REFERENCES subscription_plans(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE subscription_history ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + subscription_id BIGINT UNSIGNED NOT NULL, + old_plan_id BIGINT UNSIGNED NULL, + new_plan_id BIGINT UNSIGNED NOT NULL, + action ENUM('upgrade', 'downgrade', 'renew', 'cancel') NOT NULL, + reason TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_subscription_id (subscription_id), + FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/subscriptions/plans` | 플랜 목록 | - | +| POST | `/mng/subscriptions/plans` | 플랜 생성 | `StorePlanRequest` | +| PUT | `/mng/subscriptions/plans/{id}` | 플랜 수정 | `UpdatePlanRequest` | +| GET | `/mng/subscriptions` | 구독 목록 | - | +| GET | `/mng/subscriptions/{id}` | 구독 상세 | - | +| POST | `/mng/subscriptions/{id}/change-plan` | 플랜 변경 | `ChangePlanRequest` | +| POST | `/mng/subscriptions/{id}/cancel` | 구독 해지 | `CancelSubscriptionRequest` | + +### Service 클래스 + +```php +// app/Services/SubscriptionService.php +class SubscriptionService +{ + public function listPlans(): Collection; + public function createPlan(array $data): SubscriptionPlan; + public function updatePlan(SubscriptionPlan $plan, array $data): SubscriptionPlan; + + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): Subscription; + public function create(int $tenantId, int $planId, string $billingCycle): Subscription; + public function changePlan(Subscription $subscription, int $newPlanId, bool $immediate = false): Subscription; + public function cancel(Subscription $subscription, string $reason, bool $immediate = false): bool; + public function renew(Subscription $subscription): bool; + + public function getUsageStats(int $tenantId): array; + public function checkLimits(int $tenantId): array; // 사용자 수, 저장 용량 체크 + public function calculateProration(Subscription $subscription, int $newPlanId): float; +} +``` + +### 개발 체크리스트 + +- [ ] `SubscriptionPlan`, `Subscription`, `SubscriptionHistory` 모델 작성 +- [ ] `SubscriptionService` 클래스 작성 +- [ ] 프로레이션 계산 로직 구현 +- [ ] 사용량 체크 로직 (사용자 수, 저장 용량) +- [ ] 구독 만료 자동 처리 (스케줄러) +- [ ] FormRequest 작성 +- [ ] `SubscriptionController` 작성 +- [ ] Blade 템플릿 작성 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 2️⃣ 결제관리 (Payment Management) + +### 기능 목록 + +#### 2.1 결제 내역 조회 +- **경로:** `/mng/payments` +- **기능:** + - 전체 결제 내역 목록 + - 검색 (테넌트명, 결제 번호) + - 필터 (상태, 날짜 범위, 플랜) + - 정렬 (결제일, 금액) + - 엑셀 내보내기 +- **권한:** `payments.index` + +#### 2.2 결제 상세 조회 +- **경로:** `/mng/payments/{id}` +- **기능:** + - 결제 정보 (금액, 방법, 일시) + - 구독 정보 + - 영수증 출력 + - PG사 거래 번호 +- **권한:** `payments.show` + +#### 2.3 결제 처리 +- **경로:** `/mng/payments/process` +- **기능:** + - PG 연동 (토스페이먼츠, 이니시스 등) + - 정기 결제 등록 + - 일회성 결제 + - 결제 실패 처리 +- **권한:** `payments.process` + +#### 2.4 결제 실패 관리 +- **경로:** `/mng/payments/failed` +- **기능:** + - 실패 내역 조회 + - 재시도 + - 알림 발송 +- **권한:** `payments.failed.manage` + +### DB 스키마 + +```sql +CREATE TABLE payments ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + subscription_id BIGINT UNSIGNED NOT NULL, + payment_number VARCHAR(50) UNIQUE NOT NULL COMMENT '결제 번호 (자동 생성)', + amount DECIMAL(10,2) NOT NULL COMMENT '결제 금액', + payment_method ENUM('card', 'bank_transfer', 'virtual_account', 'paypal') DEFAULT 'card', + status ENUM('pending', 'completed', 'failed', 'refunded') DEFAULT 'pending', + pg_provider VARCHAR(50) NULL COMMENT 'PG사 (toss, inicis 등)', + pg_transaction_id VARCHAR(255) NULL COMMENT 'PG사 거래 번호', + paid_at TIMESTAMP NULL COMMENT '결제 완료 일시', + failure_reason TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_subscription_id (subscription_id), + INDEX idx_payment_number (payment_number), + INDEX idx_status (status), + INDEX idx_paid_at (paid_at), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE payment_cards ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + card_number_masked VARCHAR(20) NOT NULL COMMENT '마스킹된 카드번호 (1234-****-****-5678)', + card_type VARCHAR(50) NULL COMMENT '카드사', + expiry_date VARCHAR(7) NOT NULL COMMENT 'MM/YYYY', + is_default BOOLEAN DEFAULT FALSE, + billing_key VARCHAR(255) NULL COMMENT 'PG사 빌링키 (정기결제)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/payments` | 결제 내역 목록 | - | +| GET | `/mng/payments/{id}` | 결제 상세 | - | +| POST | `/mng/payments/process` | 결제 처리 | `ProcessPaymentRequest` | +| POST | `/mng/payments/{id}/retry` | 결제 재시도 | - | +| GET | `/mng/payments/{id}/receipt` | 영수증 출력 | - | +| GET | `/mng/payments/failed` | 실패 내역 | - | + +### Service 클래스 + +```php +// app/Services/PaymentService.php +class PaymentService +{ + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): Payment; + public function process(int $tenantId, int $subscriptionId, float $amount, string $method): Payment; + public function complete(Payment $payment, string $pgTransactionId): bool; + public function fail(Payment $payment, string $reason): bool; + public function retry(Payment $payment): Payment; + public function generateReceipt(Payment $payment): string; // PDF 경로 + + public function registerCard(int $tenantId, array $cardData): PaymentCard; + public function getCards(int $tenantId): Collection; + public function processRecurring(Subscription $subscription): Payment; +} +``` + +### 개발 체크리스트 + +- [ ] `Payment`, `PaymentCard` 모델 작성 +- [ ] `PaymentService` 클래스 작성 +- [ ] PG 연동 구현 (토스페이먼츠 우선) +- [ ] 정기 결제 스케줄러 +- [ ] 결제 실패 알림 (이메일, SMS) +- [ ] 영수증 PDF 생성 +- [ ] 결제 번호 자동 생성 +- [ ] FormRequest 작성 +- [ ] 테스트 작성 (Mock PG) + +--- + +## 3️⃣ 환불관리 (Refund Management) + +### 기능 목록 + +#### 3.1 환불 요청 목록 +- **경로:** `/mng/refunds` +- **기능:** + - 전체 환불 요청 목록 + - 검색 (테넌트명, 결제 번호) + - 필터 (상태, 날짜) + - 정렬 (요청일, 금액) +- **권한:** `refunds.index` + +#### 3.2 환불 요청 상세 +- **경로:** `/mng/refunds/{id}` +- **기능:** + - 환불 요청 정보 (사유, 금액) + - 원 결제 정보 + - 환불 처리 이력 +- **권한:** `refunds.show` + +#### 3.3 환불 승인/반려 +- **경로:** `/mng/refunds/{id}/approve` 또는 `/reject` +- **기능:** + - 전액 환불 or 부분 환불 + - 승인/반려 사유 기록 + - PG 환불 API 호출 + - 알림 발송 +- **권한:** `refunds.approve` + +#### 3.4 환불 처리 +- **경로:** `/mng/refunds/{id}/process` +- **기능:** + - PG사 환불 API 연동 + - 환불 완료 처리 + - 구독 상태 업데이트 +- **권한:** `refunds.process` + +### DB 스키마 + +```sql +CREATE TABLE refunds ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + payment_id BIGINT UNSIGNED NOT NULL, + tenant_id BIGINT UNSIGNED NOT NULL, + refund_number VARCHAR(50) UNIQUE NOT NULL COMMENT '환불 번호', + refund_amount DECIMAL(10,2) NOT NULL COMMENT '환불 금액', + refund_type ENUM('full', 'partial') DEFAULT 'full', + reason TEXT NOT NULL COMMENT '환불 사유', + status ENUM('pending', 'approved', 'rejected', 'completed', 'failed') DEFAULT 'pending', + approved_by BIGINT UNSIGNED NULL COMMENT '승인자', + approved_at TIMESTAMP NULL, + rejection_reason TEXT NULL, + processed_at TIMESTAMP NULL COMMENT '환불 완료 일시', + pg_refund_transaction_id VARCHAR(255) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_payment_id (payment_id), + INDEX idx_tenant_id (tenant_id), + INDEX idx_status (status), + FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE CASCADE, + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/refunds` | 환불 요청 목록 | - | +| GET | `/mng/refunds/{id}` | 환불 요청 상세 | - | +| POST | `/mng/refunds` | 환불 요청 생성 | `StoreRefundRequest` | +| POST | `/mng/refunds/{id}/approve` | 환불 승인 | `ApproveRefundRequest` | +| POST | `/mng/refunds/{id}/reject` | 환불 반려 | `RejectRefundRequest` | +| POST | `/mng/refunds/{id}/process` | 환불 처리 | - | + +### Service 클래스 + +```php +// app/Services/RefundService.php +class RefundService +{ + public function list(array $filters): LengthAwarePaginator; + public function find(int $id): Refund; + public function create(int $paymentId, float $amount, string $reason, string $type = 'full'): Refund; + public function approve(Refund $refund, int $approverId): bool; + public function reject(Refund $refund, string $reason): bool; + public function process(Refund $refund): bool; // PG 환불 API 호출 + public function complete(Refund $refund, string $pgRefundTransactionId): bool; + public function fail(Refund $refund, string $reason): bool; +} +``` + +### 개발 체크리스트 + +- [ ] `Refund` 모델 작성 +- [ ] `RefundService` 클래스 작성 +- [ ] PG 환불 API 연동 +- [ ] 부분 환불 로직 구현 +- [ ] 환불 후 구독 상태 업데이트 +- [ ] FormRequest 작성 +- [ ] `RefundController` 작성 +- [ ] 환불 알림 발송 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 🎯 Phase 5 완료 조건 + +### 기능 완성도 +- [ ] 구독 플랜 관리 완성 +- [ ] 결제 처리 (PG 연동) 동작 +- [ ] 환불 워크플로우 동작 +- [ ] 정기 결제 스케줄러 동작 + +### 코드 품질 +- [ ] Service-First, FormRequest 준수 +- [ ] PG 연동 에러 처리 +- [ ] i18n 키 사용 +- [ ] Pint, PHPStan 통과 + +### 비즈니스 로직 +- [ ] 프로레이션 계산 정확 +- [ ] 사용량 제한 체크 동작 +- [ ] 구독 자동 갱신/만료 처리 +- [ ] 결제 실패 재시도 로직 + +### 보안 +- [ ] PG 통신 암호화 +- [ ] 카드 정보 마스킹 +- [ ] 빌링키 안전 저장 +- [ ] 결제 내역 접근 권한 + +### 테스트 +- [ ] 구독 변경 시나리오 테스트 +- [ ] 결제 처리 Mock 테스트 +- [ ] 환불 워크플로우 테스트 + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code +**버전:** 1.0.0 \ No newline at end of file diff --git a/docs/06_PHASE6_COMM_STATS.md b/docs/06_PHASE6_COMM_STATS.md new file mode 100644 index 00000000..e49bbbe0 --- /dev/null +++ b/docs/06_PHASE6_COMM_STATS.md @@ -0,0 +1,500 @@ +ㅑ# Phase 6: 커뮤니케이션 & 통계 + +**기간:** 1-2주 +**우선순위:** 중간 (완성도 및 부가 기능) +**의존성:** Phase 1-5 (모든 데이터 활용) + +## 📋 Phase 개요 + +고객 커뮤니케이션 및 데이터 분석 기능을 완성합니다. + +**포함 기능:** +1. 설정 - 이메일 관리 +2. 설정 - 문자 관리 +3. 통계 (Statistics & Analytics) + +--- + +## 1️⃣ 설정 - 이메일 관리 + +### 기능 목록 + +#### 1.1 SMTP 설정 +- **경로:** `/mng/settings/email/smtp` +- **기능:** + - SMTP 서버 정보 (호스트, 포트, 암호화) + - 인증 정보 (사용자명, 비밀번호) + - 발신자 정보 (이름, 이메일) + - 테스트 메일 발송 +- **권한:** `settings.email.smtp` + +#### 1.2 이메일 템플릿 관리 +- **경로:** `/mng/settings/email/templates` +- **기능:** + - 템플릿 생성/수정/삭제 + - 타입별 템플릿 (회원가입, 비밀번호 재설정, 결제 완료 등) + - HTML 에디터 (변수 치환 지원) + - 미리보기 +- **권한:** `settings.email.templates` + +#### 1.3 이메일 발송 내역 +- **경로:** `/mng/settings/email/history` +- **기능:** + - 발송 내역 목록 + - 검색 (수신자, 제목) + - 필터 (상태, 날짜) + - 재발송 + - 실패 로그 확인 +- **권한:** `settings.email.history` + +#### 1.4 이메일 예약 발송 +- **경로:** `/mng/settings/email/scheduled` +- **기능:** + - 예약 발송 목록 + - 새 예약 생성 + - 예약 취소 +- **권한:** `settings.email.scheduled` + +### DB 스키마 + +```sql +CREATE TABLE email_settings ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL COMMENT 'NULL = 전역 설정', + smtp_host VARCHAR(255) NOT NULL, + smtp_port INT DEFAULT 587, + smtp_encryption ENUM('tls', 'ssl', 'none') DEFAULT 'tls', + smtp_username VARCHAR(255) NOT NULL, + smtp_password VARCHAR(255) NOT NULL COMMENT '암호화 저장', + from_name VARCHAR(255) NOT NULL, + from_email VARCHAR(255) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE email_templates ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(100) NOT NULL COMMENT 'welcome, password_reset, payment_success 등', + subject VARCHAR(255) NOT NULL, + body TEXT NOT NULL COMMENT 'HTML 내용', + variables JSON NULL COMMENT '사용 가능한 변수', + is_default BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_type (type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE email_logs ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL, + template_id BIGINT UNSIGNED NULL, + recipient_email VARCHAR(255) NOT NULL, + recipient_name VARCHAR(255) NULL, + subject VARCHAR(255) NOT NULL, + body TEXT NOT NULL, + status ENUM('pending', 'sent', 'failed') DEFAULT 'pending', + sent_at TIMESTAMP NULL, + failed_reason TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_status (status), + INDEX idx_recipient_email (recipient_email), + INDEX idx_sent_at (sent_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/settings/email/smtp` | SMTP 설정 조회 | - | +| PUT | `/mng/settings/email/smtp` | SMTP 설정 저장 | `UpdateSmtpRequest` | +| POST | `/mng/settings/email/smtp/test` | 테스트 메일 발송 | `TestEmailRequest` | +| GET | `/mng/settings/email/templates` | 템플릿 목록 | - | +| POST | `/mng/settings/email/templates` | 템플릿 생성 | `StoreEmailTemplateRequest` | +| PUT | `/mng/settings/email/templates/{id}` | 템플릿 수정 | `UpdateEmailTemplateRequest` | +| GET | `/mng/settings/email/history` | 발송 내역 | - | +| POST | `/mng/settings/email/send` | 이메일 발송 | `SendEmailRequest` | + +### Service 클래스 + +```php +// app/Services/EmailService.php +class EmailService +{ + public function getSettings(int $tenantId = null): ?EmailSetting; + public function updateSettings(array $data, int $tenantId = null): EmailSetting; + public function testConnection(EmailSetting $setting, string $testEmail): bool; + + public function listTemplates(int $tenantId = null): Collection; + public function createTemplate(array $data): EmailTemplate; + public function updateTemplate(EmailTemplate $template, array $data): EmailTemplate; + public function render(EmailTemplate $template, array $variables): string; + + public function send(string $to, string $subject, string $body, int $templateId = null): EmailLog; + public function sendBulk(array $recipients, string $subject, string $body): int; + public function getHistory(array $filters): LengthAwarePaginator; + public function retry(EmailLog $log): bool; +} +``` + +### 개발 체크리스트 + +- [ ] `EmailSetting`, `EmailTemplate`, `EmailLog` 모델 작성 +- [ ] `EmailService` 클래스 작성 +- [ ] Laravel Mail + Queue 설정 +- [ ] SMTP 연결 테스트 기능 +- [ ] 템플릿 변수 치환 로직 +- [ ] 예약 발송 스케줄러 (Laravel Schedule) +- [ ] 실패 재시도 로직 +- [ ] FormRequest 작성 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 2️⃣ 설정 - 문자 관리 + +### 기능 목록 + +#### 2.1 SMS API 설정 +- **경로:** `/mng/settings/sms/api` +- **기능:** + - API 연동 설정 (알리고, 카카오 알림톡 등) + - API 키 관리 + - 발신 번호 설정 + - 테스트 문자 발송 +- **권한:** `settings.sms.api` + +#### 2.2 문자 템플릿 관리 +- **경로:** `/mng/settings/sms/templates` +- **기능:** + - 템플릿 생성/수정/삭제 + - SMS (90자), LMS (2000자) 구분 + - 변수 치환 지원 + - 바이트 수 자동 계산 +- **권한:** `settings.sms.templates` + +#### 2.3 문자 발송 내역 +- **경로:** `/mng/settings/sms/history` +- **기능:** + - 발송 내역 목록 + - 검색 (수신자, 내용) + - 필터 (상태, 날짜, 타입) + - 재발송 + - 실패 로그 확인 +- **권한:** `settings.sms.history` + +#### 2.4 대량 문자 발송 +- **경로:** `/mng/settings/sms/bulk` +- **기능:** + - 엑셀 업로드 (수신자 목록) + - 템플릿 선택 + - 예약 발송 + - 발송 결과 확인 +- **권한:** `settings.sms.bulk` + +### DB 스키마 + +```sql +CREATE TABLE sms_settings ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL, + provider VARCHAR(50) NOT NULL COMMENT 'aligo, kakao 등', + api_key VARCHAR(255) NOT NULL COMMENT '암호화 저장', + api_secret VARCHAR(255) NULL, + sender_number VARCHAR(20) NOT NULL COMMENT '발신 번호', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE sms_templates ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL, + name VARCHAR(255) NOT NULL, + type ENUM('sms', 'lms', 'mms') DEFAULT 'sms', + content TEXT NOT NULL, + variables JSON NULL, + byte_count INT NOT NULL COMMENT '바이트 수', + is_default BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_type (type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE sms_logs ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL, + template_id BIGINT UNSIGNED NULL, + recipient_number VARCHAR(20) NOT NULL, + content TEXT NOT NULL, + type ENUM('sms', 'lms', 'mms') DEFAULT 'sms', + status ENUM('pending', 'sent', 'failed') DEFAULT 'pending', + sent_at TIMESTAMP NULL, + failed_reason TEXT NULL, + api_message_id VARCHAR(255) NULL COMMENT 'API 메시지 ID', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_status (status), + INDEX idx_sent_at (sent_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | FormRequest | +|--------|----------|-------------|-------------| +| GET | `/mng/settings/sms/api` | SMS API 설정 조회 | - | +| PUT | `/mng/settings/sms/api` | SMS API 설정 저장 | `UpdateSmsApiRequest` | +| POST | `/mng/settings/sms/api/test` | 테스트 문자 발송 | `TestSmsRequest` | +| GET | `/mng/settings/sms/templates` | 템플릿 목록 | - | +| POST | `/mng/settings/sms/templates` | 템플릿 생성 | `StoreSmsTemplateRequest` | +| POST | `/mng/settings/sms/send` | 문자 발송 | `SendSmsRequest` | +| POST | `/mng/settings/sms/bulk` | 대량 발송 | `SendBulkSmsRequest` | +| GET | `/mng/settings/sms/history` | 발송 내역 | - | + +### Service 클래스 + +```php +// app/Services/SmsService.php +class SmsService +{ + public function getSettings(int $tenantId = null): ?SmsSetting; + public function updateSettings(array $data, int $tenantId = null): SmsSetting; + public function testConnection(SmsSetting $setting, string $testNumber): bool; + + public function listTemplates(int $tenantId = null): Collection; + public function createTemplate(array $data): SmsTemplate; + public function updateTemplate(SmsTemplate $template, array $data): SmsTemplate; + public function calculateByteCount(string $content): int; + + public function send(string $to, string $content, string $type = 'sms', int $templateId = null): SmsLog; + public function sendBulk(array $recipients, string $content, string $type = 'sms'): int; + public function getHistory(array $filters): LengthAwarePaginator; + public function retry(SmsLog $log): bool; +} +``` + +### 개발 체크리스트 + +- [ ] `SmsSetting`, `SmsTemplate`, `SmsLog` 모델 작성 +- [ ] `SmsService` 클래스 작성 +- [ ] SMS API 연동 (알리고 우선) +- [ ] 바이트 수 계산 로직 (한글 2바이트) +- [ ] 대량 발송 큐 처리 +- [ ] 엑셀 업로드 파싱 (PhpSpreadsheet) +- [ ] 발송 결과 웹훅 처리 +- [ ] FormRequest 작성 +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 3️⃣ 통계 (Statistics & Analytics) + +### 기능 목록 + +#### 3.1 대시보드 +- **경로:** `/mng/dashboard` +- **기능:** + - 주요 KPI 카드 (회원 수, 매출, 구독 현황) + - 최근 활동 로그 + - 영업 파이프라인 요약 + - 결제 통계 +- **권한:** `dashboard.view` + +#### 3.2 회원 통계 +- **경로:** `/mng/statistics/users` +- **기능:** + - 가입자 추세 (일별, 월별) + - 활성/비활성 비율 + - 부서별 분포 + - 엑셀 내보내기 +- **권한:** `statistics.users` + +#### 3.3 매출 통계 +- **경로:** `/mng/statistics/revenue` +- **기능:** + - 매출 추세 (일별, 월별, 연별) + - 플랜별 매출 + - MRR (Monthly Recurring Revenue) + - ARR (Annual Recurring Revenue) + - 차트 (Chart.js 또는 ApexCharts) +- **권한:** `statistics.revenue` + +#### 3.4 영업 통계 +- **경로:** `/mng/statistics/sales` +- **기능:** + - 파이프라인 단계별 통계 + - 전환율 (Conversion Rate) + - 담당자별 성과 + - 기간별 비교 +- **권한:** `statistics.sales` + +#### 3.5 구독 통계 +- **경로:** `/mng/statistics/subscriptions` +- **기능:** + - 플랜별 구독 현황 + - 이탈률 (Churn Rate) + - 업그레이드/다운그레이드 추세 + - 리텐션 분석 +- **권한:** `statistics.subscriptions` + +### DB 스키마 + +```sql +-- 통계 스냅샷 (일별 집계) +CREATE TABLE statistics_snapshots ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL COMMENT 'NULL = 전체', + snapshot_date DATE NOT NULL, + metric_type ENUM('users', 'revenue', 'sales', 'subscriptions') NOT NULL, + metric_data JSON NOT NULL COMMENT '통계 데이터', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_snapshot_date (snapshot_date), + INDEX idx_metric_type (metric_type), + UNIQUE KEY unique_snapshot (tenant_id, snapshot_date, metric_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### API 엔드포인트 + +| Method | Endpoint | Description | Query Params | +|--------|----------|-------------|--------------| +| GET | `/mng/dashboard` | 대시보드 데이터 | - | +| GET | `/mng/statistics/users` | 회원 통계 | `start_date`, `end_date`, `group_by` | +| GET | `/mng/statistics/revenue` | 매출 통계 | `start_date`, `end_date`, `group_by` | +| GET | `/mng/statistics/sales` | 영업 통계 | `start_date`, `end_date` | +| GET | `/mng/statistics/subscriptions` | 구독 통계 | `start_date`, `end_date` | +| GET | `/mng/statistics/export` | 엑셀 내보내기 | `type`, `start_date`, `end_date` | + +### Service 클래스 + +```php +// app/Services/StatisticsService.php +class StatisticsService +{ + public function getDashboardData(int $tenantId = null): array; + + public function getUserStats(array $filters): array; + public function getRevenueStats(array $filters): array; + public function getSalesStats(array $filters): array; + public function getSubscriptionStats(array $filters): array; + + public function calculateMRR(int $tenantId = null): float; + public function calculateARR(int $tenantId = null): float; + public function calculateChurnRate(array $filters): float; + public function calculateConversionRate(array $filters): float; + + public function exportToExcel(string $type, array $filters): string; // 파일 경로 + public function createSnapshot(string $metricType, int $tenantId = null): void; // 일별 스냅샷 생성 +} +``` + +### UI 컴포넌트 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 대시보드 │ +├─────────────────────────────────────────────────────────┤ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │회원 1,234│ │매출 5.2M │ │구독 567 │ │영업 89 │ │ +│ │↑ 12% │ │↑ 8% │ │↓ 3% │ │↑ 15% │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐│ +│ │ 매출 추세 (최근 6개월) [Chart.js 꺾은선] ││ +│ └─────────────────────────────────────────────────────┘│ +│ │ +│ ┌─────────────────────────────────────────────────────┐│ +│ │ 영업 파이프라인 [Chart.js 퍼널 차트] ││ +│ └─────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────┘ +``` + +### 개발 체크리스트 + +- [ ] `StatisticsSnapshot` 모델 작성 +- [ ] `StatisticsService` 클래스 작성 +- [ ] 통계 계산 로직 (MRR, ARR, Churn Rate 등) +- [ ] Chart.js 또는 ApexCharts 통합 +- [ ] 일별 스냅샷 스케줄러 +- [ ] 엑셀 내보내기 (PhpSpreadsheet) +- [ ] 대시보드 UI 작성 +- [ ] 날짜 필터 UI (Alpine.js) +- [ ] i18n 키 작성 +- [ ] 테스트 작성 + +--- + +## 🎯 Phase 6 완료 조건 + +### 기능 완성도 +- [ ] 이메일 발송 동작 +- [ ] 문자 발송 동작 +- [ ] 통계 차트 표시 +- [ ] 엑셀 내보내기 동작 + +### 코드 품질 +- [ ] Service-First, FormRequest 준수 +- [ ] 큐 처리 안정성 +- [ ] i18n 키 사용 +- [ ] Pint, PHPStan 통과 + +### 외부 연동 +- [ ] SMTP 연결 안정성 +- [ ] SMS API 연동 동작 +- [ ] 웹훅 처리 (발송 결과) + +### 성능 +- [ ] 대량 발송 큐 처리 +- [ ] 통계 쿼리 최적화 +- [ ] 스냅샷 생성 스케줄러 + +### 테스트 +- [ ] 이메일 발송 테스트 (Mock) +- [ ] 문자 발송 테스트 (Mock) +- [ ] 통계 계산 로직 테스트 +- [ ] 엑셀 생성 테스트 + +--- + +## 🎉 MNG 애플리케이션 전체 완료 + +Phase 6가 완료되면 MNG 애플리케이션의 핵심 기능이 모두 구현됩니다. + +**다음 단계:** +1. 전체 통합 테스트 +2. 성능 최적화 +3. 사용자 매뉴얼 작성 +4. 배포 준비 + +**참고 문서:** +- `00_OVERVIEW.md` - 전체 개발 계획 +- `99_TECHNICAL_STANDARDS.md` - 기술 표준 + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code +**버전:** 1.0.0 \ No newline at end of file diff --git a/docs/99_TECHNICAL_STANDARDS.md b/docs/99_TECHNICAL_STANDARDS.md new file mode 100644 index 00000000..e2db62d0 --- /dev/null +++ b/docs/99_TECHNICAL_STANDARDS.md @@ -0,0 +1,895 @@ +# MNG 기술 표준 문서 + +**목적:** 모든 Phase에서 일관되게 적용할 기술 표준 및 개발 규칙 정의 (SAM API Rules 기반) + +## 📋 목차 + +1. [아키텍처 패턴 (SAM API Rules)](#1-아키텍처-패턴-sam-api-rules) +2. [코딩 컨벤션](#2-코딩-컨벤션) +3. [데이터베이스 설계 원칙](#3-데이터베이스-설계-원칙) +4. [보안 정책](#4-보안-정책) +5. [테스트 전략](#5-테스트-전략) +6. [성능 최적화](#6-성능-최적화) +7. [배포 프로세스](#7-배포-프로세스) + +--- + +## 1. 아키텍처 패턴 (SAM API Rules) + +### 1.1 Service-First Pattern (필수) + +**원칙:** 모든 비즈니스 로직은 Service 클래스에 위치 (SAM API Rule #1) + +``` +[Request] + ↓ +[Route] + ↓ +[Controller] (DI 주입, Service 호출만) + ↓ +[FormRequest] (유효성 검증 - SAM API Rule #8) + ↓ +[Service] (비즈니스 로직, tenantId()/apiUserId() 필수) + ↓ +[Model] (Eloquent ORM, BelongsToTenant) + ↓ +[Database] +``` + +**Controller 예시:** (SAM API Rule #5) +```php +class UserController extends Controller +{ + public function __construct(private UserService $userService) {} + + public function index(Request $request) + { + // Controller는 Service 호출만 + $users = $this->userService->list($request->all()); + return view('users.index', compact('users')); + } + + public function store(StoreUserRequest $request) + { + // FormRequest 검증 완료 데이터만 Service로 전달 + $user = $this->userService->create($request->validated()); + return redirect()->route('users.index') + ->with('success', __('message.user.created')); // i18n 키 필수 + } +} +``` + +**Service 예시:** (SAM API Rule #5) +```php +namespace App\Services; + +use App\Services\Service as BaseService; // Base Service 상속 필수 + +class UserService extends BaseService +{ + // tenantId(), apiUserId() 필수 설정 (SAM API Rule #1) + public function list(array $filters): LengthAwarePaginator + { + $query = User::query(); + + // Multi-tenant 필터링 (BelongsToTenant global scope 자동 적용) + if (isset($filters['search'])) { + $query->where(function($q) use ($filters) { + $q->where('name', 'like', "%{$filters['search']}%") + ->orWhere('email', 'like', "%{$filters['search']}%"); + }); + } + + return $query->paginate(20); + } + + public function create(array $data): User + { + DB::beginTransaction(); + try { + $user = User::create([ + 'tenant_id' => $this->tenantId(), // Base Service에서 제공 + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + 'created_by' => $this->apiUserId(), // Base Service에서 제공 + ]); + + if (isset($data['roles'])) { + $user->assignRole($data['roles']); + } + + DB::commit(); + return $user; + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + } +} +``` + +### 1.2 FormRequest Pattern (필수) + +**원칙:** Controller에서 검증 금지, FormRequest 사용 (SAM API Rule #8) + +```php +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class StoreUserRequest extends FormRequest +{ + public function authorize(): bool + { + // 권한 체크 (RBAC) + return $this->user()->can('users.create'); + } + + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email', + 'password' => 'required|min:8|confirmed', + 'roles' => 'array', + 'roles.*' => 'exists:roles,id', + ]; + } + + public function messages(): array + { + // i18n 키 사용 (SAM API Rule #6) + return [ + 'name.required' => __('validation.required', ['attribute' => __('users.name')]), + 'email.unique' => __('validation.unique', ['attribute' => __('users.email')]), + ]; + } +} +``` + +**공통 Request 재사용:** (SAM API Rule #8) +```php +// app/Http/Requests/PaginateRequest.php +class PaginateRequest extends FormRequest +{ + public function rules(): array + { + return [ + 'page' => 'integer|min:1', + 'per_page' => 'integer|min:1|max:100', + 'sort' => 'string', + 'order' => 'in:asc,desc', + ]; + } +} + +// 사용 예시 +public function index(PaginateRequest $request) +{ + $users = $this->userService->list($request->validated()); +} +``` + +### 1.3 Repository Pattern (선택적) + +**사용 시기:** 복잡한 쿼리, 여러 모델 조인, 재사용성 높은 쿼리 + +```php +namespace App\Repositories; + +class UserRepository +{ + public function findWithRolesAndDepartment(int $id): ?User + { + return User::with(['roles', 'department']) + ->find($id); + } + + public function getActiveUsersWithRecentActivity(int $days = 30): Collection + { + return User::where('is_active', true) + ->where('last_login_at', '>=', now()->subDays($days)) + ->orderBy('last_login_at', 'desc') + ->get(); + } +} +``` + +--- + +## 2. 코딩 컨벤션 + +### 2.1 네이밍 규칙 + +```php +// 클래스: PascalCase +class UserService {} +class StoreUserRequest {} + +// 메서드: camelCase +public function createUser() {} +public function getUserById() {} + +// 변수: camelCase +$userName = 'John'; +$isActive = true; + +// 상수: UPPER_SNAKE_CASE +const MAX_USERS = 100; +const DEFAULT_ROLE = 'user'; + +// 데이터베이스: snake_case +// 테이블: 복수형 +users, sales_opportunities, quotation_items + +// 컬럼: snake_case +user_id, created_at, is_active, tenant_id +``` + +### 2.2 i18n (국제화) - SAM API Rule #6 + +**원칙:** 한글 직접 사용 금지, 언어 키 사용 필수 + +```php +// ❌ 잘못된 예 +return redirect()->back()->with('success', '사용자가 생성되었습니다.'); + +// ✅ 올바른 예 (SAM API Rule #6) +return redirect()->back()->with('success', __('message.user.created')); +``` + +**언어 파일 구조:** +``` +lang/ +├── ko/ +│ ├── message.php // 성공 메시지 +│ ├── error.php // 에러 메시지 +│ ├── validation.php // 검증 메시지 +│ └── users.php // 도메인별 메시지 +└── en/ + ├── message.php + ├── error.php + ├── validation.php + └── users.php +``` + +**`lang/ko/message.php` 예시:** (SAM API Rule #6) +```php +return [ + // 공통 메시지 + 'fetched' => '조회되었습니다.', + 'created' => '생성되었습니다.', + 'updated' => '수정되었습니다.', + 'deleted' => '삭제되었습니다.', + 'bulk_upsert' => '일괄 저장되었습니다.', + 'reordered' => '순서가 변경되었습니다.', + + // 도메인별 메시지 (선택적) + 'user' => [ + 'created' => '사용자가 생성되었습니다.', + 'updated' => '사용자 정보가 수정되었습니다.', + ], +]; +``` + +**`lang/ko/error.php` 예시:** +```php +return [ + 'not_found' => '데이터를 찾을 수 없습니다.', + 'unauthorized' => '권한이 없습니다.', + 'validation_failed' => '입력값 검증에 실패했습니다.', +]; +``` + +### 2.3 코드 스타일 (SAM API Rule #7) + +**Laravel Pint 사용 (자동 포맷팅):** +```bash +./vendor/bin/pint +``` + +**주석 규칙:** +```php +/** + * 사용자를 생성합니다. + * + * @param array $data 사용자 데이터 + * @return User 생성된 사용자 + * @throws \Exception 생성 실패 시 + */ +public function create(array $data): User +{ + // 복잡한 로직 설명이 필요한 경우에만 인라인 주석 + // 단순 코드는 주석 없이 자명하게 작성 +} +``` + +--- + +## 3. 데이터베이스 설계 원칙 + +### 3.1 Multi-tenant 필수 컬럼 (SAM API Rule #2) + +```sql +CREATE TABLE example_table ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '소속 테넌트 (필수)', + -- 기타 컬럼... + created_by BIGINT UNSIGNED NULL COMMENT '생성자', + updated_by BIGINT UNSIGNED NULL COMMENT '수정자', + deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL COMMENT 'Soft Delete', + + INDEX idx_tenant_id (tenant_id), + INDEX idx_deleted_at (deleted_at), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +**Model에 BelongsToTenant trait 적용:** (SAM API Rule #2) +```php +namespace App\Models; + +use App\Traits\BelongsToTenant; +use Illuminate\Database\Eloquent\SoftDeletes; + +class ExampleModel extends Model +{ + use BelongsToTenant, SoftDeletes; + + protected $fillable = ['tenant_id', 'name', ...]; +} +``` + +**BelongsToTenant trait:** +```php +namespace App\Traits; + +use App\Scopes\TenantScope; + +trait BelongsToTenant +{ + protected static function bootBelongsToTenant() + { + // Global Scope 적용 (자동 tenant_id 필터링) + static::addGlobalScope(new TenantScope); + + // Model 생성 시 자동으로 tenant_id 설정 + static::creating(function ($model) { + if (!$model->tenant_id && auth()->check()) { + $model->tenant_id = auth()->user()->tenant_id; + } + }); + } + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } +} +``` + +### 3.2 Soft Delete 기본 정책 (SAM API Rule #2) + +**원칙:** 모든 테넌트 데이터는 Soft Delete 적용 + +```php +use Illuminate\Database\Eloquent\SoftDeletes; + +class User extends Model +{ + use SoftDeletes; + + protected $dates = ['deleted_at']; +} +``` + +### 3.3 Audit Log (감사 로그) - SAM API Rule #9 + +**원칙:** 모든 CUD 작업 자동 기록 (13개월 보관) + +**audit_logs 테이블:** +```sql +CREATE TABLE audit_logs ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NULL COMMENT '소속 테넌트', + target_type VARCHAR(255) NOT NULL COMMENT '대상 모델', + target_id BIGINT UNSIGNED NOT NULL COMMENT '대상 ID', + action VARCHAR(50) NOT NULL COMMENT 'created, updated, deleted 등', + before_values JSON NULL COMMENT '변경 전 데이터', + after_values JSON NULL COMMENT '변경 후 데이터', + actor_id BIGINT UNSIGNED NULL COMMENT '작업자 ID', + ip_address VARCHAR(45) NULL, + user_agent TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_target (target_type, target_id), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +**Observer 패턴 사용:** +```php +namespace App\Observers; + +use App\Models\AuditLog; + +class UserObserver +{ + public function created(User $user) + { + AuditLog::create([ + 'tenant_id' => $user->tenant_id, + 'target_type' => User::class, + 'target_id' => $user->id, + 'action' => 'created', + 'before_values' => null, + 'after_values' => $user->toArray(), + 'actor_id' => auth()->id(), + 'ip_address' => request()->ip(), + 'user_agent' => request()->userAgent(), + ]); + } + + public function updated(User $user) + { + AuditLog::create([ + 'tenant_id' => $user->tenant_id, + 'target_type' => User::class, + 'target_id' => $user->id, + 'action' => 'updated', + 'before_values' => $user->getOriginal(), + 'after_values' => $user->getChanges(), + 'actor_id' => auth()->id(), + 'ip_address' => request()->ip(), + 'user_agent' => request()->userAgent(), + ]); + } + + public function deleted(User $user) + { + AuditLog::create([ + 'tenant_id' => $user->tenant_id, + 'target_type' => User::class, + 'target_id' => $user->id, + 'action' => 'deleted', + 'before_values' => $user->toArray(), + 'after_values' => null, + 'actor_id' => auth()->id(), + 'ip_address' => request()->ip(), + 'user_agent' => request()->userAgent(), + ]); + } +} +``` + +**AppServiceProvider에 등록:** +```php +public function boot() +{ + User::observe(UserObserver::class); + Client::observe(ClientObserver::class); + // 기타 모델... +} +``` + +**13개월 보관 정책 (Scheduler):** +```php +// app/Console/Kernel.php +protected function schedule(Schedule $schedule) +{ + // 13개월 이전 감사 로그 삭제 + $schedule->command('audit:prune')->monthlyOn(1, '02:00'); +} + +// app/Console/Commands/PruneAuditLogs.php +public function handle() +{ + $retentionMonths = 13; + $cutoffDate = now()->subMonths($retentionMonths); + + $deleted = AuditLog::where('created_at', '<', $cutoffDate)->delete(); + + $this->info("Pruned {$deleted} audit log records older than {$retentionMonths} months."); +} +``` + +### 3.4 인덱스 전략 + +```sql +-- 필수 인덱스 +INDEX idx_tenant_id (tenant_id) -- Multi-tenant 필터링 +INDEX idx_created_at (created_at) -- 날짜 정렬 +INDEX idx_deleted_at (deleted_at) -- Soft Delete 필터링 + +-- 검색 인덱스 +INDEX idx_email (email) -- 이메일 검색 +FULLTEXT idx_search (title, content) -- 전문 검색 + +-- 복합 인덱스 +INDEX idx_tenant_status (tenant_id, status) -- 테넌트 + 상태 필터 +``` + +--- + +## 4. 보안 정책 + +### 4.1 인증 (Authentication) + +**Laravel Sanctum 사용:** +```php +// config/sanctum.php +'expiration' => 60 * 24, // 24시간 + +// 로그인 (MNG는 세션 기반, API는 토큰 기반) +public function login(Request $request) +{ + $credentials = $request->validate([ + 'email' => 'required|email', + 'password' => 'required', + ]); + + if (!Auth::attempt($credentials)) { + throw ValidationException::withMessages([ + 'email' => [__('error.auth.failed')], + ]); + } + + $user = Auth::user(); + + // 세션 기반 로그인 (MNG) + return redirect()->route('dashboard'); + + // 또는 API 토큰 발급 (필요시) + // $token = $user->createToken('mng-token')->plainTextToken; + // return response()->json(['token' => $token, 'user' => $user]); +} +``` + +### 4.2 권한 (Authorization) - RBAC + +**Spatie Laravel Permission 사용:** +```php +// Permission 체크 +if ($user->can('users.create')) { + // 권한 있음 +} + +// Middleware +Route::group(['middleware' => ['auth', 'can:users.index']], function () { + Route::get('/users', [UserController::class, 'index']); +}); + +// Blade +@can('users.create') + 새 사용자 +@endcan +``` + +### 4.3 XSS 방지 + +**Blade 자동 이스케이프:** +```blade +{{-- 자동 이스케이프 (안전) --}} +{{ $user->name }} + +{{-- 이스케이프 없음 (주의) --}} +{!! $htmlContent !!} +``` + +### 4.4 CSRF 보호 + +**모든 POST/PUT/DELETE 요청에 @csrf:** +```blade +
+ @csrf + +
+``` + +### 4.5 SQL Injection 방지 + +**Eloquent 또는 Query Builder 사용 (Raw 쿼리 금지):** +```php +// ✅ 올바른 예 +User::where('email', $email)->first(); + +// ❌ 잘못된 예 +DB::select("SELECT * FROM users WHERE email = '$email'"); +``` + +### 4.6 민감 정보 암호화 + +```php +// 비밀번호 +use Illuminate\Support\Facades\Hash; +Hash::make($password); + +// API 키, 토큰 +use Illuminate\Support\Facades\Crypt; +$encrypted = Crypt::encryptString($apiKey); +$decrypted = Crypt::decryptString($encrypted); +``` + +--- + +## 5. 테스트 전략 + +### 5.1 테스트 레벨 + +``` +┌─────────────────────────────────┐ +│ Feature Tests (통합 테스트) │ ← 주력 +├─────────────────────────────────┤ +│ Unit Tests (단위 테스트) │ ← Service 계층 +├─────────────────────────────────┤ +│ Browser Tests (E2E) │ ← 선택적 (Playwright MCP) +└─────────────────────────────────┘ +``` + +### 5.2 Unit Test 예시 + +```php +// tests/Unit/Services/UserServiceTest.php +namespace Tests\Unit\Services; + +use Tests\TestCase; +use Illuminate\Foundation\Testing\RefreshDatabase; + +class UserServiceTest extends TestCase +{ + use RefreshDatabase; + + public function test_create_user() + { + $service = new UserService(); + $service->setTenantId(1); // Base Service tenantId 설정 + $service->setApiUserId(1); // Base Service apiUserId 설정 + + $data = [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password123', + ]; + + $user = $service->create($data); + + $this->assertDatabaseHas('users', [ + 'email' => 'test@example.com', + ]); + $this->assertTrue(Hash::check('password123', $user->password)); + } +} +``` + +### 5.3 Feature Test 예시 + +```php +// tests/Feature/UserControllerTest.php +namespace Tests\Feature; + +use Tests\TestCase; +use Illuminate\Foundation\Testing\RefreshDatabase; + +class UserControllerTest extends TestCase +{ + use RefreshDatabase; + + public function test_user_can_view_users_list() + { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('users.index')); + + $response->assertStatus(200); + $response->assertViewIs('users.index'); + } + + public function test_user_can_create_new_user() + { + $admin = User::factory()->create(); + $admin->givePermissionTo('users.create'); + $this->actingAs($admin); + + $data = [ + 'name' => 'New User', + 'email' => 'new@example.com', + 'password' => 'password123', + 'password_confirmation' => 'password123', + ]; + + $response = $this->post(route('users.store'), $data); + + $response->assertRedirect(route('users.index')); + $this->assertDatabaseHas('users', ['email' => 'new@example.com']); + } +} +``` + +### 5.4 테스트 실행 + +```bash +# 전체 테스트 +php artisan test + +# 특정 테스트 +php artisan test --filter UserServiceTest + +# 커버리지 (Xdebug 필요) +php artisan test --coverage +``` + +--- + +## 6. 성능 최적화 + +### 6.1 Eager Loading + +**N+1 쿼리 방지:** +```php +// ❌ N+1 문제 +$users = User::all(); +foreach ($users as $user) { + echo $user->department->name; // N번 쿼리 +} + +// ✅ Eager Loading +$users = User::with('department')->get(); +foreach ($users as $user) { + echo $user->department->name; // 1번 쿼리 +} +``` + +### 6.2 쿼리 최적화 + +```php +// ✅ select로 필요한 컬럼만 +User::select('id', 'name', 'email')->get(); + +// ✅ chunk로 대량 데이터 처리 +User::chunk(100, function ($users) { + foreach ($users as $user) { + // 처리 + } +}); + +// ✅ 카운트 최적화 +$count = User::count(); // SELECT COUNT(*) (빠름) +// ❌ $count = User::all()->count(); (느림) +``` + +### 6.3 캐싱 + +```php +// 캐시 저장 (60분) +Cache::put('users.all', User::all(), now()->addMinutes(60)); + +// 캐시 조회 (없으면 생성) +$users = Cache::remember('users.all', 60 * 60, function () { + return User::all(); +}); + +// 캐시 삭제 +Cache::forget('users.all'); +``` + +### 6.4 큐 (Queue) + +**무거운 작업은 큐로 처리:** +```php +// Job 생성 +php artisan make:job SendWelcomeEmail + +// Job 클래스 +namespace App\Jobs; + +use Illuminate\Contracts\Queue\ShouldQueue; + +class SendWelcomeEmail implements ShouldQueue +{ + public function __construct(public User $user) {} + + public function handle() + { + Mail::to($this->user->email)->send(new WelcomeEmail($this->user)); + } +} + +// Job 디스패치 +SendWelcomeEmail::dispatch($user); + +// 큐 워커 실행 +php artisan queue:work +``` + +--- + +## 7. 배포 프로세스 + +### 7.1 환경 설정 + +``` +로컬 (sam.kr) → 개발 서버 (codebridge-x.com) → 운영 서버 (TBD) +``` + +### 7.2 배포 체크리스트 + +```bash +# 1. Git 푸시 +git add . +git commit -m "feat: 사용자 관리 구현" +git push origin main + +# 2. 서버 접속 (개발 서버) +ssh user@codebridge-x.com + +# 3. 코드 Pull +cd /var/www/mng +git pull origin main + +# 4. 의존성 업데이트 +composer install --no-dev --optimize-autoloader + +# 5. 마이그레이션 +php artisan migrate --force + +# 6. 캐시 클리어 +php artisan config:cache +php artisan route:cache +php artisan view:cache + +# 7. 권한 설정 +chown -R www-data:www-data storage bootstrap/cache + +# 8. 큐 재시작 +php artisan queue:restart + +# 9. Supervisor 재시작 (큐 워커) +sudo supervisorctl restart mng-worker:* +``` + +### 7.3 롤백 프로세스 + +```bash +# 1. Git 롤백 +git revert HEAD +git push origin main + +# 2. 서버에서 Pull +git pull origin main + +# 3. 마이그레이션 롤백 (필요 시) +php artisan migrate:rollback --step=1 + +# 4. 캐시 클리어 +php artisan cache:clear +php artisan config:clear +``` + +--- + +## 📚 참고 자료 + +### SAM 프로젝트 문서 +- **SAM CLAUDE.md:** `/SAM/CLAUDE.md` - 전체 프로젝트 구조 +- **API CLAUDE.md:** `/SAM/api/CLAUDE.md` - SAM API Development Rules 상세 +- **MNG CLAUDE.md:** `/SAM/mng/CLAUDE.md` - MNG 프로젝트 특화 가이드 +- **현재 작업:** `CURRENT_WORKS.md` (각 저장소별) + +### 외부 문서 +- **Laravel 공식 문서:** https://laravel.com/docs +- **Tailwind CSS:** https://tailwindcss.com/docs +- **DaisyUI:** https://daisyui.com/ +- **HTMX:** https://htmx.org/ +- **Spatie Permission:** https://spatie.be/docs/laravel-permission + +--- + +**최종 업데이트:** 2025-11-21 +**작성자:** Claude Code +**버전:** 2.0.0 (SAM API Rules 기반) \ No newline at end of file diff --git a/docs/API_FLOW_TESTER_DESIGN.md b/docs/API_FLOW_TESTER_DESIGN.md new file mode 100644 index 00000000..9e3a18e3 --- /dev/null +++ b/docs/API_FLOW_TESTER_DESIGN.md @@ -0,0 +1,1036 @@ + # API Flow Tester 설계문서 + +> **Version**: 1.0 +> **Date**: 2025-11-27 +> **Author**: Claude Code +> **Status**: Draft + +--- + +## 1. 개요 + +### 1.1 목적 +API Flow Tester는 MNG 관리자 패널에서 복수의 API를 순차적으로 실행하고, +이전 응답 데이터를 다음 요청에 바인딩하여 통합 API 플로우를 테스트하는 도구입니다. + +### 1.2 핵심 기능 (B 버전) +- **플로우 관리**: 생성, 조회, 수정, 삭제 (CRUD) +- **JSON 에디터**: 구문 강조 기능이 있는 플로우/데이터 편집기 +- **변수 바인딩**: `{{stepN.response.path}}` 형식으로 이전 응답 참조 +- **순차 실행**: 의존성 기반 순서대로 API 호출 +- **결과 추적**: 단계별 성공/실패 표시 및 실행 결과 저장 + +### 1.3 범위 제외 (B 버전) +- 시스템 내 AI 통합 (사용자가 외부에서 Claude로 JSON 생성) +- 자동 플로우 생성 +- AI 기반 오류 분석 + +--- + +## 2. 시스템 아키텍처 + +### 2.1 전체 구조 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ MNG Application │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ Flow List │ │ Flow Editor │ │ Flow Executor │ │ +│ │ (Index) │ │ (Create/ │ │ (Run/Monitor) │ │ +│ │ │ │ Edit) │ │ │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │ +│ │ │ │ │ +│ ┌──────▼─────────────────▼──────────────────────▼───────────┐ │ +│ │ FlowTesterController │ │ +│ └───────────────────────────┬───────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────────▼───────────────────────────────┐ │ +│ │ FlowTesterService │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ +│ │ │ FlowManager │ │ Executor │ │ VariableBinder │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ +│ └───────────────────────────┬───────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────────▼───────────────────────────────┐ │ +│ │ Database (MySQL - samdb) │ │ +│ │ ┌──────────────────┐ ┌────────────────────────────┐ │ │ +│ │ │ admin_api_flows │ │ admin_api_flow_runs │ │ │ +│ │ └──────────────────┘ └────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ External API Server │ +│ (SAM API: sam.kr/api/v1) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 컴포넌트 설명 + +| 컴포넌트 | 역할 | +|---------|------| +| Flow List | 등록된 플로우 목록 조회, 검색, 필터링 | +| Flow Editor | JSON 기반 플로우 정의 생성/수정, 구문 검증 | +| Flow Executor | 플로우 실행, 실시간 진행상황 표시 | +| FlowTesterService | 비즈니스 로직 처리 (CRUD, 실행, 바인딩) | +| VariableBinder | `{{...}}` 변수 파싱 및 치환 | + +--- + +## 3. 데이터베이스 스키마 + +### 3.1 admin_api_flows 테이블 + +```sql +CREATE TABLE admin_api_flows ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, -- 플로우 이름 + description TEXT, -- 설명 + category VARCHAR(50), -- 카테고리 (선택) + flow_definition JSON NOT NULL, -- 플로우 정의 (JSON) + is_active BOOLEAN DEFAULT 1, -- 활성화 여부 + created_by INTEGER, -- 생성자 + updated_by INTEGER, -- 수정자 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 인덱스 +CREATE INDEX idx_admin_api_flows_category ON admin_api_flows(category); +CREATE INDEX idx_admin_api_flows_is_active ON admin_api_flows(is_active); +CREATE INDEX idx_admin_api_flows_name ON admin_api_flows(name); +``` + +### 3.2 admin_api_flow_runs 테이블 + +```sql +CREATE TABLE admin_api_flow_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + flow_id INTEGER NOT NULL, -- 플로우 ID + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, RUNNING, SUCCESS, FAILED, PARTIAL + started_at TIMESTAMP, -- 시작 시간 + completed_at TIMESTAMP, -- 완료 시간 + duration_ms INTEGER, -- 실행 시간 (ms) + total_steps INTEGER, -- 총 단계 수 + completed_steps INTEGER DEFAULT 0, -- 완료된 단계 수 + failed_step INTEGER, -- 실패한 단계 (있는 경우) + execution_log JSON, -- 실행 로그 (단계별 결과) + input_variables JSON, -- 입력 변수 (실행 시 제공) + error_message TEXT, -- 에러 메시지 + executed_by INTEGER, -- 실행자 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (flow_id) REFERENCES admin_api_flows(id) ON DELETE CASCADE +); + +-- 인덱스 +CREATE INDEX idx_admin_api_flow_runs_flow_id ON admin_api_flow_runs(flow_id); +CREATE INDEX idx_admin_api_flow_runs_status ON admin_api_flow_runs(status); +CREATE INDEX idx_admin_api_flow_runs_created_at ON admin_api_flow_runs(created_at); +``` + +### 3.3 상태값 정의 + +```php +// FlowRun Status +const STATUS_PENDING = 'PENDING'; // 대기 중 +const STATUS_RUNNING = 'RUNNING'; // 실행 중 +const STATUS_SUCCESS = 'SUCCESS'; // 모든 단계 성공 +const STATUS_FAILED = 'FAILED'; // 단계 실패로 중단 +const STATUS_PARTIAL = 'PARTIAL'; // 일부 성공 (선택적 단계 실패) +``` + +--- + +## 4. JSON 플로우 정의 스키마 + +### 4.1 플로우 정의 구조 + +```json +{ + "version": "1.0", + "meta": { + "author": "사용자명", + "created": "2025-11-27", + "tags": ["item-master", "integration-test"] + }, + "config": { + "baseUrl": "https://sam.kr/api/v1", + "timeout": 30000, + "stopOnFailure": true, + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + } + }, + "variables": { + "testPrefix": "TEST_", + "timestamp": "{{$timestamp}}" + }, + "steps": [ + { + "id": "step1", + "name": "페이지 생성", + "description": "새 아이템 페이지 생성", + "method": "POST", + "endpoint": "/item-master/pages", + "headers": {}, + "body": { + "page_name": "{{variables.testPrefix}}Page_{{variables.timestamp}}", + "item_type": "PRODUCT", + "absolute_path": "/test/page" + }, + "expect": { + "status": [200, 201], + "jsonPath": { + "$.success": true, + "$.data.id": "@isNumber" + } + }, + "extract": { + "pageId": "$.data.id", + "pageName": "$.data.page_name" + }, + "continueOnFailure": false, + "retries": 0, + "delay": 0 + }, + { + "id": "step2", + "name": "섹션 생성", + "description": "페이지에 섹션 추가", + "dependsOn": ["step1"], + "method": "POST", + "endpoint": "/item-master/pages/{{step1.pageId}}/sections", + "body": { + "title": "기본 정보", + "type": "form" + }, + "expect": { + "status": [200, 201] + }, + "extract": { + "sectionId": "$.data.id" + } + }, + { + "id": "step3", + "name": "필드 생성", + "description": "섹션에 필드 추가", + "dependsOn": ["step2"], + "method": "POST", + "endpoint": "/item-master/sections/{{step2.sectionId}}/fields", + "body": { + "field_name": "제품명", + "field_type": "text", + "is_required": true + }, + "expect": { + "status": [200, 201] + }, + "extract": { + "fieldId": "$.data.id" + } + }, + { + "id": "cleanup", + "name": "테스트 데이터 정리", + "description": "생성된 페이지 삭제", + "dependsOn": ["step3"], + "method": "DELETE", + "endpoint": "/item-master/pages/{{step1.pageId}}", + "expect": { + "status": [200, 204] + }, + "continueOnFailure": true + } + ] +} +``` + +### 4.2 스키마 상세 설명 + +#### 4.2.1 Step 속성 + +| 속성 | 타입 | 필수 | 설명 | +|------|------|------|------| +| id | string | O | 고유 식별자 (변수 참조용) | +| name | string | O | 단계 이름 (UI 표시용) | +| description | string | X | 상세 설명 | +| dependsOn | string[] | X | 의존하는 이전 단계 ID 목록 | +| method | string | O | HTTP 메서드 (GET, POST, PUT, PATCH, DELETE) | +| endpoint | string | O | API 엔드포인트 (변수 치환 가능) | +| headers | object | X | 추가 헤더 | +| body | object | X | 요청 바디 (변수 치환 가능) | +| expect | object | X | 기대 응답 검증 | +| extract | object | X | 응답에서 추출할 변수 | +| continueOnFailure | boolean | X | 실패 시 계속 진행 여부 (기본: false) | +| retries | number | X | 재시도 횟수 (기본: 0) | +| delay | number | X | 실행 전 지연 시간 (ms) | + +#### 4.2.2 Expect 검증 옵션 + +| 속성 | 타입 | 설명 | +|------|------|------| +| status | number[] | 허용되는 HTTP 상태 코드 | +| jsonPath | object | JSONPath 기반 값 검증 | + +**JSONPath 검증 연산자**: +- 직접 값: `"$.success": true` +- 타입 체크: `"$.data.id": "@isNumber"` +- 존재 체크: `"$.data.items": "@exists"` +- 배열 길이: `"$.data.items": "@minLength:1"` +- 정규식: `"$.data.code": "@regex:^[A-Z]+$"` + +#### 4.2.3 변수 바인딩 문법 + +| 패턴 | 설명 | 예시 | +|------|------|------| +| `{{variables.xxx}}` | 전역 변수 | `{{variables.testPrefix}}` | +| `{{stepN.xxx}}` | 이전 단계 추출값 | `{{step1.pageId}}` | +| `{{stepN.response.xxx}}` | 이전 단계 전체 응답 | `{{step1.response.data.name}}` | +| `{{$timestamp}}` | 현재 타임스탬프 | `1701100800` | +| `{{$uuid}}` | 랜덤 UUID | `550e8400-e29b-41d4-a716-446655440000` | +| `{{$random:N}}` | N자리 랜덤 숫자 | `{{$random:6}}` → `482916` | + +--- + +## 5. 변수 바인딩 엔진 + +### 5.1 처리 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Variable Binding Engine │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Input: "POST /pages/{{step1.pageId}}/sections" │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ 1. Pattern Detection │ │ +│ │ /\{\{([^}]+)\}\}/g │ │ +│ │ Found: ["{{step1.pageId}}"] │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ 2. Reference Resolution │ │ +│ │ "step1.pageId" → context.steps.step1 │ │ +│ │ → extracted.pageId = 42 │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ 3. Value Substitution │ │ +│ │ "{{step1.pageId}}" → "42" │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Output: "POST /pages/42/sections" │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 5.2 구현 클래스 + +```php +context, $key, $value); + } + + /** + * 단계 결과 저장 + */ + public function setStepResult(string $stepId, array $extracted, array $fullResponse): void + { + $this->context['steps'][$stepId] = [ + 'extracted' => $extracted, + 'response' => $fullResponse, + ]; + } + + /** + * 문자열 내 모든 변수 치환 + */ + public function bind(mixed $input): mixed + { + if (is_string($input)) { + return $this->bindString($input); + } + + if (is_array($input)) { + return array_map(fn($v) => $this->bind($v), $input); + } + + return $input; + } + + /** + * 문자열 내 변수 패턴 치환 + */ + private function bindString(string $input): string + { + // 내장 변수 처리 + $input = $this->resolveBuiltins($input); + + // 컨텍스트 변수 처리 + return preg_replace_callback( + '/\{\{([^}]+)\}\}/', + fn($matches) => $this->resolveReference($matches[1]), + $input + ); + } + + /** + * 내장 변수 처리 ($timestamp, $uuid, $random:N) + */ + private function resolveBuiltins(string $input): string + { + $input = str_replace('{{$timestamp}}', time(), $input); + $input = str_replace('{{$uuid}}', (string) \Illuminate\Support\Str::uuid(), $input); + $input = preg_replace_callback( + '/\{\{\$random:(\d+)\}\}/', + fn($m) => str_pad(random_int(0, pow(10, $m[1]) - 1), $m[1], '0', STR_PAD_LEFT), + $input + ); + + return $input; + } + + /** + * 참조 경로 해석 (step1.pageId → 실제 값) + */ + private function resolveReference(string $path): mixed + { + // variables.xxx → $this->context['variables']['xxx'] + if (str_starts_with($path, 'variables.')) { + return data_get($this->context, $path, ''); + } + + // stepN.xxx → $this->context['steps']['stepN']['extracted']['xxx'] + if (preg_match('/^(step\w+)\.(.+)$/', $path, $m)) { + $stepId = $m[1]; + $subPath = $m[2]; + + // stepN.response.xxx → 전체 응답에서 추출 + if (str_starts_with($subPath, 'response.')) { + $responsePath = substr($subPath, 9); + return data_get($this->context['steps'][$stepId]['response'] ?? [], $responsePath, ''); + } + + // stepN.xxx → extracted에서 추출 + return data_get($this->context['steps'][$stepId]['extracted'] ?? [], $subPath, ''); + } + + return data_get($this->context, $path, ''); + } +} +``` + +--- + +## 6. 플로우 실행 엔진 + +### 6.1 실행 흐름 + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Flow Execution Engine │ +├──────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Load Flow │───▶│ Build Order │───▶│ Init Binder │ │ +│ │ Definition │ │ (TopSort) │ │ + Variables │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ For Each Step (Ordered) │ │ +│ │ ┌─────────────────────────────────────────────────────┐ │ │ +│ │ │ 1. Check Dependencies (all completed?) │ │ │ +│ │ │ 2. Apply Delay (if configured) │ │ │ +│ │ │ 3. Bind Variables (endpoint, headers, body) │ │ │ +│ │ │ 4. Execute HTTP Request │ │ │ +│ │ │ 5. Validate Response (expect) │ │ │ +│ │ │ 6. Extract Variables (extract) │ │ │ +│ │ │ 7. Store Result in Context │ │ │ +│ │ │ 8. Update Progress │ │ │ +│ │ └─────────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ┌─────────────┴─────────────┐ │ │ +│ │ ▼ ▼ │ │ +│ │ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Success │ │ Failed │ │ │ +│ │ │ → Next │ │ → Check │ │ │ +│ │ │ Step │ │ Config │ │ │ +│ │ └──────────┘ └────┬─────┘ │ │ +│ │ │ │ │ +│ │ ┌───────────────┴───────────────┐ │ │ +│ │ ▼ ▼ │ │ +│ │ ┌────────────┐ ┌──────────┐│ │ +│ │ │ Continue │ │ Stop ││ │ +│ │ │ On Failure │ │ Execution││ │ +│ │ └────────────┘ └──────────┘│ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────┐ │ +│ │ Save Run │ │ +│ │ Results │ │ +│ └─────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### 6.2 의존성 정렬 (Topological Sort) + +```php + $degree) { + if ($degree === 0) { + $queue[] = $id; + } + } + + $sorted = []; + while (!empty($queue)) { + $current = array_shift($queue); + $sorted[] = $current; + + foreach ($graph[$current] as $neighbor) { + $inDegree[$neighbor]--; + if ($inDegree[$neighbor] === 0) { + $queue[] = $neighbor; + } + } + } + + if (count($sorted) !== count($steps)) { + throw new \Exception("Circular dependency detected in flow"); + } + + return $sorted; + } +} +``` + +### 6.3 응답 검증기 + +```php +formatList($allowedStatus)}, got {$response['status']}"; + } + } + + // JSONPath 검증 + if (isset($expect['jsonPath'])) { + foreach ($expect['jsonPath'] as $path => $expected) { + $actual = data_get($response['body'], ltrim($path, '$.')); + $pathError = $this->validateValue($actual, $expected, $path); + if ($pathError) { + $errors[] = $pathError; + } + } + } + + return [ + 'success' => empty($errors), + 'errors' => $errors, + ]; + } + + /** + * 개별 값 검증 + */ + private function validateValue(mixed $actual, mixed $expected, string $path): ?string + { + // 직접 값 비교 + if (!is_string($expected) || !str_starts_with($expected, '@')) { + if ($actual !== $expected) { + return "Path {$path}: expected " . json_encode($expected) . ", got " . json_encode($actual); + } + return null; + } + + // 연산자 처리 + $operator = substr($expected, 1); + + return match (true) { + $operator === 'exists' => $actual === null ? "Path {$path}: expected to exist" : null, + $operator === 'isNumber' => !is_numeric($actual) ? "Path {$path}: expected number, got " . gettype($actual) : null, + $operator === 'isString' => !is_string($actual) ? "Path {$path}: expected string, got " . gettype($actual) : null, + $operator === 'isArray' => !is_array($actual) ? "Path {$path}: expected array, got " . gettype($actual) : null, + $operator === 'isBoolean' => !is_bool($actual) ? "Path {$path}: expected boolean, got " . gettype($actual) : null, + str_starts_with($operator, 'minLength:') => $this->validateMinLength($actual, $operator, $path), + str_starts_with($operator, 'regex:') => $this->validateRegex($actual, $operator, $path), + default => "Path {$path}: unknown operator @{$operator}", + }; + } + + private function validateMinLength(mixed $actual, string $operator, string $path): ?string + { + $min = (int) substr($operator, 10); + if (!is_array($actual) || count($actual) < $min) { + return "Path {$path}: expected array with min length {$min}"; + } + return null; + } + + private function validateRegex(mixed $actual, string $operator, string $path): ?string + { + $pattern = '/' . substr($operator, 6) . '/'; + if (!is_string($actual) || !preg_match($pattern, $actual)) { + return "Path {$path}: value does not match pattern {$pattern}"; + } + return null; + } + + private function formatList(array $items): string + { + return '[' . implode(', ', $items) . ']'; + } +} +``` + +--- + +## 7. UI 설계 + +### 7.1 화면 구성 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SAM MNG admin@sam.kr ▼ │ +├─────────────┬───────────────────────────────────────────────────────────┤ +│ │ │ +│ 대시보드 │ API Flow Tester │ +│ │ ─────────────────────────────────────────────────────── │ +│ 시스템관리 │ │ +│ ├ 사용자 │ ┌─────────────────────────────────────────────────────┐ │ +│ ├ 역할 │ │ 플로우 목록 [+ 새 플로우] │ │ +│ └ 권한 │ ├─────────────────────────────────────────────────────┤ │ +│ │ │ □ 이름 카테고리 상태 최근실행 액션 │ │ +│ 개발 도구 │ │ ─────────────────────────────────────────────────── │ │ +│ ├ API 플로우│ │ □ ItemMaster item-master 성공 5분 전 ▶ ✎ │ │ +│ 테스터 │ │ □ Auth Flow auth 실패 1시간전 ▶ ✎ │ │ +│ │ │ □ BOM Test bom 대기 - ▶ ✎ │ │ +│ │ │ │ │ +│ │ └─────────────────────────────────────────────────────┘ │ +│ │ │ +└─────────────┴───────────────────────────────────────────────────────────┘ +``` + +### 7.2 플로우 편집기 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 플로우 편집: ItemMaster Integration Test [저장] [×] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 기본 정보 │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 이름: [ItemMaster Integration Test ] │ │ +│ │ 카테고리: [item-master ▼] 설명: [페이지-섹션-필드 통합 테스트]│ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 플로우 정의 (JSON) [검증] [포맷] │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 1 { │ │ +│ │ 2 "version": "1.0", │ │ +│ │ 3 "config": { │ │ +│ │ 4 "baseUrl": "https://sam.kr/api/v1", │ │ +│ │ 5 "timeout": 30000 │ │ +│ │ 6 }, │ │ +│ │ 7 "steps": [ │ │ +│ │ 8 { │ │ +│ │ 9 "id": "step1", │ │ +│ │ 10 "name": "페이지 생성", │ │ +│ │ 11 "method": "POST", │ │ +│ │ ... ... │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ✓ JSON 문법 유효 │ 스텝 4개 │ 의존성 그래프 유효 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 7.3 플로우 실행 화면 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 플로우 실행: ItemMaster Integration Test [×] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 실행 상태 │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ████████████████░░░░░░░░ 3/4 단계 완료 │ │ +│ │ 경과 시간: 2.4s │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 단계별 결과 │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ step1: 페이지 생성 201 Created │ │ +│ │ └─ pageId: 42, pageName: "TEST_Page_1701100800" │ │ +│ │ │ │ +│ │ ✅ step2: 섹션 생성 201 Created │ │ +│ │ └─ sectionId: 108 │ │ +│ │ │ │ +│ │ ✅ step3: 필드 생성 201 Created │ │ +│ │ └─ fieldId: 256 │ │ +│ │ │ │ +│ │ 🔄 cleanup: 테스트 데이터 정리 실행 중... │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 상세 로그 ─────────────────────────────────────────── │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ [14:32:01] step1 시작 │ │ +│ │ [14:32:01] POST /item-master/pages │ │ +│ │ [14:32:01] Response: 201 Created (234ms) │ │ +│ │ [14:32:01] Extracted: pageId=42, pageName=TEST_Page_1701100800 │ │ +│ │ [14:32:01] step2 시작 │ │ +│ │ ... │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ [실행 중지] [다시 실행] │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 7.4 실행 이력 화면 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 실행 이력: ItemMaster Integration Test │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 실행 ID 상태 시작 시간 소요시간 실행자 │ │ +│ │ ─────────────────────────────────────────────────────────────── │ │ +│ │ #127 ✅ 성공 2025-11-27 14:32:01 2.8s admin │ │ +│ │ #126 ❌ 실패 2025-11-27 14:28:15 1.2s admin │ │ +│ │ #125 ✅ 성공 2025-11-27 13:45:00 2.6s admin │ │ +│ │ #124 ⚠️ 부분 2025-11-27 11:20:33 3.1s admin │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 실행 #126 상세 ──────────────────────────────── │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 실패 단계: step2 (섹션 생성) │ │ +│ │ 에러 메시지: Expected status [200, 201], got 422 │ │ +│ │ │ │ +│ │ 응답 내용: │ │ +│ │ { │ │ +│ │ "success": false, │ │ +│ │ "message": "validation_error", │ │ +│ │ "errors": { "type": ["type 필드는 필수입니다."] } │ │ +│ │ } │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 8. API 엔드포인트 + +### 8.1 MNG 내부 라우트 + +```php +// routes/web.php + +Route::prefix('dev-tools')->name('dev-tools.')->middleware(['auth'])->group(function () { + // 플로우 목록 + Route::get('/flow-tester', [FlowTesterController::class, 'index']) + ->name('flow-tester.index'); + + // 플로우 생성 폼 + Route::get('/flow-tester/create', [FlowTesterController::class, 'create']) + ->name('flow-tester.create'); + + // 플로우 저장 + Route::post('/flow-tester', [FlowTesterController::class, 'store']) + ->name('flow-tester.store'); + + // 플로우 상세/편집 + Route::get('/flow-tester/{id}', [FlowTesterController::class, 'edit']) + ->name('flow-tester.edit'); + + // 플로우 수정 + Route::put('/flow-tester/{id}', [FlowTesterController::class, 'update']) + ->name('flow-tester.update'); + + // 플로우 삭제 + Route::delete('/flow-tester/{id}', [FlowTesterController::class, 'destroy']) + ->name('flow-tester.destroy'); + + // 플로우 복제 + Route::post('/flow-tester/{id}/clone', [FlowTesterController::class, 'clone']) + ->name('flow-tester.clone'); + + // JSON 검증 (HTMX) + Route::post('/flow-tester/validate-json', [FlowTesterController::class, 'validateJson']) + ->name('flow-tester.validate-json'); + + // 플로우 실행 + Route::post('/flow-tester/{id}/run', [FlowTesterController::class, 'run']) + ->name('flow-tester.run'); + + // 실행 상태 조회 (Polling/SSE) + Route::get('/flow-tester/runs/{runId}/status', [FlowTesterController::class, 'runStatus']) + ->name('flow-tester.run-status'); + + // 실행 이력 + Route::get('/flow-tester/{id}/history', [FlowTesterController::class, 'history']) + ->name('flow-tester.history'); + + // 실행 상세 + Route::get('/flow-tester/runs/{runId}', [FlowTesterController::class, 'runDetail']) + ->name('flow-tester.run-detail'); +}); +``` + +### 8.2 HTMX 통합 + +```html + +
+
+ + + + + +
+
+``` + +--- + +## 9. 파일 구조 + +``` +mng/ +├── app/ +│ ├── Http/ +│ │ └── Controllers/ +│ │ └── FlowTesterController.php +│ ├── Models/ +│ │ ├── AdminApiFlow.php +│ │ └── AdminApiFlowRun.php +│ └── Services/ +│ └── FlowTester/ +│ ├── FlowTesterService.php # 메인 서비스 +│ ├── FlowExecutor.php # 실행 엔진 +│ ├── VariableBinder.php # 변수 바인딩 +│ ├── DependencyResolver.php # 의존성 정렬 +│ ├── ResponseValidator.php # 응답 검증 +│ └── HttpClient.php # HTTP 클라이언트 래퍼 +├── database/ +│ └── migrations/ +│ └── xxxx_create_admin_api_flow_tables.php +├── resources/ +│ └── views/ +│ └── dev-tools/ +│ └── flow-tester/ +│ ├── index.blade.php # 플로우 목록 +│ ├── create.blade.php # 생성 폼 +│ ├── edit.blade.php # 편집 폼 +│ ├── run.blade.php # 실행 화면 +│ ├── history.blade.php # 실행 이력 +│ └── partials/ +│ ├── flow-list.blade.php +│ ├── step-result.blade.php +│ └── run-status.blade.php +└── public/ + └── js/ + └── flow-tester/ + ├── json-editor.js # JSON 에디터 기능 + └── flow-runner.js # 실행 UI 제어 +``` + +--- + +## 10. 구현 일정 (4-5일) + +### Day 1: 기반 구축 +- [ ] 데이터베이스 마이그레이션 생성 및 실행 +- [ ] Model 클래스 생성 (AdminApiFlow, AdminApiFlowRun) +- [ ] 사이드바 메뉴 추가 ("개발 도구" 그룹) +- [ ] 기본 라우트 설정 +- [ ] FlowTesterController 스캐폴딩 + +### Day 2: 핵심 서비스 +- [ ] VariableBinder 구현 (변수 바인딩 엔진) +- [ ] DependencyResolver 구현 (의존성 정렬) +- [ ] ResponseValidator 구현 (응답 검증) +- [ ] HttpClient 구현 (API 호출 래퍼) +- [ ] FlowExecutor 구현 (실행 엔진) + +### Day 3: CRUD UI +- [ ] 플로우 목록 화면 (index.blade.php) +- [ ] 플로우 생성/편집 화면 (create.blade.php, edit.blade.php) +- [ ] JSON 에디터 통합 (CodeMirror 또는 Monaco) +- [ ] JSON 실시간 검증 (HTMX) +- [ ] 플로우 삭제/복제 기능 + +### Day 4: 실행 및 모니터링 +- [ ] 플로우 실행 화면 (run.blade.php) +- [ ] 실시간 진행상황 표시 (Polling/SSE) +- [ ] 단계별 결과 표시 +- [ ] 실행 이력 화면 (history.blade.php) +- [ ] 실행 상세 보기 + +### Day 5: 마무리 및 테스트 +- [ ] 에러 핸들링 강화 +- [ ] UI 폴리싱 +- [ ] 테스트 플로우 작성 (ItemMaster 예제) +- [ ] 문서화 +- [ ] 버그 수정 + +--- + +## 11. 기술 스택 + +| 영역 | 기술 | +|------|------| +| Backend | Laravel 12 + PHP 8.4 | +| Database | MySQL (samdb - admin_* 접두사 테이블) | +| Frontend | Blade + Tailwind CSS + DaisyUI | +| Interactivity | HTMX 1.9 | +| JSON Editor | CodeMirror 6 또는 Monaco Editor (lite) | +| HTTP Client | Guzzle 또는 Laravel HTTP Client | + +--- + +## 12. 보안 고려사항 + +### 12.1 접근 제어 +- MNG 관리자 인증 필수 +- 특정 권한 보유자만 접근 가능 (설정 가능) + +### 12.2 API 호출 보안 +- 저장된 API 키 사용 (환경변수에서 로드) +- 외부 URL 호출 제한 (화이트리스트) +- 민감 데이터 마스킹 (로그에서 Authorization 헤더 등) + +### 12.3 데이터 보호 +- 실행 로그 보존 기간 설정 +- 민감 정보 저장 금지 (실제 비밀번호 등) + +--- + +## 13. 확장 가능성 (향후) + +### Phase 2 (선택적) +- 스케줄 기반 자동 실행 +- Slack/Teams 알림 연동 +- 플로우 템플릿 공유 +- 환경별 설정 (dev/staging/prod) + +### Phase 3 (AI 통합 - A 버전) +- Claude API 연동으로 플로우 자동 생성 +- 에러 분석 및 수정 제안 +- 테스트 데이터 자동 생성 + +--- + +## 14. 참고 자료 + +### 유사 도구 +- Postman Collections & Runner +- Insomnia Request Chaining +- Bruno Sequential Requests +- k6 Load Testing Scripts + +### 참고 문서 +- [HTMX 공식 문서](https://htmx.org/docs/) +- [Laravel HTTP Client](https://laravel.com/docs/12.x/http-client) +- [JSONPath 표준](https://goessner.net/articles/JsonPath/) + +--- + +**문서 끝** diff --git a/docs/DEV_PROCESS.md b/docs/DEV_PROCESS.md new file mode 100644 index 00000000..e456ae39 --- /dev/null +++ b/docs/DEV_PROCESS.md @@ -0,0 +1,768 @@ +# MNG 프로젝트 개발 프로세스 + +## 🎯 개발 철학 + +``` +API 우선 → HTMX 연동 → 단순하고 수정 용이한 코드 +``` + +### 핵심 원칙 +1. **API First**: 모든 기능은 API로 먼저 개발 +2. **Service-First**: 비즈니스 로직은 Service에만 +3. **HTMX Driven**: JS 최소화, HTML 속성으로 인터랙션 +4. **DaisyUI Only**: 커스텀 CSS 금지, DaisyUI 클래스만 사용 + +--- + +## 📐 표준 개발 프로세스 (6단계) + +### Phase 0: 환경 구성 (최초 1회) +**참조**: `claudedocs/mng/SETUP_GUIDE.md` + +```bash +# SETUP_GUIDE.md의 Step 1-10 참조 +# 1. Laravel 프로젝트 생성 +# 2. Docker 설정 파일 생성 +# 3. docker-compose.yml 업데이트 +# 4. nginx.conf 업데이트 +# 5. Tailwind + DaisyUI + HTMX 설정 +# 6. admin/ 모델 복사 +# 7. Docker 빌드 및 실행 +# 8. 동작 확인 (http://mng.sam.kr) + +# 스킬 사용: +/sc:implement "SETUP_GUIDE.md 따라 MNG 환경 구성" +``` + +### Phase 1: 준비 단계 +```bash +# 1. 기능 분석 (Sequential Thinking) +/sc:analyze --think + +# 2. 요구사항 정리 +- 입력: 어떤 데이터를 받는가? +- 처리: 어떤 비즈니스 로직? +- 출력: 어떤 데이터를 반환? +- 화면: 어떤 UI 필요? + +# 3. API 명세 작성 +- 엔드포인트: GET /api/admin/users +- Request: { search: string, role_id?: number } +- Response: { success, data, message, meta } +``` + +### Phase 1: DB & Model (1단계) +```bash +# 1-1. 마이그레이션 확인 +# 기존 테이블 사용? → 마이그레이션 불필요 +# 신규 테이블? → admin_* or stat_* 접두사 + +# 1-2. 모델 확인/생성 +# admin/app/Models에서 복사했는지 확인 +# BelongsToTenant, HasAuditLog 트레잇 적용 + +# 예시: mng/app/Models/User.php +belongsTo(Role::class); + } + + public function department() + { + return $this->belongsTo(Department::class); + } +} +``` + +### Phase 2: Service Layer (2단계) +```bash +# 2-1. Service 생성 (비즈니스 로직) +# mng/app/Services/UserService.php + +where(function ($q) use ($filters) { + $q->where('name', 'like', "%{$filters['search']}%") + ->orWhere('email', 'like', "%{$filters['search']}%"); + }); + } + + // 역할 필터 + if (!empty($filters['role_id'])) { + $query->where('role_id', $filters['role_id']); + } + + return $query->paginate(20); + } + + /** + * 사용자 생성 + */ + public function createUser(array $data): User + { + $data['password'] = Hash::make($data['password']); + $data['tenant_id'] = auth()->user()->tenant_id; + + return User::create($data); + } + + /** + * 사용자 수정 + */ + public function updateUser(User $user, array $data): User + { + if (!empty($data['password'])) { + $data['password'] = Hash::make($data['password']); + } else { + unset($data['password']); + } + + $user->update($data); + return $user->fresh(); + } + + /** + * 사용자 삭제 (Soft Delete) + */ + public function deleteUser(User $user): bool + { + return $user->delete(); + } +} +``` + +### Phase 3: API Controller (3단계) +```bash +# 3-1. FormRequest 생성 +# mng/app/Http/Requests/StoreUserRequest.php + + 'required|string|max:255', + 'email' => 'required|email|unique:users,email', + 'password' => 'required|string|min:8', + 'role_id' => 'required|exists:roles,id', + 'department_id' => 'required|exists:departments,id', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'users.validation.name_required', + 'email.required' => 'users.validation.email_required', + 'email.email' => 'users.validation.email_invalid', + 'email.unique' => 'users.validation.email_unique', + ]; + } +} + +# 3-2. API Controller 생성 +# mng/app/Http/Controllers/Api/Admin/UserController.php + +userService->getUsers($request->all()); + + return response()->json([ + 'success' => true, + 'data' => $users->items(), + 'message' => 'users.retrieved', + 'meta' => [ + 'current_page' => $users->currentPage(), + 'last_page' => $users->lastPage(), + 'per_page' => $users->perPage(), + 'total' => $users->total(), + ], + ]); + } + + /** + * 사용자 생성 (API) + * POST /api/admin/users + */ + public function store(StoreUserRequest $request): JsonResponse + { + $user = $this->userService->createUser($request->validated()); + + return response()->json([ + 'success' => true, + 'data' => $user, + 'message' => 'users.created', + ], 201); + } + + /** + * 사용자 수정 (API) + * PUT /api/admin/users/{user} + */ + public function update(UpdateUserRequest $request, User $user): JsonResponse + { + $user = $this->userService->updateUser($user, $request->validated()); + + return response()->json([ + 'success' => true, + 'data' => $user, + 'message' => 'users.updated', + ]); + } + + /** + * 사용자 삭제 (API) + * DELETE /api/admin/users/{user} + */ + public function destroy(User $user): JsonResponse + { + $this->userService->deleteUser($user); + + return response()->json([ + 'success' => true, + 'message' => 'users.deleted', + ]); + } +} + +# 3-3. 라우트 등록 +# mng/routes/api.php + +Route::middleware(['auth:sanctum', 'admin.permission']) + ->prefix('admin') + ->group(function () { + Route::apiResource('users', UserController::class); + }); +``` + +### Phase 4: Blade + HTMX (4단계) +```bash +# 4-1. HTML 응답용 Controller (선택) +# API + Blade 부분 HTML 반환 + +# mng/app/Http/Controllers/Api/Admin/UserController.php (추가) + +/** + * 사용자 목록 (HTMX용 Blade HTML) + * GET /api/admin/users?format=html + */ +public function index(Request $request) +{ + $users = $this->userService->getUsers($request->all()); + + // HTMX 요청 시 부분 HTML 반환 + if ($request->header('HX-Request')) { + return view('users.partials.table', compact('users')); + } + + // 일반 요청 시 JSON 반환 + return response()->json([ + 'success' => true, + 'data' => $users->items(), + 'message' => 'users.retrieved', + 'meta' => [ + 'current_page' => $users->currentPage(), + 'last_page' => $users->lastPage(), + 'per_page' => $users->perPage(), + 'total' => $users->total(), + ], + ]); +} + +# 4-2. Blade 템플릿 작성 +# mng/resources/views/users/index.blade.php + +@extends('layouts.app') + +@section('content') +
+ {{-- 헤더 --}} +
+

사용자 관리

+ 사용자 추가 +
+ + {{-- 검색/필터 --}} +
+
+
+
+ + + + + +
+
+
+
+ + {{-- 테이블 영역 --}} +
+ {{-- 초기 로드 시 서버에서 HTML 받아서 여기 삽입 --}} +
+ +
+
+
+@endsection + +# 4-3. 부분 템플릿 (HTMX 응답용) +# mng/resources/views/users/partials/table.blade.php + +
+
+
+ + + + + + + + + + + + + + @foreach($users as $user) + + + + + + + + + + @endforeach + +
ID이름이메일역할부서상태작업
{{ $user->id }}{{ $user->name }}{{ $user->email }}{{ $user->role->name }}{{ $user->department->name }} + + {{ $user->is_active ? '활성' : '비활성' }} + + +
+ 수정 + +
+
+
+ + {{-- 페이징 (HTMX) --}} +
+ @if($users->hasPages()) +
+ @foreach($users->getUrlRange(1, $users->lastPage()) as $page => $url) + + @endforeach +
+ @endif +
+
+
+``` + +### Phase 5: 테스트 & 검증 (5단계) +```bash +# 5-1. Feature Test 작성 +# mng/tests/Feature/UserControllerTest.php + +create(); + + $response = $this->actingAs($user) + ->getJson('/api/admin/users'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data', + 'message', + 'meta', + ]); + } + + public function test_사용자_생성() + { + $admin = User::factory()->create(); + $role = Role::factory()->create(); + + $response = $this->actingAs($admin) + ->postJson('/api/admin/users', [ + 'name' => '홍길동', + 'email' => 'hong@example.com', + 'password' => 'password123', + 'role_id' => $role->id, + 'department_id' => 1, + ]); + + $response->assertStatus(201) + ->assertJson([ + 'success' => true, + 'message' => 'users.created', + ]); + + $this->assertDatabaseHas('users', [ + 'email' => 'hong@example.com', + ]); + } +} + +# 5-2. 테스트 실행 +php artisan test --filter=UserControllerTest + +# 5-3. 코드 스타일 검증 +./vendor/bin/pint + +# 5-4. 품질 체크리스트 +□ Service-First (비즈니스 로직 → Service) +□ FormRequest (컨트롤러 검증 금지) +□ BelongsToTenant (multi-tenant 스코프) +□ i18n 키 (하드코딩 금지) +□ Soft Delete (deleted_at) +□ 감사 로그 (HasAuditLog trait) +□ API 응답 형식 ({success, data, message, meta}) +□ HTMX 속성 (hx-get, hx-target, hx-swap) +□ DaisyUI 클래스만 사용 +□ Feature Test 통과 +□ Pint 통과 +``` + +--- + +## 🔄 실전 워크플로 (스킬 활용) + +### 신규 기능 개발 시 +```bash +# Step 1: 기능 분석 및 설계 +/sc:design "사용자 관리 기능" +# → Sequential Thinking으로 요구사항 분석 +# → API 명세 도출 + +# Step 2: 구현 +/sc:implement "사용자 관리 API 구현" +# → Model, Service, Controller, FormRequest 생성 +# → 자동으로 5단계 프로세스 진행 + +# Step 3: Blade + HTMX 구현 +# 직접 작성 (단순하므로 AI 불필요) +# 또는 /sc:implement "사용자 목록 Blade 화면" + +# Step 4: 테스트 +/sc:test "UserController" +# → Feature Test 자동 생성 및 실행 + +# Step 5: 검증 및 커밋 +code-workflow 스킬 사용 +# → 분석 → 수정 → 검증 → 정리 → 커밋 +``` + +### 버그 수정 시 +```bash +# Step 1: 문제 분석 +/sc:troubleshoot "사용자 목록 페이징 안됨" +# → Root Cause 분석 + +# Step 2: 수정 +/sc:improve "UserService 페이징 로직" + +# Step 3: 테스트 +/sc:test + +# Step 4: 커밋 +code-workflow +``` + +### 리팩토링 시 +```bash +/sc:improve --focus quality "UserController" +/sc:analyze --think-hard "전체 아키텍처" +``` + +--- + +## 📋 체크리스트 템플릿 + +### 기능 개발 완료 체크리스트 +``` +기능명: _______________ + +[ ] Phase 1: DB & Model + [ ] 마이그레이션 (필요 시) + [ ] 모델 생성/복사 + [ ] BelongsToTenant 적용 + [ ] HasAuditLog 적용 + [ ] 관계 설정 (belongsTo, hasMany) + +[ ] Phase 2: Service Layer + [ ] Service 생성 + [ ] 비즈니스 로직 구현 + [ ] 트랜잭션 처리 + [ ] 예외 처리 + +[ ] Phase 3: API Controller + [ ] FormRequest 생성 (Validation) + [ ] Controller 생성 + [ ] API 응답 형식 준수 + [ ] i18n 키 사용 + [ ] 라우트 등록 + +[ ] Phase 4: Blade + HTMX + [ ] 메인 페이지 (index.blade.php) + [ ] 부분 템플릿 (partials/*.blade.php) + [ ] HTMX 속성 (hx-get, hx-post, hx-delete) + [ ] DaisyUI 컴포넌트만 사용 + [ ] HX-Request 헤더 처리 + +[ ] Phase 5: 테스트 & 검증 + [ ] Feature Test 작성 + [ ] 테스트 통과 (php artisan test) + [ ] Pint 통과 (./vendor/bin/pint) + [ ] Swagger 문서화 (선택) + +[ ] 커밋 + [ ] code-workflow 스킬 사용 + [ ] CURRENT_WORKS.md 업데이트 +``` + +--- + +## 🎨 HTMX 패턴 라이브러리 + +### 1. 목록 조회 (Load) +```blade +
+ +
+``` + +### 2. 검색/필터 (Submit) +```blade +
+ + +
+``` + +### 3. 생성 (POST) +```blade +
+ + +
+``` + +### 4. 수정 (PUT) +```blade +
+ + +
+``` + +### 5. 삭제 (DELETE) +```blade + +``` + +### 6. 무한 스크롤 +```blade +
+ 더보기... +
+``` + +### 7. 폴링 (자동 갱신) +```blade +
+ 통계: {{ $stats }} +
+``` + +### 8. 디바운싱 (입력 지연) +```blade + +``` + +--- + +## 🔧 개발 환경 설정 + +### 필수 패키지 설치 +```bash +# Composer +composer require laravel/sanctum +composer require darkaonline/l5-swagger +composer require --dev laravel/pint + +# NPM +npm install -D tailwindcss daisyui @tailwindcss/forms +npm install htmx.org +``` + +### HTMX 설정 +```js +// resources/js/app.js +import htmx from 'htmx.org'; +window.htmx = htmx; + +// HTMX 전역 설정 +document.addEventListener('DOMContentLoaded', () => { + // CSRF 토큰 자동 추가 + document.body.addEventListener('htmx:configRequest', (event) => { + event.detail.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content; + }); +}); +``` + +### Blade 레이아웃 +```blade + + + + + + {{ config('app.name') }} + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + @yield('content') + + +``` + +--- + +## 📝 다음 단계 + +1. **Phase 1 시작**: Laravel 프로젝트 생성 및 환경 구성 +2. **인증 구현**: 로그인 API + Blade 화면 +3. **첫 기능 개발**: 사용자 관리 (이 프로세스 적용) + +--- + +**작성일**: 2025-01-20 +**버전**: 1.0 +**기술 스택**: Laravel 12 + MySQL 8.0 + HTMX + DaisyUI +**목표**: API 우선, 단순함, 수정 용이성 \ No newline at end of file diff --git a/docs/HTMX_API_PATTERN.md b/docs/HTMX_API_PATTERN.md new file mode 100644 index 00000000..6f3435be --- /dev/null +++ b/docs/HTMX_API_PATTERN.md @@ -0,0 +1,538 @@ +# MNG HTMX + API 패턴 가이드 + +**작성일:** 2025-01-24 +**목적:** MNG 프로젝트의 표준 HTMX + API 패턴 문서화 (Tenant 패턴 기반) + +**관련 문서:** +- [LAYOUT_PATTERN.md](./LAYOUT_PATTERN.md) - 페이지 레이아웃 및 Tenant Selector 패턴 +- [99_TECHNICAL_STANDARDS.md](./99_TECHNICAL_STANDARDS.md) - SAM API Rules 기반 기술 표준 + +--- + +## 📋 목차 + +1. [패턴 개요](#1-패턴-개요) +2. [아키텍처 구조](#2-아키텍처-구조) +3. [구현 가이드](#3-구현-가이드) +4. [파일 구조](#4-파일-구조) +5. [체크리스트](#5-체크리스트) + +--- + +## 1. 패턴 개요 + +### 1.1 왜 HTMX + API 패턴인가? + +**MNG 프로젝트의 표준 아키텍처 패턴입니다.** + +- **일관성**: 모든 CRUD 기능이 동일한 패턴 사용 +- **성능**: 페이지 전체 리로드 없이 동적 업데이트 +- **유지보수성**: Blade 템플릿 + HTMX로 간단한 인터랙션 +- **확장성**: API는 HTMX와 독립적으로 사용 가능 + +### 1.2 기본 원칙 + +1. **Blade View는 화면만 담당** - 데이터 처리 로직 없음 +2. **API Controller는 HTMX와 JSON 모두 지원** +3. **HTMX 요청 시 HTML partial 반환** +4. **일반 요청 시 JSON 반환** + +--- + +## 2. 아키텍처 구조 + +### 2.1 전체 흐름도 + +``` +[Browser] + ↓ (HTMX Request with HX-Request header) +[Route: web.php] + ↓ (Blade View 반환) +[Controller: RoleController] + ↓ (view('roles.index') - 화면만) +[Blade View: roles/index.blade.php] + ↓ (hx-get="/api/admin/roles") +[API Route: api.php] + ↓ (API 엔드포인트) +[Api\Admin\RoleController] + ↓ (Service 호출) +[RoleService] + ↓ (비즈니스 로직) +[Database] + ↓ +[RoleService] + ↓ (데이터 반환) +[Api\Admin\RoleController] + ↓ (HTMX 요청 감지: HX-Request header) + ↓ (HTML partial 렌더링) +[Blade Partial: roles/partials/table.blade.php] + ↓ (JSON with html) +[Browser - HTMX] + ↓ (DOM 업데이트: #role-table) +[User sees updated table] +``` + +### 2.2 컨트롤러 분리 + +#### Blade Controller (화면 전용) +```php +// app/Http/Controllers/RoleController.php +class RoleController extends Controller +{ + public function index(): View + { + return view('roles.index'); // 화면만 반환 + } + + public function create(): View + { + return view('roles.create'); + } + + public function edit(int $id): View + { + $role = $this->roleService->getRoleById($id); + return view('roles.edit', compact('role')); + } +} +``` + +#### API Controller (데이터 처리) +```php +// app/Http/Controllers/Api/Admin/RoleController.php +class RoleController extends Controller +{ + public function index(Request $request): JsonResponse + { + $roles = $this->roleService->getRoles($request->all()); + + // HTMX 요청 감지 + if ($request->header('HX-Request')) { + $html = view('roles.partials.table', compact('roles'))->render(); + return response()->json(['html' => $html]); + } + + // 일반 API 요청 + return response()->json([ + 'success' => true, + 'data' => $roles->items(), + 'meta' => [/*...*/], + ]); + } + + public function store(StoreRoleRequest $request): JsonResponse + { + $role = $this->roleService->createRole($request->validated()); + + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '역할이 생성되었습니다.', + 'redirect' => route('roles.index'), + ]); + } + + return response()->json([ + 'success' => true, + 'data' => $role, + ], 201); + } + + public function destroy(Request $request, int $id): JsonResponse + { + $this->roleService->deleteRole($id); + + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '역할이 삭제되었습니다.', + 'action' => 'remove', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '역할이 삭제되었습니다.', + ]); + } +} +``` + +--- + +## 3. 구현 가이드 + +### 3.1 Blade View 구조 + +#### index.blade.php (메인 화면) +```blade +@extends('layouts.app') + +@section('content') +
+

🔑 역할 관리

+ + +
+ + +
+ + +
+ +
+
+
+
+
+@endsection + +@push('scripts') + + +@endpush +``` + +#### partials/table.blade.php (HTMX 응답용 HTML partial) +```blade + + + + + + + + + + + + @forelse($roles as $role) + + + + + + + + @empty + + + + @endforelse + +
ID이름설명권한 수액션
{{ $role->id }}{{ $role->name }}{{ $role->description }}{{ $role->permissions_count }} + 수정 + +
등록된 역할이 없습니다.
+ + +@include('partials.pagination', [ + 'paginator' => $roles, + 'target' => '#role-table', + 'includeForm' => '#filterForm' +]) +``` + +### 3.2 라우트 설정 + +#### web.php (Blade 화면 라우트) +```php +Route::middleware('auth')->group(function () { + Route::prefix('roles')->name('roles.')->group(function () { + Route::get('/', [RoleController::class, 'index'])->name('index'); + Route::get('/create', [RoleController::class, 'create'])->name('create'); + Route::get('/{id}/edit', [RoleController::class, 'edit'])->name('edit'); + }); +}); +``` + +#### api.php (API 엔드포인트) +```php +Route::middleware(['web', 'auth'])->prefix('admin')->name('api.admin.')->group(function () { + Route::prefix('roles')->name('roles.')->group(function () { + Route::get('/', [RoleController::class, 'index'])->name('index'); + Route::post('/', [RoleController::class, 'store'])->name('store'); + Route::get('/{id}', [RoleController::class, 'show'])->name('show'); + Route::put('/{id}', [RoleController::class, 'update'])->name('update'); + Route::delete('/{id}', [RoleController::class, 'destroy'])->name('destroy'); + }); +}); +``` + +### 3.3 HTMX 핵심 개념 + +#### hx-get, hx-post, hx-put, hx-delete +```html + +
로딩 중...
+ + +
+ + +
+ + + +``` + +#### hx-trigger +```html + +
+ + +
+ + +
+``` + +#### hx-include +```html + +
+``` + +#### hx-headers +```html + +
+``` + +#### hx-target, hx-swap +```html + + + + +
기본값
+
엘리먼트 자체 교체
+
응답 무시
+``` + +--- + +## 4. 파일 구조 + +### 4.1 표준 디렉토리 구조 + +``` +mng/ +├── app/ +│ ├── Http/ +│ │ ├── Controllers/ +│ │ │ ├── RoleController.php # Blade 화면만 +│ │ │ └── Api/ +│ │ │ └── Admin/ +│ │ │ └── RoleController.php # API 로직 +│ │ └── Requests/ +│ │ ├── StoreRoleRequest.php +│ │ └── UpdateRoleRequest.php +│ ├── Services/ +│ │ └── RoleService.php # 비즈니스 로직 +│ └── Models/ +│ └── Role.php +├── resources/ +│ └── views/ +│ └── roles/ +│ ├── index.blade.php # 메인 화면 +│ ├── create.blade.php # 생성 화면 +│ ├── edit.blade.php # 수정 화면 +│ └── partials/ +│ ├── table.blade.php # HTMX 응답 HTML +│ └── detail.blade.php # (선택사항) +└── routes/ + ├── web.php # Blade 화면 라우트 + └── api.php # API 엔드포인트 +``` + +### 4.2 Tenant 패턴 참고 파일 + +**학습 및 복사 기준:** +- `app/Http/Controllers/TenantController.php` → Blade 컨트롤러 패턴 +- `app/Http/Controllers/Api/Admin/TenantController.php` → API 컨트롤러 패턴 +- `resources/views/tenants/index.blade.php` → HTMX 메인 화면 패턴 +- `resources/views/tenants/partials/table.blade.php` → HTML partial 패턴 + +--- + +## 5. 체크리스트 + +### 5.1 구현 전 확인사항 + +- [ ] **Tenant 패턴 파일 확인**: `tenants/` 디렉토리 구조 참고 +- [ ] **Service 작성 완료**: `RoleService.php` 비즈니스 로직 구현 +- [ ] **FormRequest 작성 완료**: `StoreRoleRequest`, `UpdateRoleRequest` +- [ ] **Model 확인**: `Role.php` 관계 설정 확인 + +### 5.2 컨트롤러 체크리스트 + +**Blade Controller (`app/Http/Controllers/RoleController.php`)** +- [ ] `index()` → `view('roles.index')` 반환만 +- [ ] `create()` → `view('roles.create')` 반환만 +- [ ] `edit($id)` → Service로 데이터 조회 → `view('roles.edit', compact('role'))` + +**API Controller (`app/Http/Controllers/Api/Admin/RoleController.php`)** +- [ ] `index()` → HTMX 요청 감지 (`$request->header('HX-Request')`) +- [ ] HTMX 요청 시 → `view('roles.partials.table')->render()` → JSON 반환 +- [ ] 일반 요청 시 → JSON 데이터 반환 +- [ ] `store()`, `update()`, `destroy()` → HTMX 지원 +- [ ] HTMX 응답 시 `redirect` 또는 `action` 포함 + +### 5.3 Blade View 체크리스트 + +**index.blade.php** +- [ ] `@extends('layouts.app')` 상속 +- [ ] 필터 폼 `
` 생성 +- [ ] HTMX 동적 영역 `
` 생성 +- [ ] `hx-get="/api/admin/roles"` 설정 +- [ ] `hx-trigger="load, filterSubmit from:body"` 설정 +- [ ] `hx-include="#filterForm"` 설정 +- [ ] `hx-headers` CSRF 토큰 포함 +- [ ] 로딩 스피너 추가 +- [ ] `@push('scripts')` HTMX 스크립트 추가 +- [ ] 폼 제출 이벤트 핸들러 (`filterSubmit` 트리거) +- [ ] HTMX 응답 처리 (`htmx:afterSwap`) +- [ ] 삭제 확인 함수 (`confirmDelete`) + +**partials/table.blade.php** +- [ ] `` 구조 생성 +- [ ] `@forelse` 루프로 데이터 출력 +- [ ] `@empty` 케이스 처리 +- [ ] 액션 버튼 (수정, 삭제) +- [ ] 삭제 버튼 `onclick="confirmDelete()"` 연결 +- [ ] 페이지네이션 `@include('partials.pagination')` + +### 5.4 라우트 체크리스트 + +**web.php** +- [ ] `Route::middleware('auth')` 적용 +- [ ] `Route::prefix('roles')->name('roles.')` 그룹 +- [ ] `GET /roles` → `index()` +- [ ] `GET /roles/create` → `create()` +- [ ] `GET /roles/{id}/edit` → `edit()` + +**api.php** +- [ ] `Route::middleware(['web', 'auth'])->prefix('admin')` 적용 +- [ ] `Route::prefix('roles')->name('api.admin.roles.')` 그룹 +- [ ] `GET /api/admin/roles` → `index()` +- [ ] `POST /api/admin/roles` → `store()` +- [ ] `GET /api/admin/roles/{id}` → `show()` +- [ ] `PUT /api/admin/roles/{id}` → `update()` +- [ ] `DELETE /api/admin/roles/{id}` → `destroy()` + +### 5.5 테스트 체크리스트 + +- [ ] 브라우저에서 `/roles` 접근 → index 화면 로드 +- [ ] HTMX 자동 로드 → 테이블 표시 +- [ ] 검색 필터 동작 → 테이블 업데이트 +- [ ] 삭제 버튼 → 확인 다이얼로그 → 테이블 업데이트 +- [ ] 페이지네이션 동작 +- [ ] 개발자 도구 Network 탭 → `HX-Request` 헤더 확인 +- [ ] API 응답 JSON 구조 확인 (`{html: "..."}`) + +--- + +## 6. 참고사항 + +### 6.1 HTMX vs 전통적 방식 비교 + +| 항목 | 전통적 방식 | HTMX 방식 | +|------|------------|-----------| +| **폼 제출** | `` → 전체 페이지 리로드 | `hx-get` → 부분 업데이트 | +| **데이터 로딩** | Controller에서 직접 데이터 전달 | API 호출 → HTML partial 반환 | +| **삭제 동작** | `` + `@method('DELETE')` | `htmx.ajax('DELETE')` | +| **검색 필터** | 페이지 리로드 + 쿼리스트링 | HTMX 트리거 → 부분 업데이트 | + +### 6.2 주의사항 + +1. **HTMX 요청 감지 필수** + ```php + if ($request->header('HX-Request')) { + // HTMX 전용 로직 + } + ``` + +2. **CSRF 토큰 포함 필수** + ```html + hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}' + ``` + +3. **JSON 응답 구조 일관성** + ```json + { + "html": "
...
", + "success": true, + "message": "작업 완료" + } + ``` + +4. **Blade와 API Controller 분리** + - Blade Controller: 화면만 반환 + - API Controller: 데이터 처리 + HTMX/JSON 응답 + +--- + +## 7. 마이그레이션 가이드 (Admin → MNG) + +### 7.1 작업 순서 + +1. **DB 확인** - 테이블이 이미 존재하는지 확인 (migrations 실행 불필요) +2. **Admin 파일 참고** - Controller, Service, Model 복사/참고 +3. **패턴 적용** - HTMX + API 패턴으로 변환 +4. **테스트** - 브라우저에서 동작 확인 + +### 7.2 마이그레이션 체크리스트 + +- [ ] DB 테이블 존재 확인 (`roles`, `permissions`, `role_has_permissions`) +- [ ] Admin Model 참고 (`admin/app/Models/Permissions/Role.php`) +- [ ] Admin Controller 참고 (비즈니스 로직 추출) +- [ ] Service 작성 (Admin 로직 → MNG Service) +- [ ] Blade Controller 작성 (화면 반환만) +- [ ] API Controller 작성 (HTMX 패턴) +- [ ] Blade View 작성 (Tenant 패턴 기반) +- [ ] 라우트 등록 (web.php, api.php) +- [ ] 브라우저 테스트 + +--- + +**작성자:** Claude +**최종 수정일:** 2025-01-24 +**버전:** 1.0 +**참고:** Tenant 관리 시스템 구현 패턴 기반 \ No newline at end of file diff --git a/docs/INDEX.md b/docs/INDEX.md index 402dd3fd..f8557cd6 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -81,9 +81,11 @@ ### 프로젝트 문서 - **[CURRENT_WORKS.md](../CURRENT_WORKS.md)** - 현재 작업 진행 상황 - **[TROUBLESHOOTING.md](./TROUBLESHOOTING.md)** - 트러블슈팅 가이드 - **[MIGRATION_PLAN.md](./MIGRATION_PLAN.md)** - Admin → MNG 마이그레이션 계획 (Phase 4) -- **[claudedocs/mng/MNG_PROJECT_PLAN.md](../../claudedocs/mng/MNG_PROJECT_PLAN.md)** - 전체 프로젝트 계획 -- **[claudedocs/mng/DEV_PROCESS.md](../../claudedocs/mng/DEV_PROCESS.md)** - 개발 프로세스 (HTMX + API 방식) -- **[claudedocs/mng/SETUP_GUIDE.md](../../claudedocs/mng/SETUP_GUIDE.md)** - 초기 설정 가이드 +- **[MNG_PROJECT_PLAN.md](./MNG_PROJECT_PLAN.md)** - 전체 프로젝트 계획 +- **[DEV_PROCESS.md](./DEV_PROCESS.md)** - 개발 프로세스 (HTMX + API 방식) +- **[SETUP_GUIDE.md](./SETUP_GUIDE.md)** - 초기 설정 가이드 +- **[HTMX_API_PATTERN.md](./HTMX_API_PATTERN.md)** - HTMX + API 패턴 가이드 +- **[LAYOUT_PATTERN.md](./LAYOUT_PATTERN.md)** - 레이아웃 패턴 가이드 **SAM 공통 문서:** - **[📊 ../../docs/specs/database-schema.md](../../docs/specs/database-schema.md)** - 데이터베이스 스키마 (Phase 4: 8개 테이블 상세) @@ -246,8 +248,8 @@ ### SAM 공통 문서 - **[docs/specs/database-schema.md](../../docs/specs/database-schema.md)** - DB 스키마 ### MNG 프로젝트 문서 -- **[claudedocs/mng/MNG_PROJECT_PLAN.md](../../claudedocs/mng/MNG_PROJECT_PLAN.md)** - 프로젝트 전체 계획 -- **[claudedocs/mng/DEV_PROCESS.md](../../claudedocs/mng/DEV_PROCESS.md)** - 개발 프로세스 +- **[MNG_PROJECT_PLAN.md](./MNG_PROJECT_PLAN.md)** - 프로젝트 전체 계획 +- **[DEV_PROCESS.md](./DEV_PROCESS.md)** - 개발 프로세스 - **[CURRENT_WORKS.md](../CURRENT_WORKS.md)** - 작업 진행 상황 --- diff --git a/docs/LAYOUT_PATTERN.md b/docs/LAYOUT_PATTERN.md new file mode 100644 index 00000000..b0b106da --- /dev/null +++ b/docs/LAYOUT_PATTERN.md @@ -0,0 +1,504 @@ +# MNG 레이아웃 패턴 가이드 + +**작성일:** 2025-01-24 +**목적:** MNG 프로젝트의 표준 페이지 레이아웃 패턴 문서화 + +--- + +## 📋 목차 + +1. [기본 레이아웃 구조](#1-기본-레이아웃-구조) +2. [Tenant Selector 패턴](#2-tenant-selector-패턴) +3. [페이지별 적용 가이드](#3-페이지별-적용-가이드) +4. [컨텐츠 영역 구조](#4-컨텐츠-영역-구조) +5. [체크리스트](#5-체크리스트) + +--- + +## 1. 기본 레이아웃 구조 + +### 1.1 전체 구조 + +``` +┌─────────────────────────────────────────────────────┐ +│ Header (상단) │ +│ - 로고, 사용자 정보, 알림 등 │ +├──────────┬──────────────────────────────────────────┤ +│ │ │ +│ │ │ +│ │ ┌─────────────────────────────────┐ │ +│ │ │ 테넌트 선택 드롭다운 │ │ +│ Sidebar │ │ [전체보기] [A회사] [B회사] │ │ +│ │ └─────────────────────────────────┘ │ +│ (좌측 │ │ +│ 메뉴) │ │ +│ │ 페이지 제목 [+ 버튼] │ +│ │ │ +│ │ │ +│ │ [검색] [필터1] [필터2] [검색버튼] │ +│ │ │ +│ │ │ +│ │ ┌─────────────────────────────────┐ │ +│ │ │ 데이터 테이블 또는 컨텐츠 │ │ +│ │ │ │ │ +│ │ │ (HTMX 동적 로딩 영역) │ │ +│ │ └─────────────────────────────────┘ │ +│ │ │ +└──────────┴──────────────────────────────────────────┘ +``` + +### 1.2 레이아웃 파일 구조 + +``` +resources/views/ +├── layouts/ +│ └── app.blade.php # 메인 레이아웃 +├── partials/ +│ ├── sidebar.blade.php # 좌측 메뉴 +│ ├── header.blade.php # 상단 헤더 +│ ├── tenant-selector.blade.php # 테넌트 선택기 (공통) +│ └── pagination.blade.php # 페이지네이션 +└── [feature]/ + ├── index.blade.php # 목록 페이지 + ├── create.blade.php # 생성 페이지 + ├── edit.blade.php # 수정 페이지 + └── partials/ + └── table.blade.php # HTMX 응답용 테이블 +``` + +--- + +## 2. Tenant Selector 패턴 + +### 2.1 역할과 목적 + +**Tenant Selector는 모든 데이터 관리 페이지 상단에 위치하는 공통 컴포넌트입니다.** + +- **목적**: 사용자가 특정 테넌트의 데이터만 필터링하여 볼 수 있도록 함 +- **위치**: `@section('content')` 직후, 페이지 컨텐츠 최상단 +- **예외**: 테넌트 관리 페이지 (`tenants/index.blade.php`)는 제외 + +### 2.2 Tenant Selector 구조 + +**파일**: `resources/views/partials/tenant-selector.blade.php` + +```blade + +
+
+
+ +
+
+ ... + +
+ + + @csrf + + +
+ + +
+ @if(session('selected_tenant_id')) + + {{ $currentTenant->company_name }} 데이터만 표시 중 + + @else + 전체 테넌트 데이터 표시 중 + @endif +
+
+
+
+``` + +### 2.3 Tenant Selector 동작 방식 + +1. **드롭다운 변경** → 폼 자동 제출 +2. **POST /tenant/switch** → TenantController@switch +3. **세션 저장** → `session('selected_tenant_id')` +4. **페이지 리로드** → 선택된 테넌트 데이터만 표시 + +### 2.4 백엔드 연동 + +**TenantController@switch** (예시): +```php +public function switch(Request $request): RedirectResponse +{ + $tenantId = $request->input('tenant_id'); + + if ($tenantId === 'all') { + session()->forget('selected_tenant_id'); + } else { + session(['selected_tenant_id' => $tenantId]); + } + + return redirect()->back(); +} +``` + +**Service Layer** (자동 필터링): +```php +public function getRoles(array $filters = []): LengthAwarePaginator +{ + $tenantId = session('selected_tenant_id'); + + $query = Role::query(); + + // Tenant 필터링 + if ($tenantId) { + $query->where('tenant_id', $tenantId); + } + + return $query->paginate(15); +} +``` + +--- + +## 3. 페이지별 적용 가이드 + +### 3.1 일반 데이터 관리 페이지 (Tenant Selector 포함) + +**적용 대상**: 역할, 사용자, 부서, 제품, 자재, BOM, 카테고리 등 + +**템플릿 구조**: +```blade +@extends('layouts.app') + +@section('title', '역할 관리') + +@section('content') + + @include('partials.tenant-selector') + + +
+

🔑 역할 관리

+ + + 새 역할 + +
+ + +
+
+ +
+
+ + +
+ +
+@endsection +``` + +### 3.2 테넌트 관리 페이지 (Tenant Selector 제외) + +**적용 대상**: `tenants/index.blade.php` + +**템플릿 구조**: +```blade +@extends('layouts.app') + +@section('title', '테넌트 관리') + +@section('content') + + + +
+

🏢 테넌트 관리

+ + + 새 테넌트 + +
+ + +
+
+ +
+
+ + +
+ +
+@endsection +``` + +**이유**: 테넌트 관리는 모든 테넌트를 관리하는 페이지이므로 테넌트 필터링이 불필요 + +### 3.3 대시보드 (Tenant Selector 포함) + +**템플릿 구조**: +```blade +@extends('layouts.app') + +@section('title', '대시보드') + +@section('content') + + @include('partials.tenant-selector') + + +
+
+

환영합니다!

+ +
+
+@endsection +``` + +--- + +## 4. 컨텐츠 영역 구조 + +### 4.1 페이지 헤더 + +```blade +
+

+ [아이콘] 페이지 제목 +

+ + + 새 항목 + +
+``` + +**주의사항**: +- Tenant Selector가 있는 경우 → `mt-6` 추가 (위쪽 여백) +- Tenant Selector가 없는 경우 → `mt-6` 생략 + +### 4.2 필터 영역 + +```blade +
+
+ +
+ +
+ + +
+ +
+ + + +
+
+``` + +### 4.3 HTMX 동적 컨텐츠 영역 + +```blade +
+ +
+
+
+
+``` + +--- + +## 5. 체크리스트 + +### 5.1 Tenant Selector 포함 여부 확인 + +**포함해야 하는 페이지** (✅): +- [ ] 역할 관리 (`roles/index.blade.php`) +- [ ] 사용자 관리 (`users/index.blade.php`) +- [ ] 부서 관리 (`departments/index.blade.php`) +- [ ] 제품 관리 (`products/index.blade.php`) +- [ ] 자재 관리 (`materials/index.blade.php`) +- [ ] BOM 관리 (`boms/index.blade.php`) +- [ ] 카테고리 관리 (`categories/index.blade.php`) +- [ ] 대시보드 (`dashboard/index.blade.php`) + +**제외해야 하는 페이지** (❌): +- [ ] 테넌트 관리 (`tenants/index.blade.php`) +- [ ] 시스템 설정 (전역 설정 페이지) +- [ ] 감사 로그 (전체 시스템 로그) + +### 5.2 레이아웃 구현 체크리스트 + +**페이지 구조**: +- [ ] `@extends('layouts.app')` 상속 +- [ ] `@section('title', '페이지 제목')` 정의 +- [ ] `@section('content')` 내부 구조: + - [ ] `@include('partials.tenant-selector')` (필요 시) + - [ ] 페이지 헤더 (`mt-6` 여백 확인) + - [ ] 필터 영역 + - [ ] HTMX 동적 컨텐츠 영역 + +**스타일 일관성**: +- [ ] 카드 스타일: `bg-white rounded-lg shadow-sm` +- [ ] 버튼 스타일: `btn-primary`, `btn-secondary` +- [ ] 간격 일관성: `mb-6`, `mt-6`, `p-4`, `p-6` + +**HTMX 설정**: +- [ ] `hx-get` 엔드포인트 설정 +- [ ] `hx-trigger="load, filterSubmit from:body"` 설정 +- [ ] `hx-include="#filterForm"` 설정 +- [ ] CSRF 토큰 헤더 포함 + +--- + +## 6. 예시 코드 + +### 6.1 완전한 페이지 예시 (Tenant Selector 포함) + +```blade +@extends('layouts.app') + +@section('title', '역할 관리') + +@section('content') + + @include('partials.tenant-selector') + + +
+

🔑 역할 관리

+ + + 새 역할 + +
+ + +
+
+
+ +
+ +
+
+ + +
+ +
+
+
+
+@endsection + +@push('scripts') + + +@endpush +``` + +### 6.2 ViewComposer로 $globalTenants 자동 주입 + +**파일**: `app/Providers/ViewServiceProvider.php` + +```php +use Illuminate\Support\Facades\View; +use App\Models\Tenant; + +public function boot(): void +{ + // 모든 뷰에 $globalTenants 변수 자동 주입 + View::composer('partials.tenant-selector', function ($view) { + $view->with('globalTenants', Tenant::orderBy('company_name')->get()); + }); +} +``` + +--- + +## 7. 주의사항 + +### 7.1 Tenant Selector 관련 + +1. **ViewComposer 필수**: `$globalTenants` 변수가 자동으로 주입되도록 ViewComposer 설정 필요 +2. **세션 관리**: `selected_tenant_id` 세션이 Service Layer에서 자동으로 필터링에 사용됨 +3. **페이지 리로드**: 테넌트 변경 시 전체 페이지가 리로드되어 모든 데이터가 새로 로드됨 +4. **HTMX 연동**: Tenant 변경 후 HTMX 테이블도 자동으로 새로 로드됨 + +### 7.2 레이아웃 스타일 + +1. **컨테이너 없음**: `@section('content')` 내부에는 기본 컨테이너가 없음 + - Tenant Selector는 자체 패딩(`p-6`) 포함 + - 나머지 컨텐츠는 페이지별로 여백 조정 + +2. **간격 일관성**: + - Tenant Selector 하단: `mb-6` (내부 카드에 포함) + - 페이지 헤더: `mt-6 mb-6` (Tenant Selector가 있을 때) + - 필터 영역: `mb-6` + - 컨텐츠 영역: 별도 여백 불필요 + +3. **반응형 디자인**: Tailwind CSS 유틸리티 클래스 사용 + +--- + +**작성자:** Claude +**최종 수정일:** 2025-01-24 +**버전:** 1.0 +**참고**: Dashboard, Tenant 관리 시스템 레이아웃 기반 \ No newline at end of file diff --git a/docs/MNG_CRITICAL_RULES.md b/docs/MNG_CRITICAL_RULES.md index 85514606..3f037217 100644 --- a/docs/MNG_CRITICAL_RULES.md +++ b/docs/MNG_CRITICAL_RULES.md @@ -22,9 +22,10 @@ ### 1. DB 마이그레이션 금지 - DB 스키마는 **api/에서만** 관리 - 여러 저장소(api, admin, mng)가 동일 DB 공유 -**예외:** -- `admin_*` 접두사 테이블만 mng/에서 생성 가능 (mng 전용 기능용) -- 그래도 가능하면 api/에 요청 권장 +**mng 전용 테이블 생성 시:** +- ✅ mng에서만 사용하는 테이블은 `admin_*` 접두사 사용 +- ✅ **마이그레이션은 무조건 api/에서 생성** (mng에서 생성 금지!) +- 예: `admin_pm_projects`, `admin_pm_tasks` 등 **실수 사례:** ```php @@ -184,13 +185,64 @@ ### 6. FormRequest 필수 사용 --- +### 7. MNG 데이터 접근 아키텍처 + +**MNG는 자체 내부 API 사용 (외부 api/ 프로젝트 호출 안 함)** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ routes/web.php (Blade 화면만) │ +│ └─ Controller → view('xxx.index') (데이터 없이 화면만) │ +│ │ +│ routes/api.php (HTMX 호출 + CRUD) │ +│ └─ Api/Admin/Controller → Service → Model → DB │ +│ │ +│ Blade에서 HTMX로 /api/admin/* 호출 │ +│ └─ hx-get="/api/admin/tenants" │ +│ └─ hx-post="/api/admin/tenants" │ +└─────────────────────────────────────────────────────────────┘ +``` + +**규칙:** +- ❌ 외부 api/ 프로젝트의 API 호출 금지 +- ✅ mng 내부 API (`/api/admin/*`) 사용 +- ✅ Service → Model → DB (직접 접근) + +**Controller 구분:** +- `app/Http/Controllers/XxxController.php` → Blade 화면만 (GET) +- `app/Http/Controllers/Api/Admin/XxxController.php` → CRUD 처리 (HTMX) + +**예시:** +```php +// ✅ Web Controller: 화면만 반환 +class TenantController extends Controller +{ + public function index(): View + { + return view('tenants.index'); // 데이터 없이 화면만 + } +} + +// ✅ API Controller: CRUD 처리 +class Api\Admin\TenantController extends Controller +{ + public function index(Request $request) + { + $tenants = $this->tenantService->getTenants($request->all()); + return view('tenants.partials.list', compact('tenants')); // HTML partial + } +} +``` + +--- + ## 📋 작업 전 체크리스트 ### DB 작업 시: ``` □ mng/에서 작업 중인가? → 마이그레이션 금지! □ 기존 테이블 수정인가? → api/에 요청! -□ 새 테이블인가? → admin_* 접두사 OR api/에 요청! +□ mng 전용 새 테이블인가? → admin_* 접두사 + api/에 마이그레이션! □ 관계만 추가인가? → OK, 모델만 수정 ``` @@ -230,5 +282,5 @@ ### 2025-11-24: users 테이블 마이그레이션 시도 --- -**최종 업데이트**: 2025-11-24 +**최종 업데이트**: 2025-11-27 **다음 리뷰**: 반복 실수 발생 시 업데이트 diff --git a/docs/MNG_PROJECT_PLAN.md b/docs/MNG_PROJECT_PLAN.md new file mode 100644 index 00000000..b6a74b43 --- /dev/null +++ b/docs/MNG_PROJECT_PLAN.md @@ -0,0 +1,838 @@ +# MNG 프로젝트 개발 계획서 + +## 📋 프로젝트 개요 + +### 목적 +- **문제점**: 기존 admin/ (Filament v4)은 AI 없이 수정이 어려움 +- **목표**: 수정 용이한 Plain Laravel 기반 관리자 패널 구축 +- **도메인**: mng.sam.kr +- **철학**: **단순함 > 복잡함**, AI 없이도 수정 가능한 직관적 코드 + +### 핵심 전략 +``` +┌─────────────────────────────────────────────┐ +│ MNG (mng.sam.kr) │ +│ ┌──────────────┐ ┌─────────────────┐ │ +│ │ Web Routes │────▶│ Blade + HTMX │ │ ← DaisyUI (심플) +│ │ (세션 인증) │ │ (수정 용이) │ │ +│ └──────────────┘ └─────────────────┘ │ +│ ┌──────────────┐ ┌─────────────────┐ │ +│ │ API Routes │────▶│ Admin API │ │ ← 처음부터 분리 +│ │ (토큰 인증) │ │ (관리자 전용) │ │ +│ └──────────────┘ └─────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Service Layer (비즈니스 로직) │ │ ← admin/ 복사 +│ └──────────────────────────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Models (admin/ 복사, Filament 제거) │ │ +│ └──────────────────────────────────────┘ │ +└─────────────────┬───────────────────────────┘ + ↓ + ┌─────────────────────────────┐ + │ MySQL 8.0 (공유 DB) │ + │ - admin/ (점차 deprecated) │ + │ - api/ (외부 API) │ + │ - mng/ (새 관리자) ← 최종 │ + └─────────────────────────────┘ +``` + +### 설계 원칙 +1. **단순성**: 복잡한 추상화 금지, 인라인 코드 허용 +2. **수정 용이성**: AI 없이도 Blade 템플릿 수정 가능 +3. **코드 재사용**: admin/ 모델/서비스 복사 후 간소화 +4. **DB 공유**: 기존 테이블 최대한 활용 + +--- + +## 🏗️ 아키텍처 설계 + +### 1. 디렉토리 구조 +``` +SAM/ +├── admin/ # Filament (점차 deprecated) +├── api/ # 외부 클라이언트 API +├── mng/ # ⭐ 운영 관리자 패널 (NEW) +│ ├── app/ +│ │ ├── Http/ +│ │ │ ├── Controllers/ +│ │ │ │ ├── Web/ # Blade 컨트롤러 (단순) +│ │ │ │ │ ├── Auth/ +│ │ │ │ │ ├── Dashboard/ +│ │ │ │ │ ├── User/ +│ │ │ │ │ └── Product/ +│ │ │ │ └── Api/ # Admin API (향후) +│ │ │ │ └── Admin/ +│ │ │ ├── Requests/ # FormRequest (필수) +│ │ │ └── Middleware/ +│ │ ├── Services/ # admin/ 복사 후 간소화 +│ │ ├── Models/ # admin/ 복사, Filament 코드 제거 +│ │ └── Traits/ +│ │ ├── BelongsToTenant.php # admin/에서 복사 +│ │ └── HasAuditLog.php # admin/에서 복사 +│ ├── routes/ +│ │ ├── web.php # Blade 라우트 +│ │ └── api.php # Admin API (/api/admin/*) +│ ├── resources/ +│ │ └── views/ +│ │ ├── layouts/ +│ │ │ ├── app.blade.php # 단순 레이아웃 +│ │ │ └── guest.blade.php +│ │ ├── auth/ # 로그인 화면 +│ │ ├── dashboard/ # 대시보드 +│ │ ├── users/ # 사용자 관리 +│ │ └── products/ # 제품 관리 +│ ├── database/ +│ │ └── migrations/ +│ │ └── # 관리자 전용: admin_* +│ │ └── # 통계 전용: stat_* +│ ├── tests/ +│ │ └── Feature/ +│ └── .env +├── docker/ +│ └── nginx/ +│ └── mng.sam.kr.conf +└── claudedocs/ + └── mng/ + ├── MNG_PROJECT_PLAN.md # 이 문서 + ├── API_SPEC.md # API 명세 + └── PROGRESS.md # 진행 상황 +``` + +### 2. 기술 스택 (확정) + +| 레이어 | 기술 | 버전 | 비고 | +|--------|------|------|------| +| **백엔드** | Laravel | 12.x | PHP 8.4+ | +| **인증** | Sanctum | 4.x | 세션 + 토큰 | +| **DB** | **MySQL** | **8.0** | **admin, api와 공유** | +| **프론트엔드** | **Blade + HTMX** | **1.x** | **단순, 수정 용이** | +| **CSS** | **Tailwind CSS** | **3.x** | 기존과 통일 | +| **UI 컴포넌트** | **DaisyUI** | **4.x** | **심플, 클래스 기반** | +| **아이콘** | Heroicons | - | Tailwind 친화적 | +| **문서화** | L5-Swagger | - | Admin API 전용 | +| **테스트** | PHPUnit | - | Feature Test | + +### 3. DB 테이블 명명 규칙 (변경) + +#### 기존 테이블 재사용 (마이그레이션 없음) +``` +✅ users, roles, departments +✅ products, materials, bom_items +✅ menus, menu_role +✅ audit_logs, categories, files +✅ tenants +``` + +#### 관리자 전용 테이블 (admin_* 접두사) +```php +// database/migrations/2025_01_20_create_admin_settings_table.php +Schema::create('admin_settings', function (Blueprint $table) { + $table->id(); + $table->string('key')->unique(); + $table->text('value')->nullable(); + $table->string('type')->default('string'); // string, json, boolean + $table->timestamps(); +}); + +// 예시 테이블 +- admin_settings # 관리자 설정 +- admin_logs # 관리자 작업 로그 +- admin_preferences # 관리자 개인 설정 +``` + +#### 통계 테이블 (stat_* 접두사) +```php +// database/migrations/2025_01_20_create_stat_daily_sales_table.php +Schema::create('stat_daily_sales', function (Blueprint $table) { + $table->id(); + $table->date('date'); + $table->decimal('total_amount', 15, 2); + $table->integer('order_count'); + $table->timestamps(); + + $table->unique('date'); +}); + +// 예시 테이블 +- stat_daily_sales # 일별 매출 통계 +- stat_inventory # 재고 통계 +- stat_user_activity # 사용자 활동 통계 +``` + +### 4. 모델/서비스 복사 전략 + +#### admin/ → mng/ 복사 프로세스 +```bash +# 1. 모델 복사 (Filament 의존성 제거) +cp -r admin/app/Models/* mng/app/Models/ +# Filament 관련 코드 제거 (getNavigationLabel, form, table 등) + +# 2. Traits 복사 (그대로 사용) +cp admin/app/Traits/BelongsToTenant.php mng/app/Traits/ +cp admin/app/Traits/HasAuditLog.php mng/app/Traits/ + +# 3. Services 복사 (있다면) +cp -r admin/app/Services/* mng/app/Services/ +# 또는 신규 작성 (Service-First 원칙) +``` + +#### 모델 예시 (Filament 제거) +```php +// admin/app/Models/User.php (Before) +class User extends Authenticatable implements FilamentUser +{ + use BelongsToTenant, HasAuditLog; + + public static function form(Form $form): Form { ... } // ❌ 제거 + public static function table(Table $table): Table { ... } // ❌ 제거 + public function canAccessPanel(Panel $panel): bool { ... } // ❌ 제거 +} + +// mng/app/Models/User.php (After) +class User extends Authenticatable +{ + use BelongsToTenant, HasAuditLog; + + protected $fillable = [ + 'tenant_id', 'email', 'password', 'name', + 'role_id', 'department_id', 'is_active', + ]; + + // 순수 Eloquent 관계만 유지 + public function role() { return $this->belongsTo(Role::class); } + public function department() { return $this->belongsTo(Department::class); } +} +``` + +--- + +## 🎨 UI 설계 원칙 (수정 용이성 최우선) + +### DaisyUI 사용 철학 +```blade +{{-- ✅ GOOD: 단순하고 직관적 --}} + +
+
+

제목

+

내용

+
+
+ +{{-- ❌ BAD: 과도한 추상화 --}} + + +``` + +### Blade 템플릿 구조 (2레벨 최대) +```blade +{{-- layouts/app.blade.php (레이아웃) --}} + + + + {{ config('app.name') }} + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ {{-- 사이드바 --}} + +
+ + +
+ + {{-- 메인 컨텐츠 --}} +
+ + +
+ @yield('content') +
+
+
+ + + +{{-- users/index.blade.php (페이지) --}} +@extends('layouts.app') + +@section('content') +
+
+

사용자 목록

+ + {{-- Alpine.js 최소 사용 --}} +
+ +
+ +
+ + + + + + + + + + + @foreach($users as $user) + + + + + + + @endforeach + +
이름이메일역할작업
{{ $user->name }}{{ $user->email }}{{ $user->role->name }} + 수정 +
+
+ + {{ $users->links() }} {{-- Pagination --}} +
+
+@endsection +``` + +### Alpine.js 사용 원칙 (최소화) +```blade +{{-- ✅ GOOD: 단순 인터랙션 --}} +
+ + +
+ +{{-- ❌ BAD: 복잡한 로직 (서버에서 처리) --}} +
+
...
+
+``` + +--- + +## 🚀 개발 로드맵 + +### Phase 1: 인프라 구축 (1일) + +#### 체크리스트 +- [ ] Laravel 12 프로젝트 생성 (`mng/`) + ```bash + cd SAM + composer create-project laravel/laravel mng + cd mng + ``` +- [ ] `.env` 환경 변수 설정 + ```env + APP_NAME=MNG + APP_URL=http://mng.sam.kr + DB_CONNECTION=pgsql + DB_HOST=postgres + DB_PORT=5432 + DB_DATABASE=sam_db + DB_USERNAME=sam_user + DB_PASSWORD=sam_password + ``` +- [ ] Composer 패키지 설치 + ```bash + composer require laravel/sanctum + composer require darkaonline/l5-swagger + composer require --dev laravel/pint + ``` +- [ ] Tailwind + DaisyUI + HTMX 설정 + ```bash + npm install -D tailwindcss daisyui @tailwindcss/forms + npm install htmx.org + ``` + ```js + // tailwind.config.js + module.exports = { + plugins: [require('daisyui')], + daisyui: { + themes: ['light', 'dark'], + }, + } + ``` +- [ ] Docker Nginx 설정 (mng.sam.kr) + ```nginx + server { + listen 80; + server_name mng.sam.kr; + root /var/www/mng/public; + index index.php; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass mng:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } + } + ``` +- [ ] admin/ 모델 복사 + ```bash + cp -r admin/app/Models/* mng/app/Models/ + cp -r admin/app/Traits/* mng/app/Traits/ + # Filament 관련 코드 제거 후 커밋 + ``` + +#### 산출물 +- `mng/` 디렉토리 (Git 독립 저장소) +- DaisyUI + Alpine.js 환경 +- 복사된 모델 (Filament 제거) + +--- + +### Phase 2: 인증 시스템 (2일) + +#### 로그인 화면 (DaisyUI) +```blade +{{-- resources/views/auth/login.blade.php --}} + + + + 로그인 - MNG + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+
+
+

MNG 로그인

+ +
+ @csrf + +
+ + +
+ +
+ + +
+ + @if ($errors->any()) +
+ {{ $errors->first() }} +
+ @endif + +
+ +
+
+
+
+
+
+ + +``` + +#### AuthService (admin/ 참고) +```php +// app/Services/AuthService.php +namespace App\Services; + +use App\Models\User; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; + +class AuthService +{ + public function login(array $credentials): bool + { + return Auth::attempt($credentials); + } + + public function logout(): void + { + Auth::logout(); + } + + public function createToken(array $credentials): ?string + { + $user = User::where('email', $credentials['email'])->first(); + + if (!$user || !Hash::check($credentials['password'], $user->password)) { + return null; + } + + return $user->createToken('mng-token')->plainTextToken; + } +} +``` + +#### 체크리스트 +- [ ] LoginRequest (FormRequest) +- [ ] AuthService 작성 +- [ ] Web 로그인 구현 (세션) +- [ ] API 로그인 구현 (토큰) +- [ ] BelongsToTenant 적용 확인 +- [ ] Feature Test 작성 + +--- + +### Phase 3: 대시보드 (1-2일) + +#### DaisyUI Drawer 레이아웃 +```blade +{{-- resources/views/layouts/app.blade.php --}} + + + + {{ config('app.name') }} + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ + + {{-- 메인 컨텐츠 --}} +
+ {{-- 네비게이션 바 --}} + + + {{-- 페이지 컨텐츠 --}} +
+ @yield('content') +
+
+ + {{-- 사이드바 --}} +
+ + +
+
+ + +``` + +#### 대시보드 컨트롤러 +```php +// app/Http/Controllers/Web/DashboardController.php +namespace App\Http\Controllers\Web; + +use App\Http\Controllers\Controller; +use App\Services\MenuService; + +class DashboardController extends Controller +{ + public function __construct( + private MenuService $menuService + ) {} + + public function index() + { + $menus = $this->menuService->getMenusForUser(auth()->user()); + + return view('dashboard.index', compact('menus')); + } +} +``` + +#### 체크리스트 +- [ ] 레이아웃 템플릿 (DaisyUI Drawer) +- [ ] 메뉴 서비스 (MenuService) +- [ ] 역할별 메뉴 필터링 +- [ ] 대시보드 메인 페이지 + +--- + +### Phase 4: 핵심 기능 (주 단위) + +#### 4.1 사용자 관리 (3-5일) +```blade +{{-- resources/views/users/index.blade.php --}} +@extends('layouts.app') + +@section('content') +
+ {{-- 헤더 --}} +
+

사용자 관리

+ 사용자 추가 +
+ + {{-- 검색/필터 --}} +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + {{-- 테이블 --}} +
+
+
+ + + + + + + + + + + + + + @foreach($users as $user) + + + + + + + + + + @endforeach + +
ID이름이메일역할부서상태작업
{{ $user->id }}{{ $user->name }}{{ $user->email }}{{ $user->role->name }}{{ $user->department->name }} + + {{ $user->is_active ? '활성' : '비활성' }} + + +
+ 수정 + +
+
+
+ + {{ $users->links() }} +
+
+
+ + +@endsection +``` + +#### 체크리스트 +- [ ] 사용자 목록 (검색, 필터, 페이징) +- [ ] 사용자 생성 (FormRequest) +- [ ] 사용자 수정 +- [ ] 사용자 삭제 (Soft Delete) +- [ ] Feature Test + +--- + +## 📊 데이터베이스 전략 + +### DB 테이블 전략 (최종) +``` +✅ 기존 테이블 재사용 (마이그레이션 없음) + - users, roles, departments + - products, materials + - menus, audit_logs + +🆕 관리자 전용 (admin_*) + - admin_settings + - admin_logs + - admin_preferences + +📊 통계 (stat_*) + - stat_daily_sales + - stat_inventory + - stat_user_activity +``` + +### 모델 관리 전략 +``` +초기 복사: admin/app/Models → mng/app/Models +Filament 제거: form(), table(), canAccessPanel() 등 +이후 운영: mng/ 독립 (admin 점차 deprecated) +``` + +--- + +## 🛡️ 품질 관리 + +### 코드 품질 체크리스트 +``` +□ Service-First (비즈니스 로직 → Service) +□ FormRequest (컨트롤러 검증 금지) +□ BelongsToTenant (multi-tenant 스코프) +□ i18n 키 (하드코딩 금지) +□ Soft Delete (deleted_at) +□ 감사 로그 (HasAuditLog trait) +□ Feature Test +□ Pint (코드 스타일) +``` + +### UI 수정 용이성 체크리스트 +``` +□ DaisyUI 클래스 직접 사용 (추상화 최소) +□ Alpine.js 단순 인터랙션만 +□ Blade 템플릿 2레벨 이하 +□ 인라인 Tailwind 허용 +□ AI 없이 수정 가능 +``` + +--- + +## 🎯 예상 타임라인 + +### MVP (최소 기능 제품) - 2주 +``` +Day 1-2: Phase 1 (인프라) + admin/ 모델 복사 +Day 3-4: Phase 2 (인증) +Day 5-6: Phase 3 (대시보드) +Day 7-14: Phase 4 (사용자, 역할, 제품 관리) +``` + +### 전체 기능 이식 - 4-6주 +``` +Week 3-4: 제품, 자재 관리 +Week 5: 게시판, 통계 +Week 6: 테스트, 최적화 +``` + +--- + +## 📚 참고 문서 + +- [DaisyUI Components](https://daisyui.com/components/) +- [Alpine.js Documentation](https://alpinejs.dev/) +- [Laravel 12 Blade](https://laravel.com/docs/12.x/blade) + +--- + +## ✅ 다음 단계 + +### 즉시 시작 가능 +- [ ] `mng/` Laravel 프로젝트 생성 +- [ ] DaisyUI + Alpine.js 설치 +- [ ] admin/ 모델 복사 및 Filament 제거 +- [ ] 로그인 화면 구현 + +--- + +**작성일**: 2025-01-20 +**버전**: 2.0 +**상태**: 정책 반영 완료 ✅ +**변경사항**: +- 폴더명: `adm2/` → `mng/` +- UI: DaisyUI + Blade + Alpine.js 확정 +- DB: 기존 테이블 재사용, `admin_*`, `stat_*` 접두사 +- 모델: admin/ 복사 후 Filament 제거 +- 철학: 단순함, 수정 용이성 최우선 \ No newline at end of file diff --git a/docs/SETUP_GUIDE.md b/docs/SETUP_GUIDE.md new file mode 100644 index 00000000..7540c6a9 --- /dev/null +++ b/docs/SETUP_GUIDE.md @@ -0,0 +1,587 @@ +# MNG 환경 구성 가이드 + +## 📋 개요 + +MNG 프로젝트를 위한 Docker 환경 구성 가이드입니다. +기존 SAM 프로젝트 (api, admin, react)에 mng 서비스를 추가합니다. + +--- + +## 🎯 목표 환경 + +``` +SAM/ +├── api/ (api.sam.kr) - 외부 API +├── admin/ (admin.sam.kr) - Filament 관리자 (deprecated) +├── react/ (dev.sam.kr) - React 프론트엔드 +├── mng/ (mng.sam.kr) - 새 관리자 패널 ⭐ NEW +└── docker/ + ├── docker-compose.yml + ├── nginx/nginx.conf + └── mng/ ⭐ NEW + ├── Dockerfile + ├── nginx.conf + └── supervisord.conf +``` + +### 도메인 구성 +| 도메인 | 서비스 | 용도 | +|--------|--------|------| +| api.sam.kr | api | 외부 클라이언트 API | +| admin.sam.kr | admin | Filament 관리자 (점차 폐기) | +| dev.sam.kr | react | React 프론트엔드 | +| **mng.sam.kr** | **mng** | **새 관리자 패널** ⭐ | + +--- + +## 📐 Phase 0: 환경 구성 (최초 1회) + +### Step 1: Laravel 프로젝트 생성 + +```bash +# SAM 디렉토리로 이동 +cd /Users/hskwon/Works/@KD_SAM/SAM + +# Laravel 12 프로젝트 생성 +composer create-project laravel/laravel mng + +cd mng + +# 필수 패키지 설치 +composer require laravel/sanctum +composer require darkaonline/l5-swagger +composer require --dev laravel/pint + +# NPM 패키지 설치 +npm install -D tailwindcss daisyui @tailwindcss/forms postcss autoprefixer +npm install htmx.org +npx tailwindcss init -p +``` + +### Step 2: 환경 변수 설정 + +**mng/.env** (기본값 수정) +```env +APP_NAME=MNG +APP_ENV=local +APP_KEY=base64:... (자동 생성됨) +APP_DEBUG=true +APP_URL=http://mng.sam.kr + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=samdb +DB_USERNAME=samuser +DB_PASSWORD=sampass + +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file + +SANCTUM_STATEFUL_DOMAINS=mng.sam.kr +``` + +### Step 3: Tailwind + DaisyUI 설정 + +**mng/tailwind.config.js** +```js +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./resources/**/*.blade.php", + "./resources/**/*.js", + "./resources/**/*.vue", + ], + theme: { + extend: {}, + }, + plugins: [ + require('daisyui'), + require('@tailwindcss/forms'), + ], + daisyui: { + themes: ['light', 'dark'], + }, +} +``` + +**mng/resources/css/app.css** +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +**mng/resources/js/app.js** +```js +import './bootstrap'; +import htmx from 'htmx.org'; + +window.htmx = htmx; + +// HTMX 전역 설정 +document.addEventListener('DOMContentLoaded', () => { + // CSRF 토큰 자동 추가 + document.body.addEventListener('htmx:configRequest', (event) => { + event.detail.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content; + }); + + // 에러 처리 + document.body.addEventListener('htmx:responseError', (event) => { + console.error('HTMX Error:', event.detail); + alert('오류가 발생했습니다. 다시 시도해주세요.'); + }); +}); +``` + +**mng/vite.config.js** (확인) +```js +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + ], +}); +``` + +### Step 4: Docker 설정 파일 생성 + +#### 4-1. Dockerfile 생성 + +**docker/mng/Dockerfile** +```dockerfile +FROM php:8.4-fpm + +# 시스템 패키지 설치 +RUN apt-get update && apt-get install -y \ + git \ + curl \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + libzip-dev \ + zip \ + unzip \ + supervisor \ + nginx + +# PHP 확장 설치 +RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip + +# Composer 설치 +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Node.js 설치 (Vite 빌드용) +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs + +# 작업 디렉토리 설정 +WORKDIR /var/www/mng + +# 권한 설정 +RUN chown -R www-data:www-data /var/www/mng + +# PHP-FPM 포트 노출 +EXPOSE 9000 + +CMD ["php-fpm"] +``` + +#### 4-2. Nginx 설정 (PHP-FPM 연동) + +**docker/mng/nginx.conf** +```nginx +server { + listen 9000; + server_name localhost; + root /var/www/mng/public; + index index.php index.html; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_pass 127.0.0.1:9000; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } +} +``` + +#### 4-3. Supervisor 설정 (선택) + +**docker/mng/supervisord.conf** +```ini +[supervisord] +nodaemon=true + +[program:php-fpm] +command=/usr/local/sbin/php-fpm +autostart=true +autorestart=true +stderr_logfile=/var/log/php-fpm.err.log +stdout_logfile=/var/log/php-fpm.out.log +``` + +#### 4-4. PHP 업로드 설정 + +**docker/mng/uploads.ini** +```ini +upload_max_filesize = 100M +post_max_size = 100M +max_execution_time = 300 +memory_limit = 256M +``` + +### Step 5: docker-compose.yml 업데이트 + +**docker/docker-compose.yml** (mng 서비스 추가) +```yaml +services: + nginx: + image: nginx:latest + ports: + - "80:80" + volumes: + - ../api:/var/www/api + - ../admin:/var/www/admin + - ../mng:/var/www/mng # ⭐ NEW + - ../docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - api + - admin + - mng # ⭐ NEW + - react + networks: + - samnet + + api: + build: + context: . + dockerfile: ../docker/api/Dockerfile + volumes: + - ../api:/var/www/api + - ../docker/api/nginx.conf:/etc/nginx/conf.d/default.conf + - ../docker/api/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf + - ../docker/api/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini + environment: + - DB_HOST=mysql + - DB_PORT=3306 + - DB_DATABASE=samdb + - DB_USERNAME=samuser + - DB_PASSWORD=sampass + networks: + - samnet + working_dir: /var/www/api + + admin: + build: + context: . + dockerfile: ../docker/admin/Dockerfile + volumes: + - ../admin:/var/www/admin + - ../docker/admin/nginx.conf:/etc/nginx/conf.d/default.conf + - ../docker/admin/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf + - ../docker/admin/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini + environment: + - DB_HOST=mysql + - DB_PORT=3306 + - DB_DATABASE=samdb + - DB_USERNAME=samuser + - DB_PASSWORD=sampass + networks: + - samnet + working_dir: /var/www/admin + + # ⭐ NEW: MNG 서비스 + mng: + build: + context: . + dockerfile: ../docker/mng/Dockerfile + volumes: + - ../mng:/var/www/mng + - ../docker/mng/nginx.conf:/etc/nginx/conf.d/default.conf + - ../docker/mng/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf + - ../docker/mng/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini + environment: + - DB_HOST=mysql + - DB_PORT=3306 + - DB_DATABASE=samdb + - DB_USERNAME=samuser + - DB_PASSWORD=sampass + networks: + - samnet + working_dir: /var/www/mng + + react: + build: + context: .. + dockerfile: docker/react/Dockerfile + volumes: + - ../react:/app + - /app/node_modules + - /app/.next + environment: + - NEXT_PUBLIC_API_BASE_URL=http://api.sam.kr + - NEXT_PUBLIC_ADMIN_URL=http://admin.sam.kr + - NEXT_PUBLIC_MNG_URL=http://mng.sam.kr # ⭐ NEW + - NEXT_PUBLIC_API_KEY=${NEXT_PUBLIC_API_KEY:-} + - NEXT_PUBLIC_APP_NAME=SAM + - NODE_ENV=development + networks: + - samnet + working_dir: /app + + mysql: + image: mysql:8.0 + restart: always + environment: + MYSQL_DATABASE: samdb + MYSQL_USER: samuser + MYSQL_PASSWORD: sampass + MYSQL_ROOT_PASSWORD: root + volumes: + - db_data:/var/lib/mysql + - ../docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "3306:3306" + networks: + - samnet + +volumes: + db_data: + +networks: + samnet: + driver: bridge +``` + +### Step 6: Nginx 메인 설정 업데이트 + +**docker/nginx/nginx.conf** (mng.sam.kr 서버 블록 추가) +```nginx +events {} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + client_max_body_size 100M; + + # ... 기존 서버 블록들 (dev.haisa.kr, api.sam.kr, admin.sam.kr, dev.sam.kr) + + # ⭐ NEW: MNG 서버 블록 + server { + listen 80; + server_name mng.sam.kr; + + root /var/www/mng/public; + index index.php index.html; + + access_log /var/log/nginx/mng.sam.kr_access.log; + error_log /var/log/nginx/mng.sam.kr_error.log; + + # 🛡️ 보안: 악의적 경로 패턴 차단 + 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; + } + + # 정적 자산 처리 + location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|map)$ { + try_files $uri =404; + access_log off; + expires 30d; + add_header Cache-Control "public"; + } + + # 일반 요청 처리 + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_pass mng:9000; # 서비스명 mng + fastcgi_param SCRIPT_FILENAME /var/www/mng/public$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + } +} +``` + +### Step 7: hosts 파일 설정 + +**/etc/hosts** (로컬 도메인 추가) +```bash +# SAM 프로젝트 도메인 +127.0.0.1 api.sam.kr +127.0.0.1 admin.sam.kr +127.0.0.1 dev.sam.kr +127.0.0.1 mng.sam.kr # ⭐ NEW +``` + +**macOS/Linux:** +```bash +sudo nano /etc/hosts +# 위 내용 추가 후 저장 +``` + +**Windows:** +``` +C:\Windows\System32\drivers\etc\hosts +# 관리자 권한으로 메모장 열어서 추가 +``` + +### Step 8: admin/ 모델 복사 + +```bash +cd /Users/hskwon/Works/@KD_SAM/SAM + +# Models 복사 +cp -r admin/app/Models/* mng/app/Models/ + +# Traits 복사 +mkdir -p mng/app/Traits +cp admin/app/Traits/BelongsToTenant.php mng/app/Traits/ +cp admin/app/Traits/HasAuditLog.php mng/app/Traits/ + +# Filament 의존성 제거 (수동 작업 필요) +# - form(), table(), canAccessPanel() 등 삭제 +# - FilamentUser 인터페이스 제거 +``` + +### Step 9: Docker 빌드 및 실행 + +```bash +cd /Users/hskwon/Works/@KD_SAM/SAM/docker + +# 기존 컨테이너 중지 +docker-compose down + +# mng 서비스 빌드 +docker-compose build mng + +# 전체 서비스 시작 +docker-compose up -d + +# 로그 확인 +docker-compose logs -f mng + +# mng 컨테이너 접속 +docker-compose exec mng bash + +# 컨테이너 내부에서 Laravel 설정 +php artisan key:generate +php artisan migrate +php artisan storage:link + +# Vite 빌드 (개발 모드) +npm run dev +# 또는 프로덕션 빌드 +npm run build +``` + +### Step 10: 동작 확인 + +```bash +# 1. 브라우저에서 확인 +http://mng.sam.kr + +# 2. API 확인 (예시) +curl http://mng.sam.kr/api/admin/health + +# 3. 로그 확인 +docker-compose logs mng +``` + +--- + +## 🔧 문제 해결 + +### 1. "502 Bad Gateway" 에러 +```bash +# PHP-FPM 상태 확인 +docker-compose exec mng ps aux | grep php-fpm + +# 재시작 +docker-compose restart mng +``` + +### 2. 권한 에러 +```bash +# 컨테이너 내부에서 +docker-compose exec mng bash +chown -R www-data:www-data /var/www/mng/storage +chown -R www-data:www-data /var/www/mng/bootstrap/cache +chmod -R 775 /var/www/mng/storage +chmod -R 775 /var/www/mng/bootstrap/cache +``` + +### 3. DB 연결 실패 +```bash +# MySQL 컨테이너 확인 +docker-compose exec mysql mysql -u samuser -psampass -e "SHOW DATABASES;" + +# mng .env 확인 +docker-compose exec mng cat .env | grep DB_ +``` + +### 4. Vite 빌드 실패 +```bash +# 컨테이너 내부에서 +docker-compose exec mng bash +npm install +npm run build +``` + +### 5. HTMX 동작 안함 +- 브라우저 개발자 도구 → Network 탭 확인 +- `HX-Request` 헤더 있는지 확인 +- CSRF 토큰 확인 (meta 태그) + +--- + +## 📋 환경 구성 체크리스트 + +``` +[ ] Step 1: Laravel 프로젝트 생성 (mng/) +[ ] Step 2: .env 설정 (DB 정보) +[ ] Step 3: Tailwind + DaisyUI 설정 +[ ] Step 4: Docker 설정 파일 생성 (Dockerfile, nginx.conf) +[ ] Step 5: docker-compose.yml 업데이트 (mng 서비스 추가) +[ ] Step 6: Nginx 메인 설정 업데이트 (mng.sam.kr 서버 블록) +[ ] Step 7: /etc/hosts 설정 (mng.sam.kr) +[ ] Step 8: admin/ 모델 복사 및 Filament 제거 +[ ] Step 9: Docker 빌드 및 실행 +[ ] Step 10: 동작 확인 (http://mng.sam.kr) +``` + +--- + +## 🚀 다음 단계 + +환경 구성 완료 후: + +1. **Phase 1**: 인증 시스템 구현 (로그인 API + Blade) +2. **Phase 2**: 대시보드 구현 (레이아웃 + HTMX) +3. **Phase 3**: 사용자 관리 (첫 CRUD 기능) + +DEV_PROCESS.md 문서를 참고하여 개발을 진행하세요. + +--- + +**작성일**: 2025-01-20 +**버전**: 1.0 +**환경**: Docker + Laravel 12 + MySQL 8.0 + HTMX + DaisyUI \ No newline at end of file diff --git a/docs/project-management/README.md b/docs/project-management/README.md new file mode 100644 index 00000000..5bae7360 --- /dev/null +++ b/docs/project-management/README.md @@ -0,0 +1,299 @@ +# 프로젝트 진행 관리 시스템 (PM) + +MNG 관리자 패널용 프로젝트/작업/이슈 관리 시스템 + +## 개요 + +Notion 수동 관리를 대체하는 웹 기반 프로젝트 진행 관리 도구로, 클릭 한 번으로 상태 변경이 가능하고 대시보드에서 전체 진행률을 한눈에 파악할 수 있습니다. + +## 데이터 구조 + +``` +Project (프로젝트) + └── Task (작업) - 1:N + └── Issue (이슈) - 1:N +``` + +### 테이블 + +| 테이블명 | 설명 | +|---------|------| +| `admin_pm_projects` | 프로젝트 | +| `admin_pm_tasks` | 작업 | +| `admin_pm_issues` | 이슈 | + +> Migration은 `api/` 저장소에 위치 + +## 상태값 + +### 프로젝트 상태 +| 값 | 라벨 | 색상 | +|----|------|------| +| `active` | 진행중 | green | +| `completed` | 완료 | blue | +| `on_hold` | 보류 | yellow | + +### 작업 상태 +| 값 | 라벨 | 색상 | +|----|------|------| +| `todo` | 예정 | gray | +| `in_progress` | 진행중 | blue | +| `done` | 완료 | green | + +### 작업 우선순위 +| 값 | 라벨 | 색상 | +|----|------|------| +| `low` | 낮음 | gray | +| `medium` | 보통 | yellow | +| `high` | 높음 | red | + +### 이슈 타입 +| 값 | 라벨 | +|----|------| +| `bug` | 버그 | +| `feature` | 기능 | +| `improvement` | 개선 | + +### 이슈 상태 +| 값 | 라벨 | +|----|------| +| `open` | 열림 | +| `in_progress` | 진행중 | +| `resolved` | 해결됨 | +| `closed` | 닫힘 | + +## 파일 구조 + +``` +app/ +├── Http/ +│ ├── Controllers/ +│ │ ├── ProjectManagementController.php # Web Controller (뷰 렌더링) +│ │ └── Api/Admin/ProjectManagement/ +│ │ ├── ProjectController.php # 프로젝트 API +│ │ ├── TaskController.php # 작업 API +│ │ ├── IssueController.php # 이슈 API +│ │ └── ImportController.php # JSON Import API +│ └── Requests/ProjectManagement/ +│ ├── StoreProjectRequest.php +│ ├── UpdateProjectRequest.php +│ ├── StoreTaskRequest.php +│ ├── UpdateTaskRequest.php +│ ├── StoreIssueRequest.php +│ ├── UpdateIssueRequest.php +│ ├── BulkActionRequest.php +│ └── ImportProjectRequest.php +├── Models/Admin/ +│ ├── AdminPmProject.php +│ ├── AdminPmTask.php +│ └── AdminPmIssue.php +└── Services/ProjectManagement/ + ├── ProjectService.php + ├── TaskService.php + ├── IssueService.php + └── ImportService.php + +resources/views/project-management/ +├── index.blade.php # 대시보드 +├── import.blade.php # JSON Import +└── projects/ + ├── index.blade.php # 프로젝트 목록 + ├── create.blade.php # 프로젝트 생성 + ├── edit.blade.php # 프로젝트 수정 + ├── show.blade.php # 프로젝트 상세 (작업/이슈 포함) + └── partials/ + └── table.blade.php # HTMX 테이블 파셜 +``` + +## API 엔드포인트 + +### 프로젝트 API (`/api/admin/pm/projects`) + +| Method | URI | Name | 설명 | +|--------|-----|------|------| +| GET | `/` | index | 목록 조회 | +| POST | `/` | store | 생성 | +| GET | `/{id}` | show | 상세 조회 | +| PUT | `/{id}` | update | 수정 | +| DELETE | `/{id}` | destroy | 삭제 (soft) | +| POST | `/{id}/restore` | restore | 복원 | +| DELETE | `/{id}/force` | forceDestroy | 영구 삭제 | +| POST | `/{id}/status` | changeStatus | 상태 변경 | +| POST | `/{id}/duplicate` | duplicate | 복제 | +| GET | `/stats` | stats | 통계 | +| GET | `/dashboard` | dashboard | 대시보드 요약 | +| GET | `/dropdown` | dropdown | 드롭다운용 목록 | + +### 작업 API (`/api/admin/pm/tasks`) + +| Method | URI | Name | 설명 | +|--------|-----|------|------| +| GET | `/` | index | 목록 조회 | +| POST | `/` | store | 생성 | +| GET | `/{id}` | show | 상세 조회 | +| PUT | `/{id}` | update | 수정 | +| DELETE | `/{id}` | destroy | 삭제 | +| POST | `/{id}/status` | changeStatus | 상태 변경 | +| GET | `/project/{projectId}` | byProject | 프로젝트별 조회 | +| POST | `/project/{projectId}/reorder` | reorder | 순서 변경 | +| GET | `/project/{projectId}/stats` | stats | 프로젝트별 통계 | +| GET | `/urgent` | urgent | 긴급 작업 목록 | +| POST | `/bulk` | bulk | 일괄 작업 | + +### 이슈 API (`/api/admin/pm/issues`) + +| Method | URI | Name | 설명 | +|--------|-----|------|------| +| GET | `/` | index | 목록 조회 | +| POST | `/` | store | 생성 | +| GET | `/{id}` | show | 상세 조회 | +| PUT | `/{id}` | update | 수정 | +| DELETE | `/{id}` | destroy | 삭제 | +| POST | `/{id}/status` | changeStatus | 상태 변경 | +| GET | `/project/{projectId}` | byProject | 프로젝트별 조회 | +| GET | `/task/{taskId}` | byTask | 작업별 조회 | +| GET | `/stats` | stats | 통계 | +| GET | `/open` | open | 열린 이슈 목록 | +| POST | `/bulk` | bulk | 일괄 작업 | + +### Import API (`/api/admin/pm/import`) + +| Method | URI | Name | 설명 | +|--------|-----|------|------| +| GET | `/template` | template | 샘플 JSON 템플릿 | +| POST | `/validate` | validate | JSON 구조 검증 | +| POST | `/` | import | 새 프로젝트 Import | +| POST | `/project/{id}/tasks` | importTasks | 기존 프로젝트에 작업 추가 | + +## Web 라우트 + +| URI | Name | 설명 | +|-----|------|------| +| `/project-management` | pm.index | 대시보드 | +| `/project-management/projects` | pm.projects.index | 프로젝트 목록 | +| `/project-management/projects/create` | pm.projects.create | 프로젝트 생성 | +| `/project-management/projects/{id}` | pm.projects.show | 프로젝트 상세 | +| `/project-management/projects/{id}/edit` | pm.projects.edit | 프로젝트 수정 | +| `/project-management/import` | pm.import | JSON Import | + +## JSON Import 기능 + +### JSON 포맷 (새 프로젝트) + +```json +{ + "project": { + "name": "프로젝트명 (필수)", + "description": "프로젝트 설명", + "status": "active", + "start_date": "2025-01-01", + "end_date": "2025-03-31" + }, + "tasks": [ + { + "title": "작업 제목 (필수)", + "description": "작업 설명", + "status": "todo", + "priority": "high", + "due_date": "2025-01-15", + "issues": [ + { + "title": "이슈 제목 (필수)", + "description": "이슈 설명", + "type": "bug", + "status": "open" + } + ] + } + ] +} +``` + +### JSON 포맷 (기존 프로젝트에 작업 추가) + +```json +{ + "tasks": [ + { + "title": "추가할 작업", + "priority": "medium", + "issues": [...] + } + ] +} +``` + +## 사용 예시 + +### 프로젝트 생성 + +```bash +curl -X POST /api/admin/pm/projects \ + -H "Content-Type: application/json" \ + -d '{ + "name": "SAM MES 개발", + "description": "MES 시스템 개발 프로젝트", + "status": "active", + "start_date": "2025-01-01" + }' +``` + +### 작업 상태 변경 + +```bash +curl -X POST /api/admin/pm/tasks/1/status \ + -H "Content-Type: application/json" \ + -d '{"status": "in_progress"}' +``` + +### 작업 순서 변경 + +```bash +curl -X POST /api/admin/pm/tasks/project/1/reorder \ + -H "Content-Type: application/json" \ + -d '{"task_ids": [3, 1, 2]}' +``` + +### JSON Import + +```bash +curl -X POST /api/admin/pm/import \ + -H "Content-Type: application/json" \ + -d @project.json +``` + +## 대시보드 요약 API 응답 + +```json +{ + "success": true, + "data": { + "projects": { + "total": 5, + "active": 3, + "completed": 1, + "on_hold": 1 + }, + "tasks": { + "total": 25, + "todo": 10, + "in_progress": 8, + "done": 7, + "overdue": 2 + }, + "issues": { + "total": 15, + "open": 5, + "in_progress": 3, + "resolved": 4, + "closed": 3 + }, + "recent_projects": [...], + "urgent_tasks": [...] + } +} +``` + +## 관련 커밋 + +- `603062e` - feat(pm): 프로젝트 진행 관리 시스템 구현 \ No newline at end of file diff --git a/docs/project-management/sam_roadmap_import.json b/docs/project-management/sam_roadmap_import.json new file mode 100644 index 00000000..a571e203 --- /dev/null +++ b/docs/project-management/sam_roadmap_import.json @@ -0,0 +1,494 @@ +{ + "project": { + "name": "SAM 프로젝트 런칭 로드맵", + "description": "SAM MES 시스템 개발 및 런칭 준비 현황 추적 (백엔드 70%, 프론트엔드 50% 완료)", + "status": "active", + "start_date": "2025-11-24", + "end_date": "2026-03-31" + }, + "tasks": [ + { + "title": "MS1: 개발 완료", + "description": "모든 핵심 기능 개발 완료 및 내부 테스트 통과 (목표: 2025-12-31)", + "status": "in_progress", + "priority": "high", + "due_date": "2025-12-31", + "issues": [ + { + "title": "공정 라우팅 (Process Routing) 구현", + "description": "공정/생산 계획 - 공정 라우팅 개발", + "type": "feature", + "status": "open" + }, + { + "title": "작업지시 (Work Order) 구현", + "description": "공정/생산 계획 - 작업지시 개발", + "type": "feature", + "status": "open" + }, + { + "title": "생산실적 (Production Record) 구현", + "description": "공정/생산 계획 - 생산실적 개발", + "type": "feature", + "status": "open" + }, + { + "title": "공정 체크시트 구현", + "description": "공정/생산 계획 - 체크시트 개발", + "type": "feature", + "status": "open" + }, + { + "title": "단가 정책 로직 개발", + "description": "단가/원가 체계 - 공장별/중량/치수 기반 단가 정책", + "type": "feature", + "status": "open" + }, + { + "title": "원가 계산 서비스 개발", + "description": "단가/원가 체계 - 제품별, BOM 기반 원가 계산", + "type": "feature", + "status": "open" + }, + { + "title": "견적-수주 단가 연결 테이블", + "description": "단가/원가 체계 - 견적/수주 단가 연결", + "type": "feature", + "status": "open" + }, + { + "title": "견적서 HTML 템플릿", + "description": "견적서 출력 - HTML 템플릿 개발", + "type": "feature", + "status": "open" + }, + { + "title": "견적서 PDF 생성", + "description": "견적서 출력 - DomPDF/Snappy로 PDF 생성", + "type": "feature", + "status": "open" + }, + { + "title": "입출고 트랜잭션 설계", + "description": "재고/자재 관리 - 트랜잭션 구조 설계 및 구현", + "type": "feature", + "status": "open" + }, + { + "title": "재고 집계 API 개발", + "description": "재고/자재 관리 - 제품/부품/자재별 재고 집계", + "type": "feature", + "status": "open" + }, + { + "title": "LOT/시리얼 관리 확장", + "description": "재고/자재 관리 - LOT 및 시리얼 관리", + "type": "feature", + "status": "open" + }, + { + "title": "창고/위치 모델 구현", + "description": "창고/위치 관리 - Warehouse, Location 모델 및 계층 구조", + "type": "feature", + "status": "open" + }, + { + "title": "파일 Upload API 개발", + "description": "파일 시스템 완성 - 업로드/썸네일/권한", + "type": "feature", + "status": "open" + }, + { + "title": "알림 시스템 구현", + "description": "알림 시스템 - 이메일/카카오 발송 및 템플릿 관리", + "type": "feature", + "status": "open" + }, + { + "title": "테넌트 초기화 API", + "description": "테넌트 초기화 - 초기 데이터 생성 및 온보딩 자동화", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "MS2: 베타 런칭", + "description": "파일럿 고객 대상 베타 서비스 오픈 및 실전 검증 (목표: 2026-01-15)", + "status": "todo", + "priority": "high", + "due_date": "2026-01-15", + "issues": [ + { + "title": "베타 서버 구축", + "description": "베타 서버 환경 구축 및 도메인/SSL 설정", + "type": "feature", + "status": "open" + }, + { + "title": "파일럿 고객 선정 (2-3개사)", + "description": "베타 테스트 대상 고객 확보 및 계약", + "type": "feature", + "status": "open" + }, + { + "title": "초기 데이터 마이그레이션", + "description": "파일럿 고객 데이터 준비 및 마이그레이션", + "type": "feature", + "status": "open" + }, + { + "title": "고객 온보딩 프로세스 검증", + "description": "온보딩 가이드 작성 및 프로세스 검증", + "type": "feature", + "status": "open" + }, + { + "title": "고객지원 체계 구축", + "description": "티켓 시스템 가동 및 지원 채널 구축", + "type": "feature", + "status": "open" + }, + { + "title": "모니터링 대시보드 가동", + "description": "시스템 모니터링 및 알림 체계 구축", + "type": "feature", + "status": "open" + }, + { + "title": "사용자 매뉴얼 작성", + "description": "관리자/사용자 매뉴얼 및 온보딩 가이드", + "type": "feature", + "status": "open" + }, + { + "title": "베타 테스트 시나리오 실행", + "description": "견적→수주→생산, BOM 자재소요, 멀티테넌트 검증 등", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "MS3: 정식 런칭", + "description": "운영 서버 오픈 및 본격적인 영업/마케팅 시작 (목표: 2026-02-15)", + "status": "todo", + "priority": "high", + "due_date": "2026-02-15", + "issues": [ + { + "title": "운영 서버 구축 (이중화)", + "description": "운영 환경 이중화 및 안정성 확보", + "type": "feature", + "status": "open" + }, + { + "title": "베타 피드백 반영", + "description": "베타 기간 수집된 개선사항 반영 및 UI/UX 개선", + "type": "improvement", + "status": "open" + }, + { + "title": "보안 감사 통과", + "description": "외부 보안 감사 실시 및 취약점 조치", + "type": "feature", + "status": "open" + }, + { + "title": "법적 문서 완비", + "description": "이용약관, 개인정보처리방침, 서비스 계약서", + "type": "feature", + "status": "open" + }, + { + "title": "제품 소개 자료 제작", + "description": "PPT, 데모 사이트, 영업 제안서 템플릿", + "type": "feature", + "status": "open" + }, + { + "title": "가격 정책 확정", + "description": "요금제 및 프로모션 정책 수립", + "type": "feature", + "status": "open" + }, + { + "title": "마케팅 캠페인 시작", + "description": "프레스 릴리스, 런칭 프로모션, 웨비나", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "MS4: 안정화 완료", + "description": "서비스 안정화 및 초기 고객 성공 사례 확보 (목표: 2026-03-31)", + "status": "todo", + "priority": "medium", + "due_date": "2026-03-31", + "issues": [ + { + "title": "고객 10개사 확보", + "description": "정식 계약 고객 10개사 목표", + "type": "feature", + "status": "open" + }, + { + "title": "시스템 가용성 99.5% 달성", + "description": "안정성 확보 및 장애 대응 체계 강화", + "type": "improvement", + "status": "open" + }, + { + "title": "고객 만족도 4.0/5.0 달성", + "description": "월간 설문 및 피드백 반영", + "type": "improvement", + "status": "open" + }, + { + "title": "성공 사례 3건 확보", + "description": "견적 처리 시간 단축, 재고 정확도 향상 등 사례", + "type": "feature", + "status": "open" + }, + { + "title": "다음 분기 로드맵 수립", + "description": "Q2 이후 기능 고도화 계획", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "인프라 및 배포", + "description": "서버 환경, 도메인, 백업, 모니터링 구축", + "status": "in_progress", + "priority": "high", + "due_date": "2026-01-31", + "issues": [ + { + "title": "개발 서버 구성", + "description": "codebridge-x.com - 현재 운영 중", + "type": "feature", + "status": "in_progress" + }, + { + "title": "스테이징 서버 준비", + "description": "테스트용 스테이징 환경 구축", + "type": "feature", + "status": "open" + }, + { + "title": "운영 서버 선정 및 구성", + "description": "운영 환경 서버 선정", + "type": "feature", + "status": "open" + }, + { + "title": "Docker 배포 스크립트", + "description": "Docker Compose 배포 자동화", + "type": "feature", + "status": "open" + }, + { + "title": "CI/CD 파이프라인 구축", + "description": "자동 빌드/배포 파이프라인", + "type": "feature", + "status": "open" + }, + { + "title": "운영 도메인 및 SSL", + "description": "도메인 확정 및 인증서 발급", + "type": "feature", + "status": "open" + }, + { + "title": "백업 정책 및 스크립트", + "description": "자동 백업 및 복구 절차", + "type": "feature", + "status": "open" + }, + { + "title": "모니터링 시스템", + "description": "Grafana/Prometheus 대시보드 및 알림", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "테스트 및 품질 보증", + "description": "기능/성능/사용자 테스트 및 품질 검증", + "status": "todo", + "priority": "high", + "due_date": "2025-12-31", + "issues": [ + { + "title": "단위 테스트 (60% 커버리지)", + "description": "핵심 서비스 단위 테스트 작성", + "type": "feature", + "status": "open" + }, + { + "title": "통합 테스트", + "description": "API 통합 테스트 시나리오", + "type": "feature", + "status": "open" + }, + { + "title": "E2E 테스트", + "description": "사용자 시나리오 E2E 테스트", + "type": "feature", + "status": "open" + }, + { + "title": "부하/스트레스 테스트", + "description": "성능 테스트 및 API 응답 속도 측정", + "type": "feature", + "status": "open" + }, + { + "title": "알파 테스트 (내부)", + "description": "내부 팀 기능 테스트", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "보안 및 컴플라이언스", + "description": "보안 점검, 취약점 스캔, 법적 준비", + "status": "todo", + "priority": "high", + "due_date": "2026-02-01", + "issues": [ + { + "title": "OWASP Top 10 체크", + "description": "웹 보안 취약점 점검", + "type": "feature", + "status": "open" + }, + { + "title": "취약점 스캔", + "description": "자동화 보안 스캔 및 조치", + "type": "feature", + "status": "open" + }, + { + "title": "API 보안 검토", + "description": "인증/인가 보안 검토", + "type": "feature", + "status": "open" + }, + { + "title": "멀티테넌트 격리 검증", + "description": "테넌트 간 데이터 격리 100% 검증", + "type": "feature", + "status": "open" + }, + { + "title": "이용약관 작성", + "description": "서비스 이용약관 법률 검토", + "type": "feature", + "status": "open" + }, + { + "title": "개인정보처리방침", + "description": "개인정보 보호 정책 작성", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "문서화", + "description": "개발/사용자/운영 문서 작성", + "status": "in_progress", + "priority": "medium", + "due_date": "2026-01-31", + "issues": [ + { + "title": "API 문서 (Swagger) 100% 완성", + "description": "모든 엔드포인트 Swagger 문서화", + "type": "feature", + "status": "in_progress" + }, + { + "title": "데이터베이스 스키마 문서", + "description": "ERD 및 테이블 명세서", + "type": "feature", + "status": "open" + }, + { + "title": "아키텍처 문서", + "description": "시스템 아키텍처 설계 문서", + "type": "feature", + "status": "open" + }, + { + "title": "배포 가이드", + "description": "Docker 배포 및 설정 가이드", + "type": "feature", + "status": "open" + }, + { + "title": "관리자 매뉴얼", + "description": "Admin 패널 사용 매뉴얼", + "type": "feature", + "status": "open" + }, + { + "title": "사용자 가이드", + "description": "일반 사용자 기능 가이드", + "type": "feature", + "status": "open" + }, + { + "title": "운영 매뉴얼", + "description": "장애 대응, 백업/복구 절차", + "type": "feature", + "status": "open" + } + ] + }, + { + "title": "React 프론트엔드 개발", + "description": "Next.js 15 사용자 포털 개발 (현재 50% 완료)", + "status": "in_progress", + "priority": "medium", + "due_date": "2026-01-31", + "issues": [ + { + "title": "공통 레이아웃 정리", + "description": "레이아웃 컴포넌트 최종 정리", + "type": "feature", + "status": "in_progress" + }, + { + "title": "기준정보 관리 UI", + "description": "Category, CommonCode 관리 화면", + "type": "feature", + "status": "open" + }, + { + "title": "제품/부품/BOM 트리 UI", + "description": "BOM 구조 트리뷰 및 관리 화면", + "type": "feature", + "status": "open" + }, + { + "title": "견적/수주 화면", + "description": "견적서 작성 및 수주 관리 UI", + "type": "feature", + "status": "open" + }, + { + "title": "재고관리 UI", + "description": "재고 현황 및 입출고 관리 화면", + "type": "feature", + "status": "open" + } + ] + } + ] +} \ No newline at end of file