Skip to content

Commit aaa3463

Browse files
authored
Merge pull request #57 from JECT-Study/feat/55-home-artwork-space-feed
feat: 작품 공간 상세 페이지 API 연동
2 parents b2e78d5 + 6efda3e commit aaa3463

6 files changed

Lines changed: 182 additions & 105 deletions

File tree

src/app/art/[id]/page.tsx

Lines changed: 77 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
11
"use client";
22

3-
import { useRouter } from "next/navigation";
3+
import { useQuery } from "@tanstack/react-query";
4+
import { useParams, useRouter } from "next/navigation";
45

56
import ExpandableText from "@/components/archive-detail/ExpandableText";
67
import SizeText from "@/components/archive-detail/SizeText";
78
import RegionText from "@/components/archive-detail/RegionText";
89
import ImageSwiper from "@/components/archive-detail/ImageSwiper";
910
import NicknameCard from "@/components/archive-detail/NicknameCard";
11+
import { getArtworkDetail } from "@/services/artworks";
12+
13+
function formatDate(date: string | null) {
14+
if (!date) return "-";
15+
return date.replaceAll("-", ".");
16+
}
1017

1118
export default function ArtDetailPage() {
1219
const router = useRouter();
20+
const params = useParams<{ id: string }>();
21+
const artworkId = params.id;
1322

14-
// 임시 이미지 배열
15-
const artworkImages = [
16-
"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe",
17-
"https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5",
18-
"https://images.unsplash.com/photo-1541701494587-cb58502866ab",
19-
];
23+
const query = useQuery({
24+
queryKey: ["artwork-detail", artworkId],
25+
queryFn: ({ signal }) => getArtworkDetail(artworkId, signal),
26+
enabled: Boolean(artworkId),
27+
});
2028

21-
// 임시 텍스트
22-
const texts =
23-
"대법원장과 대법관이 아닌 법관은 대법관회의의 동의를 얻어 대법원장이 임명한다. 재산권의 행사는 공공복리에 적합하도록 하여야 한다. 국가원로자문회의의 의장은 직전대통령이 된다. 다만, 직전대통령이 없을 때에는 대통령이 지명한다. 대한민국은 통일을 지향하며, 자유민주적 기본질서에 입각한 평화적 통일 정책을 수립하고 이를 추진한다. 국가는 농지에 관하여 경자유전의 원칙이 달성될 수 있도록 노력하여야 하며, 농지의 소작제도는 금지된다. 대법원에 대법관을 둔다. 다만, 법률이 정하는 바에 의하여 대법관이 아닌 법관을 둘 수 있다. 국가는 모성의 보호를 위하여 노력하여야 한다. 제안된 헌법개정안은 대통령이 20일 이상의 기간 이를 공고하여야 한다.";
24-
// 임시 지역 데이터
25-
const regions = ["강남구", "강동구", "강서구", "관악구", "송파구"];
29+
const artwork = query.data;
30+
const artworkImages = artwork?.imageUrls?.filter(Boolean) ?? [];
2631

2732
return (
2833
<div className="min-h-screen bg-white pb-32">
29-
{/* 헤더 */}
3034
<header className="fixed top-0 right-0 left-0 z-50 flex h-15 w-full min-w-[320px] px-4">
3135
<div className="flex items-center">
3236
<button
@@ -39,51 +43,72 @@ export default function ArtDetailPage() {
3943
</div>
4044
</header>
4145

42-
{/* 이미지 슬라이더 컨테이너 */}
43-
<ImageSwiper images={artworkImages} />
44-
{/* 컨텐츠 영역 */}
45-
<div className="text-text-primary flex flex-col gap-1.5 px-5 py-6">
46-
<div className="text-caption bg-object-secondary-light h-6 w-14 rounded-sm px-1.5 py-1 font-medium">
47-
작품 유형
46+
{query.isLoading ? (
47+
<div className="flex min-h-screen items-center justify-center px-5 text-sm text-gray-500">
48+
불러오는 중...
49+
</div>
50+
) : query.isError || !artwork ? (
51+
<div className="flex min-h-screen flex-col items-center justify-center gap-4 px-5 text-center">
52+
<p className="text-body-2 text-text-secondary">작품 정보를 불러오지 못했습니다.</p>
53+
<button
54+
onClick={() => query.refetch()}
55+
className="border-border-primary text-body-2 rounded-lg border px-4 py-2"
56+
>
57+
다시 시도
58+
</button>
4859
</div>
49-
<div className="text-title-3 font-semibold">작품 이름</div>
60+
) : (
61+
<>
62+
<ImageSwiper images={artworkImages} altPrefix="작품 이미지" />
63+
<div className="text-text-primary flex flex-col gap-1.5 px-5 py-6">
64+
<div className="text-caption bg-object-secondary-light h-6 w-fit min-w-14 rounded-sm px-1.5 py-1 font-medium">
65+
{artwork.artworkType}
66+
</div>
67+
<div className="text-title-3 font-semibold">{artwork.title}</div>
5068

51-
{/* 희망 전시 지역 아코디언 */}
52-
<RegionText regions={regions} />
69+
<RegionText regions={artwork.availableRegions} />
5370

54-
{/* 크리에이터 닉네임 영역 */}
55-
<NicknameCard nickname="크리에이터 닉네임" />
56-
</div>
71+
<NicknameCard nickname={`크리에이터 ${artwork.ownerId}`} />
72+
</div>
5773

58-
{/* 구분선 */}
59-
<div className="bg-bg-primary-darker h-1" />
74+
<div className="bg-bg-primary-darker h-1" />
6075

61-
<div className="text-text-primary flex flex-col gap-8 px-5 py-6">
62-
{/* 작품 제작일 */}
63-
<div className="text-text-primary flex flex-col gap-2">
64-
<div className="text-heading-2 font-medium">작품 제작일</div>
65-
<p className="text-body-2 font-regular">2026.01.02</p>
66-
</div>
76+
<div className="text-text-primary flex flex-col gap-8 px-5 py-6">
77+
<div className="text-text-primary flex flex-col gap-2">
78+
<div className="text-heading-2 font-medium">작품 제작일</div>
79+
<p className="text-body-2 font-regular">{formatDate(artwork.createdDate)}</p>
80+
</div>
81+
82+
<SizeText
83+
title="필요한 전시 공간 사이즈"
84+
width={artwork.widthCm ?? undefined}
85+
height={artwork.heightCm ?? undefined}
86+
depth={artwork.depthCm ?? undefined}
87+
/>
88+
89+
<ExpandableText
90+
title="작품 상세"
91+
content={artwork.description?.trim() || "등록된 작품 상세 설명이 없습니다."}
92+
maxLines={4}
93+
/>
94+
95+
<ExpandableText
96+
title="주의사항"
97+
content={artwork.caution?.trim() || "등록된 주의사항이 없습니다."}
98+
maxLines={4}
99+
/>
100+
</div>
67101

68-
{/* 필요한 전시 공간 사이즈 */}
69-
<SizeText title="필요한 전시 공간 사이즈" />
70-
71-
{/* 작품 상세 */}
72-
<ExpandableText title="작품 상세" content={texts} maxLines={4} />
73-
74-
{/* 주의사항 */}
75-
<ExpandableText title="주의사항" content={texts} maxLines={4} />
76-
</div>
77-
78-
{/* 하단 고정 전시 문의하기 버튼 */}
79-
<div className="border-border-primary bg-bg-primary fixed right-0 bottom-0 left-0 z-50 border-t px-5 pt-3 pb-9">
80-
<button
81-
onClick={() => alert("전시 문의 프로세스 시작")}
82-
className="bg-object-primary text-body-1 text-text-invert flex h-12.5 w-full items-center justify-center rounded-lg font-medium"
83-
>
84-
전시 문의하기
85-
</button>
86-
</div>
102+
<div className="border-border-primary bg-bg-primary fixed right-0 bottom-0 left-0 z-50 border-t px-5 pt-3 pb-9">
103+
<button
104+
onClick={() => alert("전시 문의 프로세스 시작")}
105+
className="bg-object-primary text-body-1 text-text-invert flex h-12.5 w-full items-center justify-center rounded-lg font-medium"
106+
>
107+
전시 문의하기
108+
</button>
109+
</div>
110+
</>
111+
)}
87112
</div>
88113
);
89114
}

src/app/space/[id]/page.tsx

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
11
"use client";
22

3-
import { useRouter } from "next/navigation";
3+
import { useQuery } from "@tanstack/react-query";
4+
import { useParams, useRouter } from "next/navigation";
45

56
import ExpandableText from "@/components/archive-detail/ExpandableText";
6-
import SizeText from "@/components/archive-detail/SizeText";
77
import ImageSwiper from "@/components/archive-detail/ImageSwiper";
88
import NicknameCard from "@/components/archive-detail/NicknameCard";
9+
import { getSpaceDetail } from "@/services/spaces";
910

1011
export default function SpaceDetailPage() {
1112
const router = useRouter();
13+
const params = useParams<{ id: string }>();
14+
const spaceId = params.id;
1215

13-
// 임시 이미지 배열
14-
const artworkImages = [
15-
"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe",
16-
"https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5",
17-
"https://images.unsplash.com/photo-1541701494587-cb58502866ab",
18-
];
16+
const query = useQuery({
17+
queryKey: ["space-detail", spaceId],
18+
queryFn: ({ signal }) => getSpaceDetail(spaceId, signal),
19+
enabled: Boolean(spaceId),
20+
});
1921

20-
// 임시 텍스트
21-
const texts =
22-
"대법원장과 대법관이 아닌 법관은 대법관회의의 동의를 얻어 대법원장이 임명한다. 재산권의 행사는 공공복리에 적합하도록 하여야 한다. 국가원로자문회의의 의장은 직전대통령이 된다. 다만, 직전대통령이 없을 때에는 대통령이 지명한다. 대한민국은 통일을 지향하며, 자유민주적 기본질서에 입각한 평화적 통일 정책을 수립하고 이를 추진한다. 국가는 농지에 관하여 경자유전의 원칙이 달성될 수 있도록 노력하여야 하며, 농지의 소작제도는 금지된다. 대법원에 대법관을 둔다. 다만, 법률이 정하는 바에 의하여 대법관이 아닌 법관을 둘 수 있다. 국가는 모성의 보호를 위하여 노력하여야 한다. 제안된 헌법개정안은 대통령이 20일 이상의 기간 이를 공고하여야 한다.";
23-
// 임시 지역 데이터
24-
const address = "주소주소주소주소";
22+
const space = query.data;
23+
const spaceImages = space?.imageUrls?.filter(Boolean) ?? [];
2524

2625
return (
2726
<div className="min-h-screen bg-white pb-32">
28-
{/* 헤더 */}
2927
<header className="fixed top-0 right-0 left-0 z-50 flex h-15 w-full min-w-[320px] px-4">
3028
<div className="flex items-center">
3129
<button
@@ -38,49 +36,53 @@ export default function SpaceDetailPage() {
3836
</div>
3937
</header>
4038

41-
{/* 이미지 슬라이더 */}
42-
<ImageSwiper images={artworkImages} />
43-
44-
{/* 위쪽 컨텐츠 */}
45-
<div className="text-text-primary flex flex-col gap-1.5 px-5 py-6">
46-
<div className="text-caption bg-object-secondary-light h-6 w-14 rounded-sm px-1.5 py-1 font-medium">
47-
공간 유형
39+
{query.isLoading ? (
40+
<div className="flex min-h-screen items-center justify-center px-5 text-sm text-gray-500">
41+
불러오는 중...
4842
</div>
49-
<div className="text-title-3 font-semibold">공간 이름</div>
50-
51-
{/* 주소 */}
52-
<div className="flex-colgap-1 flex">
53-
<div className="text-label font-semibold">주소</div>
54-
<div className="text-body-2 font-regular">{address}</div>
43+
) : query.isError || !space ? (
44+
<div className="flex min-h-screen flex-col items-center justify-center gap-4 px-5 text-center">
45+
<p className="text-body-2 text-text-secondary">공간 정보를 불러오지 못했습니다.</p>
46+
<button
47+
onClick={() => query.refetch()}
48+
className="border-border-primary text-body-2 rounded-lg border px-4 py-2"
49+
>
50+
다시 시도
51+
</button>
5552
</div>
56-
{/* 크리에이터 닉네임 영역 */}
57-
<NicknameCard nickname="공간 파트너 닉네임" />
58-
</div>
53+
) : (
54+
<>
55+
<ImageSwiper images={spaceImages} altPrefix="공간 이미지" />
5956

60-
{/* 구분선 */}
61-
<div className="bg-bg-primary-darker h-1" />
57+
<div className="text-text-primary flex flex-col gap-1.5 px-5 py-6">
58+
<div className="text-caption bg-object-secondary-light h-6 w-fit min-w-14 rounded-sm px-1.5 py-1 font-medium">
59+
공간
60+
</div>
61+
<div className="text-title-3 font-semibold">{space.title}</div>
6262

63-
{/* 아래쪽 컨텐츠 */}
64-
<div className="text-text-primary flex flex-col gap-8 px-5 py-6">
65-
{/* 제공 가능한 공간 사이즈 */}
66-
<SizeText title="제공 가능한 공간 사이즈" />
63+
<NicknameCard nickname={`공간 제공자 ${space.ownerId}`} />
64+
</div>
6765

68-
{/* 공간 상세 */}
69-
<ExpandableText title="공간 상세" content={texts} maxLines={4} />
66+
<div className="bg-bg-primary-darker h-1" />
7067

71-
{/* 주의사항 */}
72-
<ExpandableText title="주의사항" content={texts} maxLines={4} />
73-
</div>
68+
<div className="text-text-primary flex flex-col gap-8 px-5 py-6">
69+
<ExpandableText
70+
title="공간 상세"
71+
content={space.description?.trim() || "등록된 공간 상세 설명이 없습니다."}
72+
maxLines={4}
73+
/>
74+
</div>
7475

75-
{/* 전시 문의하기 버튼 */}
76-
<div className="border-border-primary bg-bg-primary fixed right-0 bottom-0 left-0 z-50 border-t px-5 pt-3 pb-9">
77-
<button
78-
onClick={() => alert("전시 문의 프로세스 시작")}
79-
className="bg-object-primary text-body-1 text-text-invert flex h-12.5 w-full items-center justify-center rounded-lg font-medium"
80-
>
81-
전시 문의하기
82-
</button>
83-
</div>
76+
<div className="border-border-primary bg-bg-primary fixed right-0 bottom-0 left-0 z-50 border-t px-5 pt-3 pb-9">
77+
<button
78+
onClick={() => alert("전시 문의 프로세스 시작")}
79+
className="bg-object-primary text-body-1 text-text-invert flex h-12.5 w-full items-center justify-center rounded-lg font-medium"
80+
>
81+
전시 문의하기
82+
</button>
83+
</div>
84+
</>
85+
)}
8486
</div>
8587
);
8688
}

src/components/archive-detail/ImageSwiper.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import "swiper/css";
77

88
interface ImageSwiperProps {
99
images: string[];
10+
altPrefix?: string;
11+
emptyLabel?: string;
1012
}
1113

12-
export default function ImageSwiper({ images }: ImageSwiperProps) {
14+
export default function ImageSwiper({
15+
images,
16+
altPrefix = "이미지",
17+
emptyLabel = "이미지가 없습니다.",
18+
}: ImageSwiperProps) {
1319
const [activeIndex, setActiveIndex] = useState(0);
1420

1521
const hasImages = images && images.length > 0;
@@ -28,14 +34,16 @@ export default function ImageSwiper({ images }: ImageSwiperProps) {
2834
<SwiperSlide key={index}>
2935
<img
3036
src={src}
31-
alt={`작품 이미지 ${index + 1}`}
37+
alt={`${altPrefix} ${index + 1}`}
3238
className="h-full w-full object-cover"
3339
/>
3440
</SwiperSlide>
3541
))}
3642
</Swiper>
3743
) : (
38-
<div>이미지가 없습니다.</div>
44+
<div className="bg-bg-primary-darker text-body-2 text-text-secondary flex h-full w-full items-center justify-center">
45+
{emptyLabel}
46+
</div>
3947
)}
4048

