# Quest Game UI Overlay — 설계 (Week 2 추가 결정)
> 작성: 박동제 (pkdje) · 2026-06-02 · Week 2 시작 시점에 추가된 결정
## 1. 동기
기존 [01-project-plan.md](01-project-plan.md) 의 DAG 시각화는 "노드 + 엣지 + 상태별 색" 의 깔끔한 todo 톤이었다. 사용자가 핵심 메커니즘(선행 작업 완료 → 후속 작업 해금)을 **게임 퀘스트 진행 느낌으로 재해석**하고 싶다고 결정함. 동시에 기존 모델이 다루지 않던 "선후관계가 없는 독립 태스크"의 분류도 명확히 한다.
이 문서는 그 결정을 구체화한다. 데이터 모델은 거의 그대로 두고, **시각/인터랙션 레이어를 퀘스트 톤으로 통일**한다.
## 2. 합의된 결정 (요약)
| 항목 | 결정 |
|---|---|
| 게이미피케이션 강도 | **C — 중간 (퀘스트 카드 톤)**. 카드 디자인 + 라벨(REWARD, LOCKED, CLEARED) + 해금 애니메이션 + QUEST LINE 진행률. |
| 독립 태스크 분류 | **B + D**. 좌측 패널 하단에 별도 "사이드 퀘스트" 섹션. 새 태스크 생성 시 prerequisite 비우면 자동 분류. |
| 퀘스트 라인 정의 | DAG 의 **연결 컴포넌트** = 1 퀘스트 라인. 크기 1 = 사이드 퀘스트. |
| REWARD 텍스트 | **DAG 구조에서 자동 생성** (직속 후속 노드 기반). 사용자 입력 없음. |
| 아이콘 | **선택 emoji 필드** (`icon?: string`). 기본 ⚡. 새 태스크/편집 시 picker. |
| 추천 패널 통합 | **A — 추천 패널을 "▶ NEXT QUEST" 로 리스킨**. 전체 UI 톤 통일. |
| 스코프 | **Week 3 안에 끼움**. #16(기존 노드 polish) 흡수 + 신규 #21–#23 추가. |
## 3. 데이터 모델 변경
```ts
type Task = {
// ... 기존 필드 그대로
icon?: string // 추가. 이모지 1자. 미지정 시 렌더 시 ⚡ fallback.
}
```
- 다른 필드는 변동 없음.
- 모든 퀘스트 라인/REWARD 정보는 **저장하지 않고** DAG 구조에서 파생 계산.
- 마이그레이션 불필요 (기존 task 들은 icon 없이도 ⚡ 로 렌더).
## 4. 파생 계산 (`lib/quest.ts`)
모두 순수 함수. TDD 대상.
```ts
// 4.1 연결 컴포넌트 (Union-Find 또는 BFS)
function connectedComponents(tasks: Task[]): Component[]
// Component { id: string, taskIds: TaskId[] }
// 사이즈 1 = 사이드 퀘스트.
// 4.2 컴포넌트 진척도
function questLineProgress(componentId, components, tasks): { done: number, total: number }
// 4.3 REWARD 텍스트 자동 생성
function rewardText(task: Task, tasks: Task[]): string
// 규칙:
// 직속 후속 1개 → "{이름} 해금"
// 직속 후속 2~3개 → "{이름1}, {이름2} 해금"
// 직속 후속 4개+ → "{N}개 작업 해금"
// 후속 없음 + 컴포넌트 크기 > 1 → "메인 퀘스트 X/Y 완료"
// 후속 없음 + 컴포넌트 크기 == 1 → "사이드 퀘스트 완료"
// 4.4 해금 감지 (애니메이션 트리거)
function detectUnlocks(prev: Task[], next: Task[]): TaskId[]
// 이전에 막힘(prereq 미완), 지금은 ready 인 task 들.
// 다음 렌더 사이클에서 0.9초 펄스 애니메이션.
```
복잡도: 모두 O(V+E). 수백 노드 기준 즉시 끝남.
## 5. UI 변경
### 5.1 DAG 캔버스 — 퀘스트 카드 노드
React Flow 의 [Custom Node](https://reactflow.dev/docs/api/nodes/custom-nodes/) 로 구현. 기존 단순 박스 노드 교체.
```
┌─────────────────────────┐
│ 🎨 priority ★★★★★ │ 아이콘 + 우선순위
│ 디자인 │ 제목 (굵게)
│ D-3 · score 7.4 │ 마감 + (추천 후보일 때) 점수
│ ───────────────── │
│ ▷ REWARD │
│ 프론트, 백엔드 해금 │ 자동 생성
└─────────────────────────┘
```
상태별 변형:
| 상태 | 시각 |
|---|---|
| `todo` (ready) | 흰 배경, 기본 테두리 |
| `todo` (locked — prereq 미완) | 회색 배경, opacity 0.6, 좌상단 `🔒 LOCKED` 배지 |
| `in_progress` | 파란 톤 |
| `done` | 초록 톤, 우상단 `✓ CLEARED` 배지 |
| just unlocked (1 사이클) | 0.9 초 scale+rotate+노란 강조 펄스 |
### 5.2 좌측 패널 — "퀘스트 로그"
기존 추천 패널을 NEXT QUEST 로 리스킨. 그 아래 SIDE QUESTS 섹션 추가. 맨 아래 진행 현황.
```
┌────────────────────────┐
│ ▶ NEXT QUEST (Top 3) │
│ ┌──────────────────┐ │
│ │ 🎨 디자인 score 8.2│ │ ← 카드 톤 통일
│ │ ★★★★★ │ │
│ │ ▷ 프론트, 통합 해금 │ │
│ └──────────────────┘ │
│ ... │
│ │
│ ⚡ SIDE QUESTS (5) │ ← 신규
│ ┌──────────────────┐ │
│ │ ⚡ 우유 사기 │ │ ← 단일 노드 컴포넌트
│ └──────────────────┘ │
│ ... │
│ │
│ ─ 진행 현황 ─ │
│ 메인 퀘스트 4/12 │
│ 사이드 2/5 │
└────────────────────────┘
```
NEXT QUEST 카드:
- 기존 추천 점수 + 별 + 핀/snooze 액션 (#15) 그대로 유지
- 시각만 퀘스트 카드 톤
- 클릭 → React Flow 캔버스에서 해당 노드로 카메라 이동 + 강조
SIDE QUESTS 카드:
- icon + 제목만 (점수/별 표시 안 함, 짧은 가로 카드)
- 클릭 → 캔버스에서 해당 노드 강조
진행 현황 박스:
- 메인 퀘스트: 사이즈 ≥ 2 인 모든 컴포넌트의 done/total 합산
- 사이드: 사이즈 == 1 인 컴포넌트의 done/total 합산
### 5.3 모달 / 드로어
- **새 태스크 모달**: 제목 입력 위에 이모지 picker 1줄. 추천 8 ~ 12 개 (`⚡ 🎨 💻 🔬 📝 🏃 🎯 🛠 🎁 📞 🍴 🛒`) + "다른 이모지" 입력. 기본 ⚡.
- **편집 드로어**: 같은 picker 추가.
- **prerequisite 비어있는 상태로 저장** → 자동으로 사이드 퀘스트 분류 (별도 토글 없음).
### 5.4 기존 인터랙션 보존
- 노드 클릭 → 편집 드로어
- 노드 더블클릭 → 상태 순환
- 노드 끝 드래그 → 의존성 엣지 생성
## 6. 스코프 변경
### Week 2 — 최소 영향
- **#6 Task CRUD API**: `icon?: string` 필드 추가 (수용/저장만, 검증 불필요).
### Week 3 — 6 → 8 이슈
| 이슈 | 변경 |
|---|---|
| #15 추천 핀 / Snooze | 그대로 |
| ~~#16 노드 시각 다듬기~~ | **삭제, #22 에 흡수** |
| #17 가중치 슬라이더 | 그대로 |
| #18 빈/로딩/에러 UI | 그대로 |
| #19 배포 | 그대로 |
| #20 회고 + 워크플로우 doc | 그대로 |
| **#21 lib/quest.ts + 단위 테스트** (신규) | 파생 함수 4개. TDD. ≈ 1일. |
| **#22 퀘스트 카드 노드 (React Flow custom node)** (신규) | 카드 디자인 + 상태별 변형 + 해금 펄스 애니메이션. ★/D-n/막힘 흐림 흡수. ≈ 1.5일. |
| **#23 좌측 패널 리스킨 + 사이드 퀘스트 + 이모지 picker** (신규) | NEXT QUEST 리스킨, SIDE QUESTS 섹션, 진행 현황, 모달/드로어 picker. ≈ 1일. |
**합계**: +2 ~ 2.5일.
### 의존성
```
#21 (lib/quest.ts) ─┬─ #22 (카드 노드 — REWARD 등 소비)
└─ #23 (사이드 퀘스트 분류 — connectedComponents 소비)
#22 ←─ #8 (기존 DAG 렌더) 의 확장
#23 ←─ #14 (기존 추천 패널) 의 확장
```
## 7. 위험
| 위험 | 대응 |
|---|---|
| Week 3 빡빡함 (+2 ~ 2.5일) | Week 2 가 1일이라도 밀리면 #15(핀/snooze) 또는 #17(가중치 슬라이더) 를 Week 4 후 추가 작업으로 미룸. |
| 퀘스트 카드 톤이 큰 그래프에서 시각 노이즈 | 스파이크 [#2 (React Flow + dagre)](https://github.com/boostcampwm-snu-2026-1/flowtodo-pkdje/issues/2) 에서 카드 톤 mock 으로 검증. 50+ 노드에서 견디는지. |
| 해금 펄스 애니메이션이 매 클릭마다 발동되어 산만 | `detectUnlocks` 가 **상태가 just-transitioned** 인 경우만 반환. 일반 리렌더에는 트리거 안 됨. |
| REWARD 텍스트가 후속 이름 길면 카드 깨짐 | 2 ~ 3개 이름 직접 노출 후 그 이상은 "{N}개 해금" 으로 축약. |
| 이모지 picker 가 모바일 키보드 UX 와 충돌 | 1주차에 모바일 반응형은 스코프 외. 데스크탑 기준만. |
## 8. Implementation Hints (Spike #2 결과 반영)
[Spike #2 (React Flow + dagre)](https://github.com/boostcampwm-snu-2026-1/flowtodo-pkdje/issues/2) 에서 검증된 구체 설정값. #8 (DAG 렌더), #22 (퀘스트 카드 노드) 구현 시 시작점으로 사용. 전체 findings: `spike/SPIKE.md` ([브랜치](https://github.com/boostcampwm-snu-2026-1/flowtodo-pkdje/tree/spike/react-flow-dagre/spike)).
### 8.1 dagre 자동 레이아웃 설정
```ts
const dagreConfig = {
rankdir: 'TB', // Top → Bottom (LR 도 옵션)
nodesep: 12, // 가로 간격. 기본 30 너무 spacious
ranksep: 28, // 세로 간격. 기본 60 너무 spacious
ranker: 'tight-tree', // 'network-simplex' 보다 컴팩트
marginx: 16,
marginy: 16,
};
```
이 값에서 50 ~ 80 노드 그래프가 시각적으로 견딤. 더 spacious 가 필요해지면 사용자 설정으로 노출 가능 (3주 스코프 외).
### 8.2 퀘스트 카드 dagre 노드 크기
dagre 에 전달할 노드 박스 사이즈 (실제 렌더 카드 사이즈와 일치):
```ts
g.setNode(taskId, { width: 150, height: 76 });
```
폰트: 본문 11px / 메타·REWARD 9px. 패딩 6/10 px. (자세한 카드 디자인은 §5.1.)
### 8.3 React Flow fitView 설정
```ts
```
`fitViewOptions.minZoom: 0.6` 이 중요. 이 값 없이 fitView 사용하면 노드 수가 늘어날수록 글자 안 보일 정도로 축소됨.
### 8.4 MiniMap 가시성
```tsx
n.data?.status === 'done' ? '#a5d6a7'
: n.id === selectedId ? '#ff6f00'
: '#90a4ae'
}
/>
```
`pannable + zoomable` 로 미니맵이 단순 표시에서 인터랙티브 컨트롤로 격상. 테두리/그림자/흰배경 으로 메인 화면과 명확히 분리.
### 8.5 노드 클릭 → 연결 엣지/노드 강조 (인지 부하 해소)
엣지 교차는 DAG 구조에서 오는 inherent 특성이라 알고리즘으로 완전히 없앨 수 없음. 대신 "지금 보고 싶은 엣지만" 분리해서 보는 UX 로 해소:
```ts
const [selectedId, setSelectedId] = useState(null);
// onNodeClick → setSelectedId(node.id)
// onPaneClick → setSelectedId(null)
const connectedIds = useMemo(() => {
if (!selectedId) return null;
const s = new Set([selectedId]);
edges.forEach(e => {
if (e.source === selectedId) s.add(e.target);
if (e.target === selectedId) s.add(e.source);
});
return s;
}, [selectedId, edges]);
// 노드: 비연결은 opacity 0.18
// 연결 엣지: 주황(#ff6f00) + strokeWidth 2.5 + animated
// 비연결 엣지: 회색 + opacity 0.3
```
이 인터랙션은 **#22 안에 포함** (퀘스트 카드 노드의 자연스러운 일부로 구현).
### 8.6 성능 측정 (스파이크)
| 노드 수 | layout 시간 (Mac M-series) |
|---|---|
| 50 | < 5 ms |
| 80 | < 10 ms |
| 120 | < 25 ms |
3주 스코프(수십 ~ 100 노드)에서 충분. 200+ 노드 측정은 Week 3 회고에서.
### 8.7 검증 안 된 항목 (구현 중 확인)
- 실제 CRUD 와 결합한 인터랙션 성능 (정적 렌더만 측정)
- 사이클 시도 시 거부 피드백 UX
- 노드 추가/삭제 시 자동 레이아웃 재계산의 시각적 부드러움 (즉시 vs 트랜지션)
- 모바일 (스코프 외)
- 다크 모드 (스코프 외)
---
## 9. 회고에서 확인할 것
- 게이미피케이션 톤이 실제 사용에서 **재미** 인가 **노이즈** 인가? (본인 사용 후 평가)
- 사이드 퀘스트 자동 분류가 직관적이었는가? 별도 토글이 필요한가?
- 해금 애니메이션의 빈도/길이가 적절했는가?
- Week 3 시간 예산 대비 +2.5일이 실제로 얼마였는가?