# Portfolio Service Design Spec > 개인 포트폴리오 정식 서비��. 취업/이직용 이력서 + 개인 브랜딩 쇼케이스 겸용. --- ## 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 | 직함 (영문) | | email | 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 + 메인 자기소개) | 응답 형태: ```json { "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: }` ### 파일 구조 ``` 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/public` fetch - 성공 시: 이름, 역할, 바이오, 기술태그 상위 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 추가 ```yaml 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 추가 ```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 내보내기" 대신 "공유" 또는 브라우저 인쇄 기능 활용