의견 받는 공식 채널 만들기 — Footer + MY 탭 + 어드민 3 레이어 통합 — POP-SPOT v2.11
사용자 피드백을 이메일·카톡으로 수기 추적하던 걸 /feedback 페이지 + MY 탭 카드 + 어드민 FEEDBACK 탭 세 곳으로 통합. 게스트도 허용, 화이트리스트는 FeedbackService 한 곳에. V7 마이그레이션.
사용자 피드백을 받는 공식 채널이 없어 산발적으로 분실됨. 이메일·카톡·인스타 DM 을 수기 추적하는 구조는 한계. Footer + MY + 어드민 세 곳을 같은 데이터 모델로 통합.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| permitAll | Spring Security 용어. "이 경로는 로그인 안 해도 접근 가능" |
| nullable user | DB 테이블의 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
// 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 이후 계속 수정됨)
// 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 마이그레이션 신규)
-- 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 탭의 "내가 보낸 의견" 카드에 바로 뜨는 걸 확인. 답변이 달릴 때 같이 표시됨.