Files
web-page-backend/docs/superpowers/specs/2026-04-27-portfolio-design.md
gahusb bb97aa3ec8 docs: portfolio 서비스 설계 스�� 문서
백엔드(portfolio 서비스 18850) + 프론트(/portfolio 페이지) 전체 설계.
프로필·경력·프로젝트·기술·자기소개(다중버전) CRUD + 비밀번호 인증 + PDF 내보내기.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 14:20:33 +09:00

356 lines
11 KiB
Markdown
Raw Blame History

# 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 | 직함 (영문) |
| 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: <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/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 내보내기" 대신 "공유" 또는 브라우저 인쇄 기능 활용