diff --git a/docs/superpowers/specs/2026-04-27-portfolio-design.md b/docs/superpowers/specs/2026-04-27-portfolio-design.md new file mode 100644 index 0000000..d4989bb --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-portfolio-design.md @@ -0,0 +1,355 @@ +# 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 내보내기" 대신 "공유" 또는 브라우저 인쇄 기능 활용