4149
{/* 이미지 개수 인덱스 표시 뱃지 */}

src/services/artworks.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { apiClient } from "./apiClient";
2+
import type { ArtworkDetail } from "@/types/archiveDetail";
23
import type { ArtworkFeedItem, FeedPage } from "@/types/feed";
34

45
export interface CreateArtworkRequest {
@@ -46,3 +47,7 @@ export const getArtworkFeed = (params: GetArtworkFeedParams = {}, signal?: Abort
4647
signal
4748
);
4849
};
50+
51+
export const getArtworkDetail = (artworkId: string | number, signal?: AbortSignal) => {
52+
return apiClient.get<ArtworkDetail>(`/api/v1/artworks/${artworkId}`, undefined, signal);
53+
};

src/services/spaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { apiClient } from "./apiClient";
2+
import type { SpaceDetail } from "@/types/archiveDetail";
23
import type { FeedPage, SpaceFeedItem } from "@/types/feed";
34

45
export interface CreateSpaceRequest {
@@ -28,3 +29,7 @@ export const getSpaceFeed = (params: GetSpaceFeedParams = {}, signal?: AbortSign
2829
signal
2930
);
3031
};
32+
33+
export const getSpaceDetail = (spaceId: string | number, signal?: AbortSignal) => {
34+
return apiClient.get<SpaceDetail>(`/api/v1/spaces/${spaceId}`, undefined, signal);
35+
};

src/types/archiveDetail.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export interface ArtworkDetail {
2+
id: number;
3+
ownerId: number;
4+
title: string;
5+
artworkType: string;
6+
description: string | null;
7+
caution: string | null;
8+
sizeType: "STANDARD" | "CUSTOM";
9+
widthCm: number | null;
10+
heightCm: number | null;
11+
depthCm: number | null;
12+
createdDate: string | null;
13+
status: "DRAFT" | "PUBLISHED" | "HIDDEN" | "DELETED";
14+
imageIds: number[];
15+
imageUrls: string[];
16+
thumbnailIndex: number | null;
17+
availableRegions: string[];
18+
createdAt: string;
19+
updatedAt: string;
20+
}
21+
22+
export interface SpaceDetail {
23+
id: number;
24+
ownerId: number;
25+
title: string;
26+
description: string | null;
27+
isPublic: boolean;
28+
imageIds: number[];
29+
imageUrls: string[];
30+
createdAt: string;
31+
updatedAt: string;
32+
}

0 commit comments

Comments
 (0)