프론트엔드백엔드

출시 직후 UX 보강 — 공통 UI 컴포넌트 + 글로벌 검색 + 알림 센터 + 메이트 신고 자동 차단 — POP-SPOT v2.18 (+v2.18.1)

김동현
··4분 읽기

운영 출시 직후 사용자 피드백과 자체 점검에서 나온 UX 보강. v2.18 (1차) 공통 UI 컴포넌트 · 글로벌 검색 → v2.18.1 (2차) 알림 센터 · 메이트 신고 자동 차단 · 위시 만료 D-3 메일.

운영 출시 직후 사용자가 겪은 피드백이 있었고 자체 점검으로 나온 항목을 짧은 사이클로 두 번에 걸쳐 패치. 1차는 빈 화면·로딩·에러 표현의 통일, 2차는 알림 모아 보기와 신고 동작.

이 글에서 다루는 것

  • 공통 UI 컴포넌트 3 종 (EmptyState / LoadingSpinner / ErrorState)
  • 글로벌 검색 헤더 — 어느 탭에서든 머릿속 키워드로 바로 진입
  • 온보딩 모달 — 첫 방문자에게 3 단계 소개
  • 최근 본 팝업 5 개 표시
  • v2.18.1 알림 센터, 메이트 신고 자동 차단, 위시 만료 D-3 메일
  • 모르는 단어 한 줄로

    용어한 줄 설명
    공통 UI 컴포넌트여러 화면에서 같은 디자인/동작으로 재사용하는 React 컴포넌트
    딥링크특정 화면의 특정 위치로 바로 이동시켜주는 URL
    SLAService 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>
      );
    }

    해석 — 한 줄씩

  • icon? description? action? 세 곳 다 선택적. 최소는 title 만 있어도 동작
  • py-12 px-6 수직 여백 넓게. 빈 화면은 시각적으로 숨돌릴 곳이 필요
  • max-w-xs description 320 px 이하 제한 — 한 줄이 너무 길면 가독성 떨어짐
  • action 은 Link 로 SPA 네비게이션 (페이지 새로고침 없이 이동)
  • 글로벌 검색 헤더

    파일: 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>

    해석

  • router.push('/?q=...') — v2.15.2 의 deep link 화이트리스트에 q 가 있어 메인의 SearchBox 가 자동 검색
  • encodeURIComponent — 한글·특수문자 URL 안전 처리
  • 최근 본 팝업 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));
    }

    해석

  • 기존 리스트에서 같은 popupId 제거 → 맨 앞에 새로 추가 → 5 개로 자름. 결과는 최근 본 순서
  • localStorage 라 서버 전송 0. 개인 브라우저에만

  • 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;
    }

    해석

  • kind — 알림 종류 (FEEDBACK_REPLY / WISH_EXPIRING 등). 프론트에서 아이콘·색 분기
  • deepLink — 클릭 시 이동할 내부 경로. v2.15.2 의 deep link 쿼리 활용
  • readAt nullable — NULL 이면 안 읽음 → 아이콘 빨간 점
  • 메이트 신고 + 자동 차단

    파일: 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()
        );
      }
    }

    해석

  • 3 회 임계값 — 출시 직후라 하드코딩. 어드민에서 조정하는 게 이상적이지만 일단 빠르게
  • setHidden(true) — 물리 삭제가 아닌 숨김. 운영자 검토 후 복구 가능
  • 운영 메일 — 자동 차단 발생 시 반드시 사람이 확인
  • 위시 만료 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());
      });
    }

    해석

  • plusDays(3) — 3 일 뒤 끝나는 팝업을 오늘 찾아 메일 (D-3)
  • notifiedFalse — 이미 보낸 row 제외. 중복 알림 방지
  • groupingBy(userId) — 같은 사용자에게 여러 팝업이 동시 만료되면 메일 한 통으로 묶음
  • markNotified() — 플래그 세팅으로 다음 날 재알림 0

  • 핵심 파일

  • popspot-frontend/src/components/common/EmptyState.tsx (신규)
  • popspot-frontend/src/components/common/LoadingSpinner.tsx (신규)
  • popspot-frontend/src/components/common/ErrorState.tsx (신규)
  • popspot-frontend/src/components/layout/Header.tsx (글로벌 검색)
  • popspot-frontend/src/lib/usePopupVisitHistory.ts (신규)
  • popspot-backend/.../entity/Notification.java (신규)
  • popspot-backend/.../service/WishlistExpirationScheduler.java (신규)

  • 관련 글

  • 이전 — v2.17, 출시 직전 12 개 핫픽스
  • 다음 — v2.19/v2.20, Caffeine 캐싱 + 약관 재동의
  • 공유

    댓글