/**
* PRD Manager - Product Requirements Document 관리 클래스
* PRD의 생성, 조회, 수정, 삭제 및 비즈니스 로직 처리
*/
import { v4 as uuidv4 } from 'uuid';
// 대시보드 중심 접근: 복잡한 스키마 검증 제거
import { SQLitePRDStorage } from '../database/SQLitePRDStorage.js';
// PRD 상태 enum
export const PRDStatus = {
DRAFT: 'draft',
ACTIVE: 'active',
IN_REVIEW: 'in_review',
APPROVED: 'approved',
COMPLETED: 'completed',
CANCELLED: 'cancelled'
};
// 우선순위 enum (데이터베이스 스키마와 일치하도록 대문자)
export const PriorityLevels = {
HIGH: 'High',
MEDIUM: 'Medium',
LOW: 'Low'
};
// Moscow 우선순위
export const MoscowPriority = {
MUST: 'must',
SHOULD: 'should',
COULD: 'could',
WONT: 'wont'
};
export class PRDManager {
constructor() {
this.storage = new SQLitePRDStorage();
this.initialized = false;
}
async ensureInitialized() {
if (!this.initialized) {
await this.storage.initialize();
this.initialized = true;
}
}
/**
* 새로운 PRD 생성
* @param {Object} prdData - PRD 기본 데이터
* @returns {Object} 생성된 PRD 객체
*/
async createPRD(prdData) {
await this.ensureInitialized();
try {
// 우선순위 정규화 (대문자 → 소문자)
const normalizedPriority = this.normalizePriority(prdData.priority);
// 기본 PRD 구조 생성
const prd = {
id: uuidv4(),
title: prdData.title,
description: prdData.description,
version: '1.0.0',
status: PRDStatus.DRAFT,
priority: normalizedPriority,
createdAt: new Date(),
updatedAt: new Date(),
createdBy: prdData.createdBy || 'system',
lastModifiedBy: prdData.createdBy || 'system',
businessObjective: prdData.businessObjective || 'Business objective to be defined',
targetUsers: prdData.targetUsers || ['General users'],
successCriteria: prdData.successCriteria || ['Success criteria to be defined'],
epics: [],
requirements: [],
userStories: [],
technicalConstraints: prdData.technicalConstraints || [],
assumptions: prdData.assumptions || [],
risks: [],
timeline: {},
qualityGates: [],
tags: prdData.tags || [],
attachments: []
};
// 요구사항 처리
if (prdData.requirements && Array.isArray(prdData.requirements)) {
prd.requirements = prdData.requirements.map(req => {
if (typeof req === 'string') {
// 문자열 요구사항을 객체로 변환
return {
id: uuidv4(),
title: req.substring(0, 100),
description: req,
type: 'functional',
priority: normalizedPriority,
moscow: MoscowPriority.MUST,
acceptanceCriteria: [`${req} 기능이 정상적으로 동작해야 함`],
dependencies: [],
estimatedHours: 0,
tags: []
};
} else {
// 이미 구조화된 요구사항
return {
id: req.id || uuidv4(),
title: req.title,
description: req.description,
type: req.type || 'functional',
priority: this.normalizePriority(req.priority) || PriorityLevels.MEDIUM,
moscow: req.moscow || MoscowPriority.MUST,
acceptanceCriteria: req.acceptanceCriteria || [],
dependencies: req.dependencies || [],
estimatedHours: req.estimatedHours || 0,
tags: req.tags || []
};
}
});
}
// 간단한 기본 검증
if (!prd.title || prd.title.trim().length === 0) {
throw new Error('제목은 필수입니다');
}
// PRD 저장
await this.storage.savePRD(prd);
return {
success: true,
prd: prd,
message: `PRD "${prd.title}" 생성 완료`
};
} catch (error) {
throw new Error(`PRD 생성 실패: ${error.message}`);
}
}
/**
* PRD 목록 조회
* @param {string} status - 필터링할 상태 (옵션)
* @returns {Array} PRD 요약 목록
*/
async listPRDs(status = null) {
await this.ensureInitialized();
try {
const allPRDs = await this.storage.listAllPRDs();
let filteredPRDs = allPRDs;
if (status) {
filteredPRDs = allPRDs.filter(prd => prd.status === status);
}
// 요약 정보만 반환
const summaries = filteredPRDs.map(prd => ({
id: prd.id,
title: prd.title,
description: prd.description.substring(0, 150) + '...',
status: prd.status,
priority: this.calculateOverallPriority(prd),
requirementsCount: prd.requirements.length,
estimatedHours: this.calculateTotalHours(prd),
createdAt: prd.createdAt,
updatedAt: prd.updatedAt,
tags: prd.tags
}));
return {
success: true,
prds: summaries,
total: summaries.length,
message: status ? `상태 "${status}" PRD ${summaries.length}개 조회` : `전체 PRD ${summaries.length}개 조회`
};
} catch (error) {
throw new Error(`PRD 목록 조회 실패: ${error.message}`);
}
}
/**
* 특정 PRD 상세 조회
* @param {string} prdId - PRD ID
* @returns {Object} PRD 상세 정보
*/
async getPRD(prdId) {
await this.ensureInitialized();
try {
const prd = await this.storage.getPRD(prdId);
if (!prd) {
throw new Error(`PRD를 찾을 수 없습니다: ${prdId}`);
}
return {
success: true,
prd: prd,
analytics: {
totalRequirements: prd.requirements.length,
requirementsByType: this.groupRequirementsByType(prd.requirements),
requirementsByPriority: this.groupRequirementsByPriority(prd.requirements),
estimatedHours: this.calculateTotalHours(prd),
completionRate: this.calculateCompletionRate(prd)
},
message: `PRD "${prd.title}" 조회 완료`
};
} catch (error) {
throw new Error(`PRD 조회 실패: ${error.message}`);
}
}
/**
* PRD 업데이트
* @param {string} prdId - PRD ID
* @param {Object} updates - 업데이트할 필드들
* @returns {Object} 업데이트된 PRD
*/
async updatePRD(prdId, updates) {
await this.ensureInitialized();
try {
const existingPRD = await this.storage.getPRD(prdId);
if (!existingPRD) {
throw new Error(`PRD를 찾을 수 없습니다: ${prdId}`);
}
// 대시보드 호환 업데이트 적용 (기존 구조 유지)
const updatedPRD = {
...existingPRD,
...updates,
id: prdId, // ID는 변경 불가
updated_at: new Date().toISOString(), // 대시보드 형식
last_modified_by: updates.lastModifiedBy || 'system'
};
// 버전 관리
if (updates.title !== existingPRD.title ||
updates.requirements !== existingPRD.requirements) {
const versionParts = existingPRD.version.split('.');
const major = parseInt(versionParts[0]);
const minor = parseInt(versionParts[1]);
const patch = parseInt(versionParts[2]);
updatedPRD.version = `${major}.${minor}.${patch + 1}`;
}
// 대시보드 호환 검증
if (updates.title !== undefined && (!updates.title || updates.title.trim().length === 0)) {
throw new Error('제목은 비어있을 수 없습니다');
}
// requirements가 문자열 배열인 경우 그대로 유지 (대시보드 형식)
if (updates.requirements && Array.isArray(updates.requirements)) {
// 대시보드 형식 유지
}
// 저장 (updatedPRD 사용)
await this.storage.savePRD(updatedPRD);
return {
success: true,
prd: updatedPRD,
message: `PRD "${updatedPRD.title}" 업데이트 완료`
};
} catch (error) {
throw new Error(`PRD 업데이트 실패: ${error.message}`);
}
}
/**
* 요구사항 추가
* @param {string} prdId - PRD ID
* @param {Object} requirementData - 요구사항 데이터
* @returns {Object} 추가된 요구사항
*/
async addRequirement(prdId, requirementData) {
await this.ensureInitialized();
try {
const prd = await this.storage.getPRD(prdId);
if (!prd) {
throw new Error(`PRD를 찾을 수 없습니다: ${prdId}`);
}
const requirement = {
id: uuidv4(),
...requirementData,
tags: requirementData.tags || []
};
// 간단한 요구사항 검증
if (!requirement || typeof requirement !== 'object' || !requirement.title) {
throw new Error('요구사항에 제목이 필요합니다');
}
// PRD에 요구사항 추가
prd.requirements.push(requirement);
prd.updatedAt = new Date();
await this.storage.savePRD(prd);
return {
success: true,
requirement: validation.value,
message: `요구사항 "${requirement.title}" 추가 완료`
};
} catch (error) {
throw new Error(`요구사항 추가 실패: ${error.message}`);
}
}
// 유틸리티 메서드들
/**
* PRD의 전체 우선순위 계산
*/
calculateOverallPriority(prd) {
if (!prd.requirements || prd.requirements.length === 0) return 'Medium';
const priorities = prd.requirements.map(req => req.priority);
if (priorities.includes(PriorityLevels.HIGH)) return PriorityLevels.HIGH;
if (priorities.includes(PriorityLevels.MEDIUM)) return PriorityLevels.MEDIUM;
return PriorityLevels.LOW;
}
/**
* 총 예상 시간 계산
*/
calculateTotalHours(prd) {
if (!prd.requirements) return 0;
return prd.requirements.reduce((total, req) => total + (req.estimatedHours || 0), 0);
}
/**
* 완성률 계산 (향후 Task 연동 시 사용)
*/
calculateCompletionRate(prd) {
// 현재는 기본값 반환, 향후 Task와 연동 시 실제 계산
return 0;
}
/**
* 요구사항을 타입별로 그룹화
*/
groupRequirementsByType(requirements) {
const grouped = {};
requirements.forEach(req => {
if (!grouped[req.type]) grouped[req.type] = 0;
grouped[req.type]++;
});
return grouped;
}
/**
* 요구사항을 우선순위별로 그룹화
*/
groupRequirementsByPriority(requirements) {
const grouped = {};
requirements.forEach(req => {
if (!grouped[req.priority]) grouped[req.priority] = 0;
grouped[req.priority]++;
});
return grouped;
}
/**
* 우선순위 정규화 (대소문자 구분 없이 데이터베이스 형식으로 변환)
* @param {string} priority - 우선순위 값
* @returns {string} 정규화된 대문자 우선순위 (High, Medium, Low)
*/
normalizePriority(priority) {
if (!priority) return PriorityLevels.MEDIUM;
const normalizedValue = priority.toLowerCase();
// 소문자로 받은 우선순위를 데이터베이스 형식(대문자)으로 변환
switch (normalizedValue) {
case 'high':
return PriorityLevels.HIGH;
case 'medium':
return PriorityLevels.MEDIUM;
case 'low':
return PriorityLevels.LOW;
default:
return PriorityLevels.MEDIUM;
}
}
}