프론트엔드백엔드

의견 받는 공식 채널 만들기 — Footer + MY 탭 + 어드민 3 레이어 통합 — POP-SPOT v2.11

김동현
··4분 읽기

사용자 피드백을 이메일·카톡으로 수기 추적하던 걸 /feedback 페이지 + MY 탭 카드 + 어드민 FEEDBACK 탭 세 곳으로 통합. 게스트도 허용, 화이트리스트는 FeedbackService 한 곳에. V7 마이그레이션.

사용자 피드백을 받는 공식 채널이 없어 산발적으로 분실됨. 이메일·카톡·인스타 DM 을 수기 추적하는 구조는 한계. Footer + MY + 어드민 세 곳을 같은 데이터 모델로 통합.

이 글에서 다루는 것

  • 의견을 받는 3 레이어 — 사용자가 보내는 곳 (Footer → /feedback) / 사용자가 확인하는 곳 (MY 탭) / 운영자가 검수하는 곳 (어드민 FEEDBACK)
  • 게스트 사용자 허용 + guestEmail 선택 필드 구조
  • "의견" 관련 화이트리스트를 한 곳에서 관리
  • DB 마이그레이션 V7 의 구조
  • 모르는 단어 한 줄로

    용어한 줄 설명
    permitAllSpring Security 용어. "이 경로는 로그인 안 해도 접근 가능"
    nullable userDB 테이블의 user 컬럼 이 NULL 이어도 괜찮은 구조. 게스트 의견 수용용
    화이트리스트"버그·개선·제안·다른 건" 같이 허용 값만 모아둔 목록

    3 레이어 구성

    레이어v2.10 까지v2.11
    Footer 링크공식 동선 없음"의견 보내기" → /feedback
    MY 탭본인 의견 · 답변 확인 못 함"내가 보낸 의견" 카드 (최근 3 건 + 전체 보기)
    어드민이메일/카톡 수기 추적FEEDBACK 탭 · 상태 카운트 4 + 필터 + 펼침형 답변 에디터

    왜 이렇게 했음

    공식 동선 — 의견을 DM·메일로 받으면 SLA ("며칠 안에 답변") 가 들쎀날숈 해지고 한 사람이 보낸 걸 잊는 경우가 생긴다. 공식 동선으로 모으면 상태 (접수/계 검토/답변/완료) 를 표준화하고, 처리됨 답변 그대로 사용자에게 돌려줄 수 있다.

    3 레이어 구성 — "보내는 곳 (Footer/feedback) · 확인하는 곳 (MY) · 검수하는 곳 (어드민)" 세 군데가 같은 테이블을 본다. 관점이 다를 뿐이므로 한 곳의 상태가 세 곳에 즉시 반영됨.

    게스트 수용 — 로그인 강제하면 피드백 볼륨 자체가 줄어든다. user 컬럼을 nullable 로 두고 게스트는 guestEmail (선택) 을 적을 수 있게.

    화이트리스트 한 곳 — 의견 종류 ("버그·개선·제안·다른 건") 을 FeedbackService 의 Set 상수 한 곳에서만 관리. 프론트·컨트롤러·어드민 모두 그 Set 을 참조하도록 수정. 나중에 종류 몇 개 더 추가해도 한 곳만 고치면 끝.


    코드로는 어떻게 (필요한 부분만)

    파일: popspot-backend/src/main/java/com/popspot/service/FeedbackService.java

    java
    // v2.11 — FeedbackService. 의견 종류 화이트리스트를 이 클래스 하나만 소유
    private static final Set<String> ALLOWED_KINDS = Set.of(
    //      ^^^^^^^^^^^^                              ^^^^^^   Java 9+ 불변 Set 팩토리
    //      상수 — static final — 클래스 로딩 한 번만 초기화 · JVM 내 불변
        "bug", "improvement", "suggestion", "other"
    //  이 네 개만 허용됨. 종류 추가 시 여기만 고치면 프론트·컨트롤러 모두 반영
    );
    
    public Feedback create(Long userId, String guestEmail,
    //                     ^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^
    //                     null 가능    게스트용 이메일 (이 둘 중 하나는 필수 - DB CHECK 제약)
                           String kind, String body) {
        if (!ALLOWED_KINDS.contains(kind))
    //                     ^^^^^^^^       포함 안 되면 거부. 클라이언트가 임의 종류 보내도 메타 의미 없음
            throw new BadRequestException("알 수 없는 의견 종류");
        return repository.save(
            Feedback.of(userId, guestEmail, kind, body));
    //      ^^^^^^^^^^^                                   정적 팩토리 메서드. status='received' 자동 할당
    }

    파일: popspot-backend/src/main/java/com/popspot/config/SecurityConfig.java (v1.1 이후 계속 수정됨)

    java
    // v2.11 — SecurityConfig. 스프링 시큐리티 보안 규칙 설정
    http.authorizeHttpRequests(a -> a
    //                         ^^^^^   람다 구성자 패턴
        .requestMatchers("/api/feedback/me").authenticated()
    //   ^^^^^^^^^^^^^^^                     ^^^^^^^^^^^^^^^
    //   |                                   "이 경로는 JWT 토큰 검증 통과 필요"
    //   +-- 주의: 가장 구체적인 규칙을 먼저 놓는다. 아래의 /** 보다 우선 적용
        .requestMatchers("/api/feedback/**").permitAll()
    //                                       ^^^^^^^^^^^   "로그인 안 해도 접근 OK" — 게스트 의견 제출 수용
    //   /api/feedback/me 는 위에서 먼저 잡혀 — permitAll 로 떨어지지 않음
        ...
    );

    파일: popspot-backend/src/main/resources/db/migration/V7__feedback.sql (Flyway 마이그레이션 신규)

    sql
    -- v2.11 — Flyway 마이그레이션 V7__feedback.sql. feedback 테이블 신규 생성
    CREATE TABLE feedback (
      id            BIGSERIAL PRIMARY KEY,
      --            ^^^^^^^^^^^^^^^^^^^^^   PostgreSQL: BIGSERIAL = bigint + 자동증가 시퀀스 (PK)
      user_id       BIGINT NULL,           -- v2.0 의 게스트 흐름 수용 되도록 NULL 허용
      guest_email   VARCHAR(255) NULL,     -- 게스트가 선택적으로 입력 (답변 받아볼 수신 수단)
      kind          VARCHAR(32) NOT NULL,  -- ALLOWED_KINDS 의 문자열 (bug/improvement/...)
      body          TEXT NOT NULL,         -- 의견 본문. 길이 제한 없음 (TEXT)
      status        VARCHAR(32) NOT NULL,  -- 상태 머신: received → reviewing → answered → closed
      answer        TEXT NULL,             -- 관리자 답변. 아직 답하기 전은 NULL
      created_at    TIMESTAMP NOT NULL DEFAULT NOW(),
      --                                   ^^^^^^^^^   PostgreSQL 함수: 현재 서버 시각. UTC 기준 저장
      CONSTRAINT chk_user_or_email
      --         ^^^^^^^^^^^^^^^^^^^^   제약 조건 이름. ALTER TABLE 으로 제거·추가 용이하도록 명명
        CHECK (user_id IS NOT NULL OR guest_email IS NOT NULL)
      --       ^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^   둘 중 하나는 있어야 함
      --                                                            INSERT 시 둘 다 NULL 이면 PostgreSQL 이 거절
    );

    핵심 파일: popspot-frontend/src/features/feedback/FeedbackForm.tsx, popspot-frontend/src/features/feedback/MyFeedbackList.tsx, popspot-frontend/src/features/feedback/AdminFeedbackPanel.tsx, popspot-backend/src/main/resources/db/migration/V7__feedback.sql


    직접 보는 법

    하단 Footer 의 "의견 보내기" 를 눌러 popspot.co.kr/feedback 페이지에서 의견 제출 → 로그인 상태면 MY 탭의 "내가 보낸 의견" 카드에 바로 뜨는 걸 확인. 답변이 달릴 때 같이 표시됨.


    관련 글

  • 이전 — v2.10, 어드민 실시간 로그
  • 다음 — v2.12, 의견 메인 탭 승격 + 등급별 부스트
  • 공유

    댓글