From a93690d03f9d8d1c2ef4476ae4195d6fa395fba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=A4=80?= Date: Thu, 1 Oct 2026 17:00:07 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20PhotoGallery=EA=B0=80=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=9C=20=EC=82=AC=EC=A7=84=C2=B7=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=20=EC=9E=A5=C2=B7=EB=B0=B0=EA=B2=BD=20=EC=8A=AC=EB=A1=AF?= =?UTF-8?q?=EC=9D=84=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/PhotoGallery.tsx | 140 ++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 23 deletions(-) diff --git a/src/components/ui/PhotoGallery.tsx b/src/components/ui/PhotoGallery.tsx index 1ca4f8a..43aacc3 100644 --- a/src/components/ui/PhotoGallery.tsx +++ b/src/components/ui/PhotoGallery.tsx @@ -1,49 +1,113 @@ import { PageCounter } from "@wanteddev/wds"; import { IconChevronLeft, IconChevronRight } from "@wanteddev/wds-icon"; -import { type ReactNode, type UIEvent, useMemo, useRef, useState } from "react"; +import { + type ReactNode, + type UIEvent, + useEffect, + useMemo, + useRef, + useState, +} from "react"; -interface PhotoGalleryProps { +export interface PhotoGalleryPhoto { + src: string; + alt: string; +} + +interface PhotoGalleryBaseProps { idPrefix: string; + // 슬라이드 뒤에 깔리는 배경(아카이빙 뷰어의 흐린 사진). 스크롤 영역 밖이라 같이 넘어가지 않는다. + background?: ReactNode; // 뒤로가기 버튼처럼 이미지 위에 얹히는 오버레이(화면마다 위치·존재 여부가 다르다 — 예: 행사 // 상세는 헤더 없이 이미지 위 오버레이 버튼, 공지 상세는 ScreenHeader를 따로 쓴다). overlay?: ReactNode; - photoCount: number; showCounter: boolean; slideClassName: string; + /** photos를 줄 때 각 사진 에 줄 클래스 — 기본은 슬라이드를 꽉 채운다 */ + photoClassName?: string; + /** 갤러리 루트에 덧붙일 클래스 — 전체 화면을 채우는 뷰어처럼 바깥 배치가 다른 경우에 쓴다 */ + className?: string; + /** 처음 보여줄 장(1부터). 누른 사진에서 열리는 뷰어가 쓴다 */ + initialPage?: number; + onPageChange?: (page: number) => void; + /** 슬라이드 스냅 기준 — 기본은 start, 전체 화면 뷰어는 center */ + snapAlign?: "start" | "center"; + /** true면 세게 밀어도 여러 장을 건너뛰지 않고 다음 한 장에서 멈춘다(scroll-snap-stop: always) */ + snapStop?: boolean; + /** 카운터 위치 — 기본은 오른쪽 아래 모서리 */ + counterClassName?: string; + /** 어두운 사진 위에 올라가는 카운터(Figma Page Indicator/Counter의 Alternative=True) */ + counterAlternative?: boolean; } -// 사진 여러 장을 가로 스크롤 스냅으로 넘기는 공용 갤러리 — 공지 상세·행사 상세가 공유한다. -// 웹(hover 가능 기기)에서는 화살표 버튼이 호버 시 뜨고, 모바일(터치)에서는 스와이프로만 -// 넘긴다. Figma에는 없는 Stream 자체 인터랙션이라 화면 쪽 디자인이 아니라 여기서 새로 만들었다. +// 실 이미지 API가 붙은 화면은 photos로 실제 사진을, 아직 장수만 아는 화면은 photoCount로 +// 회색 placeholder를 그린다. +type PhotoGalleryProps = PhotoGalleryBaseProps & + ( + | { photos: PhotoGalleryPhoto[]; photoCount?: never } + | { photoCount: number; photos?: never } + ); + +const SNAP_ALIGN_CLASS_NAMES = { + center: "snap-center", + start: "snap-start", +} as const; + +// 사진 여러 장을 가로 스크롤 스냅으로 넘기는 공용 갤러리 — 공지 상세·행사 상세·아카이빙 사진 +// 뷰어가 공유한다. 웹(hover 가능 기기)에서는 화살표 버튼이 호버 시 뜨고, 모바일(터치)에서는 +// 스와이프로만 넘긴다. Figma에는 없는 Stream 자체 인터랙션이라 화면 쪽 디자인이 아니라 여기서 +// 새로 만들었다. function PhotoGallery({ idPrefix, + background, overlay, photoCount, + photos, showCounter, slideClassName, + photoClassName = "size-full object-cover", + className = "", + initialPage = 1, + onPageChange, + snapAlign = "start", + snapStop = false, + counterClassName = "right-5 bottom-5", + counterAlternative = false, }: PhotoGalleryProps) { - const [currentPage, setCurrentPage] = useState(1); + const totalPages = photos?.length ?? photoCount ?? 0; + const [currentPage, setCurrentPage] = useState(initialPage); const galleryRef = useRef(null); - const hasMultiplePhotos = photoCount > 1; + const hasMultiplePhotos = totalPages > 1; // 실 이미지 API 전까지는 장수만 알고 URL이 없어서, 슬라이드 key를 미리 만들어 둔다 - // (map 콜백의 index를 key로 쓰면 noArrayIndexKey에 걸린다). + // (map 콜백의 index를 key로 쓰면 noArrayIndexKey에 걸린다). 실제 사진도 같은 파일이 여러 장 + // 올 수 있어서 URL이 아니라 순서로 구분한다. const photoKeys = useMemo( () => Array.from( - { length: photoCount }, + { length: totalPages }, (_, index) => `${idPrefix}-photo-${index}`, ), - [idPrefix, photoCount], + [idPrefix, totalPages], ); + // 처음 열 때 initialPage 위치로 맞춰둔다. 이후 넘기는 건 사용자 스크롤이 담당한다. + useEffect(() => { + const el = galleryRef.current; + if (el) { + el.scrollLeft = (initialPage - 1) * el.offsetWidth; + } + }, [initialPage]); + // 스크롤 위치로 현재 장을 역산한다. const handleGalleryScroll = (event: UIEvent) => { const { scrollLeft, offsetWidth } = event.currentTarget; if (offsetWidth === 0) { return; } - setCurrentPage(Math.round(scrollLeft / offsetWidth) + 1); + const page = Math.round(scrollLeft / offsetWidth) + 1; + setCurrentPage(page); + onPageChange?.(page); }; // 화살표 클릭(웹 전용)은 프로그래밍적으로 한 장만큼 스크롤한다 — 스와이프와 같은 스냅 위치로 맞춰진다. @@ -55,19 +119,46 @@ function PhotoGallery({ el.scrollTo({ behavior: "smooth", left: (page - 1) * el.offsetWidth }); }; + const slideSnapClassName = `${SNAP_ALIGN_CLASS_NAMES[snapAlign]}${ + snapStop ? " snap-always" : "" + }`; + return ( -
+
+ {background} + + {/* relative: background(absolute)가 배치된 요소라, 일반 흐름인 슬라이드보다 위에 그려진다. + 슬라이드 영역도 배치된 요소로 만들어서 DOM 순서대로 배경 위에 오게 한다. */}
- {photoKeys.map((photoKey) => ( -
- ))} + {photoKeys.map((photoKey, index) => { + const photo = photos?.[index]; + return ( +
+ {photo && ( + {photo.alt} + )} +
+ ); + })}
{overlay} @@ -86,7 +177,7 @@ function PhotoGallery({ )} - {currentPage < photoCount && ( + {currentPage < totalPages && ( diff --git a/src/features/archives/components/ArchivesPhotoCard.tsx b/src/features/archives/components/ArchivesPhotoCard.tsx index eb66119..12cd985 100644 --- a/src/features/archives/components/ArchivesPhotoCard.tsx +++ b/src/features/archives/components/ArchivesPhotoCard.tsx @@ -1,5 +1,7 @@ import { Typography } from "@wanteddev/wds"; +import LazyImage from "@/components/ui/LazyImage"; + // Figma: Archives Photo Card (Left-Large 1276:95411, Left-Medium 1276:95412, Right-Small 1276:95416) export type ArchivesPhotoCardSize = "small" | "medium" | "large"; @@ -32,11 +34,8 @@ function ArchivesPhotoCard({ onClick={onClick} type="button" > - + {/* 화면 밖 카드의 사진은 스크롤로 가까워질 때 받는다 — 그 전에는 회색 자리가 보인다 */} +
void; @@ -24,9 +26,9 @@ function ArchivesPhotoGrid({ photos, onMoreClick }: ArchivesPhotoGridProps) { // biome-ignore lint/suspicious/noArrayIndexKey: 사진 목록은 순서가 바뀌지 않는다 key={index} > - {hasMore && index === VISIBLE_PHOTO_COUNT - 1 && ( From 3d56d2b1fbbedbc123b274787e222b012f1441cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=A4=80?= Date: Thu, 1 Oct 2026 18:26:30 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=EC=82=AC=EC=A7=84=20=EA=B0=A4?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=98=A4=EB=B2=84=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EC=8B=9C=20=EC=9E=A5=20=EB=B2=88=ED=98=B8=EA=B0=80?= =?UTF-8?q?=20=EB=B2=94=EC=9C=84=EB=A5=BC=20=EB=84=98=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Safari 탄성 오버스크롤에서 scrollLeft가 범위를 벗어나 마지막 장을 세게 밀면 카운터가 "27 / 26"이 되고 뷰어의 흐린 배경이 사라졌다. FeedbacksDetailScreen과 같은 방식으로 클램프한다. 장이 실제로 바뀔 때만 onPageChange를 호출하고, 뷰어 사진은 자르지 않고 가로를 꽉 채운다. --- src/components/ui/PhotoGallery.tsx | 28 +++++++++++-------- .../archives/ArchivesPhotoViewerScreen.tsx | 4 ++- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/ui/PhotoGallery.tsx b/src/components/ui/PhotoGallery.tsx index 0d1873d..8121a44 100644 --- a/src/components/ui/PhotoGallery.tsx +++ b/src/components/ui/PhotoGallery.tsx @@ -9,8 +9,6 @@ import { useState, } from "react"; -import LazyImage from "@/components/ui/LazyImage"; - export interface PhotoGalleryPhoto { src: string; alt: string; @@ -25,7 +23,7 @@ interface PhotoGalleryBaseProps { overlay?: ReactNode; showCounter: boolean; slideClassName: string; - /** photos를 줄 때 사진 자리(LazyImage 바깥 박스)에 줄 클래스 — 기본은 슬라이드를 꽉 채운다 */ + /** photos를 줄 때 각 사진 에 줄 클래스 — 기본은 슬라이드를 꽉 채운다 */ photoClassName?: string; /** 갤러리 루트에 덧붙일 클래스 — 전체 화면을 채우는 뷰어처럼 바깥 배치가 다른 경우에 쓴다 */ className?: string; @@ -67,7 +65,7 @@ function PhotoGallery({ photos, showCounter, slideClassName, - photoClassName = "size-full", + photoClassName = "size-full object-cover", className = "", initialPage = 1, onPageChange, @@ -102,12 +100,23 @@ function PhotoGallery({ }, [initialPage]); // 스크롤 위치로 현재 장을 역산한다. + // Safari의 탄성 오버스크롤(rubber-band)은 scrollLeft가 범위를 넘어설 수 있어서 유효 범위로 클램프한다 + // (같은 처리가 FeedbacksDetailScreen에도 있다 — 안 하면 마지막 장을 세게 밀 때 "27 / 26"이 된다). const handleGalleryScroll = (event: UIEvent) => { - const { scrollLeft, offsetWidth } = event.currentTarget; - if (offsetWidth === 0) { + const { scrollLeft, scrollWidth, clientWidth } = event.currentTarget; + if (clientWidth === 0) { + return; + } + const maxScrollLeft = Math.max(scrollWidth - clientWidth, 0); + const clampedScrollLeft = Math.min(Math.max(scrollLeft, 0), maxScrollLeft); + const page = Math.min( + Math.round(clampedScrollLeft / clientWidth) + 1, + totalPages, + ); + // 스크롤 이벤트는 손가락을 따라 계속 오지만, 장이 실제로 바뀔 때만 알린다. + if (page === currentPage) { return; } - const page = Math.round(scrollLeft / offsetWidth) + 1; setCurrentPage(page); onPageChange?.(page); }; @@ -152,12 +161,9 @@ function PhotoGallery({ key={photoKey} > {photo && ( - )} diff --git a/src/features/archives/ArchivesPhotoViewerScreen.tsx b/src/features/archives/ArchivesPhotoViewerScreen.tsx index 04c2d52..f6e24f3 100644 --- a/src/features/archives/ArchivesPhotoViewerScreen.tsx +++ b/src/features/archives/ArchivesPhotoViewerScreen.tsx @@ -70,7 +70,9 @@ function ArchivesPhotoViewerScreen() { }))} showCounter /* Figma Image 375×463 — 화면 높이 안에서 세로 가운데에 둔다 */ - photoClassName="aspect-[375/463] w-full" + /* 가로는 화면을 꽉 채우고 세로는 사진 비율대로 둔다 — 자리를 375×463으로 고정하면 + 비율이 다른 사진에 좌우 여백이 생긴다. 아주 긴 사진만 max-h-full로 화면 안에 맞춘다. */ + photoClassName="max-h-full w-full object-contain" slideClassName="flex h-full items-center" snapAlign="center" snapStop From 019d34b80fcaba152d15fe4b12dc370e71970274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=A4=80?= Date: Thu, 1 Oct 2026 18:26:30 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=A7=80=EC=A0=81=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지연 로딩은 목록이 긴 아카이빙 목록·현장 사진 목록에만 둔다 - 하단 Home Bar 여백 복붙 3곳을 ScreenBottomSafeArea로 묶는다 - 필터 칩 선택 상태를 aria-pressed로 알린다 - 목록 열을 ArchivesPhotoColumn 컴포넌트로 분리한다 - 정보 그리드 라벨 칸을 minmax로 바꿔 긴 라벨이 넘치지 않게 한다 --- src/components/ui/FilterChipGroup.tsx | 1 + src/components/ui/LazyImage.tsx | 20 ++++++++- src/components/ui/ScreenBottomSafeArea.tsx | 10 +++++ .../archives/ArchivesDetailScreen.tsx | 8 ++-- src/features/archives/ArchivesListScreen.tsx | 43 ++++++------------- .../archives/ArchivesPhotosScreen.tsx | 5 +-- .../components/ArchivesPhotoColumn.tsx | 36 ++++++++++++++++ .../archives/components/ArchivesPhotoGrid.tsx | 20 ++++----- .../components/ArchivesYearFilter.tsx | 1 + 9 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 src/components/ui/ScreenBottomSafeArea.tsx create mode 100644 src/features/archives/components/ArchivesPhotoColumn.tsx diff --git a/src/components/ui/FilterChipGroup.tsx b/src/components/ui/FilterChipGroup.tsx index b9c6142..736d23b 100644 --- a/src/components/ui/FilterChipGroup.tsx +++ b/src/components/ui/FilterChipGroup.tsx @@ -22,6 +22,7 @@ function FilterChipGroup({ options, value, onChange }: FilterChipGroupProps) {
{options.map((option) => ( onChange(option.value)} diff --git a/src/components/ui/LazyImage.tsx b/src/components/ui/LazyImage.tsx index 905e42d..7bdf71f 100644 --- a/src/components/ui/LazyImage.tsx +++ b/src/components/ui/LazyImage.tsx @@ -1,5 +1,7 @@ import { useEffect, useRef, useState } from "react"; +export type LazyImageFit = "cover" | "contain"; + interface LazyImageProps { src: string; alt: string; @@ -7,8 +9,18 @@ interface LazyImageProps { className?: string; /** 열자마자 보이는 자리라 지연 없이 바로 받아야 할 때 */ isEager?: boolean; + /** + * cover(기본): 자리를 꽉 채우고 넘치는 부분은 잘린다 — 카드·썸네일처럼 자리 비율이 정해진 곳. + * contain: 사진 비율을 지키고 자리 안에 다 들어온다 — 사진 자체가 콘텐츠인 전체 화면 뷰어. + */ + fit?: LazyImageFit; } +const FIT_CLASS_NAMES: Record = { + contain: "object-contain", + cover: "object-cover", +}; + // 사진이 도착하기 전에는 회색 자리(Figma의 이미지 슬롯과 같은 thumbnail-placeholder)를 보여주고, // 도착하면 페이드인한다. 자리 크기는 바깥 박스가 들고 있어서 사진이 늦게 와도 레이아웃이 밀리지 않는다. // 받는 시점은 브라우저 기본 지연 로딩(loading="lazy")에 맡긴다 — IntersectionObserver를 직접 두면 @@ -18,6 +30,7 @@ function LazyImage({ alt, className = "", isEager = false, + fit = "cover", }: LazyImageProps) { const [isLoaded, setIsLoaded] = useState(false); const imageRef = useRef(null); @@ -32,11 +45,14 @@ function LazyImage({ return ( {alt}; +} + +export default ScreenBottomSafeArea; diff --git a/src/features/archives/ArchivesDetailScreen.tsx b/src/features/archives/ArchivesDetailScreen.tsx index 5f31044..cdd3c1b 100644 --- a/src/features/archives/ArchivesDetailScreen.tsx +++ b/src/features/archives/ArchivesDetailScreen.tsx @@ -2,6 +2,7 @@ import { IconButton, Typography } from "@wanteddev/wds"; import { IconChevronLeft, IconShare } from "@wanteddev/wds-icon"; import { useNavigate, useParams } from "react-router-dom"; +import ScreenBottomSafeArea from "@/components/ui/ScreenBottomSafeArea"; import ScreenHeader from "@/components/ui/ScreenHeader"; import ArchivesDetailSection from "@/features/archives/components/ArchivesDetailSection"; import ArchivesPhotoGrid from "@/features/archives/components/ArchivesPhotoGrid"; @@ -71,7 +72,8 @@ function ArchivesDetailScreen() { > {title} -
+ {/* 라벨 칸은 Figma의 51px을 최소폭으로만 쓴다 — 고정하면 "담당 부서"처럼 긴 라벨이 칸을 넘친다 */} +
{infoRows.map(({ label, value }) => (
- {/* 하단 Home Bar 자리 — env()라 앱 WebView(네이티브가 이미 인셋)에서는 0이 된다. - 데스크톱 프레임에서는 Figma대로 34px을 흉내 낸다(BottomNav와 같은 규칙). */} -
+
); } diff --git a/src/features/archives/ArchivesListScreen.tsx b/src/features/archives/ArchivesListScreen.tsx index 04582d9..cd65db5 100644 --- a/src/features/archives/ArchivesListScreen.tsx +++ b/src/features/archives/ArchivesListScreen.tsx @@ -3,11 +3,11 @@ import { IconChevronLeft, IconSearch } from "@wanteddev/wds-icon"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import ScreenBottomSafeArea from "@/components/ui/ScreenBottomSafeArea"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; -import ArchivesPhotoCard, { - type ArchivesPhotoCardSize, -} from "@/features/archives/components/ArchivesPhotoCard"; +import type { ArchivesPhotoCardSize } from "@/features/archives/components/ArchivesPhotoCard"; +import ArchivesPhotoColumn from "@/features/archives/components/ArchivesPhotoColumn"; import ArchivesYearFilter from "@/features/archives/components/ArchivesYearFilter"; import { ARCHIVES_ITEMS, @@ -18,27 +18,6 @@ import { const LEFT_COLUMN_SIZES: ArchivesPhotoCardSize[] = ["large", "medium"]; const RIGHT_COLUMN_SIZES: ArchivesPhotoCardSize[] = ["small", "large"]; -function renderColumn( - items: ArchivesItem[], - sizes: ArchivesPhotoCardSize[], - onItemClick: (item: ArchivesItem) => void, -) { - return ( -
- {items.map((item, index) => ( - onItemClick(item)} - size={sizes[index % sizes.length]} - title={item.title} - /> - ))} -
- ); -} - // Figma: 아카이빙 목록 (nodeId 1276:95397) function ArchivesListScreen() { const [year, setYear] = useState("2025"); @@ -77,13 +56,19 @@ function ArchivesListScreen() {
- {renderColumn(leftItems, LEFT_COLUMN_SIZES, handleItemClick)} - {renderColumn(rightItems, RIGHT_COLUMN_SIZES, handleItemClick)} + +
- {/* 하단 Home Bar 자리 — env()라 앱 WebView(네이티브가 이미 인셋)에서는 0이 된다. - 데스크톱 프레임에서는 Figma대로 34px을 흉내 낸다(BottomNav와 같은 규칙). */} -
+
); } diff --git a/src/features/archives/ArchivesPhotosScreen.tsx b/src/features/archives/ArchivesPhotosScreen.tsx index e13f901..00f3bb6 100644 --- a/src/features/archives/ArchivesPhotosScreen.tsx +++ b/src/features/archives/ArchivesPhotosScreen.tsx @@ -3,6 +3,7 @@ import { IconChevronLeft } from "@wanteddev/wds-icon"; import { useNavigate, useParams } from "react-router-dom"; import LazyImage from "@/components/ui/LazyImage"; +import ScreenBottomSafeArea from "@/components/ui/ScreenBottomSafeArea"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; import { ARCHIVES_DETAIL } from "@/features/archives/constants/archivesDetail"; @@ -51,9 +52,7 @@ function ArchivesPhotosScreen() { ))}
- {/* 하단 Home Bar 자리 — env()라 앱 WebView(네이티브가 이미 인셋)에서는 0이 된다. - 데스크톱 프레임에서는 Figma대로 34px을 흉내 낸다(BottomNav와 같은 규칙). */} -
+
); } diff --git a/src/features/archives/components/ArchivesPhotoColumn.tsx b/src/features/archives/components/ArchivesPhotoColumn.tsx new file mode 100644 index 0000000..0619f61 --- /dev/null +++ b/src/features/archives/components/ArchivesPhotoColumn.tsx @@ -0,0 +1,36 @@ +import ArchivesPhotoCard, { + type ArchivesPhotoCardSize, +} from "@/features/archives/components/ArchivesPhotoCard"; +import type { ArchivesItem } from "@/features/archives/constants/archivesItems"; + +interface ArchivesPhotoColumnProps { + items: ArchivesItem[]; + /** 위에서부터 번갈아 적용할 카드 높이 — 두 열이 서로 다른 순서를 써서 높이가 엇갈린다 */ + sizes: ArchivesPhotoCardSize[]; + onItemClick: (item: ArchivesItem) => void; +} + +// Figma: 아카이빙 목록 Photo Grid (nodeId 1276:95409)의 한 열. +// 2열 매스너리라 열마다 카드 높이 순서가 달라서, 열을 컴포넌트 단위로 둔다. +function ArchivesPhotoColumn({ + items, + sizes, + onItemClick, +}: ArchivesPhotoColumnProps) { + return ( +
+ {items.map((item, index) => ( + onItemClick(item)} + size={sizes[index % sizes.length]} + title={item.title} + /> + ))} +
+ ); +} + +export default ArchivesPhotoColumn; diff --git a/src/features/archives/components/ArchivesPhotoGrid.tsx b/src/features/archives/components/ArchivesPhotoGrid.tsx index 5837a4a..7c4766b 100644 --- a/src/features/archives/components/ArchivesPhotoGrid.tsx +++ b/src/features/archives/components/ArchivesPhotoGrid.tsx @@ -1,21 +1,21 @@ import { Typography } from "@wanteddev/wds"; -import LazyImage from "@/components/ui/LazyImage"; - interface ArchivesPhotoGridProps { photos: string[]; onMoreClick: () => void; } -const VISIBLE_PHOTO_COUNT = 3; +// Figma Photo Grid는 칸이 3개다 — 3번째 칸은 "더보기" 오버레이로 덮인다. +const TILE_COUNT = 3; // Figma: 아카이빙 상세 Photo Grid (nodeId 1526:171246) -// 3장까지 보여주고, 더 있으면 마지막 칸을 어둡게 덮고 "N장 더보기"를 띄운다(N = 칸에 안 보이는 장수). +// 3칸을 그리고, 사진이 더 있으면 마지막 칸을 어둡게 덮어 "N장 더보기"를 띄운다. +// N은 덮인 3번째 칸까지 포함한 "지금 못 보는 장수"다(26장이면 24장 더보기 — Figma와 같다). // 더보기 칸을 누르면 전체 사진 화면으로 간다(onMoreClick). function ArchivesPhotoGrid({ photos, onMoreClick }: ArchivesPhotoGridProps) { - const visiblePhotos = photos.slice(0, VISIBLE_PHOTO_COUNT); - const hiddenCount = photos.length - (VISIBLE_PHOTO_COUNT - 1); - const hasMore = photos.length > VISIBLE_PHOTO_COUNT; + const visiblePhotos = photos.slice(0, TILE_COUNT); + const hiddenCount = photos.length - (TILE_COUNT - 1); + const hasMore = photos.length > TILE_COUNT; return (
@@ -26,12 +26,12 @@ function ArchivesPhotoGrid({ photos, onMoreClick }: ArchivesPhotoGridProps) { // biome-ignore lint/suspicious/noArrayIndexKey: 사진 목록은 순서가 바뀌지 않는다 key={index} > - - {hasMore && index === VISIBLE_PHOTO_COUNT - 1 && ( + {hasMore && index === TILE_COUNT - 1 && (