@@ -36,6 +36,7 @@ function StatusIndicator({ onClick }: { onClick?: () => void }) {
3636
3737const BRAND_COLORS : Record < string , string | [ string , string ] > = {
3838 'Nol Universe' : [ '#3549FF' , '#5085FF' ] ,
39+ '미네르바소프트' : [ '#3B82F6' , '#EF4444' ] ,
3940 'Kotlin' : '#7F52FF' ,
4041 'C#' : '#68217A' ,
4142 'TypeScript' : '#3178C6' ,
@@ -101,6 +102,239 @@ interface HistoryEntry {
101102 isTyping ?: boolean
102103}
103104
105+ // Vim-style Resume Viewer
106+ function VimViewer ( { onClose } : { onClose : ( ) => void } ) {
107+ const contentRef = useRef < HTMLDivElement > ( null )
108+ const containerRef = useRef < HTMLDivElement > ( null )
109+
110+ // Focus container on mount
111+ useEffect ( ( ) => {
112+ containerRef . current ?. focus ( )
113+ } , [ ] )
114+
115+ const handleKeyDown = ( e : React . KeyboardEvent ) => {
116+ // e.code는 물리적 키 위치 (한영 상관없이 동작)
117+ if ( e . code === 'KeyQ' || e . key === 'Escape' ) {
118+ e . preventDefault ( )
119+ onClose ( )
120+ } else if ( ( e . code === 'KeyJ' || e . key === 'ArrowDown' ) && contentRef . current ) {
121+ e . preventDefault ( )
122+ contentRef . current . scrollBy ( { top : 80 , behavior : 'smooth' } )
123+ } else if ( ( e . code === 'KeyK' || e . key === 'ArrowUp' ) && contentRef . current ) {
124+ e . preventDefault ( )
125+ contentRef . current . scrollBy ( { top : - 80 , behavior : 'smooth' } )
126+ } else if ( e . code === 'KeyD' && e . ctrlKey && contentRef . current ) {
127+ e . preventDefault ( )
128+ contentRef . current . scrollBy ( { top : 300 , behavior : 'smooth' } )
129+ } else if ( e . code === 'KeyU' && e . ctrlKey && contentRef . current ) {
130+ e . preventDefault ( )
131+ contentRef . current . scrollBy ( { top : - 300 , behavior : 'smooth' } )
132+ } else if ( e . code === 'KeyG' && ! e . shiftKey && contentRef . current ) {
133+ e . preventDefault ( )
134+ contentRef . current . scrollTo ( { top : 0 , behavior : 'smooth' } )
135+ } else if ( e . code === 'KeyG' && e . shiftKey && contentRef . current ) {
136+ e . preventDefault ( )
137+ contentRef . current . scrollTo ( { top : contentRef . current . scrollHeight , behavior : 'smooth' } )
138+ } else if ( e . key === 'PageDown' && contentRef . current ) {
139+ e . preventDefault ( )
140+ contentRef . current . scrollBy ( { top : 300 , behavior : 'smooth' } )
141+ } else if ( e . key === 'PageUp' && contentRef . current ) {
142+ e . preventDefault ( )
143+ contentRef . current . scrollBy ( { top : - 300 , behavior : 'smooth' } )
144+ } else if ( e . key === 'Home' && contentRef . current ) {
145+ e . preventDefault ( )
146+ contentRef . current . scrollTo ( { top : 0 , behavior : 'smooth' } )
147+ } else if ( e . key === 'End' && contentRef . current ) {
148+ e . preventDefault ( )
149+ contentRef . current . scrollTo ( { top : contentRef . current . scrollHeight , behavior : 'smooth' } )
150+ }
151+ }
152+
153+ return (
154+ < div
155+ ref = { containerRef }
156+ tabIndex = { 0 }
157+ onKeyDown = { handleKeyDown }
158+ className = "absolute inset-0 z-10 flex flex-col bg-[#0d1117] font-mono text-sm outline-none"
159+ >
160+ { /* Vim Header */ }
161+ < div className = "flex items-center justify-between border-b border-[#30363d] bg-[#161b22] px-4 py-2" >
162+ < span className = "text-[#8b949e]" > resume.md</ span >
163+ < span className = "text-[#8b949e]" > [readonly]</ span >
164+ </ div >
165+
166+ { /* Content */ }
167+ < div ref = { contentRef } className = "flex-1 overflow-y-auto scrollbar-terminal p-4 sm:p-6" >
168+ { /* Header */ }
169+ < div className = "mb-6" >
170+ < h1 className = "text-2xl font-bold text-[#58a6ff]" > # RAVIT</ h1 >
171+ < p className = "mt-1 text-[#8b949e]" > Backend Developer · 5+ years experience</ p >
172+ </ div >
173+
174+ { /* Experience */ }
175+ < div className = "mb-6" >
176+ < h2 className = "mb-3 text-lg font-semibold text-[#7ee787]" > ## Experience</ h2 >
177+
178+ { /* Nol Universe */ }
179+ < div className = "mb-4 border-l-2 border-[#238636] pl-4" >
180+ < div className = "flex flex-wrap items-center gap-2" >
181+ < span
182+ className = "font-semibold"
183+ style = { {
184+ background : 'linear-gradient(135deg, #3549FF, #5085FF)' ,
185+ WebkitBackgroundClip : 'text' ,
186+ WebkitTextFillColor : 'transparent' ,
187+ } }
188+ >
189+ NOL Universe
190+ </ span >
191+ < span className = "text-[#8b949e]" > (구 인터파크)</ span >
192+ < span className = "rounded bg-[#238636] px-2 py-0.5 text-xs text-white" > current</ span >
193+ </ div >
194+ < div className = "mt-1 text-[#8b949e]" > Backend Developer · 2022.04 ~ present</ div >
195+ < ul className = "mt-2 space-y-1 text-[#c9d1d9]" >
196+ < li > < span className = "text-[#8b949e]" > -</ span > 패키지 투어 서비스 백엔드 개발</ li >
197+ < li > < span className = "text-[#8b949e]" > -</ span > Spring Boot, Kotlin, MSSQL 기반 시스템 구축</ li >
198+ < li > < span className = "text-[#8b949e]" > -</ span > 예약/결제 시스템 설계 및 운영</ li >
199+ < li > < span className = "text-[#8b949e]" > -</ span > 레거시 .NET 시스템 유지보수</ li >
200+ </ ul >
201+ </ div >
202+
203+ { /* Minerva Soft */ }
204+ < div className = "mb-4 border-l-2 border-[#30363d] pl-4" >
205+ < div className = "flex flex-wrap items-center gap-2" >
206+ < span
207+ className = "font-semibold"
208+ style = { {
209+ background : 'linear-gradient(135deg, #3B82F6, #EF4444)' ,
210+ WebkitBackgroundClip : 'text' ,
211+ WebkitTextFillColor : 'transparent' ,
212+ } }
213+ >
214+ 미네르바소프트
215+ </ span >
216+ </ div >
217+ < div className = "mt-1 text-[#8b949e]" > 사원 · 2020.09 ~ 2022.03</ div >
218+ < ul className = "mt-2 space-y-1 text-[#c9d1d9]" >
219+ < li > < span className = "text-[#8b949e]" > -</ span > 플랫폼 개발 및 응용 프로그램 개발</ li >
220+ < li > < span className = "text-[#8b949e]" > -</ span > 동의서 인식기 API Backend (Manager-Agent 병렬처리)</ li >
221+ < li > < span className = "text-[#8b949e]" > -</ span > 통합 모듈 플랫폼 관리 서비스 (Blazor, Socket)</ li >
222+ < li > < span className = "text-[#8b949e]" > -</ span > SQL 구문 분석기 개발 (Oracle, MSSQL, MySQL, PostgreSQL)</ li >
223+ </ ul >
224+ </ div >
225+
226+ { /* KITA */ }
227+ < div className = "mb-4 border-l-2 border-[#30363d] pl-4" >
228+ < div className = "flex flex-wrap items-center gap-2" >
229+ < span className = "font-semibold text-[#c9d1d9]" > 한국무역협회 KITA 아카데미</ span >
230+ </ div >
231+ < div className = "mt-1 text-[#8b949e]" > 프리랜서 · 2020.05 ~ 2020.08</ div >
232+ < ul className = "mt-2 space-y-1 text-[#c9d1d9]" >
233+ < li > < span className = "text-[#8b949e]" > -</ span > LMS 관리 Web 프로그램 개발</ li >
234+ </ ul >
235+ </ div >
236+ </ div >
237+
238+ { /* Skills */ }
239+ < div className = "mb-6" >
240+ < h2 className = "mb-3 text-lg font-semibold text-[#7ee787]" > ## Skills</ h2 >
241+
242+ < div className = "space-y-3" >
243+ < div >
244+ < span className = "text-[#ffa657]" > Languages:</ span >
245+ < div className = "mt-1 flex flex-wrap gap-2" >
246+ < span className = "rounded bg-[#7F52FF]/20 px-2 py-1 text-xs text-[#7F52FF]" > Kotlin</ span >
247+ < span className = "rounded bg-[#68217A]/20 px-2 py-1 text-xs text-[#68217A]" > C#</ span >
248+ < span className = "rounded bg-[#3178C6]/20 px-2 py-1 text-xs text-[#3178C6]" > TypeScript</ span >
249+ < span className = "rounded bg-[#b07219]/20 px-2 py-1 text-xs text-[#b07219]" > Java</ span >
250+ </ div >
251+ </ div >
252+
253+ < div >
254+ < span className = "text-[#ffa657]" > Frameworks:</ span >
255+ < div className = "mt-1 flex flex-wrap gap-2" >
256+ < span className = "rounded bg-[#6DB33F]/20 px-2 py-1 text-xs text-[#6DB33F]" > Spring Boot</ span >
257+ < span className = "rounded bg-[#512BD4]/20 px-2 py-1 text-xs text-[#512BD4]" > .NET / Blazor</ span >
258+ < span className = "rounded bg-[#ffffff]/20 px-2 py-1 text-xs text-[#ffffff]" > Next.js</ span >
259+ </ div >
260+ </ div >
261+
262+ < div >
263+ < span className = "text-[#ffa657]" > Database:</ span >
264+ < div className = "mt-1 flex flex-wrap gap-2" >
265+ < span className = "rounded bg-[#CC2927]/20 px-2 py-1 text-xs text-[#CC2927]" > MSSQL</ span >
266+ < span className = "rounded bg-[#47A248]/20 px-2 py-1 text-xs text-[#47A248]" > MongoDB</ span >
267+ < span className = "rounded bg-[#4479A1]/20 px-2 py-1 text-xs text-[#4479A1]" > MySQL</ span >
268+ < span className = "rounded bg-[#F80000]/20 px-2 py-1 text-xs text-[#F80000]" > Oracle</ span >
269+ </ div >
270+ </ div >
271+
272+ < div >
273+ < span className = "text-[#ffa657]" > DevOps:</ span >
274+ < div className = "mt-1 flex flex-wrap gap-2" >
275+ < span className = "rounded bg-[#2496ED]/20 px-2 py-1 text-xs text-[#2496ED]" > Docker</ span >
276+ < span className = "rounded bg-[#2088FF]/20 px-2 py-1 text-xs text-[#2088FF]" > GitHub Actions</ span >
277+ < span className = "rounded bg-[#ffffff]/20 px-2 py-1 text-xs text-[#ffffff]" > Vercel</ span >
278+ </ div >
279+ </ div >
280+ </ div >
281+ </ div >
282+
283+ { /* Education */ }
284+ < div className = "mb-6" >
285+ < h2 className = "mb-3 text-lg font-semibold text-[#7ee787]" > ## Education</ h2 >
286+ < div className = "space-y-2" >
287+ < div className = "border-l-2 border-[#30363d] pl-4" >
288+ < div className = "font-semibold text-[#c9d1d9]" > 가천대학교</ div >
289+ < div className = "text-[#8b949e]" > 컴퓨터공학과 졸업 · 2010.03 ~ 2020.02</ div >
290+ </ div >
291+ < div className = "border-l-2 border-[#30363d] pl-4" >
292+ < div className = "font-semibold text-[#c9d1d9]" > SC IT MASTER</ div >
293+ < div className = "text-[#8b949e]" > 일본 IT 전문인재 양성 부트캠프 · 2019.03 ~ 2019.12</ div >
294+ </ div >
295+ </ div >
296+ </ div >
297+
298+ { /* Certifications */ }
299+ < div className = "mb-6" >
300+ < h2 className = "mb-3 text-lg font-semibold text-[#7ee787]" > ## Certifications</ h2 >
301+ < div className = "space-y-1 text-[#c9d1d9]" >
302+ < div > < span className = "text-[#8b949e]" > -</ span > JLPT N1</ div >
303+ </ div >
304+ </ div >
305+
306+ { /* Links */ }
307+ < div className = "mb-6" >
308+ < h2 className = "mb-3 text-lg font-semibold text-[#7ee787]" > ## Links</ h2 >
309+ < div className = "space-y-1 text-[#58a6ff]" >
310+ < div > < span className = "text-[#8b949e]" > →</ span > github.com/devRavit</ div >
311+ < div > < span className = "text-[#8b949e]" > →</ span > ravit.run</ div >
312+ </ div >
313+ </ div >
314+ </ div >
315+
316+ { /* Vim Footer */ }
317+ < div className = "border-t border-[#30363d] bg-[#161b22] px-4 py-2" >
318+ < div className = "flex flex-wrap items-center justify-between gap-2 text-xs text-[#8b949e]" >
319+ < span className = "hidden sm:inline" >
320+ < span className = "text-[#ffa657]" > ↑↓</ span > scroll
321+ < span className = "mx-2" > ·</ span >
322+ < span className = "text-[#ffa657]" > PgUp/Dn</ span > page
323+ < span className = "mx-2" > ·</ span >
324+ < span className = "text-[#ffa657]" > Home/End</ span > top/bottom
325+ < span className = "mx-2" > ·</ span >
326+ < span className = "text-[#ffa657]" > q</ span > quit
327+ </ span >
328+ < button onClick = { onClose } className = "text-[#ffa657] hover:text-[#c9d1d9] sm:hidden" >
329+ [tap to close]
330+ </ button >
331+ < span > :q</ span >
332+ </ div >
333+ </ div >
334+ </ div >
335+ )
336+ }
337+
104338const AboutJson = ( ) => (
105339 < pre className = "mt-1 text-xs leading-relaxed sm:text-sm" >
106340 < code >
@@ -119,7 +353,7 @@ const AboutJson = () => (
119353 </ pre >
120354)
121355
122- type CommandResult = React . ReactNode | 'OPEN_GITHUB' | 'OPEN_WORK' | 'CLEAR' | 'FETCH_STATUS'
356+ type CommandResult = React . ReactNode | 'OPEN_GITHUB' | 'OPEN_WORK' | 'CLEAR' | 'FETCH_STATUS' | 'OPEN_RESUME'
123357
124358const COMMANDS : Record < string , { description : string ; action : ( ) => CommandResult } > = {
125359 'help' : {
@@ -130,6 +364,7 @@ const COMMANDS: Record<string, { description: string; action: () => CommandResul
130364 < div className = "ml-2 mt-1" >
131365 < div > < span className = "text-[#79c0ff]" > help</ span > Show this help message</ div >
132366 < div > < span className = "text-[#79c0ff]" > whoami</ span > Display current user</ div >
367+ < div > < span className = "text-[#79c0ff]" > resume</ span > View resume (vim mode)</ div >
133368 < div > < span className = "text-[#79c0ff]" > status</ span > Check system status</ div >
134369 < div > < span className = "text-[#79c0ff]" > open --github</ span > Open GitHub profile</ div >
135370 < div > < span className = "text-[#79c0ff]" > open --work</ span > Open work project</ div >
@@ -142,6 +377,10 @@ const COMMANDS: Record<string, { description: string; action: () => CommandResul
142377 description : 'Display current user' ,
143378 action : ( ) => < div className = "text-[#8b949e]" > ravit</ div > ,
144379 } ,
380+ 'resume' : {
381+ description : 'View resume' ,
382+ action : ( ) => 'OPEN_RESUME' ,
383+ } ,
145384 'status' : {
146385 description : 'Check system status' ,
147386 action : ( ) => 'FETCH_STATUS' ,
@@ -169,6 +408,7 @@ export default function Home() {
169408 const [ commandHistory , setCommandHistory ] = useState < string [ ] > ( [ ] )
170409 const [ historyIndex , setHistoryIndex ] = useState ( - 1 )
171410 const [ isLoading , setIsLoading ] = useState ( false )
411+ const [ showVim , setShowVim ] = useState ( false )
172412 const terminalRef = useRef < HTMLDivElement > ( null )
173413 const inputRef = useRef < HTMLInputElement > ( null )
174414
@@ -284,6 +524,9 @@ export default function Home() {
284524 setIsLoading ( false )
285525 } else if ( result === 'CLEAR' ) {
286526 setHistory ( [ ] )
527+ } else if ( result === 'OPEN_RESUME' ) {
528+ setHistory ( prev => [ ...prev , { command : cmd , output : < div className = "text-[#8b949e]" > Opening resume.md...</ div > } ] )
529+ setShowVim ( true )
287530 } else {
288531 setHistory ( prev => [ ...prev , { command : cmd , output : result } ] )
289532 }
@@ -416,7 +659,10 @@ export default function Home() {
416659 { /* Terminal Section */ }
417660 < section className = "flex h-screen flex-col pt-[57px]" >
418661 { /* Terminal Window - Full screen on mobile */ }
419- < div className = "flex min-h-0 flex-1 flex-col bg-[#161b22] sm:m-6 sm:rounded-lg sm:border sm:border-[#30363d] sm:shadow-2xl" >
662+ < div className = "relative flex min-h-0 flex-1 flex-col bg-[#161b22] sm:m-6 sm:rounded-lg sm:border sm:border-[#30363d] sm:shadow-2xl" >
663+ { /* Vim Viewer Overlay */ }
664+ { showVim && < VimViewer onClose = { ( ) => { setShowVim ( false ) ; inputRef . current ?. focus ( ) } } /> }
665+
420666 { /* Terminal Header */ }
421667 < div className = "flex items-center gap-2 border-b border-[#30363d] bg-[#21262d] px-3 py-2 sm:px-4 sm:py-3" >
422668 < div className = "h-2.5 w-2.5 rounded-full bg-[#ff5f56] sm:h-3 sm:w-3" />
0 commit comments