Skip to content

Commit 120b7e1

Browse files
authored
Merge pull request #100 from YAPP-Github/feature/PRODUCT-181
feat: 스토리 등록 구현 (#84)
2 parents 429098d + b274aed commit 120b7e1

File tree

29 files changed

+570
-3
lines changed

29 files changed

+570
-3
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
import { useRef } from "react";
5+
6+
import { useImageUploadContext } from "@/app/story/register/_contexts";
7+
import { imageFileSchema } from "@/app/story/register/_schemas";
8+
9+
export const Story = () => {
10+
const router = useRouter();
11+
const { setUpload } = useImageUploadContext();
12+
const fileInputRef = useRef<HTMLInputElement | null>(null);
13+
14+
const handleOpenPhotoGallery = () => {
15+
fileInputRef.current?.click();
16+
};
17+
18+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
19+
const file = e.target.files?.[0];
20+
if (!file) return;
21+
22+
const validationResult = imageFileSchema.safeParse(file);
23+
24+
if (!validationResult.success) {
25+
const errorMessage = validationResult.error.errors[0]?.message;
26+
// TODO: Toast 변경
27+
alert(errorMessage || "올바르지 않은 파일입니다.");
28+
29+
if (fileInputRef.current) {
30+
fileInputRef.current.value = "";
31+
}
32+
return;
33+
}
34+
35+
const previewUrl = URL.createObjectURL(file);
36+
setUpload(file, previewUrl);
37+
38+
router.push("/story/register");
39+
};
40+
return (
41+
<>
42+
<button onClick={handleOpenPhotoGallery}>스토리 사진 선택</button>
43+
<input
44+
ref={fileInputRef}
45+
type='file'
46+
accept='image/jpeg,image/jpg,image/png'
47+
onChange={handleFileChange}
48+
hidden
49+
/>
50+
</>
51+
);
52+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Story } from "./Story";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { RecentCheers } from "./RecentCheers";
22
export { RecentlySupportedStores } from "./RecentlySupportStories";
33
export { StoreStory } from "./StoreStory";
4+
export { Story } from "./Story";

src/app/(home)/layout.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
5+
import LogoWordmarkIcon from "@/assets/logo-wordmark.svg";
6+
import { Button } from "@/components/ui/Button";
7+
import { GNB } from "@/components/ui/GNB";
8+
19
import * as styles from "./layout.css";
210

311
export default function MainLayout({
412
children,
513
}: {
614
children: React.ReactNode;
715
}) {
8-
return <main className={styles.mainContainer}>{children}</main>;
16+
return (
17+
<>
18+
<GNB
19+
leftAddon={<LogoWordmarkIcon width={46} height={24} />}
20+
align='left'
21+
rightAddon={
22+
<Link href='/login'>
23+
<Button variant='primary' size='small' style={{ width: "6.3rem" }}>
24+
로그인
25+
</Button>
26+
</Link>
27+
}
28+
/>
29+
<main className={styles.mainContainer}>{children}</main>
30+
</>
31+
);
932
}

src/app/(home)/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
"use client";
2+
3+
import { Bleed } from "@/components/ui/Bleed";
14
import { Spacer } from "@/components/ui/Spacer";
25
import { VStack } from "@/components/ui/Stack";
36

47
import {
58
RecentCheers,
69
RecentlySupportedStores,
710
StoreStory,
11+
Story,
812
} from "./_components";
913

1014
export default function HomePage() {
1115
return (
1216
<>
17+
<Bleed inline={20}>
18+
<Story />
19+
</Bleed>
1320
<Spacer size={12} />
14-
<div>대충 스토리</div>
1521
<Spacer size={32} />
1622
<VStack gap={40}>
1723
<RecentCheers />

src/app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { MSWProvider, QueryProvider } from "@/providers";
99
import { pretendard } from "@/styles/pretendard";
1010

1111
import * as styles from "./layout.css";
12+
import { UploadProvider } from "./story/register/_contexts";
1213

1314
export const metadata: Metadata = {
1415
title: "Eat-da",
@@ -35,7 +36,9 @@ export default function RootLayout({
3536
<div className={styles.wrapper}>
3637
<RegisterServiceWorkerClient />
3738
<QueryProvider>
38-
<MSWProvider>{children}</MSWProvider>
39+
<MSWProvider>
40+
<UploadProvider>{children}</UploadProvider>
41+
</MSWProvider>
3942
</QueryProvider>
4043
</div>
4144
</body>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function StoryIdPage() {
2+
return <div>StoryIdPage</div>;
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./register.api";
2+
export * from "./register.queries";
3+
export * from "./register.types";
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { authHttp } from "@/lib/api";
2+
3+
import {
4+
type StoryRegisterRequest,
5+
type StoryRegisterResponse,
6+
} from "./register.types";
7+
8+
/**
9+
* 스토리 등록 API
10+
* @param {StoryRegisterRequest} storyRequest - 스토리 등록 요청 데이터
11+
* @param {File} imageFile - 업로드할 이미지 파일
12+
*
13+
* @returns {Promise<StoryRegisterResponse>} 등록된 스토리 ID 반환
14+
*/
15+
export const postStory = async (
16+
storyRequest: StoryRegisterRequest,
17+
imageFile: File
18+
): Promise<StoryRegisterResponse> => {
19+
const formData = new FormData();
20+
21+
formData.append(
22+
"request",
23+
new Blob([JSON.stringify(storyRequest)], { type: "application/json" })
24+
);
25+
26+
formData.append("image", imageFile);
27+
28+
return await authHttp
29+
.post("api/stories", {
30+
body: formData,
31+
})
32+
.json<StoryRegisterResponse>();
33+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
3+
import { postStory } from "./register.api";
4+
import type { StoryRegisterRequest } from "./register.types";
5+
6+
export const storyQueryKeys = {
7+
all: ["story"] as const,
8+
lists: () => [...storyQueryKeys.all, "list"] as const,
9+
} as const;
10+
11+
export const usePostStoryMutation = () => {
12+
const queryClient = useQueryClient();
13+
14+
return useMutation({
15+
mutationFn: ({
16+
storyRequest,
17+
imageFile,
18+
}: {
19+
storyRequest: StoryRegisterRequest;
20+
imageFile: File;
21+
}) => {
22+
return postStory(storyRequest, imageFile);
23+
},
24+
onSuccess: () => {
25+
queryClient.invalidateQueries({
26+
queryKey: storyQueryKeys.lists(),
27+
});
28+
},
29+
});
30+
};

0 commit comments

Comments
 (0)