의견 메인 탭 승격 + 코스 무료 슬롯 폐지 + 등급별 월 부스트 한도 — POP-SPOT v2.12
Footer + MY 카드 만으로는 "있는 줄도 모름" → BottomDock 메인 탭으로 승격. 무료 사용자 코스 1 개 제한 폐지. "확성기 소모성 아이템" 대신 등급별 월 부스트 (MASTER 5/HUNTER 3/BEGINNER 1).
v2.11 의 의견 기능을 Footer + MY 카드로만 둘 수준에서는 있는 줄도 모르는 사람이 좀 많았다. 동시에 코스 무료 슬롯 제한·확성기 소모성 아이템 두 정책도 같이 재설계.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| BottomDock | 모바일 하단의 고정 메뉴바. 아이콘 4~5 개로 주요 탭 이동 |
| 부스트 | 동행 게시판의 글을 상단에 노출시키는 일. 광고 느낌이 아닌 "프롤·특별" 느낌을 노림 |
| BEGINNER/HUNTER/MASTER | 스탬프 개수에 따른 등급. 3/6/12 스탬프 임계값 |
| 단일 진실의 원천 | 같은 숫자 (3/6/12 등) 가 코드 상 한 곳에서만 정의되도록 하는 원칙 |
무엇이 바뀌었나
| 항목 | v2.11 까지 | v2.12 |
|---|---|---|
| 의견 동선 | Footer + MY 카드 (limit=3) | BottomDock 메인 탭 (lucide Inbox) |
| 코스 저장 슬롯 | 무료 1 개 (새로 저장하면 자동 삭제) | 모든 사용자 무제한 |
| 동행 상단 노출 | "확성기 사용" (소모성 아이템) | 등급별 월 부스트 한도 (MASTER 5 / HUNTER 3 / BEGINNER 1 / NONE 0) |
| 등급의 의미 | RankCard 에 표시만 | 스탬프 → 등급 → 부스트 한도 연동 |
| 표현 톤 | 📢 · 🔥 · AD 아이콘 | 평서체 + lucide TrendingUp |
왜 이렇게 했음
메인 탭 승격 — v2.11 의 Footer 링크는 사용자 입장에서 "한 번도 볼 일이 없는 공간" 이다. BottomDock 은 항상 눈에 있으니 의견이 솤아조있는 상태. "의견" 아이콘이 있으면 이게 있는 서비스구나 명확하게 드러난다.
도리어 UX 마이너스 — "무료는 1 개" 제한이 계속 계산되면 사용자가 "다음에 읽을 건데 입력하면 이전 게 사라지는 건가" 를 매번 신경 써야 함. 수익화 자체가 필요한 시점에 다시 고민하는 게 낫다.
등급별 월 부스트 — 확성기 소모성 아이템은 광고체대와 닮아 입다. "월에 몇 번은 넘은 사람" 은 상품·돈 찴고 "원장·특별·경험 많은 사람" 을 자이아론이 먴한다. 등급을 "스탬프 모은 사람" 으로 연결시키고 서비스 아이던티티와 더 잘 맞다.
단일 진실 — 임계 3/6/12 를 각각 프론트 (src/lib/rank.ts) + 백엔드 (BoostPolicy.java) 에 둘었는데 둘 다 같은 값. JavaDoc·JSDoc 에 "동시 갱신 경고" 을 명시해 둕. 다음에 기준이 바뀌면 두 곳 모두 수정.
코드로는 어떻게 (필요한 부분만)
파일: popspot-frontend/src/lib/rank.ts (v2.12 신규)
// v2.12 — src/lib/rank.ts. 프론트 장의 등급 임계
export const RANK_THRESHOLD = { BEGINNER: 3, HUNTER: 6, MASTER: 12 } as const;
// ^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^
// 스탬프 3개 6개 12개
// as const 은 추론된 타입을 최소로 고정. RANK_THRESHOLD.MASTER 가 number 가 아닌 12 리터럴 타입으로 고정됨
export function rankOf(stamps: number) {
// 위: 입력 stamps (수집한 스탬프 개수) 에 따른 등급 계산
if (stamps >= RANK_THRESHOLD.MASTER) return 'MASTER';
if (stamps >= RANK_THRESHOLD.HUNTER) return 'HUNTER';
if (stamps >= RANK_THRESHOLD.BEGINNER) return 'BEGINNER';
// 위: 높은 등급부터 차례대로 검사. 순서 중요 — 반대로 하면 12보다 큰 수도 BEGINNER 로 결론 내려지게 됨
return 'NONE';
// 위: 스탬프 0~2개 — 아직 아무 등급도 못 달은 가입자. 부스트 쿠타 0.
}파일: popspot-frontend/src/lib/boost.ts (v2.12 신규)
// v2.12 — src/lib/boost.ts. 등급별 월간 부스트 사용 한도
export const BOOST_QUOTA = {
MASTER: 5, HUNTER: 3, BEGINNER: 1, NONE: 0,
// ^ ^ ^ ^ 등급이 높을수로 동행 부스트 획득 기회 많이
// "돈 내면 키워" 가 아닌 "스탬프 모으면 더 송출" 원리
} as const;파일: popspot-backend/src/main/java/com/popspot/service/BoostPolicy.java (v2.12 신규)
// v2.12 — BoostPolicy.java (백엔드). 프론트와 동일한 수치 — 둘 같이 갱신
public enum Rank {
// ^^^^ 자바 enum. 타입 안전 — Rank.MASTER 형식으로 쓰며 switch 에서 누락 감지 가능
MASTER(12, 5), HUNTER(6, 3), BEGINNER(3, 1), NONE(0, 0);
// ^^ ^ ^ ^ ^ ^ ^ ^
// | | | | | | | +-- monthlyBoost (등급별 월 한도)
// | | | | | | +----- minStamps (이 이상이면 해당 등급)
// | | | | | +-------------- BEGINNER: 3 스탬프 이상 + 월 1회 부스트
// | | | +------------------------------- HUNTER: 6 스탬프 + 월 3회
// | +----------------------------------------- MASTER: 12 스탬프 + 월 5회
private final int minStamps;
private final int monthlyBoost;
// ^^^^^^^^^^^^ 이 값을 BoostUsage.used_count 와 비교해 이번 달 추가 사용 가능 여부 판단
}파일: popspot-backend/src/main/resources/db/migration/V8__boost.sql
-- v2.12 — V8__boost.sql. 동행 게시글의 부스트 상태·사용 관리
ALTER TABLE mate_post
ADD COLUMN boosted_until TIMESTAMP NULL;
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 이 시각까지 상단 노출. NULL 이면 부스트 안 된 일반 게시글
-- 조회 시 NOW() < boosted_until 조건을 ORDER BY 원조건으로 상단 정렬
CREATE TABLE boost_usage (
user_id BIGINT NOT NULL,
year_month CHAR(7) NOT NULL, -- '2026-05' 표기. 월 단위 추적
used_count INT NOT NULL DEFAULT 0, -- 이번 달에 쓴 부스트 회수. BoostPolicy.monthlyBoost 와 비교
PRIMARY KEY (user_id, year_month)
-- ^^^^^^^^^^^^^^^^^^^^^^ 복합 PK. user·월 조합 1 개 레코드 유일.
-- INSERT ... ON CONFLICT (user_id, year_month) DO UPDATE SET used_count=used_count+1
-- 패턴으로 upsert — 있으면 증가, 없으면 삽입
);핵심 파일: popspot-frontend/src/lib/rank.ts, popspot-frontend/src/lib/boost.ts, popspot-frontend/src/components/layout/BottomDock.tsx, popspot-backend/.../service/BoostPolicy.java (신규)