
9편에서 소셜 시스템과 인라인 캘린더를 만들었습니다. 친구/그룹이 생겼으니 자연스러운 다음 단계가 보였습니다. "가족 멤버들의 하루를 한 화면에서 보고 싶다", "이번 주 운동 몇 번 했는지 자동으로 세고 싶다", "다음 일정 5분 전에 알림 받고 싶다". 이번 편에서는 가족 대시보드, 주간 목표 시스템, 브라우저 알림을 구현한 과정을 정리합니다.
1. 환경 정보
| 항목 | 버전/스펙 |
|---|---|
| Backend | PocketBase (Self-hosted, SQLite) |
| Frontend | HTML + Vanilla JS (PWA) |
| Infra | Proxmox VE — LXC 컨테이너 |
| Web Server | Nginx (리버스 프록시) |
| SSL | Let's Encrypt |
2. 가족 대시보드
2-1. 설계
👨👩👧👦 버튼을 누르면 팝업이 뜨고, 내가 속한 그룹의 멤버 전원이 카드 형태로 나옵니다. 각 멤버 카드에 들어가는 정보는 다음과 같습니다.

- 이니셜 아바타 + 이름 + 소속 그룹
- 달성률 (%) — Done 슬롯 / Plan 슬롯
- BIG 3 — 오늘의 핵심 할 일 3개
- 타임라인 바 — 5AM~1AM 84슬롯을 한 줄 그래프로 압축
- 주요 일정 칩 — 공유된 일정 라벨들
타임라인 바가 핵심입니다. 84칸짜리 슬롯을 1줄로 축약해서 보여주면, 한눈에 "이 사람이 오늘 어디까지 진행했는지"가 보입니다. Plan은 연한 색, Done은 진한 색으로 구분하니까 직관적이고요.
2-2. 데이터 로드 순서
가족 대시보드는 여러 컬렉션을 순차 조회해야 합니다. 순서가 중요합니다.
- 내 그룹 목록 조회 (
groups) - 각 그룹의 멤버 조회 (
group_members+ expand →user) - 각 멤버의 오늘
timebox_daily조회 - 각 멤버의 공유 일정 조회
- payload 파싱 → BIG 3, 타임라인, 달성률 추출
- 카드 렌더링
단계가 많아 보이지만 PocketBase의 expand 기능 덕분에 2번에서 그룹 + 멤버 정보를 한 번에 가져올 수 있습니다. 실제 API 호출은 3~4회 정도로 줄어듭니다.
2-3. API Rules 변경 — 보안과 편의 사이
여기서 중요한 결정이 하나 있었습니다. 다른 멤버의 timebox_daily를 조회하려면 List/Search rule을 수정해야 합니다. 기본 설정은 "본인 레코드만 조회 가능"이었거든요.
가족 간 일정 공유가 목적이니까 "로그인 유저 전체 조회 가능"으로 변경했습니다. PocketBase 공식 문서에서도 API Rules를 통해 멀티유저 접근 제어를 유연하게 구성할 수 있다고 안내하고 있습니다. 다만 민감한 데이터가 포함된 컬렉션이라면 그룹 기반 필터링(@request.auth.id ?= group_members_via_user.user)을 적용하는 게 맞습니다. 지금은 가족 전용이라 일단 단순하게 갔지만, 외부 사용자가 생기면 반드시 조건을 걸어야 합니다.

