백엔드(portfolio 서비스 18850) + 프론트(/portfolio 페이지) 전체 설계. 프로필·경력·프로젝트·기술·자기소개(다중버전) CRUD + 비밀번호 인증 + PDF 내보내기. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
356 lines
11 KiB
Markdown
356 lines
11 KiB
Markdown
# 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 내보내기" 대신 "공유" 또는 브라우저 인쇄 기능 활용
|