Folioo는 사용자가 자신의 경험을 기록하고, AI 인터뷰를 통해 포트폴리오를 생성한 뒤, 지원 기업과 직무에 맞게 포트폴리오를 첨삭받을 수 있는 커리어 포트폴리오 서비스입니다.
프론트엔드는 Next.js App Router 기반으로 구현되어 있으며, 긴 사용자 플로우와 AI 비동기 작업을 안정적으로 처리하는 데 초점을 맞췄습니다. 인사이트 기록, AI 채팅, 포트폴리오 생성 대기, PDF 구조화, 첨삭 결과 렌더링처럼 상태가 복잡한 화면을 React Query, Zustand, 상태머신형 UI 분기, polling, SSE 스트리밍으로 관리합니다.
인사이트 기록
→ 경험 정리 시작
→ AI 인터뷰 진행
→ 텍스트 포트폴리오 생성
→ 시각화 포트폴리오 생성 및 내보내기
→ 포트폴리오 첨삭 요청
→ 첨삭 결과 확인사용자가 경험의 재료가 되는 생각, 활동, 회고를 짧게 기록하는 기능입니다. 기록된 인사이트는 단독으로 관리할 수 있고, 이후 경험 정리 AI 채팅에서 @ 멘션으로 불러와 답변 재료로 사용할 수 있습니다.
주요 구현 내용:
- 인사이트 로그 작성, 조회, 검색, 수정, 삭제
- 카테고리별 입력 템플릿과 글자 수 제한
- 활동 태그 생성 및 조회
- 검색어와 필터 조건 기반 목록 조회
- 비로그인 사용자가 작성 중 로그인해야 할 때 draft를 보존하는 흐름
- 서버 DTO를 카드 UI 모델로 변환하는 mapper 구성
기술 포인트:
- React state로 작성 폼, 수정 모드, 선택 태그, 입력 길이 상태 관리
- React Query로 로그 목록과 태그 목록 조회
- Zustand store와 browser storage로 작성 중 상태 복원
- Zod 기반 입력 검증
사용자가 경험명, 희망 직군, 기본 정보를 입력하면 AI가 인터뷰 방식으로 질문을 이어가고, 답변을 바탕으로 포트폴리오를 생성하는 기능입니다.
주요 구현 내용:
- 경험 목록 조회와 검색
- 새 경험 생성 전 이용권 확인
- 경험 설정 폼 검증
- AI 인터뷰 채팅
- 채팅 중 인사이트 로그 멘션
- 파일 첨부
- 새로고침 또는 재진입 시 채팅 내역 복원
- 서버 진행 단계에 따른 화면 복원
- 포트폴리오 생성 요청과 생성 대기 화면
기술 포인트:
- Server-Sent Events 기반 AI 답변 스트리밍
- contentEditable 기반 멘션 입력 처리
- localStorage를 활용한 단계 역행 방지
- sessionStorage 기반 return path 저장
- 비동기 생성 작업을 전역 pending store와 polling으로 추적
AI 인터뷰가 완료되면 사용자의 경험을 구조화한 텍스트 포트폴리오를 보여주는 기능입니다. 사용자는 포트폴리오 내용을 확인하고, 기여도를 수정하고, PDF로 내보낼 수 있습니다.
주요 구현 내용:
- 생성된 포트폴리오 상세 조회
- 활동별 상세 정보, 담당 업무, 문제 해결, 배운 점 렌더링
- 기여도 progress bar 드래그 수정
- 기여도 숫자 직접 입력
- 제목 수정과 삭제
- PDF 내보내기
기술 포인트:
- 화면 표시용 DOM과 PDF 출력용 DOM을 분리
- html2canvas와 jsPDF로 브라우저 기반 PDF 생성
- progress bar drag 이벤트를 window mouse event로 추적
- 임시 기여도와 저장 기여도를 분리해 불필요한 API 호출 방지
텍스트 포트폴리오를 기반으로 발표 자료 형태의 시각화 포트폴리오를 생성하는 기능입니다. 생성된 슬라이드는 미리보기로 확인하고 PDF 또는 PPTX로 내보낼 수 있습니다.
주요 구현 내용:
- 시각화 생성 옵션 선택
- 시각화 생성 job 요청
- 슬라이드 목록 조회
- 슬라이드별 생성 상태 표시
- 생성 실패 슬라이드 재시도
- 완성된 시각화 자료 PDF/PPTX 내보내기
- 키보드 기반 슬라이드 이동
기술 포인트:
- 생성 job과 slide 상태를 분리해 부분 완료/부분 실패 처리
- React Query polling으로 슬라이드 생성 상태 추적
- preview URL 응답 형태를 방어적으로 파싱
- export status polling으로 파일 생성 완료 후 signed URL open
사용자가 생성한 텍스트 포트폴리오 또는 직접 업로드한 PDF 포트폴리오를 지원 기업, 직무, Job Description에 맞춰 AI가 첨삭하는 기능입니다.
주요 구현 내용:
- 첨삭 목록 조회와 검색
- 새 첨삭 생성 전 이용권 확인
- 지원 기업명, 직무명, JD 입력
- 내부 텍스트 포트폴리오 선택
- PDF 포트폴리오 업로드
- PDF 텍스트 추출과 AI 구조화 결과 편집
- 기업 분석 정보 생성 및 수정
- AI 첨삭 생성 요청
- 첨삭 생성 중 상태 polling
- 첨삭 실패 시 재시도
- 지원 정보, 총평, 활동별 상세 첨삭 결과 렌더링
- 원문에서 축소/제외 대상과 강조 대상 하이라이트
기술 포인트:
- 서버 status를 UI step/status로 변환하는 상태머신형 화면 제어
- PDF drag and drop overlay
- PDF 구조화 결과를 activity/category/bullet UI state로 변환
- bullet editor에서 Enter, Backspace, ArrowUp, ArrowDown 키보드 편집 지원
- debounce PATCH로 PDF 편집 내용 저장
- Markdown 원문에 부분 하이라이트를 적용한 뒤 React Markdown으로 렌더링
서비스 사용을 위한 로그인, 이용권, 결제 내역, 피드백, 프로필 관련 화면을 제공합니다.
주요 구현 내용:
- 로그인 페이지와 OAuth callback 처리
- 인증 상태 기반 라우팅
- 이용권 안내 및 충전 화면
- 결제 내역 조회
- 환불 관련 화면
- 피드백 제출
- 프로필 관리
- 회원 탈퇴
- 약관, 개인정보 처리방침, 마케팅 페이지
- 모바일 제한 안내 화면
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| TypeScript | ^5 | 컴포넌트 props, API 모델, 상태 타입 안정성 확보 |
| JavaScript | ESNext | React/Next.js 런타임 코드 실행 |
| React | 19.2.0 | UI 컴포넌트 렌더링과 클라이언트 상태 관리 |
| React DOM | 19.2.0 | 브라우저 DOM 렌더링 |
| Node.js type definitions | ^20 | Next.js 설정과 개발 환경 타입 지원 |
| React type definitions | ^19 | React 컴포넌트와 hook 타입 지원 |
| React DOM type definitions | ^19 | React DOM 렌더링 API 타입 지원 |
| pnpm | 9.15.0 | 패키지 매니저 |
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| Next.js | ^16.1.3 | App Router 기반 페이지, layout, routing, build 구성 |
| Next.js App Router | Next.js 내장 | route group, dynamic route, nested layout 구성 |
| @next/third-parties | ^16.1.6 | Next.js 환경에서 외부 third-party script 연동 |
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| Tailwind CSS | ^4.1.17 | utility-first 스타일링 |
| @tailwindcss/postcss | ^4 | Tailwind CSS v4 PostCSS 연동 |
| @tailwindcss/typography | ^0.5.19 | Markdown/prose 콘텐츠 스타일링 |
| tailwindcss-animate | ^1.0.7 | accordion 등 Tailwind animation utility |
| tw-animate-css | ^1.4.0 | CSS animation utility |
| PostCSS | ^8.5.6 | CSS transform pipeline |
| Autoprefixer | ^10.4.22 | 브라우저 prefix 자동 처리 |
| Design tokens JSON | project local | 색상, 폰트, shadow, line-height, letter-spacing 토큰 관리 |
| Pretendard font stack | project local | 한국어 UI에 적합한 기본 서체 구성 |
Tailwind 설정은 src/styles/tokens.json의 디자인 토큰을 읽어 색상, 폰트, 폰트 크기, line-height, shadow를 theme으로 확장합니다. 디자인 값이 코드에 흩어지지 않도록 토큰 기반 스타일 구조를 사용했습니다.
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| TanStack React Query | ^5.90.21 | API 조회, cache, mutation, polling, invalidation |
| Axios | ^1.7.9 | HTTP client |
| Orval | ^8.4.2 | OpenAPI schema 기반 API hook/model 자동 생성 |
| Custom Axios mutator | project local | baseURL, token, credentials 등 공통 요청 설정 |
API는 OpenAPI 문서를 Orval로 생성하고, React Query client 방식으로 사용합니다. 생성된 endpoint hook과 model을 기반으로 서버 DTO 타입을 유지하고, 화면 요구사항이 다른 경우 service/mapper 계층에서 UI 모델로 변환합니다.
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| Zustand | ^5.0.10 | 인증 상태, 경험 생성 상태, 로그 폼 등 전역 클라이언트 상태 |
| React Hook Form | ^7.71.1 | 폼 입력 상태 관리 |
| @hookform/resolvers | ^5.2.2 | React Hook Form과 Zod resolver 연결 |
| Zod | ^4.3.6 | 입력값 schema 검증 |
서버에서 가져오는 데이터는 React Query에 두고, 브라우저에서만 필요한 UI 상태는 React state 또는 Zustand로 분리했습니다. 작성 중인 입력값, pending 생성 상태, 인증 복원 상태처럼 route 간 유지가 필요한 값은 store와 browser storage를 함께 사용합니다.
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| Radix Accordion | ^1.2.12 | 접근성 기반 accordion UI |
| Radix Checkbox | ^1.3.3 | 접근성 기반 checkbox UI |
| Radix Dialog | ^1.1.15 | modal/dialog UI |
| Radix Progress | ^1.1.8 | progress indicator |
| Radix Slot | ^1.2.4 | 합성 가능한 컴포넌트 API |
| Radix Tabs | ^1.1.13 | tab UI |
| Radix Toggle | ^1.1.10 | toggle button UI |
| Radix Toggle Group | ^1.1.11 | segmented control UI |
| lucide-react | ^0.554.0 | 아이콘 |
| class-variance-authority | ^0.7.1 | variant 기반 컴포넌트 class 구성 |
| clsx | ^2.1.1 | 조건부 className 조합 |
| tailwind-merge | ^3.4.0 | Tailwind class conflict 병합 |
| Framer Motion | ^12.27.5 | 채팅, 로딩, 패널, 전환 애니메이션 |
| TanStack React Table | ^8.21.3 | 테이블형 데이터 UI 구성 기반 |
공통 버튼, 입력, 모달, 탭, 카드 컴포넌트는 Radix primitive와 Tailwind utility를 조합해 구현했습니다. variant가 필요한 컴포넌트는 CVA, clsx, tailwind-merge를 사용해 className 충돌과 중복을 줄였습니다.
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| Tiptap React | ^3.20.1 | 기업 분석 정보 Markdown editor |
| Tiptap Starter Kit | ^3.20.1 | editor 기본 node/mark 구성 |
| Tiptap Placeholder | ^3.20.1 | editor placeholder |
| @tiptap/pm | ^3.20.1 | ProseMirror 기반 editor runtime |
| tiptap-markdown | ^0.9.0 | editor content와 Markdown 문자열 변환 |
| React Markdown | ^10.1.0 | AI 생성 텍스트와 첨삭 결과 Markdown 렌더링 |
| remark-gfm | ^4.0.1 | GitHub Flavored Markdown 지원 |
| remark-breaks | ^4.0.0 | 줄바꿈 유지 |
| rehype-raw | ^7.0.0 | Markdown 내부 HTML span 렌더링 |
| react-syntax-highlighter | ^16.1.1 | 코드 블록 syntax highlight 기반 |
AI가 생성한 기업 분석, 포트폴리오 본문, 첨삭 결과는 Markdown 형태로 다뤘습니다. 편집이 필요한 기업 분석에는 Tiptap editor를 사용하고, 결과 렌더링에는 React Markdown과 remark/rehype 플러그인을 조합했습니다.
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| html2canvas | ^1.4.1 | 포트폴리오 DOM을 canvas로 변환 |
| jsPDF | ^4.0.0 | 브라우저에서 PDF 파일 생성 |
| Browser File API | Web API | PDF 업로드, 파일 크기/타입 검증 |
| Drag and Drop API | Web API | PDF drag overlay와 drop 처리 |
텍스트 포트폴리오 PDF 내보내기는 화면 DOM과 출력 DOM을 분리한 뒤 html2canvas로 이미지화하고 jsPDF로 저장합니다. 첨삭용 PDF 업로드는 파일 타입, 용량, 개수 제한을 프론트에서 먼저 검사하고, 서버의 구조화 결과를 편집 가능한 bullet UI로 변환합니다.
| 기술 | 버전 | 사용 목적 |
|---|---|---|
| ESLint | ^9 | 정적 분석 |
| eslint-config-next | 16.0.3 | Next.js 권장 lint 설정 |
| eslint-plugin-storybook | ^10.2.19 | Storybook 관련 lint 규칙 |
| Prettier | ^3.6.2 | 코드 포맷팅 |
| prettier-plugin-tailwindcss | ^0.7.1 | Tailwind class 정렬 |
| Storybook | ^10.2.19 | UI 컴포넌트 문서화와 독립 실행 |
| @storybook/nextjs-vite | ^10.2.19 | Next.js + Vite 기반 Storybook 실행 |
| @storybook/addon-a11y | ^10.2.19 | 접근성 점검 |
| @storybook/addon-docs | ^10.2.19 | 컴포넌트 문서 생성 |
| @storybook/addon-onboarding | ^10.2.19 | Storybook 온보딩 |
| @storybook/addon-vitest | ^10.2.19 | Storybook과 Vitest 연동 |
| Chromatic | ^16.0.0 | Storybook 기반 시각 회귀 테스트와 배포 |
| @chromatic-com/storybook | ^5.0.1 | Chromatic Storybook 연동 |
| Vitest | ^4.1.0 | 테스트 러너 |
| @vitest/browser-playwright | ^4.1.0 | 브라우저 환경 테스트 |
| @vitest/coverage-v8 | ^4.1.0 | 테스트 커버리지 |
| Playwright | ^1.58.2 | 브라우저 자동화 기반 테스트 |
| Vite | ^8.0.0 | Storybook/Vitest 개발 도구 기반 |
| script | 설명 |
|---|---|
pnpm dev |
Next.js 개발 서버 실행 |
pnpm build |
Next.js production build |
pnpm start |
production server 실행 |
pnpm lint |
ESLint 실행 |
pnpm api:generate |
Orval 기반 API 코드 생성 |
pnpm api:gen |
Orval 기반 API 코드 생성 alias |
pnpm storybook |
Storybook 개발 서버 실행 |
pnpm build-storybook |
Storybook 정적 빌드 |
pnpm chromatic |
Chromatic 업로드 |
src
app Next.js App Router 페이지와 layout
api Orval이 생성한 endpoint hook과 model
components 공통 UI 컴포넌트
constants 공통 상수
contexts layout 또는 route 간 공유 context
features 도메인별 기능 구현
lib axios, query client, SSE stream 등 기반 코드
services API DTO와 UI 모델 사이의 mapper/service
store Zustand 전역 store
styles global CSS와 design tokens
utils device, 날짜, 문자열 등 유틸 함수도메인 기능은 features 아래에 인사이트 로그, 경험 정리, 포트폴리오 첨삭, 피드백, 결제 내역, 로그인 등으로 분리했습니다. 라우트 파일은 app에 두고, 실제 화면 상태와 비즈니스 로직은 feature hook/component로 분리해 페이지 컴포넌트가 과도하게 커지지 않도록 구성했습니다.
서버 응답을 화면에서 그대로 사용하지 않고, 화면에 맞는 모델로 변환했습니다.
예시:
- 인사이트 로그 DTO를 카드 UI 데이터로 변환
- PDF 구조화 결과를 activity/category/bullet 편집 모델로 변환
- 직군 enum을 사용자에게 보이는 한국어 label로 변환
- 외부 PDF 포트폴리오 DTO를 내부 포트폴리오 결과 UI와 호환되는 형태로 보정
이 방식으로 서버 API 구조가 화면 구조와 달라도 컴포넌트 복잡도를 낮췄습니다.
AI 기반 기능은 즉시 결과가 나오지 않기 때문에 작업 상태를 계속 추적해야 합니다.
적용한 방식:
- AI 채팅 답변은 SSE 스트리밍으로 수신
- 포트폴리오 생성, 시각화 생성, PDF 추출, 기업 분석, 첨삭 생성은 polling으로 상태 추적
- 생성 중 페이지를 이탈해도 재진입 시 서버 status를 조회해 화면 복원
- 실패 상태에서는 재시도 버튼과 fallback UI 제공
경험 정리와 포트폴리오 첨삭은 URL만으로 화면을 결정하지 않습니다. 서버 status를 조회한 뒤 프론트에서 사용하는 step/status로 변환해 현재 보여줄 화면을 결정합니다.
예시:
NOT_STARTED → 포트폴리오 선택
COMPANY_INSIGHT → 기업 분석 입력
DOING_RAG → 첨삭 생성 중
GENERATING → 첨삭 생성 중
DONE → 첨삭 결과
FAILED → 실패/재시도이 구조 덕분에 새로고침, 뒤로가기, 직접 URL 접근, 생성 중 이탈 후 재진입 상황에서도 현재 작업 상태에 맞는 화면을 복원할 수 있습니다.
긴 플로우에서 사용자가 입력한 값이 유실되지 않도록 여러 방어 로직을 적용했습니다.
- 비로그인 작성 중 로그인 전환 시 draft 보존
- AI 채팅 단계 localStorage 저장으로 단계 역행 방지
- 검색어 debounce로 과도한 API 호출 방지
- PDF bullet 입력 debounce PATCH로 서버 저장 요청 최적화
- Tiptap editor 외부 value 동기화 시 현재 markdown과 비교 후 필요한 경우에만 setContent
AI 생성 결과는 Markdown 기반으로 렌더링합니다. 첨삭 결과에서는 원문 일부에 하이라이트를 적용해야 하므로, line item의 originalText를 찾아 span으로 감싸고 React Markdown에서 렌더링합니다.
처리 방식:
원문 Markdown
→ 첨삭 line item을 originalText 길이 내림차순 정렬
→ originalText 정규식 escape
→ reduce/emphasize 타입에 따라 span class 적용
→ React Markdown + remark/rehype plugin으로 렌더링긴 문장부터 치환해 짧은 문장이 긴 문장의 일부를 먼저 변경하는 문제를 줄였습니다.