diff --git a/public/image/skills/css.png b/public/image/skills/css.png new file mode 100644 index 0000000..ca27edc Binary files /dev/null and b/public/image/skills/css.png differ diff --git a/public/image/skills/js.png b/public/image/skills/javascript.png similarity index 100% rename from public/image/skills/js.png rename to public/image/skills/javascript.png diff --git a/public/image/skills/node.png b/public/image/skills/node.png new file mode 100644 index 0000000..1b8c05b Binary files /dev/null and b/public/image/skills/node.png differ diff --git a/public/image/skills/styled.png b/public/image/skills/styled.png new file mode 100644 index 0000000..a263dd4 Binary files /dev/null and b/public/image/skills/styled.png differ diff --git a/public/image/skills/supabase.png b/public/image/skills/supabase.png new file mode 100644 index 0000000..8027d98 Binary files /dev/null and b/public/image/skills/supabase.png differ diff --git a/public/image/skills/tailwind.png b/public/image/skills/tailwind.png new file mode 100644 index 0000000..0089c85 Binary files /dev/null and b/public/image/skills/tailwind.png differ diff --git a/public/image/skills/tanstack-query.png b/public/image/skills/tanstack-query.png new file mode 100644 index 0000000..0d0afe8 Binary files /dev/null and b/public/image/skills/tanstack-query.png differ diff --git a/public/image/skills/typescript.png b/public/image/skills/typescript.png new file mode 100644 index 0000000..21574bd Binary files /dev/null and b/public/image/skills/typescript.png differ diff --git a/public/image/skills/zustand.png b/public/image/skills/zustand.png new file mode 100644 index 0000000..81d3de2 Binary files /dev/null and b/public/image/skills/zustand.png differ diff --git a/src/app/_components/Checkbox/Checkbox.tsx b/src/app/_components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..13459b3 --- /dev/null +++ b/src/app/_components/Checkbox/Checkbox.tsx @@ -0,0 +1,47 @@ +import { ComponentProps } from 'react'; + +interface CheckboxProps extends Omit, 'size'> { + text: string; + size: 'sm' | 'lg'; +} + +function Checkbox({ + className, + checked, + onChange, + text, + size = 'sm', +}: CheckboxProps) { + const inputVariantBySize = { + sm: '', + lg: '', + }; + const inputVariantByChecked = { + checked: 'bg-primary/90', + unchecked: 'bg-primary/25 group-hover:bg-primary/40', + }; + const labelVariant = { + sm: 'text-sm', + lg: 'text-lg', + }; + + return ( + + ); +} +export default Checkbox; diff --git a/src/app/_components/SlideInView/SlideInView.tsx b/src/app/_components/SlideInView/SlideInView.tsx index 0d86fc2..c8b9ff5 100644 --- a/src/app/_components/SlideInView/SlideInView.tsx +++ b/src/app/_components/SlideInView/SlideInView.tsx @@ -17,11 +17,11 @@ function SlideInView({ opacity: 1, translateY: 0, transition: { - duration: 1, + duration: 0.7, delay, }, }} - viewport={{ once: true, amount: 0.9 }} + viewport={{ once: true, amount: 0.3 }} > {children} diff --git a/src/app/_data/experience.ts b/src/app/_data/experience.ts new file mode 100644 index 0000000..1dcaa3a --- /dev/null +++ b/src/app/_data/experience.ts @@ -0,0 +1,76 @@ +export interface TExperience { + title: string; + subtitle: string; + duration: [Date, Date]; + description?: string; + activities: string[]; + achievements: string[]; + links?: { text: string; url: string }[]; +} + +export const EXPERIENCE_CONTENTS: TExperience[] = [ + { + title: 'SearcHRight AI', + subtitle: '프론트엔드 개발 인턴', + duration: [new Date('2024.09'), new Date('2024.11')], + description: + '서치라이트는 숨어있는 ‘인재’들의 역량을 분석하여 회사에 가장 적합한 후보자를 추천하는 솔루션을 개발하는 스타트업입니다.', + activities: ['랜딩 페이지 개발', '블로그 작성 및 업로드'], + achievements: [ + '실무 경험', + '초기 스타트업 경험', + 'HR 도메인에 대한 이해', + ], + links: [ + { + text: '[포트폴리오] 프로젝트 자세히 보기', + url: '/projects/searchright', + }, + { + text: '[블로그] 서치라이트 일지', + url: 'https://velog.io/@2hanbyeol1/series/%EC%84%9C%EC%B9%98%EB%9D%BC%EC%9D%B4%ED%8A%B8', + }, + ], + }, + { + title: '내일배움캠프', + subtitle: 'React 5기', + duration: [new Date('2024.04'), new Date('2024.08')], + activities: [ + '매일 12시간 집중 학습 및 TIL 작성', + '프로젝트 11개 진행 (개인 5개, 팀 6개, 리더 3회)', + ], + achievements: [ + 'React 주요 개념 습득', + '팀원들을 이끌어 프로젝트를 기한 내에 완성하는 능력 기름', + '디자이너, 개발자와의 협업 및 소통 능력 향상', + ], + }, + { + title: '오픈소스 컨트리뷰션 아카데미', + subtitle: 'Git 활용 및 React', + duration: [new Date('2023.07'), new Date('2023.08')], + activities: [ + 'Git, React, React Native 기초 학습', + '오픈소스 `hyper`의 코드와 패키지 구조 분석', + ], + achievements: [ + '오픈소스에 기여할 수 있는 역량을 키우기 위한 목표 수립', + ' → 아카데미에서의 학습을 바탕으로 오픈소스 가상 화이트보드 `excalidraw`에 기여', + ], + links: [ + { + text: '[블로그] Git 학습 내용', + url: 'https://velog.io/@2hanbyeol1/series/Git', + }, + { + text: '[Github] Excalidraw - (CLOSED) 한국어용 손글씨 폰트 추가', + url: 'https://github.com/excalidraw/excalidraw/pull/8531', + }, + { + text: '[Github] Excalidraw - (MERGED) PNG로 내보낼 때 모든 파일로 저장되는 버그 수정', + url: 'https://github.com/excalidraw/excalidraw/pull/8946', + }, + ], + }, +]; diff --git a/src/data/project.ts b/src/app/_data/project.ts similarity index 67% rename from src/data/project.ts rename to src/app/_data/project.ts index 0c9ec01..2ec5539 100644 --- a/src/data/project.ts +++ b/src/app/_data/project.ts @@ -1,8 +1,11 @@ +import { TSkillImgFileName } from './skill'; + export interface TProject { id: string; title: string; description: string | string[]; duration: [Date, Date]; + mainSkills: TSkillImgFileName[]; isMain: boolean; // 주요 프로젝트 여부 bgDark?: boolean; } @@ -13,6 +16,7 @@ export const PROJECT_CONTENTS: TProject[] = [ title: 'Liberty 52 Frame', description: ['커스텀 스피커, Liberty 52', '쇼핑몰 및 관리자 서비스'], duration: [new Date('2023.03'), new Date('2023.12')], + mainSkills: ['react', 'javascript', 'styled-component', 'recoil'], isMain: true, }, { @@ -20,6 +24,7 @@ export const PROJECT_CONTENTS: TProject[] = [ title: 'egomogo', description: ['명지대 인근 음식점 랜덤 추첨', '모바일 애플리케이션'], duration: [new Date('2023.07'), new Date('2023.08')], + mainSkills: ['react-native', 'javascript', 'styled-component', 'recoil'], isMain: true, }, { @@ -27,6 +32,7 @@ export const PROJECT_CONTENTS: TProject[] = [ title: '챗챗', description: ['Web Socket 학습용', '채팅 애플리케이션'], duration: [new Date('2023.10'), new Date('2023.12')], + mainSkills: ['react', 'javascript', 'styled-component', 'node'], isMain: false, bgDark: true, }, @@ -35,6 +41,7 @@ export const PROJECT_CONTENTS: TProject[] = [ title: '무비무비', description: '영화 검색 웹페이지', duration: [new Date('2024.05'), new Date('2024.05')], + mainSkills: ['html', 'css', 'javascript'], isMain: false, bgDark: true, }, @@ -43,6 +50,7 @@ export const PROJECT_CONTENTS: TProject[] = [ title: '문화열차', description: '공연 예약 시스템', duration: [new Date('2024.07'), new Date('2024.07')], + mainSkills: ['next', 'typescript', 'tailwind-css', 'zustand', 'supabase'], isMain: false, }, { @@ -50,21 +58,24 @@ export const PROJECT_CONTENTS: TProject[] = [ title: 'oosie', description: ['기록, 챌린지를 통한', '헬스 케어 커뮤니티'], duration: [new Date('2024.07'), new Date('2024.08')], + mainSkills: ['next', 'typescript', 'tailwind-css', 'zustand', 'supabase'], isMain: true, bgDark: true, }, { id: 'searchright', - title: 'SearcHRight AI 랜딩페이지', - description: ['인재 추천 솔루션,', '서치라이트의 랜딩페이지'], + title: '서치라이트', + description: ['인재 추천 솔루션,', '서치라이트를 소개하는 랜딩페이지'], duration: [new Date('2024.09'), new Date('2024.11')], + mainSkills: ['next', 'typescript', 'tailwind-css', 'zustand'], isMain: true, }, { id: 'portfolio-2', title: '포트폴리오', - description: ['프론트개발자 이한별,', '포트폴리오 웹사이트'], + description: ['FE 개발자 이한별,', '포트폴리오 웹사이트'], duration: [new Date('2025.01'), new Date('2025.02')], + mainSkills: ['next', 'typescript', 'tailwind-css', 'zustand'], isMain: true, }, ]; diff --git a/src/app/_data/skill.ts b/src/app/_data/skill.ts new file mode 100644 index 0000000..3bfb9ac --- /dev/null +++ b/src/app/_data/skill.ts @@ -0,0 +1,82 @@ +export type TSkillImgFileName = + | 'html' + | 'css' + | 'javascript' + | 'typescript' + | 'react' + | 'react-native' + | 'next' + | 'aws' + | 'git' + | 'recoil' + | 'zustand' + | 'tanstack-query' + | 'styled-component' + | 'tailwind-css' + | 'node' + | 'supabase'; + +export interface TSkill { + title: string; + descriptions: string[]; + imgFileName: TSkillImgFileName; +} + +export const SKILL_CONTENTS: TSkill[] = [ + { + title: 'HTML & CSS', + descriptions: [ + '다양한 디바이스 환경에 최적화된 레이아웃과 CSS 애니메이션을 구현합니다.', + '시맨틱 태그를 사용하여 접근성과 SEO 최적화를 고려한 마크업을 작성합니다.', + ], + imgFileName: 'html', + }, + { + title: 'JS & TS', + descriptions: [ + 'ES6 문법과 최신 자바스크립트 기능들을 활용하여 효율적인 코드를 작성합니다.', + '타입 안전성을 확보하기 위해 Typescript를 적극 활용합니다.', + '코드의 가독성과 유지보수성을 높입니다.', + ], + imgFileName: 'javascript', + }, + { + title: 'React', + descriptions: [ + '재사용 가능한 컴포넌트 및 모듈화를 통해 유지보수성이 뛰어난 애플리케이션을 구현합니다.', + '다양한 React Hook을 활용하여 기능을 개발할 수 있습니다.', + 'React Native를 이용한 크로스 플랫폼 앱 개발 프로젝트를 진행한 적이 있습니다.', + ], + imgFileName: 'react', + }, + { + title: 'Next.js', + descriptions: [ + 'CSR, SSR, SSG, ISR의 동작원리에 대해 이해하고 있습니다.', + 'Next.js의 라우팅, API 라우트, 이미지 최적화 등 다양한 기능을 활용하여 성능 및 SEO에 최적화된 웹 애플리케이션을 개발합니다.', + ], + imgFileName: 'next', + }, + { + title: 'Deploy', + descriptions: [ + 'AWS S3와 CloudFront로 정적 페이지를 배포한 경험이 있습니다.', + 'GitHub Actions를 통해 CI/CD 파이프라인을 구축하여 배포 프로세스를 자동화한 경험이 있습니다.', + ], + imgFileName: 'aws', + }, + { + title: 'Git', + descriptions: [ + 'Git과 GitHub를 활용한 코드 버전 관리 및 협업 경험을 바탕으로, 효과적인 팀 커뮤니케이션과 프로젝트 관리를 수행합니다.', + ], + imgFileName: 'git', + }, + { + title: '전역 상태 관리', + descriptions: [ + '상태 관리 라이브러리로 Recoil, Zustand, Tanstack Query 등을 사용한 경험이 있습니다.', + ], + imgFileName: 'recoil', + }, +]; diff --git a/src/app/_sections/ExperiencesSection/Experience.tsx b/src/app/_sections/ExperiencesSection/Experience.tsx index 64aec25..a66aab0 100644 --- a/src/app/_sections/ExperiencesSection/Experience.tsx +++ b/src/app/_sections/ExperiencesSection/Experience.tsx @@ -1,4 +1,4 @@ -import { TExperience } from '@/data/experience'; +import { TExperience } from '@/app/_data/experience'; import { parseDateToYYYYMM } from '@/utils/util'; import Image from 'next/image'; import Link from 'next/link'; @@ -17,54 +17,64 @@ function Experience({ experience: TExperience; }) { return ( -
+
-
+
{parseDateToYYYYMM(duration[0])} - {parseDateToYYYYMM(duration[1])}
-
+
별 -

{title}

-

{subtitle}

+

{title}

+

{subtitle}

-

{description}

+ {description ? ( +

> {description}

+ ) : ( + <> + )}
-
주요 활동
+
주요 활동
    {activities.map((activity, idx) => ( -
  • - {activity} -
  • +
  • ))}
-
성과
+
성과
    {achievements.map((achievement, idx) => ( -
  • - {achievement} -
  • +
  • ))}
{links && links.length > 0 ? (
-
    +
    참조
    +
      {links.map((link, idx) => ( -
    • - - {link.text} - +
    • + {link.text}
    • ))}
    diff --git a/src/app/_sections/ExperiencesSection/ExperiencesSection.tsx b/src/app/_sections/ExperiencesSection/ExperiencesSection.tsx index 28fc581..7763143 100644 --- a/src/app/_sections/ExperiencesSection/ExperiencesSection.tsx +++ b/src/app/_sections/ExperiencesSection/ExperiencesSection.tsx @@ -1,5 +1,5 @@ import SlideInView from '@/app/_components/SlideInView'; -import { EXPERIENCE_CONTENTS } from '@/data/experience'; +import { EXPERIENCE_CONTENTS } from '@/app/_data/experience'; import SectionWatcher from '../SectionWatcher'; import Experience from './Experience'; diff --git a/src/app/_sections/ProjectsSection/Project.tsx b/src/app/_sections/ProjectsSection/Project.tsx index 852e583..65292ed 100644 --- a/src/app/_sections/ProjectsSection/Project.tsx +++ b/src/app/_sections/ProjectsSection/Project.tsx @@ -1,44 +1,59 @@ -import { TProject } from '@/data/project'; +import { TProject } from '@/app/_data/project'; import { parseDateToYYYYMM } from '@/utils/util'; import Image from 'next/image'; import Link from 'next/link'; function Project({ - project: { id, title, description, duration, bgDark }, + project: { id, title, description, duration, mainSkills, bgDark }, }: { project: TProject; }) { return (
    {title}
    -

    {title}

    -

    +

    {title}

    +

    {Array.isArray(description) ? description.map((desc, idx) => ( - + {desc} )) : description}

    -
    +
    {parseDateToYYYYMM(duration[0])} - {parseDateToYYYYMM(duration[1])}
    +
    + {mainSkills.map((skill) => ( +
    + {skill} + + {skill} + +
    + ))} +
    ); } diff --git a/src/app/_sections/ProjectsSection/ProjectsSection.tsx b/src/app/_sections/ProjectsSection/ProjectsSection.tsx index 8a37620..a56b685 100644 --- a/src/app/_sections/ProjectsSection/ProjectsSection.tsx +++ b/src/app/_sections/ProjectsSection/ProjectsSection.tsx @@ -1,24 +1,59 @@ -import { PROJECT_CONTENTS } from '@/data/project'; +'use client'; +import Checkbox from '@/app/_components/Checkbox/Checkbox'; +import { PROJECT_CONTENTS } from '@/app/_data/project'; +import { motion, useScroll, useTransform } from 'motion/react'; +import { useRef, useState } from 'react'; import SectionWatcher from '../SectionWatcher'; import Project from './Project'; function ProjectsSection() { + const [isMainChecked, setMainChecked] = useState(true); + const projectsRef = useRef(null); + const { scrollYProgress } = useScroll({ + target: projectsRef, + }); + const PROJECT_COUNT = isMainChecked ? 5 : PROJECT_CONTENTS.length; + + const x = useTransform( + scrollYProgress, + [0, 1], + [0, Math.min(0, -(320 * (PROJECT_COUNT - 1)))], + ); + + const handleCheckboxChange = () => { + setMainChecked((prev) => !prev); + scrollYProgress.set(0); + projectsRef?.current?.scrollIntoView(); + }; + return ( - -
    - - -
      - {PROJECT_CONTENTS.map((project) => ( -
    • - -
    • - ))} -
    + +
    +
    + + + {PROJECT_CONTENTS.filter( + (project) => !isMainChecked || project.isMain, + ).map((project) => ( + + + + ))} + +
    ); diff --git a/src/app/_sections/SkillsSection/Skill.tsx b/src/app/_sections/SkillsSection/Skill.tsx index 915e596..78ecc99 100644 --- a/src/app/_sections/SkillsSection/Skill.tsx +++ b/src/app/_sections/SkillsSection/Skill.tsx @@ -1,4 +1,4 @@ -import { TSkill } from '@/data/skill'; +import { TSkill } from '@/app/_data/skill'; import Image from 'next/image'; function Skill({ @@ -22,10 +22,9 @@ function Skill({ {descriptions.map((description, idx) => (

    - {description} -

    + className="text-xl font-medium leading-normal text-dark3 [&>b]:font-bold [&>b]:text-dark2" + dangerouslySetInnerHTML={{ __html: description }} + /> ))}
    diff --git a/src/app/_sections/SkillsSection/SkillsSection.tsx b/src/app/_sections/SkillsSection/SkillsSection.tsx index c8fda63..c258475 100644 --- a/src/app/_sections/SkillsSection/SkillsSection.tsx +++ b/src/app/_sections/SkillsSection/SkillsSection.tsx @@ -1,5 +1,5 @@ import SlideInView from '@/app/_components/SlideInView'; -import { SKILL_CONTENTS } from '@/data/skill'; +import { SKILL_CONTENTS } from '@/app/_data/skill'; import SectionWatcher from '../SectionWatcher'; import Skill from './Skill'; @@ -7,7 +7,7 @@ function SkillsSection() { return (
    -
    +
    {SKILL_CONTENTS.map((skill, idx) => (