3) 프론트엔드 & 사용자 워크플로우

💡 쉽게 말하면: 사용자가 검색창에 문장을 넣으면 국악 3곡이 카드로 뜨고, 카드마다 파형을 눌러 바로 들어볼 수 있습니다. “이걸로 만들래”를 누르면 필터가 채워진 샘플 페이지로 넘어갑니다. 화면은 랜딩·DISCOVER·CREATE 세 개뿐입니다.

9-1. 기술 스택

항목 값
프레임워크 Next.js 16 (App Router) · React 19
언어 TypeScript 5
스타일 Tailwind CSS 4 (@tailwindcss/postcss)
오디오 WaveSurfer.js 7.12
애니메이션 framer-motion 12
아이콘 lucide-react
유틸 clsx · tailwind-merge
폰트 @fontsource/inter

9-2. 라우트 구조

src/app/
├── layout.tsx                  Navbar · Footer · PlayerBar · BottomTabBar · Provider
├── error.tsx / not-found.tsx
├── (public)/
│   ├── page.tsx                랜딩 — 히어로 검색 + 인기 트랙 (SSR, revalidate 3600)
│   ├── discover/page.tsx       DISCOVER — 검색 결과 (CSR)
│   └── create/page.tsx         CREATE — 필터 + 샘플 목록 (CSR)
├── privacy/page.tsx
└── terms/page.tsx

랜딩만 서버 컴포넌트입니다. 인기 트랙을 서버에서 미리 받아 1시간 캐시하기 때문에 첫 화면이 즉시 렌더됩니다. DISCOVER·CREATE는 사용자 입력에 반응해야 하므로 클라이언트 컴포넌트입니다.

9-3. 사용자 워크플로우

flowchart LR
  L["랜딩 /<br/>검색창 + 제안 칩"] -->|문장 입력| D["DISCOVER /discover?q=…"]
  L -->|사운드 둘러보기| D
  D -->|"AI가 감성을 분석 중…"| DR["ResultCard × 3<br/>MatchBadge · WhyBox · EmotionTagChips"]
  DR -->|파형 클릭| P["PlayerBar 재생"]
  DR -->|CreateBridgeButton| C["CREATE /create?instrument=…&emotion=…"]
  C --> PB["PresetBanner<br/>'DISCOVER에서 넘어온 필터'"]
  C --> F["FilterPanel<br/>악기·장단·감성·BPM·루프·라이선스"]
  F --> SP["SamplePanel<br/>SampleRow + MiniWaveform + LoopBadge + LicenseBadge"]

9-4. 컴포넌트 지도

DISCOVER

컴포넌트 역할
SearchBar 자연어 입력 → /discover?q=…로 라우팅
SuggestionChips 무엇을 적어야 할지 모르는 사용자를 위한 예시 문장 칩
ResultCard 매칭 트랙 한 건 카드
MatchBadge 유사도 점수 배지
WhyBox AI가 생성한 매칭 근거 문장(KO/EN) 표시
EmotionTagChips 감성 태그 칩
CreateBridgeButton preset_url이 있을 때만 렌더 → CREATE로 이동

CREATE

컴포넌트 역할
PresetBanner 필터가 어디서 왔는지 알림
FilterPanel 악기·장단·감성·BPM·라이선스 필터
LoopUnitFilter 루프 단위(12박·6박·4박·전체) — 장단 선택 시 자동 활성화
SamplePanel / SampleRow 필터링된 샘플 목록
MiniWaveform 행 안의 소형 파형 미리듣기
LoopBadge 루프 단위 배지
LicenseBadge KOGL 1/2 → 상업 가능 여부 표시
CueMarker CUE A·B·C 마커 (v1.1 자동 추출 대응)

공통 · 레이아웃

컴포넌트 역할
Waveform WaveSurfer.js 래퍼
PlayerBar 화면 하단 고정 플레이어 (PlayerContext 전역 상태)
Navbar 로고 · Discover/Create 탭 · KO|EN 토글 · 로그인 진입
BottomTabBar 모바일 하단 탭
SkeletonCard 로딩 자리표시자
EmptyState 결과 0건
Toast / ToastProvider 알림

9-5. 오디오 플레이어

PlayerContext가 “지금 무엇이 재생 중인가”를 앱 전역에서 하나로 관리합니다.

문제 해결
여러 카드의 파형을 동시에 눌러 소리가 겹침 전역 컨텍스트가 현재 트랙 1개만 유지
페이지 이동 시 재생이 끊김 PlayerBar가 layout.tsx에 있어 라우팅과 무관하게 유지
목록에서 큰 파형을 다 그리면 무거움 목록은 MiniWaveform, 상세·플레이어만 전체 파형

