4) 프리셋 브릿지 설계

💡 쉽게 말하면: DISCOVER에서 마음에 드는 국악을 찾았는데, CREATE로 넘어가서 필터를 처음부터 다시 고르게 하면 사용자는 거기서 이탈합니다. 그래서 매칭된 트랙의 악기·감성·BPM을 주소창 파라미터로 만들어 넘깁니다. CREATE 페이지가 열릴 때 필터가 이미 채워져 있습니다.

6-1. 왜 URL인가

브릿지를 만드는 방법은 여러 가지입니다. 전역 상태, 세션 저장소, 서버 세션. 그런데 URL을 골랐습니다.

방식 문제
전역 상태(Context/Zustand) 새로고침하면 사라짐, 링크 공유 불가
sessionStorage 탭 간 공유 불가, 브라우저 정책 영향
서버 세션 익명 사용자에게 세션을 발급해야 함 (MVP에 인증 없음)
쿼리 파라미터 공유 가능 · 새로고침 견딤 · 서버 상태 0 · 북마크 가능

프리셋은 상태가 아니라 주소입니다. 그래서 URL이 맞습니다.

6-2. 변환 규칙

class CreatePresetInteractor(CreatePresetUseCase):
    """
    DISCOVER 매칭 결과(악기·감성·BPM)를 CREATE 페이지
    쿼리 파라미터 URL로 변환한다.
    외부 IO 없음 — 순수 도메인 로직.
    """

    BPM_MARGIN = 20
    BPM_MIN_FLOOR = 60
    BPM_MAX_CEIL = 200

    def build_preset_url(self, command: CreatePresetCommand) -> CreatePresetResult:
        bpm_min = max(self.BPM_MIN_FLOOR, command.bpm - self.BPM_MARGIN)
        bpm_max = min(self.BPM_MAX_CEIL, command.bpm + self.BPM_MARGIN)
        ...
        return CreatePresetResult(..., full_url=f"/create?{query_string}")
파라미터 값 규칙
instrument 트랙의 악기 있을 때만 포함
emotion emotion_tags[0] sort_order = 0인 대표 감성
bpm_min max(60, bpm − 20) 하한 60
bpm_max min(200, bpm + 20) 상한 200

변환 예시

매칭 트랙 생성되는 URL
가야금 · 서정 · BPM 100 /create?instrument=가야금&emotion=서정&bpm_min=80&bpm_max=120
장구 · 신남 · BPM 70 /create?instrument=장구&emotion=신남&bpm_min=60&bpm_max=90
대금 · 신비 · BPM 190 /create?instrument=대금&emotion=신비&bpm_min=170&bpm_max=200

±20 마진을 두는 이유

정확히 같은 BPM만 걸면 결과가 0건이 되기 쉽습니다. 500건 규모에서 특정 악기 + 특정 감성 + 정확한 BPM 교집합은 대부분 비어 있습니다. ±20은 “체감상 같은 템포”의 범위이면서 결과가 남는 폭입니다.

하한 60·상한 200은 프론트엔드 FilterPanel의 BPM 슬라이더 범위와 일치시켰습니다. 범위를 벗어난 값을 넘기면 슬라이더가 표현할 수 없는 상태가 되기 때문입니다.

6-3. 왜 외부 IO가 없는가

CreatePresetInteractor는 DB도, API도, 캐시도 건드리지 않습니다. 입력을 받아 문자열을 만들 뿐입니다.

이 설계의 실질적 이득:

이득 설명
단위 테스트가 즉시 가능 목(mock) 없이 build_preset_url()만 호출하면 검증 끝
개발 순서를 앞당김 명세서 Phase 3에서 DB·Gemini 연동 이전에 완성
프론트 병렬 개발 URL 규격이 먼저 확정되면 프론트가 백엔드 없이 CREATE를 만들 수 있음

명세서의 구현 순서에도 이 판단이 그대로 반영돼 있습니다.

Phase 3 — CREATE 프리셋 유스케이스
  외부 IO 없음 — 순수 로직. 프론트 연동 전에 단독 테스트 완료

6-4. 서버 측 부착 지점

프리셋 URL은 DISCOVER Router에서 응답에 붙습니다.

result = await use_case.discover(command=command)
for track in result.tracks:
    if track.emotion_tags:
        preset = preset_use_case.build_preset_url(
            CreatePresetCommand(
                track_id=track.track_id,
                instrument=track.instrument,
                emotion=track.emotion_tags[0],
                bpm=track.bpm,
            )
        )
        track.preset_url = preset.full_url

emotion_tags가 비어 있으면 프리셋을 만들지 않습니다(preset_url은 None). 프론트엔드 CreateBridgeButton은 이 값이 없으면 버튼을 렌더하지 않습니다. 감성 태그가 없는 트랙에는 브릿지가 걸리지 않는다는 뜻입니다.

6-5. 클라이언트 측 수신

프론트엔드는 useCreatePreset 훅으로 쿼리를 읽습니다.

// lib/constants.ts — 서버와 합의된 파라미터 키
export const PRESET_PARAMS = {
  instrument: 'instrument',
  emotion:    'emotion',
  bpmMin:     'bpm_min',
  bpmMax:     'bpm_max',
} as const;
// app/(public)/create/page.tsx
useEffect(() => {
  if (hasPreset) {
    setFilters({
      instruments: preset.instrument ? [preset.instrument] : [],
      jangdans: [],
      emotions: preset.emotion ? [preset.emotion] : [],
      bpmMin: preset.bpmMin || 60,
      bpmMax: preset.bpmMax || 200,
      loopUnit: null,
      license: 'all',
    });
    setShowPresetBanner(true);
  }
}, [hasPreset, preset.instrument, preset.emotion, preset.bpmMin, preset.bpmMax]);

PresetBanner가 상단에 떠서 “DISCOVER에서 넘어온 필터”임을 알려 줍니다. 사용자가 필터를 직접 만진 게 아닌데 값이 채워져 있으면 혼란스럽기 때문에, 출처를 명시합니다.

6-6. 계약을 지키는 방법

프리셋 URL은 백엔드와 프론트엔드가 문자열로 맺은 계약입니다. 타입 시스템이 지켜 주지 않습니다.

위험 방어
파라미터 키 오타 양쪽 모두 상수로 선언 (PRESET_PARAMS / Interactor의 params 딕셔너리)
감성 값 불일치 프론트 EMOTIONS 상수와 도메인 EmotionTag enum이 같은 6종
악기 값 불일치 프론트 INSTRUMENTS 상수와 도메인 Instrument enum 대조
BPM 범위 이탈 Interactor에서 max/min으로 클램프, 프론트에서 \|\| 60 / \|\| 200 폴백

잔여 과제: 파라미터 키·감성·악기 목록의 일치는 현재 문서와 코드 리뷰로만 보장됩니다. 계약 테스트(백엔드가 만든 URL을 프론트 파서에 넣어 왕복 검증)를 추가하면 회귀를 자동으로 잡을 수 있습니다. v1.1 과제입니다.


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

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