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:
2025-11-30 21:04:19 +09:00
parent 432ec2b1c1
commit 76c8a94e4f
18 changed files with 8861 additions and 10 deletions

259
docs/00_OVERVIEW.md Normal file
View 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

View 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
View 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

View 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
View 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
View 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

View 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

View 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 기반)

File diff suppressed because it is too large Load Diff

768
docs/DEV_PROCESS.md Normal file
View 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
View 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 관리 시스템 구현 패턴 기반

View File

@@ -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
View 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 관리 시스템 레이아웃 기반

View File

@@ -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
View 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
View 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

View 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): 프로젝트 진행 관리 시스템 구현

View 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"
}
]
}
]
}