본문 영역에는 pb-[72px] md:pb-[56px] 패딩이 들어가 있어, 고정 플레이어가 콘텐츠를 가리지 않습니다.

9-6. CUE 마커

트랙 안에서 분위기가 바뀌는 지점을 A·B·C로 표시합니다. 색이 각각 다릅니다.

export const CUE_COLORS = {
  A: { line: '#E8593C', bg: '#FDEAE6', text: '#E8593C' },   // 주황
  B: { line: '#3B8BD4', bg: '#E6EFF8', text: '#3B8BD4' },   // 파랑
  C: { line: '#4A7C59', bg: '#EAF2EE', text: '#4A7C59' },   // 초록
} as const;

크리에이터가 “3분짜리 국악에서 쓸 만한 8초”를 찾는 시간을 줄이는 장치입니다. MVP에서는 cue_points가 빈 배열이고, v1.1에서 오디오 특징 기반 자동 추출(POST /cue/extract/{track_id})이 붙습니다.

9-7. 프론트/백 계약 상수

lib/constants.ts가 백엔드와 맞춰야 하는 값을 모아 둔 파일입니다.

// [v4.0 추가] 장단 → 루프 단위(박) 매핑
// 백엔드 jangdan_vo.py 의 JANGDAN_LOOP_UNITS 와 동일한 값을 유지해야 한다
export const JANGDAN_LOOP_MAP: Record<string, number> = {
  '자진모리': 12, '중모리': 12, '굿거리': 12,
  '휘모리': 4,   '세마치': 6,  '엇모리': 10,
} as const;

export const INSTRUMENTS = ['장구','가야금','대금','해금','거문고','피리','아쟁','소금'] as const;
export const JANGDANS   = ['자진모리','중모리','굿거리','휘모리','세마치','엇모리'] as const;
export const EMOTIONS   = ['신남','서정','웅장','슬픔','신비','차분'] as const;

export const PRESET_PARAMS = {
  instrument: 'instrument', emotion: 'emotion',
  bpmMin: 'bpm_min', bpmMax: 'bpm_max',
} as const;

이 값들이 백엔드 VO와 어긋나면 필터가 조용히 0건을 반환합니다. 에러가 나지 않고 “결과가 없네”로 보이기 때문에 발견이 늦습니다. 계약 테스트가 필요한 이유이고, 프리셋 브릿지 설계 §6-6에 잔여 과제로 적어 두었습니다.

9-8. API 클라이언트

const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
const API_PREFIX = '/api/soundbridge';

ApiTrack(snake_case 응답) → GugakTrack(camelCase 도메인 타입) 변환을 mapTrack()이 담당합니다. 백엔드 스키마가 프론트 타입에 그대로 새어 들어가지 않도록 경계에 매퍼를 둔 것으로, 백엔드의 orm_mapper와 같은 발상입니다.

NEXT_PUBLIC_API_URL은 브라우저에서 접근하는 주소라 빌드 시점에 번들됩니다. docker-compose.yaml에서 build arg로 넘기는 이유입니다.

9-9. 로딩·빈 상태 처리

응답이 여러 번의 LLM 호출을 거치므로 대기 시간이 짧지 않습니다. 그 시간을 설명으로 채웁니다.

<SkeletonCard /> × 3
<span>AI가 감성을 분석하고 있어요...</span>

무엇을 기다리는지 알려 주면 같은 시간도 짧게 느껴집니다. 결과가 0건이면 EmptyState가 뜨고, DISCOVER는 검색어 없이 들어왔을 때 인기 트랙을 대신 보여 줍니다.

9-10. 반응형·접근성

항목 처리
모바일 내비 BottomTabBar (데스크톱은 Navbar 중앙 탭)
카드 그리드 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3
최대 폭 max-w-[1080px] 중앙 정렬
다국어 <html lang="ko">, KO|EN 토글 (MVP는 UI만, 응답은 lang 파라미터로 전환)
확대 제한 maximum-scale=1 — 오디오 조작 중 의도치 않은 핀치 줌 방지

maximum-scale=1은 사용자 확대를 막아 접근성을 해치는 설정입니다. 오디오 파형 드래그와의 충돌을 피하려는 선택이지만, 저시력 사용자를 배제할 수 있어 재검토 대상으로 남겨 둡니다.


SoundBridge — 당신의 음악 언어로 국악을 만나세요. 본 사이트는 개발 문서 자료입니다.

This site uses Just the Docs, a documentation theme for Jekyll.