diff --git a/app/lib/portfolio.ts b/app/lib/portfolio.ts new file mode 100644 index 0000000..ee2bcaf --- /dev/null +++ b/app/lib/portfolio.ts @@ -0,0 +1,104 @@ +export type CompanyType = 'NOL_UNIVERSE' | 'MINERVA_SOFT' | 'PERSONAL' +export type TaskType = 'FEATURE' | 'IMPROVEMENT' | 'BUG_FIX' | 'REFACTORING' | 'RESEARCH' | 'DOCUMENTATION' + +export interface Period { + startedAt: string | null + endedAt: string | null +} + +export interface TaskDetails { + background: string | null + solution: string | null + impact: string | null +} + +export interface Task { + id: string + projectId: string + type: TaskType + title: string + description: string | null + period: Period | null + workingDays: number | null + details: TaskDetails | null + keywords: string[] + createdAt: string + updatedAt: string +} + +export interface Project { + id: string + company: CompanyType + name: string + summary: string + period: Period | null + techStack: string[] + achievements: string[] + teamSize: number | null + role: string | null + tasks: Task[] | null + createdAt: string + updatedAt: string +} + +export const companyDisplayName: Record = { + NOL_UNIVERSE: 'NOL Universe (인터파크트리플)', + MINERVA_SOFT: '미네르바소프트', + PERSONAL: '개인 프로젝트', +} + +export const taskTypeDisplayName: Record = { + FEATURE: '신규 기능', + IMPROVEMENT: '개선', + BUG_FIX: '버그 수정', + REFACTORING: '리팩토링', + RESEARCH: '조사/분석', + DOCUMENTATION: '문서화', +} + +export async function fetchProjects(includeTasks = true): Promise { + const url = process.env.NEXT_PUBLIC_STASH_API_URL + + if (!url) { + throw new Error('STASH_API_URL is not configured') + } + + const res = await fetch(`${url}/externals/projects?includeTasks=${includeTasks}`, { + next: { revalidate: 300 }, + }) + + if (!res.ok) { + throw new Error(`Failed to fetch projects: ${res.status}`) + } + + return res.json() +} + +export async function fetchProjectById(id: string, includeTasks = true): Promise { + const url = process.env.NEXT_PUBLIC_STASH_API_URL + + if (!url) { + throw new Error('STASH_API_URL is not configured') + } + + const res = await fetch(`${url}/externals/projects/${id}?includeTasks=${includeTasks}`, { + next: { revalidate: 300 }, + }) + + if (res.status === 404) { + return null + } + + if (!res.ok) { + throw new Error(`Failed to fetch project: ${res.status}`) + } + + return res.json() +} + +export function formatPeriod(period: Period | null): string { + if (!period) return '' + const start = period.startedAt ? period.startedAt.substring(0, 7).replace('-', '.') : '' + const end = period.endedAt ? period.endedAt.substring(0, 7).replace('-', '.') : '현재' + return `${start} ~ ${end}` +} diff --git a/app/page.tsx b/app/page.tsx index f822bae..775954b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,12 @@ 'use client' import { useEffect, useRef, useState } from 'react' +import { + Project, + companyDisplayName, + taskTypeDisplayName, + formatPeriod, +} from './lib/portfolio' type OverallStatus = 'operational' | 'degraded' | 'outage' | 'loading' @@ -353,7 +359,7 @@ const AboutJson = () => ( ) -type CommandResult = React.ReactNode | 'OPEN_GITHUB' | 'OPEN_WORK' | 'CLEAR' | 'FETCH_STATUS' | 'OPEN_RESUME' +type CommandResult = React.ReactNode | 'OPEN_GITHUB' | 'OPEN_WORK' | 'CLEAR' | 'FETCH_STATUS' | 'FETCH_PORTFOLIO' | 'OPEN_RESUME' const COMMANDS: Record CommandResult }> = { 'help': { @@ -365,6 +371,7 @@ const COMMANDS: Record CommandResul
help Show this help message
whoami Display current user
resume View resume (vim mode)
+
portfolio View project portfolio
status Check system status
open --github Open GitHub profile
open --work Open work project
@@ -385,6 +392,10 @@ const COMMANDS: Record CommandResul description: 'Check system status', action: () => 'FETCH_STATUS', }, + 'portfolio': { + description: 'View project portfolio', + action: () => 'FETCH_PORTFOLIO', + }, 'open --github': { description: 'Open GitHub profile', action: () => 'OPEN_GITHUB', @@ -444,6 +455,81 @@ export default function Home() { } } + const fetchPortfolio = async (): Promise => { + const url = process.env.NEXT_PUBLIC_STASH_API_URL + if (!url) return null + try { + const res = await fetch(`${url}/externals/projects?includeTasks=true`) + if (!res.ok) return null + return res.json() + } catch { + return null + } + } + + const renderPortfolioOutput = (projects: Project[] | null) => { + if (!projects) { + return
Failed to fetch portfolio
+ } + + // Group by company + const grouped = projects.reduce((acc, project) => { + const company = project.company + if (!acc[company]) acc[company] = [] + acc[company].push(project) + return acc + }, {} as Record) + + return ( +
+ {Object.entries(grouped).map(([company, companyProjects]) => ( +
+
+ ## {companyDisplayName[company as keyof typeof companyDisplayName] || company} +
+ {companyProjects.map((project) => ( +
+
+ {project.name} + {project.period && ( + {formatPeriod(project.period)} + )} +
+
{project.summary}
+ {project.techStack.length > 0 && ( +
+ {project.techStack.slice(0, 5).map((tech) => ( + + {tech} + + ))} + {project.techStack.length > 5 && ( + +{project.techStack.length - 5} + )} +
+ )} + {project.tasks && project.tasks.length > 0 && ( +
+ {project.tasks.map((task) => ( +
+ └─ + [{taskTypeDisplayName[task.type]}] + {task.title} +
+ ))} +
+ )} +
+ ))} +
+ ))} +
+ Total: {projects.length} projects, {projects.reduce((acc, p) => acc + (p.tasks?.length || 0), 0)} tasks +
+
+ ) + } + const renderStatusOutput = (data: { services?: { name: string; version: string; status: string; responseTime: number; dependencies?: { type: string; status: string; details?: { replicaSet?: { nodes: { state: string; healthy: boolean }[] } } }[] }[] }) => { if (!data?.services?.length) { return
Failed to fetch status
@@ -522,6 +608,16 @@ export default function Home() { return newHistory }) setIsLoading(false) + } else if (result === 'FETCH_PORTFOLIO') { + setIsLoading(true) + setHistory(prev => [...prev, { command: cmd, output:
Fetching portfolio...
}]) + const projects = await fetchPortfolio() + setHistory(prev => { + const newHistory = [...prev] + newHistory[newHistory.length - 1] = { command: cmd, output: renderPortfolioOutput(projects) } + return newHistory + }) + setIsLoading(false) } else if (result === 'CLEAR') { setHistory([]) } else if (result === 'OPEN_RESUME') { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ab66b6c..a7b8c13 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,18 @@ +## v0.0.11 +`2026.01.15 01:30` + +Portfolio 터미널 명령어 추가 + +- `portfolio` 명령어 추가 (stash API 연동) +- lib/portfolio.ts 추가 (타입 정의 및 API 함수) +- 회사별 프로젝트 그룹핑 및 Task 목록 표시 +- fetchProjects, fetchProjectById API 함수 구현 + +--- + ## v0.0.10 `2026.01.15 00:30`