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

11 KiB
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 + 메인 자기소개)

응답 형태:

{
  "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 추가

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 내보내기" 대신 "공유" 또는 브라우저 인쇄 기능 활용