2-4. 날짜 이동
"오늘"만 보면 아쉬우니까 < > 버튼으로 어제/내일도 조회할 수 있게 했습니다. 와이프가 어제 뭘 했는지 궁금할 때 유용합니다. (물론 합의하에...)
날짜를 바꾸면 해당 날짜 기준으로 API를 다시 호출하고 카드를 재렌더링합니다. 단순하지만, 이 기능 하나로 대시보드의 활용도가 확 올라갑니다.
3. 주간 목표
3-1. 왜 만들었나
매일 타임박싱을 하면 "오늘 뭘 했는지"는 보입니다. 그런데 "이번 주에 운동을 몇 번 했는지", "블로그에 몇 시간 쓴 건지" 같은 주간 단위 추적이 안 됐습니다. 타임박싱의 단점이 하루 단위에 갇히기 쉽다는 건데, 주간 목표로 그 약점을 보완한 셈입니다.
3-2. 구현 방식 — DB 없이 해결
PocketBase에 별도 컬렉션을 만들지 않았습니다. 목표 자체는 localStorage에 저장하고, 진행률은 기존 timebox_daily 데이터를 스캔해서 계산합니다. 추가 DB 작업 없이 기존 데이터를 재활용하는 방식입니다.
목표 추가 방법:
- 이름: Brain Dump 라벨과 매칭될 키워드 (예: "달리기")
- 목표값 + 단위: 3회 또는 5시간
자동 추적 원리:
- 이번 주 월~일의
timebox_daily레코드를 PocketBase에서 로드 - 각 날짜의 Done된 슬롯을 파싱
- 슬롯의
slot-label과 목표 키워드를 매칭 - "회" 단위 = Done이 있는 날 수, "시간" 단위 = Done 슬롯 × 15분 합산
핵심은 키워드 매칭 로직입니다:
// 키워드 매칭 — 라벨에 목표 키워드가 포함되면 카운트
Object.keys(labelDays).forEach(lbl => {
if (lbl.toLowerCase().includes(keyword)) {
if (g.unit === '회') current += labelDays[lbl].size;
else current += labelMinutes[lbl] / 60;
}
});

예를 들어 "달리기"라는 목표를 세우면, 이번 주 7일간의 타임박싱 데이터에서 라벨에 "달리기"가 포함된 Done 슬롯을 찾아 자동으로 집계합니다. toLowerCase()로 대소문자 구분 없이 매칭하니까, "달리기", "아침 달리기", "달리기 30분" 같은 다양한 표현을 다 잡아냅니다.
3-3. 프로그레스 바
목표별로 컬러풀한 프로그레스 바가 표시됩니다. 100% 달성하면 테두리 강조 + 🎉 표시가 뜹니다. 직접 써보니 "채우고 싶은" 심리가 생겨서 생각보다 동기부여 효과가 큽니다. 게이미피케이션의 힘이라고 해야 할까요.
4. 알림 시스템
4-1. 브라우저 Notification API 선택
네이티브 앱이 아니니까 OS 푸시를 직접 쓸 수는 없습니다. 대신 브라우저 Notification API를 활용했습니다. PWA로 설치한 상태라면 OS 알림과 거의 동일하게 동작합니다. 2026년 기준으로 Chrome, Firefox, Edge, Safari 모두 지원하고 있어서 호환성 걱정도 없었습니다.
4-2. 🔔 버튼 구현
상단 바에 🔔 버튼을 추가했습니다.
- 첫 클릭: 브라우저 알림 권한 요청 + 테스트 알림 발송
- 이후 클릭: 켜기/끄기 토글
권한 요청 타이밍이 중요합니다. PWA 알림 베스트 프랙티스에서도 "유저 액션에 반응해서 권한을 요청하라"고 합니다. 페이지 로드 직후 바로 권한을 요청하면 대부분 거부당합니다. 그래서 🔔 버튼을 누른 시점에 요청하도록 했습니다. 설정은 localStorage에 저장해서 다음 접속 시 자동 복원됩니다.
4-3. 일정 5분 전 알림
30초마다 현재 시간 + 5분 위치의 슬롯을 체크합니다.
// 5분 뒤 슬롯 계산
const futureH = nowH + Math.floor((nowM + 5) / 60);
const futureMin = (nowM + 5) % 60;
const targetSlot = (futureH - 5) * 4 + Math.floor(futureMin / 15);
해당 슬롯에 Plan이 있고 아직 Done 처리 안 했으면 → 알림을 발송합니다. (futureH - 5)인 이유는 타임라인이 5시부터 시작하기 때문입니다. 5AM = 슬롯 0, 6AM = 슬롯 4 이런 식이죠.
4-4. 공유 일정 알림
1분마다 오늘 새로 생성된 공유 일정을 체크합니다. 최근 5분 이내에 다른 유저가 만든 공유 일정이 있고, 내가 대상에 포함되면 알림을 보냅니다. 중복 방지를 위해 _notifSentToday 객체에 슬롯/일정 ID를 기록하고, localStorage에 날짜별로 저장합니다.
5. 트러블슈팅
5-1. HTML 네스팅 사고 — display:none에 같이 묻힌 대시보드
증상: 가족 대시보드 팝업이 열리지 않음
원인: 가족 대시보드 HTML을 소셜 패널 <div> 안에 넣어버린 것. 소셜 패널이 display:none 상태라 가족 대시보드도 같이 숨겨졌습니다.
해결: 오버레이 팝업은 서로 독립적인 형제 요소로 배치했습니다. 중첩하면 부모의 display 상태에 같이 끌려가니까요.
<!-- ❌ 잘못된 구조 — 소셜 패널 안에 중첩 -->
<div id="social-panel" style="display:none">
<div id="family-dashboard">...</div>
</div>
<!-- ✅ 올바른 구조 — 형제 요소로 분리 -->
<div id="social-panel" style="display:none">...</div>
<div id="family-dashboard" style="display:none">...</div>
디버깅에 한참 걸렸는데, 원인은 단순히 <div> 태그 위치 하나였습니다. 단순한 실수일수록 찾기 어렵다는 걸 다시 느꼈습니다.

