diff --git a/src/components/ui/PhotoGallery.tsx b/src/components/ui/PhotoGallery.tsx index 1ca4f8a..4dcdc36 100644 --- a/src/components/ui/PhotoGallery.tsx +++ b/src/components/ui/PhotoGallery.tsx @@ -70,6 +70,10 @@ function PhotoGallery({ ))} + {/* Figma QA(nodeId 2849:60815/60786): 사진 위 뒤로가기가 잘 보이도록 상단에 어두운 + 그라데이션을 깐다. 오버레이 버튼보다 아래(먼저) 그려야 버튼이 그 위에 얹힌다. */} +
+ {overlay} {/* group + 웹 전용(hover:hover) 미디어에서만 화살표를 보여준다 — 터치 기기는 hover 자체가 diff --git a/src/components/ui/ScreenLayout.tsx b/src/components/ui/ScreenLayout.tsx index b746d8b..94a753a 100644 --- a/src/components/ui/ScreenLayout.tsx +++ b/src/components/ui/ScreenLayout.tsx @@ -23,7 +23,16 @@ function getBottomNavValueFromPath(pathname: string): BottomNavValue { const matched = ( Object.entries(BOTTOM_NAV_PATHS) as [BottomNavValue, string][] ).find(([, path]) => path === pathname); - return matched?.[0] ?? "home"; + if (matched) { + return matched[0]; + } + // 열린피드백은 "게시판" 탭 소속이지만 상세(모아보기)·글쓰기 화면은 각자 다른 경로 + // (/feedbacks/:feedbackId, /feedbacks/new)라 위 exact-match로는 안 잡힌다 — 진입 시에도 + // 계속 "게시판"이 활성으로 보여야 한다(Figma QA nodeId 2849:56863). + if (pathname.startsWith("/feedbacks")) { + return "board"; + } + return "home"; } // 화면 전체 배경. 카드형 화면(빌릴게·신청서)은 흰 카드가 뜨도록 회색 여백(alternative)이 필요하고, diff --git a/src/features/events/EventsDetailScreen.tsx b/src/features/events/EventsDetailScreen.tsx index d407a92..94b9cbb 100644 --- a/src/features/events/EventsDetailScreen.tsx +++ b/src/features/events/EventsDetailScreen.tsx @@ -65,7 +65,7 @@ function EventsDetailScreen() { onClick={() => navigate(-1)} variant="icon" > - +
} diff --git a/src/features/feedbacks/FeedbacksListScreen.tsx b/src/features/feedbacks/FeedbacksListScreen.tsx index 87b2716..74f4a2c 100644 --- a/src/features/feedbacks/FeedbacksListScreen.tsx +++ b/src/features/feedbacks/FeedbacksListScreen.tsx @@ -5,7 +5,14 @@ import { Typography, } from "@wanteddev/wds"; import { IconBell, IconPlus, IconSearch } from "@wanteddev/wds-icon"; -import { Fragment, useEffect, useRef, useState } from "react"; +import { + Fragment, + type UIEvent, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import FilterChipGroup from "@/components/ui/FilterChipGroup"; @@ -19,29 +26,106 @@ import { FEEDBACKS, } from "@/features/feedbacks/constants/feedbacks"; -// 카드 한 장이 차지하는 가로 길이(카드 폭 + 캐러셀 gap). FeedbacksQaCard의 폭이 폰(286px)과 -// 데스크톱 컬럼(sm 이상 391px)에서 다르기 때문에 상수로 박아두면 한쪽에서 점이 어긋난다 -// — 실제 카드 폭과 gap을 DOM에서 재서 쓴다. +// 카드 한 장이 차지하는 가로 길이(카드끼리 gap이 없어서 카드 폭 그 자체다). 카드가 캐러셀 +// 폭에 꽉 차는 크기라 뷰포트마다 실제 폭이 다르기 때문에 상수로 박아두면 한쪽에서 점이 +// 어긋난다 — 실제 카드 폭을 DOM에서 재서 쓴다. function getCarouselItemWidth(el: HTMLDivElement): number { - const firstCard = el.firstElementChild; - if (!firstCard) { - return 0; - } - const gap = Number.parseFloat(getComputedStyle(el).columnGap) || 0; - return firstCard.getBoundingClientRect().width + gap; + return el.firstElementChild?.getBoundingClientRect().width ?? 0; } interface FeedbacksListLocationState { feedbackSent?: boolean; } +// FAB 축소 상태의 지름(Figma QA nodeId 2849:56930의 44px 원형 상태). +const FAB_COLLAPSED_WIDTH = 44; + +// "최근 피드백" 캐러셀 카드 수 — 정적 목데이터라 모듈 스코프 상수로 뽑아두면 이펙트 +// 안에서도(컴포넌트 상태가 아니라) exhaustive-deps 경고 없이 안전하게 참조할 수 있다. +const TOTAL_CAROUSEL_PAGES = FEEDBACKS.filter( + (feedback) => feedback.answer, +).length; + +// 스와이프/드래그로 인정할 최소 이동 비율(카드 폭 대비). 이보다 짧게 움직이면 그대로 +// 원래 카드로 되돌아간다. +const CAROUSEL_DRAG_COMMIT_RATIO = 0.2; + +// 짧고 빠르게 튕기는(flick) 스와이프를 인정할 속도 기준(px/ms). 이동 거리가 +// CAROUSEL_DRAG_COMMIT_RATIO에 못 미쳐도, 손을 뗄 때 이 속도보다 빠르면 한 장 넘긴다 — +// 실제 네이티브 캐러셀들이 거리뿐 아니라 속도도 같이 보는 것과 같은 이유다. +const CAROUSEL_FLICK_VELOCITY_PX_MS = 0.35; + // Figma: 게시판 - 열린피드백 (nodeId 1410:50011), 피드백 전송 완료 토스트 (nodeId 1410:50080) function FeedbacksListScreen() { const navigate = useNavigate(); const location = useLocation(); const [roundFilter, setRoundFilter] = useState("all"); + const answeredFeedbacks = FEEDBACKS.filter((feedback) => feedback.answer); + + // "최근 피드백" 캐러셀. 기본은 평범한 가로 스크롤(snap)이라 마우스 휠·트랙패드는 브라우저 + // 기본 동작으로 그냥 잘 동작한다. 다만 두 가지를 추가로 가로챈다. + // 1. 트랙패드 좌우 스와이프(wheel deltaX) — snap-mandatory만으로는 세게 스와이프하면 + // 카드를 여러 장 건너뛸 수 있어서, 한 번의 스와이프 제스처당 정확히 한 페이지만 + // 이동하도록 JS로 직접 처리한다. + // 2. 실제 터치 스와이프(touchmove) — 마찬가지로 관성이 붙으면 여러 장을 건너뛴다 + // (scroll-snap-stop:always만으로는 실기기에서 부족했다). 가로 방향이 확실할 때만 + // 네이티브 스크롤을 끄고 손가락 이동 거리만으로 정확히 한 페이지씩 옮긴다 — 세로 + // 스크롤(페이지 전체)로 판단되면 즉시 손을 떼고 브라우저 기본 동작에 맡긴다. + // 마우스 클릭(Link 탭)은 이 두 경로 어디에도 안 걸쳐서 원래 하던 대로 그대로 동작한다. const carouselRef = useRef(null); const [carouselPage, setCarouselPage] = useState(1); + const carouselPageRef = useRef(carouselPage); + + useEffect(() => { + carouselPageRef.current = carouselPage; + }, [carouselPage]); + + // 피드백 작성 FAB — 맨 위에서는 아이콘+텍스트, 스크롤해서 내려가면 아이콘만 남는 원형으로 + // 축소된다(Figma QA nodeId 2849:56930). "맨 위 복귀 시" 다시 텍스트가 붙으라는 요구라 + // 스크롤 방향이 아니라 맨 위(scrollTop === 0) 여부만 본다. + const [isFabExpanded, setIsFabExpanded] = useState(true); + const fabRef = useRef(null); + // width를 애니메이션하려면 확장 상태의 실제 폭이 필요하다(padding만 바꾸면 텍스트가 + // 마운트/언마운트되며 폭이 툭 튀어서 축소 방향만 애니메이션이 안 도는 것처럼 보였다). + // 텍스트를 상수로 재지 않고 DOM에서 실측한다 — 마운트 시 한 번만 잰다. isFabExpanded가 + // true로 바뀔 때마다 다시 재면, 축소→확장 전환 중(트랜지션이 아직 44px에서 안 벗어난 + // 시점)에 값을 읽어버려 폭이 44로 고정돼버리는 문제가 있었다. + const [fabExpandedWidth, setFabExpandedWidth] = useState(); + + useLayoutEffect(() => { + setFabExpandedWidth(fabRef.current?.getBoundingClientRect().width); + }, []); + + // "최근 피드백" 카드 높이 — 질문·답변 글자 수가 카드마다 달라서 그대로 두면 카드마다 + // 높이가 들쭉날쭉했다. 첫 번째 카드 높이를 재서 나머지 카드에도 min-height로 맞춘다. + // 마운트 시 한 번만 재면, 이후 캐러셀 폭이 바뀌어(창 크기 조절, 모바일↔데스크톱 sm: + // 브레이크포인트 전환) 텍스트 줄바꿈이 달라져도 값이 갱신되지 않는다(/pr-check 지적) + // — 캐러셀 폭을 관찰해서 바뀔 때마다 다시 잰다. 카드 자신의 높이가 아니라 캐러셀 폭을 + // 관찰 대상으로 삼는 이유는, 카드 높이를 관찰하면 우리가 min-height를 다시 설정할 때마다 + // 그 변화가 또 리사이즈로 잡혀 무한 루프가 될 수 있어서다. + const firstQaCardRef = useRef(null); + const [qaCardMinHeight, setQaCardMinHeight] = useState(); + + useLayoutEffect(() => { + const cardEl = firstQaCardRef.current; + const viewportEl = carouselRef.current; + if (!cardEl || !viewportEl) { + return; + } + const measure = () => { + // 이전에 재둔 min-height가 새로 잴 값을 오염시키지 않도록(카드가 이미 min-height로 + // 커진 상태면 그보다 작게는 절대 측정되지 않는다) 먼저 비우고, 리액트가 반영한 다음 + // 프레임에 자연스러운 콘텐츠 높이를 다시 잰다. + setQaCardMinHeight(undefined); + requestAnimationFrame(() => { + setQaCardMinHeight(cardEl.getBoundingClientRect().height); + }); + }; + measure(); + const observer = new ResizeObserver(measure); + observer.observe(viewportEl); + return () => observer.disconnect(); + }, []); // 피드백 작성 화면에서 넘어올 때만 history state로 신호를 받는다(navigate state) — 그 외 // 진입(바텀 탭 등)에서는 안 뜬다. 새로고침 시 재노출을 막으려고 받자마자 state를 비운다. const [isSentToastOpen, setIsSentToastOpen] = useState( @@ -70,6 +154,10 @@ function FeedbacksListScreen() { setCarouselPage(Math.round(el.scrollLeft / itemWidth) + 1); }; + const handleContentScroll = (event: UIEvent) => { + setIsFabExpanded(event.currentTarget.scrollTop <= 0); + }; + const scrollCarouselToPage = (page: number) => { const el = carouselRef.current; if (!el) { @@ -81,6 +169,191 @@ function FeedbacksListScreen() { }); }; + // 트랙패드 좌우 스와이프(wheel deltaX)일 때만 기본 스크롤을 막고 정확히 한 페이지씩 + // 이동시킨다. 세로 휠(페이지 스크롤)은 그대로 둔다. JSX onWheel은 리액트가 기본으로 + // passive 리스너를 붙여 preventDefault가 안 먹어서, 직접 addEventListener로 등록한다. + // + // 잠금 해제를 "힘의 크기가 다시 커지면"으로만 판단한다 — 관성은 물리적으로 시간이 + // 지날수록 deltaX 크기가 줄어들기만 하고 다시 커지는 일이 없다. 반대로 사용자가 진짜 새 + // 스와이프를 시작하면 그 순간 deltaX 크기가 직전 관성 꼬리값보다 반드시 커진다. 시간 + // 기반 보조 타이머는 일부러 안 둔다 — 있으면 "이 판단이 애매한 극소수 경우"를 잡아주긴 + // 하지만, 스와이프 직후 곧바로 다시 스와이프할 때 그 타이머 시간만큼 다음 스와이프가 + // 못 먹히는 순간이 생겨 오히려 부드럽지 않게 느껴졌다(실사용 확인됨). + // "직전 샘플 단 하나"와만 비교하면 노이즈(측정 잡음)나, 한 번의 연속된 스와이프 안에서 + // 자연스럽게 살짝 가속하는 순간에도 "새 스와이프"로 오탐할 수 있다(/pr-check 지적). + // 짧은 이동평균(WHEEL_NOISE_WINDOW개)과 비교하면 이런 일시적인 튐은 평균에 묻혀 걸러지고, + // 평균보다 뚜렷하게 큰 힘만 새 스와이프로 잡힌다. + useEffect(() => { + const el = carouselRef.current; + if (!el) { + return; + } + const WHEEL_NOISE_WINDOW = 3; + let isLocked = false; + let recentAbsDeltas: number[] = []; + const handleWheel = (event: WheelEvent) => { + if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) { + return; + } + event.preventDefault(); + const absDeltaX = Math.abs(event.deltaX); + if (isLocked) { + const recentAverage = + recentAbsDeltas.reduce((sum, value) => sum + value, 0) / + recentAbsDeltas.length; + if (absDeltaX <= recentAverage) { + // 관성 꼬리(또는 노이즈) — 여전히 같은 스와이프의 연장이라 무시한다. + recentAbsDeltas.push(absDeltaX); + if (recentAbsDeltas.length > WHEEL_NOISE_WINDOW) { + recentAbsDeltas.shift(); + } + return; + } + // 최근 평균보다 뚜렷하게 커졌다 = 새 스와이프가 시작된 것이다. + isLocked = false; + } + recentAbsDeltas = [absDeltaX]; + const direction = event.deltaX > 0 ? 1 : -1; + const nextPage = Math.min( + Math.max(carouselPageRef.current + direction, 1), + TOTAL_CAROUSEL_PAGES, + ); + if (nextPage === carouselPageRef.current) { + return; + } + isLocked = true; + // carouselPageRef는 실제 스크롤 위치(onScroll)로 갱신되는데, smooth 애니메이션이 아직 + // 끝나기 전에 연속으로 스와이프하면 이 값이 못 따라와서 두 번째 스와이프가 같은 + // 페이지를 다시 목표로 잡아버린다(실사용 중 발견) — 스크롤이 실제로 끝나길 기다리지 + // 않고 여기서 바로 "의도한 다음 페이지"로 미리 갱신해 둔다. + carouselPageRef.current = nextPage; + const itemWidth = getCarouselItemWidth(el); + el.scrollTo({ behavior: "smooth", left: (nextPage - 1) * itemWidth }); + }; + el.addEventListener("wheel", handleWheel, { passive: false }); + return () => el.removeEventListener("wheel", handleWheel); + }, []); + + // 실제 터치 스와이프 — 관성(모멘텀)이 스냅 지점 여러 개를 건너뛸 수 있어서(실기기 테스트로 + // 확인됨), 가로 방향이 확실해지면 네이티브 스크롤을 끄고 손가락을 그대로 따라가다가, 뗄 + // 때 이동 "거리"만 보고 한 페이지만 옮긴다(속도·관성과 무관하게 항상 한 장만 이동). + // 세로로 움직이기 시작하면 즉시 손을 떼서 페이지 스크롤을 그대로 둔다. + useEffect(() => { + const el = carouselRef.current; + if (!el) { + return; + } + let drag: { + startX: number; + startY: number; + startScrollLeft: number; + // 드래그 중 el.scrollLeft를 직접 옮기면 네이티브 onScroll(handleCarouselScroll)이 그때 + // 그때 carouselPage를 다시 계산해버린다 — 카드 폭의 50%만 넘겨도 손을 떼기 전에 이미 + // "다음 페이지"로 갱신된다. 그 상태에서 손을 뗄 때 "현재 페이지 + 1"을 또 계산하면 + // 두 장이 건너뛰어진다(실기기 재현 확인됨). 그래서 제스처가 시작된 시점의 페이지를 + // 별도로 고정해 두고, 도착 페이지는 항상 이 값 기준으로만 계산한다. + startPage: number; + lastDeltaX: number; + isHorizontal: boolean | null; + // 짧고 빠른 flick도 인정하려고 "가장 최근 구간"의 순간 속도를 별도로 추적한다 — + // 시작점부터 뗀 시점까지 평균을 내면, 천천히 끌다가 마지막에 휙 튕기는 자연스러운 + // 스와이프의 속도가 희석돼서 flick으로 안 잡힌다. + lastMoveX: number; + lastMoveTime: number; + velocityX: number; + } | null = null; + + const handleTouchStart = (event: TouchEvent) => { + if (event.touches.length !== 1) { + return; + } + const touch = event.touches[0]; + const now = performance.now(); + drag = { + isHorizontal: null, + lastDeltaX: 0, + lastMoveTime: now, + lastMoveX: touch.clientX, + startPage: carouselPageRef.current, + startScrollLeft: el.scrollLeft, + startX: touch.clientX, + startY: touch.clientY, + velocityX: 0, + }; + }; + + const handleTouchMove = (event: TouchEvent) => { + if (!drag || event.touches.length !== 1) { + return; + } + const touch = event.touches[0]; + const deltaX = touch.clientX - drag.startX; + const deltaY = touch.clientY - drag.startY; + if (drag.isHorizontal === null) { + if (Math.abs(deltaX) < 8 && Math.abs(deltaY) < 8) { + return; + } + drag.isHorizontal = Math.abs(deltaX) > Math.abs(deltaY); + if (!drag.isHorizontal) { + // 세로 스크롤 의도 — 이 제스처는 더 이상 관여하지 않고 브라우저 기본 동작에 맡긴다. + drag = null; + return; + } + } + drag.lastDeltaX = deltaX; + const now = performance.now(); + const dt = now - drag.lastMoveTime; + if (dt > 0) { + drag.velocityX = (touch.clientX - drag.lastMoveX) / dt; + } + drag.lastMoveX = touch.clientX; + drag.lastMoveTime = now; + event.preventDefault(); + el.scrollLeft = drag.startScrollLeft - deltaX; + }; + + const handleTouchEnd = () => { + if (!drag) { + return; + } + const { lastDeltaX, velocityX, startPage } = drag; + drag = null; + const itemWidth = getCarouselItemWidth(el); + if (!itemWidth) { + return; + } + const isFarEnough = + Math.abs(lastDeltaX) > itemWidth * CAROUSEL_DRAG_COMMIT_RATIO; + const isFlick = Math.abs(velocityX) > CAROUSEL_FLICK_VELOCITY_PX_MS; + // 드래그 도중 라이브 스크롤 위치로 갱신된 carouselPageRef가 아니라, 제스처 시작 시점에 + // 고정해 둔 startPage를 기준으로 계산한다 — 그래야 카드 폭 절반을 넘게 끌어도 정확히 + // 한 장만 이동한다. + const nextPage = + lastDeltaX !== 0 && (isFarEnough || isFlick) + ? Math.min( + Math.max(startPage + (lastDeltaX < 0 ? 1 : -1), 1), + TOTAL_CAROUSEL_PAGES, + ) + : startPage; + // wheel 핸들러와 같은 이유로, 실제 스크롤(onScroll)이 따라오길 기다리지 않고 여기서 + // 바로 "의도한 다음 페이지"를 기록해 둔다 — 그래야 스크롤 애니메이션이 끝나기 전에 + // 바로 이어서 스와이프해도 같은 카드를 다시 목표로 잡지 않는다. + carouselPageRef.current = nextPage; + el.scrollTo({ behavior: "smooth", left: (nextPage - 1) * itemWidth }); + }; + + el.addEventListener("touchstart", handleTouchStart, { passive: true }); + el.addEventListener("touchmove", handleTouchMove, { passive: false }); + el.addEventListener("touchend", handleTouchEnd); + el.addEventListener("touchcancel", handleTouchEnd); + return () => { + el.removeEventListener("touchstart", handleTouchStart); + el.removeEventListener("touchmove", handleTouchMove); + el.removeEventListener("touchend", handleTouchEnd); + el.removeEventListener("touchcancel", handleTouchEnd); + }; + }, []); + // "공지"/"열린피드백" 토글은 게시판-공지 화면과 같은 ScreenHeaderToggleTitle을 쓴다. 이제 // 두 화면이 다 있어서 클릭하면 실제로 이동하도록 onChange를 연결한다(공지 쪽도 함께 연결). useScreenHeader( @@ -103,14 +376,16 @@ function FeedbacksListScreen() { />, ); - const answeredFeedbacks = FEEDBACKS.filter((feedback) => feedback.answer); const feedbacks = FEEDBACKS.filter( (feedback) => roundFilter === "all" || feedback.round === roundFilter, ); return (
-
+
- {answeredFeedbacks.map((feedback) => ( + {answeredFeedbacks.map((feedback, index) => ( @@ -197,12 +474,27 @@ function FeedbacksListScreen() { diff --git a/src/features/feedbacks/components/FeedbacksQaCard.tsx b/src/features/feedbacks/components/FeedbacksQaCard.tsx index cd20ac1..a8c75be 100644 --- a/src/features/feedbacks/components/FeedbacksQaCard.tsx +++ b/src/features/feedbacks/components/FeedbacksQaCard.tsx @@ -6,6 +6,10 @@ import questionIcon from "@/assets/icons/feedbacks/question.svg"; interface FeedbacksQaCardProps { question: string; answer: string; + // 질문·답변 글자 수가 카드마다 달라서 높이가 제각각이었다 — 첫 번째 카드의 실제 높이를 + // 재서 모두에게 min-height로 넘기면, 그 카드를 기준으로 높이가 맞춰진다(그보다 내용이 + // 긴 카드가 있어도 잘리지 않게 min-height를 쓴다). + height?: number; } // Figma: Q&A Card (nodeId 1410:50022) — Stream 로컬 컴포넌트, WDS 아님. @@ -16,15 +20,18 @@ interface FeedbacksQaCardProps { // SVG는 프레임이 아니라 도형 자체의 타이트한 바운딩(19.9998×19.8093 / 16×16.0012)이라, 칸에 // 그대로 늘려 채우면(예전엔 size-6로 강제) Figma보다 20~50% 크게 보인다(/figma-check 지적). // 칸 크기(24px, 텍스트와의 32px 오프셋)는 유지하고 아이콘만 칸 안에서 가운데 정렬한다. -// 폭은 캐러셀에서 다음 카드가 41px 엿보이도록 컬럼 폭에 맞춰 잡은 값이다. 폰은 Figma 그대로 -// 286px(375 기준), 데스크톱 컬럼(sm 이상, 480px)에서는 391px — 좌우 px-5(40px)와 gap(8px)을 -// 빼고 남는 41px이 엿보기가 된다. FeedbacksListScreen은 이 폭을 DOM에서 재서 점을 맞춘다. -function FeedbacksQaCard({ question, answer }: FeedbacksQaCardProps) { +// 폭은 캐러셀 한 칸(부모가 이미 한 장만 보이도록 폭을 잡아 준다)에 꽉 채운다 — 한 번에 +// 카드 한 장만 보이고 옆 카드가 삐져나오지 않아야 한다(Figma QA nodeId 2849:56863). +// FeedbacksListScreen은 이 폭을 DOM에서 재서 페이지네이션 점을 맞춘다. +function FeedbacksQaCard({ question, answer, height }: FeedbacksQaCardProps) { // 상세페이지에서만 쓰는 **볼드** 문법을 미리보기 카드에서는 그대로 노출하지 않는다. const plainAnswer = answer.replace(/\*\*/g, ""); return ( -
+
diff --git a/src/features/notices/NoticesDetailScreen.tsx b/src/features/notices/NoticesDetailScreen.tsx index 4d8576a..7ebb466 100644 --- a/src/features/notices/NoticesDetailScreen.tsx +++ b/src/features/notices/NoticesDetailScreen.tsx @@ -27,22 +27,25 @@ function NoticesDetailScreen() { const notice = NOTICES.find((item) => item.id === noticeId); const photoCount = notice?.photoCount ?? 0; - const backButton = ( - navigate(-1)} - variant="icon" - > - - - ); - // 사진이 있으면 행사 상세와 같은 방식으로 뒤로가기를 사진 위 오버레이로 그려서 사진이 화면 // 최상단부터 시작하게 한다(ScreenLayout 헤더 슬롯을 비우면 0px로 접힌다). 사진이 없으면 // 덮을 이미지가 없어서 기존처럼 흰 배경 헤더를 쓴다. + // 오버레이일 때만 화살표를 흰색으로 바꾼다 — 사진 위 그라데이션(PhotoGallery)과 짝을 이루는 + // 색이라, 흰 배경 헤더에 그대로 쓰면 안 보인다(Figma QA nodeId 2849:60815). useScreenHeader( notice?.hasThumbnail ? null : ( - + navigate(-1)} + variant="icon" + > + + + } + variant="normal" + /> ), ); @@ -68,7 +71,15 @@ function NoticesDetailScreen() { idPrefix={notice.id} key={notice.id} overlay={ -
{backButton}
+
+ navigate(-1)} + variant="icon" + > + + +
} photoCount={photoCount} showCounter