프론트엔드백엔드
출시 직후 UX 보강 — 공통 UI 컴포넌트 + 글로벌 검색 + 알림 센터 + 메이트 신고 자동 차단 — POP-SPOT v2.18 (+v2.18.1)
동
김동현운영 출시 직후 사용자 피드백과 자체 점검에서 나온 UX 보강. v2.18 (1차) 공통 UI 컴포넌트 · 글로벌 검색 → v2.18.1 (2차) 알림 센터 · 메이트 신고 자동 차단 · 위시 만료 D-3 메일.
운영 출시 직후 사용자가 겪은 피드백이 있었고 자체 점검으로 나온 항목을 짧은 사이클로 두 번에 걸쳐 패치. 1차는 빈 화면·로딩·에러 표현의 통일, 2차는 알림 모아 보기와 신고 동작.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| 공통 UI 컴포넌트 | 여러 화면에서 같은 디자인/동작으로 재사용하는 React 컴포넌트 |
| 딥링크 | 특정 화면의 특정 위치로 바로 이동시켜주는 URL |
| SLA | Service Level Agreement. 응대 시간 약속 |
v2.18 — UX 1차
EmptyState 공통 컴포넌트
typescript
// v2.18 — src/components/common/EmptyState.tsx (신규)
type Props = {
icon?: React.ReactNode;
title: string;
description?: string;
action?: { label: string; href: string };
};
export function EmptyState({ icon, title, description, action }: Props) {
return (
<div className="flex flex-col items-center justify-center py-12 px-6 text-center">
{icon && <div className="mb-3 text-zinc-400">{icon}</div>}
<h3 className="text-sm font-medium text-zinc-700 mb-1">{title}</h3>
{description && (
<p className="text-xs text-zinc-500 max-w-xs">{description}</p>
)}
{action && (
<Link href={action.href}
className="mt-4 px-4 py-2 text-xs font-medium rounded-full bg-lime-400 text-zinc-900">
{action.label}
</Link>
)}
</div>
);
}해석 — 한 줄씩
글로벌 검색 헤더
파일: popspot-frontend/src/components/layout/Header.tsx
typescript
// v2.18 — Header.tsx 안에 글로벌 검색 폼
const [searchQuery, setSearchQuery] = useState('');
<form onSubmit={e => {
e.preventDefault();
router.push(`/?q=${encodeURIComponent(searchQuery)}`);
}}>
<input
type="text"
placeholder="팝업을 검색해보세요"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</form>해석
최근 본 팝업 5 개
파일: popspot-frontend/src/lib/usePopupVisitHistory.ts (v2.18 신규)
typescript
// v2.18 — src/lib/usePopupVisitHistory.ts (신규). 최근 본 팝업 5개 관리
const KEY = 'popspot.visitHistory';
const MAX = 5;
export function recordVisit(popupId: number, popupName: string) {
const raw = localStorage.getItem(KEY);
const list = raw ? JSON.parse(raw) : [];
const filtered = list.filter(v => v.id !== popupId);
const next = [{ id: popupId, name: popupName, visitedAt: Date.now() }, ...filtered].slice(0, MAX);
localStorage.setItem(KEY, JSON.stringify(next));
}해석
v2.18.1 — UX 2차
알림 센터
파일: popspot-backend/src/main/java/com/popspot/entity/Notification.java (v2.18.1 신규)
java
// v2.18.1 — Notification 엔티티 신규
@Entity
public class Notification {
@Id @GeneratedValue
private Long id;
@Column(nullable = false)
private Long userId;
@Column(nullable = false)
private String kind;
@Column(nullable = false)
private String title;
private String body;
private String deepLink;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime readAt;
}해석
메이트 신고 + 자동 차단
파일: popspot-backend/src/main/java/com/popspot/service/MateBoardService.java
java
// v2.18.1 — MateBoardService.reportPost(). 3회 신고 시 자동 숨김
@Transactional
public void reportPost(Long postId, Long reporterId, String reason) {
MatePost post = matePostRepository.findById(postId).orElseThrow();
post.addReport(reporterId, reason);
if (post.getReportCount() >= 3) {
post.setHidden(true);
emailService.sendOperatorAlert(
"메이트 글 자동 숨김",
"postId: " + postId + ", reports: " + post.getReportCount()
);
}
}해석
위시 만료 D-3 메일
파일: popspot-backend/src/main/java/com/popspot/scheduler/WishlistExpirationScheduler.java (v2.18.1 신규)
java
// v2.18.1 — WishlistExpirationScheduler. 매일 09:00 위시 D-3 메일
@Scheduled(cron = "0 0 9 * * *", zone = "Asia/Seoul")
public void notifyExpiringWishlist() {
LocalDate target = LocalDate.now(clock).plusDays(3);
List<Wishlist> expiring = wishlistRepository
.findByPopupEndDateAndNotifiedFalse(target);
Map<Long, List<Wishlist>> byUser = expiring.stream()
.collect(Collectors.groupingBy(Wishlist::getUserId));
byUser.forEach((userId, wishes) -> {
User user = userRepository.findById(userId).orElseThrow();
emailService.sendWishExpiring(user.getEmail(), wishes);
wishes.forEach(w -> w.markNotified());
});
}해석
핵심 파일
관련 글
이전 글Caffeine API 캐싱 + DB 인덱스 V10 + 약관 재동의 + a11y 폴리쉬 — POP-SPOT v2.19 · v2.20 (+v2.20.1/2)다음 글운영 출시 직전 12 개 핫픽스 — 회원 탈퇴(PIPA) · DB 자동 백업 · SLA 알림 · CSP / HSTS · brute-force 잠금 · SEO 보강 — POP-SPOT v2.17 (+v2.17.2/3)
관련 글
인트로 페이지 제거 → 루트가 메인 직행 + AuthGuard 스피너 게이트 제거로 SEO 색인 해결 — POP-SPOT v2.23 (+v2.23.1/2)발견형 서비스인데 모든 방문자가 5 섭션 인트로→ENTER 거쳐야 메인 도달. 이탈을 키우는 구조라 제거. v2.23.1·2 은 SEO 색인 이시합 해결 — /popups/[slug] 이 크롤러를 /login 으로 튕긴게 수정하고, AuthGuard 의 스피너 게이트를 제거해 공개 페이지가 실제 HTML로 서버 렌더링 되도록.전면 보안 감사 + 수정 — IDOR 5 곳 · 저장형 XSS · 메모리/스토리지 누수 · 클린코드 점검 — POP-SPOT v2.22인증·인가·인젝션·암호화·프론트·컴플라이언스 6 영역 병렬 감사. C1 IDOR (Stamp/MyPage/Mate/ChatFile) + C2 저장형 XSS (카카오 로드뷰 오버레이) + H1 인증 없는 업로드 + H2/H3 무한 증가 인메모리 맵 + H4 SSE emitter 누수 + H5 토큰 노출. 동시에 운영 점검 4 건 (신고 어뻐징 · 크롤링 입력 정제 · 이메일 열거 · 백업 하드닝).Spotify OAuth + Web Playback SDK 통합 — 3-tier 재생 엔진 (Premium SDK / iTunes preview / YouTube) — POP-SPOT v2.21 S10~S18Spotify OAuth 백엔드(AES-256-GCM 토큰 암호화) + Web Playback SDK 프론트 통합 + iTunes preview 폴백 + YouTube 폴백의 3-tier 재생 엔진. Premium 은 320kbps 풀트랙, Free/미연결은 30~90초 preview, 외엔 YouTube 폴백. S17/S18 에서 운영 버그 네 건 추가 수정.