5-2. 모든 슬롯에서 알림이 울리는 문제
증상: 1시간짜리 일정을 잡으면 알림이 4번 옴 (15분 × 4슬롯)
원인: 블록의 모든 슬롯에서 알림 조건을 체크하고 있었음
해결: 라벨이 있는 셀(= 블록 시작점)에서만 알림을 발송하도록 수정했습니다.
const lbl = cell.querySelector('.slot-label');
if (!lbl) return; // 블록 중간 슬롯은 스킵
코드로는 당연해 보이지만, 실제로 써보기 전까지는 이 문제를 발견하지 못했습니다. "모든 논리적 버그는 코드에서 보이지 않고, 실사용에서 드러난다"는 교훈.
6. 배운 점
직접 만들고 써보면서 느낀 점을 정리합니다.
가족 대시보드 — 타임라인 바 하나가 핵심입니다. 84슬롯을 1줄 그래프로 보여주면 한눈에 하루가 보입니다. 상세 정보보다 시각적 요약이 대시보드에서는 훨씬 효과적이었습니다.
주간 목표 — 별도 DB 없이 기존 데이터 스캔으로 충분합니다. 라벨 키워드 매칭이 단순하지만, 실사용에서는 충분히 정확합니다. 오버엔지니어링하지 않은 게 오히려 좋았습니다.
브라우저 Notification API — PWA와 결합하면 네이티브 수준입니다. 별도 푸시 서버(VAPID, Service Worker push) 없이 로컬 알림만으로도 충분한 사용 경험을 만들 수 있습니다. 다만 브라우저 탭이 열려 있어야 동작한다는 제약은 있습니다.
실사용 테스트가 버그를 잡습니다. HTML 네스팅 버그도, 모든 슬롯 알림 버그도 코드 리뷰에서는 안 보이고 실제로 써봐야 발견됩니다. "일단 쓰면서 고치자"가 이 프로젝트의 개발 방식이 되어가고 있습니다.
다음 편: #11 — 리뷰 대시보드 강화 + Plan 레이어 보존


참고 링크:
메타 설명: 타임박싱 웹앱에 가족 대시보드, 주간 목표 자동 추적, 브라우저 Notification API 알림을 추가한 과정을 실제 코드와 트러블슈팅 포함해 정리합니다.
'IT > AI' 카테고리의 다른 글
| [AI] Claude 3.5 Sonnet API 실전 활용 가이드: 최신 버전 기능과 가격 비교 (0) | 2026.04.10 |
|---|---|
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #12 — 데이터 보호: 덮어쓰기 차단, 백업, 로그 (0) | 2026.03.31 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #11 — 리뷰 대시보드 강화 + Plan 레이어 보존 (0) | 2026.03.30 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #9 — 소셜 시스템 + 인라인 캘린더 (0) | 2026.03.27 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #8 — 인프라 보안 & 백업, 그리고 회고 (0) | 2026.03.26 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #7 — 가족끼리 일정 공유 (0) | 2026.03.25 |