diff --git a/src/app/router.tsx b/src/app/router.tsx index c9c579d..0240521 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -13,6 +13,7 @@ import EventsDetailScreen from "@/features/events/EventsDetailScreen"; import EventsListScreen from "@/features/events/EventsListScreen"; import FeedbacksDetailScreen from "@/features/feedbacks/FeedbacksDetailScreen"; import FeedbacksListScreen from "@/features/feedbacks/FeedbacksListScreen"; +import FeedbacksNewScreen from "@/features/feedbacks/FeedbacksNewScreen"; import HomeScreen from "@/features/home/HomeScreen"; import NoticesDetailScreen from "@/features/notices/NoticesDetailScreen"; import NoticesListScreen from "@/features/notices/NoticesListScreen"; @@ -45,6 +46,15 @@ const routes = [ handle: { background: "normal" } satisfies ScreenRouteHandle, path: "/feedbacks", }, + { + element: , + // Bottom Nav 대신 하단 고정 버튼(Action Area)이 있는 화면. Figma 루트 배경도 흰 면이다. + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/feedbacks/new", + }, { element: , // 상세(모아보기) 화면 — 목록 화면과 같은 흰 배경, Bottom Nav 없이 뒤로가기(닫기)로만 나간다. diff --git a/src/components/ui/ScreenToast.tsx b/src/components/ui/ScreenToast.tsx index 4d2385f..3a86225 100644 --- a/src/components/ui/ScreenToast.tsx +++ b/src/components/ui/ScreenToast.tsx @@ -8,14 +8,25 @@ interface ScreenToastProps { open: boolean; message: string; onOpenChange: (open: boolean) => void; + /** + * negative: 실패 안내(느낌표, Figma 1450:93026). positive: 성공 안내(체크, Figma 1410:50080) — + * WDS 기본 아이콘(IconCircleCheckFill)이 Figma와 이미 일치해서 따로 넘기지 않는다. + */ + variant?: "negative" | "positive"; } -// Figma: Toast/Toast (nodeId 1450:93026). 배경·흐림·여백·모서리·글자가 모두 WDS Toast 기본값이라 -// 그대로 쓰고, 두 가지만 우리가 맡는다. +// Figma: Toast/Toast (nodeId 1450:93026 negative / 1410:50080 positive). 배경·흐림·여백·모서리· +// 글자가 모두 WDS Toast 기본값이라 그대로 쓰고, 두 가지만 우리가 맡는다. // 1. 위치 — WDS 기본 배치는 자체 영역(#wds-region-manager-bottom)이라 화면 프레임을 벗어난다. // 화면 포털에 직접 그려서 Figma 좌표(좌우 20px, 아래에서 100px)에 놓는다. // 2. 아이콘 — negative variant의 기본 아이콘은 X(Circle Close)인데 Figma는 느낌표라 직접 넘긴다. -function ScreenToast({ open, message, onOpenChange }: ScreenToastProps) { +// positive는 기본 아이콘(체크)이 Figma와 그대로 맞아서 커스텀하지 않는다. +function ScreenToast({ + open, + message, + onOpenChange, + variant = "negative", +}: ScreenToastProps) { const portalEl = useScreenSheetPortal(); if (!portalEl) { @@ -37,17 +48,21 @@ function ScreenToast({ open, message, onOpenChange }: ScreenToastProps) { maxWidth: "100%", }, })} - variant="negative" + variant={variant} > - - - {/* 느낌표는 아이콘에서 뚫린 자리라 반투명한 토스트 배경이 그대로 비친다. - WDS 기본 아이콘과 같은 방식으로 흰 바탕을 뒤에 깔아 하얗게 보이게 한다. */} - - - - + {variant === "negative" ? ( + + + {/* 느낌표는 아이콘에서 뚫린 자리라 반투명한 토스트 배경이 그대로 비친다. + WDS 기본 아이콘과 같은 방식으로 흰 바탕을 뒤에 깔아 하얗게 보이게 한다. */} + + + + + ) : ( + + )} {message} diff --git a/src/features/feedbacks/FeedbacksListScreen.tsx b/src/features/feedbacks/FeedbacksListScreen.tsx index 7d25e23..87b2716 100644 --- a/src/features/feedbacks/FeedbacksListScreen.tsx +++ b/src/features/feedbacks/FeedbacksListScreen.tsx @@ -5,11 +5,12 @@ import { Typography, } from "@wanteddev/wds"; import { IconBell, IconPlus, IconSearch } from "@wanteddev/wds-icon"; -import { Fragment, useRef, useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Fragment, useEffect, useRef, useState } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import FilterChipGroup from "@/components/ui/FilterChipGroup"; import ScreenHeader from "@/components/ui/ScreenHeader"; +import ScreenToast from "@/components/ui/ScreenToast"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; import FeedbacksCard from "@/features/feedbacks/components/FeedbacksCard"; import FeedbacksQaCard from "@/features/feedbacks/components/FeedbacksQaCard"; @@ -30,12 +31,30 @@ function getCarouselItemWidth(el: HTMLDivElement): number { return firstCard.getBoundingClientRect().width + gap; } -// Figma: 게시판 - 열린피드백 (nodeId 1410:50011) +interface FeedbacksListLocationState { + feedbackSent?: boolean; +} + +// Figma: 게시판 - 열린피드백 (nodeId 1410:50011), 피드백 전송 완료 토스트 (nodeId 1410:50080) function FeedbacksListScreen() { const navigate = useNavigate(); + const location = useLocation(); const [roundFilter, setRoundFilter] = useState("all"); const carouselRef = useRef(null); const [carouselPage, setCarouselPage] = useState(1); + // 피드백 작성 화면에서 넘어올 때만 history state로 신호를 받는다(navigate state) — 그 외 + // 진입(바텀 탭 등)에서는 안 뜬다. 새로고침 시 재노출을 막으려고 받자마자 state를 비운다. + const [isSentToastOpen, setIsSentToastOpen] = useState( + Boolean( + (location.state as FeedbacksListLocationState | null)?.feedbackSent, + ), + ); + + useEffect(() => { + if ((location.state as FeedbacksListLocationState | null)?.feedbackSent) { + navigate(location.pathname, { replace: true, state: null }); + } + }, [location.pathname, location.state, navigate]); // 캐러셀은 가로 스크롤이라 PaginationDots.currentPage를 고정값으로 두면 스크롤해도 첫 점만 // 활성으로 보인다(/pr-check 리뷰 지적) — 스크롤 위치로 현재 카드를 계산해 동기화한다. @@ -176,10 +195,10 @@ function FeedbacksListScreen() { - {/* 피드백 작성 화면은 이번 범위 밖이라 클릭 동작은 아직 없다 */} navigate("/feedbacks/new")} type="button" > @@ -187,6 +206,13 @@ function FeedbacksListScreen() { 피드백 작성 + + ); } diff --git a/src/features/feedbacks/FeedbacksNewScreen.tsx b/src/features/feedbacks/FeedbacksNewScreen.tsx new file mode 100644 index 0000000..6dad8da --- /dev/null +++ b/src/features/feedbacks/FeedbacksNewScreen.tsx @@ -0,0 +1,118 @@ +import { + ActionArea, + ActionAreaButton, + TextArea, + TextAreaContent, + TopNavigationButton, + Typography, +} from "@wanteddev/wds"; +import { IconChevronLeft } from "@wanteddev/wds-icon"; +import { useId, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import ConfirmModal from "@/components/ui/ConfirmModal"; +import ScreenHeader from "@/components/ui/ScreenHeader"; +import { useScreenHeader } from "@/components/ui/useScreenHeader"; + +const FEEDBACKS_QUESTION_MAX_LENGTH = 500; + +// Figma: 피드백 질문 (nodeId 1410:49972), 피드백 질문 확인 모달 (nodeId 1410:49980) +// 실제 제출 API 전까지는 확인 모달에서 "보내기"를 누르면 바로 열린피드백 목록으로 보내고, +// 목록 화면에 완료 토스트(Figma nodeId 1410:50080)를 띄우라고 navigate state로 신호를 넘긴다. +function FeedbacksNewScreen() { + const navigate = useNavigate(); + const [content, setContent] = useState(""); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const headingId = useId(); + + const canSubmit = content.trim().length > 0; + + // 이 화면으로 바로 들어오면(딥링크·새로고침) 뒤로 갈 히스토리가 없어서 navigate(-1)이 + // 아무 일도 하지 않는다 — 행사 신청 화면과 같은 이유로 그럴 땐 목록으로 보낸다. + const goBack = () => { + const historyIndex = (window.history.state as { idx?: number } | null)?.idx; + if (historyIndex !== undefined && historyIndex > 0) { + navigate(-1); + return; + } + navigate("/feedbacks", { replace: true }); + }; + + useScreenHeader( + + + + } + title="피드백 작성" + variant="normal" + />, + ); + + return ( + + + + + 궁금하거나 건의하고 싶은 내용을 자유롭게 적어 주세요. + + setContent(event.target.value)} + placeholder="피드백 내용을 입력하세요." + trailingContent={ + + {FEEDBACKS_QUESTION_MAX_LENGTH} + + } + value={content} + width="100%" + /> + + + + + + setIsConfirmOpen(true)} + sx={{ paddingBlock: "16px" }} + > + 피드백 보내기 + + + + + + setIsConfirmOpen(false)} + onConfirm={() => + navigate("/feedbacks", { + replace: true, + state: { feedbackSent: true }, + }) + } + open={isConfirmOpen} + title="피드백을 보낼까요?" + /> + + ); +} + +export default FeedbacksNewScreen;