Files
web-page-backend/docs/superpowers/specs/2026-04-27-personal-service-migration-design.md
gahusb e3d5eaf6f3 refactor: portfolio → personal 리네이밍 + Blog/Todo 통합
- portfolio/ 디렉토리를 personal/로 리네이밍
- lotto-backend의 Blog/Todo 라우트·CRUD를 personal 서비스로 이전
- lotto-backend에서 Blog/Todo 코드 제거 (DB 테이블 스키마는 유지)
- nginx: /api/todos, /api/blog/ 라우팅을 personal로 추가
- docker-compose: portfolio → personal 서비스 변��
- deploy 스크립트: portfolio → personal 반영

데이터 마이그레이션은 배포 후 NAS에서 별도 수행 필요:
1. cp data/portfolio/portfolio.db data/personal/personal.db
2. sqlite3 data/lotto.db ".dump todos" | sqlite3 data/personal/personal.db
3. sqlite3 data/lotto.db ".dump blog_posts" | sqlite3 data/personal/personal.db

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

221 lines
7.6 KiB
Markdown

# Personal 서비스 마이그레이션 설계
## 개요
기존 `portfolio` 서비스를 `personal`로 리네이밍하고, lotto-backend에 있던 Blog/Todo 기능을 personal 서비스로 통합한다.
**목표**: 신규 컨테이너 없이, 개인 콘텐츠(포트폴리오 + 블로그 + 투두)를 하나의 서비스로 통합
**제약**: 기존 데이터 무손실 이전 필수
---
## 아키텍처
### 변경 전
```
lotto-backend (lotto.db)
├── 로또 API (/api/lotto/*)
├── 블로그 API (/api/blog/posts) ← 이전 대상
└── 투두 API (/api/todos) ← 이전 대상
portfolio (portfolio.db)
└── 포트폴리오 API (/api/profile/*)
```
### 변경 후
```
lotto-backend (lotto.db)
└── 로또 API (/api/lotto/*) ← Blog/Todo 라우트 제거
personal (personal.db)
├── 포트폴리오 API (/api/profile/*)
├── 블로그 API (/api/blog/posts) ← 통합
└── 투두 API (/api/todos) ← 통합
```
---
## 서비스 속성
| 항목 | 현재 (portfolio) | 변경 후 (personal) |
|------|-----------------|-------------------|
| 디렉토리 | `portfolio/` | `personal/` |
| 컨테이너명 | `portfolio` | `personal` |
| 포트 | 18850 | 18850 (유지) |
| DB 파일 | `data/portfolio/portfolio.db` | `data/personal/personal.db` |
| API prefix | `/api/profile/` | `/api/profile/` + `/api/todos` + `/api/blog/` |
---
## DB 스키마
personal.db에 기존 5테이블 + 신규 2테이블:
### 기존 테이블 (portfolio에서 이관)
- `profile` — 프로필 (id=1 싱글턴)
- `careers` — 경력
- `projects` — 프로젝트
- `skills` — 기술스택
- `introductions` — 자기소개
### 신규 추가 테이블 (lotto-backend에서 이관)
```sql
CREATE TABLE IF NOT EXISTS todos (
id TEXT PRIMARY KEY
DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2)))),
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'todo'
CHECK(status IN ('todo','in_progress','done')),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_todos_created ON todos(created_at DESC);
CREATE TABLE IF NOT EXISTS blog_posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '',
excerpt TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '[]',
date TEXT NOT NULL DEFAULT (date('now','localtime')),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_blog_date ON blog_posts(date DESC);
```
---
## API 엔드포인트 (personal 서비스 전체)
### 포트폴리오 (기존 유지)
| 메서드 | 경로 | 인증 | 설명 |
|--------|------|------|------|
| GET | `/api/profile/public` | - | 공개 데이터 일괄 조회 |
| POST | `/api/profile/auth` | - | 비밀번호 인증 → 토큰 |
| GET/PUT | `/api/profile/profile` | Bearer | 프로필 조회/수정 |
| GET/POST | `/api/profile/careers` | Bearer | 경력 목록/추가 |
| PUT/DELETE | `/api/profile/careers/{id}` | Bearer | 경력 수정/삭제 |
| GET/POST | `/api/profile/projects` | Bearer | 프로젝트 목록/추가 |
| PUT/DELETE | `/api/profile/projects/{id}` | Bearer | 프로젝트 수정/삭제 |
| GET/POST | `/api/profile/skills` | Bearer | 기술 목록/추가 |
| PUT/DELETE | `/api/profile/skills/{id}` | Bearer | 기술 수정/삭제 |
| GET/POST | `/api/profile/introductions` | Bearer | 자기소개 목록/추가 |
| PUT/DELETE | `/api/profile/introductions/{id}` | Bearer | 자기소개 수정/삭제 |
| PATCH | `/api/profile/introductions/{id}/main` | Bearer | 메인 자기소개 지정 |
### 투두 (lotto-backend에서 이전, 인증 없음)
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | `/api/todos` | 전체 목록 |
| POST | `/api/todos` | 생성 |
| DELETE | `/api/todos/done` | 완료 일괄 삭제 |
| PUT | `/api/todos/{id}` | 수정 |
| DELETE | `/api/todos/{id}` | 삭제 |
### 블로그 (lotto-backend에서 이전, 인증 없음)
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | `/api/blog/posts` | 목록 (`{"posts": [...]}`) |
| POST | `/api/blog/posts` | 생성 |
| PUT | `/api/blog/posts/{id}` | 수정 |
| DELETE | `/api/blog/posts/{id}` | 삭제 |
---
## Nginx 라우팅 변경
```nginx
# 추가: /api/todos → personal
location /api/todos {
resolver 127.0.0.11 valid=10s;
set $personal_backend personal:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://$personal_backend$request_uri;
}
# 추가: /api/blog/ → personal
location /api/blog/ {
resolver 127.0.0.11 valid=10s;
set $personal_backend personal:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://$personal_backend$request_uri;
}
# 변경: portfolio → personal
location /api/profile/ {
resolver 127.0.0.11 valid=10s;
set $personal_backend personal:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://$personal_backend$request_uri;
}
```
기존 `/api/` catch-all은 lotto-backend로 유지 (todos/blog 요청은 위의 더 구체적인 location에서 먼저 매칭).
---
## 인프라 변경
### docker-compose.yml
- `portfolio` 서비스 → `personal`로 리네이밍
- 볼륨: `${RUNTIME_PATH}/data/personal:/app/data`
- 환경변수 동일 (PORTFOLIO_EDIT_PASSWORD 등)
### deploy.sh / deploy-nas.sh
- SERVICES, BUILD_TARGETS, CONTAINER_NAMES 등에서 `portfolio``personal` 변경
- DATA_DIRS에서 `portfolio``personal` 변경
### lotto-backend 정리
- `main.py`에서 Blog/Todo 라우트 + Pydantic 모델 제거 (약 100줄)
- `db.py`에서 Blog/Todo CRUD 함수 제거 (약 130줄)
- `db.py``init_db()`에서 todos/blog_posts 테이블 생성 코드는 유지 (기존 DB 호환)
---
## 배포 순서 (안전 우선)
1. **코드 개발** — personal 서비스 + lotto-backend 정리 + 인프라 변경
2. **git push** — 자동 배포 트리거
3. **NAS에서 데이터 디렉토리 준비**`mkdir -p data/personal`
4. **기존 portfolio.db 이동**`cp data/portfolio/portfolio.db data/personal/personal.db`
5. **lotto.db에서 Blog/Todo 데이터 복사**:
```bash
sqlite3 data/lotto.db ".dump todos" | sqlite3 data/personal/personal.db
sqlite3 data/lotto.db ".dump blog_posts" | sqlite3 data/personal/personal.db
```
6. **컨테이너 재시작** — `docker compose restart personal`
7. **검증** — API 호출로 데이터 건수 대조
8. **lotto.db 원본 테이블** — 삭제하지 않고 당분간 유지
---
## 프론트엔드
변경 없음. 모든 API 호출이 상대경로(`/api/todos`, `/api/blog/posts`, `/api/profile/`)이므로 nginx 라우팅 변경만으로 자동 적용.
---
## 리스크
- **낮음**: Blog/Todo는 lotto 테이블과 FK/공유 쿼리 없음
- **롤백**: lotto.db 원본 테이블 유지 + nginx 라우팅 원복으로 즉시 롤백 가능
- **다운타임**: nginx reload 순간 (~1초)