Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions app/lib/portfolio.ts
Original file line number Diff line number Diff line change
@@ -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<CompanyType, string> = {
NOL_UNIVERSE: 'NOL Universe (인터파크트리플)',
MINERVA_SOFT: '미네르바소프트',
PERSONAL: '개인 프로젝트',
}

export const taskTypeDisplayName: Record<TaskType, string> = {
FEATURE: '신규 기능',
IMPROVEMENT: '개선',
BUG_FIX: '버그 수정',
REFACTORING: '리팩토링',
RESEARCH: '조사/분석',
DOCUMENTATION: '문서화',
}

export async function fetchProjects(includeTasks = true): Promise<Project[]> {
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<Project | null> {
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}`
}
98 changes: 97 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -353,7 +359,7 @@ const AboutJson = () => (
</pre>
)

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<string, { description: string; action: () => CommandResult }> = {
'help': {
Expand All @@ -365,6 +371,7 @@ const COMMANDS: Record<string, { description: string; action: () => CommandResul
<div><span className="text-[#79c0ff]">help</span> Show this help message</div>
<div><span className="text-[#79c0ff]">whoami</span> Display current user</div>
<div><span className="text-[#79c0ff]">resume</span> View resume (vim mode)</div>
<div><span className="text-[#79c0ff]">portfolio</span> View project portfolio</div>
<div><span className="text-[#79c0ff]">status</span> Check system status</div>
<div><span className="text-[#79c0ff]">open --github</span> Open GitHub profile</div>
<div><span className="text-[#79c0ff]">open --work</span> Open work project</div>
Expand All @@ -385,6 +392,10 @@ const COMMANDS: Record<string, { description: string; action: () => 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',
Expand Down Expand Up @@ -444,6 +455,81 @@ export default function Home() {
}
}

const fetchPortfolio = async (): Promise<Project[] | null> => {
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 <div className="text-[#f85149]">Failed to fetch portfolio</div>
}

// 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<string, Project[]>)

return (
<div className="space-y-4">
{Object.entries(grouped).map(([company, companyProjects]) => (
<div key={company}>
<div className="text-[#7ee787] font-semibold">
## {companyDisplayName[company as keyof typeof companyDisplayName] || company}
</div>
{companyProjects.map((project) => (
<div key={project.id} className="mt-2 border-l-2 border-[#30363d] pl-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[#58a6ff] font-medium">{project.name}</span>
{project.period && (
<span className="text-[#8b949e] text-xs">{formatPeriod(project.period)}</span>
)}
</div>
<div className="text-[#8b949e] text-xs mt-1">{project.summary}</div>
{project.techStack.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{project.techStack.slice(0, 5).map((tech) => (
<span key={tech} className="text-[10px] px-1.5 py-0.5 rounded bg-[#21262d] text-[#8b949e]">
{tech}
</span>
))}
{project.techStack.length > 5 && (
<span className="text-[10px] text-[#8b949e]">+{project.techStack.length - 5}</span>
)}
</div>
)}
{project.tasks && project.tasks.length > 0 && (
<div className="mt-2 ml-2 space-y-1">
{project.tasks.map((task) => (
<div key={task.id} className="text-[#c9d1d9] text-xs">
<span className="text-[#8b949e]">└─ </span>
<span className="text-[#ffa657]">[{taskTypeDisplayName[task.type]}]</span>
<span> {task.title}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
))}
<div className="text-[#8b949e] text-xs mt-4">
Total: {projects.length} projects, {projects.reduce((acc, p) => acc + (p.tasks?.length || 0), 0)} tasks
</div>
</div>
)
}

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 <div className="text-[#f85149]">Failed to fetch status</div>
Expand Down Expand Up @@ -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: <div className="text-[#8b949e]">Fetching portfolio...</div> }])
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') {
Expand Down
12 changes: 12 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

<!-- CHANGELOG_START -->

## 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`

Expand Down