ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기한양대학교 에리카 학생을 위한 서비스인 '하냥냥' 웹
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
한양대학교 ERICA인들을 위한 캠퍼스 라이프스타일 유틸리티 앱 학식 · 셔틀버스 · 캠퍼스맵 · 도서관 혼잡도 · 헬스장 시간표 등 매일 확인해야 하는 캠퍼스 정보를 한곳에 모아 더 슬기로운 에리카 캠퍼스 라이프를 즐겨보세요! 문의사항이 있다면 [email protected] 으로 연락주세요 :) • 🍚 식당별 학식 조회 • 🚌 셔틀버스&지하철와 공공버스 시간표 • ☁️ 날씨와 학정혼잡도 • 🏫 교내건물&흡연시설&오픈스페이스와 단과대별 제휴업체를 모은 캠퍼스맵 • 🔔 학식·날씨 맞춤 푸시알림 (FCM) • 💪 체대 헬스장 시간표 조회 • 🎓 그외 편리한 학교생활을 위한 기타기능 💡 원래는 도서관 열람실 입장용 QR 코드 발급 + 좌석 예약/반납 기능도 준비했었는데, 학교 도서관 측에 문의해보니 외부 앱에서 제공하면 안 된다고 해서 접었어요..ㅎㅎ Data & Infra - Supabase는 익명 Auth·DB·RPC, Firebase는 FCM 푸시 알림(네이티브 + Web)을 담당README 전문 보기 ↗
| 역할 | 이름 | GitHub | |
|---|---|---|---|
| 프론트엔드 | 김예은 | @yaeunjess | [email protected] |
| 백엔드 | 김동준 | @kdjidkr | [email protected] |
문의사항이 있다면 [email protected] 으로 연락주세요 :)
💡 원래는 도서관 열람실 입장용 QR 코드 발급 + 좌석 예약/반납 기능도 준비했었는데, 학교 도서관 측에 문의해보니 외부 앱에서 제공하면 안 된다고 해서 접었어요..ㅎㅎ
Frontend
Backend
Data & Infra - Supabase는 익명 Auth·DB·RPC, Firebase는 FCM 푸시 알림(네이티브 + Web)을 담당
Testing & Monitoring
외부 API: Open-Meteo(날씨·대기질), 서울열린데이터(지하철), 공공데이터포털(공휴일), 경기도 버스정보시스템(공공버스), 한양대도서관 API, Google Gemini(날씨 코멘트 AI), Instagram API
Clean Architecture는 소스 코드 의존성이 항상 바깥(UI·프레임워크·DB)에서 안쪽(비즈니스 로직)을 향하게 강제해 프레임워크·DB·외부 API가 바뀌어도 핵심 로직은 영향받지 않게 하는 것이 핵심이다.
| 원칙 | 하냥냥에서의 적용 |
|---|---|
| 의존성 규칙 — 바깥 레이어만 안쪽을 알아야 함 | domain/은 React·Supabase·fetch 등 어떤 프레임워크도 import하지 않고, data/·presentation/이 반대로 domain/을 참조 |
| 의존성 역전(DIP) — 저수준 구현이 고수준 정책의 인터페이스를 구현 | domain/repositories/에 인터페이스만 정의하고 실제 구현체(data/repositories/)가 이를 구현 — domain은 API가 REST인지, 응답 DTO가 어떤 모양인지 전혀 모름 |
| 프레임워크 독립성 | domain/entities·usecases는 순수 TS 타입·함수뿐이라 React 없이도 재사용·테스트 가능 |
| 테스트 용이성 | domain 레이어 로직(Weather.test.ts, Shuttle.test.ts 등)을 목(mock) 없이 순수 함수 단위로 테스트 |
| Composition Root | di.ts에서만 datasource→repository→usecase의 구체 구현을 실제로 조립해 훅에 주입 |
src/
├── presentation/ # UI (React 컴포넌트, Hook, Context)
│ ├── components/ # 화면 단위 컴포넌트 — 하단 탭 구조와 1:1 대응하도록 폴더를 분리
│ │ ├── cafeteria/ # 학식 탭 — 화면 + 메뉴 포맷팅/끼니별 그룹핑 로직
│ │ ├── portal/ # 소식 탭 — 날씨·도서관 좌석·배너를 한 화면에 모음
│ │ ├── shuttle/ # 셔틀 탭 — 셔틀·지하철·공공버스 시간표/도착정보
│ │ ├── misc/ # 기타 탭 — 헬스장·인스타·피드백
│ │ ├── partnership/ # 단과대별 제휴 정보 탭
│ │ ├── common/ # 특정 탭에 속하지 않는 전역 UI (스플래시, 에러 바운더리, 오프라인 모달 등)
│ │ └── ui/ # 범용 UI 프리미티브 (아코디언·바텀시트·휠피커 등)
│ ├── context/ # 앱 전체가 공유해야 하는 상태 (부팅 준비 상태, 네트워크 온/오프라인)
│ └── hooks/ # 화면별 데이터 조회·폴링·구독 로직
├── domain/ # 비즈니스 로직
│ ├── entities/ # 도메인 모델 타입 + 그에 딸린 순수 계산/변환 로직
│ ├── repositories/ # 레포지토리 "인터페이스"만 정의
│ ├── usecases/ # 화면이 호출하는 단일 진입점
│ └── utils/ # 여러 도메인이 공유하는 프레임워크 독립 순수 함수 (예: 좌표 거리 계산)
├── data/ # 데이터 레이어
│ ├── datasources/ # 실제 HTTP 요청을 실행하고 백엔드 원시 응답(DTO) 타입만 다룸
│ └── repositories/ # domain의 레포지토리 인터페이스를 구현
├── infrastructure/ # 플랫폼 종속 계층
│ ├── http/HttpClient.ts # HttpClient 인터페이스, ApiResponse<T> 응답 래퍼, parseOrThrow
│ ├── network/NetworkStatus.ts # 네이티브/웹 온라인 상태 조회 및 구독
│ └── storage/SecureStorage.ts # 네이티브 보안저장소·localStorage 겸용 Supabase storage 어댑터
├── lib/ # 외부 SDK 초기화
│ ├── supabase.ts # Supabase 클라이언트 생성, 하이브리드 storage, 익명 사용자 ID 발급
│ ├── firebase.ts # Firebase 초기화, 웹/네이티브 FCM 권한 요청·토큰 발급
│ ├── sentry.ts # Sentry 지연 로드 및 1회 init 캐싱
│ ├── queryClient.ts # QueryClient 기본옵션 + persistQueryClient 설정
│ ├── kakao.ts # 카카오 공유 SDK 지연 로딩 및 1회 초기화 캐싱
│ ├── platform.ts # Capacitor 기반 플랫폼(ios/android/web) 판별 헬퍼
│ └── androidBackHandler.ts # 안드로이드 뒤로가기 콜백 스택 관리
└── di.ts # 위 모든 데이터소스·레포지토리·유스케이스를 조립해 훅에 주입하는 컨테이너
학식·날씨·도서관 혼잡도·배너·지하철·셔틀·학사달력/공휴일·통합피드백 등 핵심 데이터는 자체 백엔드 서버(Spring Boot)를 거치고, 인스타그램 프로필·공공버스 조회는 Vercel BFF(Serverless Functions)를 경유합니다. 앱이 외부 API를 직접 호출하는 경우는 거의 없습니다.
Supabase는 익명 Auth·앱설정(다가오는 시간표 변경 배너용 period_schedule만)·알림구독 RPC 목적으로 클라이언트에서 직접 연결합니다.
푸시 알림은 Firebase Cloud Messaging(FCM)으로 발송되며, iOS 빌드·배포는 Codemagic으로 자동화되어 있습니다.
| 테이블 | 용도 |
|---|---|
app_config | 다가오는 시간표 변경 배너용 period_schedule만 조회 — 현재기간·공휴일·강제주말·미운행 오버라이드 등 나머지 필드는 /api/v1/academic/status로 이전 완료, 더 이상 조회 안 함 |
devices(FCM 토큰), subscriptions(알림 구독 설정) 테이블은 클라이언트에서 직접 조회하지 않고 RPC로만 접근합니다 — get_alarm_subscription(구독 조회), upsert_alarm_subscription(구독 생성·수정·해제)
(Tanstack Query 기본값은 staleTime 15분, gcTime 24시간을 씀. api 실패시 재시도는 2번, 그래서 총 3번 호출함. staleTime이 지났는데 트리거가 있을 경우 SWR. localStorage는 maxAge 24시간으로 설정함. maxAge는 캐싱된 값을 불러올지 말지를 결정하는 기준.)
| 엔드포인트 | 역할 | Redis TTL(백엔드) / TanStackQuery staleTime(FE) | refetch 트리거 (네트워크 재연결시 staleTime 기준으로 다시 불러옴) |
|---|---|---|---|
/api/v1/menu | 학식 메뉴 조회 | 12시간 / 1시간 | 콜드스타트 fetch, "다시 시도" 버튼 |
/api/v1/shuttle | 셔틀버스 시간표 조회 | 12시간 / 1시간 | 콜드스타트 prefetch, "다시 시도" 버튼, academic/status 기간/dayType이 실제로 바뀌는 순간 강제 재요청(useShuttle이 이전 값과 비교해 invalidateQueries). academic/status 자체는 셔틀화면이 열릴 때 낡았으면 재검증 |
/api/v1/subway/schedule | 지하철 시간표 조회 | 12시간 / 1시간 | 지하철정보가 필요한 정류장(기숙사·셔틀콕) 선택시, "다시 시도" 버튼, date-info의 dayType이 바뀌는 순간 강제 재요청(위와 동일한 이유) |
/api/v1/weather | 날씨·대기질·자외선 스냅샷, 시간별 예보 | 10분 / 10분 | 콜드스타트 prefetch, 소식탭 진입, "다시 시도" 버튼 |
/api/v1/weather/briefing | AI 기반 날씨 브리핑 | 30분(매시 22분 갱신) / 30분 | 콜드스타트 prefetch, 소식탭 진입 |
/api/v1/banners | 홈 배너 조회 | 12시간 / 5분 | 콜드스타트 prefetch (관리자 등록 시 백엔드가 즉시 evict하므로, 새 배너가 늦게 반영되는 답답함을 줄이려고 다른 엔드포인트보다 짧게 잡음) |
/api/v1/library/seats | 도서관 열람실 좌석 혼잡도 | 3분 / 3분 | 콜드스타트 prefetch, 소식탭 진입, "다시 시도" 버튼 |
/api/v1/gym/gym-periods | 체대 헬스장 운영기간·시간표 조회 | 12시간 / 1시간 | 헬스장 화면 진입시, "다시 시도" 버튼, KST 날짜(자정)가 바뀌는 순간 강제 재요청(useGymSchedule이 1분마다 날짜 확인). 셔틀과 달리 academic/status를 안 쓰고 GymPeriod.startDate/endDate로 직접 오늘과 비교해 기간을 고르는 구조라, "기간이 바뀌는 순간"이 아니라 "날짜가 바뀌는 순간"을 감지함. GymView도 자동판별을 매 gymData 갱신마다 다시 하도록 고쳐서(예전엔 최초 1회만) 화면을 계속 띄워놓은 채로 날짜가 바뀌어도 새 기간으로 따라감(사용자가 드롭다운으로 직접 고른 뒤엔 안 덮어씀) |
/api/v1/partnership/partnership-available | 단과대별 제휴 가맹점·혜택 조회 | 12시간 / 1시간 | 캠퍼스맵 탭 최초 진입시 호출, "다시 시도" 버튼 |
POST /api/v1/feedbacks | 통합 피드백 접수 | 해당없음 | 기타탭 > 피드백 보내기(category GENERAL), 캠퍼스맵 화면 상단에 제보 버튼(CAMPUS_MAP), 캠퍼스맵 검색 결과 없음 제보 버튼(PARTNERSHIP) |
/api/v1/academic/status | 학사 및 셔틀/시설 통합 운영 상태 조회 | 5분(FE staleTime, BE 캐시 주기 미확인) |
| 엔드포인트 | 역할 | 외부 호출 대상 | Vercel 캐시 TTL | 프론트엔드 TanStackQuery staleTime | refetch 트리거 |
|---|---|---|---|---|---|
/api/cron/refresh-insta-profiles* | 인스타 계정 프로필 사진 정적 이미지 갱신 | Instagram API(스크래핑) + GitHub Contents API(커밋) | 해당없음 | 해당없음 | Vercel Cron이 4개월마다 1회만 서버에서 실행, 클라이언트는 API 호출 안 하고 정적 이미지만 읽음 |
/api/bus | 공공버스 도착 정보 조회 | 공공데이터포털-경기도 버스정보시스템 | 40초 (메모리 캐시) | 기본 15분 (화면 활성 중엔 30초 간격 강제 폴링, 탭 비활성·유휴 시 중단) | "공공버스" 모드 + 화면보임 + 사용자 조작중일 때 30초 간격 자동 폴링, 새로고침 버튼 수동 refetch |
/api/music-search | Spotify 곡 검색 (곡추천하기 검색창, 검색결과화면의 "곡" 섹션) | Spotify Web API | 1시간 (s-maxage=3600, Vercel Edge 캐시) | 30초 | 곡추천하기: 검색 버튼/Enter를 누를 때(타이핑 중엔 호출 안 함), 2자 미만이면 클라이언트 검증만 하고 호출 안 함. 검색결과화면: 재검색 제출 시(queryKey에 검색어 포함돼 자동 재조회). 429(Spotify 요청 제한) 응답이면 Retry-After만큼 검색 버튼을 막고 react-query 자동 재시도는 꺼둠(retry: false) — 안 그러면 막힌 요청을 곧장 다시 쏴서 제한이 더 길어짐 |
프론트는 안 쓰지만 아직 살아있는 Vercel 함수 — api/menu.js, api/portal.js(weather+library 통합), api/holidays.js 3개는 전부 새 백엔드로 완전히 대체되어 프론트엔드 어디에서도 더 이상 호출하지 않음. 하지만 Supabase Edge Function menu-alerts(1분마다 도는 푸시 발송 로직)가 /api/menu, /api/portal?type=weather, /api/holidays를 직접 fetch()하고 있어서 세 함수 다 삭제하면 안 됨. 단, /api/portal?type=library는 Edge Function도 호출하지 않아 완전히 죽은 라우트 — api/portal.js 리팩토링/삭제 시 이 부분만은 안전하게 정리 가능.
위 표들과 별도로, Sentry 이슈 알림을 Discord로 중계하는 내부 전용 인바운드 웹훅도 있음(앱이 호출하는 게 아니라 Sentry가 호출). Sentry 무료(Developer) 플랜엔 네이티브 Discord 연동이 없어서(유료 Slack 연동을 억지로 꽂는 방식뿐) 만든 중계 함수이며, 서명 검증을 거쳐 신뢰할 수 있는 요청만 처리함.
| 키 | 내용 | 언제 생성되나 |
|---|---|---|
period_schedule_cache | Supabase app_config.period_schedule(다가오는 시간표 변경 배너용 미래 일정 테이블)만 캐싱 | 콜드스타트시 즉시 (TTL 없이 조회 성공 시에만 덮어씀) |
lastActiveTab | 마지막으로 선택한 탭 | 콜드스타트 시 즉시 + 탭 전환마다 갱신 |
sb-<project>-auth-token | Supabase 인증 세션 토큰 (Supabase SDK가 자체관리) | getOrCreateAnonymousUserId()가 처음 호출될 때(피드백 전송, 알림 구독 등) — 네이티브는 Keychain/Keystore, 웹은 localStorage |
alarm_settings, weather_alarm_settings | 학식·날씨 알림구독 설정값 | 알림 바텀시트를 닫을 때만 |
partnerCollegeFilter | 마지막으로 선택한 제휴단과대 칩 | 제휴탭에서 단과대 칩을 누르면 |
shuttle_stop, shuttle_lineId | 마지막으로 선택한 셔틀 정류장·노선 | 사용자가 직접 정류장/노선을 바꿔야 (GPS 자동 선택은 저장 안 함) |
public_bus_favorites | 즐겨찾기한 버스 정류장 | 셔틀탭 '일반 버스' 모드에서 즐겨찾기 토글 시 |
hyu_rq_cache | TanStack Query 캐시 영속화본 | 항상 |
ph_phc_<프로젝트키>_posthog | PostHog device/distinct ID | 앱 코드가 아니라 PostHog JS SDK가 초기화 시 자동 생성 |
| 키 | 내용 | 용도 |
|---|---|---|
splashShown | 이번 세션에서 스플래시를 이미 보여줬는지 | 세션당 스플래시 1회만 노출 |
| 변수 | 내용 | 용도 |
|---|---|---|
cachedNativeToken | 네이티브 FCM 토큰 | 세션 내 재조회 없이 재사용, 디스크엔 절대 저장 안 함 |
| 키 | 내용 | 저장되는곳 |
|---|---|---|
ph_phc_<프로젝트키>_posthog | device_id/distinct_id (사용자 식별) | 쿠키·로컬스토리지 이중 저장 |
카카오 공유 링크, 푸시 알림 클릭, 배너 클릭 — 앱으로 들어오는 경로가 여러 개지만, 네이티브(Android/iOS)에서는 전부 hanyang-deeplink 커스텀 이벤트 → App.tsx의 routeFromParams()라는 단일 진입점으로 모인다. 배너 클릭만 유일하게 이 파이프라인을 안 타는데, 클릭 시점에 이미 앱이 떠 있어서 딥링크 파싱 자체가 필요 없기 때문(BannerCarousel.tsx가 직접 처리).
| 진입 경로 | URL 형식 | 처리 위치 | 도착 화면 |
|---|---|---|---|
| 카카오톡 공유 (학식) | ?date=YYYY-MM-DD&cafe=<cafeId>&type=<조식|중식|석식|천원...> | App.tsx 최초 마운트 시 date/cafe/type 파라미터 체크(activeTab/isCafeteriaLink/showCafeDeepLinkLoader 초기값) → 네이티브는 MainActivity(커스텀 스킴 kakao{key}://kakaolink?...)/AppDelegate가 가로채 routeFromParams()까지 전달 | 학식탭 + 학식 딥링크 전용 로더 스플래시 |
| 푸시 알림 - 학식 | ?tab=cafe&date=...&cafe=... (끼니별 알림은 &type=...도 추가) | 네이티브: PushNotifications의 pushNotificationActionPerformed 리스너 → routeFromParams(). 콜드스타트로 인해 리스너 등록 전에 이벤트가 드랍될 수 있어, Android는 MainActivity.onCreate()에서 Intent extra(link)를 직접 읽어 같은 경로로 주입 | 학식탭 + 학식 딥링크 전용 로더 스플래시 |
| 푸시 알림 - 날씨 | ?tab=weather | 위와 동일한 routeFromParams() 파이프라인 | 소식탭(portal) |
| 배너 클릭 (내부 링크) | ?tab=<cafe|shuttle|portal|partner|misc>&chip=<...>&box=<...> | BannerCarousel.tsx의 handleClick() — url.origin === window.location.origin이 완전히 일치할 때만 onNavigateToTab()을 직접 호출. 딥링크 이벤트나 routeFromParams는 안 거침 | tab 전환 + (chip이면 캠퍼스맵 특정 칩, box면 기타탭 특정 서브뷰까지 지정) |
| 배너 클릭 (외부 링크) | 그 외 모든 URL | window.open(url, '_blank') | 외부 브라우저 |
푸시 알림 링크는 Supabase Edge Function menu-alerts가 발송 시점에 만든다(buildFCMMessage가 data.link 필드에 담아 FCM 페이로드로 전송).
chip/box 파라미터는 배너 전용 — 카카오 공유·푸시 알림 딥링크(routeFromParams)는 tab/date/cafe/type만 처리하고 chip/box는 안 읽는다. "캠퍼스맵 특정 칩으로 바로 진입하는 카카오 공유 링크" 같은 건 지금 구조에서 못 만든다 — 필요해지면 routeFromParams에 chip/box 케이스를 추가해야 함.
딥링크 진입 시 앱 내부 상태 전달 방식:
MainActivity가 React 마운트 전에 도착한 파라미터를 window.__pendingDeepLinkParams에 동기 저장 → App.tsx가 마운트되며 즉시 소비.onNewIntent)는 hanyang-deeplink CustomEvent로 전달.App.addListener('appUrlOpen', ...) 같은 Capacitor 표준 URL-open 리스너는 안 씀 — Universal Links(iOS)/App Links(Android) 둘 다 각 네이티브 레이어(AppDelegate.swift/MainActivity.java)에서 직접 가로채 위 커스텀 브릿지로 흘려보내는 자체 구현.앱부팅 시 BootContext가 prefetch(스플래시 게이팅 대상—실패해도 markReady는 호출) + 셔틀탭이 academic/shuttle 필드로 기간·셔틀 dayType·운행여부를 판정. calendar 필드는 학교 자체 공휴일까지 섞여 있어 지하철엔 안 씀(아래 date-info 참고) |
/api/v1/holidays/date-info | 특정 날짜의 평일/주말/공휴일/미운행 상태 조회 | 1시간(FE staleTime, BE 캐시 주기 미확인) | 지하철 연결정보가 필요한 정류장에서만 조회 |
/api/v1/playlist/songs | 플레이리스트 피드 곡 목록 조회 (최근추가된곡) | 0(항상 최신값) — 여러 사용자가 실시간으로 올리는 피드라 캐싱 안 함 | 콜드스타트 fetch, 최근추가된곡 화면 진입마다 |
/api/v1/playlist/songs/{id} | 게시글 단건 상세 조회 | 0(항상 최신값) | 게시글 목록(TrackPostCollectionView/SearchResultsView 등)에서 항목 클릭 시 상세화면(PostView) 진입 |
/api/v1/playlist/songs/creation-status | 곡 작성 전 사용자 기기 상태 조회 (오늘 남은 등록 횟수, 최근 7일 중복 추천곡) | 0(항상 최신값) | 곡추천하기 화면 진입(컴포넌트 재마운트)마다. 헤더에 남은 횟수 표시, 최근 7일 내 추천한 곡은 검색 결과에서 선택 자체를 막음 |
/api/v1/playlist/songs/liked | 내가 좋아요(=서비스 내 "저장") 누른 곡 목록 조회 | 0(항상 최신값) | 저장한 곡 화면 진입(컴포넌트 재마운트)마다 |
/api/v1/playlist/songs/my-songs | 내가 등록(작성)한 게시글 목록 조회 | 0(항상 최신값) | 내가 등록한 곡 화면 진입(컴포넌트 재마운트)마다 |
/api/v1/playlist/songs/search | 게시글 가중치 통합 검색 (제목/가수/코멘트) | 0(항상 최신값) | 검색 결과 화면의 "게시글" 섹션 — 검색어(query)가 바뀔 때마다(queryKey에 포함돼 자동 재조회), 2자 미만이면 호출 안 함 |
POST /api/v1/playlist/songs | 곡 추천 및 등록 | 해당없음 (뮤테이션, 캐싱 대상 아님) | 등록 확인 팝업에서 최종 확정 시, 성공하면 위 목록 캐시 맨 앞에 즉시 반영. isPending 동안 버튼 비활성화로 중복 제출 방지 |
POST /api/v1/playlist/songs/{id}/reports | 곡 게시글 신고하기 | 해당없음 (뮤테이션) | 더보기 메뉴 → 사유 선택 → 신고하기 클릭 시 |
POST /api/v1/playlist/songs/{id}/like | 좋아요(=서비스 내 "저장") 토글 | 해당없음 (뮤테이션) | 저장 배지 클릭 시. 낙관적 업데이트 + 실패 시 롤백, 이전 요청 isPending 중엔 연타 무시 |
POST /api/v1/playlist/songs/tracks/{trackId}/play | 곡 재생수 기록 (인기차트 집계용) | 해당없음 (뮤테이션) | 재생 버튼 클릭 시(모든 재생 버튼이 PlaylistView의 handlePlay 한 곳으로 모임). 같은 trackId는 10초 스로틀 — 연타로 재생수가 부풀지 않게 프론트에서 직접 제한 |
POST /api/v1/playlist/songs/{id}/reactions | 이모지 리액션 토글 (9종) | 해당없음 (뮤테이션) | 이모지 반응 버튼 클릭 시. 응답으로 온 9종 전체 최신 카운트로 로컬 상태를 통째로 동기화, 실패 시 롤백, 이전 요청 isPending 중엔 연타 무시 |
/api/v1/playlist/songs/tracks/{trackId} | 특정 곡(trackId)에 달린 추천 게시글 모아보기 | 0(항상 최신값) | 인기차트/검색결과에서 곡 선택 시, 최신·인기 정렬(sort) 전환 시(queryKey에 sort 포함돼 자동 재조회) |
/api/v1/playlist/songs/charts | 인기 차트 순위 조회 (실시간 급상승/주간/월간) | 0(항상 최신값) | 콜드스타트 fetch, 기간 필터 칩(실시간/주간/월간) 전환 시(queryKey에 기간 포함돼 자동 재조회) |