본문 바로가기
IT/AI

[AI] Claude와 함께 타임박싱 웹앱 만들기 #10 — 가족 대시보드, 주간 목표, 알림

by 수누다 2026. 3. 29.


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. 데이터 로드 순서

가족 대시보드는 여러 컬렉션을 순차 조회해야 합니다. 순서가 중요합니다.

  1. 내 그룹 목록 조회 (groups)
  2. 각 그룹의 멤버 조회 (group_members + expand → user)
  3. 각 멤버의 오늘 timebox_daily 조회
  4. 각 멤버의 공유 일정 조회
  5. payload 파싱 → BIG 3, 타임라인, 달성률 추출
  6. 카드 렌더링

단계가 많아 보이지만 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시간

자동 추적 원리:

  1. 이번 주 월~일의 timebox_daily 레코드를 PocketBase에서 로드
  2. 각 날짜의 Done된 슬롯을 파싱
  3. 슬롯의 slot-label과 목표 키워드를 매칭
  4. "회" 단위 = 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 알림을 추가한 과정을 실제 코드와 트러블슈팅 포함해 정리합니다.