Skip to content

Commit 72be3e4

Browse files
authored
Merge pull request #62 from JECT-Study/feat/60-fe-uiux-polishing-and-integration
Feat/60 fe uiux polishing and integration
2 parents 0e9f3ca + 9b7746c commit 72be3e4

16 files changed

Lines changed: 190 additions & 54 deletions

File tree

next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import type { NextConfig } from "next";
77
const backendOrigin = process.env.BACKEND_ORIGIN ?? "http://localhost:8080";
88

99
const nextConfig: NextConfig = {
10+
images: {
11+
unoptimized: true,
12+
},
1013
async rewrites() {
1114
return [{ source: "/api/:path*", destination: `${backendOrigin}/api/:path*` }];
1215
},

src/app/chat/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function ChatListPage() {
2626
) : rooms.length === 0 ? (
2727
<EmptyChat />
2828
) : (
29-
<>
29+
<div className="pb-[calc(5rem+env(safe-area-inset-bottom))]">
3030
<ul className="divide-border-primary flex flex-col divide-y">
3131
{rooms.map(room => (
3232
<ChatListItem
@@ -49,7 +49,7 @@ export default function ChatListPage() {
4949
</button>
5050
</div>
5151
)}
52-
</>
52+
</div>
5353
)}
5454
</div>
5555
);

src/app/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { Suspense } from "react";
2+
13
import type { Metadata } from "next";
24
import { Alata } from "next/font/google";
35
import localFont from "next/font/local";
46

57
import Navbar from "@/components/common/Navbar";
8+
import ScrollManager from "@/components/common/ScrollManager";
69

710
import Providers from "./providers";
811

@@ -36,6 +39,9 @@ export default function RootLayout({
3639
{/* <body className="flex min-h-full flex-col"> */}
3740
<body className="mx-auto min-h-screen w-full max-w-97.5 bg-white">
3841
<Providers>{children}</Providers>
42+
<Suspense fallback={null}>
43+
<ScrollManager />
44+
</Suspense>
3945
<Navbar />
4046
</body>
4147
</html>

src/app/mypage/activities/page.tsx

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,44 @@ export default function MypageActivitiesPage() {
4747
</div>
4848
) : activities.length > 0 ? (
4949
<div className="flex flex-col gap-4">
50-
{activities.map((activity, index) => (
51-
<Link key={activity.id} href={`/exhibitions/${activity.id}`} className="block">
52-
<div className="flex items-center gap-4">
53-
<div className="relative size-18.5 shrink-0 overflow-hidden rounded-lg">
54-
{activity.thumbnailUrl ? (
55-
<Image
56-
src={normalizeImageUrl(activity.thumbnailUrl) ?? ""}
57-
alt=""
58-
fill
59-
sizes="74px"
60-
className="object-cover"
61-
/>
62-
) : (
63-
<div className="bg-bg-primary-darker text-text-disabled flex size-full items-center justify-center">
64-
<Images size={24} />
65-
</div>
66-
)}
67-
</div>
50+
{activities.map((activity, index) => {
51+
const thumbnailUrl = normalizeImageUrl(activity.thumbnailUrl);
6852

69-
<div className="flex min-w-0 flex-1 flex-col gap-1 leading-[1.45] font-medium">
70-
<p className="text-body-1 text-text-primary truncate">{activity.title}</p>
71-
<div className="text-body-2 text-text-secondary flex flex-col gap-[3px]">
72-
<p>{`${formatDate(activity.startDate)}-${formatDate(activity.endDate)}`}</p>
73-
<p>{activity.spaceName}</p>
53+
return (
54+
<Link key={activity.id} href={`/exhibitions/${activity.id}`} className="block">
55+
<div className="flex items-center gap-4">
56+
<div className="relative size-18.5 shrink-0 overflow-hidden rounded-lg">
57+
{thumbnailUrl ? (
58+
<Image
59+
src={thumbnailUrl}
60+
alt=""
61+
fill
62+
sizes="74px"
63+
className="object-cover"
64+
unoptimized
65+
/>
66+
) : (
67+
<div className="bg-bg-primary-darker text-text-disabled flex size-full items-center justify-center">
68+
<Images size={24} />
69+
</div>
70+
)}
71+
</div>
72+
73+
<div className="flex min-w-0 flex-1 flex-col gap-1 leading-[1.45] font-medium">
74+
<p className="text-body-1 text-text-primary truncate">{activity.title}</p>
75+
<div className="text-body-2 text-text-secondary flex flex-col gap-[3px]">
76+
<p>{`${formatDate(activity.startDate)}-${formatDate(activity.endDate)}`}</p>
77+
<p>{activity.spaceName}</p>
78+
</div>
7479
</div>
7580
</div>
76-
</div>
7781

78-
{index < activities.length - 1 && (
79-
<div className="border-border-primary mt-4 border-t" />
80-
)}
81-
</Link>
82-
))}
82+
{index < activities.length - 1 && (
83+
<div className="border-border-primary mt-4 border-t" />
84+
)}
85+
</Link>
86+
);
87+
})}
8388
{query.hasNextPage && (
8489
<button
8590
type="button"

src/app/mypage/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,14 @@ function ExhibitionItem({ exhibition, hasDivider = false }: ExhibitionItemProps)
179179
<div className="flex items-center gap-4">
180180
<div className="relative size-18.5 shrink-0 overflow-hidden rounded-lg">
181181
{exhibition.imageUrl ? (
182-
<Image src={exhibition.imageUrl} alt="" fill sizes="74px" className="object-cover" />
182+
<Image
183+
src={exhibition.imageUrl}
184+
alt=""
185+
fill
186+
sizes="74px"
187+
className="object-cover"
188+
unoptimized
189+
/>
183190
) : (
184191
<ImageFallback>
185192
<Images size={24} />
@@ -288,7 +295,7 @@ export default function MypagePage() {
288295
const isProfileLoading = meQuery.isLoading || nicknamePolicyQuery.isLoading;
289296

290297
return (
291-
<main className="bg-bg-primary min-h-dvh w-full pb-10">
298+
<main className="bg-bg-primary min-h-dvh w-full pb-[calc(5rem+env(safe-area-inset-bottom))]">
292299
<MypageHeader />
293300

294301
<ProfileSummary profile={profile} isLoading={isProfileLoading} />

src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export default function Home() {
6464
const spaceFeed = (spaceFeedQuery.data?.pages ?? []).flatMap(page => page.items).map(toSpaceCard);
6565

6666
return (
67-
<div className="pb-10">
67+
<div className="pb-[calc(5rem+env(safe-area-inset-bottom))]">
6868
<div className="px-5 py-4">
6969
<div className="text-headline-1 text-text-primary h-14 font-semibold"></div>
7070
</div>

src/components/archive-detail/ImageSwiper.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { Swiper, SwiperSlide } from "swiper/react";
77

88
import "swiper/css";
99

10+
import { normalizeImageUrl } from "@/utils/normalizeImageUrl";
11+
1012
interface ImageSwiperProps {
1113
images: string[];
1214
altPrefix?: string;
@@ -20,7 +22,11 @@ export default function ImageSwiper({
2022
}: ImageSwiperProps) {
2123
const [activeIndex, setActiveIndex] = useState(0);
2224

23-
const hasImages = images && images.length > 0;
25+
const displayImages = images.flatMap(image => {
26+
const normalized = normalizeImageUrl(image);
27+
return normalized ? [normalized] : [];
28+
});
29+
const hasImages = displayImages.length > 0;
2430

2531
return (
2632
<div className="relative h-70 w-full overflow-hidden">
@@ -32,7 +38,7 @@ export default function ImageSwiper({
3238
loop={true}
3339
onRealIndexChange={swiper => setActiveIndex(swiper.realIndex)}
3440
>
35-
{images.map((src, index) => (
41+
{displayImages.map((src, index) => (
3642
<SwiperSlide key={index} className="relative">
3743
<Image
3844
src={src}
@@ -54,7 +60,7 @@ export default function ImageSwiper({
5460
{/* 이미지 개수 인덱스 표시 뱃지 */}
5561
{hasImages && (
5662
<div className="bg-object-secondary text-caption text-text-invert absolute right-5 bottom-5 z-10 h-6 w-10.5 rounded-sm px-1.5 py-1 text-center font-medium">
57-
{activeIndex + 1}/{images.length}
63+
{activeIndex + 1}/{displayImages.length}
5864
</div>
5965
)}
6066
</div>

src/components/auth/ProfileAvatarInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default function ProfileAvatarInput({ onImageChange }: ProfileAvatarInput
3333
role="img"
3434
aria-label="프로필 이미지"
3535
className="h-full w-full bg-cover bg-center"
36-
style={{ backgroundImage: `url(${imageUrl})` }}
36+
style={{ backgroundImage: `url("${imageUrl}")` }}
3737
/>
3838
</div>
3939
) : (

src/components/common/Navbar.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ export default function Navbar() {
4444
return null;
4545
}
4646
return (
47-
<nav className="border-border-primary bg-bg-primary fixed right-0 bottom-0 left-0 border-t">
47+
<nav className="border-border-primary bg-bg-primary fixed right-0 bottom-0 left-0 z-30 border-t pb-[env(safe-area-inset-bottom)]">
4848
<div className="mx-auto h-16 w-full max-w-97.5 px-3 pt-1.5 pb-2">
49-
<div className="flex gap-1.5">
49+
<div className="flex h-full gap-1.5">
5050
{menus.map(menu => {
5151
const isActive = menu.href === "/" ? pathname === "/" : pathname.startsWith(menu.href);
5252

@@ -56,7 +56,12 @@ export default function Navbar() {
5656
<Link
5757
key={menu.href}
5858
href={menu.href}
59-
className="flex flex-1 flex-col items-center justify-center gap-1"
59+
onClick={() => {
60+
if (isActive) {
61+
window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
62+
}
63+
}}
64+
className="flex h-full flex-1 flex-col items-center justify-center gap-1"
6065
>
6166
<Icon active={isActive} />
6267

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client";
2+
3+
import { useEffect, useMemo, useRef } from "react";
4+
5+
import { usePathname, useSearchParams } from "next/navigation";
6+
7+
interface ScrollPosition {
8+
x: number;
9+
y: number;
10+
}
11+
12+
const TOP_POSITION: ScrollPosition = { x: 0, y: 0 };
13+
14+
function getScrollPosition(): ScrollPosition {
15+
return { x: window.scrollX, y: window.scrollY };
16+
}
17+
18+
function scrollToPosition(position: ScrollPosition) {
19+
window.scrollTo({
20+
left: position.x,
21+
top: position.y,
22+
behavior: "auto",
23+
});
24+
}
25+
26+
export default function ScrollManager() {
27+
const pathname = usePathname();
28+
const searchParams = useSearchParams();
29+
const routeKey = useMemo(() => {
30+
const queryString = searchParams.toString();
31+
return queryString ? `${pathname}?${queryString}` : pathname;
32+
}, [pathname, searchParams]);
33+
const routeKeyRef = useRef(routeKey);
34+
const positionsRef = useRef<Record<string, ScrollPosition>>({});
35+
const isPopNavigationRef = useRef(false);
36+
const frameRef = useRef<number | null>(null);
37+
38+
useEffect(() => {
39+
if (!("scrollRestoration" in window.history)) return;
40+
41+
const previousScrollRestoration = window.history.scrollRestoration;
42+
window.history.scrollRestoration = "manual";
43+
44+
return () => {
45+
window.history.scrollRestoration = previousScrollRestoration;
46+
};
47+
}, []);
48+
49+
useEffect(() => {
50+
const handlePopState = () => {
51+
isPopNavigationRef.current = true;
52+
};
53+
54+
window.addEventListener("popstate", handlePopState);
55+
return () => window.removeEventListener("popstate", handlePopState);
56+
}, []);
57+
58+
useEffect(() => {
59+
const positions = positionsRef.current;
60+
61+
const handleScroll = () => {
62+
if (frameRef.current !== null) return;
63+
64+
frameRef.current = window.requestAnimationFrame(() => {
65+
positions[routeKeyRef.current] = getScrollPosition();
66+
frameRef.current = null;
67+
});
68+
};
69+
70+
window.addEventListener("scroll", handleScroll, { passive: true });
71+
return () => {
72+
window.removeEventListener("scroll", handleScroll);
73+
if (frameRef.current !== null) {
74+
window.cancelAnimationFrame(frameRef.current);
75+
}
76+
positions[routeKeyRef.current] = getScrollPosition();
77+
};
78+
}, []);
79+
80+
useEffect(() => {
81+
const previousRouteKey = routeKeyRef.current;
82+
if (previousRouteKey === routeKey) return;
83+
84+
positionsRef.current[previousRouteKey] ??= getScrollPosition();
85+
routeKeyRef.current = routeKey;
86+
87+
const shouldRestore = isPopNavigationRef.current;
88+
const nextPosition = shouldRestore
89+
? (positionsRef.current[routeKey] ?? TOP_POSITION)
90+
: TOP_POSITION;
91+
isPopNavigationRef.current = false;
92+
93+
window.requestAnimationFrame(() => {
94+
window.requestAnimationFrame(() => scrollToPosition(nextPosition));
95+
});
96+
}, [routeKey]);
97+
98+
return null;
99+
}

0 commit comments

Comments
 (0)