백엔드(portfolio 서비스 18850) + 프론트(/portfolio 페이지) 전체 설계. 프로필·경력·프로젝트·기술·자기소개(다중버전) CRUD + 비밀번호 인증 + PDF 내보내기. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
11 KiB
Portfolio Service Design Spec
개인 포트폴리오 정식 서비<EC849C><EBB984>. 취업/이직용 이력서 + 개인 브랜딩 쇼케이스 겸용.
1. 서비스 개요
| 항목 | 값 |
|---|---|
| 서비스명 | portfolio |
| 경로 | web-backend/portfolio/ |
| 컨테이너 | portfolio |
| 내부 포트 | 8000 |
| 외부 포트 | 18850 |
| DB | /app/data/portfolio.db (SQLite) |
| Nginx 프록시 | /api/portfolio/ → portfolio:8000 |
| 프레임워크 | FastAPI (Python 3.12) |
| 프론트 경로 | /portfolio |
목적
- 프로필, 경력, 프로젝트, 기술스택을 웹에서 관리하고 공개 전시
- 자기소개 글을 다중 버전으로 관리 (메인 1개 지정, 클립보드 복사)
- 이력서 PDF 내보내기
- 홈 페이지에 요약 카드로 연동
2. DB 스키마
profile (1행, upsert)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | INTEGER PK | 항상 1 |
| name | TEXT | 이름 (한글) |
| name_en | TEXT | 이름 (영문) |
| role | TEXT | 직함 (한글) |
| role_en | TEXT | 직함 (영문) |
| TEXT | 이메일 | |
| phone | TEXT | 전화번호 |
| github_url | TEXT | GitHub URL |
| blog_url | TEXT | 블로그 URL |
| photo_url | TEXT | 프로필 사진 URL |
| bio | TEXT | 간단 소개 (3줄 정도) |
| updated_at | TEXT | ISO8601 |
careers (경력 이력)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | INTEGER PK AUTOINCREMENT | |
| category | TEXT | company | education | etc |
| organization | TEXT | 회사/기관명 |
| role | TEXT | 직함/전공 |
| description | TEXT | 설명 |
| start_date | TEXT | YYYY-MM |
| end_date | TEXT | YYYY-MM 또는 빈 문자열(현재) |
| sort_order | INTEGER | 정렬 순서 (낮을수록 위) |
| created_at | TEXT | ISO8601 |
| updated_at | TEXT | ISO8601 |
projects (프로젝트)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | INTEGER PK AUTOINCREMENT | |
| category | TEXT | company | personal | academy |
| title | TEXT | 프로젝트명 |
| description | TEXT | 설명 |
| tech_stack | TEXT | JSON 배열 ["Python", "FastAPI", ...] |
| role | TEXT | 담당 역할 |
| start_date | TEXT | YYYY-MM |
| end_date | TEXT | YYYY-MM 또는 빈 문자열 |
| url | TEXT | 프로젝트 URL (선택) |
| image_url | TEXT | 대표 이미지 URL (선택) |
| sort_order | INTEGER | 정렬 순서 |
| created_at | TEXT | ISO8601 |
| updated_at | TEXT | ISO8601 |
skills (기술 스택)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | INTEGER PK AUTOINCREMENT | |
| category | TEXT | language | framework | infra | tool |
| name | TEXT | 기술명 |
| level | INTEGER | 숙련도 1~5 |
| sort_order | INTEGER | 정렬 순서 |
introductions (자기소개 글)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | INTEGER PK AUTOINCREMENT | |
| title | TEXT | 버전명 (예: "이직용 짧은 버전") |
| content | TEXT | 본문 |
| is_main | INTEGER | 0 | 1 (메인 자기소개 지정, 항상 1개만 1) |
| created_at | TEXT | ISO8601 |
| updated_at | TEXT | ISO8601 |
3. API 설계
공개 API (인증 불필요)
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/portfolio/public |
전체 공개 데이터 일괄 조회 (profile + careers + projects + skills + 메인 자기소개) |
응답 형태:
{
"profile": { ... },
"careers": [ ... ],
"projects": [ ... ],
"skills": [ ... ],
"main_introduction": { "id": 1, "title": "...", "content": "..." }
}
인증 API
| 메서드 | 경로 | 설명 |
|---|---|---|
| POST | /api/portfolio/auth |
비밀번호 검증 → 세션 토큰 반환 |
- 요청:
{ "password": "..." } - 응답:
{ "token": "uuid-string", "expires_in": 86400 } - 환경변수:
PORTFOLIO_EDIT_PASSWORD - 토큰: UUID, 서버 메모리 딕셔너리 저장, 24시간 TTL
- 실패: 401
편집 API (Authorization: Bearer {token} 필요)
Profile:
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/portfolio/profile |
프로필 조회 |
| PUT | /api/portfolio/profile |
프로필 수정 (upsert) |
Careers:
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/portfolio/careers |
경력 목록 |
| POST | /api/portfolio/careers |
경력 추가 |
| PUT | /api/portfolio/careers/{id} |
경력 수정 |
| DELETE | /api/portfolio/careers/{id} |
경력 삭제 |
Projects:
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/portfolio/projects |
프로젝트 목록 |
| POST | /api/portfolio/projects |
프로젝트 추가 |
| PUT | /api/portfolio/projects/{id} |
프로젝트 수정 |
| DELETE | /api/portfolio/projects/{id} |
프로젝트 삭제 |
Skills:
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/portfolio/skills |
기술 목록 |
| POST | /api/portfolio/skills |
기술 추가 |
| PUT | /api/portfolio/skills/{id} |
기술 수정 |
| DELETE | /api/portfolio/skills/{id} |
기술 삭제 |
Introductions:
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/portfolio/introductions |
자기소개 전체 목록 |
| POST | /api/portfolio/introductions |
자기소개 추가 |
| PUT | /api/portfolio/introductions/{id} |
자기소개 수정 |
| DELETE | /api/portfolio/introductions/{id} |
자기소개 삭제 |
| PATCH | /api/portfolio/introductions/{id}/main |
메인 자기소개 지정 (기존 is_main=1 → 0 리셋) |
4. 인증 흐름
편집 버튼 클릭
→ 토큰 없음 → 비밀번호 모달 표시
→ POST /api/portfolio/auth { password }
→ 성공: 토큰을 React state에 저장 (새로고침 시 재인증)
→ 이후 편집 API 호출에 Authorization: Bearer {token} 포함
→ 토큰 만료/불일치 시 401 → 재인증 모달
서버 측:
_auth_tokens: dict[str, float]메모리 딕셔너리 (token → expiry timestamp)- FastAPI Depends로 토큰 검증 미들웨어
- 서버 재시작 시 토큰 소멸 (재인증 필요, 보안상 적절)
5. 프론트엔드 구조
라우팅
routes.jsx에 추가:
- navLink:
{ id: 'portfolio', label: 'Portfolio', path: '/portfolio', subtitle: 'RESUME', accent: '#06b6d4' } - appRoute:
{ path: 'portfolio', element: <Portfolio /> }
파일 구조
src/pages/portfolio/
Portfolio.jsx — 메인 페이지 (3탭 컨테이너)
Portfolio.css — 스타일
ProfileTab.jsx — 탭 1: 프로필 & 이력 & 기술스택
ProjectTab.jsx — 탭 2: 프로젝트
IntroTab.jsx — 탭 3: 자기소개 관리
usePortfolio.js — API 호출 + 인증 상태 관리 훅
PasswordModal.jsx — 비밀번호 입력 모달
ResumeView.jsx — PDF 출력 전용 레이아웃 (print CSS)
탭 1: 프로필 & 이력
보기 모드:
- 프로필 카드 (사진, 이름, 역할, 바이오, 연락처 아이콘 링크)
- 경력 타임라인 (category별 그룹: 회사 → 교육 → 기타, sort_order 순)
- 기술 스택 (category별 그룹, level 바 표시)
- "이력서 PDF 내보내기" 버튼
편집 모드:
- 프로필: 인라인 편집 (input/textarea)
- 경력: 추가/편집/삭제/순서 변경
- 기술: 추가/편집/삭제/순서 변경
탭 2: 프로젝트
보기 모드:
- 카테고리 필터 버튼 (전체 / 회사 / 개인 / 아카데미)
- 프로젝트 카드 그리드: 제목, 설명(2줄 clamp), 기술스택 태그, 기간, 링크 아이콘
편집 모드:
- 프로젝트 추가/편집/삭제 폼
- tech_stack: 태그 입력 UI (쉼표 또는 엔터로 추가)
탭 3: 자기소개 관리
- 자기소개 글 리스트 (메인 표시: 별 배지)
- 각 항목: 제목, 미리보기(3줄), 수정일
- 액션 버튼: 복사(클립보드) / 편집 / 메인 지정 / 삭제
- 상단: "새 글 작성" 버튼 → 인라인 폼 또는 MobileSheet
- 복사 버튼:
navigator.clipboard.writeText()→ "복사됨!" 피드백 1.5초
편집 모드 진입
- 각 탭 우상단 "편집" 토글 버튼
- 첫 클릭 시 PasswordModal 표시 → 인증 성공 → 편집 UI 노출
- 인증 토큰은 usePortfolio 훅에서 관리 (React state, 새로고침 시 소멸)
6. 홈 페이지 연동
변경 내용
현재 Home.jsx Profile 섹션(하드코딩)을 요약 카드로 교체:
GET /api/portfolio/publicfetch- 성공 시: 이름, 역할, 바이오, 기술태그 상위 8개, 대표 프로젝트 3개 카드
- "포트폴리오 보기 →" 링크 버튼
- 실패 시: 기존 하드코딩 프로필 폴백 (서비스 미가동 대응)
7. PDF 내보내기
방식
window.print() + @media print 전용 CSS
- ResumeView.jsx: 이력서 레이아웃 전용 컴포넌트
- "PDF 내보내기" 버튼 → ResumeView를 화면에 렌더링 →
window.print()→ 숨김 - 프린트 CSS: 네비/탭/편집버튼 숨기고, A4 1~2페이지 레이아웃 렌더링
이력서 레이아웃 (A4)
┌──────────────────────────────┐
│ [사진] 박재오 │
│ Server Developer │
│ email | github │
├──────────────────────────────┤
│ ABOUT │
│ (메인 자기소개 또는 bio) │
├──────────────────────────────┤
│ EXPERIENCE │
│ - 현대오토에버 (2023~현재) │
│ - 롯데정보통신 (2020~2023) │
│ - SSAFY 1기 (2019) │
├──────────────────────────────┤
│ PROJECTS │
│ - 프로젝트 카드 목록 │
├──────────────────────────────┤
│ SKILLS │
│ [태그 나열] │
└──────────────────────────────┘
8. Docker / Nginx 변경
docker-compose.yml 추가
portfolio:
build: ./portfolio
container_name: portfolio
restart: unless-stopped
volumes:
- ${RUNTIME_PATH:-.}/data:/app/data
environment:
- PORTFOLIO_EDIT_PASSWORD=${PORTFOLIO_EDIT_PASSWORD}
ports:
- "18850:8000"
Nginx 추가
location /api/portfolio/ {
proxy_pass http://portfolio:8000/api/portfolio/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
9. Backlog (향후)
- Blog CRUD (
/api/blog/posts) → portfolio 서비스로 이전 - Todo CRUD (
/api/todos) → portfolio 서비스로 이전 - 이전 완료 후 lotto-backend에서 해당 테이블/라우트 제거
- Nginx 라우팅 변경 (
/api/blog/,/api/todos→ portfolio)
10. 모바일 대응
- 기존 프로젝트 패턴 그대로:
useIsMobile()+ SwipeableView 3탭 - 편집 모드: MobileSheet 활용
- 자기소개 복사: 모바일에서도
navigator.clipboard동작 - PDF: 모바일에서는 "PDF 내보내기" 대신 "공유" 또는 브라우저 인쇄 기능 활용