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 +
+``` + +### 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 + +| ID | +이름 | +이메일 | +역할 | +부서 | +상태 | +작업 | +
|---|---|---|---|---|---|---|
| {{ $user->id }} | +{{ $user->name }} | +{{ $user->email }} | +{{ $user->role->name }} | +{{ $user->department->name }} | ++ + {{ $user->is_active ? '활성' : '비활성' }} + + | +
+
+ 수정
+
+
+ |
+
| ID | +이름 | +설명 | +권한 수 | +액션 | +
|---|---|---|---|---|
| {{ $role->id }} | +{{ $role->name }} | +{{ $role->description }} | +{{ $role->permissions_count }} | ++ 수정 + + | +
| 등록된 역할이 없습니다. | +||||