docs: MNG 프로젝트 문서 정비
- 개발 단계별 문서 추가 (00_OVERVIEW ~ 06_PHASE) - 기술 표준 문서 추가 (99_TECHNICAL_STANDARDS) - 개발 프로세스 및 패턴 문서 추가 - API_FLOW_TESTER_DESIGN, DEV_PROCESS - HTMX_API_PATTERN, LAYOUT_PATTERN - SETUP_GUIDE, MNG_PROJECT_PLAN - 프로젝트 관리 문서 추가 (project-management/) - INDEX.md, MNG_CRITICAL_RULES.md 업데이트
This commit is contained in:
259
docs/00_OVERVIEW.md
Normal file
259
docs/00_OVERVIEW.md
Normal file
@@ -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
|
||||
454
docs/01_PHASE1_MASTER_DATA.md
Normal file
454
docs/01_PHASE1_MASTER_DATA.md
Normal file
@@ -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
|
||||
300
docs/02_PHASE2_SETTINGS.md
Normal file
300
docs/02_PHASE2_SETTINGS.md
Normal file
@@ -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
|
||||
491
docs/03_PHASE3_BUSINESS_CORE.md
Normal file
491
docs/03_PHASE3_BUSINESS_CORE.md
Normal file
@@ -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
|
||||
387
docs/04_PHASE4_CONTENT.md
Normal file
387
docs/04_PHASE4_CONTENT.md
Normal file
@@ -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
|
||||
447
docs/05_PHASE5_REVENUE.md
Normal file
447
docs/05_PHASE5_REVENUE.md
Normal file
@@ -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
|
||||
500
docs/06_PHASE6_COMM_STATS.md
Normal file
500
docs/06_PHASE6_COMM_STATS.md
Normal file
@@ -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
|
||||
895
docs/99_TECHNICAL_STANDARDS.md
Normal file
895
docs/99_TECHNICAL_STANDARDS.md
Normal file
@@ -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')
|
||||
<a href="{{ route('users.create') }}">새 사용자</a>
|
||||
@endcan
|
||||
```
|
||||
|
||||
### 4.3 XSS 방지
|
||||
|
||||
**Blade 자동 이스케이프:**
|
||||
```blade
|
||||
{{-- 자동 이스케이프 (안전) --}}
|
||||
{{ $user->name }}
|
||||
|
||||
{{-- 이스케이프 없음 (주의) --}}
|
||||
{!! $htmlContent !!}
|
||||
```
|
||||
|
||||
### 4.4 CSRF 보호
|
||||
|
||||
**모든 POST/PUT/DELETE 요청에 @csrf:**
|
||||
```blade
|
||||
<form method="POST" action="{{ route('users.store') }}">
|
||||
@csrf
|
||||
<!-- 폼 필드 -->
|
||||
</form>
|
||||
```
|
||||
|
||||
### 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 기반)
|
||||
1036
docs/API_FLOW_TESTER_DESIGN.md
Normal file
1036
docs/API_FLOW_TESTER_DESIGN.md
Normal file
File diff suppressed because it is too large
Load Diff
768
docs/DEV_PROCESS.md
Normal file
768
docs/DEV_PROCESS.md
Normal file
@@ -0,0 +1,768 @@
|
||||
# MNG 프로젝트 개발 프로세스
|
||||
|
||||
## 🎯 개발 철학
|
||||
|
||||
```
|
||||
API 우선 → HTMX 연동 → 단순하고 수정 용이한 코드
|
||||
```
|
||||
|
||||
### 핵심 원칙
|
||||
1. **API First**: 모든 기능은 API로 먼저 개발
|
||||
2. **Service-First**: 비즈니스 로직은 Service에만
|
||||
3. **HTMX Driven**: JS 최소화, HTML 속성으로 인터랙션
|
||||
4. **DaisyUI Only**: 커스텀 CSS 금지, DaisyUI 클래스만 사용
|
||||
|
||||
---
|
||||
|
||||
## 📐 표준 개발 프로세스 (6단계)
|
||||
|
||||
### Phase 0: 환경 구성 (최초 1회)
|
||||
**참조**: `claudedocs/mng/SETUP_GUIDE.md`
|
||||
|
||||
```bash
|
||||
# SETUP_GUIDE.md의 Step 1-10 참조
|
||||
# 1. Laravel 프로젝트 생성
|
||||
# 2. Docker 설정 파일 생성
|
||||
# 3. docker-compose.yml 업데이트
|
||||
# 4. nginx.conf 업데이트
|
||||
# 5. Tailwind + DaisyUI + HTMX 설정
|
||||
# 6. admin/ 모델 복사
|
||||
# 7. Docker 빌드 및 실행
|
||||
# 8. 동작 확인 (http://mng.sam.kr)
|
||||
|
||||
# 스킬 사용:
|
||||
/sc:implement "SETUP_GUIDE.md 따라 MNG 환경 구성"
|
||||
```
|
||||
|
||||
### Phase 1: 준비 단계
|
||||
```bash
|
||||
# 1. 기능 분석 (Sequential Thinking)
|
||||
/sc:analyze --think
|
||||
|
||||
# 2. 요구사항 정리
|
||||
- 입력: 어떤 데이터를 받는가?
|
||||
- 처리: 어떤 비즈니스 로직?
|
||||
- 출력: 어떤 데이터를 반환?
|
||||
- 화면: 어떤 UI 필요?
|
||||
|
||||
# 3. API 명세 작성
|
||||
- 엔드포인트: GET /api/admin/users
|
||||
- Request: { search: string, role_id?: number }
|
||||
- Response: { success, data, message, meta }
|
||||
```
|
||||
|
||||
### Phase 1: DB & Model (1단계)
|
||||
```bash
|
||||
# 1-1. 마이그레이션 확인
|
||||
# 기존 테이블 사용? → 마이그레이션 불필요
|
||||
# 신규 테이블? → admin_* or stat_* 접두사
|
||||
|
||||
# 1-2. 모델 확인/생성
|
||||
# admin/app/Models에서 복사했는지 확인
|
||||
# BelongsToTenant, HasAuditLog 트레잇 적용
|
||||
|
||||
# 예시: mng/app/Models/User.php
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\HasAuditLog;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use BelongsToTenant, HasAuditLog;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'email', 'password', 'name',
|
||||
'role_id', 'department_id', 'is_active',
|
||||
];
|
||||
|
||||
public function role()
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
public function department()
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Service Layer (2단계)
|
||||
```bash
|
||||
# 2-1. Service 생성 (비즈니스 로직)
|
||||
# mng/app/Services/UserService.php
|
||||
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserService
|
||||
{
|
||||
/**
|
||||
* 사용자 목록 조회 (검색, 필터, 페이징)
|
||||
*/
|
||||
public function getUsers(array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$query = User::with(['role', 'department']);
|
||||
|
||||
// 검색
|
||||
if (!empty($filters['search'])) {
|
||||
$query->where(function ($q) use ($filters) {
|
||||
$q->where('name', 'like', "%{$filters['search']}%")
|
||||
->orWhere('email', 'like', "%{$filters['search']}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 역할 필터
|
||||
if (!empty($filters['role_id'])) {
|
||||
$query->where('role_id', $filters['role_id']);
|
||||
}
|
||||
|
||||
return $query->paginate(20);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 생성
|
||||
*/
|
||||
public function createUser(array $data): User
|
||||
{
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
$data['tenant_id'] = auth()->user()->tenant_id;
|
||||
|
||||
return User::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 수정
|
||||
*/
|
||||
public function updateUser(User $user, array $data): User
|
||||
{
|
||||
if (!empty($data['password'])) {
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
} else {
|
||||
unset($data['password']);
|
||||
}
|
||||
|
||||
$user->update($data);
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 삭제 (Soft Delete)
|
||||
*/
|
||||
public function deleteUser(User $user): bool
|
||||
{
|
||||
return $user->delete();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: API Controller (3단계)
|
||||
```bash
|
||||
# 3-1. FormRequest 생성
|
||||
# mng/app/Http/Requests/StoreUserRequest.php
|
||||
|
||||
<?php
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Policy로 권한 체크
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:8',
|
||||
'role_id' => 'required|exists:roles,id',
|
||||
'department_id' => 'required|exists:departments,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'users.validation.name_required',
|
||||
'email.required' => 'users.validation.email_required',
|
||||
'email.email' => 'users.validation.email_invalid',
|
||||
'email.unique' => 'users.validation.email_unique',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
# 3-2. API Controller 생성
|
||||
# mng/app/Http/Controllers/Api/Admin/UserController.php
|
||||
|
||||
<?php
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreUserRequest;
|
||||
use App\Http\Requests\UpdateUserRequest;
|
||||
use App\Services\UserService;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private UserService $userService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 사용자 목록 (API)
|
||||
* GET /api/admin/users
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$users = $this->userService->getUsers($request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $users->items(),
|
||||
'message' => 'users.retrieved',
|
||||
'meta' => [
|
||||
'current_page' => $users->currentPage(),
|
||||
'last_page' => $users->lastPage(),
|
||||
'per_page' => $users->perPage(),
|
||||
'total' => $users->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 생성 (API)
|
||||
* POST /api/admin/users
|
||||
*/
|
||||
public function store(StoreUserRequest $request): JsonResponse
|
||||
{
|
||||
$user = $this->userService->createUser($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $user,
|
||||
'message' => 'users.created',
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 수정 (API)
|
||||
* PUT /api/admin/users/{user}
|
||||
*/
|
||||
public function update(UpdateUserRequest $request, User $user): JsonResponse
|
||||
{
|
||||
$user = $this->userService->updateUser($user, $request->validated());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $user,
|
||||
'message' => 'users.updated',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 삭제 (API)
|
||||
* DELETE /api/admin/users/{user}
|
||||
*/
|
||||
public function destroy(User $user): JsonResponse
|
||||
{
|
||||
$this->userService->deleteUser($user);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'users.deleted',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
# 3-3. 라우트 등록
|
||||
# mng/routes/api.php
|
||||
|
||||
Route::middleware(['auth:sanctum', 'admin.permission'])
|
||||
->prefix('admin')
|
||||
->group(function () {
|
||||
Route::apiResource('users', UserController::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 4: Blade + HTMX (4단계)
|
||||
```bash
|
||||
# 4-1. HTML 응답용 Controller (선택)
|
||||
# API + Blade 부분 HTML 반환
|
||||
|
||||
# mng/app/Http/Controllers/Api/Admin/UserController.php (추가)
|
||||
|
||||
/**
|
||||
* 사용자 목록 (HTMX용 Blade HTML)
|
||||
* GET /api/admin/users?format=html
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$users = $this->userService->getUsers($request->all());
|
||||
|
||||
// HTMX 요청 시 부분 HTML 반환
|
||||
if ($request->header('HX-Request')) {
|
||||
return view('users.partials.table', compact('users'));
|
||||
}
|
||||
|
||||
// 일반 요청 시 JSON 반환
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $users->items(),
|
||||
'message' => 'users.retrieved',
|
||||
'meta' => [
|
||||
'current_page' => $users->currentPage(),
|
||||
'last_page' => $users->lastPage(),
|
||||
'per_page' => $users->perPage(),
|
||||
'total' => $users->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
# 4-2. Blade 템플릿 작성
|
||||
# mng/resources/views/users/index.blade.php
|
||||
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-4">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">사용자 관리</h1>
|
||||
<a href="/users/create" class="btn btn-primary">사용자 추가</a>
|
||||
</div>
|
||||
|
||||
{{-- 검색/필터 --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<form hx-get="/api/admin/users"
|
||||
hx-target="#user-table"
|
||||
hx-trigger="submit">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<input type="text" name="search"
|
||||
placeholder="이름 또는 이메일"
|
||||
class="input input-bordered" />
|
||||
|
||||
<select name="role_id" class="select select-bordered">
|
||||
<option value="">전체 역할</option>
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->id }}">{{ $role->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-primary">검색</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 테이블 영역 --}}
|
||||
<div id="user-table"
|
||||
hx-get="/api/admin/users"
|
||||
hx-trigger="load">
|
||||
{{-- 초기 로드 시 서버에서 HTML 받아서 여기 삽입 --}}
|
||||
<div class="flex justify-center p-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
# 4-3. 부분 템플릿 (HTMX 응답용)
|
||||
# mng/resources/views/users/partials/table.blade.php
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>이름</th>
|
||||
<th>이메일</th>
|
||||
<th>역할</th>
|
||||
<th>부서</th>
|
||||
<th>상태</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
<tr>
|
||||
<td>{{ $user->id }}</td>
|
||||
<td>{{ $user->name }}</td>
|
||||
<td>{{ $user->email }}</td>
|
||||
<td>{{ $user->role->name }}</td>
|
||||
<td>{{ $user->department->name }}</td>
|
||||
<td>
|
||||
<span class="badge {{ $user->is_active ? 'badge-success' : 'badge-error' }}">
|
||||
{{ $user->is_active ? '활성' : '비활성' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="/users/{{ $user->id }}/edit" class="btn btn-sm">수정</a>
|
||||
<button hx-delete="/api/admin/users/{{ $user->id }}"
|
||||
hx-confirm="정말 삭제하시겠습니까?"
|
||||
hx-target="closest tr"
|
||||
hx-swap="outerHTML swap:1s"
|
||||
class="btn btn-sm btn-error">
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- 페이징 (HTMX) --}}
|
||||
<div class="flex justify-center mt-4">
|
||||
@if($users->hasPages())
|
||||
<div class="btn-group">
|
||||
@foreach($users->getUrlRange(1, $users->lastPage()) as $page => $url)
|
||||
<button hx-get="{{ $url }}"
|
||||
hx-target="#user-table"
|
||||
class="btn btn-sm {{ $page == $users->currentPage() ? 'btn-active' : '' }}">
|
||||
{{ $page }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Phase 5: 테스트 & 검증 (5단계)
|
||||
```bash
|
||||
# 5-1. Feature Test 작성
|
||||
# mng/tests/Feature/UserControllerTest.php
|
||||
|
||||
<?php
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
class UserControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_사용자_목록_조회()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->getJson('/api/admin/users');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data',
|
||||
'message',
|
||||
'meta',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_사용자_생성()
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$response = $this->actingAs($admin)
|
||||
->postJson('/api/admin/users', [
|
||||
'name' => '홍길동',
|
||||
'email' => 'hong@example.com',
|
||||
'password' => 'password123',
|
||||
'role_id' => $role->id,
|
||||
'department_id' => 1,
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'users.created',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'hong@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
# 5-2. 테스트 실행
|
||||
php artisan test --filter=UserControllerTest
|
||||
|
||||
# 5-3. 코드 스타일 검증
|
||||
./vendor/bin/pint
|
||||
|
||||
# 5-4. 품질 체크리스트
|
||||
□ Service-First (비즈니스 로직 → Service)
|
||||
□ FormRequest (컨트롤러 검증 금지)
|
||||
□ BelongsToTenant (multi-tenant 스코프)
|
||||
□ i18n 키 (하드코딩 금지)
|
||||
□ Soft Delete (deleted_at)
|
||||
□ 감사 로그 (HasAuditLog trait)
|
||||
□ API 응답 형식 ({success, data, message, meta})
|
||||
□ HTMX 속성 (hx-get, hx-target, hx-swap)
|
||||
□ DaisyUI 클래스만 사용
|
||||
□ Feature Test 통과
|
||||
□ Pint 통과
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 실전 워크플로 (스킬 활용)
|
||||
|
||||
### 신규 기능 개발 시
|
||||
```bash
|
||||
# Step 1: 기능 분석 및 설계
|
||||
/sc:design "사용자 관리 기능"
|
||||
# → Sequential Thinking으로 요구사항 분석
|
||||
# → API 명세 도출
|
||||
|
||||
# Step 2: 구현
|
||||
/sc:implement "사용자 관리 API 구현"
|
||||
# → Model, Service, Controller, FormRequest 생성
|
||||
# → 자동으로 5단계 프로세스 진행
|
||||
|
||||
# Step 3: Blade + HTMX 구현
|
||||
# 직접 작성 (단순하므로 AI 불필요)
|
||||
# 또는 /sc:implement "사용자 목록 Blade 화면"
|
||||
|
||||
# Step 4: 테스트
|
||||
/sc:test "UserController"
|
||||
# → Feature Test 자동 생성 및 실행
|
||||
|
||||
# Step 5: 검증 및 커밋
|
||||
code-workflow 스킬 사용
|
||||
# → 분석 → 수정 → 검증 → 정리 → 커밋
|
||||
```
|
||||
|
||||
### 버그 수정 시
|
||||
```bash
|
||||
# Step 1: 문제 분석
|
||||
/sc:troubleshoot "사용자 목록 페이징 안됨"
|
||||
# → Root Cause 분석
|
||||
|
||||
# Step 2: 수정
|
||||
/sc:improve "UserService 페이징 로직"
|
||||
|
||||
# Step 3: 테스트
|
||||
/sc:test
|
||||
|
||||
# Step 4: 커밋
|
||||
code-workflow
|
||||
```
|
||||
|
||||
### 리팩토링 시
|
||||
```bash
|
||||
/sc:improve --focus quality "UserController"
|
||||
/sc:analyze --think-hard "전체 아키텍처"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 체크리스트 템플릿
|
||||
|
||||
### 기능 개발 완료 체크리스트
|
||||
```
|
||||
기능명: _______________
|
||||
|
||||
[ ] Phase 1: DB & Model
|
||||
[ ] 마이그레이션 (필요 시)
|
||||
[ ] 모델 생성/복사
|
||||
[ ] BelongsToTenant 적용
|
||||
[ ] HasAuditLog 적용
|
||||
[ ] 관계 설정 (belongsTo, hasMany)
|
||||
|
||||
[ ] Phase 2: Service Layer
|
||||
[ ] Service 생성
|
||||
[ ] 비즈니스 로직 구현
|
||||
[ ] 트랜잭션 처리
|
||||
[ ] 예외 처리
|
||||
|
||||
[ ] Phase 3: API Controller
|
||||
[ ] FormRequest 생성 (Validation)
|
||||
[ ] Controller 생성
|
||||
[ ] API 응답 형식 준수
|
||||
[ ] i18n 키 사용
|
||||
[ ] 라우트 등록
|
||||
|
||||
[ ] Phase 4: Blade + HTMX
|
||||
[ ] 메인 페이지 (index.blade.php)
|
||||
[ ] 부분 템플릿 (partials/*.blade.php)
|
||||
[ ] HTMX 속성 (hx-get, hx-post, hx-delete)
|
||||
[ ] DaisyUI 컴포넌트만 사용
|
||||
[ ] HX-Request 헤더 처리
|
||||
|
||||
[ ] Phase 5: 테스트 & 검증
|
||||
[ ] Feature Test 작성
|
||||
[ ] 테스트 통과 (php artisan test)
|
||||
[ ] Pint 통과 (./vendor/bin/pint)
|
||||
[ ] Swagger 문서화 (선택)
|
||||
|
||||
[ ] 커밋
|
||||
[ ] code-workflow 스킬 사용
|
||||
[ ] CURRENT_WORKS.md 업데이트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 HTMX 패턴 라이브러리
|
||||
|
||||
### 1. 목록 조회 (Load)
|
||||
```blade
|
||||
<div hx-get="/api/admin/users"
|
||||
hx-trigger="load"
|
||||
hx-target="this">
|
||||
<span class="loading loading-spinner"></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. 검색/필터 (Submit)
|
||||
```blade
|
||||
<form hx-get="/api/admin/users"
|
||||
hx-target="#results"
|
||||
hx-trigger="submit">
|
||||
<input name="search" class="input input-bordered" />
|
||||
<button class="btn btn-primary">검색</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 3. 생성 (POST)
|
||||
```blade
|
||||
<form hx-post="/api/admin/users"
|
||||
hx-target="#user-list"
|
||||
hx-swap="beforeend">
|
||||
<!-- 폼 필드 -->
|
||||
<button class="btn btn-primary">저장</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 4. 수정 (PUT)
|
||||
```blade
|
||||
<form hx-put="/api/admin/users/{{ $user->id }}"
|
||||
hx-target="closest tr"
|
||||
hx-swap="outerHTML">
|
||||
<!-- 폼 필드 -->
|
||||
<button class="btn btn-primary">수정</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 5. 삭제 (DELETE)
|
||||
```blade
|
||||
<button hx-delete="/api/admin/users/{{ $user->id }}"
|
||||
hx-confirm="정말 삭제하시겠습니까?"
|
||||
hx-target="closest tr"
|
||||
hx-swap="outerHTML swap:1s"
|
||||
class="btn btn-error">
|
||||
삭제
|
||||
</button>
|
||||
```
|
||||
|
||||
### 6. 무한 스크롤
|
||||
```blade
|
||||
<div hx-get="/api/admin/users?page=2"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="afterend">
|
||||
더보기...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 7. 폴링 (자동 갱신)
|
||||
```blade
|
||||
<div hx-get="/api/admin/stats"
|
||||
hx-trigger="every 10s"
|
||||
hx-target="this">
|
||||
통계: {{ $stats }}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 8. 디바운싱 (입력 지연)
|
||||
```blade
|
||||
<input hx-get="/api/admin/users/search"
|
||||
hx-trigger="keyup changed delay:500ms"
|
||||
hx-target="#search-results"
|
||||
name="q"
|
||||
class="input input-bordered" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 개발 환경 설정
|
||||
|
||||
### 필수 패키지 설치
|
||||
```bash
|
||||
# Composer
|
||||
composer require laravel/sanctum
|
||||
composer require darkaonline/l5-swagger
|
||||
composer require --dev laravel/pint
|
||||
|
||||
# NPM
|
||||
npm install -D tailwindcss daisyui @tailwindcss/forms
|
||||
npm install htmx.org
|
||||
```
|
||||
|
||||
### HTMX 설정
|
||||
```js
|
||||
// resources/js/app.js
|
||||
import htmx from 'htmx.org';
|
||||
window.htmx = htmx;
|
||||
|
||||
// HTMX 전역 설정
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// CSRF 토큰 자동 추가
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
event.detail.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Blade 레이아웃
|
||||
```blade
|
||||
<!-- resources/views/layouts/app.blade.php -->
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="light">
|
||||
<head>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{{ config('app.name') }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body>
|
||||
@yield('content')
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 다음 단계
|
||||
|
||||
1. **Phase 1 시작**: Laravel 프로젝트 생성 및 환경 구성
|
||||
2. **인증 구현**: 로그인 API + Blade 화면
|
||||
3. **첫 기능 개발**: 사용자 관리 (이 프로세스 적용)
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-01-20
|
||||
**버전**: 1.0
|
||||
**기술 스택**: Laravel 12 + MySQL 8.0 + HTMX + DaisyUI
|
||||
**목표**: API 우선, 단순함, 수정 용이성
|
||||
538
docs/HTMX_API_PATTERN.md
Normal file
538
docs/HTMX_API_PATTERN.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# MNG HTMX + API 패턴 가이드
|
||||
|
||||
**작성일:** 2025-01-24
|
||||
**목적:** MNG 프로젝트의 표준 HTMX + API 패턴 문서화 (Tenant 패턴 기반)
|
||||
|
||||
**관련 문서:**
|
||||
- [LAYOUT_PATTERN.md](./LAYOUT_PATTERN.md) - 페이지 레이아웃 및 Tenant Selector 패턴
|
||||
- [99_TECHNICAL_STANDARDS.md](./99_TECHNICAL_STANDARDS.md) - SAM API Rules 기반 기술 표준
|
||||
|
||||
---
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [패턴 개요](#1-패턴-개요)
|
||||
2. [아키텍처 구조](#2-아키텍처-구조)
|
||||
3. [구현 가이드](#3-구현-가이드)
|
||||
4. [파일 구조](#4-파일-구조)
|
||||
5. [체크리스트](#5-체크리스트)
|
||||
|
||||
---
|
||||
|
||||
## 1. 패턴 개요
|
||||
|
||||
### 1.1 왜 HTMX + API 패턴인가?
|
||||
|
||||
**MNG 프로젝트의 표준 아키텍처 패턴입니다.**
|
||||
|
||||
- **일관성**: 모든 CRUD 기능이 동일한 패턴 사용
|
||||
- **성능**: 페이지 전체 리로드 없이 동적 업데이트
|
||||
- **유지보수성**: Blade 템플릿 + HTMX로 간단한 인터랙션
|
||||
- **확장성**: API는 HTMX와 독립적으로 사용 가능
|
||||
|
||||
### 1.2 기본 원칙
|
||||
|
||||
1. **Blade View는 화면만 담당** - 데이터 처리 로직 없음
|
||||
2. **API Controller는 HTMX와 JSON 모두 지원**
|
||||
3. **HTMX 요청 시 HTML partial 반환**
|
||||
4. **일반 요청 시 JSON 반환**
|
||||
|
||||
---
|
||||
|
||||
## 2. 아키텍처 구조
|
||||
|
||||
### 2.1 전체 흐름도
|
||||
|
||||
```
|
||||
[Browser]
|
||||
↓ (HTMX Request with HX-Request header)
|
||||
[Route: web.php]
|
||||
↓ (Blade View 반환)
|
||||
[Controller: RoleController]
|
||||
↓ (view('roles.index') - 화면만)
|
||||
[Blade View: roles/index.blade.php]
|
||||
↓ (hx-get="/api/admin/roles")
|
||||
[API Route: api.php]
|
||||
↓ (API 엔드포인트)
|
||||
[Api\Admin\RoleController]
|
||||
↓ (Service 호출)
|
||||
[RoleService]
|
||||
↓ (비즈니스 로직)
|
||||
[Database]
|
||||
↓
|
||||
[RoleService]
|
||||
↓ (데이터 반환)
|
||||
[Api\Admin\RoleController]
|
||||
↓ (HTMX 요청 감지: HX-Request header)
|
||||
↓ (HTML partial 렌더링)
|
||||
[Blade Partial: roles/partials/table.blade.php]
|
||||
↓ (JSON with html)
|
||||
[Browser - HTMX]
|
||||
↓ (DOM 업데이트: #role-table)
|
||||
[User sees updated table]
|
||||
```
|
||||
|
||||
### 2.2 컨트롤러 분리
|
||||
|
||||
#### Blade Controller (화면 전용)
|
||||
```php
|
||||
// app/Http/Controllers/RoleController.php
|
||||
class RoleController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('roles.index'); // 화면만 반환
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('roles.create');
|
||||
}
|
||||
|
||||
public function edit(int $id): View
|
||||
{
|
||||
$role = $this->roleService->getRoleById($id);
|
||||
return view('roles.edit', compact('role'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### API Controller (데이터 처리)
|
||||
```php
|
||||
// app/Http/Controllers/Api/Admin/RoleController.php
|
||||
class RoleController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$roles = $this->roleService->getRoles($request->all());
|
||||
|
||||
// HTMX 요청 감지
|
||||
if ($request->header('HX-Request')) {
|
||||
$html = view('roles.partials.table', compact('roles'))->render();
|
||||
return response()->json(['html' => $html]);
|
||||
}
|
||||
|
||||
// 일반 API 요청
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $roles->items(),
|
||||
'meta' => [/*...*/],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreRoleRequest $request): JsonResponse
|
||||
{
|
||||
$role = $this->roleService->createRole($request->validated());
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '역할이 생성되었습니다.',
|
||||
'redirect' => route('roles.index'),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $role,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$this->roleService->deleteRole($id);
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '역할이 삭제되었습니다.',
|
||||
'action' => 'remove',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '역할이 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 가이드
|
||||
|
||||
### 3.1 Blade View 구조
|
||||
|
||||
#### index.blade.php (메인 화면)
|
||||
```blade
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto">
|
||||
<h1>🔑 역할 관리</h1>
|
||||
|
||||
<!-- 필터 폼 -->
|
||||
<form id="filterForm">
|
||||
<input type="text" name="search" placeholder="검색...">
|
||||
<button type="submit">검색</button>
|
||||
</form>
|
||||
|
||||
<!-- HTMX 동적 로딩 영역 -->
|
||||
<div id="role-table"
|
||||
hx-get="/api/admin/roles"
|
||||
hx-trigger="load, filterSubmit from:body"
|
||||
hx-include="#filterForm"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
class="bg-white rounded-lg shadow-sm">
|
||||
<!-- 로딩 스피너 -->
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script>
|
||||
// 폼 제출 시 HTMX 트리거
|
||||
document.getElementById('filterForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
htmx.trigger('#role-table', 'filterSubmit');
|
||||
});
|
||||
|
||||
// HTMX 응답 처리
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'role-table') {
|
||||
const response = JSON.parse(event.detail.xhr.response);
|
||||
if (response.html) {
|
||||
event.detail.target.innerHTML = response.html;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 삭제 확인
|
||||
window.confirmDelete = function(id, name) {
|
||||
if (confirm(`"${name}" 역할을 삭제하시겠습니까?`)) {
|
||||
htmx.ajax('DELETE', `/api/admin/roles/${id}`, {
|
||||
target: '#role-table',
|
||||
swap: 'none',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
}).then(() => {
|
||||
htmx.trigger('#role-table', 'filterSubmit');
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
```
|
||||
|
||||
#### partials/table.blade.php (HTMX 응답용 HTML partial)
|
||||
```blade
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>이름</th>
|
||||
<th>설명</th>
|
||||
<th>권한 수</th>
|
||||
<th>액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($roles as $role)
|
||||
<tr>
|
||||
<td>{{ $role->id }}</td>
|
||||
<td>{{ $role->name }}</td>
|
||||
<td>{{ $role->description }}</td>
|
||||
<td>{{ $role->permissions_count }}</td>
|
||||
<td>
|
||||
<a href="{{ route('roles.edit', $role->id) }}">수정</a>
|
||||
<button onclick="confirmDelete({{ $role->id }}, '{{ $role->name }}')">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">등록된 역할이 없습니다.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
@include('partials.pagination', [
|
||||
'paginator' => $roles,
|
||||
'target' => '#role-table',
|
||||
'includeForm' => '#filterForm'
|
||||
])
|
||||
```
|
||||
|
||||
### 3.2 라우트 설정
|
||||
|
||||
#### web.php (Blade 화면 라우트)
|
||||
```php
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::prefix('roles')->name('roles.')->group(function () {
|
||||
Route::get('/', [RoleController::class, 'index'])->name('index');
|
||||
Route::get('/create', [RoleController::class, 'create'])->name('create');
|
||||
Route::get('/{id}/edit', [RoleController::class, 'edit'])->name('edit');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### api.php (API 엔드포인트)
|
||||
```php
|
||||
Route::middleware(['web', 'auth'])->prefix('admin')->name('api.admin.')->group(function () {
|
||||
Route::prefix('roles')->name('roles.')->group(function () {
|
||||
Route::get('/', [RoleController::class, 'index'])->name('index');
|
||||
Route::post('/', [RoleController::class, 'store'])->name('store');
|
||||
Route::get('/{id}', [RoleController::class, 'show'])->name('show');
|
||||
Route::put('/{id}', [RoleController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [RoleController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3.3 HTMX 핵심 개념
|
||||
|
||||
#### hx-get, hx-post, hx-put, hx-delete
|
||||
```html
|
||||
<!-- GET 요청 -->
|
||||
<div hx-get="/api/admin/roles" hx-trigger="load">로딩 중...</div>
|
||||
|
||||
<!-- POST 요청 (폼 제출) -->
|
||||
<form hx-post="/api/admin/roles" hx-target="#role-table">
|
||||
<input name="name" required>
|
||||
<button type="submit">생성</button>
|
||||
</form>
|
||||
|
||||
<!-- DELETE 요청 (JavaScript) -->
|
||||
<button onclick="htmx.ajax('DELETE', '/api/admin/roles/1', {target: '#role-table'})">삭제</button>
|
||||
```
|
||||
|
||||
#### hx-trigger
|
||||
```html
|
||||
<!-- 페이지 로드 시 -->
|
||||
<div hx-get="/api/admin/roles" hx-trigger="load"></div>
|
||||
|
||||
<!-- 커스텀 이벤트 -->
|
||||
<div hx-get="/api/admin/roles" hx-trigger="filterSubmit from:body"></div>
|
||||
|
||||
<!-- 여러 트리거 조합 -->
|
||||
<div hx-trigger="load, filterSubmit from:body"></div>
|
||||
```
|
||||
|
||||
#### hx-include
|
||||
```html
|
||||
<!-- 폼 데이터 포함 -->
|
||||
<div hx-get="/api/admin/roles" hx-include="#filterForm"></div>
|
||||
```
|
||||
|
||||
#### hx-headers
|
||||
```html
|
||||
<!-- CSRF 토큰 포함 -->
|
||||
<div hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'></div>
|
||||
```
|
||||
|
||||
#### hx-target, hx-swap
|
||||
```html
|
||||
<!-- 특정 엘리먼트 타겟 -->
|
||||
<button hx-delete="/api/admin/roles/1" hx-target="#role-table">삭제</button>
|
||||
|
||||
<!-- swap 전략 -->
|
||||
<div hx-swap="innerHTML">기본값</div>
|
||||
<div hx-swap="outerHTML">엘리먼트 자체 교체</div>
|
||||
<div hx-swap="none">응답 무시</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 파일 구조
|
||||
|
||||
### 4.1 표준 디렉토리 구조
|
||||
|
||||
```
|
||||
mng/
|
||||
├── app/
|
||||
│ ├── Http/
|
||||
│ │ ├── Controllers/
|
||||
│ │ │ ├── RoleController.php # Blade 화면만
|
||||
│ │ │ └── Api/
|
||||
│ │ │ └── Admin/
|
||||
│ │ │ └── RoleController.php # API 로직
|
||||
│ │ └── Requests/
|
||||
│ │ ├── StoreRoleRequest.php
|
||||
│ │ └── UpdateRoleRequest.php
|
||||
│ ├── Services/
|
||||
│ │ └── RoleService.php # 비즈니스 로직
|
||||
│ └── Models/
|
||||
│ └── Role.php
|
||||
├── resources/
|
||||
│ └── views/
|
||||
│ └── roles/
|
||||
│ ├── index.blade.php # 메인 화면
|
||||
│ ├── create.blade.php # 생성 화면
|
||||
│ ├── edit.blade.php # 수정 화면
|
||||
│ └── partials/
|
||||
│ ├── table.blade.php # HTMX 응답 HTML
|
||||
│ └── detail.blade.php # (선택사항)
|
||||
└── routes/
|
||||
├── web.php # Blade 화면 라우트
|
||||
└── api.php # API 엔드포인트
|
||||
```
|
||||
|
||||
### 4.2 Tenant 패턴 참고 파일
|
||||
|
||||
**학습 및 복사 기준:**
|
||||
- `app/Http/Controllers/TenantController.php` → Blade 컨트롤러 패턴
|
||||
- `app/Http/Controllers/Api/Admin/TenantController.php` → API 컨트롤러 패턴
|
||||
- `resources/views/tenants/index.blade.php` → HTMX 메인 화면 패턴
|
||||
- `resources/views/tenants/partials/table.blade.php` → HTML partial 패턴
|
||||
|
||||
---
|
||||
|
||||
## 5. 체크리스트
|
||||
|
||||
### 5.1 구현 전 확인사항
|
||||
|
||||
- [ ] **Tenant 패턴 파일 확인**: `tenants/` 디렉토리 구조 참고
|
||||
- [ ] **Service 작성 완료**: `RoleService.php` 비즈니스 로직 구현
|
||||
- [ ] **FormRequest 작성 완료**: `StoreRoleRequest`, `UpdateRoleRequest`
|
||||
- [ ] **Model 확인**: `Role.php` 관계 설정 확인
|
||||
|
||||
### 5.2 컨트롤러 체크리스트
|
||||
|
||||
**Blade Controller (`app/Http/Controllers/RoleController.php`)**
|
||||
- [ ] `index()` → `view('roles.index')` 반환만
|
||||
- [ ] `create()` → `view('roles.create')` 반환만
|
||||
- [ ] `edit($id)` → Service로 데이터 조회 → `view('roles.edit', compact('role'))`
|
||||
|
||||
**API Controller (`app/Http/Controllers/Api/Admin/RoleController.php`)**
|
||||
- [ ] `index()` → HTMX 요청 감지 (`$request->header('HX-Request')`)
|
||||
- [ ] HTMX 요청 시 → `view('roles.partials.table')->render()` → JSON 반환
|
||||
- [ ] 일반 요청 시 → JSON 데이터 반환
|
||||
- [ ] `store()`, `update()`, `destroy()` → HTMX 지원
|
||||
- [ ] HTMX 응답 시 `redirect` 또는 `action` 포함
|
||||
|
||||
### 5.3 Blade View 체크리스트
|
||||
|
||||
**index.blade.php**
|
||||
- [ ] `@extends('layouts.app')` 상속
|
||||
- [ ] 필터 폼 `<form id="filterForm">` 생성
|
||||
- [ ] HTMX 동적 영역 `<div id="role-table">` 생성
|
||||
- [ ] `hx-get="/api/admin/roles"` 설정
|
||||
- [ ] `hx-trigger="load, filterSubmit from:body"` 설정
|
||||
- [ ] `hx-include="#filterForm"` 설정
|
||||
- [ ] `hx-headers` CSRF 토큰 포함
|
||||
- [ ] 로딩 스피너 추가
|
||||
- [ ] `@push('scripts')` HTMX 스크립트 추가
|
||||
- [ ] 폼 제출 이벤트 핸들러 (`filterSubmit` 트리거)
|
||||
- [ ] HTMX 응답 처리 (`htmx:afterSwap`)
|
||||
- [ ] 삭제 확인 함수 (`confirmDelete`)
|
||||
|
||||
**partials/table.blade.php**
|
||||
- [ ] `<table>` 구조 생성
|
||||
- [ ] `@forelse` 루프로 데이터 출력
|
||||
- [ ] `@empty` 케이스 처리
|
||||
- [ ] 액션 버튼 (수정, 삭제)
|
||||
- [ ] 삭제 버튼 `onclick="confirmDelete()"` 연결
|
||||
- [ ] 페이지네이션 `@include('partials.pagination')`
|
||||
|
||||
### 5.4 라우트 체크리스트
|
||||
|
||||
**web.php**
|
||||
- [ ] `Route::middleware('auth')` 적용
|
||||
- [ ] `Route::prefix('roles')->name('roles.')` 그룹
|
||||
- [ ] `GET /roles` → `index()`
|
||||
- [ ] `GET /roles/create` → `create()`
|
||||
- [ ] `GET /roles/{id}/edit` → `edit()`
|
||||
|
||||
**api.php**
|
||||
- [ ] `Route::middleware(['web', 'auth'])->prefix('admin')` 적용
|
||||
- [ ] `Route::prefix('roles')->name('api.admin.roles.')` 그룹
|
||||
- [ ] `GET /api/admin/roles` → `index()`
|
||||
- [ ] `POST /api/admin/roles` → `store()`
|
||||
- [ ] `GET /api/admin/roles/{id}` → `show()`
|
||||
- [ ] `PUT /api/admin/roles/{id}` → `update()`
|
||||
- [ ] `DELETE /api/admin/roles/{id}` → `destroy()`
|
||||
|
||||
### 5.5 테스트 체크리스트
|
||||
|
||||
- [ ] 브라우저에서 `/roles` 접근 → index 화면 로드
|
||||
- [ ] HTMX 자동 로드 → 테이블 표시
|
||||
- [ ] 검색 필터 동작 → 테이블 업데이트
|
||||
- [ ] 삭제 버튼 → 확인 다이얼로그 → 테이블 업데이트
|
||||
- [ ] 페이지네이션 동작
|
||||
- [ ] 개발자 도구 Network 탭 → `HX-Request` 헤더 확인
|
||||
- [ ] API 응답 JSON 구조 확인 (`{html: "..."}`)
|
||||
|
||||
---
|
||||
|
||||
## 6. 참고사항
|
||||
|
||||
### 6.1 HTMX vs 전통적 방식 비교
|
||||
|
||||
| 항목 | 전통적 방식 | HTMX 방식 |
|
||||
|------|------------|-----------|
|
||||
| **폼 제출** | `<form method="GET">` → 전체 페이지 리로드 | `hx-get` → 부분 업데이트 |
|
||||
| **데이터 로딩** | Controller에서 직접 데이터 전달 | API 호출 → HTML partial 반환 |
|
||||
| **삭제 동작** | `<form method="POST">` + `@method('DELETE')` | `htmx.ajax('DELETE')` |
|
||||
| **검색 필터** | 페이지 리로드 + 쿼리스트링 | HTMX 트리거 → 부분 업데이트 |
|
||||
|
||||
### 6.2 주의사항
|
||||
|
||||
1. **HTMX 요청 감지 필수**
|
||||
```php
|
||||
if ($request->header('HX-Request')) {
|
||||
// HTMX 전용 로직
|
||||
}
|
||||
```
|
||||
|
||||
2. **CSRF 토큰 포함 필수**
|
||||
```html
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
```
|
||||
|
||||
3. **JSON 응답 구조 일관성**
|
||||
```json
|
||||
{
|
||||
"html": "<table>...</table>",
|
||||
"success": true,
|
||||
"message": "작업 완료"
|
||||
}
|
||||
```
|
||||
|
||||
4. **Blade와 API Controller 분리**
|
||||
- Blade Controller: 화면만 반환
|
||||
- API Controller: 데이터 처리 + HTMX/JSON 응답
|
||||
|
||||
---
|
||||
|
||||
## 7. 마이그레이션 가이드 (Admin → MNG)
|
||||
|
||||
### 7.1 작업 순서
|
||||
|
||||
1. **DB 확인** - 테이블이 이미 존재하는지 확인 (migrations 실행 불필요)
|
||||
2. **Admin 파일 참고** - Controller, Service, Model 복사/참고
|
||||
3. **패턴 적용** - HTMX + API 패턴으로 변환
|
||||
4. **테스트** - 브라우저에서 동작 확인
|
||||
|
||||
### 7.2 마이그레이션 체크리스트
|
||||
|
||||
- [ ] DB 테이블 존재 확인 (`roles`, `permissions`, `role_has_permissions`)
|
||||
- [ ] Admin Model 참고 (`admin/app/Models/Permissions/Role.php`)
|
||||
- [ ] Admin Controller 참고 (비즈니스 로직 추출)
|
||||
- [ ] Service 작성 (Admin 로직 → MNG Service)
|
||||
- [ ] Blade Controller 작성 (화면 반환만)
|
||||
- [ ] API Controller 작성 (HTMX 패턴)
|
||||
- [ ] Blade View 작성 (Tenant 패턴 기반)
|
||||
- [ ] 라우트 등록 (web.php, api.php)
|
||||
- [ ] 브라우저 테스트
|
||||
|
||||
---
|
||||
|
||||
**작성자:** Claude
|
||||
**최종 수정일:** 2025-01-24
|
||||
**버전:** 1.0
|
||||
**참고:** Tenant 관리 시스템 구현 패턴 기반
|
||||
@@ -81,9 +81,11 @@ ### 프로젝트 문서
|
||||
- **[CURRENT_WORKS.md](../CURRENT_WORKS.md)** - 현재 작업 진행 상황
|
||||
- **[TROUBLESHOOTING.md](./TROUBLESHOOTING.md)** - 트러블슈팅 가이드
|
||||
- **[MIGRATION_PLAN.md](./MIGRATION_PLAN.md)** - Admin → MNG 마이그레이션 계획 (Phase 4)
|
||||
- **[claudedocs/mng/MNG_PROJECT_PLAN.md](../../claudedocs/mng/MNG_PROJECT_PLAN.md)** - 전체 프로젝트 계획
|
||||
- **[claudedocs/mng/DEV_PROCESS.md](../../claudedocs/mng/DEV_PROCESS.md)** - 개발 프로세스 (HTMX + API 방식)
|
||||
- **[claudedocs/mng/SETUP_GUIDE.md](../../claudedocs/mng/SETUP_GUIDE.md)** - 초기 설정 가이드
|
||||
- **[MNG_PROJECT_PLAN.md](./MNG_PROJECT_PLAN.md)** - 전체 프로젝트 계획
|
||||
- **[DEV_PROCESS.md](./DEV_PROCESS.md)** - 개발 프로세스 (HTMX + API 방식)
|
||||
- **[SETUP_GUIDE.md](./SETUP_GUIDE.md)** - 초기 설정 가이드
|
||||
- **[HTMX_API_PATTERN.md](./HTMX_API_PATTERN.md)** - HTMX + API 패턴 가이드
|
||||
- **[LAYOUT_PATTERN.md](./LAYOUT_PATTERN.md)** - 레이아웃 패턴 가이드
|
||||
|
||||
**SAM 공통 문서:**
|
||||
- **[📊 ../../docs/specs/database-schema.md](../../docs/specs/database-schema.md)** - 데이터베이스 스키마 (Phase 4: 8개 테이블 상세)
|
||||
@@ -246,8 +248,8 @@ ### SAM 공통 문서
|
||||
- **[docs/specs/database-schema.md](../../docs/specs/database-schema.md)** - DB 스키마
|
||||
|
||||
### MNG 프로젝트 문서
|
||||
- **[claudedocs/mng/MNG_PROJECT_PLAN.md](../../claudedocs/mng/MNG_PROJECT_PLAN.md)** - 프로젝트 전체 계획
|
||||
- **[claudedocs/mng/DEV_PROCESS.md](../../claudedocs/mng/DEV_PROCESS.md)** - 개발 프로세스
|
||||
- **[MNG_PROJECT_PLAN.md](./MNG_PROJECT_PLAN.md)** - 프로젝트 전체 계획
|
||||
- **[DEV_PROCESS.md](./DEV_PROCESS.md)** - 개발 프로세스
|
||||
- **[CURRENT_WORKS.md](../CURRENT_WORKS.md)** - 작업 진행 상황
|
||||
|
||||
---
|
||||
|
||||
504
docs/LAYOUT_PATTERN.md
Normal file
504
docs/LAYOUT_PATTERN.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# MNG 레이아웃 패턴 가이드
|
||||
|
||||
**작성일:** 2025-01-24
|
||||
**목적:** MNG 프로젝트의 표준 페이지 레이아웃 패턴 문서화
|
||||
|
||||
---
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [기본 레이아웃 구조](#1-기본-레이아웃-구조)
|
||||
2. [Tenant Selector 패턴](#2-tenant-selector-패턴)
|
||||
3. [페이지별 적용 가이드](#3-페이지별-적용-가이드)
|
||||
4. [컨텐츠 영역 구조](#4-컨텐츠-영역-구조)
|
||||
5. [체크리스트](#5-체크리스트)
|
||||
|
||||
---
|
||||
|
||||
## 1. 기본 레이아웃 구조
|
||||
|
||||
### 1.1 전체 구조
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Header (상단) │
|
||||
│ - 로고, 사용자 정보, 알림 등 │
|
||||
├──────────┬──────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ │ <!-- Tenant Selector (공통) --> │
|
||||
│ │ ┌─────────────────────────────────┐ │
|
||||
│ │ │ 테넌트 선택 드롭다운 │ │
|
||||
│ Sidebar │ │ [전체보기] [A회사] [B회사] │ │
|
||||
│ │ └─────────────────────────────────┘ │
|
||||
│ (좌측 │ │
|
||||
│ 메뉴) │ <!-- 페이지 헤더 --> │
|
||||
│ │ 페이지 제목 [+ 버튼] │
|
||||
│ │ │
|
||||
│ │ <!-- 필터 영역 --> │
|
||||
│ │ [검색] [필터1] [필터2] [검색버튼] │
|
||||
│ │ │
|
||||
│ │ <!-- 테이블/컨텐츠 영역 --> │
|
||||
│ │ ┌─────────────────────────────────┐ │
|
||||
│ │ │ 데이터 테이블 또는 컨텐츠 │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ (HTMX 동적 로딩 영역) │ │
|
||||
│ │ └─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└──────────┴──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 레이아웃 파일 구조
|
||||
|
||||
```
|
||||
resources/views/
|
||||
├── layouts/
|
||||
│ └── app.blade.php # 메인 레이아웃
|
||||
├── partials/
|
||||
│ ├── sidebar.blade.php # 좌측 메뉴
|
||||
│ ├── header.blade.php # 상단 헤더
|
||||
│ ├── tenant-selector.blade.php # 테넌트 선택기 (공통)
|
||||
│ └── pagination.blade.php # 페이지네이션
|
||||
└── [feature]/
|
||||
├── index.blade.php # 목록 페이지
|
||||
├── create.blade.php # 생성 페이지
|
||||
├── edit.blade.php # 수정 페이지
|
||||
└── partials/
|
||||
└── table.blade.php # HTMX 응답용 테이블
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Tenant Selector 패턴
|
||||
|
||||
### 2.1 역할과 목적
|
||||
|
||||
**Tenant Selector는 모든 데이터 관리 페이지 상단에 위치하는 공통 컴포넌트입니다.**
|
||||
|
||||
- **목적**: 사용자가 특정 테넌트의 데이터만 필터링하여 볼 수 있도록 함
|
||||
- **위치**: `@section('content')` 직후, 페이지 컨텐츠 최상단
|
||||
- **예외**: 테넌트 관리 페이지 (`tenants/index.blade.php`)는 제외
|
||||
|
||||
### 2.2 Tenant Selector 구조
|
||||
|
||||
**파일**: `resources/views/partials/tenant-selector.blade.php`
|
||||
|
||||
```blade
|
||||
<!-- Tenant Selector Card -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- 좌측: 테넌트 선택 -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg>...</svg>
|
||||
<label>테넌트 선택:</label>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('tenant.switch') }}" method="POST">
|
||||
@csrf
|
||||
<select name="tenant_id" onchange="this.form.submit()">
|
||||
<option value="all">전체 보기</option>
|
||||
@foreach($globalTenants as $tenant)
|
||||
<option value="{{ $tenant->id }}">
|
||||
{{ $tenant->company_name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 현재 테넌트 정보 -->
|
||||
<div class="flex items-center gap-2">
|
||||
@if(session('selected_tenant_id'))
|
||||
<span class="badge">
|
||||
{{ $currentTenant->company_name }} 데이터만 표시 중
|
||||
</span>
|
||||
@else
|
||||
<span>전체 테넌트 데이터 표시 중</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2.3 Tenant Selector 동작 방식
|
||||
|
||||
1. **드롭다운 변경** → 폼 자동 제출
|
||||
2. **POST /tenant/switch** → TenantController@switch
|
||||
3. **세션 저장** → `session('selected_tenant_id')`
|
||||
4. **페이지 리로드** → 선택된 테넌트 데이터만 표시
|
||||
|
||||
### 2.4 백엔드 연동
|
||||
|
||||
**TenantController@switch** (예시):
|
||||
```php
|
||||
public function switch(Request $request): RedirectResponse
|
||||
{
|
||||
$tenantId = $request->input('tenant_id');
|
||||
|
||||
if ($tenantId === 'all') {
|
||||
session()->forget('selected_tenant_id');
|
||||
} else {
|
||||
session(['selected_tenant_id' => $tenantId]);
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
```
|
||||
|
||||
**Service Layer** (자동 필터링):
|
||||
```php
|
||||
public function getRoles(array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$query = Role::query();
|
||||
|
||||
// Tenant 필터링
|
||||
if ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
return $query->paginate(15);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 페이지별 적용 가이드
|
||||
|
||||
### 3.1 일반 데이터 관리 페이지 (Tenant Selector 포함)
|
||||
|
||||
**적용 대상**: 역할, 사용자, 부서, 제품, 자재, BOM, 카테고리 등
|
||||
|
||||
**템플릿 구조**:
|
||||
```blade
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '역할 관리')
|
||||
|
||||
@section('content')
|
||||
<!-- Tenant Selector (필수) -->
|
||||
@include('partials.tenant-selector')
|
||||
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex justify-between items-center mt-6 mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">🔑 역할 관리</h1>
|
||||
<a href="{{ route('roles.create') }}" class="btn-primary">
|
||||
+ 새 역할
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 필터 영역 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<form id="filterForm">
|
||||
<!-- 검색, 필터 -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 컨텐츠 영역 (HTMX) -->
|
||||
<div id="role-table" hx-get="/api/admin/roles">
|
||||
<!-- 로딩 스피너 -->
|
||||
</div>
|
||||
@endsection
|
||||
```
|
||||
|
||||
### 3.2 테넌트 관리 페이지 (Tenant Selector 제외)
|
||||
|
||||
**적용 대상**: `tenants/index.blade.php`
|
||||
|
||||
**템플릿 구조**:
|
||||
```blade
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '테넌트 관리')
|
||||
|
||||
@section('content')
|
||||
<!-- Tenant Selector 없음 -->
|
||||
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">🏢 테넌트 관리</h1>
|
||||
<a href="{{ route('tenants.create') }}" class="btn-primary">
|
||||
+ 새 테넌트
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 필터 영역 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<form id="filterForm">
|
||||
<!-- 검색, 상태 필터, 삭제된 항목 포함 -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 컨텐츠 영역 (HTMX) -->
|
||||
<div id="tenant-table" hx-get="/api/admin/tenants">
|
||||
<!-- 로딩 스피너 -->
|
||||
</div>
|
||||
@endsection
|
||||
```
|
||||
|
||||
**이유**: 테넌트 관리는 모든 테넌트를 관리하는 페이지이므로 테넌트 필터링이 불필요
|
||||
|
||||
### 3.3 대시보드 (Tenant Selector 포함)
|
||||
|
||||
**템플릿 구조**:
|
||||
```blade
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '대시보드')
|
||||
|
||||
@section('content')
|
||||
<!-- Tenant Selector (포함) -->
|
||||
@include('partials.tenant-selector')
|
||||
|
||||
<!-- Welcome Card -->
|
||||
<div class="bg-white rounded-lg shadow mt-6">
|
||||
<div class="p-6">
|
||||
<h2>환영합니다!</h2>
|
||||
<!-- 통계, 퀵 액션 등 -->
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 컨텐츠 영역 구조
|
||||
|
||||
### 4.1 페이지 헤더
|
||||
|
||||
```blade
|
||||
<div class="flex justify-between items-center {{ $hasTenantSelector ? 'mt-6 mb-6' : 'mb-6' }}">
|
||||
<h1 class="text-2xl font-bold text-gray-800">
|
||||
[아이콘] 페이지 제목
|
||||
</h1>
|
||||
<a href="{{ route('resource.create') }}" class="btn-primary">
|
||||
+ 새 항목
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
**주의사항**:
|
||||
- Tenant Selector가 있는 경우 → `mt-6` 추가 (위쪽 여백)
|
||||
- Tenant Selector가 없는 경우 → `mt-6` 생략
|
||||
|
||||
### 4.2 필터 영역
|
||||
|
||||
```blade
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<form id="filterForm" class="flex gap-4">
|
||||
<!-- 검색 입력 -->
|
||||
<div class="flex-1">
|
||||
<input type="text" name="search" placeholder="검색...">
|
||||
</div>
|
||||
|
||||
<!-- 추가 필터 (선택사항) -->
|
||||
<div class="w-48">
|
||||
<select name="status">
|
||||
<option value="">전체 상태</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 검색 버튼 -->
|
||||
<button type="submit" class="btn-secondary">검색</button>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 4.3 HTMX 동적 컨텐츠 영역
|
||||
|
||||
```blade
|
||||
<div id="resource-table"
|
||||
hx-get="/api/admin/resources"
|
||||
hx-trigger="load, filterSubmit from:body"
|
||||
hx-include="#filterForm"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<!-- 로딩 스피너 -->
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 체크리스트
|
||||
|
||||
### 5.1 Tenant Selector 포함 여부 확인
|
||||
|
||||
**포함해야 하는 페이지** (✅):
|
||||
- [ ] 역할 관리 (`roles/index.blade.php`)
|
||||
- [ ] 사용자 관리 (`users/index.blade.php`)
|
||||
- [ ] 부서 관리 (`departments/index.blade.php`)
|
||||
- [ ] 제품 관리 (`products/index.blade.php`)
|
||||
- [ ] 자재 관리 (`materials/index.blade.php`)
|
||||
- [ ] BOM 관리 (`boms/index.blade.php`)
|
||||
- [ ] 카테고리 관리 (`categories/index.blade.php`)
|
||||
- [ ] 대시보드 (`dashboard/index.blade.php`)
|
||||
|
||||
**제외해야 하는 페이지** (❌):
|
||||
- [ ] 테넌트 관리 (`tenants/index.blade.php`)
|
||||
- [ ] 시스템 설정 (전역 설정 페이지)
|
||||
- [ ] 감사 로그 (전체 시스템 로그)
|
||||
|
||||
### 5.2 레이아웃 구현 체크리스트
|
||||
|
||||
**페이지 구조**:
|
||||
- [ ] `@extends('layouts.app')` 상속
|
||||
- [ ] `@section('title', '페이지 제목')` 정의
|
||||
- [ ] `@section('content')` 내부 구조:
|
||||
- [ ] `@include('partials.tenant-selector')` (필요 시)
|
||||
- [ ] 페이지 헤더 (`mt-6` 여백 확인)
|
||||
- [ ] 필터 영역
|
||||
- [ ] HTMX 동적 컨텐츠 영역
|
||||
|
||||
**스타일 일관성**:
|
||||
- [ ] 카드 스타일: `bg-white rounded-lg shadow-sm`
|
||||
- [ ] 버튼 스타일: `btn-primary`, `btn-secondary`
|
||||
- [ ] 간격 일관성: `mb-6`, `mt-6`, `p-4`, `p-6`
|
||||
|
||||
**HTMX 설정**:
|
||||
- [ ] `hx-get` 엔드포인트 설정
|
||||
- [ ] `hx-trigger="load, filterSubmit from:body"` 설정
|
||||
- [ ] `hx-include="#filterForm"` 설정
|
||||
- [ ] CSRF 토큰 헤더 포함
|
||||
|
||||
---
|
||||
|
||||
## 6. 예시 코드
|
||||
|
||||
### 6.1 완전한 페이지 예시 (Tenant Selector 포함)
|
||||
|
||||
```blade
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '역할 관리')
|
||||
|
||||
@section('content')
|
||||
<!-- Tenant Selector -->
|
||||
@include('partials.tenant-selector')
|
||||
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex justify-between items-center mt-6 mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">🔑 역할 관리</h1>
|
||||
<a href="{{ route('roles.create') }}"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition">
|
||||
+ 새 역할
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 필터 영역 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<form id="filterForm" class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<input type="text" name="search" placeholder="역할 이름, 설명으로 검색..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition">
|
||||
검색
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 테이블 영역 (HTMX로 로드) -->
|
||||
<div id="role-table"
|
||||
hx-get="/api/admin/roles"
|
||||
hx-trigger="load, filterSubmit from:body"
|
||||
hx-include="#filterForm"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<!-- 로딩 스피너 -->
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script>
|
||||
// 폼 제출 시 HTMX 이벤트 트리거
|
||||
document.getElementById('filterForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
htmx.trigger('#role-table', 'filterSubmit');
|
||||
});
|
||||
|
||||
// HTMX 응답 처리
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'role-table') {
|
||||
const response = JSON.parse(event.detail.xhr.response);
|
||||
if (response.html) {
|
||||
event.detail.target.innerHTML = response.html;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 삭제 확인
|
||||
window.confirmDelete = function(id, name) {
|
||||
if (confirm(`"${name}" 역할을 삭제하시겠습니까?`)) {
|
||||
htmx.ajax('DELETE', `/api/admin/roles/${id}`, {
|
||||
target: '#role-table',
|
||||
swap: 'none',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
}).then(() => {
|
||||
htmx.trigger('#role-table', 'filterSubmit');
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
```
|
||||
|
||||
### 6.2 ViewComposer로 $globalTenants 자동 주입
|
||||
|
||||
**파일**: `app/Providers/ViewServiceProvider.php`
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\View;
|
||||
use App\Models\Tenant;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// 모든 뷰에 $globalTenants 변수 자동 주입
|
||||
View::composer('partials.tenant-selector', function ($view) {
|
||||
$view->with('globalTenants', Tenant::orderBy('company_name')->get());
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 주의사항
|
||||
|
||||
### 7.1 Tenant Selector 관련
|
||||
|
||||
1. **ViewComposer 필수**: `$globalTenants` 변수가 자동으로 주입되도록 ViewComposer 설정 필요
|
||||
2. **세션 관리**: `selected_tenant_id` 세션이 Service Layer에서 자동으로 필터링에 사용됨
|
||||
3. **페이지 리로드**: 테넌트 변경 시 전체 페이지가 리로드되어 모든 데이터가 새로 로드됨
|
||||
4. **HTMX 연동**: Tenant 변경 후 HTMX 테이블도 자동으로 새로 로드됨
|
||||
|
||||
### 7.2 레이아웃 스타일
|
||||
|
||||
1. **컨테이너 없음**: `@section('content')` 내부에는 기본 컨테이너가 없음
|
||||
- Tenant Selector는 자체 패딩(`p-6`) 포함
|
||||
- 나머지 컨텐츠는 페이지별로 여백 조정
|
||||
|
||||
2. **간격 일관성**:
|
||||
- Tenant Selector 하단: `mb-6` (내부 카드에 포함)
|
||||
- 페이지 헤더: `mt-6 mb-6` (Tenant Selector가 있을 때)
|
||||
- 필터 영역: `mb-6`
|
||||
- 컨텐츠 영역: 별도 여백 불필요
|
||||
|
||||
3. **반응형 디자인**: Tailwind CSS 유틸리티 클래스 사용
|
||||
|
||||
---
|
||||
|
||||
**작성자:** Claude
|
||||
**최종 수정일:** 2025-01-24
|
||||
**버전:** 1.0
|
||||
**참고**: Dashboard, Tenant 관리 시스템 레이아웃 기반
|
||||
@@ -22,9 +22,10 @@ ### 1. DB 마이그레이션 금지
|
||||
- DB 스키마는 **api/에서만** 관리
|
||||
- 여러 저장소(api, admin, mng)가 동일 DB 공유
|
||||
|
||||
**예외:**
|
||||
- `admin_*` 접두사 테이블만 mng/에서 생성 가능 (mng 전용 기능용)
|
||||
- 그래도 가능하면 api/에 요청 권장
|
||||
**mng 전용 테이블 생성 시:**
|
||||
- ✅ mng에서만 사용하는 테이블은 `admin_*` 접두사 사용
|
||||
- ✅ **마이그레이션은 무조건 api/에서 생성** (mng에서 생성 금지!)
|
||||
- 예: `admin_pm_projects`, `admin_pm_tasks` 등
|
||||
|
||||
**실수 사례:**
|
||||
```php
|
||||
@@ -184,13 +185,64 @@ ### 6. FormRequest 필수 사용
|
||||
|
||||
---
|
||||
|
||||
### 7. MNG 데이터 접근 아키텍처
|
||||
|
||||
**MNG는 자체 내부 API 사용 (외부 api/ 프로젝트 호출 안 함)**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ routes/web.php (Blade 화면만) │
|
||||
│ └─ Controller → view('xxx.index') (데이터 없이 화면만) │
|
||||
│ │
|
||||
│ routes/api.php (HTMX 호출 + CRUD) │
|
||||
│ └─ Api/Admin/Controller → Service → Model → DB │
|
||||
│ │
|
||||
│ Blade에서 HTMX로 /api/admin/* 호출 │
|
||||
│ └─ hx-get="/api/admin/tenants" │
|
||||
│ └─ hx-post="/api/admin/tenants" │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**규칙:**
|
||||
- ❌ 외부 api/ 프로젝트의 API 호출 금지
|
||||
- ✅ mng 내부 API (`/api/admin/*`) 사용
|
||||
- ✅ Service → Model → DB (직접 접근)
|
||||
|
||||
**Controller 구분:**
|
||||
- `app/Http/Controllers/XxxController.php` → Blade 화면만 (GET)
|
||||
- `app/Http/Controllers/Api/Admin/XxxController.php` → CRUD 처리 (HTMX)
|
||||
|
||||
**예시:**
|
||||
```php
|
||||
// ✅ Web Controller: 화면만 반환
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('tenants.index'); // 데이터 없이 화면만
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ API Controller: CRUD 처리
|
||||
class Api\Admin\TenantController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$tenants = $this->tenantService->getTenants($request->all());
|
||||
return view('tenants.partials.list', compact('tenants')); // HTML partial
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 작업 전 체크리스트
|
||||
|
||||
### DB 작업 시:
|
||||
```
|
||||
□ mng/에서 작업 중인가? → 마이그레이션 금지!
|
||||
□ 기존 테이블 수정인가? → api/에 요청!
|
||||
□ 새 테이블인가? → admin_* 접두사 OR api/에 요청!
|
||||
□ mng 전용 새 테이블인가? → admin_* 접두사 + api/에 마이그레이션!
|
||||
□ 관계만 추가인가? → OK, 모델만 수정
|
||||
```
|
||||
|
||||
@@ -230,5 +282,5 @@ ### 2025-11-24: users 테이블 마이그레이션 시도
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2025-11-24
|
||||
**최종 업데이트**: 2025-11-27
|
||||
**다음 리뷰**: 반복 실수 발생 시 업데이트
|
||||
|
||||
838
docs/MNG_PROJECT_PLAN.md
Normal file
838
docs/MNG_PROJECT_PLAN.md
Normal file
@@ -0,0 +1,838 @@
|
||||
# MNG 프로젝트 개발 계획서
|
||||
|
||||
## 📋 프로젝트 개요
|
||||
|
||||
### 목적
|
||||
- **문제점**: 기존 admin/ (Filament v4)은 AI 없이 수정이 어려움
|
||||
- **목표**: 수정 용이한 Plain Laravel 기반 관리자 패널 구축
|
||||
- **도메인**: mng.sam.kr
|
||||
- **철학**: **단순함 > 복잡함**, AI 없이도 수정 가능한 직관적 코드
|
||||
|
||||
### 핵심 전략
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ MNG (mng.sam.kr) │
|
||||
│ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Web Routes │────▶│ Blade + HTMX │ │ ← DaisyUI (심플)
|
||||
│ │ (세션 인증) │ │ (수정 용이) │ │
|
||||
│ └──────────────┘ └─────────────────┘ │
|
||||
│ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ API Routes │────▶│ Admin API │ │ ← 처음부터 분리
|
||||
│ │ (토큰 인증) │ │ (관리자 전용) │ │
|
||||
│ └──────────────┘ └─────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Service Layer (비즈니스 로직) │ │ ← admin/ 복사
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Models (admin/ 복사, Filament 제거) │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└─────────────────┬───────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────┐
|
||||
│ MySQL 8.0 (공유 DB) │
|
||||
│ - admin/ (점차 deprecated) │
|
||||
│ - api/ (외부 API) │
|
||||
│ - mng/ (새 관리자) ← 최종 │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### 설계 원칙
|
||||
1. **단순성**: 복잡한 추상화 금지, 인라인 코드 허용
|
||||
2. **수정 용이성**: AI 없이도 Blade 템플릿 수정 가능
|
||||
3. **코드 재사용**: admin/ 모델/서비스 복사 후 간소화
|
||||
4. **DB 공유**: 기존 테이블 최대한 활용
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 아키텍처 설계
|
||||
|
||||
### 1. 디렉토리 구조
|
||||
```
|
||||
SAM/
|
||||
├── admin/ # Filament (점차 deprecated)
|
||||
├── api/ # 외부 클라이언트 API
|
||||
├── mng/ # ⭐ 운영 관리자 패널 (NEW)
|
||||
│ ├── app/
|
||||
│ │ ├── Http/
|
||||
│ │ │ ├── Controllers/
|
||||
│ │ │ │ ├── Web/ # Blade 컨트롤러 (단순)
|
||||
│ │ │ │ │ ├── Auth/
|
||||
│ │ │ │ │ ├── Dashboard/
|
||||
│ │ │ │ │ ├── User/
|
||||
│ │ │ │ │ └── Product/
|
||||
│ │ │ │ └── Api/ # Admin API (향후)
|
||||
│ │ │ │ └── Admin/
|
||||
│ │ │ ├── Requests/ # FormRequest (필수)
|
||||
│ │ │ └── Middleware/
|
||||
│ │ ├── Services/ # admin/ 복사 후 간소화
|
||||
│ │ ├── Models/ # admin/ 복사, Filament 코드 제거
|
||||
│ │ └── Traits/
|
||||
│ │ ├── BelongsToTenant.php # admin/에서 복사
|
||||
│ │ └── HasAuditLog.php # admin/에서 복사
|
||||
│ ├── routes/
|
||||
│ │ ├── web.php # Blade 라우트
|
||||
│ │ └── api.php # Admin API (/api/admin/*)
|
||||
│ ├── resources/
|
||||
│ │ └── views/
|
||||
│ │ ├── layouts/
|
||||
│ │ │ ├── app.blade.php # 단순 레이아웃
|
||||
│ │ │ └── guest.blade.php
|
||||
│ │ ├── auth/ # 로그인 화면
|
||||
│ │ ├── dashboard/ # 대시보드
|
||||
│ │ ├── users/ # 사용자 관리
|
||||
│ │ └── products/ # 제품 관리
|
||||
│ ├── database/
|
||||
│ │ └── migrations/
|
||||
│ │ └── # 관리자 전용: admin_*
|
||||
│ │ └── # 통계 전용: stat_*
|
||||
│ ├── tests/
|
||||
│ │ └── Feature/
|
||||
│ └── .env
|
||||
├── docker/
|
||||
│ └── nginx/
|
||||
│ └── mng.sam.kr.conf
|
||||
└── claudedocs/
|
||||
└── mng/
|
||||
├── MNG_PROJECT_PLAN.md # 이 문서
|
||||
├── API_SPEC.md # API 명세
|
||||
└── PROGRESS.md # 진행 상황
|
||||
```
|
||||
|
||||
### 2. 기술 스택 (확정)
|
||||
|
||||
| 레이어 | 기술 | 버전 | 비고 |
|
||||
|--------|------|------|------|
|
||||
| **백엔드** | Laravel | 12.x | PHP 8.4+ |
|
||||
| **인증** | Sanctum | 4.x | 세션 + 토큰 |
|
||||
| **DB** | **MySQL** | **8.0** | **admin, api와 공유** |
|
||||
| **프론트엔드** | **Blade + HTMX** | **1.x** | **단순, 수정 용이** |
|
||||
| **CSS** | **Tailwind CSS** | **3.x** | 기존과 통일 |
|
||||
| **UI 컴포넌트** | **DaisyUI** | **4.x** | **심플, 클래스 기반** |
|
||||
| **아이콘** | Heroicons | - | Tailwind 친화적 |
|
||||
| **문서화** | L5-Swagger | - | Admin API 전용 |
|
||||
| **테스트** | PHPUnit | - | Feature Test |
|
||||
|
||||
### 3. DB 테이블 명명 규칙 (변경)
|
||||
|
||||
#### 기존 테이블 재사용 (마이그레이션 없음)
|
||||
```
|
||||
✅ users, roles, departments
|
||||
✅ products, materials, bom_items
|
||||
✅ menus, menu_role
|
||||
✅ audit_logs, categories, files
|
||||
✅ tenants
|
||||
```
|
||||
|
||||
#### 관리자 전용 테이블 (admin_* 접두사)
|
||||
```php
|
||||
// database/migrations/2025_01_20_create_admin_settings_table.php
|
||||
Schema::create('admin_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique();
|
||||
$table->text('value')->nullable();
|
||||
$table->string('type')->default('string'); // string, json, boolean
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 예시 테이블
|
||||
- admin_settings # 관리자 설정
|
||||
- admin_logs # 관리자 작업 로그
|
||||
- admin_preferences # 관리자 개인 설정
|
||||
```
|
||||
|
||||
#### 통계 테이블 (stat_* 접두사)
|
||||
```php
|
||||
// database/migrations/2025_01_20_create_stat_daily_sales_table.php
|
||||
Schema::create('stat_daily_sales', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('date');
|
||||
$table->decimal('total_amount', 15, 2);
|
||||
$table->integer('order_count');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('date');
|
||||
});
|
||||
|
||||
// 예시 테이블
|
||||
- stat_daily_sales # 일별 매출 통계
|
||||
- stat_inventory # 재고 통계
|
||||
- stat_user_activity # 사용자 활동 통계
|
||||
```
|
||||
|
||||
### 4. 모델/서비스 복사 전략
|
||||
|
||||
#### admin/ → mng/ 복사 프로세스
|
||||
```bash
|
||||
# 1. 모델 복사 (Filament 의존성 제거)
|
||||
cp -r admin/app/Models/* mng/app/Models/
|
||||
# Filament 관련 코드 제거 (getNavigationLabel, form, table 등)
|
||||
|
||||
# 2. Traits 복사 (그대로 사용)
|
||||
cp admin/app/Traits/BelongsToTenant.php mng/app/Traits/
|
||||
cp admin/app/Traits/HasAuditLog.php mng/app/Traits/
|
||||
|
||||
# 3. Services 복사 (있다면)
|
||||
cp -r admin/app/Services/* mng/app/Services/
|
||||
# 또는 신규 작성 (Service-First 원칙)
|
||||
```
|
||||
|
||||
#### 모델 예시 (Filament 제거)
|
||||
```php
|
||||
// admin/app/Models/User.php (Before)
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
use BelongsToTenant, HasAuditLog;
|
||||
|
||||
public static function form(Form $form): Form { ... } // ❌ 제거
|
||||
public static function table(Table $table): Table { ... } // ❌ 제거
|
||||
public function canAccessPanel(Panel $panel): bool { ... } // ❌ 제거
|
||||
}
|
||||
|
||||
// mng/app/Models/User.php (After)
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use BelongsToTenant, HasAuditLog;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'email', 'password', 'name',
|
||||
'role_id', 'department_id', 'is_active',
|
||||
];
|
||||
|
||||
// 순수 Eloquent 관계만 유지
|
||||
public function role() { return $this->belongsTo(Role::class); }
|
||||
public function department() { return $this->belongsTo(Department::class); }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 설계 원칙 (수정 용이성 최우선)
|
||||
|
||||
### DaisyUI 사용 철학
|
||||
```blade
|
||||
{{-- ✅ GOOD: 단순하고 직관적 --}}
|
||||
<button class="btn btn-primary">저장</button>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">제목</h2>
|
||||
<p>내용</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ❌ BAD: 과도한 추상화 --}}
|
||||
<x-custom-button variant="primary" size="large" />
|
||||
<x-card-wrapper :config="$complexConfig" />
|
||||
```
|
||||
|
||||
### Blade 템플릿 구조 (2레벨 최대)
|
||||
```blade
|
||||
{{-- layouts/app.blade.php (레이아웃) --}}
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="light">
|
||||
<head>
|
||||
<title>{{ config('app.name') }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body>
|
||||
<div class="drawer lg:drawer-open">
|
||||
{{-- 사이드바 --}}
|
||||
<input id="drawer" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-side">
|
||||
<label for="drawer" class="drawer-overlay"></label>
|
||||
<ul class="menu p-4 w-64 bg-base-200">
|
||||
@foreach($menus as $menu)
|
||||
<li><a href="{{ $menu->url }}">{{ __($menu->name) }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{-- 메인 컨텐츠 --}}
|
||||
<div class="drawer-content">
|
||||
<div class="navbar bg-base-100">
|
||||
<div class="flex-1">
|
||||
<a class="btn btn-ghost normal-case text-xl">MNG</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost">
|
||||
{{ auth()->user()->name }}
|
||||
</label>
|
||||
<ul class="menu dropdown-content">
|
||||
<li><a href="/logout">로그아웃</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="p-6">
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{{-- users/index.blade.php (페이지) --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">사용자 목록</h2>
|
||||
|
||||
{{-- Alpine.js 최소 사용 --}}
|
||||
<div x-data="{ search: '' }">
|
||||
<input x-model="search" type="text"
|
||||
placeholder="검색..."
|
||||
class="input input-bordered w-full" />
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>이름</th>
|
||||
<th>이메일</th>
|
||||
<th>역할</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
<tr>
|
||||
<td>{{ $user->name }}</td>
|
||||
<td>{{ $user->email }}</td>
|
||||
<td>{{ $user->role->name }}</td>
|
||||
<td>
|
||||
<a href="/users/{{ $user->id }}/edit" class="btn btn-sm">수정</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ $users->links() }} {{-- Pagination --}}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
```
|
||||
|
||||
### Alpine.js 사용 원칙 (최소화)
|
||||
```blade
|
||||
{{-- ✅ GOOD: 단순 인터랙션 --}}
|
||||
<div x-data="{ open: false }">
|
||||
<button @click="open = !open" class="btn">메뉴 열기</button>
|
||||
<div x-show="open" class="dropdown-content">메뉴 내용</div>
|
||||
</div>
|
||||
|
||||
{{-- ❌ BAD: 복잡한 로직 (서버에서 처리) --}}
|
||||
<div x-data="complexDataFetching()">
|
||||
<div x-init="loadData()">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 개발 로드맵
|
||||
|
||||
### Phase 1: 인프라 구축 (1일)
|
||||
|
||||
#### 체크리스트
|
||||
- [ ] Laravel 12 프로젝트 생성 (`mng/`)
|
||||
```bash
|
||||
cd SAM
|
||||
composer create-project laravel/laravel mng
|
||||
cd mng
|
||||
```
|
||||
- [ ] `.env` 환경 변수 설정
|
||||
```env
|
||||
APP_NAME=MNG
|
||||
APP_URL=http://mng.sam.kr
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=sam_db
|
||||
DB_USERNAME=sam_user
|
||||
DB_PASSWORD=sam_password
|
||||
```
|
||||
- [ ] Composer 패키지 설치
|
||||
```bash
|
||||
composer require laravel/sanctum
|
||||
composer require darkaonline/l5-swagger
|
||||
composer require --dev laravel/pint
|
||||
```
|
||||
- [ ] Tailwind + DaisyUI + HTMX 설정
|
||||
```bash
|
||||
npm install -D tailwindcss daisyui @tailwindcss/forms
|
||||
npm install htmx.org
|
||||
```
|
||||
```js
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
plugins: [require('daisyui')],
|
||||
daisyui: {
|
||||
themes: ['light', 'dark'],
|
||||
},
|
||||
}
|
||||
```
|
||||
- [ ] Docker Nginx 설정 (mng.sam.kr)
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name mng.sam.kr;
|
||||
root /var/www/mng/public;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass mng:9000;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] admin/ 모델 복사
|
||||
```bash
|
||||
cp -r admin/app/Models/* mng/app/Models/
|
||||
cp -r admin/app/Traits/* mng/app/Traits/
|
||||
# Filament 관련 코드 제거 후 커밋
|
||||
```
|
||||
|
||||
#### 산출물
|
||||
- `mng/` 디렉토리 (Git 독립 저장소)
|
||||
- DaisyUI + Alpine.js 환경
|
||||
- 복사된 모델 (Filament 제거)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 인증 시스템 (2일)
|
||||
|
||||
#### 로그인 화면 (DaisyUI)
|
||||
```blade
|
||||
{{-- resources/views/auth/login.blade.php --}}
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="light">
|
||||
<head>
|
||||
<title>로그인 - MNG</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-base-200">
|
||||
<div class="hero min-h-screen">
|
||||
<div class="hero-content flex-col">
|
||||
<div class="card w-96 bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title justify-center mb-4">MNG 로그인</h2>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
@csrf
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">이메일</span>
|
||||
</label>
|
||||
<input type="email" name="email"
|
||||
placeholder="email@example.com"
|
||||
class="input input-bordered"
|
||||
required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">비밀번호</span>
|
||||
</label>
|
||||
<input type="password" name="password"
|
||||
class="input input-bordered"
|
||||
required />
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-error mt-4">
|
||||
{{ $errors->first() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-control mt-6">
|
||||
<button class="btn btn-primary">로그인</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
#### AuthService (admin/ 참고)
|
||||
```php
|
||||
// app/Services/AuthService.php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
public function login(array $credentials): bool
|
||||
{
|
||||
return Auth::attempt($credentials);
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
Auth::logout();
|
||||
}
|
||||
|
||||
public function createToken(array $credentials): ?string
|
||||
{
|
||||
$user = User::where('email', $credentials['email'])->first();
|
||||
|
||||
if (!$user || !Hash::check($credentials['password'], $user->password)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->createToken('mng-token')->plainTextToken;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 체크리스트
|
||||
- [ ] LoginRequest (FormRequest)
|
||||
- [ ] AuthService 작성
|
||||
- [ ] Web 로그인 구현 (세션)
|
||||
- [ ] API 로그인 구현 (토큰)
|
||||
- [ ] BelongsToTenant 적용 확인
|
||||
- [ ] Feature Test 작성
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 대시보드 (1-2일)
|
||||
|
||||
#### DaisyUI Drawer 레이아웃
|
||||
```blade
|
||||
{{-- resources/views/layouts/app.blade.php --}}
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="light">
|
||||
<head>
|
||||
<title>{{ config('app.name') }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body>
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
{{-- 메인 컨텐츠 --}}
|
||||
<div class="drawer-content flex flex-col">
|
||||
{{-- 네비게이션 바 --}}
|
||||
<div class="w-full navbar bg-base-300">
|
||||
<div class="flex-none lg:hidden">
|
||||
<label for="drawer" class="btn btn-square btn-ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-6 h-6 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-1 px-2 mx-2">MNG</div>
|
||||
<div class="flex-none">
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost">
|
||||
{{ auth()->user()->name }}
|
||||
</label>
|
||||
<ul tabindex="0" class="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li><a href="/profile">프로필</a></li>
|
||||
<li>
|
||||
<form method="POST" action="/logout">
|
||||
@csrf
|
||||
<button type="submit">로그아웃</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 페이지 컨텐츠 --}}
|
||||
<main class="p-6 flex-1">
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{-- 사이드바 --}}
|
||||
<div class="drawer-side">
|
||||
<label for="drawer" class="drawer-overlay"></label>
|
||||
<ul class="menu p-4 w-64 bg-base-200 text-base-content">
|
||||
@foreach($menus as $menu)
|
||||
@if($menu->children->isEmpty())
|
||||
<li>
|
||||
<a href="{{ $menu->url }}"
|
||||
class="{{ request()->is($menu->url) ? 'active' : '' }}">
|
||||
{{ __($menu->name) }}
|
||||
</a>
|
||||
</li>
|
||||
@else
|
||||
<li>
|
||||
<details>
|
||||
<summary>{{ __($menu->name) }}</summary>
|
||||
<ul>
|
||||
@foreach($menu->children as $child)
|
||||
<li><a href="{{ $child->url }}">{{ __($child->name) }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
#### 대시보드 컨트롤러
|
||||
```php
|
||||
// app/Http/Controllers/Web/DashboardController.php
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\MenuService;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private MenuService $menuService
|
||||
) {}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$menus = $this->menuService->getMenusForUser(auth()->user());
|
||||
|
||||
return view('dashboard.index', compact('menus'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 체크리스트
|
||||
- [ ] 레이아웃 템플릿 (DaisyUI Drawer)
|
||||
- [ ] 메뉴 서비스 (MenuService)
|
||||
- [ ] 역할별 메뉴 필터링
|
||||
- [ ] 대시보드 메인 페이지
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 핵심 기능 (주 단위)
|
||||
|
||||
#### 4.1 사용자 관리 (3-5일)
|
||||
```blade
|
||||
{{-- resources/views/users/index.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-4">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">사용자 관리</h1>
|
||||
<a href="/users/create" class="btn btn-primary">사용자 추가</a>
|
||||
</div>
|
||||
|
||||
{{-- 검색/필터 --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/users">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<input type="text" name="search"
|
||||
placeholder="이름 또는 이메일"
|
||||
class="input input-bordered"
|
||||
value="{{ request('search') }}" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<select name="role_id" class="select select-bordered">
|
||||
<option value="">전체 역할</option>
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->id }}"
|
||||
{{ request('role_id') == $role->id ? 'selected' : '' }}>
|
||||
{{ $role->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<button type="submit" class="btn btn-primary">검색</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 테이블 --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>이름</th>
|
||||
<th>이메일</th>
|
||||
<th>역할</th>
|
||||
<th>부서</th>
|
||||
<th>상태</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
<tr>
|
||||
<td>{{ $user->id }}</td>
|
||||
<td>{{ $user->name }}</td>
|
||||
<td>{{ $user->email }}</td>
|
||||
<td>{{ $user->role->name }}</td>
|
||||
<td>{{ $user->department->name }}</td>
|
||||
<td>
|
||||
<span class="badge {{ $user->is_active ? 'badge-success' : 'badge-error' }}">
|
||||
{{ $user->is_active ? '활성' : '비활성' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="/users/{{ $user->id }}/edit" class="btn btn-sm">수정</a>
|
||||
<button class="btn btn-sm btn-error"
|
||||
onclick="confirmDelete({{ $user->id }})">삭제</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmDelete(userId) {
|
||||
if (confirm('정말 삭제하시겠습니까?')) {
|
||||
document.getElementById('delete-form-' + userId).submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
```
|
||||
|
||||
#### 체크리스트
|
||||
- [ ] 사용자 목록 (검색, 필터, 페이징)
|
||||
- [ ] 사용자 생성 (FormRequest)
|
||||
- [ ] 사용자 수정
|
||||
- [ ] 사용자 삭제 (Soft Delete)
|
||||
- [ ] Feature Test
|
||||
|
||||
---
|
||||
|
||||
## 📊 데이터베이스 전략
|
||||
|
||||
### DB 테이블 전략 (최종)
|
||||
```
|
||||
✅ 기존 테이블 재사용 (마이그레이션 없음)
|
||||
- users, roles, departments
|
||||
- products, materials
|
||||
- menus, audit_logs
|
||||
|
||||
🆕 관리자 전용 (admin_*)
|
||||
- admin_settings
|
||||
- admin_logs
|
||||
- admin_preferences
|
||||
|
||||
📊 통계 (stat_*)
|
||||
- stat_daily_sales
|
||||
- stat_inventory
|
||||
- stat_user_activity
|
||||
```
|
||||
|
||||
### 모델 관리 전략
|
||||
```
|
||||
초기 복사: admin/app/Models → mng/app/Models
|
||||
Filament 제거: form(), table(), canAccessPanel() 등
|
||||
이후 운영: mng/ 독립 (admin 점차 deprecated)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 품질 관리
|
||||
|
||||
### 코드 품질 체크리스트
|
||||
```
|
||||
□ Service-First (비즈니스 로직 → Service)
|
||||
□ FormRequest (컨트롤러 검증 금지)
|
||||
□ BelongsToTenant (multi-tenant 스코프)
|
||||
□ i18n 키 (하드코딩 금지)
|
||||
□ Soft Delete (deleted_at)
|
||||
□ 감사 로그 (HasAuditLog trait)
|
||||
□ Feature Test
|
||||
□ Pint (코드 스타일)
|
||||
```
|
||||
|
||||
### UI 수정 용이성 체크리스트
|
||||
```
|
||||
□ DaisyUI 클래스 직접 사용 (추상화 최소)
|
||||
□ Alpine.js 단순 인터랙션만
|
||||
□ Blade 템플릿 2레벨 이하
|
||||
□ 인라인 Tailwind 허용
|
||||
□ AI 없이 수정 가능
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 예상 타임라인
|
||||
|
||||
### MVP (최소 기능 제품) - 2주
|
||||
```
|
||||
Day 1-2: Phase 1 (인프라) + admin/ 모델 복사
|
||||
Day 3-4: Phase 2 (인증)
|
||||
Day 5-6: Phase 3 (대시보드)
|
||||
Day 7-14: Phase 4 (사용자, 역할, 제품 관리)
|
||||
```
|
||||
|
||||
### 전체 기능 이식 - 4-6주
|
||||
```
|
||||
Week 3-4: 제품, 자재 관리
|
||||
Week 5: 게시판, 통계
|
||||
Week 6: 테스트, 최적화
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고 문서
|
||||
|
||||
- [DaisyUI Components](https://daisyui.com/components/)
|
||||
- [Alpine.js Documentation](https://alpinejs.dev/)
|
||||
- [Laravel 12 Blade](https://laravel.com/docs/12.x/blade)
|
||||
|
||||
---
|
||||
|
||||
## ✅ 다음 단계
|
||||
|
||||
### 즉시 시작 가능
|
||||
- [ ] `mng/` Laravel 프로젝트 생성
|
||||
- [ ] DaisyUI + Alpine.js 설치
|
||||
- [ ] admin/ 모델 복사 및 Filament 제거
|
||||
- [ ] 로그인 화면 구현
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-01-20
|
||||
**버전**: 2.0
|
||||
**상태**: 정책 반영 완료 ✅
|
||||
**변경사항**:
|
||||
- 폴더명: `adm2/` → `mng/`
|
||||
- UI: DaisyUI + Blade + Alpine.js 확정
|
||||
- DB: 기존 테이블 재사용, `admin_*`, `stat_*` 접두사
|
||||
- 모델: admin/ 복사 후 Filament 제거
|
||||
- 철학: 단순함, 수정 용이성 최우선
|
||||
587
docs/SETUP_GUIDE.md
Normal file
587
docs/SETUP_GUIDE.md
Normal file
@@ -0,0 +1,587 @@
|
||||
# MNG 환경 구성 가이드
|
||||
|
||||
## 📋 개요
|
||||
|
||||
MNG 프로젝트를 위한 Docker 환경 구성 가이드입니다.
|
||||
기존 SAM 프로젝트 (api, admin, react)에 mng 서비스를 추가합니다.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 목표 환경
|
||||
|
||||
```
|
||||
SAM/
|
||||
├── api/ (api.sam.kr) - 외부 API
|
||||
├── admin/ (admin.sam.kr) - Filament 관리자 (deprecated)
|
||||
├── react/ (dev.sam.kr) - React 프론트엔드
|
||||
├── mng/ (mng.sam.kr) - 새 관리자 패널 ⭐ NEW
|
||||
└── docker/
|
||||
├── docker-compose.yml
|
||||
├── nginx/nginx.conf
|
||||
└── mng/ ⭐ NEW
|
||||
├── Dockerfile
|
||||
├── nginx.conf
|
||||
└── supervisord.conf
|
||||
```
|
||||
|
||||
### 도메인 구성
|
||||
| 도메인 | 서비스 | 용도 |
|
||||
|--------|--------|------|
|
||||
| api.sam.kr | api | 외부 클라이언트 API |
|
||||
| admin.sam.kr | admin | Filament 관리자 (점차 폐기) |
|
||||
| dev.sam.kr | react | React 프론트엔드 |
|
||||
| **mng.sam.kr** | **mng** | **새 관리자 패널** ⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 📐 Phase 0: 환경 구성 (최초 1회)
|
||||
|
||||
### Step 1: Laravel 프로젝트 생성
|
||||
|
||||
```bash
|
||||
# SAM 디렉토리로 이동
|
||||
cd /Users/hskwon/Works/@KD_SAM/SAM
|
||||
|
||||
# Laravel 12 프로젝트 생성
|
||||
composer create-project laravel/laravel mng
|
||||
|
||||
cd mng
|
||||
|
||||
# 필수 패키지 설치
|
||||
composer require laravel/sanctum
|
||||
composer require darkaonline/l5-swagger
|
||||
composer require --dev laravel/pint
|
||||
|
||||
# NPM 패키지 설치
|
||||
npm install -D tailwindcss daisyui @tailwindcss/forms postcss autoprefixer
|
||||
npm install htmx.org
|
||||
npx tailwindcss init -p
|
||||
```
|
||||
|
||||
### Step 2: 환경 변수 설정
|
||||
|
||||
**mng/.env** (기본값 수정)
|
||||
```env
|
||||
APP_NAME=MNG
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:... (자동 생성됨)
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://mng.sam.kr
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=samdb
|
||||
DB_USERNAME=samuser
|
||||
DB_PASSWORD=sampass
|
||||
|
||||
CACHE_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
|
||||
SANCTUM_STATEFUL_DOMAINS=mng.sam.kr
|
||||
```
|
||||
|
||||
### Step 3: Tailwind + DaisyUI 설정
|
||||
|
||||
**mng/tailwind.config.js**
|
||||
```js
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./resources/**/*.blade.php",
|
||||
"./resources/**/*.js",
|
||||
"./resources/**/*.vue",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('daisyui'),
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
daisyui: {
|
||||
themes: ['light', 'dark'],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**mng/resources/css/app.css**
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
```
|
||||
|
||||
**mng/resources/js/app.js**
|
||||
```js
|
||||
import './bootstrap';
|
||||
import htmx from 'htmx.org';
|
||||
|
||||
window.htmx = htmx;
|
||||
|
||||
// HTMX 전역 설정
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// CSRF 토큰 자동 추가
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
event.detail.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content;
|
||||
});
|
||||
|
||||
// 에러 처리
|
||||
document.body.addEventListener('htmx:responseError', (event) => {
|
||||
console.error('HTMX Error:', event.detail);
|
||||
alert('오류가 발생했습니다. 다시 시도해주세요.');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**mng/vite.config.js** (확인)
|
||||
```js
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
refresh: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Step 4: Docker 설정 파일 생성
|
||||
|
||||
#### 4-1. Dockerfile 생성
|
||||
|
||||
**docker/mng/Dockerfile**
|
||||
```dockerfile
|
||||
FROM php:8.4-fpm
|
||||
|
||||
# 시스템 패키지 설치
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
supervisor \
|
||||
nginx
|
||||
|
||||
# PHP 확장 설치
|
||||
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
|
||||
|
||||
# Composer 설치
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Node.js 설치 (Vite 빌드용)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# 작업 디렉토리 설정
|
||||
WORKDIR /var/www/mng
|
||||
|
||||
# 권한 설정
|
||||
RUN chown -R www-data:www-data /var/www/mng
|
||||
|
||||
# PHP-FPM 포트 노출
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["php-fpm"]
|
||||
```
|
||||
|
||||
#### 4-2. Nginx 설정 (PHP-FPM 연동)
|
||||
|
||||
**docker/mng/nginx.conf**
|
||||
```nginx
|
||||
server {
|
||||
listen 9000;
|
||||
server_name localhost;
|
||||
root /var/www/mng/public;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4-3. Supervisor 설정 (선택)
|
||||
|
||||
**docker/mng/supervisord.conf**
|
||||
```ini
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
|
||||
[program:php-fpm]
|
||||
command=/usr/local/sbin/php-fpm
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/php-fpm.err.log
|
||||
stdout_logfile=/var/log/php-fpm.out.log
|
||||
```
|
||||
|
||||
#### 4-4. PHP 업로드 설정
|
||||
|
||||
**docker/mng/uploads.ini**
|
||||
```ini
|
||||
upload_max_filesize = 100M
|
||||
post_max_size = 100M
|
||||
max_execution_time = 300
|
||||
memory_limit = 256M
|
||||
```
|
||||
|
||||
### Step 5: docker-compose.yml 업데이트
|
||||
|
||||
**docker/docker-compose.yml** (mng 서비스 추가)
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ../api:/var/www/api
|
||||
- ../admin:/var/www/admin
|
||||
- ../mng:/var/www/mng # ⭐ NEW
|
||||
- ../docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
depends_on:
|
||||
- api
|
||||
- admin
|
||||
- mng # ⭐ NEW
|
||||
- react
|
||||
networks:
|
||||
- samnet
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ../docker/api/Dockerfile
|
||||
volumes:
|
||||
- ../api:/var/www/api
|
||||
- ../docker/api/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- ../docker/api/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf
|
||||
- ../docker/api/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=samdb
|
||||
- DB_USERNAME=samuser
|
||||
- DB_PASSWORD=sampass
|
||||
networks:
|
||||
- samnet
|
||||
working_dir: /var/www/api
|
||||
|
||||
admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ../docker/admin/Dockerfile
|
||||
volumes:
|
||||
- ../admin:/var/www/admin
|
||||
- ../docker/admin/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- ../docker/admin/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf
|
||||
- ../docker/admin/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=samdb
|
||||
- DB_USERNAME=samuser
|
||||
- DB_PASSWORD=sampass
|
||||
networks:
|
||||
- samnet
|
||||
working_dir: /var/www/admin
|
||||
|
||||
# ⭐ NEW: MNG 서비스
|
||||
mng:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ../docker/mng/Dockerfile
|
||||
volumes:
|
||||
- ../mng:/var/www/mng
|
||||
- ../docker/mng/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- ../docker/mng/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf
|
||||
- ../docker/mng/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=samdb
|
||||
- DB_USERNAME=samuser
|
||||
- DB_PASSWORD=sampass
|
||||
networks:
|
||||
- samnet
|
||||
working_dir: /var/www/mng
|
||||
|
||||
react:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/react/Dockerfile
|
||||
volumes:
|
||||
- ../react:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
environment:
|
||||
- NEXT_PUBLIC_API_BASE_URL=http://api.sam.kr
|
||||
- NEXT_PUBLIC_ADMIN_URL=http://admin.sam.kr
|
||||
- NEXT_PUBLIC_MNG_URL=http://mng.sam.kr # ⭐ NEW
|
||||
- NEXT_PUBLIC_API_KEY=${NEXT_PUBLIC_API_KEY:-}
|
||||
- NEXT_PUBLIC_APP_NAME=SAM
|
||||
- NODE_ENV=development
|
||||
networks:
|
||||
- samnet
|
||||
working_dir: /app
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_DATABASE: samdb
|
||||
MYSQL_USER: samuser
|
||||
MYSQL_PASSWORD: sampass
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ../docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
networks:
|
||||
- samnet
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
networks:
|
||||
samnet:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### Step 6: Nginx 메인 설정 업데이트
|
||||
|
||||
**docker/nginx/nginx.conf** (mng.sam.kr 서버 블록 추가)
|
||||
```nginx
|
||||
events {}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
client_max_body_size 100M;
|
||||
|
||||
# ... 기존 서버 블록들 (dev.haisa.kr, api.sam.kr, admin.sam.kr, dev.sam.kr)
|
||||
|
||||
# ⭐ NEW: MNG 서버 블록
|
||||
server {
|
||||
listen 80;
|
||||
server_name mng.sam.kr;
|
||||
|
||||
root /var/www/mng/public;
|
||||
index index.php index.html;
|
||||
|
||||
access_log /var/log/nginx/mng.sam.kr_access.log;
|
||||
error_log /var/log/nginx/mng.sam.kr_error.log;
|
||||
|
||||
# 🛡️ 보안: 악의적 경로 패턴 차단
|
||||
if ($request_uri ~* "(\.\.\/|\.\.\\|etc\/passwd|\.env|\.git|\.htaccess|\.sql|@fs\/)") {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# 🛡️ 보안: 의심스러운 User-Agent 차단
|
||||
if ($http_user_agent ~* "(sqlmap|nikto|nmap|masscan|metasploit|nessus)") {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# 정적 자산 처리
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|map)$ {
|
||||
try_files $uri =404;
|
||||
access_log off;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
# 일반 요청 처리
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass mng:9000; # 서비스명 mng
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/mng/public$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7: hosts 파일 설정
|
||||
|
||||
**/etc/hosts** (로컬 도메인 추가)
|
||||
```bash
|
||||
# SAM 프로젝트 도메인
|
||||
127.0.0.1 api.sam.kr
|
||||
127.0.0.1 admin.sam.kr
|
||||
127.0.0.1 dev.sam.kr
|
||||
127.0.0.1 mng.sam.kr # ⭐ NEW
|
||||
```
|
||||
|
||||
**macOS/Linux:**
|
||||
```bash
|
||||
sudo nano /etc/hosts
|
||||
# 위 내용 추가 후 저장
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
C:\Windows\System32\drivers\etc\hosts
|
||||
# 관리자 권한으로 메모장 열어서 추가
|
||||
```
|
||||
|
||||
### Step 8: admin/ 모델 복사
|
||||
|
||||
```bash
|
||||
cd /Users/hskwon/Works/@KD_SAM/SAM
|
||||
|
||||
# Models 복사
|
||||
cp -r admin/app/Models/* mng/app/Models/
|
||||
|
||||
# Traits 복사
|
||||
mkdir -p mng/app/Traits
|
||||
cp admin/app/Traits/BelongsToTenant.php mng/app/Traits/
|
||||
cp admin/app/Traits/HasAuditLog.php mng/app/Traits/
|
||||
|
||||
# Filament 의존성 제거 (수동 작업 필요)
|
||||
# - form(), table(), canAccessPanel() 등 삭제
|
||||
# - FilamentUser 인터페이스 제거
|
||||
```
|
||||
|
||||
### Step 9: Docker 빌드 및 실행
|
||||
|
||||
```bash
|
||||
cd /Users/hskwon/Works/@KD_SAM/SAM/docker
|
||||
|
||||
# 기존 컨테이너 중지
|
||||
docker-compose down
|
||||
|
||||
# mng 서비스 빌드
|
||||
docker-compose build mng
|
||||
|
||||
# 전체 서비스 시작
|
||||
docker-compose up -d
|
||||
|
||||
# 로그 확인
|
||||
docker-compose logs -f mng
|
||||
|
||||
# mng 컨테이너 접속
|
||||
docker-compose exec mng bash
|
||||
|
||||
# 컨테이너 내부에서 Laravel 설정
|
||||
php artisan key:generate
|
||||
php artisan migrate
|
||||
php artisan storage:link
|
||||
|
||||
# Vite 빌드 (개발 모드)
|
||||
npm run dev
|
||||
# 또는 프로덕션 빌드
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Step 10: 동작 확인
|
||||
|
||||
```bash
|
||||
# 1. 브라우저에서 확인
|
||||
http://mng.sam.kr
|
||||
|
||||
# 2. API 확인 (예시)
|
||||
curl http://mng.sam.kr/api/admin/health
|
||||
|
||||
# 3. 로그 확인
|
||||
docker-compose logs mng
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 문제 해결
|
||||
|
||||
### 1. "502 Bad Gateway" 에러
|
||||
```bash
|
||||
# PHP-FPM 상태 확인
|
||||
docker-compose exec mng ps aux | grep php-fpm
|
||||
|
||||
# 재시작
|
||||
docker-compose restart mng
|
||||
```
|
||||
|
||||
### 2. 권한 에러
|
||||
```bash
|
||||
# 컨테이너 내부에서
|
||||
docker-compose exec mng bash
|
||||
chown -R www-data:www-data /var/www/mng/storage
|
||||
chown -R www-data:www-data /var/www/mng/bootstrap/cache
|
||||
chmod -R 775 /var/www/mng/storage
|
||||
chmod -R 775 /var/www/mng/bootstrap/cache
|
||||
```
|
||||
|
||||
### 3. DB 연결 실패
|
||||
```bash
|
||||
# MySQL 컨테이너 확인
|
||||
docker-compose exec mysql mysql -u samuser -psampass -e "SHOW DATABASES;"
|
||||
|
||||
# mng .env 확인
|
||||
docker-compose exec mng cat .env | grep DB_
|
||||
```
|
||||
|
||||
### 4. Vite 빌드 실패
|
||||
```bash
|
||||
# 컨테이너 내부에서
|
||||
docker-compose exec mng bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 5. HTMX 동작 안함
|
||||
- 브라우저 개발자 도구 → Network 탭 확인
|
||||
- `HX-Request` 헤더 있는지 확인
|
||||
- CSRF 토큰 확인 (meta 태그)
|
||||
|
||||
---
|
||||
|
||||
## 📋 환경 구성 체크리스트
|
||||
|
||||
```
|
||||
[ ] Step 1: Laravel 프로젝트 생성 (mng/)
|
||||
[ ] Step 2: .env 설정 (DB 정보)
|
||||
[ ] Step 3: Tailwind + DaisyUI 설정
|
||||
[ ] Step 4: Docker 설정 파일 생성 (Dockerfile, nginx.conf)
|
||||
[ ] Step 5: docker-compose.yml 업데이트 (mng 서비스 추가)
|
||||
[ ] Step 6: Nginx 메인 설정 업데이트 (mng.sam.kr 서버 블록)
|
||||
[ ] Step 7: /etc/hosts 설정 (mng.sam.kr)
|
||||
[ ] Step 8: admin/ 모델 복사 및 Filament 제거
|
||||
[ ] Step 9: Docker 빌드 및 실행
|
||||
[ ] Step 10: 동작 확인 (http://mng.sam.kr)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 다음 단계
|
||||
|
||||
환경 구성 완료 후:
|
||||
|
||||
1. **Phase 1**: 인증 시스템 구현 (로그인 API + Blade)
|
||||
2. **Phase 2**: 대시보드 구현 (레이아웃 + HTMX)
|
||||
3. **Phase 3**: 사용자 관리 (첫 CRUD 기능)
|
||||
|
||||
DEV_PROCESS.md 문서를 참고하여 개발을 진행하세요.
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-01-20
|
||||
**버전**: 1.0
|
||||
**환경**: Docker + Laravel 12 + MySQL 8.0 + HTMX + DaisyUI
|
||||
299
docs/project-management/README.md
Normal file
299
docs/project-management/README.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# 프로젝트 진행 관리 시스템 (PM)
|
||||
|
||||
MNG 관리자 패널용 프로젝트/작업/이슈 관리 시스템
|
||||
|
||||
## 개요
|
||||
|
||||
Notion 수동 관리를 대체하는 웹 기반 프로젝트 진행 관리 도구로, 클릭 한 번으로 상태 변경이 가능하고 대시보드에서 전체 진행률을 한눈에 파악할 수 있습니다.
|
||||
|
||||
## 데이터 구조
|
||||
|
||||
```
|
||||
Project (프로젝트)
|
||||
└── Task (작업) - 1:N
|
||||
└── Issue (이슈) - 1:N
|
||||
```
|
||||
|
||||
### 테이블
|
||||
|
||||
| 테이블명 | 설명 |
|
||||
|---------|------|
|
||||
| `admin_pm_projects` | 프로젝트 |
|
||||
| `admin_pm_tasks` | 작업 |
|
||||
| `admin_pm_issues` | 이슈 |
|
||||
|
||||
> Migration은 `api/` 저장소에 위치
|
||||
|
||||
## 상태값
|
||||
|
||||
### 프로젝트 상태
|
||||
| 값 | 라벨 | 색상 |
|
||||
|----|------|------|
|
||||
| `active` | 진행중 | green |
|
||||
| `completed` | 완료 | blue |
|
||||
| `on_hold` | 보류 | yellow |
|
||||
|
||||
### 작업 상태
|
||||
| 값 | 라벨 | 색상 |
|
||||
|----|------|------|
|
||||
| `todo` | 예정 | gray |
|
||||
| `in_progress` | 진행중 | blue |
|
||||
| `done` | 완료 | green |
|
||||
|
||||
### 작업 우선순위
|
||||
| 값 | 라벨 | 색상 |
|
||||
|----|------|------|
|
||||
| `low` | 낮음 | gray |
|
||||
| `medium` | 보통 | yellow |
|
||||
| `high` | 높음 | red |
|
||||
|
||||
### 이슈 타입
|
||||
| 값 | 라벨 |
|
||||
|----|------|
|
||||
| `bug` | 버그 |
|
||||
| `feature` | 기능 |
|
||||
| `improvement` | 개선 |
|
||||
|
||||
### 이슈 상태
|
||||
| 값 | 라벨 |
|
||||
|----|------|
|
||||
| `open` | 열림 |
|
||||
| `in_progress` | 진행중 |
|
||||
| `resolved` | 해결됨 |
|
||||
| `closed` | 닫힘 |
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
app/
|
||||
├── Http/
|
||||
│ ├── Controllers/
|
||||
│ │ ├── ProjectManagementController.php # Web Controller (뷰 렌더링)
|
||||
│ │ └── Api/Admin/ProjectManagement/
|
||||
│ │ ├── ProjectController.php # 프로젝트 API
|
||||
│ │ ├── TaskController.php # 작업 API
|
||||
│ │ ├── IssueController.php # 이슈 API
|
||||
│ │ └── ImportController.php # JSON Import API
|
||||
│ └── Requests/ProjectManagement/
|
||||
│ ├── StoreProjectRequest.php
|
||||
│ ├── UpdateProjectRequest.php
|
||||
│ ├── StoreTaskRequest.php
|
||||
│ ├── UpdateTaskRequest.php
|
||||
│ ├── StoreIssueRequest.php
|
||||
│ ├── UpdateIssueRequest.php
|
||||
│ ├── BulkActionRequest.php
|
||||
│ └── ImportProjectRequest.php
|
||||
├── Models/Admin/
|
||||
│ ├── AdminPmProject.php
|
||||
│ ├── AdminPmTask.php
|
||||
│ └── AdminPmIssue.php
|
||||
└── Services/ProjectManagement/
|
||||
├── ProjectService.php
|
||||
├── TaskService.php
|
||||
├── IssueService.php
|
||||
└── ImportService.php
|
||||
|
||||
resources/views/project-management/
|
||||
├── index.blade.php # 대시보드
|
||||
├── import.blade.php # JSON Import
|
||||
└── projects/
|
||||
├── index.blade.php # 프로젝트 목록
|
||||
├── create.blade.php # 프로젝트 생성
|
||||
├── edit.blade.php # 프로젝트 수정
|
||||
├── show.blade.php # 프로젝트 상세 (작업/이슈 포함)
|
||||
└── partials/
|
||||
└── table.blade.php # HTMX 테이블 파셜
|
||||
```
|
||||
|
||||
## API 엔드포인트
|
||||
|
||||
### 프로젝트 API (`/api/admin/pm/projects`)
|
||||
|
||||
| Method | URI | Name | 설명 |
|
||||
|--------|-----|------|------|
|
||||
| GET | `/` | index | 목록 조회 |
|
||||
| POST | `/` | store | 생성 |
|
||||
| GET | `/{id}` | show | 상세 조회 |
|
||||
| PUT | `/{id}` | update | 수정 |
|
||||
| DELETE | `/{id}` | destroy | 삭제 (soft) |
|
||||
| POST | `/{id}/restore` | restore | 복원 |
|
||||
| DELETE | `/{id}/force` | forceDestroy | 영구 삭제 |
|
||||
| POST | `/{id}/status` | changeStatus | 상태 변경 |
|
||||
| POST | `/{id}/duplicate` | duplicate | 복제 |
|
||||
| GET | `/stats` | stats | 통계 |
|
||||
| GET | `/dashboard` | dashboard | 대시보드 요약 |
|
||||
| GET | `/dropdown` | dropdown | 드롭다운용 목록 |
|
||||
|
||||
### 작업 API (`/api/admin/pm/tasks`)
|
||||
|
||||
| Method | URI | Name | 설명 |
|
||||
|--------|-----|------|------|
|
||||
| GET | `/` | index | 목록 조회 |
|
||||
| POST | `/` | store | 생성 |
|
||||
| GET | `/{id}` | show | 상세 조회 |
|
||||
| PUT | `/{id}` | update | 수정 |
|
||||
| DELETE | `/{id}` | destroy | 삭제 |
|
||||
| POST | `/{id}/status` | changeStatus | 상태 변경 |
|
||||
| GET | `/project/{projectId}` | byProject | 프로젝트별 조회 |
|
||||
| POST | `/project/{projectId}/reorder` | reorder | 순서 변경 |
|
||||
| GET | `/project/{projectId}/stats` | stats | 프로젝트별 통계 |
|
||||
| GET | `/urgent` | urgent | 긴급 작업 목록 |
|
||||
| POST | `/bulk` | bulk | 일괄 작업 |
|
||||
|
||||
### 이슈 API (`/api/admin/pm/issues`)
|
||||
|
||||
| Method | URI | Name | 설명 |
|
||||
|--------|-----|------|------|
|
||||
| GET | `/` | index | 목록 조회 |
|
||||
| POST | `/` | store | 생성 |
|
||||
| GET | `/{id}` | show | 상세 조회 |
|
||||
| PUT | `/{id}` | update | 수정 |
|
||||
| DELETE | `/{id}` | destroy | 삭제 |
|
||||
| POST | `/{id}/status` | changeStatus | 상태 변경 |
|
||||
| GET | `/project/{projectId}` | byProject | 프로젝트별 조회 |
|
||||
| GET | `/task/{taskId}` | byTask | 작업별 조회 |
|
||||
| GET | `/stats` | stats | 통계 |
|
||||
| GET | `/open` | open | 열린 이슈 목록 |
|
||||
| POST | `/bulk` | bulk | 일괄 작업 |
|
||||
|
||||
### Import API (`/api/admin/pm/import`)
|
||||
|
||||
| Method | URI | Name | 설명 |
|
||||
|--------|-----|------|------|
|
||||
| GET | `/template` | template | 샘플 JSON 템플릿 |
|
||||
| POST | `/validate` | validate | JSON 구조 검증 |
|
||||
| POST | `/` | import | 새 프로젝트 Import |
|
||||
| POST | `/project/{id}/tasks` | importTasks | 기존 프로젝트에 작업 추가 |
|
||||
|
||||
## Web 라우트
|
||||
|
||||
| URI | Name | 설명 |
|
||||
|-----|------|------|
|
||||
| `/project-management` | pm.index | 대시보드 |
|
||||
| `/project-management/projects` | pm.projects.index | 프로젝트 목록 |
|
||||
| `/project-management/projects/create` | pm.projects.create | 프로젝트 생성 |
|
||||
| `/project-management/projects/{id}` | pm.projects.show | 프로젝트 상세 |
|
||||
| `/project-management/projects/{id}/edit` | pm.projects.edit | 프로젝트 수정 |
|
||||
| `/project-management/import` | pm.import | JSON Import |
|
||||
|
||||
## JSON Import 기능
|
||||
|
||||
### JSON 포맷 (새 프로젝트)
|
||||
|
||||
```json
|
||||
{
|
||||
"project": {
|
||||
"name": "프로젝트명 (필수)",
|
||||
"description": "프로젝트 설명",
|
||||
"status": "active",
|
||||
"start_date": "2025-01-01",
|
||||
"end_date": "2025-03-31"
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"title": "작업 제목 (필수)",
|
||||
"description": "작업 설명",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"due_date": "2025-01-15",
|
||||
"issues": [
|
||||
{
|
||||
"title": "이슈 제목 (필수)",
|
||||
"description": "이슈 설명",
|
||||
"type": "bug",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### JSON 포맷 (기존 프로젝트에 작업 추가)
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"title": "추가할 작업",
|
||||
"priority": "medium",
|
||||
"issues": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 사용 예시
|
||||
|
||||
### 프로젝트 생성
|
||||
|
||||
```bash
|
||||
curl -X POST /api/admin/pm/projects \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "SAM MES 개발",
|
||||
"description": "MES 시스템 개발 프로젝트",
|
||||
"status": "active",
|
||||
"start_date": "2025-01-01"
|
||||
}'
|
||||
```
|
||||
|
||||
### 작업 상태 변경
|
||||
|
||||
```bash
|
||||
curl -X POST /api/admin/pm/tasks/1/status \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "in_progress"}'
|
||||
```
|
||||
|
||||
### 작업 순서 변경
|
||||
|
||||
```bash
|
||||
curl -X POST /api/admin/pm/tasks/project/1/reorder \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"task_ids": [3, 1, 2]}'
|
||||
```
|
||||
|
||||
### JSON Import
|
||||
|
||||
```bash
|
||||
curl -X POST /api/admin/pm/import \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @project.json
|
||||
```
|
||||
|
||||
## 대시보드 요약 API 응답
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"projects": {
|
||||
"total": 5,
|
||||
"active": 3,
|
||||
"completed": 1,
|
||||
"on_hold": 1
|
||||
},
|
||||
"tasks": {
|
||||
"total": 25,
|
||||
"todo": 10,
|
||||
"in_progress": 8,
|
||||
"done": 7,
|
||||
"overdue": 2
|
||||
},
|
||||
"issues": {
|
||||
"total": 15,
|
||||
"open": 5,
|
||||
"in_progress": 3,
|
||||
"resolved": 4,
|
||||
"closed": 3
|
||||
},
|
||||
"recent_projects": [...],
|
||||
"urgent_tasks": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 관련 커밋
|
||||
|
||||
- `603062e` - feat(pm): 프로젝트 진행 관리 시스템 구현
|
||||
494
docs/project-management/sam_roadmap_import.json
Normal file
494
docs/project-management/sam_roadmap_import.json
Normal file
@@ -0,0 +1,494 @@
|
||||
{
|
||||
"project": {
|
||||
"name": "SAM 프로젝트 런칭 로드맵",
|
||||
"description": "SAM MES 시스템 개발 및 런칭 준비 현황 추적 (백엔드 70%, 프론트엔드 50% 완료)",
|
||||
"status": "active",
|
||||
"start_date": "2025-11-24",
|
||||
"end_date": "2026-03-31"
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"title": "MS1: 개발 완료",
|
||||
"description": "모든 핵심 기능 개발 완료 및 내부 테스트 통과 (목표: 2025-12-31)",
|
||||
"status": "in_progress",
|
||||
"priority": "high",
|
||||
"due_date": "2025-12-31",
|
||||
"issues": [
|
||||
{
|
||||
"title": "공정 라우팅 (Process Routing) 구현",
|
||||
"description": "공정/생산 계획 - 공정 라우팅 개발",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "작업지시 (Work Order) 구현",
|
||||
"description": "공정/생산 계획 - 작업지시 개발",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "생산실적 (Production Record) 구현",
|
||||
"description": "공정/생산 계획 - 생산실적 개발",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "공정 체크시트 구현",
|
||||
"description": "공정/생산 계획 - 체크시트 개발",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "단가 정책 로직 개발",
|
||||
"description": "단가/원가 체계 - 공장별/중량/치수 기반 단가 정책",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "원가 계산 서비스 개발",
|
||||
"description": "단가/원가 체계 - 제품별, BOM 기반 원가 계산",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "견적-수주 단가 연결 테이블",
|
||||
"description": "단가/원가 체계 - 견적/수주 단가 연결",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "견적서 HTML 템플릿",
|
||||
"description": "견적서 출력 - HTML 템플릿 개발",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "견적서 PDF 생성",
|
||||
"description": "견적서 출력 - DomPDF/Snappy로 PDF 생성",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "입출고 트랜잭션 설계",
|
||||
"description": "재고/자재 관리 - 트랜잭션 구조 설계 및 구현",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "재고 집계 API 개발",
|
||||
"description": "재고/자재 관리 - 제품/부품/자재별 재고 집계",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "LOT/시리얼 관리 확장",
|
||||
"description": "재고/자재 관리 - LOT 및 시리얼 관리",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "창고/위치 모델 구현",
|
||||
"description": "창고/위치 관리 - Warehouse, Location 모델 및 계층 구조",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "파일 Upload API 개발",
|
||||
"description": "파일 시스템 완성 - 업로드/썸네일/권한",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "알림 시스템 구현",
|
||||
"description": "알림 시스템 - 이메일/카카오 발송 및 템플릿 관리",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "테넌트 초기화 API",
|
||||
"description": "테넌트 초기화 - 초기 데이터 생성 및 온보딩 자동화",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "MS2: 베타 런칭",
|
||||
"description": "파일럿 고객 대상 베타 서비스 오픈 및 실전 검증 (목표: 2026-01-15)",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"due_date": "2026-01-15",
|
||||
"issues": [
|
||||
{
|
||||
"title": "베타 서버 구축",
|
||||
"description": "베타 서버 환경 구축 및 도메인/SSL 설정",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "파일럿 고객 선정 (2-3개사)",
|
||||
"description": "베타 테스트 대상 고객 확보 및 계약",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "초기 데이터 마이그레이션",
|
||||
"description": "파일럿 고객 데이터 준비 및 마이그레이션",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "고객 온보딩 프로세스 검증",
|
||||
"description": "온보딩 가이드 작성 및 프로세스 검증",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "고객지원 체계 구축",
|
||||
"description": "티켓 시스템 가동 및 지원 채널 구축",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "모니터링 대시보드 가동",
|
||||
"description": "시스템 모니터링 및 알림 체계 구축",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "사용자 매뉴얼 작성",
|
||||
"description": "관리자/사용자 매뉴얼 및 온보딩 가이드",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "베타 테스트 시나리오 실행",
|
||||
"description": "견적→수주→생산, BOM 자재소요, 멀티테넌트 검증 등",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "MS3: 정식 런칭",
|
||||
"description": "운영 서버 오픈 및 본격적인 영업/마케팅 시작 (목표: 2026-02-15)",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"due_date": "2026-02-15",
|
||||
"issues": [
|
||||
{
|
||||
"title": "운영 서버 구축 (이중화)",
|
||||
"description": "운영 환경 이중화 및 안정성 확보",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "베타 피드백 반영",
|
||||
"description": "베타 기간 수집된 개선사항 반영 및 UI/UX 개선",
|
||||
"type": "improvement",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "보안 감사 통과",
|
||||
"description": "외부 보안 감사 실시 및 취약점 조치",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "법적 문서 완비",
|
||||
"description": "이용약관, 개인정보처리방침, 서비스 계약서",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "제품 소개 자료 제작",
|
||||
"description": "PPT, 데모 사이트, 영업 제안서 템플릿",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "가격 정책 확정",
|
||||
"description": "요금제 및 프로모션 정책 수립",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "마케팅 캠페인 시작",
|
||||
"description": "프레스 릴리스, 런칭 프로모션, 웨비나",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "MS4: 안정화 완료",
|
||||
"description": "서비스 안정화 및 초기 고객 성공 사례 확보 (목표: 2026-03-31)",
|
||||
"status": "todo",
|
||||
"priority": "medium",
|
||||
"due_date": "2026-03-31",
|
||||
"issues": [
|
||||
{
|
||||
"title": "고객 10개사 확보",
|
||||
"description": "정식 계약 고객 10개사 목표",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "시스템 가용성 99.5% 달성",
|
||||
"description": "안정성 확보 및 장애 대응 체계 강화",
|
||||
"type": "improvement",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "고객 만족도 4.0/5.0 달성",
|
||||
"description": "월간 설문 및 피드백 반영",
|
||||
"type": "improvement",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "성공 사례 3건 확보",
|
||||
"description": "견적 처리 시간 단축, 재고 정확도 향상 등 사례",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "다음 분기 로드맵 수립",
|
||||
"description": "Q2 이후 기능 고도화 계획",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "인프라 및 배포",
|
||||
"description": "서버 환경, 도메인, 백업, 모니터링 구축",
|
||||
"status": "in_progress",
|
||||
"priority": "high",
|
||||
"due_date": "2026-01-31",
|
||||
"issues": [
|
||||
{
|
||||
"title": "개발 서버 구성",
|
||||
"description": "codebridge-x.com - 현재 운영 중",
|
||||
"type": "feature",
|
||||
"status": "in_progress"
|
||||
},
|
||||
{
|
||||
"title": "스테이징 서버 준비",
|
||||
"description": "테스트용 스테이징 환경 구축",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "운영 서버 선정 및 구성",
|
||||
"description": "운영 환경 서버 선정",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "Docker 배포 스크립트",
|
||||
"description": "Docker Compose 배포 자동화",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "CI/CD 파이프라인 구축",
|
||||
"description": "자동 빌드/배포 파이프라인",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "운영 도메인 및 SSL",
|
||||
"description": "도메인 확정 및 인증서 발급",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "백업 정책 및 스크립트",
|
||||
"description": "자동 백업 및 복구 절차",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "모니터링 시스템",
|
||||
"description": "Grafana/Prometheus 대시보드 및 알림",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "테스트 및 품질 보증",
|
||||
"description": "기능/성능/사용자 테스트 및 품질 검증",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"due_date": "2025-12-31",
|
||||
"issues": [
|
||||
{
|
||||
"title": "단위 테스트 (60% 커버리지)",
|
||||
"description": "핵심 서비스 단위 테스트 작성",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "통합 테스트",
|
||||
"description": "API 통합 테스트 시나리오",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "E2E 테스트",
|
||||
"description": "사용자 시나리오 E2E 테스트",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "부하/스트레스 테스트",
|
||||
"description": "성능 테스트 및 API 응답 속도 측정",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "알파 테스트 (내부)",
|
||||
"description": "내부 팀 기능 테스트",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "보안 및 컴플라이언스",
|
||||
"description": "보안 점검, 취약점 스캔, 법적 준비",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"due_date": "2026-02-01",
|
||||
"issues": [
|
||||
{
|
||||
"title": "OWASP Top 10 체크",
|
||||
"description": "웹 보안 취약점 점검",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "취약점 스캔",
|
||||
"description": "자동화 보안 스캔 및 조치",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "API 보안 검토",
|
||||
"description": "인증/인가 보안 검토",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "멀티테넌트 격리 검증",
|
||||
"description": "테넌트 간 데이터 격리 100% 검증",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "이용약관 작성",
|
||||
"description": "서비스 이용약관 법률 검토",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "개인정보처리방침",
|
||||
"description": "개인정보 보호 정책 작성",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "문서화",
|
||||
"description": "개발/사용자/운영 문서 작성",
|
||||
"status": "in_progress",
|
||||
"priority": "medium",
|
||||
"due_date": "2026-01-31",
|
||||
"issues": [
|
||||
{
|
||||
"title": "API 문서 (Swagger) 100% 완성",
|
||||
"description": "모든 엔드포인트 Swagger 문서화",
|
||||
"type": "feature",
|
||||
"status": "in_progress"
|
||||
},
|
||||
{
|
||||
"title": "데이터베이스 스키마 문서",
|
||||
"description": "ERD 및 테이블 명세서",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "아키텍처 문서",
|
||||
"description": "시스템 아키텍처 설계 문서",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "배포 가이드",
|
||||
"description": "Docker 배포 및 설정 가이드",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "관리자 매뉴얼",
|
||||
"description": "Admin 패널 사용 매뉴얼",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "사용자 가이드",
|
||||
"description": "일반 사용자 기능 가이드",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "운영 매뉴얼",
|
||||
"description": "장애 대응, 백업/복구 절차",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "React 프론트엔드 개발",
|
||||
"description": "Next.js 15 사용자 포털 개발 (현재 50% 완료)",
|
||||
"status": "in_progress",
|
||||
"priority": "medium",
|
||||
"due_date": "2026-01-31",
|
||||
"issues": [
|
||||
{
|
||||
"title": "공통 레이아웃 정리",
|
||||
"description": "레이아웃 컴포넌트 최종 정리",
|
||||
"type": "feature",
|
||||
"status": "in_progress"
|
||||
},
|
||||
{
|
||||
"title": "기준정보 관리 UI",
|
||||
"description": "Category, CommonCode 관리 화면",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "제품/부품/BOM 트리 UI",
|
||||
"description": "BOM 구조 트리뷰 및 관리 화면",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "견적/수주 화면",
|
||||
"description": "견적서 작성 및 수주 관리 UI",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"title": "재고관리 UI",
|
||||
"description": "재고 현황 및 입출고 관리 화면",
|
||||
"type": "feature",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user